@fusion.dev/helix 0.31.0 → 0.33.0

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.
Files changed (35) hide show
  1. package/custom-elements.json +1419 -173
  2. package/dist/aura/root/aura-root.css.js +4 -0
  3. package/dist/aura/root/aura-root.d.ts +66 -3
  4. package/dist/aura/root/aura-root.js +61 -5
  5. package/dist/bar-chart/root/bar-chart-root.css.js +4 -0
  6. package/dist/breadcrumbs/breadcrumb/breadcrumb.css.js +0 -4
  7. package/dist/carousel/root/carousel-root.css.js +4 -7
  8. package/dist/color-picker/root/color-picker-root.css.js +13 -3
  9. package/dist/data-table/root/data-table-root.css.js +15 -10
  10. package/dist/date-picker/root/date-picker-root.css.js +0 -2
  11. package/dist/donut-chart/root/donut-chart-root.css.js +8 -0
  12. package/dist/drilldown/item/drilldown-item.css.js +7 -0
  13. package/dist/drilldown/root/drilldown-root.css.js +0 -4
  14. package/dist/heatmap/root/heatmap-root.d.ts +8 -0
  15. package/dist/menu/item/menu-item.css.js +0 -4
  16. package/dist/menu/root/menu-root.css.js +0 -4
  17. package/dist/object-inspector/root/object-inspector-root.css.js +0 -4
  18. package/dist/orb/root/orb-root.css.js +4 -0
  19. package/dist/orb/root/orb-root.d.ts +24 -1
  20. package/dist/orb/root/orb-root.js +30 -2
  21. package/dist/pagination/root/pagination-root.css.js +0 -4
  22. package/dist/pin-input/root/pin-input-root.css.js +1 -0
  23. package/dist/scatter-chart/root/scatter-chart-root.css.js +7 -2
  24. package/dist/scroll-area/root/scroll-area-root.css.js +9 -2
  25. package/dist/shared/chart/terminus.css.js +8 -0
  26. package/dist/slider/root/slider-root.css.js +43 -7
  27. package/dist/table/cell/table-cell.css.js +6 -1
  28. package/dist/table/column/table-column.css.js +23 -5
  29. package/dist/tooltip/root/tooltip-root.css.js +16 -32
  30. package/dist/tour/root/tour-root.css.js +3 -2
  31. package/dist/tour/stop/tour-stop.css.js +13 -17
  32. package/dist/tree/item/tree-item.css.js +4 -0
  33. package/package.json +6 -6
  34. package/types/jsx.d.ts +1570 -628
  35. package/types/vue.d.ts +61 -18
@@ -27,6 +27,10 @@ inline-size: 100%;
27
27
  block-size: 100%;
28
28
  }
29
29
 
30
+ [part="canvas"][hidden] {
31
+ display: none;
32
+ }
33
+
30
34
  @media print {
31
35
  :host {
32
36
  display: none;
@@ -1,5 +1,5 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
- import type { CSSResultGroup, TemplateResult } from 'lit';
2
+ import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
3
3
  /**
4
4
  * @summary A lit, drifting background, drawn from the token layer.
5
5
  *
@@ -20,7 +20,9 @@ import type { CSSResultGroup, TemplateResult } from 'lit';
20
20
  * from `gl_VertexID`, so there is no attribute, no buffer and no upload.
21
21
  *
22
22
  * **It stops when nobody is looking**, off screen and in a background tab, and
23
- * `prefers-reduced-motion` gets one still frame rather than an empty box.
23
+ * `prefers-reduced-motion` gets one still frame rather than an empty box. A
24
+ * context the browser takes back, on a GPU reset or past its cap of about
25
+ * sixteen, stops it too, and the gradient shows until the context returns.
24
26
  *
25
27
  * Styled only, and with behavior, which the tier split does not otherwise
26
28
  * allow: the behavior is drawing to a canvas, so there is no structure a
@@ -218,6 +220,11 @@ export declare class HelixAura extends HelixElement {
218
220
  private readonly eased;
219
221
  private motion;
220
222
  private visible;
223
+ /**
224
+ * What the intersection observer last said, kept because it says so only on
225
+ * a change: every other path to `play` has to ask it.
226
+ */
227
+ private onScreen;
221
228
  /**
222
229
  * The element's box, cached.
223
230
  *
@@ -265,7 +272,7 @@ export declare class HelixAura extends HelixElement {
265
272
  */
266
273
  private openBy;
267
274
  protected render(): TemplateResult;
268
- protected firstUpdated(): void;
275
+ protected firstUpdated(changed: PropertyValues): void;
269
276
  private start;
270
277
  /**
271
278
  * The context and the program, from whatever canvas can still give one.
@@ -286,8 +293,21 @@ export declare class HelixAura extends HelixElement {
286
293
  * event default-prevented and the extension held from before the loss, and it
287
294
  * restores asynchronously, so `start` would have to become a state machine to
288
295
  * use it. A fresh canvas is synchronous and cannot half-succeed.
296
+ *
297
+ * That is a context this element released. One the browser takes is not
298
+ * spent: `handleLost` asks for it back, and `start` waits for it rather than
299
+ * coming here.
289
300
  */
290
301
  private build;
302
+ /**
303
+ * The program and where its uniforms live, on the context already held.
304
+ *
305
+ * Apart from `build` because a restored context comes here alone. It is the
306
+ * same object the browser took, with every program and location it had made
307
+ * gone, and `build` would replace a canvas that had been lost again before
308
+ * this ran rather than wait for it a second time.
309
+ */
310
+ private prepare;
291
311
  connectedCallback(): void;
292
312
  protected updated(): void;
293
313
  disconnectedCallback(): void;
@@ -308,6 +328,35 @@ export declare class HelixAura extends HelixElement {
308
328
  * fresh one.
309
329
  */
310
330
  private readonly drop;
331
+ /**
332
+ * Stops for a context the browser has taken, and asks for it back.
333
+ *
334
+ * **Taken on a GPU reset, and past the cap** of about sixteen, where the
335
+ * browser drops the oldest context for the newest while its aura is still on
336
+ * screen. `tick` books its own next frame, so the loop went on at 30 frames a
337
+ * second against a context that draws nothing, and nothing made the program
338
+ * again once the context came back: the aura never recovered.
339
+ *
340
+ * **The canvas is hidden until then, because a lost one is not reliably
341
+ * transparent.** Measured 15 Sep 2026: headless Chromium painted a plain
342
+ * canvas white with a broken-image glyph in its corner, force-lost and on a
343
+ * crashed GPU process alike, while this one came out clear there and on
344
+ * Metal, and the landing page once held a spent one's last frame (`build`
345
+ * has it). Hidden, it shows what a machine with no WebGL is shown, the host's
346
+ * gradient, whatever the engine does with a lost canvas.
347
+ *
348
+ * `preventDefault` is the asking: a browser restores only a context whose
349
+ * loss was canceled.
350
+ */
351
+ private readonly handleLost;
352
+ /**
353
+ * Draws again on the context the browser gave back.
354
+ *
355
+ * Everything made on it went with the loss, so the program is linked again
356
+ * and the buffer measured as though it were new, which is what gives the new
357
+ * program its resolution.
358
+ */
359
+ private readonly handleRestored;
311
360
  /**
312
361
  * Resolves the ramp. Called when the palette moves, not when a frame draws.
313
362
  *
@@ -368,6 +417,20 @@ export declare class HelixAura extends HelixElement {
368
417
  */
369
418
  private static readonly CAPPED;
370
419
  private readonly tick;
420
+ /**
421
+ * Starts the loop, where it is wanted and can be ended.
422
+ *
423
+ * **Never while detached.** `tick` books its own next frame, so the only
424
+ * thing that ends the loop is `stop`, and a disconnection is the last time
425
+ * `stop` is called. Everything that plays is canceled or disconnected there,
426
+ * and this holds for whatever callback was already on its way.
427
+ *
428
+ * **Nor off screen, whoever asks.** The observer is the one caller that
429
+ * knows, and a tab coming back, a resize and a restored context all play
430
+ * too. An aura 2600 pixels below the fold booked no frame at rest and one on
431
+ * every frame after its tab came back, until it had been scrolled in and out
432
+ * again. Nor on a lost context, which draws nothing.
433
+ */
371
434
  private play;
372
435
  private stop;
373
436
  /**
@@ -52,6 +52,7 @@ let HelixAura = class HelixAura extends HelixElement {
52
52
  this.head = { x: 1, y: 1 };
53
53
  this.target = { presence: 0, x: 0, y: 0 };
54
54
  this.eased = { presence: 0, x: 0, y: 0 };
55
+ this.onScreen = false;
55
56
  this.box = { height: 1, left: 0, top: 0, width: 1 };
56
57
  this.opened = false;
57
58
  this.opening = 0;
@@ -65,6 +66,31 @@ let HelixAura = class HelixAura extends HelixElement {
65
66
  this.program = null;
66
67
  this.gl = null;
67
68
  };
69
+ this.handleLost = (event) => {
70
+ event.preventDefault();
71
+ this.stop();
72
+ if (this.opening) {
73
+ cancelAnimationFrame(this.opening);
74
+ this.opening = 0;
75
+ }
76
+ this.program = null;
77
+ this.canvas?.toggleAttribute('hidden', true);
78
+ };
79
+ this.handleRestored = () => {
80
+ if (!this.prepare()) {
81
+ return;
82
+ }
83
+ this.width = 0;
84
+ this.height = 0;
85
+ this.measure();
86
+ if (this.opened) {
87
+ this.paint();
88
+ this.play();
89
+ }
90
+ else if (this.width) {
91
+ this.open();
92
+ }
93
+ };
68
94
  this.tick = (now) => {
69
95
  if (!this.last) {
70
96
  this.last = now;
@@ -129,21 +155,32 @@ let HelixAura = class HelixAura extends HelixElement {
129
155
  render() {
130
156
  return html `<canvas part="canvas" aria-hidden="true"></canvas>`;
131
157
  }
132
- firstUpdated() {
158
+ firstUpdated(changed) {
159
+ super.firstUpdated(changed);
133
160
  this.start();
134
161
  }
135
162
  start() {
163
+ if (!this.isConnected) {
164
+ return;
165
+ }
136
166
  if (!this.renderRoot.querySelector('canvas')) {
137
167
  return;
138
168
  }
139
- if (!this.program && !this.build()) {
169
+ if (this.program && this.gl?.isContextLost()) {
170
+ this.program = null;
171
+ this.gl = null;
172
+ }
173
+ if (!this.program && !this.gl?.isContextLost() && !this.build()) {
140
174
  return;
141
175
  }
176
+ this.canvas?.addEventListener('webglcontextlost', this.handleLost);
177
+ this.canvas?.addEventListener('webglcontextrestored', this.handleRestored);
142
178
  this.motion = window.matchMedia('(prefers-reduced-motion: reduce)');
143
179
  this.addEventListener('pointermove', this.handlePointer);
144
180
  this.addEventListener('pointerleave', this.handleLeave);
145
181
  this.visible = new IntersectionObserver(([entry]) => {
146
- if (entry?.isIntersecting) {
182
+ this.onScreen = entry?.isIntersecting ?? false;
183
+ if (this.onScreen) {
147
184
  this.play();
148
185
  }
149
186
  else {
@@ -167,7 +204,7 @@ let HelixAura = class HelixAura extends HelixElement {
167
204
  });
168
205
  this.sized.observe(this);
169
206
  this.readPalette();
170
- if (this.measure()) {
207
+ if (this.measure() || (this.width && !this.opened)) {
171
208
  this.open();
172
209
  }
173
210
  }
@@ -192,10 +229,18 @@ let HelixAura = class HelixAura extends HelixElement {
192
229
  if (!gl || gl.isContextLost()) {
193
230
  return false;
194
231
  }
232
+ return this.prepare();
233
+ }
234
+ prepare() {
235
+ const gl = this.gl;
236
+ if (!gl) {
237
+ return false;
238
+ }
195
239
  this.program = this.link(VERTEX, FRAGMENT);
196
240
  if (!this.program) {
197
241
  return false;
198
242
  }
243
+ this.canvas?.toggleAttribute('hidden', false);
199
244
  gl.useProgram(this.program);
200
245
  for (const name of [
201
246
  'uBloom',
@@ -236,8 +281,16 @@ let HelixAura = class HelixAura extends HelixElement {
236
281
  disconnectedCallback() {
237
282
  super.disconnectedCallback();
238
283
  this.stop();
284
+ if (this.opening) {
285
+ cancelAnimationFrame(this.opening);
286
+ this.opening = 0;
287
+ this.openBy = 0;
288
+ }
239
289
  this.visible?.disconnect();
240
290
  this.visible = undefined;
291
+ this.onScreen = false;
292
+ this.canvas?.removeEventListener('webglcontextlost', this.handleLost);
293
+ this.canvas?.removeEventListener('webglcontextrestored', this.handleRestored);
241
294
  this.removeEventListener('pointermove', this.handlePointer);
242
295
  this.removeEventListener('pointerleave', this.handleLeave);
243
296
  document.removeEventListener('visibilitychange', this.handleVisibility);
@@ -382,7 +435,7 @@ let HelixAura = class HelixAura extends HelixElement {
382
435
  this.opening = requestAnimationFrame(draw);
383
436
  }
384
437
  paintOnce() {
385
- if (this.frame) {
438
+ if (this.frame || !this.opened || !this.isConnected) {
386
439
  return;
387
440
  }
388
441
  this.paint();
@@ -390,9 +443,12 @@ let HelixAura = class HelixAura extends HelixElement {
390
443
  static { this.CAPPED = 1000 / 30; }
391
444
  play() {
392
445
  if (this.frame ||
446
+ !this.isConnected ||
447
+ !this.onScreen ||
393
448
  this.motion?.matches ||
394
449
  document.hidden ||
395
450
  !this.gl ||
451
+ this.gl.isContextLost() ||
396
452
  !this.width ||
397
453
  !this.opened) {
398
454
  return;
@@ -220,6 +220,10 @@ translate: 0 -50%;
220
220
  translate: -50% 0;
221
221
  }
222
222
 
223
+ :host([direction="horizontal"]:dir(rtl)) [part="tick"] {
224
+ translate: 50% 0;
225
+ }
226
+
223
227
  :host(:not([direction="horizontal"])) [part="axis"][data-axis="secondary"] [part="tick"] {
224
228
  inset-inline-end: auto;
225
229
  inset-inline-start: 0;
@@ -68,10 +68,6 @@ font-weight: var(--helix-weight-medium);
68
68
  color: var(--helix-color-foreground);
69
69
  }
70
70
 
71
- :host(:dir(rtl)) [part="separator"] svg {
72
- scale: -1 1;
73
- }
74
-
75
71
  [part="link"] {
76
72
  align-items: center;
77
73
  border-radius: var(--helix-radii-sm);
@@ -199,9 +199,11 @@ scroll-snap-type: y mandatory;
199
199
  [part="slide"] {
200
200
  display: flex;
201
201
  flex: 0 0
202
- calc(
202
+ round(
203
+ up,
203
204
  (100% - (var(--_carousel-per-view) - 1) * var(--_carousel-gap)) /
204
- var(--_carousel-per-view)
205
+ var(--_carousel-per-view),
206
+ 1px
205
207
  );
206
208
  scroll-snap-align: start;
207
209
  }
@@ -312,11 +314,6 @@ margin-inline-start: auto;
312
314
  order: 1;
313
315
  }
314
316
 
315
- :host(:dir(rtl)) [part="previous"] svg,
316
- :host(:dir(rtl)) [part="next"] svg {
317
- scale: -1 1;
318
- }
319
-
320
317
  :host([orientation="vertical"]:not([indicator-placement])) [part="indicators"] {
321
318
  align-self: var(--_carousel-indicators-along);
322
319
  flex-direction: column;
@@ -1,9 +1,15 @@
1
1
  import { createSheet } from '@fusion.dev/helix-core';
2
2
  export const styles = createSheet(`:host {
3
+
4
+ --_color-picker-along: to right;
3
5
  display: block;
4
6
  padding-block-end: var(--color-picker-padding, var(--helix-space-none));
5
7
  }
6
8
 
9
+ :host(:dir(rtl)) {
10
+ --_color-picker-along: to left;
11
+ }
12
+
7
13
  [part="sample"],
8
14
  [part="swatch"] {
9
15
  background-image:
@@ -29,7 +35,7 @@ box-sizing: border-box;
29
35
  aspect-ratio: var(--color-picker-grid-ratio, 1 / 0.7);
30
36
  background-image:
31
37
  linear-gradient(to top, hsl(0 0% 0%), transparent),
32
- linear-gradient(to right, hsl(0 0% 100%), transparent),
38
+ linear-gradient(var(--_color-picker-along), hsl(0 0% 100%), transparent),
33
39
  linear-gradient(hsl(var(--color-picker-hue) 100% 50%), hsl(var(--color-picker-hue) 100% 50%));
34
40
  border-radius: var(--color-picker-grid-radius, var(--helix-radii-sm));
35
41
  cursor: crosshair;
@@ -50,6 +56,10 @@ position: absolute;
50
56
  translate: -50% -50%;
51
57
  }
52
58
 
59
+ :host(:dir(rtl)) [part="grid-handle"] {
60
+ translate: 50% -50%;
61
+ }
62
+
53
63
  [part="grid-saturation"],
54
64
  [part="grid-brightness"] {
55
65
  appearance: none;
@@ -135,7 +145,7 @@ white-space: nowrap;
135
145
 
136
146
  [part="channel"][data-channel="hue"] {
137
147
  --slider-track-image: linear-gradient(
138
- to right,
148
+ var(--_color-picker-along),
139
149
  hsl(0 100% 50%),
140
150
  hsl(60 100% 50%),
141
151
  hsl(120 100% 50%),
@@ -148,7 +158,7 @@ hsl(360 100% 50%)
148
158
 
149
159
  [part="channel"][data-channel="alpha"] {
150
160
  --slider-track-image:
151
- linear-gradient(to right, transparent, hsl(var(--color-picker-hue) 100% 50%)),
161
+ linear-gradient(var(--_color-picker-along), transparent, hsl(var(--color-picker-hue) 100% 50%)),
152
162
  conic-gradient(
153
163
  var(--helix-color-surface-sunken) 0 25%,
154
164
  var(--helix-color-surface) 0 50%,
@@ -73,15 +73,11 @@ border-block-end-color: transparent;
73
73
  }
74
74
 
75
75
  ::slotted(helix-table-column) {
76
- border-block-end: var(--helix-border-thin) solid
77
- var(--data-table-rule-color, var(--helix-color-border));
78
- color: var(--data-table-heading-color, var(--helix-color-foreground-subtle));
79
- font-family: var(--helix-font-sans);
80
- font-size: var(--data-table-text, var(--helix-text-sm));
81
- font-weight: var(--helix-weight-medium);
82
- padding-block: var(--data-table-padding-block, var(--helix-space-sm));
83
- padding-inline: var(--data-table-padding-inline, var(--helix-space-sm));
84
- text-align: start;
76
+ --_table-heading-color: var(--data-table-heading-color);
77
+ --_table-heading-padding-block: var(--data-table-padding-block);
78
+ --_table-heading-padding-inline: var(--data-table-padding-inline);
79
+ --_table-heading-rule-color: var(--data-table-rule-color);
80
+ --_table-heading-text: var(--data-table-text);
85
81
  }
86
82
 
87
83
  ::slotted(helix-table-column[sortable]) {
@@ -144,10 +140,19 @@ overscroll-behavior: contain;
144
140
  }
145
141
 
146
142
  :host([windowed]) ::slotted(helix-table-column) {
143
+ --_table-heading-background-color: var(
144
+ --data-table-heading-background-color,
145
+ var(--helix-color-background)
146
+ );
147
+ --_table-heading-layer: var(--helix-layer-sticky);
148
+ --_table-heading-position: sticky;
149
+ }
150
+
151
+ :host([windowed]) [part="select-all"] {
147
152
  background-color: var(--data-table-heading-background-color, var(--helix-color-background));
148
153
  inset-block-start: 0;
149
154
  position: sticky;
150
- z-index: 1;
155
+ z-index: var(--helix-layer-sticky);
151
156
  }
152
157
 
153
158
  [part="before"],
@@ -177,8 +177,6 @@ outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);
177
177
  outline-offset: var(--helix-size-focus-offset);
178
178
  }
179
179
 
180
- :host(:dir(rtl)) [part="previous"] svg,
181
- :host(:dir(rtl)) [part="next"] svg,
182
180
  :host(:dir(rtl)) ::slotted([slot="previous-icon"]),
183
181
  :host(:dir(rtl)) ::slotted([slot="next-icon"]) {
184
182
  scale: -1 1;
@@ -128,6 +128,14 @@ block-size: calc(
128
128
  font-size: 6.5cqi;
129
129
  }
130
130
 
131
+ :host(:dir(rtl)) [part="center"] {
132
+ translate: 50% -50%;
133
+ }
134
+
135
+ :host([sweep="half"]:dir(rtl)) [part="center"] {
136
+ translate: 50% 0;
137
+ }
138
+
131
139
  @media print {
132
140
  :host {
133
141
  print-color-adjust: exact;
@@ -27,6 +27,13 @@ text-overflow: ellipsis;
27
27
  white-space: nowrap;
28
28
  }
29
29
 
30
+ [part="mark"] {
31
+ block-size: var(--helix-size-icon-tight);
32
+ color: var(--helix-color-foreground-subtle);
33
+ flex: none;
34
+ inline-size: var(--helix-size-icon-tight);
35
+ }
36
+
30
37
  :host(:not([disabled])) [part="item"]:hover {
31
38
  background-color: var(
32
39
  --drilldown-item-hover-color,
@@ -98,10 +98,6 @@ animation: none;
98
98
  }
99
99
  }
100
100
 
101
- :host(:dir(rtl)) [part="back"] svg {
102
- scale: -1 1;
103
- }
104
-
105
101
  :host([loading]) [part="page"] {
106
102
  opacity: 0.5;
107
103
  }
@@ -34,9 +34,17 @@ import '../../tooltip/index.js';
34
34
  * @csspart tooltip - The bubble, forwarded from `Tooltip`.
35
35
  * @csspart tooltip-anchor - The `Tooltip` element itself.
36
36
  * @csspart tooltip-arrow - The bubble's point, forwarded from `Tooltip`.
37
+ * @csspart tooltip-arrow-bottom - The point while the bubble is below the reading, so on its top edge, beside `tooltip-arrow`.
38
+ * @csspart tooltip-arrow-left - The point while the bubble is left of the reading, so on its right edge, beside `tooltip-arrow`.
39
+ * @csspart tooltip-arrow-right - The point while the bubble is right of the reading, so on its left edge, beside `tooltip-arrow`.
40
+ * @csspart tooltip-arrow-top - The point while the bubble is above the reading, so on its bottom edge, beside `tooltip-arrow`.
41
+ * @csspart tooltip-bottom - The bubble while it is below the reading, beside `tooltip`.
37
42
  * @csspart tooltip-cell - One column's name and value inside the tooltip.
38
43
  * @csspart tooltip-label - The reading's own name inside the tooltip.
44
+ * @csspart tooltip-left - The bubble while it is left of the reading, beside `tooltip`.
39
45
  * @csspart tooltip-name - A column's name inside the tooltip.
46
+ * @csspart tooltip-right - The bubble while it is right of the reading, beside `tooltip`.
47
+ * @csspart tooltip-top - The bubble while it is above the reading, beside `tooltip`.
40
48
  * @csspart tooltip-value - A column's value inside the tooltip.
41
49
  *
42
50
  * @cssproperty --heatmap-cell-height - How tall a cell is. A row of a tall thin cell is what a status page draws.
@@ -100,8 +100,4 @@ display: contents;
100
100
 
101
101
  slot[name="submenu"]::slotted(helix-divider) {
102
102
  margin-block: var(--helix-space-2xs);
103
- }
104
-
105
- :host(:dir(rtl)) [part="mark"][data-submenu] {
106
- scale: -1 1;
107
103
  }`);
@@ -100,10 +100,6 @@ margin-block: var(--helix-space-2xs);
100
100
  display: none;
101
101
  }
102
102
 
103
- :host(:dir(rtl)) [part="back"] svg {
104
- scale: -1 1;
105
- }
106
-
107
103
  :host([loading]) [part="surface"] {
108
104
  opacity: 0.5;
109
105
  }
@@ -98,10 +98,6 @@ margin-inline-end: var(--helix-space-2xs);
98
98
  vertical-align: -0.125em;
99
99
  }
100
100
 
101
- :host(:dir(rtl)) [part~="row"][aria-expanded="false"] > [part="toggle"] {
102
- scale: -1 1;
103
- }
104
-
105
101
  [part="key"] {
106
102
  color: var(--helix-color-syntax-name);
107
103
  }
@@ -31,6 +31,10 @@ inline-size: 100%;
31
31
  text-decoration-color: var(--_orb-ground);
32
32
  }
33
33
 
34
+ [part="canvas"][hidden] {
35
+ display: none;
36
+ }
37
+
34
38
  @media print {
35
39
  :host {
36
40
  display: none;
@@ -92,6 +92,8 @@ export declare class HelixOrb extends HelixElement {
92
92
  private frame;
93
93
  private running;
94
94
  private onScreen;
95
+ /** Whether the browser has taken the canvas, as its own two events say. */
96
+ private lost;
95
97
  /** The box the canvas was last measured at, in CSS pixels. */
96
98
  private box;
97
99
  /**
@@ -121,7 +123,7 @@ export declare class HelixOrb extends HelixElement {
121
123
  private last;
122
124
  protected willUpdate(changed: PropertyValues): void;
123
125
  protected render(): TemplateResult;
124
- protected firstUpdated(): void;
126
+ protected firstUpdated(changed: PropertyValues): void;
125
127
  connectedCallback(): void;
126
128
  disconnectedCallback(): void;
127
129
  private start;
@@ -150,6 +152,27 @@ export declare class HelixOrb extends HelixElement {
150
152
  private stop;
151
153
  private readonly tick;
152
154
  private readonly handleVisibility;
155
+ /**
156
+ * Stops for a canvas the browser has taken, on a GPU reset, until it is
157
+ * given back.
158
+ *
159
+ * **Not canceled, which is the aura's contract turned over.** A 2D context
160
+ * comes back on its own unless its loss is canceled, where a WebGL one comes
161
+ * back only if it is. Hidden meanwhile, because a lost canvas is not reliably
162
+ * transparent (the aura's `handleLost` has what was measured), and hidden it
163
+ * leaves the empty box a machine with no 2D context is shown.
164
+ */
165
+ private readonly handleLost;
166
+ /**
167
+ * Draws again on the canvas the browser gave back.
168
+ *
169
+ * **Sized again, because it comes back in its default state.** Found 15 Sep
170
+ * 2026 by crashing the GPU process under headless Chromium: the transform
171
+ * `measure` sets came back the identity, and `measure` sets it only when the
172
+ * box changes. At twice the density that is an orb drawn at half its size in
173
+ * the top left corner, for as long as the box holds still.
174
+ */
175
+ private readonly handleRestored;
153
176
  private readonly handleMotion;
154
177
  /**
155
178
  * One frame.
@@ -32,6 +32,7 @@ let HelixOrb = class HelixOrb extends HelixElement {
32
32
  this.frame = 0;
33
33
  this.running = false;
34
34
  this.onScreen = false;
35
+ this.lost = false;
35
36
  this.box = 0;
36
37
  this.from = paramsFor('thinking');
37
38
  this.morphAt = 0;
@@ -56,6 +57,19 @@ let HelixOrb = class HelixOrb extends HelixElement {
56
57
  this.play();
57
58
  }
58
59
  };
60
+ this.handleLost = () => {
61
+ this.lost = true;
62
+ this.stop();
63
+ this.canvas?.toggleAttribute('hidden', true);
64
+ };
65
+ this.handleRestored = () => {
66
+ this.lost = false;
67
+ this.canvas?.toggleAttribute('hidden', false);
68
+ this.box = 0;
69
+ this.measure();
70
+ this.paint(0);
71
+ this.play();
72
+ };
59
73
  this.handleMotion = () => {
60
74
  if (this.motion?.matches) {
61
75
  this.stop();
@@ -86,7 +100,8 @@ let HelixOrb = class HelixOrb extends HelixElement {
86
100
  render() {
87
101
  return html `<canvas part="canvas" aria-hidden="true"></canvas>`;
88
102
  }
89
- firstUpdated() {
103
+ firstUpdated(changed) {
104
+ super.firstUpdated(changed);
90
105
  this.start();
91
106
  }
92
107
  connectedCallback() {
@@ -98,6 +113,10 @@ let HelixOrb = class HelixOrb extends HelixElement {
98
113
  disconnectedCallback() {
99
114
  super.disconnectedCallback();
100
115
  this.stop();
116
+ this.onScreen = false;
117
+ this.box = 0;
118
+ this.canvas?.removeEventListener('contextlost', this.handleLost);
119
+ this.canvas?.removeEventListener('contextrestored', this.handleRestored);
101
120
  this.visible?.disconnect();
102
121
  this.visible = undefined;
103
122
  this.sized?.disconnect();
@@ -107,6 +126,9 @@ let HelixOrb = class HelixOrb extends HelixElement {
107
126
  document.removeEventListener('visibilitychange', this.handleVisibility);
108
127
  }
109
128
  start() {
129
+ if (!this.isConnected) {
130
+ return;
131
+ }
110
132
  const canvas = this.renderRoot.querySelector('canvas');
111
133
  if (!canvas) {
112
134
  return;
@@ -116,6 +138,12 @@ let HelixOrb = class HelixOrb extends HelixElement {
116
138
  if (!this.context) {
117
139
  return;
118
140
  }
141
+ canvas.addEventListener('contextlost', this.handleLost);
142
+ canvas.addEventListener('contextrestored', this.handleRestored);
143
+ if (this.lost && !this.context.isContextLost()) {
144
+ this.lost = false;
145
+ canvas.toggleAttribute('hidden', false);
146
+ }
119
147
  this.motion ??= window.matchMedia('(prefers-reduced-motion: reduce)');
120
148
  this.motion.addEventListener('change', this.handleMotion);
121
149
  this.visible ??= new IntersectionObserver(([entry]) => {
@@ -177,7 +205,7 @@ let HelixOrb = class HelixOrb extends HelixElement {
177
205
  return blendStaged(this.from, to, smoother(k), smoother(Math.max(0, k - TRAIL) / (1 - TRAIL)));
178
206
  }
179
207
  play() {
180
- if (this.running || this.motion?.matches || !this.onScreen) {
208
+ if (this.running || this.motion?.matches || !this.onScreen || this.lost) {
181
209
  return;
182
210
  }
183
211
  this.running = true;
@@ -103,10 +103,6 @@ block-size: var(--helix-size-icon-tight);
103
103
  inline-size: var(--helix-size-icon-tight);
104
104
  }
105
105
 
106
- :host(:dir(rtl)) [part="first"] svg,
107
- :host(:dir(rtl)) [part="last"] svg,
108
- :host(:dir(rtl)) [part="previous"] svg,
109
- :host(:dir(rtl)) [part="next"] svg,
110
106
  :host(:dir(rtl)) ::slotted([slot="first-icon"]),
111
107
  :host(:dir(rtl)) ::slotted([slot="last-icon"]),
112
108
  :host(:dir(rtl)) ::slotted([slot="previous-icon"]),
@@ -6,6 +6,7 @@ gap: var(--helix-space-2xs);
6
6
  }
7
7
 
8
8
  [part="field"] {
9
+ align-self: start;
9
10
  display: flex;
10
11
  gap: var(--pin-input-gap, var(--helix-space-xs));
11
12
  }