From b7f49d099d6ae5d65242c9664d389f346e809641 Mon Sep 17 00:00:00 2001 From: sup39 Date: Wed, 9 Nov 2022 01:16:35 +0900 Subject: [PATCH] init --- LICENSE | 21 +++++ README.md | 11 +++ package.json | 7 +- public/index.html | 4 +- src/App.css | 38 --------- src/App.module.sass | 6 ++ src/App.test.tsx | 9 --- src/App.tsx | 170 ++++++++++++++++++++++++++++++++++++----- src/db.json | 1 + src/events.ts | 90 ++++++++++++++++++++++ src/i18n.ts | 25 ++++++ src/index.css | 13 ---- src/index.sass | 32 ++++++++ src/index.tsx | 6 +- src/reportWebVitals.ts | 15 ---- src/setupTests.ts | 5 -- src/sup39.sass | 31 ++++++++ yarn.lock | 18 ++++- 18 files changed, 393 insertions(+), 109 deletions(-) create mode 100644 LICENSE create mode 100644 README.md delete mode 100644 src/App.css create mode 100644 src/App.module.sass delete mode 100644 src/App.test.tsx create mode 100644 src/db.json create mode 100644 src/events.ts create mode 100644 src/i18n.ts delete mode 100644 src/index.css create mode 100644 src/index.sass delete mode 100644 src/reportWebVitals.ts delete mode 100644 src/setupTests.ts create mode 100644 src/sup39.sass diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c85a4d5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 sup39 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..31dad45 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# botw-lang-cmp +A tool to compare cutscene times among languages in Breath of the Wild + +## CREADITS +[Time data](https://github.com/sup39/botw-lang-cmp/src/db.json) is based on [Cephla's document](https://docs.google.com/document/d/1H0gqxqR2AZqc-MEDUoftJHn_FteyBBm_Zcowieev5ek/edit?usp=sharing), which is based on [Swiffy22's video](https://youtu.be/yVaZdsgjWz8). + +## TODO +- [ ] Rename cutscenes +- [ ] Add more presets +- [ ] Complete Japanese translation +- [ ] Memorize settings with localStorage diff --git a/package.json b/package.json index 3e020ea..5d9bde2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,9 @@ { - "name": "nya", + "name": "botw-lang-cmp", "version": "0.1.0", - "private": true, + "author": "sup39 ", + "repository": "https://github.com/sup39/botw-lang-cmp", + "license": "MIT", "dependencies": { "@sup39/eslint-config-typescript": "^0.1.2", "@testing-library/jest-dom": "^5.14.1", @@ -14,6 +16,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "5.0.1", + "sass": "^1.56.0", "typescript": "^4.4.2", "web-vitals": "^2.1.0" }, diff --git a/public/index.html b/public/index.html index bf61628..4b0a095 100644 --- a/public/index.html +++ b/public/index.html @@ -7,7 +7,7 @@ - React App + BotW Language Comparison diff --git a/src/App.css b/src/App.css deleted file mode 100644 index 74b5e05..0000000 --- a/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/App.module.sass b/src/App.module.sass new file mode 100644 index 0000000..a848709 --- /dev/null +++ b/src/App.module.sass @@ -0,0 +1,6 @@ +.SettingsRoot + > div + display: flex + margin-bottom: 0.5em + span + margin-right: 0.25em diff --git a/src/App.test.tsx b/src/App.test.tsx deleted file mode 100644 index 14b0140..0000000 --- a/src/App.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import {render, screen} from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/src/App.tsx b/src/App.tsx index a53698a..3a599f7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,26 +1,156 @@ -import React from 'react'; -import logo from './logo.svg'; -import './App.css'; +import React, {useState, useEffect} from 'react'; +import db from './db.json'; +import {i18nLabels, defaultLang, $lang0, setLang} from './i18n'; +import {events, i18nEventNames, presets} from './events'; +import styles from './App.module.sass'; + +const langs = [ + 'English', + 'French (Canada)', + 'French (France)', + 'German', + 'Italian', + 'Japanese', + 'Russian', + 'Spanish (Latin)', + 'Spanish (Spain)', +] as const; +const dbEntries = Object.entries(db); + +const f2f = (f: number) => { + let sign = ''; + if (f < 0) { + sign = '-'; + f = -f; + } + const sf = String(f%30)+'f'; + const s = f/30|0; + if (s === 0) return sign+sf; + const ssf = `${s%30}:${sf.padStart(3, '0')}`; + const m = s/60|0; + if (m === 0) return sign+ssf; + return `${sign}${m}:${ssf.padStart(6, '0')}`; +}; +const f2s = (f: number) => { + let sign = ''; + if (f < 0) { + sign = '-'; + f = -f; + } + const s = f/30; + const m = s/60|0; + const ss = (s-m*60).toFixed(3); + return sign+(m ? `${m}:${ss.padStart(6, '0')}` : ss); +}; + +export const RadioGroup = ({name, value: val, values, onChange, ...props}: { + values: (string | [value: string, label: string])[] +} & React.ComponentProps<'input'>) => <>{values.map(o => { + const [value, label] = typeof o === 'string' ? [o, o] : o; + return
+ + {label} +
; +})}; function App() { - return ( -
-
- logo -

- Edit src/App.tsx and save to reload. -

- - Learn React - -
-
+ const [selected, setSelected] = useState( + Object.fromEntries(events.map(l => [l, false])), ); + const [timeFormat, setTimeFormat] = useState('s'); + const f2t = timeFormat === 'f' ? f2f : f2s; + + const [$lang, set$lang] = useState($lang0); + useEffect(() => setLang($lang), [$lang]); + + const eventNames = i18nEventNames[ + ($lang in i18nEventNames) ? $lang : defaultLang + ]; + const L = (id: keyof typeof i18nLabels) => + i18nLabels[id][$lang] ?? i18nLabels[id][defaultLang]; + + const selectedTimes = dbEntries.filter(([event]) => selected[event]); + const l2tEntries = langs.map( + lang => [lang, selectedTimes.reduce( + (sum, [_event, times]) => sum+times[lang], + 0, + )] as const, + ).sort(([_a, tA], [_b, tB]) => tA-tB); + const tMin = Math.min(...l2tEntries.map(([_, t]) => t)); + + return
+
+ CREADITS +
+

This tool is made by sup39 with MIT license. The source code can be found here.

+

Time data is based on Cephla's document, which is based on Swiffy22's video.

+
+
+

Time Comparison

+
+ + + + + + + {l2tEntries.map(([lang, t]) => + + + + )} +
LanguageTimeDiff
{lang}{f2t(t)}{(t>tMin?'+':'±')+f2t(t-tMin)}
+
+
+

Settings

+
+
+ {L('displayLang')} + +
+
+ {L('timeFormat')} + setTimeFormat(e.target.value)} /> +
+
+
+
+

Preset

+
+ +
+
+
+

Cutscenes

+
+ {events.map(id =>
+ setSelected(o => ({...o, [id]: e.target.checked}))} + /> + {eventNames[id]} +
)} +
+
+
; } export default App; diff --git a/src/db.json b/src/db.json new file mode 100644 index 0000000..20cc285 --- /dev/null +++ b/src/db.json @@ -0,0 +1 @@ +{"Sheikah Slate Get": {"French (France)": 487, "German": 490, "French (Canada)": 490, "Spanish (Latin)": 549, "Italian": 551, "Russian": 581, "Spanish (Spain)": 594, "English": 598, "Japanese": 608}, "Out Of SoR": {"German": 536, "Russian": 602, "French (France)": 627, "Italian": 647, "Spanish (Spain)": 666, "Spanish (Latin)": 686, "French (Canada)": 719, "Japanese": 737, "English": 837}, "First Divine Beast Introduction": {"Spanish (Spain)": 619, "Italian": 621, "French (France)": 648, "German": 719, "Spanish (Latin)": 734, "French (Canada)": 739, "English": 781, "Russian": 804, "Japanese": 992}, "All Divine Beasts Completion": {"Italian": 603, "Russian": 628, "German": 658, "Spanish (Spain)": 661, "French (France)": 745, "French (Canada)": 768, "English": 772, "Spanish (Latin)": 828, "Japanese": 1044}, "Lomei Labyrinth Island Introduction": {"Russian": 217, "English": 219, "Japanese": 426, "Spanish (Latin)": 427, "Spanish (Spain)": 427, "French (Canada)": 428, "German": 431, "French (France)": 431, "Italian": 431}, "South Lomei Labyrinth Introduction": {"English": 217, "Russian": 217, "Spanish (Latin)": 427, "Spanish (Spain)": 427, "Japanese": 427, "French (Canada)": 429, "French (France)": 432, "Italian": 432, "German": 434}, "North Lomei Labyrinth Introduction": {"Russian": 215, "English": 218, "Spanish (Latin)": 427, "Spanish (Spain)": 427, "Japanese": 427, "German": 428, "French (Canada)": 429, "Italian": 429, "French (France)": 432}, "Thyphlo Ruins Introduction": {"Russian": 324, "English": 327, "Spanish (Latin)": 536, "Spanish (Spain)": 536, "Japanese": 536, "German": 537, "French (Canada)": 537, "Italian": 539, "French (France)": 540}, "Eventide Island Introduction": {"Italian": 933, "English": 938, "Japanese": 1141, "Spanish (Latin)": 1141, "Spanish (Spain)": 1141, "French (Canada)": 1143, "French (France)": 1153, "German": 1158, "Russian": 1352}, "Eventide Island Completion": {"Japanese": 501, "German": 501, "Russian": 501, "French (Canada)": 502, "Italian": 504, "English": 505, "French (France)": 506, "Spanish (Latin)": 712, "Spanish (Spain)": 713}, "All Shrines Completion": {"Japanese": 639, "French (Canada)": 643, "German": 657, "French (France)": 660, "Russian": 855, "English": 866, "Italian": 873, "Spanish (Latin)": 1275, "Spanish (Spain)": 1277}, "(EX) Trial Of The Sword Invitation": {"French (Canada)": 1287, "Italian": 1288, "English": 1289, "German": 1289, "Japanese": 1290, "Russian": 1291, "French (France)": 1295, "Spanish (Spain)": 1499, "Spanish (Latin)": 1505}, "(EX) Naboris Re-Entry Introduction": {"German": 719, "Spanish (Spain)": 720, "French (France)": 721, "Italian": 721, "Russian": 721, "Japanese": 722, "French (Canada)": 722, "Spanish (Latin)": 725, "English": 932}, "(EX) Medoh Re-Entry Introduction": {"Russian": 720, "French (France)": 721, "German": 721, "Spanish (Spain)": 722, "Spanish (Latin)": 723, "Italian": 723, "French (Canada)": 724, "Japanese": 725, "English": 934}, "(EX) Ruta Re-Entry Introduction": {"English": 649, "Spanish (Latin)": 712, "Russian": 722, "French (France)": 736, "French (Canada)": 750, "German": 766, "Japanese": 776, "Spanish (Spain)": 778, "Italian": 780}, "(EX) Rudania Re-Entry Introduction": {"English": 568, "German": 686, "French (France)": 687, "French (Canada)": 667, "Russian": 700, "Italian": 706, "Spanish (Latin)": 717, "Spanish (Spain)": 720, "Japanese": 722}, "Head For The Point On Your SS": {"Italian": 361, "German": 363, "Spanish (Latin)": 364, "French (France)": 367, "Japanese": 371, "English": 373, "Spanish (Spain)": 373, "Russian": 376, "French (Canada)": 417}, "Impa": {"English": 869, "Spanish (Latin)": 870, "Italian": 871, "Russian": 871, "French (France)": 872, "French (Canada)": 872, "German": 872, "Japanese": 873, "Spanish (Spain)": 873}, "All 13 Memories": {"Spanish (Spain)": 1162, "French (Canada)": 1163, "Russian": 1163, "Spanish (Latin)": 1164, "Japanese": 1165, "German": 1167, "French (France)": 1167, "Italian": 1169, "English": 1169}, "Ta'loh Naeg": {"Italian": 1776, "Japanese": 1778, "Spanish (Spain)": 1778, "English": 1779, "French (Canada)": 1781, "German": 1782, "Russian": 1784, "Spanish (Latin)": 1789, "French (France)": 1796}, "Thundra Plateau Completion": {"Spanish (Latin)": 463, "Russian": 463, "French (Canada)": 464, "German": 466, "Italian": 466, "Japanese": 467, "French (France)": 467, "English": 468, "Spanish (Spain)": 471}, "(EX) Introduction": {"Spanish (Latin)": 1482, "Spanish (Spain)": 1482, "Japanese": 1484, "German": 1485, "English": 1487, "Italian": 1487, "French (France)": 1488, "Russian": 1498, "French (Canada)": 1512}, "(EX) Obtaining The One Hit Obliterator": {"Russian": 2400, "Spanish (Latin)": 2401, "French (Canada)": 2401, "Japanese": 2406, "Spanish (Spain)": 2407, "French (France)": 2412, "Italian": 2412, "German": 2412, "English": 2416}, "(EX) One Hit Obliterator Completion": {"Spanish (Spain)": 1184, "English": 1185, "French (Canada)": 1185, "French (France)": 1185, "Japanese": 1185, "Russian": 1186, "Italian": 1187, "German": 1189, "Spanish (Latin)": 1189}, "(EX) Trial Of The Sword Completion": {"English": 2860, "German": 2861, "Spanish (Spain)": 2861, "French (France)": 2861, "Japanese": 2862, "Russian": 2862, "Italian": 2862, "French (Canada)": 2863, "Spanish (Latin)": 2864}, "(EX) Single Shrine Completion": {"English": 1103, "German": 1104, "French (France)": 1104, "Spanish (Spain)": 1104, "Japanese": 1105, "Italian": 1105, "Russian": 1105, "French (Canada)": 1106, "Spanish (Latin)": 1107}, "(EX) Vah Naboris Kass Song": {"English": 2875, "Spanish (Latin)": 2875, "Japanese": 2875, "Spanish (Spain)": 2875, "Italian": 2877, "Russian": 2878, "French (Canada)": 2881, "French (France)": 2883, "German": 2909}, "(EX) Vah Medoh Kass Song": {"Spanish (Spain)": 2858, "French (France)": 2861, "English": 2862, "Spanish (Latin)": 2863, "Italian": 2864, "Japanese": 2865, "Russian": 2865, "French (Canada)": 2868, "German": 2870}, "(EX) Vah Ruta Kass Song": {"Spanish (Spain)": 2860, "Spanish (Latin)": 2864, "Russian": 2873, "French (France)": 2873, "Japanese": 2874, "English": 2874, "Italian": 2875, "German": 2876, "French (Canada)": 2902}, "(EX) Vah Rudania Kass Song": {"Spanish (Spain)": 2875, "English": 2877, "Japanese": 2879, "Italian": 2879, "Spanish (Latin)": 2880, "Russian": 2880, "French (France)": 2880, "German": 2884, "French (Canada)": 2887}, "(EX) All Champion Songs": {"English": 800, "French (France)": 801, "Italian": 801, "German": 801, "Spanish (Spain)": 801, "French (Canada)": 803, "Spanish (Latin)": 804, "Japanese": 804, "Russian": 804}, "(EX) SoR Revisit Instructions": {"French (France)": 1015, "Russian": 1016, "German": 1016, "Spanish (Spain)": 1017, "Italian": 1017, "English": 1018, "French (Canada)": 1018, "Spanish (Latin)": 1020, "Japanese": 1023}, "(EX) Final Trial Introduction": {"English": 972, "German": 972, "Italian": 973, "French (France)": 973, "Spanish (Spain)": 974, "French (Canada)": 974, "Russian": 974, "Japanese": 977, "Spanish (Latin)": 977}, "(EX) Final Trial Completion": {"Japanese": 1335, "French (Canada)": 1336, "French (France)": 1336, "German": 1338, "Spanish (Latin)": 1339, "Spanish (Spain)": 1339, "Russian": 1341, "English": 1345, "Italian": 1348}, "(EX) Maz Koshia Battle Completion": {"German": 1183, "French (Canada)": 1184, "French (France)": 1185, "English": 1186, "Japanese": 1186, "Spanish (Spain)": 1186, "Italian": 1187, "Russian": 1187, "Spanish (Latin)": 1188}, "(EX) The Champion's Ballad": {"French (France)": 1665, "French (Canada)": 1666, "Japanese": 1667, "German": 1667, "English": 1668, "Spanish (Latin)": 1668, "Spanish (Spain)": 1668, "Italian": 1668, "Russian": 1668}} \ No newline at end of file diff --git a/src/events.ts b/src/events.ts new file mode 100644 index 0000000..ad5101b --- /dev/null +++ b/src/events.ts @@ -0,0 +1,90 @@ +import type {I18N} from './i18n'; + +type EEvent = (typeof events)[number]; +export const events = [ + 'Sheikah Slate Get', + 'Out Of SoR', + 'Head For The Point On Your SS', + 'First Divine Beast Introduction', + 'All Divine Beasts Completion', + 'Lomei Labyrinth Island Introduction', + 'South Lomei Labyrinth Introduction', + 'North Lomei Labyrinth Introduction', + 'Thyphlo Ruins Introduction', + 'Eventide Island Introduction', + 'Eventide Island Completion', + 'All Shrines Completion', + '(EX) Trial Of The Sword Invitation', + '(EX) Naboris Re-Entry Introduction', + '(EX) Medoh Re-Entry Introduction', + '(EX) Ruta Re-Entry Introduction', + '(EX) Rudania Re-Entry Introduction', + 'Impa', + 'All 13 Memories', + "Ta'loh Naeg", + 'Thundra Plateau Completion', + '(EX) Introduction', + '(EX) Obtaining The One Hit Obliterator', + '(EX) One Hit Obliterator Completion', + '(EX) Trial Of The Sword Completion', + '(EX) Single Shrine Completion', + '(EX) Vah Naboris Kass Song', + '(EX) Vah Medoh Kass Song', + '(EX) Vah Ruta Kass Song', + '(EX) Vah Rudania Kass Song', + '(EX) All Champion Songs', + '(EX) SoR Revisit Instructions', + '(EX) Final Trial Introduction', + '(EX) Final Trial Completion', + '(EX) Maz Koshia Battle Completion', + "(EX) The Champion's Ballad", +] as const; + +type TEventNameMap = {[event in EEvent]: string}; +const eventNameDefault = + Object.fromEntries(events.map(e => [e, e])) as TEventNameMap; +export const i18nEventNames: I18N = { + 'en-US': { + ...eventNameDefault, + // 0:10 + 'Sheikah Slate Get': 'Sheikah Slate Get: "That is a Sheikah Slate."', + // 0:27 + 'Out Of SoR': 'Out of SoR: "Hold the Sheikah Slate up to the pedstal.""', + // 12:05 + 'Head For The Point On Your SS': '"Head for the point marked on the map in your Sheikah Slate."', + // 1:06 + 'First Divine Beast Introduction': 'First Divine Beast Introduction: "That Divine Beast was taken over by Ganon 100 years ago"', + // 1:46 + 'All Divine Beasts Completion': 'All Divine Beasts Completion: "Thanks to you, all of the Divine Beasts have returned to us and the spirits of the Champions are free."', + }, + 'ja-JP': { + ...eventNameDefault, + 'Sheikah Slate Get': 'シーカーストーン入手「それはシーカーストーン…」', + 'Out Of SoR': '回生の祠を出る直前「シーカーストーンをかざすのです…」', + 'Head For The Point On Your SS': '「シーカーストーンのマップに示された場所へ向かうのです…」', + 'First Divine Beast Introduction': '最初の神獣イントロ「あれが今から100年前ガノンに奪われてしまった神獣です…」', + 'All Divine Beasts Completion': '四神獣クリア直後「ありがとう…貴方のおかげで全ての神獣と英傑達の魂が解放されました」', + }, +}; + +type Preset = {[event in (typeof events)[number]]?: boolean}; +// type Preset = (typeof events)[number][]; +export const presets: {[name: string]: Preset} = Object.fromEntries([ + ['Any%', [ + 'Sheikah Slate Get', + 'Head For The Point On Your SS', + ]], + ['All Dungeons', [ + 'Sheikah Slate Get', + 'Head For The Point On Your SS', + 'First Divine Beast Introduction', + 'All Divine Beasts Completion', + ]], + ['Dog%', [ + 'Sheikah Slate Get', + 'Head For The Point On Your SS', + 'First Divine Beast Introduction', + ]], +].map(([k, sels]) => [k, Object.fromEntries(events.map(event => [ + event, sels.includes(event), +]))])); diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..65f9377 --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,25 @@ +export const defaultLang = 'en-US'; + +export const lskey$lang = '$lang'; +export const $lang0 = localStorage.getItem(lskey$lang) ?? defaultLang; +export function setLang($lang: string) { + localStorage.setItem(lskey$lang, $lang); + document.documentElement.lang = $lang; +} + +// assert label of defaultLang is specified +export type I18N = + {[lang: string]: T} & {[lang in typeof defaultLang]: T}; + +const _i18nLabels = { + 'displayLang': { + 'en-US': 'Display Language', + 'ja-JP': '表示言語', + }, + 'timeFormat': { + 'en-US': 'Time Format', + 'ja-JP': 'タイムフォーマット', + }, +}; +export const i18nLabels: {[id in keyof typeof _i18nLabels]: I18N} + = _i18nLabels; diff --git a/src/index.css b/src/index.css deleted file mode 100644 index ec2585e..0000000 --- a/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} diff --git a/src/index.sass b/src/index.sass new file mode 100644 index 0000000..f703818 --- /dev/null +++ b/src/index.sass @@ -0,0 +1,32 @@ +@import './sup39.sass' + +body + margin: 1em + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif + -webkit-font-smoothing: antialiased + -moz-osx-font-smoothing: grayscale + background-color: #282c34 + color: white + +code + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace + +table, th, td + border-collapse: collapse + border: 1px solid #777 + th, td + padding: 0.25em +table.time + td:nth-child(2), + td:nth-child(3) + text-align: right + min-width: 4.5em + font-variant-numeric: tabular-nums + +section + margin-bottom: 1em + +div.option-ctn + display: flex + align-items: flex-start + margin-bottom: 4px diff --git a/src/index.tsx b/src/index.tsx index 07924fc..8f1ac32 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,8 +1,8 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import './index.css'; +import './index.sass'; import App from './App'; -import reportWebVitals from './reportWebVitals'; +// import reportWebVitals from './reportWebVitals'; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement, @@ -16,4 +16,4 @@ root.render( // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals(); +// reportWebVitals(); diff --git a/src/reportWebVitals.ts b/src/reportWebVitals.ts deleted file mode 100644 index b7876a4..0000000 --- a/src/reportWebVitals.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {ReportHandler} from 'web-vitals'; - -const reportWebVitals = (onPerfEntry?: ReportHandler) => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({getCLS, getFID, getFCP, getLCP, getTTFB}) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); - } -}; - -export default reportWebVitals; diff --git a/src/setupTests.ts b/src/setupTests.ts deleted file mode 100644 index 8f2609b..0000000 --- a/src/setupTests.ts +++ /dev/null @@ -1,5 +0,0 @@ -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom'; diff --git a/src/sup39.sass b/src/sup39.sass new file mode 100644 index 0000000..c4517b5 --- /dev/null +++ b/src/sup39.sass @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 sup39 + +a, .link + color: #72E5DB + text-decoration: none + cursor: pointer +a:hover, .link:hover + color: #72E5DB + text-decoration: underline +a:active, .link:active + color: #A0E5DF + text-decoration: underline + +details + border: 1px solid + padding: 0.5em 1em + margin-block-start: 0.5em + margin-block-end: 0.5em + > summary + padding: 4px 0.5em + margin: -0.5em -1em -0.5em + cursor: pointer +details[open] + padding-bottom: 0 + > summary + border-bottom: 1px solid + margin: -0.5em -1em 0 + > ul, > ol + padding-inline-start: 1.5em + diff --git a/yarn.lock b/yarn.lock index d441df7..51bd2e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3078,7 +3078,7 @@ check-types@^11.1.1: resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f" integrity sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ== -chokidar@^3.4.2, chokidar@^3.5.3: +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.2, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -4975,6 +4975,11 @@ immer@^9.0.7: resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.16.tgz#8e7caab80118c2b54b37ad43e05758cdefad0198" integrity sha512-qenGE7CstVm1NrHQbMh8YaSzTZTFNP3zPqr3YU0S0UY441j4bJTg4A2Hh5KAhwgaiU6ZZ1Ar6y/2f4TblnMReQ== +immutable@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef" + integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ== + import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -7832,6 +7837,15 @@ sass-loader@^12.3.0: klona "^2.0.4" neo-async "^2.6.2" +sass@^1.56.0: + version "1.56.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.0.tgz#134032075a3223c8d49cb5c35e091e5ba1de8e0a" + integrity sha512-WFJ9XrpkcnqZcYuLRJh5qiV6ibQOR4AezleeEjTjMsCocYW59dEG19U3fwTTXxzi2Ed3yjPBp727hbbj53pHFw== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -8038,7 +8052,7 @@ source-list-map@^2.0.0, source-list-map@^2.0.1: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== -source-map-js@^1.0.1, source-map-js@^1.0.2: +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==