jssm 5.147.10 → 5.148.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.
@@ -0,0 +1,1113 @@
1
+ import { css, html, LitElement } from 'lit';
2
+ import { state, property } from 'lit/decorators.js';
3
+ import { machine_to_dot, machine_to_svg_string } from 'jssm/viz';
4
+
5
+ /**
6
+ * Shared FSL appearance contract — the `--fsl-*` design-token vocabulary.
7
+ *
8
+ * Components include this in `static styles` and consume the **private**
9
+ * `--_fsl-*` vars, which resolve: embedder's public `--fsl-*` token →
10
+ * `[theme="dark"]` default → built-in light fallback. White-label by setting
11
+ * `--fsl-*` on any ancestor (custom properties inherit through shadow DOM);
12
+ * flip the built-in default with the host's `theme="dark"` attribute.
13
+ *
14
+ * Companion conventions (declared per-component): expose structural elements as
15
+ * `::part(...)` (e.g. `part="toolbar"`, `"gutter"`, `"editor"`) and forward
16
+ * child parts with `exportparts`; chrome components carry brand slots
17
+ * (`<slot name="brand">` / `"logo">`).
18
+ */
19
+ const fslTokens = css `
20
+ :host {
21
+ --_fsl-surface: var(--fsl-color-surface, #ffffff);
22
+ --_fsl-text: var(--fsl-color-text, #222222);
23
+ --_fsl-accent: var(--fsl-color-accent, #5b9dff);
24
+ --_fsl-border: var(--fsl-color-border, #e5e5e5);
25
+ --_fsl-muted: var(--fsl-color-muted, #9aa0a6);
26
+ --_fsl-font: var(--fsl-font, system-ui, -apple-system, "Segoe UI", sans-serif);
27
+ --_fsl-font-mono: var(--fsl-font-mono, ui-monospace, Consolas, monospace);
28
+ --_fsl-radius: var(--fsl-radius, 6px);
29
+ --_fsl-space-1: var(--fsl-space-1, 4px);
30
+ --_fsl-space-2: var(--fsl-space-2, 8px);
31
+ --_fsl-space-3: var(--fsl-space-3, 12px);
32
+ --_fsl-space-4: var(--fsl-space-4, 16px);
33
+ }
34
+ :host([theme="dark"]) {
35
+ --_fsl-surface: var(--fsl-color-surface, #1e1e22);
36
+ --_fsl-text: var(--fsl-color-text, #d6d6d6);
37
+ --_fsl-accent: var(--fsl-color-accent, #82aaff);
38
+ --_fsl-border: var(--fsl-color-border, #2a2a2e);
39
+ --_fsl-muted: var(--fsl-color-muted, #5a5f66);
40
+ }
41
+ `;
42
+
43
+ /**
44
+ * Shared helpers for the dual-prefix (`fsl-` canonical, `jssm-` synonym)
45
+ * web-component naming convention. Centralizes the "match either prefix"
46
+ * rule so it lives in exactly one place.
47
+ */
48
+ /**
49
+ * Returns true when `tag_name` is exactly `fsl-<suffix>` or `jssm-<suffix>`
50
+ * (case-insensitive).
51
+ *
52
+ * @param tag_name - The element tag name to test (e.g. `"FSL-VIZ"`, `"jssm-viz"`).
53
+ * @param suffix - The suffix to match after the prefix (e.g. `"viz"`).
54
+ * @returns `true` when `tag_name` is `fsl-<suffix>` or `jssm-<suffix>`.
55
+ *
56
+ * @example
57
+ * wc_suffix_matches('FSL-VIZ', 'viz'); // true
58
+ * wc_suffix_matches('jssm-viz', 'viz'); // true
59
+ * wc_suffix_matches('div', 'viz'); // false
60
+ * wc_suffix_matches('fsl-vizard', 'viz'); // false — suffix must match exactly
61
+ */
62
+ /**
63
+ * Returns the nearest ancestor of `el` (or `el` itself) whose tag is
64
+ * `fsl-<suffix>` or `jssm-<suffix>`, or `null` if none exists.
65
+ *
66
+ * @param el - The element to start the search from.
67
+ * @param suffix - The suffix to match (e.g. `"instance"`).
68
+ * @returns The closest matching ancestor element, or `null`.
69
+ *
70
+ * @example
71
+ * // <fsl-instance><div id="k"></div></fsl-instance>
72
+ * closest_wc(document.getElementById('k'), 'instance'); // <fsl-instance>
73
+ *
74
+ * @see wc_suffix_matches
75
+ */
76
+ function closest_wc(el, suffix) {
77
+ return el.closest(`fsl-${suffix}, jssm-${suffix}`);
78
+ }
79
+
80
+ var __decorate$7 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
81
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
82
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
83
+ 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;
84
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
85
+ };
86
+ /** Theme modes offered by the Theme pulldown. */
87
+ const THEME_MODES = [
88
+ { value: 'system', label: 'System' },
89
+ { value: 'light', label: 'Light' },
90
+ { value: 'dark', label: 'Dark' },
91
+ ];
92
+ /** Export formats offered by the Export pulldown. */
93
+ const EXPORT_FORMATS = [
94
+ { value: 'dot', label: 'Graphviz DOT' },
95
+ { value: 'json', label: 'JSON (serialized)' },
96
+ { value: 'fsl', label: 'FSL source' },
97
+ { value: 'svg', label: 'SVG' },
98
+ ];
99
+ /* Panel icons — Solar (CC BY 4.0) bold-duotone. Layout icons — hand-drawn
100
+ duotone split-rects. All use currentColor (+ baked opacity on the secondary
101
+ tone), so they theme with the button's text color and pressed state. Each is
102
+ a static html`` literal (Lit needs compile-time template strings). */
103
+ const ICON_VIZ = html `<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M2 12c0-4.714 0-7.071 1.464-8.536C4.93 2 7.286 2 12 2s7.071 0 8.535 1.464C22 4.93 22 7.286 22 12s0 7.071-1.465 8.535C19.072 22 16.714 22 12 22s-7.071 0-8.536-1.465C2 19.072 2 16.714 2 12" opacity=".5"/><path fill="currentColor" d="M17.576 10.48a.75.75 0 0 0-1.152-.96l-1.797 2.156c-.37.445-.599.716-.786.885a.8.8 0 0 1-.163.122l-.011.005l-.008-.004l-.003-.001a.8.8 0 0 1-.164-.122c-.187-.17-.415-.44-.786-.885l-.292-.35c-.328-.395-.625-.75-.901-1c-.301-.272-.68-.514-1.18-.514s-.878.242-1.18.514c-.276.25-.572.605-.9 1l-1.83 2.194a.75.75 0 0 0 1.153.96l1.797-2.156c.37-.445.599-.716.786-.885a.8.8 0 0 1 .163-.122l.007-.003l.004-.001q.004 0 .011.004a.8.8 0 0 1 .164.122c.187.17.415.44.786.885l.292.35c.329.395.625.75.901 1c.301.272.68.514 1.18.514s.878-.242 1.18-.514c.276-.25.572-.605.9-1z"/></svg>`;
104
+ const ICON_CODE = html `<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M8.502 5.387A.75.75 0 0 0 7.5 4.272L5.76 5.836c-.736.663-1.347 1.212-1.766 1.71c-.441.525-.755 1.088-.755 1.784c0 .695.314 1.258.755 1.782c.42.499 1.03 1.049 1.766 1.711l1.74 1.564a.75.75 0 1 0 1.003-1.115l-1.696-1.527c-.788-.709-1.32-1.19-1.663-1.598c-.33-.393-.403-.622-.403-.817c0-.196.072-.425.403-.818c.344-.409.875-.889 1.663-1.598zm6.941 5.111a.75.75 0 0 1 1.06-.055l1.737 1.563c.736.663 1.347 1.213 1.766 1.711c.441.524.755 1.088.755 1.783s-.314 1.259-.755 1.783c-.42.498-1.03 1.048-1.766 1.71l-1.738 1.565a.75.75 0 1 1-1.003-1.116l1.696-1.526c.788-.71 1.32-1.19 1.663-1.599c.33-.392.403-.622.403-.817s-.072-.425-.403-.817c-.344-.41-.875-.89-1.663-1.599L15.5 11.557a.75.75 0 0 1-.056-1.059"/><path fill="currentColor" d="M14.18 4.275a.75.75 0 0 1 .532.918l-3.987 15a.75.75 0 0 1-1.45-.386l3.987-15a.75.75 0 0 1 .918-.532" opacity=".5"/></svg>`;
105
+ const ICON_HISTORY = html `<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M5.079 5.069c3.795-3.79 9.965-3.75 13.783.069c3.82 3.82 3.86 9.993.064 13.788s-9.968 3.756-13.788-.064a9.81 9.81 0 0 1-2.798-8.28a.75.75 0 1 1 1.487.203a8.31 8.31 0 0 0 2.371 7.017c3.245 3.244 8.468 3.263 11.668.064c3.199-3.2 3.18-8.423-.064-11.668c-3.243-3.242-8.463-3.263-11.663-.068l.748.003a.75.75 0 1 1-.007 1.5l-2.546-.012a.75.75 0 0 1-.746-.747L3.575 4.33a.75.75 0 1 1 1.5-.008z" clip-rule="evenodd"/><path fill="currentColor" d="M12 7.25a.75.75 0 0 1 .75.75v3.69l2.28 2.28a.75.75 0 1 1-1.06 1.06l-2.427-2.426a1 1 0 0 1-.293-.708V8a.75.75 0 0 1 .75-.75" opacity=".5"/></svg>`;
106
+ const ICON_DATA = html `<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M12 10c4.418 0 8-1.79 8-4s-3.582-4-8-4s-8 1.79-8 4s3.582 4 8 4"/><path fill="currentColor" d="M4 12v6c0 2.21 3.582 4 8 4s8-1.79 8-4v-6c0 2.21-3.582 4-8 4s-8-1.79-8-4" opacity=".5"/><path fill="currentColor" d="M4 6v6c0 2.21 3.582 4 8 4s8-1.79 8-4V6c0 2.21-3.582 4-8 4S4 8.21 4 6" opacity=".7"/></svg>`;
107
+ const ICON_HOOKS = html `<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.732 5.771L5.67 9.914c-1.285 1.739-1.928 2.608-1.574 3.291l.018.034c.375.673 1.485.673 3.704.673c1.233 0 1.85 0 2.236.363l.02.02l3.872-4.57l-.02-.02c-.379-.371-.379-.963-.379-2.148v-.31c0-3.285 0-4.927-.923-5.21s-1.913 1.056-3.892 3.734" clip-rule="evenodd"/><path fill="currentColor" d="M10.453 16.443v.31c0 3.284 0 4.927.923 5.21s1.913-1.056 3.893-3.734l3.062-4.143c1.284-1.739 1.927-2.608 1.573-3.291l-.018-.034c-.375-.673-1.485-.673-3.704-.673c-1.233 0-1.85 0-2.236-.363l-3.872 4.57c.379.371.379.963.379 2.148" opacity=".5"/></svg>`;
108
+ const ICON_INFO = html `<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2s10 4.477 10 10" opacity=".5"/><path fill="currentColor" d="M12 17.75a.75.75 0 0 0 .75-.75v-6a.75.75 0 0 0-1.5 0v6c0 .414.336.75.75.75M12 7a1 1 0 1 1 0 2a1 1 0 0 1 0-2"/></svg>`;
109
+ const ICON_PROPS = html `<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M9.25 14a3 3 0 1 1 0 6a3 3 0 0 1 0-6m5-10a3 3 0 1 0 0 6a3 3 0 0 0 0-6"/><path fill="currentColor" d="M17.166 7.709a3 3 0 0 0-.021-1.5h4.605a.75.75 0 0 1 0 1.5zm-5.81-1.5a3 3 0 0 0-.022 1.5H1.75a.75.75 0 0 1 0-1.5zm-5 10H1.75a.75.75 0 0 0 0 1.5h4.584a3 3 0 0 1 .022-1.5m5.81 1.5h9.584a.75.75 0 0 0 0-1.5h-9.605a3 3 0 0 1 .02 1.5" opacity=".5"/></svg>`;
110
+ const ICON_SIM = html `<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M23 12c0-1.035-.53-2.07-1.591-2.647L8.597 2.385C6.534 1.264 4 2.724 4 5.033V12z" clip-rule="evenodd"/><path fill="currentColor" d="m8.597 21.615l12.812-6.968A2.99 2.99 0 0 0 23 12H4v6.967c0 2.31 2.534 3.769 4.597 2.648" opacity=".5"/></svg>`;
111
+ const ICON_EXPORT = html `<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M20.536 20.536C19.07 22 16.714 22 12 22s-7.071 0-8.536-1.465C2 19.072 2 16.714 2 12s0-7.071 1.464-8.536C4.93 2 7.286 2 12 2s7.071 0 8.535 1.464C22 4.93 22 7.286 22 12s0 7.071-1.465 8.535" opacity=".5"/><path fill="currentColor" d="M15.579 14.828a.75.75 0 0 1-.75.75h-4.243a.75.75 0 0 1 0-1.5h2.432L8.642 9.7a.75.75 0 0 1 1.06-1.06l4.377 4.376v-2.432a.75.75 0 0 1 1.5 0z"/></svg>`;
112
+ const ICON_LR = html `<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="8" height="14" rx="1.5" fill="currentColor"/><rect x="13" y="5" width="8" height="14" rx="1.5" fill="currentColor" opacity=".4"/></svg>`;
113
+ const ICON_RL = html `<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="8" height="14" rx="1.5" fill="currentColor" opacity=".4"/><rect x="13" y="5" width="8" height="14" rx="1.5" fill="currentColor"/></svg>`;
114
+ const ICON_TB = html `<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4" width="18" height="7" rx="1.5" fill="currentColor"/><rect x="3" y="13" width="18" height="7" rx="1.5" fill="currentColor" opacity=".4"/></svg>`;
115
+ const ICON_BT = html `<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4" width="18" height="7" rx="1.5" fill="currentColor" opacity=".4"/><rect x="3" y="13" width="18" height="7" rx="1.5" fill="currentColor"/></svg>`;
116
+ const ICON_M_EDITOR = html `<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="4" width="16" height="16" rx="2" fill="currentColor"/></svg>`;
117
+ const ICON_M_VIEWER = html `<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="4" width="16" height="16" rx="2" fill="currentColor" opacity=".4"/></svg>`;
118
+ const ICON_TABS = html `<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="8" width="18" height="12" rx="1.5" fill="currentColor" opacity=".4"/><rect x="3" y="4" width="8" height="3.6" rx="1" fill="currentColor"/></svg>`;
119
+ const ICON_AUTO = html `<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="4" width="16" height="16" rx="2" fill="currentColor" opacity=".4"/><path d="M5 19L19 5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" fill="none"/></svg>`;
120
+ /* Solar bolt-bold-duotone — the actions panel. */
121
+ const ICON_ACTIONS = html `<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.732 5.771L5.67 9.914c-1.285 1.739-1.928 2.608-1.574 3.291l.018.034c.375.673 1.485.673 3.704.673c1.233 0 1.85 0 2.236.363l.02.02l3.872-4.57l-.02-.02c-.379-.371-.379-.963-.379-2.148v-.31c0-3.285 0-4.927-.923-5.21s-1.913 1.056-3.892 3.734" clip-rule="evenodd"/><path fill="currentColor" d="M10.453 16.443v.31c0 3.284 0 4.927.923 5.21s1.913-1.056 3.893-3.734l3.062-4.143c1.284-1.739 1.927-2.608 1.573-3.291l-.018-.034c-.375-.673-1.485-.673-3.704-.673c-1.233 0-1.85 0-2.236-.363l-3.872 4.57c.379.371.379.963.379 2.148" opacity=".5"/></svg>`;
122
+ /* Solar palette-round-bold-duotone — the theme pulldown. */
123
+ const ICON_THEME = html `<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M7 18a1 1 0 1 1-2 0a1 1 0 0 1 2 0"/><path fill="currentColor" d="M10 6v12a4 4 0 0 1-8 0V6a4 4 0 1 1 8 0" opacity=".4"/><path fill="currentColor" d="m9.248 20.336l3.974-3.975l5.838-6.09a4.042 4.042 0 0 0-5.776-5.655L10 7.9V18c0 .872-.279 1.679-.752 2.336" opacity=".7"/><path fill="currentColor" d="m13.222 16.362l-3.974 3.974A4 4 0 0 1 6 22h11.9a4 4 0 1 0 0-8h-2.414z"/></svg>`;
124
+ const PANELS = [
125
+ { slot: 'actions', label: 'Actions', icon: ICON_ACTIONS },
126
+ { slot: 'viz', label: 'Renderer', icon: ICON_VIZ },
127
+ { slot: 'editor', label: 'Code', icon: ICON_CODE },
128
+ { slot: 'history', label: 'History', icon: ICON_HISTORY },
129
+ { slot: 'data-inspector', label: 'Data', icon: ICON_DATA },
130
+ { slot: 'hook-log', label: 'Events', icon: ICON_HOOKS },
131
+ { slot: 'info-panel', label: 'Info', icon: ICON_INFO },
132
+ { slot: 'effective-properties', label: 'Properties', icon: ICON_PROPS },
133
+ { slot: 'simulation', label: 'Simulation', icon: ICON_SIM },
134
+ ];
135
+ /** Layout menu entries (icon + label), in the sketch's order. */
136
+ const LAYOUTS = [
137
+ { value: 'auto', label: 'Auto · by aspect ratio', icon: ICON_AUTO },
138
+ { value: 'lr', label: 'Side by side · editor left', icon: ICON_LR },
139
+ { value: 'rl', label: 'Side by side · editor right', icon: ICON_RL },
140
+ { value: 'tb', label: 'Top & bottom · editor top', icon: ICON_TB },
141
+ { value: 'bt', label: 'Top & bottom · editor bottom', icon: ICON_BT },
142
+ { value: 'editor', label: 'Just editor', icon: ICON_M_EDITOR },
143
+ { value: 'viewer', label: 'Just viewer', icon: ICON_M_VIEWER },
144
+ { value: 'tabs', label: 'Tabbed', icon: ICON_TABS },
145
+ ];
146
+ /**
147
+ * `<fsl-toolbar>` — a control bar for a parent `<fsl-instance>`. A light/dark
148
+ * theme toggle on the left; on the right, an icon toggle to show/hide each
149
+ * panel present in the host (renderer, code, history, …) plus a View menu of
150
+ * the layout set (its button shows the current layout's icon). Standalone (no
151
+ * host) the panel toggles disappear. A trailing slot carries extra buttons.
152
+ *
153
+ * @element fsl-toolbar
154
+ * @csspart toolbar - The bar container.
155
+ * @slot - Trailing custom controls.
156
+ */
157
+ class FslToolbar extends LitElement {
158
+ constructor() {
159
+ super(...arguments);
160
+ this._openMenu = '';
161
+ /** Active export destination; the next format click targets it. */
162
+ this._dest = 'clipboard';
163
+ /**
164
+ * The last directory an export was saved to this session (its final path
165
+ * segment). When non-empty, the Export menu offers a `to <name>` destination.
166
+ * The embedder sets this after fulfilling a `pick` export.
167
+ */
168
+ this.lastDirectory = '';
169
+ this._host = null;
170
+ /** Panels actually present in the host — one toggle each. */
171
+ this._present = [];
172
+ }
173
+ connectedCallback() {
174
+ super.connectedCallback();
175
+ const host = closest_wc(this, 'instance');
176
+ this._host = host;
177
+ this._present = host === null ? [] : PANELS.filter(p => host.querySelector(`[slot="${p.slot}"]`) !== null);
178
+ }
179
+ /** Set the theme mode (System/Light/Dark). The host applies the palette + drives
180
+ * the editor; the menu stays open so a theme can be picked in the same trip. */
181
+ _setMode(mode) {
182
+ if (this._host !== null) {
183
+ this._host.theme = mode;
184
+ }
185
+ this.requestUpdate();
186
+ }
187
+ /** Select a named theme from the host's registry. The theme-name buttons only
188
+ * render when a host exists, so `_host` is non-null here. */
189
+ _setThemeName(name) {
190
+ this._host.themeName = name;
191
+ this.requestUpdate();
192
+ }
193
+ _setLayout(layout) {
194
+ if (this._host !== null) {
195
+ this._host.layout = layout;
196
+ }
197
+ this._openMenu = '';
198
+ }
199
+ /** Set the active export destination; the menu stays open so a format can be
200
+ * chosen next. */
201
+ _setDest(dest) { this._dest = dest; }
202
+ /** Emit `fsl-export` with the chosen format's content + the active destination.
203
+ * The embedder performs the actual clipboard / file save. */
204
+ async _export(format) {
205
+ const host = this._host;
206
+ const destination = this._dest;
207
+ this._openMenu = '';
208
+ if (host === null) {
209
+ return;
210
+ }
211
+ let content;
212
+ if (format === 'dot') {
213
+ content = machine_to_dot(host.machine);
214
+ }
215
+ else if (format === 'json') {
216
+ content = JSON.stringify(host.machine.serialize(), null, 2);
217
+ }
218
+ else if (format === 'svg') {
219
+ content = await machine_to_svg_string(host.machine);
220
+ }
221
+ else {
222
+ content = host.fsl;
223
+ }
224
+ this.dispatchEvent(new CustomEvent('fsl-export', { detail: { format, content, destination }, bubbles: true, composed: true }));
225
+ }
226
+ _toggleMenu(which) { this._openMenu = this._openMenu === which ? '' : which; }
227
+ render() {
228
+ var _a, _b, _c, _d, _e;
229
+ const host = this._host;
230
+ const mode = (_a = host === null || host === void 0 ? void 0 : host.theme) !== null && _a !== void 0 ? _a : 'light';
231
+ const themeName = (_b = host === null || host === void 0 ? void 0 : host.themeName) !== null && _b !== void 0 ? _b : 'Default';
232
+ const themeNames = host ? Object.keys(host.themes) : [];
233
+ const layout = (_c = host === null || host === void 0 ? void 0 : host.layout) !== null && _c !== void 0 ? _c : '';
234
+ const layoutIcon = (_e = (_d = LAYOUTS.find(l => l.value === layout)) === null || _d === void 0 ? void 0 : _d.icon) !== null && _e !== void 0 ? _e : ICON_LR;
235
+ return html `
236
+ <div class="toolbar" part="toolbar" role="toolbar" aria-label="Workbench controls">
237
+ <span class="spacer"></span>
238
+ <div class="grp">
239
+ ${host
240
+ ? this._present.map(p => html `
241
+ <button class="tb icon" aria-pressed=${!host.isPanelHidden(p.slot)} aria-label=${p.label} title=${p.label}
242
+ @click=${() => { host.togglePanel(p.slot); this.requestUpdate(); }}>${p.icon}</button>`)
243
+ : ''}
244
+ </div>
245
+ <div class="grp">
246
+ <button class="tb layout" aria-haspopup="true" aria-expanded=${this._openMenu === 'layout'} aria-label="Layout" title="Layout" @click=${() => this._toggleMenu('layout')}>${layoutIcon}<span class="caret">▾</span></button>
247
+ ${this._openMenu === 'layout' ? html `
248
+ <div class="menu" role="menu">
249
+ ${LAYOUTS.map(o => html `
250
+ <button role="menuitemradio" aria-checked=${layout === o.value} @click=${() => this._setLayout(o.value)}>${o.icon}${o.label}</button>`)}
251
+ </div>` : ''}
252
+ </div>
253
+ <div class="grp">
254
+ <button class="tb icon" aria-haspopup="true" aria-expanded=${this._openMenu === 'export'} aria-label="Export" title="Export" @click=${() => this._toggleMenu('export')}>${ICON_EXPORT}</button>
255
+ ${this._openMenu === 'export' ? html `
256
+ <div class="menu" role="menu">
257
+ <button role="menuitemradio" aria-checked=${this._dest === 'clipboard'} @click=${() => this._setDest('clipboard')}>to clipboard</button>
258
+ ${this.lastDirectory ? html `
259
+ <button role="menuitemradio" aria-checked=${this._dest === 'lastdir'} @click=${() => this._setDest('lastdir')}>to ${this.lastDirectory}</button>` : ''}
260
+ <button role="menuitemradio" aria-checked=${this._dest === 'pick'} @click=${() => this._setDest('pick')}>choose directory…</button>
261
+ <div class="divider" role="separator"></div>
262
+ ${EXPORT_FORMATS.map(f => html `
263
+ <button role="menuitem" @click=${() => this._export(f.value)}>${f.label}</button>`)}
264
+ </div>` : ''}
265
+ </div>
266
+ <div class="grp">
267
+ <button class="tb icon" aria-haspopup="true" aria-expanded=${this._openMenu === 'theme'} aria-label="Theme" title="Theme" @click=${() => this._toggleMenu('theme')}>${ICON_THEME}</button>
268
+ ${this._openMenu === 'theme' ? html `
269
+ <div class="menu" role="menu">
270
+ ${THEME_MODES.map(m => html `
271
+ <button role="menuitemradio" aria-checked=${mode === m.value} @click=${() => this._setMode(m.value)}>${m.label}</button>`)}
272
+ <div class="divider" role="separator"></div>
273
+ ${themeNames.map(n => html `
274
+ <button role="menuitemradio" aria-checked=${themeName === n} @click=${() => this._setThemeName(n)}>${n}</button>`)}
275
+ </div>` : ''}
276
+ </div>
277
+ <slot></slot>
278
+ </div>`;
279
+ }
280
+ }
281
+ FslToolbar.styles = css `
282
+ :host { display: block; }
283
+ .toolbar {
284
+ display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
285
+ padding: 0.4rem 0.5rem; background: var(--_fsl-surface);
286
+ border-bottom: 1px solid var(--_fsl-border); font: 0.8rem var(--_fsl-font);
287
+ }
288
+ .grp { display: inline-flex; position: relative; }
289
+ .grp .tb { margin-left: -1px; } .grp .tb:first-child { margin-left: 0; }
290
+ .tb {
291
+ min-width: 2rem; height: 1.9rem; padding: 0 0.5rem; cursor: pointer;
292
+ display: inline-flex; align-items: center; justify-content: center; gap: 0.3rem;
293
+ border: 1px solid var(--_fsl-border); background: var(--_fsl-surface); color: var(--_fsl-text);
294
+ font: 600 0.8rem var(--_fsl-font);
295
+ }
296
+ .tb:hover { background: color-mix(in srgb, var(--_fsl-text) 8%, var(--_fsl-surface)); }
297
+ .tb[aria-pressed="true"], .tb[aria-expanded="true"] {
298
+ /* lift the active button above its -1px-overlapped neighbours so its full
299
+ accent border paints on top (its right edge isn't covered by the next,
300
+ unpressed, button's left border). */
301
+ position: relative; z-index: 1;
302
+ background: color-mix(in srgb, var(--_fsl-accent) 20%, var(--_fsl-surface));
303
+ color: var(--_fsl-text); /* keep the symbol readable; the tint + border mark "selected" */
304
+ border-color: color-mix(in srgb, var(--_fsl-accent) 55%, var(--_fsl-border));
305
+ }
306
+ .tb.icon { width: 1.9rem; min-width: 1.9rem; padding: 0; }
307
+ .tb.layout { padding: 0 0.35rem; gap: 0.12rem; }
308
+ .tb .ico { width: 1.15rem; height: 1.15rem; display: block; }
309
+ .tb.layout .ico { width: 1.1rem; height: 1.1rem; }
310
+ .caret { font-size: 0.6rem; line-height: 1; color: var(--_fsl-muted); }
311
+ .tb[aria-expanded="true"] .caret { color: inherit; }
312
+ .menu {
313
+ position: absolute; top: calc(100% + 4px); right: 0; z-index: 20; min-width: 220px;
314
+ background: var(--_fsl-surface); border: 1px solid var(--_fsl-border); border-radius: 6px;
315
+ padding: 5px; box-shadow: 0 10px 30px rgba(0,0,0,0.35);
316
+ }
317
+ .menu button {
318
+ display: flex; align-items: center; width: 100%; box-sizing: border-box;
319
+ background: none; border: 0; border-radius: 4px; padding: 6px 9px; gap: 8px;
320
+ color: var(--_fsl-text); cursor: pointer; text-align: left; font: 0.82rem var(--_fsl-font);
321
+ }
322
+ .menu button:hover { background: color-mix(in srgb, var(--_fsl-text) 12%, transparent); }
323
+ .menu button[aria-checked="true"] { font-weight: 700; }
324
+ .menu button[aria-checked="true"]::after { content: "✓"; margin-left: auto; color: var(--_fsl-accent); }
325
+ .menu .ico { width: 1.05rem; height: 1.05rem; display: block; flex: 0 0 auto; }
326
+ .menu .divider { height: 1px; background: var(--_fsl-border); margin: 5px 4px; }
327
+ .spacer { flex: 1; }
328
+ ${fslTokens}
329
+ `;
330
+ __decorate$7([
331
+ state()
332
+ ], FslToolbar.prototype, "_openMenu", void 0);
333
+ __decorate$7([
334
+ state()
335
+ ], FslToolbar.prototype, "_dest", void 0);
336
+ __decorate$7([
337
+ property({ attribute: false })
338
+ ], FslToolbar.prototype, "lastDirectory", void 0);
339
+
340
+ var __decorate$6 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
341
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
342
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
343
+ 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;
344
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
345
+ };
346
+ /** Host events that can change which actions / transitions are currently legal. */
347
+ const ACTION_EVENTS = ['fsl-transition', 'fsl-machine-rebuilt'];
348
+ /**
349
+ * `<fsl-actions>` — interactive controls derived from a parent `<fsl-instance>`'s
350
+ * machine. Renders a button for every currently-legal named **action** and, split
351
+ * into three groups, every reachable transition target by edge kind: **main**
352
+ * (`=>`) first, plain **transitions** (`->`) next, and **forced** (`~>`) last.
353
+ * Legal targets fire with `transition`, forced-only ones with `force_transition`.
354
+ * Re-derives on the host's transition / rebuild events; only firable controls
355
+ * appear, and each group is omitted when empty, so a self-loop-only state shows
356
+ * just its actions and a terminal state shows `no actions available`. Standalone
357
+ * (no host ancestor) renders empty.
358
+ *
359
+ * @element fsl-actions
360
+ * @csspart actions - The container.
361
+ *
362
+ * @example
363
+ * // For `A 'x' -> B; A 'y' => C; A ~> D;` while in A:
364
+ * // Actions: [ x ] [ y ]
365
+ * // Main: [ → C ]
366
+ * // Transitions: [ → B ]
367
+ * // Forced: [ → D ]
368
+ */
369
+ class FslActions extends LitElement {
370
+ constructor() {
371
+ super(...arguments);
372
+ this._actions = [];
373
+ this._main = [];
374
+ this._regular = [];
375
+ this._forced = [];
376
+ this._host = null;
377
+ this._unbind = null;
378
+ }
379
+ connectedCallback() {
380
+ super.connectedCallback();
381
+ const host = closest_wc(this, 'instance');
382
+ this._host = host;
383
+ if (host === null) {
384
+ return;
385
+ }
386
+ const sync = () => { this._derive(host); };
387
+ for (const ev of ACTION_EVENTS) {
388
+ host.addEventListener(ev, sync);
389
+ }
390
+ this._unbind = () => { for (const ev of ACTION_EVENTS) {
391
+ host.removeEventListener(ev, sync);
392
+ } };
393
+ this._derive(host);
394
+ }
395
+ disconnectedCallback() {
396
+ super.disconnectedCallback();
397
+ if (this._unbind !== null) {
398
+ this._unbind();
399
+ this._unbind = null;
400
+ }
401
+ }
402
+ /** Recompute the legal actions and the three transition groups from the host. */
403
+ _derive(host) {
404
+ const current = host.machine.state();
405
+ this._actions = host.machine.list_exit_actions();
406
+ const main = new Set(), regular = new Set(), forced = new Set();
407
+ for (const e of host.machine.list_edges()) {
408
+ if (e.from !== current || e.to === current) {
409
+ continue;
410
+ } // only non-self exits from here
411
+ const to = String(e.to);
412
+ if (e.main_path) {
413
+ main.add(to);
414
+ }
415
+ else if (e.forced_only) {
416
+ forced.add(to);
417
+ }
418
+ else {
419
+ regular.add(to);
420
+ }
421
+ }
422
+ this._main = [...main];
423
+ this._regular = [...regular];
424
+ this._forced = [...forced];
425
+ }
426
+ /** A transition group, or `''` when empty. Forced targets fire via force. */
427
+ _group(host, label, targets, forced) {
428
+ if (targets.length === 0) {
429
+ return '';
430
+ }
431
+ return html `
432
+ <div class="group">
433
+ <div class="label">${label}</div>
434
+ ${targets.map(to => html `
435
+ <button class="trn" @click=${() => { if (forced) {
436
+ host.force_transition(to);
437
+ }
438
+ else {
439
+ host.transition(to);
440
+ } }}>→ ${to}</button>`)}
441
+ </div>`;
442
+ }
443
+ render() {
444
+ const host = this._host;
445
+ if (host === null) {
446
+ return html `<div class="actions" part="actions"><span class="empty">no machine</span></div>`;
447
+ }
448
+ if (this._actions.length === 0 && this._main.length === 0
449
+ && this._regular.length === 0 && this._forced.length === 0) {
450
+ return html `<div class="actions" part="actions"><span class="empty">no actions available</span></div>`;
451
+ }
452
+ return html `
453
+ <div class="actions" part="actions">
454
+ ${this._actions.length === 0 ? '' : html `
455
+ <div class="group">
456
+ <div class="label">Actions</div>
457
+ ${this._actions.map(a => html `
458
+ <button class="act" @click=${() => host.do(a)}>${a}</button>`)}
459
+ </div>`}
460
+ ${this._group(host, 'Main', this._main, false)}
461
+ ${this._group(host, 'Transitions', this._regular, false)}
462
+ ${this._group(host, 'Forced', this._forced, true)}
463
+ </div>`;
464
+ }
465
+ }
466
+ FslActions.styles = css `
467
+ :host { display: block; }
468
+ .actions {
469
+ display: flex; flex-direction: column; gap: 0.7rem;
470
+ padding: 0.55rem 0.65rem; background: var(--_fsl-surface);
471
+ color: var(--_fsl-text); font: 0.8rem var(--_fsl-font);
472
+ }
473
+ .group { display: flex; flex-direction: column; gap: 0.3rem; }
474
+ .label {
475
+ font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.05em;
476
+ color: var(--_fsl-muted);
477
+ }
478
+ button {
479
+ text-align: left; padding: 0.4rem 0.6rem; cursor: pointer; border-radius: 4px;
480
+ border: 1px solid var(--_fsl-border); background: var(--_fsl-surface);
481
+ color: var(--_fsl-text); font: 600 0.8rem var(--_fsl-font);
482
+ }
483
+ button:hover { background: color-mix(in srgb, var(--_fsl-text) 8%, var(--_fsl-surface)); }
484
+ .empty { color: var(--_fsl-muted); font-style: italic; }
485
+ ${fslTokens}
486
+ `;
487
+ __decorate$6([
488
+ state()
489
+ ], FslActions.prototype, "_actions", void 0);
490
+ __decorate$6([
491
+ state()
492
+ ], FslActions.prototype, "_main", void 0);
493
+ __decorate$6([
494
+ state()
495
+ ], FslActions.prototype, "_regular", void 0);
496
+ __decorate$6([
497
+ state()
498
+ ], FslActions.prototype, "_forced", void 0);
499
+
500
+ var __decorate$5 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
501
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
502
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
503
+ 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;
504
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
505
+ };
506
+ /**
507
+ * `<fsl-footer>` — a status bar for a parent `<fsl-instance>`.
508
+ *
509
+ * Reflects the host's current state, legal-action count, and
510
+ * terminal/complete status by observing the host's reflected attributes
511
+ * (`current-state`, `legal-actions`, `terminal`, `complete`) — so it tracks
512
+ * transitions *and* survives a live machine rebuild (#1387) without a machine
513
+ * subscription. A default slot carries embedder status (line/column, parse
514
+ * state, …). Standalone (no `<fsl-instance>` ancestor) it renders just the slot.
515
+ *
516
+ * @element fsl-footer
517
+ * @csspart bar - The footer bar container.
518
+ * @slot - Trailing custom status, right-aligned.
519
+ */
520
+ class FslFooter extends LitElement {
521
+ constructor() {
522
+ super(...arguments);
523
+ this._state = '';
524
+ this._actions = 0;
525
+ this._terminal = false;
526
+ this._complete = false;
527
+ this._observer = null;
528
+ }
529
+ connectedCallback() {
530
+ super.connectedCallback();
531
+ const host = closest_wc(this, 'instance');
532
+ if (host === null) {
533
+ return;
534
+ }
535
+ const sync = () => {
536
+ var _a, _b;
537
+ this._state = (_a = host.getAttribute('current-state')) !== null && _a !== void 0 ? _a : '';
538
+ const la = ((_b = host.getAttribute('legal-actions')) !== null && _b !== void 0 ? _b : '').trim();
539
+ this._actions = la === '' ? 0 : la.split(/\s+/).length;
540
+ this._terminal = host.hasAttribute('terminal');
541
+ this._complete = host.hasAttribute('complete');
542
+ };
543
+ this._observer = new MutationObserver(sync);
544
+ this._observer.observe(host, {
545
+ attributes: true,
546
+ attributeFilter: ['current-state', 'legal-actions', 'terminal', 'complete'],
547
+ });
548
+ sync();
549
+ }
550
+ disconnectedCallback() {
551
+ super.disconnectedCallback();
552
+ if (this._observer !== null) {
553
+ this._observer.disconnect();
554
+ this._observer = null;
555
+ }
556
+ }
557
+ render() {
558
+ return html `
559
+ <div class="bar" part="bar">
560
+ ${this._state ? html `<span class="state">${this._state}</span>` : ''}
561
+ <span>${this._actions} action${this._actions === 1 ? '' : 's'}</span>
562
+ ${this._terminal ? html `<span class="badge">terminal</span>` : ''}
563
+ ${this._complete ? html `<span class="badge">complete</span>` : ''}
564
+ <span class="spacer"></span>
565
+ <slot></slot>
566
+ </div>`;
567
+ }
568
+ }
569
+ FslFooter.styles = css `
570
+ :host { display: block; }
571
+ .bar {
572
+ display: flex; align-items: center; gap: 0.6rem;
573
+ font: 12px var(--_fsl-font-mono); color: var(--_fsl-muted);
574
+ padding: 0.3rem 0.7rem; background: var(--_fsl-surface);
575
+ border-top: 1px solid var(--_fsl-border);
576
+ }
577
+ .state { color: var(--_fsl-text); font-weight: 600; }
578
+ .badge { padding: 0 0.4rem; border-radius: 3px; background: var(--_fsl-accent); color: #06101f; font-weight: 600; }
579
+ .spacer { flex: 1; }
580
+ ${fslTokens}
581
+ `;
582
+ __decorate$5([
583
+ state()
584
+ ], FslFooter.prototype, "_state", void 0);
585
+ __decorate$5([
586
+ state()
587
+ ], FslFooter.prototype, "_actions", void 0);
588
+ __decorate$5([
589
+ state()
590
+ ], FslFooter.prototype, "_terminal", void 0);
591
+ __decorate$5([
592
+ state()
593
+ ], FslFooter.prototype, "_complete", void 0);
594
+
595
+ var __decorate$4 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
596
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
597
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
598
+ 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;
599
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
600
+ };
601
+ /**
602
+ * `<fsl-help>` — a documentation drawer shell: a titled, scrollable panel with
603
+ * a close button and a default slot for content (typically foldable
604
+ * `<details>` sections). Presentational and self-contained — it owns no machine
605
+ * binding. The reflected `open` attribute drives visibility, so embedders can
606
+ * animate it (e.g. a width transition on the host) purely from CSS.
607
+ *
608
+ * @element fsl-help
609
+ * @csspart drawer - The drawer container.
610
+ * @csspart head - The header bar.
611
+ * @csspart body - The scrollable content area.
612
+ * @csspart close - The close button.
613
+ * @slot - The documentation content.
614
+ * @fires {CustomEvent<void>} close - When the close button is pressed.
615
+ */
616
+ class FslHelp extends LitElement {
617
+ constructor() {
618
+ super(...arguments);
619
+ /** Whether the drawer is shown. Reflected so embedders can animate off it. */
620
+ this.open = false;
621
+ /** Heading shown in the drawer's header. */
622
+ this.heading = 'Documentation';
623
+ /** Close the drawer and emit `close`. */
624
+ this._onClose = () => {
625
+ this.open = false;
626
+ this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }));
627
+ };
628
+ }
629
+ render() {
630
+ return html `
631
+ <aside class="drawer" part="drawer" ?hidden=${!this.open}>
632
+ <div class="head" part="head">
633
+ <h2>${this.heading}</h2>
634
+ <button class="close" part="close" @click=${this._onClose} aria-label="Close documentation" title="Close">&times;</button>
635
+ </div>
636
+ <div class="body" part="body"><slot></slot></div>
637
+ </aside>`;
638
+ }
639
+ }
640
+ FslHelp.styles = css `
641
+ :host { display: block; }
642
+ :host([open]) { /* embedders animate the host (e.g. width) off this attribute */ }
643
+ .drawer {
644
+ display: flex; flex-direction: column; height: 100%; overflow: hidden;
645
+ background: var(--_fsl-surface); color: var(--_fsl-text);
646
+ border-left: 1px solid var(--_fsl-border);
647
+ }
648
+ .head {
649
+ display: flex; align-items: center; gap: 0.5rem;
650
+ padding: 0.7rem 0.9rem; border-bottom: 1px solid var(--_fsl-border);
651
+ }
652
+ .head h2 {
653
+ margin: 0; font-size: 0.7rem; text-transform: uppercase;
654
+ letter-spacing: 0.08em; color: var(--_fsl-muted);
655
+ }
656
+ .close {
657
+ margin-left: auto; width: 1.6rem; height: 1.6rem; display: grid; place-items: center;
658
+ background: none; border: 0; border-radius: 4px; color: inherit; cursor: pointer; font-size: 1.1rem; line-height: 1;
659
+ }
660
+ .close:hover { background: rgba(127, 127, 127, 0.18); }
661
+ .body { overflow-y: auto; padding: 0.5rem 0.9rem; font-size: 0.86rem; line-height: 1.5; }
662
+ ${fslTokens}
663
+ `;
664
+ __decorate$4([
665
+ property({ type: Boolean, reflect: true })
666
+ ], FslHelp.prototype, "open", void 0);
667
+ __decorate$4([
668
+ property({ type: String })
669
+ ], FslHelp.prototype, "heading", void 0);
670
+
671
+ var __decorate$3 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
672
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
673
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
674
+ 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;
675
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
676
+ };
677
+ /**
678
+ * `<fsl-history>` — the visited-state timeline for a parent `<fsl-instance>`.
679
+ *
680
+ * Listens to the host's `fsl-transition` DOM events (re-emitted once per
681
+ * transition, #639) and records the host's reflected `current-state`, so it
682
+ * captures every transition and survives a live machine rebuild without a
683
+ * machine subscription. Standalone (no host ancestor) renders empty.
684
+ *
685
+ * @element fsl-history
686
+ * @csspart timeline - The timeline container.
687
+ */
688
+ class FslHistory extends LitElement {
689
+ constructor() {
690
+ super(...arguments);
691
+ this._history = [];
692
+ this._unbind = null;
693
+ }
694
+ connectedCallback() {
695
+ super.connectedCallback();
696
+ const host = closest_wc(this, 'instance');
697
+ if (host === null) {
698
+ return;
699
+ }
700
+ const push = () => {
701
+ const s = host.getAttribute('current-state');
702
+ if (s !== null && s !== this._history[this._history.length - 1]) {
703
+ this._history = [...this._history, s];
704
+ }
705
+ };
706
+ host.addEventListener('fsl-transition', push);
707
+ this._unbind = () => { host.removeEventListener('fsl-transition', push); };
708
+ push(); // seed with the initial state
709
+ }
710
+ disconnectedCallback() {
711
+ super.disconnectedCallback();
712
+ if (this._unbind !== null) {
713
+ this._unbind();
714
+ this._unbind = null;
715
+ }
716
+ }
717
+ render() {
718
+ if (this._history.length === 0) {
719
+ return html `<div class="timeline" part="timeline"><span class="empty">no history</span></div>`;
720
+ }
721
+ const last = this._history.length - 1;
722
+ return html `
723
+ <div class="timeline" part="timeline">
724
+ ${this._history.map((s, i) => html `${i > 0 ? html `<span class="arrow">→</span>` : ''}<span class="state ${i === last ? 'current' : ''}">${s}</span>`)}
725
+ </div>`;
726
+ }
727
+ }
728
+ FslHistory.styles = css `
729
+ :host { display: block; }
730
+ .timeline {
731
+ display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem;
732
+ padding: 0.5rem 0.7rem; font: 0.82rem var(--_fsl-font);
733
+ color: var(--_fsl-text); background: var(--_fsl-surface);
734
+ }
735
+ .state {
736
+ padding: 0.1rem 0.45rem; border-radius: 4px; border: 1px solid var(--_fsl-border);
737
+ background: color-mix(in srgb, var(--_fsl-accent) 14%, transparent);
738
+ }
739
+ .state.current { background: var(--_fsl-accent); color: #06101f; font-weight: 600; }
740
+ .arrow { color: var(--_fsl-muted); }
741
+ .empty { color: var(--_fsl-muted); font-style: italic; padding: 0.5rem 0.7rem; }
742
+ ${fslTokens}
743
+ `;
744
+ __decorate$3([
745
+ state()
746
+ ], FslHistory.prototype, "_history", void 0);
747
+
748
+ var __decorate$2 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
749
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
750
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
751
+ 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;
752
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
753
+ };
754
+ /** Host DOM events that can change the machine's data. */
755
+ const DATA_EVENTS = ['fsl-transition', 'fsl-data-change', 'fsl-machine-rebuilt'];
756
+ const JSON_TOKEN_RE = /"(?:\\.|[^"\\])*"|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g;
757
+ /**
758
+ * Split a pretty-printed JSON string into classified tokens for syntax
759
+ * highlighting. A quoted run is a `key` when the next non-space character is
760
+ * `:`, otherwise a `string`; `true`/`false` are `bool`, `null` is `null`,
761
+ * numbers are `number`, and everything between (braces, commas, whitespace) is
762
+ * `plain`. Driven by the text, not a JSON parse, so it never throws.
763
+ *
764
+ * @param json - A JSON string (typically `JSON.stringify(value, null, 2)`).
765
+ * @returns The tokens in source order; concatenating their `text` reproduces `json`.
766
+ *
767
+ * @example
768
+ * tokenizeJson('{"a": 1}');
769
+ * // [{text:'{',kind:'plain'}, {text:'"a"',kind:'key'}, {text:': ',kind:'plain'},
770
+ * // {text:'1',kind:'number'}, {text:'}',kind:'plain'}]
771
+ */
772
+ function tokenizeJson(json) {
773
+ const out = [];
774
+ let last = 0;
775
+ let m;
776
+ JSON_TOKEN_RE.lastIndex = 0;
777
+ while ((m = JSON_TOKEN_RE.exec(json)) !== null) {
778
+ if (m.index > last) {
779
+ out.push({ text: json.slice(last, m.index), kind: 'plain' });
780
+ }
781
+ const s = m[0];
782
+ let kind;
783
+ if (s[0] === '"') {
784
+ kind = /^\s*:/.test(json.slice(m.index + s.length)) ? 'key' : 'string';
785
+ }
786
+ else if (s === 'true' || s === 'false') {
787
+ kind = 'bool';
788
+ }
789
+ else if (s === 'null') {
790
+ kind = 'null';
791
+ }
792
+ else {
793
+ kind = 'number';
794
+ }
795
+ out.push({ text: s, kind });
796
+ last = m.index + s.length;
797
+ }
798
+ if (last < json.length) {
799
+ out.push({ text: json.slice(last), kind: 'plain' });
800
+ }
801
+ return out;
802
+ }
803
+ /**
804
+ * `<fsl-data-inspector>` — a syntax-highlighted view of a parent
805
+ * `<fsl-instance>`'s extended-state data. Re-reads `host.machine.data()` on the
806
+ * host's transition / data-change / rebuild DOM events. The panel is bounded and
807
+ * scrolls internally (a self-contained vertical column). Renders `no data` when
808
+ * the machine carries none; standalone (no host) renders empty.
809
+ *
810
+ * @element fsl-data-inspector
811
+ * @csspart inspector - The scrollable container.
812
+ */
813
+ class FslDataInspector extends LitElement {
814
+ constructor() {
815
+ super(...arguments);
816
+ this._data = undefined;
817
+ this._unbind = null;
818
+ }
819
+ connectedCallback() {
820
+ super.connectedCallback();
821
+ const host = closest_wc(this, 'instance');
822
+ if (host === null) {
823
+ return;
824
+ }
825
+ const sync = () => { this._data = host.machine.data(); };
826
+ for (const ev of DATA_EVENTS) {
827
+ host.addEventListener(ev, sync);
828
+ }
829
+ this._unbind = () => { for (const ev of DATA_EVENTS) {
830
+ host.removeEventListener(ev, sync);
831
+ } };
832
+ sync();
833
+ }
834
+ disconnectedCallback() {
835
+ super.disconnectedCallback();
836
+ if (this._unbind !== null) {
837
+ this._unbind();
838
+ this._unbind = null;
839
+ }
840
+ }
841
+ render() {
842
+ return html `
843
+ <div class="inspector" part="inspector">
844
+ ${this._data === undefined
845
+ ? html `<span class="empty">no data</span>`
846
+ : html `<pre class="json">${tokenizeJson(JSON.stringify(this._data, null, 2)).map(t => t.kind === 'plain' ? t.text : html `<span class="tok-${t.kind}">${t.text}</span>`)}</pre>`}
847
+ </div>`;
848
+ }
849
+ }
850
+ FslDataInspector.styles = css `
851
+ :host { display: block; }
852
+ .inspector {
853
+ padding: 0.5rem 0.7rem; font: 0.8rem var(--_fsl-font-mono);
854
+ color: var(--_fsl-text); background: var(--_fsl-surface);
855
+ /* Bounded + scrollable so a large data tree stays a self-contained panel. */
856
+ max-height: var(--fsl-data-inspector-max-height, 16em); overflow: auto;
857
+ }
858
+ .json { margin: 0; white-space: pre-wrap; }
859
+ .empty { color: var(--_fsl-muted); font-style: italic; }
860
+ /* JSON syntax colors — token-overridable, with light defaults; the host's
861
+ theme cascades dark values through --fsl-color-json-*. */
862
+ .tok-key { color: var(--fsl-color-json-key, #5b3da8); }
863
+ .tok-string { color: var(--fsl-color-json-string, #2e7d32); }
864
+ .tok-number { color: var(--fsl-color-json-number, #b8860b); }
865
+ .tok-bool, .tok-null { color: var(--fsl-color-json-atom, #c2185b); }
866
+ ${fslTokens}
867
+ `;
868
+ __decorate$2([
869
+ state()
870
+ ], FslDataInspector.prototype, "_data", void 0);
871
+
872
+ var __decorate$1 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
873
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
874
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
875
+ 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;
876
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
877
+ };
878
+ /** Machine events (re-emitted by the host as `fsl-<name>`, #639) the log shows. */
879
+ const LOGGED_EVENTS = [
880
+ 'transition', 'entry', 'exit', 'terminal', 'complete',
881
+ 'action', 'rejection', 'override', 'data-change', 'timeout', 'error',
882
+ ];
883
+ /**
884
+ * `<fsl-hook-log>` — a running log of a parent `<fsl-instance>`'s machine
885
+ * events, listening to the host's re-emitted `fsl-*` DOM events (#639). Keeps
886
+ * the most recent {@link MAX_ENTRIES}. Standalone (no host ancestor) is empty.
887
+ *
888
+ * @element fsl-hook-log
889
+ * @csspart log - The log container.
890
+ */
891
+ class FslHookLog extends LitElement {
892
+ constructor() {
893
+ super(...arguments);
894
+ this._log = [];
895
+ this._unbind = null;
896
+ }
897
+ connectedCallback() {
898
+ super.connectedCallback();
899
+ const host = closest_wc(this, 'instance');
900
+ if (host === null) {
901
+ return;
902
+ }
903
+ const offs = LOGGED_EVENTS.map(name => {
904
+ const handler = () => { this._log = [...this._log.slice(-49), name]; };
905
+ host.addEventListener(`fsl-${name}`, handler);
906
+ return () => { host.removeEventListener(`fsl-${name}`, handler); };
907
+ });
908
+ this._unbind = () => { for (const off of offs) {
909
+ off();
910
+ } };
911
+ }
912
+ disconnectedCallback() {
913
+ super.disconnectedCallback();
914
+ if (this._unbind !== null) {
915
+ this._unbind();
916
+ this._unbind = null;
917
+ }
918
+ }
919
+ render() {
920
+ return html `
921
+ <div class="log" part="log">
922
+ ${this._log.length === 0
923
+ ? html `<span class="empty">no events</span>`
924
+ : this._log.map(name => html `<div class="entry">${name}</div>`)}
925
+ </div>`;
926
+ }
927
+ }
928
+ FslHookLog.styles = css `
929
+ :host { display: block; }
930
+ .log {
931
+ padding: 0.4rem 0.6rem; font: 0.78rem var(--_fsl-font-mono);
932
+ color: var(--_fsl-text); background: var(--_fsl-surface);
933
+ /* Bounded + scrollable so the running log stays a self-contained panel
934
+ instead of growing without limit and crowding sibling panels out. */
935
+ max-height: var(--fsl-hook-log-max-height, 12em); overflow-y: auto;
936
+ }
937
+ .entry { padding: 0.05rem 0; color: var(--_fsl-text); }
938
+ .entry::before { content: "▸ "; color: var(--_fsl-accent); }
939
+ .empty { color: var(--_fsl-muted); font-style: italic; }
940
+ ${fslTokens}
941
+ `;
942
+ __decorate$1([
943
+ state()
944
+ ], FslHookLog.prototype, "_log", void 0);
945
+
946
+ var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
947
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
948
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
949
+ 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;
950
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
951
+ };
952
+ /**
953
+ * `<fsl-simulation>` — a random-walk driver for a parent `<fsl-instance>`.
954
+ *
955
+ * `Step` fires one uniformly-random legal action; `Play` auto-steps every
956
+ * {@link FslSimulation.interval} ms and stops automatically when the machine
957
+ * reaches a terminal state (no legal actions). Standalone (no host ancestor)
958
+ * the controls are disabled.
959
+ *
960
+ * @element fsl-simulation
961
+ * @csspart sim - The control row.
962
+ * @attr {number} interval - Auto-step period in milliseconds (default 600).
963
+ */
964
+ class FslSimulation extends LitElement {
965
+ constructor() {
966
+ super(...arguments);
967
+ /** Auto-step period in milliseconds. */
968
+ this.interval = 600;
969
+ this._running = false;
970
+ this._steps = 0;
971
+ this._host = null;
972
+ this._timer = null;
973
+ this._step = () => {
974
+ if (this._host === null) {
975
+ return;
976
+ }
977
+ const actions = this._host.machine.list_exit_actions();
978
+ if (actions.length === 0) {
979
+ this._stop();
980
+ return;
981
+ } // terminal — nothing to do
982
+ this._host.do(actions[Math.floor(Math.random() * actions.length)]);
983
+ this._steps += 1;
984
+ };
985
+ this._toggle = () => { if (this._running) {
986
+ this._stop();
987
+ }
988
+ else {
989
+ this._start();
990
+ } };
991
+ }
992
+ connectedCallback() {
993
+ super.connectedCallback();
994
+ this._host = closest_wc(this, 'instance');
995
+ }
996
+ disconnectedCallback() {
997
+ super.disconnectedCallback();
998
+ this._stop();
999
+ }
1000
+ _start() {
1001
+ if (this._host === null) {
1002
+ return;
1003
+ }
1004
+ this._running = true;
1005
+ this._timer = setInterval(this._step, this.interval);
1006
+ }
1007
+ _stop() {
1008
+ this._running = false;
1009
+ if (this._timer !== null) {
1010
+ clearInterval(this._timer);
1011
+ this._timer = null;
1012
+ }
1013
+ }
1014
+ render() {
1015
+ return html `
1016
+ <div class="sim" part="sim">
1017
+ <button class="btn" @click=${this._step}>Step</button>
1018
+ <button class="btn" @click=${this._toggle}>${this._running ? 'Pause' : 'Play'}</button>
1019
+ <span class="count ${this._host === null ? 'idle' : ''}">${this._steps} step${this._steps === 1 ? '' : 's'}</span>
1020
+ </div>`;
1021
+ }
1022
+ }
1023
+ FslSimulation.styles = css `
1024
+ :host { display: block; }
1025
+ .sim {
1026
+ display: flex; align-items: center; gap: 0.5rem;
1027
+ padding: 0.4rem 0.6rem; background: var(--_fsl-surface);
1028
+ border-top: 1px solid var(--_fsl-border); font: 0.8rem var(--_fsl-font);
1029
+ }
1030
+ .btn {
1031
+ height: 1.8rem; padding: 0 0.7rem; cursor: pointer; border-radius: 4px;
1032
+ border: 1px solid var(--_fsl-border); background: var(--_fsl-surface); color: var(--_fsl-text);
1033
+ font: 600 0.8rem var(--_fsl-font);
1034
+ }
1035
+ .btn:hover { background: color-mix(in srgb, var(--_fsl-text) 8%, var(--_fsl-surface)); }
1036
+ .count { color: var(--_fsl-muted); }
1037
+ .count.idle { font-style: italic; }
1038
+ ${fslTokens}
1039
+ `;
1040
+ __decorate([
1041
+ property({ type: Number })
1042
+ ], FslSimulation.prototype, "interval", void 0);
1043
+ __decorate([
1044
+ state()
1045
+ ], FslSimulation.prototype, "_running", void 0);
1046
+ __decorate([
1047
+ state()
1048
+ ], FslSimulation.prototype, "_steps", void 0);
1049
+
1050
+ /**
1051
+ * `<fsl-export>` — export buttons for a parent `<fsl-instance>`. Each produces a
1052
+ * string from the host's machine and fires `fsl-export` with `{ format,
1053
+ * content }`; the embedder decides what to do with it (copy, download, show).
1054
+ * Formats: Graphviz `dot` (via `machine_to_dot`), `json` (the machine's
1055
+ * `serialize()`), and `fsl` (the source). Standalone is inert.
1056
+ *
1057
+ * @element fsl-export
1058
+ * @csspart export - The button row.
1059
+ * @fires {CustomEvent<{format: FslExportFormat, content: string}>} fsl-export
1060
+ */
1061
+ class FslExport extends LitElement {
1062
+ constructor() {
1063
+ super(...arguments);
1064
+ this._host = null;
1065
+ }
1066
+ connectedCallback() {
1067
+ super.connectedCallback();
1068
+ this._host = closest_wc(this, 'instance');
1069
+ }
1070
+ _emit(format) {
1071
+ if (this._host === null) {
1072
+ return;
1073
+ }
1074
+ let content;
1075
+ if (format === 'dot') {
1076
+ content = machine_to_dot(this._host.machine);
1077
+ }
1078
+ else if (format === 'json') {
1079
+ content = JSON.stringify(this._host.machine.serialize(), null, 2);
1080
+ }
1081
+ else {
1082
+ content = this._host.fsl;
1083
+ }
1084
+ this.dispatchEvent(new CustomEvent('fsl-export', { detail: { format, content }, bubbles: true, composed: true }));
1085
+ }
1086
+ render() {
1087
+ return html `
1088
+ <div class="export" part="export">
1089
+ <span class="label">export</span>
1090
+ <button class="btn" @click=${() => this._emit('dot')}>DOT</button>
1091
+ <button class="btn" @click=${() => this._emit('json')}>JSON</button>
1092
+ <button class="btn" @click=${() => this._emit('fsl')}>FSL</button>
1093
+ </div>`;
1094
+ }
1095
+ }
1096
+ FslExport.styles = css `
1097
+ :host { display: block; }
1098
+ .export {
1099
+ display: flex; align-items: center; gap: 0.4rem;
1100
+ padding: 0.4rem 0.6rem; background: var(--_fsl-surface);
1101
+ border-top: 1px solid var(--_fsl-border);
1102
+ }
1103
+ .btn {
1104
+ height: 1.8rem; padding: 0 0.7rem; cursor: pointer; border-radius: 4px;
1105
+ border: 1px solid var(--_fsl-border); background: var(--_fsl-surface); color: var(--_fsl-text);
1106
+ font: 600 0.78rem var(--_fsl-font);
1107
+ }
1108
+ .btn:hover { background: color-mix(in srgb, var(--_fsl-text) 8%, var(--_fsl-surface)); }
1109
+ .label { color: var(--_fsl-muted); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.07em; }
1110
+ ${fslTokens}
1111
+ `;
1112
+
1113
+ export { FslActions, FslDataInspector, FslExport, FslFooter, FslHelp, FslHistory, FslHookLog, FslSimulation, FslToolbar };