@atom.io/template-solid-lossless-numbers 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.0.1
4
+
5
+ - Initial Solid rationality playground template
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # Solid Rationality Playground
2
+
3
+ A Vite + Solid + `atom.io/solid` template for exploring exact rational arithmetic with the `rationality` package.
4
+
5
+ ## What it shows
6
+
7
+ - editable `Rational` instances stored in `atom.io`
8
+ - exact simplification and consolidation
9
+ - chained multiplication and division across multiple rationals
10
+ - comparison between rationals without floating-point drift
11
+ - prime-factor inspection of the simplified numerator and denominator
12
+
13
+ ## Local development
14
+
15
+ ```bash
16
+ bun install
17
+ bun run dev
18
+ ```
package/_gitignore ADDED
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ dist
3
+ .DS_Store
@@ -0,0 +1 @@
1
+ app-shell._class_b83uo_1{color:red}:root{--bg:#f6f1e8;--bg-deep:#efe3d0;--panel:#fffcf7db;--panel-strong:#fffaf2;--ink:#172433;--muted:#5d6670;--gold:#d88d1f;--gold-soft:#f5d8a5;--mint:#bfd7c2;--line:#17243324;--shadow:0 24px 80px #3721081f;--radius-xl:28px;--radius-lg:20px;--radius-md:14px;--radius-sm:10px;color:var(--ink);background:radial-gradient(circle at top left, #d88d1f29, transparent 28%), radial-gradient(circle at right center, #1724331f, transparent 30%), linear-gradient(180deg, var(--bg) 0%, var(--bg-deep) 100%);font-family:Iowan Old Style,Palatino Linotype,Book Antiqua,Palatino,serif}*{box-sizing:border-box}html,body,#app{min-height:100%;margin:0}body{min-height:100vh}button,input,select{font:inherit}button,select,input{border:1px solid var(--line);border-radius:var(--radius-sm)}button{cursor:pointer;background:var(--ink);color:#fff;padding:.7rem 1rem;transition:transform .14s,box-shadow .14s,background .14s}button:hover{transform:translateY(-1px);box-shadow:0 12px 28px #17243324}button:disabled{cursor:not-allowed;opacity:.45;box-shadow:none;transform:none}button.secondary,button.ghost-button{color:var(--ink);background:0 0}button.secondary{border-style:dashed}button.ghost-button{padding-inline:.85rem}input,select{background:#ffffffbf;width:100%;padding:.75rem .85rem}code{font-family:SFMono-Regular,Cascadia Code,Fira Code,Menlo,Consolas,monospace;font-size:.9em}
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0"
8
+ />
9
+ <title>Rationality Playground</title>
10
+ <script type="module" crossorigin src="/assets/index-CX-h4DwZ.js"></script>
11
+ <link rel="stylesheet" crossorigin href="/assets/index-CulqNJBq.css">
12
+ </head>
13
+ <body>
14
+ <div id="app"></div>
15
+ </body>
16
+ </html>
@@ -0,0 +1,178 @@
1
+ import { default as TypeScriptPlugin } from "@typescript-eslint/eslint-plugin"
2
+ import * as parser from "@typescript-eslint/parser"
3
+ import AtomIOPlugin from "atom.io/eslint-plugin"
4
+ import type { Linter } from "eslint"
5
+ import * as ImportPlugin from "eslint-plugin-import-x"
6
+ import { default as SimpleImportSortPlugin } from "eslint-plugin-simple-import-sort"
7
+
8
+ const ERROR = 2
9
+
10
+ const parserOptions = {
11
+ project: [`./tsconfig.app.json`, `./tsconfig.node.json`],
12
+ sourceType: `module`,
13
+ } satisfies parser.ParserOptions
14
+
15
+ const commonRules = {
16
+ "@typescript-eslint/adjacent-overload-signatures": ERROR,
17
+ "@typescript-eslint/array-type": 0,
18
+ "@typescript-eslint/await-thenable": 0,
19
+ "@typescript-eslint/ban-ts-comment": ERROR,
20
+ "@typescript-eslint/ban-tslint-comment": 0,
21
+ "@typescript-eslint/class-literal-property-style": ERROR,
22
+ "@typescript-eslint/consistent-generic-constructors": 0,
23
+ "@typescript-eslint/consistent-indexed-object-style": 0,
24
+ "@typescript-eslint/consistent-return": 0,
25
+ "@typescript-eslint/consistent-type-assertions": 0,
26
+ "@typescript-eslint/consistent-type-definitions": 0,
27
+ "@typescript-eslint/consistent-type-exports": ERROR,
28
+ "@typescript-eslint/consistent-type-imports": [
29
+ ERROR,
30
+ {
31
+ fixStyle: `separate-type-imports`,
32
+ prefer: `type-imports`,
33
+ },
34
+ ],
35
+ "@typescript-eslint/default-param-last": ERROR,
36
+ "@typescript-eslint/dot-notation": 0,
37
+ "@typescript-eslint/explicit-function-return-type": 0,
38
+ "@typescript-eslint/explicit-member-accessibility": ERROR,
39
+ "@typescript-eslint/explicit-module-boundary-types": ERROR,
40
+ "@typescript-eslint/init-declarations": 0,
41
+ "@typescript-eslint/max-params": 0,
42
+ "@typescript-eslint/member-ordering": 0,
43
+ "@typescript-eslint/method-signature-style": 0,
44
+ "@typescript-eslint/naming-convention": [
45
+ 0,
46
+ {
47
+ selector: `variable`,
48
+ format: [`strictCamelCase`, `StrictPascalCase`, `UPPER_CASE`],
49
+ leadingUnderscore: `allow`,
50
+ trailingUnderscore: `allow`,
51
+ },
52
+ ],
53
+ "@typescript-eslint/no-array-constructor": ERROR,
54
+ "@typescript-eslint/no-array-delete": ERROR,
55
+ "@typescript-eslint/no-base-to-string": ERROR,
56
+ "@typescript-eslint/no-confusing-non-null-assertion": ERROR,
57
+ "@typescript-eslint/no-confusing-void-expression": ERROR,
58
+ "@typescript-eslint/no-duplicate-enum-values": ERROR,
59
+ "@typescript-eslint/no-duplicate-type-constituents": ERROR,
60
+ "@typescript-eslint/no-dynamic-delete": ERROR,
61
+ "@typescript-eslint/no-empty-function": 0,
62
+ "@typescript-eslint/no-empty-interface": ERROR,
63
+ "@typescript-eslint/no-explicit-any": 0,
64
+ "@typescript-eslint/no-extra-non-null-assertion": ERROR,
65
+ "@typescript-eslint/no-extraneous-class": 0,
66
+ "@typescript-eslint/no-floating-promises": ERROR,
67
+ "@typescript-eslint/no-for-in-array": ERROR,
68
+ "@typescript-eslint/no-implied-eval": ERROR,
69
+ "@typescript-eslint/no-import-type-side-effects": ERROR,
70
+ "@typescript-eslint/no-inferrable-types": 0, // not compatible with isolatedDeclarations
71
+ "@typescript-eslint/no-invalid-void-type": 0, // void is good in unions sometimes?
72
+ "@typescript-eslint/no-loop-func": 0,
73
+ "@typescript-eslint/no-loss-of-precision": ERROR,
74
+ "@typescript-eslint/no-magic-numbers": 0,
75
+ "@typescript-eslint/no-meaningless-void-operator": ERROR,
76
+ "@typescript-eslint/no-misused-new": ERROR,
77
+ "@typescript-eslint/no-misused-promises": 0,
78
+ "@typescript-eslint/no-mixed-enums": ERROR,
79
+ "@typescript-eslint/no-namespace": 0,
80
+ "@typescript-eslint/no-non-null-asserted-nullish-coalescing": ERROR,
81
+ "@typescript-eslint/no-non-null-asserted-optional-chain": ERROR,
82
+ "@typescript-eslint/no-non-null-assertion": 0,
83
+ "@typescript-eslint/no-redundant-type-constituents": ERROR,
84
+ "@typescript-eslint/no-require-imports": ERROR,
85
+ "@typescript-eslint/no-restricted-imports": 0,
86
+ "@typescript-eslint/no-shadow": ERROR,
87
+ "@typescript-eslint/no-this-alias": ERROR,
88
+ "@typescript-eslint/no-throw-literal": 0,
89
+ "@typescript-eslint/no-unnecessary-boolean-literal-compare": 0,
90
+ "@typescript-eslint/no-unnecessary-condition": 0,
91
+ "@typescript-eslint/no-unnecessary-qualifier": 0,
92
+ "@typescript-eslint/no-unnecessary-type-arguments": 0,
93
+ "@typescript-eslint/no-unnecessary-type-assertion": ERROR,
94
+ "@typescript-eslint/no-unnecessary-type-constraint": ERROR,
95
+ "@typescript-eslint/no-unnecessary-type-conversion": ERROR,
96
+ "@typescript-eslint/no-unsafe-argument": 0,
97
+ "@typescript-eslint/no-unsafe-assignment": 0,
98
+ "@typescript-eslint/no-unsafe-call": 0,
99
+ "@typescript-eslint/no-unsafe-member-access": 0,
100
+ "@typescript-eslint/no-unsafe-return": 0,
101
+ "@typescript-eslint/no-unsafe-unary-minus": ERROR,
102
+ "@typescript-eslint/no-unused-expressions": 0,
103
+ "@typescript-eslint/no-unused-vars": 0,
104
+ "@typescript-eslint/no-use-before-define": 0,
105
+ "@typescript-eslint/no-useless-constructor": ERROR,
106
+ "@typescript-eslint/no-useless-empty-export": ERROR,
107
+ "@typescript-eslint/no-var-requires": ERROR,
108
+ "@typescript-eslint/non-nullable-type-assertion-style": 0,
109
+ "@typescript-eslint/only-throw-error": ERROR,
110
+ "@typescript-eslint/parameter-properties": 0,
111
+ "@typescript-eslint/prefer-as-const": ERROR,
112
+ "@typescript-eslint/prefer-destructuring": 0,
113
+ "@typescript-eslint/prefer-enum-initializers": ERROR,
114
+ "@typescript-eslint/prefer-find": ERROR,
115
+ "@typescript-eslint/prefer-for-of": ERROR,
116
+ "@typescript-eslint/prefer-function-type": ERROR,
117
+ "@typescript-eslint/prefer-includes": ERROR,
118
+ "@typescript-eslint/prefer-literal-enum-member": ERROR,
119
+ "@typescript-eslint/prefer-namespace-keyword": ERROR,
120
+ "@typescript-eslint/prefer-nullish-coalescing": ERROR,
121
+ "@typescript-eslint/prefer-optional-chain": ERROR,
122
+ "@typescript-eslint/prefer-promise-reject-errors": 0,
123
+ "@typescript-eslint/prefer-readonly": 0,
124
+ "@typescript-eslint/prefer-readonly-parameter-types": 0,
125
+ "@typescript-eslint/prefer-reduce-type-parameter": 0,
126
+ "@typescript-eslint/prefer-regexp-exec": 0,
127
+ "@typescript-eslint/prefer-return-this-type": ERROR,
128
+ "@typescript-eslint/prefer-string-starts-ends-with": ERROR,
129
+ "@typescript-eslint/prefer-ts-expect-error": ERROR,
130
+ "@typescript-eslint/promise-function-async": 0,
131
+ "@typescript-eslint/require-array-sort-compare": 0,
132
+ "@typescript-eslint/require-await": ERROR,
133
+ "@typescript-eslint/restrict-plus-operands": ERROR,
134
+ "@typescript-eslint/restrict-template-expressions": ERROR,
135
+ "@typescript-eslint/return-await": ERROR,
136
+ "@typescript-eslint/sort-type-constituents": ERROR,
137
+ "@typescript-eslint/strict-boolean-expressions": 0,
138
+ "@typescript-eslint/switch-exhaustiveness-check": [
139
+ ERROR,
140
+ { requireDefaultForNonUnion: true },
141
+ ],
142
+ "@typescript-eslint/triple-slash-reference": ERROR,
143
+ "@typescript-eslint/typedef": 0,
144
+ "@typescript-eslint/unbound-method": 0,
145
+ "@typescript-eslint/unified-signatures": ERROR,
146
+
147
+ "atom.io/exact-catch-types": ERROR,
148
+ "atom.io/explicit-state-types": ERROR,
149
+ "atom.io/naming-convention": ERROR,
150
+
151
+ "import/newline-after-import": ERROR,
152
+ "import/no-duplicates": ERROR,
153
+
154
+ "simple-import-sort/imports": ERROR,
155
+ "simple-import-sort/exports": ERROR,
156
+
157
+ "no-mixed-spaces-and-tabs": 0,
158
+ quotes: [ERROR, `backtick`],
159
+ } satisfies Linter.Config[`rules`]
160
+
161
+ const configs: Linter.Config[] = [
162
+ {
163
+ ignores: [`**/dist/**`, `**/gen/**`, `**/node_modules/**`],
164
+ },
165
+ {
166
+ languageOptions: { parser, parserOptions },
167
+ files: [`**/*.ts{,x}`, `eslint.config.ts`],
168
+ plugins: {
169
+ "@typescript-eslint": TypeScriptPlugin,
170
+ "atom.io": AtomIOPlugin,
171
+ import: ImportPlugin,
172
+ "simple-import-sort": SimpleImportSortPlugin,
173
+ },
174
+ rules: commonRules,
175
+ },
176
+ ]
177
+
178
+ export default configs
package/eslint.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ // eslint-disable-next-line quotes
2
+ declare module "eslint-plugin-import-x" {
3
+ import type { ESLint } from "eslint"
4
+
5
+ const plugin: ESLint.Plugin
6
+ export = plugin
7
+ }
8
+
9
+ // eslint-disable-next-line quotes
10
+ declare module "eslint-plugin-simple-import-sort" {
11
+ import type { ESLint } from "eslint"
12
+
13
+ const plugin: ESLint.Plugin
14
+ export = plugin
15
+ }
16
+
17
+ // eslint-disable-next-line quotes
18
+ declare module "@typescript-eslint/eslint-plugin" {
19
+ import type { ESLint } from "eslint"
20
+
21
+ const plugin: ESLint.Plugin
22
+ export = plugin
23
+ }
24
+
25
+ // eslint-disable-next-line quotes
26
+ declare module "eslint-config-preact" {
27
+ import type { Linter } from "eslint"
28
+
29
+ const config: Linter.Config
30
+ export = config
31
+ }
package/index.html ADDED
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0"
8
+ />
9
+ <title>Rationality Playground</title>
10
+ </head>
11
+ <body>
12
+ <div id="app"></div>
13
+ <script type="module" src="/src/main.tsx"></script>
14
+ </body>
15
+ </html>
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@atom.io/template-solid-lossless-numbers",
3
+ "version": "0.0.1",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/jeremybanka/wayforge.git",
10
+ "directory": "templates/solid-lossless-numbers"
11
+ },
12
+ "type": "module",
13
+ "scripts": {
14
+ "dev": "vite",
15
+ "build": "vite build",
16
+ "preview": "vite preview"
17
+ },
18
+ "dependencies": {
19
+ "atom.io": "workspace:*",
20
+ "rationality": "workspace:*",
21
+ "solid-js": "1.9.11"
22
+ },
23
+ "devDependencies": {
24
+ "eslint": "10.1.0",
25
+ "typescript": "5.9.3",
26
+ "vite": "8.0.1",
27
+ "vite-plugin-solid": "2.11.11"
28
+ }
29
+ }
package/src/App.tsx ADDED
@@ -0,0 +1,298 @@
1
+ import type { AtomToken } from "atom.io"
2
+ import {
3
+ atom,
4
+ atomFamily,
5
+ findState,
6
+ getState,
7
+ mutableAtom,
8
+ resetState,
9
+ runTransaction,
10
+ selectorFamily,
11
+ setState,
12
+ transaction,
13
+ } from "atom.io"
14
+ import { useI, useO } from "atom.io/solid"
15
+ import { OList } from "atom.io/transceivers/o-list"
16
+ import { Rational } from "rationality"
17
+ import type { JSX } from "solid-js"
18
+ import { For, Show } from "solid-js"
19
+
20
+ import css from "./App.tsx.module.css"
21
+
22
+ type RationalKey = `rat::${string}`
23
+ const arbitrary = () => Math.random().toString(36).slice(2)
24
+
25
+ const newRationalNumeratorAtom = atom<bigint | undefined>({
26
+ key: `newRationalNumerator`,
27
+ default: undefined,
28
+ })
29
+ const newRationalDenominatorAtom = atom<bigint | undefined>({
30
+ key: `newRationalDenominator`,
31
+ default: undefined,
32
+ })
33
+ const newNumeratorAtoms = atomFamily<bigint | undefined, RationalKey>({
34
+ key: `newNumerator`,
35
+ default: undefined,
36
+ })
37
+ const newDenominatorAtoms = atomFamily<bigint | undefined, RationalKey>({
38
+ key: `newDenominator`,
39
+ default: undefined,
40
+ })
41
+
42
+ const rationalListAtom = mutableAtom<OList<RationalKey>>({
43
+ key: `rationalList`,
44
+ class: OList,
45
+ })
46
+
47
+ const rationalAtoms = atomFamily<Rational, RationalKey>({
48
+ key: `rational`,
49
+ default: () => new Rational(0n, 1n),
50
+ })
51
+ const simplifiedSelectors = selectorFamily<
52
+ [numerator: bigint, denominator: bigint],
53
+ RationalKey
54
+ >({
55
+ key: `simplified`,
56
+ get:
57
+ (key) =>
58
+ ({ get }) => {
59
+ const [numerator, denominator] = get(rationalAtoms, key).simplify()
60
+ return [numerator, denominator]
61
+ },
62
+ })
63
+ const floatSelectors = selectorFamily<number, RationalKey>({
64
+ key: `float`,
65
+ get:
66
+ (key) =>
67
+ ({ get }) => {
68
+ const [numerator, denominator] = get(simplifiedSelectors, key)
69
+ return Number(numerator) / Number(denominator)
70
+ },
71
+ })
72
+ const numberBitsSelectors = selectorFamily<(`0` | `1`)[], RationalKey>({
73
+ key: `numberBits`,
74
+ get:
75
+ (key) =>
76
+ ({ get }) => {
77
+ const [numerator, denominator] = get(simplifiedSelectors, key)
78
+ const value = Number(numerator) / Number(denominator)
79
+ const buffer = new ArrayBuffer(8)
80
+ const view = new DataView(buffer)
81
+ view.setFloat64(0, value, false) // big-endian
82
+ const bytes = new Uint8Array(buffer)
83
+ const bits = Array.from(bytes)
84
+ .map((b) => b.toString(2).padStart(8, `0`))
85
+ .join(``)
86
+ return [...bits].map((bit) => (bit === `1` ? `1` : `0`))
87
+ },
88
+ })
89
+ const isFloatPreciseSelectors = selectorFamily<boolean, RationalKey>({
90
+ key: `isFloatPrecise`,
91
+ get:
92
+ (key) =>
93
+ ({ get }) => {
94
+ const [, den] = get(simplifiedSelectors, key)
95
+ if (den === 0n) return false
96
+ return den > 0n && (den & (den - 1n)) === 0n
97
+ },
98
+ })
99
+ const approximateValueSelectors = selectorFamily<string, RationalKey>({
100
+ key: `approximateValue`,
101
+ get:
102
+ (key) =>
103
+ ({ get }) => {
104
+ const value = get(floatSelectors, key)
105
+ if (!Number.isFinite(value)) return value.toString()
106
+ const rounded = Number(value.toPrecision(5))
107
+ return rounded.toString()
108
+ },
109
+ })
110
+
111
+ const combineIntoRationalTx = transaction<
112
+ (key: RationalKey, operation: `add` | `div` | `mul` | `sub`) => void
113
+ >({
114
+ key: `combineIntoRationalTx`,
115
+ do: ({ get, set, reset }, key, operation) => {
116
+ const num = get(newNumeratorAtoms, key) ?? 1n
117
+ const den = get(newDenominatorAtoms, key) ?? 1n
118
+ set(rationalAtoms, key, get(rationalAtoms, key)[operation](num, den))
119
+ reset(newNumeratorAtoms, key)
120
+ reset(newDenominatorAtoms, key)
121
+ },
122
+ })
123
+
124
+ export function App(): JSX.Element {
125
+ const rationals = useO(rationalListAtom)
126
+ return (
127
+ <app class={css.class}>
128
+ <header>
129
+ <h1>Rationality Playground</h1>
130
+ </header>
131
+ <main>
132
+ <For each={rationals()}>{(key) => <RationalBreakdown key={key} />}</For>
133
+ <new-rational>
134
+ <RationalFields
135
+ numeratorAtom={newRationalNumeratorAtom}
136
+ denominatorAtom={newRationalDenominatorAtom}
137
+ />
138
+ <button
139
+ type="button"
140
+ onClick={() => {
141
+ const key: RationalKey = `rat::${arbitrary()}`
142
+ runTransaction(combineIntoRationalTx)(key, `add`)
143
+ const newNumerator = getState(newRationalNumeratorAtom) ?? 1n
144
+ const newDenominator = getState(newRationalDenominatorAtom) ?? 1n
145
+ const rational = new Rational(newNumerator, newDenominator)
146
+ setState(rationalAtoms, key, rational)
147
+ setState(rationalListAtom, (list) => (list.push(key), list))
148
+ resetState(newRationalNumeratorAtom)
149
+ resetState(newRationalDenominatorAtom)
150
+ }}
151
+ >
152
+ Add
153
+ </button>
154
+ </new-rational>
155
+ </main>
156
+ </app>
157
+ )
158
+ }
159
+
160
+ export function RationalBreakdown(props: { key: RationalKey }): JSX.Element {
161
+ const rational = useO(rationalAtoms, props.key)
162
+ const simplified = useO(simplifiedSelectors, props.key)
163
+ const float = useO(floatSelectors, props.key)
164
+ const isFloatPrecise = useO(isFloatPreciseSelectors, props.key)
165
+ const approximateValue = useO(approximateValueSelectors, props.key)
166
+ const bits = useO(numberBitsSelectors, props.key)
167
+ return (
168
+ <rational-breakdown>
169
+ <fraction-list>
170
+ <For each={[...rational().entries()]}>
171
+ {([denominator, numerator], index) => {
172
+ return (
173
+ <>
174
+ <Show when={index() !== 0}>
175
+ <plus>+</plus>
176
+ </Show>
177
+ <Fraction numerator={numerator} denominator={denominator} />
178
+ </>
179
+ )
180
+ }}
181
+ </For>
182
+ <infix-buttons>
183
+ <button
184
+ type="button"
185
+ onClick={() => {
186
+ runTransaction(combineIntoRationalTx)(props.key, `sub`)
187
+ }}
188
+ >
189
+ −
190
+ </button>
191
+ <button
192
+ type="button"
193
+ onClick={() => {
194
+ runTransaction(combineIntoRationalTx)(props.key, `add`)
195
+ }}
196
+ >
197
+ +
198
+ </button>
199
+ <button
200
+ type="button"
201
+ onClick={() => {
202
+ runTransaction(combineIntoRationalTx)(props.key, `div`)
203
+ }}
204
+ >
205
+ ÷
206
+ </button>
207
+ <button
208
+ type="button"
209
+ onClick={() => {
210
+ runTransaction(combineIntoRationalTx)(props.key, `mul`)
211
+ }}
212
+ >
213
+ ×
214
+ </button>
215
+ </infix-buttons>
216
+ <RationalCombineFields key={props.key} />
217
+ </fraction-list>
218
+
219
+ <equals>=</equals>
220
+ <Fraction numerator={simplified()[0]} denominator={simplified()[1]} />
221
+ <equals>{isFloatPrecise() ? `=` : `≈`}</equals>
222
+ <quotient>
223
+ <quotient-bits>
224
+ <For each={bits()}>
225
+ {(bit) => <bit data-css={bit === `1` ? `one` : undefined} />}
226
+ </For>
227
+ </quotient-bits>
228
+ <number>{isFloatPrecise() ? float() : approximateValue()}</number>
229
+ </quotient>
230
+ </rational-breakdown>
231
+ )
232
+ }
233
+
234
+ export function Fraction(props: {
235
+ numerator: bigint
236
+ denominator: bigint
237
+ }): JSX.Element {
238
+ return (
239
+ <fraction>
240
+ <numerator data-css={props.numerator === 1n ? `one` : undefined}>
241
+ {Number(props.numerator)}
242
+ </numerator>
243
+ <solidus />
244
+ <denominator data-css={props.denominator === 1n ? `one` : undefined}>
245
+ {Number(props.denominator)}
246
+ </denominator>
247
+ </fraction>
248
+ )
249
+ }
250
+
251
+ export function RationalCombineFields({
252
+ key,
253
+ }: {
254
+ key: RationalKey
255
+ }): JSX.Element {
256
+ const numeratorAtom = findState(newNumeratorAtoms, key)
257
+ const denominatorAtom = findState(newDenominatorAtoms, key)
258
+ return (
259
+ <RationalFields
260
+ numeratorAtom={numeratorAtom}
261
+ denominatorAtom={denominatorAtom}
262
+ />
263
+ )
264
+ }
265
+
266
+ export function RationalFields({
267
+ numeratorAtom,
268
+ denominatorAtom,
269
+ }: {
270
+ numeratorAtom: AtomToken<bigint | undefined>
271
+ denominatorAtom: AtomToken<bigint | undefined>
272
+ }): JSX.Element {
273
+ const numerator = useO(numeratorAtom)
274
+ const setNumerator = useI(numeratorAtom)
275
+ const denominator = useO(denominatorAtom)
276
+ const setDenominator = useI(denominatorAtom)
277
+ return (
278
+ <fieldset>
279
+ <input
280
+ type="number"
281
+ value={numerator()?.toString()}
282
+ onInput={(event) => {
283
+ setNumerator(BigInt(event.currentTarget.value))
284
+ }}
285
+ placeholder="1"
286
+ />
287
+ <solidus />
288
+ <input
289
+ type="number"
290
+ value={denominator()?.toString()}
291
+ onInput={(event) => {
292
+ setDenominator(BigInt(event.currentTarget.value))
293
+ }}
294
+ placeholder="1"
295
+ />
296
+ </fieldset>
297
+ )
298
+ }
@@ -0,0 +1,101 @@
1
+ /** biome-ignore-all lint/correctness/noUnknownTypeSelector: <explanation> */
2
+ app.class {
3
+ --line: rgba(23, 36, 51, 0.14);
4
+
5
+ & /* local globals */,
6
+ * {
7
+ display: flex;
8
+ }
9
+
10
+ flex-direction: column;
11
+
12
+ fraction,
13
+ fieldset {
14
+ flex-flow: column;
15
+ font-size: 24px;
16
+ border: none;
17
+ padding: 0;
18
+ > numerator,
19
+ > denominator,
20
+ > input {
21
+ margin: 0;
22
+ height: 36px;
23
+ flex-grow: 1;
24
+ align-items: center;
25
+ justify-content: center;
26
+ &[data-css="one"] {
27
+ color: #0002;
28
+ }
29
+ }
30
+ > solidus {
31
+ height: 2px;
32
+ background: #000;
33
+ }
34
+ }
35
+
36
+ > header {
37
+ align-items: center;
38
+ justify-content: space-between;
39
+ padding: 12px;
40
+ > h1 {
41
+ }
42
+ > button {
43
+ align-items: center;
44
+ justify-content: center;
45
+ }
46
+ }
47
+ > main {
48
+ padding: 2px;
49
+ gap: 2px;
50
+ flex-flow: column;
51
+ > rational-breakdown {
52
+ border: 1px solid #0002;
53
+ padding: 2px 12px;
54
+ > fraction-list {
55
+ flex-grow: 1;
56
+ > plus {
57
+ font-size: 30px;
58
+ color: #0002;
59
+ align-items: center;
60
+ }
61
+ > infix-buttons {
62
+ flex-flow: column;
63
+ }
64
+ }
65
+ > equals {
66
+ font-size: 48px;
67
+ align-items: center;
68
+ color: #0002;
69
+ }
70
+ > fraction {
71
+ }
72
+ > quotient {
73
+ align-items: center;
74
+ font-size: 48px;
75
+ position: relative;
76
+ > quotient-bits {
77
+ width: 40px;
78
+ position: absolute;
79
+ top: 0;
80
+ right: 0;
81
+ flex-wrap: wrap;
82
+ align-items: flex-start;
83
+ justify-content: flex-start;
84
+ justify-items: flex-start;
85
+ align-content: flex-start;
86
+ > bit {
87
+ flex-shrink: 0;
88
+ flex-grow: 0;
89
+ height: 5px;
90
+ width: 5px;
91
+ background: none;
92
+ border: 1px solid #0002;
93
+ &[data-css="one"] {
94
+ background: #0002;
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
@@ -0,0 +1,99 @@
1
+ :root {
2
+ --bg: #f6f1e8;
3
+ --bg-deep: #efe3d0;
4
+ --panel: rgba(255, 252, 247, 0.86);
5
+ --panel-strong: #fffaf2;
6
+ --ink: #172433;
7
+ --muted: #5d6670;
8
+ --gold: #d88d1f;
9
+ --gold-soft: #f5d8a5;
10
+ --mint: #bfd7c2;
11
+ --line: rgba(23, 36, 51, 0.14);
12
+ --shadow: 0 24px 80px rgba(55, 33, 8, 0.12);
13
+ --radius-xl: 28px;
14
+ --radius-lg: 20px;
15
+ --radius-md: 14px;
16
+ --radius-sm: 10px;
17
+ font-family:
18
+ "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Palatino, serif;
19
+ color: var(--ink);
20
+ background:
21
+ radial-gradient(
22
+ circle at top left,
23
+ rgba(216, 141, 31, 0.16),
24
+ transparent 28%
25
+ ),
26
+ radial-gradient(
27
+ circle at right center,
28
+ rgba(23, 36, 51, 0.12),
29
+ transparent 30%
30
+ ),
31
+ linear-gradient(180deg, var(--bg) 0%, var(--bg-deep) 100%);
32
+ }
33
+
34
+ * {
35
+ box-sizing: border-box;
36
+ }
37
+
38
+ html,
39
+ body,
40
+ #app {
41
+ margin: 0;
42
+ min-height: 100%;
43
+ }
44
+
45
+ body {
46
+ min-height: 100vh;
47
+ }
48
+
49
+ button,
50
+ input,
51
+ select {
52
+ font: inherit;
53
+ }
54
+
55
+ button,
56
+ select,
57
+ input {
58
+ border: 1px solid var(--line);
59
+ border-radius: var(--radius-sm);
60
+ }
61
+
62
+ button:hover {
63
+ transform: translateY(-1px);
64
+ box-shadow: 0 12px 28px rgba(23, 36, 51, 0.14);
65
+ }
66
+
67
+ button:disabled {
68
+ cursor: not-allowed;
69
+ opacity: 0.45;
70
+ transform: none;
71
+ box-shadow: none;
72
+ }
73
+
74
+ button.secondary,
75
+ button.ghost-button {
76
+ background: transparent;
77
+ color: var(--ink);
78
+ }
79
+
80
+ button.secondary {
81
+ border-style: dashed;
82
+ }
83
+
84
+ button.ghost-button {
85
+ padding-inline: 0.85rem;
86
+ }
87
+
88
+ input,
89
+ select {
90
+ width: 100%;
91
+ padding: 0.75rem 0.85rem;
92
+ background: rgba(255, 255, 255, 0.75);
93
+ }
94
+
95
+ code {
96
+ font-family:
97
+ "SFMono-Regular", "Cascadia Code", "Fira Code", Menlo, Consolas, monospace;
98
+ font-size: 0.9em;
99
+ }
@@ -0,0 +1,10 @@
1
+ /* eslint-disable quotes */
2
+ import type * as SolidJs from "solid-js"
3
+
4
+ declare module "solid-js" {
5
+ namespace JSX {
6
+ interface IntrinsicElements {
7
+ [tagname: string]: SolidJs.JSX.HTMLAttributes<HTMLDivElement>
8
+ }
9
+ }
10
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,14 @@
1
+ import { StoreProvider } from "atom.io/solid"
2
+ import { render } from "solid-js/web"
3
+
4
+ import { App } from "./App"
5
+ import "./globals.css"
6
+
7
+ render(
8
+ () => (
9
+ <StoreProvider>
10
+ <App />
11
+ </StoreProvider>
12
+ ),
13
+ document.getElementById(`app`)!,
14
+ )
@@ -0,0 +1,5 @@
1
+ // eslint-disable-next-line quotes
2
+ declare module "*.tsx.module.css" {
3
+ const maintainableModule: { class: string }
4
+ export default maintainableModule
5
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2024",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "noEmit": true,
7
+ "strict": true,
8
+ "allowImportingTsExtensions": true,
9
+ "exactOptionalPropertyTypes": true,
10
+ "jsx": "preserve",
11
+ "jsxImportSource": "solid-js",
12
+ "types": ["vite/client"]
13
+ },
14
+ "include": ["src", "vite.config.ts"]
15
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ESNext"],
6
+ "module": "ESNext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["vite.config.ts", "eslint.config.ts", "eslint.d.ts", "node"]
26
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,9 @@
1
+ import solid from "vite-plugin-solid"
2
+ import type { UserConfig } from "vite"
3
+ import { defineConfig } from "vite"
4
+
5
+ const config: UserConfig = defineConfig({
6
+ plugins: [solid()],
7
+ })
8
+
9
+ export default config