tosijs-ui 1.16.40 → 1.17.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/bin/make-icon-data.js +1 -1
  3. package/bin/release-notes.ts +1 -1
  4. package/dist/ab-test.js +1 -1
  5. package/dist/affix.js +1 -1
  6. package/dist/babylon-3d.js +1 -1
  7. package/dist/bodymovin-player.js +1 -1
  8. package/dist/carousel.js +1 -1
  9. package/dist/code-editor.js +1 -1
  10. package/dist/color-input.js +1 -1
  11. package/dist/crud.js +1 -1
  12. package/dist/data-table.js +1 -1
  13. package/dist/dialog.js +1 -1
  14. package/dist/diff.js +1 -1
  15. package/dist/doc-browser.d.ts +6 -0
  16. package/dist/doc-browser.js +11 -6
  17. package/dist/doc-system/book-html.d.ts +3 -1
  18. package/dist/doc-system/book-html.js +5 -2
  19. package/dist/doc-system/data-client.js +1 -1
  20. package/dist/doc-system/doc-system-styles.js +5 -1
  21. package/dist/doc-system/doc-system.js +5 -1
  22. package/dist/doc-system/highlight-styles.js +1 -1
  23. package/dist/doc-system/render.d.ts +19 -0
  24. package/dist/doc-system/render.js +145 -1
  25. package/dist/doc-system/site/a11y-audit-cli.d.ts +1 -0
  26. package/dist/doc-system/site/a11y-audit-cli.js +103 -0
  27. package/dist/doc-system/site/a11y-audit.d.ts +30 -0
  28. package/dist/doc-system/site/a11y-audit.js +71 -0
  29. package/dist/doc-system/site/a11y-report.d.ts +22 -0
  30. package/dist/doc-system/site/a11y-report.js +138 -0
  31. package/dist/doc-system/site/alt-text.d.ts +31 -0
  32. package/dist/doc-system/site/alt-text.js +55 -0
  33. package/dist/doc-system/site/docs.js +1 -1
  34. package/dist/doc-system/site/epub-a11y.d.ts +38 -0
  35. package/dist/doc-system/site/epub-a11y.js +119 -0
  36. package/dist/doc-system/site/epub.d.ts +11 -0
  37. package/dist/doc-system/site/epub.js +84 -19
  38. package/dist/doc-system/site/generate-site.d.ts +3 -1
  39. package/dist/doc-system/site/generate-site.js +33 -1
  40. package/dist/doc-system/site/index.d.ts +2 -0
  41. package/dist/doc-system/site/make-llms-txt.d.ts +1 -0
  42. package/dist/doc-system/site/make-llms-txt.js +5 -3
  43. package/dist/doc-system/site/orchestrator.js +70 -5
  44. package/dist/doc-system/site/site-config.d.ts +21 -0
  45. package/dist/drag-and-drop.js +2 -2
  46. package/dist/editable-rect.js +1 -1
  47. package/dist/filter-builder.js +1 -1
  48. package/dist/float.js +1 -1
  49. package/dist/form.js +1 -1
  50. package/dist/header.js +1 -1
  51. package/dist/highlight-block.js +1 -1
  52. package/dist/icon-svg.js +1 -1
  53. package/dist/icons.js +1 -1
  54. package/dist/iife.js +113 -111
  55. package/dist/iife.js.map +60 -60
  56. package/dist/layout.js +1 -1
  57. package/dist/live-example/component.js +1 -1
  58. package/dist/live-theme.js +1 -1
  59. package/dist/localize.js +3 -3
  60. package/dist/make-sorter.js +1 -1
  61. package/dist/mapbox.js +1 -1
  62. package/dist/markdown-viewer.js +1 -1
  63. package/dist/menu.js +1 -1
  64. package/dist/month.js +1 -1
  65. package/dist/notifications.js +1 -1
  66. package/dist/palette.d.ts +2 -0
  67. package/dist/palette.js +39 -7
  68. package/dist/password-strength.js +1 -1
  69. package/dist/rating.js +1 -1
  70. package/dist/rich-text.js +3 -1
  71. package/dist/router.js +1 -1
  72. package/dist/schema-form.js +1 -1
  73. package/dist/search-field.js +1 -1
  74. package/dist/segmented.js +1 -1
  75. package/dist/select.js +1 -1
  76. package/dist/side-nav.js +1 -1
  77. package/dist/size-break.js +1 -1
  78. package/dist/sizer.js +1 -1
  79. package/dist/tab-selector.js +1 -1
  80. package/dist/tag-list.js +1 -1
  81. package/dist/theme.js +1 -1
  82. package/dist/tooltip.js +1 -1
  83. package/dist/track-drag.js +1 -1
  84. package/dist/value-renderer.js +1 -1
  85. package/dist/version.d.ts +1 -1
  86. package/dist/version.js +1 -1
  87. package/dist/via-tag.js +1 -1
  88. package/llms.txt +64 -70
  89. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.17.0-beta.1
4
+
5
+ > Prerelease, published under the `beta` dist-tag: install with `tosijs-ui@beta`. `latest` stays
6
+ > on 1.16.41. It carries the accessibility half of 1.17.0; the SEO work and #245/#246 follow.
7
+
8
+ - **An opt-in accessibility mode for doc sites (#3521).** `accessibility: 'wcag22-aa'` in the
9
+ site config makes the build do what it can decide on its own and report what it cannot, on the
10
+ site, in print and in the ePub:
11
+ - an image alone in its paragraph, followed by an italic-only paragraph, renders as a
12
+ `<figure>` with that text as its `<figcaption>`;
13
+ - every page has exactly one `<h1>`, its title: a page's first heading is promoted when its
14
+ title came from it, a later `#` becomes `##`, and the rest shift to start at `##`;
15
+ - the build lists, by file and line, each image with no alt text and no caption, each heading
16
+ that skips a level, and each link named "here" or the like, and fails on them under `strict`.
17
+
18
+ Off by default; a site that does not set it renders exactly as before.
19
+ - **The mode audits the built site with axe.** A full build opens every page in Chromium and runs
20
+ axe-core's WCAG 2.2 AA rules, listing each failure by page, rule and element and failing under
21
+ `strict`. It needs `playwright` and `axe-core` in your project; without them the build says the
22
+ audit did not run rather than reporting a pass. Never under a dev server.
23
+ - **Accessible ePubs (#3522).** In the mode, the book's package document declares its access
24
+ modes, features, hazards and a summary (EPUB Accessibility 1.1), derived from the build: it
25
+ claims alternative text only when every image has alt text or a caption. Conformance to WCAG
26
+ 2.2 AA is claimed only when DAISY Ace, installed in your project, has checked the finished book
27
+ and passed it. `epub: { accessibility: { summary, hazards, certifiedBy } }` overrides the
28
+ defaults. Checked against real Ace: this site's own book passes.
29
+ - **Every ePub, mode or not: the right language, readable links, landmarks.** Each chapter, the
30
+ nav, the contents and the cover said `xml:lang="en"` whatever the book's language, and the
31
+ package said none, so a reading system pronounced a French book as English. Links were
32
+ `#0088cc` (3.9:1 on white) and marked by colour alone; they are now 6.6:1 and underlined.
33
+ The nav gains a landmarks list (cover, contents, start of the text) and its table of contents
34
+ `role="doc-toc"`.
35
+ - **`altText`: a hook that supplies alt text for undescribed images.** With the mode on, it is
36
+ called for each image the build would report, and its answer becomes that image's alt text in
37
+ the built site, `docs.json` and the ePub. It never writes a source file. It runs on every
38
+ build, so cache anything slow by `src`.
39
+ - **The doc-site search trigger's name contains its visible text (#3569).** Its `aria-label`
40
+ was "Search (⌘K)" beside visible text "Search ⌘K", which fails WCAG 2.5.3 (a voice-control user
41
+ says what they see) on every page of every doc site. A `<tosi-palette>` field trigger is now
42
+ named by its text, an icon trigger keeps an `aria-label`, and the shortcut is announced through
43
+ `aria-keyshortcuts`. The frame's axe test now runs every WCAG 2.2 A and AA rule, not four.
44
+ - **The header's site name is no longer a heading.** It was an `<h2>` above every page's own
45
+ `<h1>`, so it was each page's first heading. It is now `<span class="site-name">` and looks the
46
+ same; site CSS that styled `header h2` should target `header .site-name`.
47
+
48
+ ## 1.16.41
49
+
50
+ - **A `noindex` page is left out of `llms.txt`.** The sitemap and the markdown copies already
51
+ left it out, but `llms.txt` still listed it, so an agent reading the index was sent to pages
52
+ the author had asked not to be found.
53
+ - **Every page on ui.tosijs.net has a written description, and the terse titles say what the
54
+ component is.** 63 pages described themselves with whatever their first paragraph said ("A
55
+ mapboxgl wrapper.", sentences cut off at 155 characters), which is what search results, link
56
+ previews and `llms.txt` showed. Short nav labels such as "map" and "table" keep their place in
57
+ the nav and get a fuller `<title>` through `headTitle`. The kitchen sink and the two layout
58
+ demo pages are `noindex`. The practice behind the rewrite is proposed for the shared coding
59
+ practices (tosijs-coding-practices #3561).
60
+
3
61
  ## 1.16.40
4
62
 
5
63
  - **An example with no script runs under a strict CSP (#245, #246).** A pure-HTML example still
@@ -86,7 +86,7 @@ byte-identical files, so it never shows up as spurious diff noise.
86
86
  > your prettier config, which it now honours) — not the output.
87
87
  */
88
88
 
89
- /*{ "parent": "icons", "order": 30 }*/
89
+ /*{ "parent": "icons", "order": 30, "description": "A build tool that turns folders of SVG files into the icon-data module behind the tosijs-ui icon system, optimizing the paths as it goes." }*/
90
90
 
91
91
  import fs from 'fs'
92
92
  import path from 'path'
@@ -60,7 +60,7 @@ commit whose diff never touched the file it named. Cheap to detect, and the fail
60
60
  catches is expensive.
61
61
  */
62
62
 
63
- /*{ "parent": "Appendices" }*/
63
+ /*{ "parent": "Appendices", "description": "tosijs-release-notes builds CHANGELOG sections from tagged bullets in commit messages, and fails a release when one is left unwritten." }*/
64
64
 
65
65
  import { $ } from 'bun'
66
66
  import {
package/dist/ab-test.js CHANGED
@@ -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" }*/
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." }*/
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" }*/
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." }*/
146
146
  import { Component, elements } from 'tosijs';
147
147
  const { slot } = elements;
148
148
  const POSITIONS = new Set([
@@ -220,7 +220,7 @@ be used to load `.glb` files. Setting `src` calls this for you on connect.
220
220
 
221
221
  `<tosi-3d>.loadUI(options: B3dUIOptions)` loads babylonjs guis, which you can create programmatically or using the [babylonjs gui tool](https://gui.babylonjs.com/).
222
222
  */
223
- /*{ "parent": "Components" }*/
223
+ /*{ "parent": "Components", "headTitle": "<tosi-3d> Babylon.js wrapper (deprecated) — tosijs-ui", "description": "Deprecated: <tosi-3d> is a thin Babylon.js wrapper kept for compatibility. New 3D work should use the tosijs-3d package." }*/
224
224
  import { elements, withAttributes } from 'tosijs';
225
225
  import { scriptTag } from './via-tag.js';
226
226
  import { icons, svg2DataUrl } from './icons.js';
@@ -101,7 +101,7 @@ And of course just access the element's `animation` property to [use the bodymov
101
101
 
102
102
  Also see the [documentation for advanced interactions](https://lottiefiles.github.io/lottie-docs/advanced_interactions/)
103
103
  */
104
- /*{ "parent": "Components" }*/
104
+ /*{ "parent": "Components", "headTitle": "<tosi-lottie> Lottie animation player — tosijs-ui", "description": "<tosi-lottie> plays Lottie (bodymovin) animations like an <img>: set src or json, and use the bodymovin API through its animation property." }*/
105
105
  import { withAttributes } from 'tosijs';
106
106
  import { scriptTag } from './via-tag.js';
107
107
  export class BodymovinPlayer extends withAttributes({
package/dist/carousel.js CHANGED
@@ -68,7 +68,7 @@ This is a minimalist carousel component that supports the usual stuff.
68
68
 
69
69
  <tosi-css-var-editor element-selector="tosi-carousel"></tosi-css-var-editor>
70
70
  */
71
- /*{ "parent": "Components" }*/
71
+ /*{ "parent": "Components", "description": "A minimal carousel with optional arrows and dots, several items per page, looping, and auto-advance that holds still while someone is looking." }*/
72
72
  import { elements, vars, withAttributes } from 'tosijs';
73
73
  import { icons } from './icons.js';
74
74
  import { prefersReducedMotion } from './reduced-motion.js';
@@ -111,7 +111,7 @@ Only `@codemirror/state` and `@codemirror/view` are re-exported — the two you
111
111
  view. The language, lint and search packages are internal composition details; ask if you need
112
112
  one exposed.
113
113
  */
114
- /*{ "parent": "Components" }*/
114
+ /*{ "parent": "Components", "headTitle": "<tosi-code> CodeMirror 6 code editor — tosijs-ui", "description": "<tosi-code> embeds a CodeMirror 6 editor for JavaScript, TypeScript, CSS, HTML and Markdown, with undo, a diff view and a change event." }*/
115
115
  import { elements, varDefault, withAttributes, } from 'tosijs';
116
116
  import { tosiDiff } from './diff.js';
117
117
  const { div } = elements;
@@ -27,7 +27,7 @@ colorInput.addEventListener('change', () => {
27
27
 
28
28
  <tosi-css-var-editor element-selector="tosi-color"></tosi-css-var-editor>
29
29
  */
30
- /*{ "parent": "Form Components" }*/
30
+ /*{ "parent": "Form Components", "description": "<tosi-color> is a colour input that also edits opacity, so it can produce translucent colours that the native colour picker cannot." }*/
31
31
  import { Component, elements, Color, vars, } from 'tosijs';
32
32
  const { input } = elements;
33
33
  const defaultColor = Color.fromCss('#8888');
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" }*/
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." }*/
308
308
  import { elements, withAttributes } from 'tosijs';
309
309
  import { tosiTable } from './data-table.js';
310
310
  import { tosiSchemaForm } from './schema-form.js';
@@ -1241,7 +1241,7 @@ As well as any column names you want localized — these are also used as the ac
1241
1241
  (`aria-label`) of each editable cell, so an editable table wants them present even if you are
1242
1242
  happy with the untranslated headers.
1243
1243
  */
1244
- /*{ "parent": "Components" }*/
1244
+ /*{ "parent": "Components", "headTitle": "<tosi-table> virtual data table — tosijs-ui", "description": "A virtual data table for very large datasets: sortable, resizable, pinnable columns inferred from the data, keyboard navigation and row detail." }*/
1245
1245
  import { elements, vars, varDefault, tosiValue, getListItem, getListBinding, tosi, withAttributes, } from 'tosijs';
1246
1246
  import { trackDrag } from './track-drag.js';
1247
1247
  import { naturalSorter } from './natural-compare.js';
package/dist/dialog.js CHANGED
@@ -143,7 +143,7 @@ preview.append(
143
143
  ```
144
144
 
145
145
  */
146
- /*{ "parent": "Components" }*/
146
+ /*{ "parent": "Components", "description": "<tosi-dialog> wraps the native <dialog> element with an OK button, a footer slot, and promise-based alert, confirm and prompt helpers." }*/
147
147
  import { Component, elements, on, vars, varDefault } from 'tosijs';
148
148
  import { findHighestZ } from './track-drag.js';
149
149
  const { dialog, button, header, footer, tosiSlot, h3, p, label, input, div } = elements;
package/dist/diff.js CHANGED
@@ -101,7 +101,7 @@ and writable if you want to drive it yourself. Resolutions **reset when either t
101
101
  changes** — decisions belong to the diff they were made about.
102
102
 
103
103
  */
104
- /*{ "parent": "Components" }*/
104
+ /*{ "parent": "Components", "description": "<tosi-diff> shows a unified, line-by-line diff between two strings, with removed and added lines marked, as in a code review." }*/
105
105
  import { elements, vars, varDefault, withAttributes, } from 'tosijs';
106
106
  const { div, span, button } = elements;
107
107
  /** Group a line diff into context runs and change blocks, in display order. */
@@ -1,3 +1,4 @@
1
+ import { type AccessibilityMode } from './doc-system/render.js';
1
2
  import { TestResults } from './live-example/test-harness.js';
2
3
  import { type ExamplePolicy } from './doc-system/example-policy.js';
3
4
  export declare const hasTestBlock: (text: string | undefined, policy?: ExamplePolicy) => boolean;
@@ -123,6 +124,11 @@ export interface DocBrowserOptions {
123
124
  * strings, like every other piece of the doc system's own text.
124
125
  */
125
126
  searchPlaceholder?: string;
127
+ /**
128
+ * `SiteConfig.accessibility` (#3521): render figures with captions and exactly one `<h1>`
129
+ * per page, as the static build does, so a hydrated page matches it.
130
+ */
131
+ accessibility?: AccessibilityMode;
126
132
  /**
127
133
  * Header-bar links. When provided, these replace the legacy `projectLinks` icon
128
134
  * set in the header (each renders as an icon if `icon` names a known icon, else
@@ -98,12 +98,12 @@ The search opens on Cmd-K (Ctrl-K off a Mac) anywhere on the page. A host that o
98
98
  or embeds more than one browser, passes `searchShortcut: ''` (or another key in
99
99
  `<tosi-palette>`'s `shortcut` form).
100
100
  */
101
- /*{ "parent": "Documentation System", "order": 50 }*/
101
+ /*{ "parent": "Documentation System", "order": 50, "description": "createDocBrowser() renders a documentation corpus as a searchable browser with live, self-testing examples: a lighter alternative to Storybook." }*/
102
102
  import { elements, vars, varDefault, bindings, getListItem, tosi, StyleSheet, } from 'tosijs';
103
103
  import { buildSlugMap, pathForSlug, filenameForPath, homeDoc, wikilinkTargets, } from './doc-system/routing.js';
104
104
  import { buildNavTree } from './doc-system/nav-tree.js';
105
105
  import { navSectionStyleSpec } from './doc-system/nav-styles.js';
106
- import { renderDocMarkdown } from './doc-system/render.js';
106
+ import { renderDocMarkdown, } from './doc-system/render.js';
107
107
  import { pageTitle } from './doc-system/doc-title.js';
108
108
  import { unsettledExamples, tallyExamples, closePage, } from './doc-system/test-completion.js';
109
109
  import { LiveExample, testManager, pageTestCount, enableTests, disableTests, } from './live-example.js';
@@ -149,7 +149,7 @@ export const hasTestBlock = (text, policy = 'auto') => typeof text === 'string'
149
149
  // Cheap reject first: lexing every doc in a large corpus is not free.
150
150
  text.includes('test') &&
151
151
  collectCodeTokens(text).some((t) => t.lang === 'test' && isLiveFence('test', t.mode, policy));
152
- const { div, span, a, header, button, template, h2, img, ul, li } = elements;
152
+ const { div, span, a, header, button, template, img, ul, li } = elements;
153
153
  /*
154
154
  Test colors, and the text color that goes ON each — because contrast is a PAIR.
155
155
 
@@ -395,7 +395,7 @@ let docBrowserSeq = 0;
395
395
  // Likewise for the nav's aria-controls ids.
396
396
  let navIdCounter = 0;
397
397
  export function createDocBrowser(options) {
398
- const { docs, context = {}, projectName = '', projectLinks = {}, logo, navSize = 200, minSize = 600, routing = 'query', initialRoute, aliases = {}, onRouteChange, searchShortcut = 'mod+k', searchPlaceholder = 'Search', navbarLinks, contentElement, } = options;
398
+ const { docs, context = {}, projectName = '', projectLinks = {}, logo, navSize = 200, minSize = 600, routing = 'query', initialRoute, aliases = {}, onRouteChange, searchShortcut = 'mod+k', searchPlaceholder = 'Search', accessibility, navbarLinks, contentElement, } = options;
399
399
  // Memory routing is fully self-contained: it never reads or writes
400
400
  // window.history/location, so an embedded or nested browser can't hijack the
401
401
  // host page's URL (or recurse into it).
@@ -906,7 +906,10 @@ export function createDocBrowser(options) {
906
906
  alignItems: 'center',
907
907
  borderBottom: 'none',
908
908
  },
909
- }, logoMark(logo, projectLinks), h2(projectName)));
909
+ }, logoMark(logo, projectLinks),
910
+ // Not a heading: the site name above every page made it each page's first heading,
911
+ // an <h2> ahead of the page's own <h1> (#3521).
912
+ span({ class: 'site-name' }, projectName)));
910
913
  }
911
914
  headerContent.push(span({ class: 'elastic' }), searchBox);
912
915
  // A header link renders as an icon (if `icon` names a known icon) or its label.
@@ -1054,6 +1057,8 @@ export function createDocBrowser(options) {
1054
1057
  docContent.innerHTML = renderDocMarkdown(doc.text, {
1055
1058
  bakes: doc.bakes ? new Map(doc.bakes) : undefined,
1056
1059
  wikilinks,
1060
+ accessible: !!accessibility,
1061
+ title: doc.title,
1057
1062
  });
1058
1063
  }
1059
1064
  /*
@@ -1345,7 +1350,7 @@ export function createDocBrowser(options) {
1345
1350
  return;
1346
1351
  editUI.view = view;
1347
1352
  if (view === 'preview') {
1348
- docContent.innerHTML = renderDocMarkdown(docMarkdownFromSource(editUI.editor.value, editUI.doc.path), { wikilinks });
1353
+ docContent.innerHTML = renderDocMarkdown(docMarkdownFromSource(editUI.editor.value, editUI.doc.path), { wikilinks, accessible: !!accessibility, title: editUI.doc.title });
1349
1354
  rewriteContentLinks();
1350
1355
  LiveExample.insertExamples(docContent, context, editUI.doc.path || undefined);
1351
1356
  focusableCodeBlocks(docContent);
@@ -8,7 +8,7 @@ export interface BookDoc {
8
8
  parent?: string;
9
9
  hidden?: boolean;
10
10
  }
11
- export declare const DEFAULT_BOOK_CSS = "/* doc-system book stylesheet */\nhtml { font-size: 100%; }\nbody {\n font-family: Georgia, 'Times New Roman', serif;\n line-height: 1.5;\n margin: 0 1em;\n color: #1a1a1a;\n}\nh1, h2, h3, h4, h5, h6 {\n font-family: -apple-system, 'Helvetica Neue', Arial, sans-serif;\n line-height: 1.2;\n margin: 1.4em 0 0.5em;\n}\nh1 { font-size: 1.8em; }\nh2 { font-size: 1.4em; }\nh3 { font-size: 1.2em; }\np { margin: 0.6em 0; }\na { color: #08c; text-decoration: none; }\nul, ol { margin: 0.6em 0; padding-left: 1.4em; }\nblockquote {\n margin: 0.8em 0;\n padding: 0 0 0 1em;\n border-left: 3px solid #ccc;\n color: #555;\n}\nimg { max-width: 100%; height: auto; }\ntable { border-collapse: collapse; margin: 0.8em 0; font-size: 0.85em; }\nth, td { border: 1px solid #ccc; padding: 0.3em 0.6em; text-align: left; }\ncode, pre {\n font-family: 'SF Mono', Menlo, Consolas, monospace;\n font-size: 0.8em;\n}\n:not(pre) > code {\n background: #f3f3f3;\n padding: 0.1em 0.3em;\n border-radius: 3px;\n}\npre {\n background: #f6f8fa;\n border: 1px solid #e1e4e8;\n border-radius: 4px;\n padding: 0.7em 0.9em;\n margin: 0.8em 0;\n white-space: pre-wrap;\n word-break: break-word;\n overflow-wrap: anywhere;\n}\npre code { background: none; padding: 0; }\n/*\nPrism token colours for the book \u2014 a LIGHT palette, deliberately distinct from the site's.\n\nThe site has TWO palettes, one per mode. A book has one background and it is light: the\npre rule above is #f6f8fa, an ePub reader may impose white or sepia, and paper is paper.\nReusing the site palette here would be light-on-light \u2014 invisible body text dressed as syntax\nhighlighting, which is the exact failure a nested diff hit in tosijs-ui#143.\n\nContrast-checked against #f6f8fa; the comment green is the weakest at ~4.6:1, still above\nWCAG AA for body text. Colours are literal rather than custom properties because this\nstylesheet ships inside an ePub, where custom properties are not reliably supported.\n\nNO BACKTICKS in this comment: it lives inside a JS template literal, so one would end the\nstring and drop the rest of the stylesheet into code. (It did. Same shape as a block comment\nending a doc comment early \u2014 punctuation that means something to the container.)\n*/\n.token.comment, .token.prolog, .token.cdata { color: #5c6f5c; font-style: italic; }\n.token.punctuation { color: #666; }\n.token.string, .token.char, .token.attr-value, .token.regex { color: #a03030; }\n.token.number, .token.boolean, .token.constant { color: #0b7285; }\n.token.keyword, .token.important, .token.atrule { color: #0a5bb5; }\n.token.function, .token.class-name { color: #7a4b00; }\n.token.operator, .token.entity, .token.url { color: #444; }\n.token.tag, .token.selector, .token.builtin { color: #0a6b52; }\n.token.attr-name, .token.property { color: #2a5db0; }\n.token.deleted { color: #b02020; }\n.token.inserted { color: #2a6b2a; }\n.token.bold { font-weight: bold; }\n.token.italic { font-style: italic; }\n/* the in-flow Contents page (ePub spine TOC) */\n.toc-title { text-align: center; }\nol.toc, ol.toc ol { list-style: none; padding-left: 0; }\nol.toc ol { padding-left: 1.2em; }\nol.toc li { margin: 0.3em 0; }\nol.toc > li > a { font-weight: bold; }\n/* per-example \"run this live\" deep link */\n.example-live-link { margin: 0.6em 0 0; font-size: 0.85em; }\n.example-live-link a { color: #08c; font-weight: bold; }\n/* footnotes / endnotes + wikilinks */\nsup.footnote-ref { font-size: 0.7em; line-height: 0; }\nsup.footnote-ref a { text-decoration: none; }\n.footnotes { margin-top: 2em; font-size: 0.9em; }\n.footnotes hr { border: none; border-top: 1px solid #ccc; margin: 1em 0; }\n.footnote-backref { text-decoration: none; }\na.wikilink { text-decoration: none; border-bottom: 1px dotted currentColor; }\n";
11
+ export declare const DEFAULT_BOOK_CSS = "/* doc-system book stylesheet */\nhtml { font-size: 100%; }\nbody {\n font-family: Georgia, 'Times New Roman', serif;\n line-height: 1.5;\n margin: 0 1em;\n color: #1a1a1a;\n}\nh1, h2, h3, h4, h5, h6 {\n font-family: -apple-system, 'Helvetica Neue', Arial, sans-serif;\n line-height: 1.2;\n margin: 1.4em 0 0.5em;\n}\nh1 { font-size: 1.8em; }\nh2 { font-size: 1.4em; }\nh3 { font-size: 1.2em; }\np { margin: 0.6em 0; }\n/* 6.6:1 on white, and underlined: colour alone does not mark a link in running text (#3522, DAISY Ace) */\na { color: #0a5bb5; text-decoration: underline; text-underline-offset: 0.15em; }\nul, ol { margin: 0.6em 0; padding-left: 1.4em; }\nblockquote {\n margin: 0.8em 0;\n padding: 0 0 0 1em;\n border-left: 3px solid #ccc;\n color: #555;\n}\nimg { max-width: 100%; height: auto; }\ntable { border-collapse: collapse; margin: 0.8em 0; font-size: 0.85em; }\nth, td { border: 1px solid #ccc; padding: 0.3em 0.6em; text-align: left; }\ncode, pre {\n font-family: 'SF Mono', Menlo, Consolas, monospace;\n font-size: 0.8em;\n}\n:not(pre) > code {\n background: #f3f3f3;\n padding: 0.1em 0.3em;\n border-radius: 3px;\n}\npre {\n background: #f6f8fa;\n border: 1px solid #e1e4e8;\n border-radius: 4px;\n padding: 0.7em 0.9em;\n margin: 0.8em 0;\n white-space: pre-wrap;\n word-break: break-word;\n overflow-wrap: anywhere;\n}\npre code { background: none; padding: 0; }\n/*\nPrism token colours for the book \u2014 a LIGHT palette, deliberately distinct from the site's.\n\nThe site has TWO palettes, one per mode. A book has one background and it is light: the\npre rule above is #f6f8fa, an ePub reader may impose white or sepia, and paper is paper.\nReusing the site palette here would be light-on-light \u2014 invisible body text dressed as syntax\nhighlighting, which is the exact failure a nested diff hit in tosijs-ui#143.\n\nContrast-checked against #f6f8fa; the comment green is the weakest at ~4.6:1, still above\nWCAG AA for body text. Colours are literal rather than custom properties because this\nstylesheet ships inside an ePub, where custom properties are not reliably supported.\n\nNO BACKTICKS in this comment: it lives inside a JS template literal, so one would end the\nstring and drop the rest of the stylesheet into code. (It did. Same shape as a block comment\nending a doc comment early \u2014 punctuation that means something to the container.)\n*/\n.token.comment, .token.prolog, .token.cdata { color: #5c6f5c; font-style: italic; }\n.token.punctuation { color: #666; }\n.token.string, .token.char, .token.attr-value, .token.regex { color: #a03030; }\n.token.number, .token.boolean, .token.constant { color: #0b7285; }\n.token.keyword, .token.important, .token.atrule { color: #0a5bb5; }\n.token.function, .token.class-name { color: #7a4b00; }\n.token.operator, .token.entity, .token.url { color: #444; }\n.token.tag, .token.selector, .token.builtin { color: #0a6b52; }\n.token.attr-name, .token.property { color: #2a5db0; }\n.token.deleted { color: #b02020; }\n.token.inserted { color: #2a6b2a; }\n.token.bold { font-weight: bold; }\n.token.italic { font-style: italic; }\n/* the in-flow Contents page (ePub spine TOC) */\n.toc-title { text-align: center; }\nol.toc, ol.toc ol { list-style: none; padding-left: 0; }\nol.toc ol { padding-left: 1.2em; }\nol.toc li { margin: 0.3em 0; }\nol.toc > li > a { font-weight: bold; }\n/* per-example \"run this live\" deep link */\n.example-live-link { margin: 0.6em 0 0; font-size: 0.85em; }\n.example-live-link a { color: #0a5bb5; font-weight: bold; }\n/* footnotes / endnotes + wikilinks */\nsup.footnote-ref { font-size: 0.7em; line-height: 0; }\nsup.footnote-ref a { text-decoration: none; }\n.footnotes { margin-top: 2em; font-size: 0.9em; }\n.footnotes hr { border: none; border-top: 1px solid #ccc; margin: 1em 0; }\n.footnote-backref { text-decoration: none; }\na.wikilink { text-decoration: none; border-bottom: 1px dotted currentColor; }\n";
12
12
  export declare const PRINT_CSS = "\n@page { margin: 18mm 16mm; }\n.book-cover { display: block; margin: 0 auto; max-width: 100%; max-height: 100vh; page-break-after: always; break-inside: avoid; }\n.book-title { text-align: center; margin: 30vh 0 1em; }\n.book-toc { page-break-after: always; }\n.book-toc h2 { page-break-before: avoid; }\n.book-toc ol { list-style: none; padding-left: 0; }\n.book-toc ol ol { padding-left: 1.2em; }\n.book-toc a { color: inherit; }\n.chapter { page-break-before: always; }\n";
13
13
  export declare function escapeHtml(s: string): string;
14
14
  export declare function slugify(s: string): string;
@@ -27,6 +27,8 @@ export interface BookHtmlOptions {
27
27
  /** full stylesheet (defaults to DEFAULT_BOOK_CSS + PRINT_CSS) */
28
28
  css?: string;
29
29
  lang?: string;
30
+ /** `SiteConfig.accessibility`: figures and one `<h1>` per chapter (#3521) */
31
+ accessible?: boolean;
30
32
  /** inject a script that opens the print dialog once the page has loaded */
31
33
  autoPrint?: boolean;
32
34
  }
@@ -28,7 +28,8 @@ h1 { font-size: 1.8em; }
28
28
  h2 { font-size: 1.4em; }
29
29
  h3 { font-size: 1.2em; }
30
30
  p { margin: 0.6em 0; }
31
- a { color: #08c; text-decoration: none; }
31
+ /* 6.6:1 on white, and underlined: colour alone does not mark a link in running text (#3522, DAISY Ace) */
32
+ a { color: #0a5bb5; text-decoration: underline; text-underline-offset: 0.15em; }
32
33
  ul, ol { margin: 0.6em 0; padding-left: 1.4em; }
33
34
  blockquote {
34
35
  margin: 0.8em 0;
@@ -96,7 +97,7 @@ ol.toc li { margin: 0.3em 0; }
96
97
  ol.toc > li > a { font-weight: bold; }
97
98
  /* per-example "run this live" deep link */
98
99
  .example-live-link { margin: 0.6em 0 0; font-size: 0.85em; }
99
- .example-live-link a { color: #08c; font-weight: bold; }
100
+ .example-live-link a { color: #0a5bb5; font-weight: bold; }
100
101
  /* footnotes / endnotes + wikilinks */
101
102
  sup.footnote-ref { font-size: 0.7em; line-height: 0; }
102
103
  sup.footnote-ref a { text-decoration: none; }
@@ -155,6 +156,8 @@ export function buildBookHtml(docs, opts) {
155
156
  const chapters = flatten(roots)
156
157
  .map((node) => `<section class="chapter" id="${slugify(node.doc.filename)}">\n${renderDocMarkdown(stripDocMeta(bookTextOf(node.doc)), {
157
158
  wikilinks,
159
+ accessible: opts.accessible,
160
+ title: node.doc.title,
158
161
  })}\n</section>`)
159
162
  .join('\n');
160
163
  return `<!DOCTYPE html>
@@ -1,4 +1,4 @@
1
- /*{ "parent": "Documentation System", "order": 85 }*/
1
+ /*{ "parent": "Documentation System", "order": 85, "description": "Open and save a project's data files from a dev-server page, for in-browser tools such as level or scene editors, optionally committing each save." }*/
2
2
  /*#
3
3
  # data client
4
4
 
@@ -393,9 +393,13 @@ export function docSystemStyleSpec(theme = {}) {
393
393
  margin: `${vars.spacing} 0 ${vars.spacing200}`,
394
394
  padding: 0,
395
395
  },
396
- 'header h2': {
396
+ 'header .site-name': {
397
397
  color: vars.brandTextColor,
398
398
  whiteSpace: 'nowrap',
399
+ fontWeight: '700',
400
+ fontSize: 'calc(var(--font-size) * var(--h2-scale))',
401
+ lineHeight: 'calc(var(--line-height) * var(--h2-scale))',
402
+ margin: 'calc(var(--spacing) * var(--h2-scale)) 0',
399
403
  },
400
404
  h2: {
401
405
  color: vars.brandColor,
@@ -55,7 +55,7 @@ A self-contained, controllable embed (e.g. docs in a floating panel):
55
55
  </tosi-doc-system>
56
56
  ```
57
57
  */
58
- /*{ "parent": "Documentation System", "order": 40 }*/
58
+ /*{ "parent": "Documentation System", "order": 40, "description": "<tosi-doc-system> hydrates pre-rendered static doc pages into a fast single-page doc browser, keeping full HTML for search engines and no-JS readers." }*/
59
59
  import { StyleSheet, elements, tosi, vars, withAttributes, } from 'tosijs';
60
60
  import * as tosijsModule from 'tosijs';
61
61
  import { createDocBrowser, } from '../doc-browser.js';
@@ -332,11 +332,13 @@ export class TosiDocSystem extends withAttributes({
332
332
  let projectName = '';
333
333
  let bookVolumes;
334
334
  let ebooks;
335
+ let accessible = false;
335
336
  try {
336
337
  const parsed = JSON.parse(this.config || '{}');
337
338
  projectName = parsed.projectName || '';
338
339
  bookVolumes = parsed.bookVolumes || undefined;
339
340
  ebooks = parsed.ebooks;
341
+ accessible = !!parsed.accessibility;
340
342
  }
341
343
  catch {
342
344
  // ignore — fall through to document.title
@@ -372,6 +374,7 @@ export class TosiDocSystem extends withAttributes({
372
374
  title: bookTitle,
373
375
  cover: bookCover,
374
376
  autoPrint: false,
377
+ accessible,
375
378
  }));
376
379
  win.document.close();
377
380
  /*
@@ -609,6 +612,7 @@ export class TosiDocSystem extends withAttributes({
609
612
  logo: config.logo,
610
613
  navbarLinks: navbarLinks.length ? navbarLinks : undefined,
611
614
  searchPlaceholder: config.searchPlaceholder,
615
+ accessibility: config.accessibility,
612
616
  contentElement,
613
617
  });
614
618
  // Add the settings (theme + language) menu — except in a nested demo, where
@@ -1,4 +1,4 @@
1
- /*{ "parent": "Documentation System", "order": 80 }*/
1
+ /*{ "parent": "Documentation System", "order": 80, "description": "The syntax-highlighting token colours as a stylesheet you can import and retheme, for code highlighted outside <tosi-highlight>." }*/
2
2
  /*#
3
3
  # highlight-styles
4
4
 
@@ -3,12 +3,31 @@ export interface ExampleBake {
3
3
  js: string;
4
4
  }
5
5
  export type ExampleBakes = Map<string, ExampleBake>;
6
+ /** `SiteConfig.accessibility`. One level for now; the name says which standard it targets. */
7
+ export type AccessibilityMode = 'wcag22-aa';
8
+ /**
9
+ * The accessibility mode's token rewrites (#3521), exported so the build's report reads exactly
10
+ * the structure the page renders.
11
+ */
12
+ export declare function accessibleTokens(tokens: any[], title?: string): any[];
6
13
  export declare function unwrapLoneCustomElements(html: string): string;
7
14
  export declare function renderDocMarkdown(text: string, opts?: {
8
15
  bakes?: ExampleBakes;
9
16
  /** the corpus's `wikilinkTargets`; without it a `[[target]]` is slugified as written */
10
17
  wikilinks?: Map<string, string>;
18
+ /**
19
+ * The accessibility mode (#3521): figures with captions, and exactly one `<h1>`. `title` is
20
+ * the page title, rendered as that `<h1>` when the markdown has none.
21
+ */
22
+ accessible?: boolean;
23
+ title?: string;
11
24
  }): string;
25
+ /**
26
+ * A doc's tokens as the accessibility mode renders them, for the build's report: the same
27
+ * lexer and the same rewrites as `renderDocMarkdown`, so the report and the page cannot
28
+ * disagree about what is a figure or which heading is the `<h1>`.
29
+ */
30
+ export declare function accessibleDocTokens(text: string, title?: string): any[];
12
31
  /**
13
32
  * Derive a clean <meta name="description"> from a doc's first prose paragraph.
14
33
  * Skips the title heading, code fences, tables, quotes, html and image/link-only
@@ -10,13 +10,131 @@ give that example a stable anchor — see the docMarked renderer below.
10
10
  */
11
11
  import { Marked, Renderer } from 'marked';
12
12
  import { parseFenceInfo } from './example-policy.js';
13
- import { HeadingIds, headingSlug } from './heading-id.js';
13
+ import { HeadingIds, headingSlug, headingText } from './heading-id.js';
14
14
  import { pathForSlug, slugify } from './routing.js';
15
15
  const baseRenderer = new Renderer();
16
16
  let currentBakes;
17
17
  let currentWikilinks;
18
18
  // Per-parse, reset in `preprocess` below (same pattern as the footnote state).
19
19
  let headingIds = new HeadingIds();
20
+ let currentA11y;
21
+ const isBlank = (t) => (t.type === 'text' && !String(t.raw).trim()) || t.type === 'br';
22
+ /** The image token of a paragraph whose only content is one image, else undefined. */
23
+ function loneImage(tokens) {
24
+ const content = tokens.filter((t) => !isBlank(t));
25
+ return content.length === 1 && content[0].type === 'image'
26
+ ? content[0]
27
+ : undefined;
28
+ }
29
+ /** The em token of a paragraph whose only content is italic text, else undefined. */
30
+ function loneEmphasis(tokens) {
31
+ const content = tokens.filter((t) => !isBlank(t));
32
+ return content.length === 1 && content[0].type === 'em'
33
+ ? content[0]
34
+ : undefined;
35
+ }
36
+ function figureFrom(paragraph) {
37
+ // `![…](…)` and `*caption*` on consecutive lines are one paragraph to marked.
38
+ const content = paragraph.tokens.filter((t) => !isBlank(t));
39
+ if (content.length === 2 &&
40
+ content[0].type === 'image' &&
41
+ content[1].type === 'em')
42
+ return {
43
+ type: 'figure',
44
+ raw: paragraph.raw,
45
+ image: content[0],
46
+ caption: content[1],
47
+ };
48
+ return undefined;
49
+ }
50
+ function withFigures(tokens) {
51
+ const out = [];
52
+ for (let i = 0; i < tokens.length; i++) {
53
+ const token = tokens[i];
54
+ if (token.type === 'blockquote' && Array.isArray(token.tokens)) {
55
+ token.tokens = withFigures(token.tokens);
56
+ }
57
+ if (token.type !== 'paragraph') {
58
+ out.push(token);
59
+ continue;
60
+ }
61
+ const joined = figureFrom(token);
62
+ if (joined) {
63
+ out.push(joined);
64
+ continue;
65
+ }
66
+ const image = loneImage(token.tokens);
67
+ let next = i + 1;
68
+ while (tokens[next]?.type === 'space')
69
+ next += 1;
70
+ const caption = image &&
71
+ tokens[next]?.type === 'paragraph' &&
72
+ loneEmphasis(tokens[next].tokens);
73
+ if (caption) {
74
+ out.push({
75
+ type: 'figure',
76
+ raw: tokens
77
+ .slice(i, next + 1)
78
+ .map((t) => t.raw)
79
+ .join(''),
80
+ image,
81
+ caption,
82
+ });
83
+ i = next;
84
+ continue;
85
+ }
86
+ out.push(token);
87
+ }
88
+ return out;
89
+ }
90
+ function withOneTitle(tokens, title) {
91
+ const headings = tokens.filter((t) => t.type === 'heading');
92
+ /*
93
+ A page with no `#` takes its title from its first heading, whatever its level, so that
94
+ heading IS the title: promote it rather than print the same words twice.
95
+ */
96
+ const lead = headings[0];
97
+ const first = headings.find((t) => t.depth === 1) ??
98
+ (lead && title && headingText(lead).trim() === title.trim()
99
+ ? lead
100
+ : undefined);
101
+ // The shift comes from the sub-headings as written; a demoted `#` is not one of them.
102
+ const below = headings
103
+ .filter((h) => h !== first && h.depth > 1)
104
+ .map((h) => h.depth);
105
+ const shift = below.length ? Math.min(...below) - 2 : 0;
106
+ if (first)
107
+ first.depth = 1;
108
+ for (const h of headings) {
109
+ if (h === first)
110
+ continue;
111
+ if (h.depth === 1)
112
+ h.depth = 2;
113
+ else if (shift > 0)
114
+ h.depth -= shift;
115
+ }
116
+ if (!first && title)
117
+ return [
118
+ {
119
+ type: 'heading',
120
+ raw: '',
121
+ depth: 1,
122
+ text: title,
123
+ tokens: [
124
+ { type: 'text', raw: title, text: escapeHtml(title), escaped: true },
125
+ ],
126
+ },
127
+ ...tokens,
128
+ ];
129
+ return tokens;
130
+ }
131
+ /**
132
+ * The accessibility mode's token rewrites (#3521), exported so the build's report reads exactly
133
+ * the structure the page renders.
134
+ */
135
+ export function accessibleTokens(tokens, title) {
136
+ return withOneTitle(withFigures(tokens), title);
137
+ }
20
138
  const docMarked = new Marked();
21
139
  docMarked.use({
22
140
  renderer: {
@@ -86,6 +204,13 @@ let footnoteDefs = new Map();
86
204
  let footnoteOrder = [];
87
205
  docMarked.use({
88
206
  hooks: {
207
+ processAllTokens(tokens) {
208
+ if (!currentA11y)
209
+ return tokens;
210
+ const rewritten = accessibleTokens([...tokens], currentA11y.title);
211
+ // A TokensList carries `links` (reference definitions); keep it.
212
+ return Object.assign(rewritten, { links: tokens.links });
213
+ },
89
214
  preprocess(markdown) {
90
215
  footnoteDefs = new Map();
91
216
  footnoteOrder = [];
@@ -106,6 +231,15 @@ docMarked.use({
106
231
  },
107
232
  },
108
233
  extensions: [
234
+ {
235
+ // Built only by `accessibleTokens` above; there is no syntax of its own to tokenize.
236
+ name: 'figure',
237
+ renderer(token) {
238
+ const image = this.parser.parseInline([token.image]);
239
+ const caption = this.parser.parseInline(token.caption.tokens);
240
+ return `<figure>${image}<figcaption>${caption}</figcaption></figure>\n`;
241
+ },
242
+ },
109
243
  {
110
244
  // [[slug]] / [[slug|label]] → link to /slug/. Not matched inside code spans
111
245
  // (Marked tokenizes those first). Given the corpus's `wikilinkTargets`, a target is
@@ -209,14 +343,24 @@ export function unwrapLoneCustomElements(html) {
209
343
  export function renderDocMarkdown(text, opts = {}) {
210
344
  currentBakes = opts.bakes;
211
345
  currentWikilinks = opts.wikilinks;
346
+ currentA11y = opts.accessible ? { title: opts.title } : undefined;
212
347
  try {
213
348
  return unwrapLoneCustomElements(docMarked.parse(text));
214
349
  }
215
350
  finally {
216
351
  currentBakes = undefined;
217
352
  currentWikilinks = undefined;
353
+ currentA11y = undefined;
218
354
  }
219
355
  }
356
+ /**
357
+ * A doc's tokens as the accessibility mode renders them, for the build's report: the same
358
+ * lexer and the same rewrites as `renderDocMarkdown`, so the report and the page cannot
359
+ * disagree about what is a figure or which heading is the `<h1>`.
360
+ */
361
+ export function accessibleDocTokens(text, title) {
362
+ return accessibleTokens([...docMarked.lexer(text)], title);
363
+ }
220
364
  /**
221
365
  * Derive a clean <meta name="description"> from a doc's first prose paragraph.
222
366
  * Skips the title heading, code fences, tables, quotes, html and image/link-only
@@ -0,0 +1 @@
1
+ export {};