tosijs-ui 1.16.0 → 1.16.2
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 +120 -0
- package/dist/carousel.d.ts +30 -5
- package/dist/carousel.js +110 -6
- package/dist/doc-system/example-policy.d.ts +43 -3
- package/dist/doc-system/example-policy.js +89 -8
- package/dist/doc-system/highlight.d.ts +5 -1
- package/dist/doc-system/highlight.js +11 -14
- package/dist/doc-system/render.js +10 -1
- package/dist/doc-system/site/check-examples.js +8 -12
- package/dist/doc-system/site/epub.js +6 -5
- package/dist/doc-system/site/index.d.ts +1 -0
- package/dist/doc-system/site/index.js +3 -0
- package/dist/doc-system/site/orchestrator.js +11 -1
- package/dist/doc-system/site/site-config.d.ts +7 -0
- package/dist/iife.js +96 -96
- package/dist/iife.js.map +20 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/live-example/component.d.ts +83 -3
- package/dist/live-example/component.js +288 -16
- package/dist/live-example/dialects.d.ts +73 -0
- package/dist/live-example/dialects.js +89 -0
- 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 +3 -0
- package/dist/live-example/index.js +4 -0
- package/dist/live-example/insert-examples.js +51 -21
- package/dist/live-example/save-to-source.js +4 -5
- package/dist/live-example/styles.d.ts +69 -0
- package/dist/live-example/styles.js +76 -0
- package/dist/live-example/types.d.ts +2 -0
- package/dist/reduced-motion.d.ts +4 -0
- package/dist/reduced-motion.js +21 -0
- package/dist/search-field.d.ts +74 -0
- package/dist/search-field.js +742 -0
- package/dist/tag-colors.d.ts +8 -0
- package/dist/tag-colors.js +25 -0
- package/dist/tag-list.d.ts +1 -62
- package/dist/tag-list.js +7 -89
- package/dist/tag.d.ts +63 -0
- package/dist/tag.js +77 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +3 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,125 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.16.2
|
|
4
|
+
|
|
5
|
+
A patch: live examples show what they log, a spinner while a `run` dialect works, and three
|
|
6
|
+
carousel fixes. The one visible change to existing pages is the first: an example that logs
|
|
7
|
+
now shows its output under the preview. `dist/iife.js` grows by 1.8 kB gzip (472.8 → 474.6 kB).
|
|
8
|
+
|
|
9
|
+
### A spinner while a `run` dialect is working (#184)
|
|
10
|
+
|
|
11
|
+
A `run` that waits on the network or an LLM used to leave an empty box that looked broken.
|
|
12
|
+
Now a spinner shows over the example while the run is pending, after a quarter-second so a
|
|
13
|
+
fast run never flashes it, and a re-run replaces it. Under `prefers-reduced-motion` it is a
|
|
14
|
+
still ring. Asked for by tjs-lang, whose weather and GitHub examples take 5 to 10 seconds.
|
|
15
|
+
|
|
16
|
+
### `<tosi-carousel>`: tappable dots, `auto` that waits, resize-safe (#204)
|
|
17
|
+
|
|
18
|
+
Found by tosijs-virta using a carousel as a full-bleed hero on a phone.
|
|
19
|
+
|
|
20
|
+
- **The dots are tappable.** Each still draws an 8px dot, but answers taps across its whole
|
|
21
|
+
slot and 44px tall (`--carousel-dot-hit-size`), so a tap that just misses a dot no longer
|
|
22
|
+
opens the slide underneath.
|
|
23
|
+
- **`auto` holds still while someone is looking:** under a mouse hover, while keyboard focus is
|
|
24
|
+
inside it, and always under `prefers-reduced-motion: reduce` (which also makes paging jump
|
|
25
|
+
instead of animating). A touch is never treated as a hover.
|
|
26
|
+
- **A resize re-seats the current slide** instead of leaving it partway into the next.
|
|
27
|
+
|
|
28
|
+
### Live examples show what they log
|
|
29
|
+
|
|
30
|
+
What an example logs (`console.log`, `info`, `warn`, `error`, `debug`, `dir`, `table`) now
|
|
31
|
+
appears in a console under its preview, as well as in devtools. tjs-lang asked for it on #184:
|
|
32
|
+
a `tjs` example that logs its results showed a reader an empty box.
|
|
33
|
+
|
|
34
|
+
- Each example gets its own `console`, passed in the way `preview` is. The global console is
|
|
35
|
+
never patched, so examples never see each other's output.
|
|
36
|
+
- The panel appears only when an example logs something. **Existing examples that log now show
|
|
37
|
+
it**; examples that don't look exactly as before.
|
|
38
|
+
- A re-run starts a clean console, and a late log from the previous run (a timer it left
|
|
39
|
+
behind) is not shown. The panel keeps 500 lines and counts the rest.
|
|
40
|
+
- **To turn it off:** `exampleConsole: false` in a `tosijs-ui/site` config (site-wide, no
|
|
41
|
+
code needed), the fence option `{"console": false}` for one example, or
|
|
42
|
+
`setExampleConsole(false)` from `tosijs-ui/live-example` in your own bundle.
|
|
43
|
+
- An example that declares its own top-level `console` (`const console = …`) keeps it and
|
|
44
|
+
isn't captured, rather than failing to run.
|
|
45
|
+
- Values print readably: strings as written, data as JSON (Maps and Sets included), errors as
|
|
46
|
+
`Name: message`, also for errors and elements from an `:iframe` example.
|
|
47
|
+
- A `run` dialect receives the same console as `context.console`, so a VM can forward its
|
|
48
|
+
output to the panel.
|
|
49
|
+
|
|
50
|
+
## 1.16.1
|
|
51
|
+
|
|
52
|
+
A patch: everything here is additive. Two features, a search field that suggests refinements
|
|
53
|
+
and pluggable live-example dialects, and nothing that changes how existing code behaves.
|
|
54
|
+
`dist/iife.js` grows by 2.9 kB gzip (469.9 → 472.8 kB), which is both features going through
|
|
55
|
+
the batteries-included bundle; the ESM path pays only for what it imports.
|
|
56
|
+
|
|
57
|
+
### `<tosi-search-field>`: a search box that suggests refinements (snowfox, virta #2459)
|
|
58
|
+
|
|
59
|
+
A new component in the spirit of Finder's search field. As you type, it lists **hints** from
|
|
60
|
+
rules you supply: type `doc` and it can offer *name contains "doc"* or *Word documents*.
|
|
61
|
+
Picking one turns it into a **tag** in the query and consumes the text, and you can keep
|
|
62
|
+
typing. `value` is `{ tags, text }`.
|
|
63
|
+
|
|
64
|
+
- A tag is `{ caption, background?, color?, …your data }`, with chips coloured like
|
|
65
|
+
`<tosi-tag-list>`'s.
|
|
66
|
+
- It fires `change` when the tags change, and `action` on Enter when no hint is highlighted.
|
|
67
|
+
- It is a real combobox: focus stays in the field, ↓/↑ move through the hints, Escape closes
|
|
68
|
+
them, and Backspace in an empty field removes the last tag. The hints close only for a
|
|
69
|
+
scroll that moves the field itself, so a table re-filtering on every keystroke (which
|
|
70
|
+
scrolls it) doesn't close them the instant they open, and scrolling past the end of the list
|
|
71
|
+
doesn't scroll the page.
|
|
72
|
+
- A clear (✕) button, shown when there's anything to clear, removes the tags and the text
|
|
73
|
+
(also `clear()`).
|
|
74
|
+
- **As a filter:** give each tag a `test(item)` and the field a `textTest(item, text)`, and
|
|
75
|
+
`field.filter` is an array filter to hand straight to `<tosi-table>.filter`. The docs filter
|
|
76
|
+
the 3,655-row emoji table this way. `hints` and `textTest` can be passed to the element
|
|
77
|
+
creator like any other property; `textTest` defaults to `null`, because tosijs sets a creator
|
|
78
|
+
prop as a property only when its default is not `undefined` (otherwise it was stored as an
|
|
79
|
+
attribute, a stringified function). `filter` returns the same function until the query
|
|
80
|
+
changes, so a table's filter memo holds.
|
|
81
|
+
- Importing it on its own (`tosijs-ui/search-field`) costs 25.5 kB gzip: `<tosi-tag>` moved
|
|
82
|
+
into its own module (still exported from `tag-list`), so it no longer pulls in the menu
|
|
83
|
+
system.
|
|
84
|
+
|
|
85
|
+
### Pluggable dialects for live examples (#184)
|
|
86
|
+
|
|
87
|
+
Live examples can now be written in languages tosijs-ui doesn't know about. `registerDialect`
|
|
88
|
+
from `tosijs-ui/live-example` adds one, and a fence in that language becomes a live example
|
|
89
|
+
like `js`, `tjs` and `ts`: it groups with `html`/`css`/`test` blocks, honours `:static` and the
|
|
90
|
+
site's example policy, and saves back to source. `js`, `tjs` and `ts` are now built-in entries
|
|
91
|
+
of the same registry.
|
|
92
|
+
|
|
93
|
+
A dialect either **transforms** its source to JavaScript that the example runs as usual, or
|
|
94
|
+
**runs** it itself — given the example's preview, its options, the page's modules, an
|
|
95
|
+
`AbortSignal` for when the example re-runs, and `report()` for showing a result. Registering
|
|
96
|
+
over a built-in (`registerDialect('tjs', { transform })`) swaps in your own tjs-lang build.
|
|
97
|
+
This is what tjs-lang asked for to move AJS into doc pages; see the live-example page, "Adding
|
|
98
|
+
a dialect".
|
|
99
|
+
|
|
100
|
+
If your dialect's name is also a syntax-highlighting grammar, call `registerLiveLanguage` from
|
|
101
|
+
`tosijs-ui/site` in your site config too: the build-time highlighter never sees page
|
|
102
|
+
registrations, and would otherwise tokenize the source the example reads.
|
|
103
|
+
|
|
104
|
+
A replaced built-in is what runs in the browser, in dev and on the deployed site alike: the
|
|
105
|
+
build pre-transpiles `tjs` examples with the installed tjs-lang (so readers don't load the
|
|
106
|
+
transpiler), and the page skips that pre-transpiled code once the site registers its own `tjs`.
|
|
107
|
+
The build-time example check still transpiles `tjs` fences with the installed tjs-lang, so an
|
|
108
|
+
override that accepts newer syntax can fail that check; `checkExamples: false` is the escape
|
|
109
|
+
until the site config can say `tjs` is overridden.
|
|
110
|
+
|
|
111
|
+
Fence languages are case-sensitive, as they were in 1.16.0: ```` ```JS ```` is not a live
|
|
112
|
+
example. One visible difference: 1.16.0 also left such a fence unhighlighted, and it is now
|
|
113
|
+
highlighted as ordinary static code, on the page and in the ePub.
|
|
114
|
+
|
|
115
|
+
**Fence options.** Any fence can carry a JSON object after its language,
|
|
116
|
+
`` ```tjs {"runTests": "report"} ``, passed to its dialect as `options`. Malformed options are
|
|
117
|
+
reported in the console with the example they belong to.
|
|
118
|
+
|
|
119
|
+
Two more copies of "which languages run" were found while doing this, a hard-coded selector in
|
|
120
|
+
`insertExamples` and a private fence parser in the example checker, which would have read
|
|
121
|
+
`{"debug":true}` as the mode `true`. Both now use the shared rule.
|
|
122
|
+
|
|
3
123
|
## 1.16.0
|
|
4
124
|
|
|
5
125
|
### `<tosi-md>` sanitizes by default (#179)
|
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,37 @@
|
|
|
1
1
|
/** Fence languages that EXECUTE. Everything else is display-only. */
|
|
2
2
|
export declare const EXECUTABLE_LANGS: Set<string>;
|
|
3
|
+
/**
|
|
4
|
+
* Make fences in `lang` live examples, as an example's source block.
|
|
5
|
+
*
|
|
6
|
+
* You normally call `registerDialect` (which calls this). Call it directly only in a BUILD
|
|
7
|
+
* process that must agree with the page: the static highlighter runs at build time, where the
|
|
8
|
+
* page's `registerDialect` calls never happen, so a dialect named like a highlighter grammar
|
|
9
|
+
* (`python`, say) would be tokenized there and the example would read markup instead of code.
|
|
10
|
+
* Registering the name in your site config avoids that.
|
|
11
|
+
*/
|
|
12
|
+
export declare function registerLiveLanguage(lang: string): void;
|
|
13
|
+
/** Is `lang` an example's source language — `js`, `tjs`, `ts`, or a registered dialect? */
|
|
14
|
+
export declare function isDialectLanguage(lang: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* THE `language-*` class pattern, and the only place it is written.
|
|
17
|
+
*
|
|
18
|
+
* It was spelled `[A-Za-z0-9_+#-]+` four times in `highlight.ts` and `[\w-]+` once in
|
|
19
|
+
* `epub.ts`, so a `c++` or `c#` fence was read as a language by one and as `c` by the other.
|
|
20
|
+
* `+` and `#` are in the class deliberately: they are real language names. It lives here, not
|
|
21
|
+
* in `highlight.ts`, because live examples need it too and must not import the highlighter's
|
|
22
|
+
* grammar map to get it.
|
|
23
|
+
*/
|
|
24
|
+
export declare const LANGUAGE_NAME = "[A-Za-z0-9_+#-]+";
|
|
25
|
+
/**
|
|
26
|
+
* The fence language of a `<code class="language-…">`, **as written** (case preserved), or `''`.
|
|
27
|
+
*
|
|
28
|
+
* Case matters: fence languages are lowercase (`parseFenceInfo` reads `[a-z]+`), so ```` ```JS ````
|
|
29
|
+
* is not a live example. 1.16.1's first cut matched classes case-insensitively in one place
|
|
30
|
+
* and not the others, and an uppercase fence became live in `insertExamples` but not in
|
|
31
|
+
* save-to-source's scan — its content vanished from the page and every later save landed
|
|
32
|
+
* on the wrong block. Use `langOfClass` (lowercased) only for grammar lookups.
|
|
33
|
+
*/
|
|
34
|
+
export declare function languageOfClass(className: string | null | undefined): string;
|
|
3
35
|
/**
|
|
4
36
|
* - `'auto'` — the six executable languages become live examples (the web default).
|
|
5
37
|
* - `'opt-in'` — only a fence that asks, via `:inline` / `:iframe` / `:ide`.
|
|
@@ -13,24 +45,30 @@ export type ExamplePolicy = 'auto' | 'opt-in' | 'none';
|
|
|
13
45
|
/**
|
|
14
46
|
* Will this fence become a live example?
|
|
15
47
|
*
|
|
16
|
-
* @param lang the fence language
|
|
48
|
+
* @param lang the fence language as written (`js`, `html`, `typescript`, …); case-sensitive
|
|
17
49
|
* @param mode the `:<mode>` suffix, if any (`inline` | `iframe` | `ide` | `static`)
|
|
18
50
|
* @param policy `'auto'` (executables run) or `'opt-in'` (only fences that ask)
|
|
19
51
|
*/
|
|
20
52
|
export declare function isLiveFence(lang: string, mode: string | undefined, policy?: ExamplePolicy): boolean;
|
|
21
53
|
/**
|
|
22
|
-
* THE fence-info parser. `js`, `css#anchor`, `js:iframe`, `ts:ide#demo`, `ts#demo:ide
|
|
54
|
+
* THE fence-info parser. `js`, `css#anchor`, `js:iframe`, `ts:ide#demo`, `ts#demo:ide`,
|
|
55
|
+
* `tjs {"runTests": "report"}`.
|
|
23
56
|
*
|
|
24
57
|
* `:mode` (inline | iframe | ide | static) sets the live example's execution mode; `#id` gives
|
|
25
58
|
* it a stable anchor. `#id` is `[A-Za-z0-9_-]+` and `:mode` is `[a-z]+`, so the two cannot
|
|
26
59
|
* overlap and each is parsed independently, order-free.
|
|
27
60
|
*
|
|
61
|
+
* A JSON object after the head is the example's **options**, passed to its dialect (#184). It
|
|
62
|
+
* is split off FIRST: `{"debug":true}` contains `:true`, which the mode pattern would
|
|
63
|
+
* otherwise read as a mode. Options that are not a JSON object are reported in
|
|
64
|
+
* `optionsError` rather than dropped, so an author sees why their options did nothing.
|
|
65
|
+
*
|
|
28
66
|
* Extracted because a SECOND, worse copy existed in `save-to-source.ts` — a regex that
|
|
29
67
|
* captured the language as `[\w-]*` and therefore stopped dead at the colon, so it could not
|
|
30
68
|
* see `:static` at all. With a `:static` fence in a document, its example ordinals diverged
|
|
31
69
|
* from the ones `insert-examples` assigns, and an edit saved over a DIFFERENT block than the
|
|
32
70
|
* one edited. Silently: the "couldn't locate this example" guard only fires when the ordinal
|
|
33
|
-
* is out of range, and here a group existed at that index
|
|
71
|
+
* is out of range, and here a group existed at that index.
|
|
34
72
|
*
|
|
35
73
|
* This is the same failure the `isLiveFence` docblock above describes, one layer down: the
|
|
36
74
|
* rule was stated twice and the copies disagreed. Parse fence info here or not at all.
|
|
@@ -39,4 +77,6 @@ export declare function parseFenceInfo(info: string): {
|
|
|
39
77
|
lang: string;
|
|
40
78
|
mode?: string;
|
|
41
79
|
id?: string;
|
|
80
|
+
options?: Record<string, unknown>;
|
|
81
|
+
optionsError?: string;
|
|
42
82
|
};
|
|
@@ -25,10 +25,66 @@ export const EXECUTABLE_LANGS = new Set([
|
|
|
25
25
|
'css',
|
|
26
26
|
'test',
|
|
27
27
|
]);
|
|
28
|
+
/**
|
|
29
|
+
* The languages that are an example's SOURCE block (its one executable slot). `js`, `tjs` and
|
|
30
|
+
* `ts` are built in; `registerLiveLanguage` adds more (`registerDialect` in
|
|
31
|
+
* `tosijs-ui/live-example` calls it, so a registered dialect's fences run).
|
|
32
|
+
*
|
|
33
|
+
* Kept here, beside `EXECUTABLE_LANGS`, because the same fence is judged in four places
|
|
34
|
+
* (grouping, save-to-source, the highlighter, the ePub) and they must all see a registered
|
|
35
|
+
* dialect or none of them may.
|
|
36
|
+
*/
|
|
37
|
+
const SOURCE_LANGS = new Set(['js', 'ts', 'tjs']);
|
|
38
|
+
/**
|
|
39
|
+
* Make fences in `lang` live examples, as an example's source block.
|
|
40
|
+
*
|
|
41
|
+
* You normally call `registerDialect` (which calls this). Call it directly only in a BUILD
|
|
42
|
+
* process that must agree with the page: the static highlighter runs at build time, where the
|
|
43
|
+
* page's `registerDialect` calls never happen, so a dialect named like a highlighter grammar
|
|
44
|
+
* (`python`, say) would be tokenized there and the example would read markup instead of code.
|
|
45
|
+
* Registering the name in your site config avoids that.
|
|
46
|
+
*/
|
|
47
|
+
export function registerLiveLanguage(lang) {
|
|
48
|
+
const name = lang.toLowerCase();
|
|
49
|
+
if (!/^[a-z]+$/.test(name)) {
|
|
50
|
+
throw new Error(`registerLiveLanguage: "${lang}" is not a fence language (lowercase letters only, since the fence parser reads \`[a-z]+\`)`);
|
|
51
|
+
}
|
|
52
|
+
if (EXECUTABLE_LANGS.has(name) && !SOURCE_LANGS.has(name)) {
|
|
53
|
+
throw new Error(`registerLiveLanguage: "${name}" is already an example block (html/css/test) and cannot be a dialect`);
|
|
54
|
+
}
|
|
55
|
+
SOURCE_LANGS.add(name);
|
|
56
|
+
}
|
|
57
|
+
/** Is `lang` an example's source language — `js`, `tjs`, `ts`, or a registered dialect? */
|
|
58
|
+
export function isDialectLanguage(lang) {
|
|
59
|
+
return SOURCE_LANGS.has(lang);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* THE `language-*` class pattern, and the only place it is written.
|
|
63
|
+
*
|
|
64
|
+
* It was spelled `[A-Za-z0-9_+#-]+` four times in `highlight.ts` and `[\w-]+` once in
|
|
65
|
+
* `epub.ts`, so a `c++` or `c#` fence was read as a language by one and as `c` by the other.
|
|
66
|
+
* `+` and `#` are in the class deliberately: they are real language names. It lives here, not
|
|
67
|
+
* in `highlight.ts`, because live examples need it too and must not import the highlighter's
|
|
68
|
+
* grammar map to get it.
|
|
69
|
+
*/
|
|
70
|
+
export const LANGUAGE_NAME = '[A-Za-z0-9_+#-]+';
|
|
71
|
+
const LANGUAGE_CLASS = new RegExp(`language-(${LANGUAGE_NAME})`);
|
|
72
|
+
/**
|
|
73
|
+
* The fence language of a `<code class="language-…">`, **as written** (case preserved), or `''`.
|
|
74
|
+
*
|
|
75
|
+
* Case matters: fence languages are lowercase (`parseFenceInfo` reads `[a-z]+`), so ```` ```JS ````
|
|
76
|
+
* is not a live example. 1.16.1's first cut matched classes case-insensitively in one place
|
|
77
|
+
* and not the others, and an uppercase fence became live in `insertExamples` but not in
|
|
78
|
+
* save-to-source's scan — its content vanished from the page and every later save landed
|
|
79
|
+
* on the wrong block. Use `langOfClass` (lowercased) only for grammar lookups.
|
|
80
|
+
*/
|
|
81
|
+
export function languageOfClass(className) {
|
|
82
|
+
return String(className ?? '').match(LANGUAGE_CLASS)?.[1] ?? '';
|
|
83
|
+
}
|
|
28
84
|
/**
|
|
29
85
|
* Will this fence become a live example?
|
|
30
86
|
*
|
|
31
|
-
* @param lang the fence language
|
|
87
|
+
* @param lang the fence language as written (`js`, `html`, `typescript`, …); case-sensitive
|
|
32
88
|
* @param mode the `:<mode>` suffix, if any (`inline` | `iframe` | `ide` | `static`)
|
|
33
89
|
* @param policy `'auto'` (executables run) or `'opt-in'` (only fences that ask)
|
|
34
90
|
*/
|
|
@@ -37,7 +93,8 @@ export function isLiveFence(lang, mode, policy = 'auto') {
|
|
|
37
93
|
// costs the reader the highlighting.
|
|
38
94
|
if (policy === 'none')
|
|
39
95
|
return false;
|
|
40
|
-
|
|
96
|
+
// Case-sensitive, like `parseFenceInfo`: see `languageOfClass`.
|
|
97
|
+
if (!EXECUTABLE_LANGS.has(lang) && !SOURCE_LANGS.has(lang))
|
|
41
98
|
return false;
|
|
42
99
|
// `:static` opts out under any policy, so one corpus can target all of them.
|
|
43
100
|
if (mode === 'static')
|
|
@@ -45,27 +102,51 @@ export function isLiveFence(lang, mode, policy = 'auto') {
|
|
|
45
102
|
return policy === 'opt-in' ? mode !== undefined : true;
|
|
46
103
|
}
|
|
47
104
|
/**
|
|
48
|
-
* THE fence-info parser. `js`, `css#anchor`, `js:iframe`, `ts:ide#demo`, `ts#demo:ide
|
|
105
|
+
* THE fence-info parser. `js`, `css#anchor`, `js:iframe`, `ts:ide#demo`, `ts#demo:ide`,
|
|
106
|
+
* `tjs {"runTests": "report"}`.
|
|
49
107
|
*
|
|
50
108
|
* `:mode` (inline | iframe | ide | static) sets the live example's execution mode; `#id` gives
|
|
51
109
|
* it a stable anchor. `#id` is `[A-Za-z0-9_-]+` and `:mode` is `[a-z]+`, so the two cannot
|
|
52
110
|
* overlap and each is parsed independently, order-free.
|
|
53
111
|
*
|
|
112
|
+
* A JSON object after the head is the example's **options**, passed to its dialect (#184). It
|
|
113
|
+
* is split off FIRST: `{"debug":true}` contains `:true`, which the mode pattern would
|
|
114
|
+
* otherwise read as a mode. Options that are not a JSON object are reported in
|
|
115
|
+
* `optionsError` rather than dropped, so an author sees why their options did nothing.
|
|
116
|
+
*
|
|
54
117
|
* Extracted because a SECOND, worse copy existed in `save-to-source.ts` — a regex that
|
|
55
118
|
* captured the language as `[\w-]*` and therefore stopped dead at the colon, so it could not
|
|
56
119
|
* see `:static` at all. With a `:static` fence in a document, its example ordinals diverged
|
|
57
120
|
* from the ones `insert-examples` assigns, and an edit saved over a DIFFERENT block than the
|
|
58
121
|
* one edited. Silently: the "couldn't locate this example" guard only fires when the ordinal
|
|
59
|
-
* is out of range, and here a group existed at that index
|
|
122
|
+
* is out of range, and here a group existed at that index.
|
|
60
123
|
*
|
|
61
124
|
* This is the same failure the `isLiveFence` docblock above describes, one layer down: the
|
|
62
125
|
* rule was stated twice and the copies disagreed. Parse fence info here or not at all.
|
|
63
126
|
*/
|
|
64
127
|
export function parseFenceInfo(info) {
|
|
65
128
|
const text = String(info || '');
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
129
|
+
const brace = text.indexOf('{');
|
|
130
|
+
const head = brace === -1 ? text : text.slice(0, brace);
|
|
131
|
+
const parsed = {
|
|
132
|
+
lang: head.match(/^[a-z]+/)?.[0] ?? '',
|
|
133
|
+
mode: head.match(/:([a-z]+)/)?.[1],
|
|
134
|
+
id: head.match(/#([A-Za-z0-9_-]+)/)?.[1],
|
|
70
135
|
};
|
|
136
|
+
if (brace !== -1) {
|
|
137
|
+
const json = text.slice(brace).trim();
|
|
138
|
+
try {
|
|
139
|
+
const value = JSON.parse(json);
|
|
140
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
141
|
+
parsed.options = value;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
parsed.optionsError = `fence options must be a JSON object, got ${json}`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
parsed.optionsError = `fence options are not valid JSON (${error.message}): ${json}`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return parsed;
|
|
71
152
|
}
|
|
@@ -21,7 +21,11 @@ export declare function registeredGrammars(): string[];
|
|
|
21
21
|
export declare function grammarFor(fenceLang: string): string;
|
|
22
22
|
/** Grammars this build can load — for tests, diagnostics, and the docs. */
|
|
23
23
|
export declare function loadableGrammars(): string[];
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* The fence language of a `<code class="language-…">`, lowercased, or `''` — for GRAMMAR
|
|
26
|
+
* lookups. The pattern itself is `languageOfClass` in `example-policy.ts`, the only place it
|
|
27
|
+
* is written; ask `isLiveFence` with the case-preserved `languageOfClass`, not with this.
|
|
28
|
+
*/
|
|
25
29
|
export declare function langOfClass(className: string | null | undefined): string;
|
|
26
30
|
export declare function ensureGrammar(lang: string): Promise<boolean>;
|
|
27
31
|
/**
|