FEAT Infinite scroll works. Sort-of.

I've implemented a Cards/Card interface, but it's heavy and direct,
with absolutely not a care in the world about how much memory your
browser can give me.  It only moves forward in time, and accumulates as
it goes.  The React is brutal and heavy, but for a prototype, eh... it's
a prototype.

I'm a firm believer in prototypes.

Criticism: "Cards" is doing too much, and not enough.  It would make
more sense to have a moving window, showing no more than what it would
take to show the user what is expected at that moment, with the ability
to load high and low, keeping only what the user is _looking at_ in the
DOM tree.

Proposal: Cards needs a CardCollection proxy object, which the Cards view
object (personal convention: TSX files are named for what they show,
semantically; TS files may be suffixed with "Model" or "Collection",
a'la Backbone, to seperate them from the views, if needed; "Model"
objects are very rarely needed) taps it as the user moves up and down;
the IntersectionObserver tool would be pretty good for this; and if the
proxy actually _cached_ the fields it had already seen, it wouldn't be
slow at all.

The proxy object could also support the search feature by keeping two
collections: one of the "whole" set, and one of the "last search" set;
the proxy could swap between them as needed.

Enhancement: Well, it could use a "zoom" feature with a nice overlay.
It might even be lovely to have a second page with all the things I
haven't shown, like attributes, subtype, rarity (not, not the pony),
or keywords.

Enhancement: It is possible to search based on set, attributes, and so
forth, so a sidebar with those as filters would also be kinda cool.
This commit is contained in:
Elf M. Sternberg 2020-09-15 19:41:37 -07:00
parent 58200aa79b
commit f71d439a22
20 changed files with 2253 additions and 1330 deletions

29
Makefile Normal file
View File

@ -0,0 +1,29 @@
.PHONY: all
all: help
.PHONY: help
help:
@M=$$(perl -ne 'm/((\w|-)*):.*##/ && print length($$1)."\n"' Makefile | \
sort -nr | head -1) && \
perl -ne "m/^((\w|-)*):.*##\s*(.*)/ && print(sprintf(\"%s: %s\t%s\n\", \$$1, \" \"x($$M-length(\$$1)), \$$3))" Makefile
./node_modules/.yarn-integrity: package.json
yarn install
./build/asset-manifest.json: ./node_modules/.yarn-integrity src/App.tsx src/cards/Cards.tsx src/cards/Card.tsx
yarn build
.PHONY: install
install: ./node_modules/.yarn-integrity ## Install any yarn packages necessary
.PHONY: build
build: ./build/asset-manifest.json ## Build the package if it is out of date
.PHONY: serve
serve: build/asset-manifest.json ## Do everything necessary to get up and running
yarn run http-server build/
.PHONY: dev
dev: ./node_modules/.yarn-integrity ## Run the server in "dev" mode, with hot reload
yarn start

View File

@ -9,16 +9,16 @@ set out in the Code Challenge are:
### Requirements Checklist
- Show results in a card grid format with the image prominently displayed.
- Each card displays: Image, Name, Text, Set Name, and Type. Additional fields are optional.
- Display a loading indicator when communicating with the API.
- Use a responsive design that accommod_es, at minimum, desktop and mobile.
- Initially, fetch and display the first 20 results returned by the API.
- As the user scrolls down the page, load and append additional cards using "infinite scroll."
- Retrieve additional pages of results as-needed but do not load more than 20 cards with
- Show results in a card grid format with the image prominently displayed.
- Each card displays: Image, Name, Text, Set Name, and Type. Additional fields are optional.
- Display a loading indicator when communicating with the API.
- Use a responsive design that accommod_es, at minimum, desktop and mobile.
- Initially, fetch and display the first 20 results returned by the API.
- As the user scrolls down the page, load and append additional cards using "infinite scroll."
- Retrieve additional pages of results as-needed but do not load more than 20 cards with
each request.
- Allow the user to search for cards by Name.
- Use modern open-source web technologies to implement your solution (React, Backbone,
- Use modern open-source web technologies to implement your solution (React, Backbone,
Angular, Vue, Underscore, etc.).
- Provide instructions for prerequisites, installation, and application setup and build in a
README file.
@ -45,14 +45,6 @@ by an independent developer; it is not affiliated with Highspot, or with
the intellectual property owners of Elder Scrolls Legends. Please help
us use it responsibly.
### UNSTATED Requirement
- Does the app work well on both mobile and desktop platforms?
Although this requirement is not stated in the requirements checklist,
it is earlier stated that "A successful submission will function on
modern desktop and mobile browsers in a visually appealing way."
## Initial impressions
This is a single-page application. The 'search' is more of a filter,

View File

@ -14,13 +14,13 @@
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.3",
"sass": "^1.26.10"
"sass": "^1.26.10",
"typescript": "^3.9.3"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"test": "react-scripts test"
},
"eslintConfig": {
"extends": "react-app"
@ -38,6 +38,6 @@
]
},
"devDependencies": {
"typescript": "^4.0.2"
"http-server": "^0.12.3"
}
}

Binary file not shown.

Binary file not shown.

93
public/fonts/OFL.txt Normal file
View File

@ -0,0 +1,93 @@
Copyright (c) 2012, Brian J. Bonislawsky DBA Astigmatic (AOETI) (astigma@astigmatic.com), with Reserved Font Names "Marcellus"
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -1,14 +1,95 @@
@import-normalize;
@import "styles/_normalize";
@import "styles/_fonts";
@import "styles/_typography";
@import "styles/_loading";
body {
background: repeat url("images/elder_scrolling_bg.png") #0c0c0c;
color: #f8f8f8;
font-family: "Marcellus";
}
body * {
font-family: "Marcellus";
}
.container {
position: relative;
margin: 0 auto 0 auto;
max-width: 1200px;
flex-wrap: wrap;
}
header {
position: relative;
display: flex;
flex-wrap: wrap;
flex-direction: row;
justify-content: space-between;
width: 100%;
min-height: 64px;
.left, right {
height: 100%;
top: 0;
align-items: center;
}
.left {
margin-left: 15px;
h1 {
text-shadow: 0 0 2px rgba(252, 252, 252, 0.8);
font-weight: 400;
font-size: 4.6rem;
}
}
.right {
right: 0;
}
}
.cardbox {
display: flex;
flex-wrap: wrap;
flex-direction: row;
font-size: 1.4rem;
justify-content: space-between;
align-content: space-between;
.card {
width: 192px;
border: 1px solid #707c80;
border-radius: 8px;
padding: 0.5rem;
margin: 0.5rem;
img {
height: 290px;
width: 175px;
}
.card-text {
line-height: 1.4;
}
}
}
.lds-spinner-container {
display: flex;
justify-content: center;
}
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
#page-bottom {
min-height: 4rem;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;

View File

@ -5,14 +5,16 @@ import { Cards } from "./cards/Cards";
function App() {
return (
<div className="elder-scrolling">
<header>
<div className="left">
<h1>The Elder Scrolls</h1>
</div>
<div className="right">
<h2>Search goes here</h2>
</div>
</header>
<div className="container">
<header>
<div className="left">
<h1>The Elder Scrolls</h1>
</div>
<div className="right">
<h2>Search goes here</h2>
</div>
</header>
</div>
<section>
<Cards />
</section>

31
src/cards/Card.tsx Normal file
View File

@ -0,0 +1,31 @@
import * as React from "react";
import { CardProps } from "./types";
export const Card = ({ card }: { card: CardProps }) => {
return (
<div className="card" key="{idx}">
<img src={card.imageUrl} alt={card.name} />
<div className="content">
<h4>{card.name}</h4>
<p className="card-text">{card.text}</p>
<table>
<tbody>
<tr>
<td>
<strong>Set Name:</strong>
</td>
<td>{card.set.name}</td>
</tr>
<tr>
<td>
<strong>Type:</strong>
</td>
<td>{card.type}</td>
</tr>
</tbody>
</table>
</div>
</div>
);
};

View File

@ -1,3 +1,69 @@
import * as React from "react";
import { useState, useEffect, useRef } from "react";
import { Loading } from "./Loading";
import { Card } from "./Card";
import { useInfiniteScroll } from "./infiniteScroll";
import {
CardProps,
CardRequestProps,
CardState,
CardStateHandler
} from "./types";
export const Cards = () => <div>Cards go here</div>;
const PAGE_SIZE = 20;
const firstUrl = `https://api.elderscrollslegends.io/v1/cards?pageSize=${PAGE_SIZE}&page=1`;
const emptyCards: CardState = {
cards: [],
next: firstUrl
};
export const Cards = () => {
const [cards, setCards]: CardStateHandler = useState(emptyCards);
const [cardPage, setCardPage]: [string | null, Function] = useState(null);
const [loading, setLoading]: [boolean, Function] = useState(false);
const getNextPage = () => {
setCardPage(cards.next);
setLoading(true);
};
let pageBottomRef = useRef(null);
useInfiniteScroll(pageBottomRef, getNextPage);
const fetchMoreCards = async () => {
if (cards.next === null || cardPage === null) {
return cards;
}
const request = new Request(cardPage!);
const response = await fetch(request);
const data: CardRequestProps = await response.json();
setCards({
cards: cards.cards.concat(data.cards),
next: data._links && data._links.next ? data._links.next : null
});
setLoading(false);
};
if (cards.next && cardPage === null) {
getNextPage();
}
useEffect(() => {
fetchMoreCards();
}, [cardPage]);
return (
<div className="container">
<div className="cardbox">
{cards.cards.map((card: CardProps, count: number) => (
<Card card={card} key={count} />
))}
</div>
{loading && <Loading />}
<div id="page-bottom" ref={pageBottomRef} />
</div>
);
};

20
src/cards/Loading.tsx Normal file
View File

@ -0,0 +1,20 @@
import * as React from "react";
export const Loading = () => (
<div className="lds-spinner-container">
<div className="lds-spinner">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
);

View File

@ -0,0 +1,33 @@
import * as React from "react";
import { useEffect, useCallback } from "react";
// From the Smashing Magazine article,
// https://www.smashingmagazine.com/2020/03/infinite-scroll-lazy-image-loading-react/
// As this is new to me, I'm going to try to explain it;
// IntersectionObserver takes a callback to be called when it's
// triggred. It's trigged when the node it's been asked to observe
// changes its relationship with another object or, without a
// specified object, with the viewport. When the `intersectionRatio`
// exceed zero, it is _intersecting_ the viewport, i.e. it is now
// visible.
// useCallback is a memoizer; it only updates the contents of the
// function its wrapped if the included function changes, which
// it shouldn't.
export const useInfiniteScroll = (scrollRef: React.RefObject<HTMLElement>, stateUpdate: Function) => {
const scrollObserver = (node: HTMLElement) => {
new IntersectionObserver(entries => {
if (entries.some(en => en.intersectionRatio > 0)) {
stateUpdate();
}
}).observe(node);
};
useEffect(() => {
if (scrollRef.current) {
scrollObserver(scrollRef.current);
}
}, [scrollObserver, scrollRef]);
}

43
src/cards/types.ts Normal file
View File

@ -0,0 +1,43 @@
interface SetProps {
id: string;
name: string;
_self: string;
}
export interface CardProps {
name: string;
rarity: string;
type: string;
cost: number;
set: SetProps;
collectible: boolean;
soulSummon: number;
soulTrap: number;
text: string;
attributes: string[];
keywords: string[];
unique: boolean;
imageUrl: string;
id: string;
}
interface LinkProps {
next?: string;
prev?: string;
}
export interface CardRequestProps {
cards: CardProps[];
_links: LinkProps;
_pageSize: number;
_totalCount: number;
}
export interface CardState {
cards: CardProps[];
next: string | null;
}
export type CardStateHandler = [CardState, Function];

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -1,45 +0,0 @@
$white: #ffffff;
$cream: #f8f8f8;
$raven: #373d3f; // For body text
$jet: #131516; // For header text
$stone: #707c80; // For meta text
$pearl: #dadedf; // Meta elsewhere
$menufade: rgba(255, 255, 255, 0.2);
$whiteblur: rgba(252, 252, 252, 0.8);
$shadow: rgba(1, 1, 1, 0.15);
$shadowjet: rgba(19, 21, 20, 0.15);
$darkshadow: rgba(0, 0, 0, 0.1);
$clearjet: rgba(19, 21, 20, 0.1);
$lightjet: rgba(19, 21, 20, 0.6);
$bigorange: #ef6c00;
$color-background: #fcfcfc;
$color-text: #2C3531;
$color-primary-0: #EF6C00; // Main Primary color */
$color-primary-1: #FF9E4E;
$color-primary-2: #FF8A29;
$color-primary-3: #C15800;
$color-primary-4: #9D4700;
$color-secondary-1-0: #EF9F00; // Main Secondary color (1) */
$color-secondary-1-1: #FFC44E;
$color-secondary-1-2: #FFB829;
$color-secondary-1-3: #C18100;
$color-secondary-1-4: #9D6800;
$color-secondary-2-0: #113CA0; // Main Secondary color (2) */
$color-secondary-2-1: #4669BA;
$color-secondary-2-2: #2B53AF;
$color-secondary-2-3: #0D3082;
$color-secondary-2-4: #092669;
$color-complement-0: #008F8F; // Main Complement color */
$color-complement-1: #34ACAC;
$color-complement-2: #199E9E;
$color-complement-3: #007474;
$color-complement-4: #005E5E;

View File

@ -1,59 +1,11 @@
$unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
$josefinfonts:
100 Thin normal,
100 ThinItalic italic,
200 ExtraLight normal,
200 ExtraLight italic,
300 Light normal,
300 LightItalic italic,
400 Regular normal,
400 RegularItalic italic,
500 Medium normal,
500 MediumItalic italic,
600 SemiBold normal,
600 SemiBoldItalic italic,
700 Bold normal,
700 Bold italic;
@each $weight, $filename, $style in $josefinfonts {
@font-face {
font-family: 'JosefinSans';
font-style: $style;
font-weight: $weight;
src: url("./fonts/JosefinSans-#{$filename}.woff2") format("woff"),
url("./fonts/JosefinSans-#{$filename}.ttf") format("truetype");
@font-face {
font-family: 'Marcellus';
font-style: normal;
font-weight: 400;
src: url("/fonts/Marcellus-Regular.woff2") format("woff"),
url("/fonts/Marcellus-Regular.ttf") format("truetype");
unicode-range: $unicode-range;
}
}
$cardofonts:
400 Regular normal,
400 Italic italic,
700 Bold normal;
@each $weight, $filename, $style in $cardofonts {
@font-face {
font-family: 'Cardo';
font-style: $style;
font-weight: $weight;
src: url("./fonts/Cardo-#{$filename}.woff2") format("woff"),
url("./fonts/Cardo-#{$filename}.ttf") format("truetype");
unicode-range: $unicode-range;
}
}
$firafonts:
400 Regular normal,;
@each $weight, $filename, $style in $firafonts {
@font-face {
font-family: 'FiraCode';
font-style: $style;
font-weight: $weight;
src: url("./fonts/FiraCode-#{$filename}.woff2") format("woff"),
url("./fonts/Fira-#{$filename}.ttf") format("truetype");
unicode-range: $unicode-range;
}
}

80
src/styles/_loading.css Normal file
View File

@ -0,0 +1,80 @@
/* https://loading.io/css/ */
.lds-spinner {
color: #373d3f;
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-spinner div {
transform-origin: 40px 40px;
animation: lds-spinner 1.2s linear infinite;
}
.lds-spinner div:after {
content: " ";
display: block;
position: absolute;
top: 3px;
left: 37px;
width: 6px;
height: 18px;
border-radius: 20%;
background: #fff;
}
.lds-spinner div:nth-child(1) {
transform: rotate(0deg);
animation-delay: -1.1s;
}
.lds-spinner div:nth-child(2) {
transform: rotate(30deg);
animation-delay: -1s;
}
.lds-spinner div:nth-child(3) {
transform: rotate(60deg);
animation-delay: -0.9s;
}
.lds-spinner div:nth-child(4) {
transform: rotate(90deg);
animation-delay: -0.8s;
}
.lds-spinner div:nth-child(5) {
transform: rotate(120deg);
animation-delay: -0.7s;
}
.lds-spinner div:nth-child(6) {
transform: rotate(150deg);
animation-delay: -0.6s;
}
.lds-spinner div:nth-child(7) {
transform: rotate(180deg);
animation-delay: -0.5s;
}
.lds-spinner div:nth-child(8) {
transform: rotate(210deg);
animation-delay: -0.4s;
}
.lds-spinner div:nth-child(9) {
transform: rotate(240deg);
animation-delay: -0.3s;
}
.lds-spinner div:nth-child(10) {
transform: rotate(270deg);
animation-delay: -0.2s;
}
.lds-spinner div:nth-child(11) {
transform: rotate(300deg);
animation-delay: -0.1s;
}
.lds-spinner div:nth-child(12) {
transform: rotate(330deg);
animation-delay: 0s;
}
@keyframes lds-spinner {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}

428
src/styles/_normalize.css Normal file
View File

@ -0,0 +1,428 @@
/* normalize.css v4.0.0 | MIT License | github.com/necolas/normalize.css */
html {
font-family: sans-serif;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
main,
menu,
nav,
section,
summary {
display: block;
}
audio,
canvas,
progress,
video {
display: inline-block;
}
audio:not([controls]) {
display: none;
height: 0;
}
progress {
vertical-align: baseline;
}
template,
[hidden] {
display: none;
}
a:active,
a:hover {
outline-width: 0;
}
abbr[title] {
border-bottom: none;
text-decoration: underline;
text-decoration: underline dotted;
}
b,
strong {
font-weight: inherit;
}
b,
strong {
font-weight: bolder;
}
dfn {
font-style: italic;
}
h1 {
font-size: 2em;
margin: 0.67em 0;
}
mark {
}
small {
font-size: 80%;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
img {
border-style: none;
}
svg:not(:root) {
overflow: hidden;
}
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
figure {
margin: 1em 40px;
}
hr {
-webkit-box-sizing: content-box;
box-sizing: content-box;
height: 0;
overflow: visible;
}
button,
input,
select,
textarea {
font: inherit;
}
optgroup {
font-weight: bold;
}
button,
input,
select {
overflow: visible;
}
button,
input,
select,
textarea {
margin: 0;
}
button,
select {
text-transform: none;
}
button,
[type="button"],
[type="reset"],
[type="submit"] {
cursor: pointer;
}
[disabled] {
cursor: default;
}
button,
html [type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
button:-moz-focusring,
input:-moz-focusring {
outline: 1px dotted ButtonText;
}
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
legend {
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: inherit;
display: table;
max-width: 100%;
padding: 0;
white-space: normal;
}
textarea {
overflow: auto;
}
[type="checkbox"],
[type="radio"] {
-webkit-box-sizing: border-box;
box-sizing: border-box;
padding: 0;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
-webkit-appearance: textfield;
}
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
html {
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
*,
*::before,
*::after {
-webkit-box-sizing: inherit;
box-sizing: inherit;
}
@-ms-viewport {
width: device-width;
}
[tabindex="-1"]:focus {
outline: none !important;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: .5rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-original-title] {
cursor: help;
border-bottom: 1px dotted #818a91;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: bold;
}
dd {
margin-bottom: .5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
a {
text-decoration: none;
}
a:focus, a:hover {
text-decoration: underline;
}
a:focus {
outline: none;
}
a:not([href]):not([tabindex]) {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus {
outline: none;
}
pre {
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
}
figure {
margin: 0 0 1rem;
}
img {
vertical-align: middle;
}
[role="button"] {
cursor: pointer;
}
a,
area,
button,
[role="button"],
input,
label,
select,
summary,
textarea {
-ms-touch-action: manipulation;
touch-action: manipulation;
}
table {
border-collapse: collapse;
background-color: transparent;
}
caption {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
color: #818a91;
text-align: left;
caption-side: bottom;
}
th {
text-align: left;
}
label {
display: inline-block;
margin-bottom: .5rem;
}
button:focus {
outline: none;
}
input,
button,
select,
textarea {
margin: 0;
line-height: inherit;
border-radius: 0;
}
input[type="radio"]:disabled,
input[type="checkbox"]:disabled {
cursor: not-allowed;
}
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"] {
-webkit-appearance: listbox;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
display: block;
width: 100%;
padding: 0;
margin-bottom: .5rem;
font-size: 1.5rem;
line-height: inherit;
}
input[type="search"] {
-webkit-appearance: none;
}
output {
display: inline-block;
}

View File

@ -1,18 +1,18 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,

2504
yarn.lock

File diff suppressed because it is too large Load Diff