tosijs-ui 1.17.0-beta.4 → 1.17.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.
Files changed (91) hide show
  1. package/CHANGELOG.md +231 -66
  2. package/README.md +1 -1
  3. package/bin/make-icon-data.js +39 -4
  4. package/dist/ab-test.js +2 -2
  5. package/dist/affix.js +1 -1
  6. package/dist/carousel.d.ts +6 -1
  7. package/dist/carousel.js +32 -3
  8. package/dist/code-editor-cm.d.ts +2 -0
  9. package/dist/code-editor-cm.js +13 -2
  10. package/dist/code-editor.js +4 -0
  11. package/dist/crud.js +1 -1
  12. package/dist/data-table.d.ts +30 -0
  13. package/dist/data-table.js +205 -14
  14. package/dist/dialog.js +1 -1
  15. package/dist/diff.js +1 -1
  16. package/dist/doc-browser.js +31 -4
  17. package/dist/doc-system/example-policy.d.ts +15 -1
  18. package/dist/doc-system/example-policy.js +28 -0
  19. package/dist/doc-system/render.d.ts +21 -0
  20. package/dist/doc-system/render.js +55 -9
  21. package/dist/doc-system/site/check-examples-cli.js +1 -0
  22. package/dist/doc-system/site/check-examples.d.ts +18 -0
  23. package/dist/doc-system/site/check-examples.js +46 -3
  24. package/dist/doc-system/site/config-fingerprint.d.ts +1 -0
  25. package/dist/doc-system/site/config-fingerprint.js +33 -0
  26. package/dist/doc-system/site/dev-server.d.ts +1 -0
  27. package/dist/doc-system/site/dev-server.js +25 -0
  28. package/dist/doc-system/site/docs.d.ts +1 -0
  29. package/dist/doc-system/site/docs.js +1 -1
  30. package/dist/doc-system/site/generate-site.d.ts +11 -1
  31. package/dist/doc-system/site/generate-site.js +141 -16
  32. package/dist/doc-system/site/image-size.d.ts +8 -0
  33. package/dist/doc-system/site/image-size.js +72 -0
  34. package/dist/doc-system/site/make-llms-txt.d.ts +24 -11
  35. package/dist/doc-system/site/make-llms-txt.js +118 -93
  36. package/dist/doc-system/site/orchestrator.d.ts +8 -0
  37. package/dist/doc-system/site/orchestrator.js +112 -15
  38. package/dist/doc-system/site/page-dates.d.ts +8 -0
  39. package/dist/doc-system/site/page-dates.js +85 -0
  40. package/dist/doc-system/site/page-globals.d.ts +12 -1
  41. package/dist/doc-system/site/page-globals.js +26 -4
  42. package/dist/doc-system/site/page-metadata.d.ts +36 -0
  43. package/dist/doc-system/site/page-metadata.js +96 -0
  44. package/dist/doc-system/site/sections.js +27 -17
  45. package/dist/doc-system/site/site-config.d.ts +51 -10
  46. package/dist/filter-builder.js +4 -4
  47. package/dist/float.js +3 -3
  48. package/dist/form.js +1 -1
  49. package/dist/header.js +2 -2
  50. package/dist/icon-data.js +11 -11
  51. package/dist/icons.js +43 -5
  52. package/dist/iife.js +105 -105
  53. package/dist/iife.js.map +45 -44
  54. package/dist/layout.js +1 -1
  55. package/dist/live-example/component.d.ts +7 -0
  56. package/dist/live-example/component.js +158 -36
  57. package/dist/live-example/example-hooks.d.ts +55 -0
  58. package/dist/live-example/example-hooks.js +48 -0
  59. package/dist/live-example/execution.d.ts +8 -11
  60. package/dist/live-example/execution.js +60 -5
  61. package/dist/live-example/index.d.ts +5 -1
  62. package/dist/live-example/index.js +3 -1
  63. package/dist/live-example/insert-examples.d.ts +4 -0
  64. package/dist/live-example/insert-examples.js +42 -4
  65. package/dist/live-theme.js +4 -4
  66. package/dist/make-sorter.js +2 -1
  67. package/dist/mapbox.js +1 -1
  68. package/dist/markdown-viewer.js +1 -1
  69. package/dist/menu.d.ts +1 -0
  70. package/dist/menu.js +20 -2
  71. package/dist/month.js +9 -3
  72. package/dist/palette.d.ts +4 -2
  73. package/dist/palette.js +29 -14
  74. package/dist/password-strength.js +2 -2
  75. package/dist/rating.js +1 -1
  76. package/dist/rich-text.js +3 -0
  77. package/dist/router.js +1 -1
  78. package/dist/schema-form.d.ts +2 -0
  79. package/dist/schema-form.js +27 -3
  80. package/dist/select.d.ts +34 -1
  81. package/dist/select.js +111 -16
  82. package/dist/sizer.js +1 -1
  83. package/dist/tab-selector.d.ts +7 -5
  84. package/dist/tab-selector.js +46 -13
  85. package/dist/tag-list.js +11 -2
  86. package/dist/theme.js +1 -1
  87. package/dist/tooltip.js +1 -1
  88. package/dist/version.d.ts +1 -1
  89. package/dist/version.js +1 -1
  90. package/llms.txt +95 -155
  91. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,73 +1,88 @@
1
1
  # Changelog
2
2
 
3
- ## 1.17.0-beta.4
4
-
5
- > Prerelease, published under the `beta` dist-tag: install with `tosijs-ui@beta`. `latest` stays
6
- > on 1.16.41. Fixes from Learn 3D and from tosijs-native's first site build.
7
-
8
- - **Every doc page can be scrolled by keyboard, and has a main landmark (#3575).** A page of
9
- plain prose had nothing to tab to inside its scrolling content, so a keyboard user could not
10
- scroll it, which axe reports as `scrollable-region-focusable` (Learn 3D: 71 of 126 pages). The
11
- content scroller is now focusable, and it is the page's main landmark, which the doc site did not
12
- have at all: a screen reader's "jump to main content" now works. A doc browser embedded in
13
- another page stays focusable without claiming that page's main landmark.
14
- - **`buildSite` says it needs `happy-dom` before it starts (#3600).** Without it, the build died
15
- partway through with "Cannot find package 'happy-dom'" from a file inside tosijs-ui. It now
16
- stops before writing anything and prints the install command. `happy-dom` stays an optional
17
- peer: a project that only uses the components never needs it.
18
- - **TJS's own doc comments, `/# … #/`, become documentation (#3601).** In `.tjs` and `.ajs`
19
- files they are read in source order with any `/*#` blocks. tjs-lang finds them, so its grammar
20
- applies exactly; that needs a tjs-lang that exports `findDocCommentSpans` (requested). With an
21
- older one, the build names each file whose `/#` comments it could not read, instead of
22
- dropping the page silently. `.ajs` files are now scraped for `/*#` blocks too.
23
-
24
- ### 1.17.0-beta.3 (prerelease detail)
25
-
26
- > Prerelease, published under the `beta` dist-tag: install with `tosijs-ui@beta`. `latest` stays
27
- > on 1.16.41. beta.2 was staged and never published; its notes follow.
28
-
29
- - **The axe audit waits for a page marked `aria-busy`, and one slow page no longer ends it.** A
30
- page that is still loading (a 3D scene fetching large models, say) marks an element
31
- `aria-busy="true"`, the standard ARIA signal, and is waited for up to 30 seconds; other pages
32
- are audited once they have stopped adding elements, or after five seconds. A page that never
33
- loads, or is still busy at 30 seconds, is listed as not fully checked, and the build makes no
34
- claim about it. Before, each page load waited for the network to go idle, so a page that kept
35
- downloading for 30 seconds, or that polled, threw and ended the audit of every page. The doc
36
- site reference shows a component setting and clearing `aria-busy`.
37
-
38
- ### 1.17.0-beta.2 (prerelease detail)
39
-
40
- > Prerelease, published under the `beta` dist-tag: install with `tosijs-ui@beta`. `latest` stays
41
- > on 1.16.41. Fixes from Learn 3D's first run of beta.1.
42
-
43
- - **Accessibility mode: headings in a blockquote or list shift with the rest of the page
44
- (#3576).** A sidebar's `> ####` under `###` sections stayed an `<h4>` under the new `<h2>`s, so
45
- the build reported a skipped level and DAISY Ace failed the ePub on heading order. A heading
46
- inside a blockquote still never becomes the page's title.
47
- - **DAISY Ace is found on `PATH` (#3577).** The docs said `bun add -d @daisy/ace`, which brings
48
- Electron and Puppeteer into the project, and their advisories then failed the build's own
49
- dependency audit. Install it globally (`npm install -g @daisy/ace`) instead; the build looks in
50
- the project first, then on `PATH`.
51
- - **The axe audit is about twice as fast, and audits the page a reader gets (#3585).** Pages are
52
- audited four at a time (this site: 80s to 43s). Each is audited once it stops adding elements,
53
- rather than after a fixed 300ms, which on a busy machine caught pages half-built. And the
54
- audit no longer starts the doc browser's background test runner, which runs on localhost and
55
- never on a deployed site. `TOSIJS_A11Y_AUDIT=off` skips the audit and the Ace check for one
56
- build while the mode stays on; the ePub then claims no conformance.
57
- - **Section stubs are not written where they cannot be read, or mid-edit (#3584).** A page whose
58
- `parent` names a missing doc gets a stub section doc in `sectionsDir`. When `sectionsDir` is
59
- outside every `docPaths` entry, the build now warns and writes nothing, since the stub would
60
- never be read back. Stubs are created only on the first build in a process, so a parent that
61
- disappears for a second during a rename no longer leaves files behind. Each stub written is
62
- named in the build output.
63
- - **`preview.host` is optional, and the preview docs no longer say `host` is required (#3586).**
64
- The type has allowed a config with only `preview.tunnel` since 1.10.0, but the doc comment and
65
- the reference table still said so. `as any` casts written for it can go.
3
+ ## 1.17.0
4
+
5
+ > Accessibility and SEO for doc sites, and the components themselves passing WCAG 2.2 AA. A doc
6
+ > site can now be built to a stated accessibility level, with an accessible ePub, and every page
7
+ > carries share images, structured data and an llms.txt worth reading. Live examples can run
8
+ > under a Content Security Policy with no `'unsafe-eval'`. `<tosi-table>` gains elastic columns,
9
+ > and the icon generator keeps open paths open. Released as five betas; this section folds them
10
+ > together.
11
+ >
12
+ > **`dist/iife.js` grew 505.2 kB → 509.5 kB gzip (+4.3 kB, +0.9%)**, measured with `gzip -9`
13
+ > against 1.16.41. The ESM entry points pay only for what they import.
14
+
15
+ **Visible changes.** Nothing an adopter must change, but these look or behave differently:
16
+
17
+ - An editable `<tosi-select>` looks like a text field rather than a button, because it is one.
18
+ - `<tosi-month>` uses mediumvioletred where it used hotpink, which failed contrast;
19
+ `--month-day-color` and `--month-date-checked-background` restore the old colour.
20
+ - `<tosi-carousel>`'s dots are 24px apart rather than 16px.
21
+ - An icon whose classes are both `stroked filled` now draws its outline, over a knockout fill
22
+ (`--tosi-icon-knockout`, default `--tosi-bg`). The `filled` class used to win and remove every
23
+ stroke.
24
+ - The doc site header's site name is a `<span class="site-name">`, not an `<h2>`: site CSS that
25
+ styled `header h2` should target `header .site-name`.
26
+ - A section page's `<!-- toc -->` list is no longer written into its source file.
66
27
 
67
- ### 1.17.0-beta.1 (prerelease detail)
28
+ ### Security
68
29
 
69
- > Prerelease, published under the `beta` dist-tag: install with `tosijs-ui@beta`. `latest` stays
70
- > on 1.16.41. It carries the accessibility half of 1.17.0; the SEO work and #245/#246 follow.
30
+ - **Image alt and title text are escaped in rendered pages.** marked wrote alt text into the
31
+ attribute as it was, so a `"` ended it. An ordinary caption (`A sign reading "Open"`) broke the
32
+ tag in every release that renders markdown images. In the 1.17 betas, alt text from an
33
+ `altText` hook, which is machine-generated and so untrusted, could inject markup such as a live
34
+ `<img onerror>`. **Affected:** 1.17.0-beta.1 through beta.5 with an `altText` hook (author
35
+ markdown with a `"` in its alt text rendered a broken tag in every earlier version, but that
36
+ text is the site author's own).
37
+ - **What a `setExampleHooks` `sanitize` hook returns is what the preview gets.** The betas
38
+ serialized the hook's fragment and parsed it again, and HTML does not survive that round trip
39
+ through MathML or SVG: markup the hook had judged harmless could come back as a live
40
+ `<img onerror>` (shown in Chromium, Firefox and WebKit). The fragment is now inserted as it is.
41
+ **Affected:** 1.17.0-beta.5, on pages that register a `sanitize` hook.
42
+
43
+ The 1.17.0 betas are deprecated on npm with a message pointing here; upgrade to 1.17.0.
44
+
45
+ ### Accessibility: the components pass axe's WCAG 2.2 AA rules
46
+
47
+ The doc site's accessibility audit found 142 failures across 75 pages, most of them in
48
+ components every adopter ships. A new Playwright spec holds 20 component pages, examples
49
+ included, at zero.
50
+
51
+ - **`<tosi-table>` is a grid of rows (#3570).** Its scroll area is `role="grid"` and every row
52
+ `role="row"`. It was a list of list items holding grid cells, which a screen reader announced
53
+ as a list of unnamed items. A custom `dataCell` gets `role="gridcell"` like the table's own
54
+ cells, unless it carries a role of its own, and an expanded row's detail is a full-width
55
+ `gridcell` rather than a `region`, because a row may only contain cells.
56
+ - **`<tosi-tag-list>`'s chips are list items**, and an empty tag list is no longer an empty list.
57
+ - **`<tosi-select>` no longer puts its input inside its button (#3571).** A select you pick from
58
+ is its button, which is the combobox and shows the caption as text; an editable one is its
59
+ input, in a field beside the button. The combobox is named by the host's `aria-label` or
60
+ `title`, its `<label>`, or the placeholder, and ↓ opens the list.
61
+ - **`<tosi-tabs>` (#3572):** the tab row is the `tablist` and each body a `tabpanel`; a tab
62
+ points at its panel through `ariaControlsElements` (an id cannot cross the shadow root), and
63
+ `aria-selected` is `"true"` or absent rather than empty. The close glyph is no longer a button
64
+ inside the tab: Delete or Backspace closes a closeable tab from the keyboard, and the glyph
65
+ keeps a 24px pointer target.
66
+ - **Names for icon-only controls.** An icon-only `<tosi-menu>`'s trigger is named by the host's
67
+ `aria-label` or `title`, and says it opens a menu. `<tosi-month>`'s header buttons and
68
+ month/year selects have names. `<tosi-code>`'s editable area is named (the host's `aria-label`
69
+ or `title`, else "<mode> code"). `<tosi-theme-editor>`'s inputs and the filter builder's
70
+ controls are named. `<tosi-schema-form>`'s labels are tied to their controls (a plugin's
71
+ through its first input), so every field has a name.
72
+ - **Contrast.** `<tosi-month>`'s day headings and checked dates use mediumvioletred (5:1)
73
+ instead of hotpink (2.6:1); `--month-day-color` and `--month-date-checked-background` restore
74
+ the old colour. `<tosi-code>`'s light theme's type names are #075 instead of CodeMirror's #085
75
+ (4.43:1). `<tosi-palette>`'s trigger text is #0009 instead of #0008, just under 4.5:1 on a
76
+ near-white field.
77
+ - **Targets and keyboard.** `<tosi-carousel>`'s dots sit 24px apart (WCAG 2.5.8), and its slide
78
+ strip takes focus, with ← and → paging it (#3573). `<tosi-rich-text>`'s toolbar buttons are
79
+ at least 24px, and a live example's scrolling test results take focus.
80
+ - The doc examples name their controls and use colours that pass AA (#3574).
81
+ - **Enter opens a `<tosi-select>` you pick from**, like Space and ↓. (beta.5 cancelled it.)
82
+ - **`<tosi-table>` gives `role="row"` only to its own rows**: a list inside a cell keeps its list
83
+ items.
84
+
85
+ ### Accessibility mode for doc sites
71
86
 
72
87
  - **An opt-in accessibility mode for doc sites (#3521).** `accessibility: 'wcag22-aa'` in the
73
88
  site config makes the build do what it can decide on its own and report what it cannot, on the
@@ -108,6 +123,156 @@
108
123
  - **The header's site name is no longer a heading.** It was an `<h2>` above every page's own
109
124
  `<h1>`, so it was each page's first heading. It is now `<span class="site-name">` and looks the
110
125
  same; site CSS that styled `header h2` should target `header .site-name`.
126
+ - **Accessibility mode: headings in a blockquote or list shift with the rest of the page
127
+ (#3576).** A sidebar's `> ####` under `###` sections stayed an `<h4>` under the new `<h2>`s, so
128
+ the build reported a skipped level and DAISY Ace failed the ePub on heading order. A heading
129
+ inside a blockquote still never becomes the page's title.
130
+ - **DAISY Ace is found on `PATH` (#3577).** The docs said `bun add -d @daisy/ace`, which brings
131
+ Electron and Puppeteer into the project, and their advisories then failed the build's own
132
+ dependency audit. Install it globally (`npm install -g @daisy/ace`) instead; the build looks in
133
+ the project first, then on `PATH`.
134
+ - **The axe audit is about twice as fast, and audits the page a reader gets (#3585).** Pages are
135
+ audited four at a time (this site: 80s to 43s). Each is audited once it stops adding elements,
136
+ rather than after a fixed 300ms, which on a busy machine caught pages half-built. And the
137
+ audit no longer starts the doc browser's background test runner, which runs on localhost and
138
+ never on a deployed site. `TOSIJS_A11Y_AUDIT=off` skips the audit and the Ace check for one
139
+ build while the mode stays on; the ePub then claims no conformance.
140
+ - **Section stubs are not written where they cannot be read, or mid-edit (#3584).** A page whose
141
+ `parent` names a missing doc gets a stub section doc in `sectionsDir`. When `sectionsDir` is
142
+ outside every `docPaths` entry, the build now warns and writes nothing, since the stub would
143
+ never be read back. Stubs are created only on the first build in a process, so a parent that
144
+ disappears for a second during a rename no longer leaves files behind. Each stub written is
145
+ named in the build output.
146
+ - **`preview.host` is optional, and the preview docs no longer say `host` is required (#3586).**
147
+ The type has allowed a config with only `preview.tunnel` since 1.10.0, but the doc comment and
148
+ the reference table still said so. `as any` casts written for it can go.
149
+ - **The axe audit waits for a page marked `aria-busy`, and one slow page no longer ends it.** A
150
+ page that is still loading (a 3D scene fetching large models, say) marks an element
151
+ `aria-busy="true"`, the standard ARIA signal, and is waited for up to 30 seconds; other pages
152
+ are audited once they have stopped adding elements, or after five seconds. A page that never
153
+ loads, or is still busy at 30 seconds, is listed as not fully checked, and the build makes no
154
+ claim about it. Before, each page load waited for the network to go idle, so a page that kept
155
+ downloading for 30 seconds, or that polled, threw and ended the audit of every page. The doc
156
+ site reference shows a component setting and clearing `aria-busy`.
157
+ - **Every doc page can be scrolled by keyboard, and has a main landmark (#3575).** A page of
158
+ plain prose had nothing to tab to inside its scrolling content, so a keyboard user could not
159
+ scroll it, which axe reports as `scrollable-region-focusable` (Learn 3D: 71 of 126 pages). The
160
+ content scroller is now focusable, and it is the page's main landmark, which the doc site did not
161
+ have at all: a screen reader's "jump to main content" now works. A doc browser embedded in
162
+ another page stays focusable without claiming that page's main landmark.
163
+ - **`buildSite` says it needs `happy-dom` before it starts (#3600).** Without it, the build died
164
+ partway through with "Cannot find package 'happy-dom'" from a file inside tosijs-ui. It now
165
+ stops before writing anything and prints the install command. `happy-dom` stays an optional
166
+ peer: a project that only uses the components never needs it.
167
+ - **TJS's own doc comments, `/# … #/`, become documentation (#3601).** In `.tjs` and `.ajs`
168
+ files they are read in source order with any `/*#` blocks. tjs-lang finds them, so its grammar
169
+ applies exactly; that needs a tjs-lang that exports `findDocCommentSpans` (requested). With an
170
+ older one, the build names each file whose `/#` comments it could not read, instead of
171
+ dropping the page silently. `.ajs` files are now scraped for `/*#` blocks too.
172
+
173
+ ### Live examples under a strict CSP
174
+
175
+ - **`liveExamples: 'no-code'` (tosijs-ui#246):** only `html` and `css` fences run; script,
176
+ dialect and `test` fences are shown as highlighted code. Nothing on a no-code example's path
177
+ constructs a `Function` or calls `eval` (no Console tab, since its REPL evaluates), so a doc
178
+ system served under a CSP without `'unsafe-eval'` runs its examples.
179
+ - **`setExampleHooks({ sanitize, hydrate, chat })`** from `tosijs-ui/live-example`:
180
+ `sanitize(fragment, example)` sees the markup parsed but inert before it enters the preview;
181
+ `hydrate(preview, signal, example)` runs once it is in (after any script), with a signal that
182
+ aborts on the next run; `chat(session)` builds a Chat tab whose `session.apply({ html, css })`
183
+ runs like an edit. They apply in every mode.
184
+ - **`exampleTabs`** in the site config, and `{"tabs": [...]}` per fence, choose the code panel's
185
+ tabs from `js`, `html`, `css`, `test`, `console`, `chat`. Under no-code the default is
186
+ `['html', 'css', 'chat']`. A misspelt tab fails the build by name.
187
+ - **`setExamplePolicy` and `setExampleTabs` are exported** from `tosijs-ui/live-example`; the
188
+ first was documented as callable but never exported.
189
+ - **`exampleModules: true` (tosijs-ui#245):** the build writes each script example (`js`, `ts`,
190
+ `tjs`) to `<outputDir>/_examples/<hash>.js`, and the page runs an unedited inline example by
191
+ `import()`-ing its module instead of compiling its source, so it runs under
192
+ `script-src 'self'` with no `'unsafe-eval'`. The module's lines are the example's own, so an
193
+ error still names its line. Under such a policy, an EDITED example (which must compile) says
194
+ the policy refused it and that reverting runs the published example, instead of an engine
195
+ message about `'unsafe-eval'`.
196
+
197
+ ### The doc site's SEO surface
198
+
199
+ - **Share images (#3562).** A page's share image (og:image, twitter:image) is its own `image`,
200
+ else its card at `/og/<slug>.webp|png|jpg` when the static files have one, else the site's
201
+ `ogImage`, else the home page's card. The cards `bun run og` wrote were linked from no page,
202
+ so every share unfurled without an image. A local share image gets `og:image:width`/`height`,
203
+ read from its header (PNG, WebP, JPEG, GIF), and every one gets an alt of the page's title.
204
+ - **`bun run og`** cards only the home page and pages with a live example and no `image` of
205
+ their own; every other page shares the home page's card instead of getting a text-only one.
206
+ It encodes with cwebp (`brew install webp`): Homebrew's ffmpeg 8 has no webp encoder, so every
207
+ card failed to encode. A card with an example screenshot gives its title and description the
208
+ left column (they ran under the shot) and captures the preview without the example's toolbar,
209
+ and a page whose example cannot be captured now fails the run (exit 1) instead of quietly
210
+ getting a text-only card.
211
+ - **`sitemap.xml` entries carry `lastmod` (#3565):** the last commit that touched the page's
212
+ source, or its mtime while it has uncommitted edits. Without git there is none, rather than
213
+ an invented one.
214
+ - **JSON-LD names the page properly** (its headTitle without the project suffix), with
215
+ `dateModified`, `datePublished` (frontmatter `date`) and `author` (the page's, else
216
+ `epub.author`); a page inside a nav section gets a `BreadcrumbList`; the home page is the
217
+ `WebSite` rather than a `TechArticle`.
218
+ - **`kind: 'book'`** in the site config: pages become `Chapter`s of a `Book`, numbered in
219
+ reading order, and link the previous and next page with `<link rel="prev/next">`.
220
+ - **`llms.txt` follows llmstxt.org (#3564):** an H1, the site description as a `>` summary,
221
+ then `- [title](url): description` lines in nav order, with each nav section as its own `##`
222
+ section. Links are the pages' markdown copies, and descriptions are the ones the pages' heads
223
+ carry. **`llms-full.txt`** is the whole indexed corpus as one markdown file, in nav order,
224
+ each page followed by its URL, written to the output dir only.
225
+ - **The metadata lint (#3563):** every build lists pages whose title or description needs
226
+ writing — missing, too short or long, derived from a paragraph cut mid-sentence, or shared
227
+ with another page. It warns (once per change under `bun start`) and fails a `strict` build.
228
+ A description derived from a page's first paragraph no longer keeps `_emphasis_`,
229
+ `~~strikethrough~~`, wikilinks or footnote markers, and is cut at 155 characters, where
230
+ search results cut it.
231
+ - **`bun run og` no longer stops the dev server it captures from.** It wrote each card into the
232
+ watched `demo/static/og/` as it went, and the server's rebuild-loop guard shut the server down
233
+ part way through; cards are now written elsewhere and copied in at the end. It also removes
234
+ haltija's dev widget before capturing, which sat in the corner of every card.
235
+
236
+ ### Components
237
+
238
+ - **Elastic table columns (snowfox, #3858).** A `<tosi-table>` column with `elastic: true` takes
239
+ a share of the space left over when the columns add up to less than the table's width, in
240
+ proportion to its `width`, and never shrinks below it, so a narrow table still scrolls. The
241
+ table's `elastic` attribute makes every unpinned column elastic unless it says
242
+ `elastic: false`, which covers columns inferred from the data. Pinned columns are never
243
+ elastic, and elastic columns take precedence over `full-width-header`. Dragging an elastic
244
+ column's edge keeps the edge under the pointer, and a stretched column's edge can be grabbed
245
+ where it is drawn.
246
+ - **`--tosi-table-fill`** paints the space to the right of a table's columns when they don't
247
+ fill it. Transparent by default.
248
+ - **`<tosi-palette>`'s `localizer` localizes every caption the palette owns (tosijs-ui#247):** the
249
+ footer's Navigate, Select and Close, the dialog's Search and the clear button's Clear names,
250
+ `emptyCaption` and `quickLinksCaption`, not only the placeholder.
251
+ - **`tosijs-make-icons` keeps open paths open (tosijs-ui#249).** In a folder that is both stroked
252
+ and filled (`stroked/filled/`), an element's own `fill="none"`, or `fill:none` in its style, is
253
+ kept: those icons' fill is a knockout, and `fill="none"` is what keeps an arc or arrowhead
254
+ open. It also no longer swallows the rest of an icon after a `style` whose last fill or stroke
255
+ had no trailing `;`, and drops a style left empty. Checked against tosijs-3d's icons: the
256
+ output matches their post-pass, which can now go.
257
+
258
+ ### Doc system and build
259
+
260
+ - **A memory-routed doc browser follows `/slug/` links in memory (tosijs-ui#243).** It resolved
261
+ only `?filename`, yet rewrote every internal link to `/slug/` itself, so every doc-to-doc link
262
+ in an embedded browser was a full page load. A `/slug/#heading` link scrolls to the heading, and
263
+ canonicalising a content link keeps its `#heading`; it used to drop it.
264
+ - **A build handed to a running dev server is refused when the server has a different site
265
+ config (#3772, tosijs-ui#241).** The server builds the config it was started with, so editing
266
+ the config used to produce a build of the old one that reported success. Now `bun run build`
267
+ says to restart the server. The dev server also watches the `bundleEntry` file, so editing it
268
+ rebuilds the hydration bundle.
269
+ - **`buildSite` no longer rewrites the `<!-- toc -->` block in a section's source `.md` on every
270
+ build (#3733),** which left every adopter with section pages a dirty tree after building. The
271
+ list of child pages is filled in the built page (and its book variant) only; a section with no
272
+ `<!-- toc -->` marker still gets the list at the end. A committed block's old list is harmless,
273
+ but can be emptied to the bare markers.
274
+ - **A `liveExamples` or `kind` value the build does not know fails the build**, naming the allowed
275
+ values. `'nocode'` silently ran every example, on a site meant to run none.
111
276
 
112
277
  ## 1.16.41
113
278
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # tosijs-ui
2
2
 
3
- <!--{ "pin": "top", "order": 0, "layout": "full-width", "headTitle": "tosijs-ui — robust, standards-based web components", "description": "A small, fast library of standards-based web components (data tables, dialogs, forms, rich text, carousels, and more) built on tosijs — smaller and faster than a React app, with no virtual DOM.", "keywords": "web components, custom elements, tosijs, data table, dialog, form, rich text, no virtual dom" }-->
3
+ <!--{ "pin": "top", "order": 0, "layout": "full-width", "headTitle": "tosijs-ui — robust, standards-based web components", "description": "Standards-based web components (data tables, dialogs, forms, rich text, carousels) built on tosijs: smaller and faster than React, with no virtual DOM.", "keywords": "web components, custom elements, tosijs, data table, dialog, form, rich text, no virtual dom" }-->
4
4
 
5
5
  [ui.tosijs.net live demo](https://ui.tosijs.net) | [tosijs](https://tosijs.net) | [discord](https://discord.gg/ramJ9rgky5) | [github](https://github.com/tonioloewald/tosijs-ui#readme) | [npm](https://www.npmjs.com/package/tosijs-ui)
6
6
 
@@ -32,6 +32,9 @@ Folder names control CSS classes on the generated SVGs:
32
32
  - `color/` — icons with baked-in colors (class `color`, fill/stroke preserved)
33
33
  - `stroked/` — stroke-only icons (class `stroked`, fill styles removed)
34
34
  - `filled/` — fill-only icons (class `filled`, stroke styles removed)
35
+ - a folder that is both (`stroked/filled/`) — classes `stroked filled`; fill colours are
36
+ removed (the renderer supplies the fill), but an element's own `fill="none"` (or
37
+ `fill:none` in its style) is kept, because that is what keeps an arc or arrowhead open
35
38
 
36
39
  ### Directional folders
37
40
 
@@ -195,6 +198,9 @@ function getDirectionConfig(dir) {
195
198
  return DIRECTION_FOLDERS[dirName] || null
196
199
  }
197
200
 
201
+ // Stands in for an element's `fill="none"` while the strips run (see keepsOpenPaths).
202
+ const OPEN_PATH_MARK = 'data-tosi-open-path'
203
+
198
204
  function generateDirectionalRedirects(name, dirConfig) {
199
205
  // Find the base direction word in the camelCase name
200
206
  const baseDir = dirConfig.base
@@ -239,7 +245,28 @@ function findIcons(dirs, ignore = []) {
239
245
  .replace(/-([a-z0-9])/g, (_, char) => char.toLocaleUpperCase())
240
246
  )
241
247
  const name = names[0]
242
- let svgSource = content
248
+ /*
249
+ In a folder that is BOTH stroked and filled, a fill is a knockout (the renderer sets
250
+ it on the root) and an element's own `fill="none"` is what keeps a path OPEN: an arc,
251
+ an arrowhead. Every strip below removes fills, so such an element filled with the
252
+ knockout colour, and an open arc painted a lens over whatever it crossed
253
+ (tosijs-ui#249). Mark those elements first; the mark survives the strips and becomes
254
+ `fill="none"` again at the end. The root <svg> is not an element here: its fill is
255
+ the folder's to decide.
256
+ */
257
+ const keepsOpenPaths = dir.includes('stroked') && dir.includes('filled')
258
+ let svgSource = (
259
+ keepsOpenPaths
260
+ ? content.replace(/<(?!svg\b)(\w+)\b[^>]*>/g, (tag) =>
261
+ /\bfill="none"|\bfill\s*:\s*none/.test(tag)
262
+ ? tag
263
+ .replace(/\s?\bfill="none"/, '')
264
+ .replace(/\bfill\s*:\s*none\s*;?/, '')
265
+ .replace(/^<\w+/, (open) => `${open} ${OPEN_PATH_MARK}`)
266
+ : tag
267
+ )
268
+ : content
269
+ )
243
270
  .replace(/(<\?xml.*?>|<!DOCTYPE.*?>)\s?/g, '')
244
271
  .replace(/<svg.*?>/, (a) =>
245
272
  a.replace(
@@ -280,7 +307,7 @@ function findIcons(dirs, ignore = []) {
280
307
  out of the tinting path for an unrelated reason (tosijs-ui#30).
281
308
  */
282
309
  svgSource = svgSource.replace(
283
- /(fill|stroke)(?!-rule)(-\w+)?:[^;]+;?/g,
310
+ /(fill|stroke)(?!-rule)(-\w+)?:[^;"]+;?/g,
284
311
  ''
285
312
  )
286
313
  }
@@ -289,12 +316,20 @@ function findIcons(dirs, ignore = []) {
289
316
  // survives every strip. `clip-rule` likewise.
290
317
  if (dir.includes('stroked')) {
291
318
  classes.push('stroked')
292
- svgSource = svgSource.replace(/(fill)(?!-rule)(-\w+)?:[^;]+;?/g, '')
319
+ svgSource = svgSource.replace(/(fill)(?!-rule)(-\w+)?:[^;"]+;?/g, '')
293
320
  }
294
321
  if (dir.includes('filled')) {
295
322
  classes.push('filled')
296
- svgSource = svgSource.replace(/(stroke)(?!-rule)(-\w+)?:[^;]+;?/g, '')
323
+ svgSource = svgSource.replace(
324
+ /(stroke)(?!-rule)(-\w+)?:[^;"]+;?/g,
325
+ ''
326
+ )
327
+ }
328
+ if (keepsOpenPaths) {
329
+ svgSource = svgSource.replaceAll(OPEN_PATH_MARK, 'fill="none"')
297
330
  }
331
+ // A style that held only colours is empty now.
332
+ svgSource = svgSource.replace(/\sstyle=""/g, '')
298
333
  if (classes.length) {
299
334
  svgSource = svgSource.replace(
300
335
  /^<svg/,
package/dist/ab-test.js CHANGED
@@ -66,7 +66,7 @@ randomize()
66
66
  }
67
67
 
68
68
  .preview tosi-ab[not] p {
69
- background: red;
69
+ background: #c00;
70
70
  }
71
71
  ```
72
72
 
@@ -75,7 +75,7 @@ randomize()
75
75
  - `condition` attribute determines which value in `AbTest.conditions` controls the element
76
76
  - `not` reverses the condition (so `<tosi-ab not condition="foo">` will be visible if `conditions.foo` is `false`)
77
77
  */
78
- /*{ "parent": "Helper Libraries", "description": "<tosi-ab> shows or hides content by condition, so you can run A/B tests or feature flags by setting AbTest.conditions." }*/
78
+ /*{ "headTitle": "<tosi-ab> A/B tests and feature flags — tosijs-ui", "parent": "Helper Libraries", "description": "<tosi-ab> shows or hides content by condition, so you can run A/B tests or feature flags by setting AbTest.conditions." }*/
79
79
  import { withAttributes } from 'tosijs';
80
80
  const abTestConditions = {};
81
81
  export class AbTest extends withAttributes({
package/dist/affix.js CHANGED
@@ -142,7 +142,7 @@ and `describePlacement({ position, inside })` convert between the attribute and
142
142
  `{ position, inside }`, and `affixCell(placement)` gives the grid cell and alignment a
143
143
  placement takes.
144
144
  */
145
- /*{ "parent": "Components", "description": "<tosi-affix> attaches a toolbar, badge or caption to one edge of its content, laid out beside it rather than floated, so it never covers it." }*/
145
+ /*{ "headTitle": "<tosi-affix> edge toolbars and badges — tosijs-ui", "parent": "Components", "description": "<tosi-affix> attaches a toolbar, badge or caption to one edge of its content, laid out beside it rather than floated, so it never covers it." }*/
146
146
  import { Component, elements } from 'tosijs';
147
147
  const { slot } = elements;
148
148
  const POSITIONS = new Set([
@@ -36,7 +36,7 @@ export declare class TosiCarousel extends TosiCarousel_base {
36
36
  _carouseButtonWidth: number;
37
37
  _carouselDotCurrentColor: string;
38
38
  _carouselDotSize: number;
39
- _carouselDotSpacing: string;
39
+ _carouselDotSpacing: number;
40
40
  _carouselDotHitSize: number;
41
41
  _carouselProgressPadding: number;
42
42
  _carouselDotTransition: string;
@@ -127,6 +127,10 @@ export declare class TosiCarousel extends TosiCarousel_base {
127
127
  ':host .dot.current::before': {
128
128
  background: string;
129
129
  };
130
+ ':host::part(scroller):focus-visible': {
131
+ outline: string;
132
+ outlineOffset: string;
133
+ };
130
134
  ':host .dot:focus-visible::before': {
131
135
  boxShadow: string;
132
136
  };
@@ -140,6 +144,7 @@ export declare class TosiCarousel extends TosiCarousel_base {
140
144
  easing: (t: number) => number;
141
145
  indicateCurrent: () => void;
142
146
  snapPosition: () => void;
147
+ handleScrollerKey: (event: Event) => void;
143
148
  back: () => void;
144
149
  forward: () => void;
145
150
  handleDotClick: (event: Event) => void;
package/dist/carousel.js CHANGED
@@ -62,7 +62,9 @@ This is a minimalist carousel component that supports the usual stuff.
62
62
  would ever end it.
63
63
  - **The dots are tappable:** each draws a `--carousel-dot-size` dot (8px) but answers taps across
64
64
  its whole slot and `--carousel-dot-hit-size` (44px) tall, so a tap that just misses a dot
65
- doesn't open the slide underneath.
65
+ doesn't open the slide underneath. Dots sit 24px apart (`--carousel-dot-spacing`, 16px), the
66
+ spacing WCAG 2.5.8 asks of a target smaller than 24px.
67
+ - **The keyboard reaches the slides:** the slide strip takes focus, and ← and → page it.
66
68
  - **Resizing keeps the current slide in place** (rotation, a window resize) instead of leaving
67
69
  it partway into the next one.
68
70
 
@@ -173,7 +175,12 @@ export class TosiCarousel extends withAttributes({
173
175
  _carouseButtonWidth: 48,
174
176
  _carouselDotCurrentColor: '#0008',
175
177
  _carouselDotSize: 8,
176
- _carouselDotSpacing: vars.carouselDotSize,
178
+ /*
179
+ 16px of spacing puts the dots 24px apart: WCAG 2.5.8 takes a target under 24px as long
180
+ as a 24px circle on each one touches no neighbour, and at 16px apart they overlapped
181
+ (#3573).
182
+ */
183
+ _carouselDotSpacing: 16,
177
184
  // The tappable area around each dot; the dot itself stays --carousel-dot-size (#204).
178
185
  _carouselDotHitSize: 44,
179
186
  _carouselProgressPadding: 12,
@@ -276,6 +283,10 @@ export class TosiCarousel extends withAttributes({
276
283
  ':host .dot.current::before': {
277
284
  background: vars.carouselDotCurrentColor,
278
285
  },
286
+ ':host::part(scroller):focus-visible': {
287
+ outline: `2px solid ${vars.carouselButtonActiveColor}`,
288
+ outlineOffset: '-2px',
289
+ },
279
290
  ':host .dot:focus-visible::before': {
280
291
  boxShadow: `0 0 0 2px ${vars.carouselButtonActiveColor}`,
281
292
  },
@@ -311,6 +322,18 @@ export class TosiCarousel extends withAttributes({
311
322
  }
312
323
  this.lastAutoAdvance = Date.now();
313
324
  };
325
+ handleScrollerKey = (event) => {
326
+ if (event.target !== event.currentTarget)
327
+ return;
328
+ const { key } = event;
329
+ if (key === 'ArrowLeft')
330
+ this.back();
331
+ else if (key === 'ArrowRight')
332
+ this.forward();
333
+ else
334
+ return;
335
+ event.preventDefault();
336
+ };
314
337
  back = () => {
315
338
  this.page = this.page > 0 ? this.page - 1 : this.lastPage;
316
339
  };
@@ -359,7 +382,12 @@ export class TosiCarousel extends withAttributes({
359
382
  }
360
383
  }
361
384
  content = () => [
362
- div({ part: 'pager' }, button({ title: 'previous slide', part: 'back' }, icons.chevronLeft()), div({ title: 'slides', role: 'group', part: 'scroller' }, div({ part: 'grid' }, slot())), button({ title: 'next slide', part: 'forward' }, icons.chevronRight())),
385
+ div({ part: 'pager' }, button({ title: 'previous slide', part: 'back' }, icons.chevronLeft()), div(
386
+ /*
387
+ Focusable, so the strip can be scrolled from the keyboard (#3573); ← and → page it,
388
+ like the arrow buttons.
389
+ */
390
+ { title: 'slides', role: 'group', part: 'scroller', tabindex: 0 }, div({ part: 'grid' }, slot())), button({ title: 'next slide', part: 'forward' }, icons.chevronRight())),
363
391
  div({ title: 'choose slide to display', role: 'group', part: 'progress' }),
364
392
  ];
365
393
  connectedCallback() {
@@ -371,6 +399,7 @@ export class TosiCarousel extends withAttributes({
371
399
  back.addEventListener('click', this.back);
372
400
  forward.addEventListener('click', this.forward);
373
401
  scroller.addEventListener('scroll', this.indicateCurrent);
402
+ scroller.addEventListener('keydown', this.handleScrollerKey);
374
403
  progress.addEventListener('click', this.handleDotClick);
375
404
  this.addEventListener('pointerenter', this.handlePointerEnter);
376
405
  this.addEventListener('pointerleave', this.handlePointerLeave);
@@ -53,6 +53,8 @@ export interface CmOptions {
53
53
  onChange?: (value: string) => void;
54
54
  /** style-injection root — pass the host's ShadowRoot when mounted in shadow DOM */
55
55
  root?: Document | ShadowRoot;
56
+ /** the editable area's accessible name */
57
+ label?: string;
56
58
  }
57
59
  /** Create a CodeMirror editor mounted in `parent`, returning an editor-agnostic handle. */
58
60
  export declare function createCmEditor(parent: HTMLElement, opts?: CmOptions): CmHandle;
@@ -13,7 +13,7 @@ layered in separately and only when tjs-lang is installed.
13
13
  import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine, tooltips, } from '@codemirror/view';
14
14
  import { EditorState, Compartment, Annotation, Transaction, } from '@codemirror/state';
15
15
  import { indentWithTab, history, defaultKeymap, historyKeymap, undo, redo, undoDepth, redoDepth, } from '@codemirror/commands';
16
- import { indentUnit, foldGutter, foldKeymap, indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, } from '@codemirror/language';
16
+ import { indentUnit, foldGutter, foldKeymap, indentOnInput, syntaxHighlighting, defaultHighlightStyle, HighlightStyle, bracketMatching, } from '@codemirror/language';
17
17
  import { closeBrackets, closeBracketsKeymap, autocompletion, completionKeymap, } from '@codemirror/autocomplete';
18
18
  import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
19
19
  import { lintKeymap } from '@codemirror/lint';
@@ -44,9 +44,17 @@ const isDarkMode = () => typeof document !== 'undefined' &&
44
44
  * semantic to preserve — and tightening it after 1.7 is on `latest` would be breaking.
45
45
  */
46
46
  const programmatic = Annotation.define();
47
+ /*
48
+ CodeMirror's light style, with its one colour under WCAG AA's 4.5:1 darkened (#3572): type and
49
+ namespace names are #085, 4.43:1 on a near-white editor. #075 is 5.5:1 and reads the same.
50
+ */
51
+ const AA_COLORS = { '#085': '#075' };
52
+ const lightHighlightStyle = HighlightStyle.define(defaultHighlightStyle.specs.map((spec) => typeof spec.color === 'string' && AA_COLORS[spec.color]
53
+ ? { ...spec, color: AA_COLORS[spec.color] }
54
+ : spec));
47
55
  const highlightFor = (dark) => dark
48
56
  ? syntaxHighlighting(oneDarkHighlightStyle)
49
- : syntaxHighlighting(defaultHighlightStyle, { fallback: true });
57
+ : syntaxHighlighting(lightHighlightStyle, { fallback: true });
50
58
  /** Map a `<tosi-code mode>` value to a CodeMirror language extension. */
51
59
  export function languageForMode(mode) {
52
60
  switch (mode) {
@@ -209,6 +217,9 @@ export function createCmEditor(parent, opts = {}) {
209
217
  highlight.of(highlightFor(isDarkMode())),
210
218
  language.of(langExt),
211
219
  readonly.of(EditorState.readOnly.of(!!opts.readOnly)),
220
+ EditorView.contentAttributes.of({
221
+ 'aria-label': opts.label || 'code editor',
222
+ }),
212
223
  EditorView.updateListener.of((u) => {
213
224
  if (!u.docChanged || !opts.onChange)
214
225
  return;
@@ -409,6 +409,10 @@ export class CodeEditor extends withAttributes({
409
409
  mode: this.mode,
410
410
  readOnly: this.disabled,
411
411
  root: this.shadowRoot ?? undefined,
412
+ // The editable area is what a screen reader reaches; the host's name is its name.
413
+ label: this.getAttribute('aria-label') ||
414
+ this.title ||
415
+ (this.mode ? `${this.mode} code` : ''),
412
416
  onChange: (value) => this.dispatchEvent(new CustomEvent('change', { detail: { value } })),
413
417
  });
414
418
  this._handle = handle;
package/dist/crud.js CHANGED
@@ -304,7 +304,7 @@ for every instance; each one re-checks it the next time it is resized.
304
304
  - `change` — the selection or the saved record changed.
305
305
  - `error` — a store operation failed.
306
306
  */
307
- /*{ "parent": "Components", "description": "<tosi-crud> pairs a searchable table with a schema-driven form over a store you supply (REST, IndexedDB or an array) for list-and-edit screens." }*/
307
+ /*{ "headTitle": "<tosi-crud> list-and-edit screens — tosijs-ui", "parent": "Components", "description": "<tosi-crud> pairs a searchable table with a schema-driven form over a store you supply (REST, IndexedDB or an array) for list-and-edit screens." }*/
308
308
  import { elements, withAttributes } from 'tosijs';
309
309
  import { tosiTable } from './data-table.js';
310
310
  import { tosiSchemaForm } from './schema-form.js';