@fusion.dev/helix 0.162.1 → 0.164.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.
@@ -6,4 +6,6 @@ color-mix(in oklab, var(--_callout-color) 10%, transparent)
6
6
  color-mix(in oklab, var(--_callout-color) 45%, var(--helix-color-background));}:host([separation="plain"]) {background-color: var(--callout-background-color, transparent);}[part="icon"] {color: var(--_callout-color);display: inline-flex;flex: 0 0 auto;}[part="icon"] svg {block-size: var(--helix-size-icon);inline-size: var(--helix-size-icon);}slot[name="icon"]::slotted(*) {--icon-size: var(--helix-size-icon);block-size: var(--helix-size-icon);inline-size: var(--helix-size-icon);}[part="content"] {line-height: var(--helix-leading-normal);min-inline-size: 0;}[part="dismiss"] {all: unset;box-sizing: border-box;block-size: calc(var(--helix-size-icon) + var(--helix-space-2xs) * 2);border-radius: var(--helix-radii-sm);color: var(--helix-color-foreground-muted);cursor: pointer;display: inline-flex;flex: 0 0 auto;inline-size: calc(var(--helix-size-icon) + var(--helix-space-2xs) * 2);margin-block: calc(var(--helix-space-2xs) * -1);margin-inline: auto calc(var(--helix-space-2xs) * -1);place-content: center;place-items: center;transition:
7
7
  background-color var(--helix-duration-fast) var(--helix-easing-standard),
8
8
  color var(--helix-duration-fast) var(--helix-easing-standard);}[part="dismiss"]:hover {background-color: color-mix(in oklab, var(--helix-color-foreground) 8%, transparent);color: var(--helix-color-foreground);}[part="dismiss"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: var(--helix-size-focus-offset);}[part="dismiss"] svg,
9
- slot[name="dismiss-icon"]::slotted(*) {--icon-size: var(--helix-size-icon);block-size: var(--helix-size-icon);inline-size: var(--helix-size-icon);}@media (prefers-reduced-motion: reduce) {[part="dismiss"] {transition: none;}}slot[name="heading"]::slotted(*) {color: var(--helix-color-foreground);display: block;font-size: var(--callout-font-size, var(--helix-text-control));font-weight: var(--helix-weight-semibold);margin: var(--helix-space-none);}@media print {:host {background-color: transparent;}}`);
9
+ slot[name="dismiss-icon"]::slotted(*) {--icon-size: var(--helix-size-icon);block-size: var(--helix-size-icon);inline-size: var(--helix-size-icon);}:host(:state(exiting)) {opacity: 0;transition: opacity var(--helix-duration-normal) var(--helix-easing-accelerate);}:host(:state(entering)) {transition: opacity var(--helix-duration-normal) var(--helix-easing-decelerate);}@starting-style {:host(:state(entering)) {opacity: 0;}}@media (prefers-reduced-motion: reduce) {[part="dismiss"],
10
+ :host(:state(exiting)),
11
+ :host(:state(entering)) {transition: none;}}slot[name="heading"]::slotted(*) {color: var(--helix-color-foreground);display: block;font-size: var(--callout-font-size, var(--helix-text-control));font-weight: var(--helix-weight-semibold);margin: var(--helix-space-none);}@media print {:host {background-color: transparent;}}`);
@@ -1,4 +1,4 @@
1
- export type { CanvasDocument, CanvasEdge, CanvasEdgeProps, CanvasEnd, CanvasNode, CanvasNodeProps, CanvasPath, CanvasSide, CanvasTone, } from '@fusion.dev/helix-headless/canvas';
1
+ export type { CanvasDocument, CanvasEdge, CanvasEdgeProps, CanvasEnd, CanvasNode, CanvasNodeProps, CanvasPath, CanvasRoute, CanvasRouteEnds, CanvasSide, CanvasTone, } from '@fusion.dev/helix-headless/canvas';
2
2
  export { fromJsonCanvas } from '@fusion.dev/helix-headless/canvas';
3
3
  export { HelixCanvasEdge } from './edge/canvas-edge.js';
4
4
  export { HelixCanvasNode } from './node/canvas-node.js';
@@ -1,5 +1,5 @@
1
1
  import { createSheet } from '@fusion.dev/helix-core';
2
- export const styles = createSheet(`:host {display: contents;}[part="region"] {background-color: var(--canvas-background-color, var(--helix-color-surface-sunken));block-size: var(--canvas-block-size, 30rem);border: var(--helix-border-thin) solid var(--canvas-border-color, var(--helix-color-border));border-radius: var(--canvas-radius, var(--helix-radii-sm));box-sizing: border-box;display: grid;grid-template-rows: auto 1fr auto;overflow: hidden;position: relative;}:host([separation="plain"]) [part="region"] {background-color: transparent;border: none;border-radius: 0;}[part="viewport"] {min-block-size: 0;overflow: hidden;position: relative;}:host(:not(:state(empty))) ::slotted([slot="empty"]) {display: none;}[part="board"] {block-size: var(--_canvas-plane-block-size);inline-size: var(--_canvas-plane-inline-size);left: 0;position: absolute;top: 0;transform: translate(var(--_canvas-x), var(--_canvas-y)) scale(var(--canvas-zoom));transform-origin: 0 0;transition: transform var(--helix-duration-normal) var(--helix-easing-standard);}:host([grid]) [part="viewport"] {background-position: var(--_canvas-rule-x) var(--_canvas-rule-y);background-size: calc(var(--canvas-grid-size, 32px) * var(--_canvas-rule-zoom))
2
+ export const styles = createSheet(`:host {display: contents;}[part="region"] {background-color: var(--canvas-background-color, var(--helix-color-surface-sunken));block-size: var(--canvas-block-size, 30rem);border: var(--helix-border-thin) solid var(--canvas-border-color, var(--helix-color-border));border-radius: var(--canvas-radius, var(--helix-radii-sm));box-sizing: border-box;display: grid;grid-template-rows: auto 1fr auto;overflow: hidden;position: relative;}:host([separation="plain"]) [part="region"] {background-color: transparent;border: none;border-radius: 0;}[part="viewport"] {min-block-size: 0;overflow: hidden;position: relative;}[part="minimap"] {background-color: var(--viewport-control-background-color, var(--helix-color-surface-overlay));block-size: var(--canvas-minimap-block-size, 7.5rem);border: var(--helix-border-floating) solid var(--helix-color-border);border-radius: var(--helix-radii-sm);box-shadow: var(--helix-shadow-floating);box-sizing: border-box;cursor: pointer;inline-size: var(--canvas-minimap-inline-size, 10rem);inset-block-end: var(--helix-space-sm);inset-inline-end: var(--helix-space-sm);position: absolute;touch-action: none;}:host([controls-placement^="bottom"]) [part="minimap"] {inset-block: var(--helix-space-sm) auto;}[part="minimap-node"] {fill: var(--helix-color-foreground-subtle);}[part="minimap-node"][data-group] {fill: none;stroke: var(--helix-color-foreground-subtle);vector-effect: non-scaling-stroke;}[part="minimap-view"] {fill: color-mix(in oklab, var(--helix-color-accent) 12%, transparent);stroke: var(--helix-color-accent);stroke-width: var(--helix-border-thick);vector-effect: non-scaling-stroke;}:host(:not(:state(empty))) ::slotted([slot="empty"]) {display: none;}[part="board"] {block-size: var(--_canvas-plane-block-size);inline-size: var(--_canvas-plane-inline-size);left: 0;position: absolute;top: 0;transform: translate(var(--_canvas-x), var(--_canvas-y)) scale(var(--canvas-zoom));transform-origin: 0 0;transition: transform var(--helix-duration-normal) var(--helix-easing-standard);}:host([grid]) [part="viewport"] {background-position: var(--_canvas-rule-x) var(--_canvas-rule-y);background-size: calc(var(--canvas-grid-size, 32px) * var(--_canvas-rule-zoom))
3
3
  calc(var(--canvas-grid-size, 32px) * var(--_canvas-rule-zoom));transition:
4
4
  background-position var(--helix-duration-normal) var(--helix-easing-standard),
5
5
  background-size var(--helix-duration-normal) var(--helix-easing-standard);}:host([grid="lines"]) [part="viewport"] {--_canvas-rule: color-mix(in oklab, var(--helix-color-border) 50%, transparent);background-image:
@@ -30,6 +30,8 @@ import type { CSSResultGroup } from 'lit';
30
30
  * @cssproperty --canvas-label-color - The color of the label on a connection.
31
31
  * @cssproperty --canvas-label-editor-inline-size - The width of the field for editing a label.
32
32
  * @cssproperty --canvas-label-font-size - The size of the label on a connection.
33
+ * @cssproperty --canvas-minimap-block-size - The height of the minimap, under `controls="minimap"`.
34
+ * @cssproperty --canvas-minimap-inline-size - The width of the minimap, under `controls="minimap"`.
33
35
  * @cssproperty --canvas-radius - The frame's corners.
34
36
  * @cssproperty --canvas-selected-color - The color of what is selected: a card's outline, its corners and its move handle, a connection and its ends, and the box drawn to select.
35
37
  * @cssproperty --viewport-control-background-color - The background of the pan and zoom buttons. `Map` uses the same property.
@@ -21,7 +21,7 @@ var(--code-block-mark-edge-color, var(--helix-color-primary));box-sizing: border
21
21
  [part="context"],
22
22
  [part="added"],
23
23
  [part="removed"],
24
- [part="filler"] {display: flex;padding-inline: var(--helix-space-md);text-decoration: none;}[part="hunk"] {background-color: var(--helix-color-surface-sunken);color: var(--helix-color-foreground-muted);}[part="added"] {background-color: color-mix(
24
+ [part="filler"] {display: flex;padding-inline: var(--helix-space-md);text-decoration: none;}[part="hunk"] {background-color: var(--helix-color-surface-sunken);color: var(--helix-color-foreground-muted);}[part="fold"] {all: unset;background-color: var(--helix-color-surface-sunken);box-sizing: border-box;color: var(--helix-color-foreground-muted);cursor: pointer;display: flex;font-family: var(--helix-font-sans);inline-size: 100%;padding-inline: var(--helix-space-md);}[part="fold"]:hover {color: var(--helix-color-foreground);}[part="fold"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1);}[part="added"] {background-color: color-mix(
25
25
  in srgb,
26
26
  var(--code-block-added-color, var(--helix-color-success)) 14%,
27
27
  var(--code-block-background-color, var(--helix-color-background))
@@ -29,7 +29,15 @@ var(--code-block-background-color, var(--helix-color-background))
29
29
  in srgb,
30
30
  var(--code-block-removed-color, var(--helix-color-danger)) 14%,
31
31
  var(--code-block-background-color, var(--helix-color-background))
32
- );}[part="marker"] {padding-inline-end: var(--helix-space-xs);user-select: none;}[part="added"] [part="marker"] {color: color-mix(
32
+ );}[part="added"] [part="changed"] {background-color: color-mix(
33
+ in srgb,
34
+ var(--code-block-added-color, var(--helix-color-success)) 32%,
35
+ var(--code-block-background-color, var(--helix-color-background))
36
+ );border-radius: var(--helix-radii-sm);}[part="removed"] [part="changed"] {background-color: color-mix(
37
+ in srgb,
38
+ var(--code-block-removed-color, var(--helix-color-danger)) 32%,
39
+ var(--code-block-background-color, var(--helix-color-background))
40
+ );border-radius: var(--helix-radii-sm);}[part="marker"] {padding-inline-end: var(--helix-space-xs);user-select: none;}[part="added"] [part="marker"] {color: color-mix(
33
41
  in oklab,
34
42
  var(--code-block-added-color, var(--helix-color-success)) 55%,
35
43
  var(--helix-color-foreground)
@@ -41,5 +49,6 @@ var(--helix-color-foreground)
41
49
  [part="old"],
42
50
  [part="new"] {overflow: visible;white-space: pre-wrap;}[part="added"],
43
51
  [part="removed"],
52
+ [part="changed"],
44
53
  [part="hunk"],
45
54
  [part="filler"] {print-color-adjust: exact;}}`);
@@ -2,6 +2,7 @@ import { HelixHeadlessCommand } from '@fusion.dev/helix-headless/command';
2
2
  import type { CSSResultGroup } from 'lit';
3
3
  import '../../dialog/index.js';
4
4
  import '../../listbox/index.js';
5
+ import '../../option/index.js';
5
6
  /**
6
7
  * @summary A command palette: a list of commands that opens from anywhere on the page and filters as you type.
7
8
  *
@@ -8,6 +8,7 @@ import { customElement } from '@fusion.dev/helix-core';
8
8
  import { HelixHeadlessCommand } from '@fusion.dev/helix-headless/command';
9
9
  import '../../dialog/index.js';
10
10
  import '../../listbox/index.js';
11
+ import '../../option/index.js';
11
12
  import { styles as liveStyles } from '../../shared/live/live.css.js';
12
13
  import { styles } from './command-root.css.js';
13
14
  let HelixCommand = class HelixCommand extends HelixHeadlessCommand {
@@ -211,6 +211,8 @@ registerElementEvents({
211
211
  'helix-radio-group': ['change', 'helix-before-change', 'helix-select', 'input'],
212
212
  'helix-rating': ['change', 'helix-before-change', 'helix-clear', 'helix-preview', 'input'],
213
213
  'helix-sandbox': [
214
+ 'change',
215
+ 'helix-before-change',
214
216
  'helix-before-edit',
215
217
  'helix-before-panel-resize',
216
218
  'helix-before-split',
@@ -335,7 +337,13 @@ registerElementEvents({
335
337
  'volumechange',
336
338
  'waiting',
337
339
  ],
338
- 'helix-video-playlist': ['helix-activate', 'helix-before-activate', 'helix-end'],
340
+ 'helix-video-playlist': [
341
+ 'helix-activate',
342
+ 'helix-before-activate',
343
+ 'helix-before-reorder',
344
+ 'helix-end',
345
+ 'helix-reorder',
346
+ ],
339
347
  'helix-video-playlist-item': ['helix-select'],
340
348
  'helix-video-playlist-menu': ['helix-select'],
341
349
  'helix-virtual-list': ['change', 'helix-before-change', 'helix-load-more', 'helix-range-change'],
@@ -12,7 +12,7 @@ var(--helix-space-lg) +
12
12
  var(--helix-space-sm) +
13
13
  1em +
14
14
  var(--helix-space-2xs)
15
- );text-overflow: ellipsis;white-space: nowrap;}[part~="row"][aria-expanded] {cursor: pointer;padding-inline-start: calc(
15
+ );text-overflow: ellipsis;white-space: nowrap;}[part="empty"] {color: var(--helix-color-foreground-muted);font-family: var(--helix-font-sans);padding-inline: calc(var(--helix-space-sm) + 1em + var(--helix-space-2xs));}[part~="row"][aria-expanded] {cursor: pointer;padding-inline-start: calc(
16
16
  var(--object-inspector-level) *
17
17
  var(--helix-space-lg) +
18
18
  var(--helix-space-sm)
@@ -1,2 +1,5 @@
1
1
  import { createSheet } from '@fusion.dev/helix-core';
2
- export const styles = createSheet(`:host {display: block;min-block-size: 0;}[part="frame"] {background-color: var(--helix-color-background);block-size: 100%;border: none;display: block;inline-size: 100%;}`);
2
+ export const styles = createSheet(`:host {display: block;min-block-size: 0;position: relative;}[part="frame"] {background-color: var(--helix-color-background);block-size: 100%;border: none;display: block;inline-size: 100%;}[part="error"] {background-color: var(--helix-color-background);box-sizing: border-box;color: var(--helix-color-danger);font-family: var(--helix-font-mono);font-size: var(--helix-text-xs);inset: 0;line-height: var(--helix-leading-normal);margin: var(--helix-space-none);overflow: auto;padding: var(--helix-space-md);position: absolute;white-space: pre-wrap;word-break: break-word;}[part="controls"] {display: flex;font-family: var(--helix-font-sans);font-size: var(--helix-text-xs);gap: var(--helix-space-2xs);inset-block-end: var(--helix-space-xs);inset-inline-end: var(--helix-space-xs);position: absolute;}[part="reload"],
3
+ [part="open"] {background-color: var(--helix-color-surface);border: var(--helix-border-thin) solid var(--helix-color-border);border-radius: var(--helix-radii-sm);color: var(--helix-color-foreground);cursor: pointer;font: inherit;padding: var(--helix-space-2xs) var(--helix-space-xs);}[part="reload"]:hover,
4
+ [part="open"]:hover {background-color: var(--helix-color-surface-sunken);}[part="reload"]:focus-visible,
5
+ [part="open"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: var(--helix-size-focus-offset);}`);
@@ -72,6 +72,15 @@ export type PreviewMessage = PreviewConsole | PreviewPress | PreviewReady | Prev
72
72
  * reports. A page draws its own spinner or message from either, such as a
73
73
  * sibling shown by `helix-sandbox-preview:state(failed) + .message`.
74
74
  *
75
+ * **A failed preview shows the error over the example**, in the `error` part,
76
+ * until the next source is sent. The terminal says it too, so the part is not
77
+ * announced. Hide it with `::part(error) { display: none }` where the page
78
+ * draws its own.
79
+ *
80
+ * **`controls` draws two buttons in the preview's corner**: one reloads the
81
+ * frame's document, as `reload()` does, and one opens the example in a new
82
+ * tab, as `open()` does. The tab runs it in a frame sandboxed as this one is.
83
+ *
75
84
  * @since 0.1.0
76
85
  * @status stable
77
86
  * @aliases Result, Output
@@ -80,7 +89,11 @@ export type PreviewMessage = PreviewConsole | PreviewPress | PreviewReady | Prev
80
89
  * @event {HelixConsoleEvent} helix-console - Fired once per console call in the frame.
81
90
  * @event {HelixResizeEvent} helix-resize - Fired when the height of the frame's content changes, with the new height.
82
91
  *
92
+ * @csspart controls - The buttons in the preview's corner, under `controls`.
93
+ * @csspart error - The error the example threw and did not catch, shown over it while the preview is `failed`.
83
94
  * @csspart frame - The iframe.
95
+ * @csspart open - The button that opens the example in a new tab, under `controls`.
96
+ * @csspart reload - The button that reloads the frame's document, under `controls`.
84
97
  *
85
98
  * @cssstate failed - Since the example threw an error it did not catch, until the next source is sent.
86
99
  * @cssstate loading - While the frame's document loads, until it can take the example.
@@ -107,6 +120,11 @@ export declare class HelixSandboxPreview extends HelixElement {
107
120
  * including every one of the 24 series colors a chart reads.
108
121
  */
109
122
  colorMode?: 'dark' | 'light';
123
+ /**
124
+ * Whether the preview draws a reload button and a button that opens the
125
+ * example in a new tab, in its corner.
126
+ */
127
+ controls: boolean;
110
128
  /**
111
129
  * Writing direction inside the frame.
112
130
  *
@@ -134,6 +152,16 @@ export declare class HelixSandboxPreview extends HelixElement {
134
152
  * could read it.
135
153
  */
136
154
  layout?: 'padded' | 'centered' | 'fullscreen' | 'hug';
155
+ /**
156
+ * The name of the button that opens the example in a new tab, under
157
+ * `controls`.
158
+ */
159
+ openLabel: string;
160
+ /**
161
+ * The name of the button that reloads the frame's document, under
162
+ * `controls`.
163
+ */
164
+ reloadLabel: string;
137
165
  /**
138
166
  * A script to run as a module after the markup, on every render of new
139
167
  * source. A sandbox writes its `js` pane here.
@@ -158,6 +186,10 @@ export declare class HelixSandboxPreview extends HelixElement {
158
186
  private sentSource;
159
187
  /** The source the frame held when it last reported an uncaught error. */
160
188
  private failedSource;
189
+ /** What the example threw and did not catch, while the preview is `failed`. */
190
+ private failure;
191
+ /** Says the preview failed with `text`, or, with none, that it has not. */
192
+ private fail;
161
193
  /** What the frame is sent as its source: the markup, and the script after it. */
162
194
  private get sending();
163
195
  private get frame();
@@ -237,8 +269,24 @@ export declare class HelixSandboxPreview extends HelixElement {
237
269
  * short enough to read as immediate when the typing stops.
238
270
  */
239
271
  private scheduleSend;
272
+ /** What the frame is sent: the source, its stylesheet and its settings. */
273
+ private get message();
240
274
  /** Sends the current source to the frame, once it is listening. */
241
275
  private send;
276
+ /**
277
+ * Opens the example in a new tab, and returns the tab's window, or `null`
278
+ * where the browser refused to open one.
279
+ *
280
+ * **The tab runs the example as this preview does**: in a frame that fills
281
+ * it, with this frame's `sandbox`, loading `src` or the built-in document,
282
+ * and sent the example once it loads. The example stays on an opaque origin,
283
+ * and the tab's `opener` is cleared, so it cannot reach this page. The tab
284
+ * shows the example as it was when it opened, and edits after that do not
285
+ * reach it. A short inline script in the tab sends the example, so on a page
286
+ * whose Content Security Policy refuses inline scripts the tab opens empty,
287
+ * as the built-in document does there.
288
+ */
289
+ open(): Window | null;
242
290
  connectedCallback(): void;
243
291
  disconnectedCallback(): void;
244
292
  /**
@@ -262,6 +310,8 @@ export declare class HelixSandboxPreview extends HelixElement {
262
310
  */
263
311
  private unready;
264
312
  protected updated(changed: PropertyValues): void;
313
+ /** The reload and open buttons, under `controls`. */
314
+ private renderControls;
265
315
  protected render(): TemplateResult;
266
316
  static styles: CSSResultGroup;
267
317
  }
@@ -8,13 +8,17 @@ var HelixSandboxPreview_1;
8
8
  import { customElement, DelayController, HelixConsoleEvent, HelixElement, HelixResizeEvent, words, } from '@fusion.dev/helix-core';
9
9
  const SETTLE = 120;
10
10
  import { html, nothing } from 'lit';
11
- import { property } from 'lit/decorators.js';
11
+ 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
+ const SANDBOX = 'allow-scripts allow-forms';
14
15
  let HelixSandboxPreview = class HelixSandboxPreview extends HelixElement {
15
16
  constructor() {
16
17
  super(...arguments);
18
+ this.controls = false;
17
19
  this.label = 'Preview';
20
+ this.openLabel = 'Open preview in a new tab';
21
+ this.reloadLabel = 'Reload preview';
18
22
  this.script = '';
19
23
  this.source = '';
20
24
  this.src = '';
@@ -38,7 +42,7 @@ let HelixSandboxPreview = class HelixSandboxPreview extends HelixElement {
38
42
  this.dispatchEvent(new HelixConsoleEvent({ level: data.level, text: data.text }));
39
43
  if (data.uncaught) {
40
44
  this.failedSource = this.sentSource;
41
- this.toggleState('failed', true);
45
+ this.fail(data.text);
42
46
  }
43
47
  }
44
48
  if (data?.type === 'helix-preview-press') {
@@ -52,6 +56,10 @@ let HelixSandboxPreview = class HelixSandboxPreview extends HelixElement {
52
56
  }
53
57
  static { HelixSandboxPreview_1 = this; }
54
58
  static { this.words = 'helix-sandbox-preview'; }
59
+ fail(text) {
60
+ this.failure = text;
61
+ this.toggleState('failed', text !== undefined);
62
+ }
55
63
  get sending() {
56
64
  return withScript(this.source, this.script);
57
65
  }
@@ -62,16 +70,13 @@ let HelixSandboxPreview = class HelixSandboxPreview extends HelixElement {
62
70
  scheduleSend() {
63
71
  this.settle.start(SETTLE, () => {
64
72
  if (this.sending !== this.failedSource) {
65
- this.toggleState('failed', false);
73
+ this.fail();
66
74
  }
67
75
  this.send();
68
76
  });
69
77
  }
70
- send() {
71
- if (!this.ready) {
72
- return;
73
- }
74
- const message = {
78
+ get message() {
79
+ return {
75
80
  ...(this.colorMode ? { colorMode: this.colorMode } : {}),
76
81
  direction: this.direction ?? 'ltr',
77
82
  layout: this.layout ?? 'padded',
@@ -79,9 +84,39 @@ let HelixSandboxPreview = class HelixSandboxPreview extends HelixElement {
79
84
  styles: this.styles,
80
85
  type: 'helix-preview-render',
81
86
  };
82
- this.frame?.contentWindow?.postMessage(message, HelixSandboxPreview_1.target);
87
+ }
88
+ send() {
89
+ if (!this.ready) {
90
+ return;
91
+ }
92
+ this.frame?.contentWindow?.postMessage(this.message, HelixSandboxPreview_1.target);
83
93
  this.sentSource = this.sending;
84
94
  }
95
+ open() {
96
+ const tab = window.open('', '_blank');
97
+ if (!tab) {
98
+ return null;
99
+ }
100
+ tab.opener = null;
101
+ const { body } = tab.document;
102
+ tab.document.title = this.label;
103
+ body.style.margin = '0';
104
+ const frame = tab.document.createElement('iframe');
105
+ frame.setAttribute('sandbox', SANDBOX);
106
+ frame.title = this.label;
107
+ frame.style.cssText = 'block-size: 100vh; border: none; display: block; inline-size: 100vw';
108
+ if (this.src === '') {
109
+ frame.srcdoc = defaultFrame();
110
+ }
111
+ else {
112
+ frame.src = this.frame?.src || this.src;
113
+ }
114
+ const send = tab.document.createElement('script');
115
+ send.textContent = `const frame = document.currentScript.previousElementSibling
116
+ frame.onload = () => frame.contentWindow.postMessage(${JSON.stringify(this.message)}, '*')`;
117
+ body.append(frame, send);
118
+ return tab;
119
+ }
85
120
  connectedCallback() {
86
121
  super.connectedCallback();
87
122
  window.addEventListener('message', this.handleMessage);
@@ -106,7 +141,7 @@ let HelixSandboxPreview = class HelixSandboxPreview extends HelixElement {
106
141
  this.ready = false;
107
142
  this.sentSource = undefined;
108
143
  this.failedSource = undefined;
109
- this.toggleState('failed', false);
144
+ this.fail();
110
145
  this.toggleState('loading', true);
111
146
  }
112
147
  updated(changed) {
@@ -119,7 +154,20 @@ let HelixSandboxPreview = class HelixSandboxPreview extends HelixElement {
119
154
  this.scheduleSend();
120
155
  return;
121
156
  }
122
- this.send();
157
+ if (changed.has('colorMode') || changed.has('direction') || changed.has('layout')) {
158
+ this.send();
159
+ }
160
+ }
161
+ renderControls() {
162
+ if (!this.controls) {
163
+ return nothing;
164
+ }
165
+ const reload = this.reloadLabel;
166
+ const open = this.openLabel;
167
+ return html `<div part="controls">
168
+ <button part="reload" type="button" @click=${() => this.reload()}>${reload}</button>
169
+ <button part="open" type="button" @click=${() => this.open()}>${open}</button>
170
+ </div>`;
123
171
  }
124
172
  render() {
125
173
  return html `<iframe
@@ -129,14 +177,17 @@ let HelixSandboxPreview = class HelixSandboxPreview extends HelixElement {
129
177
  srcdoc=${this.src === '' ? defaultFrame() : nothing}
130
178
  title=${this.label}
131
179
  allow="clipboard-write *; fullscreen *"
132
- sandbox="allow-scripts allow-forms"
133
- ></iframe>`;
180
+ sandbox=${SANDBOX}
181
+ ></iframe>${this.failure === undefined ? nothing : html `<pre part="error">${this.failure}</pre>`}${this.renderControls()}`;
134
182
  }
135
183
  static { this.styles = sheet; }
136
184
  };
137
185
  __decorate([
138
186
  property({ attribute: 'color-mode' })
139
187
  ], HelixSandboxPreview.prototype, "colorMode", void 0);
188
+ __decorate([
189
+ property({ reflect: true, type: Boolean })
190
+ ], HelixSandboxPreview.prototype, "controls", void 0);
140
191
  __decorate([
141
192
  property()
142
193
  ], HelixSandboxPreview.prototype, "direction", void 0);
@@ -147,6 +198,14 @@ __decorate([
147
198
  __decorate([
148
199
  property()
149
200
  ], HelixSandboxPreview.prototype, "layout", void 0);
201
+ __decorate([
202
+ words,
203
+ property({ attribute: 'open-label' })
204
+ ], HelixSandboxPreview.prototype, "openLabel", void 0);
205
+ __decorate([
206
+ words,
207
+ property({ attribute: 'reload-label' })
208
+ ], HelixSandboxPreview.prototype, "reloadLabel", void 0);
150
209
  __decorate([
151
210
  property()
152
211
  ], HelixSandboxPreview.prototype, "script", void 0);
@@ -159,6 +218,9 @@ __decorate([
159
218
  __decorate([
160
219
  property()
161
220
  ], HelixSandboxPreview.prototype, "styles", void 0);
221
+ __decorate([
222
+ state()
223
+ ], HelixSandboxPreview.prototype, "failure", void 0);
162
224
  HelixSandboxPreview = HelixSandboxPreview_1 = __decorate([
163
225
  customElement('helix-sandbox-preview')
164
226
  ], HelixSandboxPreview);
@@ -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="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"]: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;}}`);
@@ -20,6 +20,20 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
20
20
  * pointer, the keyboard, and the corner. The inner splitter holds the panes and
21
21
  * the outer one holds the panel, so the panel spans the full width.
22
22
  *
23
+ * **Several source panes are files**, one shown at a time under a row of
24
+ * tabs, each named by the pane's `label` or, without one, its `name`. `file`
25
+ * holds the `name` of the pane shown. The tabs are a tab list: the arrow keys
26
+ * move between them and show each pane, and `Home` and `End` go to the first
27
+ * and the last. Choosing one sends `helix-before-change` with the pane's
28
+ * `name`, which you can cancel, and `change` once `file` holds it. The panes
29
+ * not shown carry `hidden`. One pane draws no tabs.
30
+ *
31
+ * **Any editor can be the source pane**: an element in the `source` slot with
32
+ * a `name` that dispatches `helix-before-edit` with its text, which bubbles.
33
+ * A wrapper around CodeMirror or Monaco sends one on each change and is a pane
34
+ * like `helix-sandbox-source`, and the sandbox reads its `value` to seed the
35
+ * preview. Dispatch `helix-caret` from it too for the status bar's position.
36
+ *
23
37
  * On a focused divider, the arrow keys move it, and `Home` and `End` move it
24
38
  * to its ends. `Enter` on the panes divider shuts the lead pane, and pressed
25
39
  * again it opens the pane where it was. `Enter` on the panel divider shuts the
@@ -38,6 +52,8 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
38
52
  * @slot status - A status bar, spanning both panes at the bottom.
39
53
  * @slot toolbar - A bar across the top, spanning both panes.
40
54
  *
55
+ * @event {Event} change - A file tab showed its pane, whose `name` `file` now holds.
56
+ * @event {HelixBeforeChangeEvent} helix-before-change - Fired before a file tab shows its pane, with the pane's `name`. Cancelable: cancel it to keep the pane shown.
41
57
  * @event {HelixBeforeEditEvent} helix-before-edit - Fired before the source is edited. The source pane dispatches it, and you can listen for it on the sandbox. Cancelable: cancel it to keep the source unchanged.
42
58
  * @event {HelixBeforePanelResizeEvent} helix-before-panel-resize - Fired before the panel's divider is released, with the panel's height in pixels. A panel dragged closed reports `panel-collapsed-size`, its smallest height. Cancelable: cancel it to keep the current height.
43
59
  * @event {HelixBeforeSplitEvent} helix-before-split - Fired before the panes' divider is released, with the lead pane's share. Cancelable: cancel it to keep the current split.
@@ -50,6 +66,8 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
50
66
  *
51
67
  * @csspart body - The splitter holding the panes above the panel. `::part(body):state(dragging)` matches while its divider is held.
52
68
  * @csspart corner - The handle where the two dividers cross.
69
+ * @csspart file - One file tab, `aria-selected` while its pane is shown.
70
+ * @csspart files - The file tabs over the source panes, where there are several.
53
71
  * @csspart pane - Either pane.
54
72
  * @csspart panel - The box that holds the slotted panel.
55
73
  * @csspart panes - The splitter holding the source and preview panes. `::part(panes):state(dragging)` matches while its divider is held.
@@ -92,6 +110,16 @@ export declare class HelixSandbox extends HelixElement {
92
110
  * change, and it needs no first frame to be read on.
93
111
  */
94
112
  defaultSplit: number;
113
+ /**
114
+ * The `name` of the source pane shown, where the sandbox holds several.
115
+ *
116
+ * Unset, or a name no pane has, shows the first.
117
+ */
118
+ file?: string;
119
+ /**
120
+ * The name of the file tabs, read by screen readers.
121
+ */
122
+ filesLabel: string;
95
123
  /**
96
124
  * Which pane comes first, before the divider.
97
125
  *
@@ -163,6 +191,26 @@ export declare class HelixSandbox extends HelixElement {
163
191
  * platform answering on the client, which is where a divider is any use.
164
192
  */
165
193
  private hasPanel;
194
+ /** The source panes where there are several, each a file with a tab. */
195
+ private files;
196
+ /** The panes this element wrote `hidden` on, to take it off again. */
197
+ private readonly hid;
198
+ /** The pane shown among the files. */
199
+ private get shownFile();
200
+ /** Notes the source panes, which are files where there are several. */
201
+ private readFiles;
202
+ /** Shows the pane a file tab names, once nobody refused. */
203
+ private chooseFile;
204
+ /**
205
+ * Arrow keys, `Home` and `End` on the file tabs, which move to a tab and
206
+ * show its pane, as a tab list that follows focus does. Left and Right swap
207
+ * on a right-to-left page.
208
+ */
209
+ private readonly handleFileKey;
210
+ /** The row of file tabs, where there are several source panes. */
211
+ private renderFiles;
212
+ /** Hides the files not shown, and shows any this element hid that is now. */
213
+ private showFile;
166
214
  /**
167
215
  * The body's height, which is what a pixel panel is a fraction of.
168
216
  *
@@ -457,6 +505,13 @@ export declare class HelixSandbox extends HelixElement {
457
505
  * question the host can answer directly.
458
506
  */
459
507
  private readonly readPanel;
508
+ /** Shows the file chosen, once the tabs that choose it are drawn. */
509
+ protected updated(changed: PropertyValues): void;
510
+ /**
511
+ * One pane and the slot it shows. The source pane holds the file tabs, where
512
+ * there are several, and is then the panel they control.
513
+ */
514
+ private renderPane;
460
515
  protected render(): TemplateResult;
461
516
  static styles: CSSResultGroup;
462
517
  }