diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..edbd296 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,34 @@ +module.exports = { + env: { + browser: true, + es2021: true, + node: true + }, + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: 13, + ecmaFeatures: { + jsx: true + }, + sourceType: "module" + }, + settings: { + "import/resolver": { + typescript: {} + }, + react: { + version: "detect" + } + }, + extends: [ + "plugin:@typescript-eslint/recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:prettier/recommended" + ], + plugins: ["prettier"], + rules: { + "prettier/prettier": "warn", + "react/react-in-jsx-scope": "off" + } +} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..682cef2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,20 @@ +{ + "bracketSpacing": false, + "printWidth": 80, + "proseWrap": "never", + "requirePragma": false, + "semi": false, + "singleQuote": false, + "trailingComma": "none", + "useTabs": true, + "overrides": [ + { + "files": [".prettierrc", "*.json"], + "options": { + "printWidth": 200, + "tabWidth": 2, + "useTabs": false + } + } + ] +} diff --git a/src/App.tsx b/src/App.tsx index 7f2fe61..7cc91f1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,45 +1,55 @@ -import Counter from './counter' -import TempConvertor from './temp-convertor' -import Timer from './timer' -import FlightBooker from './flight-booker' -import CRUD from './CRUD' -import CircleDrawer from './circle-drawer' +import Counter from "./counter" +import TempConvertor from "./temp-convertor" +import Timer from "./timer" +import FlightBooker from "./flight-booker" +import CRUD from "./CRUD" +import CircleDrawer from "./circle-drawer" +import Cells from "./cells" +import {GuiSection} from "./GuiSection" function App() { - return ( - - 7GUIs with Valtio - 7 GUIs a Programming Benchmark - {' | '} - Valtio - - 1. Counter - - - - - 2. Temperature Converter - - - - - 3. Flight Booker - - - - 4. Timer - - - - 5. CRUD - - - - 6. Circle Drawer - - - - ) + return ( + + 7GUIs with Valtio + + 7 GUIs a Programming Benchmark + + {" | "} + Valtio + {" | "} + + Jotai 7GUIs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) } export default App diff --git a/src/CRUD/index.css b/src/CRUD/index.css index 83f321c..3d68bfd 100644 --- a/src/CRUD/index.css +++ b/src/CRUD/index.css @@ -1,20 +1,20 @@ select { - width: 416px; + width: 416px; } .buttons { - display: flex; - width: 416px; + display: flex; + width: 416px; } .buttons > button { - width: calc(416px / 3); + width: calc(416px / 3); } .buttons > button + button { - margin-left: 4px; + margin-left: 4px; } -input[name='filter'] { - width: 396px; +input[name="filter"] { + width: 396px; } diff --git a/src/CRUD/index.tsx b/src/CRUD/index.tsx index ce4f915..1b5cded 100644 --- a/src/CRUD/index.tsx +++ b/src/CRUD/index.tsx @@ -1,129 +1,151 @@ -import * as React from 'react' -import { proxy, useSnapshot } from 'valtio' -import './index.css' +import * as React from "react" +import {proxy, useSnapshot} from "valtio" +import "./index.css" type Person = { - first: string - last: string - id: number + first: string + last: string + id: number } type State = { - people: Array - query: string - first: string - last: string - selected: number | undefined + people: Array + query: string + first: string + last: string + selected: number | undefined } const state = proxy({ - people: [], - query: '', - first: '', - last: '', - selected: undefined, + people: [], + query: "", + first: "", + last: "", + selected: undefined }) const setQuery = (e: React.ChangeEvent) => { - state.query = e.target.value + state.query = e.target.value } const reset = () => { - state.first = '' - state.last = '' - state.selected = undefined + state.first = "" + state.last = "" + state.selected = undefined } const create = () => { - state.people.push({ first: state.first, last: state.last, id: performance.now() }) - reset() + state.people.push({ + first: state.first, + last: state.last, + id: performance.now() + }) + reset() } const remove = () => { - if (state.selected === undefined) return - const index = state.people.findIndex((p) => p.id === state.selected) - if (index === -1) return - state.people.splice(index, 1) + if (state.selected === undefined) return + const index = state.people.findIndex((p) => p.id === state.selected) + if (index === -1) return + state.people.splice(index, 1) } const update = () => { - if (state.selected === undefined) return - const person = state.people.find((p) => p.id === state.selected) - if (!person) return - if (state.first.length) person.first = state.first - if (state.last.length) person.last = state.last - reset() + if (state.selected === undefined) return + const person = state.people.find((p) => p.id === state.selected) + if (!person) return + if (state.first.length) person.first = state.first + if (state.last.length) person.last = state.last + reset() } const setSelected = (e: React.ChangeEvent) => { - e.preventDefault() - state.selected = Number(e.target.value) + e.preventDefault() + state.selected = Number(e.target.value) } -type NameKey = keyof Pick +type NameKey = keyof Pick const setName = (e: React.ChangeEvent) => { - const { name, value } = e.target - state[name as NameKey] = value + const {name, value} = e.target + state[name as NameKey] = value } function usePeople() { - const snap = useSnapshot(state) - const people = snap.people - const query = snap.query - return { - people: people.filter( - (person) => !query || person.first.startsWith(query) || person.last.startsWith(query) - ), - selected: snap.selected, - } + const snap = useSnapshot(state) + const people = snap.people + const query = snap.query + return { + people: people.filter( + (person) => + !query || + person.first.startsWith(query) || + person.last.startsWith(query) + ), + selected: snap.selected + } } const Select = () => { - const selectRef = React.useRef(null) - const { people, selected } = usePeople() - return ( - { - if (selectRef.current) selectRef.current.selectedIndex = -1 - }} - > - {people.map((person) => ( - - {person.last}, {person.first} - - ))} - - ) + const selectRef = React.useRef(null) + const {people, selected} = usePeople() + return ( + { + if (selectRef.current) selectRef.current.selectedIndex = -1 + }} + > + {people.map((person) => ( + + {person.last}, {person.first} + + ))} + + ) } const CRUD = () => { - const snap = useSnapshot(state) + const snap = useSnapshot(state) - const hasName = snap.first.length > 0 && snap.last.length > 0 - const hasPartOfName = snap.first.length + snap.last.length > 0 - const hasSelected = snap.selected !== undefined + const hasName = snap.first.length > 0 && snap.last.length > 0 + const hasPartOfName = snap.first.length + snap.last.length > 0 + const hasSelected = snap.selected !== undefined - return ( - - - - - - - - - - - - create - - - update - - - delete - - - - ) + return ( + + + + + + + + + + + + create + + + update + + + delete + + + + ) } export default CRUD diff --git a/src/GuiSection.tsx b/src/GuiSection.tsx new file mode 100644 index 0000000..fb66ba8 --- /dev/null +++ b/src/GuiSection.tsx @@ -0,0 +1,22 @@ +import code from "./code.svg" + +type Props = { + children: React.ReactNode + title: string + dir: string +} +export const GuiSection = ({children, title, dir}: Props) => { + return ( + + + {title} + + + + + {children} + + ) +} diff --git a/src/cells/index.css b/src/cells/index.css new file mode 100644 index 0000000..dbbbdf0 --- /dev/null +++ b/src/cells/index.css @@ -0,0 +1,53 @@ +.grid { + display: grid; + overflow: scroll; + width: 100%; + height: 800px; + border: 1px solid var(--border); + grid-template-rows: repeat(27, 50px); + grid-gap: 0; +} + +.row { + line-height: 1; +} + +.cell { + border: 1px solid var(--border); + width: 100px; +} + +.cell.active { + border: 1px solid var(--focus); +} + +.cell-input { + width: 96px; + height: 45px; + margin: 0; + padding: 0; + border-radius: 0; + padding-left: 5px; + box-shadow: none; +} + +.init-col { + width: 50px; +} + +.init-row, +.init-col { + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; +} + +.read-only { + display: flex; + height: 50px; + width: 100%; + align-items: center; + justify-content: flex-start; + padding-left: 5px; +} diff --git a/src/cells/index.tsx b/src/cells/index.tsx new file mode 100644 index 0000000..c1b5cf6 --- /dev/null +++ b/src/cells/index.tsx @@ -0,0 +1,119 @@ +import * as React from "react" +import {proxy, useSnapshot} from "valtio" +import {proxyMap} from "valtio/utils" +import "./index.css" + +type CellValue = {exp?: string} +type Cells = Record + +const getColId = (col: number) => String.fromCharCode("A".charCodeAt(0) + col) + +const COLUMNS = Array.from(Array(26).keys()) +const ROWS = Array.from(Array(100).keys()) + +const cellState = proxyMap() +const uiState = proxy<{active: string | undefined}>({active: undefined}) +const setEditing = (id: string | undefined) => { + uiState.active = id +} + +const evalCell = (exp: string) => { + if (!exp.startsWith("=")) { + return exp + } + try { + const fn = Function( + "get", + ` + 'use strict'; return ${exp + .slice(1) + .replace(/\b([A-Z]\d{1,2})\b/g, (m) => `get('${m}')`)}; + ` + ) + return fn((cellId: string) => { + const val = evalCell(cellState.get(cellId) || ("" as string)) + const num = Number(val) + return Number.isFinite(num) ? num : val + }) + } catch (e) { + return `#ERROR ${e}` + } +} + +const Cell = React.memo(({id, exp}: {id: string; exp: string}) => { + const uiSnap = useSnapshot(uiState) + const active = uiSnap.active === id + const value = evalCell(exp) + return ( + { + setEditing(id) + }} + > + {active ? ( + { + if (e.key === "Enter") { + cellState.set(id, e.target.value) + } + }} + onBlur={(e) => { + cellState.set(id, e.target.value) + setEditing(undefined) + }} + /> + ) : ( + {value} + )} + + ) +}) + +const Cells = () => { + const cellSnap = useSnapshot(cellState) + return ( + + {[-1, ...ROWS].map((row) => { + return ( + + {[-1, ...COLUMNS].map((column) => { + const key = `${column}${row}` + const colId = getColId(column) + if (row === -1 && column === -1) { + return + } + if (column === -1 && row > -1) { + return ( + + {row} + + ) + } + if (row === -1 && column > -1) { + return ( + + {colId} + + ) + } + return ( + + ) + })} + + ) + })} + + ) +} + +export default Cells diff --git a/src/circle-drawer/index.css b/src/circle-drawer/index.css index bf53748..18981bd 100644 --- a/src/circle-drawer/index.css +++ b/src/circle-drawer/index.css @@ -1,45 +1,45 @@ .adjuster { - position: relative; - text-align: center; - border: var(--border); - width: 300px; - border: var(--border); - margin-top: -30px; + position: relative; + text-align: center; + border: var(--border); + width: 300px; + border: var(--border); + margin-top: -30px; } .adjuster > input { - width: 100%; + width: 100%; } svg { - background-color: var(--background); - width: 100%; - height: 500px; - cursor: pointer; + background-color: var(--background); + width: 100%; + height: 500px; + cursor: pointer; } circle { - stroke: var(--border); + stroke: var(--border); } .controls { - display: flex; - justify-content: space-between; - align-items: center; + display: flex; + justify-content: space-between; + align-items: center; } .closer { - position: absolute; - top: -5px; - right: 0; - padding: 0.3em; - cursor: pointer; - color: var(--highlight); - background: var(--background-alt); - border-radius: 3px; + position: absolute; + top: -5px; + right: 0; + padding: 0.3em; + cursor: pointer; + color: var(--highlight); + background: var(--background-alt); + border-radius: 3px; } .closer:hover { - background: var(--button-hover); - border: 1px solid var(--border); + background: var(--button-hover); + border: 1px solid var(--border); } diff --git a/src/circle-drawer/index.tsx b/src/circle-drawer/index.tsx index 1d89f40..9f9f046 100644 --- a/src/circle-drawer/index.tsx +++ b/src/circle-drawer/index.tsx @@ -1,111 +1,118 @@ -import * as React from 'react' -import { proxy, useSnapshot } from 'valtio' -import { proxyWithHistory } from 'valtio/utils' -import './index.css' +import * as React from "react" +import {proxy, useSnapshot} from "valtio" +import {proxyWithHistory} from "valtio/utils" +import "./index.css" type Circle = { - cx: number - cy: number - r: number - id: number + cx: number + cy: number + r: number + id: number } const historyState = proxyWithHistory>([]) const state = proxy<{ - selected: number | undefined - circleHistory: typeof historyState + selected: number | undefined + circleHistory: typeof historyState }>({ - circleHistory: historyState, - selected: undefined, + circleHistory: historyState, + selected: undefined }) const selectCircle = (e: React.MouseEvent, index: number) => { - e.preventDefault() - e.stopPropagation() - if (state.selected === index) { - state.selected = undefined - } else { - state.selected = index - } + e.preventDefault() + e.stopPropagation() + if (state.selected === index) { + state.selected = undefined + } else { + state.selected = index + } } const unselectCircle = () => { - state.selected = undefined + state.selected = undefined } const addCircle = (e: React.MouseEvent) => { - e.preventDefault() - if (state.selected !== undefined) return - const { x, y } = (e.currentTarget as any).getBoundingClientRect() - historyState.value.push({ - r: 20, - cx: e.clientX - x, - cy: e.clientY - y, - id: performance.now(), - }) + e.preventDefault() + if (state.selected !== undefined) return + const {x, y} = (e.currentTarget as any).getBoundingClientRect() + historyState.value.push({ + r: 20, + cx: e.clientX - x, + cy: e.clientY - y, + id: performance.now() + }) } const adjustRadius = (e: React.ChangeEvent) => { - if (state.selected === undefined) return - historyState.value[state.selected].r = Number(e.target.value) + if (state.selected === undefined) return + historyState.value[state.selected].r = Number(e.target.value) } const CircleDrawer = () => { - const snap = useSnapshot(state) + const snap = useSnapshot(state) - return ( - - - - { - unselectCircle() - snap.circleHistory.undo() - }} - > - Undo - - { - unselectCircle() - snap.circleHistory.redo() - }} - > - Redo - - - {snap.selected !== undefined && snap.circleHistory.value[snap.selected] && ( - - Adjust circle at ({snap.circleHistory.value[snap.selected].cx.toFixed(1)}, - {snap.circleHistory.value[snap.selected].cy.toFixed(1)}) - - X - - - - )} - - - {state.circleHistory.value.map((circle, index) => ( - selectCircle(e, index)} - fill={index === snap.selected ? 'var(--highlight)' : 'var(--background-alt)'} - /> - ))} - - - ) + return ( + + + + { + unselectCircle() + snap.circleHistory.undo() + }} + > + Undo + + { + unselectCircle() + snap.circleHistory.redo() + }} + > + Redo + + + {snap.selected !== undefined && + snap.circleHistory.value[snap.selected] && ( + + Adjust circle at ( + {snap.circleHistory.value[snap.selected].cx.toFixed(1)}, + {snap.circleHistory.value[snap.selected].cy.toFixed(1)}) + + X + + + + )} + + + {state.circleHistory.value.map((circle, index) => ( + selectCircle(e, index)} + fill={ + index === snap.selected + ? "var(--highlight)" + : "var(--background-alt)" + } + /> + ))} + + Click circle to resize + + ) } export default CircleDrawer diff --git a/src/code.svg b/src/code.svg new file mode 100644 index 0000000..380f2d7 --- /dev/null +++ b/src/code.svg @@ -0,0 +1,32 @@ + + + + + + + diff --git a/src/counter/index.tsx b/src/counter/index.tsx index ff6c24a..9e3c009 100644 --- a/src/counter/index.tsx +++ b/src/counter/index.tsx @@ -1,14 +1,15 @@ -import { proxy, useSnapshot } from 'valtio' -const state = proxy({ count: 0, inc: () => ++state.count }) +import {proxy, useSnapshot} from "valtio" +const state = proxy({count: 0, inc: () => ++state.count}) const Counter = () => { - const snap = useSnapshot(state) + const snap = useSnapshot(state) - return ( - - + - - ) + return ( + + {" "} + + + + ) } export default Counter diff --git a/src/flight-booker/index.css b/src/flight-booker/index.css index 53d4236..72fa0b8 100644 --- a/src/flight-booker/index.css +++ b/src/flight-booker/index.css @@ -1,3 +1,3 @@ .invalid { - border: 1px solid var(--highlight); + border: 1px solid var(--highlight); } diff --git a/src/flight-booker/index.tsx b/src/flight-booker/index.tsx index 5d0f2ba..e1cb1f1 100644 --- a/src/flight-booker/index.tsx +++ b/src/flight-booker/index.tsx @@ -1,115 +1,121 @@ -import { proxy, useSnapshot } from 'valtio' -import './index.css' +import {proxy, useSnapshot} from "valtio" +import "./index.css" const validDate = (dateString: string) => { - const date = new Date(dateString) - if (isNaN(+date)) { - return false - } - return true + const date = new Date(dateString) + if (isNaN(+date)) { + return false + } + return true } const beforeReturn = (departureStr: string, returnStr: string) => - new Date(departureStr).getTime() >= new Date(returnStr).getTime() + new Date(departureStr).getTime() >= new Date(returnStr).getTime() // each date has validation but it is really not possible to create an invalid date with the date input const departureState = proxy({ - value: new Date().toISOString().slice(0, 10), - invalid: false, - set(e: React.ChangeEvent) { - e.preventDefault() - const str = e.target.value - departureState.value = str - if (validDate(str)) { - departureState.invalid = false - } else { - departureState.invalid = true - } - }, + value: new Date().toISOString().slice(0, 10), + invalid: false, + set(e: React.ChangeEvent) { + e.preventDefault() + const str = e.target.value + departureState.value = str + if (validDate(str)) { + departureState.invalid = false + } else { + departureState.invalid = true + } + } }) const returnState = proxy({ - value: new Date(Date.now() + 86400000).toISOString().slice(0, 10), - invalid: false, - set(e: React.ChangeEvent) { - e.preventDefault() - const str = e.target.value - returnState.value = str - if (validDate(str)) { - returnState.invalid = false - } else { - returnState.invalid = true - } - }, + value: new Date(Date.now() + 86400000).toISOString().slice(0, 10), + invalid: false, + set(e: React.ChangeEvent) { + e.preventDefault() + const str = e.target.value + returnState.value = str + if (validDate(str)) { + returnState.invalid = false + } else { + returnState.invalid = true + } + } }) const state = proxy({ - departure: departureState, - return: returnState, - isReturn: false, - setIsReturn(e: React.ChangeEvent) { - e.preventDefault() - state.isReturn = e.target.value === 'true' - }, + departure: departureState, + return: returnState, + isReturn: false, + setIsReturn(e: React.ChangeEvent) { + e.preventDefault() + state.isReturn = e.target.value === "true" + } }) -type DateKeys = keyof Pick +type DateKeys = keyof Pick function useDurationValidation() { - const snap = useSnapshot(state) - return !snap.isReturn || !beforeReturn(snap.departure.value, snap.return.value) + const snap = useSnapshot(state) + return ( + !snap.isReturn || !beforeReturn(snap.departure.value, snap.return.value) + ) } const DateInput = ({ - name, - disabled, - invalid, + name, + disabled, + invalid }: { - name: DateKeys - disabled?: boolean - invalid?: boolean + name: DateKeys + disabled?: boolean + invalid?: boolean }) => { - const snap = useSnapshot(state[name]) - return ( - - ) + const snap = useSnapshot(state[name]) + return ( + + ) } const handleBooking = () => { - let msg = `Flight booked form ${state.departure.value}` - if (state.isReturn) { - msg += ` to ${state.return.value}` - } - alert(msg) + let msg = `Flight booked form ${state.departure.value}` + if (state.isReturn) { + msg += ` to ${state.return.value}` + } + alert(msg) } const FlightBooker = () => { - const snap = useSnapshot(state) - const isValidDuration = useDurationValidation() - return ( - - - one-way flight - return flight - - - - { - isValidDuration && handleBooking() - }} - > - book - - - ) + const snap = useSnapshot(state) + const isValidDuration = useDurationValidation() + return ( + + + one-way flight + return flight + + + + { + isValidDuration && handleBooking() + }} + > + book + + + ) } export default FlightBooker diff --git a/src/index.css b/src/index.css index db3e60c..9dc81c1 100644 --- a/src/index.css +++ b/src/index.css @@ -1,9 +1,19 @@ .row { - display: flex; + display: flex; } .row > button { - margin-left: 12px; + margin-left: 12px; } section { - padding-bottom: 18px; + padding-bottom: 18px; +} + +.section-title { + display: flex; + align-items: baseline; +} + +.section-title > a { + margin-left: 12px; + line-height: 0; } diff --git a/src/main.tsx b/src/main.tsx index 4a1b150..32f558d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,10 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App' -import './index.css' +import React from "react" +import ReactDOM from "react-dom/client" +import App from "./App" +import "./index.css" -ReactDOM.createRoot(document.getElementById('root')!).render( - - - +ReactDOM.createRoot(document.getElementById("root")!).render( + + + ) diff --git a/src/temp-convertor/index.tsx b/src/temp-convertor/index.tsx index 0589155..80aaf06 100644 --- a/src/temp-convertor/index.tsx +++ b/src/temp-convertor/index.tsx @@ -1,36 +1,40 @@ -import { proxy, useSnapshot } from 'valtio' +import {proxy, useSnapshot} from "valtio" const c2f = (x: number) => x * (9 / 5) + 32 const f2c = (x: number) => (x - 32) * (5 / 9) const state = proxy({ - celsius: Number(5).toFixed(1), - fahrenheit: c2f(5).toFixed(1), - convert: (e: React.ChangeEvent) => { - const { name, value } = e.target - const n = Number(value) - if (!isFinite(n)) return - if (name === 'celsius') { - state.celsius = value - state.fahrenheit = c2f(n).toFixed(1) - } else { - state.fahrenheit = value - state.celsius = f2c(n).toFixed(1) - } - }, + celsius: Number(5).toFixed(1), + fahrenheit: c2f(5).toFixed(1), + convert: (e: React.ChangeEvent) => { + const {name, value} = e.target + const n = Number(value) + if (!isFinite(n)) return + if (name === "celsius") { + state.celsius = value + state.fahrenheit = c2f(n).toFixed(1) + } else { + state.fahrenheit = value + state.celsius = f2c(n).toFixed(1) + } + } }) const TempConvertor = () => { - const snap = useSnapshot(state) + const snap = useSnapshot(state) - return ( - - Celsius - - Fahrenheit - - - ) + return ( + + Celsius + + Fahrenheit + + + ) } export default TempConvertor diff --git a/src/timer/index.css b/src/timer/index.css index e50c035..546a136 100644 --- a/src/timer/index.css +++ b/src/timer/index.css @@ -1,20 +1,20 @@ .timer { - width: 100%; + width: 100%; } .progress-container { - display: flex; - align-items: center; - width: 100%; + display: flex; + align-items: center; + width: 100%; } .progress-container > progress { - width: 100%; - margin-right: 12px; + width: 100%; + margin-right: 12px; } .progress-container > label { - width: 50px; + width: 50px; } .timer > input { - width: 50%; + width: 50%; } diff --git a/src/timer/index.tsx b/src/timer/index.tsx index 8f44591..c5491d3 100644 --- a/src/timer/index.tsx +++ b/src/timer/index.tsx @@ -1,67 +1,74 @@ -import { proxy, useSnapshot } from 'valtio' -import { subscribeKey } from 'valtio/utils' -import './index.css' +import {proxy, useSnapshot} from "valtio" +import {subscribeKey} from "valtio/utils" +import "./index.css" const tick = () => { - state.elapsed = window.performance.now() - state.start + state.elapsed = window.performance.now() - state.start } const state = proxy({ - start: window.performance.now(), - duration: 10000, - elapsed: 0, - setDuration(e: React.ChangeEvent) { - state.duration = Number(e.target.value) - setTimeout(() => state.reset()) - }, - reset() { - state.elapsed = 0 - state.start = window.performance.now() - tick() - }, + start: window.performance.now(), + duration: 10000, + elapsed: 0, + setDuration(e: React.ChangeEvent) { + state.duration = Number(e.target.value) + setTimeout(() => state.reset()) + }, + reset() { + state.elapsed = 0 + state.start = window.performance.now() + tick() + } }) let raf: number -subscribeKey(state, 'elapsed', () => { - if (state.elapsed < state.duration) { - raf = requestAnimationFrame(tick) - } - return () => { - cancelAnimationFrame(raf) - } +subscribeKey(state, "elapsed", () => { + if (state.elapsed < state.duration) { + raf = requestAnimationFrame(tick) + } + return () => { + cancelAnimationFrame(raf) + } }) raf = requestAnimationFrame(tick) const Progress = () => { - const snap = useSnapshot(state) - const progress = snap.elapsed / snap.duration - return ( - - {((progress * snap.duration) / 1000).toFixed(1)}s - - ) + const snap = useSnapshot(state) + const progress = snap.elapsed / snap.duration + return ( + + {" "} + {((progress * snap.duration) / 1000).toFixed(1)}s + + ) } const Duration = () => { - const snap = useSnapshot(state) + const snap = useSnapshot(state) - return ( - - ) + return ( + + ) } const Timer = () => { - const snap = useSnapshot(state) + const snap = useSnapshot(state) - return ( - - - - Reset - - ) + return ( + + + + Reset + + ) } export default Timer