@fusion.dev/helix 0.160.0 → 0.162.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 (43) hide show
  1. package/custom-elements.json +5183 -526
  2. package/dist/aura/root/aura-root.d.ts +0 -49
  3. package/dist/aura/root/aura-root.js +2 -16
  4. package/dist/chat/message/chat-message.css.js +1 -1
  5. package/dist/chat/root/chat-root.css.js +6 -1
  6. package/dist/code-block/root/code-block-root.css.js +10 -3
  7. package/dist/code-block/root/code-block-root.d.ts +4 -10
  8. package/dist/code-block/root/code-block-root.js +0 -8
  9. package/dist/element-events.generated.js +5 -0
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.js +1 -0
  12. package/dist/strands/index.d.ts +2 -0
  13. package/dist/strands/index.js +2 -0
  14. package/dist/strands/palette.d.ts +42 -0
  15. package/dist/strands/palette.js +24 -0
  16. package/dist/strands/path.d.ts +89 -0
  17. package/dist/strands/path.js +153 -0
  18. package/dist/strands/renderer.d.ts +84 -0
  19. package/dist/strands/renderer.js +255 -0
  20. package/dist/strands/root/strands-root.css.d.ts +1 -0
  21. package/dist/strands/root/strands-root.css.js +8 -0
  22. package/dist/strands/root/strands-root.d.ts +230 -0
  23. package/dist/strands/root/strands-root.js +466 -0
  24. package/dist/strands/shader.d.ts +22 -0
  25. package/dist/strands/shader.js +198 -0
  26. package/dist/strands/stop/strands-stop.css.d.ts +1 -0
  27. package/dist/strands/stop/strands-stop.css.js +2 -0
  28. package/dist/strands/stop/strands-stop.d.ts +44 -0
  29. package/dist/strands/stop/strands-stop.js +29 -0
  30. package/dist/table/root/table-root.css.js +2 -1
  31. package/dist/table/row/table-row.css.js +8 -2
  32. package/dist/table/row/table-row.d.ts +2 -0
  33. package/dist/timeline/item/timeline-item.css.js +5 -1
  34. package/dist/video-playlist/root/video-playlist-root.css.js +1 -1
  35. package/dist/video-playlist/root/video-playlist-root.d.ts +2 -0
  36. package/dist/virtual-list/root/virtual-list-root.css.js +1 -1
  37. package/dist/virtual-list/root/virtual-list-root.d.ts +1 -0
  38. package/lint.json +1 -1
  39. package/package.json +6 -6
  40. package/types/jsx.d.ts +1206 -66
  41. package/types/svelte.d.ts +2 -0
  42. package/types/vue.d.ts +314 -12
  43. package/types/words.d.ts +79 -0
@@ -325,55 +325,6 @@ export declare class HelixAura extends HelixElement {
325
325
  protected render(): TemplateResult;
326
326
  protected firstUpdated(changed: PropertyValues): void;
327
327
  private start;
328
- /**
329
- * Whether WebGL here is drawn on the CPU, asked before one is drawn into.
330
- *
331
- * **The shader costs what a GPU makes it cheap to ask for.** The note at the
332
- * top of this file measures it: on Metal the browser shares the compiled
333
- * program and each aura after the first draws its opening frame in about 2ms,
334
- * while under SwiftShader nothing is shared and each one compiles the pipeline
335
- * again for about 450ms. Measured again 15 Sep 2026 on a page of 720 pixel
336
- * auras, SwiftShader against the same page with this check in place: the worst
337
- * frame gap went 568ms to 50ms with one aura, 1626ms to 34ms with three and
338
- * 3200ms to 42ms with six, and a page of three or six ran at 13 frames a
339
- * second before and at the display's own rate after. What that buys is a field
340
- * a reader barely sees behind content, against a gradient ramped from the same
341
- * tokens. So the software path takes the fallback.
342
- *
343
- * **The unmasked name is the only one that answers.** `gl.RENDERER` is masked
344
- * to `WebKit WebGL` on Chromium, the same string on SwiftShader and on Metal,
345
- * and `WEBGL_debug_renderer_info` is what lifts the mask.
346
- *
347
- * **An unreadable renderer draws**, which is the safe direction and not the
348
- * tidy one. The extension is optional and is withheld deliberately: Firefox's
349
- * `privacy.resistFingerprinting` and Safari's equivalent both drop it, and a
350
- * browser may drop it at any time, since a GPU name is a fingerprinting
351
- * surface. Treating "we cannot tell" as software would take the shader away
352
- * from every reader on a machine that hides its hardware, which is a real GPU
353
- * far more often than not. Treating it as hardware costs a machine that hides
354
- * a software renderer the 450ms, which is the cost the component has today.
355
- *
356
- * **A probe context of its own, and given straight back.** Asking the
357
- * element's own canvas would spend it: `build` below is written around a
358
- * canvas whose context was force-lost never yielding a working one again, and
359
- * the probe has to release what it took or a page of auras runs the browser
360
- * into its cap of about sixteen. One pixel by one, and released through
361
- * `WEBGL_lose_context` the same way `drop` does.
362
- *
363
- * **The same attributes the real context asks for**, because they are part of
364
- * the question: `powerPreference` can select a different adapter, so a probe
365
- * asking for a high-performance context would name a GPU the drawing one is
366
- * never given.
367
- *
368
- * **Asked per context rather than once for the page.** The answer is not
369
- * fixed: a GPU process that crashes takes the page down to SwiftShader for
370
- * the rest of its life, and an aura rebuilding after that should get the
371
- * current answer rather than the one from before. It costs a context creation
372
- * and two calls, measured 15 Sep 2026 at about 2ms under SwiftShader and 2ms
373
- * on Metal, against the 450ms and 135ms first draws it stands in front of, and
374
- * it happens once per context rather than once per frame.
375
- */
376
- private static drawsInSoftware;
377
328
  /**
378
329
  * The context and the program, from whatever canvas can still give one.
379
330
  *
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  var HelixAura_1;
8
- import { customElement, HelixAuraFallbackEvent, HelixElement, MAX_FRAME_STEP, PaintGateController, readColor, } from '@fusion.dev/helix-core';
8
+ import { customElement, drawsInSoftware, HelixAuraFallbackEvent, HelixElement, MAX_FRAME_STEP, PaintGateController, readColor, } from '@fusion.dev/helix-core';
9
9
  import { html } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
11
11
  import { tiltFor } from '../axis.js';
@@ -15,7 +15,6 @@ import { styles } from './aura-root.css.js';
15
15
  const MAX_CANVAS_EDGE = 4096;
16
16
  const MAX_CANVAS_PIXELS = 4_194_304;
17
17
  const OPENING_PATIENCE = 500;
18
- const SOFTWARE_RENDERER = /swiftshader|llvmpipe|softpipe|software rasterizer|basic render/i;
19
18
  let HelixAura = class HelixAura extends HelixElement {
20
19
  constructor() {
21
20
  super(...arguments);
@@ -163,25 +162,12 @@ let HelixAura = class HelixAura extends HelixElement {
163
162
  this.open();
164
163
  }
165
164
  }
166
- static drawsInSoftware() {
167
- const probe = document.createElement('canvas');
168
- probe.width = 1;
169
- probe.height = 1;
170
- const gl = probe.getContext('webgl2', HelixAura_1.CONTEXT);
171
- if (!gl) {
172
- return false;
173
- }
174
- const debug = gl.getExtension('WEBGL_debug_renderer_info');
175
- const name = debug ? gl.getParameter(debug.UNMASKED_RENDERER_WEBGL) : null;
176
- gl.getExtension('WEBGL_lose_context')?.loseContext();
177
- return typeof name === 'string' && SOFTWARE_RENDERER.test(name);
178
- }
179
165
  build() {
180
166
  let canvas = this.renderRoot.querySelector('canvas');
181
167
  if (!canvas) {
182
168
  return false;
183
169
  }
184
- if (HelixAura_1.drawsInSoftware()) {
170
+ if (drawsInSoftware(HelixAura_1.CONTEXT)) {
185
171
  this.refused = 'software';
186
172
  return false;
187
173
  }
@@ -1,5 +1,5 @@
1
1
  import { createSheet } from '@fusion.dev/helix-core';
2
- export const styles = createSheet(`:host {display: block;}[part="message"] {align-items: start;display: grid;grid-template-columns: auto minmax(0, 1fr);}[part="avatar"] > slot::slotted(*) {margin-inline-end: var(--helix-space-sm);}[part="avatar"] {align-items: center;display: flex;font-size: var(--helix-text-sm);grid-column: 1;line-height: var(--helix-leading-normal);min-block-size: calc(1lh + var(--helix-space-xs) * 2);}[part="body"] {grid-column: 2;}:host([author="me"]) [part="avatar"] {display: none;}[part="body"] {display: grid;gap: var(--helix-space-2xs);justify-items: start;max-inline-size: min(var(--chat-message-measure, 42rem), 80%);}:host([author="me"]) [part="body"] {justify-items: end;justify-self: end;}[part="actions"] {opacity: 0;pointer-events: none;transition: opacity var(--helix-duration-fast) var(--helix-easing-standard);}:host(:hover) [part="actions"],
2
+ export const styles = createSheet(`:host {display: block;}:host(:focus-visible) {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1);}[part="message"] {align-items: start;display: grid;grid-template-columns: auto minmax(0, 1fr);}[part="avatar"] > slot::slotted(*) {margin-inline-end: var(--helix-space-sm);}[part="avatar"] {align-items: center;display: flex;font-size: var(--helix-text-sm);grid-column: 1;line-height: var(--helix-leading-normal);min-block-size: calc(1lh + var(--helix-space-xs) * 2);}[part="body"] {grid-column: 2;}:host([author="me"]) [part="avatar"] {display: none;}[part="body"] {display: grid;gap: var(--helix-space-2xs);justify-items: start;max-inline-size: min(var(--chat-message-measure, 42rem), 80%);}:host([author="me"]) [part="body"] {justify-items: end;justify-self: end;}[part="actions"] {opacity: 0;pointer-events: none;transition: opacity var(--helix-duration-fast) var(--helix-easing-standard);}:host(:hover) [part="actions"],
3
3
  :host(:focus-within) [part="actions"] {opacity: 1;pointer-events: auto;}@media (hover: none) {[part="actions"] {opacity: 1;pointer-events: auto;}}[part="actions"] {--helix-size-control: calc(var(--helix-size-control-sm) - var(--helix-space-2xs));--helix-size-control-loose: calc(var(--helix-size-control) * 1.2);--helix-size-control-tight: calc(var(--helix-size-control) * 0.8);--helix-size-icon: var(--helix-size-icon-sm);--helix-size-icon-loose: calc(var(--helix-size-icon) * 1.2);--helix-size-icon-tight: calc(var(--helix-size-icon) * 0.8);align-items: center;display: inline-flex;gap: var(--helix-space-2xs);}[part="actions"][hidden] {display: none;}[part="sources"] {--helix-size-avatar: var(--helix-size-avatar-sm);--helix-size-tag: var(--helix-size-tag-sm);--helix-size-icon: var(--helix-size-icon-sm);--helix-size-icon-loose: calc(var(--helix-size-icon) * 1.2);--helix-size-icon-tight: calc(var(--helix-size-icon) * 0.8);align-items: center;color: var(--helix-color-foreground-muted);display: flex;flex-wrap: wrap;font-family: var(--helix-font-sans);font-size: var(--helix-text-xs);gap: var(--helix-space-2xs);line-height: var(--helix-leading-normal);}[part="sources"][hidden] {display: none;}[part="content"] {background-color: var(
4
4
  --chat-message-background-color,
5
5
  color-mix(in oklab, var(--helix-color-foreground) 14%, transparent)
@@ -4,6 +4,11 @@ var(--chat-scrollbar-track-color, var(--helix-scrollbar-track-color));scrollbar-
4
4
  var(--chat-scrollbar-track-color, var(--helix-scrollbar-track-color));}[part="header"][hidden],
5
5
  [part="resume"][hidden],
6
6
  [part="composer"][hidden] {display: none;}[part="status"][data-empty] {padding-block: 0;}[part="header"] {--helix-size-tag: var(--helix-size-tag-sm);align-items: center;border-block-end: var(--helix-border-thin) solid var(--chat-rule, var(--helix-color-border));display: flex;gap: var(--helix-space-sm);justify-content: space-between;min-block-size: var(--helix-size-control);padding-block: var(--helix-space-2xs);padding-inline: var(--chat-padding-inline, var(--helix-space-md));}[part="header"] > [part="actions"] {--helix-size-control: var(--helix-size-control-sm);--helix-size-control-loose: calc(var(--helix-size-control) * 1.2);--helix-size-control-tight: calc(var(--helix-size-control) * 0.8);--helix-size-icon: var(--helix-size-icon-sm);--helix-size-icon-loose: calc(var(--helix-size-icon) * 1.2);--helix-size-icon-tight: calc(var(--helix-size-icon) * 0.8);align-items: center;display: flex;flex: 0 0 auto;gap: var(--helix-space-2xs);}[part="header"] > [part="actions"][hidden] {display: none;}[part="resume"] {display: grid;justify-items: center;margin-block-start: calc(var(--helix-space-xl) * -1);pointer-events: none;position: relative;}[part="resume"] > * {pointer-events: auto;}[part="resume"] slot::slotted(*) {border: var(--chat-resume-border-width, var(--helix-border-floating)) solid
7
- var(--chat-resume-border-color, var(--helix-color-border));box-shadow: var(--chat-resume-shadow, var(--helix-shadow-floating));}[part="status"] {color: var(--helix-color-foreground-muted);font-family: var(--helix-font-sans);font-size: var(--helix-text-xs);padding-block-end: var(--helix-space-2xs);padding-block-start: var(--helix-space-xs);padding-inline: var(--chat-padding-inline, var(--helix-space-md));}[part="typing"] {align-items: center;color: var(--chat-typing-color, var(--helix-color-foreground-muted));display: inline-flex;gap: var(--chat-typing-gap, var(--helix-space-xs));}[part="composer"] {padding-block: var(--helix-space-sm);padding-inline: var(--chat-padding-inline, var(--helix-space-md));}[part="empty"],
7
+ var(--chat-resume-border-color, var(--helix-color-border));box-shadow: var(--chat-resume-shadow, var(--helix-shadow-floating));}[part="resume-button"] {all: unset;box-sizing: border-box;align-items: center;background-color: var(--helix-color-surface-raised);block-size: var(--helix-size-control);border: var(--chat-resume-border-width, var(--helix-border-floating)) solid
8
+ var(--chat-resume-border-color, var(--helix-color-border));border-radius: var(--helix-radii-sm);box-shadow: var(--chat-resume-shadow, var(--helix-shadow-floating));color: var(--helix-color-foreground);cursor: pointer;display: inline-flex;font-family: var(--helix-font-sans);font-size: var(--helix-text-control);font-weight: var(--helix-weight-medium);gap: var(--helix-space-xs);line-height: var(--helix-leading-tight);padding-inline: var(--helix-space-control);pointer-events: auto;white-space: nowrap;}[part="resume-button"]:hover {background-color: color-mix(
9
+ in oklab,
10
+ var(--helix-color-foreground) 6%,
11
+ var(--helix-color-surface-raised)
12
+ );}[part="resume-button"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: var(--helix-size-focus-offset);}[part="resume-mark"] {block-size: var(--helix-size-icon-sm);display: inline-flex;inline-size: var(--helix-size-icon-sm);}[part="resume-mark"] svg {block-size: 100%;inline-size: 100%;}[part="status"] {color: var(--helix-color-foreground-muted);font-family: var(--helix-font-sans);font-size: var(--helix-text-xs);padding-block-end: var(--helix-space-2xs);padding-block-start: var(--helix-space-xs);padding-inline: var(--chat-padding-inline, var(--helix-space-md));}[part="typing"] {align-items: center;color: var(--chat-typing-color, var(--helix-color-foreground-muted));display: inline-flex;gap: var(--chat-typing-gap, var(--helix-space-xs));}[part="composer"] {padding-block: var(--helix-space-sm);padding-inline: var(--chat-padding-inline, var(--helix-space-md));}[part="empty"],
8
13
  [part="loading"] {--_collection-inset-block: var(--helix-space-lg);--_collection-inset-inline: var(--helix-space-none);}[part="history"] {color: var(--helix-color-foreground-subtle);font-family: var(--helix-font-sans);font-size: var(--helix-text-sm);margin: var(--helix-space-none);padding-block: var(--helix-space-lg);text-align: center;}[part="log"][data-windowed] {gap: var(--helix-space-none);}[part="turn"] {flex: none;}[part="turn"]:not([data-index="0"]) {padding-block-start: var(--chat-gap, var(--helix-space-md));}@media print {:host {max-block-size: none;}[part="log"] {overflow-y: visible;}[part="composer"],
9
14
  [part="resume"] {display: none;}}`);
@@ -1,8 +1,15 @@
1
1
  import { createSheet } from '@fusion.dev/helix-core';
2
- export const styles = createSheet(`:host {--_code-block-size: var(--code-block-font-size, var(--helix-text-sm));--_code-block-leading: var(--code-block-line-height, var(--helix-leading-normal));--_code-block-face: var(--code-block-font-family, var(--helix-font-mono));--_code-block-copy-inset: calc(var(--helix-space-sm) + (1lh - var(--helix-size-control-sm)) / 2);--_code-block-gutter: calc(var(--helix-size-control-sm) + var(--_code-block-copy-inset) * 2);display: flex;flex-direction: column;}[part="frame"] {flex: 1;}:host(:not([diff])) [part="frame"] {font-size: var(--_code-block-size);line-height: var(--_code-block-leading);}[part="header"] {align-items: center;backdrop-filter: var(--code-block-backdrop, none);background-color: var(--code-block-header-background-color, var(--helix-color-surface-raised));border: var(--helix-border-thin) solid var(--code-block-border-color, var(--helix-color-border));border-end-end-radius: var(--helix-radii-none);border-end-start-radius: var(--helix-radii-none);border-start-end-radius: var(--code-block-radius, var(--helix-radii-lg));border-start-start-radius: var(--code-block-radius, var(--helix-radii-lg));display: flex;gap: var(--helix-space-sm);padding-block: var(--helix-space-xs);padding-inline: var(--helix-space-md);}[part="header"]:has(> [part="copy"]) {padding-block: var(--helix-space-2xs);padding-inline-end: var(--helix-space-2xs);}[part="label"] {color: var(--code-block-label-color, var(--helix-color-foreground-muted));font-family: var(--helix-font-sans);font-size: var(--helix-text-xs);font-weight: var(--helix-weight-medium);min-inline-size: 0;overflow-wrap: anywhere;}[part="frame"] {position: relative;}[part="block"] {block-size: 100%;}[part="copy"] {--icon-button-size: var(--helix-size-control-sm);}[part="header"] > [part="copy"] {margin-inline-start: auto;}[part="frame"] > [part="copy"] {font-size: var(--_code-block-size);inset-block-start: var(--_code-block-copy-inset);inset-inline-end: var(--_code-block-copy-inset);line-height: var(--_code-block-leading);position: absolute;}[part="block"] {backdrop-filter: var(--code-block-backdrop, none);background-color: var(--code-block-background-color, var(--helix-color-background));border: var(--helix-border-thin) solid var(--code-block-border-color, var(--helix-color-border));border-radius: var(--code-block-radius, var(--helix-radii-lg));margin: var(--helix-space-none);overflow-x: auto;padding-block: var(--helix-space-sm);padding-inline: var(--helix-space-md);}:host(:not([diff], [no-copy])) [part="frame"]:not([part="header"] + *) > [part="block"] {padding-inline-end: var(--_code-block-gutter);}[part="header"] + [part="frame"] [part="block"] {border-block-start-width: var(--helix-space-none);border-start-end-radius: var(--helix-radii-none);border-start-start-radius: var(--helix-radii-none);}[part="code"] {color: var(--code-block-color, var(--helix-color-foreground));font-family: var(--_code-block-face);font-size: var(--_code-block-size);line-height: var(--_code-block-leading);}:host(:not([diff])) [part="code"] {display: block;max-block-size: var(--code-block-max-block-size, none);overflow-x: auto;}:host(:state(expanded)) [part="code"] {max-block-size: none;}[part="expand"] {all: unset;box-sizing: border-box;border-radius: var(--helix-radii-sm);color: var(--code-block-expand-color, var(--helix-color-foreground-muted));cursor: pointer;font-size: var(--helix-text-sm);padding: var(--helix-space-xs);}[part="expand"]:hover {color: var(--helix-color-foreground);}[part="expand"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: var(--helix-size-focus-offset);}[part="block"]:focus-visible,
2
+ export const styles = createSheet(`:host {--_code-block-size: var(--code-block-font-size, var(--helix-text-sm));--_code-block-leading: var(--code-block-line-height, var(--helix-leading-normal));--_code-block-face: var(--code-block-font-family, var(--helix-font-mono));--_code-block-copy-inset: calc(var(--helix-space-sm) + (1lh - var(--helix-size-control-sm)) / 2);--_code-block-gutter: calc(var(--helix-size-control-sm) + var(--_code-block-copy-inset) * 2);display: flex;flex-direction: column;}[part="frame"] {flex: 1;}:host(:not([diff])) [part="frame"] {font-size: var(--_code-block-size);line-height: var(--_code-block-leading);}[part="header"] {align-items: center;backdrop-filter: var(--code-block-backdrop, none);background-color: var(--code-block-header-background-color, var(--helix-color-surface-raised));border: var(--helix-border-thin) solid var(--code-block-border-color, var(--helix-color-border));border-end-end-radius: var(--helix-radii-none);border-end-start-radius: var(--helix-radii-none);border-start-end-radius: var(--code-block-radius, var(--helix-radii-lg));border-start-start-radius: var(--code-block-radius, var(--helix-radii-lg));display: flex;gap: var(--helix-space-sm);padding-block: var(--helix-space-xs);padding-inline: var(--helix-space-md);}[part="header"]:has(> [part="copy"]) {padding-block: var(--helix-space-2xs);padding-inline-end: var(--helix-space-2xs);}[part="label"] {color: var(--code-block-label-color, var(--helix-color-foreground-muted));font-family: var(--helix-font-sans);font-size: var(--helix-text-xs);font-weight: var(--helix-weight-medium);min-inline-size: 0;overflow-wrap: anywhere;}[part="frame"] {position: relative;}[part="block"] {block-size: 100%;}[part="copy"] {--icon-button-size: var(--helix-size-control-sm);}[part="header"] > [part="copy"] {margin-inline-start: auto;}[part="actions"] {align-items: center;display: flex;gap: var(--helix-space-2xs);margin-inline-start: auto;}[part="header"]
3
+ > :is([part="actions"], [part="wrap-toggle"])
4
+ ~ :is([part="copy"], [part="wrap-toggle"]) {margin-inline-start: var(--helix-space-none);}[part="wrap-toggle"] {all: unset;box-sizing: border-box;border-radius: var(--helix-radii-sm);color: var(--code-block-label-color, var(--helix-color-foreground-muted));cursor: pointer;font-family: var(--helix-font-sans);font-size: var(--helix-text-xs);font-weight: var(--helix-weight-medium);margin-inline-start: auto;padding-block: var(--helix-space-2xs);padding-inline: var(--helix-space-xs);}[part="wrap-toggle"]:hover,
5
+ [part="wrap-toggle"][aria-pressed="true"] {color: var(--helix-color-foreground);}[part="wrap-toggle"][aria-pressed="true"] {background-color: color-mix(in oklab, var(--helix-color-foreground) 8%, transparent);}[part="wrap-toggle"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: var(--helix-size-focus-offset);}[part="frame"] > [part="copy"] {font-size: var(--_code-block-size);inset-block-start: var(--_code-block-copy-inset);inset-inline-end: var(--_code-block-copy-inset);line-height: var(--_code-block-leading);position: absolute;}[part="block"] {backdrop-filter: var(--code-block-backdrop, none);background-color: var(--code-block-background-color, var(--helix-color-background));border: var(--helix-border-thin) solid var(--code-block-border-color, var(--helix-color-border));border-radius: var(--code-block-radius, var(--helix-radii-lg));margin: var(--helix-space-none);overflow-x: auto;padding-block: var(--helix-space-sm);padding-inline: var(--helix-space-md);}:host(:not([diff], [no-copy])) [part="frame"]:not([part="header"] + *) > [part="block"] {padding-inline-end: var(--_code-block-gutter);}[part="header"] + [part="frame"] [part="block"] {border-block-start-width: var(--helix-space-none);border-start-end-radius: var(--helix-radii-none);border-start-start-radius: var(--helix-radii-none);}[part="code"] {color: var(--code-block-color, var(--helix-color-foreground));font-family: var(--_code-block-face);font-size: var(--_code-block-size);line-height: var(--_code-block-leading);}:host(:not([diff])) [part="code"] {display: block;isolation: isolate;max-block-size: var(--code-block-max-block-size, none);overflow-x: auto;position: relative;}:host(:state(expanded)) [part="code"] {max-block-size: none;}[part="expand"] {all: unset;box-sizing: border-box;border-radius: var(--helix-radii-sm);color: var(--code-block-expand-color, var(--helix-color-foreground-muted));cursor: pointer;font-size: var(--helix-text-sm);padding: var(--helix-space-xs);}[part="expand"]:hover {color: var(--helix-color-foreground);}[part="expand"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: var(--helix-size-focus-offset);}[part="block"]:focus-visible,
3
6
  [part="code"]:focus-visible,
4
7
  [part="old"]:focus-visible,
5
- [part="new"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1);}:host([wrap]:not([diff], [line-numbers])) [part="code"] {overflow-wrap: anywhere;white-space: pre-wrap;}:host([line-numbers]:not([diff])) [part="block"] {column-gap: var(--helix-space-md);display: grid;grid-template-columns: auto minmax(0, 1fr);}[part="gutter"] {color: var(--code-block-gutter-color, var(--helix-color-foreground-subtle));font-family: var(--_code-block-face);font-size: var(--_code-block-size);font-variant-numeric: tabular-nums;line-height: var(--_code-block-leading);text-align: end;user-select: none;}:host([diff]) {gap: var(--helix-space-md);}:host([diff]) [part="file"] {container-type: inline-size;display: flex;flex-direction: column;}:host([diff]) [part="block"],
8
+ [part="new"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1);}:host([wrap]:not([diff])) [part="code"] {overflow-wrap: anywhere;white-space: pre-wrap;}[part="mark"] {background-color: var(
9
+ --code-block-mark-color,
10
+ color-mix(in oklab, var(--helix-color-primary) 12%, transparent)
11
+ );block-size: var(--code-block-mark-size, 0);border-inline-start: var(--helix-border-thick) solid
12
+ var(--code-block-mark-edge-color, var(--helix-color-primary));box-sizing: border-box;inline-size: max(100%, var(--code-block-mark-inline-size, 100%));inset-block-start: var(--code-block-mark-start, 0);inset-inline-start: 0;pointer-events: none;position: absolute;z-index: var(--helix-layer-beneath);}:host([line-numbers]:not([diff])) [part="block"] {column-gap: var(--helix-space-md);display: grid;grid-template-columns: auto minmax(0, 1fr);}[part="gutter"] {color: var(--code-block-gutter-color, var(--helix-color-foreground-subtle));font-family: var(--_code-block-face);font-size: var(--_code-block-size);font-variant-numeric: tabular-nums;line-height: var(--_code-block-leading);text-align: end;user-select: none;}:host([diff]) {gap: var(--helix-space-md);}:host([diff]) [part="file"] {container-type: inline-size;display: flex;flex-direction: column;}:host([diff]) [part="block"],
6
13
  [part="old"],
7
14
  [part="new"] {backdrop-filter: var(--code-block-backdrop, none);background-color: var(--code-block-background-color, var(--helix-color-background));border: var(--helix-border-thin) solid var(--code-block-border-color, var(--helix-color-border));border-radius: var(--code-block-radius, var(--helix-radii-lg));margin: var(--helix-space-none);min-inline-size: 0;overflow-x: auto;padding-block: var(--helix-space-none) var(--helix-space-sm);padding-inline: var(--helix-space-none);}:host([diff]:not([no-copy]))
8
15
  [part="frame"]:not([part="header"] + *)
@@ -30,7 +37,7 @@ var(--helix-color-foreground)
30
37
  in oklab,
31
38
  var(--code-block-removed-color, var(--helix-color-danger)) 55%,
32
39
  var(--helix-color-foreground)
33
- );}[part="filler"] {background-color: var(--helix-color-surface-sunken);min-block-size: 1lh;}[part="number"] {color: var(--helix-color-foreground-muted);font-variant-numeric: tabular-nums;padding-inline-end: var(--helix-space-sm);user-select: none;}@media print {:host {background-color: transparent;}:host([diff]) [part="block"],
40
+ );}[part="filler"] {background-color: var(--helix-color-surface-sunken);min-block-size: 1lh;}[part="number"] {color: var(--helix-color-foreground-muted);font-variant-numeric: tabular-nums;padding-inline-end: var(--helix-space-sm);user-select: none;}[part="gutter"] > [part="number"] {block-size: var(--code-block-line-size, auto);color: inherit;display: block;padding-inline-end: var(--helix-space-none);}@media print {:host {background-color: transparent;}:host([diff]) [part="block"],
34
41
  [part="old"],
35
42
  [part="new"] {overflow: visible;white-space: pre-wrap;}[part="added"],
36
43
  [part="removed"],
@@ -18,7 +18,8 @@ import '../../copy-button/index.js';
18
18
  *
19
19
  * Long lines scroll by default. Set `wrap` to wrap them instead, for prose-like
20
20
  * code such as a long command, and set `--code-block-max-block-size` to cap the
21
- * height of a long file, which then scrolls.
21
+ * height of a long file, which then scrolls. A line `mark-lines` names takes a
22
+ * band in the primary color, with an edge at its start.
22
23
  *
23
24
  * @since 0.1.0
24
25
  * @status stable
@@ -38,21 +39,14 @@ import '../../copy-button/index.js';
38
39
  * @cssproperty --code-block-header-background-color - The background behind the name. The default is `surface-raised`.
39
40
  * @cssproperty --code-block-label-color - The color of the name in the header.
40
41
  * @cssproperty --code-block-line-height - The line height of the code and its line numbers, as a number or a length.
42
+ * @cssproperty --code-block-mark-color - The band behind the lines `mark-lines` names. The primary color at a wash by default.
43
+ * @cssproperty --code-block-mark-edge-color - The edge at the start of a band behind marked lines. The primary color by default.
41
44
  * @cssproperty --code-block-max-block-size - The tallest the code grows before it scrolls, or until `expandable`'s button shows every line. The default is no limit.
42
45
  * @cssproperty --code-block-radius - The corner radius of the header's top and the block's bottom, which form one shape.
43
46
  * @cssproperty --code-block-removed-color - The color a diff's removed lines are tinted in and their `-` is drawn in. The danger color by default.
44
47
  */
45
48
  export declare class HelixCodeBlock extends HelixHeadlessCodeBlock {
46
49
  static tier: string;
47
- /**
48
- * Whether long lines wrap.
49
- *
50
- * Off by default, so long lines scroll, because a line break can be
51
- * significant in code and a wrapped line reads as two. For prose-like code, such as a long command or
52
- * a log line, a wrap reads better than a scroll. Line numbers keep the lines
53
- * unwrapped, since a wrapped line would take one number and several rows.
54
- */
55
- wrap: boolean;
56
50
  static styles: CSSResultGroup;
57
51
  }
58
52
  declare global {
@@ -6,20 +6,12 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { customElement } from '@fusion.dev/helix-core';
8
8
  import { HelixHeadlessCodeBlock } from '@fusion.dev/helix-headless/code-block';
9
- import { property } from 'lit/decorators.js';
10
9
  import { styles } from './code-block-root.css.js';
11
10
  import '../../copy-button/index.js';
12
11
  let HelixCodeBlock = class HelixCodeBlock extends HelixHeadlessCodeBlock {
13
- constructor() {
14
- super(...arguments);
15
- this.wrap = false;
16
- }
17
12
  static { this.tier = 'helix-'; }
18
13
  static { this.styles = styles; }
19
14
  };
20
- __decorate([
21
- property({ reflect: true, type: Boolean })
22
- ], HelixCodeBlock.prototype, "wrap", void 0);
23
15
  HelixCodeBlock = __decorate([
24
16
  customElement('helix-code-block')
25
17
  ], HelixCodeBlock);
@@ -32,6 +32,7 @@ registerElementEvents({
32
32
  'helix-chat-message': ['helix-send'],
33
33
  'helix-checkbox': ['change', 'helix-before-toggle', 'input'],
34
34
  'helix-checkbox-group': ['change', 'helix-before-change', 'input'],
35
+ 'helix-code-block': ['helix-before-toggle', 'helix-toggle'],
35
36
  'helix-color-input': [
36
37
  'change',
37
38
  'helix-before-change',
@@ -247,16 +248,20 @@ registerElementEvents({
247
248
  ],
248
249
  'helix-step': ['helix-select'],
249
250
  'helix-steps': ['helix-activate', 'helix-before-activate', 'helix-select'],
251
+ 'helix-strands': ['helix-strands-fallback'],
250
252
  'helix-switch': ['change', 'helix-before-toggle', 'input'],
251
253
  'helix-tab': ['helix-dismiss', 'helix-select'],
252
254
  'helix-tab-panel': ['helix-load'],
253
255
  'helix-table': [
254
256
  'helix-before-reorder',
257
+ 'helix-before-toggle',
258
+ 'helix-load-more',
255
259
  'helix-reorder',
256
260
  'helix-select',
257
261
  'helix-selected',
258
262
  'helix-sort',
259
263
  'helix-sorted',
264
+ 'helix-toggle',
260
265
  ],
261
266
  'helix-table-column': ['helix-before-resize', 'helix-resized'],
262
267
  'helix-tabs': ['helix-activate', 'helix-before-activate', 'helix-select'],
package/dist/index.d.ts CHANGED
@@ -90,6 +90,7 @@ export * from './speed-dial/index.js';
90
90
  export * from './splitter/index.js';
91
91
  export * from './stat/index.js';
92
92
  export * from './steps/index.js';
93
+ export * from './strands/index.js';
93
94
  export * from './switch/index.js';
94
95
  export * from './table/index.js';
95
96
  export * from './tabs/index.js';
package/dist/index.js CHANGED
@@ -85,6 +85,7 @@ export * from './speed-dial/index.js';
85
85
  export * from './splitter/index.js';
86
86
  export * from './stat/index.js';
87
87
  export * from './steps/index.js';
88
+ export * from './strands/index.js';
88
89
  export * from './switch/index.js';
89
90
  export * from './table/index.js';
90
91
  export * from './tabs/index.js';
@@ -0,0 +1,2 @@
1
+ export * from './root/strands-root.js';
2
+ export * from './stop/strands-stop.js';
@@ -0,0 +1,2 @@
1
+ export * from './root/strands-root.js';
2
+ export * from './stop/strands-stop.js';
@@ -0,0 +1,42 @@
1
+ import type { ColorBytes, TokenColor } from '@fusion.dev/helix-core';
2
+ /** The colors the ribbon is drawn and composited in, as channels in `0..1`. */
3
+ export interface StrandsPalette {
4
+ /** Most strands. */
5
+ glow: TokenColor;
6
+ /** The page the light is added to. */
7
+ ground: TokenColor;
8
+ /** Whether the ground is light, which turns added light into ink. */
9
+ light: boolean;
10
+ /** The hottest strands, and on a light page the ink everything darkens toward. */
11
+ peak: TokenColor;
12
+ /** A minority of strands, for depth. */
13
+ tint: TokenColor;
14
+ }
15
+ /** The computed colors the canvas carries, one per stop and two for the peak. */
16
+ export type StrandsPaletteStyle = Pick<CSSStyleDeclaration, 'caretColor' | 'color' | 'columnRuleColor' | 'outlineColor' | 'textDecorationColor'>;
17
+ /** The default theme's dark page, ink on black, which is drawn until a read. */
18
+ export declare const RESTING_PALETTE: StrandsPalette;
19
+ /**
20
+ * The palette, from the colors the canvas's stylesheet resolved for it.
21
+ *
22
+ * **Carried on color properties of the canvas, which a canvas never paints**,
23
+ * the way `Aura` carries its ramp: a custom property is handed back as the
24
+ * text that was written, and a color property is resolved by the engine first.
25
+ * Read there, each color takes the `--strands-*` hook a page wrote anywhere
26
+ * above the element, and the token otherwise:
27
+ *
28
+ * - `color`: `--strands-glow`, or `primary`.
29
+ * - `text-decoration-color`: `--strands-tint`, or `info`.
30
+ * - `outline-color`: `--strands-ground`, or the background.
31
+ * - `caret-color` and `column-rule-color`: `--strands-peak`, or the dark and
32
+ * the light page's own peak.
33
+ *
34
+ * **The ground says which way the page runs**, read from its luminance rather
35
+ * than asked of the color mode, which is a private token this tier may not
36
+ * reach for. A dark page adds light toward a bright peak; a light page darkens
37
+ * toward a deep one, because light added to near-white is invisible.
38
+ *
39
+ * @param style The canvas's computed style.
40
+ * @param read Turns a resolved color into bytes, `undefined` where it cannot.
41
+ */
42
+ export declare const paletteOf: (style: StrandsPaletteStyle, read: (value: string) => ColorBytes | undefined) => StrandsPalette;
@@ -0,0 +1,24 @@
1
+ export const RESTING_PALETTE = {
2
+ glow: [0.88, 0.88, 0.88],
3
+ ground: [0, 0, 0],
4
+ light: false,
5
+ peak: [1, 1, 1],
6
+ tint: [0.6, 0.6, 0.6],
7
+ };
8
+ export const paletteOf = (style, read) => {
9
+ const channels = (value, fallback) => {
10
+ const bytes = read(value);
11
+ return bytes ? [bytes[0] / 255, bytes[1] / 255, bytes[2] / 255] : [...fallback];
12
+ };
13
+ const ground = channels(style.outlineColor, RESTING_PALETTE.ground);
14
+ const light = ground[0] * 0.2126 + ground[1] * 0.7152 + ground[2] * 0.0722 > 0.5;
15
+ return {
16
+ glow: channels(style.color, RESTING_PALETTE.glow),
17
+ ground,
18
+ light,
19
+ peak: light
20
+ ? channels(style.columnRuleColor, [0.02, 0.02, 0.02])
21
+ : channels(style.caretColor, RESTING_PALETTE.peak),
22
+ tint: channels(style.textDecorationColor, RESTING_PALETTE.tint),
23
+ };
24
+ };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The shape of the ribbon, as points the shader joins with a Catmull-Rom
3
+ * spline.
4
+ *
5
+ * Pure, so `path.pure.spec.ts` can hold the geometry without a GPU: what a
6
+ * shape is, where a stop puts it, and which stretch of a page-long path is on
7
+ * screen are all arithmetic.
8
+ */
9
+ /** The three forms the ribbon takes. */
10
+ export type StrandsVariant = 'fan' | 'ribbon' | 'waves';
11
+ /**
12
+ * One point on the centerline: `x` as a share of the width, `y` in CSS pixels,
13
+ * the ribbon's width as a share of the width, and its brightness from `0` to
14
+ * `1`.
15
+ *
16
+ * **`x` as a share and `y` in pixels**, because the two ends of a page answer
17
+ * differently to a resize: a reader widening the window wants the ribbon to
18
+ * keep its place across the page, and a section's position down the page is
19
+ * pixels the layout already decided.
20
+ */
21
+ export type PathPoint = readonly [x: number, y: number, width: number, brightness: number];
22
+ /** A stop, resolved: the form it asks for, and the element it is pinned to. */
23
+ export interface PlacedStop {
24
+ /** Where the shape sits across the page, as a share of the width. */
25
+ at: number | undefined;
26
+ /** The target's height, in CSS pixels. */
27
+ height: number;
28
+ /** The target's top, in page pixels. */
29
+ top: number;
30
+ variant: StrandsVariant;
31
+ }
32
+ /**
33
+ * The most points a path may carry, which is the length of the shader's
34
+ * uniform array.
35
+ *
36
+ * WebGL2 guarantees 256 vertex uniform vectors, and this takes 96 of them. A
37
+ * page-long ribbon spends one point per half screen, so 96 covers a page more
38
+ * than forty screens long; a longer one keeps its first 96 points and fades.
39
+ */
40
+ export declare const MAX_POINTS = 96;
41
+ /**
42
+ * The ribbon drawn inside its own box, which is what an element without
43
+ * `travel` shows.
44
+ *
45
+ * @param variant The form to draw.
46
+ * @param at Where it sits across the box, as a share of the width. A wave
47
+ * crosses the whole width and does not read it.
48
+ * @param height The box's height, in CSS pixels. Its width needs no
49
+ * argument: `x` is a share of it.
50
+ */
51
+ export declare const boxPath: (variant: StrandsVariant, at: number | undefined, height: number) => PathPoint[];
52
+ /**
53
+ * The ribbon laid down a whole page, which is what an element with `travel`
54
+ * shows.
55
+ *
56
+ * Each stop places its form at its target, in page order. Between two stops
57
+ * the ribbon fills the gap with soft S-curves in `fill`'s place, except
58
+ * between two waves, where it turns off screen so each wave reads alone.
59
+ * Without stops, `fill` runs the length of the page: a fan opens the page and
60
+ * a ribbon follows it, waves cross every screen and a third, and a ribbon
61
+ * meanders the whole way. After a last wave the ribbon does not come back, so
62
+ * the page ends on the wave.
63
+ *
64
+ * @param stops The resolved stops, in any order.
65
+ * @param fill The root's own form, for the stretches no stop claims.
66
+ * @param width The viewport's width, in CSS pixels.
67
+ * @param height The viewport's height, in CSS pixels.
68
+ * @param page The page's height, in CSS pixels.
69
+ */
70
+ export declare const pagePath: (stops: readonly PlacedStop[], fill: StrandsVariant, width: number, height: number, page: number) => PathPoint[];
71
+ /** One coordinate of a uniform Catmull-Rom segment, the same one the shader runs. */
72
+ export declare const catmull: (p0: number, p1: number, p2: number, p3: number, f: number) => number;
73
+ /**
74
+ * The centerline's `y` at even steps of its parameter, for finding which
75
+ * stretch is on screen.
76
+ *
77
+ * Twenty-four samples a segment, which is finer than any segment is tall on a
78
+ * screen, and computed once a layout rather than once a frame.
79
+ */
80
+ export declare const lookup: (points: readonly PathPoint[]) => Float32Array;
81
+ /**
82
+ * The stretch of the path that lies between `top` and `bottom`, as a range of
83
+ * its parameter.
84
+ *
85
+ * Drawing only that stretch is what keeps a page-long ribbon as fine as a
86
+ * screen-sized one: the same number of segments, spent where a reader looks.
87
+ * A path that does not reach the window answers an empty range.
88
+ */
89
+ export declare const visibleRange: (table: Float32Array, top: number, bottom: number) => readonly [number, number];
@@ -0,0 +1,153 @@
1
+ export const MAX_POINTS = 96;
2
+ const NARROW = 760;
3
+ const GLOW = { fan: 1, ribbon: 0.36, waves: 0.7 };
4
+ const WIDTH = { fan: 0.07, ribbon: 0.24, waves: 0.06 };
5
+ const FAN_AT = 0.4;
6
+ const fan = (top, height, at, narrow) => {
7
+ const x = narrow ? Math.max(at, 0.8) : at;
8
+ const y = top + height * 0.58;
9
+ const arm = narrow ? 0.12 : 0.15;
10
+ return [
11
+ [x + 0.38, y - height * 0.73, WIDTH.fan, 0.2],
12
+ [x + arm, y - height * 0.3, WIDTH.fan * 0.5, 0.9],
13
+ [x, y, 0.005, GLOW.fan],
14
+ [x + arm, y + height * 0.3, WIDTH.fan * 0.5, 0.9],
15
+ [x + 0.45, y + height * 0.54, WIDTH.fan * 1.15, 0.7],
16
+ ];
17
+ };
18
+ const wave = (y, height, fromRight, narrow) => {
19
+ const w = narrow ? WIDTH.waves * 2 : WIDTH.waves;
20
+ const glow = narrow ? GLOW.waves * 0.65 : GLOW.waves;
21
+ const side = (share) => (fromRight ? 1 - share : share);
22
+ return [
23
+ [side(-0.15), y - height * 0.35, w, 0],
24
+ [side(-0.05), y, w, glow * 0.6],
25
+ [side(0.3), y + 18, w, glow],
26
+ [side(0.7), y - 18, w, glow],
27
+ [side(1.05), y, w, glow * 0.6],
28
+ [side(1.15), y + height * 0.35, w, 0],
29
+ ];
30
+ };
31
+ const ribbon = (from, to, height, at, phase, narrow) => {
32
+ const points = [];
33
+ const step = height * 0.55;
34
+ const swing = narrow ? 0.26 : 0.3;
35
+ const wide = narrow ? 0.45 : WIDTH.ribbon;
36
+ const glow = narrow ? GLOW.ribbon * 0.65 : GLOW.ribbon;
37
+ let a = phase;
38
+ for (let y = from; y < to; y += step) {
39
+ a += (step / (1.7 * height)) * Math.PI;
40
+ points.push([at + swing * Math.sin(a), y, wide * (0.8 + 0.3 * Math.sin(a * 0.63 + 0.4)), glow]);
41
+ }
42
+ return { phase: a, points };
43
+ };
44
+ export const boxPath = (variant, at, height) => {
45
+ const narrow = false;
46
+ if (variant === 'fan') {
47
+ return fan(0, height, at ?? FAN_AT, narrow);
48
+ }
49
+ if (variant === 'waves') {
50
+ return wave(height * 0.5, height, false, narrow);
51
+ }
52
+ const start = -height * 0.4;
53
+ const lead = [at ?? 0.5, start - height * 0.4, WIDTH.ribbon, 0];
54
+ const body = ribbon(start, height * 1.4, height, at ?? 0.5, 0, narrow).points.map(([x, y, w]) => [x, y, w, narrow ? GLOW.ribbon * 0.65 * 1.4 : GLOW.ribbon * 1.4]);
55
+ return [lead, ...body];
56
+ };
57
+ export const pagePath = (stops, fill, width, height, page) => {
58
+ const narrow = width < NARROW;
59
+ const placed = [...stops].sort((one, two) => one.top - two.top);
60
+ if (placed.length === 0) {
61
+ if (fill === 'waves') {
62
+ for (let y = height * 0.6; y < page; y += height * 1.3) {
63
+ placed.push({ at: undefined, height, top: y, variant: 'waves' });
64
+ }
65
+ }
66
+ else if (fill === 'fan') {
67
+ placed.push({ at: undefined, height, top: 0, variant: 'fan' });
68
+ }
69
+ }
70
+ const points = [[0.6, -height * 0.9, WIDTH.ribbon, 0]];
71
+ let phase = 1.2;
72
+ let cursor = 0;
73
+ let fromRight = true;
74
+ let previous;
75
+ const gap = (to, next) => {
76
+ if (to - cursor < height * 0.7) {
77
+ return;
78
+ }
79
+ if (previous === 'waves' && next === 'waves') {
80
+ return;
81
+ }
82
+ const stretch = ribbon(cursor + height * 0.2, to - height * 0.45, height, 0.5, phase, narrow);
83
+ phase = stretch.phase;
84
+ points.push(...stretch.points);
85
+ const last = stretch.points.at(-1);
86
+ if (last) {
87
+ fromRight = last[0] > 0.5;
88
+ }
89
+ };
90
+ for (const stop of placed) {
91
+ gap(stop.top, stop.variant);
92
+ if (stop.variant === 'fan') {
93
+ const span = Math.min(stop.height, height);
94
+ points.push(...fan(stop.top, span, stop.at ?? FAN_AT, narrow));
95
+ cursor = stop.top + span * 1.12;
96
+ fromRight = true;
97
+ }
98
+ else if (stop.variant === 'waves') {
99
+ points.push(...wave(stop.top, height, fromRight, narrow));
100
+ fromRight = !fromRight;
101
+ cursor = stop.top + height * 0.35;
102
+ }
103
+ else {
104
+ const y = stop.top + Math.min(stop.height, height) / 2;
105
+ points.push([stop.at ?? 0.5, y, WIDTH.ribbon, GLOW.ribbon]);
106
+ cursor = y;
107
+ }
108
+ previous = stop.variant;
109
+ }
110
+ if (previous !== 'waves') {
111
+ gap(page + height * 0.4, undefined);
112
+ }
113
+ const last = points.at(-1) ?? [0.5, page, WIDTH.ribbon, 0];
114
+ points.push([last[0], Math.max(last[1], page) + height, last[2], 0]);
115
+ return points.slice(0, MAX_POINTS);
116
+ };
117
+ export const catmull = (p0, p1, p2, p3, f) => {
118
+ const f2 = f * f;
119
+ const f3 = f2 * f;
120
+ return (0.5 *
121
+ (2 * p1 +
122
+ (-p0 + p2) * f +
123
+ (2 * p0 - 5 * p1 + 4 * p2 - p3) * f2 +
124
+ (-p0 + 3 * p1 - 3 * p2 + p3) * f3));
125
+ };
126
+ export const lookup = (points) => {
127
+ const n = points.length;
128
+ const samples = Math.max(1, (n - 1) * 24);
129
+ const out = new Float32Array(samples + 1);
130
+ const y = (i) => points[Math.max(0, Math.min(n - 1, i))]?.[1] ?? 0;
131
+ for (let j = 0; j <= samples; j++) {
132
+ const s = (j / samples) * (n - 1);
133
+ const i = Math.min(Math.floor(s), n - 2);
134
+ out[j] = n < 2 ? y(0) : catmull(y(i - 1), y(i), y(i + 1), y(i + 2), s - i);
135
+ }
136
+ return out;
137
+ };
138
+ export const visibleRange = (table, top, bottom) => {
139
+ const last = table.length - 1;
140
+ let low = Number.POSITIVE_INFINITY;
141
+ let high = Number.NEGATIVE_INFINITY;
142
+ for (let j = 0; j <= last; j++) {
143
+ const y = table[j] ?? 0;
144
+ if (y >= top && y <= bottom) {
145
+ low = Math.min(low, j);
146
+ high = Math.max(high, j);
147
+ }
148
+ }
149
+ if (high < low || last < 1) {
150
+ return [0, 0];
151
+ }
152
+ return [Math.max(0, (low - 1) / last), Math.min(1, (high + 1) / last)];
153
+ };