@fusion.dev/helix 0.170.0 → 0.171.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.
@@ -1,6 +1,6 @@
1
1
  import { createSheet } from '@fusion.dev/helix-core';
2
2
  export const styles = createSheet(`:host {display: grid;grid-template-rows: auto minmax(0, 1fr) auto auto auto;max-block-size: var(--chat-max-block-size, none);min-block-size: 0;}[part="header"] {grid-row: 1;}[part="log"] {grid-row: 2;}[part="resume"] {grid-row: 3;}[part="status"] {grid-row: 4;}[part="composer"] {grid-row: 5;}[part="log"] {display: flex;flex-direction: column;scrollbar-color: var(--chat-scrollbar-thumb-color, var(--helix-scrollbar-thumb-color))
3
- var(--chat-scrollbar-track-color, var(--helix-scrollbar-track-color));scrollbar-width: thin;gap: var(--chat-gap, var(--helix-space-md));overflow-y: auto;padding-block: var(--helix-space-md);padding-inline: var(--chat-padding-inline, var(--helix-space-md));justify-content: safe flex-end;}[part="log"]:hover {scrollbar-color: var(--chat-scrollbar-thumb-hover-color, var(--helix-scrollbar-thumb-hover-color))
3
+ var(--chat-scrollbar-track-color, var(--helix-scrollbar-track-color));scrollbar-width: thin;gap: var(--chat-gap, var(--helix-space-md));overflow-y: auto;padding-block: var(--helix-space-md);padding-inline: var(--chat-padding-inline, var(--helix-space-md));justify-content: safe flex-end;}[part="log"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1);}[part="log"]:hover {scrollbar-color: var(--chat-scrollbar-thumb-hover-color, var(--helix-scrollbar-thumb-hover-color))
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
@@ -3,14 +3,22 @@ export const styles = createSheet(`:host {--_code-block-size: var(--code-block-f
3
3
  > :is([part="actions"], [part="wrap-toggle"])
4
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
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="files"] {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-block-end-width: var(--helix-space-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;overflow-x: auto;padding-inline: var(--helix-space-xs);}[part="file-tab"] {background-color: transparent;border: none;border-block-end: var(--helix-border-thick) solid transparent;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);padding-block: var(--helix-space-xs);padding-inline: var(--helix-space-sm);white-space: nowrap;}[part="file-tab"]:hover,
6
- [part="file-tab"][aria-selected="true"] {color: var(--helix-color-foreground);}[part="file-tab"][aria-selected="true"] {border-block-end-color: var(--helix-color-accent);}[part="file-tab"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1);}:host(:state(tabbed)) :is([part="header"], [part="block"]) {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,
6
+ [part="file-tab"][aria-selected="true"] {color: var(--helix-color-foreground);}[part="file-tab"][aria-selected="true"] {border-block-end-color: var(--helix-color-accent);}[part="file-tab"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1);}:host(:state(tabbed)) :is([part="header"], [part="block"]) {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);}::slotted([slot="expand-icon"]) {margin-inline-end: var(--helix-space-xs);vertical-align: middle;}[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,
7
7
  [part="code"]:focus-visible,
8
8
  [part="old"]:focus-visible,
9
- [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
+ [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(
10
10
  --code-block-mark-color,
11
11
  color-mix(in oklab, var(--helix-color-primary) 12%, transparent)
12
12
  );block-size: var(--code-block-mark-size, 0);border-inline-start: var(--helix-border-thick) solid
13
- 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"],
13
+ 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);}[part~="mark-added"] {background-color: color-mix(
14
+ in oklab,
15
+ var(--code-block-added-color, var(--helix-color-success)) 14%,
16
+ transparent
17
+ );border-inline-start-color: var(--code-block-added-color, var(--helix-color-success));}[part~="mark-removed"] {background-color: color-mix(
18
+ in oklab,
19
+ var(--code-block-removed-color, var(--helix-color-danger)) 14%,
20
+ transparent
21
+ );border-inline-start-color: var(--code-block-removed-color, var(--helix-color-danger));}: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"],
14
22
  [part="old"],
15
23
  [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]))
16
24
  [part="frame"]:not([part="header"] + *)
@@ -5,4 +5,5 @@ border-color var(--helix-duration-fast) var(--helix-easing-standard),
5
5
  background-color var(--helix-duration-fast) var(--helix-easing-standard);}::slotted([slot="tags"]) {flex: 0 0 auto;max-inline-size: 100%;}[part="control"] {flex: 1 1 var(--filter-input-control-size, 6ch);}[part="lead"] {align-items: center;align-self: stretch;border-inline-end: var(--helix-border-thin) solid
6
6
  var(--filter-input-lead-border-color, var(--helix-color-border));display: flex;margin-block: calc(var(--helix-space-2xs) * -1);margin-inline-start: calc(var(--helix-space-xs) * -1);padding-inline: var(--helix-space-2xs);}[part="lead"][hidden] {display: none;}[part="lead"],
7
7
  slot[name="end"] {--button-size: var(--filter-input-action-size, var(--helix-size-tag));--icon-button-size: var(--filter-input-action-size, var(--helix-size-tag));--button-padding-inline: var(--filter-input-action-padding-inline, var(--helix-space-xs));}slot[name="start"]::slotted(*),
8
- slot[name="end"]::slotted(*) {color: var(--helix-color-foreground-subtle);flex: 0 0 auto;}[part="clear"] {--icon-size: 0.85em;all: unset;align-items: center;align-self: center;border-radius: var(--helix-radii-sm);box-sizing: border-box;color: var(--filter-input-clear-color, var(--helix-color-foreground-subtle));cursor: pointer;display: flex;flex: 0 0 auto;justify-content: center;min-block-size: var(--helix-size-icon-loose);min-inline-size: var(--helix-size-icon-loose);transition: color var(--helix-duration-fast) var(--helix-easing-standard);}[part="clear"]:hover {background-color: color-mix(in oklab, currentcolor 12%, transparent);color: var(--filter-input-clear-color-hover, var(--helix-color-foreground));}[part="clear"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: var(--helix-size-focus-offset);}:host(:disabled) [part="field"] {cursor: not-allowed;opacity: 0.5;}@media print {:host {print-color-adjust: exact;}}[part="surface"] {--_floating-border-color: var(--filter-input-border-color, var(--helix-color-border));--_floating-border-width: var(--filter-input-border-width, var(--helix-border-floating));--_floating-shadow: var(--filter-input-shadow, var(--helix-shadow-floating));box-sizing: border-box;inline-size: var(--filter-input-panel-size, var(--floating-anchor-inline-size, 100%));max-block-size: var(--floating-available-block-size, 20rem);min-inline-size: max-content;overflow-y: auto;padding-block: var(--helix-space-2xs);padding-inline: var(--helix-space-none);position: absolute;}`);
8
+ slot[name="end"]::slotted(*) {color: var(--helix-color-foreground-subtle);flex: 0 0 auto;}[part="clear"] {--icon-size: 0.85em;all: unset;align-items: center;align-self: center;border-radius: var(--helix-radii-sm);box-sizing: border-box;color: var(--filter-input-clear-color, var(--helix-color-foreground-subtle));cursor: pointer;display: flex;flex: 0 0 auto;justify-content: center;min-block-size: var(--helix-size-icon-loose);min-inline-size: var(--helix-size-icon-loose);transition: color var(--helix-duration-fast) var(--helix-easing-standard);}[part="clear"]:hover {background-color: color-mix(in oklab, currentcolor 12%, transparent);color: var(--filter-input-clear-color-hover, var(--helix-color-foreground));}[part="clear"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: var(--helix-size-focus-offset);}:host(:disabled) [part="field"] {cursor: not-allowed;opacity: 0.5;}@media print {:host {print-color-adjust: exact;}}[part="surface"] {--_floating-border-color: var(--filter-input-border-color, var(--helix-color-border));--_floating-border-width: var(--filter-input-border-width, var(--helix-border-floating));--_floating-shadow: var(--filter-input-shadow, var(--helix-shadow-floating));box-sizing: border-box;inline-size: var(--filter-input-panel-size, var(--floating-anchor-inline-size, 100%));max-block-size: var(--floating-available-block-size, 20rem);min-inline-size: max-content;overflow-y: auto;padding-block: var(--helix-space-2xs);padding-inline: var(--helix-space-none);position: absolute;}[part="empty"],
9
+ [part="loading"] {--_collection-inset-block: var(--helix-space-xs);--_collection-inset-inline: var(--helix-space-xs);}`);
@@ -46,6 +46,7 @@ import '../../option/index.js';
46
46
  * @cssproperty --filter-input-size - The height of the empty field. The field grows from this height.
47
47
  */
48
48
  export declare class HelixFilterInput extends HelixHeadlessFilterInput {
49
+ static tier: string;
49
50
  static styles: CSSResultGroup;
50
51
  }
51
52
  declare global {
@@ -7,6 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement } from '@fusion.dev/helix-core';
8
8
  import { HelixHeadlessFilterInput } from '@fusion.dev/helix-headless/filter-input';
9
9
  import '../../option/index.js';
10
+ import { styles as collectionStateStyles } from '../../shared/collection/state.css.js';
10
11
  import { styles as fieldStyles } from '../../shared/field/field.css.js';
11
12
  import { styles as fieldFrameStyles } from '../../shared/field/field-frame.css.js';
12
13
  import { styles as fieldStackedStyles } from '../../shared/field/field-stacked.css.js';
@@ -17,6 +18,7 @@ import { styles as liveStyles } from '../../shared/live/live.css.js';
17
18
  import { styles as tagRowStyles } from '../../shared/tags/tags.css.js';
18
19
  import { styles } from './filter-input-root.css.js';
19
20
  let HelixFilterInput = class HelixFilterInput extends HelixHeadlessFilterInput {
21
+ static { this.tier = 'helix-'; }
20
22
  static { this.styles = [
21
23
  liveStyles,
22
24
  glyphStyles,
@@ -26,6 +28,7 @@ let HelixFilterInput = class HelixFilterInput extends HelixHeadlessFilterInput {
26
28
  tagRowStyles,
27
29
  floatingPanelStyles,
28
30
  floatingEntranceStyles,
31
+ collectionStateStyles,
29
32
  styles,
30
33
  ]; }
31
34
  };
@@ -69,4 +69,12 @@ export declare const servePreview: (options?: ServePreviewOptions) => (() => voi
69
69
  * a sandbox's `js` pane adds to its `html` pane.
70
70
  */
71
71
  export declare const withScript: (source: string, script: string) => string;
72
- export declare const defaultFrame: () => string;
72
+ /**
73
+ * The built-in document, with an import map for `imports` where there are any.
74
+ *
75
+ * **The map comes first in the head**, because a browser reads an import map
76
+ * only before the first module it loads, and the example's scripts are
77
+ * modules. `</` is escaped in the map as in the script, so a URL cannot close
78
+ * the element early.
79
+ */
80
+ export declare const defaultFrame: (imports?: Readonly<Record<string, string>>) => string;
@@ -169,10 +169,14 @@ export const servePreview = (options = {}) => serveWith(TOOLS, options);
169
169
  let builtIn;
170
170
  export const withScript = (source, script) => script === '' ? source : `${source}<script type="module">${script}${SCRIPT_END}`;
171
171
  const SCRIPT_END = ['<', 'script>'].join('/');
172
- export const defaultFrame = () => {
172
+ export const defaultFrame = (imports) => {
173
173
  if (builtIn === undefined) {
174
174
  const script = `(${serveWith})({drawn:${drawn},isRender:${isRender},layoutRules:${JSON.stringify(LAYOUT_RULES)},loggedText:${loggedText},runScripts:${runScripts},withoutStylesheets:${withoutStylesheets}})`;
175
175
  builtIn = `<!doctype html><html><head><meta charset="utf-8"><meta content="width=device-width" name="viewport"><style>:root{color-scheme:light dark;font-family:system-ui,sans-serif}body{margin:0}</style></head><body><div id="preview-root"></div><script>${script.replaceAll('</', '<\\/')}${SCRIPT_END}</body></html>`;
176
176
  }
177
- return builtIn;
177
+ if (!imports || Object.keys(imports).length === 0) {
178
+ return builtIn;
179
+ }
180
+ const map = JSON.stringify({ imports }).replaceAll('</', '<\\/');
181
+ return builtIn.replace('<head>', `<head><script type="importmap">${map}${SCRIPT_END}`);
178
182
  };
@@ -54,6 +54,15 @@ export type PreviewMessage = PreviewConsole | PreviewPress | PreviewReady | Prev
54
54
  * `servePreview` does. It defines no element, so an example that uses Helix
55
55
  * components needs a `src` that imports them.
56
56
  *
57
+ * **`imports` lets an example import packages by name** in the built-in
58
+ * document, as an import map: each name maps to the address of an ES module,
59
+ * such as `{"canvas-confetti": "https://esm.sh/canvas-confetti@1.9.3"}`, and
60
+ * `import confetti from 'canvas-confetti'` in the `js` pane then loads it. A
61
+ * CDN that serves npm packages as modules, such as esm.sh or jsDelivr's
62
+ * `+esm`, gives every package an address. A new map loads the document again,
63
+ * since a browser reads one only as a document starts. A `src` document
64
+ * declares its own.
65
+ *
57
66
  * **Examples render in a frame**, so an example's styles and scripts do not
58
67
  * affect the page around the sandbox, and `position: fixed` stays inside the
59
68
  * pane. The frame starts empty, with its own stylesheets and its own custom
@@ -134,6 +143,13 @@ export declare class HelixSandboxPreview extends HelixElement {
134
143
  * `colorMode` has to be sent rather than set.
135
144
  */
136
145
  direction?: 'rtl';
146
+ /**
147
+ * The packages an example can import by name in the built-in document,
148
+ * each mapped to the address of an ES module, as an import map's `imports`.
149
+ * Set it as JSON in the attribute or as an object. Not read where `src` is
150
+ * set.
151
+ */
152
+ imports?: Record<string, string>;
137
153
  /**
138
154
  * The frame's name, read by screen readers.
139
155
  */
@@ -106,7 +106,7 @@ let HelixSandboxPreview = class HelixSandboxPreview extends HelixElement {
106
106
  frame.title = this.label;
107
107
  frame.style.cssText = 'block-size: 100vh; border: none; display: block; inline-size: 100vw';
108
108
  if (this.src === '') {
109
- frame.srcdoc = defaultFrame();
109
+ frame.srcdoc = defaultFrame(this.imports);
110
110
  }
111
111
  else {
112
112
  frame.src = this.frame?.src || this.src;
@@ -132,7 +132,7 @@ frame.onload = () => frame.contentWindow.postMessage(${JSON.stringify(this.messa
132
132
  }
133
133
  this.unready();
134
134
  if (this.src === '') {
135
- frame.srcdoc = defaultFrame();
135
+ frame.srcdoc = defaultFrame(this.imports);
136
136
  return;
137
137
  }
138
138
  frame.src = this.src;
@@ -146,7 +146,7 @@ frame.onload = () => frame.contentWindow.postMessage(${JSON.stringify(this.messa
146
146
  }
147
147
  updated(changed) {
148
148
  super.updated(changed);
149
- if (changed.has('src')) {
149
+ if (changed.has('src') || (changed.has('imports') && this.src === '')) {
150
150
  this.unready();
151
151
  return;
152
152
  }
@@ -174,7 +174,7 @@ frame.onload = () => frame.contentWindow.postMessage(${JSON.stringify(this.messa
174
174
  part="frame"
175
175
  @load=${this.handleFrameLoad}
176
176
  src=${this.src || nothing}
177
- srcdoc=${this.src === '' ? defaultFrame() : nothing}
177
+ srcdoc=${this.src === '' ? defaultFrame(this.imports) : nothing}
178
178
  title=${this.label}
179
179
  allow="clipboard-write *; fullscreen *"
180
180
  sandbox=${SANDBOX}
@@ -191,6 +191,9 @@ __decorate([
191
191
  __decorate([
192
192
  property()
193
193
  ], HelixSandboxPreview.prototype, "direction", void 0);
194
+ __decorate([
195
+ property({ type: Object })
196
+ ], HelixSandboxPreview.prototype, "imports", void 0);
194
197
  __decorate([
195
198
  words,
196
199
  property()
@@ -1,4 +1,4 @@
1
1
  import { createSheet } from '@fusion.dev/helix-core';
2
2
  export const styles = createSheet(`:host {display: block;block-size: 100%;}[part="highlight"],
3
3
  [part="source"] {border: none;color: var(--helix-color-foreground-muted);font-family: var(--helix-font-mono);font-size: max(var(--helix-text-field-min), var(--helix-text-xs));inline-size: 100%;line-height: var(--helix-leading-normal);min-block-size: var(--sandbox-source-min-block-size, 12rem);padding: var(--helix-space-sm);tab-size: 2;}[part="source"] {background-color: var(--sandbox-source-background-color, var(--helix-color-background));block-size: 100%;display: block;resize: none;}[part="source"]:focus-visible {background-color: var(--helix-color-surface);outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1);}[part="field"] {background-color: var(--sandbox-source-background-color, var(--helix-color-background));block-size: 100%;display: grid;grid-template-rows: minmax(max-content, auto);overflow: auto;}[part="field"]:focus-within {background-color: var(--helix-color-surface);}[part="field"]:has(> [part="source"]:focus-visible) {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1);}[part="field"] > [part] {grid-area: 1 / 1;letter-spacing: normal;margin: 0;overflow-wrap: break-word;white-space: pre-wrap;}[part="field"] > [part="source"],
4
- [part="field"] > [part="source"]:focus-visible {-webkit-text-fill-color: transparent;align-self: stretch;background-color: transparent;block-size: auto;caret-color: var(--helix-color-foreground);color: transparent;outline: none;overflow: hidden;}[data-kind="callable"] {color: var(--sandbox-source-callable-color, var(--helix-color-syntax-callable));}[data-kind="comment"] {color: var(--sandbox-source-comment-color, var(--helix-color-syntax-comment));}[data-kind="constant"] {color: var(--sandbox-source-constant-color, var(--helix-color-syntax-constant));}[data-kind="invalid"] {color: var(--sandbox-source-invalid-color, var(--helix-color-syntax-invalid));}[data-kind="keyword"] {color: var(--sandbox-source-keyword-color, var(--helix-color-syntax-keyword));}[data-kind="name"] {color: var(--sandbox-source-name-color, var(--helix-color-syntax-name));}[data-kind="string"] {color: var(--sandbox-source-string-color, var(--helix-color-syntax-string));}[data-kind="tag"] {color: var(--sandbox-source-tag-color, var(--helix-color-syntax-tag));}`);
4
+ [part="field"] > [part="source"]:focus-visible {-webkit-text-fill-color: transparent;align-self: stretch;background-color: transparent;block-size: auto;caret-color: var(--helix-color-foreground);color: transparent;outline: none;overflow: hidden;}:host([line-numbers]) [part="field"] > [part] {padding-inline-start: calc(var(--helix-space-sm) * 3 + var(--_sandbox-source-gutter, 1ch));}:host([line-numbers]) [part="highlight"] {counter-reset: line;}[part="line"] {counter-increment: line;display: block;min-block-size: 1lh;position: relative;}[part="line"]::before {color: var(--sandbox-source-line-number-color, var(--helix-color-foreground-subtle));content: counter(line);inset-inline-end: calc(100% + var(--helix-space-sm) * 2);position: absolute;text-align: end;}[data-kind="callable"] {color: var(--sandbox-source-callable-color, var(--helix-color-syntax-callable));}[data-kind="comment"] {color: var(--sandbox-source-comment-color, var(--helix-color-syntax-comment));}[data-kind="constant"] {color: var(--sandbox-source-constant-color, var(--helix-color-syntax-constant));}[data-kind="invalid"] {color: var(--sandbox-source-invalid-color, var(--helix-color-syntax-invalid));}[data-kind="keyword"] {color: var(--sandbox-source-keyword-color, var(--helix-color-syntax-keyword));}[data-kind="name"] {color: var(--sandbox-source-name-color, var(--helix-color-syntax-name));}[data-kind="string"] {color: var(--sandbox-source-string-color, var(--helix-color-syntax-string));}[data-kind="tag"] {color: var(--sandbox-source-tag-color, var(--helix-color-syntax-tag));}`);
@@ -1,5 +1,5 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
- import type { CSSResultGroup, TemplateResult } from 'lit';
2
+ import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
3
3
  import { type SandboxTokenizer } from '../tokens.js';
4
4
  /**
5
5
  * @summary An editable source pane for a sandbox preview.
@@ -24,6 +24,18 @@ import { type SandboxTokenizer } from '../tokens.js';
24
24
  * that throws leaves the source drawn plain and the field editable, and the
25
25
  * error is reported to the page as an uncaught one.
26
26
  *
27
+ * **`line-numbers` numbers each line** in a gutter beside the source. A line
28
+ * that wraps keeps one number. The numbers are drawn, and hidden from screen
29
+ * readers, which read the field.
30
+ *
31
+ * Enter keeps the indentation of the line it breaks, and the new line is
32
+ * undone with the next undo as typing is.
33
+ *
34
+ * **The source can be written inside the pane** where `value` is not set: its
35
+ * text, or the markup of a `<template>` it holds, without the indentation the
36
+ * page gave it. Markup goes in a template so the page does not render it or
37
+ * run its scripts. Text inside the pane is read once, when it connects.
38
+ *
27
39
  * @since 0.1.0
28
40
  * @status stable
29
41
  * @aliases Editor, Code pane
@@ -33,8 +45,9 @@ import { type SandboxTokenizer } from '../tokens.js';
33
45
  * @event {HelixCaretEvent} helix-caret - Fired when the cursor moves.
34
46
  * @event {HelixEditEvent} helix-edit - Fired after the source was edited, with the new source.
35
47
  *
36
- * @csspart field - The box that scrolls the field and its colors together, under `tokenize`.
37
- * @csspart highlight - The colored copy of the source drawn under the field, under `tokenize`.
48
+ * @csspart field - The box that scrolls the field and its colors together, under `tokenize` or `line-numbers`.
49
+ * @csspart highlight - The colored copy of the source drawn under the field, under `tokenize` or `line-numbers`.
50
+ * @csspart line - One line of that copy, under `line-numbers`, numbered in the gutter by its `::before`.
38
51
  * @csspart source - The editable region.
39
52
  * @csspart token - One run of the source that has a kind, under `tokenize`.
40
53
  *
@@ -44,6 +57,7 @@ import { type SandboxTokenizer } from '../tokens.js';
44
57
  * @cssproperty --sandbox-source-constant-color - Color of a `constant` run, under `tokenize`. Defaults to `--helix-color-syntax-constant`.
45
58
  * @cssproperty --sandbox-source-invalid-color - Color of an `invalid` run, under `tokenize`. Defaults to `--helix-color-syntax-invalid`.
46
59
  * @cssproperty --sandbox-source-keyword-color - Color of a `keyword` run, under `tokenize`. Defaults to `--helix-color-syntax-keyword`.
60
+ * @cssproperty --sandbox-source-line-number-color - Color of the line numbers, under `line-numbers`. Defaults to `--helix-color-foreground-subtle`.
47
61
  * @cssproperty --sandbox-source-min-block-size - Minimum height of the pane.
48
62
  * @cssproperty --sandbox-source-name-color - Color of a `name` run, under `tokenize`. Defaults to `--helix-color-syntax-name`.
49
63
  * @cssproperty --sandbox-source-string-color - Color of a `string` run, under `tokenize`. Defaults to `--helix-color-syntax-string`.
@@ -74,6 +88,10 @@ export declare class HelixSandboxSource extends HelixElement {
74
88
  * holds several panes. Empty puts the pane's `name` on the tab.
75
89
  */
76
90
  label: string;
91
+ /**
92
+ * Whether each line of the source is numbered, in a gutter beside it.
93
+ */
94
+ lineNumbers: boolean;
77
95
  /**
78
96
  * Which source this pane holds. Every `helix-before-edit` event includes it.
79
97
  *
@@ -107,6 +125,22 @@ export declare class HelixSandboxSource extends HelixElement {
107
125
  value: string;
108
126
  /** Set by Escape, so the next Tab leaves the field. */
109
127
  private escaped;
128
+ /** Whether the source written inside the pane has been read. */
129
+ private read;
130
+ /**
131
+ * Takes the source written inside the pane where `value` is not set: a
132
+ * template's markup, or else the text. Client only, like every read of the
133
+ * light DOM, and once there is something to read.
134
+ *
135
+ * **As it connects, and again before its first render.** A pane that
136
+ * upgrades after the parser is done holds its text as it connects, which is
137
+ * before a sandbox around it reads `value` to seed its preview. A parser that
138
+ * connects the pane before its children, as one with the element already
139
+ * defined does, leaves the text for the render.
140
+ */
141
+ private readWritten;
142
+ connectedCallback(): void;
143
+ protected willUpdate(changed: PropertyValues): void;
110
144
  /**
111
145
  * Reports where the cursor is, for a status bar to read.
112
146
  *
@@ -145,7 +179,13 @@ export declare class HelixSandboxSource extends HelixElement {
145
179
  private edit;
146
180
  private readonly handleKeydown;
147
181
  /**
148
- * Writes the two spaces Tab inserts, as typing would.
182
+ * Breaks the line with the indentation it has, where it has some. A line at
183
+ * the margin is left to the platform's own Enter.
184
+ */
185
+ private breakLine;
186
+ /**
187
+ * Writes text at the caret, as typing would: the two spaces Tab inserts,
188
+ * and the indentation Enter keeps.
149
189
  *
150
190
  * **Through `insertText`, so undo takes the indent back.** Assigning
151
191
  * `field.value` empties the textarea's undo history: a reader who typed,
@@ -155,7 +195,7 @@ export declare class HelixSandboxSource extends HelixElement {
155
195
  * into whatever has focus, so it is used only while the field has it. Where it
156
196
  * is not supported, the field is written directly.
157
197
  */
158
- private indent;
198
+ private insert;
159
199
  protected render(): TemplateResult;
160
200
  /** The field itself and its description, which is the whole pane without `tokenize`. */
161
201
  private renderField;
@@ -10,6 +10,30 @@ import { property } from 'lit/decorators.js';
10
10
  import { drawnRuns } from '../tokens.js';
11
11
  import { styles } from './sandbox-source.css.js';
12
12
  const MODIFIERS = new Set(['Alt', 'AltGraph', 'Control', 'Meta', 'Shift']);
13
+ const dedent = (text) => {
14
+ const lines = text
15
+ .replace(/^(?:[ \t]*\n)+/, '')
16
+ .trimEnd()
17
+ .split('\n');
18
+ const shared = Math.min(...lines
19
+ .filter((line) => line.trim() !== '')
20
+ .map((line) => /^[ \t]*/.exec(line)?.[0].length ?? 0));
21
+ return Number.isFinite(shared) ? lines.map((line) => line.slice(shared)).join('\n') : '';
22
+ };
23
+ const linesOf = (runs) => {
24
+ const lines = [[]];
25
+ for (const { kind, text } of runs) {
26
+ text.split('\n').forEach((piece, at) => {
27
+ if (at > 0) {
28
+ lines.push([]);
29
+ }
30
+ if (piece !== '') {
31
+ lines.at(-1)?.push(kind ? { kind, text: piece } : { text: piece });
32
+ }
33
+ });
34
+ }
35
+ return lines;
36
+ };
13
37
  let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
14
38
  constructor() {
15
39
  super(...arguments);
@@ -17,10 +41,12 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
17
41
  this.fieldNameLabel = '{field}, {name}';
18
42
  this.keysDescription = 'Tab inserts two spaces. To leave, press Escape and then Tab, or press Shift+Tab to go back.';
19
43
  this.label = '';
44
+ this.lineNumbers = false;
20
45
  this.name = 'html';
21
46
  this.tabIndents = true;
22
47
  this.value = '';
23
48
  this.escaped = false;
49
+ this.read = false;
24
50
  this.reportCaret = (event) => {
25
51
  const field = event.target;
26
52
  const at = field.selectionStart ?? 0;
@@ -43,15 +69,46 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
43
69
  if (MODIFIERS.has(event.key)) {
44
70
  return;
45
71
  }
72
+ if (event.key === 'Enter' &&
73
+ !event.isComposing &&
74
+ !(event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)) {
75
+ this.escaped = false;
76
+ this.breakLine(event);
77
+ return;
78
+ }
46
79
  if (event.key !== 'Tab' || event.shiftKey || !this.tabIndents || this.escaped) {
47
80
  this.escaped = false;
48
81
  return;
49
82
  }
50
83
  event.preventDefault();
51
- this.indent(event.target);
84
+ this.insert(event.target, ' ');
52
85
  };
53
86
  }
54
87
  static { this.words = 'helix-sandbox-source'; }
88
+ readWritten() {
89
+ if (this.read ||
90
+ this.children === undefined ||
91
+ this.hasAttribute('value') ||
92
+ this.value !== '') {
93
+ return;
94
+ }
95
+ const template = this.querySelector(':scope > template');
96
+ const written = dedent(template ? template.innerHTML : (this.textContent ?? ''));
97
+ if (written !== '') {
98
+ this.read = true;
99
+ this.value = written;
100
+ }
101
+ }
102
+ connectedCallback() {
103
+ super.connectedCallback();
104
+ this.readWritten();
105
+ }
106
+ willUpdate(changed) {
107
+ super.willUpdate(changed);
108
+ if (!this.hasUpdated) {
109
+ this.readWritten();
110
+ }
111
+ }
55
112
  edit(field, next) {
56
113
  const request = new HelixBeforeEditEvent({ value: next });
57
114
  const accepted = this.dispatchEvent(request);
@@ -63,32 +120,50 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
63
120
  this.dispatchEvent(new HelixEditEvent({ value: next }));
64
121
  }
65
122
  }
66
- indent(field) {
123
+ breakLine(event) {
124
+ const field = event.target;
125
+ const at = field.selectionStart ?? 0;
126
+ const start = field.value.lastIndexOf('\n', at - 1) + 1;
127
+ const indent = /^[ \t]*/.exec(field.value.slice(start, at))?.[0] ?? '';
128
+ if (indent !== '') {
129
+ event.preventDefault();
130
+ this.insert(field, `\n${indent}`);
131
+ }
132
+ }
133
+ insert(field, text) {
67
134
  const { ownerDocument } = field;
68
135
  const focused = field.getRootNode().activeElement === field;
69
136
  if (focused &&
70
137
  typeof ownerDocument.execCommand === 'function' &&
71
- ownerDocument.execCommand('insertText', false, ' ')) {
138
+ ownerDocument.execCommand('insertText', false, text)) {
72
139
  return;
73
140
  }
74
- field.setRangeText(' ', field.selectionStart, field.selectionEnd, 'end');
141
+ field.setRangeText(text, field.selectionStart, field.selectionEnd, 'end');
75
142
  this.edit(field, field.value);
76
143
  }
77
144
  render() {
78
145
  const field = this.renderField();
79
- if (!this.tokenize) {
146
+ if (!this.tokenize && !this.lineNumbers) {
80
147
  return field;
81
148
  }
82
149
  let given = [];
83
150
  try {
84
- given = this.tokenize(this.value, this.name);
151
+ given = this.tokenize?.(this.value, this.name) ?? [];
85
152
  }
86
153
  catch (error) {
87
154
  globalThis.reportError?.(error);
88
155
  }
89
156
  const runs = drawnRuns(this.value, given);
157
+ const draw = (some) => some.map(({ kind, text }) => kind ? html `<span part="token" data-kind=${kind}>${text}</span>` : text);
158
+ if (this.lineNumbers) {
159
+ const lines = linesOf(runs);
160
+ return html `<div part="field" style="--_sandbox-source-gutter: ${String(lines.length).length}ch">
161
+ <pre part="highlight" aria-hidden="true">${lines.map((line) => html `<span part="line">${draw(line)}</span>`)}</pre>
162
+ ${field}
163
+ </div>`;
164
+ }
90
165
  return html `<div part="field">
91
- <pre part="highlight" aria-hidden="true">${runs.map(({ kind, text }) => kind ? html `<span part="token" data-kind=${kind}>${text}</span>` : text)}${'\n'}</pre>
166
+ <pre part="highlight" aria-hidden="true">${draw(runs)}${'\n'}</pre>
92
167
  ${field}
93
168
  </div>`;
94
169
  }
@@ -126,6 +201,9 @@ __decorate([
126
201
  __decorate([
127
202
  property()
128
203
  ], HelixSandboxSource.prototype, "label", void 0);
204
+ __decorate([
205
+ property({ attribute: 'line-numbers', reflect: true, type: Boolean })
206
+ ], HelixSandboxSource.prototype, "lineNumbers", void 0);
129
207
  __decorate([
130
208
  property()
131
209
  ], HelixSandboxSource.prototype, "name", void 0);