@cueloop/client 0.1.0-alpha.64 → 0.1.0-alpha.66

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.
Files changed (109) hide show
  1. package/package.json +5 -5
  2. package/src/App.autoclose.test.tsx +2 -2
  3. package/src/App.diff.test.tsx +8 -73
  4. package/src/App.export.test.tsx +2 -2
  5. package/src/App.plan-review.test.tsx +60 -412
  6. package/src/App.prototype.test.tsx +1 -8
  7. package/src/App.readonly.test.tsx +85 -62
  8. package/src/App.rendering.test.tsx +5 -6
  9. package/src/App.submit-confirm.test.tsx +32 -70
  10. package/src/App.test.tsx +136 -66
  11. package/src/App.toast.test.tsx +17 -8
  12. package/src/App.tsx +243 -284
  13. package/src/App.walk.test.tsx +1 -1
  14. package/src/app-screens.tsx +67 -52
  15. package/src/app-view-model.ts +6 -100
  16. package/src/components/AnnotationCard.stories.tsx +1 -1
  17. package/src/components/AnnotationCard.tsx +2 -2
  18. package/src/components/AppShell.stories.tsx +84 -0
  19. package/src/components/AppShell.tsx +127 -0
  20. package/src/components/Breadcrumb.stories.tsx +1 -1
  21. package/src/components/ChangesColumn.stories.tsx +39 -0
  22. package/src/components/ChangesColumn.tsx +147 -0
  23. package/src/components/CodeBlock.stories.tsx +1 -1
  24. package/src/components/CompletionOverlay.stories.tsx +1 -1
  25. package/src/components/ConfirmCard.stories.tsx +1 -1
  26. package/src/components/ConfirmCard.tsx +3 -4
  27. package/src/components/ConfirmDialog.stories.tsx +1 -1
  28. package/src/components/DiffSheet.stories.tsx +1 -1
  29. package/src/components/InboxList.stories.tsx +23 -8
  30. package/src/components/InboxList.tsx +45 -19
  31. package/src/components/KeybindsDialog.stories.tsx +1 -1
  32. package/src/components/KeybindsDialog.tsx +61 -17
  33. package/src/components/MarkerPopover.stories.tsx +1 -1
  34. package/src/components/MenuBar.stories.tsx +3 -12
  35. package/src/components/MenuBar.tsx +25 -56
  36. package/src/components/PromptDialog.stories.tsx +1 -1
  37. package/src/components/PrototypeSheet.stories.tsx +1 -1
  38. package/src/components/ReviewPanel.stories.tsx +15 -1
  39. package/src/components/ReviewRail.stories.tsx +15 -1
  40. package/src/components/ReviewRail.tsx +13 -6
  41. package/src/components/SettingsDialog.stories.tsx +7 -1
  42. package/src/components/SettingsDialog.tsx +112 -66
  43. package/src/components/SettingsRows.stories.tsx +1 -1
  44. package/src/components/ShellHeader.stories.tsx +50 -0
  45. package/src/components/ShellHeader.tsx +137 -0
  46. package/src/components/ThreadFooter.stories.tsx +30 -0
  47. package/src/components/ThreadFooter.tsx +49 -0
  48. package/src/components/ThreadView.selection.test.tsx +812 -0
  49. package/src/components/ThreadView.slash.test.tsx +113 -0
  50. package/src/components/ThreadView.stories.tsx +26 -0
  51. package/src/components/ThreadView.tsx +1708 -0
  52. package/src/components/ThreadsSidebar.stories.tsx +36 -0
  53. package/src/components/ThreadsSidebar.tsx +57 -0
  54. package/src/components/Toast.stories.tsx +1 -1
  55. package/src/components/TreePane.stories.tsx +106 -0
  56. package/src/components/TreePane.tsx +156 -0
  57. package/src/components/WalkWizard.stories.tsx +1 -1
  58. package/src/components/__snapshots__/stories.test.tsx.snap +463 -305
  59. package/src/components/agent-launcher.stories.tsx +1 -1
  60. package/src/components/file-tree.test.ts +45 -0
  61. package/src/components/file-tree.ts +57 -0
  62. package/src/components/pages/Workspace.stories.tsx +181 -0
  63. package/src/components/primitives/Button.stories.tsx +1 -1
  64. package/src/components/primitives/Card.stories.tsx +1 -1
  65. package/src/components/primitives/Dialog.stories.tsx +1 -1
  66. package/src/components/primitives/IconButton.stories.tsx +36 -0
  67. package/src/components/primitives/IconButton.tsx +43 -0
  68. package/src/components/primitives/StatusBar.stories.tsx +1 -1
  69. package/src/components/primitives/Tabs.stories.tsx +1 -1
  70. package/src/components/primitives/Toolbar.stories.tsx +1 -1
  71. package/src/components/primitives/Tree.stories.tsx +124 -0
  72. package/src/components/primitives/Tree.tsx +87 -0
  73. package/src/components/primitives/icons.ts +38 -0
  74. package/src/components/primitives/tree-model.test.ts +99 -0
  75. package/src/components/primitives/tree-model.ts +137 -0
  76. package/src/components/quick-actions-editor.stories.tsx +1 -1
  77. package/src/components/session-tree.test.ts +114 -0
  78. package/src/components/session-tree.ts +94 -0
  79. package/src/components/stories-app.tsx +142 -82
  80. package/src/components/story-tree.test.ts +100 -0
  81. package/src/components/story.ts +55 -0
  82. package/src/components/theme-context.stories.tsx +1 -1
  83. package/src/diff-hunk-curate.ts +4 -104
  84. package/src/index.ts +1 -0
  85. package/src/intent-dispatch.test.ts +136 -3
  86. package/src/intent-dispatch.ts +92 -24
  87. package/src/key-bindings.test.ts +1 -1
  88. package/src/keymap.test.ts +4 -4
  89. package/src/keymap.ts +14 -7
  90. package/src/session-controller.curate.test.ts +89 -2
  91. package/src/session-controller.share.test.ts +195 -55
  92. package/src/session-controller.tree.test.ts +228 -0
  93. package/src/session-controller.ts +331 -72
  94. package/src/share.ts +114 -16
  95. package/src/test-support.test.ts +193 -1
  96. package/src/test-support.ts +64 -0
  97. package/src/theme.ts +4 -4
  98. package/src/thread-chords.test.ts +154 -0
  99. package/src/thread-chords.ts +144 -0
  100. package/src/thread-selection.test.ts +210 -0
  101. package/src/thread-selection.ts +243 -0
  102. package/src/tree-view.test.ts +128 -0
  103. package/src/tree-view.ts +148 -0
  104. package/src/use-settings-dialog.tsx +9 -0
  105. package/src/view-plan.ts +29 -12
  106. package/src/App.review-panel.test.tsx +0 -143
  107. package/src/components/PlanSheet.stories.tsx +0 -113
  108. package/src/components/PlanSheet.tsx +0 -419
  109. package/src/components/plan-sheet-run-style.ts +0 -88
@@ -5,10 +5,24 @@ import type { Story, StoryMeta } from "./story";
5
5
  import { ReviewPanel } from "./ReviewPanel";
6
6
  import { FIXTURE_ANNOTATIONS, fixturePlanSession } from "./story-fixtures";
7
7
 
8
- export const meta: StoryMeta = { title: "ReviewPanel" };
8
+ export const meta: StoryMeta = { title: "Surfaces/ReviewPanel" };
9
+
10
+ /** An empty Tree tab: the stories here show the review tab. */
11
+ const tree = {
12
+ rows: [],
13
+ canMove: true,
14
+ canFork: true,
15
+ onSelect: () => {},
16
+ onGo: () => {},
17
+ onBranch: () => {},
18
+ onLabel: () => {},
19
+ onFork: () => {},
20
+ onForkAndShare: () => {},
21
+ };
9
22
 
10
23
  const rail = {
11
24
  session: fixturePlanSession(),
25
+ tree,
12
26
  authorNames: {},
13
27
  selectedId: FIXTURE_ANNOTATIONS[0]!.id,
14
28
  resolvedIds: new Set(FIXTURE_ANNOTATIONS.map((annotation) => annotation.id)),
@@ -4,10 +4,24 @@ import type { Story, StoryMeta } from "./story";
4
4
  import { ReviewRail } from "./ReviewRail";
5
5
  import { FIXTURE_ANNOTATIONS, fixtureDiffSession, fixturePlanSession } from "./story-fixtures";
6
6
 
7
- export const meta: StoryMeta = { title: "ReviewRail" };
7
+ export const meta: StoryMeta = { title: "Surfaces/ReviewRail" };
8
+
9
+ /** An empty Tree tab: the stories here show the review tab. */
10
+ const tree = {
11
+ rows: [],
12
+ canMove: true,
13
+ canFork: true,
14
+ onSelect: () => {},
15
+ onGo: () => {},
16
+ onBranch: () => {},
17
+ onLabel: () => {},
18
+ onFork: () => {},
19
+ onForkAndShare: () => {},
20
+ };
8
21
 
9
22
  const callbacks = {
10
23
  curationItems: [],
24
+ tree,
11
25
  onTabChange: () => {},
12
26
  onSelectCard: () => {},
13
27
  onActivateCard: () => {},
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The right review rail: tab strip (Review/Agent), the scrollable annotation
2
+ * The right review rail: tab strip (Review/Tree/Agent), the scrollable annotation
3
3
  * stack, and the pinned submit affordance. The confirm card sits OUTSIDE the
4
4
  * scrollbox so the stack scrolls while the card stays at the rail bottom.
5
5
  * Card reveal goes through the scrollbox's own scrollChildIntoView - no
@@ -18,11 +18,12 @@ import { Toolbar } from "./primitives/Toolbar";
18
18
  import { AnnotationCard, type AnnotationDraft } from "./AnnotationCard";
19
19
  import { ConfirmCard, type ConfirmCardProps } from "./ConfirmCard";
20
20
  import { AgentLauncher, type AgentTerminalHandle } from "./agent-launcher";
21
+ import { TreePane, type TreePaneProps } from "./TreePane";
21
22
  import { Card } from "./primitives/Card";
22
23
  import { truncateToSingleLine } from "./truncate-text";
23
24
  import { resolveDisplayName } from "../attribution";
24
25
 
25
- export type RailTab = "review" | "agent";
26
+ export type RailTab = "review" | "agent" | "tree";
26
27
 
27
28
  export interface RailCardEdit extends AnnotationDraft {
28
29
  id: string;
@@ -65,6 +66,8 @@ export interface ReviewRailProps {
65
66
  onLaunchHarness: (command: string, seedText?: string) => void;
66
67
  /** In-tab agent terminal handle (embedded path); null when detached. Lets the app route keys to it. */
67
68
  onAgentTerminal?: (handle: AgentTerminalHandle | null) => void;
69
+ /** The Tree tab: the session's history rows and the primitives that move it. */
70
+ tree: Omit<TreePaneProps, "theme">;
68
71
  /** Rail column width; the app derives it from the persisted review layout. */
69
72
  width?: number;
70
73
  /**
@@ -95,6 +98,7 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
95
98
  onSubmitRequest,
96
99
  onLaunchHarness,
97
100
  onAgentTerminal,
101
+ tree,
98
102
  width = 34,
99
103
  onCollapse,
100
104
  theme,
@@ -188,16 +192,19 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
188
192
  <Tabs
189
193
  selectedKey={railTab}
190
194
  onSelectionChange={(key) => {
191
- if (key === "review" || key === "agent") onTabChange(key);
195
+ if (key === "review" || key === "agent" || key === "tree") onTabChange(key);
192
196
  }}
193
197
  theme={theme}
194
198
  >
195
199
  <TabList>
196
200
  <Tab id="review">Review</Tab>
201
+ <Tab id="tree">Tree</Tab>
197
202
  <Tab id="agent">Agent</Tab>
198
203
  </TabList>
199
204
  </Tabs>
200
- {railTab === "agent" ? (
205
+ {railTab === "tree" ? (
206
+ <TreePane {...tree} theme={theme} />
207
+ ) : railTab === "agent" ? (
201
208
  <box style={{ flexGrow: 1, flexDirection: "column" }}>
202
209
  <AgentLauncher
203
210
  session={session}
@@ -223,7 +230,7 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
223
230
  </box>
224
231
  )}
225
232
  {/* the confirm card keeps full width, above the footer row */}
226
- {railTab !== "agent" && submitConfirm ? (
233
+ {railTab === "review" && submitConfirm ? (
227
234
  <ConfirmCard {...submitConfirm} theme={theme} />
228
235
  ) : null}
229
236
  {/* footer row: collapse chevron left-bound, the submit affordance inline
@@ -234,7 +241,7 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
234
241
  <text fg={tokens.textDim}>{">"}</text>
235
242
  </box>
236
243
  ) : null}
237
- {railTab !== "agent" && !submitConfirm ? (
244
+ {railTab === "review" && !submitConfirm ? (
238
245
  session.status === "resolved" ? (
239
246
  <text fg={tokens.green}>resolved: {session.verdict!.kind.replace("_", " ")}</text>
240
247
  ) : (
@@ -4,7 +4,7 @@ import { THEME_PRESETS } from "../theme-presets";
4
4
  import type { Story, StoryMeta } from "./story";
5
5
  import { SettingsDialog, type SettingsCategory } from "./SettingsDialog";
6
6
 
7
- export const meta: StoryMeta = { title: "SettingsDialog" };
7
+ export const meta: StoryMeta = { title: "Overlays/SettingsDialog" };
8
8
 
9
9
  const CATEGORIES: SettingsCategory[] = [
10
10
  {
@@ -62,6 +62,8 @@ export const NavZone: Story = {
62
62
  render: () => (
63
63
  <SettingsDialog
64
64
  isOpen
65
+ version="0.1.0-alpha.32"
66
+ keybindsSections={[]}
65
67
  categories={CATEGORIES}
66
68
  values={VALUES}
67
69
  activeCategoryId="general"
@@ -79,6 +81,8 @@ export const BodyZone: Story = {
79
81
  render: () => (
80
82
  <SettingsDialog
81
83
  isOpen
84
+ version="0.1.0-alpha.32"
85
+ keybindsSections={[]}
82
86
  categories={CATEGORIES}
83
87
  values={VALUES}
84
88
  activeCategoryId="display"
@@ -96,6 +100,8 @@ export const AppearanceThemed: Story = {
96
100
  render: () => (
97
101
  <SettingsDialog
98
102
  isOpen
103
+ version="0.1.0-alpha.32"
104
+ keybindsSections={[]}
99
105
  categories={CATEGORIES}
100
106
  values={VALUES}
101
107
  activeCategoryId="appearance"
@@ -1,17 +1,20 @@
1
1
  /**
2
- * The settings overlay, promoted from the design prototype: a centered
3
- * Dialog with a left category nav and a right body of typed rows, all driven
4
- * by one settings record. The component is controlled - the caller owns the
5
- * open flag, the active category/row/zone, and value changes - so the app's
6
- * keyboard grammar (and the stories catalog) can drive it without a second
7
- * key handler inside. The modal sizes itself from the terminal dimensions.
2
+ * The settings overlay: a centered Dialog whose left nav is a tree - a Settings
3
+ * group over its categories, plus Keybinds as a sibling leaf - and whose right
4
+ * body shows the active category's typed rows or the keybinds cheatsheet. The
5
+ * component is controlled: the caller owns the open flag, the active
6
+ * category/row/zone, and value changes, so the app's keyboard grammar (and the
7
+ * stories catalog) drive it without a second key handler inside.
8
8
  */
9
9
 
10
10
  import React from "react";
11
11
  import { useTerminalDimensions } from "@opentui/react";
12
12
  import type { Theme } from "../theme";
13
+ import type { CheatsheetSection } from "../key-bindings";
13
14
  import { useComponentTheme } from "./theme-context";
14
15
  import { Dialog } from "./primitives/Dialog";
16
+ import { Tree } from "./primitives/Tree";
17
+ import type { TreeNode } from "./primitives/tree-model";
15
18
  import { CycleRow, TextRow, ToggleRow, type SettingsRowDescriptor } from "./SettingsRows";
16
19
 
17
20
  export interface SettingsCategory {
@@ -25,10 +28,17 @@ export interface SettingsCategory {
25
28
 
26
29
  export type SettingsValues = Record<string, string | boolean>;
27
30
 
31
+ /** The category id whose right pane shows the keybinds cheatsheet rather than rows. */
32
+ const KEYBINDS_CATEGORY_ID = "keybinds";
33
+
28
34
  export interface SettingsDialogProps {
29
35
  isOpen: boolean;
36
+ /** The client version, shown in the dialog footer (its home now the menu bar is gone). */
37
+ version: string;
30
38
  categories: SettingsCategory[];
31
39
  values: SettingsValues;
40
+ /** The keybinds cheatsheet, shown when the Keybinds leaf is active. */
41
+ keybindsSections: CheatsheetSection[];
32
42
  activeCategoryId: string;
33
43
  activeRowIndex: number;
34
44
  activeZone: "nav" | "body";
@@ -37,10 +47,50 @@ export interface SettingsDialogProps {
37
47
  theme?: Theme;
38
48
  }
39
49
 
50
+ /** The left nav tree: a Settings group over its categories, then Keybinds as a leaf. */
51
+ function navTree(categories: SettingsCategory[]): TreeNode[] {
52
+ const children = categories
53
+ .filter((category) => category.id !== KEYBINDS_CATEGORY_ID)
54
+ .map((category) => ({ id: category.id, label: category.name }));
55
+ const nodes: TreeNode[] = [{ id: "settings", label: "Settings", children }];
56
+ const keybinds = categories.find((category) => category.id === KEYBINDS_CATEGORY_ID);
57
+
58
+ if (keybinds !== undefined) nodes.push({ id: keybinds.id, label: keybinds.name });
59
+
60
+ return nodes;
61
+ }
62
+
63
+ function KeybindsBody({
64
+ sections,
65
+ tokens,
66
+ }: {
67
+ sections: CheatsheetSection[];
68
+ tokens: Theme;
69
+ }): React.ReactNode {
70
+ return (
71
+ <scrollbox style={{ flexGrow: 1 }} focused={false}>
72
+ {sections.map((section) => (
73
+ <box key={section.title} style={{ flexDirection: "column" }}>
74
+ <text fg={tokens.accent}>{section.title}</text>
75
+ {section.entries.map((entry, index) => (
76
+ <text key={`${section.title}-${index}`}>
77
+ <span fg={tokens.text}>{entry.keys.padEnd(12)}</span>
78
+ <span fg={tokens.textMuted}>{entry.label}</span>
79
+ </text>
80
+ ))}
81
+ <text> </text>
82
+ </box>
83
+ ))}
84
+ </scrollbox>
85
+ );
86
+ }
87
+
40
88
  export function SettingsDialog({
41
89
  isOpen,
90
+ version,
42
91
  categories,
43
92
  values,
93
+ keybindsSections,
44
94
  activeCategoryId,
45
95
  activeRowIndex,
46
96
  activeZone,
@@ -56,6 +106,7 @@ export function SettingsDialog({
56
106
  categories.find((candidate) => candidate.id === activeCategoryId) ?? categories[0];
57
107
 
58
108
  if (!category) return null;
109
+ const onKeybinds = category.id === KEYBINDS_CATEGORY_ID;
59
110
 
60
111
  return (
61
112
  <Dialog
@@ -67,80 +118,75 @@ export function SettingsDialog({
67
118
  theme={theme}
68
119
  >
69
120
  <box style={{ flexDirection: "row", flexGrow: 1 }}>
70
- <box style={{ flexDirection: "column", width: 18, paddingLeft: 1, paddingRight: 1 }}>
71
- <text fg={tokens.textDim}>SETTINGS</text>
72
- {categories.map((candidate) => {
73
- const isActive = candidate.id === category.id;
74
-
75
- return (
76
- <box
77
- key={candidate.id}
78
- style={{
79
- backgroundColor: isActive && activeZone === "nav" ? tokens.border : undefined,
80
- }}
81
- onMouseUp={() => onCategorySelect(candidate.id)}
82
- >
83
- <text
84
- fg={isActive ? tokens.accent : tokens.textMuted}
85
- >{`${isActive ? "› " : " "}${candidate.name}`}</text>
86
- </box>
87
- );
88
- })}
121
+ <box style={{ flexDirection: "column", width: 20, paddingLeft: 1, paddingRight: 1 }}>
122
+ <Tree
123
+ nodes={navTree(categories)}
124
+ expandedIds={new Set(["settings"])}
125
+ selectedId={category.id}
126
+ onSelect={onCategorySelect}
127
+ onToggle={onCategorySelect}
128
+ theme={theme}
129
+ />
89
130
  </box>
90
131
  <box style={{ width: 1, backgroundColor: tokens.border }} />
91
132
  <box style={{ flexDirection: "column", flexGrow: 1, paddingLeft: 2, paddingRight: 1 }}>
92
133
  <text fg={tokens.text}>{category.name}</text>
93
134
  <text fg={tokens.textDim}>{category.description}</text>
94
135
  <box style={{ height: 1 }} />
95
- {category.customBody}
96
- {category.rows.map((row, rowIndex) => {
97
- const isActive = activeZone === "body" && rowIndex === activeRowIndex;
98
- const value = values[row.key];
136
+ {onKeybinds ? <KeybindsBody sections={keybindsSections} tokens={tokens} /> : null}
137
+ {onKeybinds ? null : category.customBody}
138
+ {onKeybinds
139
+ ? null
140
+ : category.rows.map((row, rowIndex) => {
141
+ const isActive = activeZone === "body" && rowIndex === activeRowIndex;
142
+ const value = values[row.key];
99
143
 
100
- if (row.kind === "toggle") {
101
- return (
102
- <ToggleRow
103
- key={row.key}
104
- label={row.label}
105
- value={value === true}
106
- isActive={isActive}
107
- onPress={() => onRowActivate(row)}
108
- theme={theme}
109
- />
110
- );
111
- }
112
- if (row.kind === "cycle") {
113
- return (
114
- <CycleRow
115
- key={row.key}
116
- label={row.label}
117
- value={String(value ?? "")}
118
- isActive={isActive}
119
- onPress={() => onRowActivate(row)}
120
- theme={theme}
121
- />
122
- );
123
- }
144
+ if (row.kind === "toggle") {
145
+ return (
146
+ <ToggleRow
147
+ key={row.key}
148
+ label={row.label}
149
+ value={value === true}
150
+ isActive={isActive}
151
+ onPress={() => onRowActivate(row)}
152
+ theme={theme}
153
+ />
154
+ );
155
+ }
156
+ if (row.kind === "cycle") {
157
+ return (
158
+ <CycleRow
159
+ key={row.key}
160
+ label={row.label}
161
+ value={String(value ?? "")}
162
+ isActive={isActive}
163
+ onPress={() => onRowActivate(row)}
164
+ theme={theme}
165
+ />
166
+ );
167
+ }
124
168
 
125
- return (
126
- <TextRow
127
- key={row.key}
128
- label={row.label}
129
- value={String(value ?? "")}
130
- isActive={isActive}
131
- onPress={() => onRowActivate(row)}
132
- theme={theme}
133
- />
134
- );
135
- })}
169
+ return (
170
+ <TextRow
171
+ key={row.key}
172
+ label={row.label}
173
+ value={String(value ?? "")}
174
+ isActive={isActive}
175
+ onPress={() => onRowActivate(row)}
176
+ theme={theme}
177
+ />
178
+ );
179
+ })}
136
180
  </box>
137
181
  </box>
138
- <box style={{ flexDirection: "row", height: 1, paddingLeft: 1 }}>
182
+ <box style={{ flexDirection: "row", height: 1, paddingLeft: 1, paddingRight: 1 }}>
139
183
  <text fg={tokens.textDim}>
140
184
  {activeZone === "nav"
141
- ? "j/k category · l/enter into settings · esc close"
185
+ ? "j/k nav · l/tab open · esc close"
142
186
  : "j/k row · l/space change · h back · esc close"}
143
187
  </text>
188
+ <box style={{ flexGrow: 1 }} />
189
+ <text fg={tokens.textDim}>{`cueloop v${version}`}</text>
144
190
  </box>
145
191
  </Dialog>
146
192
  );
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
3
3
  import type { Story, StoryMeta } from "./story";
4
4
  import { CycleRow, TextRow, ToggleRow } from "./SettingsRows";
5
5
 
6
- export const meta: StoryMeta = { title: "SettingsRows" };
6
+ export const meta: StoryMeta = { title: "Chrome/SettingsRows" };
7
7
 
8
8
  export const AllKinds: Story = {
9
9
  render: () => (
@@ -0,0 +1,50 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { ShellHeader } from "./ShellHeader";
5
+ import { NERD } from "./primitives/icons";
6
+
7
+ export const meta: StoryMeta = { title: "Layout/ShellHeader" };
8
+
9
+ function Frame({ children }: { children: React.ReactNode }): React.ReactNode {
10
+ return <box style={{ width: "100%", backgroundColor: DARK.panel }}>{children}</box>;
11
+ }
12
+
13
+ export const ReviewSurface: Story = {
14
+ render: () => (
15
+ <Frame>
16
+ <ShellHeader
17
+ leftIcons={[{ glyph: NERD.settings }]}
18
+ leftLabel="Read Cueloop Repository"
19
+ tabs={[{ label: "Changes", active: true }]}
20
+ rightIcons={[
21
+ { glyph: NERD.expand },
22
+ { glyph: NERD.diff, active: true },
23
+ { glyph: NERD.listTree },
24
+ ]}
25
+ onToggleSidebar={() => {}}
26
+ onToggleInspector={() => {}}
27
+ />
28
+ </Frame>
29
+ ),
30
+ expectedColors: [DARK.accent, DARK.textMuted],
31
+ size: { width: 120, height: 3 },
32
+ };
33
+
34
+ export const InspectorCollapsed: Story = {
35
+ render: () => (
36
+ <Frame>
37
+ <ShellHeader
38
+ leftIcons={[{ glyph: NERD.settings }]}
39
+ leftLabel="Read Cueloop Repository"
40
+ rightIcons={[{ glyph: NERD.search }, { glyph: NERD.expand }]}
41
+ onToggleSidebar={() => {}}
42
+ sidebarOpen={false}
43
+ onToggleInspector={() => {}}
44
+ inspectorOpen={false}
45
+ />
46
+ </Frame>
47
+ ),
48
+ expectedColors: [DARK.textMuted],
49
+ size: { width: 120, height: 3 },
50
+ };
@@ -0,0 +1,137 @@
1
+ // The app shell's top bar: left panel toggle + icons + label, an optional title, right icons + panel toggle.
2
+
3
+ import React from "react";
4
+ import { DARK, type Theme } from "../theme";
5
+ import { NERD } from "./primitives/icons";
6
+
7
+ export interface ShellTab {
8
+ label: string;
9
+ active?: boolean;
10
+ }
11
+
12
+ // A header icon; active paints it in the brand accent to mark the current view or mode.
13
+ export interface ShellIcon {
14
+ glyph: string;
15
+ active?: boolean;
16
+ onPress?: () => void;
17
+ }
18
+
19
+ export interface ShellHeaderProps {
20
+ leftIcons?: readonly ShellIcon[];
21
+ leftLabel?: string;
22
+ titleIcon?: string;
23
+ title?: string;
24
+ tabs?: readonly ShellTab[];
25
+ rightIcons?: readonly ShellIcon[];
26
+ sidebarOpen?: boolean;
27
+ onToggleSidebar?: () => void;
28
+ inspectorOpen?: boolean;
29
+ onToggleInspector?: () => void;
30
+ theme?: Theme;
31
+ }
32
+
33
+ function IconView({
34
+ icon,
35
+ tokens,
36
+ marginRight,
37
+ }: {
38
+ icon: ShellIcon;
39
+ tokens: Theme;
40
+ marginRight: number;
41
+ }): React.ReactNode {
42
+ const color = icon.active ? tokens.accent : tokens.textMuted;
43
+
44
+ // alignSelf center keeps the clickable box on the same row as sibling icons
45
+ return (
46
+ <box onMouseUp={icon.onPress} style={{ alignSelf: "center", marginRight }}>
47
+ <text fg={color}>{icon.glyph}</text>
48
+ </box>
49
+ );
50
+ }
51
+
52
+ export function ShellHeader({
53
+ leftIcons = [],
54
+ leftLabel,
55
+ titleIcon,
56
+ title,
57
+ tabs = [],
58
+ rightIcons = [],
59
+ sidebarOpen = true,
60
+ onToggleSidebar,
61
+ inspectorOpen = true,
62
+ onToggleInspector,
63
+ theme,
64
+ }: ShellHeaderProps): React.ReactNode {
65
+ const tokens = theme ?? DARK;
66
+
67
+ return (
68
+ <box
69
+ style={{
70
+ flexDirection: "row",
71
+ height: "100%",
72
+ alignItems: "center",
73
+ paddingLeft: 2,
74
+ paddingRight: 2,
75
+ }}
76
+ >
77
+ <box style={{ flexDirection: "row", alignItems: "center" }}>
78
+ {onToggleSidebar !== undefined ? (
79
+ <IconView
80
+ icon={{
81
+ glyph: sidebarOpen ? NERD.sidebarLeft : NERD.sidebarLeftOff,
82
+ onPress: onToggleSidebar,
83
+ }}
84
+ tokens={tokens}
85
+ marginRight={3}
86
+ />
87
+ ) : null}
88
+ {leftIcons.map((icon, index) => (
89
+ <IconView
90
+ key={`left-${index}-${icon.glyph}`}
91
+ icon={icon}
92
+ tokens={tokens}
93
+ marginRight={3}
94
+ />
95
+ ))}
96
+ {leftLabel !== undefined ? (
97
+ <text fg={tokens.textMuted}>
98
+ {leftLabel}
99
+ {" "}
100
+ </text>
101
+ ) : null}
102
+ {titleIcon !== undefined ? <text fg={tokens.textDim}>{titleIcon} </text> : null}
103
+ {title !== undefined ? <text fg={tokens.text}>{title}</text> : null}
104
+ </box>
105
+ <box style={{ flexGrow: 1 }} />
106
+ <box style={{ flexDirection: "row", alignItems: "center" }}>
107
+ {tabs.map((tab, index) => (
108
+ <text
109
+ key={`tab-${index}-${tab.label}`}
110
+ fg={tab.active ? tokens.accent : tokens.textMuted}
111
+ >
112
+ {tab.label}
113
+ {" "}
114
+ </text>
115
+ ))}
116
+ {rightIcons.map((icon, index) => (
117
+ <IconView
118
+ key={`right-${index}-${icon.glyph}`}
119
+ icon={icon}
120
+ tokens={tokens}
121
+ marginRight={3}
122
+ />
123
+ ))}
124
+ {onToggleInspector !== undefined ? (
125
+ <IconView
126
+ icon={{
127
+ glyph: inspectorOpen ? NERD.sidebarRight : NERD.sidebarRightOff,
128
+ onPress: onToggleInspector,
129
+ }}
130
+ tokens={tokens}
131
+ marginRight={0}
132
+ />
133
+ ) : null}
134
+ </box>
135
+ </box>
136
+ );
137
+ }
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { ThreadFooter } from "./ThreadFooter";
5
+
6
+ export const meta: StoryMeta = { title: "Chrome/ThreadFooter" };
7
+
8
+ function Frame({ children }: { children: React.ReactNode }): React.ReactNode {
9
+ return <box style={{ width: "100%", backgroundColor: DARK.panel }}>{children}</box>;
10
+ }
11
+
12
+ export const Default: Story = {
13
+ render: () => (
14
+ <Frame>
15
+ <ThreadFooter repo="cueloop" branch="main" onSubmit={() => {}} />
16
+ </Frame>
17
+ ),
18
+ expectedColors: [DARK.accent, DARK.textMuted],
19
+ size: { width: 80, height: 1 },
20
+ };
21
+
22
+ export const ReadOnly: Story = {
23
+ render: () => (
24
+ <Frame>
25
+ <ThreadFooter repo="cueloop" branch="design/tui" canSubmit={false} onSubmit={() => {}} />
26
+ </Frame>
27
+ ),
28
+ expectedColors: [DARK.textDim],
29
+ size: { width: 80, height: 1 },
30
+ };