@fusion.dev/helix 0.172.0 → 0.174.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 (41) hide show
  1. package/custom-elements.json +2140 -93
  2. package/dist/copy-button/root/copy-button-root.css.js +1 -1
  3. package/dist/element-events.generated.js +8 -1
  4. package/dist/graph/root/graph-root.css.js +2 -1
  5. package/dist/orb/root/orb-root.css.js +1 -1
  6. package/dist/orb/root/orb-root.d.ts +24 -5
  7. package/dist/orb/root/orb-root.js +24 -6
  8. package/dist/sandbox/index.d.ts +1 -0
  9. package/dist/sandbox/index.js +1 -0
  10. package/dist/sandbox/preview/sandbox-preview.d.ts +39 -2
  11. package/dist/sandbox/preview/sandbox-preview.js +37 -5
  12. package/dist/sandbox/root/sandbox-root.css.js +1 -1
  13. package/dist/sandbox/root/sandbox-root.d.ts +44 -4
  14. package/dist/sandbox/root/sandbox-root.js +92 -17
  15. package/dist/sandbox/source/editing.d.ts +111 -0
  16. package/dist/sandbox/source/editing.js +185 -0
  17. package/dist/sandbox/source/sandbox-source.css.js +2 -1
  18. package/dist/sandbox/source/sandbox-source.d.ts +131 -6
  19. package/dist/sandbox/source/sandbox-source.js +301 -17
  20. package/dist/strands/palette.d.ts +1 -1
  21. package/dist/strands/path.d.ts +17 -6
  22. package/dist/strands/path.js +149 -43
  23. package/dist/strands/renderer.d.ts +14 -0
  24. package/dist/strands/renderer.js +14 -4
  25. package/dist/strands/root/strands-root.css.js +1 -1
  26. package/dist/strands/root/strands-root.d.ts +41 -10
  27. package/dist/strands/root/strands-root.js +32 -5
  28. package/dist/strands/shader.d.ts +3 -3
  29. package/dist/strands/shader.js +35 -12
  30. package/dist/strands/stop/strands-stop.d.ts +5 -5
  31. package/dist/timeline/root/timeline-root.css.js +1 -1
  32. package/dist/virtual-list/reorder/index.d.ts +1 -0
  33. package/dist/virtual-list/reorder/index.js +5 -0
  34. package/dist/virtual-list/reorder/virtual-list-reorder.css.d.ts +1 -0
  35. package/dist/virtual-list/reorder/virtual-list-reorder.css.js +12 -0
  36. package/dist/virtual-list/root/virtual-list-root.d.ts +9 -0
  37. package/lint.json +1 -1
  38. package/package.json +6 -6
  39. package/types/jsx.d.ts +919 -79
  40. package/types/vue.d.ts +214 -15
  41. package/types/words.d.ts +27 -0
@@ -2,5 +2,5 @@ import { createSheet } from '@fusion.dev/helix-core';
2
2
  export const styles = createSheet(`:host {--_icon-button-ink: var(--copy-button-color, var(--helix-color-foreground-muted));--_icon-button-ink-hover: var(
3
3
  --copy-button-color-hover,
4
4
  var(--copy-button-color, var(--helix-color-foreground))
5
- );--_icon-button-size: var(--copy-button-size, var(--icon-button-size, var(--helix-size-control)));}[part="button"] {display: inline-grid;place-items: center;}[part="button"] > slot {grid-area: 1 / 1;}slot[hidden] {display: none;}[part="button"][data-copied] {color: var(--copy-button-success-color, var(--helix-color-success));}:host(:not(:disabled)) [part="button"][data-copied]:hover {color: var(--copy-button-success-color, var(--helix-color-success));}[part="button"][data-failed],
5
+ );--_icon-button-size: var(--copy-button-size, var(--icon-button-size, var(--helix-size-control)));}[part="button"] {display: inline-grid;place-items: center;}[part="button"] > slot {grid-area: 1 / 1;}:host([show-label]) [part="button"] {column-gap: var(--helix-space-xs);grid-auto-flow: column;inline-size: auto;padding-inline: var(--helix-space-sm);}[part="text"] {display: inline-grid;font-weight: var(--helix-weight-medium);grid-area: 1 / 2;justify-items: start;}[part="text"] > span {grid-area: 1 / 1;}[part="text"] > [hidden] {display: block;visibility: hidden;}slot[hidden] {display: none;}[part="button"][data-copied] {color: var(--copy-button-success-color, var(--helix-color-success));}:host(:not(:disabled)) [part="button"][data-copied]:hover {color: var(--copy-button-success-color, var(--helix-color-success));}[part="button"][data-failed],
6
6
  :host(:not(:disabled)) [part="button"][data-failed]:hover {color: var(--copy-button-error-color, var(--helix-color-danger));}slot > svg {block-size: var(--copy-button-icon-size, var(--helix-size-icon-tight));inline-size: var(--copy-button-icon-size, var(--helix-size-icon-tight));}@media print {:host {display: none;}}`);
@@ -362,5 +362,12 @@ registerElementEvents({
362
362
  ],
363
363
  'helix-video-playlist-item': ['helix-select'],
364
364
  'helix-video-playlist-menu': ['helix-select'],
365
- 'helix-virtual-list': ['change', 'helix-before-change', 'helix-load-more', 'helix-range-change'],
365
+ 'helix-virtual-list': [
366
+ 'change',
367
+ 'helix-before-change',
368
+ 'helix-before-reorder',
369
+ 'helix-load-more',
370
+ 'helix-range-change',
371
+ 'helix-reorder',
372
+ ],
366
373
  });
@@ -9,7 +9,8 @@ export const styles = createSheet(`:host {display: contents;}[part="region"] {ba
9
9
  ". down . notes"
10
10
  ". . . full";}[part="legend"] {display: flex;flex-wrap: wrap;gap: var(--helix-space-2xs);inset-block-end: var(--helix-space-sm);inset-inline-start: var(--helix-space-sm);max-inline-size: calc(100% - var(--helix-space-sm) * 3 - var(--helix-size-control-sm));position: absolute;}:host([controls-placement="bottom-start"]) [part="legend"] {inset-inline: auto var(--helix-space-sm);}@container (inline-size < 40rem) {[part="legend"] {flex-wrap: nowrap;inset-block-end: calc(var(--helix-space-sm) - var(--helix-space-2xs));inset-inline-start: calc(var(--helix-space-sm) - var(--helix-space-2xs));overflow-x: auto;padding: var(--helix-space-2xs);scrollbar-width: none;}}[part~="legend-item"] {align-items: center;background-color: var(--viewport-control-background-color, var(--helix-color-surface-overlay));block-size: var(--helix-size-control-tight);border: var(--helix-border-floating) solid var(--helix-color-border);border-radius: var(--helix-radii-sm);box-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-xs);font-weight: var(--helix-weight-medium);gap: var(--helix-space-2xs);padding-inline: var(--helix-space-xs);white-space: nowrap;}[part~="legend-item"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: var(--helix-size-focus-offset);}[part~="legend-item"]:nth-child(24n + 1) {--_graph-swatch: var(--helix-color-series-1);}[part~="legend-item"]:nth-child(24n + 2) {--_graph-swatch: var(--helix-color-series-2);}[part~="legend-item"]:nth-child(24n + 3) {--_graph-swatch: var(--helix-color-series-3);}[part~="legend-item"]:nth-child(24n + 4) {--_graph-swatch: var(--helix-color-series-4);}[part~="legend-item"]:nth-child(24n + 5) {--_graph-swatch: var(--helix-color-series-5);}[part~="legend-item"]:nth-child(24n + 6) {--_graph-swatch: var(--helix-color-series-6);}[part~="legend-item"]:nth-child(24n + 7) {--_graph-swatch: var(--helix-color-series-7);}[part~="legend-item"]:nth-child(24n + 8) {--_graph-swatch: var(--helix-color-series-8);}[part~="legend-item"]:nth-child(24n + 9) {--_graph-swatch: var(--helix-color-series-9);}[part~="legend-item"]:nth-child(24n + 10) {--_graph-swatch: var(--helix-color-series-10);}[part~="legend-item"]:nth-child(24n + 11) {--_graph-swatch: var(--helix-color-series-11);}[part~="legend-item"]:nth-child(24n + 12) {--_graph-swatch: var(--helix-color-series-12);}[part~="legend-item"]:nth-child(24n + 13) {--_graph-swatch: var(--helix-color-series-13);}[part~="legend-item"]:nth-child(24n + 14) {--_graph-swatch: var(--helix-color-series-14);}[part~="legend-item"]:nth-child(24n + 15) {--_graph-swatch: var(--helix-color-series-15);}[part~="legend-item"]:nth-child(24n + 16) {--_graph-swatch: var(--helix-color-series-16);}[part~="legend-item"]:nth-child(24n + 17) {--_graph-swatch: var(--helix-color-series-17);}[part~="legend-item"]:nth-child(24n + 18) {--_graph-swatch: var(--helix-color-series-18);}[part~="legend-item"]:nth-child(24n + 19) {--_graph-swatch: var(--helix-color-series-19);}[part~="legend-item"]:nth-child(24n + 20) {--_graph-swatch: var(--helix-color-series-20);}[part~="legend-item"]:nth-child(24n + 21) {--_graph-swatch: var(--helix-color-series-21);}[part~="legend-item"]:nth-child(24n + 22) {--_graph-swatch: var(--helix-color-series-22);}[part~="legend-item"]:nth-child(24n + 23) {--_graph-swatch: var(--helix-color-series-23);}[part~="legend-item"]:nth-child(24n + 24) {--_graph-swatch: var(--helix-color-series-24);}[part~="legend-item"][data-tone="accent"] {--_graph-swatch: var(--helix-color-accent);}[part~="legend-item"][data-tone="danger"] {--_graph-swatch: var(--helix-color-danger);}[part~="legend-item"][data-tone="info"] {--_graph-swatch: var(--helix-color-info);}[part~="legend-item"][data-tone="neutral"] {--_graph-swatch: var(--helix-color-foreground-muted);}[part~="legend-item"][data-tone="primary"] {--_graph-swatch: var(--helix-color-primary);}[part~="legend-item"][data-tone="success"] {--_graph-swatch: var(--helix-color-success);}[part~="legend-item"][data-tone="warning"] {--_graph-swatch: var(--helix-color-warning);}[part="legend-swatch"] {block-size: var(--helix-space-xs);border: var(--helix-border-thin) solid var(--_graph-swatch, var(--helix-color-foreground-muted));border-radius: var(--helix-radii-full);box-sizing: border-box;background-color: var(--_graph-swatch, var(--helix-color-foreground-muted));flex: 0 0 auto;inline-size: var(--helix-space-xs);}[part~="legend-item-hidden"] {color: var(--helix-color-foreground-subtle);}[part~="legend-item-hidden"] [part="legend-swatch"] {background-color: transparent;}[part="search"] {align-items: center;border-block-end: var(--helix-border-thin) solid var(--helix-color-border);display: flex;gap: var(--helix-space-2xs);padding: var(--helix-space-2xs);padding-inline-start: var(--helix-space-xs);}[part="filter-icon"] {align-items: center;color: var(--helix-color-foreground-subtle);display: inline-flex;flex: 0 0 auto;}[part="filter-icon"] svg,
11
11
  slot[name="filter-icon"]::slotted(*) {--icon-size: var(--helix-size-icon-tight);block-size: var(--helix-size-icon-tight);inline-size: var(--helix-size-icon-tight);}[part="search-field"] {appearance: none;background-color: transparent;block-size: var(--helix-size-control-tight);border: none;box-sizing: border-box;color: var(--helix-color-foreground);flex: 1 1 auto;font-family: var(--helix-font-sans);font-size: max(var(--helix-text-field-min), var(--helix-text-sm));font-weight: var(--helix-weight-medium);min-inline-size: 0;padding-inline: var(--helix-space-2xs);}[part="search-field"]::placeholder {color: var(--helix-color-foreground-subtle);}[part="search-field"]:focus-visible {outline: none;}[part="search"]:has([part="search-field"]:focus-visible),
12
- [part="list"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1);}[part="count"] {color: var(--helix-color-foreground-muted);flex: 0 0 auto;font-size: var(--helix-text-xs);font-variant-numeric: tabular-nums;padding-inline-end: calc(var(--helix-space-xs) - var(--helix-space-2xs));}[part="empty"] {color: var(--helix-color-foreground-muted);font-size: var(--helix-text-sm);grid-area: 2 / 1;padding: var(--helix-space-sm);}[part="list"] {min-block-size: 0;overflow-y: auto;overscroll-behavior: contain;padding-block: var(--helix-space-2xs);}[part~="note"] {align-items: center;box-sizing: border-box;cursor: pointer;display: flex;font-size: var(--graph-note-text, var(--helix-text-sm));gap: var(--helix-space-sm);min-block-size: var(--helix-size-control-tight);padding-block: var(--helix-space-2xs);padding-inline: var(--graph-note-padding-inline, var(--helix-space-xs));user-select: none;}[part~="note"]:hover,
12
+ [part="list"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1);}[part="count"] {color: var(--helix-color-foreground-muted);flex: 0 0 auto;font-size: var(--helix-text-xs);font-variant-numeric: tabular-nums;padding-inline-end: calc(var(--helix-space-xs) - var(--helix-space-2xs));}[part="empty"],
13
+ [part="loading"] {color: var(--helix-color-foreground-muted);font-size: var(--helix-text-sm);grid-area: 2 / 1;padding: var(--helix-space-sm);}[part="list"] {min-block-size: 0;overflow-y: auto;overscroll-behavior: contain;padding-block: var(--helix-space-2xs);}[part~="note"] {align-items: center;box-sizing: border-box;cursor: pointer;display: flex;font-size: var(--graph-note-text, var(--helix-text-sm));gap: var(--helix-space-sm);min-block-size: var(--helix-size-control-tight);padding-block: var(--helix-space-2xs);padding-inline: var(--graph-note-padding-inline, var(--helix-space-xs));user-select: none;}[part~="note"]:hover,
13
14
  [part="list"]:focus-visible [part~="note-cursor"] {--_graph-wash: var(
14
15
  --graph-note-hover-color,
15
16
  light-dark(
@@ -1,2 +1,2 @@
1
1
  import { createSheet } from '@fusion.dev/helix-core';
2
- export const styles = createSheet(`:host {--_orb-size: calc(var(--helix-size-loader-lg) * 1.5);--_orb-color: var(--orb-color, currentColor);--_orb-ground: var(--orb-ground, var(--helix-color-background));block-size: var(--orb-size, var(--_orb-size));display: inline-block;flex-shrink: 0;inline-size: var(--orb-size, var(--_orb-size));vertical-align: middle;}:host([size="sm"]) {--_orb-size: var(--helix-size-loader-md);}:host([size="lg"]) {--_orb-size: calc(var(--helix-size-loader-lg) * 2);}[part="canvas"] {block-size: 100%;color: var(--_orb-color);display: block;inline-size: 100%;text-decoration-color: var(--_orb-ground);}[part="canvas"][hidden] {display: none;}@media print {:host {display: none;}}`);
2
+ export const styles = createSheet(`:host {--_orb-size: calc(var(--helix-size-loader-lg) * 1.5);--_orb-color: var(--orb-color, currentColor);--_orb-ground: var(--orb-ground, var(--helix-color-background));block-size: var(--orb-size, var(--_orb-size));display: inline-block;flex-shrink: 0;inline-size: var(--orb-size, var(--_orb-size));position: relative;vertical-align: middle;}::slotted(*) {block-size: 100%;inline-size: 100%;inset: 0;position: absolute;}:host([size="sm"]) {--_orb-size: var(--helix-size-loader-md);}:host([size="lg"]) {--_orb-size: calc(var(--helix-size-loader-lg) * 2);}[part="canvas"] {animation-iteration-count: var(--orb-speed, 1);block-size: 100%;color: var(--_orb-color);display: block;inline-size: 100%;text-decoration-color: var(--_orb-ground);}[part="canvas"][hidden] {display: none;}@media print {:host {display: none;}}`);
@@ -32,10 +32,18 @@ import { type OrbState } from '../shapes.js';
32
32
  *
33
33
  * The orb is styled only and has behavior, which the tier split does not
34
34
  * otherwise allow. Its role, name and busy state follow `state`, unless
35
- * `announce` makes it a status region, and the rest
36
- * is drawing dots to a canvas, so there is no structure to restyle. For a
37
- * drawing of your own, the headless `Loader` holds the same busy progress bar
38
- * and its name.
35
+ * `announce` makes it a status region, and the rest is drawing dots to a
36
+ * canvas.
37
+ *
38
+ * **A shape of your own goes in the default slot**, such as an SVG, an
39
+ * image, or an animation. The orb draws it over its box in place of the dots
40
+ * and stops drawing them, and keeps its role, name, busy state and custom
41
+ * states, so your stylesheet can change what you slotted by state, such as
42
+ * `helix-orb:state(working) svg { animation-duration: 1s }`. What you slot is
43
+ * for the eye: a screen reader hears the orb's name.
44
+ *
45
+ * `--orb-speed` scales how fast the dots move, so `0.5` is a calmer orb at
46
+ * half the tempo.
39
47
  *
40
48
  * Each state is also a custom state, such as `:state(thinking)` or
41
49
  * `:state(done)`, so a page can style what sits around an orb by what it shows,
@@ -47,12 +55,15 @@ import { type OrbState } from '../shapes.js';
47
55
  * @aliases Thinking, Activity, Agent, Status
48
56
  * @documentation https://fusion.dev/helix/components/orb
49
57
  *
50
- * @csspart canvas - The canvas the dots are drawn on.
58
+ * @slot - A shape of your own, drawn over the orb's box in place of the dots.
59
+ *
60
+ * @csspart canvas - The canvas the dots are drawn on. Hidden while the default slot holds a shape.
51
61
  * @csspart live - The word for the state, which the orb says as it changes when `announce` is set. Rendered only then, and clipped out of view.
52
62
  *
53
63
  * @cssproperty --orb-color - The color of the nearest dots. Defaults to `currentColor`.
54
64
  * @cssproperty --orb-ground - The color the farthest dots fade toward. Defaults to the page's background.
55
65
  * @cssproperty --orb-size - The diameter. Overrides `size`.
66
+ * @cssproperty --orb-speed - How fast the dots move, as a multiple of each state's own tempo. Defaults to `1`.
56
67
  *
57
68
  * @cssstate connecting - The orb shows `connecting`.
58
69
  * @cssstate done - The orb shows `done`.
@@ -137,6 +148,12 @@ export declare class HelixOrb extends HelixElement {
137
148
  stateLabels: Partial<Record<OrbState, string>>;
138
149
  private canvas;
139
150
  private context;
151
+ /**
152
+ * Whether the default slot holds a shape of the page's, which the dots give
153
+ * way to. A plain field, written to the canvas by hand as a lost context is,
154
+ * so finding a shape on the first update asks for no second one.
155
+ */
156
+ private own;
140
157
  /**
141
158
  * Whether anybody is looking, and the loop that draws for them.
142
159
  *
@@ -181,6 +198,8 @@ export declare class HelixOrb extends HelixElement {
181
198
  /** The word for the state drawn, in the page's words where it gave them. */
182
199
  private get word();
183
200
  protected render(): TemplateResult;
201
+ /** Stops the dots for a shape the page slotted, and starts them again once it is gone. */
202
+ private readonly handleSlotChange;
184
203
  protected firstUpdated(changed: PropertyValues): void;
185
204
  connectedCallback(): void;
186
205
  disconnectedCallback(): void;
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { customElement, HelixElement, hostAria, PaintGateController, readColorAlpha, words, wordsConverter, } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, hostAria, PaintGateController, readColorAlpha, slotFilled, words, wordsConverter, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { styles as liveStyles } from '../../shared/live/live.css.js';
@@ -31,8 +31,9 @@ let HelixOrb = class HelixOrb extends HelixElement {
31
31
  working: 'Working',
32
32
  };
33
33
  this.context = null;
34
+ this.own = false;
34
35
  this.gate = new PaintGateController(this, {
35
- canRun: () => !!this.context && !this.lost,
36
+ canRun: () => !!this.context && !this.lost && !this.own,
36
37
  onFrame: (delta) => {
37
38
  this.paint(delta);
38
39
  },
@@ -54,6 +55,19 @@ let HelixOrb = class HelixOrb extends HelixElement {
54
55
  this.morphAt = 0;
55
56
  this.reach = 0;
56
57
  this.phase = STILL_PHASE;
58
+ this.handleSlotChange = (event) => {
59
+ this.own = slotFilled(event.target);
60
+ this.canvas?.toggleAttribute('hidden', this.own || this.lost);
61
+ if (this.own) {
62
+ this.gate.stop();
63
+ }
64
+ else if (this.hasUpdated) {
65
+ this.box = 0;
66
+ this.measure();
67
+ this.paint(0);
68
+ this.gate.play();
69
+ }
70
+ };
57
71
  this.handleLost = () => {
58
72
  this.lost = true;
59
73
  this.gate.stop();
@@ -61,7 +75,7 @@ let HelixOrb = class HelixOrb extends HelixElement {
61
75
  };
62
76
  this.handleRestored = () => {
63
77
  this.lost = false;
64
- this.canvas?.toggleAttribute('hidden', false);
78
+ this.canvas?.toggleAttribute('hidden', this.own);
65
79
  this.box = 0;
66
80
  this.measure();
67
81
  this.paint(0);
@@ -98,7 +112,8 @@ let HelixOrb = class HelixOrb extends HelixElement {
98
112
  return this.stateLabels?.[this.shown] ?? '';
99
113
  }
100
114
  render() {
101
- return html `<canvas part="canvas" aria-hidden="true"></canvas>${this.announce ? html `<span part="live">${this.word}</span>` : nothing}`;
115
+ return html `<canvas part="canvas" aria-hidden="true"></canvas
116
+ ><slot @slotchange=${this.handleSlotChange}></slot>${this.announce ? html `<span part="live">${this.word}</span>` : nothing}`;
102
117
  }
103
118
  firstUpdated(changed) {
104
119
  super.firstUpdated(changed);
@@ -125,6 +140,8 @@ let HelixOrb = class HelixOrb extends HelixElement {
125
140
  return;
126
141
  }
127
142
  this.canvas = canvas;
143
+ this.own = slotFilled(this.renderRoot.querySelector('slot'));
144
+ canvas.toggleAttribute('hidden', this.own || this.lost);
128
145
  this.context ??= canvas.getContext('2d');
129
146
  if (!this.context) {
130
147
  return;
@@ -133,7 +150,7 @@ let HelixOrb = class HelixOrb extends HelixElement {
133
150
  canvas.addEventListener('contextrestored', this.handleRestored);
134
151
  if (this.lost && !this.context.isContextLost()) {
135
152
  this.lost = false;
136
- canvas.toggleAttribute('hidden', false);
153
+ canvas.toggleAttribute('hidden', this.own);
137
154
  }
138
155
  this.gate.watch();
139
156
  this.measure();
@@ -186,8 +203,9 @@ let HelixOrb = class HelixOrb extends HelixElement {
186
203
  }
187
204
  const still = this.gate.reduced;
188
205
  const params = still ? paramsFor(this.state) : this.paramsNow();
189
- this.phase += dt * params.rate;
190
206
  const painted = getComputedStyle(ctx.canvas);
207
+ const speed = Number.parseFloat(painted.animationIterationCount);
208
+ this.phase += dt * params.rate * (Number.isFinite(speed) ? speed : 1);
191
209
  const ink = readColorAlpha(painted.color) ?? GRAY;
192
210
  const ground = readColorAlpha(painted.textDecorationColor) ?? GRAY;
193
211
  const mesh = meshFor(size);
@@ -15,6 +15,7 @@
15
15
  export { isRender, LAYOUT_RULES, loggedText, type ServePreviewOptions, servePreview, withoutStylesheets, } from './frame.js';
16
16
  export * from './preview/sandbox-preview.js';
17
17
  export * from './root/sandbox-root.js';
18
+ export { type ManifestLike, manifestWords } from './source/editing.js';
18
19
  export * from './source/sandbox-source.js';
19
20
  export * from './status-bar/sandbox-status-bar.js';
20
21
  export * from './terminal/sandbox-terminal.js';
@@ -1,6 +1,7 @@
1
1
  export { isRender, LAYOUT_RULES, loggedText, servePreview, withoutStylesheets, } from './frame.js';
2
2
  export * from './preview/sandbox-preview.js';
3
3
  export * from './root/sandbox-root.js';
4
+ export { manifestWords } from './source/editing.js';
4
5
  export * from './source/sandbox-source.js';
5
6
  export * from './status-bar/sandbox-status-bar.js';
6
7
  export * from './terminal/sandbox-terminal.js';
@@ -63,6 +63,16 @@ export type PreviewMessage = PreviewConsole | PreviewPress | PreviewReady | Prev
63
63
  * since a browser reads one only as a document starts. A `src` document
64
64
  * declares its own.
65
65
  *
66
+ * **`dependencies` names packages by version instead**, as a `package.json`
67
+ * does: `{"canvas-confetti": "^1.9"}` loads `canvas-confetti` and the paths
68
+ * under it from `cdn`, `https://esm.sh/` by default, at the version or range
69
+ * given. An address in `imports` wins over a package of the same name.
70
+ *
71
+ * **`modules` holds files the example imports by a relative path**: each path,
72
+ * such as `utils.js`, to its source, and `import { add } from './utils.js'`
73
+ * in the `js` pane or in another module loads it. A sandbox writes each pane
74
+ * named for a module file here.
75
+ *
66
76
  * **Examples render in a frame**, so an example's styles and scripts do not
67
77
  * affect the page around the sandbox, and `position: fixed` stays inside the
68
78
  * pane. The frame starts empty, with its own stylesheets and its own custom
@@ -83,8 +93,9 @@ export type PreviewMessage = PreviewConsole | PreviewPress | PreviewReady | Prev
83
93
  *
84
94
  * **A failed preview shows the error over the example**, in the `error` part,
85
95
  * until the next source is sent. The terminal says it too, so the part is not
86
- * announced. Hide it with `::part(error) { display: none }` where the page
87
- * draws its own.
96
+ * announced, and it is the frame's description while it shows, so a screen
97
+ * reader reads it with the frame. Hide it with `::part(error) { display: none }`
98
+ * where the page draws its own.
88
99
  *
89
100
  * **`controls` draws two buttons in the preview's corner**: one reloads the
90
101
  * frame's document, as `reload()` does, and one opens the example in a new
@@ -113,6 +124,11 @@ export declare class HelixSandboxPreview extends HelixElement {
113
124
  * `registerWords('de', { 'helix-sandbox-preview': { label: 'Vorschau' } })`.
114
125
  */
115
126
  static words: string;
127
+ /**
128
+ * The address the packages in `dependencies` load from, which the name, an
129
+ * `@` and the version follow.
130
+ */
131
+ cdn: string;
116
132
  /**
117
133
  * The color mode the frame renders in.
118
134
  *
@@ -134,6 +150,13 @@ export declare class HelixSandboxPreview extends HelixElement {
134
150
  * example in a new tab, in its corner.
135
151
  */
136
152
  controls: boolean;
153
+ /**
154
+ * The npm packages an example can import by name in the built-in document,
155
+ * each name to a version or a range, as a `package.json` lists them. Each
156
+ * loads from `cdn`. Set it as JSON in the attribute or as an object. Not
157
+ * read where `src` is set.
158
+ */
159
+ dependencies?: Record<string, string>;
137
160
  /**
138
161
  * Writing direction inside the frame.
139
162
  *
@@ -168,6 +191,12 @@ export declare class HelixSandboxPreview extends HelixElement {
168
191
  * could read it.
169
192
  */
170
193
  layout?: 'padded' | 'centered' | 'fullscreen' | 'hug';
194
+ /**
195
+ * Module files an example imports by a relative path in the built-in
196
+ * document, each path, such as `utils.js`, to its source. Not read where
197
+ * `src` is set.
198
+ */
199
+ modules?: Record<string, string>;
171
200
  /**
172
201
  * The name of the button that opens the example in a new tab, under
173
202
  * `controls`.
@@ -209,6 +238,12 @@ export declare class HelixSandboxPreview extends HelixElement {
209
238
  /** What the frame is sent as its source: the markup, and the script after it. */
210
239
  private get sending();
211
240
  private get frame();
241
+ /**
242
+ * The import map of the built-in document: each module file, by its path
243
+ * and by the address its neighbors import it at, each package in
244
+ * `dependencies` and the paths under it, and `imports` over all of them.
245
+ */
246
+ private get importMap();
212
247
  /**
213
248
  * The frame's document has finished loading, so its listener exists, the
214
249
  * example is sent and the preview is no longer `loading`. The frame saying
@@ -320,6 +355,8 @@ export declare class HelixSandboxPreview extends HelixElement {
320
355
  * new document announces itself the same way the first one did.
321
356
  */
322
357
  reload(): void;
358
+ /** The import map the built-in document was last loaded with, as JSON. */
359
+ private loadedMap;
323
360
  /**
324
361
  * A new document is on its way, so nothing is sent until it listens, and
325
362
  * nothing has failed in it. The built-in document loads too.
@@ -12,9 +12,12 @@ import { property, state } from 'lit/decorators.js';
12
12
  import { defaultFrame, withScript } from '../frame.js';
13
13
  import { styles as sheet } from './sandbox-preview.css.js';
14
14
  const SANDBOX = 'allow-scripts allow-forms';
15
+ const RELATIVE = /(\bfrom\s*|\bimport\s*\(?\s*)(["'])(\.{1,2}\/[^"']+)\2/g;
16
+ const moduleText = (path, source) => `data:text/javascript;charset=utf-8,${encodeURIComponent(source.replace(RELATIVE, (_, lead, quote, specifier) => `${lead}${quote}${new URL(specifier, `sandbox:/${path}`).href}${quote}`))}`;
15
17
  let HelixSandboxPreview = class HelixSandboxPreview extends HelixElement {
16
18
  constructor() {
17
19
  super(...arguments);
20
+ this.cdn = 'https://esm.sh/';
18
21
  this.controls = false;
19
22
  this.label = 'Preview';
20
23
  this.openLabel = 'Open preview in a new tab';
@@ -53,6 +56,7 @@ let HelixSandboxPreview = class HelixSandboxPreview extends HelixElement {
53
56
  }
54
57
  };
55
58
  this.settle = new DelayController(this, { renders: false });
59
+ this.loadedMap = '';
56
60
  }
57
61
  static { HelixSandboxPreview_1 = this; }
58
62
  static { this.words = 'helix-sandbox-preview'; }
@@ -66,6 +70,19 @@ let HelixSandboxPreview = class HelixSandboxPreview extends HelixElement {
66
70
  get frame() {
67
71
  return this.renderRoot.querySelector('iframe');
68
72
  }
73
+ get importMap() {
74
+ const map = {};
75
+ for (const [name, version] of Object.entries(this.dependencies ?? {})) {
76
+ const address = `${this.cdn}${name}@${version}`;
77
+ map[name] = address;
78
+ map[`${name}/`] = `${address}/`;
79
+ }
80
+ for (const [given, source] of Object.entries(this.modules ?? {})) {
81
+ const path = given.replace(/^\.?\//, '');
82
+ map[`./${path}`] = map[`sandbox:/${path}`] = moduleText(path, source);
83
+ }
84
+ return { ...map, ...this.imports };
85
+ }
69
86
  static { this.target = '*'; }
70
87
  scheduleSend() {
71
88
  this.settle.start(SETTLE, () => {
@@ -106,7 +123,7 @@ let HelixSandboxPreview = class HelixSandboxPreview extends HelixElement {
106
123
  frame.title = this.label;
107
124
  frame.style.cssText = 'block-size: 100vh; border: none; display: block; inline-size: 100vw';
108
125
  if (this.src === '') {
109
- frame.srcdoc = defaultFrame(this.imports);
126
+ frame.srcdoc = defaultFrame(this.importMap);
110
127
  }
111
128
  else {
112
129
  frame.src = this.frame?.src || this.src;
@@ -132,7 +149,7 @@ frame.onload = () => frame.contentWindow.postMessage(${JSON.stringify(this.messa
132
149
  }
133
150
  this.unready();
134
151
  if (this.src === '') {
135
- frame.srcdoc = defaultFrame(this.imports);
152
+ frame.srcdoc = defaultFrame(this.importMap);
136
153
  return;
137
154
  }
138
155
  frame.src = this.src;
@@ -146,7 +163,10 @@ frame.onload = () => frame.contentWindow.postMessage(${JSON.stringify(this.messa
146
163
  }
147
164
  updated(changed) {
148
165
  super.updated(changed);
149
- if (changed.has('src') || (changed.has('imports') && this.src === '')) {
166
+ const map = this.src === '' ? JSON.stringify(this.importMap) : '';
167
+ const remapped = map !== this.loadedMap;
168
+ this.loadedMap = map;
169
+ if (changed.has('src') || remapped) {
150
170
  this.unready();
151
171
  return;
152
172
  }
@@ -174,20 +194,29 @@ frame.onload = () => frame.contentWindow.postMessage(${JSON.stringify(this.messa
174
194
  part="frame"
175
195
  @load=${this.handleFrameLoad}
176
196
  src=${this.src || nothing}
177
- srcdoc=${this.src === '' ? defaultFrame(this.imports) : nothing}
197
+ srcdoc=${this.src === '' ? defaultFrame(this.importMap) : nothing}
178
198
  title=${this.label}
199
+ aria-describedby=${this.failure === undefined ? nothing : 'error'}
179
200
  allow="clipboard-write *; fullscreen *"
180
201
  sandbox=${SANDBOX}
181
- ></iframe>${this.failure === undefined ? nothing : html `<pre part="error">${this.failure}</pre>`}${this.renderControls()}`;
202
+ ></iframe>${this.failure === undefined
203
+ ? nothing
204
+ : html `<pre part="error" id="error">${this.failure}</pre>`}${this.renderControls()}`;
182
205
  }
183
206
  static { this.styles = sheet; }
184
207
  };
208
+ __decorate([
209
+ property()
210
+ ], HelixSandboxPreview.prototype, "cdn", void 0);
185
211
  __decorate([
186
212
  property({ attribute: 'color-mode' })
187
213
  ], HelixSandboxPreview.prototype, "colorMode", void 0);
188
214
  __decorate([
189
215
  property({ reflect: true, type: Boolean })
190
216
  ], HelixSandboxPreview.prototype, "controls", void 0);
217
+ __decorate([
218
+ property({ type: Object })
219
+ ], HelixSandboxPreview.prototype, "dependencies", void 0);
191
220
  __decorate([
192
221
  property()
193
222
  ], HelixSandboxPreview.prototype, "direction", void 0);
@@ -201,6 +230,9 @@ __decorate([
201
230
  __decorate([
202
231
  property()
203
232
  ], HelixSandboxPreview.prototype, "layout", void 0);
233
+ __decorate([
234
+ property({ type: Object })
235
+ ], HelixSandboxPreview.prototype, "modules", void 0);
204
236
  __decorate([
205
237
  words,
206
238
  property({ attribute: 'open-label' })
@@ -1,4 +1,4 @@
1
1
  import { createSheet } from '@fusion.dev/helix-core';
2
2
  export const styles = createSheet(`:host {background-color: var(--helix-color-background);display: grid;grid-template-rows: auto minmax(0, 1fr) auto;min-block-size: var(--sandbox-min-block-size, 20rem);overflow: hidden;}::slotted([slot="toolbar"]) {grid-row: 1;}[part="body"] {grid-row: 2;}::slotted([slot="status"]) {grid-row: 3;}[part="body"],
3
3
  [part="panes"] {--splitter-color: var(--sandbox-separator-color, var(--helix-color-border));--splitter-target: calc(var(--sandbox-grab, var(--helix-space-2xs)) * 2);min-block-size: 0;min-inline-size: 0;}[part="panes"] {block-size: 100%;}[part="body"]:not([data-panel])::part(corner),
4
- [part="body"]:not([data-panel])::part(separator) {display: none;}[part="pane"] {block-size: 100%;min-block-size: 0;min-inline-size: 0;overflow: hidden;}[part="pane"][data-files] {display: grid;grid-template-rows: auto minmax(0, 1fr);}[part="files"] {border-block-end: var(--helix-border-thin) solid var(--helix-color-border);display: flex;font-family: var(--helix-font-sans);font-size: var(--helix-text-xs);overflow-x: auto;}[part="file"] {background-color: transparent;border: none;border-block-end: var(--helix-border-thick) solid transparent;color: var(--helix-color-foreground-muted);cursor: pointer;font: inherit;padding: var(--helix-space-xs) var(--helix-space-sm);white-space: nowrap;}[part="file"]:hover {color: var(--helix-color-foreground);}[part="file"][aria-selected="true"] {border-block-end-color: var(--helix-color-accent);color: var(--helix-color-foreground);}[part="file"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1);}[data-files] > [role="tabpanel"] {min-block-size: 0;}::slotted([hidden]) {display: none;}[part="panel"] {block-size: 100%;min-block-size: 0;overflow: hidden;}::slotted([slot="panel"]) {block-size: 100%;min-block-size: 0;}::slotted(helix-sandbox-preview) {background-color: var(--helix-color-background);block-size: 100%;color: var(--helix-color-foreground);display: block;}@media print {:host {display: none;}}`);
4
+ [part="body"]:not([data-panel])::part(separator) {display: none;}[part="pane"] {block-size: 100%;min-block-size: 0;min-inline-size: 0;overflow: hidden;}[part="pane"][data-files] {display: grid;grid-template-rows: auto minmax(0, 1fr);}[part="files"] {border-block-end: var(--helix-border-thin) solid var(--helix-color-border);display: flex;font-family: var(--helix-font-sans);font-size: var(--helix-text-xs);overflow-x: auto;}[part~="file"] {background-color: transparent;border: none;border-block-end: var(--helix-border-thick) solid transparent;color: var(--helix-color-foreground-muted);cursor: pointer;font: inherit;padding: var(--helix-space-xs) var(--helix-space-sm);white-space: nowrap;}[part~="file"]:hover {color: var(--helix-color-foreground);}[part~="file"][aria-selected="true"] {border-block-end-color: var(--helix-color-accent);color: var(--helix-color-foreground);}[part~="file-modified"]::after {background-color: currentcolor;block-size: var(--helix-space-xs);border-radius: var(--helix-radii-full);content: "";display: inline-block;inline-size: var(--helix-space-xs);margin-inline-start: var(--helix-space-xs);vertical-align: middle;}[part~="file"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1);}[data-files] > [role="tabpanel"] {min-block-size: 0;}::slotted([hidden]) {display: none;}[part="panel"] {block-size: 100%;min-block-size: 0;overflow: hidden;}::slotted([slot="panel"]) {block-size: 100%;min-block-size: 0;}::slotted(helix-sandbox-preview) {background-color: var(--helix-color-background);block-size: 100%;color: var(--helix-color-foreground);display: block;}@media print {:host {display: none;}}`);
@@ -14,10 +14,22 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
14
14
  * milliseconds by default, so a word typed reloads it once. A pane's text when the sandbox starts reaches it at once.
15
15
  *
16
16
  * The event carries the pane's `name`, so one sandbox can hold several panes.
17
- * `html` sets the preview's markup, `css` sets its stylesheet, and `js` sets
18
- * a script that runs as a module after the markup. The sandbox ignores a pane
19
- * with any other name, so that pane can show content without changing the
20
- * preview.
17
+ * The names the sandbox writes into its previews:
18
+ *
19
+ * - `html` sets the preview's markup.
20
+ * - `css` sets its stylesheet.
21
+ * - `js` sets a script that runs as a module after the markup.
22
+ * - A module file name ending in `.js` or `.mjs`, such as `utils.js`, is a
23
+ * module the other scripts import by its path, `./utils.js`.
24
+ * - `package.json` sets the npm packages the scripts import by name, from its
25
+ * `dependencies`, which load from the preview's `cdn`.
26
+ *
27
+ * The sandbox ignores a pane with any other name, so that pane can show
28
+ * content without changing the preview.
29
+ *
30
+ * **The sandbox is `:state(modified)` while a source pane holds an edit**, and
31
+ * that pane's file tab is also `file-modified` and described as modified to
32
+ * a screen reader. `reset()` puts every pane back to the source it was given.
21
33
  *
22
34
  * **The dividers are two nested `helix-splitter` elements**, which handle the
23
35
  * pointer, the keyboard, and the corner. The inner splitter holds the panes and
@@ -80,6 +92,7 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
80
92
  * @csspart body - The splitter holding the panes above the panel. `::part(body):state(dragging)` matches while its divider is held.
81
93
  * @csspart corner - The handle where the two dividers cross.
82
94
  * @csspart file - One file tab, `aria-selected` while its pane is shown.
95
+ * @csspart file-modified - A file tab whose pane holds an edit.
83
96
  * @csspart files - The file tabs over the source panes, where there are several.
84
97
  * @csspart pane - Either pane.
85
98
  * @csspart panel - The box that holds the slotted panel.
@@ -90,6 +103,7 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
90
103
  * @cssproperty --sandbox-min-block-size - Minimum height of the sandbox.
91
104
  * @cssproperty --sandbox-separator-color - Color of the divider.
92
105
  *
106
+ * @cssstate modified - While a source pane holds an edit, until `reset()`.
93
107
  * @cssstate panel-collapsed - While there is a panel and it is no taller than `panel-collapsed-size`.
94
108
  */
95
109
  export declare class HelixSandbox extends HelixElement {
@@ -151,6 +165,11 @@ export declare class HelixSandbox extends HelixElement {
151
165
  * the same thing in both directions: thirty percent to whatever comes first.
152
166
  */
153
167
  lead: 'source' | 'preview';
168
+ /**
169
+ * The description of a file tab whose pane holds an edit, read by screen
170
+ * readers after the file's name.
171
+ */
172
+ modifiedLabel: string;
154
173
  /**
155
174
  * Whether the panes sit side by side or stacked.
156
175
  *
@@ -192,6 +211,27 @@ export declare class HelixSandbox extends HelixElement {
192
211
  previewDelay: number;
193
212
  /** The edits waiting out `previewDelay`, the last text of each pane. */
194
213
  private readonly edits;
214
+ /** The source each pane was given, for a pane that does not say whether it is modified. */
215
+ private readonly given;
216
+ /** The source each pane holds since its last edit. */
217
+ private readonly texts;
218
+ /** Whether each pane held an edit when the file tabs were last drawn. */
219
+ private readonly drawnModified;
220
+ /** The source panes slotted here. */
221
+ private get sources();
222
+ /**
223
+ * Whether a pane holds an edit: as the pane says, where it has `modified`,
224
+ * as `helix-sandbox-source` has, and otherwise against the text it started
225
+ * with.
226
+ */
227
+ private modifiedOf;
228
+ /**
229
+ * Puts every source pane back to the source it was given: through the
230
+ * pane's own `reset()` where it has one, which sends an edit the previews
231
+ * take, and otherwise by setting its `value` and writing it into the
232
+ * previews.
233
+ */
234
+ reset(): void;
195
235
  /** The wait before the edits reach the previews. */
196
236
  private readonly resting;
197
237
  /** Hands the waiting edits to the previews. */