@pie-players/pie-calculator-cortex 0.3.75 → 1.0.0-beta.0

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/README.md CHANGED
@@ -1,9 +1,22 @@
1
1
  # `@pie-players/pie-calculator-cortex`
2
2
 
3
- A fully bundled, open-source basic, scientific, and graphing calculator
4
- implementation for PIE Players. It implements the provider-neutral contracts
5
- from `@pie-players/pie-calculator` using MathLive, Cortex Compute Engine, and
6
- JSXGraph.
3
+ A basic, scientific and graphing calculator adapter built on MathLive, the
4
+ CortexJS Compute Engine and JSXGraph. It implements the provider contract from
5
+ [`@pie-players/pie-calculator`](../calculator/README.md) and bundles all runtime
6
+ code and assets, so it needs no API key, CDN or network connection. Toolkit hosts
7
+ select it with `provider.id: "calculator-cortex"`
8
+ ([Calculator providers](../default-tool-loaders/README.md#calculator-providers));
9
+ hosts that mount a calculator themselves use `CortexCalculatorProvider` directly.
10
+ The [Desmos](../calculator-desmos/README.md) and
11
+ [GeoGebra](../calculator-geogebra/README.md) adapters implement the same contract.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ bun add @pie-players/pie-calculator-cortex
17
+ ```
18
+
19
+ ## Usage
7
20
 
8
21
  ```ts
9
22
  import { CortexCalculatorProvider } from "@pie-players/pie-calculator-cortex";
@@ -20,28 +33,154 @@ const calculator = await provider.createCalculator(
20
33
  );
21
34
  ```
22
35
 
23
- The package bundles all runtime code and required assets. It does not require
24
- an API key, CDN, or network connection.
36
+ `initialize()` takes `CortexCalculatorProviderInit`, which carries only
37
+ `onTelemetry` because there is no credential to supply. `createCalculator()`
38
+ initializes the provider on first use. Both fail with `worker-unavailable`
39
+ outside a browser, where module workers are unsupported, and after `destroy()`.
40
+ Evaluation runs in a Web Worker under the configured time limit.
41
+
42
+ The calculator implements every optional `Calculator` method. `getValue()`
43
+ returns the current input as LaTeX, `evaluate(latex)` returns the formatted
44
+ answer with a `.` decimal separator, and the capabilities report history,
45
+ graphing and a `maxPrecision` of 21.
46
+
47
+ ## Modes
48
+
49
+ | Mode | Accepted input |
50
+ | --- | --- |
51
+ | `basic` | Numbers, `+ − × ÷`, unary negation, parentheses, square root and percent. No constants. |
52
+ | `scientific` | Basic plus powers and roots, exponential and logarithms (natural, base 10 and any other base), trigonometric and inverse-trigonometric functions in the configured angle mode, absolute value, factorial, `π`, `e` and scientific notation. |
53
+ | `graphing` | Scientific plus the variable `x`, entered as `f(x)` or `y=f(x)`, for up to six expressions with pan, zoom, reset and keyboard tracing. |
54
+
55
+ Input is checked against the mode's function set after parsing, whichever key or
56
+ keystroke produced it. The factorial is the Gamma continuation off the integers,
57
+ matching Desmos, and a domain error at the negative integers.
58
+
59
+ ## Configuration
60
+
61
+ `createCalculator()` takes `CortexCalculatorProviderConfig`: the provider-neutral
62
+ config with `settings` typed as `CortexCalculatorSettings`. A value outside its
63
+ range rejects `createCalculator()` with `invalid-state`.
64
+
65
+ | Field | Default | Accepted values |
66
+ | --- | --- | --- |
67
+ | `locale` | `"en-US"` | A BCP 47 tag; see [Localization](#localization). |
68
+ | `theme` | `"auto"` | `"light"`, `"dark"` or `"auto"`; see [Theming](#theming). |
69
+ | `restrictedMode` | `false` | See [Restricted mode](#restricted-mode). |
70
+ | `settings.angleMode` | `"degree"` | `"degree"` or `"radian"` |
71
+ | `settings.calculationPrecision` | `15` | Integer 1–21, in significant digits |
72
+ | `settings.displayPrecision` | `10` | Integer 1–12 |
73
+ | `settings.historyLimit` | `20` | Integer 0–50; `0` keeps no history |
74
+ | `settings.evaluationTimeLimitMs` | `1000` | Integer 100–2000 |
75
+ | `settings.allowedFunctions` | The mode's full set | Function ids from the mode's set |
76
+ | `settings.allowClipboard` | `true` | Boolean |
77
+ | `settings.messages` | Built-in catalog | Partial message overrides; see [Localization](#localization) |
78
+ | `settings.direction` | `"auto"` | `"ltr"`, `"rtl"` or `"auto"` |
79
+ | `settings.graph.viewport` | `-10` to `10` on both axes | `{ xMin, xMax, yMin, yMax }`, finite, each minimum below its maximum |
80
+ | `settings.graph.showAxes` | `true` | Boolean |
81
+ | `settings.graph.showGrid` | `true` | Boolean |
82
+
83
+ The function ids are `square-root`, `power`, `root`, `exponential`,
84
+ `natural-log`, `common-log`, `log-base-n`, `sine`, `cosine`, `tangent`,
85
+ `inverse-sine`, `inverse-cosine`, `inverse-tangent`, `absolute-value` and
86
+ `factorial`. Basic mode's set is `square-root` alone; scientific and graphing
87
+ take all fifteen. `allowedFunctions` only narrows the mode's set: an id the mode
88
+ lacks is dropped, and an unknown id is rejected. The keypad hides the keys of
89
+ removed functions.
90
+
91
+ ## Restricted mode
92
+
93
+ `restrictedMode: true` blocks copy, cut, paste and the context menu in the
94
+ expression input, and forces `allowClipboard` to `false`. No setting relaxes it.
95
+ Clipboard blocking follows `restrictedMode` alone; `allowClipboard` is validated
96
+ and has no effect of its own. Clipboard policy beyond the input belongs to the
97
+ host.
98
+
99
+ ## Limits
100
+
101
+ | Limit | Value | Error |
102
+ | --- | --- | --- |
103
+ | Expression input | 1,024 UTF-16 code units | `expression-too-complex` |
104
+ | Parsed expression | 256 nodes, depth 32 | `expression-too-complex` |
105
+ | Evaluation time | `evaluationTimeLimitMs` | `evaluation-timeout` |
106
+ | Worker start | 20 seconds | `worker-unavailable` |
107
+ | Graph expressions | 6 | `invalid-state` on import |
108
+ | Graph samples | 200–1,200 points per expression, by plot width | — |
109
+
110
+ A timed-out evaluation terminates the worker, and the next calculation starts a
111
+ new one.
112
+
113
+ ## Errors
114
+
115
+ Failures throw `CortexCalculatorError` with a `code` and a `recoverable` flag.
116
+ The calculator shows recoverable errors beside the input and keeps the learner's
117
+ edit buffer.
118
+
119
+ | Code | Raised for | Recoverable |
120
+ | --- | --- | --- |
121
+ | `invalid-expression` | Input that does not parse or has no finite real result, and a calculation superseded by newer input. | Yes |
122
+ | `unsupported-expression` | A command, function or symbol outside the mode's set, or an unsupported calculator type. | Yes |
123
+ | `expression-too-complex` | Input over the size limits. | Yes |
124
+ | `evaluation-timeout` | An evaluation over the time limit. | Yes |
125
+ | `invalid-state` | Invalid settings, a container that is not an element, and a rejected state import. | Yes |
126
+ | `worker-unavailable` | No browser or module-worker support, a worker that fails to start or stops, and a destroyed provider or calculator. | No |
127
+
128
+ ## Telemetry
129
+
130
+ `onTelemetry` receives `pie-tool-operation-start`, `pie-tool-operation-success`
131
+ and `pie-tool-operation-error` for each evaluation. The payload carries
132
+ `toolId: "calculator"`, `backend: "cortex"`, `calculatorType`,
133
+ `operation: "evaluate"`, `duration` on completion and `errorType` (the error
134
+ code) on failure. It never carries expressions, results, history, state or graph
135
+ coordinates. A throwing or rejecting callback does not affect the calculator.
136
+
137
+ ## State
138
+
139
+ `exportState()` returns the provider-neutral `CalculatorState` with
140
+ `provider: "cortex"`, `value` equal to the current input, `history` capped at
141
+ `historyLimit`, and `providerState` in this schema:
25
142
 
26
- `initialize()` takes `CortexCalculatorProviderInit`, narrowed to `onTelemetry`
27
- for that reason -- there is no credential to supply. `createCalculator()` takes
28
- `CortexCalculatorProviderConfig`, which is the provider-neutral configuration
29
- with `settings` typed as `CortexCalculatorSettings`.
143
+ ```ts
144
+ import type { CortexCalculatorStateV1 } from "@pie-players/pie-calculator-cortex";
145
+
146
+ const providerState: CortexCalculatorStateV1 = {
147
+ schema: "pie-calculator-cortex",
148
+ version: 1,
149
+ type: "graphing",
150
+ angleMode: "radian",
151
+ calculationPrecision: 15,
152
+ displayPrecision: 10,
153
+ inputLatex: "\\sin(x)",
154
+ graph: {
155
+ viewport: { xMin: -10, xMax: 10, yMin: -10, yMax: 10 },
156
+ expressions: [
157
+ { id: "e1", latex: "\\sin(x)", colorIndex: 0, lineStyle: "solid", hidden: false },
158
+ ],
159
+ },
160
+ };
161
+ ```
30
162
 
31
- ## Isolated demos
163
+ `importState()` is atomic: the calculator is unchanged unless the whole state
164
+ validates. It rejects with `invalid-state`:
32
165
 
33
- Run `bun run --cwd packages/calculator-cortex demo`, then open the basic,
34
- scientific, or graphing page from the mode navigation. Each page mounts one
35
- calculator directly through `CortexCalculatorProvider`; it does not load an
36
- assessment player, toolkit coordinator, or tool wrapper.
166
+ - a `provider` other than `"cortex"`, or a `type` other than the calculator's;
167
+ - a `schema` other than `"pie-calculator-cortex"` or a `version` other than `1`;
168
+ - a `value` that differs from `inputLatex`;
169
+ - a precision outside its range;
170
+ - `graph` on a non-graphing state, or none on a graphing one;
171
+ - more than six expressions, an `id` that is empty, longer than 128 characters
172
+ or repeated, a `colorIndex` outside 0–5, or a `lineStyle` other than `solid`,
173
+ `dashed` or `dotted`;
174
+ - more history entries than `historyLimit`.
37
175
 
38
- The demo controls switch interface language, theme, and text direction by
39
- destroying and recreating only that calculator instance. **Panel size** does not
40
- recreate anything — it resizes the container to the box the tool shell actually
41
- gives the calculator. Check every change at both sizes it offers: *Shipped tool
42
- panel* is what the panel opens at for that type, *Panel minimum* is its configured
43
- resize floor, and the package's size-dependent rules are container queries on width
44
- and density tiers on height, so a fluid demo at 1280px reaches neither.
176
+ A persisted expression or history entry that fails validation rejects the import
177
+ with that expression's own error code.
178
+
179
+ Import restores the angle mode and keeps the calculator's configured precision.
180
+ Unknown fields are ignored and not re-emitted. Version 1 has no migration path,
181
+ and no state converts to or from another provider. The toolkit's
182
+ `<pie-tool-calculator>` does not persist state, so saving and restoring belong to
183
+ hosts that drive the adapter directly.
45
184
 
46
185
  ## Localization
47
186
 
@@ -53,11 +192,11 @@ displayed answer, and writing direction.
53
192
 
54
193
  One resolver serves the mathfield, the keypad's separator key and the displayed
55
194
  answer, so an `nl-NL` calculator whose keypad writes `1,5` answers `1,5`. The
56
- locale reaches the display only: `getResult`, the history entries a host reads and
57
- the serialized state stay `.`-separated, so state saved under one locale is not
58
- reinterpreted under another. It is a separator swap rather than a reformat, which
59
- is what keeps `displayPrecision` and an exponential answer like `2.432902008e+18`
60
- intact.
195
+ locale reaches the display only: the value `evaluate()` returns, the history
196
+ entries a host reads and the serialized state stay `.`-separated, so state saved
197
+ under one locale is not reinterpreted under another. The display swaps the
198
+ separator without reformatting, which keeps `displayPrecision` and an exponential
199
+ answer like `2.432902008e+18` intact.
61
200
 
62
201
  Every package-owned visible string, accessible name, status, and recoverable
63
202
  error can be replaced with typed per-instance messages:
@@ -76,181 +215,203 @@ await provider.createCalculator("basic", container, {
76
215
  ```
77
216
 
78
217
  Unspecified messages fall back to the selected built-in catalog, then English.
79
- Message templates retain their named placeholders, such as `{index}`,
80
- `{lineStyle}`, and `{result}`. `settings.direction` defaults to `"auto"`, which
81
- derives `ltr` or `rtl` from the locale; a host may explicitly set `"ltr"` or
82
- `"rtl"` when its language policy requires it.
83
-
84
- ## Keypad
85
-
86
- Basic and scientific render a display and a keypad; graphing puts the keypad in its
87
- expression rail. The keypad is this package's own — real `<button>` elements with
88
- localized accessible names, one tab stop with arrow-key movement inside it, and PIE
89
- tokens throughout.
90
-
91
- It is deliberately not MathLive's virtual keyboard, which is switched off entirely
92
- (`mathVirtualKeyboardPolicy = "manual"`) rather than merely hidden. MathLive's is a
93
- viewport-fixed singleton whose keycaps are `div[tabindex="-1"]` with no `role` and
94
- whose toggle is a `div[role="button"]` with no `tabindex`, so it contains no
95
- focusable elements at all and cannot be opened or operated by keyboard or switch
96
- access; under `"auto"` it also auto-shows on any touch-capable device, dropping
97
- itself across the bottom of the assessment rather than inside the tool panel. Its
98
- `container` setter throws inside an iframe, which is how assessments are commonly
99
- delivered.
100
-
101
- Keys are gated on `settings.allowedFunctions`, so a host that narrows the set gets a
102
- keypad that cannot offer a key the validator would reject. Basic omits the constants
103
- outright, matching `validateSymbol`. Scientific and graphing put their function keys
104
- on a second layer rather than in extra rows: a row costs about 50px of panel height
105
- at every size that ships, and eight rows in one layer puts the keypad past the
106
- panel's 480px floor. Four rows is the budget; the graphing layer spends five because
107
- it carries the graph keys too. The e2e suite switches to every layer and measures it
108
- at both the size the panel opens at and its resize floor.
109
-
110
- The commit key is on every layer, in the same corner. On the numeric layer alone it
111
- was unreachable from the function keys — Enter still committed, but a pointer or
112
- switch-access user has no Enter.
113
-
114
- Every key inserts a template with at most one placeholder. A second is
115
- unreachable: `ArrowRight` leaves a subscript or a fraction rather than crossing to
116
- the next placeholder, and MathLive binds `moveToNextPlaceholder` to Tab, which this
117
- keypad spends on being a single tab stop. `nth-root` and `fraction` therefore use
118
- `#@` to take the expression already typed as their second operand, and `log-base-n`
119
- fills its base and lets the argument follow the subscript.
120
-
121
- Layouts live as data in `src/keypad-layouts.ts`. Adding a key needs a message key in
122
- both catalogs — `as const satisfies CortexCalculatorMessages` makes that a
123
- compile-time obligation. Where a key's visible label is a word, its accessible name
124
- must contain that word (WCAG 2.5.3, and what voice control speaks): `keySine` is
125
- `"sin, sine"`, not `"sine of"`.
126
-
127
- ## Fitting the panel
128
-
129
- The panel is two surfaces with no card between them: a screen and a console, each
130
- running to its edges. The screen holds the tape, the live expression and the answer,
131
- with the angle mode pinned above its scroller so history passes behind it; the
132
- console is the keypad's recessed plane, carrying the layer tabs and the backspace and
133
- clear icons above the grid — inline SVG in `currentColor`, because `⌫` is the face a
134
- backspace button wants and the code point least likely to be in a host's font stack. Nothing sits on bare card, and the type's name is not drawn — the
135
- tool shell's header already carries it, and a second copy cost 46px of a 500px panel.
136
- It stays as visually-hidden text for the document outline. `--cortex-tape-inset` and
137
- the keypad's inline padding are one value, so the mathfield's text and the first key
138
- column share a left edge.
139
-
140
- A tool panel is resizable, so the height available is a runtime fact and every fixed
141
- size in the tool answers to it. `CalculatorView.svelte` measures its own box with a
142
- `ResizeObserver` and stamps `data-pie-density` — `comfortable` at 400px of content
143
- and up, `compact` to 320, `tight` below — and the metrics live as tokens that each
144
- tier re-declares in one place: key and control target sizes, the display's floor,
145
- the result's type size and the board's floor. A `ResizeObserver` rather than a
146
- `container-type: size` query, which carries `contain: layout` and would make the
147
- calculator the containing block for every fixed-position descendant, MathLive's
148
- popovers among them.
149
-
150
- Keys hold the 44px of WCAG 2.5.5 at every size a panel opens at, which is what the
151
- tiers are measured against: basic needs 398px of content and scientific 385px, and
152
- both open at more. Below that, keys give up height before the keypad gives up rows —
153
- a row scrolled out of the panel costs a pointer or switch-access learner the key
154
- entirely — and the smallest tier is 28px, clear of 2.5.8's 24px floor at Level AA.
155
-
156
- Nothing is ever clipped. The calculator root scrolls its own content as the floor
157
- case; it cannot be left to the tool shell, because the wrapper pins the calculator to
158
- `height: 100% !important` inside an `overflow: hidden` box, so the shell's
159
- `overflow-y: auto` never sees anything to scroll. Above that floor the graphing view
160
- places the pressure deliberately: stacked, the two panels hold their content and the
161
- calculator takes one scroll; side by side they scroll in their own columns instead,
162
- so a readout does not push the plot off the panel. Flex shrinking is what makes this
163
- load-bearing — an item shrunk below its content paints outside its box rather than
164
- clipping, which is how keypad rows were drawn over the graph controls.
218
+ Unknown keys are ignored. Message templates retain their named placeholders,
219
+ such as `{index}`, `{lineStyle}`, and `{result}`. `settings.direction` defaults
220
+ to `"auto"`, which derives `ltr` or `rtl` from the locale; a host sets `"ltr"`
221
+ or `"rtl"` when its language policy requires it.
165
222
 
166
223
  ## Theming
167
224
 
168
225
  `theme: "light" | "dark" | "auto"` supplies accessible package defaults.
169
226
  `"auto"` follows `prefers-color-scheme`.
170
227
 
171
- Those defaults are **fallbacks**, not declarations. Every colour resolves as
228
+ Those defaults are fallbacks. Every color resolves as
172
229
  `var(--pie-x, var(--cortex-x))`, so a host's tokens reach the tool and the
173
- package's own values apply only where the host has none. This matters beyond
174
- looks: `@pie-players/pie-theme` publishes ten `[data-color-scheme]` PNP palettes
175
- and marks every token used here as `required`, and declaring `--pie-*` on the
176
- calculator element — as this package once did — wins over anything an ancestor
177
- sets, so a learner's colour-scheme accommodation stopped at the calculator's edge.
178
- `tests/calculator-cortex-style-contract.test.ts` fails if such a declaration
179
- returns.
230
+ package's own values apply only where the host has none. `@pie-players/pie-theme`
231
+ publishes ten `[data-color-scheme]` PNP palettes and marks every token used here
232
+ as `required`, so the package never declares a `--pie-*` token on the calculator
233
+ element: a declaration there would override every ancestor, a learner's
234
+ color-scheme accommodation included.
235
+ `tests/calculator-cortex-style-contract.test.ts` enforces it.
180
236
 
181
237
  Consumed: `--pie-text`, `--pie-white`, `--pie-background-dark`, `--pie-border`,
182
238
  `--pie-border-gray`, `--pie-blue-grey-300`, `--pie-button-bg`,
183
- `--pie-button-color`, `--pie-button-hover-bg`, `--pie-button-active-bg`,
184
- `--pie-button-focus-outline`, `--pie-primary`, `--pie-primary-dark`,
185
- `--pie-incorrect`, `--pie-incorrect-secondary`, and `--pie-content-emphasis`.
186
-
187
- `--pie-background` is deliberately **not** among them. It is the page token, which
188
- a host may point at its own backdrop or at a translucent value, and a calculator
189
- resolving its fill through it would take every contrast guarantee out of this
190
- package's hands. Surfaces take `--pie-white` for the card and
191
- `--pie-background-dark` for the recessed keypad plane, both opaque in the base
192
- themes and in all ten schemes. A host wanting different surfaces has package
193
- hooks: `--pie-calculator-surface` and `--pie-calculator-surface-raised`.
194
- Both are package-private rather than registered host tokens: read as
195
- `var(--x, fallback)` and so overridable, with no compatibility guarantee.
196
-
197
- `--pie-font-scale` is **not** consumed, matching the recorded decision in
239
+ `--pie-button-color`, `--pie-button-hover-bg`, `--pie-button-hover-border`,
240
+ `--pie-button-active-bg`, `--pie-button-focus-outline`, `--pie-primary`,
241
+ `--pie-primary-dark`, `--pie-incorrect`, `--pie-incorrect-secondary`,
242
+ `--pie-content-emphasis` and `--pie-font-family`.
243
+
244
+ `--pie-background` is excluded. It is the page token, which a host may point at
245
+ its own backdrop or at a translucent value, and resolving the calculator's fill
246
+ through it would take every contrast guarantee out of this package's hands.
247
+ Surfaces take `--pie-white` for the card and `--pie-background-dark` for the
248
+ recessed keypad plane, both opaque in the base themes and in all ten schemes. The
249
+ card and keypad surfaces are package-private (`--pie-calculator-surface`,
250
+ `--pie-calculator-surface-raised`) and carry no compatibility guarantee.
251
+
252
+ `--pie-font-scale` is not consumed, matching the recorded decision in
198
253
  `section-player/tests/content-text-follows-font-scale.test.ts`: the font
199
254
  accommodation applies to what the learner reads, and a keypad growing with the
200
- passage is a layout problem rather than an accommodation.
255
+ passage is a layout problem.
201
256
 
202
257
  Graph colors have package-owned hooks because no canonical series palette
203
258
  exists: `--pie-calculator-series-1`, `--pie-calculator-series-2`,
204
259
  `--pie-calculator-series-3`, `--pie-calculator-series-4`,
205
- `--pie-calculator-series-5`, and `--pie-calculator-series-6`. Each series also
206
- has a solid, dashed, or dotted line style; hosts overriding colors must retain
207
- 3:1 contrast against the graph surface and keep the palette distinguishable.
260
+ `--pie-calculator-series-5`, and `--pie-calculator-series-6`, all registered
261
+ `component-public` tokens. Each series also has a solid, dashed, or dotted line
262
+ style; hosts overriding colors must retain 3:1 contrast against the graph surface
263
+ and keep the palette distinguishable.
264
+
265
+ JSXGraph's own axis and grid defaults are light in every theme, so the package
266
+ themes the plot's axes, tick labels and grid from the resolved tokens and
267
+ re-applies them when `theme: "auto"` follows an OS change. The plot is
268
+ `aria-hidden`, out of axe's reach, so `e2e/calculator-cortex.spec.ts` asserts its
269
+ contrast directly: tick labels at 4.5:1 as text and axes at 3:1 as a graphical
270
+ object.
271
+
272
+ ## Keypad
273
+
274
+ Basic and scientific render a display and a keypad; graphing puts the keypad in
275
+ its expression rail. The keypad is this package's own: real `<button>` elements
276
+ with localized accessible names, one tab stop with arrow-key movement inside it,
277
+ and PIE tokens throughout.
278
+
279
+ MathLive's virtual keyboard is switched off entirely
280
+ (`mathVirtualKeyboardPolicy = "manual"`). Its keycaps are `div[tabindex="-1"]`
281
+ with no `role` and its toggle is a `div[role="button"]` with no `tabindex`, so it
282
+ cannot be opened or operated by keyboard or switch access. It is a viewport-fixed
283
+ singleton that, under `"auto"`, shows on any touch-capable device across the
284
+ bottom of the assessment, and its `container` setter throws inside an iframe,
285
+ where assessments are commonly delivered.
286
+
287
+ Keys are gated on `settings.allowedFunctions`, so a narrowed set never offers a
288
+ key the validator would reject, and basic omits the constants. Scientific and
289
+ graphing put their function keys on a second layer: a row costs about 50px of
290
+ panel height at every shipped size, and eight rows in one layer puts the keypad
291
+ past the panel's 480px floor. Four rows is the budget; the graphing layer spends
292
+ five because it carries the graph keys too. The e2e suite switches to every layer
293
+ and measures it at both the size the panel opens at and its resize floor. The
294
+ commit key is on every layer, in the same corner, because a pointer or
295
+ switch-access user has no Enter key.
296
+
297
+ Every key inserts a template with at most one placeholder. A second is
298
+ unreachable: `ArrowRight` leaves a subscript or a fraction without crossing to
299
+ the next placeholder, and MathLive binds `moveToNextPlaceholder` to Tab, which
300
+ this keypad spends on being a single tab stop. `nth-root` and `fraction`
301
+ therefore use `#@` to take the expression already typed as their second operand,
302
+ and `log-base-n` fills its base and lets the argument follow the subscript.
303
+
304
+ Layouts live as data in `src/keypad-layouts.ts`. Adding a key needs a message key
305
+ in both catalogs, which `as const satisfies CortexCalculatorMessages` makes a
306
+ compile-time obligation. Where a key's visible label is a word, its accessible
307
+ name contains that word (WCAG 2.5.3, and what voice control speaks): `keySine` is
308
+ `"sin, sine"`.
309
+
310
+ ## Panel fit
311
+
312
+ The panel is two surfaces with no card between them, a screen and a console, each
313
+ running to its edges. The screen holds the tape, the live expression and the
314
+ answer, with the angle mode pinned above its scroller so history passes behind
315
+ it. The console is the keypad's recessed plane, carrying the layer tabs and the
316
+ backspace and clear icons above the grid, drawn as inline SVG in `currentColor`
317
+ because `⌫` is the code point least likely to be in a host's font stack. The
318
+ type's name is not drawn, since the tool shell's header already carries it and a
319
+ second copy costs 46px of a 500px panel; it stays as visually hidden text for the
320
+ document outline. `--cortex-tape-inset` and the keypad's inline padding are one
321
+ value, so the mathfield's text and the first key column share a left edge.
322
+
323
+ A tool panel is resizable, so every fixed size answers to the height available
324
+ at runtime. `CalculatorView.svelte` measures its own box with a `ResizeObserver`
325
+ and stamps `data-pie-density`: `comfortable` at 400px of content and up,
326
+ `compact` to 320px, `tight` below. Each tier re-declares the metrics tokens in
327
+ one place: key and control target sizes, the display's floor, the result's type
328
+ size and the board's floor. A `container-type: size` query would carry
329
+ `contain: layout` and make the calculator the containing block for every
330
+ fixed-position descendant, MathLive's popovers among them.
331
+
332
+ Keys hold the 44px of WCAG 2.5.5 at every size a panel opens at, which is what the
333
+ tiers are measured against: basic needs 398px of content and scientific 385px,
334
+ and both open at more. Below that, keys give up height before the keypad gives up
335
+ rows, because a row scrolled out of the panel costs a pointer or switch-access
336
+ learner the key entirely. The smallest tier is 28px, clear of 2.5.8's 24px floor
337
+ at Level AA.
338
+
339
+ Nothing is clipped. The calculator root scrolls its own content as the floor
340
+ case, because the wrapper pins the calculator to `height: 100% !important` inside
341
+ an `overflow: hidden` box, so the shell's `overflow-y: auto` never has anything
342
+ to scroll. Above that floor the graphing view places the pressure: stacked, the
343
+ two panels hold their content and the calculator takes one scroll; side by side,
344
+ each scrolls in its own column, so a readout does not push the plot off the
345
+ panel. The arrangement is load-bearing because a flex item shrunk below its
346
+ content paints outside its box without clipping.
347
+
348
+ ## Isolated demos
208
349
 
209
- The plot's axes, tick labels and grid are themed from the resolved tokens and
210
- re-applied when `theme: "auto"` follows the OS across a change. They have to be:
211
- JSXGraph initialised with bare `axis: true` / `grid: true` uses its light defaults
212
- in every theme, which put black tick labels on a dark plot at 1.43:1. The plot div
213
- is `aria-hidden`, so axe never sees inside it — the contrast is asserted directly
214
- in `e2e/calculator-cortex.spec.ts` instead, tick labels at 4.5:1 as text and axes
215
- at 3:1 as a graphical object.
350
+ Run `bun run --cwd packages/calculator-cortex demo`, then open the basic,
351
+ scientific, or graphing page from the mode navigation. Each page mounts one
352
+ calculator directly through `CortexCalculatorProvider`, without an assessment
353
+ player, toolkit coordinator or tool wrapper.
354
+
355
+ The demo controls switch interface language, theme, and text direction by
356
+ destroying and recreating only that calculator instance. **Panel size** resizes
357
+ the container to the box the tool shell gives the calculator, without recreating
358
+ it. Check every change at both sizes it offers: *Shipped tool panel* is what the
359
+ panel opens at for that type, and *Panel minimum* is its configured resize floor.
360
+ The package's size-dependent rules are container queries on width and density
361
+ tiers on height, which a fluid demo at 1280px reaches neither of.
216
362
 
217
- ## Coverage
363
+ ## Test coverage
218
364
 
219
365
  Feature coverage rests on three suites with different jobs.
220
366
 
221
- `tests/calculator-cortex-keypad-coverage.test.ts` is the self-maintaining one:
222
- every shipped keypad key must map to an expression proven to validate and
223
- evaluate. A key with no entry fails, and an entry naming a retired key fails.
224
- Implicit multiplication, parenthesised groups and the inverse-trigonometric keys
225
- were all refused by the expression policy until this test reached them.
367
+ `tests/calculator-cortex-keypad-coverage.test.ts` maintains itself: every shipped
368
+ keypad key must map to an expression proven to validate and evaluate. A key with
369
+ no entry fails, and an entry naming a retired key fails.
226
370
 
227
371
  `tests/calculator-cortex-scenarios.test.ts` pins values, traced from the PRD's
228
372
  capability spec: precedence, boundary values, display thresholds, the domain
229
373
  edges of every function, and the refusals each mode owes. Its LaTeX entry shapes
230
374
  are derived from the public corner-case corpora in `mathquill` and Doenet's
231
- `math-expressions`, which test their own parsers — the shapes carry over, the
375
+ `math-expressions`, which test their own parsers; the shapes carry over and the
232
376
  expectations do not.
233
377
 
234
378
  `tests/calculator-cortex-corpus.test.ts` covers volume, and asserts properties
235
- rather than values, because a fixture of individual expectations at corpus size
236
- fails in ways nobody can act on. The corpus is GSM8K's inline calculator
237
- annotations — `<<48/2=24>>`, expression/result pairs authored to be executed by a
238
- calculator — over `0-9 + - * / . ( )` alone, which is exactly basic mode's
239
- capability set. Four properties hold: every outcome is a declared error code or an
240
- answer, never an undeclared throw; every answer matches its authored result
241
- numerically, since the annotations carry their author's currency formatting;
242
- capability sets nest, so what basic accepts scientific and graphing accept
243
- identically; and a displayed answer re-entered answers itself. Only the second
244
- uses the labels — the rest would hold against any corpus.
379
+ because a fixture of individual expectations at corpus size fails in ways nobody
380
+ can act on. The corpus is GSM8K's inline calculator annotations (`<<48/2=24>>`,
381
+ expression/result pairs authored to be executed by a calculator) over
382
+ `0-9 + - * / . ( )` alone, which is exactly basic mode's capability set. Four
383
+ properties hold: every outcome is a declared error code or an answer, never an
384
+ undeclared throw; every answer matches its authored result numerically, since the
385
+ annotations carry their author's currency formatting; capability sets nest, so
386
+ what basic accepts scientific and graphing accept identically; and a displayed
387
+ answer re-entered answers itself. Only the second uses the labels; the rest would
388
+ hold against any corpus.
245
389
 
246
390
  300 entries are committed under `tests/fixtures/`, chosen by a deterministic
247
- stride so regenerating produces no diff. For the full 10770:
391
+ stride so regenerating produces no diff. For the full 10,770:
248
392
 
249
393
  ```bash
250
- bun run test:corpus
394
+ bun run --cwd packages/calculator-cortex test:corpus
251
395
  ```
252
396
 
253
- Playwright covers what no unit test can reach: what MathLive builds from real
254
- keystrokes. Those tests assert only the answer, because turning `/` into a
255
- fraction is MathLive's behaviour, while the LaTeX it hands to `validateExpression`
256
- is this package's seam — and the two have disagreed, `2x` and `(4+5)` among them.
397
+ Playwright covers what MathLive builds from real keystrokes, which no unit test
398
+ reaches. Those tests assert only the answer: turning `/` into a fraction is
399
+ MathLive's behavior, while the LaTeX it hands to `validateExpression` is this
400
+ package's seam, and the two have disagreed, `2x` and `(4+5)` among them.
401
+
402
+ ## Exports
403
+
404
+ | Export | Kind |
405
+ | --- | --- |
406
+ | `CortexCalculatorProvider` | The `CalculatorProvider` implementation (`providerId: "cortex"`). |
407
+ | `CortexCalculatorError`, `CortexCalculatorErrorCode` | Error class and its code union. |
408
+ | `cortexEnglishMessages`, `cortexDutchMessages` | The built-in message catalogs. |
409
+ | `localeDirection(locale)` | The `ltr` or `rtl` direction the package derives from a locale. |
410
+ | `CortexCalculatorProviderInit`, `CortexCalculatorProviderConfig`, `CortexCalculatorSettings`, `CortexGraphSettings`, `CortexGraphViewport`, `CortexAngleMode`, `CortexFunctionId`, `CortexTextDirection` | Configuration types. |
411
+ | `CortexCalculatorMessages`, `CortexCalculatorMessageKey`, `CortexCalculatorMessageOverrides` | Message types. |
412
+ | `CortexCalculatorState`, `CortexCalculatorStateV1`, `CortexGraphState`, `CortexGraphExpressionState`, `CortexGraphLineStyle` | State types. |
413
+
414
+ ## License
415
+
416
+ MIT. `LICENSE.md` lists the bundled MathLive, CortexJS Compute Engine and
417
+ JSXGraph with their licenses.