tosijs-ui 1.16.1 → 1.16.3
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 +95 -0
- package/dist/carousel.d.ts +30 -5
- package/dist/carousel.js +110 -6
- 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 -11
- 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 +19 -0
- package/dist/float.d.ts +5 -0
- package/dist/float.js +45 -2
- package/dist/iife.js +136 -136
- package/dist/iife.js.map +37 -34
- 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 +88 -0
- package/dist/live-example/component.js +221 -8
- package/dist/live-example/dialects.d.ts +18 -0
- package/dist/live-example/dialects.js +17 -3
- package/dist/live-example/example-console.d.ts +25 -0
- package/dist/live-example/example-console.js +138 -0
- package/dist/live-example/execution.d.ts +5 -0
- package/dist/live-example/execution.js +48 -15
- package/dist/live-example/index.d.ts +1 -0
- package/dist/live-example/index.js +2 -0
- package/dist/live-example/insert-examples.js +30 -1
- package/dist/live-example/styles.d.ts +73 -0
- package/dist/live-example/styles.js +81 -0
- package/dist/live-example/types.d.ts +2 -0
- package/dist/menu.js +3 -0
- package/dist/pop-float.d.ts +13 -0
- package/dist/pop-float.js +62 -2
- package/dist/reduced-motion.d.ts +4 -0
- package/dist/reduced-motion.js +21 -0
- 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 +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,100 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.16.3
|
|
4
|
+
|
|
5
|
+
A patch: iPhone menus that stay reachable, a Docs tab for live examples, and a way to declare
|
|
6
|
+
your dialects once in the site config. Nothing changes for existing code, with one behaviour
|
|
7
|
+
fix to know about: menus and other popped floats no longer close when an unrelated scroller
|
|
8
|
+
on the page scrolls, only when a scroll moves the element they came from. `dist/iife.js` grows
|
|
9
|
+
by 2.1 kB gzip (474.9 → 477.0 kB). 1.4 kB of that is the Docs tab loading its markdown renderer
|
|
10
|
+
on first use: that keeps `marked` out of `tosijs-ui/live-example` for ESM apps, but the iife
|
|
11
|
+
already contains the renderer, so there it only adds lazy-loading wrappers.
|
|
12
|
+
|
|
13
|
+
### A Docs tab for live examples (#184)
|
|
14
|
+
|
|
15
|
+
A dialect can document its source: `registerDialect(name, { …, docs(source, options) })` returns
|
|
16
|
+
markdown, shown in a **Docs** tab beside the code. tjs-lang asked for it: in TJS the signature
|
|
17
|
+
*is* the documentation. It's called only when someone opens the code panel, and again as they
|
|
18
|
+
edit, so readers pay nothing. It works for `run` dialects too.
|
|
19
|
+
|
|
20
|
+
The built-in `tjs` fills the tab from tjs-lang's own generator (`generateDocsMarkdown`) as soon
|
|
21
|
+
as tjs-lang's browser bundle exports it; it doesn't yet, as of 0.14.0-rc.1. Until then, a site
|
|
22
|
+
with the full tjs-lang can supply `docs` through a `tjs` override.
|
|
23
|
+
|
|
24
|
+
### Declare your dialects once, in the site config (#2463)
|
|
25
|
+
|
|
26
|
+
`dialects: ['ajs']` in a `tosijs-ui/site` config, plus any built-in you replace
|
|
27
|
+
(`dialects: ['ajs', 'tjs']`), tells the build what your bundle registers with `registerDialect`:
|
|
28
|
+
|
|
29
|
+
- The build-time highlighter leaves those fences alone, even when the name is also a Prism
|
|
30
|
+
grammar. Before, this needed a separate `registerLiveLanguage` call.
|
|
31
|
+
- A replaced built-in isn't checked or pre-transpiled with the installed tjs-lang. That check
|
|
32
|
+
used to fail correct examples written for the site's own build.
|
|
33
|
+
- On the page, a declared dialect that nothing registered is reported in the console, by name.
|
|
34
|
+
|
|
35
|
+
### Menus on iPhone stay reachable (#2460)
|
|
36
|
+
|
|
37
|
+
A long menu popped low on an iPhone ran below the visible screen, and scrolling the page to
|
|
38
|
+
reach its last items dismissed it.
|
|
39
|
+
|
|
40
|
+
- **Floats are sized to what's visible.** `positionFloat` now measures against
|
|
41
|
+
`window.visualViewport` instead of `100vh`/`100vw`. On Mobile Safari `100vh` is the large
|
|
42
|
+
viewport (as if the URL bar were hidden). A long menu now fits and scrolls inside itself.
|
|
43
|
+
Desktop browsers, where the two agree, see no change.
|
|
44
|
+
- **Scrolling past a menu's end doesn't scroll the page** (`overscroll-behavior: contain`).
|
|
45
|
+
- **A float closes only for a scroll that moves its anchor:** the page, or a scroller that
|
|
46
|
+
contains the element it was popped from. Before, any scroll anywhere closed a menu or
|
|
47
|
+
tooltip, such as a sidebar or a table re-rendering. `<tosi-float>` gained an `anchor`
|
|
48
|
+
property (`positionFloat`, and so `popFloat`, sets it); a submenu closes with its parent, and `<tosi-search-field>` now uses this rule rather than a
|
|
49
|
+
copy of its own.
|
|
50
|
+
|
|
51
|
+
## 1.16.2
|
|
52
|
+
|
|
53
|
+
A patch: live examples show what they log, a spinner while a `run` dialect works, and three
|
|
54
|
+
carousel fixes. The one visible change to existing pages is the first: an example that logs
|
|
55
|
+
now shows its output under the preview. `dist/iife.js` grows by 1.8 kB gzip (472.8 → 474.6 kB).
|
|
56
|
+
|
|
57
|
+
### A spinner while a `run` dialect is working (#184)
|
|
58
|
+
|
|
59
|
+
A `run` that waits on the network or an LLM used to leave an empty box that looked broken.
|
|
60
|
+
Now a spinner shows over the example while the run is pending, after a quarter-second so a
|
|
61
|
+
fast run never flashes it, and a re-run replaces it. Under `prefers-reduced-motion` it is a
|
|
62
|
+
still ring. Asked for by tjs-lang, whose weather and GitHub examples take 5 to 10 seconds.
|
|
63
|
+
|
|
64
|
+
### `<tosi-carousel>`: tappable dots, `auto` that waits, resize-safe (#204)
|
|
65
|
+
|
|
66
|
+
Found by tosijs-virta using a carousel as a full-bleed hero on a phone.
|
|
67
|
+
|
|
68
|
+
- **The dots are tappable.** Each still draws an 8px dot, but answers taps across its whole
|
|
69
|
+
slot and 44px tall (`--carousel-dot-hit-size`), so a tap that just misses a dot no longer
|
|
70
|
+
opens the slide underneath.
|
|
71
|
+
- **`auto` holds still while someone is looking:** under a mouse hover, while keyboard focus is
|
|
72
|
+
inside it, and always under `prefers-reduced-motion: reduce` (which also makes paging jump
|
|
73
|
+
instead of animating). A touch is never treated as a hover.
|
|
74
|
+
- **A resize re-seats the current slide** instead of leaving it partway into the next.
|
|
75
|
+
|
|
76
|
+
### Live examples show what they log
|
|
77
|
+
|
|
78
|
+
What an example logs (`console.log`, `info`, `warn`, `error`, `debug`, `dir`, `table`) now
|
|
79
|
+
appears in a console under its preview, as well as in devtools. tjs-lang asked for it on #184:
|
|
80
|
+
a `tjs` example that logs its results showed a reader an empty box.
|
|
81
|
+
|
|
82
|
+
- Each example gets its own `console`, passed in the way `preview` is. The global console is
|
|
83
|
+
never patched, so examples never see each other's output.
|
|
84
|
+
- The panel appears only when an example logs something. **Existing examples that log now show
|
|
85
|
+
it**; examples that don't look exactly as before.
|
|
86
|
+
- A re-run starts a clean console, and a late log from the previous run (a timer it left
|
|
87
|
+
behind) is not shown. The panel keeps 500 lines and counts the rest.
|
|
88
|
+
- **To turn it off:** `exampleConsole: false` in a `tosijs-ui/site` config (site-wide, no
|
|
89
|
+
code needed), the fence option `{"console": false}` for one example, or
|
|
90
|
+
`setExampleConsole(false)` from `tosijs-ui/live-example` in your own bundle.
|
|
91
|
+
- An example that declares its own top-level `console` (`const console = …`) keeps it and
|
|
92
|
+
isn't captured, rather than failing to run.
|
|
93
|
+
- Values print readably: strings as written, data as JSON (Maps and Sets included), errors as
|
|
94
|
+
`Name: message`, also for errors and elements from an `:iframe` example.
|
|
95
|
+
- A `run` dialect receives the same console as `context.console`, so a VM can forward its
|
|
96
|
+
output to the panel.
|
|
97
|
+
|
|
3
98
|
## 1.16.1
|
|
4
99
|
|
|
5
100
|
A patch: everything here is additive. Two features, a search field that suggests refinements
|
package/dist/carousel.d.ts
CHANGED
|
@@ -12,7 +12,16 @@ export declare class TosiCarousel extends TosiCarousel_base {
|
|
|
12
12
|
static preferredTagName: string;
|
|
13
13
|
private lastAutoAdvance;
|
|
14
14
|
private interval?;
|
|
15
|
+
private hovered;
|
|
16
|
+
private focusedVisibly;
|
|
17
|
+
private resizeObserver?;
|
|
18
|
+
get autoPaused(): boolean;
|
|
15
19
|
private autoAdvance;
|
|
20
|
+
private handlePointerEnter;
|
|
21
|
+
private handlePointerLeave;
|
|
22
|
+
private handleFocusIn;
|
|
23
|
+
private handleFocusOut;
|
|
24
|
+
private realign;
|
|
16
25
|
private _page;
|
|
17
26
|
get page(): number;
|
|
18
27
|
set page(p: number);
|
|
@@ -28,6 +37,7 @@ export declare class TosiCarousel extends TosiCarousel_base {
|
|
|
28
37
|
_carouselDotCurrentColor: string;
|
|
29
38
|
_carouselDotSize: number;
|
|
30
39
|
_carouselDotSpacing: string;
|
|
40
|
+
_carouselDotHitSize: number;
|
|
31
41
|
_carouselProgressPadding: number;
|
|
32
42
|
_carouselDotTransition: string;
|
|
33
43
|
display: string;
|
|
@@ -87,27 +97,42 @@ export declare class TosiCarousel extends TosiCarousel_base {
|
|
|
87
97
|
display: string;
|
|
88
98
|
};
|
|
89
99
|
':host .dot': {
|
|
100
|
+
position: string;
|
|
101
|
+
zIndex: number;
|
|
102
|
+
display: string;
|
|
103
|
+
placeItems: string;
|
|
104
|
+
flex: string;
|
|
105
|
+
width: string;
|
|
106
|
+
height: string;
|
|
107
|
+
margin: string;
|
|
108
|
+
background: string;
|
|
109
|
+
};
|
|
110
|
+
':host .dot::before': {
|
|
111
|
+
content: string;
|
|
112
|
+
display: string;
|
|
90
113
|
background: string;
|
|
91
114
|
borderRadius: string;
|
|
92
115
|
height: string;
|
|
93
116
|
width: string;
|
|
94
117
|
transition: string;
|
|
95
118
|
};
|
|
96
|
-
':host .dot:not(.current):hover': {
|
|
119
|
+
':host .dot:not(.current):hover::before': {
|
|
97
120
|
background: string;
|
|
98
121
|
height: string;
|
|
99
122
|
width: string;
|
|
100
|
-
margin: string;
|
|
101
123
|
};
|
|
102
|
-
':host .dot:not(.current):active': {
|
|
124
|
+
':host .dot:not(.current):active::before': {
|
|
103
125
|
background: string;
|
|
104
126
|
};
|
|
105
|
-
':host .dot.current': {
|
|
127
|
+
':host .dot.current::before': {
|
|
106
128
|
background: string;
|
|
107
129
|
};
|
|
130
|
+
':host .dot:focus-visible::before': {
|
|
131
|
+
boxShadow: string;
|
|
132
|
+
};
|
|
108
133
|
':host::part(progress)': {
|
|
109
134
|
display: string;
|
|
110
|
-
gap:
|
|
135
|
+
gap: number;
|
|
111
136
|
justifyContent: string;
|
|
112
137
|
padding: string;
|
|
113
138
|
};
|
package/dist/carousel.js
CHANGED
|
@@ -53,11 +53,25 @@ This is a minimalist carousel component that supports the usual stuff.
|
|
|
53
53
|
- `loop` (boolean, false by default) causes next/previous buttons to loop
|
|
54
54
|
- `auto` (number, 0 [seconds] by default) if > 0, automatically advances after that many seconds (always loops!)
|
|
55
55
|
|
|
56
|
+
## Behaviour
|
|
57
|
+
|
|
58
|
+
- **`auto` holds still while someone is looking:** while a mouse hovers over it, while keyboard
|
|
59
|
+
focus is inside it, and always when the reader asks for reduced motion
|
|
60
|
+
(`prefers-reduced-motion: reduce`, which also makes paging jump instead of animating). A swipe
|
|
61
|
+
or a chosen slide restarts the countdown. A touch is never treated as a hover, since nothing
|
|
62
|
+
would ever end it.
|
|
63
|
+
- **The dots are tappable:** each draws a `--carousel-dot-size` dot (8px) but answers taps across
|
|
64
|
+
its whole slot and `--carousel-dot-hit-size` (44px) tall, so a tap that just misses a dot
|
|
65
|
+
doesn't open the slide underneath.
|
|
66
|
+
- **Resizing keeps the current slide in place** (rotation, a window resize) instead of leaving
|
|
67
|
+
it partway into the next one.
|
|
68
|
+
|
|
56
69
|
<tosi-css-var-editor element-selector="tosi-carousel"></tosi-css-var-editor>
|
|
57
70
|
*/
|
|
58
71
|
/*{ "parent": "Components" }*/
|
|
59
72
|
import { elements, vars, withAttributes } from 'tosijs';
|
|
60
73
|
import { icons } from './icons.js';
|
|
74
|
+
import { prefersReducedMotion } from './reduced-motion.js';
|
|
61
75
|
const { button, slot, div } = elements;
|
|
62
76
|
export class TosiCarousel extends withAttributes({
|
|
63
77
|
dots: false,
|
|
@@ -71,11 +85,59 @@ export class TosiCarousel extends withAttributes({
|
|
|
71
85
|
static preferredTagName = 'tosi-carousel';
|
|
72
86
|
lastAutoAdvance = Date.now();
|
|
73
87
|
interval;
|
|
88
|
+
hovered = false;
|
|
89
|
+
focusedVisibly = false;
|
|
90
|
+
resizeObserver;
|
|
91
|
+
/*
|
|
92
|
+
`auto` holds still while someone is looking at it (#204): under a mouse hover, while keyboard
|
|
93
|
+
focus is inside it, and always under `prefers-reduced-motion: reduce`. A swipe already
|
|
94
|
+
restarts the countdown (every scroll resets `lastAutoAdvance`), so a slide someone just chose
|
|
95
|
+
stays up for a full `auto` interval.
|
|
96
|
+
*/
|
|
97
|
+
get autoPaused() {
|
|
98
|
+
return this.hovered || this.focusedVisibly || prefersReducedMotion();
|
|
99
|
+
}
|
|
74
100
|
autoAdvance = () => {
|
|
101
|
+
if (this.auto <= 0)
|
|
102
|
+
return;
|
|
103
|
+
if (this.autoPaused) {
|
|
104
|
+
this.lastAutoAdvance = Date.now();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
75
107
|
if (this.auto > 0 && this.auto * 1000 < Date.now() - this.lastAutoAdvance) {
|
|
76
108
|
this.forward();
|
|
77
109
|
}
|
|
78
110
|
};
|
|
111
|
+
// Mouse only. A phone's tap also sends pointerenter (and focus), and nothing ever sends the
|
|
112
|
+
// matching leave, so treating a touch as "hover" would stop the carousel for good.
|
|
113
|
+
handlePointerEnter = (event) => {
|
|
114
|
+
if (event.pointerType === 'mouse')
|
|
115
|
+
this.hovered = true;
|
|
116
|
+
};
|
|
117
|
+
handlePointerLeave = (event) => {
|
|
118
|
+
if (event.pointerType === 'mouse')
|
|
119
|
+
this.hovered = false;
|
|
120
|
+
};
|
|
121
|
+
// Keyboard focus only (`:focus-visible`), for the same reason: a tap focuses too.
|
|
122
|
+
handleFocusIn = (event) => {
|
|
123
|
+
const target = event.composedPath()[0];
|
|
124
|
+
this.focusedVisibly = target?.matches?.(':focus-visible') === true;
|
|
125
|
+
};
|
|
126
|
+
handleFocusOut = (event) => {
|
|
127
|
+
const next = event.relatedTarget;
|
|
128
|
+
if (!next || !this.contains(next))
|
|
129
|
+
this.focusedVisibly = false;
|
|
130
|
+
};
|
|
131
|
+
// A resize (rotation, a window drag) changes the slide width, so re-seat the current page
|
|
132
|
+
// without animating; otherwise it rests partway into the next slide until it next moves.
|
|
133
|
+
realign = () => {
|
|
134
|
+
if (!this.hydrated)
|
|
135
|
+
return;
|
|
136
|
+
const { scroller } = this.parts;
|
|
137
|
+
cancelAnimationFrame(this.animationFrame);
|
|
138
|
+
this.animationFrame = null;
|
|
139
|
+
scroller.scrollLeft = this.page * scroller.offsetWidth;
|
|
140
|
+
};
|
|
79
141
|
_page = 0;
|
|
80
142
|
get page() {
|
|
81
143
|
return this._page;
|
|
@@ -113,6 +175,8 @@ export class TosiCarousel extends withAttributes({
|
|
|
113
175
|
_carouselDotCurrentColor: '#0008',
|
|
114
176
|
_carouselDotSize: 8,
|
|
115
177
|
_carouselDotSpacing: vars.carouselDotSize,
|
|
178
|
+
// The tappable area around each dot; the dot itself stays --carousel-dot-size (#204).
|
|
179
|
+
_carouselDotHitSize: 44,
|
|
116
180
|
_carouselProgressPadding: 12,
|
|
117
181
|
_carouselDotTransition: '0.125s ease-in-out',
|
|
118
182
|
display: 'flex',
|
|
@@ -171,28 +235,55 @@ export class TosiCarousel extends withAttributes({
|
|
|
171
235
|
':host *::-webkit-scrollbar, *::-webkit-scrollbar-thumb': {
|
|
172
236
|
display: 'none',
|
|
173
237
|
},
|
|
238
|
+
/*
|
|
239
|
+
Each dot is a button drawing a --carousel-dot-size dot (#204). Its hit area is the dot's
|
|
240
|
+
whole PITCH across (dot + spacing, so the strip has no gaps and no two dots overlap) and
|
|
241
|
+
--carousel-dot-hit-size (44px, a touch target) tall. A negative vertical margin gives the
|
|
242
|
+
extra height back to the layout, so the strip is sized as before and the hit area reaches
|
|
243
|
+
past it — over the slide, where an overlaid dot strip sits, which is exactly where a tap
|
|
244
|
+
that just missed a dot used to open the slide instead.
|
|
245
|
+
|
|
246
|
+
Not 44px across: the first cut did that, and with dots 16px apart each dot's hit area
|
|
247
|
+
covered its neighbour's centre, so a tap dead on one dot selected the next.
|
|
248
|
+
*/
|
|
174
249
|
':host .dot': {
|
|
250
|
+
position: 'relative',
|
|
251
|
+
zIndex: 3,
|
|
252
|
+
display: 'grid',
|
|
253
|
+
placeItems: 'center',
|
|
254
|
+
flex: `0 0 calc(${vars.carouselDotSize} + ${vars.carouselDotSpacing})`,
|
|
255
|
+
width: `calc(${vars.carouselDotSize} + ${vars.carouselDotSpacing})`,
|
|
256
|
+
height: vars.carouselDotHitSize,
|
|
257
|
+
margin: `calc((${vars.carouselDotSize} - ${vars.carouselDotHitSize}) / 2) 0`,
|
|
258
|
+
background: 'transparent',
|
|
259
|
+
},
|
|
260
|
+
':host .dot::before': {
|
|
261
|
+
content: '""',
|
|
262
|
+
display: 'block',
|
|
175
263
|
background: vars.carouselButtonColor,
|
|
176
264
|
borderRadius: vars.carouselDotSize,
|
|
177
265
|
height: vars.carouselDotSize,
|
|
178
266
|
width: vars.carouselDotSize,
|
|
179
267
|
transition: vars.carouselDotTransition,
|
|
180
268
|
},
|
|
181
|
-
':host .dot:not(.current):hover': {
|
|
269
|
+
':host .dot:not(.current):hover::before': {
|
|
182
270
|
background: vars.carouselButtonHoverColor,
|
|
183
271
|
height: vars.carouselDotSize150,
|
|
184
272
|
width: vars.carouselDotSize150,
|
|
185
|
-
margin: vars.carouselDotSize_25,
|
|
186
273
|
},
|
|
187
|
-
':host .dot:not(.current):active': {
|
|
274
|
+
':host .dot:not(.current):active::before': {
|
|
188
275
|
background: vars.carouselButtonActiveColor,
|
|
189
276
|
},
|
|
190
|
-
':host .dot.current': {
|
|
277
|
+
':host .dot.current::before': {
|
|
191
278
|
background: vars.carouselDotCurrentColor,
|
|
192
279
|
},
|
|
280
|
+
':host .dot:focus-visible::before': {
|
|
281
|
+
boxShadow: `0 0 0 2px ${vars.carouselButtonActiveColor}`,
|
|
282
|
+
},
|
|
193
283
|
':host::part(progress)': {
|
|
194
284
|
display: 'flex',
|
|
195
|
-
|
|
285
|
+
// the spacing is inside each dot's hit area (see .dot), so no gap between them
|
|
286
|
+
gap: 0,
|
|
196
287
|
justifyContent: 'center',
|
|
197
288
|
padding: vars.carouselProgressPadding,
|
|
198
289
|
},
|
|
@@ -248,7 +339,8 @@ export class TosiCarousel extends withAttributes({
|
|
|
248
339
|
return;
|
|
249
340
|
}
|
|
250
341
|
const elapsed = (Date.now() - timestamp) / 1000;
|
|
251
|
-
if (
|
|
342
|
+
if (prefersReducedMotion() ||
|
|
343
|
+
elapsed >= this.snapDuration ||
|
|
252
344
|
Math.abs(scroller.scrollLeft - position) < 2) {
|
|
253
345
|
scroller.scrollLeft = position;
|
|
254
346
|
this.animationFrame = null;
|
|
@@ -276,11 +368,23 @@ export class TosiCarousel extends withAttributes({
|
|
|
276
368
|
forward.addEventListener('click', this.forward);
|
|
277
369
|
scroller.addEventListener('scroll', this.indicateCurrent);
|
|
278
370
|
progress.addEventListener('click', this.handleDotClick);
|
|
371
|
+
this.addEventListener('pointerenter', this.handlePointerEnter);
|
|
372
|
+
this.addEventListener('pointerleave', this.handlePointerLeave);
|
|
373
|
+
this.addEventListener('focusin', this.handleFocusIn);
|
|
374
|
+
this.addEventListener('focusout', this.handleFocusOut);
|
|
375
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
376
|
+
this.resizeObserver ??= new ResizeObserver(this.realign);
|
|
377
|
+
this.resizeObserver.observe(scroller);
|
|
378
|
+
}
|
|
279
379
|
this.lastAutoAdvance = Date.now();
|
|
380
|
+
clearInterval(this.interval);
|
|
280
381
|
this.interval = setInterval(this.autoAdvance, 100);
|
|
281
382
|
}
|
|
282
383
|
disconnectedCallback() {
|
|
384
|
+
super.disconnectedCallback();
|
|
283
385
|
clearInterval(this.interval);
|
|
386
|
+
this.resizeObserver?.disconnect();
|
|
387
|
+
this.hovered = this.focusedVisibly = false;
|
|
284
388
|
}
|
|
285
389
|
render() {
|
|
286
390
|
super.render();
|
|
@@ -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,14 +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
|
-
: '';
|
|
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);
|
|
1210
1212
|
// Optional (tjs-lang 0.11+): the import-resolver service worker. Lets live examples
|
|
1211
1213
|
// import real npm packages from anywhere — bare specifiers the doc-system doesn't
|
|
1212
1214
|
// inject become `/<prefix>/<spec>` requests the worker resolves + caches. GATED behind
|
|
@@ -1401,6 +1403,12 @@ export async function buildSite(config, opts = {}) {
|
|
|
1401
1403
|
.digest('hex')
|
|
1402
1404
|
.slice(0, 12);
|
|
1403
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
|
+
}
|
|
1404
1412
|
const pageCount = await generateSite({
|
|
1405
1413
|
liveExamples: config.liveExamples,
|
|
1406
1414
|
docs,
|
|
@@ -1419,7 +1427,7 @@ export async function buildSite(config, opts = {}) {
|
|
|
1419
1427
|
// instead of the classic IIFE. See the ESM hydration bundle above.
|
|
1420
1428
|
hydrateUrl: hydrateName ? `/${hydrateName}` : undefined,
|
|
1421
1429
|
bakes: exampleBakes,
|
|
1422
|
-
headExtra: [config.headExtra, tjsHead,
|
|
1430
|
+
headExtra: [config.headExtra, tjsHead, globalsHead, importResolverHead]
|
|
1423
1431
|
.filter(Boolean)
|
|
1424
1432
|
.join('') || undefined,
|
|
1425
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
|
+
}
|
|
@@ -129,6 +129,25 @@ export interface SiteConfig {
|
|
|
129
129
|
* Per-fence, ` ```js:static ` opts a single block out under either policy.
|
|
130
130
|
*/
|
|
131
131
|
liveExamples?: 'auto' | 'opt-in';
|
|
132
|
+
/**
|
|
133
|
+
* Show what a live example logs in a console under its preview (default `true`, 1.16.2).
|
|
134
|
+
* The panel appears only once an example logs something. `false` turns it off site-wide;
|
|
135
|
+
* logs still reach the browser's devtools. One example opts out with the fence option
|
|
136
|
+
* `{"console": false}`.
|
|
137
|
+
*/
|
|
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[];
|
|
132
151
|
/** modules to leave external in the bundle, e.g. ['jolt-physics'] */
|
|
133
152
|
bundleExternals?: string[];
|
|
134
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': {
|