@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.
Files changed (316) hide show
  1. package/native/build-pty.sh +51 -0
  2. package/native/build.sh +82 -0
  3. package/native/darwin-arm64/libcuelooppty.dylib +0 -0
  4. package/native/darwin-arm64/libcueloopvt.dylib +0 -0
  5. package/native/src/pty.zig +193 -0
  6. package/native/src/shim.zig +114 -0
  7. package/package.json +42 -34
  8. package/src/App.appearance.test.tsx +53 -0
  9. package/src/App.autoclose.test.tsx +77 -51
  10. package/src/App.diff.test.tsx +259 -45
  11. package/src/App.export.test.tsx +58 -41
  12. package/src/App.focus.test.ts +89 -0
  13. package/src/App.identity.test.tsx +78 -0
  14. package/src/App.plan-render.test.tsx +209 -0
  15. package/src/App.plan-review.test.tsx +330 -161
  16. package/src/App.prototype-markdown.test.tsx +128 -0
  17. package/src/App.prototype.test.tsx +176 -0
  18. package/src/App.readonly.test.tsx +129 -67
  19. package/src/App.regression.test.tsx +94 -0
  20. package/src/App.rendering.test.tsx +61 -35
  21. package/src/App.share-dialog.test.tsx +172 -0
  22. package/src/App.submit-confirm.test.tsx +264 -0
  23. package/src/App.test.tsx +402 -126
  24. package/src/App.toast.test.tsx +98 -0
  25. package/src/App.tsx +1402 -1060
  26. package/src/App.visual.test.tsx +57 -0
  27. package/src/App.walk.test.tsx +299 -0
  28. package/src/App.workbench.test.tsx +774 -0
  29. package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  30. package/src/annotation-palette.ts +62 -0
  31. package/src/ansi-screen.test.ts +28 -0
  32. package/src/ansi-screen.ts +72 -0
  33. package/src/app-key-state.ts +61 -0
  34. package/src/app-screens.tsx +577 -0
  35. package/src/app-view-model.ts +168 -0
  36. package/src/attribution.test.ts +31 -0
  37. package/src/attribution.ts +42 -0
  38. package/src/client-extension-registry.test.tsx +61 -0
  39. package/src/client-extension-registry.ts +159 -0
  40. package/src/clipboard.test.ts +23 -0
  41. package/src/clipboard.ts +33 -0
  42. package/src/components/AnnotatableFileView.stories.tsx +42 -0
  43. package/src/components/AnnotatableFileView.test.tsx +102 -0
  44. package/src/components/AnnotatableFileView.tsx +134 -0
  45. package/src/components/AnnotationCards.stories.tsx +86 -0
  46. package/src/components/AnnotationCards.tsx +341 -0
  47. package/src/components/AnsiScreen.stories.tsx +12 -0
  48. package/src/components/AnsiScreen.tsx +28 -0
  49. package/src/components/AppHeader.stories.tsx +50 -0
  50. package/src/components/AppHeader.tsx +83 -0
  51. package/src/components/AppShell.brand.test.tsx +58 -0
  52. package/src/components/AppShell.extensions.test.tsx +191 -0
  53. package/src/components/AppShell.stories.tsx +89 -0
  54. package/src/components/AppShell.tsx +362 -0
  55. package/src/components/AppShell.zoom.test.tsx +135 -0
  56. package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
  57. package/src/components/BareWorkbenchFileView.tsx +64 -0
  58. package/src/components/Breadcrumb.stories.tsx +19 -0
  59. package/src/components/Breadcrumb.tsx +45 -0
  60. package/src/components/ChangesColumn.stories.tsx +44 -0
  61. package/src/components/ChangesColumn.tsx +199 -0
  62. package/src/components/ChangesFileTree.test.tsx +61 -0
  63. package/src/components/CodeBlock.stories.tsx +42 -0
  64. package/src/components/CodeBlock.tsx +84 -0
  65. package/src/components/CompletionOverlay.stories.tsx +37 -0
  66. package/src/components/CompletionOverlay.tsx +77 -0
  67. package/src/components/ConfirmCard.stories.tsx +37 -0
  68. package/src/components/ConfirmCard.tsx +150 -0
  69. package/src/components/ConfirmDialog.stories.tsx +21 -0
  70. package/src/components/ConfirmDialog.tsx +72 -0
  71. package/src/components/DiffContentView.scroll.test.tsx +149 -0
  72. package/src/components/DiffContentView.split.test.tsx +140 -0
  73. package/src/components/DiffContentView.stories.tsx +80 -0
  74. package/src/components/DiffContentView.syntax.test.tsx +82 -0
  75. package/src/components/DiffContentView.tsx +906 -0
  76. package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
  77. package/src/components/DiscussionMarkerRail.tsx +186 -0
  78. package/src/components/EditorGrid.stories.tsx +59 -0
  79. package/src/components/EditorGrid.tabs.test.tsx +118 -0
  80. package/src/components/EditorGrid.tsx +426 -0
  81. package/src/components/EditorGrid.zoom.test.tsx +68 -0
  82. package/src/components/ExtensionZone.stories.tsx +28 -0
  83. package/src/components/ExtensionZone.tsx +145 -0
  84. package/src/components/FileContentsView.stories.tsx +16 -0
  85. package/src/components/FileContentsView.test.tsx +29 -0
  86. package/src/components/FileContentsView.tsx +98 -0
  87. package/src/components/GridTabContent.stories.tsx +59 -0
  88. package/src/components/GridTabContent.tsx +182 -0
  89. package/src/components/HandleChipsInput.stories.tsx +24 -0
  90. package/src/components/HandleChipsInput.test.tsx +35 -0
  91. package/src/components/HandleChipsInput.tsx +145 -0
  92. package/src/components/KeybindsDialog.stories.tsx +31 -0
  93. package/src/components/KeybindsDialog.tsx +103 -0
  94. package/src/components/MarkdownGridBlock.stories.tsx +33 -0
  95. package/src/components/MarkdownGridBlock.tsx +72 -0
  96. package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
  97. package/src/components/MarkdownThreadEditor.test.tsx +98 -0
  98. package/src/components/MarkdownThreadEditor.tsx +162 -0
  99. package/src/components/MenuBar.stories.tsx +28 -0
  100. package/src/components/MenuBar.tsx +44 -0
  101. package/src/components/NavModeHint.stories.tsx +40 -0
  102. package/src/components/NavModeHint.tsx +31 -0
  103. package/src/components/OverlayScrollbar.stories.tsx +33 -0
  104. package/src/components/OverlayScrollbar.test.tsx +139 -0
  105. package/src/components/OverlayScrollbar.tsx +129 -0
  106. package/src/components/PaneGrid.stories.tsx +84 -0
  107. package/src/components/PaneGrid.tsx +128 -0
  108. package/src/components/PanelColumn.stories.tsx +25 -0
  109. package/src/components/PanelColumn.tsx +98 -0
  110. package/src/components/ProjectTreeView.stories.tsx +25 -0
  111. package/src/components/ProjectTreeView.test.tsx +75 -0
  112. package/src/components/ProjectTreeView.tsx +125 -0
  113. package/src/components/PromptDialog.stories.tsx +36 -0
  114. package/src/components/PromptDialog.test.tsx +61 -0
  115. package/src/components/PromptDialog.tsx +99 -0
  116. package/src/components/PrototypeContentView.stories.tsx +19 -0
  117. package/src/components/PrototypeContentView.tsx +446 -0
  118. package/src/components/RootOverlay.stories.tsx +45 -0
  119. package/src/components/RootOverlay.tsx +48 -0
  120. package/src/components/ScrollArea.stories.tsx +20 -0
  121. package/src/components/ScrollArea.tsx +44 -0
  122. package/src/components/SettingsDialog.stories.tsx +117 -0
  123. package/src/components/SettingsDialog.tsx +186 -0
  124. package/src/components/SettingsRows.stories.tsx +18 -0
  125. package/src/components/SettingsRows.tsx +112 -0
  126. package/src/components/ShareDialog.stories.tsx +121 -0
  127. package/src/components/ShareDialog.tsx +552 -0
  128. package/src/components/ShellHeader.stories.tsx +50 -0
  129. package/src/components/ShellHeader.tsx +137 -0
  130. package/src/components/SlashComposer.stories.tsx +37 -0
  131. package/src/components/SlashComposer.tsx +87 -0
  132. package/src/components/ThreadFooter.stories.tsx +30 -0
  133. package/src/components/ThreadFooter.test.tsx +47 -0
  134. package/src/components/ThreadFooter.tsx +76 -0
  135. package/src/components/ThreadTree.stories.tsx +46 -0
  136. package/src/components/ThreadTree.tsx +255 -0
  137. package/src/components/ThreadView.selection.test.tsx +951 -0
  138. package/src/components/ThreadView.slash.test.tsx +146 -0
  139. package/src/components/ThreadView.stories.tsx +26 -0
  140. package/src/components/ThreadView.tsx +525 -0
  141. package/src/components/Toast.stories.tsx +19 -0
  142. package/src/components/Toast.tsx +52 -0
  143. package/src/components/Tooltip.stories.tsx +26 -0
  144. package/src/components/Tooltip.tsx +72 -0
  145. package/src/components/TreePane.stories.tsx +106 -0
  146. package/src/components/TreePane.tsx +157 -0
  147. package/src/components/WalkWizard.stories.tsx +100 -0
  148. package/src/components/WalkWizard.tsx +150 -0
  149. package/src/components/WelcomePlayground.stories.tsx +12 -0
  150. package/src/components/WelcomePlayground.test.tsx +49 -0
  151. package/src/components/WelcomePlayground.tsx +139 -0
  152. package/src/components/WorkspacePanels.stories.tsx +36 -0
  153. package/src/components/WorkspacePanels.tsx +116 -0
  154. package/src/components/__snapshots__/stories.test.tsx.snap +2264 -0
  155. package/src/components/diff-content-view-layout.test.ts +198 -0
  156. package/src/components/diff-content-view-layout.ts +84 -0
  157. package/src/components/editor-grid.test.ts +86 -0
  158. package/src/components/editor-grid.ts +138 -0
  159. package/src/components/file-tree.test.ts +60 -0
  160. package/src/components/file-tree.ts +112 -0
  161. package/src/components/menu-control.stories.tsx +35 -0
  162. package/src/components/menu-control.tsx +43 -0
  163. package/src/components/pages/Workspace.stories.tsx +181 -0
  164. package/src/components/primitives/Button.stories.tsx +32 -0
  165. package/src/components/primitives/Button.tsx +59 -0
  166. package/src/components/primitives/Card.stories.tsx +25 -0
  167. package/src/components/primitives/Card.tsx +63 -0
  168. package/src/components/primitives/Dialog.stories.tsx +24 -0
  169. package/src/components/primitives/Dialog.test.tsx +37 -0
  170. package/src/components/primitives/Dialog.tsx +71 -0
  171. package/src/components/primitives/DialogActions.stories.tsx +20 -0
  172. package/src/components/primitives/DialogActions.tsx +39 -0
  173. package/src/components/primitives/IconButton.stories.tsx +36 -0
  174. package/src/components/primitives/IconButton.tsx +62 -0
  175. package/src/components/primitives/StatusBar.stories.tsx +11 -0
  176. package/src/components/primitives/StatusBar.tsx +23 -0
  177. package/src/components/primitives/Switch.stories.tsx +17 -0
  178. package/src/components/primitives/Switch.tsx +32 -0
  179. package/src/components/primitives/Tabs.stories.tsx +29 -0
  180. package/src/components/primitives/Tabs.tsx +102 -0
  181. package/src/components/primitives/Toolbar.stories.tsx +17 -0
  182. package/src/components/primitives/Toolbar.tsx +14 -0
  183. package/src/components/primitives/Tree.stories.tsx +124 -0
  184. package/src/components/primitives/Tree.tsx +100 -0
  185. package/src/components/primitives/frame.test.ts +12 -0
  186. package/src/components/primitives/frame.ts +17 -0
  187. package/src/components/primitives/icons.ts +61 -0
  188. package/src/components/primitives/tree-model.test.ts +99 -0
  189. package/src/components/primitives/tree-model.ts +137 -0
  190. package/src/components/quick-actions-editor.stories.tsx +58 -0
  191. package/src/components/quick-actions-editor.test.tsx +117 -0
  192. package/src/components/quick-actions-editor.tsx +226 -0
  193. package/src/components/share-dialog-store.ts +58 -0
  194. package/src/components/stories-app.tsx +193 -0
  195. package/src/components/stories.test.tsx +74 -0
  196. package/src/components/story-fixtures.ts +102 -0
  197. package/src/components/story-tree.test.ts +108 -0
  198. package/src/components/story.ts +159 -0
  199. package/src/components/syntax-highlight.ts +133 -0
  200. package/src/components/terminal-pane.test.tsx +40 -0
  201. package/src/components/terminal-pane.ts +199 -0
  202. package/src/components/theme-context.stories.tsx +46 -0
  203. package/src/components/theme-context.tsx +37 -0
  204. package/src/components/thread-tree.test.ts +134 -0
  205. package/src/components/thread-tree.ts +109 -0
  206. package/src/components/truncate-text.ts +7 -0
  207. package/src/components/truncate-title.test.ts +31 -0
  208. package/src/components/truncate-title.ts +15 -0
  209. package/src/config-identity.test.ts +78 -0
  210. package/src/config.test.ts +579 -34
  211. package/src/config.ts +533 -57
  212. package/src/diff-fold.test.ts +78 -0
  213. package/src/diff-fold.ts +126 -0
  214. package/src/diff-hunk-curate.test.ts +315 -0
  215. package/src/diff-hunk-curate.ts +153 -0
  216. package/src/diff-intraline.test.ts +238 -0
  217. package/src/diff-intraline.ts +236 -0
  218. package/src/diff-syntax.test.ts +112 -0
  219. package/src/diff-syntax.ts +179 -0
  220. package/src/discussions.ts +79 -0
  221. package/src/editor.test.ts +149 -0
  222. package/src/editor.ts +139 -9
  223. package/src/ghostty-terminal.test.ts +85 -0
  224. package/src/ghostty-terminal.ts +199 -0
  225. package/src/github-identity.test.ts +58 -0
  226. package/src/github-identity.ts +41 -0
  227. package/src/herdr.test.ts +9 -34
  228. package/src/herdr.ts +25 -21
  229. package/src/index.ts +28 -0
  230. package/src/integrations.test.ts +41 -0
  231. package/src/integrations.ts +32 -0
  232. package/src/intent-dispatch.test.ts +724 -0
  233. package/src/intent-dispatch.ts +690 -0
  234. package/src/key-bindings.test.ts +156 -0
  235. package/src/key-bindings.ts +459 -0
  236. package/src/keymap.test.ts +456 -115
  237. package/src/keymap.ts +315 -81
  238. package/src/kitty-image.test.ts +66 -0
  239. package/src/kitty-image.ts +138 -0
  240. package/src/launch-layout.test.ts +67 -0
  241. package/src/launch-layout.ts +47 -0
  242. package/src/mark-runs.ts +121 -0
  243. package/src/markdown-editor-syntax-style.ts +50 -0
  244. package/src/markdown-grid.test.ts +101 -0
  245. package/src/markdown-grid.ts +203 -0
  246. package/src/markdown-highlight.test.ts +61 -0
  247. package/src/markdown-highlight.ts +122 -0
  248. package/src/pasted-image.test.ts +67 -0
  249. package/src/pasted-image.ts +60 -0
  250. package/src/perf/otlp-trace.test.ts +116 -0
  251. package/src/perf/otlp-trace.ts +130 -0
  252. package/src/perf/perf-report.ts +17 -0
  253. package/src/perf/perf-timings.test.ts +18 -0
  254. package/src/perf/perf-timings.ts +38 -0
  255. package/src/prototype-browser.test.ts +81 -0
  256. package/src/prototype-browser.ts +329 -0
  257. package/src/prototype-pixels.tsx +35 -0
  258. package/src/pty.test.ts +81 -0
  259. package/src/pty.ts +245 -0
  260. package/src/ready-signal.test.ts +85 -0
  261. package/src/ready-signal.ts +52 -0
  262. package/src/run.ts +85 -12
  263. package/src/serve.test.ts +24 -12
  264. package/src/serve.ts +52 -10
  265. package/src/share.ts +237 -0
  266. package/src/skills.test.ts +74 -0
  267. package/src/skills.ts +75 -0
  268. package/src/slash-palette.test.ts +103 -0
  269. package/src/slash-palette.ts +129 -0
  270. package/src/split-diff.test.ts +110 -0
  271. package/src/split-diff.ts +149 -0
  272. package/src/test-support.test.ts +264 -0
  273. package/src/test-support.ts +369 -0
  274. package/src/theme-presets.test.ts +51 -0
  275. package/src/theme-presets.ts +219 -0
  276. package/src/theme.ts +88 -19
  277. package/src/thread-chords.test.ts +79 -0
  278. package/src/thread-chords.ts +163 -0
  279. package/src/thread-code-syntax.ts +45 -0
  280. package/src/thread-controller.curate.test.ts +476 -0
  281. package/src/thread-controller.derivation.test.ts +62 -0
  282. package/src/thread-controller.live-diff.test.ts +322 -0
  283. package/src/thread-controller.optimistic.test.ts +69 -0
  284. package/src/thread-controller.share.test.ts +561 -0
  285. package/src/thread-controller.switch-cache.test.ts +176 -0
  286. package/src/thread-controller.tree.test.ts +261 -0
  287. package/src/thread-controller.ts +2050 -0
  288. package/src/thread-selection.test.ts +210 -0
  289. package/src/thread-selection.ts +243 -0
  290. package/src/tree-view.test.ts +128 -0
  291. package/src/tree-view.ts +153 -0
  292. package/src/use-annotation-surface.tsx +975 -0
  293. package/src/use-changes-workbench.ts +190 -0
  294. package/src/use-frame-measure.ts +41 -0
  295. package/src/use-remember-layout.ts +31 -0
  296. package/src/use-repo-changes.ts +87 -0
  297. package/src/use-settings-dialog.tsx +364 -0
  298. package/src/use-terminal-virtualizer.ts +259 -0
  299. package/src/use-thread-body-editing.tsx +58 -0
  300. package/src/version.ts +5 -0
  301. package/src/view-diff.test.ts +279 -9
  302. package/src/view-diff.ts +460 -24
  303. package/src/view-plan.regression.test.ts +60 -0
  304. package/src/view-plan.test.ts +545 -0
  305. package/src/view-plan.ts +1019 -0
  306. package/src/visual/contrast.test.ts +35 -0
  307. package/src/visual/contrast.ts +33 -0
  308. package/src/walk.test.ts +121 -0
  309. package/src/walk.ts +74 -0
  310. package/src/workbench-snapshot.test.ts +99 -0
  311. package/src/workbench-snapshot.ts +59 -0
  312. package/src/session-controller.ts +0 -398
  313. package/src/syntax.test.ts +0 -41
  314. package/src/syntax.ts +0 -88
  315. package/src/view.test.ts +0 -157
  316. package/src/view.ts +0 -364
@@ -0,0 +1,552 @@
1
+ /**
2
+ * The share overlay: a settings-style two-column modal. The left nav lists
3
+ * "share externally" and "export"; the right pane shows either the thread's
4
+ * share links - each an address with copy and a ⋮ menu to revoke or open its
5
+ * settings - or, reached from "+ new link" / "open link settings", a full-body
6
+ * wizard that names a link and toggles a GitHub-login allowlist. Export is a
7
+ * coming-soon placeholder. The dialog's UI state (which category, whether the
8
+ * wizard is open, its draft) lives in the shared zustand store; the link data
9
+ * arrives from the controller through props and the actions call back into it.
10
+ */
11
+
12
+ import React, { useEffect, useRef, useState } from "react";
13
+ import { useKeyboard, useTerminalDimensions } from "@opentui/react";
14
+ import type { KeyBinding, MouseEvent, TextareaRenderable } from "@opentui/core";
15
+ import { DEFAULT_SHARE_HOST } from "@cueloop/daemon/share-blob";
16
+ import type { ShareLink } from "@cueloop/schema";
17
+ import type { Theme } from "../theme";
18
+ import type { NewShareLink } from "../thread-controller";
19
+ import { useComponentTheme } from "./theme-context";
20
+ import { Dialog } from "./primitives/Dialog";
21
+ import { DialogActions } from "./primitives/DialogActions";
22
+ import { Button } from "./primitives/Button";
23
+ import { Switch } from "./primitives/Switch";
24
+ import { Tree } from "./primitives/Tree";
25
+ import type { TreeNode } from "./primitives/tree-model";
26
+ import { NERD } from "./primitives/icons";
27
+ import { HandleChipsInput } from "./HandleChipsInput";
28
+ import {
29
+ newWizardDraft,
30
+ useShareDialog,
31
+ shareDialogStore,
32
+ type ShareCategory,
33
+ type ShareWizardDraft,
34
+ } from "./share-dialog-store";
35
+
36
+ // ⏎ saves the link rather than adding a newline to the one-line name field.
37
+ const NAME_KEY_BINDINGS: KeyBinding[] = [{ name: "return", action: "submit" }];
38
+
39
+ /** The two left-nav categories, in order. */
40
+ const CATEGORIES: { id: ShareCategory; label: string }[] = [
41
+ { id: "external", label: "share externally" },
42
+ { id: "export", label: "export" },
43
+ ];
44
+
45
+ const NAV_NODES: TreeNode[] = CATEGORIES.map((category) => ({
46
+ id: category.id,
47
+ label: category.label,
48
+ }));
49
+
50
+ /** Narrow a nav row id back to its category (the nav only holds the two known ids). */
51
+ function categoryFromId(id: string): ShareCategory {
52
+ return id === "export" ? "export" : "external";
53
+ }
54
+
55
+ /** The address a viewer connects to, e.g. `p_ab12@cueloop.dev` (no `ssh ` prefix; the row is a label). */
56
+ function shareAddress(id: string): string {
57
+ return `${id}@${DEFAULT_SHARE_HOST}`;
58
+ }
59
+
60
+ /** Leave the wizard back to the links list. */
61
+ function backToList(): void {
62
+ shareDialogStore.getState().closeWizard();
63
+ }
64
+
65
+ /** The wizard draft that edits an existing link in place. */
66
+ function draftForLink(link: ShareLink, threadName: string): ShareWizardDraft {
67
+ return {
68
+ editingId: link.id,
69
+ name: link.name ?? threadName,
70
+ requireAuth: link.requireAuth,
71
+ allowlist: link.allowlist,
72
+ };
73
+ }
74
+
75
+ function NavColumn({
76
+ activeCategory,
77
+ onSelect,
78
+ theme,
79
+ }: {
80
+ activeCategory: ShareCategory;
81
+ onSelect: (category: ShareCategory) => void;
82
+ theme?: Theme;
83
+ }): React.ReactNode {
84
+ const tokens = useComponentTheme(theme);
85
+
86
+ return (
87
+ <box style={{ flexDirection: "column", width: 22, paddingLeft: 1, paddingRight: 1 }}>
88
+ <Tree
89
+ nodes={NAV_NODES}
90
+ expandedIds={new Set<string>()}
91
+ selectedId={activeCategory}
92
+ hideIcons
93
+ selectedBackground={tokens.border}
94
+ onSelect={(id) => onSelect(categoryFromId(id))}
95
+ onToggle={(id) => onSelect(categoryFromId(id))}
96
+ theme={theme}
97
+ />
98
+ </box>
99
+ );
100
+ }
101
+
102
+ function KebabMenu({
103
+ onDelete,
104
+ onSettings,
105
+ tokens,
106
+ }: {
107
+ onDelete: () => void;
108
+ onSettings: () => void;
109
+ tokens: Theme;
110
+ }): React.ReactNode {
111
+ const item = (label: string, onPick: () => void): React.ReactNode => (
112
+ <box
113
+ onMouseUp={(event: MouseEvent) => {
114
+ event.stopPropagation();
115
+ onPick();
116
+ }}
117
+ style={{ paddingLeft: 1, paddingRight: 1 }}
118
+ >
119
+ <text fg={tokens.text}>{label}</text>
120
+ </box>
121
+ );
122
+
123
+ return (
124
+ <box
125
+ onMouseUp={(event: MouseEvent) => event.stopPropagation()}
126
+ style={{
127
+ position: "absolute",
128
+ top: 1,
129
+ right: 0,
130
+ flexDirection: "column",
131
+ border: true,
132
+ borderStyle: "single",
133
+ borderColor: tokens.border,
134
+ backgroundColor: tokens.elevated,
135
+ }}
136
+ >
137
+ {item("delete link", onDelete)}
138
+ {item("open link settings", onSettings)}
139
+ </box>
140
+ );
141
+ }
142
+
143
+ function LinkRow({
144
+ link,
145
+ selected,
146
+ menuOpen,
147
+ onCopy,
148
+ onToggleMenu,
149
+ onDelete,
150
+ onSettings,
151
+ tokens,
152
+ }: {
153
+ link: ShareLink;
154
+ selected: boolean;
155
+ menuOpen: boolean;
156
+ onCopy: () => void;
157
+ onToggleMenu: () => void;
158
+ onDelete: () => void;
159
+ onSettings: () => void;
160
+ tokens: Theme;
161
+ }): React.ReactNode {
162
+ // the name is a short local label; unnamed links show their address instead
163
+ const label = link.name ?? shareAddress(link.id);
164
+
165
+ return (
166
+ <box
167
+ style={{
168
+ flexDirection: "row",
169
+ position: "relative",
170
+ alignItems: "center",
171
+ paddingLeft: 1,
172
+ backgroundColor: selected ? tokens.border : undefined,
173
+ }}
174
+ >
175
+ <text
176
+ fg={selected ? tokens.text : tokens.textMuted}
177
+ style={{ wrapMode: "none", flexShrink: 1 }}
178
+ >
179
+ {label}
180
+ </text>
181
+ <box style={{ flexGrow: 1 }} />
182
+ <box onMouseUp={onCopy} style={{ paddingLeft: 2 }}>
183
+ <text fg={tokens.textDim}>copy link</text>
184
+ </box>
185
+ <box
186
+ onMouseUp={(event: MouseEvent) => {
187
+ event.stopPropagation();
188
+ onToggleMenu();
189
+ }}
190
+ style={{ paddingLeft: 2 }}
191
+ >
192
+ <text fg={tokens.textDim}>{NERD.kebab}</text>
193
+ </box>
194
+ {menuOpen ? <KebabMenu onDelete={onDelete} onSettings={onSettings} tokens={tokens} /> : null}
195
+ </box>
196
+ );
197
+ }
198
+
199
+ function LinksList({
200
+ threadName,
201
+ links,
202
+ selectedIndex,
203
+ menuOpenId,
204
+ onCopy,
205
+ onToggleMenu,
206
+ onDelete,
207
+ onSettings,
208
+ onNewLink,
209
+ tokens,
210
+ theme,
211
+ }: {
212
+ threadName: string;
213
+ links: ShareLink[];
214
+ selectedIndex: number;
215
+ menuOpenId: string | null;
216
+ onCopy: (id: string) => void;
217
+ onToggleMenu: (id: string) => void;
218
+ onDelete: (id: string) => void;
219
+ onSettings: (link: ShareLink) => void;
220
+ onNewLink: () => void;
221
+ tokens: Theme;
222
+ theme?: Theme;
223
+ }): React.ReactNode {
224
+ if (links.length === 0) {
225
+ return (
226
+ <box style={{ flexGrow: 1, alignItems: "center", justifyContent: "center" }}>
227
+ <Button variant="solid" onPress={onNewLink} theme={theme}>
228
+ {" + new link "}
229
+ </Button>
230
+ </box>
231
+ );
232
+ }
233
+
234
+ return (
235
+ <box style={{ flexDirection: "column", flexGrow: 1 }}>
236
+ <text fg={tokens.textDim}>{`links created for ${threadName}`}</text>
237
+ <text> </text>
238
+ {links.map((link, index) => (
239
+ <LinkRow
240
+ key={link.id}
241
+ link={link}
242
+ selected={index === selectedIndex}
243
+ menuOpen={menuOpenId === link.id}
244
+ onCopy={() => onCopy(link.id)}
245
+ onToggleMenu={() => onToggleMenu(link.id)}
246
+ onDelete={() => onDelete(link.id)}
247
+ onSettings={() => onSettings(link)}
248
+ tokens={tokens}
249
+ />
250
+ ))}
251
+ <text> </text>
252
+ <box style={{ flexDirection: "row" }}>
253
+ <Button variant="solid" onPress={onNewLink} theme={theme}>
254
+ {" + new link "}
255
+ </Button>
256
+ </box>
257
+ </box>
258
+ );
259
+ }
260
+
261
+ /** The focus ring positions inside the wizard, in vertical order. */
262
+ type WizardFocus = "name" | "auth" | "allowlist" | "actions";
263
+
264
+ function Wizard({
265
+ draft,
266
+ focus,
267
+ saveDisabled,
268
+ onName,
269
+ onToggleAuth,
270
+ onAllowlist,
271
+ onSave,
272
+ onCancel,
273
+ tokens,
274
+ theme,
275
+ }: {
276
+ draft: ShareWizardDraft;
277
+ focus: WizardFocus;
278
+ saveDisabled: boolean;
279
+ onName: (name: string) => void;
280
+ onToggleAuth: () => void;
281
+ onAllowlist: (logins: string[]) => void;
282
+ onSave: () => void;
283
+ onCancel: () => void;
284
+ tokens: Theme;
285
+ theme?: Theme;
286
+ }): React.ReactNode {
287
+ const nameRef = useRef<TextareaRenderable | null>(null);
288
+
289
+ useEffect(() => {
290
+ if (focus !== "name") return;
291
+ nameRef.current?.focus();
292
+ if (nameRef.current) nameRef.current.cursorOffset = draft.name.length;
293
+ // eslint-disable-next-line react-hooks/exhaustive-deps
294
+ }, [focus]);
295
+
296
+ return (
297
+ <box style={{ flexDirection: "column", flexGrow: 1, paddingLeft: 2, paddingRight: 1 }}>
298
+ <text fg={focus === "name" ? tokens.accent : tokens.textDim}>link name</text>
299
+ <textarea
300
+ ref={nameRef}
301
+ focused={focus === "name"}
302
+ initialValue={draft.name}
303
+ placeholder="link name"
304
+ keyBindings={NAME_KEY_BINDINGS}
305
+ onSubmit={onSave}
306
+ onContentChange={() => onName(nameRef.current?.plainText ?? "")}
307
+ style={{
308
+ height: 1,
309
+ backgroundColor: tokens.elevated,
310
+ focusedBackgroundColor: tokens.elevated,
311
+ textColor: tokens.text,
312
+ focusedTextColor: tokens.text,
313
+ }}
314
+ />
315
+ <text> </text>
316
+ <box style={{ flexDirection: "row", alignItems: "center" }}>
317
+ <text fg={focus === "auth" ? tokens.accent : tokens.textDim}>require auth</text>
318
+ <box style={{ flexGrow: 1 }} />
319
+ <Switch on={draft.requireAuth} onToggle={onToggleAuth} theme={theme} />
320
+ </box>
321
+ {draft.requireAuth ? (
322
+ <box style={{ marginTop: 1 }}>
323
+ <HandleChipsInput
324
+ logins={draft.allowlist}
325
+ onChange={onAllowlist}
326
+ focused={focus === "allowlist"}
327
+ onEmptyBackspace={onToggleAuth}
328
+ theme={theme}
329
+ />
330
+ </box>
331
+ ) : null}
332
+ <box style={{ flexGrow: 1 }} />
333
+ <DialogActions
334
+ confirmLabel={draft.editingId ? "save" : "create"}
335
+ onConfirm={onSave}
336
+ onCancel={onCancel}
337
+ confirmDisabled={saveDisabled}
338
+ theme={theme}
339
+ />
340
+ </box>
341
+ );
342
+ }
343
+
344
+ export interface ShareDialogProps {
345
+ isOpen: boolean;
346
+ /** The thread's title, shown in the list header and prefilled as a new link's name. */
347
+ threadName: string;
348
+ /** The thread's live share links, newest last. */
349
+ links: ShareLink[];
350
+ /** Owners publish and edit; an observer sees the list read-only (no create/edit/delete). */
351
+ isOwner: boolean;
352
+ onCreateLink: (input: NewShareLink) => void;
353
+ onUpdateLink: (id: string, input: NewShareLink) => void;
354
+ onDeleteLink: (id: string) => void;
355
+ onCopyLink: (id: string) => void;
356
+ onClose: () => void;
357
+ theme?: Theme;
358
+ }
359
+
360
+ export function ShareDialog({
361
+ isOpen,
362
+ threadName,
363
+ links,
364
+ isOwner,
365
+ onCreateLink,
366
+ onUpdateLink,
367
+ onDeleteLink,
368
+ onCopyLink,
369
+ onClose,
370
+ theme,
371
+ }: ShareDialogProps): React.ReactNode {
372
+ const tokens = useComponentTheme(theme);
373
+ const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
374
+ const category = useShareDialog((state) => state.category);
375
+ const wizard = useShareDialog((state) => state.wizard);
376
+
377
+ const [activeZone, setActiveZone] = useState<"nav" | "body">("nav");
378
+ const [listCursor, setListCursor] = useState(0);
379
+ const [menuOpenId, setMenuOpenId] = useState<string | null>(null);
380
+ const [wizardFocus, setWizardFocus] = useState<WizardFocus>("name");
381
+
382
+ const rowCount = links.length + 1; // links plus the "+ new link" row
383
+
384
+ const openWizardWith = (draft: ShareWizardDraft): void => {
385
+ setMenuOpenId(null);
386
+ setWizardFocus("name");
387
+ shareDialogStore.getState().openWizard(draft);
388
+ };
389
+
390
+ const wizardRing = (): WizardFocus[] =>
391
+ wizard?.requireAuth ? ["name", "auth", "allowlist", "actions"] : ["name", "auth", "actions"];
392
+
393
+ const moveWizardFocus = (delta: number): void => {
394
+ const ring = wizardRing();
395
+ const at = Math.max(0, ring.indexOf(wizardFocus));
396
+ setWizardFocus(ring[Math.min(ring.length - 1, Math.max(0, at + delta))] ?? "name");
397
+ };
398
+
399
+ // a private link needs at least one handle, else the gateway locks everyone out
400
+ const saveDisabled = wizard !== null && wizard.requireAuth && wizard.allowlist.length === 0;
401
+
402
+ const saveWizard = (): void => {
403
+ if (!wizard || saveDisabled) return;
404
+ const input: NewShareLink = {
405
+ name: wizard.name.trim() || undefined,
406
+ requireAuth: wizard.requireAuth,
407
+ allowlist: wizard.allowlist,
408
+ };
409
+
410
+ if (wizard.editingId) onUpdateLink(wizard.editingId, input);
411
+ else onCreateLink(input);
412
+ backToList();
413
+ };
414
+
415
+ const activateListRow = (): void => {
416
+ if (listCursor >= links.length) return openWizardWith(newWizardDraft(threadName));
417
+ const link = links[listCursor];
418
+
419
+ if (link) onCopyLink(link.id);
420
+ };
421
+
422
+ // turning auth on drops the cursor straight into the handles field; turning it off resets them
423
+ const toggleAuth = (): void => {
424
+ if (!wizard) return;
425
+ const next = !wizard.requireAuth;
426
+
427
+ shareDialogStore.getState().setRequireAuth(next);
428
+ if (!next) shareDialogStore.getState().setAllowlist([]);
429
+ setWizardFocus(next ? "allowlist" : "auth");
430
+ };
431
+
432
+ // grammar keys only bite outside a focused text field, where they cannot be typed
433
+ const handleWizardKey = (name: string): void => {
434
+ if (!wizard) return;
435
+ if (name === "escape") return backToList();
436
+ if (name === "down") return moveWizardFocus(1);
437
+ if (name === "up") return moveWizardFocus(-1);
438
+ const activated = name === "return" || name === "enter";
439
+
440
+ if (wizardFocus === "auth" && (activated || name === "space")) return toggleAuth();
441
+ if (wizardFocus === "actions" && activated) saveWizard();
442
+ };
443
+
444
+ const handleNavKey = (name: string): void => {
445
+ if (name === "j" || name === "down") return shareDialogStore.getState().setCategory("export");
446
+ if (name === "k" || name === "up") return shareDialogStore.getState().setCategory("external");
447
+ const enter =
448
+ name === "l" || name === "right" || name === "return" || name === "enter" || name === "tab";
449
+
450
+ if (enter && category === "external" && isOwner) setActiveZone("body");
451
+ };
452
+
453
+ const editLinkAtCursor = (): void => {
454
+ const link = links[listCursor];
455
+
456
+ if (link) openWizardWith(draftForLink(link, threadName));
457
+ };
458
+
459
+ const handleBodyKey = (name: string): void => {
460
+ if (name === "h" || name === "left") return setActiveZone("nav");
461
+ if (name === "j" || name === "down")
462
+ return setListCursor((cursor) => Math.min(cursor + 1, rowCount - 1));
463
+ if (name === "k" || name === "up") return setListCursor((cursor) => Math.max(cursor - 1, 0));
464
+ if (name === "return" || name === "enter") return activateListRow();
465
+ if (listCursor >= links.length) return;
466
+ if (name === "e") return editLinkAtCursor();
467
+ if (name === "d") onDeleteLink(links[listCursor]!.id);
468
+ };
469
+
470
+ useKeyboard((key) => {
471
+ if (!isOpen) return;
472
+ if (wizard) return handleWizardKey(key.name);
473
+ if (menuOpenId !== null) return void (key.name === "escape" && setMenuOpenId(null));
474
+ if (key.name === "escape") return onClose();
475
+ if (activeZone === "nav") return handleNavKey(key.name);
476
+ handleBodyKey(key.name);
477
+ });
478
+
479
+ if (!isOpen) return null;
480
+
481
+ const footerHint = wizard
482
+ ? "↑/↓ move · space toggle · enter save · esc back"
483
+ : activeZone === "nav"
484
+ ? "j/k nav · l/enter open · esc close"
485
+ : "j/k row · enter copy · e edit · d delete · h back";
486
+
487
+ return (
488
+ <Dialog
489
+ isOpen
490
+ title=" Share "
491
+ width={Math.min(76, terminalWidth - 6)}
492
+ height={Math.min(22, terminalHeight - 4)}
493
+ background={tokens.elevated}
494
+ onDismiss={onClose}
495
+ theme={theme}
496
+ >
497
+ {wizard ? (
498
+ <Wizard
499
+ draft={wizard}
500
+ focus={wizardFocus}
501
+ saveDisabled={saveDisabled}
502
+ onName={(name) => shareDialogStore.getState().setName(name)}
503
+ onToggleAuth={toggleAuth}
504
+ onAllowlist={(logins) => shareDialogStore.getState().setAllowlist(logins)}
505
+ onSave={saveWizard}
506
+ onCancel={backToList}
507
+ tokens={tokens}
508
+ theme={theme}
509
+ />
510
+ ) : (
511
+ <box style={{ flexDirection: "row", flexGrow: 1 }}>
512
+ <NavColumn
513
+ activeCategory={category}
514
+ onSelect={(next) => {
515
+ shareDialogStore.getState().setCategory(next);
516
+ setActiveZone("nav");
517
+ }}
518
+ theme={theme}
519
+ />
520
+ <box style={{ borderStyle: "single", border: ["left"], borderColor: tokens.border }} />
521
+ <box style={{ flexDirection: "column", flexGrow: 1, paddingLeft: 2, paddingRight: 1 }}>
522
+ {category === "export" ? (
523
+ <box style={{ flexGrow: 1, alignItems: "center", justifyContent: "center" }}>
524
+ <text fg={tokens.textDim}>coming soon</text>
525
+ </box>
526
+ ) : (
527
+ <LinksList
528
+ threadName={threadName}
529
+ links={links}
530
+ selectedIndex={activeZone === "body" ? listCursor : -1}
531
+ menuOpenId={menuOpenId}
532
+ onCopy={onCopyLink}
533
+ onToggleMenu={(id) => setMenuOpenId((open) => (open === id ? null : id))}
534
+ onDelete={(id) => {
535
+ setMenuOpenId(null);
536
+ onDeleteLink(id);
537
+ }}
538
+ onSettings={(link) => openWizardWith(draftForLink(link, threadName))}
539
+ onNewLink={() => openWizardWith(newWizardDraft(threadName))}
540
+ tokens={tokens}
541
+ theme={theme}
542
+ />
543
+ )}
544
+ </box>
545
+ </box>
546
+ )}
547
+ <box style={{ flexDirection: "row", height: 1, paddingLeft: 1, paddingRight: 1 }}>
548
+ <text fg={tokens.textDim}>{footerHint}</text>
549
+ </box>
550
+ </Dialog>
551
+ );
552
+ }
@@ -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
+ };