@cueloop/client 0.1.0-alpha.65 → 0.1.0-alpha.67

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 (160) hide show
  1. package/package.json +6 -5
  2. package/src/App.autoclose.test.tsx +2 -2
  3. package/src/App.diff.test.tsx +79 -112
  4. package/src/App.plan-review.test.tsx +23 -205
  5. package/src/App.prototype.test.tsx +1 -8
  6. package/src/App.readonly.test.tsx +9 -27
  7. package/src/App.submit-confirm.test.tsx +19 -72
  8. package/src/App.test.tsx +136 -31
  9. package/src/App.tsx +491 -322
  10. package/src/App.walk.test.tsx +15 -16
  11. package/src/App.workbench.test.tsx +390 -0
  12. package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
  13. package/src/annotation-palette.ts +61 -0
  14. package/src/app-screens.tsx +197 -77
  15. package/src/app-view-model.ts +9 -69
  16. package/src/components/AnnotationCard.stories.tsx +1 -1
  17. package/src/components/AnnotationCard.tsx +1 -1
  18. package/src/components/AnnotationCards.stories.tsx +87 -0
  19. package/src/components/AnnotationCards.tsx +303 -0
  20. package/src/components/AppHeader.stories.tsx +50 -0
  21. package/src/components/AppHeader.tsx +83 -0
  22. package/src/components/AppShell.stories.tsx +89 -0
  23. package/src/components/AppShell.tsx +239 -0
  24. package/src/components/AppShell.zoom.test.tsx +94 -0
  25. package/src/components/Breadcrumb.stories.tsx +1 -1
  26. package/src/components/ChangesColumn.stories.tsx +39 -0
  27. package/src/components/ChangesColumn.tsx +170 -0
  28. package/src/components/CodeBlock.stories.tsx +1 -1
  29. package/src/components/CompletionOverlay.stories.tsx +1 -1
  30. package/src/components/ConfirmCard.stories.tsx +1 -1
  31. package/src/components/ConfirmCard.tsx +3 -4
  32. package/src/components/ConfirmDialog.stories.tsx +1 -1
  33. package/src/components/DiffSheet.scroll.test.tsx +38 -55
  34. package/src/components/DiffSheet.split.test.tsx +140 -0
  35. package/src/components/DiffSheet.stories.tsx +56 -54
  36. package/src/components/DiffSheet.syntax.test.tsx +24 -7
  37. package/src/components/DiffSheet.tsx +607 -276
  38. package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
  39. package/src/components/DiscussionMarkerRail.tsx +181 -0
  40. package/src/components/EditorGrid.stories.tsx +59 -0
  41. package/src/components/EditorGrid.tabs.test.tsx +105 -0
  42. package/src/components/EditorGrid.tsx +338 -0
  43. package/src/components/EditorGrid.zoom.test.tsx +68 -0
  44. package/src/components/FileContentsView.stories.tsx +16 -0
  45. package/src/components/FileContentsView.test.tsx +29 -0
  46. package/src/components/FileContentsView.tsx +78 -0
  47. package/src/components/InboxList.stories.tsx +23 -8
  48. package/src/components/InboxList.tsx +188 -33
  49. package/src/components/KeybindsDialog.stories.tsx +1 -1
  50. package/src/components/MarkerPopover.stories.tsx +1 -1
  51. package/src/components/MenuBar.stories.tsx +3 -12
  52. package/src/components/MenuBar.tsx +25 -56
  53. package/src/components/PaneGrid.stories.tsx +84 -0
  54. package/src/components/PaneGrid.tsx +128 -0
  55. package/src/components/PanelColumn.stories.tsx +25 -0
  56. package/src/components/PanelColumn.tsx +95 -0
  57. package/src/components/ProjectTreeView.stories.tsx +25 -0
  58. package/src/components/ProjectTreeView.test.tsx +32 -0
  59. package/src/components/ProjectTreeView.tsx +82 -0
  60. package/src/components/PromptDialog.stories.tsx +1 -1
  61. package/src/components/PromptDialog.tsx +8 -1
  62. package/src/components/PrototypeSheet.stories.tsx +1 -1
  63. package/src/components/RootOverlay.stories.tsx +44 -0
  64. package/src/components/RootOverlay.tsx +35 -0
  65. package/src/components/SettingsDialog.stories.tsx +7 -1
  66. package/src/components/SettingsDialog.tsx +114 -67
  67. package/src/components/SettingsRows.stories.tsx +1 -1
  68. package/src/components/ShellHeader.stories.tsx +50 -0
  69. package/src/components/ShellHeader.tsx +137 -0
  70. package/src/components/SurfaceScrollbar.stories.tsx +34 -0
  71. package/src/components/SurfaceScrollbar.test.tsx +111 -0
  72. package/src/components/SurfaceScrollbar.tsx +93 -0
  73. package/src/components/ThreadFooter.stories.tsx +30 -0
  74. package/src/components/ThreadFooter.test.tsx +31 -0
  75. package/src/components/ThreadFooter.tsx +54 -0
  76. package/src/components/ThreadView.slash.test.tsx +113 -0
  77. package/src/components/ThreadView.stories.tsx +1 -1
  78. package/src/components/ThreadView.tsx +62 -1352
  79. package/src/components/ThreadsSidebar.stories.tsx +36 -0
  80. package/src/components/ThreadsSidebar.tsx +70 -0
  81. package/src/components/Toast.stories.tsx +1 -1
  82. package/src/components/Tooltip.stories.tsx +26 -0
  83. package/src/components/Tooltip.tsx +72 -0
  84. package/src/components/TreePane.stories.tsx +1 -1
  85. package/src/components/WalkWizard.stories.tsx +1 -1
  86. package/src/components/WelcomeSurface.stories.tsx +12 -0
  87. package/src/components/WelcomeSurface.tsx +52 -0
  88. package/src/components/__snapshots__/stories.test.tsx.snap +732 -572
  89. package/src/components/agent-launcher.stories.tsx +1 -1
  90. package/src/components/diff-sheet-layout.test.ts +125 -55
  91. package/src/components/diff-sheet-layout.ts +3 -103
  92. package/src/components/editor-grid.test.ts +86 -0
  93. package/src/components/editor-grid.ts +138 -0
  94. package/src/components/fade-title.test.ts +49 -0
  95. package/src/components/fade-title.ts +61 -0
  96. package/src/components/file-tree.test.ts +60 -0
  97. package/src/components/file-tree.ts +112 -0
  98. package/src/components/pages/Workspace.stories.tsx +181 -0
  99. package/src/components/primitives/Button.stories.tsx +1 -1
  100. package/src/components/primitives/Card.stories.tsx +1 -1
  101. package/src/components/primitives/Dialog.stories.tsx +1 -1
  102. package/src/components/primitives/IconButton.stories.tsx +36 -0
  103. package/src/components/primitives/IconButton.tsx +62 -0
  104. package/src/components/primitives/StatusBar.stories.tsx +1 -1
  105. package/src/components/primitives/Tabs.stories.tsx +1 -1
  106. package/src/components/primitives/Toolbar.stories.tsx +1 -1
  107. package/src/components/primitives/Tree.stories.tsx +124 -0
  108. package/src/components/primitives/Tree.tsx +96 -0
  109. package/src/components/primitives/icons.ts +61 -0
  110. package/src/components/primitives/tree-model.test.ts +99 -0
  111. package/src/components/primitives/tree-model.ts +137 -0
  112. package/src/components/quick-actions-editor.stories.tsx +1 -1
  113. package/src/components/session-tree.test.ts +134 -0
  114. package/src/components/session-tree.ts +108 -0
  115. package/src/components/stories-app.tsx +142 -82
  116. package/src/components/story-tree.test.ts +100 -0
  117. package/src/components/story.ts +55 -0
  118. package/src/components/theme-context.stories.tsx +1 -1
  119. package/src/config.test.ts +20 -49
  120. package/src/config.ts +26 -28
  121. package/src/diff-fold.test.ts +78 -0
  122. package/src/diff-fold.ts +126 -0
  123. package/src/diff-syntax.test.ts +16 -0
  124. package/src/diff-syntax.ts +6 -3
  125. package/src/discussions.ts +79 -0
  126. package/src/intent-dispatch.test.ts +31 -11
  127. package/src/intent-dispatch.ts +93 -48
  128. package/src/key-bindings.test.ts +1 -1
  129. package/src/keymap.test.ts +3 -25
  130. package/src/keymap.ts +17 -18
  131. package/src/mark-runs.ts +120 -0
  132. package/src/pty.ts +10 -2
  133. package/src/run.ts +17 -7
  134. package/src/serve.test.ts +4 -8
  135. package/src/session-controller.curate.test.ts +75 -0
  136. package/src/session-controller.share.test.ts +1 -0
  137. package/src/session-controller.tree.test.ts +26 -0
  138. package/src/session-controller.ts +160 -31
  139. package/src/slash-palette.ts +83 -0
  140. package/src/split-diff.test.ts +110 -0
  141. package/src/split-diff.ts +149 -0
  142. package/src/test-support.test.ts +3 -3
  143. package/src/test-support.ts +39 -8
  144. package/src/theme.ts +4 -4
  145. package/src/thread-chords.test.ts +25 -9
  146. package/src/thread-chords.ts +33 -8
  147. package/src/use-annotation-surface.tsx +837 -0
  148. package/src/use-changes-workbench.ts +151 -0
  149. package/src/use-frame-measure.ts +38 -0
  150. package/src/use-settings-dialog.tsx +23 -24
  151. package/src/use-terminal-virtualizer.ts +233 -0
  152. package/src/view-diff.ts +114 -0
  153. package/src/App.review-panel.test.tsx +0 -140
  154. package/src/App.tree.test.tsx +0 -224
  155. package/src/components/ReviewPanel.stories.tsx +0 -100
  156. package/src/components/ReviewPanel.tsx +0 -103
  157. package/src/components/ReviewRail.stories.tsx +0 -232
  158. package/src/components/ReviewRail.tsx +0 -327
  159. package/src/review-panel.test.ts +0 -113
  160. package/src/review-panel.ts +0 -83
@@ -1,61 +1,216 @@
1
1
  /**
2
- * The pending-review inbox: a cursor-driven session list. Selection stays with
3
- * the keyboard grammar; the list only renders the snapshot. The selected row
4
- * carries a [delete] word-button (click, or `d` in the grammar) that asks the
5
- * caller to confirm removal. App supplies the surrounding header and MenuBar
6
- * chrome (the same as plan/diff review); this is the flex-growing body.
2
+ * The pending-review inbox: a Pinned / Projects / Threads tree. Selection stays
3
+ * with the keyboard grammar - the cursor indexes the flat thread order and this
4
+ * component renders the snapshot. A hovered or selected thread reveals a kebab
5
+ * that opens an inline pin / rename / delete menu; long titles fade on the right
6
+ * instead of wrapping. App supplies the surrounding chrome.
7
7
  */
8
8
 
9
- import React from "react";
10
- import type { ReviewSession } from "@cueloop/schema";
9
+ import React, { useState } from "react";
11
10
  import type { Theme } from "../theme";
12
11
  import { useComponentTheme } from "./theme-context";
13
- import { Button } from "./primitives/Button";
12
+ import { IconButton } from "./primitives/IconButton";
13
+ import { NERD } from "./primitives/icons";
14
+ import { fadeTitle } from "./fade-title";
15
+ import { threadTitle, type InboxRow } from "./session-tree";
14
16
 
15
17
  export interface InboxListProps {
16
- inbox: ReviewSession[];
18
+ rows: InboxRow[];
17
19
  cursor: number;
18
- /** Ask to delete a plan (the selected row's [delete] button). */
20
+ /** The open thread's id; highlights it instead of the cursor (the left sidebar case). */
21
+ activeId?: string;
22
+ /** Ids of pinned threads; a pinned row carries the pin glyph and the menu offers Unpin. */
23
+ pinnedIds?: ReadonlySet<string>;
24
+ /** The column width, so titles fade to fit one line. */
25
+ width?: number;
26
+ /** Open a thread by clicking its row (the left sidebar case). */
27
+ onSelect?: (sessionId: string) => void;
28
+ /** Ask to delete a thread (the menu's Delete, wired to the confirm dialog). */
19
29
  onRequestDelete?: (id: string, title: string) => void;
30
+ /** Toggle a thread's pinned state (the menu's Pin/Unpin). */
31
+ onPin?: (id: string) => void;
32
+ /** Rename a thread's title (the menu's Rename). */
33
+ onRename?: (id: string, title: string) => void;
20
34
  theme?: Theme;
21
35
  }
22
36
 
37
+ function ActionsMenu({
38
+ pinned,
39
+ onPin,
40
+ onRename,
41
+ onDelete,
42
+ tokens,
43
+ }: {
44
+ pinned: boolean;
45
+ onPin?: () => void;
46
+ onRename?: () => void;
47
+ onDelete?: () => void;
48
+ tokens: Theme;
49
+ }): React.ReactNode {
50
+ const item = (
51
+ glyph: string,
52
+ label: string,
53
+ color: string,
54
+ onPick?: () => void,
55
+ ): React.ReactNode =>
56
+ onPick !== undefined ? (
57
+ <box onMouseUp={onPick} style={{ flexDirection: "row", paddingLeft: 4, paddingRight: 1 }}>
58
+ <text fg={color}>{`${glyph} ${label}`}</text>
59
+ </box>
60
+ ) : null;
61
+
62
+ return (
63
+ <box
64
+ style={{
65
+ flexDirection: "column",
66
+ marginLeft: 3,
67
+ borderStyle: "single",
68
+ border: ["left"],
69
+ borderColor: tokens.border,
70
+ }}
71
+ >
72
+ {item(NERD.pin, pinned ? "Unpin" : "Pin", tokens.text, onPin)}
73
+ {item(NERD.file, "Rename", tokens.text, onRename)}
74
+ {item(NERD.close, "Delete", tokens.red, onDelete)}
75
+ </box>
76
+ );
77
+ }
78
+
79
+ interface ThreadRowProps {
80
+ title: string;
81
+ selected: boolean;
82
+ pinned: boolean;
83
+ titleWidth: number;
84
+ menuOpen: boolean;
85
+ onToggleMenu: () => void;
86
+ onSelect?: () => void;
87
+ onPin?: () => void;
88
+ onRename?: () => void;
89
+ onDelete?: () => void;
90
+ tokens: Theme;
91
+ theme?: Theme;
92
+ }
93
+
94
+ function ThreadRow(props: ThreadRowProps): React.ReactNode {
95
+ const { title, selected, pinned, titleWidth, menuOpen, onToggleMenu, tokens, theme } = props;
96
+ const [hovered, setHovered] = useState(false);
97
+ // the tail fades toward the row's own background, so it masks out instead of
98
+ // leaving a colored band over the selected (elevated) or hovered (panel) row
99
+ const rowBackground = selected ? tokens.elevated : hovered ? tokens.panel : tokens.background;
100
+ const segments = fadeTitle(
101
+ title,
102
+ titleWidth,
103
+ selected ? tokens.accent : tokens.textMuted,
104
+ rowBackground,
105
+ );
106
+ const hasActions =
107
+ props.onPin !== undefined || props.onRename !== undefined || props.onDelete !== undefined;
108
+
109
+ return (
110
+ <box style={{ flexDirection: "column" }} onMouseOut={() => setHovered(false)}>
111
+ <box
112
+ onMouseUp={props.onSelect}
113
+ onMouseOver={() => setHovered(true)}
114
+ style={{
115
+ flexDirection: "row",
116
+ backgroundColor: selected ? tokens.elevated : hovered ? tokens.panel : undefined,
117
+ }}
118
+ >
119
+ <text>
120
+ <span fg={tokens.textDim}>{pinned ? ` ${NERD.pin} ` : " "}</span>
121
+ {segments.map((segment, index) => (
122
+ <span key={index} fg={segment.fg}>
123
+ {segment.text}
124
+ </span>
125
+ ))}
126
+ </text>
127
+ <box style={{ flexGrow: 1 }} />
128
+ {(hovered || menuOpen) && hasActions ? (
129
+ <IconButton
130
+ glyph={NERD.kebab}
131
+ active={menuOpen}
132
+ onPress={onToggleMenu}
133
+ marginRight={1}
134
+ theme={theme}
135
+ />
136
+ ) : null}
137
+ </box>
138
+ {menuOpen ? (
139
+ <ActionsMenu
140
+ pinned={pinned}
141
+ onPin={props.onPin}
142
+ onRename={props.onRename}
143
+ onDelete={props.onDelete}
144
+ tokens={tokens}
145
+ />
146
+ ) : null}
147
+ </box>
148
+ );
149
+ }
150
+
23
151
  export function InboxList({
24
- inbox,
152
+ rows,
25
153
  cursor,
154
+ activeId,
155
+ pinnedIds,
156
+ width = 30,
157
+ onSelect,
26
158
  onRequestDelete,
159
+ onPin,
160
+ onRename,
27
161
  theme,
28
162
  }: InboxListProps): React.ReactNode {
29
163
  const tokens = useComponentTheme(theme);
164
+ const [actionsForId, setActionsForId] = useState<string | null>(null);
165
+ // leave room for the 3-char prefix, the border, and the hover kebab so a row
166
+ // never wraps: the title fades to fit whatever is left
167
+ const titleWidth = Math.max(8, width - 9);
168
+ const closeMenu = (): void => setActionsForId(null);
30
169
 
31
170
  return (
32
171
  <box style={{ flexGrow: 1, flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
33
- {inbox.length === 0 ? (
172
+ {rows.length === 0 ? (
34
173
  <text fg={tokens.textDim}>nothing waiting for review</text>
35
174
  ) : (
36
- inbox.map((session, index) => {
37
- const selected = index === cursor;
38
- const title = session.artifact.meta.title ?? session.id;
175
+ rows.map((row) => {
176
+ if (row.kind === "section") {
177
+ return (
178
+ <text key={row.id} fg={tokens.textDim}>
179
+ {row.label}
180
+ </text>
181
+ );
182
+ }
39
183
 
40
- return (
41
- <box
42
- key={session.id}
43
- style={{
44
- flexDirection: "row",
45
- backgroundColor: selected ? tokens.cursorBackground : undefined,
46
- }}
47
- >
48
- <text fg={selected ? tokens.text : tokens.textMuted}>
49
- {selected ? "> " : " "}
50
- {title} · {session.workspace.branch} · {session.artifact.type}
184
+ if (row.kind === "project") {
185
+ return (
186
+ <text key={row.id}>
187
+ <span fg={tokens.blue}>{` ${NERD.folderOpen} `}</span>
188
+ <span fg={tokens.textMuted}>{row.label}</span>
51
189
  </text>
52
- <box style={{ flexGrow: 1 }} />
53
- {selected && onRequestDelete ? (
54
- <Button onPress={() => onRequestDelete(session.id, title)} theme={theme}>
55
- {" [delete] "}
56
- </Button>
57
- ) : null}
58
- </box>
190
+ );
191
+ }
192
+
193
+ const id = row.session.id;
194
+ const title = threadTitle(row.session);
195
+
196
+ return (
197
+ <ThreadRow
198
+ key={row.id}
199
+ title={title}
200
+ selected={activeId !== undefined ? activeId === id : row.selectionIndex === cursor}
201
+ pinned={pinnedIds?.has(id) ?? false}
202
+ titleWidth={titleWidth}
203
+ menuOpen={actionsForId === id}
204
+ onToggleMenu={() => setActionsForId(actionsForId === id ? null : id)}
205
+ onSelect={onSelect ? () => onSelect(id) : undefined}
206
+ onPin={onPin ? () => (onPin(id), closeMenu()) : undefined}
207
+ onRename={onRename ? () => (onRename(id, title), closeMenu()) : undefined}
208
+ onDelete={
209
+ onRequestDelete ? () => (onRequestDelete(id, title), closeMenu()) : undefined
210
+ }
211
+ tokens={tokens}
212
+ theme={theme}
213
+ />
59
214
  );
60
215
  })
61
216
  )}
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
3
3
  import type { Story, StoryMeta } from "./story";
4
4
  import { KeybindsDialog } from "./KeybindsDialog";
5
5
 
6
- export const meta: StoryMeta = { title: "KeybindsDialog" };
6
+ export const meta: StoryMeta = { title: "Overlays/KeybindsDialog" };
7
7
 
8
8
  const SECTIONS = [
9
9
  {
@@ -4,7 +4,7 @@ import { DEFAULT_QUICK_ACTIONS } from "../config";
4
4
  import type { Story, StoryMeta } from "./story";
5
5
  import { MarkerPopover } from "./MarkerPopover";
6
6
 
7
- export const meta: StoryMeta = { title: "MarkerPopover" };
7
+ export const meta: StoryMeta = { title: "Overlays/MarkerPopover" };
8
8
 
9
9
  const callbacks = {
10
10
  onComment: () => {},
@@ -3,9 +3,9 @@ import { DARK } from "../theme";
3
3
  import type { Story, StoryMeta } from "./story";
4
4
  import { MenuBar } from "./MenuBar";
5
5
 
6
- export const meta: StoryMeta = { title: "MenuBar" };
6
+ export const meta: StoryMeta = { title: "Chrome/MenuBar" };
7
7
 
8
- const callbacks = { onToggle: () => {}, onSettings: () => {}, onKeybinds: () => {} };
8
+ const callbacks = { onSettings: () => {}, onKeybinds: () => {} };
9
9
 
10
10
  function BottomFrame({ children }: { children: React.ReactNode }): React.ReactNode {
11
11
  return (
@@ -17,19 +17,10 @@ function BottomFrame({ children }: { children: React.ReactNode }): React.ReactNo
17
17
  );
18
18
  }
19
19
 
20
- export const Closed: Story = {
21
- render: () => (
22
- <BottomFrame>
23
- <MenuBar open={false} version="0.1.0-alpha.32" {...callbacks} />
24
- </BottomFrame>
25
- ),
26
- size: { width: 60, height: 8 },
27
- };
28
-
29
20
  export const Open: Story = {
30
21
  render: () => (
31
22
  <BottomFrame>
32
- <MenuBar open version="0.1.0-alpha.32" {...callbacks} />
23
+ <MenuBar open {...callbacks} />
33
24
  </BottomFrame>
34
25
  ),
35
26
  expectedColors: [DARK.text],
@@ -1,75 +1,44 @@
1
1
  import React from "react";
2
- import { useTerminalDimensions } from "@opentui/react";
3
2
  import type { Theme } from "../theme";
4
3
  import { useComponentTheme } from "./theme-context";
5
4
  import { FRAME_BORDER_STYLE } from "./primitives/frame";
6
5
 
7
6
  export interface MenuBarProps {
8
7
  open: boolean;
9
- version: string;
10
- /** The transient status line, shown between the menu and the version. */
11
- status?: string;
12
- onToggle: () => void;
13
8
  onSettings: () => void;
14
9
  onKeybinds: () => void;
15
10
  theme?: Theme;
16
11
  }
17
12
 
18
- const DROPUP_OPTION_ROWS = 2;
13
+ const DROPDOWN_OPTION_ROWS = 2;
19
14
 
20
- export function MenuBar({
21
- open,
22
- version,
23
- status,
24
- onToggle,
25
- onSettings,
26
- onKeybinds,
27
- theme,
28
- }: MenuBarProps): React.ReactNode {
15
+ /** The settings gear's drop-down: Settings and Keybinds, below the top-left gear. */
16
+ export function MenuBar({ open, onSettings, onKeybinds, theme }: MenuBarProps): React.ReactNode {
29
17
  const tokens = useComponentTheme(theme);
30
- const { height: terminalHeight } = useTerminalDimensions();
31
- const dropupHeight = DROPUP_OPTION_ROWS + 2;
18
+ const dropdownHeight = DROPDOWN_OPTION_ROWS + 2;
19
+
20
+ if (!open) return null;
32
21
 
33
22
  return (
34
- <>
35
- {open ? (
36
- <box
37
- style={{
38
- position: "absolute",
39
- left: 1,
40
- top: Math.max(0, terminalHeight - 1 - dropupHeight),
41
- width: 16,
42
- height: dropupHeight,
43
- border: true,
44
- borderStyle: FRAME_BORDER_STYLE,
45
- borderColor: tokens.text,
46
- flexDirection: "column",
47
- }}
48
- >
49
- <box onMouseUp={onSettings}>
50
- <text fg={tokens.text}> Settings</text>
51
- </box>
52
- <box onMouseUp={onKeybinds}>
53
- <text fg={tokens.text}> Keybinds</text>
54
- </box>
55
- </box>
56
- ) : null}
57
- <box
58
- style={{
59
- flexDirection: "row",
60
- height: 1,
61
- backgroundColor: tokens.panel,
62
- paddingLeft: 1,
63
- paddingRight: 1,
64
- }}
65
- >
66
- <box onMouseUp={onToggle}>
67
- <text fg={tokens.textDim}>menu</text>
68
- </box>
69
- {status ? <text fg={tokens.accent}>{` ${status}`}</text> : null}
70
- <box style={{ flexGrow: 1 }} />
71
- <text fg={tokens.textDim}>v{version}</text>
23
+ <box
24
+ style={{
25
+ position: "absolute",
26
+ left: 1,
27
+ top: 2,
28
+ width: 16,
29
+ height: dropdownHeight,
30
+ border: true,
31
+ borderStyle: FRAME_BORDER_STYLE,
32
+ borderColor: tokens.text,
33
+ flexDirection: "column",
34
+ }}
35
+ >
36
+ <box onMouseUp={onSettings}>
37
+ <text fg={tokens.text}> Settings</text>
38
+ </box>
39
+ <box onMouseUp={onKeybinds}>
40
+ <text fg={tokens.text}> Keybinds</text>
72
41
  </box>
73
- </>
42
+ </box>
74
43
  );
75
44
  }
@@ -0,0 +1,84 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { PaneGrid } from "./PaneGrid";
5
+
6
+ export const meta: StoryMeta = { title: "Layout/PaneGrid" };
7
+
8
+ function Sidebar(): React.ReactNode {
9
+ return (
10
+ <box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
11
+ <text fg={DARK.textDim}>Projects</text>
12
+ <text fg={DARK.text}> cueloop</text>
13
+ <box style={{ backgroundColor: DARK.elevated }}>
14
+ <text fg={DARK.accent}> Read Cueloop Repository</text>
15
+ </box>
16
+ <text> </text>
17
+ <text fg={DARK.textDim}>Threads</text>
18
+ <text fg={DARK.textMuted}> Welcome to cueloop</text>
19
+ </box>
20
+ );
21
+ }
22
+
23
+ function Main(): React.ReactNode {
24
+ return (
25
+ <box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
26
+ <text fg={DARK.text}>read the repo</text>
27
+ <text> </text>
28
+ <text fg={DARK.textMuted}>Repository context loaded. main is clean.</text>
29
+ <text> </text>
30
+ <text fg={DARK.text}>Core architecture</text>
31
+ <text fg={DARK.textMuted}> - packages/schema is the dependency root</text>
32
+ <text fg={DARK.textMuted}> - packages/daemon owns session state</text>
33
+ </box>
34
+ );
35
+ }
36
+
37
+ function Inspector(): React.ReactNode {
38
+ return (
39
+ <box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
40
+ <text fg={DARK.text}>cueloop/main</text>
41
+ <text fg={DARK.textDim}> No changes</text>
42
+ </box>
43
+ );
44
+ }
45
+
46
+ function Header(): React.ReactNode {
47
+ return (
48
+ <box style={{ flexDirection: "row", paddingLeft: 1 }}>
49
+ <text fg={DARK.textMuted}>Read Cueloop Repository</text>
50
+ <box style={{ flexGrow: 1 }} />
51
+ <text fg={DARK.textDim}>markdown.test.ts</text>
52
+ </box>
53
+ );
54
+ }
55
+
56
+ function Footer(): React.ReactNode {
57
+ return (
58
+ <box style={{ flexDirection: "row", paddingLeft: 1 }}>
59
+ <text fg={DARK.textDim}>cueloop / main</text>
60
+ <box style={{ flexGrow: 1 }} />
61
+ <text fg={DARK.textDim}>Fable 5</text>
62
+ </box>
63
+ );
64
+ }
65
+
66
+ export const ThreePane: Story = {
67
+ render: () => (
68
+ <PaneGrid
69
+ sidebar={<Sidebar />}
70
+ main={<Main />}
71
+ mainHeader={<Header />}
72
+ inspector={<Inspector />}
73
+ footer={<Footer />}
74
+ />
75
+ ),
76
+ expectedColors: [DARK.border, DARK.accent],
77
+ size: { width: 120, height: 28 },
78
+ };
79
+
80
+ export const SidebarAndMain: Story = {
81
+ render: () => <PaneGrid sidebar={<Sidebar />} main={<Main />} />,
82
+ expectedColors: [DARK.border],
83
+ size: { width: 90, height: 20 },
84
+ };
@@ -0,0 +1,128 @@
1
+ // A generic pane grid: a row of columns, each stacking a header cell over its body, with the
2
+ // vertical rules run full height and every header cell's bottom rule joined into one horizontal
3
+ // line. Rules only - no enclosing frames. This is the layout primitive for dev surfaces (the
4
+ // storybook runner); the product's four-pane workbench is AppShell.
5
+
6
+ import React from "react";
7
+ import { DARK, type Theme } from "../theme";
8
+ import type { BorderSides } from "@opentui/core";
9
+
10
+ export interface PaneGridProps {
11
+ headerHeight?: number;
12
+ sidebar?: React.ReactNode;
13
+ sidebarHeader?: React.ReactNode;
14
+ sidebarWidth?: number;
15
+ main: React.ReactNode;
16
+ mainHeader?: React.ReactNode;
17
+ inspector?: React.ReactNode;
18
+ inspectorHeader?: React.ReactNode;
19
+ inspectorWidth?: number;
20
+ footer?: React.ReactNode;
21
+ theme?: Theme;
22
+ }
23
+
24
+ interface PaneColumnProps {
25
+ width?: number;
26
+ grow?: boolean;
27
+ side?: BorderSides;
28
+ header: React.ReactNode;
29
+ body: React.ReactNode;
30
+ headerHeight: number;
31
+ theme: Theme;
32
+ }
33
+
34
+ function PaneColumn({
35
+ width,
36
+ grow,
37
+ side,
38
+ header,
39
+ body,
40
+ headerHeight,
41
+ theme,
42
+ }: PaneColumnProps): React.ReactNode {
43
+ return (
44
+ <box
45
+ style={{
46
+ flexDirection: "column",
47
+ ...(grow ? { flexGrow: 1 } : { width }),
48
+ borderStyle: "single",
49
+ border: side ? [side] : [],
50
+ borderColor: theme.border,
51
+ }}
52
+ >
53
+ <box
54
+ style={{
55
+ height: headerHeight,
56
+ borderStyle: "single",
57
+ border: ["bottom"],
58
+ borderColor: theme.border,
59
+ }}
60
+ >
61
+ {header}
62
+ </box>
63
+ <box style={{ flexGrow: 1 }}>{body}</box>
64
+ </box>
65
+ );
66
+ }
67
+
68
+ export function PaneGrid({
69
+ headerHeight = 3,
70
+ sidebar,
71
+ sidebarHeader,
72
+ sidebarWidth = 28,
73
+ main,
74
+ mainHeader,
75
+ inspector,
76
+ inspectorHeader,
77
+ inspectorWidth = 40,
78
+ footer,
79
+ theme,
80
+ }: PaneGridProps): React.ReactNode {
81
+ const tokens = theme ?? DARK;
82
+
83
+ return (
84
+ <box
85
+ style={{
86
+ flexDirection: "column",
87
+ width: "100%",
88
+ height: "100%",
89
+ backgroundColor: tokens.background,
90
+ }}
91
+ >
92
+ <box style={{ flexDirection: "row", flexGrow: 1 }}>
93
+ {sidebar !== undefined ? (
94
+ <PaneColumn
95
+ width={sidebarWidth}
96
+ side="right"
97
+ header={sidebarHeader}
98
+ body={sidebar}
99
+ headerHeight={headerHeight}
100
+ theme={tokens}
101
+ />
102
+ ) : null}
103
+ <PaneColumn
104
+ grow
105
+ header={mainHeader}
106
+ body={main}
107
+ headerHeight={headerHeight}
108
+ theme={tokens}
109
+ />
110
+ {inspector !== undefined ? (
111
+ <PaneColumn
112
+ width={inspectorWidth}
113
+ side="left"
114
+ header={inspectorHeader}
115
+ body={inspector}
116
+ headerHeight={headerHeight}
117
+ theme={tokens}
118
+ />
119
+ ) : null}
120
+ </box>
121
+ {footer !== undefined ? (
122
+ <box style={{ borderStyle: "single", border: ["top"], borderColor: tokens.border }}>
123
+ {footer}
124
+ </box>
125
+ ) : null}
126
+ </box>
127
+ );
128
+ }
@@ -0,0 +1,25 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { PanelColumn, FileTab } from "./PanelColumn";
5
+
6
+ export const meta: StoryMeta = { title: "Surfaces/PanelColumn" };
7
+
8
+ export const WithFileTab: Story = {
9
+ render: () => (
10
+ <box style={{ width: "100%", height: "100%", flexDirection: "row" }}>
11
+ <PanelColumn
12
+ width={40}
13
+ border="left"
14
+ header={<FileTab label="Welcome" active onClose={() => {}} theme={DARK} />}
15
+ theme={DARK}
16
+ >
17
+ <box style={{ paddingLeft: 1, paddingTop: 1 }}>
18
+ <text fg={DARK.textDim}>panel body</text>
19
+ </box>
20
+ </PanelColumn>
21
+ </box>
22
+ ),
23
+ expectedColors: [DARK.accent, DARK.border],
24
+ size: { width: 50, height: 8 },
25
+ };