tosijs-ui 1.16.2 → 1.16.4
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 +83 -0
- package/dist/doc-browser.js +76 -7
- package/dist/doc-system/example-policy.d.ts +15 -0
- package/dist/doc-system/example-policy.js +13 -1
- package/dist/doc-system/site/check-examples-cli.js +5 -0
- package/dist/doc-system/site/check-examples.d.ts +6 -0
- package/dist/doc-system/site/check-examples.js +3 -0
- package/dist/doc-system/site/epub-cli.js +7 -0
- package/dist/doc-system/site/orchestrator.js +19 -21
- package/dist/doc-system/site/page-globals.d.ts +6 -0
- package/dist/doc-system/site/page-globals.js +25 -0
- package/dist/doc-system/site/site-config.d.ts +14 -2
- package/dist/float.d.ts +5 -0
- package/dist/float.js +45 -2
- package/dist/iife.js +141 -135
- package/dist/iife.js.map +36 -35
- package/dist/live-example/code-transform.d.ts +11 -0
- package/dist/live-example/code-transform.js +23 -0
- package/dist/live-example/component.d.ts +93 -25
- package/dist/live-example/component.js +378 -77
- package/dist/live-example/dialects.d.ts +11 -0
- package/dist/live-example/dialects.js +17 -3
- package/dist/live-example/example-console.js +1 -1
- package/dist/live-example/execution.d.ts +28 -0
- package/dist/live-example/execution.js +51 -5
- package/dist/live-example/insert-examples.js +30 -1
- package/dist/live-example/styles.d.ts +45 -20
- package/dist/live-example/styles.js +51 -27
- package/dist/live-example/types.d.ts +0 -1
- package/dist/menu.js +3 -0
- package/dist/pop-float.d.ts +13 -0
- package/dist/pop-float.js +62 -2
- package/dist/search-field.d.ts +1 -1
- package/dist/search-field.js +16 -16
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,88 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.16.4
|
|
4
|
+
|
|
5
|
+
A patch: the example console becomes a tab with a REPL, test-only examples render on deployed
|
|
6
|
+
sites, and the doc nav search gets a clear button in every browser. The console's move from a
|
|
7
|
+
panel under the preview to a tab changes how live examples look; it ships as a patch because
|
|
8
|
+
live examples are a developer-facing tool. It does remove the panel's styling hooks: the
|
|
9
|
+
`[part="console"]` element, the `-has-console` class, and `console` from the `ExampleParts`
|
|
10
|
+
type. Nothing in tosijs-ui used them, and a site that styled the 1.16.2 panel should restyle
|
|
11
|
+
`.example-console` in the tab instead. `dist/iife.js` grows by 1.7 kB gzip (477.0 → 478.7 kB).
|
|
12
|
+
|
|
13
|
+
### The example console is a tab, and a REPL
|
|
14
|
+
|
|
15
|
+
What an example logs now appears in a **Console** tab beside its code, instead of a panel under
|
|
16
|
+
its preview, so the preview is never squeezed. The tab is also a REPL: type an expression and
|
|
17
|
+
press Enter to evaluate it inside the example's own scope (its top-level variables and
|
|
18
|
+
functions, `preview`, its `console`, the page's modules), as in the browser's console: input
|
|
19
|
+
gives its value, `await` works, Shift+Enter adds a line, and ↑/↓ recall what you've entered.
|
|
20
|
+
The REPL costs nothing until it's used: the first input re-runs the example once to give it
|
|
21
|
+
the example's scope. The error that stopped an
|
|
22
|
+
example appears there too. `exampleConsole: false` and `{"console": false}` still turn it off.
|
|
23
|
+
|
|
24
|
+
### A test-only example shows its results on deployed sites too
|
|
25
|
+
|
|
26
|
+
A ` ```test ` fence with nothing beside it shows its results as its body, but it ran its tests
|
|
27
|
+
only when the page's test toggle was on. That toggle is off by default anywhere but localhost,
|
|
28
|
+
so on every deployed site the example was an empty box. It now always runs its tests, because
|
|
29
|
+
they are its content; ordinary examples still follow the toggle.
|
|
30
|
+
|
|
31
|
+
### The doc navigation search has a clear button in every browser
|
|
32
|
+
|
|
33
|
+
The doc system's nav search relied on `type="search"` for its clear (✕) button, which browsers
|
|
34
|
+
draw inconsistently: Chromium only while the field is focused, Safari while it has text, and
|
|
35
|
+
Firefox never. It now has its own ✕, shown whenever there's text, with the native one hidden so
|
|
36
|
+
there's never two. Escape clears it as well.
|
|
37
|
+
|
|
38
|
+
## 1.16.3
|
|
39
|
+
|
|
40
|
+
A patch: iPhone menus that stay reachable, a Docs tab for live examples, and a way to declare
|
|
41
|
+
your dialects once in the site config. Nothing changes for existing code, with one behaviour
|
|
42
|
+
fix to know about: menus and other popped floats no longer close when an unrelated scroller
|
|
43
|
+
on the page scrolls, only when a scroll moves the element they came from. `dist/iife.js` grows
|
|
44
|
+
by 2.1 kB gzip (474.9 → 477.0 kB). 1.4 kB of that is the Docs tab loading its markdown renderer
|
|
45
|
+
on first use: that keeps `marked` out of `tosijs-ui/live-example` for ESM apps, but the iife
|
|
46
|
+
already contains the renderer, so there it only adds lazy-loading wrappers.
|
|
47
|
+
|
|
48
|
+
### A Docs tab for live examples (#184)
|
|
49
|
+
|
|
50
|
+
A dialect can document its source: `registerDialect(name, { …, docs(source, options) })` returns
|
|
51
|
+
markdown, shown in a **Docs** tab beside the code. tjs-lang asked for it: in TJS the signature
|
|
52
|
+
*is* the documentation. It's called only when someone opens the code panel, and again as they
|
|
53
|
+
edit, so readers pay nothing. It works for `run` dialects too.
|
|
54
|
+
|
|
55
|
+
The built-in `tjs` fills the tab from tjs-lang's own generator (`generateDocsMarkdown`) as soon
|
|
56
|
+
as tjs-lang's browser bundle exports it; it doesn't yet, as of 0.14.0-rc.1. Until then, a site
|
|
57
|
+
with the full tjs-lang can supply `docs` through a `tjs` override.
|
|
58
|
+
|
|
59
|
+
### Declare your dialects once, in the site config (#2463)
|
|
60
|
+
|
|
61
|
+
`dialects: ['ajs']` in a `tosijs-ui/site` config, plus any built-in you replace
|
|
62
|
+
(`dialects: ['ajs', 'tjs']`), tells the build what your bundle registers with `registerDialect`:
|
|
63
|
+
|
|
64
|
+
- The build-time highlighter leaves those fences alone, even when the name is also a Prism
|
|
65
|
+
grammar. Before, this needed a separate `registerLiveLanguage` call.
|
|
66
|
+
- A replaced built-in isn't checked or pre-transpiled with the installed tjs-lang. That check
|
|
67
|
+
used to fail correct examples written for the site's own build.
|
|
68
|
+
- On the page, a declared dialect that nothing registered is reported in the console, by name.
|
|
69
|
+
|
|
70
|
+
### Menus on iPhone stay reachable (#2460)
|
|
71
|
+
|
|
72
|
+
A long menu popped low on an iPhone ran below the visible screen, and scrolling the page to
|
|
73
|
+
reach its last items dismissed it.
|
|
74
|
+
|
|
75
|
+
- **Floats are sized to what's visible.** `positionFloat` now measures against
|
|
76
|
+
`window.visualViewport` instead of `100vh`/`100vw`. On Mobile Safari `100vh` is the large
|
|
77
|
+
viewport (as if the URL bar were hidden). A long menu now fits and scrolls inside itself.
|
|
78
|
+
Desktop browsers, where the two agree, see no change.
|
|
79
|
+
- **Scrolling past a menu's end doesn't scroll the page** (`overscroll-behavior: contain`).
|
|
80
|
+
- **A float closes only for a scroll that moves its anchor:** the page, or a scroller that
|
|
81
|
+
contains the element it was popped from. Before, any scroll anywhere closed a menu or
|
|
82
|
+
tooltip, such as a sidebar or a table re-rendering. `<tosi-float>` gained an `anchor`
|
|
83
|
+
property (`positionFloat`, and so `popFloat`, sets it); a submenu closes with its parent, and `<tosi-search-field>` now uses this rule rather than a
|
|
84
|
+
copy of its own.
|
|
85
|
+
|
|
3
86
|
## 1.16.2
|
|
4
87
|
|
|
5
88
|
A patch: live examples show what they log, a spinner while a `run` dialect works, and three
|
package/dist/doc-browser.js
CHANGED
|
@@ -344,6 +344,44 @@ function logoMark(logo, projectLinks) {
|
|
|
344
344
|
mark.classList.add('logo-mark');
|
|
345
345
|
return mark;
|
|
346
346
|
}
|
|
347
|
+
// The nav search box: the field, and its own clear button over the field's right end.
|
|
348
|
+
const navSearchStyleSpec = {
|
|
349
|
+
'.nav-search': {
|
|
350
|
+
position: 'relative',
|
|
351
|
+
margin: '5px',
|
|
352
|
+
},
|
|
353
|
+
'.nav-search input[type="search"]': {
|
|
354
|
+
width: '100%',
|
|
355
|
+
boxSizing: 'border-box',
|
|
356
|
+
// room for the clear button, so text never runs under it
|
|
357
|
+
paddingRight: varDefault.touchSize('44px'),
|
|
358
|
+
},
|
|
359
|
+
'.nav-search input[type="search"]::-webkit-search-cancel-button': {
|
|
360
|
+
display: 'none',
|
|
361
|
+
},
|
|
362
|
+
'.nav-search .nav-search-clear': {
|
|
363
|
+
position: 'absolute',
|
|
364
|
+
right: 0,
|
|
365
|
+
top: 0,
|
|
366
|
+
bottom: 0,
|
|
367
|
+
display: 'inline-flex',
|
|
368
|
+
alignItems: 'center',
|
|
369
|
+
justifyContent: 'center',
|
|
370
|
+
width: varDefault.touchSize('44px'),
|
|
371
|
+
padding: 0,
|
|
372
|
+
border: 'none',
|
|
373
|
+
boxShadow: 'none',
|
|
374
|
+
background: 'transparent',
|
|
375
|
+
color: 'inherit',
|
|
376
|
+
opacity: '0.5',
|
|
377
|
+
},
|
|
378
|
+
'.nav-search .nav-search-clear:hover, .nav-search .nav-search-clear:focus-visible': {
|
|
379
|
+
opacity: '1',
|
|
380
|
+
},
|
|
381
|
+
'.nav-search .nav-search-clear[hidden]': {
|
|
382
|
+
display: 'none',
|
|
383
|
+
},
|
|
384
|
+
};
|
|
347
385
|
const logoMarkStyleSpec = {
|
|
348
386
|
'.logo-mark': {
|
|
349
387
|
height: 'var(--tosi-logo-mark-size, 32px)',
|
|
@@ -565,8 +603,32 @@ export function createDocBrowser(options) {
|
|
|
565
603
|
// section; while typing, matching sections expand via the needle branch.
|
|
566
604
|
refreshNav(true);
|
|
567
605
|
});
|
|
606
|
+
/*
|
|
607
|
+
The nav search's clear (✕) button. `type="search"` alone gave a clear affordance only in
|
|
608
|
+
some browsers and some states: Chromium shows its ✕ only while the field is focused, Safari
|
|
609
|
+
while it has text, Firefox never. So the field has its own, shown whenever there is text,
|
|
610
|
+
and the native one is hidden so there is never two. Escape clears too.
|
|
611
|
+
*/
|
|
612
|
+
const updateSearchClear = () => {
|
|
613
|
+
searchClear.hidden = searchField.value === '';
|
|
614
|
+
};
|
|
615
|
+
const clearSearch = () => {
|
|
616
|
+
searchField.value = '';
|
|
617
|
+
updateSearchClear();
|
|
618
|
+
filterDocs();
|
|
619
|
+
searchField.focus();
|
|
620
|
+
};
|
|
621
|
+
const searchInput = () => {
|
|
622
|
+
updateSearchClear();
|
|
623
|
+
filterDocs();
|
|
624
|
+
};
|
|
625
|
+
const searchKeydown = (event) => {
|
|
626
|
+
if (event.key === 'Escape' && searchField.value !== '') {
|
|
627
|
+
event.preventDefault();
|
|
628
|
+
clearSearch();
|
|
629
|
+
}
|
|
630
|
+
};
|
|
568
631
|
const searchField = input({
|
|
569
|
-
slot: 'nav',
|
|
570
632
|
placeholder: 'search',
|
|
571
633
|
// A placeholder is not a label: it is not a reliable accessible name, and it
|
|
572
634
|
// disappears the moment anything is typed. This is the only text input in the
|
|
@@ -574,12 +636,18 @@ export function createDocBrowser(options) {
|
|
|
574
636
|
// site built with `tosijs-ui/site`.
|
|
575
637
|
'aria-label': 'Search documentation',
|
|
576
638
|
type: 'search',
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
margin: '5px',
|
|
580
|
-
},
|
|
581
|
-
onInput: filterDocs,
|
|
639
|
+
onInput: searchInput,
|
|
640
|
+
onKeydown: searchKeydown,
|
|
582
641
|
});
|
|
642
|
+
const searchClear = button({
|
|
643
|
+
type: 'button',
|
|
644
|
+
class: 'nav-search-clear',
|
|
645
|
+
title: 'Clear search',
|
|
646
|
+
'aria-label': 'Clear search',
|
|
647
|
+
hidden: true,
|
|
648
|
+
onClick: clearSearch,
|
|
649
|
+
}, icons.x());
|
|
650
|
+
const searchBox = div({ slot: 'nav', class: 'nav-search' }, searchField, searchClear);
|
|
583
651
|
// Memory routing is decoupled from the page URL, so it ignores browser
|
|
584
652
|
// back/forward (the host owns history, if any).
|
|
585
653
|
if (!memoryRouting) {
|
|
@@ -1462,7 +1530,7 @@ export function createDocBrowser(options) {
|
|
|
1462
1530
|
// The sidenav has just introduced itself; give it the layout the doc asked for.
|
|
1463
1531
|
applyFullScreen();
|
|
1464
1532
|
},
|
|
1465
|
-
},
|
|
1533
|
+
}, searchBox, navContent, div({
|
|
1466
1534
|
style: {
|
|
1467
1535
|
position: 'relative',
|
|
1468
1536
|
overflowY: 'scroll',
|
|
@@ -1475,6 +1543,7 @@ export function createDocBrowser(options) {
|
|
|
1475
1543
|
StyleSheet('test-indicators', testIndicatorStyleSpec);
|
|
1476
1544
|
// Header brand-mark sizing (icon / <img> / inline-<svg>), one CSS var each.
|
|
1477
1545
|
StyleSheet('doc-logo-mark', logoMarkStyleSpec);
|
|
1546
|
+
StyleSheet('doc-nav-search', navSearchStyleSpec);
|
|
1478
1547
|
// Floating widget for test status
|
|
1479
1548
|
const testWidget = button({
|
|
1480
1549
|
class: 'test-widget',
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
/** Fence languages that EXECUTE. Everything else is display-only. */
|
|
2
2
|
export declare const EXECUTABLE_LANGS: Set<string>;
|
|
3
|
+
/**
|
|
4
|
+
* The languages that are an example's SOURCE block (its one executable slot). `js`, `tjs` and
|
|
5
|
+
* `ts` are built in; `registerLiveLanguage` adds more (`registerDialect` in
|
|
6
|
+
* `tosijs-ui/live-example` calls it, so a registered dialect's fences run).
|
|
7
|
+
*
|
|
8
|
+
* Kept here, beside `EXECUTABLE_LANGS`, because the same fence is judged in four places
|
|
9
|
+
* (grouping, save-to-source, the highlighter, the ePub) and they must all see a registered
|
|
10
|
+
* dialect or none of them may.
|
|
11
|
+
*/
|
|
12
|
+
export declare const BUILT_IN_DIALECTS: ReadonlySet<string>;
|
|
13
|
+
/**
|
|
14
|
+
* `SiteConfig.dialects` as the rest of the system compares it: lowercased (fence languages
|
|
15
|
+
* are lowercase), so `dialects: ['TJS']` means `tjs` everywhere, not in some places.
|
|
16
|
+
*/
|
|
17
|
+
export declare function normalizeDialectNames(names?: readonly string[]): string[];
|
|
3
18
|
/**
|
|
4
19
|
* Make fences in `lang` live examples, as an example's source block.
|
|
5
20
|
*
|
|
@@ -34,7 +34,19 @@ export const EXECUTABLE_LANGS = new Set([
|
|
|
34
34
|
* (grouping, save-to-source, the highlighter, the ePub) and they must all see a registered
|
|
35
35
|
* dialect or none of them may.
|
|
36
36
|
*/
|
|
37
|
-
const
|
|
37
|
+
export const BUILT_IN_DIALECTS = new Set([
|
|
38
|
+
'js',
|
|
39
|
+
'ts',
|
|
40
|
+
'tjs',
|
|
41
|
+
]);
|
|
42
|
+
const SOURCE_LANGS = new Set(BUILT_IN_DIALECTS);
|
|
43
|
+
/**
|
|
44
|
+
* `SiteConfig.dialects` as the rest of the system compares it: lowercased (fence languages
|
|
45
|
+
* are lowercase), so `dialects: ['TJS']` means `tjs` everywhere, not in some places.
|
|
46
|
+
*/
|
|
47
|
+
export function normalizeDialectNames(names = []) {
|
|
48
|
+
return [...new Set(names.map((name) => name.toLowerCase()))];
|
|
49
|
+
}
|
|
38
50
|
/**
|
|
39
51
|
* Make fences in `lang` live examples, as an example's source block.
|
|
40
52
|
*
|
|
@@ -42,11 +42,16 @@ child always assumed 'auto', so a `:static` fence — or any fence under `liveEx
|
|
|
42
42
|
'opt-in'` that never runs — could still hard-fail the build (review major M2).
|
|
43
43
|
*/
|
|
44
44
|
const liveExamples = (process.env.TOSI_LIVE_EXAMPLES || 'auto');
|
|
45
|
+
// Built-ins the site replaces (SiteConfig.dialects), comma-separated (#2463).
|
|
46
|
+
const overriddenDialects = (process.env.TOSI_OVERRIDDEN_DIALECTS || '')
|
|
47
|
+
.split(',')
|
|
48
|
+
.filter(Boolean);
|
|
45
49
|
const corpus = JSON.parse(await Bun.file(docsJson).text());
|
|
46
50
|
const { problems, warnings, bakes, skipped } = await checkExamples(corpus, {
|
|
47
51
|
...(contextKeys.length ? { contextKeys } : {}),
|
|
48
52
|
...(importPrefix ? { importPrefix } : {}),
|
|
49
53
|
liveExamples,
|
|
54
|
+
overriddenDialects,
|
|
50
55
|
});
|
|
51
56
|
// stdout is the channel — the parent parses this. Anything else this process prints
|
|
52
57
|
// (warnings from the transform, say) goes to stderr so it can't corrupt the payload.
|
|
@@ -48,6 +48,12 @@ export declare function checkExamples(docs: Doc[], opts?: {
|
|
|
48
48
|
importPrefix?: string;
|
|
49
49
|
/** Mirrors `SiteConfig.liveExamples`. A fence that will never run must not fail a build. */
|
|
50
50
|
liveExamples?: ExamplePolicy;
|
|
51
|
+
/**
|
|
52
|
+
* Built-in dialects the site replaces with its own transform (`SiteConfig.dialects`):
|
|
53
|
+
* not checked and not pre-transpiled here, because the installed tjs-lang is not what
|
|
54
|
+
* runs them, and checking would fail correct examples written for the site's own (#2463).
|
|
55
|
+
*/
|
|
56
|
+
overriddenDialects?: string[];
|
|
51
57
|
}): Promise<ExampleCheck>;
|
|
52
58
|
/** Format problems for a build log. */
|
|
53
59
|
export declare function formatExampleProblems(problems: ExampleProblem[]): string;
|
|
@@ -122,6 +122,7 @@ export async function checkExamples(docs, opts = {}) {
|
|
|
122
122
|
if (!(await transformAvailable(d)))
|
|
123
123
|
unavailable.add(d);
|
|
124
124
|
const skipped = new Map();
|
|
125
|
+
const overridden = new Set(opts.overriddenDialects ?? []);
|
|
125
126
|
for (const doc of docs) {
|
|
126
127
|
for (const block of collectCodeTokens(doc.text)) {
|
|
127
128
|
/*
|
|
@@ -144,6 +145,8 @@ export async function checkExamples(docs, opts = {}) {
|
|
|
144
145
|
continue;
|
|
145
146
|
// `test` blocks are conventional JS/TS, transpiled as plain js.
|
|
146
147
|
const dialect = block.lang === 'test' ? 'js' : block.lang;
|
|
148
|
+
if (block.lang !== 'test' && overridden.has(dialect))
|
|
149
|
+
continue;
|
|
147
150
|
if (unavailable.has(dialect)) {
|
|
148
151
|
skipped.set(dialect, (skipped.get(dialect) ?? 0) + 1);
|
|
149
152
|
continue;
|
|
@@ -13,10 +13,17 @@ the Bun.build note in orchestrator.ts.
|
|
|
13
13
|
Build-time only. Never import this from browser code.
|
|
14
14
|
*/
|
|
15
15
|
import { buildEpub } from './epub.js';
|
|
16
|
+
import { BUILT_IN_DIALECTS, normalizeDialectNames, registerLiveLanguage, } from '../example-policy.js';
|
|
16
17
|
const payloadPath = process.argv[2];
|
|
17
18
|
if (!payloadPath) {
|
|
18
19
|
console.error('epub-cli: expected a payload file path');
|
|
19
20
|
process.exit(1);
|
|
20
21
|
}
|
|
21
22
|
const { config, opts } = JSON.parse(await Bun.file(payloadPath).text());
|
|
23
|
+
// Declared dialects are live examples here too, or the ePub's example numbering (and so its
|
|
24
|
+
// "run this example live" links) drifts from the page's after the first one (review F3).
|
|
25
|
+
for (const name of normalizeDialectNames(config.dialects)) {
|
|
26
|
+
if (!BUILT_IN_DIALECTS.has(name))
|
|
27
|
+
registerLiveLanguage(name);
|
|
28
|
+
}
|
|
22
29
|
await buildEpub(config, opts ?? {});
|
|
@@ -11,6 +11,8 @@ runs as a SEPARATE subprocess on purpose — importing the full tosijs module or
|
|
|
11
11
|
the icon system here would put src/icon-data.ts into `bun --watch`'s graph and
|
|
12
12
|
cause an endless rebuild loop.
|
|
13
13
|
*/
|
|
14
|
+
import { pageGlobalsHead } from './page-globals.js';
|
|
15
|
+
import { BUILT_IN_DIALECTS, normalizeDialectNames, registerLiveLanguage, } from '../example-policy.js';
|
|
14
16
|
import * as path from 'path';
|
|
15
17
|
import { namedBooks, partitionByBook, DEFAULT_BOOK } from '../book-target.js';
|
|
16
18
|
import { listEpubVolumes, renderEpubDownloads } from './epub-volumes.js';
|
|
@@ -67,6 +69,9 @@ async function buildEpubInChild(config, opts) {
|
|
|
67
69
|
lang: config.lang,
|
|
68
70
|
name: config.name,
|
|
69
71
|
outputDir: config.outputDir,
|
|
72
|
+
// the ePub's "run this example live" links count examples the way the page does, so it
|
|
73
|
+
// must know the site's dialects too (1.16.3 review F3)
|
|
74
|
+
dialects: config.dialects,
|
|
70
75
|
},
|
|
71
76
|
opts,
|
|
72
77
|
};
|
|
@@ -113,7 +118,7 @@ const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
|
113
118
|
* because the health-conscious path is unavailable. A child that runs and reports
|
|
114
119
|
* problems is not a failure — that is the whole point of it.
|
|
115
120
|
*/
|
|
116
|
-
async function checkExamplesInChild(docsJson, importPrefix, contextKeys = [], liveExamples = 'auto') {
|
|
121
|
+
async function checkExamplesInChild(docsJson, importPrefix, contextKeys = [], liveExamples = 'auto', overriddenDialects = []) {
|
|
117
122
|
const cliTs = `${import.meta.dir}/check-examples-cli.ts`;
|
|
118
123
|
const cli = existsSync(cliTs)
|
|
119
124
|
? cliTs
|
|
@@ -129,6 +134,8 @@ async function checkExamplesInChild(docsJson, importPrefix, contextKeys = [], li
|
|
|
129
134
|
...(importPrefix ? { TOSI_IMPORT_PREFIX: importPrefix } : {}),
|
|
130
135
|
// A fence that will never run must not fail a build (major M2).
|
|
131
136
|
TOSI_LIVE_EXAMPLES: liveExamples,
|
|
137
|
+
// Built-ins the site replaces: its transform runs them, not ours (#2463).
|
|
138
|
+
TOSI_OVERRIDDEN_DIALECTS: overriddenDialects.join(','),
|
|
132
139
|
},
|
|
133
140
|
});
|
|
134
141
|
// Drain BOTH pipes while awaiting exit. An undrained pipe fills its buffer, the
|
|
@@ -657,7 +664,7 @@ export async function buildSite(config, opts = {}) {
|
|
|
657
664
|
if (config.checkExamples !== false) {
|
|
658
665
|
const { problems, warnings, bakes } = await checkExamplesInChild(DOCS_JSON, resolverPrefix, typeof config.checkExamples === 'object'
|
|
659
666
|
? config.checkExamples.contextKeys ?? []
|
|
660
|
-
: [], config.liveExamples ?? 'auto');
|
|
667
|
+
: [], config.liveExamples ?? 'auto', normalizeDialectNames(config.dialects).filter((name) => BUILT_IN_DIALECTS.has(name)));
|
|
661
668
|
exampleBakes = bakes;
|
|
662
669
|
// Unsupported imports don't fail the build — the code isn't broken, it just
|
|
663
670
|
// can't run in the doc environment (almost always illustrative code that
|
|
@@ -1199,18 +1206,9 @@ export async function buildSite(config, opts = {}) {
|
|
|
1199
1206
|
catch {
|
|
1200
1207
|
// tjs-lang not installed — live examples fall back to the CDN chain
|
|
1201
1208
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
byte-for-byte and no existing corpus moves.
|
|
1206
|
-
*/
|
|
1207
|
-
const policyHead = config.liveExamples === 'opt-in'
|
|
1208
|
-
? `<script>globalThis.__TOSI_EXAMPLE_POLICY="opt-in"</script>`
|
|
1209
|
-
: '';
|
|
1210
|
-
// Likewise only when switched off, so a default site's HTML does not change.
|
|
1211
|
-
const consoleHead = config.exampleConsole === false
|
|
1212
|
-
? `<script>globalThis.__TOSI_EXAMPLE_CONSOLE=false</script>`
|
|
1213
|
-
: '';
|
|
1209
|
+
// The site config's client-side switches (example policy, example console), stamped as
|
|
1210
|
+
// globals in <head> — only when they differ from the default. See page-globals.ts.
|
|
1211
|
+
const globalsHead = pageGlobalsHead(config);
|
|
1214
1212
|
// Optional (tjs-lang 0.11+): the import-resolver service worker. Lets live examples
|
|
1215
1213
|
// import real npm packages from anywhere — bare specifiers the doc-system doesn't
|
|
1216
1214
|
// inject become `/<prefix>/<spec>` requests the worker resolves + caches. GATED behind
|
|
@@ -1405,6 +1403,12 @@ export async function buildSite(config, opts = {}) {
|
|
|
1405
1403
|
.digest('hex')
|
|
1406
1404
|
.slice(0, 12);
|
|
1407
1405
|
const docs = JSON.parse(docsJsonText);
|
|
1406
|
+
// Declared dialects are live fences at build time too, so the static highlighter
|
|
1407
|
+
// leaves their source alone (#2463). Idempotent: a watch rebuild re-adds the same names.
|
|
1408
|
+
for (const name of normalizeDialectNames(config.dialects)) {
|
|
1409
|
+
if (!BUILT_IN_DIALECTS.has(name))
|
|
1410
|
+
registerLiveLanguage(name);
|
|
1411
|
+
}
|
|
1408
1412
|
const pageCount = await generateSite({
|
|
1409
1413
|
liveExamples: config.liveExamples,
|
|
1410
1414
|
docs,
|
|
@@ -1423,13 +1427,7 @@ export async function buildSite(config, opts = {}) {
|
|
|
1423
1427
|
// instead of the classic IIFE. See the ESM hydration bundle above.
|
|
1424
1428
|
hydrateUrl: hydrateName ? `/${hydrateName}` : undefined,
|
|
1425
1429
|
bakes: exampleBakes,
|
|
1426
|
-
headExtra: [
|
|
1427
|
-
config.headExtra,
|
|
1428
|
-
tjsHead,
|
|
1429
|
-
policyHead,
|
|
1430
|
-
consoleHead,
|
|
1431
|
-
importResolverHead,
|
|
1432
|
-
]
|
|
1430
|
+
headExtra: [config.headExtra, tjsHead, globalsHead, importResolverHead]
|
|
1433
1431
|
.filter(Boolean)
|
|
1434
1432
|
.join('') || undefined,
|
|
1435
1433
|
scriptUrl: config.scriptUrl,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The client-side switches a site config sets, stamped into each page's <head> as globals the
|
|
3
|
+
bundle reads when it runs (so they must come BEFORE the bundle script — generate-site puts
|
|
4
|
+
`headExtra` in <head>, and the bundle loads at the end of <body>).
|
|
5
|
+
|
|
6
|
+
Each is emitted only when it differs from the default, so a default site's HTML is unchanged
|
|
7
|
+
byte-for-byte when a new switch is added.
|
|
8
|
+
*/
|
|
9
|
+
import { normalizeDialectNames } from '../example-policy.js';
|
|
10
|
+
export function pageGlobalsHead(config) {
|
|
11
|
+
return [
|
|
12
|
+
// #140: which fences run
|
|
13
|
+
config.liveExamples === 'opt-in'
|
|
14
|
+
? `<script>globalThis.__TOSI_EXAMPLE_POLICY="opt-in"</script>`
|
|
15
|
+
: '',
|
|
16
|
+
// 1.16.2: the example console
|
|
17
|
+
config.exampleConsole === false
|
|
18
|
+
? `<script>globalThis.__TOSI_EXAMPLE_CONSOLE=false</script>`
|
|
19
|
+
: '',
|
|
20
|
+
// 1.16.3: the dialects the site says its bundle registers, so the page can check
|
|
21
|
+
config.dialects?.length
|
|
22
|
+
? `<script>globalThis.__TOSI_DIALECTS=${JSON.stringify(normalizeDialectNames(config.dialects)).replace(/</g, '\\u003c')}</script>`
|
|
23
|
+
: '',
|
|
24
|
+
].join('');
|
|
25
|
+
}
|
|
@@ -130,12 +130,24 @@ export interface SiteConfig {
|
|
|
130
130
|
*/
|
|
131
131
|
liveExamples?: 'auto' | 'opt-in';
|
|
132
132
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
133
|
+
* Give live examples a Console tab: what the example logs, and a REPL in its scope (default
|
|
134
|
+
* `true`; a tab since 1.16.4, a panel under the preview in 1.16.2–1.16.3). `false` turns it off site-wide;
|
|
135
135
|
* logs still reach the browser's devtools. One example opts out with the fence option
|
|
136
136
|
* `{"console": false}`.
|
|
137
137
|
*/
|
|
138
138
|
exampleConsole?: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* The live-example dialects your bundle registers with `registerDialect` (1.16.3, #2463):
|
|
141
|
+
* your own (`['ajs']`) and any built-in you replace (`['tjs']`). Declaring them here is
|
|
142
|
+
* what keeps the build and the page in agreement:
|
|
143
|
+
*
|
|
144
|
+
* - a declared language is a live example at build time too, so the static highlighter
|
|
145
|
+
* leaves its source alone (otherwise a name that is also a Prism grammar is tokenized);
|
|
146
|
+
* - a declared BUILT-IN (`js`, `tjs`, `ts`) is not checked or pre-transpiled by the build,
|
|
147
|
+
* because your transform is the one that runs;
|
|
148
|
+
* - on the page, a declared dialect nobody registered is reported in the console, by name.
|
|
149
|
+
*/
|
|
150
|
+
dialects?: string[];
|
|
139
151
|
/** modules to leave external in the bundle, e.g. ['jolt-physics'] */
|
|
140
152
|
bundleExternals?: string[];
|
|
141
153
|
/**
|
package/dist/float.d.ts
CHANGED
|
@@ -7,6 +7,11 @@ declare const TosiFloat_base: import("tosijs").WithAttributes<{
|
|
|
7
7
|
export declare class TosiFloat extends TosiFloat_base {
|
|
8
8
|
static preferredTagName: string;
|
|
9
9
|
static floats: Set<TosiFloat>;
|
|
10
|
+
/**
|
|
11
|
+
* The element this float was popped from (set by `popFloat`). Only a scroll that moves it
|
|
12
|
+
* (the page, or a scroller containing it) triggers `remainOnScroll`. `null`: any scroll does.
|
|
13
|
+
*/
|
|
14
|
+
anchor: Element | null;
|
|
10
15
|
content: HTMLSlotElement;
|
|
11
16
|
static shadowStyleSpec: {
|
|
12
17
|
':host': {
|
package/dist/float.js
CHANGED
|
@@ -82,8 +82,10 @@ shadow and background and so on.
|
|
|
82
82
|
- `remain-on-resize` 'remove' | 'hide' | 'remain' — by default, floats will hide if the window is resized
|
|
83
83
|
- `remain-on-scroll` 'remain' | 'remove' | 'hide' — by default, floats will remain if the document is scrolled
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
A float made by `popFloat` (menus, tooltips) has an **anchor**, the element it was popped from, and only a
|
|
86
|
+
scroll that MOVES the anchor (the page, or a scroller containing it) hides or removes it. Before 1.16.3 any
|
|
87
|
+
scroll anywhere did, so filtering a table on every keystroke (which scrolls it) closed a menu or hint list the
|
|
88
|
+
moment it opened. A float with no anchor still reacts to any scroll outside itself.
|
|
87
89
|
|
|
88
90
|
To prevent dragging for an interior element (e.g. if you want a floating palette with buttons or input fields)
|
|
89
91
|
just add the `no-drag` class to an element or its container.
|
|
@@ -99,6 +101,11 @@ export class TosiFloat extends withAttributes({
|
|
|
99
101
|
}) {
|
|
100
102
|
static preferredTagName = 'tosi-float';
|
|
101
103
|
static floats = new Set();
|
|
104
|
+
/**
|
|
105
|
+
* The element this float was popped from (set by `popFloat`). Only a scroll that moves it
|
|
106
|
+
* (the page, or a scroller containing it) triggers `remainOnScroll`. `null`: any scroll does.
|
|
107
|
+
*/
|
|
108
|
+
anchor = null;
|
|
102
109
|
content = slot();
|
|
103
110
|
static shadowStyleSpec = {
|
|
104
111
|
':host': {
|
|
@@ -154,6 +161,40 @@ export const XinFloat = TosiFloat;
|
|
|
154
161
|
export const tosiFloat = TosiFloat.elementCreator();
|
|
155
162
|
/** @deprecated Use tosiFloat instead */
|
|
156
163
|
export const xinFloat = tosiFloat;
|
|
164
|
+
/*
|
|
165
|
+
Does this scroll move the anchor? The page scrolling does; so does any scroller that contains it,
|
|
166
|
+
across shadow roots (a menu popped from inside a component's shadow DOM is still moved by a
|
|
167
|
+
scroller around the component). Any other scroller — a table re-filtering, a sidebar — doesn't.
|
|
168
|
+
*/
|
|
169
|
+
function scrollMovesAnchor(scrolled, anchor) {
|
|
170
|
+
if (anchor === null)
|
|
171
|
+
return true;
|
|
172
|
+
if (scrolled === document ||
|
|
173
|
+
scrolled === document.documentElement ||
|
|
174
|
+
scrolled === document.body)
|
|
175
|
+
return true;
|
|
176
|
+
if (!(scrolled instanceof Node))
|
|
177
|
+
return true;
|
|
178
|
+
const visited = new Set(); // floats anchored inside each other must not loop
|
|
179
|
+
for (let node = anchor; node && !visited.has(node);) {
|
|
180
|
+
visited.add(node);
|
|
181
|
+
if (node === scrolled)
|
|
182
|
+
return true;
|
|
183
|
+
/*
|
|
184
|
+
A float's anchor can be INSIDE another float — a submenu's anchor is an item of its parent
|
|
185
|
+
menu, and the parent float is mounted on <body>, so walking up from it never meets the
|
|
186
|
+
scroller around the root menu button. Continue from the containing float's own anchor:
|
|
187
|
+
a chain of menus moves (and closes) together. Before this, scrolling that scroller closed
|
|
188
|
+
the parent and left the submenu on screen, orphaned (1.16.3 review B1).
|
|
189
|
+
*/
|
|
190
|
+
if (node instanceof TosiFloat && node.anchor) {
|
|
191
|
+
node = node.anchor;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
node = node.parentNode ?? node.host ?? null;
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
157
198
|
// Register the global reposition/dismiss handlers on first float connecting,
|
|
158
199
|
// not at import (keeps the module side-effect-free for tree-shaking). They
|
|
159
200
|
// iterate TosiFloat.floats, so they're no-ops until a float exists — registered
|
|
@@ -179,6 +220,8 @@ function ensureFloatListeners() {
|
|
|
179
220
|
return;
|
|
180
221
|
}
|
|
181
222
|
Array.from(TosiFloat.floats).forEach((float) => {
|
|
223
|
+
if (!scrollMovesAnchor(event.target, float.anchor))
|
|
224
|
+
return;
|
|
182
225
|
if (float.remainOnScroll === 'hide') {
|
|
183
226
|
float.hidden = true;
|
|
184
227
|
}
|