@volter/editor-ui 0.5.210 → 0.5.212
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.
- package/NOTICE +12 -0
- package/contributions/component-board.service.ts +10 -2
- package/contributions/design-time-mount.service.ts +39 -2
- package/contributions/react-inspector.service.ts +11 -2
- package/contributions/story-documents.service.ts +14 -2
- package/contributions/ui.page.ts +28 -0
- package/package.json +78 -13
- package/src/host/authoring/struct-write-pipe.ts +102 -0
- package/src/host/components/ResolutionPicker.tsx +83 -0
- package/src/host/crash-null-boundary.ts +24 -0
- package/src/host/portable-stories.ts +44 -0
- package/src/host/projection/dom.ts +40 -0
- package/src/host/react-mount-runtime.ts +76 -0
- package/src/host/realm-services.ts +53 -0
- package/src/host/roots/react-root.ts +82 -0
- package/src/host/stories/story-arg-descriptors.ts +50 -0
- package/src/host/stories/story-media-presence.ts +89 -0
- package/src/host/stories/story-presentation.ts +203 -0
- package/src/host/surface-content.ts +44 -0
- package/src/react/composition-movie.ts +247 -0
- package/src/react/design-time-react-mount.ts +525 -0
- package/src/react/react-inspector-section.tsx +2247 -0
- package/src/react/react-world-authoring-adapter.ts +2602 -0
- package/src/react/story-documents/story-args-section.ts +9 -0
- package/src/react/story-documents/story-documents.tsx +1342 -0
- package/src/react/story-paint-bounds.ts +94 -0
- package/src/react/ui-board-document.tsx +55 -0
- package/src/react/ui-board-title.ts +9 -0
- package/src/react/ui-component-board.ts +52 -0
- package/src/runtime/presentation.ts +60 -0
- package/contributions/play-layers.service.ts +0 -31
|
@@ -0,0 +1,1342 @@
|
|
|
1
|
+
/** CSF stories open as workspace documents with isolated portable-story
|
|
2
|
+
* previews, editable primitive args, and story-scoped Actions, Interactions
|
|
3
|
+
* and Accessibility utilities. Per-document state is released on disposal.
|
|
4
|
+
* Preview roots are independent of Play's live React roots. */
|
|
5
|
+
|
|
6
|
+
import { editorHost } from '@volter/sdk/host';
|
|
7
|
+
import { Checkbox, TextInput, themeVars } from '@volter/sdk/widgets';
|
|
8
|
+
import { getCurrentProject } from '@volter/sdk/active-project';
|
|
9
|
+
import { type Resolution, ResolutionPicker } from '../../host/components/ResolutionPicker';
|
|
10
|
+
import { SurfaceStateOverlay } from '@volter/sdk/components/SurfaceStateOverlay';
|
|
11
|
+
import { registerDocumentOpener } from '@volter/sdk/document-open-registry';
|
|
12
|
+
import { createHmrRegistrationGroup } from '@volter/sdk/hmr-registration-group';
|
|
13
|
+
import {
|
|
14
|
+
CONTRIBUTED_SECTION_ORDER,
|
|
15
|
+
STORY_ARGS_SECTION_ID,
|
|
16
|
+
STORY_ARGS_SECTION_TITLE,
|
|
17
|
+
} from '@volter/sdk/inspection-model';
|
|
18
|
+
import { registerInspectorSections } from '@volter/sdk/inspector-section-registry';
|
|
19
|
+
import type { ComposedProjectStory } from '@volter/sdk/stories/story-types';
|
|
20
|
+
import { mountIsolatedStory, runStoryPlay } from '@volter/sdk/stories/StoryPreviewMount';
|
|
21
|
+
import { declaredStoryMedium } from '@volter/sdk/stories/story-declared-medium';
|
|
22
|
+
import {
|
|
23
|
+
ISOLATED_STORY_DOCUMENT_OPENER,
|
|
24
|
+
STORY_DOCS_DOCUMENT_OPENER,
|
|
25
|
+
type StoryDocumentOpenRequest,
|
|
26
|
+
} from '@volter/sdk/story-document-openers';
|
|
27
|
+
import { domHasRenderableContent } from '../../host/surface-content';
|
|
28
|
+
import { explainSurface, type SurfaceContentState } from '@volter/sdk/surface-state';
|
|
29
|
+
import { CONTRIBUTED_WORKSPACE_UTILITIES } from '@volter/sdk/workspace-core-utilities';
|
|
30
|
+
import {
|
|
31
|
+
activeWorkspaceDocument,
|
|
32
|
+
openWorkspaceDocument,
|
|
33
|
+
type WorkspaceDocumentContentProps,
|
|
34
|
+
type WorkspaceDocumentSelection,
|
|
35
|
+
} from '@volter/sdk/workspace-document-registry';
|
|
36
|
+
import { registerWorkspaceDocumentRestorer } from '@volter/sdk/workspace-document-restore';
|
|
37
|
+
import { registerWorkspaceUtility } from '@volter/sdk/workspace-utility-registry';
|
|
38
|
+
import { fitPresentation } from '../../runtime/presentation';
|
|
39
|
+
import { Button, fontMono, fontSizeVar, spaceVar } from '@volter/sdk/widgets';
|
|
40
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
41
|
+
import { STORY_ARGS_SECTION_ICON } from './story-args-section';
|
|
42
|
+
import { compositionRenders, declaresMovie, startCompositionRender } from '../composition-movie';
|
|
43
|
+
|
|
44
|
+
/** One logged action-arg invocation (the actions-addon semantic). */
|
|
45
|
+
interface StoryActionEntry {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly args: readonly unknown[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface StoryAccessibilityViolation {
|
|
51
|
+
readonly id: string;
|
|
52
|
+
readonly impact: string | null;
|
|
53
|
+
readonly help: string;
|
|
54
|
+
readonly helpUrl: string;
|
|
55
|
+
readonly nodes: readonly { readonly html: string; readonly target: readonly string[] }[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface StoryDocumentState {
|
|
59
|
+
readonly modulePath: string;
|
|
60
|
+
readonly storyName: string;
|
|
61
|
+
readonly mode: 'preview' | 'docs';
|
|
62
|
+
argOverrides: Record<string, unknown>;
|
|
63
|
+
actionLog: StoryActionEntry[];
|
|
64
|
+
playResult: { ran: boolean; error?: string } | null;
|
|
65
|
+
accessibility:
|
|
66
|
+
| { status: 'idle' | 'running'; violations: StoryAccessibilityViolation[]; error?: undefined }
|
|
67
|
+
| { status: 'complete'; violations: StoryAccessibilityViolation[]; error?: undefined }
|
|
68
|
+
| { status: 'error'; violations: StoryAccessibilityViolation[]; error: string };
|
|
69
|
+
/** The live isolated preview container (set by the content on mount) —
|
|
70
|
+
* what `play()` receives as its `canvasElement` scope. */
|
|
71
|
+
container: HTMLElement | null;
|
|
72
|
+
/** True only after the current composed story + loaders have mounted. */
|
|
73
|
+
previewReady: boolean;
|
|
74
|
+
/** Invalidates async play/accessibility results across remounts. */
|
|
75
|
+
previewRevision: number;
|
|
76
|
+
resolution: Resolution;
|
|
77
|
+
scale: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const _states = new Map<string, StoryDocumentState>();
|
|
81
|
+
let _version = 0;
|
|
82
|
+
const _listeners = new Set<() => void>();
|
|
83
|
+
|
|
84
|
+
function notifyChanged(): void {
|
|
85
|
+
_version++;
|
|
86
|
+
for (const fn of _listeners) fn();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Subscribe to story-document state changes. Returns unsubscribe. */
|
|
90
|
+
function subscribeStoryDocuments(fn: () => void): () => void {
|
|
91
|
+
_listeners.add(fn);
|
|
92
|
+
return () => {
|
|
93
|
+
_listeners.delete(fn);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Monotonic change counter — the `getSnapshot` for `useSyncExternalStore`. */
|
|
98
|
+
function storyDocumentsVersion(): number {
|
|
99
|
+
return _version;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Stable identity for one story document. */
|
|
103
|
+
function storyDocumentId(modulePath: string, storyName: string): string {
|
|
104
|
+
return `story:${modulePath}#${storyName}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Stable identity for the Storybook Docs companion document. */
|
|
108
|
+
function storyDocsDocumentId(modulePath: string, storyName: string): string {
|
|
109
|
+
return `story-docs:${modulePath}#${storyName}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The ACTIVE center document's story state, or `null` when the active
|
|
113
|
+
* document isn't a story (utilities/inspector section gate on this). */
|
|
114
|
+
function activeStoryDocumentState(): (StoryDocumentState & { id: string }) | null {
|
|
115
|
+
const active = activeWorkspaceDocument();
|
|
116
|
+
if (!active || active.descriptor.kind !== 'story') return null;
|
|
117
|
+
const state = _states.get(active.descriptor.id);
|
|
118
|
+
return state?.mode === 'preview' ? { ...state, id: active.descriptor.id } : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Set one arg override (Inspector Story Args section) — remounts the story
|
|
122
|
+
* against the merged args. `value === undefined` CLEARS the override (the
|
|
123
|
+
* `delete`-the-key convention) rather than storing `undefined`/`NaN` — the composed-args
|
|
124
|
+
* spread merge (`composedStoryArgs`/`activeStoryDocumentState` callers) only
|
|
125
|
+
* falls back to the authored value when the key is ABSENT, so a stored
|
|
126
|
+
* `NaN` (e.g. from an emptied number input) would otherwise win over the
|
|
127
|
+
* authored value and also make the input go from controlled to `NaN`. */
|
|
128
|
+
function setStoryArgOverride(id: string, key: string, value: unknown): void {
|
|
129
|
+
const state = _states.get(id);
|
|
130
|
+
if (!state) return;
|
|
131
|
+
if (value === undefined) {
|
|
132
|
+
const next = { ...state.argOverrides };
|
|
133
|
+
delete next[key];
|
|
134
|
+
state.argOverrides = next;
|
|
135
|
+
} else {
|
|
136
|
+
state.argOverrides = { ...state.argOverrides, [key]: value };
|
|
137
|
+
}
|
|
138
|
+
notifyChanged();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function logStoryAction(id: string, name: string, args: readonly unknown[]): void {
|
|
142
|
+
const state = _states.get(id);
|
|
143
|
+
if (!state) return;
|
|
144
|
+
state.actionLog = [...state.actionLog, { name, args }].slice(-200);
|
|
145
|
+
notifyChanged();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Clear the action log (Actions utility affordance). */
|
|
149
|
+
function clearStoryActionLog(id: string): void {
|
|
150
|
+
const state = _states.get(id);
|
|
151
|
+
if (!state || state.actionLog.length === 0) return;
|
|
152
|
+
state.actionLog = [];
|
|
153
|
+
notifyChanged();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Open or activate an isolated workspace preview for one named story. */
|
|
157
|
+
function openStoryDocument(
|
|
158
|
+
modulePath: string,
|
|
159
|
+
storyName: string,
|
|
160
|
+
options: { activate?: boolean; title?: string } = {},
|
|
161
|
+
): string {
|
|
162
|
+
ensureStoryContributionsRegistered();
|
|
163
|
+
const id = storyDocumentId(modulePath, storyName);
|
|
164
|
+
if (!_states.has(id)) {
|
|
165
|
+
_states.set(id, {
|
|
166
|
+
modulePath,
|
|
167
|
+
storyName,
|
|
168
|
+
mode: 'preview',
|
|
169
|
+
argOverrides: {},
|
|
170
|
+
actionLog: [],
|
|
171
|
+
playResult: null,
|
|
172
|
+
accessibility: { status: 'idle', violations: [] },
|
|
173
|
+
container: null,
|
|
174
|
+
previewReady: false,
|
|
175
|
+
previewRevision: 0,
|
|
176
|
+
resolution: { label: 'Fill', width: null, height: null },
|
|
177
|
+
scale: 1,
|
|
178
|
+
});
|
|
179
|
+
notifyChanged();
|
|
180
|
+
}
|
|
181
|
+
const openOptions = options.activate === undefined ? {} : { activate: options.activate };
|
|
182
|
+
return openWorkspaceDocument(
|
|
183
|
+
{
|
|
184
|
+
id,
|
|
185
|
+
// The story's export name exactly as authored in the CSF module
|
|
186
|
+
// (`Default`, `Playing`), never a humanized/uppercased variant.
|
|
187
|
+
title: options.title ?? storyName,
|
|
188
|
+
kind: 'story',
|
|
189
|
+
provenance: { sourcePath: modulePath },
|
|
190
|
+
Content: StoryDocumentContent,
|
|
191
|
+
Toolbar: StoryDocumentToolbar,
|
|
192
|
+
presentation: () => ({ kind: 'story', modulePath, storyName, mode: 'preview' }),
|
|
193
|
+
selection: storyIsTheSubject,
|
|
194
|
+
persist: () => ({ modulePath, storyName }),
|
|
195
|
+
onDispose: (documentId) => {
|
|
196
|
+
if (_states.delete(documentId)) notifyChanged();
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
openOptions,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** A story document has no authoring of its own to pick inside: the document is the inspector's subject, which the
|
|
204
|
+
* registry's selection contract says by registering `{adapter: null, nodeId: null}`. Without it the inspector
|
|
205
|
+
* answers for whatever the scene or Content had selected before the story opened. */
|
|
206
|
+
function storyIsTheSubject(): WorkspaceDocumentSelection {
|
|
207
|
+
return { adapter: null, nodeId: null };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Open the composed story's Storybook Docs companion as a center document. */
|
|
211
|
+
function openStoryDocsDocument(modulePath: string, storyName: string): string {
|
|
212
|
+
const id = storyDocsDocumentId(modulePath, storyName);
|
|
213
|
+
if (!_states.has(id)) {
|
|
214
|
+
_states.set(id, {
|
|
215
|
+
modulePath,
|
|
216
|
+
storyName,
|
|
217
|
+
mode: 'docs',
|
|
218
|
+
argOverrides: {},
|
|
219
|
+
actionLog: [],
|
|
220
|
+
playResult: null,
|
|
221
|
+
accessibility: { status: 'idle', violations: [] },
|
|
222
|
+
container: null,
|
|
223
|
+
previewReady: false,
|
|
224
|
+
previewRevision: 0,
|
|
225
|
+
resolution: { label: 'Fill', width: null, height: null },
|
|
226
|
+
scale: 1,
|
|
227
|
+
});
|
|
228
|
+
notifyChanged();
|
|
229
|
+
}
|
|
230
|
+
return openWorkspaceDocument({
|
|
231
|
+
id,
|
|
232
|
+
title: `${storyName} — Docs`,
|
|
233
|
+
kind: 'story',
|
|
234
|
+
provenance: { sourcePath: modulePath },
|
|
235
|
+
Content: StoryDocsDocumentContent,
|
|
236
|
+
presentation: () => ({ kind: 'story', modulePath, storyName, mode: 'docs' }),
|
|
237
|
+
selection: storyIsTheSubject,
|
|
238
|
+
persist: () => ({ modulePath, storyName, view: 'docs' }),
|
|
239
|
+
onDispose: (documentId) => {
|
|
240
|
+
if (_states.delete(documentId)) notifyChanged();
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* THE STORY REGISTRY MUST BE SETTLED BEFORE ANY STORY DOCUMENT RESTORES —
|
|
247
|
+
* the restore reads it synchronously to verify. ONE function so the restore host,
|
|
248
|
+
* which dedupes prepares by identity, refreshes once rather than twice (a
|
|
249
|
+
* second concurrent refresh bumps the registry's generation and aborts the
|
|
250
|
+
* first).
|
|
251
|
+
*/
|
|
252
|
+
function prepareStoryDocumentRestore(): Promise<void> {
|
|
253
|
+
const project = getCurrentProject();
|
|
254
|
+
if (!project) return Promise.resolve();
|
|
255
|
+
// Awaiting OUR refresh is not enough: a concurrent later pass (the
|
|
256
|
+
// discovery lifecycle's own, a project change) aborts this one, which then
|
|
257
|
+
// returns early over an EMPTY registry — and an empty registry reads as "the
|
|
258
|
+
// story is gone", so the restore drops the tab. Settle on the registry's own
|
|
259
|
+
// readiness instead; see `EditorHostStories.whenReady`.
|
|
260
|
+
const { stories } = editorHost();
|
|
261
|
+
return stories.refresh(project).then(stories.whenReady, stories.whenReady);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Reopen a persisted story document. VERIFIED against the settled registry:
|
|
265
|
+
* the module must still load AND still export that story name. A preview
|
|
266
|
+
* additionally checks the story's DECLARED medium — a saved renderer is
|
|
267
|
+
* convenience state, not the story's current declaration, so a preview whose
|
|
268
|
+
* medium has moved to `stage` is dropped here rather than mounted on the
|
|
269
|
+
* wrong one. */
|
|
270
|
+
registerWorkspaceDocumentRestorer({
|
|
271
|
+
kind: 'story',
|
|
272
|
+
owner: 'story-documents',
|
|
273
|
+
prepare: prepareStoryDocumentRestore,
|
|
274
|
+
restore: ({ state }) => {
|
|
275
|
+
const record = state as
|
|
276
|
+
| { modulePath?: unknown; storyName?: unknown; view?: unknown }
|
|
277
|
+
| null
|
|
278
|
+
| undefined;
|
|
279
|
+
const modulePath = typeof record?.modulePath === 'string' ? record.modulePath : null;
|
|
280
|
+
const storyName = typeof record?.storyName === 'string' ? record.storyName : null;
|
|
281
|
+
if (!modulePath || !storyName) return false;
|
|
282
|
+
const module_ = editorHost().stories.modules().find((m) => m.modulePath === modulePath);
|
|
283
|
+
if (!module_?.ok) return false;
|
|
284
|
+
if (!module_.stories.some((story) => story.name === storyName)) return false;
|
|
285
|
+
if (record?.view === 'docs') {
|
|
286
|
+
openStoryDocsDocument(modulePath, storyName);
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
const { medium } = declaredStoryMedium({ modulePath, regions: editorHost().stories.regions() });
|
|
290
|
+
if (medium !== 'dom' && medium !== 'canvas') return false;
|
|
291
|
+
openStoryDocument(modulePath, storyName);
|
|
292
|
+
return true;
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The two addresses this module answers (`document-open-registry.ts`): the
|
|
298
|
+
* isolated preview and the Docs companion. Which address a gesture uses is
|
|
299
|
+
* the CALLER's decision — the declared medium for a routed open.
|
|
300
|
+
*/
|
|
301
|
+
registerDocumentOpener<StoryDocumentOpenRequest>({
|
|
302
|
+
id: ISOLATED_STORY_DOCUMENT_OPENER,
|
|
303
|
+
owner: 'story-documents',
|
|
304
|
+
open: (request) =>
|
|
305
|
+
openStoryDocument(request.modulePath, request.storyName, {
|
|
306
|
+
...(request.title === undefined ? {} : { title: request.title }),
|
|
307
|
+
...(request.activate === undefined ? {} : { activate: request.activate }),
|
|
308
|
+
}),
|
|
309
|
+
});
|
|
310
|
+
registerDocumentOpener<StoryDocumentOpenRequest>({
|
|
311
|
+
id: STORY_DOCS_DOCUMENT_OPENER,
|
|
312
|
+
owner: 'story-documents',
|
|
313
|
+
open: (request) => openStoryDocsDocument(request.modulePath, request.storyName),
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// --- Story resolution against the live registry ---------------------------
|
|
317
|
+
|
|
318
|
+
function findComposedStory(
|
|
319
|
+
modulePath: string,
|
|
320
|
+
storyName: string,
|
|
321
|
+
): { story: ComposedProjectStory | undefined; moduleError: string | undefined } {
|
|
322
|
+
const module_ = editorHost().stories.modules().find((m) => m.modulePath === modulePath);
|
|
323
|
+
if (!module_) return { story: undefined, moduleError: undefined };
|
|
324
|
+
if (!module_.ok) return { story: undefined, moduleError: module_.error };
|
|
325
|
+
return { story: module_.stories.find((s) => s.name === storyName), moduleError: undefined };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** A composed story's own args (Storybook attaches them to the portable
|
|
329
|
+
* component). Defensive read — `{}` when absent. */
|
|
330
|
+
function composedStoryArgs(story: ComposedProjectStory): Record<string, unknown> {
|
|
331
|
+
const args = (story.Component as unknown as Record<string, unknown>)['args'];
|
|
332
|
+
return args && typeof args === 'object' ? (args as Record<string, unknown>) : {};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Merge order: composed args ← wrapped action spies ← user overrides. The
|
|
336
|
+
* spies re-wrap the ORIGINAL function so authored handlers still run. */
|
|
337
|
+
function buildOverrideProps(id: string, story: ComposedProjectStory): Record<string, unknown> {
|
|
338
|
+
const state = _states.get(id);
|
|
339
|
+
const overrides: Record<string, unknown> = {};
|
|
340
|
+
for (const [key, value] of Object.entries(composedStoryArgs(story))) {
|
|
341
|
+
if (typeof value === 'function') {
|
|
342
|
+
overrides[key] = (...callArgs: unknown[]) => {
|
|
343
|
+
logStoryAction(id, key, callArgs);
|
|
344
|
+
return (value as (...a: unknown[]) => unknown)(...callArgs);
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return { ...overrides, ...(state?.argOverrides ?? {}) };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// --- Document content + toolbar -------------------------------------------
|
|
352
|
+
|
|
353
|
+
function StoryDocumentContent({ documentId }: WorkspaceDocumentContentProps) {
|
|
354
|
+
useSyncExternalStore(subscribeStoryDocuments, storyDocumentsVersion);
|
|
355
|
+
// Re-resolve the composed story on every registry refresh (project change,
|
|
356
|
+
// `story-files-changed` SSE) — same live-rescan physics as the Stories dock.
|
|
357
|
+
useSyncExternalStore(editorHost().stories.subscribe, () => editorHost().stories.modules());
|
|
358
|
+
const state = _states.get(documentId);
|
|
359
|
+
const outerRef = useRef<HTMLDivElement | null>(null);
|
|
360
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
361
|
+
const [loading, setLoading] = useState(false);
|
|
362
|
+
const [previewContent, setPreviewContent] = useState<SurfaceContentState>('unknown');
|
|
363
|
+
const [previewFailure, setPreviewFailure] = useState<string | null>(null);
|
|
364
|
+
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
|
365
|
+
const { story, moduleError } = state
|
|
366
|
+
? findComposedStory(state.modulePath, state.storyName)
|
|
367
|
+
: { story: undefined, moduleError: undefined };
|
|
368
|
+
// Arg-override identity for the remount effect below.
|
|
369
|
+
const overridesKey = state ? JSON.stringify(state.argOverrides) : '';
|
|
370
|
+
|
|
371
|
+
useEffect(() => {
|
|
372
|
+
const element = outerRef.current;
|
|
373
|
+
if (!element) return;
|
|
374
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
375
|
+
setContainerSize({ width: element.clientWidth, height: element.clientHeight });
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
379
|
+
if (!entry) return;
|
|
380
|
+
const { width, height } = entry.contentRect;
|
|
381
|
+
setContainerSize((current) =>
|
|
382
|
+
current.width === width && current.height === height ? current : { width, height },
|
|
383
|
+
);
|
|
384
|
+
});
|
|
385
|
+
observer.observe(element);
|
|
386
|
+
return () => observer.disconnect();
|
|
387
|
+
}, []);
|
|
388
|
+
|
|
389
|
+
useEffect(() => {
|
|
390
|
+
const container = containerRef.current;
|
|
391
|
+
if (!container || !state) return;
|
|
392
|
+
if (state.container !== container) {
|
|
393
|
+
state.container = container; // play()'s canvasElement scope
|
|
394
|
+
notifyChanged();
|
|
395
|
+
}
|
|
396
|
+
const previewRevision = state.previewRevision + 1;
|
|
397
|
+
state.previewRevision = previewRevision;
|
|
398
|
+
state.previewReady = false;
|
|
399
|
+
state.playResult = null;
|
|
400
|
+
state.accessibility = { status: 'idle', violations: [] };
|
|
401
|
+
notifyChanged();
|
|
402
|
+
setPreviewContent('unknown');
|
|
403
|
+
setPreviewFailure(null);
|
|
404
|
+
if (!story) {
|
|
405
|
+
setLoading(false);
|
|
406
|
+
setPreviewContent('empty');
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
let cancelled = false;
|
|
410
|
+
let unmountFn: (() => void) | undefined;
|
|
411
|
+
let settleTask: ReturnType<typeof setTimeout> | undefined;
|
|
412
|
+
const updatePreviewContent = (): void => {
|
|
413
|
+
if (cancelled) return;
|
|
414
|
+
setPreviewContent(domHasRenderableContent(container) ? 'present' : 'empty');
|
|
415
|
+
};
|
|
416
|
+
const observer = new MutationObserver(updatePreviewContent);
|
|
417
|
+
observer.observe(container, { childList: true, subtree: true, characterData: true });
|
|
418
|
+
setLoading(true);
|
|
419
|
+
void mountIsolatedStory(
|
|
420
|
+
container,
|
|
421
|
+
state.modulePath,
|
|
422
|
+
state.storyName,
|
|
423
|
+
story.Component,
|
|
424
|
+
buildOverrideProps(documentId, story),
|
|
425
|
+
).then(
|
|
426
|
+
(result) => {
|
|
427
|
+
if (cancelled) {
|
|
428
|
+
result?.unmount();
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (!result) {
|
|
432
|
+
if (_states.get(documentId)?.previewRevision === previewRevision) {
|
|
433
|
+
setLoading(false);
|
|
434
|
+
setPreviewContent('empty');
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const current = _states.get(documentId);
|
|
439
|
+
if (!current || current.previewRevision !== previewRevision) {
|
|
440
|
+
result.unmount();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
unmountFn = result.unmount;
|
|
444
|
+
current.previewReady = true;
|
|
445
|
+
setLoading(false);
|
|
446
|
+
// `root.render()` may commit after mountIsolatedStory resolves. Cross one event-loop task;
|
|
447
|
+
// unlike rAF this also runs in a hidden editor tab, where live control and capture remain
|
|
448
|
+
// supported. The observer above keeps the answer live for later Suspense/async commits.
|
|
449
|
+
settleTask = setTimeout(updatePreviewContent, 0);
|
|
450
|
+
notifyChanged();
|
|
451
|
+
},
|
|
452
|
+
(reason: unknown) => {
|
|
453
|
+
if (cancelled || _states.get(documentId)?.previewRevision !== previewRevision) return;
|
|
454
|
+
setLoading(false);
|
|
455
|
+
setPreviewFailure(reason instanceof Error ? reason.message : String(reason));
|
|
456
|
+
setPreviewContent('empty');
|
|
457
|
+
},
|
|
458
|
+
);
|
|
459
|
+
return () => {
|
|
460
|
+
cancelled = true;
|
|
461
|
+
observer.disconnect();
|
|
462
|
+
if (settleTask !== undefined) clearTimeout(settleTask);
|
|
463
|
+
unmountFn?.();
|
|
464
|
+
};
|
|
465
|
+
// `story.Component` identity changes on registry refresh; overridesKey on
|
|
466
|
+
// arg edits — both remount, which is exactly the wanted behavior.
|
|
467
|
+
}, [documentId, state, story, overridesKey]);
|
|
468
|
+
|
|
469
|
+
const isFill = state?.resolution.width === null;
|
|
470
|
+
// Device previews fit the viewport without upscaling.
|
|
471
|
+
const scale =
|
|
472
|
+
!state || isFill
|
|
473
|
+
? 1
|
|
474
|
+
: fitPresentation(containerSize, {
|
|
475
|
+
width: state.resolution.width!,
|
|
476
|
+
height: state.resolution.height!,
|
|
477
|
+
}).scale;
|
|
478
|
+
useEffect(() => {
|
|
479
|
+
if (!state || !Number.isFinite(scale) || scale <= 0 || state.scale === scale) return;
|
|
480
|
+
state.scale = scale;
|
|
481
|
+
notifyChanged();
|
|
482
|
+
}, [scale, state]);
|
|
483
|
+
|
|
484
|
+
if (!state) {
|
|
485
|
+
return (
|
|
486
|
+
<div
|
|
487
|
+
style={{
|
|
488
|
+
padding: spaceVar[6],
|
|
489
|
+
fontSize: fontSizeVar.md,
|
|
490
|
+
color: themeVars.content.muted,
|
|
491
|
+
pointerEvents: 'auto',
|
|
492
|
+
}}
|
|
493
|
+
>
|
|
494
|
+
This story document is no longer available.
|
|
495
|
+
</div>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const storyRootId = `${state.modulePath}#${state.storyName}`;
|
|
500
|
+
const storyExplanation = !story
|
|
501
|
+
? {
|
|
502
|
+
tone: 'error' as const,
|
|
503
|
+
title: `Story ${storyRootId} is unavailable`,
|
|
504
|
+
description:
|
|
505
|
+
moduleError ??
|
|
506
|
+
`No story export named “${state.storyName}” exists in ${state.modulePath}; it may have been renamed or removed.`,
|
|
507
|
+
}
|
|
508
|
+
: previewFailure
|
|
509
|
+
? {
|
|
510
|
+
tone: 'error' as const,
|
|
511
|
+
title: `Story ${storyRootId} failed`,
|
|
512
|
+
description: previewFailure,
|
|
513
|
+
}
|
|
514
|
+
: explainSurface({
|
|
515
|
+
surface: 'Story',
|
|
516
|
+
rootIds: [storyRootId],
|
|
517
|
+
phase: loading ? 'loading' : 'ready',
|
|
518
|
+
content: previewContent,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
return (
|
|
522
|
+
<div
|
|
523
|
+
style={{
|
|
524
|
+
// No root fill — the dock document surface shows through.
|
|
525
|
+
flex: 1,
|
|
526
|
+
minHeight: 0,
|
|
527
|
+
display: 'flex',
|
|
528
|
+
flexDirection: 'column',
|
|
529
|
+
pointerEvents: 'auto',
|
|
530
|
+
}}
|
|
531
|
+
>
|
|
532
|
+
<div
|
|
533
|
+
ref={outerRef}
|
|
534
|
+
style={{
|
|
535
|
+
// The letterbox around the device canvas is transparent —
|
|
536
|
+
// the story ARTBOARD below keeps its inset stage fill (a preview
|
|
537
|
+
// canvas backing, exempt like the viewport's).
|
|
538
|
+
position: 'relative',
|
|
539
|
+
flex: 1,
|
|
540
|
+
minHeight: 0,
|
|
541
|
+
overflow: 'auto',
|
|
542
|
+
display: 'flex',
|
|
543
|
+
alignItems: 'center',
|
|
544
|
+
justifyContent: 'center',
|
|
545
|
+
}}
|
|
546
|
+
>
|
|
547
|
+
<div
|
|
548
|
+
style={
|
|
549
|
+
isFill
|
|
550
|
+
? { position: 'absolute', inset: 0 }
|
|
551
|
+
: {
|
|
552
|
+
position: 'relative',
|
|
553
|
+
width: state.resolution.width! * scale,
|
|
554
|
+
height: state.resolution.height! * scale,
|
|
555
|
+
flex: '0 0 auto',
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
>
|
|
559
|
+
<div
|
|
560
|
+
data-testid="story-preview-root"
|
|
561
|
+
ref={containerRef}
|
|
562
|
+
// A story owns the whole selected device canvas. Containment
|
|
563
|
+
// keeps fixed/full-page UI inside that canvas, never over editor
|
|
564
|
+
// chrome.
|
|
565
|
+
style={{
|
|
566
|
+
position: 'absolute',
|
|
567
|
+
inset: 0,
|
|
568
|
+
isolation: 'isolate',
|
|
569
|
+
contain: 'layout paint',
|
|
570
|
+
overflow: 'auto',
|
|
571
|
+
background: themeVars.surface.inset,
|
|
572
|
+
...(isFill
|
|
573
|
+
? {}
|
|
574
|
+
: {
|
|
575
|
+
width: state.resolution.width!,
|
|
576
|
+
height: state.resolution.height!,
|
|
577
|
+
transform: `scale(${scale})`,
|
|
578
|
+
transformOrigin: 'top left',
|
|
579
|
+
}),
|
|
580
|
+
}}
|
|
581
|
+
/>
|
|
582
|
+
<SurfaceStateOverlay explanation={storyExplanation} testId="story-surface-status" />
|
|
583
|
+
</div>
|
|
584
|
+
</div>
|
|
585
|
+
</div>
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function docsDescription(story: ComposedProjectStory): { component?: string; story?: string } {
|
|
590
|
+
const docs = story.parameters['docs'];
|
|
591
|
+
if (!docs || typeof docs !== 'object') return {};
|
|
592
|
+
const description = (docs as Record<string, unknown>)['description'];
|
|
593
|
+
if (typeof description === 'string') return { story: description };
|
|
594
|
+
if (!description || typeof description !== 'object') return {};
|
|
595
|
+
const record = description as Record<string, unknown>;
|
|
596
|
+
return {
|
|
597
|
+
...(typeof record['component'] === 'string' ? { component: record['component'] } : {}),
|
|
598
|
+
...(typeof record['story'] === 'string' ? { story: record['story'] } : {}),
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Storybook Docs companion: composed CSF metadata, canvas, and args table. */
|
|
603
|
+
function StoryDocsDocumentContent({ documentId }: WorkspaceDocumentContentProps) {
|
|
604
|
+
useSyncExternalStore(subscribeStoryDocuments, storyDocumentsVersion);
|
|
605
|
+
useSyncExternalStore(editorHost().stories.subscribe, () => editorHost().stories.modules());
|
|
606
|
+
const state = _states.get(documentId);
|
|
607
|
+
const previewRef = useRef<HTMLDivElement | null>(null);
|
|
608
|
+
const [previewContent, setPreviewContent] = useState<SurfaceContentState>('unknown');
|
|
609
|
+
const [previewFailure, setPreviewFailure] = useState<string | null>(null);
|
|
610
|
+
const { story, moduleError } = state
|
|
611
|
+
? findComposedStory(state.modulePath, state.storyName)
|
|
612
|
+
: { story: undefined, moduleError: undefined };
|
|
613
|
+
useEffect(() => {
|
|
614
|
+
const container = previewRef.current;
|
|
615
|
+
if (!container || !state || !story) return;
|
|
616
|
+
let disposed = false;
|
|
617
|
+
let unmount: (() => void) | null = null;
|
|
618
|
+
let settleTask: ReturnType<typeof setTimeout> | undefined;
|
|
619
|
+
setPreviewContent('unknown');
|
|
620
|
+
setPreviewFailure(null);
|
|
621
|
+
const updatePreviewContent = (): void => {
|
|
622
|
+
if (!disposed) setPreviewContent(domHasRenderableContent(container) ? 'present' : 'empty');
|
|
623
|
+
};
|
|
624
|
+
const observer = new MutationObserver(updatePreviewContent);
|
|
625
|
+
observer.observe(container, { childList: true, subtree: true, characterData: true });
|
|
626
|
+
void mountIsolatedStory(container, state.modulePath, state.storyName, story.Component).then(
|
|
627
|
+
(result) => {
|
|
628
|
+
if (!result) {
|
|
629
|
+
if (!disposed) setPreviewContent('empty');
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (disposed) result.unmount();
|
|
633
|
+
else {
|
|
634
|
+
unmount = result.unmount;
|
|
635
|
+
settleTask = setTimeout(updatePreviewContent, 0);
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
(reason: unknown) => {
|
|
639
|
+
if (disposed) return;
|
|
640
|
+
setPreviewFailure(reason instanceof Error ? reason.message : String(reason));
|
|
641
|
+
setPreviewContent('empty');
|
|
642
|
+
},
|
|
643
|
+
);
|
|
644
|
+
return () => {
|
|
645
|
+
disposed = true;
|
|
646
|
+
observer.disconnect();
|
|
647
|
+
if (settleTask !== undefined) clearTimeout(settleTask);
|
|
648
|
+
unmount?.();
|
|
649
|
+
};
|
|
650
|
+
}, [state, story]);
|
|
651
|
+
|
|
652
|
+
if (!state || !story) {
|
|
653
|
+
return (
|
|
654
|
+
<div
|
|
655
|
+
style={{ padding: spaceVar[8], color: themeVars.semantic.danger, fontSize: fontSizeVar.md }}
|
|
656
|
+
>
|
|
657
|
+
{moduleError ?? 'This Storybook Docs document is no longer available.'}
|
|
658
|
+
</div>
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
const description = docsDescription(story);
|
|
662
|
+
const args = story.args;
|
|
663
|
+
const storyRootId = `${state.modulePath}#${state.storyName}`;
|
|
664
|
+
const canvasExplanation = previewFailure
|
|
665
|
+
? {
|
|
666
|
+
tone: 'error' as const,
|
|
667
|
+
title: `Story ${storyRootId} failed`,
|
|
668
|
+
description: previewFailure,
|
|
669
|
+
}
|
|
670
|
+
: explainSurface({
|
|
671
|
+
surface: 'Story',
|
|
672
|
+
rootIds: [storyRootId],
|
|
673
|
+
phase: previewContent === 'unknown' ? 'loading' : 'ready',
|
|
674
|
+
content: previewContent,
|
|
675
|
+
});
|
|
676
|
+
return (
|
|
677
|
+
<article
|
|
678
|
+
data-testid="story-docs-document"
|
|
679
|
+
// Keep documentation prose readable over the local frost layer.
|
|
680
|
+
className="volter-content-frost"
|
|
681
|
+
style={{
|
|
682
|
+
// No root fill — the dock document surface shows through.
|
|
683
|
+
flex: 1,
|
|
684
|
+
minHeight: 0,
|
|
685
|
+
overflow: 'auto',
|
|
686
|
+
pointerEvents: 'auto',
|
|
687
|
+
padding: '24px clamp(18px, 5vw, 64px)',
|
|
688
|
+
}}
|
|
689
|
+
>
|
|
690
|
+
<div
|
|
691
|
+
style={{
|
|
692
|
+
maxWidth: 920,
|
|
693
|
+
margin: '0 auto',
|
|
694
|
+
display: 'flex',
|
|
695
|
+
flexDirection: 'column',
|
|
696
|
+
gap: 22,
|
|
697
|
+
}}
|
|
698
|
+
>
|
|
699
|
+
<header>
|
|
700
|
+
<div
|
|
701
|
+
style={{ color: themeVars.content.dim, fontSize: fontSizeVar.base, marginBottom: 5 }}
|
|
702
|
+
>
|
|
703
|
+
{story.title ?? state.modulePath}
|
|
704
|
+
</div>
|
|
705
|
+
<h1
|
|
706
|
+
style={{ color: themeVars.content.primary, fontSize: fontSizeVar.heading, margin: 0 }}
|
|
707
|
+
>
|
|
708
|
+
{story.label}
|
|
709
|
+
</h1>
|
|
710
|
+
{(description.story ?? description.component) && (
|
|
711
|
+
<p
|
|
712
|
+
style={{
|
|
713
|
+
color: themeVars.content.primary,
|
|
714
|
+
lineHeight: 1.6,
|
|
715
|
+
maxWidth: 760,
|
|
716
|
+
whiteSpace: 'pre-wrap',
|
|
717
|
+
}}
|
|
718
|
+
>
|
|
719
|
+
{description.story ?? description.component}
|
|
720
|
+
</p>
|
|
721
|
+
)}
|
|
722
|
+
</header>
|
|
723
|
+
<section>
|
|
724
|
+
<h2 style={{ color: themeVars.content.primary, fontSize: fontSizeVar.xl }}>Canvas</h2>
|
|
725
|
+
<div style={{ position: 'relative', minHeight: 180 }}>
|
|
726
|
+
<div
|
|
727
|
+
ref={previewRef}
|
|
728
|
+
data-testid="story-docs-canvas"
|
|
729
|
+
style={{
|
|
730
|
+
position: 'relative',
|
|
731
|
+
isolation: 'isolate',
|
|
732
|
+
contain: 'layout paint',
|
|
733
|
+
minHeight: 180,
|
|
734
|
+
padding: 18,
|
|
735
|
+
overflow: 'auto',
|
|
736
|
+
background: themeVars.surface.inset,
|
|
737
|
+
border: `1px solid ${themeVars.boundary.default}`,
|
|
738
|
+
borderRadius: themeVars.shape.medium,
|
|
739
|
+
}}
|
|
740
|
+
/>
|
|
741
|
+
<SurfaceStateOverlay
|
|
742
|
+
explanation={canvasExplanation}
|
|
743
|
+
testId="story-docs-surface-status"
|
|
744
|
+
/>
|
|
745
|
+
</div>
|
|
746
|
+
</section>
|
|
747
|
+
<section>
|
|
748
|
+
<h2 style={{ color: themeVars.content.primary, fontSize: fontSizeVar.xl }}>Args</h2>
|
|
749
|
+
<div
|
|
750
|
+
style={{
|
|
751
|
+
border: `1px solid ${themeVars.boundary.default}`,
|
|
752
|
+
borderRadius: themeVars.shape.medium,
|
|
753
|
+
overflow: 'hidden',
|
|
754
|
+
}}
|
|
755
|
+
>
|
|
756
|
+
{Object.entries(args).map(([name, value]) => (
|
|
757
|
+
<div
|
|
758
|
+
key={name}
|
|
759
|
+
style={{
|
|
760
|
+
display: 'grid',
|
|
761
|
+
gridTemplateColumns: 'minmax(120px, .4fr) minmax(140px, .6fr) 1fr',
|
|
762
|
+
gap: spaceVar[6],
|
|
763
|
+
padding: `${spaceVar[4]} ${spaceVar[5]}`,
|
|
764
|
+
borderBottom: `1px solid ${themeVars.boundary.default}`,
|
|
765
|
+
fontSize: fontSizeVar.base,
|
|
766
|
+
}}
|
|
767
|
+
>
|
|
768
|
+
<code style={{ color: themeVars.accent.default }}>{name}</code>
|
|
769
|
+
<span style={{ color: themeVars.content.muted }}>
|
|
770
|
+
{Array.isArray(value) ? 'array' : typeof value}
|
|
771
|
+
</span>
|
|
772
|
+
<code
|
|
773
|
+
style={{
|
|
774
|
+
color: themeVars.content.primary,
|
|
775
|
+
overflow: 'hidden',
|
|
776
|
+
textOverflow: 'ellipsis',
|
|
777
|
+
}}
|
|
778
|
+
>
|
|
779
|
+
{typeof value === 'function' ? 'function' : JSON.stringify(value)}
|
|
780
|
+
</code>
|
|
781
|
+
</div>
|
|
782
|
+
))}
|
|
783
|
+
{Object.keys(args).length === 0 && (
|
|
784
|
+
<div
|
|
785
|
+
style={{
|
|
786
|
+
padding: spaceVar[5],
|
|
787
|
+
color: themeVars.content.dim,
|
|
788
|
+
fontSize: fontSizeVar.base,
|
|
789
|
+
}}
|
|
790
|
+
>
|
|
791
|
+
This story declares no args.
|
|
792
|
+
</div>
|
|
793
|
+
)}
|
|
794
|
+
</div>
|
|
795
|
+
</section>
|
|
796
|
+
<footer style={{ color: themeVars.content.dim, fontSize: fontSizeVar.sm }}>
|
|
797
|
+
{state.modulePath} · {state.storyName}
|
|
798
|
+
</footer>
|
|
799
|
+
</div>
|
|
800
|
+
</article>
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/** Document-local toolbar: source module, Run play(), and Render for stories declaring
|
|
805
|
+
* `parameters.volter.movie` (`../composition-movie.ts`). */
|
|
806
|
+
function StoryDocumentToolbar({ documentId }: WorkspaceDocumentContentProps) {
|
|
807
|
+
useSyncExternalStore(subscribeStoryDocuments, storyDocumentsVersion);
|
|
808
|
+
useSyncExternalStore(editorHost().stories.subscribe, () => editorHost().stories.modules());
|
|
809
|
+
useSyncExternalStore(compositionRenders.subscribe, compositionRenders.version);
|
|
810
|
+
const [refused, setRefused] = useState<string | null>(null);
|
|
811
|
+
const state = _states.get(documentId);
|
|
812
|
+
if (!state) return null;
|
|
813
|
+
const { story } = findComposedStory(state.modulePath, state.storyName);
|
|
814
|
+
const hasPlay = typeof story?.Component.play === 'function';
|
|
815
|
+
const result = state.playResult;
|
|
816
|
+
const composition = story && declaresMovie(story) ? story : null;
|
|
817
|
+
const render = composition ? compositionRenders.get(composition.id) : null;
|
|
818
|
+
return (
|
|
819
|
+
<div
|
|
820
|
+
data-testid="story-document-toolbar"
|
|
821
|
+
style={{
|
|
822
|
+
display: 'flex',
|
|
823
|
+
alignItems: 'center',
|
|
824
|
+
gap: spaceVar[4],
|
|
825
|
+
padding: '3px 10px',
|
|
826
|
+
fontSize: fontSizeVar.sm,
|
|
827
|
+
color: themeVars.content.muted,
|
|
828
|
+
borderBottom: `1px solid ${themeVars.boundary.default}`,
|
|
829
|
+
}}
|
|
830
|
+
>
|
|
831
|
+
<span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
832
|
+
{state.modulePath} —{' '}
|
|
833
|
+
<code style={{ color: themeVars.accent.default }}>{state.storyName}</code>
|
|
834
|
+
</span>
|
|
835
|
+
<ResolutionPicker
|
|
836
|
+
resolution={state.resolution}
|
|
837
|
+
scale={state.scale}
|
|
838
|
+
onResolutionChange={(resolution) => {
|
|
839
|
+
state.resolution = resolution;
|
|
840
|
+
notifyChanged();
|
|
841
|
+
}}
|
|
842
|
+
/>
|
|
843
|
+
<Button
|
|
844
|
+
size="compact"
|
|
845
|
+
data-testid="story-open-docs"
|
|
846
|
+
onClick={() => openStoryDocsDocument(state.modulePath, state.storyName)}
|
|
847
|
+
style={{ flexShrink: 0 }}
|
|
848
|
+
>
|
|
849
|
+
Docs
|
|
850
|
+
</Button>
|
|
851
|
+
{hasPlay && story && (
|
|
852
|
+
<Button
|
|
853
|
+
variant="outline"
|
|
854
|
+
size="compact"
|
|
855
|
+
data-testid="story-run-play"
|
|
856
|
+
onClick={async () => {
|
|
857
|
+
if (!state.container || !state.previewReady) return;
|
|
858
|
+
const previewRevision = state.previewRevision;
|
|
859
|
+
const playResult = await runStoryPlay(
|
|
860
|
+
state.modulePath,
|
|
861
|
+
state.storyName,
|
|
862
|
+
story.Component,
|
|
863
|
+
state.container,
|
|
864
|
+
);
|
|
865
|
+
const current = _states.get(documentId);
|
|
866
|
+
if (current?.previewReady && current.previewRevision === previewRevision) {
|
|
867
|
+
current.playResult = playResult;
|
|
868
|
+
notifyChanged();
|
|
869
|
+
}
|
|
870
|
+
}}
|
|
871
|
+
disabled={!state.previewReady}
|
|
872
|
+
style={{ flexShrink: 0 }}
|
|
873
|
+
>
|
|
874
|
+
Run play()
|
|
875
|
+
</Button>
|
|
876
|
+
)}
|
|
877
|
+
{composition && (
|
|
878
|
+
<Button
|
|
879
|
+
variant="outline"
|
|
880
|
+
size="compact"
|
|
881
|
+
data-testid="story-render-movie"
|
|
882
|
+
disabled={render?.state === 'rendering'}
|
|
883
|
+
// a render's failure, or a refusal to start one, says why in the state beside it
|
|
884
|
+
onClick={() => {
|
|
885
|
+
setRefused(null);
|
|
886
|
+
startCompositionRender({ story: composition.id }).catch((error: unknown) => {
|
|
887
|
+
setRefused(error instanceof Error ? error.message : String(error));
|
|
888
|
+
});
|
|
889
|
+
}}
|
|
890
|
+
style={{ flexShrink: 0 }}
|
|
891
|
+
>
|
|
892
|
+
Render
|
|
893
|
+
</Button>
|
|
894
|
+
)}
|
|
895
|
+
{refused && (
|
|
896
|
+
<span data-testid="story-render-state" style={{ color: themeVars.semantic.danger }}>
|
|
897
|
+
{refused}
|
|
898
|
+
</span>
|
|
899
|
+
)}
|
|
900
|
+
{render && !refused && (
|
|
901
|
+
<span
|
|
902
|
+
data-testid="story-render-state"
|
|
903
|
+
style={{
|
|
904
|
+
color: render.state === 'failed' ? themeVars.semantic.danger
|
|
905
|
+
: render.state === 'done' ? themeVars.semantic.success : themeVars.content.muted,
|
|
906
|
+
}}
|
|
907
|
+
>
|
|
908
|
+
{render.state === 'rendering' ? `Rendering ${render.frame}/${render.frames || '…'}`
|
|
909
|
+
: render.state === 'done' ? `Rendered ${render.out}` : render.error}
|
|
910
|
+
</span>
|
|
911
|
+
)}
|
|
912
|
+
{result?.ran && !result.error && (
|
|
913
|
+
<span
|
|
914
|
+
data-testid="story-play-result"
|
|
915
|
+
style={{ color: themeVars.semantic.success, fontWeight: 600 }}
|
|
916
|
+
>
|
|
917
|
+
play() ran
|
|
918
|
+
</span>
|
|
919
|
+
)}
|
|
920
|
+
{result?.error && (
|
|
921
|
+
<span data-testid="story-play-result" style={{ color: themeVars.semantic.danger }}>
|
|
922
|
+
{result.error}
|
|
923
|
+
</span>
|
|
924
|
+
)}
|
|
925
|
+
</div>
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// --- Inspector "Story Args" section (Storybook args → right Inspector) -
|
|
930
|
+
|
|
931
|
+
function ArgRow({ id, name, value }: { id: string; name: string; value: unknown }) {
|
|
932
|
+
return (
|
|
933
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: spaceVar[3], padding: '3px 0' }}>
|
|
934
|
+
<span
|
|
935
|
+
style={{
|
|
936
|
+
width: 90,
|
|
937
|
+
flexShrink: 0,
|
|
938
|
+
fontSize: fontSizeVar.base,
|
|
939
|
+
color: themeVars.content.muted,
|
|
940
|
+
overflow: 'hidden',
|
|
941
|
+
textOverflow: 'ellipsis',
|
|
942
|
+
}}
|
|
943
|
+
title={name}
|
|
944
|
+
>
|
|
945
|
+
{name}
|
|
946
|
+
</span>
|
|
947
|
+
{typeof value === 'boolean' ? (
|
|
948
|
+
<Checkbox
|
|
949
|
+
className="volter-checkbox"
|
|
950
|
+
data-testid={`story-arg-${name}`}
|
|
951
|
+
checked={value}
|
|
952
|
+
onChange={(e) => setStoryArgOverride(id, name, e.target.checked)}
|
|
953
|
+
/>
|
|
954
|
+
) : typeof value === 'number' ? (
|
|
955
|
+
<TextInput
|
|
956
|
+
type="number"
|
|
957
|
+
className="volter-input"
|
|
958
|
+
data-testid={`story-arg-${name}`}
|
|
959
|
+
value={value}
|
|
960
|
+
onChange={(e) => {
|
|
961
|
+
// Empty/unparseable input clears the override (falls back to the
|
|
962
|
+
// authored arg) instead of setting NaN — `Number.parseFloat('')`
|
|
963
|
+
// is NaN, and storing NaN would both win over the authored value
|
|
964
|
+
// in the composed-args merge (see `setStoryArgOverride`'s doc
|
|
965
|
+
// comment) and make this a controlled input whose displayed
|
|
966
|
+
// `value` is `NaN`, which React warns about.
|
|
967
|
+
const raw = e.target.value;
|
|
968
|
+
const parsed = Number.parseFloat(raw);
|
|
969
|
+
setStoryArgOverride(id, name, raw === '' || Number.isNaN(parsed) ? undefined : parsed);
|
|
970
|
+
}}
|
|
971
|
+
style={{ width: 80 }}
|
|
972
|
+
/>
|
|
973
|
+
) : typeof value === 'string' ? (
|
|
974
|
+
<TextInput
|
|
975
|
+
type="text"
|
|
976
|
+
className="volter-input"
|
|
977
|
+
data-testid={`story-arg-${name}`}
|
|
978
|
+
value={value}
|
|
979
|
+
onChange={(e) => setStoryArgOverride(id, name, e.target.value)}
|
|
980
|
+
style={{ flex: 1, minWidth: 0 }}
|
|
981
|
+
/>
|
|
982
|
+
) : (
|
|
983
|
+
<span
|
|
984
|
+
style={{ fontSize: fontSizeVar.base, color: themeVars.content.dim }}
|
|
985
|
+
data-testid={`story-arg-${name}`}
|
|
986
|
+
>
|
|
987
|
+
{typeof value === 'function' ? 'fn (logged to Actions)' : JSON.stringify(value)}
|
|
988
|
+
</span>
|
|
989
|
+
)}
|
|
990
|
+
</div>
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/** Registered section: renders while the ACTIVE center document is a story —
|
|
995
|
+
* the args of THAT story (composed args merged with live overrides). */
|
|
996
|
+
function StoryArgsSection() {
|
|
997
|
+
useSyncExternalStore(subscribeStoryDocuments, storyDocumentsVersion);
|
|
998
|
+
useSyncExternalStore(editorHost().stories.subscribe, () => editorHost().stories.modules());
|
|
999
|
+
const active = activeStoryDocumentState();
|
|
1000
|
+
if (!active) return null;
|
|
1001
|
+
const { story } = findComposedStory(active.modulePath, active.storyName);
|
|
1002
|
+
const args = story ? { ...composedStoryArgs(story), ...active.argOverrides } : {};
|
|
1003
|
+
const names = Object.keys(args);
|
|
1004
|
+
return (
|
|
1005
|
+
<div
|
|
1006
|
+
data-testid="inspector-story-args"
|
|
1007
|
+
style={{
|
|
1008
|
+
pointerEvents: 'auto',
|
|
1009
|
+
padding: spaceVar[4],
|
|
1010
|
+
borderBottom: `1px solid ${themeVars.boundary.default}`,
|
|
1011
|
+
}}
|
|
1012
|
+
>
|
|
1013
|
+
<div
|
|
1014
|
+
style={{
|
|
1015
|
+
fontSize: fontSizeVar.base,
|
|
1016
|
+
color: themeVars.content.dim,
|
|
1017
|
+
textTransform: 'uppercase',
|
|
1018
|
+
marginBottom: spaceVar[2],
|
|
1019
|
+
}}
|
|
1020
|
+
>
|
|
1021
|
+
Story Args
|
|
1022
|
+
</div>
|
|
1023
|
+
<div
|
|
1024
|
+
style={{
|
|
1025
|
+
fontSize: fontSizeVar.base,
|
|
1026
|
+
color: themeVars.content.primary,
|
|
1027
|
+
fontWeight: 600,
|
|
1028
|
+
marginBottom: spaceVar[2],
|
|
1029
|
+
}}
|
|
1030
|
+
>
|
|
1031
|
+
{active.storyName}
|
|
1032
|
+
</div>
|
|
1033
|
+
{names.length === 0 && (
|
|
1034
|
+
<div style={{ fontSize: fontSizeVar.base, color: themeVars.content.dim }}>
|
|
1035
|
+
{story ? 'This story declares no args.' : 'Story source unavailable.'}
|
|
1036
|
+
</div>
|
|
1037
|
+
)}
|
|
1038
|
+
{names.map((name) => (
|
|
1039
|
+
<ArgRow key={name} id={active.id} name={name} value={args[name]} />
|
|
1040
|
+
))}
|
|
1041
|
+
</div>
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Story-scoped Actions, Interactions and Accessibility utilities.
|
|
1046
|
+
|
|
1047
|
+
/** Actions utility content: the wrapped-arg call log of the active story. */
|
|
1048
|
+
function StoryActionsUtility() {
|
|
1049
|
+
useSyncExternalStore(subscribeStoryDocuments, storyDocumentsVersion);
|
|
1050
|
+
const active = activeStoryDocumentState();
|
|
1051
|
+
if (!active) {
|
|
1052
|
+
return (
|
|
1053
|
+
<div style={{ padding: spaceVar[6], fontSize: fontSizeVar.md, color: themeVars.content.dim }}>
|
|
1054
|
+
No active story.
|
|
1055
|
+
</div>
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
return (
|
|
1059
|
+
<div
|
|
1060
|
+
data-testid="story-actions-utility"
|
|
1061
|
+
style={{
|
|
1062
|
+
flex: 1,
|
|
1063
|
+
minHeight: 0,
|
|
1064
|
+
overflowY: 'auto',
|
|
1065
|
+
padding: spaceVar[4],
|
|
1066
|
+
fontSize: fontSizeVar.base,
|
|
1067
|
+
}}
|
|
1068
|
+
>
|
|
1069
|
+
<Button
|
|
1070
|
+
size="compact"
|
|
1071
|
+
style={{ marginBottom: spaceVar[3] }}
|
|
1072
|
+
onClick={() => clearStoryActionLog(active.id)}
|
|
1073
|
+
>
|
|
1074
|
+
Clear
|
|
1075
|
+
</Button>
|
|
1076
|
+
{active.actionLog.length === 0 ? (
|
|
1077
|
+
<div style={{ color: themeVars.content.dim }}>
|
|
1078
|
+
No actions logged — function-valued args are wrapped with logging spies; interact with the
|
|
1079
|
+
story preview to see calls here.
|
|
1080
|
+
</div>
|
|
1081
|
+
) : (
|
|
1082
|
+
active.actionLog.map((entry, i) => (
|
|
1083
|
+
<div
|
|
1084
|
+
key={i}
|
|
1085
|
+
style={{
|
|
1086
|
+
padding: `${spaceVar[1]} 0`,
|
|
1087
|
+
borderBottom: `1px solid ${themeVars.boundary.default}`,
|
|
1088
|
+
}}
|
|
1089
|
+
>
|
|
1090
|
+
<code style={{ color: themeVars.accent.default }}>{entry.name}</code>{' '}
|
|
1091
|
+
<span style={{ color: themeVars.content.muted }}>
|
|
1092
|
+
(
|
|
1093
|
+
{entry.args
|
|
1094
|
+
.map((a) => {
|
|
1095
|
+
try {
|
|
1096
|
+
return typeof a === 'object' ? JSON.stringify(a)?.slice(0, 80) : String(a);
|
|
1097
|
+
} catch {
|
|
1098
|
+
return String(a);
|
|
1099
|
+
}
|
|
1100
|
+
})
|
|
1101
|
+
.join(', ')}
|
|
1102
|
+
)
|
|
1103
|
+
</span>
|
|
1104
|
+
</div>
|
|
1105
|
+
))
|
|
1106
|
+
)}
|
|
1107
|
+
</div>
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/** Interactions utility content: the active story's play() run results. */
|
|
1112
|
+
function StoryInteractionsUtility() {
|
|
1113
|
+
useSyncExternalStore(subscribeStoryDocuments, storyDocumentsVersion);
|
|
1114
|
+
const active = activeStoryDocumentState();
|
|
1115
|
+
if (!active) {
|
|
1116
|
+
return (
|
|
1117
|
+
<div style={{ padding: spaceVar[6], fontSize: fontSizeVar.md, color: themeVars.content.dim }}>
|
|
1118
|
+
No active story.
|
|
1119
|
+
</div>
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
const result = active.playResult;
|
|
1123
|
+
return (
|
|
1124
|
+
<div
|
|
1125
|
+
data-testid="story-interactions-utility"
|
|
1126
|
+
style={{
|
|
1127
|
+
flex: 1,
|
|
1128
|
+
minHeight: 0,
|
|
1129
|
+
overflowY: 'auto',
|
|
1130
|
+
padding: spaceVar[6],
|
|
1131
|
+
fontSize: fontSizeVar.md,
|
|
1132
|
+
}}
|
|
1133
|
+
>
|
|
1134
|
+
{!result && (
|
|
1135
|
+
<div style={{ color: themeVars.content.dim }}>
|
|
1136
|
+
No interaction run yet — use the story document's “Run play()”.
|
|
1137
|
+
</div>
|
|
1138
|
+
)}
|
|
1139
|
+
{result && !result.ran && (
|
|
1140
|
+
<div style={{ color: themeVars.content.dim }}>This story declares no play() function.</div>
|
|
1141
|
+
)}
|
|
1142
|
+
{result?.ran && !result.error && (
|
|
1143
|
+
<div style={{ color: themeVars.semantic.success, fontWeight: 600 }}>
|
|
1144
|
+
play() ran without errors.
|
|
1145
|
+
</div>
|
|
1146
|
+
)}
|
|
1147
|
+
{result?.error && (
|
|
1148
|
+
<div style={{ color: themeVars.semantic.danger, whiteSpace: 'pre-wrap' }}>
|
|
1149
|
+
{result.error}
|
|
1150
|
+
</div>
|
|
1151
|
+
)}
|
|
1152
|
+
</div>
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/** Run axe-core against the active story's isolated preview root. */
|
|
1157
|
+
async function runStoryAccessibility(id: string): Promise<void> {
|
|
1158
|
+
const state = _states.get(id);
|
|
1159
|
+
if (!state?.container || !state.previewReady) return;
|
|
1160
|
+
const container = state.container;
|
|
1161
|
+
const previewRevision = state.previewRevision;
|
|
1162
|
+
state.accessibility = { status: 'running', violations: [] };
|
|
1163
|
+
notifyChanged();
|
|
1164
|
+
try {
|
|
1165
|
+
const axe = (await import('axe-core')).default;
|
|
1166
|
+
const result = await axe.run(container);
|
|
1167
|
+
const current = _states.get(id);
|
|
1168
|
+
if (
|
|
1169
|
+
!current?.previewReady ||
|
|
1170
|
+
current.previewRevision !== previewRevision ||
|
|
1171
|
+
current.container !== container
|
|
1172
|
+
)
|
|
1173
|
+
return;
|
|
1174
|
+
current.accessibility = {
|
|
1175
|
+
status: 'complete',
|
|
1176
|
+
violations: result.violations.map((violation) => ({
|
|
1177
|
+
id: violation.id,
|
|
1178
|
+
impact: violation.impact ?? null,
|
|
1179
|
+
help: violation.help,
|
|
1180
|
+
helpUrl: violation.helpUrl,
|
|
1181
|
+
nodes: violation.nodes.map((node) => ({
|
|
1182
|
+
html: node.html,
|
|
1183
|
+
target: node.target.map(String),
|
|
1184
|
+
})),
|
|
1185
|
+
})),
|
|
1186
|
+
};
|
|
1187
|
+
notifyChanged();
|
|
1188
|
+
} catch (reason) {
|
|
1189
|
+
const current = _states.get(id);
|
|
1190
|
+
if (
|
|
1191
|
+
!current?.previewReady ||
|
|
1192
|
+
current.previewRevision !== previewRevision ||
|
|
1193
|
+
current.container !== container
|
|
1194
|
+
)
|
|
1195
|
+
return;
|
|
1196
|
+
current.accessibility = {
|
|
1197
|
+
status: 'error',
|
|
1198
|
+
violations: [],
|
|
1199
|
+
error: reason instanceof Error ? reason.message : String(reason),
|
|
1200
|
+
};
|
|
1201
|
+
notifyChanged();
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/** Story-scoped accessibility results, powered by the same axe engine Storybook uses. */
|
|
1206
|
+
function StoryAccessibilityUtility() {
|
|
1207
|
+
useSyncExternalStore(subscribeStoryDocuments, storyDocumentsVersion);
|
|
1208
|
+
const active = activeStoryDocumentState();
|
|
1209
|
+
if (!active)
|
|
1210
|
+
return (
|
|
1211
|
+
<div style={{ padding: spaceVar[6], fontSize: fontSizeVar.md, color: themeVars.content.dim }}>
|
|
1212
|
+
No active story.
|
|
1213
|
+
</div>
|
|
1214
|
+
);
|
|
1215
|
+
const result = active.accessibility;
|
|
1216
|
+
return (
|
|
1217
|
+
<div
|
|
1218
|
+
data-testid="story-accessibility-utility"
|
|
1219
|
+
style={{
|
|
1220
|
+
flex: 1,
|
|
1221
|
+
minHeight: 0,
|
|
1222
|
+
overflowY: 'auto',
|
|
1223
|
+
padding: spaceVar[5],
|
|
1224
|
+
fontSize: fontSizeVar.base,
|
|
1225
|
+
}}
|
|
1226
|
+
>
|
|
1227
|
+
<Button
|
|
1228
|
+
variant="outline"
|
|
1229
|
+
size="compact"
|
|
1230
|
+
disabled={!active.container || !active.previewReady || result.status === 'running'}
|
|
1231
|
+
onClick={() => void runStoryAccessibility(active.id)}
|
|
1232
|
+
style={{ marginBottom: spaceVar[4] }}
|
|
1233
|
+
>
|
|
1234
|
+
{result.status === 'running' ? 'Checking…' : 'Run accessibility check'}
|
|
1235
|
+
</Button>
|
|
1236
|
+
{result.status === 'idle' && (
|
|
1237
|
+
<div style={{ color: themeVars.content.dim }}>
|
|
1238
|
+
Run axe against the mounted story preview.
|
|
1239
|
+
</div>
|
|
1240
|
+
)}
|
|
1241
|
+
{result.status === 'error' && (
|
|
1242
|
+
<div style={{ color: themeVars.semantic.danger }}>{result.error}</div>
|
|
1243
|
+
)}
|
|
1244
|
+
{result.status === 'complete' && result.violations.length === 0 && (
|
|
1245
|
+
<div style={{ color: themeVars.semantic.success, fontWeight: 600 }}>
|
|
1246
|
+
No accessibility violations found.
|
|
1247
|
+
</div>
|
|
1248
|
+
)}
|
|
1249
|
+
{result.violations.map((violation) => (
|
|
1250
|
+
<details
|
|
1251
|
+
key={violation.id}
|
|
1252
|
+
style={{
|
|
1253
|
+
borderBottom: `1px solid ${themeVars.boundary.default}`,
|
|
1254
|
+
padding: `${spaceVar[3]} 0`,
|
|
1255
|
+
}}
|
|
1256
|
+
>
|
|
1257
|
+
<summary
|
|
1258
|
+
style={{
|
|
1259
|
+
color:
|
|
1260
|
+
violation.impact === 'critical' || violation.impact === 'serious'
|
|
1261
|
+
? themeVars.semantic.danger
|
|
1262
|
+
: themeVars.content.primary,
|
|
1263
|
+
cursor: 'pointer',
|
|
1264
|
+
}}
|
|
1265
|
+
>
|
|
1266
|
+
{violation.help} · {violation.nodes.length} node
|
|
1267
|
+
{violation.nodes.length === 1 ? '' : 's'}
|
|
1268
|
+
</summary>
|
|
1269
|
+
<a
|
|
1270
|
+
href={violation.helpUrl}
|
|
1271
|
+
target="_blank"
|
|
1272
|
+
rel="noreferrer"
|
|
1273
|
+
style={{ color: themeVars.accent.default, display: 'inline-block', margin: '5px 0' }}
|
|
1274
|
+
>
|
|
1275
|
+
How to fix
|
|
1276
|
+
</a>
|
|
1277
|
+
{violation.nodes.map((node, index) => (
|
|
1278
|
+
<pre
|
|
1279
|
+
key={`${violation.id}:${index}`}
|
|
1280
|
+
style={{
|
|
1281
|
+
color: themeVars.content.muted,
|
|
1282
|
+
fontFamily: fontMono,
|
|
1283
|
+
fontSize: fontSizeVar.sm,
|
|
1284
|
+
whiteSpace: 'pre-wrap',
|
|
1285
|
+
overflowWrap: 'anywhere',
|
|
1286
|
+
}}
|
|
1287
|
+
>
|
|
1288
|
+
{node.target.join(' ')}
|
|
1289
|
+
{`\n`}
|
|
1290
|
+
{node.html}
|
|
1291
|
+
</pre>
|
|
1292
|
+
))}
|
|
1293
|
+
</details>
|
|
1294
|
+
))}
|
|
1295
|
+
</div>
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const registrationGroup = createHmrRegistrationGroup(import.meta.hot, 'story-contributions');
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* Idempotent one-time registration of the story-scoped shell contributions:
|
|
1303
|
+
* the Inspector Story Args section and the Actions, Interactions and
|
|
1304
|
+
* Accessibility utilities. Called from `openStoryDocument` (and importable directly) so
|
|
1305
|
+
* merely importing this module in a test doesn't mutate the registries.
|
|
1306
|
+
*/
|
|
1307
|
+
export function ensureStoryContributionsRegistered(): void {
|
|
1308
|
+
registrationGroup.ensure((track) => {
|
|
1309
|
+
track(
|
|
1310
|
+
registerInspectorSections({
|
|
1311
|
+
match: () => activeWorkspaceDocument()?.descriptor.kind === 'story',
|
|
1312
|
+
id: STORY_ARGS_SECTION_ID,
|
|
1313
|
+
title: STORY_ARGS_SECTION_TITLE,
|
|
1314
|
+
icon: STORY_ARGS_SECTION_ICON,
|
|
1315
|
+
order: CONTRIBUTED_SECTION_ORDER,
|
|
1316
|
+
Section: StoryArgsSection,
|
|
1317
|
+
}),
|
|
1318
|
+
);
|
|
1319
|
+
const storyActive = () => activeStoryDocumentState() !== null;
|
|
1320
|
+
track(
|
|
1321
|
+
registerWorkspaceUtility({
|
|
1322
|
+
...CONTRIBUTED_WORKSPACE_UTILITIES.storyActions,
|
|
1323
|
+
Content: StoryActionsUtility,
|
|
1324
|
+
available: storyActive,
|
|
1325
|
+
}),
|
|
1326
|
+
);
|
|
1327
|
+
track(
|
|
1328
|
+
registerWorkspaceUtility({
|
|
1329
|
+
...CONTRIBUTED_WORKSPACE_UTILITIES.storyInteractions,
|
|
1330
|
+
Content: StoryInteractionsUtility,
|
|
1331
|
+
available: storyActive,
|
|
1332
|
+
}),
|
|
1333
|
+
);
|
|
1334
|
+
track(
|
|
1335
|
+
registerWorkspaceUtility({
|
|
1336
|
+
...CONTRIBUTED_WORKSPACE_UTILITIES.storyAccessibility,
|
|
1337
|
+
Content: StoryAccessibilityUtility,
|
|
1338
|
+
available: storyActive,
|
|
1339
|
+
}),
|
|
1340
|
+
);
|
|
1341
|
+
});
|
|
1342
|
+
}
|