tosijs-ui 1.16.1 → 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 CHANGED
@@ -1,5 +1,52 @@
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
+
3
50
  ## 1.16.1
4
51
 
5
52
  A patch: everything here is additive. Two features, a search field that suggests refinements
@@ -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: string;
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
- gap: vars.carouselDotSpacing,
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 (elapsed >= this.snapDuration ||
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();
@@ -1207,6 +1207,10 @@ export async function buildSite(config, opts = {}) {
1207
1207
  const policyHead = config.liveExamples === 'opt-in'
1208
1208
  ? `<script>globalThis.__TOSI_EXAMPLE_POLICY="opt-in"</script>`
1209
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
+ : '';
1210
1214
  // Optional (tjs-lang 0.11+): the import-resolver service worker. Lets live examples
1211
1215
  // import real npm packages from anywhere — bare specifiers the doc-system doesn't
1212
1216
  // inject become `/<prefix>/<spec>` requests the worker resolves + caches. GATED behind
@@ -1419,7 +1423,13 @@ export async function buildSite(config, opts = {}) {
1419
1423
  // instead of the classic IIFE. See the ESM hydration bundle above.
1420
1424
  hydrateUrl: hydrateName ? `/${hydrateName}` : undefined,
1421
1425
  bakes: exampleBakes,
1422
- headExtra: [config.headExtra, tjsHead, policyHead, importResolverHead]
1426
+ headExtra: [
1427
+ config.headExtra,
1428
+ tjsHead,
1429
+ policyHead,
1430
+ consoleHead,
1431
+ importResolverHead,
1432
+ ]
1423
1433
  .filter(Boolean)
1424
1434
  .join('') || undefined,
1425
1435
  scriptUrl: config.scriptUrl,
@@ -129,6 +129,13 @@ 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;
132
139
  /** modules to leave external in the bundle, e.g. ['jolt-physics'] */
133
140
  bundleExternals?: string[];
134
141
  /**