@altnovatrail/canvas 0.1.1 → 0.1.2

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/NOTICE ADDED
@@ -0,0 +1,6 @@
1
+ @altnovatrail/canvas
2
+
3
+ The package embeds icons of Material Symbols, by Google, under the Apache License, Version 2.0,
4
+ the license in LICENSE: https://github.com/google/material-design-icons. They come from the npm
5
+ package @iconify-json/material-symbols 1.2.97, and the package holds only the icons of its
6
+ own buttons.
package/README.md CHANGED
@@ -17,11 +17,11 @@
17
17
 
18
18
  The NovaTrail canvases. A canvas is the workplace of a task, where the learner writes, draws and types; this UI package holds the canvas kinds — for now two, the handwriting sheet and the workbook — with their layers and tools, captures the learner's strokes with time and pressure, draws the picture of the work that goes to be checked and shows the tutor's marks tied to the learner's strokes, and the app builds it in at an exact version. A separate build of it is the samples page, where adult volunteers make samples for eval on the same canvas that learners use: a sample is saved as a file on the volunteer's device and reaches a module's eval set by pull request, and nothing goes to the platform's server.
19
19
 
20
- A component of [NovaTrail](https://github.com/altnovatrail/spec/tree/v0.6.2), a universal open platform for adaptive learning, interactive lessons and AI tutoring. It belongs to stage 2, First slice with learners, with tutor, api, app, skill-map and infra; with tutor it is the stage's first code: first the handwriting sheet, while the samples page waits until a measurement needs volunteers' or children's samples.
20
+ A component of [NovaTrail](https://github.com/altnovatrail/spec/tree/v0.6.5), a universal open platform for adaptive learning, interactive lessons and AI tutoring. It belongs to stage 2, First slice with learners, with tutor, api, app, skill-map and infra; with tutor it is the stage's first code: first the handwriting sheet, while the samples page waits until a measurement needs volunteers' or children's samples.
21
21
 
22
22
  ## Status
23
23
 
24
- Code started. The detailing is done — epic E1, archived in [docs/plan/E1.md](docs/plan/E1.md). Epic E2, replanned for the MVP on 2026-10-22, built the handwriting sheet as the element `<novatrail-canvas>`, for tutor's check board to take in place of its own sheet; the board has decided to keep its own. Its last task was the author's try of the sheet by hand on the demo page, which found nothing to fix. E2 is done and archived in [docs/plan/E2.md](docs/plan/E2.md). The text canvas, epic E3, was dropped on 2026-10-25: canvas builds no text canvas. Epic E4, taken the same day, built the sheet for the product: the eraser and the history of writing, the layers and the grid, the groups of strokes and the crop for checking, the tutor's marks tied to the strokes, the package's full interface and the input on tablets. It was done on 2026-10-26 and is archived in [docs/plan/E4.md](docs/plan/E4.md). Epic E6, taken the same day, detailed and built the workbook, the first slice's second canvas kind, for school maths in grade 3, and was done on 2026-10-27; it is archived in [docs/plan/E6.md](docs/plan/E6.md). Epic E7, taken the same day, took what spec v0.2.42 brought: the task's new open fields, the exit condition that every answer slot holds something — `filled` on a sheet too — and the lesson's worked example, shown on a sheet the learner does not write on. On the way canvas moved to spec v0.4.1, whose npm package gives the types of the module format. E7 was done on 2026-11-01 and is archived in [docs/plan/E7.md](docs/plan/E7.md). Epic E8, taken the same day, is the first release of the package, which spec's cross-repository plan asks for before the app's code; the tests' coverage and the release's build come first. Then canvas moved to spec v0.6.2, of spec's 0.6 line, and its CI now checks the signature of each package of `@altnovatrail` in its lock file, as spec's rules ask; then the element took the module format 0.2 and the types of its contract with the app from spec's npm package, and the package came out in its first release, v0.1.0; from the next one on, the release puts it in npm itself. The samples page is epic E5 for later; it waits until a measurement needs volunteers' or children's samples. So far there is the package, `@altnovatrail/canvas`, with its tools and the sheet's input: the element takes a pen, a finger or a mouse, one pointer at a time, and draws the strokes, and its methods `undo()` and `clear()` undo the last action and clear the sheet. Its eraser, E4's first task, takes strokes off whole along the path of a press: the element's `tool` switches to it, and a pen's eraser erases whatever the tool, if the task allows the eraser. The sheet keeps the history of writing — every stroke, erasing, clearing and undo, at the sheet's time — for the app to keep. The sheet is a stack of layers, in the order of spec's canvas layers: its task, the element's `task` property, rules it in squares, lines or dots, and the task's statement stands above the sheet, its pictures from the consumer's `media`. The task and the module's manifest come in the types of spec's npm package, `@altnovatrail/spec`; the element reads the module format 0.2, which spec v0.6.0 brought — a manifest of another version is a TypeError — and the task is checked as its schema checks it, a field it does not know a TypeError; what the statement gives and asks, the fields `givens` and `unknown`, the element takes and leaves for the app to show; a workbook's task the element shows too: its lines laid out on a page the size of the sheet — numbers, text fitted to the layout's own widths, and boxes, up to ten rows — which a screen reader reads as a list. On a workbook each stroke falls in one box or in none, by a rule in integers on the middle of its points' box; what falls in no box is draft, which stays on the page and goes to no check, and the property `filled` tells which boxes hold writing, and on a sheet whether its one answer anywhere on it, or the whole sheet, does. The module's manifest, the `manifest` property, gives the height of a digit on the crop, 24 px unless it gives another, as without a manifest, and the language of the statement. The sheet is at most 1000 CSS pixels wide, and the consumer themes it by custom properties. The DOM-free part parts the strokes into groups by place and time — two strokes no more than 2 seconds apart whose boxes come within a cell of each other are neighbours, and a chain of neighbours is a group — labels the groups A, B, C… by place and gives each one's rectangle on the sheet, for the crop, the tutor's marks and the samples to come; the rule's test vectors are a proposal for the eval protocol. It also draws the crop for checking, with its own raster and PNG encoder: the ink of the visible strokes, scaled so that the writing comes out at the digit height the module's manifest asks for, within the size every Claude model takes as it is, with the groups' frames in its fractions. Its test vectors go to the eval protocol too. The element takes the tutor's marks of a submission back by its property `marks`, with the context it is to give with the crop: a mark holds on to the learner's strokes — a group's, or those a model's box on the crop takes, two thirds of their points within a quarter of a digit of it — and a frame stands round those of them the sheet shows, over the work, read by its name and fading while the learner writes into it. The history goes out by the property `history` and the event `historychange`, which tells of every action with the events it adds, and comes back by the same property: the same history changes nothing, and another, one the sheet could have written, replaces the sheet, undo and all. `exportWork()` gives the work for checking: the PNG of the crop and the context of its submission, which the app keeps and gives back with the tutor's marks, its digest telling the sheet's own context from another sheet's. On a workbook it gives the crop of each filled box, with its item and slot, and the context of the table of boxes; a tutor's mark there stands on an answer cell by its place and frames the box with its ink. Above the statement and the sheet, outside their scrolling, stands the element's own row of buttons — the pen and the eraser the task allows, undo and clear — a toolbar worked by pointer and by keyboard, labelled in English or Russian by the element's own `lang`; a `toolchange` event tells of the learner's choice of tool. On a tablet a pen's touch puts the whole page in pen mode, where a finger writes nothing until the row's Draw with finger lets it write again; a touch wider or taller than 100 CSS pixels is a palm and writes nothing, and two fingers scroll the sheet. The row's Up and Down scroll it the same way with a single pointer, on any device, by four fifths of what is in sight. A lesson's worked example comes by the property `workedExample`, in place of a task: its statement stands above the sheet and its grid on it, but the learner does not write there — a finger or a pen scrolls the sheet as the browser scrolls the statement, and the row, named Worked example, has Next, Demo, Up and Down. Next opens the parts of the author's demo one at a time, into a numbered list above the sheet that a polite region tells a screen reader of, and Demo hides the whole demo and shows it again; the property `demoState` and the event `demochange` tell the app how far the learner has gone, and the app may give it back to a new element after the example — in React's JSX `workedExample` comes before `demoState`, and `ondemochange` listens. The pictures of the demo come from the consumer's `media` with the statement's, one file for each reference, all of them as the example comes, so that a part opened shows its picture at once; a picture on an area of the sheet stands on the sheet itself, stretched over its area, and the sheet scrolls to it as its part opens. The element now waits for the app and the samples page; a test plays the app's part, and `just demo` serves a page to try the element on by hand. A release comes out by itself after CI passes on `main`, when a path it is built from has changed: the package's archive, LICENSE and their sums, built, signed and checked by .github's shared release, and the same archive goes to npm: the first version by hand, the later ones by the release's own npm step, through trusted publishing, with no long-lived token. A pull request from a branch of this repository tries the release and publishes nothing.
24
+ Code started. The detailing is done — epic E1, archived in [docs/plan/E1.md](docs/plan/E1.md). Epic E2, replanned for the MVP on 2026-10-22, built the handwriting sheet as the element `<novatrail-canvas>`, for tutor's check board to take in place of its own sheet; the board has decided to keep its own. Its last task was the author's try of the sheet by hand on the demo page, which found nothing to fix. E2 is done and archived in [docs/plan/E2.md](docs/plan/E2.md). The text canvas, epic E3, was dropped on 2026-10-25: canvas builds no text canvas. Epic E4, taken the same day, built the sheet for the product: the eraser and the history of writing, the layers and the grid, the groups of strokes and the crop for checking, the tutor's marks tied to the strokes, the package's full interface and the input on tablets. It was done on 2026-10-26 and is archived in [docs/plan/E4.md](docs/plan/E4.md). Epic E6, taken the same day, detailed and built the workbook, the first slice's second canvas kind, for school maths in grade 3, and was done on 2026-10-27; it is archived in [docs/plan/E6.md](docs/plan/E6.md). Epic E7, taken the same day, took what spec v0.2.42 brought: the task's new open fields, the exit condition that every answer slot holds something — `filled` on a sheet too — and the lesson's worked example, shown on a sheet the learner does not write on. On the way canvas moved to spec v0.4.1, whose npm package gives the types of the module format. E7 was done on 2026-11-01 and is archived in [docs/plan/E7.md](docs/plan/E7.md). Epic E8, taken the same day, is the first release of the package, which spec's cross-repository plan asks for before the app's code; the tests' coverage and the release's build come first. Then canvas moved to spec v0.6.2, of spec's 0.6 line, and its CI now checks the signature of each package of `@altnovatrail` in its lock file, as spec's rules ask; then the element took the module format 0.2 and the types of its contract with the app from spec's npm package, and the package came out in its first release, v0.1.0; from the next one on, the release puts it in npm itself. E8 was done on 2026-11-02 and is archived in [docs/plan/E8.md](docs/plan/E8.md). The same day canvas moved to spec v0.6.5, which made the `media` property part of the element's contract with the app and gave the buttons of every NovaTrail interface package one set of icons, and epic E5, the samples page, was taken: first trying a build opened from a file on the devices and building the page, while the sample file waits for spec's eval protocol. So far there is the package, `@altnovatrail/canvas`, with its tools and the sheet's input: the element takes a pen, a finger or a mouse, one pointer at a time, and draws the strokes, and its methods `undo()` and `clear()` undo the last action and clear the sheet. Its eraser, E4's first task, takes strokes off whole along the path of a press: the element's `tool` switches to it, and a pen's eraser erases whatever the tool, if the task allows the eraser. The sheet keeps the history of writing — every stroke, erasing, clearing and undo, at the sheet's time — for the app to keep. The sheet is a stack of layers, in the order of spec's canvas layers: its task, the element's `task` property, rules it in squares, lines or dots, and the task's statement stands above the sheet, its pictures from the consumer's `media`. The task and the module's manifest come in the types of spec's npm package, `@altnovatrail/spec`; the element reads the module format 0.2, which spec v0.6.0 brought — a manifest of another version is a TypeError — and the task is checked as its schema checks it, a field it does not know a TypeError; what the statement gives and asks, the fields `givens` and `unknown`, the element takes and leaves for the app to show; a workbook's task the element shows too: its lines laid out on a page the size of the sheet — numbers, text fitted to the layout's own widths, and boxes, up to ten rows — which a screen reader reads as a list. On a workbook each stroke falls in one box or in none, by a rule in integers on the middle of its points' box; what falls in no box is draft, which stays on the page and goes to no check, and the property `filled` tells which boxes hold writing, and on a sheet whether its one answer anywhere on it, or the whole sheet, does. The module's manifest, the `manifest` property, gives the height of a digit on the crop, 24 px unless it gives another, as without a manifest, and the language of the statement. The sheet is at most 1000 CSS pixels wide, and the consumer themes it by custom properties. The DOM-free part parts the strokes into groups by place and time — two strokes no more than 2 seconds apart whose boxes come within a cell of each other are neighbours, and a chain of neighbours is a group — labels the groups A, B, C… by place and gives each one's rectangle on the sheet, for the crop, the tutor's marks and the samples to come; the rule's test vectors are a proposal for the eval protocol. It also draws the crop for checking, with its own raster and PNG encoder: the ink of the visible strokes, scaled so that the writing comes out at the digit height the module's manifest asks for, within the size every Claude model takes as it is, with the groups' frames in its fractions. Its test vectors go to the eval protocol too. The element takes the tutor's marks of a submission back by its property `marks`, with the context it is to give with the crop: a mark holds on to the learner's strokes — a group's, or those a model's box on the crop takes, two thirds of their points within a quarter of a digit of it — and a frame stands round those of them the sheet shows, over the work, read by its name and fading while the learner writes into it. The history goes out by the property `history` and the event `historychange`, which tells of every action with the events it adds, and comes back by the same property: the same history changes nothing, and another, one the sheet could have written, replaces the sheet, undo and all. The pictures' files come from the consumer's `media`, a function that gives a file's Blob by its reference, the contract's own. `exportWork()` gives the work for checking: the PNG of the crop and the context of its submission, which the app keeps and gives back with the tutor's marks, its digest telling the sheet's own context from another sheet's. On a workbook it gives the crop of each filled box, with its item and slot, and the context of the table of boxes; a tutor's mark there stands on an answer cell by its place and frames the box with its ink. Above the statement and the sheet, outside their scrolling, stands the element's own row of buttons — the pen and the eraser the task allows, undo and clear — a toolbar worked by pointer and by keyboard, labelled in English or Russian by the element's own `lang`, each label after an icon of Material Symbols, Google's set, which the build embeds, so the element asks no network for one and the package's NOTICE names it; a `toolchange` event tells of the learner's choice of tool. On a tablet a pen's touch puts the whole page in pen mode, where a finger writes nothing until the row's Draw with finger lets it write again; a touch wider or taller than 100 CSS pixels is a palm and writes nothing, and two fingers scroll the sheet. The row's Up and Down scroll it the same way with a single pointer, on any device, by four fifths of what is in sight. A lesson's worked example comes by the property `workedExample`, in place of a task: its statement stands above the sheet and its grid on it, but the learner does not write there — a finger or a pen scrolls the sheet as the browser scrolls the statement, and the row, named Worked example, has Next, Demo, Up and Down. Next opens the parts of the author's demo one at a time, into a numbered list above the sheet that a polite region tells a screen reader of, and Demo hides the whole demo and shows it again; the property `demoState` and the event `demochange` tell the app how far the learner has gone, and the app may give it back to a new element after the example — in React's JSX `workedExample` comes before `demoState`, and `ondemochange` listens. The pictures of the demo come from the consumer's `media` with the statement's, one file for each reference, all of them as the example comes, so that a part opened shows its picture at once; a picture on an area of the sheet stands on the sheet itself, stretched over its area, and the sheet scrolls to it as its part opens. The element now waits for the app and the samples page; a test plays the app's part, and `just demo` serves a page to try the element on by hand. A release comes out by itself after CI passes on `main`, when a path it is built from has changed: the package's archive, with its NOTICE, LICENSE and their sums, built, signed and checked by .github's shared release, and the same archive goes to npm: the first version by hand, the later ones by the release's own npm step, through trusted publishing, with no long-lived token. A pull request from a branch of this repository tries the release and publishes nothing.
25
25
 
26
26
  ## Install
27
27
 
@@ -37,13 +37,13 @@ A pull request that changes the lock file has spec's recipe check it, as NovaTra
37
37
  ## Code
38
38
 
39
39
  - `src/core/` — the DOM-free part, `@altnovatrail/canvas/core`: it runs the same in a browser and in Node, and its entry point gives the sheet's size, the types of the data and the fold of a history into its strokes.
40
- - `src/element/` — the element `<novatrail-canvas>`, `@altnovatrail/canvas`: importing it registers the element. The types of its contract with the app — the element as `CanvasElement` has it, its events, the history of writing, the work for checking and its context — are spec's, from `@altnovatrail/spec/canvas`, and this entry point gives them under spec's names, the same types; the DOM-free part gives the data's among them, the history of writing and the context of a submission. The app takes them from spec's package, `import type { CanvasElement } from "@altnovatrail/spec/canvas"`, at the version canvas has in its `dependencies`; canvas's own are only what the contract does not name: the tutor's marks, the media and the task's temporary tools.
41
- - `scripts/` — the check of the package as a consumer takes it, and of the archive a release puts out.
40
+ - `src/element/` — the element `<novatrail-canvas>`, `@altnovatrail/canvas`: importing it registers the element. The types of its contract with the app — the element as `CanvasElement` has it, its events, the media, the history of writing, the work for checking and its context — are spec's, from `@altnovatrail/spec/canvas`, and this entry point gives them under spec's names, the same types; the DOM-free part gives the data's among them, the history of writing and the context of a submission. The app takes them from spec's package, `import type { CanvasElement } from "@altnovatrail/spec/canvas"`, at the version canvas has in its `dependencies`; canvas's own are only what the contract does not name: the tutor's marks, and the task's temporary tools.
41
+ - `scripts/` — the check of the package as a consumer takes it, and of the archive a release puts out, and the script that writes the row's icons, `src/element/icons.ts`, and `NOTICE` from the set of icons the lock file pins: `just icons` runs it, and `just ci` fails if they are not what it writes.
42
42
  - `demo/` — a page to try the element on by hand, as the app is to have it: it gives the element a language and its task the tools allowed, keeps the history of writing as the element tells it and opens the sheet again from it, draws the groups of the strokes and the crop for checking under the sheet, gives the tutor's marks back over the crop Check sent, and shows a workbook's pages in the font and the language it gives, how many boxes are filled, the crop of each filled box Check sent and a mark given back on a box, and a lesson's worked example on a new element, on which nothing is written, whose demo opens part by part — a picture on an area of the sheet on the sheet itself — the page keeping how far it has gone and giving it back to the next element. `just demo` serves it on this machine.
43
43
  - `vectors/` — the test vectors of the rule of stroke groups, of the crop for checking, of the snapping of a model's box to the strokes, of the workbook's page layout and of the rule of boxes, which their tests write and keep current.
44
44
  - `licenses.json` — the packages the license check accepts by name, each with why.
45
45
 
46
- `just build` builds the package into `lib/`, and `just release-files v0.1.0` writes the files of that release into `dist/` — the package's archive, LICENSE and SHA256SUMS — and prints the sums, which a build of the same commit gives again. `just ci` runs the checks CI runs: format, lint and types; the tests in Node and in Chromium; advisories; licenses. They run in the devcontainer, whose toolchain stage holds the browser the tests run in. The license check is spec's recipe, run from the `spec/` submodule with the exceptions in `licenses.json`, so it needs `spec/` checked out: see [CLAUDE.md](CLAUDE.md), Environment.
46
+ `just build` builds the package into `lib/`, and `just release-files v0.1.0` writes the files of that release into `dist/` — the package's archive, with NOTICE in it, LICENSE and SHA256SUMS — and prints the sums, which a build of the same commit gives again. `just ci` runs the checks CI runs: format, lint and types; the tests in Node and in Chromium; advisories; licenses. They run in the devcontainer, whose toolchain stage holds the browser the tests run in. The license check is spec's recipe, run from the `spec/` submodule with the exceptions in `licenses.json`, so it needs `spec/` checked out: see [CLAUDE.md](CLAUDE.md), Environment.
47
47
 
48
48
  ## Documents
49
49
 
@@ -53,7 +53,7 @@ The documents are in Russian.
53
53
  - [docs/decisions.md](docs/decisions.md) — the decision log: what was decided, why, and what was rejected.
54
54
  - [RUN.md](RUN.md) — the work plan, in epics.
55
55
 
56
- The working rules, for people and coding agents alike, are in [CLAUDE.md](CLAUDE.md) and [AGENTS.md](AGENTS.md). The platform's shared vocabulary, formats, contracts and decisions are in [spec](https://github.com/altnovatrail/spec/tree/v0.6.2), checked out here as a git submodule at `v0.6.2`: clone this repository with `git clone --recurse-submodules`.
56
+ The working rules, for people and coding agents alike, are in [CLAUDE.md](CLAUDE.md) and [AGENTS.md](AGENTS.md). The platform's shared vocabulary, formats, contracts and decisions are in [spec](https://github.com/altnovatrail/spec/tree/v0.6.5), checked out here as a git submodule at `v0.6.5`: clone this repository with `git clone --recurse-submodules`.
57
57
 
58
58
  ## License
59
59
 
@@ -1,4 +1,4 @@
1
- /** The namespace of the SVG elements the element makes: its layers, and the row's triangle. */
1
+ /** The namespace of the SVG elements the element makes: its layers, and the row's icons. */
2
2
  export declare const SVG = "http://www.w3.org/2000/svg";
3
3
  /** An SVG shape — a workbook's box, a demo's picture — set over a box in its layer's units. */
4
4
  export declare function placeOver(shape: SVGElement, { left, top, width, height }: {
@@ -2,7 +2,7 @@
2
2
  // only when its value changes, so that what has not changed tells no observer of a change, the
3
3
  // namespace of the SVG it writes them on, and the place of an SVG shape of a box. This module is
4
4
  // no entry point of the package.
5
- /** The namespace of the SVG elements the element makes: its layers, and the row's triangle. */
5
+ /** The namespace of the SVG elements the element makes: its layers, and the row's icons. */
6
6
  export const SVG = "http://www.w3.org/2000/svg";
7
7
  /** An SVG shape — a workbook's box, a demo's picture — set over a box in its layer's units. */
8
8
  export function placeOver(shape, { left, top, width, height }) {
@@ -0,0 +1,110 @@
1
+ /** The icons of the row's buttons, by their names in the set, as the element draws them. */
2
+ export declare const ICONS: {
3
+ readonly "arrow-forward-outline-rounded": {
4
+ readonly viewBox: "0 0 24 24";
5
+ readonly paths: readonly ["M16.175 13H5q-.425 0-.712-.288T4 12t.288-.712T5 11h11.175l-4.9-4.9q-.3-.3-.288-.7t.313-.7q.3-.275.7-.288t.7.288l6.6 6.6q.15.15.213.325t.062.375t-.062.375t-.213.325l-6.6 6.6q-.275.275-.687.275T11.3 19.3q-.3-.3-.3-.712t.3-.713z"];
6
+ };
7
+ readonly "keyboard-arrow-down-outline-rounded": {
8
+ readonly viewBox: "0 0 24 24";
9
+ readonly paths: readonly ["M11.625 14.913q-.175-.063-.325-.213l-4.6-4.6q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275l3.9 3.9l3.9-3.9q.275-.275.7-.275t.7.275t.275.7t-.275.7l-4.6 4.6q-.15.15-.325.213t-.375.062t-.375-.062"];
10
+ };
11
+ readonly "keyboard-arrow-right-outline-rounded": {
12
+ readonly viewBox: "0 0 24 24";
13
+ readonly paths: readonly ["M12.6 12L8.7 8.1q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275l4.6 4.6q.15.15.213.325t.062.375t-.062.375t-.213.325l-4.6 4.6q-.275.275-.7.275t-.7-.275t-.275-.7t.275-.7z"];
14
+ };
15
+ readonly "ink-pen-outline-rounded": {
16
+ readonly viewBox: "0 0 24 24";
17
+ readonly paths: readonly ["m12.25 10.825l.925.925L18.6 6.325l-.925-.925zM5 19h.925l5.825-5.825l-.925-.925L5 18.075zm8.875-5.125l-3.75-3.75L14.3 5.95l-.725-.725L8.8 10q-.3.3-.7.3t-.7-.3t-.3-.712t.3-.713l4.75-4.75q.6-.6 1.413-.6t1.412.6l.725.725l1.25-1.25q.3-.3.713-.3t.712.3L20.7 5.625q.3.3.3.712t-.3.713zM4 21q-.425 0-.712-.288T3 20v-1.925q0-.4.15-.763t.425-.637l6.55-6.55l3.75 3.75l-6.55 6.55q-.275.275-.638.425t-.762.15z"];
18
+ };
19
+ readonly "ink-pen-rounded": {
20
+ readonly viewBox: "0 0 24 24";
21
+ readonly paths: readonly ["M15.275 12.475L11.525 8.7L14.3 5.95l-.725-.725L8.8 10q-.3.3-.7.3t-.7-.3t-.3-.712t.3-.713l4.75-4.75q.6-.6 1.413-.6t1.412.6l.725.725l1.25-1.25q.3-.3.713-.3t.712.3L20.7 5.625q.3.3.3.713t-.3.712zM4 21q-.425 0-.712-.288T3 20v-1.925q0-.4.15-.763t.425-.637l6.525-6.55l3.775 3.75l-6.55 6.55q-.275.275-.637.425t-.763.15z"];
22
+ };
23
+ readonly "ink-eraser-outline-rounded": {
24
+ readonly viewBox: "0 0 24 24";
25
+ readonly paths: readonly ["M17.25 18H21q.425 0 .713.288T22 19t-.288.713T21 20h-5.75zM5.175 20q-.2 0-.388-.075T4.45 19.7l-1.825-1.825q-.575-.575-.588-1.425T2.6 15l11-11.4q.575-.6 1.412-.6t1.413.575L21.4 8.55q.575.575.575 1.425T21.4 11.4l-8.1 8.3q-.15.15-.337.225t-.388.075zm6.975-2L20 9.95L15.05 5L4 16.4L5.6 18zM12 12"];
26
+ };
27
+ readonly "ink-eraser-rounded": {
28
+ readonly viewBox: "0 0 24 24";
29
+ readonly paths: readonly ["M17.25 18H21q.425 0 .713.288T22 19t-.288.713T21 20h-5.75zM5.175 20q-.2 0-.388-.075T4.45 19.7l-1.825-1.825q-.575-.575-.588-1.425T2.6 15l11-11.4q.575-.6 1.412-.6t1.413.575L21.4 8.55q.575.575.575 1.425T21.4 11.4l-8.1 8.3q-.15.15-.337.225t-.388.075z"];
30
+ };
31
+ readonly "undo-outline-rounded": {
32
+ readonly viewBox: "0 0 24 24";
33
+ readonly paths: readonly ["M8 19q-.425 0-.712-.288T7 18t.288-.712T8 17h6.1q1.575 0 2.738-1T18 13.5T16.838 11T14.1 10H7.8l1.9 1.9q.275.275.275.7t-.275.7t-.7.275t-.7-.275L4.7 9.7q-.15-.15-.213-.325T4.426 9t.063-.375T4.7 8.3l3.6-3.6q.275-.275.7-.275t.7.275t.275.7t-.275.7L7.8 8h6.3q2.425 0 4.163 1.575T20 13.5t-1.737 3.925T14.1 19z"];
34
+ };
35
+ readonly "delete-sweep-outline-rounded": {
36
+ readonly viewBox: "0 0 24 24";
37
+ readonly paths: readonly ["M5 19q-.825 0-1.412-.587T3 17V8q-.425 0-.712-.288T2 7t.288-.712T3 6h3v-.5q0-.425.288-.712T7 4.5h2q.425 0 .713.288T10 5.5V6h3q.425 0 .713.288T14 7t-.288.713T13 8v9q0 .825-.587 1.413T11 19zm11-1q-.425 0-.712-.288T15 17t.288-.712T16 16h2q.425 0 .713.288T19 17t-.288.713T18 18zm0-4q-.425 0-.712-.288T15 13t.288-.712T16 12h4q.425 0 .713.288T21 13t-.288.713T20 14zm0-4q-.425 0-.712-.288T15 9t.288-.712T16 8h5q.425 0 .713.288T22 9t-.288.713T21 10zM5 8v9h6V8z"];
38
+ };
39
+ readonly "arrow-upward-outline-rounded": {
40
+ readonly viewBox: "0 0 24 24";
41
+ readonly paths: readonly ["m11 7.825l-4.9 4.9q-.3.3-.7.288t-.7-.313q-.275-.3-.288-.7t.288-.7l6.6-6.6q.15-.15.325-.212T12 4.425t.375.063t.325.212l6.6 6.6q.275.275.275.688t-.275.712q-.3.3-.712.3t-.713-.3L13 7.825V19q0 .425-.288.713T12 20t-.712-.288T11 19z"];
42
+ };
43
+ readonly "arrow-downward-outline-rounded": {
44
+ readonly viewBox: "0 0 24 24";
45
+ readonly paths: readonly ["M11 16.175V5q0-.425.288-.712T12 4t.713.288T13 5v11.175l4.9-4.9q.3-.3.7-.288t.7.313q.275.3.287.7t-.287.7l-6.6 6.6q-.15.15-.325.213t-.375.062t-.375-.062t-.325-.213l-6.6-6.6q-.275-.275-.275-.687T4.7 11.3q.3-.3.713-.3t.712.3z"];
46
+ };
47
+ readonly "touch-app-outline-rounded": {
48
+ readonly viewBox: "0 0 24 24";
49
+ readonly paths: readonly ["M10.475 22q-.7 0-1.312-.3t-1.038-.85L3.1 14.475q-.2-.225-.175-.537t.225-.513q.5-.525 1.2-.625t1.3.275L7.5 14.2V6q0-.425.288-.712T8.5 5t.725.288t.3.712v10q0 .575-.513.863T8 16.85l-.9-.55l2.6 3.325q.15.175.35.275t.425.1H16q.825 0 1.413-.587T18 18v-4q0-.425-.288-.712T17 13h-4.475q-.425 0-.712-.288T11.525 12t.288-.712t.712-.288H17q1.25 0 2.125.875T20 14v4q0 1.65-1.175 2.825T16 22zm1.5-13q-.425 0-.712-.288T10.975 8q0-.05.125-.5q.2-.35.3-.712T11.5 6q0-1.25-.875-2.125T8.5 3t-2.125.875T5.5 6q0 .425.1.788t.3.712q.075.125.1.25t.025.25q0 .425-.275.713T5.05 9q-.275 0-.512-.15t-.363-.375q-.325-.55-.5-1.175T3.5 6q0-2.075 1.463-3.537T8.5 1t3.538 1.463T13.5 6q0 .675-.175 1.3t-.5 1.175q-.125.225-.35.375t-.5.15"];
50
+ };
51
+ };
52
+ /** The name of an icon of the row. */
53
+ export type IconName = keyof typeof ICONS;
54
+ /**
55
+ * Each value of the row, by its button's key: the icon in outline, the one the button shows while
56
+ * chosen — the same for a value without a fill — and whether the icon is mirrored in a row that
57
+ * runs right to left (architecture 6 spec).
58
+ */
59
+ export declare const ROW: {
60
+ readonly next: {
61
+ readonly outline: "arrow-forward-outline-rounded";
62
+ readonly filled: "arrow-forward-outline-rounded";
63
+ readonly mirrored: true;
64
+ };
65
+ readonly demoShown: {
66
+ readonly outline: "keyboard-arrow-down-outline-rounded";
67
+ readonly filled: "keyboard-arrow-down-outline-rounded";
68
+ readonly mirrored: false;
69
+ };
70
+ readonly demoHidden: {
71
+ readonly outline: "keyboard-arrow-right-outline-rounded";
72
+ readonly filled: "keyboard-arrow-right-outline-rounded";
73
+ readonly mirrored: true;
74
+ };
75
+ readonly pen: {
76
+ readonly outline: "ink-pen-outline-rounded";
77
+ readonly filled: "ink-pen-rounded";
78
+ readonly mirrored: false;
79
+ };
80
+ readonly eraser: {
81
+ readonly outline: "ink-eraser-outline-rounded";
82
+ readonly filled: "ink-eraser-rounded";
83
+ readonly mirrored: false;
84
+ };
85
+ readonly undo: {
86
+ readonly outline: "undo-outline-rounded";
87
+ readonly filled: "undo-outline-rounded";
88
+ readonly mirrored: true;
89
+ };
90
+ readonly clear: {
91
+ readonly outline: "delete-sweep-outline-rounded";
92
+ readonly filled: "delete-sweep-outline-rounded";
93
+ readonly mirrored: false;
94
+ };
95
+ readonly up: {
96
+ readonly outline: "arrow-upward-outline-rounded";
97
+ readonly filled: "arrow-upward-outline-rounded";
98
+ readonly mirrored: false;
99
+ };
100
+ readonly down: {
101
+ readonly outline: "arrow-downward-outline-rounded";
102
+ readonly filled: "arrow-downward-outline-rounded";
103
+ readonly mirrored: false;
104
+ };
105
+ readonly finger: {
106
+ readonly outline: "touch-app-outline-rounded";
107
+ readonly filled: "touch-app-outline-rounded";
108
+ readonly mirrored: false;
109
+ };
110
+ };
@@ -0,0 +1,124 @@
1
+ // Written by scripts/icons.ts — `just icons` — from @iconify-json/material-symbols 1.2.97:
2
+ // Material Symbols by Google, under the Apache License 2.0 (R46 in docs/decisions.md; R78 spec).
3
+ // ci-lint fails unless it is what the script writes, so it is never edited by hand. This module is
4
+ // no entry point of the package.
5
+ /** The icons of the row's buttons, by their names in the set, as the element draws them. */
6
+ export const ICONS = {
7
+ "arrow-forward-outline-rounded": {
8
+ viewBox: "0 0 24 24",
9
+ paths: [
10
+ "M16.175 13H5q-.425 0-.712-.288T4 12t.288-.712T5 11h11.175l-4.9-4.9q-.3-.3-.288-.7t.313-.7q.3-.275.7-.288t.7.288l6.6 6.6q.15.15.213.325t.062.375t-.062.375t-.213.325l-6.6 6.6q-.275.275-.687.275T11.3 19.3q-.3-.3-.3-.712t.3-.713z",
11
+ ],
12
+ },
13
+ "keyboard-arrow-down-outline-rounded": {
14
+ viewBox: "0 0 24 24",
15
+ paths: [
16
+ "M11.625 14.913q-.175-.063-.325-.213l-4.6-4.6q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275l3.9 3.9l3.9-3.9q.275-.275.7-.275t.7.275t.275.7t-.275.7l-4.6 4.6q-.15.15-.325.213t-.375.062t-.375-.062",
17
+ ],
18
+ },
19
+ "keyboard-arrow-right-outline-rounded": {
20
+ viewBox: "0 0 24 24",
21
+ paths: [
22
+ "M12.6 12L8.7 8.1q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275l4.6 4.6q.15.15.213.325t.062.375t-.062.375t-.213.325l-4.6 4.6q-.275.275-.7.275t-.7-.275t-.275-.7t.275-.7z",
23
+ ],
24
+ },
25
+ "ink-pen-outline-rounded": {
26
+ viewBox: "0 0 24 24",
27
+ paths: [
28
+ "m12.25 10.825l.925.925L18.6 6.325l-.925-.925zM5 19h.925l5.825-5.825l-.925-.925L5 18.075zm8.875-5.125l-3.75-3.75L14.3 5.95l-.725-.725L8.8 10q-.3.3-.7.3t-.7-.3t-.3-.712t.3-.713l4.75-4.75q.6-.6 1.413-.6t1.412.6l.725.725l1.25-1.25q.3-.3.713-.3t.712.3L20.7 5.625q.3.3.3.712t-.3.713zM4 21q-.425 0-.712-.288T3 20v-1.925q0-.4.15-.763t.425-.637l6.55-6.55l3.75 3.75l-6.55 6.55q-.275.275-.638.425t-.762.15z",
29
+ ],
30
+ },
31
+ "ink-pen-rounded": {
32
+ viewBox: "0 0 24 24",
33
+ paths: [
34
+ "M15.275 12.475L11.525 8.7L14.3 5.95l-.725-.725L8.8 10q-.3.3-.7.3t-.7-.3t-.3-.712t.3-.713l4.75-4.75q.6-.6 1.413-.6t1.412.6l.725.725l1.25-1.25q.3-.3.713-.3t.712.3L20.7 5.625q.3.3.3.713t-.3.712zM4 21q-.425 0-.712-.288T3 20v-1.925q0-.4.15-.763t.425-.637l6.525-6.55l3.775 3.75l-6.55 6.55q-.275.275-.637.425t-.763.15z",
35
+ ],
36
+ },
37
+ "ink-eraser-outline-rounded": {
38
+ viewBox: "0 0 24 24",
39
+ paths: [
40
+ "M17.25 18H21q.425 0 .713.288T22 19t-.288.713T21 20h-5.75zM5.175 20q-.2 0-.388-.075T4.45 19.7l-1.825-1.825q-.575-.575-.588-1.425T2.6 15l11-11.4q.575-.6 1.412-.6t1.413.575L21.4 8.55q.575.575.575 1.425T21.4 11.4l-8.1 8.3q-.15.15-.337.225t-.388.075zm6.975-2L20 9.95L15.05 5L4 16.4L5.6 18zM12 12",
41
+ ],
42
+ },
43
+ "ink-eraser-rounded": {
44
+ viewBox: "0 0 24 24",
45
+ paths: [
46
+ "M17.25 18H21q.425 0 .713.288T22 19t-.288.713T21 20h-5.75zM5.175 20q-.2 0-.388-.075T4.45 19.7l-1.825-1.825q-.575-.575-.588-1.425T2.6 15l11-11.4q.575-.6 1.412-.6t1.413.575L21.4 8.55q.575.575.575 1.425T21.4 11.4l-8.1 8.3q-.15.15-.337.225t-.388.075z",
47
+ ],
48
+ },
49
+ "undo-outline-rounded": {
50
+ viewBox: "0 0 24 24",
51
+ paths: [
52
+ "M8 19q-.425 0-.712-.288T7 18t.288-.712T8 17h6.1q1.575 0 2.738-1T18 13.5T16.838 11T14.1 10H7.8l1.9 1.9q.275.275.275.7t-.275.7t-.7.275t-.7-.275L4.7 9.7q-.15-.15-.213-.325T4.426 9t.063-.375T4.7 8.3l3.6-3.6q.275-.275.7-.275t.7.275t.275.7t-.275.7L7.8 8h6.3q2.425 0 4.163 1.575T20 13.5t-1.737 3.925T14.1 19z",
53
+ ],
54
+ },
55
+ "delete-sweep-outline-rounded": {
56
+ viewBox: "0 0 24 24",
57
+ paths: [
58
+ "M5 19q-.825 0-1.412-.587T3 17V8q-.425 0-.712-.288T2 7t.288-.712T3 6h3v-.5q0-.425.288-.712T7 4.5h2q.425 0 .713.288T10 5.5V6h3q.425 0 .713.288T14 7t-.288.713T13 8v9q0 .825-.587 1.413T11 19zm11-1q-.425 0-.712-.288T15 17t.288-.712T16 16h2q.425 0 .713.288T19 17t-.288.713T18 18zm0-4q-.425 0-.712-.288T15 13t.288-.712T16 12h4q.425 0 .713.288T21 13t-.288.713T20 14zm0-4q-.425 0-.712-.288T15 9t.288-.712T16 8h5q.425 0 .713.288T22 9t-.288.713T21 10zM5 8v9h6V8z",
59
+ ],
60
+ },
61
+ "arrow-upward-outline-rounded": {
62
+ viewBox: "0 0 24 24",
63
+ paths: [
64
+ "m11 7.825l-4.9 4.9q-.3.3-.7.288t-.7-.313q-.275-.3-.288-.7t.288-.7l6.6-6.6q.15-.15.325-.212T12 4.425t.375.063t.325.212l6.6 6.6q.275.275.275.688t-.275.712q-.3.3-.712.3t-.713-.3L13 7.825V19q0 .425-.288.713T12 20t-.712-.288T11 19z",
65
+ ],
66
+ },
67
+ "arrow-downward-outline-rounded": {
68
+ viewBox: "0 0 24 24",
69
+ paths: [
70
+ "M11 16.175V5q0-.425.288-.712T12 4t.713.288T13 5v11.175l4.9-4.9q.3-.3.7-.288t.7.313q.275.3.287.7t-.287.7l-6.6 6.6q-.15.15-.325.213t-.375.062t-.375-.062t-.325-.213l-6.6-6.6q-.275-.275-.275-.687T4.7 11.3q.3-.3.713-.3t.712.3z",
71
+ ],
72
+ },
73
+ "touch-app-outline-rounded": {
74
+ viewBox: "0 0 24 24",
75
+ paths: [
76
+ "M10.475 22q-.7 0-1.312-.3t-1.038-.85L3.1 14.475q-.2-.225-.175-.537t.225-.513q.5-.525 1.2-.625t1.3.275L7.5 14.2V6q0-.425.288-.712T8.5 5t.725.288t.3.712v10q0 .575-.513.863T8 16.85l-.9-.55l2.6 3.325q.15.175.35.275t.425.1H16q.825 0 1.413-.587T18 18v-4q0-.425-.288-.712T17 13h-4.475q-.425 0-.712-.288T11.525 12t.288-.712t.712-.288H17q1.25 0 2.125.875T20 14v4q0 1.65-1.175 2.825T16 22zm1.5-13q-.425 0-.712-.288T10.975 8q0-.05.125-.5q.2-.35.3-.712T11.5 6q0-1.25-.875-2.125T8.5 3t-2.125.875T5.5 6q0 .425.1.788t.3.712q.075.125.1.25t.025.25q0 .425-.275.713T5.05 9q-.275 0-.512-.15t-.363-.375q-.325-.55-.5-1.175T3.5 6q0-2.075 1.463-3.537T8.5 1t3.538 1.463T13.5 6q0 .675-.175 1.3t-.5 1.175q-.125.225-.35.375t-.5.15",
77
+ ],
78
+ },
79
+ };
80
+ /**
81
+ * Each value of the row, by its button's key: the icon in outline, the one the button shows while
82
+ * chosen — the same for a value without a fill — and whether the icon is mirrored in a row that
83
+ * runs right to left (architecture 6 spec).
84
+ */
85
+ export const ROW = {
86
+ next: {
87
+ outline: "arrow-forward-outline-rounded",
88
+ filled: "arrow-forward-outline-rounded",
89
+ mirrored: true,
90
+ },
91
+ demoShown: {
92
+ outline: "keyboard-arrow-down-outline-rounded",
93
+ filled: "keyboard-arrow-down-outline-rounded",
94
+ mirrored: false,
95
+ },
96
+ demoHidden: {
97
+ outline: "keyboard-arrow-right-outline-rounded",
98
+ filled: "keyboard-arrow-right-outline-rounded",
99
+ mirrored: true,
100
+ },
101
+ pen: { outline: "ink-pen-outline-rounded", filled: "ink-pen-rounded", mirrored: false },
102
+ eraser: { outline: "ink-eraser-outline-rounded", filled: "ink-eraser-rounded", mirrored: false },
103
+ undo: { outline: "undo-outline-rounded", filled: "undo-outline-rounded", mirrored: true },
104
+ clear: {
105
+ outline: "delete-sweep-outline-rounded",
106
+ filled: "delete-sweep-outline-rounded",
107
+ mirrored: false,
108
+ },
109
+ up: {
110
+ outline: "arrow-upward-outline-rounded",
111
+ filled: "arrow-upward-outline-rounded",
112
+ mirrored: false,
113
+ },
114
+ down: {
115
+ outline: "arrow-downward-outline-rounded",
116
+ filled: "arrow-downward-outline-rounded",
117
+ mirrored: false,
118
+ },
119
+ finger: {
120
+ outline: "touch-app-outline-rounded",
121
+ filled: "touch-app-outline-rounded",
122
+ mirrored: false,
123
+ },
124
+ };
@@ -1,20 +1,19 @@
1
- import type { CanvasElement, CanvasEventMap, ExportedWork } from "@altnovatrail/spec/canvas";
1
+ import type { CanvasElement, CanvasEventMap, ExportedWork, Media } from "@altnovatrail/spec/canvas";
2
2
  import { type Filled } from "../core/boxes.js";
3
3
  import { type DemoState, type WorkedExample } from "../core/example.js";
4
4
  import { type HistoryEvent } from "../core/history.js";
5
5
  import { type Manifest } from "../core/manifest.js";
6
6
  import { type Marks } from "../core/marks.js";
7
7
  import { type Task, type Tool } from "../core/task.js";
8
- import { type Media } from "./pictures.js";
9
8
  /**
10
9
  * The types of the contract of canvas and app are spec's (R74 spec; R07): the element as
11
10
  * CanvasElement has it, what its events tell and the work it gives for checking, here, and the data
12
11
  * — the history of writing, the context of a submission, the filled slots and the state of a demo
13
- * — through the DOM-free part. The entry point gives them under spec's names, the same types. The
14
- * tutor's marks, the media, the crop and a task with its temporary tools are the element's own
15
- * (R26, R41).
12
+ * — through the DOM-free part. The entry point gives them under spec's names, the same types, the
13
+ * media too (T73). The tutor's marks, the crop and a task with its temporary tools are the
14
+ * element's own (R26, R41).
16
15
  */
17
- export type { Box, CanvasElement, CanvasEventMap, DemoChangeEvent, ExportedCell, ExportedSheet, ExportedWork, ExportedWorkbook, HistoryChange, HistoryChangeEvent, ToolChange, ToolChangeEvent, } from "@altnovatrail/spec/canvas";
16
+ export type { Box, CanvasElement, CanvasEventMap, DemoChangeEvent, ExportedCell, ExportedSheet, ExportedWork, ExportedWorkbook, HistoryChange, HistoryChangeEvent, Media, ToolChange, ToolChangeEvent, } from "@altnovatrail/spec/canvas";
18
17
  export type { Filled, Filling } from "../core/boxes.js";
19
18
  export type { Crop, Frame } from "../core/crop.js";
20
19
  export type { DemoState, WorkedExample } from "../core/example.js";
@@ -25,7 +24,6 @@ export type { InkPoint, Stroke } from "../core/ink.js";
25
24
  export type { Manifest } from "../core/manifest.js";
26
25
  export type { Anchor, CellPlace, CropNumbers, Mark, Marks, SheetSubmission, Submission, WorkbookSubmission, } from "../core/marks.js";
27
26
  export type { Canvas, Task, Tool } from "../core/task.js";
28
- export type { Media } from "./pictures.js";
29
27
  /** The name the element is registered under. */
30
28
  export declare const TAG = "novatrail-canvas";
31
29
  /**
@@ -114,7 +112,8 @@ export declare class NovatrailCanvas extends HTMLElement implements CanvasElemen
114
112
  get manifest(): Manifest | undefined;
115
113
  set manifest(manifest: Manifest | undefined);
116
114
  /**
117
- * What gives the files of the pictures, by their references (R07): a task's statement's, and a
115
+ * What gives the files of the pictures, by their references, as the contract has it (the
116
+ * 2026-11-02 amendment to R36 spec; R07): a task's statement's, and a
118
117
  * worked example's — its statement's and every part's of its demo, which it asks for as the
119
118
  * example comes, before the learner opens them (R42). A reference is asked for once, whichever
120
119
  * pictures show it, and its file stays while the task or the example names it. Until a picture's
@@ -136,7 +136,7 @@ const STYLE = `
136
136
  display: flex; gap: 8px; margin-inline-end: 16px;
137
137
  }
138
138
  .tools button {
139
- box-sizing: border-box; min-width: 44px; min-height: 44px; padding: 0 16px; margin: 0;
139
+ box-sizing: border-box; min-width: 44px; min-height: 44px; padding: 0 8px; margin: 0;
140
140
  font: inherit; color: inherit; background: transparent; cursor: pointer;
141
141
  border: 1px solid currentColor; border-radius: 6px;
142
142
  }
@@ -147,11 +147,10 @@ const STYLE = `
147
147
  .tools [aria-disabled="true"] { opacity: 0.4; cursor: default; }
148
148
  .tools [hidden] { display: none; }
149
149
  .tools button svg {
150
- display: inline-block; width: 0.75em; height: 0.75em; margin-inline-end: 6px;
151
- vertical-align: -0.05em; fill: currentColor;
150
+ display: inline-block; width: 1.125em; height: 1.125em; margin-inline-end: 4px;
151
+ vertical-align: -0.25em; fill: currentColor;
152
152
  }
153
- .tools [aria-expanded="false"] svg { transform: rotate(-90deg); }
154
- .tools:dir(rtl) [aria-expanded="false"] svg { transform: rotate(90deg); }
153
+ .tools:dir(rtl) svg.mirrored { transform: scaleX(-1); }
155
154
  .viewport {
156
155
  flex: 1 1 auto; min-height: 0; overflow-x: hidden; overflow-y: auto; overflow-anchor: none;
157
156
  }
@@ -643,7 +642,8 @@ export class NovatrailCanvas extends HTMLElement {
643
642
  this.#settings = settings;
644
643
  }
645
644
  /**
646
- * What gives the files of the pictures, by their references (R07): a task's statement's, and a
645
+ * What gives the files of the pictures, by their references, as the contract has it (the
646
+ * 2026-11-02 amendment to R36 spec; R07): a task's statement's, and a
647
647
  * worked example's — its statement's and every part's of its demo, which it asks for as the
648
648
  * example comes, before the learner opens them (R42). A reference is asked for once, whichever
649
649
  * pictures show it, and its file stays while the task or the example names it. Until a picture's
@@ -1,9 +1,4 @@
1
- /**
2
- * What gives the file of a picture of a task or a worked example: the consumer's, for the canvas
3
- * goes to no network (R07; repositories 2.1 spec). app takes the files from what api serves, the
4
- * samples page from its own build.
5
- */
6
- export type Media = (ref: string) => Promise<Blob>;
1
+ import type { Media } from "@altnovatrail/spec/canvas";
7
2
  /** A picture on show, as what shows it has it. */
8
3
  export interface PictureView {
9
4
  /** Its file's address came, decoded: the picture shows it. */
@@ -48,7 +48,8 @@ export declare class Tools {
48
48
  * The row as the sheet now is. Only what has changed is written. A worked example's row has no
49
49
  * tools of writing, no undo and no clearing, for the learner does not write on its sheet, and has
50
50
  * Next, unavailable while the demo is hidden or open whole, and Demo, expanded while the demo is
51
- * shown (R42).
51
+ * shown (R42). The tool in use shows its icon filled, and Demo's icon points down while the demo
52
+ * is shown, and along the row while it is hidden (R46).
52
53
  */
53
54
  show({ kind, tools, tool, undoable, clearable, finger, opens, expanded }: ToolState): void;
54
55
  /**
@@ -9,9 +9,12 @@
9
9
  // workbook (R36). A lesson's worked example names it too, and has no tools of writing in it: the
10
10
  // learner does not write on its sheet. Its first buttons are its demo's, Next, which opens the next
11
11
  // part, and Demo, a disclosure button that hides the whole demo and shows it again (R42). A button
12
- // that goes gives its stop of Tab and the focus to the first button shown. This module is no entry
12
+ // that goes gives its stop of Tab and the focus to the first button shown. Each button shows an
13
+ // icon of the set every NovaTrail package of the interface draws from, before its label, which
14
+ // stays its name: the icon says nothing to a screen reader (R46; R78 spec). This module is no entry
13
15
  // point of the package.
14
16
  import { SVG, write } from "./attributes.js";
17
+ import { ICONS, ROW } from "./icons.js";
15
18
  import { labelsFor } from "./labels.js";
16
19
  /** The row of the element's own buttons, as the element shows it. */
17
20
  export class Tools {
@@ -19,31 +22,35 @@ export class Tools {
19
22
  element = document.createElement("div");
20
23
  /** Next and Demo, a worked example's first buttons, kept together on a narrow screen (R42). */
21
24
  #demoPair = document.createElement("div");
22
- #next = plainButton();
23
- #demo = plainButton();
24
- #demoLabel = document.createElement("span");
25
+ #next = plainButton("next");
26
+ #demo = plainButton("demoShown");
25
27
  #group = document.createElement("div");
26
- #pen = radioButton();
27
- #eraser = radioButton();
28
- #undo = plainButton();
29
- #clear = plainButton();
28
+ #pen = radioButton("pen");
29
+ #eraser = radioButton("eraser");
30
+ #undo = plainButton("undo");
31
+ #clear = plainButton("clear");
30
32
  /** Up and Down, a pair that a row wrapping on a narrow screen keeps together. */
31
33
  #pair = document.createElement("div");
32
- #up = plainButton();
33
- #down = plainButton();
34
- #finger = plainButton();
35
- /** The buttons in the order the arrow keys go through them: the row's own. */
36
- #buttons = [
37
- this.#next,
38
- this.#demo,
39
- this.#pen,
40
- this.#eraser,
41
- this.#undo,
42
- this.#clear,
43
- this.#up,
44
- this.#down,
45
- this.#finger,
34
+ #up = plainButton("up");
35
+ #down = plainButton("down");
36
+ #finger = plainButton("finger");
37
+ /**
38
+ * The buttons with the keys of their labels, in the order the arrow keys go through them: the
39
+ * row's own. A button is named only by its label (R46), so each is here with its key.
40
+ */
41
+ #labelled = [
42
+ [this.#next, "next"],
43
+ [this.#demo, "demo"],
44
+ [this.#pen, "pen"],
45
+ [this.#eraser, "eraser"],
46
+ [this.#undo, "undo"],
47
+ [this.#clear, "clear"],
48
+ [this.#up, "up"],
49
+ [this.#down, "down"],
50
+ [this.#finger, "finger"],
46
51
  ];
52
+ /** The buttons in the order the arrow keys go through them. */
53
+ #buttons = this.#labelled.map(([button]) => button);
47
54
  /** The language of the labels shown. */
48
55
  #lang;
49
56
  /** What the row is of, named so. */
@@ -58,7 +65,6 @@ export class Tools {
58
65
  this.#next.setAttribute("aria-controls", controls.status);
59
66
  this.#demo.setAttribute("aria-controls", controls.parts);
60
67
  this.#demo.setAttribute("aria-expanded", "true");
61
- this.#demo.append(triangle(), this.#demoLabel);
62
68
  this.#demoPair.append(this.#next, this.#demo);
63
69
  this.#group.className = "choice";
64
70
  this.#group.setAttribute("role", "radiogroup");
@@ -107,7 +113,8 @@ export class Tools {
107
113
  * The row as the sheet now is. Only what has changed is written. A worked example's row has no
108
114
  * tools of writing, no undo and no clearing, for the learner does not write on its sheet, and has
109
115
  * Next, unavailable while the demo is hidden or open whole, and Demo, expanded while the demo is
110
- * shown (R42).
116
+ * shown (R42). The tool in use shows its icon filled, and Demo's icon points down while the demo
117
+ * is shown, and along the row while it is hidden (R46).
111
118
  */
112
119
  show({ kind, tools, tool, undoable, clearable, finger, opens, expanded }) {
113
120
  if (kind !== this.#kind) {
@@ -132,6 +139,9 @@ export class Tools {
132
139
  write(this.#clear, "aria-disabled", clearable ? null : "true");
133
140
  write(this.#next, "aria-disabled", opens ? null : "true");
134
141
  write(this.#demo, "aria-expanded", String(expanded));
142
+ draw(this.#pen, "pen", tool === "pen");
143
+ draw(this.#eraser, "eraser", tool === "eraser");
144
+ draw(this.#demo, expanded ? "demoShown" : "demoHidden", false);
135
145
  }
136
146
  /**
137
147
  * Whether anything scrolls back up the sheet, for Up, and on down it, for Down: a button with
@@ -153,15 +163,11 @@ export class Tools {
153
163
  this.element.lang = lang;
154
164
  this.#name();
155
165
  this.#group.setAttribute("aria-label", labels.tool);
156
- this.#next.textContent = labels.next;
157
- this.#demoLabel.textContent = labels.demo;
158
- this.#pen.textContent = labels.pen;
159
- this.#eraser.textContent = labels.eraser;
160
- this.#undo.textContent = labels.undo;
161
- this.#clear.textContent = labels.clear;
162
- this.#up.textContent = labels.up;
163
- this.#down.textContent = labels.down;
164
- this.#finger.textContent = labels.finger;
166
+ for (const [button, key] of this.#labelled) {
167
+ const label = button.lastElementChild;
168
+ if (label !== null)
169
+ label.textContent = labels[key];
170
+ }
165
171
  }
166
172
  /** The row's name: the canvas kind's, in the labels' language. */
167
173
  #name() {
@@ -169,7 +175,8 @@ export class Tools {
169
175
  write(this.element, "aria-label", labels[this.#kind]);
170
176
  }
171
177
  /**
172
- * The arrow keys move the focus to the next button or the one before, round the row, and Home
178
+ * The arrow keys move the focus to the next button or the one before, round the row — the next
179
+ * is the one the arrow points to, so in a row that runs right to left it is ArrowLeft — and Home
173
180
  * and End to the first and the last: nothing is chosen, and no other key is the row's.
174
181
  */
175
182
  #key(event) {
@@ -179,10 +186,12 @@ export class Tools {
179
186
  const at = shown.indexOf(event.target);
180
187
  if (at < 0)
181
188
  return;
189
+ const onward = this.element.matches(":dir(rtl)") ? "ArrowLeft" : "ArrowRight";
190
+ const back = onward === "ArrowLeft" ? "ArrowRight" : "ArrowLeft";
182
191
  let next;
183
- if (event.key === "ArrowRight")
192
+ if (event.key === onward)
184
193
  next = shown[(at + 1) % shown.length];
185
- else if (event.key === "ArrowLeft")
194
+ else if (event.key === back)
186
195
  next = shown[(at - 1 + shown.length) % shown.length];
187
196
  else if (event.key === "Home")
188
197
  next = shown[0];
@@ -228,27 +237,48 @@ export class Tools {
228
237
  }
229
238
  }
230
239
  /** A button of a tool: a radio button of the group, a button still, which the arrows leave alone. */
231
- function radioButton() {
232
- const button = plainButton();
240
+ function radioButton(value) {
241
+ const button = plainButton(value);
233
242
  button.setAttribute("role", "radio");
234
243
  button.setAttribute("aria-checked", "false");
235
244
  return button;
236
245
  }
237
246
  /**
238
- * The triangle of a disclosure button, pointing down while what it shows is shown: a picture that
239
- * says nothing, for aria-expanded says it (R42).
247
+ * A button: its icon, an SVG that says nothing — no title, hidden from a screen reader — and its
248
+ * label, which names it (R46; R78 spec).
240
249
  */
241
- function triangle() {
242
- const svg = document.createElementNS(SVG, "svg");
243
- svg.setAttribute("viewBox", "0 0 10 10");
244
- svg.setAttribute("aria-hidden", "true");
245
- const path = document.createElementNS(SVG, "path");
246
- path.setAttribute("d", "M1 3h8L5 8z");
247
- svg.append(path);
248
- return svg;
249
- }
250
- function plainButton() {
250
+ function plainButton(value) {
251
251
  const button = document.createElement("button");
252
252
  button.type = "button";
253
+ const icon = document.createElementNS(SVG, "svg");
254
+ icon.setAttribute("aria-hidden", "true");
255
+ // The size of a letter, should the element's style not come; the style sets the one shown.
256
+ icon.setAttribute("width", "1em");
257
+ icon.setAttribute("height", "1em");
258
+ button.append(icon, document.createElement("span"));
259
+ draw(button, value, false);
253
260
  return button;
254
261
  }
262
+ /**
263
+ * A value's icon on its button, filled or in outline, written only when it is another than the one
264
+ * there: the icon's name stays on the SVG, as data. One mirrored in a row that runs right to left
265
+ * says so by its class, which the style turns (R46).
266
+ */
267
+ function draw(button, value, filled) {
268
+ const icon = button.firstElementChild;
269
+ if (!(icon instanceof SVGSVGElement))
270
+ return;
271
+ const { outline, filled: full, mirrored } = ROW[value];
272
+ const name = filled ? full : outline;
273
+ if (icon.dataset.icon === name)
274
+ return;
275
+ const { viewBox, paths } = ICONS[name];
276
+ icon.dataset.icon = name;
277
+ icon.setAttribute("viewBox", viewBox);
278
+ icon.classList.toggle("mirrored", mirrored);
279
+ icon.replaceChildren(...paths.map((d) => {
280
+ const path = document.createElementNS(SVG, "path");
281
+ path.setAttribute("d", d);
282
+ return path;
283
+ }));
284
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@altnovatrail/canvas",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "NovaTrail's canvases: the workplace of a task, where the learner writes, draws and types.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -9,7 +9,8 @@
9
9
  },
10
10
  "type": "module",
11
11
  "files": [
12
- "lib"
12
+ "lib",
13
+ "NOTICE"
13
14
  ],
14
15
  "exports": {
15
16
  ".": {
@@ -25,12 +26,14 @@
25
26
  "./lib/element/index.js"
26
27
  ],
27
28
  "dependencies": {
28
- "@altnovatrail/spec": "0.6.2",
29
+ "@altnovatrail/spec": "0.6.5",
29
30
  "perfect-freehand": "1.2.3",
30
31
  "rbush": "4.0.1"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@biomejs/biome": "2.6.4",
35
+ "@iconify-json/material-symbols": "1.2.97",
36
+ "@iconify/utils": "3.1.7",
34
37
  "@types/node": "24.19.2",
35
38
  "@types/rbush": "4.0.0",
36
39
  "@vitest/browser-playwright": "5.0.5",