@cueloop/client 0.1.0-alpha.10 → 0.1.0-alpha.101

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 (356) 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/annotations/annotation-palette.ts +62 -0
  9. package/src/annotations/components/AnnotationCards.stories.tsx +86 -0
  10. package/src/annotations/components/AnnotationCards.test.tsx +42 -0
  11. package/src/annotations/components/AnnotationCards.tsx +369 -0
  12. package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
  13. package/src/annotations/components/DiscussionMarkerRail.tsx +186 -0
  14. package/src/annotations/discussions.ts +77 -0
  15. package/src/annotations/thread-selection.test.ts +210 -0
  16. package/src/annotations/thread-selection.ts +242 -0
  17. package/src/annotations/use-annotation-surface.tsx +1243 -0
  18. package/src/app/App.appearance.test.tsx +53 -0
  19. package/src/{App.autoclose.test.tsx → app/App.autoclose.test.tsx} +77 -51
  20. package/src/app/App.deleted-scroll.test.tsx +187 -0
  21. package/src/app/App.diff.test.tsx +313 -0
  22. package/src/app/App.export.test.tsx +105 -0
  23. package/src/app/App.focus.test.ts +89 -0
  24. package/src/app/App.identity.test.tsx +84 -0
  25. package/src/app/App.plan-render.test.tsx +209 -0
  26. package/src/app/App.plan-review.test.tsx +428 -0
  27. package/src/app/App.prototype-markdown.test.tsx +128 -0
  28. package/src/app/App.prototype.test.tsx +176 -0
  29. package/src/app/App.readonly.test.tsx +210 -0
  30. package/src/app/App.regression.test.tsx +99 -0
  31. package/src/app/App.rendering.test.tsx +134 -0
  32. package/src/app/App.share-dialog.test.tsx +177 -0
  33. package/src/app/App.submit-confirm.test.tsx +267 -0
  34. package/src/app/App.test.tsx +496 -0
  35. package/src/app/App.thread-agent.test.ts +20 -0
  36. package/src/app/App.toast.test.tsx +124 -0
  37. package/src/app/App.tsx +1667 -0
  38. package/src/app/App.visual.test.tsx +59 -0
  39. package/src/app/App.walk.test.tsx +299 -0
  40. package/src/app/App.workbench.test.tsx +1149 -0
  41. package/src/app/app-key-state.ts +61 -0
  42. package/src/app/app-screens.tsx +606 -0
  43. package/src/app/app-view-model.ts +168 -0
  44. package/src/app/components/AppHeader.stories.tsx +50 -0
  45. package/src/app/components/AppHeader.tsx +81 -0
  46. package/src/app/components/AppShell.brand.test.tsx +58 -0
  47. package/src/app/components/AppShell.extensions.test.tsx +191 -0
  48. package/src/app/components/AppShell.stories.tsx +89 -0
  49. package/src/app/components/AppShell.tsx +358 -0
  50. package/src/app/components/AppShell.zoom.test.tsx +149 -0
  51. package/src/app/components/Breadcrumb.stories.tsx +19 -0
  52. package/src/app/components/Breadcrumb.tsx +45 -0
  53. package/src/app/components/MenuBar.stories.tsx +28 -0
  54. package/src/app/components/MenuBar.tsx +44 -0
  55. package/src/app/components/PaneGrid.stories.tsx +84 -0
  56. package/src/app/components/PaneGrid.tsx +128 -0
  57. package/src/app/components/PanelColumn.stories.tsx +25 -0
  58. package/src/app/components/PanelColumn.tsx +93 -0
  59. package/src/app/components/ShellHeader.stories.tsx +50 -0
  60. package/src/app/components/ShellHeader.tsx +137 -0
  61. package/src/app/components/WorkspacePanels.stories.tsx +36 -0
  62. package/src/app/components/WorkspacePanels.tsx +120 -0
  63. package/src/app/components/pages/Workspace.stories.tsx +181 -0
  64. package/src/app/intent-dispatch.test.ts +728 -0
  65. package/src/app/intent-dispatch.ts +687 -0
  66. package/src/app/launch-layout.test.ts +78 -0
  67. package/src/app/launch-layout.ts +57 -0
  68. package/src/app/ready-signal.test.ts +85 -0
  69. package/src/app/ready-signal.ts +52 -0
  70. package/src/app/run.ts +109 -0
  71. package/src/{serve.test.ts → app/serve.test.ts} +24 -12
  72. package/src/app/serve.ts +111 -0
  73. package/src/app/version.ts +5 -0
  74. package/src/appearance/components/theme-context.stories.tsx +46 -0
  75. package/src/appearance/components/theme-context.tsx +37 -0
  76. package/src/appearance/theme-presets.test.ts +51 -0
  77. package/src/appearance/theme-presets.ts +219 -0
  78. package/src/appearance/theme.ts +112 -0
  79. package/src/appearance/visual/contrast.test.ts +35 -0
  80. package/src/appearance/visual/contrast.ts +33 -0
  81. package/src/diff/components/AnnotatableFileView.stories.tsx +42 -0
  82. package/src/diff/components/AnnotatableFileView.test.tsx +157 -0
  83. package/src/diff/components/AnnotatableFileView.tsx +141 -0
  84. package/src/diff/components/BareWorkbenchFileView.stories.tsx +27 -0
  85. package/src/diff/components/BareWorkbenchFileView.tsx +63 -0
  86. package/src/diff/components/DiffContentView.scroll.test.tsx +434 -0
  87. package/src/diff/components/DiffContentView.split.test.tsx +140 -0
  88. package/src/diff/components/DiffContentView.stories.tsx +80 -0
  89. package/src/diff/components/DiffContentView.syntax.test.tsx +82 -0
  90. package/src/diff/components/DiffContentView.tsx +1055 -0
  91. package/src/diff/components/WalkWizard.stories.tsx +100 -0
  92. package/src/diff/components/WalkWizard.tsx +146 -0
  93. package/src/diff/components/diff-content-view-layout.test.ts +198 -0
  94. package/src/diff/components/diff-content-view-layout.ts +84 -0
  95. package/src/diff/diff-fold.test.ts +78 -0
  96. package/src/diff/diff-fold.ts +126 -0
  97. package/src/diff/diff-hunk-curate.test.ts +315 -0
  98. package/src/diff/diff-hunk-curate.ts +152 -0
  99. package/src/diff/diff-intraline.test.ts +239 -0
  100. package/src/diff/diff-intraline.ts +238 -0
  101. package/src/diff/diff-syntax.test.ts +125 -0
  102. package/src/diff/diff-syntax.ts +186 -0
  103. package/src/diff/split-diff.test.ts +110 -0
  104. package/src/diff/split-diff.ts +144 -0
  105. package/src/diff/view-diff.test.ts +325 -0
  106. package/src/diff/view-diff.ts +522 -0
  107. package/src/diff/walk.test.ts +121 -0
  108. package/src/diff/walk.ts +73 -0
  109. package/src/index.ts +31 -2
  110. package/src/integrations/attribution.test.ts +31 -0
  111. package/src/integrations/attribution.ts +42 -0
  112. package/src/integrations/client-extension-registry.test.tsx +61 -0
  113. package/src/integrations/client-extension-registry.ts +159 -0
  114. package/src/integrations/clipboard.test.ts +23 -0
  115. package/src/integrations/clipboard.ts +33 -0
  116. package/src/integrations/components/ExtensionZone.stories.tsx +28 -0
  117. package/src/integrations/components/ExtensionZone.tsx +145 -0
  118. package/src/integrations/components/ShareDialog.agent.test.tsx +50 -0
  119. package/src/integrations/components/ShareDialog.stories.tsx +121 -0
  120. package/src/integrations/components/ShareDialog.tsx +574 -0
  121. package/src/integrations/components/share-dialog-store.ts +59 -0
  122. package/src/integrations/editor.test.ts +149 -0
  123. package/src/integrations/editor.ts +160 -0
  124. package/src/integrations/github-identity.test.ts +58 -0
  125. package/src/integrations/github-identity.ts +41 -0
  126. package/src/integrations/herdr.test.ts +43 -0
  127. package/src/integrations/herdr.ts +38 -0
  128. package/src/integrations/integrations.test.ts +41 -0
  129. package/src/integrations/integrations.ts +32 -0
  130. package/src/integrations/pasted-image.test.ts +67 -0
  131. package/src/integrations/pasted-image.ts +60 -0
  132. package/src/integrations/share.ts +238 -0
  133. package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
  134. package/src/keyboard/components/CompletionOverlay.tsx +75 -0
  135. package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
  136. package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
  137. package/src/keyboard/components/HandleChipsInput.tsx +143 -0
  138. package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
  139. package/src/keyboard/components/KeybindsDialog.tsx +103 -0
  140. package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
  141. package/src/keyboard/components/NavModeHint.tsx +31 -0
  142. package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
  143. package/src/keyboard/components/SlashComposer.tsx +87 -0
  144. package/src/keyboard/key-bindings.test.ts +156 -0
  145. package/src/keyboard/key-bindings.ts +459 -0
  146. package/src/keyboard/keymap.test.ts +611 -0
  147. package/src/keyboard/keymap.ts +395 -0
  148. package/src/keyboard/skills.test.ts +74 -0
  149. package/src/keyboard/skills.ts +75 -0
  150. package/src/keyboard/slash-palette.test.ts +103 -0
  151. package/src/keyboard/slash-palette.ts +129 -0
  152. package/src/keyboard/thread-chords.test.ts +79 -0
  153. package/src/keyboard/thread-chords.ts +163 -0
  154. package/src/keyboard/use-shared-keyboard.ts +55 -0
  155. package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  156. package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
  157. package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
  158. package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
  159. package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
  160. package/src/markdown/components/MarkdownThreadEditor.tsx +160 -0
  161. package/src/markdown/components/ThreadView.selection.test.tsx +951 -0
  162. package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
  163. package/src/markdown/components/ThreadView.stories.tsx +26 -0
  164. package/src/markdown/components/ThreadView.tsx +560 -0
  165. package/src/markdown/mark-runs.ts +121 -0
  166. package/src/markdown/markdown-editor-syntax-style.ts +50 -0
  167. package/src/markdown/markdown-grid.test.ts +101 -0
  168. package/src/markdown/markdown-grid.ts +203 -0
  169. package/src/markdown/markdown-highlight.test.ts +61 -0
  170. package/src/markdown/markdown-highlight.ts +124 -0
  171. package/src/markdown/thread-code-syntax.ts +45 -0
  172. package/src/markdown/use-thread-body-editing.tsx +54 -0
  173. package/src/markdown/view-plan.regression.test.ts +60 -0
  174. package/src/markdown/view-plan.test.ts +545 -0
  175. package/src/markdown/view-plan.ts +1009 -0
  176. package/src/performance/otlp-trace.test.ts +119 -0
  177. package/src/performance/otlp-trace.ts +134 -0
  178. package/src/performance/perf-report.ts +18 -0
  179. package/src/performance/perf-timings.test.ts +18 -0
  180. package/src/performance/perf-timings.ts +39 -0
  181. package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
  182. package/src/prototype/components/PrototypeContentView.tsx +442 -0
  183. package/src/prototype/prototype-browser.test.ts +81 -0
  184. package/src/prototype/prototype-browser.ts +321 -0
  185. package/src/prototype/prototype-pixels.tsx +35 -0
  186. package/src/settings/components/SettingsDialog.stories.tsx +117 -0
  187. package/src/settings/components/SettingsDialog.tsx +183 -0
  188. package/src/settings/components/SettingsRows.stories.tsx +18 -0
  189. package/src/settings/components/SettingsRows.tsx +112 -0
  190. package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
  191. package/src/settings/components/quick-actions-editor.test.tsx +120 -0
  192. package/src/settings/components/quick-actions-editor.tsx +222 -0
  193. package/src/settings/config-identity.test.ts +78 -0
  194. package/src/settings/config.test.ts +676 -0
  195. package/src/settings/config.ts +628 -0
  196. package/src/settings/use-settings-dialog.tsx +351 -0
  197. package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
  198. package/src/stories/stories-app.tsx +196 -0
  199. package/src/stories/stories.test.tsx +74 -0
  200. package/src/stories/story-fixtures.ts +102 -0
  201. package/src/stories/story-tree.test.ts +108 -0
  202. package/src/stories/story.ts +154 -0
  203. package/src/terminal/ansi-screen.test.ts +28 -0
  204. package/src/terminal/ansi-screen.ts +72 -0
  205. package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
  206. package/src/terminal/components/AnsiScreen.tsx +28 -0
  207. package/src/terminal/components/terminal-pane.test.tsx +40 -0
  208. package/src/terminal/components/terminal-pane.ts +196 -0
  209. package/src/terminal/ghostty-terminal.test.ts +85 -0
  210. package/src/terminal/ghostty-terminal.ts +197 -0
  211. package/src/terminal/kitty-image.test.ts +66 -0
  212. package/src/terminal/kitty-image.ts +137 -0
  213. package/src/terminal/pty.test.ts +81 -0
  214. package/src/terminal/pty.ts +246 -0
  215. package/src/testing/test-support.test.ts +264 -0
  216. package/src/testing/test-support.ts +367 -0
  217. package/src/thread/agent-continuation.test.ts +32 -0
  218. package/src/thread/agent-continuation.ts +13 -0
  219. package/src/thread/agent-transcript.test.ts +161 -0
  220. package/src/thread/agent-transcript.ts +336 -0
  221. package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
  222. package/src/thread/components/AgentConfigControls.test.tsx +112 -0
  223. package/src/thread/components/AgentConfigControls.tsx +114 -0
  224. package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
  225. package/src/thread/components/AgentThreadPane.test.tsx +935 -0
  226. package/src/thread/components/AgentThreadPane.tsx +377 -0
  227. package/src/thread/components/ThreadFooter.stories.tsx +30 -0
  228. package/src/thread/components/ThreadFooter.test.tsx +49 -0
  229. package/src/thread/components/ThreadFooter.tsx +77 -0
  230. package/src/thread/components/ThreadTree.stories.tsx +46 -0
  231. package/src/thread/components/ThreadTree.test.tsx +41 -0
  232. package/src/thread/components/ThreadTree.tsx +267 -0
  233. package/src/thread/components/thread-tree.test.ts +137 -0
  234. package/src/thread/components/thread-tree.ts +112 -0
  235. package/src/thread/thread-controller.curate.test.ts +502 -0
  236. package/src/thread/thread-controller.derivation.test.ts +62 -0
  237. package/src/thread/thread-controller.live-diff.test.ts +357 -0
  238. package/src/thread/thread-controller.optimistic.test.ts +69 -0
  239. package/src/thread/thread-controller.share.test.ts +573 -0
  240. package/src/thread/thread-controller.switch-cache.test.ts +174 -0
  241. package/src/thread/thread-controller.tree.test.ts +263 -0
  242. package/src/thread/thread-controller.ts +2026 -0
  243. package/src/thread/tree-view.test.ts +128 -0
  244. package/src/thread/tree-view.ts +146 -0
  245. package/src/thread/use-thread-agent.test.tsx +208 -0
  246. package/src/thread/use-thread-agent.ts +117 -0
  247. package/src/ui/components/CodeBlock.stories.tsx +42 -0
  248. package/src/ui/components/CodeBlock.tsx +80 -0
  249. package/src/ui/components/ConfirmCard.stories.tsx +37 -0
  250. package/src/ui/components/ConfirmCard.tsx +151 -0
  251. package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
  252. package/src/ui/components/ConfirmDialog.tsx +72 -0
  253. package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
  254. package/src/ui/components/OverlayScrollbar.test.tsx +139 -0
  255. package/src/ui/components/OverlayScrollbar.tsx +129 -0
  256. package/src/ui/components/PromptDialog.stories.tsx +36 -0
  257. package/src/ui/components/PromptDialog.test.tsx +65 -0
  258. package/src/ui/components/PromptDialog.tsx +99 -0
  259. package/src/ui/components/RootOverlay.stories.tsx +45 -0
  260. package/src/ui/components/RootOverlay.tsx +49 -0
  261. package/src/ui/components/ScrollArea.stories.tsx +20 -0
  262. package/src/ui/components/ScrollArea.tsx +61 -0
  263. package/src/ui/components/Toast.stories.tsx +19 -0
  264. package/src/ui/components/Toast.tsx +54 -0
  265. package/src/ui/components/Tooltip.stories.tsx +26 -0
  266. package/src/ui/components/Tooltip.tsx +72 -0
  267. package/src/ui/components/menu-control.stories.tsx +35 -0
  268. package/src/ui/components/menu-control.tsx +43 -0
  269. package/src/ui/components/primitives/Button.stories.tsx +32 -0
  270. package/src/ui/components/primitives/Button.tsx +57 -0
  271. package/src/ui/components/primitives/Card.stories.tsx +25 -0
  272. package/src/ui/components/primitives/Card.tsx +62 -0
  273. package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
  274. package/src/ui/components/primitives/Dialog.test.tsx +38 -0
  275. package/src/ui/components/primitives/Dialog.tsx +69 -0
  276. package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
  277. package/src/ui/components/primitives/DialogActions.tsx +39 -0
  278. package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
  279. package/src/ui/components/primitives/IconButton.tsx +58 -0
  280. package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
  281. package/src/ui/components/primitives/StatusBar.tsx +23 -0
  282. package/src/ui/components/primitives/Switch.stories.tsx +17 -0
  283. package/src/ui/components/primitives/Switch.tsx +32 -0
  284. package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
  285. package/src/ui/components/primitives/Tabs.tsx +101 -0
  286. package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
  287. package/src/ui/components/primitives/Toolbar.tsx +14 -0
  288. package/src/ui/components/primitives/Tree.stories.tsx +124 -0
  289. package/src/ui/components/primitives/Tree.tsx +133 -0
  290. package/src/ui/components/primitives/frame.test.ts +12 -0
  291. package/src/ui/components/primitives/frame.ts +17 -0
  292. package/src/ui/components/primitives/icons.ts +61 -0
  293. package/src/ui/components/primitives/tree-model.test.ts +99 -0
  294. package/src/ui/components/primitives/tree-model.ts +137 -0
  295. package/src/ui/components/syntax-highlight.ts +133 -0
  296. package/src/ui/components/truncate-text.ts +7 -0
  297. package/src/ui/components/truncate-title.test.ts +31 -0
  298. package/src/ui/components/truncate-title.ts +15 -0
  299. package/src/ui/use-frame-measure.ts +74 -0
  300. package/src/ui/use-terminal-virtualizer.ts +259 -0
  301. package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
  302. package/src/workbench/components/ChangesColumn.tsx +203 -0
  303. package/src/workbench/components/ChangesFileTree.test.tsx +181 -0
  304. package/src/workbench/components/EditorGrid.split.test.tsx +118 -0
  305. package/src/workbench/components/EditorGrid.stories.tsx +86 -0
  306. package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
  307. package/src/workbench/components/EditorGrid.tsx +544 -0
  308. package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
  309. package/src/workbench/components/FileContentsView.stories.tsx +16 -0
  310. package/src/workbench/components/FileContentsView.test.tsx +29 -0
  311. package/src/workbench/components/FileContentsView.tsx +100 -0
  312. package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
  313. package/src/workbench/components/GridTabContent.stories.tsx +59 -0
  314. package/src/workbench/components/GridTabContent.tsx +193 -0
  315. package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
  316. package/src/workbench/components/ProjectTreeView.test.tsx +236 -0
  317. package/src/workbench/components/ProjectTreeView.tsx +151 -0
  318. package/src/workbench/components/TreePane.stories.tsx +106 -0
  319. package/src/workbench/components/TreePane.tsx +155 -0
  320. package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
  321. package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
  322. package/src/workbench/components/WelcomePlayground.tsx +138 -0
  323. package/src/workbench/components/editor-grid.test.ts +184 -0
  324. package/src/workbench/components/editor-grid.ts +281 -0
  325. package/src/workbench/components/file-tree.test.ts +60 -0
  326. package/src/workbench/components/file-tree.ts +112 -0
  327. package/src/workbench/use-changes-workbench.ts +205 -0
  328. package/src/workbench/use-remember-layout.ts +31 -0
  329. package/src/workbench/use-repo-changes.ts +91 -0
  330. package/src/workbench/workbench-snapshot.test.ts +99 -0
  331. package/src/workbench/workbench-snapshot.ts +59 -0
  332. package/src/App.diff.test.tsx +0 -99
  333. package/src/App.export.test.tsx +0 -88
  334. package/src/App.plan-review.test.tsx +0 -259
  335. package/src/App.readonly.test.tsx +0 -148
  336. package/src/App.rendering.test.tsx +0 -108
  337. package/src/App.submit-confirm.test.tsx +0 -183
  338. package/src/App.test.tsx +0 -218
  339. package/src/App.tsx +0 -1249
  340. package/src/config.test.ts +0 -116
  341. package/src/config.ts +0 -150
  342. package/src/editor.ts +0 -34
  343. package/src/herdr.test.ts +0 -68
  344. package/src/herdr.ts +0 -34
  345. package/src/keymap.test.ts +0 -270
  346. package/src/keymap.ts +0 -173
  347. package/src/run.ts +0 -30
  348. package/src/serve.ts +0 -75
  349. package/src/session-controller.ts +0 -398
  350. package/src/syntax.test.ts +0 -41
  351. package/src/syntax.ts +0 -88
  352. package/src/theme.ts +0 -46
  353. package/src/view-diff.test.ts +0 -55
  354. package/src/view-diff.ts +0 -86
  355. package/src/view.test.ts +0 -157
  356. package/src/view.ts +0 -364
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ import { DARK } from "../../appearance/theme";
3
+ import type { Story, StoryMeta } from "../../stories/story";
4
+ import { PromptDialog } from "./PromptDialog";
5
+
6
+ export const meta: StoryMeta = { title: "Overlays/PromptDialog" };
7
+
8
+ export const RenameAuthor: Story = {
9
+ render: () => (
10
+ <PromptDialog
11
+ isOpen
12
+ title=" Rename author "
13
+ label="Display name for this collaborator:"
14
+ value="Robin"
15
+ placeholder="their name"
16
+ onInput={() => {}}
17
+ />
18
+ ),
19
+ expectedColors: [DARK.elevated, DARK.accent],
20
+ size: { width: 64, height: 16 },
21
+ };
22
+
23
+ export const NameYourself: Story = {
24
+ render: () => (
25
+ <PromptDialog
26
+ isOpen
27
+ title=" Welcome "
28
+ label="Your name (optional) - it attributes the notes you leave:"
29
+ value=""
30
+ placeholder="your name"
31
+ onInput={() => {}}
32
+ />
33
+ ),
34
+ expectedColors: [DARK.elevated],
35
+ size: { width: 64, height: 16 },
36
+ };
@@ -0,0 +1,65 @@
1
+ /** The prompt dialog's footer actions are mouse-clickable: clicking "save" commits and "cancel"
2
+ * dismisses, the same as pressing enter or esc. */
3
+
4
+ import { test, expect } from "bun:test";
5
+ import React from "react";
6
+ import { testRender } from "@opentui/react/test-utils";
7
+ import { PromptDialog } from "./PromptDialog";
8
+ import { locateText, settle, typeText } from "../../testing/test-support";
9
+ import { DARK } from "../../appearance/theme";
10
+
11
+ test("the input is focused on open, so typing lands in the dialog", async () => {
12
+ const inputs: string[] = [];
13
+ const setup = await testRender(
14
+ <PromptDialog
15
+ isOpen
16
+ title=" rename thread "
17
+ label="new title for this thread:"
18
+ value="hi"
19
+ onInput={(text) => inputs.push(text)}
20
+ theme={DARK}
21
+ />,
22
+ { width: 60, height: 12 },
23
+ );
24
+
25
+ await settle(setup);
26
+
27
+ await typeText(setup, "!");
28
+ expect(inputs.at(-1)).toBe("hi!");
29
+
30
+ setup.renderer.destroy();
31
+ });
32
+
33
+ test("clicking save fires onSave and clicking cancel fires onCancel", async () => {
34
+ let saved = 0;
35
+ let cancelled = 0;
36
+ const setup = await testRender(
37
+ <PromptDialog
38
+ isOpen
39
+ title=" rename thread "
40
+ label="new title for this thread:"
41
+ value="a title"
42
+ onInput={() => {}}
43
+ onSave={() => (saved += 1)}
44
+ onCancel={() => (cancelled += 1)}
45
+ theme={DARK}
46
+ />,
47
+ { width: 60, height: 12 },
48
+ );
49
+
50
+ await settle(setup);
51
+
52
+ const save = locateText(setup, "save");
53
+
54
+ await setup.mockMouse.click(save.column, save.row);
55
+ expect(saved).toBe(1);
56
+ expect(cancelled).toBe(0);
57
+
58
+ const cancel = locateText(setup, "cancel");
59
+
60
+ await setup.mockMouse.click(cancel.column, cancel.row);
61
+ expect(cancelled).toBe(1);
62
+ expect(saved).toBe(1);
63
+
64
+ setup.renderer.destroy();
65
+ });
@@ -0,0 +1,99 @@
1
+ /**
2
+ * A centered single-field text prompt on the shared Dialog overlay. The focused
3
+ * input owns typing/cursor natively; the reducer's `prompt` overlay drives ⏎
4
+ * save / esc cancel (like the composer). Used to rename an author and to ask a
5
+ * collaborator for their name.
6
+ */
7
+
8
+ import React, { useEffect, useRef } from "react";
9
+ import { useTerminalDimensions } from "@opentui/react";
10
+ import type { KeyBinding, TextareaRenderable } from "@opentui/core";
11
+ import type { Theme } from "../../appearance/theme";
12
+ import { useComponentTheme } from "../../appearance/components/theme-context";
13
+ import { Dialog } from "./primitives/Dialog";
14
+ import { DialogActions } from "./primitives/DialogActions";
15
+
16
+ // ⏎ submits (suppresses the textarea's default newline); the grammar owns save.
17
+ const PROMPT_KEY_BINDINGS: KeyBinding[] = [{ name: "return", action: "submit" }];
18
+
19
+ export interface PromptDialogProps {
20
+ isOpen: boolean;
21
+ title?: string;
22
+ label?: string;
23
+ value: string;
24
+ placeholder?: string;
25
+ onInput: (text: string) => void;
26
+ onSave?: () => void;
27
+ onCancel?: () => void;
28
+ theme?: Theme;
29
+ }
30
+
31
+ export function PromptDialog({
32
+ isOpen,
33
+ title,
34
+ label,
35
+ value,
36
+ placeholder,
37
+ onInput,
38
+ onSave,
39
+ onCancel,
40
+ theme,
41
+ }: PromptDialogProps): React.ReactNode {
42
+ const tokens = useComponentTheme(theme);
43
+ const { width: terminalWidth } = useTerminalDimensions();
44
+ const inputRef = useRef<TextareaRenderable | null>(null);
45
+
46
+ useEffect(() => {
47
+ if (!inputRef.current) return;
48
+ inputRef.current.focus();
49
+ inputRef.current.cursorOffset = value.length;
50
+ // eslint-disable-next-line react-hooks/exhaustive-deps
51
+ }, []);
52
+ if (!isOpen) return null;
53
+
54
+ return (
55
+ <Dialog
56
+ isOpen
57
+ title={title}
58
+ width={Math.min(54, terminalWidth - 6)}
59
+ height={7}
60
+ background={tokens.elevated}
61
+ onDismiss={onCancel}
62
+ theme={theme}
63
+ >
64
+ <box
65
+ style={{
66
+ flexDirection: "column",
67
+ flexGrow: 1,
68
+ paddingLeft: 1,
69
+ paddingRight: 1,
70
+ paddingTop: 1,
71
+ }}
72
+ >
73
+ {label ? <text fg={tokens.textDim}>{label}</text> : null}
74
+ <textarea
75
+ ref={inputRef}
76
+ focused
77
+ initialValue={value}
78
+ placeholder={placeholder}
79
+ keyBindings={PROMPT_KEY_BINDINGS}
80
+ onContentChange={() => onInput(inputRef.current?.plainText ?? "")}
81
+ style={{
82
+ height: 1,
83
+ backgroundColor: tokens.elevated,
84
+ focusedBackgroundColor: tokens.elevated,
85
+ textColor: tokens.text,
86
+ focusedTextColor: tokens.text,
87
+ }}
88
+ />
89
+ <box style={{ flexGrow: 1 }} />
90
+ <DialogActions
91
+ confirmLabel="save"
92
+ onConfirm={() => onSave?.()}
93
+ onCancel={() => onCancel?.()}
94
+ theme={theme}
95
+ />
96
+ </box>
97
+ </Dialog>
98
+ );
99
+ }
@@ -0,0 +1,45 @@
1
+ import React, { useEffect } from "react";
2
+ import { DARK } from "../../appearance/theme";
3
+ import type { Story, StoryMeta } from "../../stories/story";
4
+ import { RootOverlayProvider, useRootOverlay } from "./RootOverlay";
5
+
6
+ export const meta: StoryMeta = { title: "Layout/RootOverlay" };
7
+
8
+ // The provider renders whatever a descendant pushes after the rest of the tree, so it draws on top.
9
+ function OverlayDemo(): React.ReactNode {
10
+ const { setOverlay } = useRootOverlay();
11
+
12
+ useEffect(() => {
13
+ setOverlay(
14
+ "demo",
15
+ <box
16
+ style={{
17
+ position: "absolute",
18
+ top: 2,
19
+ left: 6,
20
+ borderStyle: "single",
21
+ borderColor: DARK.border,
22
+ backgroundColor: DARK.elevated,
23
+ paddingLeft: 1,
24
+ paddingRight: 1,
25
+ }}
26
+ >
27
+ <text fg={DARK.text}>floating overlay</text>
28
+ </box>,
29
+ );
30
+ }, [setOverlay]);
31
+
32
+ return <text fg={DARK.textDim}>base content under the overlay</text>;
33
+ }
34
+
35
+ export const FloatingNode: Story = {
36
+ render: () => (
37
+ <RootOverlayProvider>
38
+ <box style={{ padding: 1, backgroundColor: DARK.panel }}>
39
+ <OverlayDemo />
40
+ </box>
41
+ </RootOverlayProvider>
42
+ ),
43
+ expectedColors: [DARK.text],
44
+ size: { width: 40, height: 6 },
45
+ };
@@ -0,0 +1,49 @@
1
+ // A root overlay slot: OpenTUI has no z-index, so an overlay drawn inside a pane is sliced by the
2
+ // next pane's border. Anything pushed here renders after every pane (last in draw order), so it sits
3
+ // on top of the whole shell - the same trick the tooltip uses. Callers position their node with
4
+ // absolute screen coordinates and hold a stable key, so several overlays (a menu, a hover preview)
5
+ // coexist instead of clobbering one shared slot.
6
+
7
+ import React, { createContext, useContext, useMemo, useState } from "react";
8
+
9
+ interface RootOverlayApi {
10
+ setOverlay: (key: string, node: React.ReactNode) => void;
11
+ clearOverlay: (key: string) => void;
12
+ }
13
+
14
+ const RootOverlayContext = createContext<RootOverlayApi>({
15
+ setOverlay: () => {},
16
+ clearOverlay: () => {},
17
+ });
18
+
19
+ export function useRootOverlay(): RootOverlayApi {
20
+ return useContext(RootOverlayContext);
21
+ }
22
+
23
+ export function RootOverlayProvider({ children }: { children: React.ReactNode }): React.ReactNode {
24
+ const [overlays, setOverlays] = useState<Map<string, React.ReactNode>>(new Map());
25
+ const api = useMemo<RootOverlayApi>(
26
+ () => ({
27
+ setOverlay: (key, node) => setOverlays((current) => new Map(current).set(key, node)),
28
+ clearOverlay: (key) =>
29
+ setOverlays((current) => {
30
+ if (!current.has(key)) return current;
31
+ const next = new Map(current);
32
+
33
+ next.delete(key);
34
+
35
+ return next;
36
+ }),
37
+ }),
38
+ [],
39
+ );
40
+
41
+ return (
42
+ <RootOverlayContext.Provider value={api}>
43
+ {children}
44
+ {[...overlays].map(([key, node]) => (
45
+ <React.Fragment key={key}>{node}</React.Fragment>
46
+ ))}
47
+ </RootOverlayContext.Provider>
48
+ );
49
+ }
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+ import { DARK } from "../../appearance/theme";
3
+ import { ScrollArea } from "./ScrollArea";
4
+ import type { Story, StoryMeta } from "../../stories/story";
5
+
6
+ export const meta: StoryMeta = { title: "Chrome/ScrollArea" };
7
+
8
+ // Taller-than-viewport content; the overlay bar stays hidden until the surface scrolls.
9
+ export const AtRest: Story = {
10
+ render: () => (
11
+ <box style={{ width: 24, height: 8, backgroundColor: DARK.background }}>
12
+ <ScrollArea>
13
+ {Array.from({ length: 24 }, (_unused, index) => (
14
+ <text key={index} fg={DARK.textMuted}>{`row ${index + 1}`}</text>
15
+ ))}
16
+ </ScrollArea>
17
+ </box>
18
+ ),
19
+ size: { width: 24, height: 8 },
20
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * A scrollbox whose bar follows the global rule: no always-on scrollbar. The built-in bar is
3
+ * hidden and an OverlayScrollbar rides the rightmost column, appearing only while scrolling.
4
+ * Pass scrollRef to keep scrollTo / scrollChildIntoView on the underlying ScrollBox.
5
+ */
6
+
7
+ import React, { useLayoutEffect, useMemo, useRef, type ReactNode, type RefObject } from "react";
8
+ import { MacOSScrollAccel, type ScrollBoxRenderable } from "@opentui/core";
9
+ import type { Theme } from "../../appearance/theme";
10
+ import { OverlayScrollbar } from "./OverlayScrollbar";
11
+
12
+ export interface ScrollAreaProps {
13
+ children: ReactNode;
14
+ focused?: boolean;
15
+ theme?: Theme;
16
+ scrollRef?: RefObject<ScrollBoxRenderable | null>;
17
+ id?: string;
18
+ revealId?: string;
19
+ gestureWheel?: boolean;
20
+ }
21
+
22
+ export function ScrollArea({
23
+ children,
24
+ focused = false,
25
+ theme,
26
+ scrollRef,
27
+ id,
28
+ revealId,
29
+ gestureWheel = false,
30
+ }: ScrollAreaProps): React.ReactNode {
31
+ const innerRef = useRef<ScrollBoxRenderable | null>(null);
32
+ const wheelAcceleration = useMemo(
33
+ () => (gestureWheel ? new MacOSScrollAccel({ A: 0.4, tau: 4, maxMultiplier: 3 }) : undefined),
34
+ [gestureWheel],
35
+ );
36
+ const attach = (node: ScrollBoxRenderable | null): void => {
37
+ innerRef.current = node;
38
+ if (scrollRef) scrollRef.current = node;
39
+ };
40
+
41
+ useLayoutEffect(() => {
42
+ if (revealId === undefined) return;
43
+ innerRef.current?.scrollChildIntoView(`tree-row-${revealId}`);
44
+ }, [revealId]);
45
+
46
+ return (
47
+ <box style={{ flexGrow: 1, flexDirection: "row" }}>
48
+ <scrollbox
49
+ id={id}
50
+ ref={attach}
51
+ style={{ flexGrow: 1 }}
52
+ focused={focused}
53
+ scrollAcceleration={wheelAcceleration}
54
+ verticalScrollbarOptions={{ visible: false }}
55
+ >
56
+ {children}
57
+ </scrollbox>
58
+ <OverlayScrollbar scrollbox={innerRef} theme={theme} />
59
+ </box>
60
+ );
61
+ }
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ import { DARK } from "../../appearance/theme";
3
+ import type { Story, StoryMeta } from "../../stories/story";
4
+ import { Toast } from "./Toast";
5
+
6
+ export const meta: StoryMeta = { title: "Overlays/Toast" };
7
+
8
+ export const ShareLink: Story = {
9
+ render: () => <Toast title="share link copied" body="ssh review-4f2a@cueloop.dev" />,
10
+ expectedColors: [DARK.accent],
11
+ size: { width: 60, height: 12 },
12
+ };
13
+
14
+ export const LongMessage: Story = {
15
+ render: () => (
16
+ <Toast body="could not stop sharing: gateway revoke failed - the share may already be gone, try again in a moment" />
17
+ ),
18
+ size: { width: 90, height: 14 },
19
+ };
@@ -0,0 +1,54 @@
1
+ import React from "react";
2
+ import { useTerminalDimensions } from "@opentui/react";
3
+ import type { Theme } from "../../appearance/theme";
4
+ import { useComponentTheme } from "../../appearance/components/theme-context";
5
+ import { Dialog } from "./primitives/Dialog";
6
+
7
+ export interface ToastProps {
8
+ title?: string;
9
+ body: string;
10
+ onDismiss?: () => void;
11
+ theme?: Theme;
12
+ }
13
+
14
+ /** A toast never spans the screen: a long message wraps inside this, like the settings dialog. */
15
+ const TOAST_MAX_WIDTH = 64;
16
+
17
+ export function Toast({ title, body, onDismiss, theme }: ToastProps): React.ReactNode {
18
+ const tokens = useComponentTheme(theme);
19
+ const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
20
+ const width = Math.min(
21
+ terminalWidth - 6,
22
+ Math.max(28, Math.min(TOAST_MAX_WIDTH, body.length + 4)),
23
+ );
24
+ // the message wraps at the content width, so grow the box to fit the wrapped lines
25
+ const contentWidth = Math.max(1, width - 4);
26
+ const lines = body
27
+ .split("\n")
28
+ .reduce((total, line) => total + Math.max(1, Math.ceil(line.length / contentWidth)), 0);
29
+ const height = Math.min(terminalHeight - 4, Math.max(6, lines + 4));
30
+
31
+ return (
32
+ <Dialog
33
+ isOpen
34
+ title={title ? ` ${title} ` : undefined}
35
+ width={width}
36
+ height={height}
37
+ background={tokens.elevated}
38
+ onDismiss={onDismiss}
39
+ theme={theme}
40
+ >
41
+ <box
42
+ style={{
43
+ flexDirection: "column",
44
+ flexGrow: 1,
45
+ paddingLeft: 1,
46
+ paddingRight: 1,
47
+ paddingTop: 1,
48
+ }}
49
+ >
50
+ <text fg={tokens.text}>{body}</text>
51
+ </box>
52
+ </Dialog>
53
+ );
54
+ }
@@ -0,0 +1,26 @@
1
+ import React from "react";
2
+ import { DARK } from "../../appearance/theme";
3
+ import type { Story, StoryMeta } from "../../stories/story";
4
+ import { TooltipProvider } from "./Tooltip";
5
+ import { IconButton } from "./primitives/IconButton";
6
+ import { NERD } from "./primitives/icons";
7
+
8
+ export const meta: StoryMeta = { title: "Layout/Tooltip" };
9
+
10
+ // The tooltip surfaces only on hover, so a static story shows the provider wrapping a labelled control.
11
+ export const HoverControl: Story = {
12
+ render: () => (
13
+ <TooltipProvider theme={DARK}>
14
+ <box style={{ flexDirection: "row", padding: 1, backgroundColor: DARK.panel }}>
15
+ <IconButton
16
+ glyph={NERD.sidebarRight}
17
+ tip="Toggle Right Sidebar"
18
+ onPress={() => {}}
19
+ theme={DARK}
20
+ />
21
+ </box>
22
+ </TooltipProvider>
23
+ ),
24
+ expectedColors: [DARK.textMuted],
25
+ size: { width: 40, height: 4 },
26
+ };
@@ -0,0 +1,72 @@
1
+ // Hover tooltips for the header controls. A tiny header cell clips its own children, so the label is
2
+ // surfaced at the screen root instead: controls report the hovered label and cursor cell, and this layer
3
+ // paints it one row below, nudged left when it would run past the right edge.
4
+
5
+ import React, { createContext, useContext, useMemo, useState } from "react";
6
+ import { useTerminalDimensions } from "@opentui/react";
7
+ import { DARK, type Theme } from "../../appearance/theme";
8
+
9
+ interface TooltipTarget {
10
+ label: string;
11
+ x: number;
12
+ y: number;
13
+ }
14
+
15
+ interface TooltipApi {
16
+ showTooltip: (label: string, x: number, y: number) => void;
17
+ hideTooltip: () => void;
18
+ }
19
+
20
+ const TooltipContext = createContext<TooltipApi>({
21
+ showTooltip: () => {},
22
+ hideTooltip: () => {},
23
+ });
24
+
25
+ /** Report or clear the hovered control's tooltip; a no-op outside a TooltipProvider. */
26
+ export function useTooltip(): TooltipApi {
27
+ return useContext(TooltipContext);
28
+ }
29
+
30
+ /** Wrap a shell so its header controls can surface hover tooltips at the screen root. */
31
+ export function TooltipProvider({
32
+ theme,
33
+ children,
34
+ }: {
35
+ theme?: Theme;
36
+ children: React.ReactNode;
37
+ }): React.ReactNode {
38
+ const tokens = theme ?? DARK;
39
+ const { width, height } = useTerminalDimensions();
40
+ const [target, setTarget] = useState<TooltipTarget | null>(null);
41
+ const api = useMemo<TooltipApi>(
42
+ () => ({
43
+ showTooltip: (label, x, y) => setTarget({ label, x, y }),
44
+ hideTooltip: () => setTarget(null),
45
+ }),
46
+ [],
47
+ );
48
+
49
+ const boxWidth = target ? target.label.length + 2 : 0;
50
+ const left = target ? Math.max(0, Math.min(target.x, width - boxWidth)) : 0;
51
+ const top = target ? (target.y + 1 <= height - 1 ? target.y + 1 : Math.max(0, target.y - 1)) : 0;
52
+
53
+ return (
54
+ <TooltipContext.Provider value={api}>
55
+ {children}
56
+ {target ? (
57
+ <box
58
+ style={{
59
+ position: "absolute",
60
+ left,
61
+ top,
62
+ paddingLeft: 1,
63
+ paddingRight: 1,
64
+ backgroundColor: tokens.elevated,
65
+ }}
66
+ >
67
+ <text fg={tokens.text}>{target.label}</text>
68
+ </box>
69
+ ) : null}
70
+ </TooltipContext.Provider>
71
+ );
72
+ }
@@ -0,0 +1,35 @@
1
+ import React from "react";
2
+ import { DARK } from "../../appearance/theme";
3
+ import type { Story, StoryMeta } from "../../stories/story";
4
+ import { MenuControlProvider, useMenuControl } from "./menu-control";
5
+
6
+ export const meta: StoryMeta = { title: "Layout/MenuControl" };
7
+
8
+ // One open menu at a time: with "alpha" open, only its row reads as open.
9
+ function MenuControlDemo(): React.ReactNode {
10
+ const { openMenuId } = useMenuControl();
11
+ const row = (id: string): React.ReactNode => (
12
+ <text fg={openMenuId === id ? DARK.accent : DARK.textDim}>
13
+ {`${id}: ${openMenuId === id ? "open" : "closed"}`}
14
+ </text>
15
+ );
16
+
17
+ return (
18
+ <box style={{ flexDirection: "column" }}>
19
+ {row("alpha")}
20
+ {row("beta")}
21
+ </box>
22
+ );
23
+ }
24
+
25
+ export const OneOpenAtATime: Story = {
26
+ render: () => (
27
+ <MenuControlProvider value={{ openMenuId: "alpha", toggleMenu: () => {}, closeMenu: () => {} }}>
28
+ <box style={{ padding: 1, backgroundColor: DARK.panel }}>
29
+ <MenuControlDemo />
30
+ </box>
31
+ </MenuControlProvider>
32
+ ),
33
+ expectedColors: [DARK.accent],
34
+ size: { width: 30, height: 5 },
35
+ };
@@ -0,0 +1,43 @@
1
+ // One open menu at a time across a shell: the thread row menu and the editor split menu share this
2
+ // single open id, so opening either closes the other. Owned by App and by the no-thread shell, one each.
3
+
4
+ import React, { createContext, useContext, useMemo, useState } from "react";
5
+
6
+ export interface MenuControlApi {
7
+ openMenuId: string | null;
8
+ toggleMenu: (id: string) => void;
9
+ closeMenu: () => void;
10
+ }
11
+
12
+ const MenuControlContext = createContext<MenuControlApi>({
13
+ openMenuId: null,
14
+ toggleMenu: () => {},
15
+ closeMenu: () => {},
16
+ });
17
+
18
+ export function useMenuControl(): MenuControlApi {
19
+ return useContext(MenuControlContext);
20
+ }
21
+
22
+ export function useMenuControlState(): MenuControlApi {
23
+ const [openMenuId, setOpenMenuId] = useState<string | null>(null);
24
+
25
+ return useMemo(
26
+ () => ({
27
+ openMenuId,
28
+ toggleMenu: (id: string) => setOpenMenuId((current) => (current === id ? null : id)),
29
+ closeMenu: () => setOpenMenuId(null),
30
+ }),
31
+ [openMenuId],
32
+ );
33
+ }
34
+
35
+ export function MenuControlProvider({
36
+ value,
37
+ children,
38
+ }: {
39
+ value: MenuControlApi;
40
+ children: React.ReactNode;
41
+ }): React.ReactNode {
42
+ return <MenuControlContext.Provider value={value}>{children}</MenuControlContext.Provider>;
43
+ }
@@ -0,0 +1,32 @@
1
+ import React from "react";
2
+ import { DARK } from "../../../appearance/theme";
3
+ import type { Story, StoryMeta } from "../../../stories/story";
4
+ import { Button } from "./Button";
5
+ import { Toolbar } from "./Toolbar";
6
+
7
+ export const meta: StoryMeta = { title: "Primitives/Button" };
8
+
9
+ export const Variants: Story = {
10
+ render: () => (
11
+ <Toolbar>
12
+ <Button variant="solid" marginRight={2} onPress={() => {}}>
13
+ {" Save "}
14
+ </Button>
15
+ <Button marginRight={2} onPress={() => {}}>
16
+ {" Cancel esc "}
17
+ </Button>
18
+ <Button variant="accent-text" onPress={() => {}}>
19
+ {"Submit review"}
20
+ </Button>
21
+ </Toolbar>
22
+ ),
23
+ expectedColors: [DARK.accent, DARK.accentInk, DARK.textDim],
24
+ };
25
+
26
+ export const Disabled: Story = {
27
+ render: () => (
28
+ <Button isDisabled onPress={() => {}}>
29
+ {" Edit "}
30
+ </Button>
31
+ ),
32
+ };