@cueloop/client 0.1.0-alpha.9 → 0.1.0-alpha.90
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/native/build-pty.sh +51 -0
- package/native/build.sh +82 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/native/src/shim.zig +114 -0
- package/package.json +42 -34
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +77 -51
- package/src/App.diff.test.tsx +259 -45
- package/src/App.export.test.tsx +58 -41
- package/src/App.focus.test.ts +89 -0
- package/src/App.identity.test.tsx +78 -0
- package/src/App.plan-render.test.tsx +209 -0
- package/src/App.plan-review.test.tsx +330 -161
- package/src/App.prototype-markdown.test.tsx +128 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +129 -67
- package/src/App.regression.test.tsx +94 -0
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.share-dialog.test.tsx +172 -0
- package/src/App.submit-confirm.test.tsx +264 -0
- package/src/App.test.tsx +402 -126
- package/src/App.toast.test.tsx +98 -0
- package/src/App.tsx +1402 -1060
- package/src/App.visual.test.tsx +57 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/App.workbench.test.tsx +774 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +62 -0
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +577 -0
- package/src/app-view-model.ts +168 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -0
- package/src/client-extension-registry.test.tsx +61 -0
- package/src/client-extension-registry.ts +159 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +33 -0
- package/src/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/components/AnnotatableFileView.test.tsx +102 -0
- package/src/components/AnnotatableFileView.tsx +134 -0
- package/src/components/AnnotationCards.stories.tsx +86 -0
- package/src/components/AnnotationCards.tsx +341 -0
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.brand.test.tsx +58 -0
- package/src/components/AppShell.extensions.test.tsx +191 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +362 -0
- package/src/components/AppShell.zoom.test.tsx +135 -0
- package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/components/BareWorkbenchFileView.tsx +64 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +45 -0
- package/src/components/ChangesColumn.stories.tsx +44 -0
- package/src/components/ChangesColumn.tsx +199 -0
- package/src/components/ChangesFileTree.test.tsx +61 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +37 -0
- package/src/components/CompletionOverlay.tsx +77 -0
- package/src/components/ConfirmCard.stories.tsx +37 -0
- package/src/components/ConfirmCard.tsx +150 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +72 -0
- package/src/components/DiffContentView.scroll.test.tsx +149 -0
- package/src/components/DiffContentView.split.test.tsx +140 -0
- package/src/components/DiffContentView.stories.tsx +80 -0
- package/src/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/components/DiffContentView.tsx +906 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +186 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +426 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -0
- package/src/components/ExtensionZone.stories.tsx +28 -0
- package/src/components/ExtensionZone.tsx +145 -0
- package/src/components/FileContentsView.stories.tsx +16 -0
- package/src/components/FileContentsView.test.tsx +29 -0
- package/src/components/FileContentsView.tsx +98 -0
- package/src/components/GridTabContent.stories.tsx +59 -0
- package/src/components/GridTabContent.tsx +182 -0
- package/src/components/HandleChipsInput.stories.tsx +24 -0
- package/src/components/HandleChipsInput.test.tsx +35 -0
- package/src/components/HandleChipsInput.tsx +145 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -0
- package/src/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/components/MarkdownGridBlock.tsx +72 -0
- package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/components/MarkdownThreadEditor.tsx +162 -0
- package/src/components/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -0
- package/src/components/NavModeHint.stories.tsx +40 -0
- package/src/components/NavModeHint.tsx +31 -0
- package/src/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/components/OverlayScrollbar.test.tsx +139 -0
- package/src/components/OverlayScrollbar.tsx +129 -0
- package/src/components/PaneGrid.stories.tsx +84 -0
- package/src/components/PaneGrid.tsx +128 -0
- package/src/components/PanelColumn.stories.tsx +25 -0
- package/src/components/PanelColumn.tsx +98 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +75 -0
- package/src/components/ProjectTreeView.tsx +125 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.test.tsx +61 -0
- package/src/components/PromptDialog.tsx +99 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +45 -0
- package/src/components/RootOverlay.tsx +48 -0
- package/src/components/ScrollArea.stories.tsx +20 -0
- package/src/components/ScrollArea.tsx +44 -0
- package/src/components/SettingsDialog.stories.tsx +117 -0
- package/src/components/SettingsDialog.tsx +186 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +47 -0
- package/src/components/ThreadFooter.tsx +76 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +255 -0
- package/src/components/ThreadView.selection.test.tsx +951 -0
- package/src/components/ThreadView.slash.test.tsx +146 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +525 -0
- package/src/components/Toast.stories.tsx +19 -0
- package/src/components/Toast.tsx +52 -0
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/TreePane.stories.tsx +106 -0
- package/src/components/TreePane.tsx +157 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +150 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +49 -0
- package/src/components/WelcomePlayground.tsx +139 -0
- package/src/components/WorkspacePanels.stories.tsx +36 -0
- package/src/components/WorkspacePanels.tsx +116 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +2264 -0
- package/src/components/diff-content-view-layout.test.ts +198 -0
- package/src/components/diff-content-view-layout.ts +84 -0
- package/src/components/editor-grid.test.ts +86 -0
- package/src/components/editor-grid.ts +138 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -0
- package/src/components/menu-control.stories.tsx +35 -0
- package/src/components/menu-control.tsx +43 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +59 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +63 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.test.tsx +37 -0
- package/src/components/primitives/Dialog.tsx +71 -0
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +62 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +23 -0
- package/src/components/primitives/Switch.stories.tsx +17 -0
- package/src/components/primitives/Switch.tsx +32 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +102 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +100 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +17 -0
- package/src/components/primitives/icons.ts +61 -0
- package/src/components/primitives/tree-model.test.ts +99 -0
- package/src/components/primitives/tree-model.ts +137 -0
- package/src/components/quick-actions-editor.stories.tsx +58 -0
- package/src/components/quick-actions-editor.test.tsx +117 -0
- package/src/components/quick-actions-editor.tsx +226 -0
- package/src/components/share-dialog-store.ts +58 -0
- package/src/components/stories-app.tsx +193 -0
- package/src/components/stories.test.tsx +74 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +108 -0
- package/src/components/story.ts +159 -0
- package/src/components/syntax-highlight.ts +133 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +199 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +37 -0
- package/src/components/thread-tree.test.ts +134 -0
- package/src/components/thread-tree.ts +109 -0
- package/src/components/truncate-text.ts +7 -0
- package/src/components/truncate-title.test.ts +31 -0
- package/src/components/truncate-title.ts +15 -0
- package/src/config-identity.test.ts +78 -0
- package/src/config.test.ts +579 -34
- package/src/config.ts +533 -57
- package/src/diff-fold.test.ts +78 -0
- package/src/diff-fold.ts +126 -0
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +153 -0
- package/src/diff-intraline.test.ts +238 -0
- package/src/diff-intraline.ts +236 -0
- package/src/diff-syntax.test.ts +112 -0
- package/src/diff-syntax.ts +179 -0
- package/src/discussions.ts +79 -0
- package/src/editor.test.ts +149 -0
- package/src/editor.ts +139 -9
- package/src/ghostty-terminal.test.ts +85 -0
- package/src/ghostty-terminal.ts +199 -0
- package/src/github-identity.test.ts +58 -0
- package/src/github-identity.ts +41 -0
- package/src/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +28 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +724 -0
- package/src/intent-dispatch.ts +690 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +459 -0
- package/src/keymap.test.ts +456 -115
- package/src/keymap.ts +315 -81
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/launch-layout.test.ts +67 -0
- package/src/launch-layout.ts +47 -0
- package/src/mark-runs.ts +121 -0
- package/src/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown-grid.test.ts +101 -0
- package/src/markdown-grid.ts +203 -0
- package/src/markdown-highlight.test.ts +61 -0
- package/src/markdown-highlight.ts +122 -0
- package/src/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/perf/otlp-trace.test.ts +116 -0
- package/src/perf/otlp-trace.ts +130 -0
- package/src/perf/perf-report.ts +17 -0
- package/src/perf/perf-timings.test.ts +18 -0
- package/src/perf/perf-timings.ts +38 -0
- package/src/prototype-browser.test.ts +81 -0
- package/src/prototype-browser.ts +329 -0
- package/src/prototype-pixels.tsx +35 -0
- package/src/pty.test.ts +81 -0
- package/src/pty.ts +245 -0
- package/src/ready-signal.test.ts +85 -0
- package/src/ready-signal.ts +52 -0
- package/src/run.ts +85 -12
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +52 -10
- package/src/share.ts +237 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +75 -0
- package/src/slash-palette.test.ts +103 -0
- package/src/slash-palette.ts +129 -0
- package/src/split-diff.test.ts +110 -0
- package/src/split-diff.ts +149 -0
- package/src/test-support.test.ts +264 -0
- package/src/test-support.ts +369 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +219 -0
- package/src/theme.ts +88 -19
- package/src/thread-chords.test.ts +79 -0
- package/src/thread-chords.ts +163 -0
- package/src/thread-code-syntax.ts +45 -0
- package/src/thread-controller.curate.test.ts +476 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +322 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +561 -0
- package/src/thread-controller.switch-cache.test.ts +176 -0
- package/src/thread-controller.tree.test.ts +261 -0
- package/src/thread-controller.ts +2050 -0
- package/src/thread-selection.test.ts +210 -0
- package/src/thread-selection.ts +243 -0
- package/src/tree-view.test.ts +128 -0
- package/src/tree-view.ts +153 -0
- package/src/use-annotation-surface.tsx +975 -0
- package/src/use-changes-workbench.ts +190 -0
- package/src/use-frame-measure.ts +41 -0
- package/src/use-remember-layout.ts +31 -0
- package/src/use-repo-changes.ts +87 -0
- package/src/use-settings-dialog.tsx +364 -0
- package/src/use-terminal-virtualizer.ts +259 -0
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +279 -9
- package/src/view-diff.ts +460 -24
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +545 -0
- package/src/view-plan.ts +1019 -0
- package/src/visual/contrast.test.ts +35 -0
- package/src/visual/contrast.ts +33 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- package/src/workbench-snapshot.test.ts +99 -0
- package/src/workbench-snapshot.ts +59 -0
- package/src/session-controller.ts +0 -398
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -364
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interactive stories catalog (`bun run stories`): a nested tree of
|
|
3
|
+
* component modules on the left (j/k to move, h/l to fold or open) and the
|
|
4
|
+
* selected story rendered live in the preview pane. The same story objects
|
|
5
|
+
* back the snapshot harness - what this TUI shows is what the tests assert.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
9
|
+
import { createCliRenderer, type ScrollBoxRenderable } from "@opentui/core";
|
|
10
|
+
import { createRoot, useKeyboard } from "@opentui/react";
|
|
11
|
+
import { DARK } from "../theme";
|
|
12
|
+
import { ThemeProvider } from "./theme-context";
|
|
13
|
+
import { buildStoryTree, loadStories, type LoadedStory } from "./story";
|
|
14
|
+
import { PaneGrid } from "./PaneGrid";
|
|
15
|
+
import { ShellHeader } from "./ShellHeader";
|
|
16
|
+
import { AnsiScreen } from "./AnsiScreen";
|
|
17
|
+
import { Tree } from "./primitives/Tree";
|
|
18
|
+
import { NERD } from "./primitives/icons";
|
|
19
|
+
import { flattenTree } from "./primitives/tree-model";
|
|
20
|
+
|
|
21
|
+
interface StoriesAppProps {
|
|
22
|
+
stories: LoadedStory[];
|
|
23
|
+
onExit: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function StoriesApp({ stories, onExit }: StoriesAppProps): React.ReactNode {
|
|
27
|
+
const treeNodes = useMemo(() => buildStoryTree(stories), [stories]);
|
|
28
|
+
const storiesById = useMemo(
|
|
29
|
+
() => new Map(stories.map((story) => [`${story.moduleTitle}/${story.storyName}`, story])),
|
|
30
|
+
[stories],
|
|
31
|
+
);
|
|
32
|
+
const firstStoryId = `${stories[0]!.moduleTitle}/${stories[0]!.storyName}`;
|
|
33
|
+
|
|
34
|
+
const [expandedIds, setExpandedIds] = useState<ReadonlySet<string>>(
|
|
35
|
+
() => new Set(treeNodes.filter((node) => node.children !== undefined).map((node) => node.id)),
|
|
36
|
+
);
|
|
37
|
+
const [selectedId, setSelectedId] = useState(firstStoryId);
|
|
38
|
+
const [openedId, setOpenedId] = useState(firstStoryId);
|
|
39
|
+
const [treeOpen, setTreeOpen] = useState(true);
|
|
40
|
+
|
|
41
|
+
const rows = flattenTree(treeNodes, { expandedIds });
|
|
42
|
+
const selectedRow = rows.find((row) => row.id === selectedId) ?? rows[0];
|
|
43
|
+
const opened = storiesById.get(openedId);
|
|
44
|
+
const sidebarScroll = useRef<ScrollBoxRenderable | null>(null);
|
|
45
|
+
|
|
46
|
+
// keep the moving selection inside the scrolled viewport
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
try {
|
|
49
|
+
sidebarScroll.current?.scrollChildIntoView(`tree-row-${selectedId}`);
|
|
50
|
+
} catch {
|
|
51
|
+
// reveal is best-effort; the selection state is already correct
|
|
52
|
+
}
|
|
53
|
+
}, [selectedId]);
|
|
54
|
+
|
|
55
|
+
const openStory = (id: string): void => {
|
|
56
|
+
if (storiesById.has(id)) setOpenedId(id);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const toggleFolder = (id: string): void => {
|
|
60
|
+
setExpandedIds((current) => {
|
|
61
|
+
const next = new Set(current);
|
|
62
|
+
if (next.has(id)) next.delete(id);
|
|
63
|
+
else next.add(id);
|
|
64
|
+
|
|
65
|
+
return next;
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const expandFolder = (id: string): void => {
|
|
70
|
+
setExpandedIds((current) => new Set(current).add(id));
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const collapseFolder = (id: string): void => {
|
|
74
|
+
setExpandedIds((current) => {
|
|
75
|
+
const next = new Set(current);
|
|
76
|
+
next.delete(id);
|
|
77
|
+
|
|
78
|
+
return next;
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const moveSelection = (delta: number): void => {
|
|
83
|
+
const index = rows.findIndex((row) => row.id === selectedId);
|
|
84
|
+
const nextIndex = Math.max(0, Math.min(rows.length - 1, index + delta));
|
|
85
|
+
const nextRow = rows[nextIndex];
|
|
86
|
+
if (nextRow !== undefined) setSelectedId(nextRow.id);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
useKeyboard((key) => {
|
|
90
|
+
if (key.name === "q") onExit();
|
|
91
|
+
else if (key.name === "j" || key.name === "down") moveSelection(1);
|
|
92
|
+
else if (key.name === "k" || key.name === "up") moveSelection(-1);
|
|
93
|
+
else if (selectedRow === undefined) return;
|
|
94
|
+
else if (key.name === "l" || key.name === "right") {
|
|
95
|
+
if (selectedRow.isFolder) expandFolder(selectedRow.id);
|
|
96
|
+
else openStory(selectedRow.id);
|
|
97
|
+
} else if (key.name === "h" || key.name === "left") {
|
|
98
|
+
if (selectedRow.isFolder) collapseFolder(selectedRow.id);
|
|
99
|
+
} else if (key.name === "return" || key.name === "space") {
|
|
100
|
+
if (selectedRow.isFolder) toggleFolder(selectedRow.id);
|
|
101
|
+
else openStory(selectedRow.id);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<ThemeProvider theme={DARK}>
|
|
107
|
+
<PaneGrid
|
|
108
|
+
theme={DARK}
|
|
109
|
+
mainHeader={
|
|
110
|
+
<ShellHeader
|
|
111
|
+
theme={DARK}
|
|
112
|
+
leftIcons={[{ glyph: NERD.settings }]}
|
|
113
|
+
leftLabel="cueloop storybook"
|
|
114
|
+
inspectorOpen={treeOpen}
|
|
115
|
+
onToggleInspector={treeOpen ? undefined : () => setTreeOpen(true)}
|
|
116
|
+
/>
|
|
117
|
+
}
|
|
118
|
+
inspectorHeader={
|
|
119
|
+
<ShellHeader
|
|
120
|
+
theme={DARK}
|
|
121
|
+
inspectorOpen={treeOpen}
|
|
122
|
+
onToggleInspector={() => setTreeOpen(false)}
|
|
123
|
+
/>
|
|
124
|
+
}
|
|
125
|
+
main={
|
|
126
|
+
opened !== undefined ? (
|
|
127
|
+
<scrollbox style={{ flexGrow: 1, paddingLeft: 1, paddingTop: 1 }} focused={false}>
|
|
128
|
+
{/* remount per story so component-local state never leaks across */}
|
|
129
|
+
<box key={openedId} style={{ flexDirection: "column" }}>
|
|
130
|
+
{opened.story.ansi ? (
|
|
131
|
+
<AnsiScreen
|
|
132
|
+
ansi={opened.story.ansi()}
|
|
133
|
+
cols={opened.story.size?.width ?? 80}
|
|
134
|
+
rows={opened.story.size?.height ?? 24}
|
|
135
|
+
/>
|
|
136
|
+
) : (
|
|
137
|
+
opened.story.render?.()
|
|
138
|
+
)}
|
|
139
|
+
</box>
|
|
140
|
+
</scrollbox>
|
|
141
|
+
) : (
|
|
142
|
+
<box style={{ flexGrow: 1, paddingLeft: 1, paddingTop: 1 }}>
|
|
143
|
+
<text fg={DARK.textDim}>Select a story</text>
|
|
144
|
+
</box>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
inspectorWidth={34}
|
|
148
|
+
inspector={
|
|
149
|
+
treeOpen ? (
|
|
150
|
+
<box style={{ flexDirection: "column", flexGrow: 1, paddingTop: 1 }}>
|
|
151
|
+
<scrollbox ref={sidebarScroll} style={{ flexGrow: 1 }} focused={false}>
|
|
152
|
+
<Tree
|
|
153
|
+
nodes={treeNodes}
|
|
154
|
+
expandedIds={expandedIds}
|
|
155
|
+
selectedId={selectedId}
|
|
156
|
+
theme={DARK}
|
|
157
|
+
onSelect={(id) => {
|
|
158
|
+
setSelectedId(id);
|
|
159
|
+
openStory(id);
|
|
160
|
+
}}
|
|
161
|
+
onToggle={(id) => {
|
|
162
|
+
setSelectedId(id);
|
|
163
|
+
toggleFolder(id);
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
</scrollbox>
|
|
167
|
+
</box>
|
|
168
|
+
) : undefined
|
|
169
|
+
}
|
|
170
|
+
footer={
|
|
171
|
+
<box style={{ height: 1, paddingLeft: 1 }}>
|
|
172
|
+
<text fg={DARK.textDim}>j/k move · h/l fold or open · enter open · q quit</text>
|
|
173
|
+
</box>
|
|
174
|
+
}
|
|
175
|
+
/>
|
|
176
|
+
</ThemeProvider>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (import.meta.main) {
|
|
181
|
+
const stories = await loadStories();
|
|
182
|
+
const renderer = await createCliRenderer();
|
|
183
|
+
|
|
184
|
+
createRoot(renderer).render(
|
|
185
|
+
<StoriesApp
|
|
186
|
+
stories={stories}
|
|
187
|
+
onExit={() => {
|
|
188
|
+
renderer.destroy();
|
|
189
|
+
queueMicrotask(() => process.exit(0));
|
|
190
|
+
}}
|
|
191
|
+
/>,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The catalog IS the regression suite: every exported story renders in
|
|
3
|
+
* a virtual terminal, its char frame snapshots, and its declared colors must
|
|
4
|
+
* appear among the styled spans. A component file without a colocated
|
|
5
|
+
* stories file fails the harness, so the safety net cannot silently thin.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
11
|
+
import { RGBA } from "@opentui/core";
|
|
12
|
+
import { componentFilesMissingStories, loadStories } from "./story";
|
|
13
|
+
import { AnsiScreen } from "./AnsiScreen";
|
|
14
|
+
import { settle } from "../test-support";
|
|
15
|
+
|
|
16
|
+
const stories = await loadStories();
|
|
17
|
+
|
|
18
|
+
/** Hex of a painted color; undefined for unpainted (transparent) spans. */
|
|
19
|
+
function hexOf(color: RGBA | null | undefined): string | undefined {
|
|
20
|
+
if (!color) return undefined;
|
|
21
|
+
const [red, green, blue] = color.toInts();
|
|
22
|
+
|
|
23
|
+
return "#" + [red, green, blue].map((part) => part!.toString(16).padStart(2, "0")).join("");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("stories catalog", () => {
|
|
27
|
+
test("every component file ships stories", () => {
|
|
28
|
+
expect(componentFilesMissingStories()).toEqual([]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("the catalog is not empty", () => {
|
|
32
|
+
expect(stories.length).toBeGreaterThan(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
for (const { moduleTitle, storyName, story } of stories) {
|
|
36
|
+
test(`${moduleTitle}/${storyName} renders, snapshots, and carries its colors`, async () => {
|
|
37
|
+
// Arrange
|
|
38
|
+
const size = story.size ?? { width: 80, height: 24 };
|
|
39
|
+
const node = story.ansi ? (
|
|
40
|
+
<AnsiScreen ansi={story.ansi()} cols={size.width} rows={size.height} />
|
|
41
|
+
) : (
|
|
42
|
+
story.render!()
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Act
|
|
46
|
+
const setup = await testRender(node, size);
|
|
47
|
+
|
|
48
|
+
// settle, not just visual-idle: async stories (a file read, a virtualizer's
|
|
49
|
+
// first measure) commit after React's own scheduler, invisible to idle
|
|
50
|
+
await settle(setup);
|
|
51
|
+
const frame = setup.captureCharFrame();
|
|
52
|
+
|
|
53
|
+
// Assert
|
|
54
|
+
expect(frame).toMatchSnapshot(`${moduleTitle}/${storyName}`);
|
|
55
|
+
if (story.expectedColors?.length) {
|
|
56
|
+
const seenColors = new Set<string>();
|
|
57
|
+
|
|
58
|
+
for (const line of setup.captureSpans().lines) {
|
|
59
|
+
for (const span of line.spans) {
|
|
60
|
+
const foregroundHex = hexOf(span.fg);
|
|
61
|
+
const backgroundHex = hexOf(span.bg);
|
|
62
|
+
|
|
63
|
+
if (foregroundHex) seenColors.add(foregroundHex);
|
|
64
|
+
if (backgroundHex) seenColors.add(backgroundHex);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const expectedColor of story.expectedColors) {
|
|
68
|
+
expect(seenColors).toContain(expectedColor.toLowerCase());
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
setup.renderer.destroy();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Shared fixture data for the stories catalog: one plan session, one diff. */
|
|
2
|
+
|
|
3
|
+
import type { Annotation, Thread } from "@cueloop/schema";
|
|
4
|
+
import { buildDisplay, marksByDisplay } from "../view-plan";
|
|
5
|
+
import { diffRows } from "../view-diff";
|
|
6
|
+
|
|
7
|
+
export const FIXTURE_PLAN = `# Migration Plan
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
The daemon persists sessions to disk atomically.
|
|
12
|
+
|
|
13
|
+
## Steps
|
|
14
|
+
|
|
15
|
+
- move the store
|
|
16
|
+
- add recovery
|
|
17
|
+
|
|
18
|
+
\`\`\`ts
|
|
19
|
+
export function persist(session: Session): void {
|
|
20
|
+
// one JSON document per session
|
|
21
|
+
writeAtomically(pathFor(session.id), serialize(session));
|
|
22
|
+
}
|
|
23
|
+
\`\`\`
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
export const FIXTURE_PATCH = `diff --git a/src/store.ts b/src/store.ts
|
|
27
|
+
index 111..222 100644
|
|
28
|
+
--- a/src/store.ts
|
|
29
|
+
+++ b/src/store.ts
|
|
30
|
+
@@ -1,4 +1,4 @@
|
|
31
|
+
export class Store {
|
|
32
|
+
- private items = [];
|
|
33
|
+
+ private items = new Map();
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
export const FIXTURE_ANNOTATIONS: Annotation[] = [
|
|
38
|
+
{
|
|
39
|
+
id: "a_story_1",
|
|
40
|
+
kind: "comment",
|
|
41
|
+
anchor: { quote: "persists sessions", prefix: "The daemon ", suffix: " to disk" },
|
|
42
|
+
body: "Which daemon owns this?",
|
|
43
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "a_story_2",
|
|
47
|
+
kind: "comment",
|
|
48
|
+
anchor: { quote: "move the store", prefix: "", suffix: "" },
|
|
49
|
+
body: "move the store behind one interface",
|
|
50
|
+
createdAt: "2026-01-01T00:00:01Z",
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
export function fixturePlanSession(overrides: Partial<Thread> = {}): Thread {
|
|
55
|
+
return {
|
|
56
|
+
schemaVersion: "1",
|
|
57
|
+
id: "s_story_plan",
|
|
58
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
59
|
+
artifact: {
|
|
60
|
+
type: "plan",
|
|
61
|
+
content: FIXTURE_PLAN,
|
|
62
|
+
meta: { title: "Migration Plan", agent: "agent/worker-3" },
|
|
63
|
+
},
|
|
64
|
+
revisions: [{ revision: 1, content: FIXTURE_PLAN, submittedAt: "2026-01-01T00:00:00Z" }],
|
|
65
|
+
annotations: FIXTURE_ANNOTATIONS,
|
|
66
|
+
message: null,
|
|
67
|
+
status: "pending",
|
|
68
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
69
|
+
...overrides,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function fixtureDiffSession(overrides: Partial<Thread> = {}): Thread {
|
|
74
|
+
return {
|
|
75
|
+
schemaVersion: "1",
|
|
76
|
+
id: "s_story_diff",
|
|
77
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
78
|
+
artifact: {
|
|
79
|
+
type: "diff",
|
|
80
|
+
content: FIXTURE_PATCH,
|
|
81
|
+
meta: { title: "working tree", agent: "agent/worker-3" },
|
|
82
|
+
},
|
|
83
|
+
revisions: [{ revision: 1, content: FIXTURE_PATCH, submittedAt: "2026-01-01T00:00:00Z" }],
|
|
84
|
+
annotations: [],
|
|
85
|
+
message: null,
|
|
86
|
+
status: "pending",
|
|
87
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
88
|
+
...overrides,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function fixtureDisplay() {
|
|
93
|
+
return buildDisplay(FIXTURE_PLAN);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function fixtureMarks() {
|
|
97
|
+
return marksByDisplay(FIXTURE_ANNOTATIONS, fixtureDisplay());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function fixtureDiffRows() {
|
|
101
|
+
return diffRows(FIXTURE_PATCH);
|
|
102
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { buildStoryTree, type LoadedStory, type StorySection } from "./story";
|
|
3
|
+
import type { TreeNode } from "./primitives/tree-model";
|
|
4
|
+
|
|
5
|
+
/** A story object whose render is never invoked in these structural tests. */
|
|
6
|
+
const STUB = { render: () => null };
|
|
7
|
+
|
|
8
|
+
function story(moduleTitle: string, storyName: string, section: StorySection = "App"): LoadedStory {
|
|
9
|
+
return { section, moduleTitle, storyName, story: STUB };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function childLabels(node: TreeNode): string[] {
|
|
13
|
+
return (node.children ?? []).map((child) => child.label);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("buildStoryTree", () => {
|
|
17
|
+
test("groups the top level by section", () => {
|
|
18
|
+
const tree = buildStoryTree([
|
|
19
|
+
story("Primitives/Tree", "Default"),
|
|
20
|
+
story("Collaborator Join", "JoinSplash", "SSH"),
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
expect(tree.map((node) => [node.id, node.label])).toEqual([
|
|
24
|
+
["App", "App"],
|
|
25
|
+
["SSH", "SSH"],
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("splits moduleTitle on / into nested folders under the section", () => {
|
|
30
|
+
// Arrange
|
|
31
|
+
const stories = [story("Primitives/Tree", "Default"), story("Layout/AppShell", "Default")];
|
|
32
|
+
|
|
33
|
+
// Act
|
|
34
|
+
const app = buildStoryTree(stories)[0]!;
|
|
35
|
+
|
|
36
|
+
// Assert
|
|
37
|
+
expect(app.children!.map((node) => [node.id, node.label])).toEqual([
|
|
38
|
+
["App/Primitives", "Primitives"],
|
|
39
|
+
["App/Layout", "Layout"],
|
|
40
|
+
]);
|
|
41
|
+
expect(app.children![0]!.children!.map((node) => node.id)).toEqual(["App/Primitives/Tree"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("groups sibling components under one shared parent folder", () => {
|
|
45
|
+
// Arrange
|
|
46
|
+
const stories = [story("Primitives/Tree", "Default"), story("Primitives/Card", "Default")];
|
|
47
|
+
|
|
48
|
+
// Act
|
|
49
|
+
const app = buildStoryTree(stories)[0]!;
|
|
50
|
+
|
|
51
|
+
// Assert
|
|
52
|
+
expect(app.children).toHaveLength(1);
|
|
53
|
+
expect(childLabels(app.children![0]!)).toEqual(["Tree", "Card"]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("nests to the full depth of the moduleTitle path", () => {
|
|
57
|
+
// Arrange
|
|
58
|
+
const stories = [story("A/B/C", "Only")];
|
|
59
|
+
|
|
60
|
+
// Act
|
|
61
|
+
const a = buildStoryTree(stories)[0]!.children![0]!;
|
|
62
|
+
const b = a.children![0]!;
|
|
63
|
+
|
|
64
|
+
// Assert
|
|
65
|
+
expect([a.id, b.id]).toEqual(["App/A", "App/A/B"]);
|
|
66
|
+
expect(b.children![0]!.id).toBe("App/A/B/C");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("adds each story as a leaf keyed `${moduleTitle}/${storyName}` under its component", () => {
|
|
70
|
+
// Arrange
|
|
71
|
+
const stories = [story("Primitives/Tree", "Empty"), story("Primitives/Tree", "Deep")];
|
|
72
|
+
|
|
73
|
+
// Act
|
|
74
|
+
const component = buildStoryTree(stories)[0]!.children![0]!.children![0]!;
|
|
75
|
+
|
|
76
|
+
// Assert
|
|
77
|
+
expect(component.id).toBe("App/Primitives/Tree");
|
|
78
|
+
expect(component.children!.map((leaf) => [leaf.id, leaf.label])).toEqual([
|
|
79
|
+
["Primitives/Tree/Empty", "Empty"],
|
|
80
|
+
["Primitives/Tree/Deep", "Deep"],
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("hoists a component with a single story of its own name into one leaf row", () => {
|
|
85
|
+
// Arrange
|
|
86
|
+
const stories = [story("Primitives/Card", "Card")];
|
|
87
|
+
|
|
88
|
+
// Act
|
|
89
|
+
const hoisted = buildStoryTree(stories)[0]!.children![0]!.children![0]!;
|
|
90
|
+
|
|
91
|
+
// Assert
|
|
92
|
+
expect(hoisted.children).toBeUndefined();
|
|
93
|
+
expect(hoisted.id).toBe("Primitives/Card/Card");
|
|
94
|
+
expect(hoisted.label).toBe("Card");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("keeps the folder when its single story differs from the component name", () => {
|
|
98
|
+
// Arrange
|
|
99
|
+
const stories = [story("Primitives/Card", "Default")];
|
|
100
|
+
|
|
101
|
+
// Act
|
|
102
|
+
const component = buildStoryTree(stories)[0]!.children![0]!.children![0]!;
|
|
103
|
+
|
|
104
|
+
// Assert
|
|
105
|
+
expect(component.id).toBe("App/Primitives/Card");
|
|
106
|
+
expect(component.children!.map((leaf) => leaf.id)).toEqual(["Primitives/Card/Default"]);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The story contract. Every component file in components/ ships a
|
|
3
|
+
* colocated *.stories.tsx that exports `meta` plus named Story objects.
|
|
4
|
+
* The catalog IS the regression suite: stories.test.tsx walks every story,
|
|
5
|
+
* snapshots its char frame, and asserts declared colors against the styled
|
|
6
|
+
* spans; the `bun run stories` TUI renders the same objects interactively.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type React from "react";
|
|
10
|
+
import type { TreeNode } from "./primitives/tree-model";
|
|
11
|
+
|
|
12
|
+
export interface StoryMeta {
|
|
13
|
+
/** Catalog path, e.g. "primitives/Card". */
|
|
14
|
+
title: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Story {
|
|
18
|
+
render?: () => React.ReactNode;
|
|
19
|
+
/** Raw ANSI to paint through AnsiScreen instead of a React tree; exactly one of render or ansi is set. */
|
|
20
|
+
ansi?: () => string;
|
|
21
|
+
/**
|
|
22
|
+
* Colors (hex tokens) that must appear among the rendered frame's styled
|
|
23
|
+
* spans - the color regression net for color-bearing stories.
|
|
24
|
+
*/
|
|
25
|
+
expectedColors?: string[];
|
|
26
|
+
/** Virtual terminal size; defaults to 80x24. */
|
|
27
|
+
size?: { width: number; height: number };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type StorySection = "App" | "SSH";
|
|
31
|
+
|
|
32
|
+
export interface LoadedStory {
|
|
33
|
+
section: StorySection;
|
|
34
|
+
moduleTitle: string;
|
|
35
|
+
storyName: string;
|
|
36
|
+
story: Story;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isStory(value: unknown): value is Story {
|
|
40
|
+
if (typeof value !== "object" || value === null) return false;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
("render" in value && typeof value.render === "function") ||
|
|
44
|
+
("ansi" in value && typeof value.ansi === "function")
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isStoryMeta(value: unknown): value is StoryMeta {
|
|
49
|
+
return (
|
|
50
|
+
typeof value === "object" &&
|
|
51
|
+
value !== null &&
|
|
52
|
+
"title" in value &&
|
|
53
|
+
typeof value.title === "string"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const GATEWAY_SRC_DIR = `${import.meta.dir}/../../../gateway/src`;
|
|
58
|
+
|
|
59
|
+
async function collectStories(
|
|
60
|
+
dir: string,
|
|
61
|
+
pattern: string,
|
|
62
|
+
section: StorySection,
|
|
63
|
+
loaded: LoadedStory[],
|
|
64
|
+
): Promise<void> {
|
|
65
|
+
const glob = new Bun.Glob(pattern);
|
|
66
|
+
const files = [...glob.scanSync({ cwd: dir })].sort();
|
|
67
|
+
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
const moduleExports = await import(`${dir}/${file}`);
|
|
70
|
+
const meta = moduleExports["meta"];
|
|
71
|
+
const moduleTitle = isStoryMeta(meta) ? meta.title : file.replace(/\.stories\.tsx?$/, "");
|
|
72
|
+
|
|
73
|
+
for (const [exportName, exported] of Object.entries(moduleExports)) {
|
|
74
|
+
if (exportName === "meta" || !isStory(exported)) continue;
|
|
75
|
+
loaded.push({ section, moduleTitle, storyName: exportName, story: exported });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function loadStories(): Promise<LoadedStory[]> {
|
|
81
|
+
const loaded: LoadedStory[] = [];
|
|
82
|
+
|
|
83
|
+
await collectStories(import.meta.dir, "**/*.stories.tsx", "App", loaded);
|
|
84
|
+
await collectStories(GATEWAY_SRC_DIR, "**/*.stories.ts", "SSH", loaded);
|
|
85
|
+
|
|
86
|
+
return loaded;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A folder node keeps its children array live so the builder can append to it. */
|
|
90
|
+
interface StoryTreeFolder extends TreeNode {
|
|
91
|
+
children: TreeNode[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A component folder holding one story of its own name reads as one row, not a folder wrapping a single leaf.
|
|
95
|
+
function hoistSingleStoryFolders(nodes: TreeNode[]): TreeNode[] {
|
|
96
|
+
return nodes.map((node) => {
|
|
97
|
+
if (node.children === undefined) return node;
|
|
98
|
+
const children = hoistSingleStoryFolders(node.children);
|
|
99
|
+
const onlyChild = children.length === 1 ? children[0] : undefined;
|
|
100
|
+
if (
|
|
101
|
+
onlyChild !== undefined &&
|
|
102
|
+
onlyChild.children === undefined &&
|
|
103
|
+
onlyChild.label === node.label
|
|
104
|
+
) {
|
|
105
|
+
return onlyChild;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { ...node, children };
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Nest the flat catalog into folder/component groups by splitting each
|
|
114
|
+
* moduleTitle on "/". Each story becomes a leaf keyed `${moduleTitle}/${storyName}`
|
|
115
|
+
* under its component node; a component holding a single story of its own name
|
|
116
|
+
* collapses so its row IS that story leaf.
|
|
117
|
+
*/
|
|
118
|
+
export function buildStoryTree(stories: LoadedStory[]): TreeNode[] {
|
|
119
|
+
const roots: TreeNode[] = [];
|
|
120
|
+
const foldersByPath = new Map<string, StoryTreeFolder>();
|
|
121
|
+
|
|
122
|
+
for (const { section, moduleTitle, storyName } of stories) {
|
|
123
|
+
let siblings = roots;
|
|
124
|
+
let path = "";
|
|
125
|
+
|
|
126
|
+
for (const segment of [section, ...moduleTitle.split("/")]) {
|
|
127
|
+
path = path === "" ? segment : `${path}/${segment}`;
|
|
128
|
+
let folder = foldersByPath.get(path);
|
|
129
|
+
if (folder === undefined) {
|
|
130
|
+
folder = { id: path, label: segment, children: [] };
|
|
131
|
+
foldersByPath.set(path, folder);
|
|
132
|
+
siblings.push(folder);
|
|
133
|
+
}
|
|
134
|
+
siblings = folder.children;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
siblings.push({ id: `${moduleTitle}/${storyName}`, label: storyName });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return hoistSingleStoryFolders(roots);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Component files that must carry stories: every .tsx that is not a story/test/prototype. */
|
|
144
|
+
export function componentFilesMissingStories(): string[] {
|
|
145
|
+
const componentGlob = new Bun.Glob("**/*.tsx");
|
|
146
|
+
const files = [...componentGlob.scanSync({ cwd: import.meta.dir })];
|
|
147
|
+
const missing: string[] = [];
|
|
148
|
+
|
|
149
|
+
for (const file of files) {
|
|
150
|
+
if (file.endsWith(".stories.tsx") || file.endsWith(".test.tsx")) continue;
|
|
151
|
+
if (file.endsWith(".prototype.tsx")) continue;
|
|
152
|
+
if (file === "stories-app.tsx") continue;
|
|
153
|
+
const storiesSibling = file.replace(/\.tsx$/, ".stories.tsx");
|
|
154
|
+
|
|
155
|
+
if (!files.includes(storiesSibling)) missing.push(file);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return missing;
|
|
159
|
+
}
|