@tangle-network/ui 1.0.0

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 (220) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +33 -0
  4. package/dist/active-sessions-store-CeOmXgv5.d.ts +85 -0
  5. package/dist/artifact-pane-DvJyPWV4.d.ts +24 -0
  6. package/dist/auth.d.ts +74 -0
  7. package/dist/auth.js +15 -0
  8. package/dist/button-CMQuQEW_.d.ts +17 -0
  9. package/dist/chat.d.ts +232 -0
  10. package/dist/chat.js +30 -0
  11. package/dist/chunk-2NFQRQOD.js +1009 -0
  12. package/dist/chunk-2VH6PUXD.js +186 -0
  13. package/dist/chunk-34A66VBG.js +214 -0
  14. package/dist/chunk-3OI2QKFD.js +0 -0
  15. package/dist/chunk-4CLN43XT.js +45 -0
  16. package/dist/chunk-54SQQMMM.js +156 -0
  17. package/dist/chunk-5Z5ZYMOJ.js +0 -0
  18. package/dist/chunk-66BNMOVT.js +167 -0
  19. package/dist/chunk-6BGQA4BQ.js +0 -0
  20. package/dist/chunk-7UO2ZMRQ.js +133 -0
  21. package/dist/chunk-BX6AQMUS.js +183 -0
  22. package/dist/chunk-CD53GZOM.js +59 -0
  23. package/dist/chunk-CSAIKY36.js +54 -0
  24. package/dist/chunk-EEE55AVS.js +1201 -0
  25. package/dist/chunk-GYPQXTJU.js +230 -0
  26. package/dist/chunk-HFL6R6IF.js +37 -0
  27. package/dist/chunk-HJKCSXCH.js +737 -0
  28. package/dist/chunk-LISXUB4D.js +1222 -0
  29. package/dist/chunk-LQS34IGP.js +0 -0
  30. package/dist/chunk-MKTSMWVD.js +109 -0
  31. package/dist/chunk-NKDZ7GZE.js +192 -0
  32. package/dist/chunk-OEX7NZE3.js +321 -0
  33. package/dist/chunk-Q56BYXQF.js +61 -0
  34. package/dist/chunk-Q7EIIWTC.js +0 -0
  35. package/dist/chunk-REJESC5U.js +117 -0
  36. package/dist/chunk-RQGKSCEZ.js +0 -0
  37. package/dist/chunk-RQHJBTEU.js +10 -0
  38. package/dist/chunk-TMFOPHHN.js +299 -0
  39. package/dist/chunk-XGKULLYE.js +40 -0
  40. package/dist/chunk-XIHMJ7ZQ.js +614 -0
  41. package/dist/chunk-YJ2G3XO5.js +1048 -0
  42. package/dist/chunk-YNN4O57I.js +754 -0
  43. package/dist/code-block-DjXf8eOG.d.ts +19 -0
  44. package/dist/document-editor-pane-A5LT5H4N.js +12 -0
  45. package/dist/document-editor-pane-DyDEX_Zm.d.ts +124 -0
  46. package/dist/editor.d.ts +120 -0
  47. package/dist/editor.js +34 -0
  48. package/dist/files.d.ts +175 -0
  49. package/dist/files.js +20 -0
  50. package/dist/hooks.d.ts +56 -0
  51. package/dist/hooks.js +41 -0
  52. package/dist/index.d.ts +43 -0
  53. package/dist/index.js +446 -0
  54. package/dist/markdown.d.ts +15 -0
  55. package/dist/markdown.js +14 -0
  56. package/dist/message-BHWbxBtT.d.ts +15 -0
  57. package/dist/openui.d.ts +115 -0
  58. package/dist/openui.js +12 -0
  59. package/dist/parts-dj7AcUg0.d.ts +36 -0
  60. package/dist/primitives.d.ts +332 -0
  61. package/dist/primitives.js +191 -0
  62. package/dist/run-PfLmDAox.d.ts +41 -0
  63. package/dist/run.d.ts +69 -0
  64. package/dist/run.js +36 -0
  65. package/dist/sdk-hooks.d.ts +285 -0
  66. package/dist/sdk-hooks.js +31 -0
  67. package/dist/stores.d.ts +17 -0
  68. package/dist/stores.js +76 -0
  69. package/dist/tool-call-feed-Bs3MyQMT.d.ts +68 -0
  70. package/dist/tool-display-z4JcDmMQ.d.ts +32 -0
  71. package/dist/tool-previews.d.ts +48 -0
  72. package/dist/tool-previews.js +21 -0
  73. package/dist/types.d.ts +19 -0
  74. package/dist/types.js +1 -0
  75. package/dist/utils.d.ts +45 -0
  76. package/dist/utils.js +32 -0
  77. package/package.json +193 -0
  78. package/src/auth/auth.tsx +228 -0
  79. package/src/auth/index.ts +13 -0
  80. package/src/auth/login-layout.tsx +46 -0
  81. package/src/chat/agent-timeline.stories.tsx +429 -0
  82. package/src/chat/agent-timeline.tsx +360 -0
  83. package/src/chat/chat-container.tsx +486 -0
  84. package/src/chat/chat-input.stories.tsx +142 -0
  85. package/src/chat/chat-input.tsx +389 -0
  86. package/src/chat/chat-message.stories.tsx +237 -0
  87. package/src/chat/chat-message.tsx +129 -0
  88. package/src/chat/index.ts +18 -0
  89. package/src/chat/message-list.stories.tsx +336 -0
  90. package/src/chat/message-list.tsx +79 -0
  91. package/src/chat/thinking-indicator.stories.tsx +56 -0
  92. package/src/chat/thinking-indicator.tsx +30 -0
  93. package/src/chat/user-message.stories.tsx +92 -0
  94. package/src/chat/user-message.tsx +43 -0
  95. package/src/editor/document-editor-pane.tsx +351 -0
  96. package/src/editor/editor-provider.tsx +428 -0
  97. package/src/editor/editor-toolbar.tsx +130 -0
  98. package/src/editor/index.ts +31 -0
  99. package/src/editor/markdown-conversion.ts +21 -0
  100. package/src/editor/markdown-document-editor.tsx +137 -0
  101. package/src/editor/tiptap-editor.tsx +331 -0
  102. package/src/editor/use-editor.ts +221 -0
  103. package/src/files/file-artifact-pane.tsx +183 -0
  104. package/src/files/file-preview.tsx +342 -0
  105. package/src/files/file-tabs.tsx +71 -0
  106. package/src/files/file-tree.tsx +258 -0
  107. package/src/files/index.ts +17 -0
  108. package/src/files/rich-file-tree.stories.tsx +104 -0
  109. package/src/files/rich-file-tree.test.tsx +42 -0
  110. package/src/files/rich-file-tree.tsx +232 -0
  111. package/src/hooks/index.ts +10 -0
  112. package/src/hooks/use-auth.ts +153 -0
  113. package/src/hooks/use-auto-scroll.ts +59 -0
  114. package/src/hooks/use-dropdown-menu.ts +40 -0
  115. package/src/hooks/use-live-time.test.tsx +40 -0
  116. package/src/hooks/use-live-time.ts +27 -0
  117. package/src/hooks/use-realtime-session.ts +319 -0
  118. package/src/hooks/use-run-collapse-state.ts +25 -0
  119. package/src/hooks/use-run-groups.ts +111 -0
  120. package/src/hooks/use-sdk-session.ts +575 -0
  121. package/src/hooks/use-sse-stream.ts +475 -0
  122. package/src/hooks/use-tool-call-stream.ts +96 -0
  123. package/src/index.ts +14 -0
  124. package/src/lib/utils.ts +6 -0
  125. package/src/markdown/code-block.tsx +198 -0
  126. package/src/markdown/index.ts +2 -0
  127. package/src/markdown/markdown.stories.tsx +190 -0
  128. package/src/markdown/markdown.tsx +62 -0
  129. package/src/openui/index.ts +20 -0
  130. package/src/openui/openui-artifact-renderer.tsx +542 -0
  131. package/src/primitives/artifact-pane.tsx +91 -0
  132. package/src/primitives/avatar.stories.tsx +95 -0
  133. package/src/primitives/avatar.tsx +47 -0
  134. package/src/primitives/badge.stories.tsx +57 -0
  135. package/src/primitives/badge.tsx +97 -0
  136. package/src/primitives/button.stories.tsx +48 -0
  137. package/src/primitives/button.tsx +115 -0
  138. package/src/primitives/card.stories.tsx +53 -0
  139. package/src/primitives/card.tsx +98 -0
  140. package/src/primitives/code-block.stories.tsx +115 -0
  141. package/src/primitives/code-block.tsx +22 -0
  142. package/src/primitives/design-tokens.stories.tsx +162 -0
  143. package/src/primitives/dialog.stories.tsx +176 -0
  144. package/src/primitives/dialog.tsx +137 -0
  145. package/src/primitives/drop-zone.stories.tsx +123 -0
  146. package/src/primitives/drop-zone.tsx +131 -0
  147. package/src/primitives/dropdown-menu.stories.tsx +122 -0
  148. package/src/primitives/dropdown-menu.tsx +214 -0
  149. package/src/primitives/empty-state.stories.tsx +81 -0
  150. package/src/primitives/empty-state.tsx +40 -0
  151. package/src/primitives/index.ts +118 -0
  152. package/src/primitives/input.stories.tsx +113 -0
  153. package/src/primitives/input.tsx +136 -0
  154. package/src/primitives/label.stories.tsx +84 -0
  155. package/src/primitives/label.tsx +24 -0
  156. package/src/primitives/progress.stories.tsx +93 -0
  157. package/src/primitives/progress.tsx +50 -0
  158. package/src/primitives/segmented-control.test.tsx +328 -0
  159. package/src/primitives/segmented-control.tsx +154 -0
  160. package/src/primitives/select.stories.tsx +164 -0
  161. package/src/primitives/select.tsx +158 -0
  162. package/src/primitives/sidebar-drop-zone.stories.tsx +100 -0
  163. package/src/primitives/sidebar-drop-zone.tsx +149 -0
  164. package/src/primitives/skeleton.stories.tsx +79 -0
  165. package/src/primitives/skeleton.tsx +55 -0
  166. package/src/primitives/stat-card.stories.tsx +137 -0
  167. package/src/primitives/stat-card.tsx +97 -0
  168. package/src/primitives/switch.stories.tsx +85 -0
  169. package/src/primitives/switch.tsx +28 -0
  170. package/src/primitives/table.stories.tsx +170 -0
  171. package/src/primitives/table.tsx +116 -0
  172. package/src/primitives/tabs.stories.tsx +180 -0
  173. package/src/primitives/tabs.tsx +71 -0
  174. package/src/primitives/terminal-display.stories.tsx +191 -0
  175. package/src/primitives/terminal-display.tsx +189 -0
  176. package/src/primitives/theme-toggle.stories.tsx +32 -0
  177. package/src/primitives/theme-toggle.tsx +96 -0
  178. package/src/primitives/toast.stories.tsx +155 -0
  179. package/src/primitives/toast.tsx +190 -0
  180. package/src/primitives/upload-progress.stories.tsx +120 -0
  181. package/src/primitives/upload-progress.tsx +110 -0
  182. package/src/run/expanded-tool-detail.stories.tsx +182 -0
  183. package/src/run/expanded-tool-detail.tsx +186 -0
  184. package/src/run/index.ts +13 -0
  185. package/src/run/inline-thinking-item.stories.tsx +136 -0
  186. package/src/run/inline-thinking-item.tsx +120 -0
  187. package/src/run/inline-tool-item.stories.tsx +222 -0
  188. package/src/run/inline-tool-item.tsx +190 -0
  189. package/src/run/run-group.stories.tsx +322 -0
  190. package/src/run/run-group.tsx +569 -0
  191. package/src/run/run-item-primitives.tsx +17 -0
  192. package/src/run/tool-call-feed.stories.tsx +294 -0
  193. package/src/run/tool-call-feed.tsx +192 -0
  194. package/src/run/tool-call-step.stories.tsx +198 -0
  195. package/src/run/tool-call-step.tsx +240 -0
  196. package/src/sdk-hooks.ts +38 -0
  197. package/src/stores/active-sessions-store.ts +455 -0
  198. package/src/stores/chat-store.ts +43 -0
  199. package/src/stores/index.ts +2 -0
  200. package/src/tool-previews/command-preview.tsx +116 -0
  201. package/src/tool-previews/diff-preview.tsx +85 -0
  202. package/src/tool-previews/glob-results-preview.tsx +98 -0
  203. package/src/tool-previews/grep-results-preview.tsx +157 -0
  204. package/src/tool-previews/index.ts +22 -0
  205. package/src/tool-previews/preview-primitives.tsx +84 -0
  206. package/src/tool-previews/question-preview.tsx +101 -0
  207. package/src/tool-previews/web-search-preview.tsx +117 -0
  208. package/src/tool-previews/write-file-preview.tsx +80 -0
  209. package/src/types/branding.ts +11 -0
  210. package/src/types/index.ts +5 -0
  211. package/src/types/message.ts +13 -0
  212. package/src/types/parts.ts +51 -0
  213. package/src/types/run.ts +56 -0
  214. package/src/types/tool-display.ts +41 -0
  215. package/src/utils/copy-text.ts +30 -0
  216. package/src/utils/format.test.ts +43 -0
  217. package/src/utils/format.ts +56 -0
  218. package/src/utils/index.ts +10 -0
  219. package/src/utils/time-ago.ts +9 -0
  220. package/src/utils/tool-display.ts +238 -0
@@ -0,0 +1,71 @@
1
+ import * as TabsPrimitive from "@radix-ui/react-tabs";
2
+ import * as React from "react";
3
+ import { cn } from "../lib/utils";
4
+
5
+ const Tabs = TabsPrimitive.Root;
6
+
7
+ const TabsList = React.forwardRef<
8
+ React.ElementRef<typeof TabsPrimitive.List>,
9
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {
10
+ variant?: "default" | "pills" | "underline";
11
+ }
12
+ >(({ className, variant = "default", ...props }, ref) => {
13
+ const variants = {
14
+ default:
15
+ "inline-flex h-10 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
16
+ pills: "inline-flex items-center gap-2",
17
+ underline: "inline-flex items-center gap-4 border-b border-border",
18
+ };
19
+
20
+ return (
21
+ <TabsPrimitive.List
22
+ ref={ref}
23
+ className={cn(variants[variant], className)}
24
+ {...props}
25
+ />
26
+ );
27
+ });
28
+ TabsList.displayName = TabsPrimitive.List.displayName;
29
+
30
+ const TabsTrigger = React.forwardRef<
31
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
32
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> & {
33
+ variant?: "default" | "pills" | "underline";
34
+ }
35
+ >(({ className, variant = "default", ...props }, ref) => {
36
+ const variants = {
37
+ default:
38
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
39
+ pills:
40
+ "inline-flex items-center justify-center whitespace-nowrap rounded-full px-4 py-2 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-primary data-[state=active]:text-primary-foreground data-[state=inactive]:text-muted-foreground data-[state=inactive]:hover:bg-muted",
41
+ underline:
42
+ "inline-flex items-center justify-center whitespace-nowrap pb-3 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:text-foreground data-[state=inactive]:text-muted-foreground",
43
+ };
44
+
45
+ return (
46
+ <TabsPrimitive.Trigger
47
+ ref={ref}
48
+ className={cn(variants[variant], className)}
49
+ {...props}
50
+ />
51
+ );
52
+ });
53
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
54
+
55
+ const TabsContent = React.forwardRef<
56
+ React.ElementRef<typeof TabsPrimitive.Content>,
57
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
58
+ >(({ className, ...props }, ref) => (
59
+ <TabsPrimitive.Content
60
+ ref={ref}
61
+ className={cn(
62
+ "mt-4 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
63
+ "data-[state=active]:fade-in-0 data-[state=active]:slide-in-from-bottom-2 data-[state=active]:animate-in",
64
+ className,
65
+ )}
66
+ {...props}
67
+ />
68
+ ));
69
+ TabsContent.displayName = TabsPrimitive.Content.displayName;
70
+
71
+ export { Tabs, TabsList, TabsTrigger, TabsContent };
@@ -0,0 +1,191 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { useState } from 'react'
3
+ import {
4
+ TerminalCursor,
5
+ TerminalDisplay,
6
+ TerminalInput,
7
+ TerminalLine,
8
+ } from './terminal-display'
9
+
10
+ const meta: Meta<typeof TerminalDisplay> = {
11
+ title: 'Primitives/TerminalDisplay',
12
+ component: TerminalDisplay,
13
+ parameters: { layout: 'centered', backgrounds: { default: 'dark' } },
14
+ }
15
+
16
+ export default meta
17
+ type Story = StoryObj<typeof TerminalDisplay>
18
+
19
+ export const Default: Story = {
20
+ name: 'Default',
21
+ render: () => (
22
+ <TerminalDisplay title="bash — sess_01j9x8k2m" className="w-[640px]">
23
+ <TerminalLine type="command">npm install</TerminalLine>
24
+ <TerminalLine type="output">
25
+ {'added 312 packages, and audited 313 packages in 4s'}
26
+ </TerminalLine>
27
+ <TerminalLine type="output">{'found 0 vulnerabilities'}</TerminalLine>
28
+ <TerminalLine type="command">npm run build</TerminalLine>
29
+ <TerminalLine type="output">
30
+ {'\n> sandbox-app@1.0.0 build\n> tsc && vite build'}
31
+ </TerminalLine>
32
+ <TerminalLine type="success">
33
+ {'✓ built in 2.14s'}
34
+ </TerminalLine>
35
+ <TerminalLine type="command">
36
+ node dist/index.js<TerminalCursor />
37
+ </TerminalLine>
38
+ </TerminalDisplay>
39
+ ),
40
+ }
41
+
42
+ export const SandboxVariant: Story = {
43
+ name: 'Sandbox Variant',
44
+ render: () => (
45
+ <TerminalDisplay
46
+ variant="sandbox"
47
+ title="agent — sess_01j9x8k2m"
48
+ className="w-[640px]"
49
+ >
50
+ <TerminalLine type="info" timestamp="14:02:11">
51
+ Session initialized · node:20-alpine · us-east-1
52
+ </TerminalLine>
53
+ <TerminalLine type="command">
54
+ git clone https://github.com/acme/api-service.git
55
+ </TerminalLine>
56
+ <TerminalLine type="output">
57
+ {'Cloning into \'api-service\'...\nremote: Enumerating objects: 1847, done.'}
58
+ </TerminalLine>
59
+ <TerminalLine type="output">
60
+ {'Receiving objects: 100% (1847/1847), 2.31 MiB | 14.2 MiB/s, done.'}
61
+ </TerminalLine>
62
+ <TerminalLine type="command">cd api-service && npm ci</TerminalLine>
63
+ <TerminalLine type="output">
64
+ {'added 421 packages in 6s'}
65
+ </TerminalLine>
66
+ <TerminalLine type="command">npm test -- --reporter=dot</TerminalLine>
67
+ <TerminalLine type="output">
68
+ {'............................................'}
69
+ </TerminalLine>
70
+ <TerminalLine type="success">
71
+ {'✓ 44 tests passed (2.8s)'}
72
+ </TerminalLine>
73
+ <TerminalLine type="thinking">
74
+ Analyzing test coverage...
75
+ </TerminalLine>
76
+ </TerminalDisplay>
77
+ ),
78
+ }
79
+
80
+ export const WithErrors: Story = {
81
+ name: 'With Errors',
82
+ render: () => (
83
+ <TerminalDisplay title="bash — sess_01j9x7r9" className="w-[640px]">
84
+ <TerminalLine type="command">python main.py</TerminalLine>
85
+ <TerminalLine type="output">Loading model weights...</TerminalLine>
86
+ <TerminalLine type="error">
87
+ {'Traceback (most recent call last):\n File "main.py", line 12, in <module>\n model = load_checkpoint(args.ckpt)'}
88
+ </TerminalLine>
89
+ <TerminalLine type="error">
90
+ {'FileNotFoundError: [Errno 2] No such file or directory: \'model.pt\''}
91
+ </TerminalLine>
92
+ <TerminalLine type="warning">
93
+ Checkpoint not found. Run download_weights.sh first.
94
+ </TerminalLine>
95
+ <TerminalLine type="command">
96
+ ./download_weights.sh<TerminalCursor />
97
+ </TerminalLine>
98
+ </TerminalDisplay>
99
+ ),
100
+ }
101
+
102
+ export const AgentSession: Story = {
103
+ name: 'Agent Session',
104
+ render: () => (
105
+ <TerminalDisplay
106
+ variant="sandbox"
107
+ title="Claude — task execution"
108
+ className="w-[640px]"
109
+ maxHeight="320px"
110
+ >
111
+ <TerminalLine type="info" timestamp="14:49:58">
112
+ Task: Write and run unit tests for auth module
113
+ </TerminalLine>
114
+ <TerminalLine type="thinking">Reading src/auth/jwt.ts...</TerminalLine>
115
+ <TerminalLine type="output">Found 4 exported functions to test</TerminalLine>
116
+ <TerminalLine type="command">
117
+ {'cat > src/auth/__tests__/jwt.test.ts << \'EOF\''}
118
+ </TerminalLine>
119
+ <TerminalLine type="output">
120
+ {'Writing 6 test cases for: signToken, verifyToken, refreshToken, revokeToken'}
121
+ </TerminalLine>
122
+ <TerminalLine type="command">npx vitest run src/auth/__tests__/jwt.test.ts</TerminalLine>
123
+ <TerminalLine type="output">
124
+ {'✓ signToken returns a valid JWT (12ms)\n✓ verifyToken accepts valid tokens (3ms)\n✓ verifyToken rejects expired tokens (2ms)\n✓ refreshToken issues new token (8ms)\n✗ revokeToken marks token invalid (5ms)'}
125
+ </TerminalLine>
126
+ <TerminalLine type="error">
127
+ AssertionError: Expected token to be revoked but found status: "active"
128
+ </TerminalLine>
129
+ <TerminalLine type="thinking">
130
+ Investigating revocation logic...
131
+ </TerminalLine>
132
+ <TerminalLine type="output">
133
+ Found bug: redis.del() call missing await. Patching...
134
+ </TerminalLine>
135
+ <TerminalLine type="command">npx vitest run src/auth/__tests__/jwt.test.ts</TerminalLine>
136
+ <TerminalLine type="success">
137
+ {'✓ 6 tests passed (31ms)'}
138
+ </TerminalLine>
139
+ <TerminalLine type="info">Task complete. Patch written to src/auth/jwt.ts:L84.</TerminalLine>
140
+ </TerminalDisplay>
141
+ ),
142
+ }
143
+
144
+ export const NoHeader: Story = {
145
+ name: 'No Header',
146
+ render: () => (
147
+ <TerminalDisplay showHeader={false} className="w-[480px]">
148
+ <TerminalLine type="command">ls -la /workspace</TerminalLine>
149
+ <TerminalLine type="output">
150
+ {'total 48\ndrwxr-xr-x 8 node node 4096 Mar 30 14:02 .\ndrwxr-xr-x 3 root root 4096 Mar 30 14:00 ..\n-rw-r--r-- 1 node node 842 Mar 30 14:01 package.json\ndrwxr-xr-x 4 node node 4096 Mar 30 14:02 src'}
151
+ </TerminalLine>
152
+ <TerminalLine type="command">
153
+ {'cat package.json | jq \'.scripts\''}
154
+ </TerminalLine>
155
+ <TerminalLine type="output">
156
+ {'{\n "build": "tsc && vite build",\n "test": "vitest",\n "dev": "vite"\n}'}
157
+ </TerminalLine>
158
+ </TerminalDisplay>
159
+ ),
160
+ }
161
+
162
+ export const WithInput: Story = {
163
+ name: 'With Input',
164
+ render: () => {
165
+ const [lines, setLines] = useState<{ type: 'info' | 'command' | 'output' | 'error' | 'success' | 'warning' | 'thinking'; text: string }[]>([
166
+ { type: 'info', text: 'Session ready · node:20-alpine' },
167
+ ])
168
+
169
+ return (
170
+ <div className="flex flex-col gap-2 w-[560px]">
171
+ <TerminalDisplay title="interactive shell" maxHeight="240px">
172
+ {lines.map((l, i) => (
173
+ <TerminalLine key={i} type={l.type}>
174
+ {l.text}
175
+ </TerminalLine>
176
+ ))}
177
+ </TerminalDisplay>
178
+ <TerminalInput
179
+ placeholder="Enter a command..."
180
+ onSubmit={(cmd) => {
181
+ setLines((prev) => [
182
+ ...prev,
183
+ { type: 'command', text: cmd },
184
+ { type: 'output', text: `[${cmd}] executed` },
185
+ ])
186
+ }}
187
+ />
188
+ </div>
189
+ )
190
+ },
191
+ }
@@ -0,0 +1,189 @@
1
+ import * as React from "react";
2
+ import { useEffect, useRef } from "react";
3
+ import { cn } from "../lib/utils";
4
+
5
+ export interface TerminalDisplayProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ variant?: "default" | "sandbox";
7
+ title?: string;
8
+ showHeader?: boolean;
9
+ autoScroll?: boolean;
10
+ maxHeight?: string;
11
+ }
12
+
13
+ const TerminalDisplay = React.forwardRef<HTMLDivElement, TerminalDisplayProps>(
14
+ (
15
+ {
16
+ className,
17
+ variant = "default",
18
+ title = "Terminal",
19
+ showHeader = true,
20
+ autoScroll = true,
21
+ maxHeight = "400px",
22
+ children,
23
+ ...props
24
+ },
25
+ ref,
26
+ ) => {
27
+ const containerRef = useRef<HTMLDivElement>(null);
28
+
29
+ useEffect(() => {
30
+ if (autoScroll && containerRef.current) {
31
+ containerRef.current.scrollTop = containerRef.current.scrollHeight;
32
+ }
33
+ }, [autoScroll]);
34
+
35
+ const variants = {
36
+ default: "border-border",
37
+ sandbox: "border-border shadow-[var(--shadow-accent)]",
38
+ };
39
+
40
+ return (
41
+ <div
42
+ ref={ref}
43
+ className={cn(
44
+ "overflow-hidden rounded-xl border bg-background font-mono text-sm",
45
+ variants[variant],
46
+ className,
47
+ )}
48
+ {...props}
49
+ >
50
+ {showHeader && (
51
+ <div className="flex items-center border-b border-border bg-card px-4 py-3">
52
+ <span className="text-muted-foreground text-xs">{title}</span>
53
+ </div>
54
+ )}
55
+ <div
56
+ ref={containerRef}
57
+ className="overflow-auto p-4"
58
+ style={{ maxHeight }}
59
+ >
60
+ {children}
61
+ </div>
62
+ </div>
63
+ );
64
+ },
65
+ );
66
+ TerminalDisplay.displayName = "TerminalDisplay";
67
+
68
+ export interface TerminalLineProps
69
+ extends React.HTMLAttributes<HTMLDivElement> {
70
+ type?:
71
+ | "input"
72
+ | "output"
73
+ | "error"
74
+ | "success"
75
+ | "info"
76
+ | "thinking"
77
+ | "command"
78
+ | "warning";
79
+ prompt?: string;
80
+ timestamp?: string;
81
+ }
82
+
83
+ const TerminalLine = React.forwardRef<HTMLDivElement, TerminalLineProps>(
84
+ (
85
+ { className, type = "output", prompt = "$", timestamp, children, ...props },
86
+ ref,
87
+ ) => {
88
+ const typeStyles = {
89
+ input: "text-foreground",
90
+ output: "text-foreground",
91
+ error: "text-[var(--surface-danger-text)]",
92
+ success: "text-[var(--surface-success-text)]",
93
+ info: "text-[var(--surface-info-text)]",
94
+ thinking: "text-[var(--surface-warning-text)] animate-pulse",
95
+ command: "text-foreground",
96
+ warning: "text-[var(--surface-warning-text)]",
97
+ };
98
+
99
+ return (
100
+ <div
101
+ ref={ref}
102
+ className={cn(
103
+ "flex items-start gap-2 py-0.5 leading-relaxed",
104
+ typeStyles[type],
105
+ className,
106
+ )}
107
+ {...props}
108
+ >
109
+ {(type === "input" || type === "command") && (
110
+ <span className="shrink-0 select-none text-[var(--surface-success-text)]">{prompt}</span>
111
+ )}
112
+ {type === "thinking" && (
113
+ <span className="shrink-0 select-none">...</span>
114
+ )}
115
+ {timestamp && (
116
+ <span className="shrink-0 select-none text-muted-foreground opacity-50">
117
+ [{timestamp}]
118
+ </span>
119
+ )}
120
+ <span className="whitespace-pre-wrap break-all">{children}</span>
121
+ </div>
122
+ );
123
+ },
124
+ );
125
+ TerminalLine.displayName = "TerminalLine";
126
+
127
+ const TerminalCursor = React.forwardRef<
128
+ HTMLSpanElement,
129
+ React.HTMLAttributes<HTMLSpanElement>
130
+ >(({ className, ...props }, ref) => (
131
+ <span
132
+ ref={ref}
133
+ className={cn(
134
+ "ml-0.5 inline-block h-4 w-2 animate-pulse bg-foreground",
135
+ className,
136
+ )}
137
+ {...props}
138
+ />
139
+ ));
140
+ TerminalCursor.displayName = "TerminalCursor";
141
+
142
+ export interface TerminalInputProps
143
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "onSubmit"> {
144
+ onSubmit?: (value: string) => void;
145
+ variant?: "default" | "sandbox";
146
+ }
147
+
148
+ const TerminalInput = React.forwardRef<HTMLInputElement, TerminalInputProps>(
149
+ ({ className, onSubmit, variant = "default", ...props }, ref) => {
150
+ const [value, setValue] = React.useState("");
151
+
152
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
153
+ if (e.key === "Enter" && value.trim() && onSubmit) {
154
+ onSubmit(value.trim());
155
+ setValue("");
156
+ }
157
+ };
158
+
159
+ const variants = {
160
+ default: "border-border focus-within:border-border",
161
+ sandbox: "border-border focus-within:border-[var(--border-accent-hover)]",
162
+ };
163
+
164
+ return (
165
+ <div
166
+ className={cn(
167
+ "flex items-center rounded-lg border bg-background px-4 py-2.5 font-mono text-sm transition-colors",
168
+ variants[variant],
169
+ className,
170
+ )}
171
+ >
172
+ <span className="mr-2 select-none text-[var(--surface-success-text)]">$</span>
173
+ <input
174
+ ref={ref}
175
+ type="text"
176
+ value={value}
177
+ onChange={(e) => setValue(e.target.value)}
178
+ onKeyDown={handleKeyDown}
179
+ className="flex-1 bg-transparent text-foreground outline-none placeholder:text-muted-foreground"
180
+ {...props}
181
+ />
182
+ <TerminalCursor />
183
+ </div>
184
+ );
185
+ },
186
+ );
187
+ TerminalInput.displayName = "TerminalInput";
188
+
189
+ export { TerminalDisplay, TerminalLine, TerminalCursor, TerminalInput };
@@ -0,0 +1,32 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { ThemeToggle } from './theme-toggle'
3
+
4
+ const meta: Meta<typeof ThemeToggle> = {
5
+ title: 'Primitives/ThemeToggle',
6
+ component: ThemeToggle,
7
+ parameters: { layout: 'centered', backgrounds: { default: 'dark' } },
8
+ }
9
+
10
+ export default meta
11
+ type Story = StoryObj<typeof ThemeToggle>
12
+
13
+ export const Default: Story = {}
14
+
15
+ export const InToolbar: Story = {
16
+ name: 'In Toolbar',
17
+ render: () => (
18
+ <div className="flex items-center gap-1 rounded-lg border border-border bg-card px-2 py-1">
19
+ <button className="rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:bg-muted hover:text-foreground">
20
+ Sessions
21
+ </button>
22
+ <button className="rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:bg-muted hover:text-foreground">
23
+ Logs
24
+ </button>
25
+ <button className="rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:bg-muted hover:text-foreground">
26
+ Billing
27
+ </button>
28
+ <div className="mx-2 h-4 w-px bg-border" />
29
+ <ThemeToggle />
30
+ </div>
31
+ ),
32
+ }
@@ -0,0 +1,96 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+
3
+ type Theme = "light" | "dark" | "system";
4
+
5
+ function getSystemTheme(): "light" | "dark" {
6
+ return window.matchMedia("(prefers-color-scheme: dark)").matches
7
+ ? "dark"
8
+ : "light";
9
+ }
10
+
11
+ function applyTheme(theme: Theme) {
12
+ const resolved = theme === "system" ? getSystemTheme() : theme;
13
+ document.documentElement.classList.toggle("dark", resolved === "dark");
14
+ }
15
+
16
+ export function useTheme() {
17
+ const [theme, setThemeState] = useState<Theme>(() => {
18
+ if (typeof window === "undefined") return "system";
19
+ return (localStorage.getItem("theme") as Theme) ?? "system";
20
+ });
21
+
22
+ const setTheme = useCallback((next: Theme) => {
23
+ setThemeState(next);
24
+ if (next === "system") {
25
+ localStorage.removeItem("theme");
26
+ } else {
27
+ localStorage.setItem("theme", next);
28
+ }
29
+ applyTheme(next);
30
+ }, []);
31
+
32
+ useEffect(() => {
33
+ applyTheme(theme);
34
+ if (theme !== "system") return;
35
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
36
+ const handler = () => applyTheme("system");
37
+ mq.addEventListener("change", handler);
38
+ return () => mq.removeEventListener("change", handler);
39
+ }, [theme]);
40
+
41
+ return { theme, setTheme };
42
+ }
43
+
44
+ const iconClass = "h-4 w-4";
45
+
46
+ function SunIcon() {
47
+ return (
48
+ <svg
49
+ xmlns="http://www.w3.org/2000/svg"
50
+ viewBox="0 0 24 24"
51
+ fill="none"
52
+ stroke="currentColor"
53
+ strokeWidth={2}
54
+ strokeLinecap="round"
55
+ strokeLinejoin="round"
56
+ className={iconClass}
57
+ >
58
+ <circle cx={12} cy={12} r={5} />
59
+ <path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
60
+ </svg>
61
+ );
62
+ }
63
+
64
+ function MoonIcon() {
65
+ return (
66
+ <svg
67
+ xmlns="http://www.w3.org/2000/svg"
68
+ viewBox="0 0 24 24"
69
+ fill="none"
70
+ stroke="currentColor"
71
+ strokeWidth={2}
72
+ strokeLinecap="round"
73
+ strokeLinejoin="round"
74
+ className={iconClass}
75
+ >
76
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
77
+ </svg>
78
+ );
79
+ }
80
+
81
+ export function ThemeToggle() {
82
+ const { theme, setTheme } = useTheme();
83
+ const resolved =
84
+ theme === "system" ? getSystemTheme() : theme;
85
+
86
+ return (
87
+ <button
88
+ type="button"
89
+ onClick={() => setTheme(resolved === "dark" ? "light" : "dark")}
90
+ className="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors"
91
+ aria-label={`Switch to ${resolved === "dark" ? "light" : "dark"} mode`}
92
+ >
93
+ {resolved === "dark" ? <SunIcon /> : <MoonIcon />}
94
+ </button>
95
+ );
96
+ }