@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,123 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { Upload, FileText, ImageIcon } from 'lucide-react'
3
+ import { DropZone } from './drop-zone'
4
+
5
+ const AppContent = () => (
6
+ <div className="w-[600px] h-[380px] flex items-center justify-center rounded-xl border border-dashed border-border text-muted-foreground text-sm bg-muted">
7
+ App content area — drag files over this window
8
+ </div>
9
+ )
10
+
11
+ const meta: Meta<typeof DropZone> = {
12
+ title: 'Primitives/DropZone',
13
+ component: DropZone,
14
+ parameters: { layout: 'centered', backgrounds: { default: 'dark' } },
15
+ args: {
16
+ onDrop: (files) => console.log('dropped', files),
17
+ children: <AppContent />,
18
+ },
19
+ }
20
+
21
+ export default meta
22
+ type Story = StoryObj<typeof DropZone>
23
+
24
+ export const Idle: Story = {
25
+ name: 'Idle (no drag)',
26
+ }
27
+
28
+ export const WithAcceptFilter: Story = {
29
+ name: 'With Accept Filter (.pdf, .csv)',
30
+ args: {
31
+ accept: '.pdf,.csv',
32
+ title: 'Drop CSV or PDF files',
33
+ description: 'Only .pdf and .csv files will be accepted.',
34
+ },
35
+ }
36
+
37
+ export const Disabled: Story = {
38
+ name: 'Disabled',
39
+ args: { disabled: true },
40
+ }
41
+
42
+ /** Static render of the default drop overlay — the real one only mounts during dragenter. */
43
+ export const OverlayDefault: Story = {
44
+ name: 'Overlay — Default',
45
+ render: () => (
46
+ <div className="relative w-[600px] h-[380px]">
47
+ <AppContent />
48
+ <div className="absolute inset-0 z-10 flex items-center justify-center rounded-xl bg-background/80">
49
+ <div className="rounded-2xl border-2 border-dashed border-border bg-card p-12 text-center max-w-sm w-full mx-4">
50
+ <div className="mx-auto mb-5 flex h-16 w-16 items-center justify-center rounded-2xl border border-border bg-[var(--accent-surface-soft)]">
51
+ <Upload className="h-8 w-8 text-[var(--accent-text)]" />
52
+ </div>
53
+ <h2 className="text-xl font-bold text-foreground">Drop files to upload</h2>
54
+ <p className="mt-2 text-sm text-muted-foreground">Your files will be securely stored in the workspace.</p>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ ),
59
+ }
60
+
61
+ export const OverlayFiltered: Story = {
62
+ name: 'Overlay — Accept filter (.pdf, .csv)',
63
+ render: () => (
64
+ <div className="relative w-[600px] h-[380px]">
65
+ <AppContent />
66
+ <div className="absolute inset-0 z-10 flex items-center justify-center rounded-xl bg-background/80">
67
+ <div className="rounded-2xl border-2 border-dashed border-border bg-card p-12 text-center max-w-sm w-full mx-4">
68
+ <div className="mx-auto mb-5 flex h-16 w-16 items-center justify-center rounded-2xl border border-border bg-[var(--accent-surface-soft)]">
69
+ <FileText className="h-8 w-8 text-[var(--accent-text)]" />
70
+ </div>
71
+ <h2 className="text-xl font-bold text-foreground">Drop CSV or PDF files</h2>
72
+ <p className="mt-2 text-sm text-muted-foreground">Only .pdf and .csv files will be accepted.</p>
73
+ <div className="mt-4 flex items-center justify-center gap-2">
74
+ <span className="rounded-full border border-border bg-[var(--accent-surface-soft)] px-2.5 py-1 text-[11px] font-mono text-[var(--accent-text)]">.pdf</span>
75
+ <span className="rounded-full border border-border bg-[var(--accent-surface-soft)] px-2.5 py-1 text-[11px] font-mono text-[var(--accent-text)]">.csv</span>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ ),
81
+ }
82
+
83
+ export const OverlayImages: Story = {
84
+ name: 'Overlay — Images',
85
+ render: () => (
86
+ <div className="relative w-[600px] h-[380px]">
87
+ <AppContent />
88
+ <div className="absolute inset-0 z-10 flex items-center justify-center rounded-xl bg-background/80">
89
+ <div className="rounded-2xl border-2 border-dashed border-emerald-500/40 bg-card p-12 text-center max-w-sm w-full mx-4">
90
+ <div className="mx-auto mb-5 flex h-16 w-16 items-center justify-center rounded-2xl border border-emerald-500/30 bg-emerald-500/10">
91
+ <ImageIcon className="h-8 w-8 text-emerald-400" />
92
+ </div>
93
+ <h2 className="text-xl font-bold text-foreground">Drop images here</h2>
94
+ <p className="mt-2 text-sm text-muted-foreground">Accepts .png, .jpg, .webp, .gif</p>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ ),
99
+ }
100
+
101
+ export const OverlayCustom: Story = {
102
+ name: 'Overlay — Custom content',
103
+ render: () => (
104
+ <div className="relative w-[600px] h-[380px]">
105
+ <AppContent />
106
+ <DropZone
107
+ onDrop={(files) => console.log('dropped', files)}
108
+ overlay={
109
+ <div className="fixed inset-0 z-[100] flex items-center justify-center pointer-events-none bg-background/80">
110
+ <div className="rounded-2xl border-2 border-dashed border-[var(--brand-emerald)]/50 bg-card p-14 text-center">
111
+ <p className="text-2xl font-bold text-[var(--brand-emerald)]">Custom overlay</p>
112
+ <p className="mt-2 text-muted-foreground text-sm">Replace default drop UI with anything.</p>
113
+ </div>
114
+ </div>
115
+ }
116
+ >
117
+ <div className="w-[600px] h-[380px] flex items-center justify-center rounded-xl border border-dashed border-border text-muted-foreground text-sm bg-muted">
118
+ Drag a real file here to trigger custom overlay
119
+ </div>
120
+ </DropZone>
121
+ </div>
122
+ ),
123
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * DropZone — full-window drag-and-drop file upload overlay.
3
+ *
4
+ * Detects when files are dragged over the browser window and shows a
5
+ * customizable overlay. Products provide the onDrop handler and optional
6
+ * custom overlay content.
7
+ *
8
+ * Usage:
9
+ * <DropZone onDrop={files => uploadToR2(files)} accept=".pdf,.csv">
10
+ * <YourApp />
11
+ * </DropZone>
12
+ */
13
+
14
+ import { useCallback, useRef, useState, type DragEvent, type ReactNode } from "react";
15
+ import { cn } from "../lib/utils";
16
+
17
+ export interface DropZoneProps {
18
+ /** Called with dropped files */
19
+ onDrop: (files: File[]) => void;
20
+ /** Accepted file types (e.g. ".pdf,.csv,.xlsx") */
21
+ accept?: string;
22
+ /** Whether drop zone is active */
23
+ disabled?: boolean;
24
+ /** Custom overlay content (replaces default) */
25
+ overlay?: ReactNode;
26
+ /** Overlay title */
27
+ title?: string;
28
+ /** Overlay description */
29
+ description?: string;
30
+ /** Overlay icon (Material Symbols name or ReactNode) */
31
+ icon?: string | ReactNode;
32
+ /** Children wrapped by the drop zone */
33
+ children: ReactNode;
34
+ className?: string;
35
+ }
36
+
37
+ export function DropZone({
38
+ onDrop,
39
+ accept,
40
+ disabled,
41
+ overlay,
42
+ title = "Drop files to upload",
43
+ description = "Your files will be securely stored in the workspace.",
44
+ icon = "cloud_upload",
45
+ children,
46
+ className,
47
+ }: DropZoneProps) {
48
+ const [dragOver, setDragOver] = useState(false);
49
+ const counter = useRef(0);
50
+
51
+ const isAccepted = useCallback(
52
+ (file: File) => {
53
+ if (!accept) return true;
54
+ const extensions = accept.split(",").map((ext) => ext.trim().toLowerCase());
55
+ const fileName = file.name.toLowerCase();
56
+ return extensions.some((ext) => fileName.endsWith(ext));
57
+ },
58
+ [accept],
59
+ );
60
+
61
+ const handleDragEnter = useCallback(
62
+ (e: DragEvent) => {
63
+ e.preventDefault();
64
+ if (disabled) return;
65
+ counter.current++;
66
+ if (e.dataTransfer?.types.includes("Files")) setDragOver(true);
67
+ },
68
+ [disabled],
69
+ );
70
+
71
+ const handleDragLeave = useCallback((e: DragEvent) => {
72
+ e.preventDefault();
73
+ counter.current--;
74
+ if (counter.current === 0) setDragOver(false);
75
+ }, []);
76
+
77
+ const handleDragOver = useCallback(
78
+ (e: DragEvent) => {
79
+ e.preventDefault();
80
+ if (!disabled) e.dataTransfer.dropEffect = "copy";
81
+ },
82
+ [disabled],
83
+ );
84
+
85
+ const handleDrop = useCallback(
86
+ (e: DragEvent) => {
87
+ e.preventDefault();
88
+ counter.current = 0;
89
+ setDragOver(false);
90
+ if (disabled) return;
91
+
92
+ const allFiles = Array.from(e.dataTransfer?.files || []);
93
+ const accepted = accept ? allFiles.filter(isAccepted) : allFiles;
94
+ if (accepted.length > 0) onDrop(accepted);
95
+ },
96
+ [disabled, accept, isAccepted, onDrop],
97
+ );
98
+
99
+ return (
100
+ <div
101
+ onDragEnter={handleDragEnter}
102
+ onDragLeave={handleDragLeave}
103
+ onDragOver={handleDragOver}
104
+ onDrop={handleDrop}
105
+ className={cn("relative", className)}
106
+ >
107
+ {dragOver &&
108
+ (overlay || (
109
+ <div className="fixed inset-0 z-[100] flex items-center justify-center pointer-events-none bg-background">
110
+ <div className="rounded-2xl border-2 border-dashed border-border bg-card p-16 text-center shadow-[var(--shadow-dropdown)] max-w-lg mx-auto">
111
+ <div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-2xl border border-border bg-[var(--accent-surface-soft)]">
112
+ {typeof icon === "string" ? (
113
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="h-10 w-10 text-[var(--accent-text)]">
114
+ <title>Upload</title>
115
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
116
+ <polyline points="17 8 12 3 7 8" />
117
+ <line x1="12" x2="12" y1="3" y2="15" />
118
+ </svg>
119
+ ) : (
120
+ icon
121
+ )}
122
+ </div>
123
+ <h2 className="text-2xl font-bold text-foreground">{title}</h2>
124
+ <p className="mt-2 text-sm text-muted-foreground">{description}</p>
125
+ </div>
126
+ </div>
127
+ ))}
128
+ {children}
129
+ </div>
130
+ );
131
+ }
@@ -0,0 +1,122 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { ChevronDown, MoreHorizontal, Settings, Terminal } from 'lucide-react'
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuGroup,
7
+ DropdownMenuItem,
8
+ DropdownMenuLabel,
9
+ DropdownMenuSeparator,
10
+ DropdownMenuShortcut,
11
+ DropdownMenuTrigger,
12
+ } from './dropdown-menu'
13
+
14
+ const meta: Meta = {
15
+ title: 'Primitives/DropdownMenu',
16
+ parameters: { layout: 'centered', backgrounds: { default: 'dark' } },
17
+ }
18
+
19
+ export default meta
20
+ type Story = StoryObj
21
+
22
+ export const Default: Story = {
23
+ name: 'Default',
24
+ render: () => (
25
+ <DropdownMenu defaultOpen>
26
+ <DropdownMenuTrigger asChild>
27
+ <button className="flex items-center gap-1.5 rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground hover:bg-muted">
28
+ Options
29
+ <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
30
+ </button>
31
+ </DropdownMenuTrigger>
32
+ <DropdownMenuContent className="w-48">
33
+ <DropdownMenuLabel>Session</DropdownMenuLabel>
34
+ <DropdownMenuSeparator />
35
+ <DropdownMenuGroup>
36
+ <DropdownMenuItem>
37
+ <Terminal className="mr-2 h-4 w-4" />
38
+ Open terminal
39
+ <DropdownMenuShortcut>⌘T</DropdownMenuShortcut>
40
+ </DropdownMenuItem>
41
+ <DropdownMenuItem>
42
+ <Settings className="mr-2 h-4 w-4" />
43
+ Configure
44
+ <DropdownMenuShortcut>⌘,</DropdownMenuShortcut>
45
+ </DropdownMenuItem>
46
+ </DropdownMenuGroup>
47
+ <DropdownMenuSeparator />
48
+ <DropdownMenuItem className="text-red-400 focus:text-red-400">
49
+ Terminate session
50
+ <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
51
+ </DropdownMenuItem>
52
+ </DropdownMenuContent>
53
+ </DropdownMenu>
54
+ ),
55
+ }
56
+
57
+ export const SessionRowMenu: Story = {
58
+ name: 'Session Row Menu',
59
+ render: () => (
60
+ <DropdownMenu defaultOpen>
61
+ <DropdownMenuTrigger asChild>
62
+ <button className="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground">
63
+ <MoreHorizontal className="h-4 w-4" />
64
+ </button>
65
+ </DropdownMenuTrigger>
66
+ <DropdownMenuContent className="w-44" align="end">
67
+ <DropdownMenuItem>View logs</DropdownMenuItem>
68
+ <DropdownMenuItem>Open shell</DropdownMenuItem>
69
+ <DropdownMenuItem>Copy session ID</DropdownMenuItem>
70
+ <DropdownMenuSeparator />
71
+ <DropdownMenuItem>Restart</DropdownMenuItem>
72
+ <DropdownMenuSeparator />
73
+ <DropdownMenuItem className="text-red-400 focus:text-red-400">
74
+ Terminate
75
+ </DropdownMenuItem>
76
+ </DropdownMenuContent>
77
+ </DropdownMenu>
78
+ ),
79
+ }
80
+
81
+ export const RegionSelector: Story = {
82
+ name: 'Region Selector',
83
+ render: () => (
84
+ <DropdownMenu defaultOpen>
85
+ <DropdownMenuTrigger asChild>
86
+ <button className="flex items-center gap-2 rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground hover:bg-muted">
87
+ <span className="h-2 w-2 rounded-full bg-green-400" />
88
+ us-east-1
89
+ <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
90
+ </button>
91
+ </DropdownMenuTrigger>
92
+ <DropdownMenuContent className="w-52">
93
+ <DropdownMenuLabel className="text-xs text-muted-foreground font-normal tracking-widest uppercase">
94
+ Regions
95
+ </DropdownMenuLabel>
96
+ <DropdownMenuSeparator />
97
+ <DropdownMenuGroup>
98
+ <DropdownMenuItem>
99
+ <span className="mr-2 h-2 w-2 rounded-full bg-green-400" />
100
+ us-east-1
101
+ <DropdownMenuShortcut className="text-green-400">Online</DropdownMenuShortcut>
102
+ </DropdownMenuItem>
103
+ <DropdownMenuItem>
104
+ <span className="mr-2 h-2 w-2 rounded-full bg-green-400" />
105
+ us-west-2
106
+ <DropdownMenuShortcut className="text-green-400">Online</DropdownMenuShortcut>
107
+ </DropdownMenuItem>
108
+ <DropdownMenuItem>
109
+ <span className="mr-2 h-2 w-2 rounded-full bg-green-400" />
110
+ eu-central-1
111
+ <DropdownMenuShortcut className="text-green-400">Online</DropdownMenuShortcut>
112
+ </DropdownMenuItem>
113
+ <DropdownMenuItem disabled>
114
+ <span className="mr-2 h-2 w-2 rounded-full bg-muted-foreground" />
115
+ ap-southeast-1
116
+ <DropdownMenuShortcut>Soon</DropdownMenuShortcut>
117
+ </DropdownMenuItem>
118
+ </DropdownMenuGroup>
119
+ </DropdownMenuContent>
120
+ </DropdownMenu>
121
+ ),
122
+ }
@@ -0,0 +1,214 @@
1
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
2
+ import { Check, ChevronRight, Circle } from "lucide-react";
3
+ import * as React from "react";
4
+ import { cn } from "../lib/utils";
5
+
6
+ const DropdownMenu = DropdownMenuPrimitive.Root;
7
+
8
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
9
+
10
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group;
11
+
12
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
13
+
14
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
15
+
16
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
17
+
18
+ const DropdownMenuSubTrigger = React.forwardRef<
19
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
20
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
21
+ inset?: boolean;
22
+ }
23
+ >(({ className, inset, children, ...props }, ref) => (
24
+ <DropdownMenuPrimitive.SubTrigger
25
+ ref={ref}
26
+ className={cn(
27
+ "flex cursor-default select-none items-center rounded-md px-2 py-1.5 text-sm outline-none",
28
+ "focus:bg-accent data-[state=open]:bg-muted/50",
29
+ inset && "pl-8",
30
+ className,
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <ChevronRight className="ml-auto h-4 w-4" />
36
+ </DropdownMenuPrimitive.SubTrigger>
37
+ ));
38
+ DropdownMenuSubTrigger.displayName =
39
+ DropdownMenuPrimitive.SubTrigger.displayName;
40
+
41
+ const DropdownMenuSubContent = React.forwardRef<
42
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
43
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
44
+ >(({ className, ...props }, ref) => (
45
+ <DropdownMenuPrimitive.SubContent
46
+ ref={ref}
47
+ className={cn(
48
+ "z-50 min-w-[8rem] overflow-hidden rounded-xl border border-border bg-card p-1 text-foreground shadow-[var(--shadow-card)]",
49
+ "data-[state=closed]:animate-out data-[state=open]:animate-in",
50
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
51
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
52
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
53
+ "data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
54
+ className,
55
+ )}
56
+ {...props}
57
+ />
58
+ ));
59
+ DropdownMenuSubContent.displayName =
60
+ DropdownMenuPrimitive.SubContent.displayName;
61
+
62
+ const DropdownMenuContent = React.forwardRef<
63
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
64
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
65
+ >(({ className, sideOffset = 4, ...props }, ref) => (
66
+ <DropdownMenuPrimitive.Portal>
67
+ <DropdownMenuPrimitive.Content
68
+ ref={ref}
69
+ sideOffset={sideOffset}
70
+ className={cn(
71
+ "z-50 min-w-[8rem] overflow-hidden rounded-xl border border-border bg-card p-1 text-foreground shadow-[var(--shadow-card)]",
72
+ "data-[state=closed]:animate-out data-[state=open]:animate-in",
73
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
74
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
75
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
76
+ "data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
77
+ className,
78
+ )}
79
+ {...props}
80
+ />
81
+ </DropdownMenuPrimitive.Portal>
82
+ ));
83
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
84
+
85
+ const DropdownMenuItem = React.forwardRef<
86
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
87
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
88
+ inset?: boolean;
89
+ }
90
+ >(({ className, inset, ...props }, ref) => (
91
+ <DropdownMenuPrimitive.Item
92
+ ref={ref}
93
+ className={cn(
94
+ "relative flex cursor-pointer select-none items-center rounded-md px-2 py-1.5 text-sm outline-none transition-colors",
95
+ "focus:bg-muted/50 focus:text-foreground",
96
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
97
+ inset && "pl-8",
98
+ className,
99
+ )}
100
+ {...props}
101
+ />
102
+ ));
103
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
104
+
105
+ const DropdownMenuCheckboxItem = React.forwardRef<
106
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
107
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
108
+ >(({ className, children, checked, ...props }, ref) => (
109
+ <DropdownMenuPrimitive.CheckboxItem
110
+ ref={ref}
111
+ className={cn(
112
+ "relative flex cursor-pointer select-none items-center rounded-md py-1.5 pr-2 pl-8 text-sm outline-none transition-colors",
113
+ "focus:bg-muted/50 focus:text-foreground",
114
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
115
+ className,
116
+ )}
117
+ checked={checked}
118
+ {...props}
119
+ >
120
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
121
+ <DropdownMenuPrimitive.ItemIndicator>
122
+ <Check className="h-4 w-4" />
123
+ </DropdownMenuPrimitive.ItemIndicator>
124
+ </span>
125
+ {children}
126
+ </DropdownMenuPrimitive.CheckboxItem>
127
+ ));
128
+ DropdownMenuCheckboxItem.displayName =
129
+ DropdownMenuPrimitive.CheckboxItem.displayName;
130
+
131
+ const DropdownMenuRadioItem = React.forwardRef<
132
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
133
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
134
+ >(({ className, children, ...props }, ref) => (
135
+ <DropdownMenuPrimitive.RadioItem
136
+ ref={ref}
137
+ className={cn(
138
+ "relative flex cursor-pointer select-none items-center rounded-md py-1.5 pr-2 pl-8 text-sm outline-none transition-colors",
139
+ "focus:bg-muted/50 focus:text-foreground",
140
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
141
+ className,
142
+ )}
143
+ {...props}
144
+ >
145
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
146
+ <DropdownMenuPrimitive.ItemIndicator>
147
+ <Circle className="h-2 w-2 fill-current" />
148
+ </DropdownMenuPrimitive.ItemIndicator>
149
+ </span>
150
+ {children}
151
+ </DropdownMenuPrimitive.RadioItem>
152
+ ));
153
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
154
+
155
+ const DropdownMenuLabel = React.forwardRef<
156
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
157
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
158
+ inset?: boolean;
159
+ }
160
+ >(({ className, inset, ...props }, ref) => (
161
+ <DropdownMenuPrimitive.Label
162
+ ref={ref}
163
+ className={cn(
164
+ "px-2 py-1.5 font-semibold text-sm",
165
+ inset && "pl-8",
166
+ className,
167
+ )}
168
+ {...props}
169
+ />
170
+ ));
171
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
172
+
173
+ const DropdownMenuSeparator = React.forwardRef<
174
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
175
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
176
+ >(({ className, ...props }, ref) => (
177
+ <DropdownMenuPrimitive.Separator
178
+ ref={ref}
179
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
180
+ {...props}
181
+ />
182
+ ));
183
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
184
+
185
+ const DropdownMenuShortcut = ({
186
+ className,
187
+ ...props
188
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
189
+ return (
190
+ <span
191
+ className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
192
+ {...props}
193
+ />
194
+ );
195
+ };
196
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
197
+
198
+ export {
199
+ DropdownMenu,
200
+ DropdownMenuTrigger,
201
+ DropdownMenuContent,
202
+ DropdownMenuItem,
203
+ DropdownMenuCheckboxItem,
204
+ DropdownMenuRadioItem,
205
+ DropdownMenuLabel,
206
+ DropdownMenuSeparator,
207
+ DropdownMenuShortcut,
208
+ DropdownMenuGroup,
209
+ DropdownMenuPortal,
210
+ DropdownMenuSub,
211
+ DropdownMenuSubContent,
212
+ DropdownMenuSubTrigger,
213
+ DropdownMenuRadioGroup,
214
+ };
@@ -0,0 +1,81 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { Box, Clock, Search, Terminal, Zap } from 'lucide-react'
3
+ import { EmptyState } from './empty-state'
4
+
5
+ const meta: Meta<typeof EmptyState> = {
6
+ title: 'Primitives/EmptyState',
7
+ component: EmptyState,
8
+ parameters: { layout: 'centered', backgrounds: { default: 'dark' } },
9
+ }
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof EmptyState>
13
+
14
+ export const NoSessions: Story = {
15
+ name: 'No Sessions',
16
+ args: {
17
+ icon: <Terminal className="h-6 w-6" />,
18
+ title: 'No active sessions',
19
+ description:
20
+ 'Provision a sandbox to run code in an isolated container. Sessions spin up in under a second.',
21
+ action: (
22
+ <button className="rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90">
23
+ New session
24
+ </button>
25
+ ),
26
+ },
27
+ }
28
+
29
+ export const NoResults: Story = {
30
+ name: 'No Search Results',
31
+ args: {
32
+ icon: <Search className="h-6 w-6" />,
33
+ title: 'No sessions found',
34
+ description: 'No sessions match your current filters. Try adjusting the status or date range.',
35
+ action: (
36
+ <button className="rounded-md border border-border px-4 py-2 text-sm text-muted-foreground hover:bg-muted">
37
+ Clear filters
38
+ </button>
39
+ ),
40
+ },
41
+ }
42
+
43
+ export const NoLogs: Story = {
44
+ name: 'No Logs',
45
+ args: {
46
+ icon: <Clock className="h-6 w-6" />,
47
+ title: 'No logs yet',
48
+ description: 'Logs will appear here once the session starts executing commands.',
49
+ },
50
+ }
51
+
52
+ export const NoSnapshots: Story = {
53
+ name: 'No Snapshots',
54
+ args: {
55
+ icon: <Box className="h-6 w-6" />,
56
+ title: 'No snapshots',
57
+ description:
58
+ 'Snapshots capture the full filesystem state of a session. Create one to resume from a known point.',
59
+ action: (
60
+ <button className="rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90">
61
+ Create snapshot
62
+ </button>
63
+ ),
64
+ },
65
+ }
66
+
67
+ export const BillingIdle: Story = {
68
+ name: 'Idle — No Usage',
69
+ args: {
70
+ icon: <Zap className="h-6 w-6" />,
71
+ title: 'No usage this period',
72
+ description: 'You have not run any sandbox sessions in the current billing cycle.',
73
+ },
74
+ }
75
+
76
+ export const TitleOnly: Story = {
77
+ name: 'Title Only',
78
+ args: {
79
+ title: 'Nothing here yet',
80
+ },
81
+ }