tosijs-ui 1.16.38 → 1.16.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/dist/doc-browser.d.ts +6 -0
- package/dist/doc-browser.js +57 -11
- package/dist/doc-system/doc-system-styles.d.ts +12 -1
- package/dist/doc-system/doc-system-styles.js +53 -48
- package/dist/doc-system/doc-system.js +1 -0
- package/dist/doc-system/nav-styles.d.ts +2 -0
- package/dist/doc-system/nav-styles.js +65 -0
- package/dist/doc-system/site/dev-server.d.ts +6 -0
- package/dist/doc-system/site/dev-server.js +9 -0
- package/dist/doc-system/site/generate-site.d.ts +9 -0
- package/dist/doc-system/site/generate-site.js +35 -8
- package/dist/doc-system/site/orchestrator.js +1 -0
- package/dist/doc-system/site/site-config.d.ts +5 -0
- package/dist/iife.js +112 -112
- package/dist/iife.js.map +10 -9
- package/dist/live-example/execution.d.ts +11 -0
- package/dist/live-example/execution.js +18 -0
- package/dist/palette.d.ts +7 -0
- package/dist/palette.js +31 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.16.40
|
|
4
|
+
|
|
5
|
+
- **An example with no script runs under a strict CSP (#245, #246).** A pure-HTML example still
|
|
6
|
+
built an `AsyncFunction` from its empty script, and a dialect that runs its own source (`run`)
|
|
7
|
+
had one built before its `run` was called. Under a Content Security Policy without
|
|
8
|
+
`'unsafe-eval'` (`script-src 'self'`) the constructor throws, so each such example showed an
|
|
9
|
+
error and filed a violation report every time it ran. Neither now constructs a function at
|
|
10
|
+
all, so HTML examples, and `run` dialects that only sanitize and bind markup, work on a
|
|
11
|
+
CSP-protected host. Script examples still need `'unsafe-eval'`; a CSP-compatible path for
|
|
12
|
+
them, and an HTML-only example mode, are planned for 1.17.0.
|
|
13
|
+
- **The doc header's search says "Search", and a site can set it.** `searchPlaceholder` in the
|
|
14
|
+
site config (or `createDocBrowser`) sets the hint in the header's search field, and the
|
|
15
|
+
trigger's accessible name; the default was "Search documentation" (set
|
|
16
|
+
`searchPlaceholder: 'Search documentation'` to keep it, and update any test that looks the
|
|
17
|
+
field up by that name). It is localized through the site's strings, and its placeholder
|
|
18
|
+
follows the locale the reader picks. `<tosi-palette>` gained a `localizer` property for this: a function its placeholder
|
|
19
|
+
goes through, such as `localize`. It is a function rather than a `localized` attribute so the
|
|
20
|
+
palette does not import the localization module, and `<tosi-select>` with it, wherever it is
|
|
21
|
+
used.
|
|
22
|
+
- **The ⌘K hint in the search field has no outline.** An outline inside the field's own outline
|
|
23
|
+
read as a second control. The hints inside the open palette keep theirs.
|
|
24
|
+
|
|
25
|
+
## 1.16.39
|
|
26
|
+
|
|
27
|
+
- **The doc-site frame passes WCAG 2.2 AA where an axe scan said it did not (#3520).** An
|
|
28
|
+
adopter's scan found four failures in the frame, so every doc site had them:
|
|
29
|
+
- The header's "Search documentation" trigger and its ⌘K hint measured 1.88:1 (AA asks
|
|
30
|
+
4.5:1). `--placeholder-color` was the brand at 40% opacity; it is now the brand darkened
|
|
31
|
+
(or, in dark mode, brightened) just far enough to reach 4.5:1 on the input, for any accent.
|
|
32
|
+
It colours every `::placeholder` on a doc site, live-example inputs included, so
|
|
33
|
+
placeholders are darker everywhere.
|
|
34
|
+
- Code (inline and in blocks) was the brand colour, 4.56:1 on the page and 3.72:1 inside a
|
|
35
|
+
blockquote. It is now `--code-color`: the brand made readable on the blockquote and code
|
|
36
|
+
backgrounds. It is computed from your config's `accent`, so code no longer follows a
|
|
37
|
+
runtime override of `--brand-color`; set `--code-color` too if you do that.
|
|
38
|
+
- Each nav section was `<details><summary><a>`, a link inside a control, which a screen reader
|
|
39
|
+
announces as one and loses the other. A section is now a toggle `<button>` beside its link,
|
|
40
|
+
with `aria-expanded` and `aria-controls`, at least 24px square (the old triangle was under
|
|
41
|
+
WCAG 2.2's target size). Without JavaScript every section is open, since nothing could open
|
|
42
|
+
one. Until the bundle loads, a small script in each generated page opens and closes
|
|
43
|
+
sections, so a slow or failed bundle does not strand a reader; it also marks `<html>` with
|
|
44
|
+
`tosi-js`. A doc browser made with `createDocBrowser` outside `<tosi-doc-system>` brings
|
|
45
|
+
its own section styles. **If your site's CSS targets `.doc-nav details` or
|
|
46
|
+
`.doc-nav summary`, it no longer matches**: use `.doc-nav .nav-branch` (`.-open` when
|
|
47
|
+
open), `.nav-branch-head` and `.nav-toggle`.
|
|
48
|
+
- A code block that scrolls sideways could not be reached by keyboard. Static code blocks are
|
|
49
|
+
now focusable once the page hydrates.
|
|
50
|
+
- **`devServer()` builds on start when it is watching (#3524).** The documented `bin/site.ts`
|
|
51
|
+
one-liner never built: it served whatever the previous run left in the output folder, so a
|
|
52
|
+
config change (a theme accent, an ePub cover) showed nothing until a source file changed, and
|
|
53
|
+
nothing said the page was stale. (`DEV_NO_WATCH=1` and `{ test: true }` still serve what is
|
|
54
|
+
there.) **If your script builds and then calls `devServer`, as the full-pipeline example in
|
|
55
|
+
the docs used to, it now builds twice on start**: pass `initialBuild: false`, or drop your
|
|
56
|
+
own build. That build is your `build` function when you pass one, so the dependency audit
|
|
57
|
+
inside `buildSite` runs there too, and a blocking advisory leaves the server up with the
|
|
58
|
+
failure on the page rather than exiting. The config file itself is not watched: restart the
|
|
59
|
+
server after editing it.
|
|
60
|
+
- **A table has a margin (#3553).** Tables were the one block element without one, so a
|
|
61
|
+
blockquote after one sat flush against it. The ePub was already right.
|
|
62
|
+
|
|
3
63
|
## 1.16.38
|
|
4
64
|
|
|
5
65
|
- **The ePub carries its images (#3519).** Every local image went into the book as a site path
|
package/dist/doc-browser.d.ts
CHANGED
|
@@ -118,6 +118,11 @@ export interface DocBrowserOptions {
|
|
|
118
118
|
* for a host page that owns that key or embeds more than one browser.
|
|
119
119
|
*/
|
|
120
120
|
searchShortcut?: string;
|
|
121
|
+
/**
|
|
122
|
+
* The hint in the header's search field (default `'Search'`). Localized through the site's
|
|
123
|
+
* strings, like every other piece of the doc system's own text.
|
|
124
|
+
*/
|
|
125
|
+
searchPlaceholder?: string;
|
|
121
126
|
/**
|
|
122
127
|
* Header-bar links. When provided, these replace the legacy `projectLinks` icon
|
|
123
128
|
* set in the header (each renders as an icon if `icon` names a known icon, else
|
|
@@ -132,4 +137,5 @@ export interface DocBrowserOptions {
|
|
|
132
137
|
*/
|
|
133
138
|
contentElement?: HTMLElement;
|
|
134
139
|
}
|
|
140
|
+
export declare function focusableCodeBlocks(root: ParentNode): void;
|
|
135
141
|
export declare function createDocBrowser(options: DocBrowserOptions): HTMLElement;
|
package/dist/doc-browser.js
CHANGED
|
@@ -102,6 +102,7 @@ or embeds more than one browser, passes `searchShortcut: ''` (or another key in
|
|
|
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
|
+
import { navSectionStyleSpec } from './doc-system/nav-styles.js';
|
|
105
106
|
import { renderDocMarkdown } from './doc-system/render.js';
|
|
106
107
|
import { pageTitle } from './doc-system/doc-title.js';
|
|
107
108
|
import { unsettledExamples, tallyExamples, closePage, } from './doc-system/test-completion.js';
|
|
@@ -114,7 +115,7 @@ import { tosiSidenav, TosiSidenav } from './side-nav.js';
|
|
|
114
115
|
import { icons } from './icons.js';
|
|
115
116
|
import { tosiPalette } from './palette.js';
|
|
116
117
|
import { buildDocIndex, searchDocs } from './doc-system/doc-search.js';
|
|
117
|
-
import { tosiLocalized } from './localize.js';
|
|
118
|
+
import { tosiLocalized, localize, i18n } from './localize.js';
|
|
118
119
|
import { popMenu } from './menu.js';
|
|
119
120
|
import { codeEditor } from './code-editor.js';
|
|
120
121
|
import { tosiDiff } from './diff.js';
|
|
@@ -148,7 +149,7 @@ export const hasTestBlock = (text, policy = 'auto') => typeof text === 'string'
|
|
|
148
149
|
// Cheap reject first: lexing every doc in a large corpus is not free.
|
|
149
150
|
text.includes('test') &&
|
|
150
151
|
collectCodeTokens(text).some((t) => t.lang === 'test' && isLiveFence('test', t.mode, policy));
|
|
151
|
-
const { div, span, a, header, button, template, h2, img,
|
|
152
|
+
const { div, span, a, header, button, template, h2, img, ul, li } = elements;
|
|
152
153
|
/*
|
|
153
154
|
Test colors, and the text color that goes ON each — because contrast is a PAIR.
|
|
154
155
|
|
|
@@ -374,11 +375,27 @@ const logoMarkStyleSpec = {
|
|
|
374
375
|
// Inline-<svg> logos are wrapped in a flex holder; scale the embedded svg to it.
|
|
375
376
|
'.logo-mark > svg': { height: '100%', width: 'auto' },
|
|
376
377
|
};
|
|
378
|
+
/*
|
|
379
|
+
A code block that scrolls sideways must be reachable by keyboard, or its overflow cannot be
|
|
380
|
+
read without a pointer (WCAG 2.1.1, axe `scrollable-region-focusable`, #3520). Every static
|
|
381
|
+
block gets it, not only the ones overflowing now: overflow changes with the window, and a
|
|
382
|
+
check at render time would go stale on the first resize. Done here rather than in the
|
|
383
|
+
markdown renderer so the pre-rendered page stays byte-identical to what it was.
|
|
384
|
+
*/
|
|
385
|
+
export function focusableCodeBlocks(root) {
|
|
386
|
+
for (const pre of root.querySelectorAll('pre')) {
|
|
387
|
+
if (pre.hasAttribute('tabindex') || pre.closest('tosi-example'))
|
|
388
|
+
continue;
|
|
389
|
+
pre.tabIndex = 0;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
377
392
|
// Monotonic per-page counter so each createDocBrowser() call gets a distinct
|
|
378
393
|
// tosi() registry key (see stateKey below) and two browsers can't share state.
|
|
379
394
|
let docBrowserSeq = 0;
|
|
395
|
+
// Likewise for the nav's aria-controls ids.
|
|
396
|
+
let navIdCounter = 0;
|
|
380
397
|
export function createDocBrowser(options) {
|
|
381
|
-
const { docs, context = {}, projectName = '', projectLinks = {}, logo, navSize = 200, minSize = 600, routing = 'query', initialRoute, aliases = {}, onRouteChange, searchShortcut = 'mod+k', navbarLinks, contentElement, } = 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;
|
|
382
399
|
// Memory routing is fully self-contained: it never reads or writes
|
|
383
400
|
// window.history/location, so an embedded or nested browser can't hijack the
|
|
384
401
|
// host page's URL (or recurse into it).
|
|
@@ -733,11 +750,14 @@ export function createDocBrowser(options) {
|
|
|
733
750
|
// In the header bar (see headerContent); the nav no longer holds a search field.
|
|
734
751
|
const searchBox = tosiPalette({
|
|
735
752
|
class: 'doc-search',
|
|
736
|
-
placeholder:
|
|
753
|
+
placeholder: searchPlaceholder,
|
|
754
|
+
localizer: localize,
|
|
737
755
|
shortcut: searchShortcut,
|
|
738
756
|
sources: [docSearchSource],
|
|
739
757
|
quickLinks,
|
|
740
758
|
});
|
|
759
|
+
// The hint follows the reader's language, like the rest of the doc system's own text.
|
|
760
|
+
i18n.locale.observe(() => searchBox.queueRender());
|
|
741
761
|
// Memory routing is decoupled from the page URL, so it ignores browser
|
|
742
762
|
// back/forward (the host owns history, if any).
|
|
743
763
|
if (!memoryRouting) {
|
|
@@ -1094,6 +1114,7 @@ export function createDocBrowser(options) {
|
|
|
1094
1114
|
// Stamp each example with its source file (for the source↔doc map). doc.path
|
|
1095
1115
|
// is the extracted file (.md, or a source file with doc comments).
|
|
1096
1116
|
LiveExample.insertExamples(docContent, context, doc.path || undefined);
|
|
1117
|
+
focusableCodeBlocks(docContent);
|
|
1097
1118
|
/*
|
|
1098
1119
|
Highlight whatever is left — the static blocks that are not live examples.
|
|
1099
1120
|
|
|
@@ -1327,6 +1348,7 @@ export function createDocBrowser(options) {
|
|
|
1327
1348
|
docContent.innerHTML = renderDocMarkdown(docMarkdownFromSource(editUI.editor.value, editUI.doc.path), { wikilinks });
|
|
1328
1349
|
rewriteContentLinks();
|
|
1329
1350
|
LiveExample.insertExamples(docContent, context, editUI.doc.path || undefined);
|
|
1351
|
+
focusableCodeBlocks(docContent);
|
|
1330
1352
|
}
|
|
1331
1353
|
else if (view === 'diff') {
|
|
1332
1354
|
docContent.replaceChildren(tosiDiff({
|
|
@@ -1506,7 +1528,7 @@ export function createDocBrowser(options) {
|
|
|
1506
1528
|
return menuItems;
|
|
1507
1529
|
};
|
|
1508
1530
|
// ── Hierarchical nav (path routing) ───────────────────────────────────────
|
|
1509
|
-
// Build nested
|
|
1531
|
+
// Build nested sections from the doc tree; current-highlight, test status,
|
|
1510
1532
|
// search visibility, and auto-open are applied imperatively by refreshNav.
|
|
1511
1533
|
const navStyle = {
|
|
1512
1534
|
slot: 'nav',
|
|
@@ -1523,6 +1545,8 @@ export function createDocBrowser(options) {
|
|
|
1523
1545
|
};
|
|
1524
1546
|
const buildHierarchicalNav = () => {
|
|
1525
1547
|
const roots = buildNavTree(docs, slugMap);
|
|
1548
|
+
// Ids for aria-controls, unique per browser: an embedded doc browser shares the page.
|
|
1549
|
+
const navIdPrefix = `tosi-nav-${(navIdCounter += 1)}-`;
|
|
1526
1550
|
const leaves = new Map();
|
|
1527
1551
|
const branches = [];
|
|
1528
1552
|
const subtreeFilenames = (node) => {
|
|
@@ -1560,18 +1584,39 @@ export function createDocBrowser(options) {
|
|
|
1560
1584
|
leaves.set(node.doc.filename, { li: item, link });
|
|
1561
1585
|
return item;
|
|
1562
1586
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1587
|
+
/*
|
|
1588
|
+
A toggle button BESIDE the link, not a <summary> around it (#3520): a link inside a
|
|
1589
|
+
summary is a control inside a control. Same markup as the static nav
|
|
1590
|
+
(`navToggleHtml` in generate-site.ts), so hydration does not move anything.
|
|
1591
|
+
*/
|
|
1592
|
+
const listId = `${navIdPrefix}${node.slug || 'home'}`;
|
|
1593
|
+
const toggle = button({
|
|
1594
|
+
type: 'button',
|
|
1595
|
+
class: 'nav-toggle',
|
|
1596
|
+
ariaExpanded: 'false',
|
|
1597
|
+
ariaControls: listId,
|
|
1598
|
+
ariaLabel: String(node.doc.title),
|
|
1599
|
+
onClick: () => section.setOpen(!item.classList.contains('-open')),
|
|
1600
|
+
});
|
|
1601
|
+
const item = li({ class: 'nav-branch' }, div({ class: 'nav-branch-head' }, toggle, link), ul({ id: listId }, ...node.children.map(renderNode)));
|
|
1602
|
+
const section = {
|
|
1603
|
+
setOpen(open) {
|
|
1604
|
+
item.classList.toggle('-open', open);
|
|
1605
|
+
toggle.setAttribute('aria-expanded', String(open));
|
|
1606
|
+
},
|
|
1607
|
+
};
|
|
1565
1608
|
branches.push({
|
|
1566
1609
|
li: item,
|
|
1567
|
-
el:
|
|
1610
|
+
el: section,
|
|
1568
1611
|
link,
|
|
1569
1612
|
filename: node.doc.filename,
|
|
1570
1613
|
subtree: subtreeFilenames(node),
|
|
1571
1614
|
});
|
|
1572
1615
|
return item;
|
|
1573
1616
|
};
|
|
1574
|
-
|
|
1617
|
+
// `-live`: these sections have working toggles, so closed ones collapse even where no
|
|
1618
|
+
// pre-paint script marked <html> (a browser embedded outside a generated page).
|
|
1619
|
+
const root = div({ ...navStyle, class: `${navStyle.class} -live` }, ul(...roots.map(renderNode)));
|
|
1575
1620
|
const applyStatus = (link, filename, current) => {
|
|
1576
1621
|
link.classList.toggle('current', filename === current);
|
|
1577
1622
|
const r = pageTestResults[filename];
|
|
@@ -1590,9 +1635,9 @@ export function createDocBrowser(options) {
|
|
|
1590
1635
|
// On navigation (resetOpen), collapse everything except the current doc's section.
|
|
1591
1636
|
// Otherwise (e.g. test-status refresh) leave user toggles alone.
|
|
1592
1637
|
if (resetOpen)
|
|
1593
|
-
el.
|
|
1638
|
+
el.setOpen(subtree.includes(current));
|
|
1594
1639
|
else if (subtree.includes(current))
|
|
1595
|
-
el.
|
|
1640
|
+
el.setOpen(true);
|
|
1596
1641
|
}
|
|
1597
1642
|
};
|
|
1598
1643
|
refreshNav(true);
|
|
@@ -1691,6 +1736,7 @@ export function createDocBrowser(options) {
|
|
|
1691
1736
|
// Header brand-mark sizing (icon / <img> / inline-<svg>), one CSS var each.
|
|
1692
1737
|
StyleSheet('doc-logo-mark', logoMarkStyleSpec);
|
|
1693
1738
|
StyleSheet('doc-nav-search', navSearchStyleSpec);
|
|
1739
|
+
StyleSheet('doc-nav-sections', navSectionStyleSpec);
|
|
1694
1740
|
/*
|
|
1695
1741
|
The far end has something to tell you.
|
|
1696
1742
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { XinStyleSheet, XinStyleRule } from 'tosijs';
|
|
1
|
+
import { XinStyleSheet, XinStyleRule, Color } from 'tosijs';
|
|
2
2
|
export declare const SIDEBAR_WIDTH = 200;
|
|
3
3
|
export declare const SIDEBAR_BREAKPOINT = 600;
|
|
4
4
|
export interface DocSystemTheme {
|
|
@@ -22,6 +22,17 @@ export interface DocSystemTheme {
|
|
|
22
22
|
*/
|
|
23
23
|
codeBg?: string;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* WCAG 2 contrast ratio between two opaque colors. Throws on a translucent one: its real
|
|
27
|
+
* contrast depends on what is behind it, and ignoring alpha measured the old 40%-opacity
|
|
28
|
+
* placeholder at 4.74:1 where axe, compositing it, measured 1.88:1.
|
|
29
|
+
*/
|
|
30
|
+
export declare function contrastRatio(a: Color, b: Color): number;
|
|
31
|
+
/**
|
|
32
|
+
* `color`, darkened (on a light `bg`) or brightened (on a dark one) just far enough to reach
|
|
33
|
+
* `min` contrast against `bg`, so it keeps as much of its hue as AA allows.
|
|
34
|
+
*/
|
|
35
|
+
export declare function readableOn(color: Color, bg: Color, min?: number): Color;
|
|
25
36
|
/** Compute the full set of `:root` color variables from a few base colors. */
|
|
26
37
|
export declare function docSystemColors(theme?: DocSystemTheme): XinStyleRule;
|
|
27
38
|
/** Build the full doc-system stylesheet for a given base theme. */
|
|
@@ -10,6 +10,7 @@ Colors are computed from a small set of base colors via tosijs `Color` math, so
|
|
|
10
10
|
palette can eventually be driven entirely by a few attributes on the element
|
|
11
11
|
(accent/background/text) — most of the palette is derived from `accent`.
|
|
12
12
|
*/
|
|
13
|
+
import { navSectionStyleSpec } from './nav-styles.js';
|
|
13
14
|
import { highlightStyleSpec } from './highlight-styles.js';
|
|
14
15
|
import { vars, Color, invertLuminance, } from 'tosijs';
|
|
15
16
|
import { icons, svg2DataUrl } from '../icons.js';
|
|
@@ -19,6 +20,36 @@ import { icons, svg2DataUrl } from '../icons.js';
|
|
|
19
20
|
// `SIDEBAR_BREAKPOINT`. Keep them in step, or the page jumps as it hydrates.
|
|
20
21
|
export const SIDEBAR_WIDTH = 200;
|
|
21
22
|
export const SIDEBAR_BREAKPOINT = 600;
|
|
23
|
+
/**
|
|
24
|
+
* WCAG 2 contrast ratio between two opaque colors. Throws on a translucent one: its real
|
|
25
|
+
* contrast depends on what is behind it, and ignoring alpha measured the old 40%-opacity
|
|
26
|
+
* placeholder at 4.74:1 where axe, compositing it, measured 1.88:1.
|
|
27
|
+
*/
|
|
28
|
+
export function contrastRatio(a, b) {
|
|
29
|
+
if (a.RGBA[3] < 1 || b.RGBA[3] < 1)
|
|
30
|
+
throw new Error('contrastRatio: composite a translucent color first');
|
|
31
|
+
const lum = (c) => {
|
|
32
|
+
const [r, g, bl] = c.RGBA.map((v) => v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
|
|
33
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * bl;
|
|
34
|
+
};
|
|
35
|
+
const [hi, lo] = [lum(a), lum(b)].sort((x, y) => y - x);
|
|
36
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `color`, darkened (on a light `bg`) or brightened (on a dark one) just far enough to reach
|
|
40
|
+
* `min` contrast against `bg`, so it keeps as much of its hue as AA allows.
|
|
41
|
+
*/
|
|
42
|
+
export function readableOn(color, bg, min = 4.5) {
|
|
43
|
+
const darkBg = contrastRatio(bg, Color.black) < contrastRatio(bg, Color.white);
|
|
44
|
+
// Measured on the hex the stylesheet will actually carry: rounding to 8-bit channels
|
|
45
|
+
// can drop a ratio just over `min` to just under it.
|
|
46
|
+
const shipped = (c) => Color.fromCss(c.html);
|
|
47
|
+
let c = shipped(color.opaque);
|
|
48
|
+
for (let step = 0; step < 20 && contrastRatio(c, bg) < min; step++) {
|
|
49
|
+
c = shipped(darkBg ? c.brighten(0.1) : c.darken(0.1));
|
|
50
|
+
}
|
|
51
|
+
return contrastRatio(c, bg) < min ? bg.contrasting() : c;
|
|
52
|
+
}
|
|
22
53
|
/** Compute the full set of `:root` color variables from a few base colors. */
|
|
23
54
|
export function docSystemColors(theme = {}) {
|
|
24
55
|
const brandColor = Color.fromCss(theme.accent ?? '#d92270');
|
|
@@ -32,7 +63,12 @@ export function docSystemColors(theme = {}) {
|
|
|
32
63
|
_navBg: brandColor.rotate(30).desaturate(0.5).brighten(0.9),
|
|
33
64
|
_barColor: brandColor.opacity(0.4),
|
|
34
65
|
_focusColor: brandColor.opacity(0.7),
|
|
35
|
-
|
|
66
|
+
/*
|
|
67
|
+
Readable, not faint (#3520). This was the brand at 40% opacity, which an axe scan measured
|
|
68
|
+
at 1.88:1 on the input (#eda7c5 on #fdfdfd) where AA asks 4.5:1, and it colours the doc
|
|
69
|
+
header's search trigger, a real label, not just a hint.
|
|
70
|
+
*/
|
|
71
|
+
_placeholderColor: readableOn(brandColor, Color.fromCss(theme.inputBg ?? '#fdfdfd')),
|
|
36
72
|
/*
|
|
37
73
|
`contrasting()` rather than a hand-tuned tint.
|
|
38
74
|
|
|
@@ -46,6 +82,10 @@ export function docSystemColors(theme = {}) {
|
|
|
46
82
|
*/
|
|
47
83
|
_brandTextColor: brandColor.contrasting(),
|
|
48
84
|
_insetBg: brandColor.rotate(45).brighten(0.8),
|
|
85
|
+
// The brand, made readable on the inset (blockquote) background, the darker of the two
|
|
86
|
+
// surfaces code sits on. The raw brand measured 3.72:1 there (#3520).
|
|
87
|
+
// Checked against the code background too: a custom `codeBg` can be the darker one.
|
|
88
|
+
_codeColor: readableOn(readableOn(brandColor, brandColor.rotate(45).brighten(0.8)), Color.fromCss(theme.codeBg ?? '#fdfdfd')),
|
|
49
89
|
// Neutral off-white (not brand-tinted); dark mode inverts its luminance to
|
|
50
90
|
// off-black, so the editor surface stays a clean neutral in both themes.
|
|
51
91
|
_codeBg: theme.codeBg ?? '#fdfdfd',
|
|
@@ -182,6 +222,8 @@ export function docSystemStyleSpec(theme = {}) {
|
|
|
182
222
|
brand, the luminance belongs to the background it sits on.
|
|
183
223
|
*/
|
|
184
224
|
_brandColor: brandColor.brighten(0.18),
|
|
225
|
+
_codeColor: readableOn(readableOn(brandColor.brighten(0.18), brandColor.rotate(45).brighten(0.8).inverseLuminance), Color.fromCss(theme.codeBg ?? '#fdfdfd').inverseLuminance),
|
|
226
|
+
_placeholderColor: readableOn(brandColor.brighten(0.18), Color.fromCss(theme.inputBg ?? '#fdfdfd').inverseLuminance),
|
|
185
227
|
_shadowColor: brandColor.opacity(0.5),
|
|
186
228
|
_menuShadow: `0 0 0 2px ${brandColor.opacity(0.75)}`,
|
|
187
229
|
_menuSeparatorColor: brandColor.opacity(0.5),
|
|
@@ -281,9 +323,9 @@ export function docSystemStyleSpec(theme = {}) {
|
|
|
281
323
|
display: 'none',
|
|
282
324
|
},
|
|
283
325
|
// Wide enough for the sidebar: reserve its column, and show the REAL nav tree
|
|
284
|
-
// in it. It's a plain <ul>
|
|
285
|
-
//
|
|
286
|
-
// and hydration merely upgrades it in place.
|
|
326
|
+
// in it. It's a plain <ul> of real links, every section expanded when there is no
|
|
327
|
+
// JavaScript to collapse one (no `tosi-js` on <html>) — a no-JS reader gets a
|
|
328
|
+
// working site, and hydration merely upgrades it in place.
|
|
287
329
|
[`@media (min-width: ${SIDEBAR_BREAKPOINT}px)`]: {
|
|
288
330
|
'tosi-doc-system:not(:defined)': {
|
|
289
331
|
left: `${SIDEBAR_WIDTH}px`,
|
|
@@ -314,9 +356,6 @@ export function docSystemStyleSpec(theme = {}) {
|
|
|
314
356
|
//
|
|
315
357
|
// Only genuinely pre-hydration-specific rules belong in this block — the fixed
|
|
316
358
|
// positioning above, which the upgraded component takes over itself.
|
|
317
|
-
'tosi-doc-system:not(:defined) .doc-nav summary': {
|
|
318
|
-
cursor: 'pointer',
|
|
319
|
-
},
|
|
320
359
|
},
|
|
321
360
|
'.center': {
|
|
322
361
|
display: 'flex',
|
|
@@ -507,7 +546,7 @@ export function docSystemStyleSpec(theme = {}) {
|
|
|
507
546
|
// `--text-color` token rather than setting a colour, so everything inside a `<pre>`
|
|
508
547
|
// — including every syntax-highlighting token span — inherited a palette where the
|
|
509
548
|
// theme's text colour had been replaced by the brand colour.
|
|
510
|
-
color: vars.
|
|
549
|
+
color: vars.codeColor,
|
|
511
550
|
},
|
|
512
551
|
/*
|
|
513
552
|
Prism token colours for STATIC code blocks — TWO palettes, because there are two
|
|
@@ -554,7 +593,7 @@ export function docSystemStyleSpec(theme = {}) {
|
|
|
554
593
|
boxShadow: 'none',
|
|
555
594
|
color: vars.textColor,
|
|
556
595
|
},
|
|
557
|
-
'.current,
|
|
596
|
+
'.current, .nav-branch-head:has(.current)': {
|
|
558
597
|
background: vars.background,
|
|
559
598
|
},
|
|
560
599
|
'.doc-link': {
|
|
@@ -584,8 +623,11 @@ export function docSystemStyleSpec(theme = {}) {
|
|
|
584
623
|
"[class*='icon-']": {
|
|
585
624
|
verticalAlign: 'middle',
|
|
586
625
|
},
|
|
626
|
+
// Every other block element brings its own margin; without one a table sat flush
|
|
627
|
+
// against a following blockquote, whose top margin is 0 by design (#3553).
|
|
587
628
|
table: {
|
|
588
629
|
borderCollapse: 'collapse',
|
|
630
|
+
margin: `${vars.spacing} 0 ${vars.spacing200} 0`,
|
|
589
631
|
},
|
|
590
632
|
thead: {
|
|
591
633
|
background: vars.brandColor,
|
|
@@ -601,7 +643,7 @@ export function docSystemStyleSpec(theme = {}) {
|
|
|
601
643
|
padding: 'calc(var(--spacing) * 0.5) var(--spacing)',
|
|
602
644
|
},
|
|
603
645
|
// Nav lists carry semantics only — no list styling. Hierarchy indentation
|
|
604
|
-
// comes from left-padding on each section's child list
|
|
646
|
+
// comes from left-padding on each section's child list.
|
|
605
647
|
'.doc-nav ul, .doc-nav li': {
|
|
606
648
|
listStyle: 'none',
|
|
607
649
|
margin: 0,
|
|
@@ -615,44 +657,7 @@ export function docSystemStyleSpec(theme = {}) {
|
|
|
615
657
|
display: 'flex',
|
|
616
658
|
flexDirection: 'column',
|
|
617
659
|
},
|
|
618
|
-
|
|
619
|
-
paddingLeft: vars.spacing,
|
|
620
|
-
},
|
|
621
|
-
// Collapse a closed section's list SYNCHRONOUSLY. The UA collapses <details>
|
|
622
|
-
// via `::details-content { content-visibility: hidden }`, which is applied
|
|
623
|
-
// lazily and — for this static tree — non-deterministically on first paint:
|
|
624
|
-
// sometimes the closed section is still laid out at full height for a frame
|
|
625
|
-
// before it snaps shut. That frame is a visible flicker on refresh, and it made
|
|
626
|
-
// the "nav does not move on hydration" test measure a 36px row that hydration
|
|
627
|
-
// then collapses to 0. An explicit display:none collapses it in the first layout
|
|
628
|
-
// pass with no content-visibility race, and still opens on a no-JS <summary>
|
|
629
|
-
// toggle (the UA adds [open], so this rule stops matching).
|
|
630
|
-
'.doc-nav details:not([open]) > ul': {
|
|
631
|
-
display: 'none',
|
|
632
|
-
},
|
|
633
|
-
// <summary> is the section header (its triangle toggles; the link navigates).
|
|
634
|
-
'.doc-nav summary': {
|
|
635
|
-
cursor: 'pointer',
|
|
636
|
-
// indent the disclosure marker a bit from the edge
|
|
637
|
-
paddingLeft: 'calc(var(--spacing) * 0.5)',
|
|
638
|
-
},
|
|
639
|
-
'.doc-nav summary::marker': {
|
|
640
|
-
color: vars.brandColor,
|
|
641
|
-
},
|
|
642
|
-
'.doc-nav summary > .doc-link': {
|
|
643
|
-
display: 'inline-block',
|
|
644
|
-
// push the summary text in a bit less than a normal doc-link
|
|
645
|
-
paddingLeft: 'calc(var(--spacing) * 0.5)',
|
|
646
|
-
/*
|
|
647
|
-
Room for the disclosure triangle on the same line (#219). An inline-block as wide as
|
|
648
|
-
its text does not fit beside the marker once the title is long, so the whole link
|
|
649
|
-
dropped to the next line and left the triangle alone on a row of its own, which reads
|
|
650
|
-
as an empty nav entry. Capped, it stays beside the triangle and wraps inside itself.
|
|
651
|
-
*/
|
|
652
|
-
boxSizing: 'border-box',
|
|
653
|
-
maxWidth: 'calc(100% - 1.5em)',
|
|
654
|
-
verticalAlign: 'top',
|
|
655
|
-
},
|
|
660
|
+
...navSectionStyleSpec,
|
|
656
661
|
// Declarative header link list — shown for no-JS, removed on hydration.
|
|
657
662
|
'.doc-navbar': {
|
|
658
663
|
listStyle: 'none',
|
|
@@ -608,6 +608,7 @@ export class TosiDocSystem extends withAttributes({
|
|
|
608
608
|
projectLinks: config.projectLinks,
|
|
609
609
|
logo: config.logo,
|
|
610
610
|
navbarLinks: navbarLinks.length ? navbarLinks : undefined,
|
|
611
|
+
searchPlaceholder: config.searchPlaceholder,
|
|
611
612
|
contentElement,
|
|
612
613
|
});
|
|
613
614
|
// Add the settings (theme + language) menu — except in a nested demo, where
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { vars } from 'tosijs';
|
|
2
|
+
/*
|
|
3
|
+
The doc nav's collapsible sections, in a module of their own because two places need them:
|
|
4
|
+
the site stylesheet (`docSystemStyleSpec`), which paints the pre-rendered nav before anything
|
|
5
|
+
hydrates, and `createDocBrowser`, which injects them itself. A browser created outside
|
|
6
|
+
<tosi-doc-system> (an embedded, memory-routed one) never gets the site stylesheet, and
|
|
7
|
+
before #3520 it did not need it: native <details> collapsed with no CSS at all.
|
|
8
|
+
*/
|
|
9
|
+
export const navSectionStyleSpec = {
|
|
10
|
+
'.doc-nav .nav-branch > ul': {
|
|
11
|
+
paddingLeft: vars.spacing,
|
|
12
|
+
},
|
|
13
|
+
/*
|
|
14
|
+
A section is a toggle button BESIDE its link (#3520). It was <details><summary><a>, a link
|
|
15
|
+
inside a control, which a screen reader announces as one and loses the other (axe
|
|
16
|
+
`nested-interactive`), and its marker was under the 24px target size WCAG 2.2 asks.
|
|
17
|
+
A closed section collapses only when JavaScript is running (`tosi-js` is set on <html>
|
|
18
|
+
before first paint; a nav `createDocBrowser` built is `.-live`): without it nothing
|
|
19
|
+
could open the section, so every one stays expanded.
|
|
20
|
+
*/
|
|
21
|
+
'html.tosi-js .doc-nav .nav-branch:not(.-open) > ul, .doc-nav.-live .nav-branch:not(.-open) > ul': {
|
|
22
|
+
display: 'none',
|
|
23
|
+
},
|
|
24
|
+
'.doc-nav .nav-branch-head': {
|
|
25
|
+
display: 'flex',
|
|
26
|
+
alignItems: 'flex-start',
|
|
27
|
+
},
|
|
28
|
+
'.doc-nav .nav-toggle': {
|
|
29
|
+
flex: '0 0 auto',
|
|
30
|
+
minWidth: '24px',
|
|
31
|
+
minHeight: '24px',
|
|
32
|
+
padding: 0,
|
|
33
|
+
margin: 0,
|
|
34
|
+
border: 'none',
|
|
35
|
+
background: 'transparent',
|
|
36
|
+
boxShadow: 'none',
|
|
37
|
+
cursor: 'pointer',
|
|
38
|
+
color: vars.brandColor,
|
|
39
|
+
display: 'inline-flex',
|
|
40
|
+
alignItems: 'center',
|
|
41
|
+
justifyContent: 'center',
|
|
42
|
+
alignSelf: 'stretch',
|
|
43
|
+
maxHeight: vars.lineHeight,
|
|
44
|
+
},
|
|
45
|
+
// A disclosure triangle, pointing right when closed and down when open.
|
|
46
|
+
'.doc-nav .nav-toggle::before': {
|
|
47
|
+
content: "''",
|
|
48
|
+
borderStyle: 'solid',
|
|
49
|
+
borderColor: 'transparent transparent transparent currentcolor',
|
|
50
|
+
borderWidth: '0.3em 0 0.3em 0.5em',
|
|
51
|
+
transition: 'transform 0.15s ease-out',
|
|
52
|
+
},
|
|
53
|
+
'.doc-nav .nav-branch.-open > .nav-branch-head > .nav-toggle::before': {
|
|
54
|
+
transform: 'rotate(90deg)',
|
|
55
|
+
},
|
|
56
|
+
// Without JavaScript every section is open and nothing could close one, so the toggle is
|
|
57
|
+
// hidden: kept in the layout, out of the tab order and the accessibility tree.
|
|
58
|
+
'html:not(.tosi-js) .doc-nav:not(.-live) .nav-toggle': {
|
|
59
|
+
visibility: 'hidden',
|
|
60
|
+
},
|
|
61
|
+
'.doc-nav .nav-branch-head > .doc-link': {
|
|
62
|
+
flex: '1 1 auto',
|
|
63
|
+
minWidth: 0,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -179,4 +179,10 @@ export declare function isCompressible(filePath: string): boolean;
|
|
|
179
179
|
export declare function devServer(config: SiteConfig, opts?: {
|
|
180
180
|
test?: boolean;
|
|
181
181
|
build?: () => unknown | Promise<unknown>;
|
|
182
|
+
/**
|
|
183
|
+
* Build once on start (default `true`). Pass `false` only when the caller has just
|
|
184
|
+
* built, as `bin/dev.ts` does; otherwise the server serves whatever the LAST run
|
|
185
|
+
* left in the output dir, built from whatever config that run had (#3524).
|
|
186
|
+
*/
|
|
187
|
+
initialBuild?: boolean;
|
|
182
188
|
}): Promise<void>;
|
|
@@ -1796,6 +1796,15 @@ export async function devServer(config, opts = {}) {
|
|
|
1796
1796
|
}
|
|
1797
1797
|
void rebuild();
|
|
1798
1798
|
});
|
|
1799
|
+
/*
|
|
1800
|
+
Build on start (#3524). The documented one-liner, `… : devServer(config)`, never built:
|
|
1801
|
+
it served the previous run's output, so a config edit (a theme accent, an ePub cover)
|
|
1802
|
+
showed nothing until some watched file changed, and nothing said the page was stale.
|
|
1803
|
+
Started without awaiting, so the port binds at once; requests during the build fall
|
|
1804
|
+
back to the last good tree, as they do for every rebuild.
|
|
1805
|
+
*/
|
|
1806
|
+
if (opts.initialBuild !== false)
|
|
1807
|
+
void rebuild();
|
|
1799
1808
|
}
|
|
1800
1809
|
/*
|
|
1801
1810
|
── dependency audit (NON-BLOCKING — the port comes up first) ─────────────────
|
|
@@ -8,6 +8,8 @@ export interface GenerateSiteConfig {
|
|
|
8
8
|
docs: Doc[];
|
|
9
9
|
/** `SiteConfig.aliases`: { alias path -> slug } (board #3513) */
|
|
10
10
|
aliases?: Record<string, string>;
|
|
11
|
+
/** `SiteConfig.searchPlaceholder` */
|
|
12
|
+
searchPlaceholder?: string;
|
|
11
13
|
/**
|
|
12
14
|
* Mirrors `SiteConfig.liveExamples`. The static highlighter needs it so it skips the
|
|
13
15
|
* fences that will become live examples — a highlighted block hands the example markup
|
|
@@ -105,6 +107,13 @@ export interface GenerateSiteConfig {
|
|
|
105
107
|
/** extra lines injected into every <head> (favicon, analytics, etc.) */
|
|
106
108
|
headExtra?: string;
|
|
107
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* A section's disclosure toggle. A real button BESIDE the link, not a `<summary>` around it:
|
|
112
|
+
* a link inside a summary is a control inside a control, which a screen reader announces as
|
|
113
|
+
* one and loses the other (axe `nested-interactive`, #3520). Named by the section title, so
|
|
114
|
+
* it reads "Components, expanded, button". The hydrated nav builds the same markup.
|
|
115
|
+
*/
|
|
116
|
+
export declare function navToggleHtml(title: string, listId: string, open: boolean): string;
|
|
108
117
|
/**
|
|
109
118
|
* Prefix a root-relative path with basePath. No-op for '/', empty, protocol-
|
|
110
119
|
* relative (`//…`), or absolute (`https://…`) URLs.
|