@djangocfg/ui-tools 2.1.381 → 2.1.383

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 (178) hide show
  1. package/README.md +132 -899
  2. package/dist/ChatRoot-6IZFM5HM.mjs +5 -0
  3. package/dist/{ChatRoot-EJC5Y2YM.cjs.map → ChatRoot-6IZFM5HM.mjs.map} +1 -1
  4. package/dist/ChatRoot-LW4XNIKP.cjs +14 -0
  5. package/dist/{ChatRoot-QOSKJPM6.mjs.map → ChatRoot-LW4XNIKP.cjs.map} +1 -1
  6. package/dist/DictationField-U25MEYAL.mjs +4 -0
  7. package/dist/DictationField-U25MEYAL.mjs.map +1 -0
  8. package/dist/DictationField-XWR5VOID.cjs +13 -0
  9. package/dist/DictationField-XWR5VOID.cjs.map +1 -0
  10. package/dist/{DocsLayout-2YKPXZYO.mjs → DocsLayout-2P3ONDWJ.mjs} +3 -3
  11. package/dist/{DocsLayout-2YKPXZYO.mjs.map → DocsLayout-2P3ONDWJ.mjs.map} +1 -1
  12. package/dist/{DocsLayout-Q4KS3QWW.cjs → DocsLayout-2YZNS5VK.cjs} +8 -8
  13. package/dist/{DocsLayout-Q4KS3QWW.cjs.map → DocsLayout-2YZNS5VK.cjs.map} +1 -1
  14. package/dist/chunk-4PFW7MIJ.cjs +837 -0
  15. package/dist/chunk-4PFW7MIJ.cjs.map +1 -0
  16. package/dist/chunk-C2YN6WEO.mjs +833 -0
  17. package/dist/chunk-C2YN6WEO.mjs.map +1 -0
  18. package/dist/{chunk-XACCHZH2.cjs → chunk-FIRK5CEH.cjs} +42 -4
  19. package/dist/chunk-FIRK5CEH.cjs.map +1 -0
  20. package/dist/{chunk-NWUT327A.mjs → chunk-HIK6BPL7.mjs} +38 -5
  21. package/dist/chunk-HIK6BPL7.mjs.map +1 -0
  22. package/dist/chunk-OZAU3QWD.cjs +2493 -0
  23. package/dist/chunk-OZAU3QWD.cjs.map +1 -0
  24. package/dist/chunk-UWVP6LCW.mjs +2447 -0
  25. package/dist/chunk-UWVP6LCW.mjs.map +1 -0
  26. package/dist/index.cjs +1668 -99
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +1215 -107
  29. package/dist/index.d.ts +1215 -107
  30. package/dist/index.mjs +1555 -50
  31. package/dist/index.mjs.map +1 -1
  32. package/package.json +16 -15
  33. package/src/audio-assets.d.ts +8 -0
  34. package/src/components/markdown/MarkdownMessage/CollapseToggle.tsx +3 -1
  35. package/src/components/markdown/MarkdownMessage/components.tsx +2 -5
  36. package/src/tools/Chat/README.md +347 -530
  37. package/src/tools/Chat/components/Attachments.tsx +6 -1
  38. package/src/tools/Chat/components/ChatRoot.tsx +30 -2
  39. package/src/tools/Chat/components/Composer.tsx +20 -3
  40. package/src/tools/Chat/components/ErrorBanner.tsx +7 -3
  41. package/src/tools/Chat/components/MessageActions.tsx +3 -1
  42. package/src/tools/Chat/components/MessageBubble.tsx +6 -5
  43. package/src/tools/Chat/components/MessageList.tsx +87 -1
  44. package/src/tools/Chat/components/ToolCalls.tsx +21 -3
  45. package/src/tools/Chat/context/ChatProvider.tsx +21 -3
  46. package/src/tools/Chat/core/audio/audioBus.ts +10 -163
  47. package/src/tools/Chat/core/audio/defaults.ts +43 -0
  48. package/src/tools/Chat/core/audio/index.ts +1 -0
  49. package/src/tools/Chat/core/audio/preferences.ts +5 -59
  50. package/src/tools/Chat/core/audio/sounds/error.mp3 +0 -0
  51. package/src/tools/Chat/core/audio/sounds/mention.mp3 +0 -0
  52. package/src/tools/Chat/core/audio/sounds/notification.mp3 +0 -0
  53. package/src/tools/Chat/core/audio/sounds/received.mp3 +0 -0
  54. package/src/tools/Chat/core/audio/sounds/sent.mp3 +0 -0
  55. package/src/tools/Chat/core/audio/sounds/start.mp3 +0 -0
  56. package/src/tools/Chat/core/audio/types.ts +28 -0
  57. package/src/tools/Chat/core/reducer.ts +33 -0
  58. package/src/tools/Chat/core/transport/index.ts +13 -0
  59. package/src/tools/Chat/core/transport/mappers/index.ts +6 -0
  60. package/src/tools/Chat/core/transport/mappers/pydantic-ai.ts +142 -0
  61. package/src/tools/Chat/core/transport/pydantic-ai-transport.ts +208 -0
  62. package/src/tools/Chat/core/transport/sse.ts +18 -5
  63. package/src/tools/Chat/hooks/index.ts +25 -0
  64. package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +5 -3
  65. package/src/tools/Chat/hooks/useChat.ts +28 -0
  66. package/src/tools/Chat/hooks/useChatAudio.ts +59 -180
  67. package/src/tools/Chat/hooks/useChatDockPrefs.ts +74 -0
  68. package/src/tools/Chat/hooks/useChatReset.ts +70 -0
  69. package/src/tools/Chat/hooks/useChatUnread.ts +87 -0
  70. package/src/tools/Chat/hooks/useFocusOnEmptyClick.ts +111 -0
  71. package/src/tools/Chat/hooks/useVisitorFingerprint.ts +48 -0
  72. package/src/tools/Chat/index.ts +84 -1
  73. package/src/tools/Chat/launcher/ChatDock.tsx +263 -0
  74. package/src/tools/Chat/launcher/ChatFAB.tsx +349 -0
  75. package/src/tools/Chat/launcher/ChatGreeting.tsx +200 -0
  76. package/src/tools/Chat/launcher/ChatHeader.tsx +76 -0
  77. package/src/tools/Chat/launcher/ChatHeaderActionButton.tsx +87 -0
  78. package/src/tools/Chat/launcher/ChatHeaderAudioToggle.tsx +47 -0
  79. package/src/tools/Chat/launcher/ChatHeaderLanguageButton.tsx +179 -0
  80. package/src/tools/Chat/launcher/ChatHeaderModeToggle.tsx +57 -0
  81. package/src/tools/Chat/launcher/ChatHeaderResetButton.tsx +93 -0
  82. package/src/tools/Chat/launcher/ChatLauncher.tsx +321 -0
  83. package/src/tools/Chat/launcher/ChatUnreadPreview.tsx +197 -0
  84. package/src/tools/Chat/launcher/index.ts +46 -0
  85. package/src/tools/Chat/launcher/useChatPresence.ts +44 -0
  86. package/src/tools/Chat/styles/bubbleTokens.ts +71 -0
  87. package/src/tools/Chat/styles/index.ts +16 -0
  88. package/src/tools/Chat/styles/useChatStyles.ts +101 -0
  89. package/src/tools/Chat/types/attachment.ts +25 -0
  90. package/src/tools/Chat/types/config.ts +48 -0
  91. package/src/tools/Chat/types/events.ts +35 -0
  92. package/src/tools/Chat/types/index.ts +34 -0
  93. package/src/tools/Chat/types/labels.ts +38 -0
  94. package/src/tools/Chat/types/message.ts +32 -0
  95. package/src/tools/Chat/types/persona.ts +31 -0
  96. package/src/tools/Chat/types/session.ts +43 -0
  97. package/src/tools/Chat/types/tool-call.ts +17 -0
  98. package/src/tools/Chat/types/transport.ts +28 -0
  99. package/src/tools/Chat/types.ts +5 -240
  100. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +50 -14
  101. package/src/tools/MarkdownEditor/index.ts +1 -1
  102. package/src/tools/SpeechRecognition/README.md +336 -0
  103. package/src/tools/SpeechRecognition/__tests__/ids.test.ts +15 -0
  104. package/src/tools/SpeechRecognition/__tests__/language.test.ts +59 -0
  105. package/src/tools/SpeechRecognition/__tests__/reducer.test.ts +71 -0
  106. package/src/tools/SpeechRecognition/__tests__/transcript.test.ts +52 -0
  107. package/src/tools/SpeechRecognition/components/DevicePicker.tsx +49 -0
  108. package/src/tools/SpeechRecognition/components/DictationButton.tsx +93 -0
  109. package/src/tools/SpeechRecognition/components/EngineBadge.tsx +30 -0
  110. package/src/tools/SpeechRecognition/components/ErrorBanner.tsx +52 -0
  111. package/src/tools/SpeechRecognition/components/LanguagePicker.tsx +63 -0
  112. package/src/tools/SpeechRecognition/components/MicMeter.tsx +63 -0
  113. package/src/tools/SpeechRecognition/components/PushToTalkHint.tsx +51 -0
  114. package/src/tools/SpeechRecognition/components/TranscriptView.tsx +55 -0
  115. package/src/tools/SpeechRecognition/components/index.ts +16 -0
  116. package/src/tools/SpeechRecognition/context/SpeechRecognitionProvider.tsx +47 -0
  117. package/src/tools/SpeechRecognition/context/index.ts +6 -0
  118. package/src/tools/SpeechRecognition/core/audio/defaults.ts +24 -0
  119. package/src/tools/SpeechRecognition/core/engine/external.ts +222 -0
  120. package/src/tools/SpeechRecognition/core/engine/http.ts +147 -0
  121. package/src/tools/SpeechRecognition/core/engine/index.ts +52 -0
  122. package/src/tools/SpeechRecognition/core/engine/mediarecorder.ts +105 -0
  123. package/src/tools/SpeechRecognition/core/engine/websocket.ts +211 -0
  124. package/src/tools/SpeechRecognition/core/engine/webspeech.ts +188 -0
  125. package/src/tools/SpeechRecognition/core/ids.ts +11 -0
  126. package/src/tools/SpeechRecognition/core/index.ts +14 -0
  127. package/src/tools/SpeechRecognition/core/language.ts +78 -0
  128. package/src/tools/SpeechRecognition/core/languages-catalog.ts +229 -0
  129. package/src/tools/SpeechRecognition/core/logger.ts +3 -0
  130. package/src/tools/SpeechRecognition/core/reducer.ts +105 -0
  131. package/src/tools/SpeechRecognition/core/transcript.ts +36 -0
  132. package/src/tools/SpeechRecognition/hooks/index.ts +14 -0
  133. package/src/tools/SpeechRecognition/hooks/useDictation.ts +59 -0
  134. package/src/tools/SpeechRecognition/hooks/useEnginePrefs.ts +15 -0
  135. package/src/tools/SpeechRecognition/hooks/useMicDevices.ts +57 -0
  136. package/src/tools/SpeechRecognition/hooks/useMicLevel.ts +52 -0
  137. package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +85 -0
  138. package/src/tools/SpeechRecognition/hooks/useResolvedLanguage.ts +28 -0
  139. package/src/tools/SpeechRecognition/hooks/useSpeechLanguageInfo.ts +108 -0
  140. package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +188 -0
  141. package/src/tools/SpeechRecognition/hooks/useVoiceSupport.ts +78 -0
  142. package/src/tools/SpeechRecognition/index.ts +82 -0
  143. package/src/tools/SpeechRecognition/lazy.tsx +19 -0
  144. package/src/tools/SpeechRecognition/store/index.ts +2 -0
  145. package/src/tools/SpeechRecognition/store/prefsStore.ts +54 -0
  146. package/src/tools/SpeechRecognition/types.ts +133 -0
  147. package/src/tools/SpeechRecognition/widgets/DictationField.tsx +105 -0
  148. package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +305 -0
  149. package/src/tools/SpeechRecognition/widgets/VoiceMessageRecorder.tsx +88 -0
  150. package/src/tools/SpeechRecognition/widgets/index.ts +6 -0
  151. package/dist/ChatRoot-EJC5Y2YM.cjs +0 -14
  152. package/dist/ChatRoot-QOSKJPM6.mjs +0 -5
  153. package/dist/chunk-NWUT327A.mjs.map +0 -1
  154. package/dist/chunk-QLMKCSR6.mjs +0 -2420
  155. package/dist/chunk-QLMKCSR6.mjs.map +0 -1
  156. package/dist/chunk-SI5RD2GD.cjs +0 -2460
  157. package/dist/chunk-SI5RD2GD.cjs.map +0 -1
  158. package/dist/chunk-XACCHZH2.cjs.map +0 -1
  159. package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +0 -771
  160. package/src/stories/index.ts +0 -33
  161. package/src/tools/AudioPlayer/AudioPlayer.story.tsx +0 -481
  162. package/src/tools/Chat/Chat.story.tsx +0 -1457
  163. package/src/tools/CodeEditor/CodeEditor.story.tsx +0 -202
  164. package/src/tools/CronScheduler/CronScheduler.story.tsx +0 -300
  165. package/src/tools/Gallery/Gallery.story.tsx +0 -237
  166. package/src/tools/ImageViewer/ImageViewer.story.tsx +0 -85
  167. package/src/tools/JsonForm/JsonForm.story.tsx +0 -350
  168. package/src/tools/JsonTree/JsonTree.story.tsx +0 -141
  169. package/src/tools/LottiePlayer/LottiePlayer.story.tsx +0 -95
  170. package/src/tools/Map/Map.story.tsx +0 -458
  171. package/src/tools/MarkdownEditor/MarkdownEditor.story.tsx +0 -225
  172. package/src/tools/Mermaid/Mermaid.story.tsx +0 -251
  173. package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +0 -230
  174. package/src/tools/PrettyCode/PrettyCode.story.tsx +0 -304
  175. package/src/tools/Tour/Tour.story.tsx +0 -279
  176. package/src/tools/Tree/Tree.story.tsx +0 -620
  177. package/src/tools/Uploader/Uploader.story.tsx +0 -415
  178. package/src/tools/VideoPlayer/VideoPlayer.story.tsx +0 -87
@@ -1,620 +0,0 @@
1
- import { useMemo, useState } from 'react';
2
- import {
3
- AlertCircle,
4
- Braces,
5
- Code2,
6
- Copy,
7
- Eye,
8
- FileText,
9
- Folder,
10
- FolderOpen,
11
- Image as ImageIcon,
12
- Pencil,
13
- RefreshCw,
14
- Settings,
15
- Trash,
16
- } from 'lucide-react';
17
- import { defineStory, useBoolean, useSelect } from '@djangocfg/playground';
18
- import {
19
- ContextMenu,
20
- ContextMenuContent,
21
- ContextMenuItem,
22
- ContextMenuSeparator,
23
- ContextMenuShortcut,
24
- ContextMenuTrigger,
25
- } from '@djangocfg/ui-core/components';
26
-
27
- import { TreeRoot } from './TreeRoot';
28
- import { TreeProvider } from './context/TreeContext';
29
- import { TreeContent } from './components/TreeContent';
30
- import { useTreeActions } from './context/hooks';
31
- import { createDemoTree, type DemoNode } from './data/createDemoTree';
32
- import type { TreeItemId, TreeNode } from './types';
33
-
34
- export default defineStory({
35
- title: 'Tools/Tree',
36
- component: TreeRoot,
37
- description: 'Decomposed shadcn-style tree (own engine, no external libs).',
38
- });
39
-
40
- // ---------------------------------------------------------------------------
41
- // Sample data
42
- // ---------------------------------------------------------------------------
43
-
44
- interface FsNode {
45
- name: string;
46
- ext?: string;
47
- status?: 'modified' | 'error' | 'disabled';
48
- }
49
-
50
- const fs: TreeNode<FsNode>[] = [
51
- {
52
- id: 'src',
53
- data: { name: 'src' },
54
- children: [
55
- {
56
- id: 'components',
57
- data: { name: 'components' },
58
- children: [
59
- { id: 'Button.tsx', data: { name: 'Button.tsx', ext: 'tsx' } },
60
- {
61
- id: 'Card.tsx',
62
- data: { name: 'Card.tsx', ext: 'tsx', status: 'modified' },
63
- },
64
- { id: 'Tree.tsx', data: { name: 'Tree.tsx', ext: 'tsx' } },
65
- ],
66
- },
67
- {
68
- id: 'hooks',
69
- data: { name: 'hooks' },
70
- children: [
71
- { id: 'useDebounce.ts', data: { name: 'useDebounce.ts', ext: 'ts' } },
72
- {
73
- id: 'useTheme.ts',
74
- data: { name: 'useTheme.ts', ext: 'ts', status: 'error' },
75
- },
76
- ],
77
- },
78
- { id: 'index.ts', data: { name: 'index.ts', ext: 'ts' } },
79
- {
80
- id: '_old.ts',
81
- data: { name: '_old.ts', ext: 'ts', status: 'disabled' },
82
- disabled: true,
83
- },
84
- ],
85
- },
86
- {
87
- id: 'public',
88
- data: { name: 'public' },
89
- children: [
90
- { id: 'favicon.ico', data: { name: 'favicon.ico', ext: 'ico' } },
91
- { id: 'logo.svg', data: { name: 'logo.svg', ext: 'svg' } },
92
- ],
93
- },
94
- { id: 'package.json', data: { name: 'package.json', ext: 'json' } },
95
- { id: 'README.md', data: { name: 'README.md', ext: 'md' } },
96
- ];
97
-
98
- const getName = (n: TreeNode<FsNode | DemoNode>) => n.data.name;
99
-
100
- // ---------------------------------------------------------------------------
101
- // 1) Default — sensible cozy defaults
102
- // ---------------------------------------------------------------------------
103
-
104
- export const Default = () => (
105
- <div className="h-96 w-80 rounded-md border border-border bg-card">
106
- <TreeRoot<FsNode>
107
- data={fs}
108
- getItemName={getName}
109
- initialExpandedIds={['src']}
110
- />
111
- </div>
112
- );
113
-
114
- // ---------------------------------------------------------------------------
115
- // 1.5) WithActivationModes — VSCode-style click semantics
116
- // ---------------------------------------------------------------------------
117
-
118
- export const WithActivationModes = () => {
119
- const [log, setLog] = useState<string[]>([]);
120
- const append = (s: string) =>
121
- setLog((prev) => [s, ...prev].slice(0, 8));
122
-
123
- return (
124
- <div className="flex flex-col gap-3">
125
- <div className="grid grid-cols-3 gap-3">
126
- {(['single-click', 'double-click', 'single-click-preview'] as const).map(
127
- (mode) => (
128
- <div key={mode} className="flex flex-col gap-2">
129
- <span className="text-xs font-medium text-muted-foreground">
130
- {mode}
131
- </span>
132
- <div className="h-72 w-64 rounded-md border border-border bg-card">
133
- <TreeRoot<FsNode>
134
- data={fs}
135
- getItemName={getName}
136
- initialExpandedIds={['src']}
137
- activationMode={mode}
138
- onActivate={(node, { preview }) =>
139
- append(
140
- `[${mode}] ${node.data.name}${preview ? ' (preview)' : ''}`,
141
- )
142
- }
143
- />
144
- </div>
145
- </div>
146
- ),
147
- )}
148
- </div>
149
- <div className="rounded-md border border-border bg-muted/30 p-3 font-mono text-xs">
150
- {log.length === 0 ? (
151
- <span className="text-muted-foreground">
152
- click / double-click a file to log activations
153
- </span>
154
- ) : (
155
- log.map((line, i) => <div key={i}>{line}</div>)
156
- )}
157
- </div>
158
- </div>
159
- );
160
- };
161
-
162
- // ---------------------------------------------------------------------------
163
- // 1.7) WithHiddenFilter — filterNode prop hides dot-prefixed entries
164
- // ---------------------------------------------------------------------------
165
-
166
- const fsWithDotfiles: TreeNode<FsNode>[] = [
167
- ...fs,
168
- { id: '.env', data: { name: '.env' } },
169
- { id: '.gitignore', data: { name: '.gitignore' } },
170
- {
171
- id: '.git',
172
- data: { name: '.git' },
173
- isFolder: true,
174
- children: [
175
- { id: '.git/HEAD', data: { name: 'HEAD' } },
176
- { id: '.git/config', data: { name: 'config' } },
177
- ],
178
- },
179
- ];
180
-
181
- export const WithHiddenFilter = () => {
182
- const [showHidden] = useBoolean('showHidden', {
183
- defaultValue: false,
184
- label: 'Show hidden',
185
- });
186
- return (
187
- <div className="h-96 w-80 rounded-md border border-border bg-card">
188
- <TreeRoot<FsNode>
189
- data={fsWithDotfiles}
190
- getItemName={getName}
191
- initialExpandedIds={['src']}
192
- filterNode={(n) => showHidden || !n.data.name.startsWith('.')}
193
- />
194
- </div>
195
- );
196
- };
197
-
198
- // ---------------------------------------------------------------------------
199
- // 2) Densities — three presets side-by-side for comparison
200
- // ---------------------------------------------------------------------------
201
-
202
- export const Densities = () => (
203
- <div className="grid grid-cols-3 gap-3">
204
- {(['compact', 'cozy', 'comfortable'] as const).map((density) => (
205
- <div key={density} className="flex flex-col gap-2">
206
- <span className="text-xs font-medium text-muted-foreground capitalize">
207
- {density}
208
- </span>
209
- <div className="h-80 w-64 rounded-md border border-border bg-card">
210
- <TreeRoot<FsNode>
211
- data={fs}
212
- getItemName={getName}
213
- initialExpandedIds={['src', 'components']}
214
- appearance={{ density }}
215
- />
216
- </div>
217
- </div>
218
- ))}
219
- </div>
220
- );
221
-
222
- // ---------------------------------------------------------------------------
223
- // 3) WithIcons — file-type icons via renderIcon slot
224
- // ---------------------------------------------------------------------------
225
-
226
- const FileIcon = ({ ext }: { ext?: string }) => {
227
- const props = {
228
- 'aria-hidden': true,
229
- style: { width: 'var(--tree-icon-size)', height: 'var(--tree-icon-size)' },
230
- strokeWidth: 1.5 as const,
231
- className: 'shrink-0',
232
- };
233
- if (ext === 'tsx' || ext === 'ts')
234
- return <Code2 {...props} className={`${props.className} text-blue-400`} />;
235
- if (ext === 'json')
236
- return <Braces {...props} className={`${props.className} text-amber-400`} />;
237
- if (ext === 'svg' || ext === 'ico')
238
- return <ImageIcon {...props} className={`${props.className} text-pink-400`} />;
239
- if (ext === 'md')
240
- return <FileText {...props} className={`${props.className} text-sky-400`} />;
241
- return <FileText {...props} className={`${props.className} text-muted-foreground/80`} />;
242
- };
243
-
244
- const FolderTinted = ({ isExpanded }: { isExpanded: boolean }) => {
245
- const Icon = isExpanded ? FolderOpen : Folder;
246
- return (
247
- <Icon
248
- aria-hidden
249
- strokeWidth={1.5}
250
- style={{
251
- width: 'var(--tree-icon-size)',
252
- height: 'var(--tree-icon-size)',
253
- }}
254
- className="shrink-0 text-amber-300/90"
255
- />
256
- );
257
- };
258
-
259
- export const WithIcons = () => (
260
- <div className="h-96 w-80 rounded-md border border-border bg-card">
261
- <TreeRoot<FsNode>
262
- data={fs}
263
- getItemName={getName}
264
- initialExpandedIds={['src', 'components', 'hooks', 'public']}
265
- renderIcon={({ node, isFolder, isExpanded }) =>
266
- isFolder ? (
267
- <FolderTinted isExpanded={isExpanded} />
268
- ) : (
269
- <FileIcon ext={node.data.ext} />
270
- )
271
- }
272
- />
273
- </div>
274
- );
275
-
276
- // ---------------------------------------------------------------------------
277
- // 4) WithStatus — modified / error / disabled rows via renderLabel
278
- // ---------------------------------------------------------------------------
279
-
280
- export const WithStatus = () => (
281
- <div className="h-96 w-80 rounded-md border border-border bg-card">
282
- <TreeRoot<FsNode>
283
- data={fs}
284
- getItemName={getName}
285
- initialExpandedIds={['src', 'components', 'hooks']}
286
- renderLabel={({ node }) => {
287
- const { name, status } = node.data;
288
- if (status === 'modified')
289
- return (
290
- <span className="flex min-w-0 items-center gap-1.5">
291
- <span className="truncate text-amber-400" style={{ fontSize: 'var(--tree-font-size)' }}>
292
- {name}
293
- </span>
294
- <span className="text-[10px] font-medium text-amber-400/80">M</span>
295
- </span>
296
- );
297
- if (status === 'error')
298
- return (
299
- <span className="flex min-w-0 items-center gap-1.5">
300
- <AlertCircle aria-hidden strokeWidth={2} className="size-3 shrink-0 text-destructive" />
301
- <span className="truncate text-destructive" style={{ fontSize: 'var(--tree-font-size)' }}>
302
- {name}
303
- </span>
304
- </span>
305
- );
306
- return (
307
- <span
308
- className="truncate"
309
- style={{ fontSize: 'var(--tree-font-size)' }}
310
- >
311
- {name}
312
- </span>
313
- );
314
- }}
315
- />
316
- </div>
317
- );
318
-
319
- // ---------------------------------------------------------------------------
320
- // 5) WithActions — rename / delete buttons appear on hover
321
- // ---------------------------------------------------------------------------
322
-
323
- export const WithActions = () => {
324
- const [last, setLast] = useState<string>('');
325
- return (
326
- <div className="flex h-96 w-80 flex-col gap-2">
327
- <div className="text-xs text-muted-foreground">Last: {last || '—'}</div>
328
- <div className="flex-1 rounded-md border border-border bg-card">
329
- <TreeRoot<FsNode>
330
- data={fs}
331
- getItemName={getName}
332
- initialExpandedIds={['src']}
333
- renderActions={({ node }) => (
334
- <>
335
- <button
336
- type="button"
337
- aria-label={`Rename ${node.data.name}`}
338
- onClick={() => setLast(`Rename ${node.id}`)}
339
- className="rounded p-0.5 hover:bg-foreground/10"
340
- >
341
- <Pencil aria-hidden className="size-3 text-muted-foreground" />
342
- </button>
343
- <button
344
- type="button"
345
- aria-label={`Delete ${node.data.name}`}
346
- onClick={() => setLast(`Delete ${node.id}`)}
347
- className="rounded p-0.5 hover:bg-destructive/15 hover:text-destructive"
348
- >
349
- <Trash aria-hidden className="size-3 text-muted-foreground" />
350
- </button>
351
- </>
352
- )}
353
- />
354
- </div>
355
- </div>
356
- );
357
- };
358
-
359
- // ---------------------------------------------------------------------------
360
- // 6) WithSearch — built-in search bar + match highlight
361
- // ---------------------------------------------------------------------------
362
-
363
- export const WithSearch = () => (
364
- <div className="h-96 w-80 rounded-md border border-border bg-card">
365
- <TreeRoot<FsNode>
366
- data={fs}
367
- getItemName={getName}
368
- initialExpandedIds={['src', 'components', 'hooks', 'public']}
369
- enableSearch
370
- />
371
- </div>
372
- );
373
-
374
- // ---------------------------------------------------------------------------
375
- // 7) WithIndentGuides
376
- // ---------------------------------------------------------------------------
377
-
378
- export const WithIndentGuides = () => (
379
- <div className="h-96 w-80 rounded-md border border-border bg-card">
380
- <TreeRoot<FsNode>
381
- data={fs}
382
- getItemName={getName}
383
- initialExpandedIds={['src', 'components']}
384
- showIndentGuides
385
- />
386
- </div>
387
- );
388
-
389
- // ---------------------------------------------------------------------------
390
- // 8) WithContextMenu — right-click via renderContextMenu slot
391
- // ---------------------------------------------------------------------------
392
-
393
- export const WithContextMenu = () => {
394
- const [last, setLast] = useState<string>('');
395
- return (
396
- <div className="flex h-96 w-80 flex-col gap-2">
397
- <div className="text-xs text-muted-foreground">Last: {last || '—'}</div>
398
- <div className="flex-1 rounded-md border border-border bg-card">
399
- <TreeRoot<FsNode>
400
- data={fs}
401
- getItemName={getName}
402
- initialExpandedIds={['src']}
403
- renderContextMenu={({ node }, trigger) => (
404
- <ContextMenu>
405
- <ContextMenuTrigger asChild>{trigger}</ContextMenuTrigger>
406
- <ContextMenuContent className="w-52">
407
- <ContextMenuItem onClick={() => setLast(`Reveal ${node.id}`)}>
408
- <Eye className="mr-2 size-3.5" /> Reveal
409
- </ContextMenuItem>
410
- <ContextMenuItem onClick={() => setLast(`Copy ${node.id}`)}>
411
- <Copy className="mr-2 size-3.5" /> Copy path
412
- <ContextMenuShortcut>⌘C</ContextMenuShortcut>
413
- </ContextMenuItem>
414
- <ContextMenuSeparator />
415
- <ContextMenuItem onClick={() => setLast(`Refresh ${node.id}`)}>
416
- <RefreshCw className="mr-2 size-3.5" /> Refresh
417
- </ContextMenuItem>
418
- <ContextMenuItem
419
- variant="destructive"
420
- onClick={() => setLast(`Delete ${node.id}`)}
421
- >
422
- <Trash className="mr-2 size-3.5" /> Delete
423
- </ContextMenuItem>
424
- </ContextMenuContent>
425
- </ContextMenu>
426
- )}
427
- />
428
- </div>
429
- </div>
430
- );
431
- };
432
-
433
- // ---------------------------------------------------------------------------
434
- // 9) AsyncLazyChildren — load children on expand
435
- // ---------------------------------------------------------------------------
436
-
437
- interface RemoteNode {
438
- name: string;
439
- }
440
- const remote: Record<string, { name: string; children?: string[] }> = {
441
- 'a-root': { name: 'remote', children: ['a/1', 'a/2', 'a/3'] },
442
- 'a/1': { name: 'docs', children: ['a/1/x'] },
443
- 'a/2': { name: 'images', children: ['a/2/x', 'a/2/y'] },
444
- 'a/3': { name: 'manifest.yml' },
445
- 'a/1/x': { name: 'intro.md' },
446
- 'a/2/x': { name: 'logo.png' },
447
- 'a/2/y': { name: 'cover.jpg' },
448
- };
449
- const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
450
-
451
- export const AsyncLazyChildren = () => {
452
- const data = useMemo<TreeNode<RemoteNode>[]>(
453
- () => [{ id: 'a-root', data: { name: 'remote' }, isFolder: true }],
454
- [],
455
- );
456
-
457
- const loadChildren = async (node: TreeNode<RemoteNode>) => {
458
- await sleep(350);
459
- const meta = remote[node.id];
460
- if (!meta?.children) return [];
461
- return meta.children.map<TreeNode<RemoteNode>>((id) => ({
462
- id,
463
- data: { name: remote[id].name },
464
- isFolder: !!remote[id].children,
465
- }));
466
- };
467
-
468
- return (
469
- <div className="h-96 w-80 rounded-md border border-border bg-card">
470
- <TreeRoot<RemoteNode>
471
- data={data}
472
- getItemName={(n) => n.data.name}
473
- loadChildren={loadChildren}
474
- initialExpandedIds={['a-root']}
475
- />
476
- </div>
477
- );
478
- };
479
-
480
- // ---------------------------------------------------------------------------
481
- // 10) ExpandCollapseAll — composition mode + toolbar
482
- // ---------------------------------------------------------------------------
483
-
484
- export const ExpandCollapseAll = () => {
485
- const data = useMemo(() => createDemoTree({ depth: 4, breadth: 3 }), []);
486
- return (
487
- <div className="flex h-96 w-80 flex-col gap-2 rounded-md border border-border bg-card p-2">
488
- <TreeProvider<DemoNode> data={data} getItemName={getName}>
489
- <Toolbar />
490
- <div className="min-h-0 flex-1 overflow-auto">
491
- <TreeContent<DemoNode> />
492
- </div>
493
- </TreeProvider>
494
- </div>
495
- );
496
- };
497
-
498
- function Toolbar() {
499
- const { expandAll, collapseAll } = useTreeActions();
500
- return (
501
- <div className="flex gap-2 text-xs">
502
- <button
503
- type="button"
504
- onClick={() => expandAll()}
505
- className="rounded border border-border px-2 py-1 hover:bg-accent"
506
- >
507
- Expand all
508
- </button>
509
- <button
510
- type="button"
511
- onClick={() => collapseAll()}
512
- className="rounded border border-border px-2 py-1 hover:bg-accent"
513
- >
514
- Collapse all
515
- </button>
516
- </div>
517
- );
518
- }
519
-
520
- // ---------------------------------------------------------------------------
521
- // 11) Persisted — localStorage
522
- // ---------------------------------------------------------------------------
523
-
524
- export const Persisted = () => (
525
- <div className="flex h-96 w-80 flex-col gap-2">
526
- <p className="text-xs text-muted-foreground">
527
- Toggle folders, refresh — state restores from localStorage.
528
- </p>
529
- <div className="flex-1 rounded-md border border-border bg-card">
530
- <TreeRoot<FsNode>
531
- data={fs}
532
- getItemName={getName}
533
- persistKey="story.tree.fs"
534
- persistSelection
535
- />
536
- </div>
537
- </div>
538
- );
539
-
540
- // ---------------------------------------------------------------------------
541
- // 12) LargeTree — scalability sanity check (~500 nodes)
542
- // ---------------------------------------------------------------------------
543
-
544
- export const LargeTree = () => {
545
- const data = useMemo(() => createDemoTree({ depth: 4, breadth: 5 }), []);
546
- return (
547
- <div className="h-[28rem] w-96 rounded-md border border-border bg-card">
548
- <TreeRoot<DemoNode>
549
- data={data}
550
- getItemName={getName}
551
- appearance={{ density: 'compact' }}
552
- showIndentGuides
553
- enableSearch
554
- />
555
- </div>
556
- );
557
- };
558
-
559
- // ---------------------------------------------------------------------------
560
- // 13) Playground — every knob at once
561
- // ---------------------------------------------------------------------------
562
-
563
- export const Playground = () => {
564
- const [selectionMode] = useSelect('selectionMode', {
565
- options: ['none', 'single', 'multiple'] as const,
566
- defaultValue: 'single',
567
- label: 'Selection mode',
568
- });
569
- const [density] = useSelect('density', {
570
- options: ['compact', 'cozy', 'comfortable'] as const,
571
- defaultValue: 'cozy',
572
- label: 'Density',
573
- });
574
- const [accent] = useSelect('accent', {
575
- options: ['subtle', 'default', 'strong'] as const,
576
- defaultValue: 'default',
577
- label: 'Accent',
578
- });
579
- const [radius] = useSelect('radius', {
580
- options: ['none', 'sm', 'md'] as const,
581
- defaultValue: 'sm',
582
- label: 'Radius',
583
- });
584
- const [showSearch] = useBoolean('search', { defaultValue: true, label: 'Search' });
585
- const [typeAhead] = useBoolean('typeAhead', {
586
- defaultValue: true,
587
- label: 'Type-ahead',
588
- });
589
- const [indentGuides] = useBoolean('indentGuides', {
590
- defaultValue: true,
591
- label: 'Indent guides',
592
- });
593
- const [activeIndicator] = useBoolean('activeIndicator', {
594
- defaultValue: true,
595
- label: 'Active row indicator',
596
- });
597
- const [selected, setSelected] = useState<TreeItemId[]>([]);
598
-
599
- return (
600
- <div className="flex h-[28rem] w-[28rem] flex-col gap-2">
601
- <div className="flex items-center gap-2 text-xs text-muted-foreground">
602
- <Settings aria-hidden className="size-3" />
603
- Selected: {selected.length === 0 ? '(none)' : selected.join(', ')}
604
- </div>
605
- <div className="flex-1 rounded-md border border-border bg-card">
606
- <TreeRoot<FsNode>
607
- data={fs}
608
- getItemName={getName}
609
- initialExpandedIds={['src', 'components', 'hooks', 'public']}
610
- selectionMode={selectionMode}
611
- appearance={{ density, accent, radius, showActiveIndicator: activeIndicator }}
612
- enableSearch={showSearch}
613
- enableTypeAhead={typeAhead}
614
- showIndentGuides={indentGuides}
615
- onSelectionChange={setSelected}
616
- />
617
- </div>
618
- </div>
619
- );
620
- };