@hanzo/rooms 0.1.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 (235) hide show
  1. package/LICENSE-APACHE +205 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +23 -0
  4. package/dist/Account.d.ts +8 -0
  5. package/dist/Account.js +58 -0
  6. package/dist/Beluga.d.ts +12 -0
  7. package/dist/Beluga.js +152 -0
  8. package/dist/Billing.d.ts +1 -0
  9. package/dist/Billing.js +31 -0
  10. package/dist/Board.d.ts +9 -0
  11. package/dist/Board.js +447 -0
  12. package/dist/Boxes.d.ts +1 -0
  13. package/dist/Boxes.js +173 -0
  14. package/dist/CalScheduler.d.ts +15 -0
  15. package/dist/CalScheduler.js +196 -0
  16. package/dist/Chat.d.ts +35 -0
  17. package/dist/Chat.js +1049 -0
  18. package/dist/Column.d.ts +15 -0
  19. package/dist/Column.js +88 -0
  20. package/dist/Contacts.d.ts +1 -0
  21. package/dist/Contacts.js +301 -0
  22. package/dist/Directory.d.ts +24 -0
  23. package/dist/Directory.js +356 -0
  24. package/dist/Drive.d.ts +2 -0
  25. package/dist/Drive.js +466 -0
  26. package/dist/Find.d.ts +7 -0
  27. package/dist/Find.js +82 -0
  28. package/dist/Guide.d.ts +6 -0
  29. package/dist/Guide.js +272 -0
  30. package/dist/Home.d.ts +39 -0
  31. package/dist/Home.js +306 -0
  32. package/dist/Inbox.d.ts +9 -0
  33. package/dist/Inbox.js +193 -0
  34. package/dist/Market.d.ts +3 -0
  35. package/dist/Market.js +149 -0
  36. package/dist/Me.d.ts +27 -0
  37. package/dist/Me.js +251 -0
  38. package/dist/Meet.d.ts +9 -0
  39. package/dist/Meet.js +370 -0
  40. package/dist/ModeMark.d.ts +8 -0
  41. package/dist/ModeMark.js +18 -0
  42. package/dist/OrgSwitcher.d.ts +31 -0
  43. package/dist/OrgSwitcher.js +120 -0
  44. package/dist/Orgs.d.ts +10 -0
  45. package/dist/Orgs.js +582 -0
  46. package/dist/Profile.d.ts +4 -0
  47. package/dist/Profile.js +67 -0
  48. package/dist/Prose.d.ts +5 -0
  49. package/dist/Prose.js +111 -0
  50. package/dist/ProviderMark.d.ts +17 -0
  51. package/dist/ProviderMark.js +277 -0
  52. package/dist/Recents.d.ts +10 -0
  53. package/dist/Recents.js +91 -0
  54. package/dist/RightPane.d.ts +13 -0
  55. package/dist/RightPane.js +426 -0
  56. package/dist/Room.d.ts +8 -0
  57. package/dist/Room.js +50 -0
  58. package/dist/Scene.d.ts +12 -0
  59. package/dist/Scene.js +105 -0
  60. package/dist/Settings.d.ts +46 -0
  61. package/dist/Settings.js +321 -0
  62. package/dist/Setup.d.ts +1 -0
  63. package/dist/Setup.js +36 -0
  64. package/dist/Share.d.ts +3 -0
  65. package/dist/Share.js +130 -0
  66. package/dist/Shared.d.ts +1 -0
  67. package/dist/Shared.js +113 -0
  68. package/dist/Shell.d.ts +63 -0
  69. package/dist/Shell.js +1225 -0
  70. package/dist/Start.d.ts +1 -0
  71. package/dist/Start.js +96 -0
  72. package/dist/Support.d.ts +14 -0
  73. package/dist/Support.js +122 -0
  74. package/dist/Talk.d.ts +4 -0
  75. package/dist/Talk.js +107 -0
  76. package/dist/Upgrade.d.ts +11 -0
  77. package/dist/Upgrade.js +75 -0
  78. package/dist/Usage.d.ts +1 -0
  79. package/dist/Usage.js +21 -0
  80. package/dist/Vibe.d.ts +2 -0
  81. package/dist/Vibe.js +188 -0
  82. package/dist/Work.d.ts +12 -0
  83. package/dist/Work.js +256 -0
  84. package/dist/bots/Bots.d.ts +2 -0
  85. package/dist/bots/Bots.js +456 -0
  86. package/dist/bots/Lights.d.ts +8 -0
  87. package/dist/bots/Lights.js +13 -0
  88. package/dist/bots/api.d.ts +120 -0
  89. package/dist/bots/api.js +75 -0
  90. package/dist/bots/face.d.ts +5 -0
  91. package/dist/bots/face.js +13 -0
  92. package/dist/bots/ink.d.ts +7 -0
  93. package/dist/bots/ink.js +29 -0
  94. package/dist/bots/kit.d.ts +113 -0
  95. package/dist/bots/kit.js +43 -0
  96. package/dist/bots/runtime.d.ts +4 -0
  97. package/dist/bots/runtime.js +326 -0
  98. package/dist/card.d.ts +52 -0
  99. package/dist/card.js +52 -0
  100. package/dist/cast.d.ts +143 -0
  101. package/dist/cast.js +251 -0
  102. package/dist/connect.d.ts +29 -0
  103. package/dist/connect.js +64 -0
  104. package/dist/contacts-store.d.ts +111 -0
  105. package/dist/contacts-store.js +276 -0
  106. package/dist/conversations.d.ts +54 -0
  107. package/dist/conversations.js +98 -0
  108. package/dist/copy.d.ts +23 -0
  109. package/dist/copy.js +39 -0
  110. package/dist/crew.d.ts +36 -0
  111. package/dist/crew.js +304 -0
  112. package/dist/dev.d.ts +3 -0
  113. package/dist/dev.js +102 -0
  114. package/dist/enso.d.ts +70 -0
  115. package/dist/enso.js +112 -0
  116. package/dist/failure.d.ts +57 -0
  117. package/dist/failure.js +143 -0
  118. package/dist/ground.d.ts +32 -0
  119. package/dist/ground.js +32 -0
  120. package/dist/host.d.ts +76 -0
  121. package/dist/host.js +76 -0
  122. package/dist/index.d.ts +14 -0
  123. package/dist/index.js +12 -0
  124. package/dist/kept.d.ts +5 -0
  125. package/dist/kept.js +27 -0
  126. package/dist/lib/account.d.ts +49 -0
  127. package/dist/lib/account.js +229 -0
  128. package/dist/lib/ai.d.ts +46 -0
  129. package/dist/lib/ai.js +140 -0
  130. package/dist/lib/api.d.ts +43 -0
  131. package/dist/lib/api.js +62 -0
  132. package/dist/lib/bare.d.ts +9 -0
  133. package/dist/lib/bare.js +7 -0
  134. package/dist/lib/coding.d.ts +105 -0
  135. package/dist/lib/coding.js +87 -0
  136. package/dist/lib/destination.d.ts +102 -0
  137. package/dist/lib/destination.js +260 -0
  138. package/dist/lib/durable.d.ts +66 -0
  139. package/dist/lib/durable.js +107 -0
  140. package/dist/lib/first.d.ts +11 -0
  141. package/dist/lib/first.js +23 -0
  142. package/dist/lib/home.d.ts +61 -0
  143. package/dist/lib/home.js +54 -0
  144. package/dist/lib/host.d.ts +104 -0
  145. package/dist/lib/host.js +132 -0
  146. package/dist/lib/hostname.d.ts +1 -0
  147. package/dist/lib/hostname.js +14 -0
  148. package/dist/lib/hydrated.d.ts +1 -0
  149. package/dist/lib/hydrated.js +20 -0
  150. package/dist/lib/integrations.d.ts +21 -0
  151. package/dist/lib/integrations.js +1 -0
  152. package/dist/lib/intent.d.ts +6 -0
  153. package/dist/lib/intent.js +71 -0
  154. package/dist/lib/invite.d.ts +155 -0
  155. package/dist/lib/invite.js +245 -0
  156. package/dist/lib/limits.d.ts +55 -0
  157. package/dist/lib/limits.js +119 -0
  158. package/dist/lib/list.d.ts +9 -0
  159. package/dist/lib/list.js +11 -0
  160. package/dist/lib/mark.d.ts +13 -0
  161. package/dist/lib/mark.js +85 -0
  162. package/dist/lib/mix.d.ts +22 -0
  163. package/dist/lib/mix.js +22 -0
  164. package/dist/lib/models.d.ts +27 -0
  165. package/dist/lib/models.js +106 -0
  166. package/dist/lib/org.d.ts +109 -0
  167. package/dist/lib/org.js +140 -0
  168. package/dist/lib/path.d.ts +9 -0
  169. package/dist/lib/path.js +29 -0
  170. package/dist/lib/plans.d.ts +193 -0
  171. package/dist/lib/plans.js +310 -0
  172. package/dist/lib/prose.d.ts +13 -0
  173. package/dist/lib/prose.js +62 -0
  174. package/dist/lib/reach.d.ts +17 -0
  175. package/dist/lib/reach.js +70 -0
  176. package/dist/lib/rooms.json +23 -0
  177. package/dist/lib/served.d.ts +20 -0
  178. package/dist/lib/served.js +92 -0
  179. package/dist/lib/session.d.ts +211 -0
  180. package/dist/lib/session.js +493 -0
  181. package/dist/lib/share.d.ts +131 -0
  182. package/dist/lib/share.js +216 -0
  183. package/dist/lib/signout.d.ts +14 -0
  184. package/dist/lib/signout.js +40 -0
  185. package/dist/lib/tags.d.ts +27 -0
  186. package/dist/lib/tags.js +46 -0
  187. package/dist/lib/tap.d.ts +17 -0
  188. package/dist/lib/tap.js +9 -0
  189. package/dist/lib/tier.d.ts +115 -0
  190. package/dist/lib/tier.js +207 -0
  191. package/dist/lib/tiers.d.ts +9 -0
  192. package/dist/lib/tiers.js +141 -0
  193. package/dist/lib/todo.d.ts +125 -0
  194. package/dist/lib/todo.js +379 -0
  195. package/dist/look.d.ts +1 -0
  196. package/dist/look.js +21 -0
  197. package/dist/meters.d.ts +8 -0
  198. package/dist/meters.js +31 -0
  199. package/dist/model.d.ts +4 -0
  200. package/dist/model.js +44 -0
  201. package/dist/network.d.ts +64 -0
  202. package/dist/network.js +216 -0
  203. package/dist/open.d.ts +81 -0
  204. package/dist/open.js +149 -0
  205. package/dist/pane.d.ts +92 -0
  206. package/dist/pane.js +231 -0
  207. package/dist/progress.d.ts +46 -0
  208. package/dist/progress.js +51 -0
  209. package/dist/recent.d.ts +67 -0
  210. package/dist/recent.js +161 -0
  211. package/dist/rooms.d.ts +131 -0
  212. package/dist/rooms.js +195 -0
  213. package/dist/roster.d.ts +28 -0
  214. package/dist/roster.js +70 -0
  215. package/dist/span.d.ts +14 -0
  216. package/dist/span.js +50 -0
  217. package/dist/speech.d.ts +44 -0
  218. package/dist/speech.js +229 -0
  219. package/dist/stars.d.ts +16 -0
  220. package/dist/stars.js +75 -0
  221. package/dist/tabs.d.ts +26 -0
  222. package/dist/tabs.js +25 -0
  223. package/dist/team.d.ts +11 -0
  224. package/dist/team.gen.d.ts +10 -0
  225. package/dist/team.gen.js +11 -0
  226. package/dist/team.js +30 -0
  227. package/dist/tools.d.ts +35 -0
  228. package/dist/tools.js +143 -0
  229. package/dist/tours.d.ts +24 -0
  230. package/dist/tours.js +311 -0
  231. package/dist/waiting.d.ts +15 -0
  232. package/dist/waiting.js +17 -0
  233. package/dist/where.d.ts +54 -0
  234. package/dist/where.js +14 -0
  235. package/package.json +108 -0
package/dist/Drive.js ADDED
@@ -0,0 +1,466 @@
1
+ 'use client';
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { ArrowRight, ChevronDown, ChevronRight, Clock, Cloud, Download, File as FileIcon, FileArchive, FileAudio, FileCode, FileImage, FileText, FileVideo, Folder, FolderInput, FolderPlus, HardDrive, LayoutGrid, List, MoreHorizontal, PanelLeft, Plus, Search, Trash2, Upload, } from 'lucide-react';
5
+ import { Box, Button, Text, View, XStack, YStack } from '@hanzo/ui';
6
+ import { useIam, useOrganizations } from '@hanzo/iam/react';
7
+ import { useAi } from './lib/ai.js';
8
+ import { useOpen } from './open.js';
9
+ import { checkoutUrl } from './lib/plans.js';
10
+ import { BAD, mix } from './lib/mix.js';
11
+ const blame = (e) => {
12
+ const text = e instanceof Error ? e.message : String(e ?? '');
13
+ return text || 'The store did not say what went wrong.';
14
+ };
15
+ /** Format byte count nicely */
16
+ const weigh = (bytes) => {
17
+ if (!bytes)
18
+ return '—';
19
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
20
+ const step = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
21
+ const size = bytes / 1024 ** step;
22
+ return `${size < 10 && step > 0 ? size.toFixed(1) : Math.round(size)} ${units[step]}`;
23
+ };
24
+ /** Format timestamp */
25
+ const when = (stamp) => {
26
+ if (!stamp)
27
+ return '';
28
+ const ms = stamp < 1e12 ? stamp * 1000 : stamp;
29
+ const d = new Date(ms);
30
+ const now = new Date();
31
+ const sameDay = d.toDateString() === now.toDateString();
32
+ if (sameDay)
33
+ return d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
34
+ const sameYear = d.getFullYear() === now.getFullYear();
35
+ return d.toLocaleDateString([], sameYear ? { month: 'short', day: 'numeric' } : { year: 'numeric', month: 'short', day: 'numeric' });
36
+ };
37
+ /** The store publishes a bucket's own display name; the slug is the fallback. */
38
+ const getBucketDisplayName = (b) => (typeof b === 'string' ? b : b?.displayName || b?.name) || 'Drive';
39
+ const bucketName = (workspace) => workspace
40
+ .toLowerCase()
41
+ .replace(/[^a-z0-9]+/g, '-')
42
+ .replace(/^-+|-+$/g, '')
43
+ .slice(0, 40) || 'hanzo';
44
+ const wire = (key) => key.split('/').map(encodeURIComponent).join('/');
45
+ const objects = (bucket) => `/v1/s3/buckets/${encodeURIComponent(bucket)}/objects`;
46
+ async function putBytes(client, bucket, key, body, type) {
47
+ const grant = await client.http.json({
48
+ method: 'POST',
49
+ path: objects(bucket),
50
+ body: { bucket, key },
51
+ });
52
+ if (!grant?.url)
53
+ throw new Error('The store issued no upload address.');
54
+ const sent = await fetch(grant.url, {
55
+ method: grant.method || 'PUT',
56
+ body,
57
+ headers: type ? { 'Content-Type': type } : undefined,
58
+ });
59
+ if (!sent.ok)
60
+ throw new Error(`${key.split('/').pop()} did not upload (${sent.status}).`);
61
+ }
62
+ async function urlFor(client, bucket, key) {
63
+ const grant = await client.http.json({ path: `${objects(bucket)}/${wire(key)}` });
64
+ if (!grant?.url)
65
+ throw new Error('The store issued no download address.');
66
+ return grant.url;
67
+ }
68
+ async function remove(client, bucket, key) {
69
+ await client.http.json({ method: 'DELETE', path: `${objects(bucket)}/${wire(key)}` });
70
+ }
71
+ function Glyph({ entry, size = 16 }) {
72
+ if (entry.isDir)
73
+ return _jsx(Folder, { size: size, color: "var(--foreground)", "aria-hidden": true });
74
+ const ext = (entry.key.split('.').pop() || '').toLowerCase();
75
+ if (/^(png|jpe?g|gif|webp|svg|heic|avif|bmp)$/.test(ext))
76
+ return _jsx(FileImage, { size: size, color: "var(--muted-foreground)", "aria-hidden": true });
77
+ if (/^(mp4|mov|webm|mkv|avi)$/.test(ext))
78
+ return _jsx(FileVideo, { size: size, color: "var(--muted-foreground)", "aria-hidden": true });
79
+ if (/^(mp3|wav|m4a|flac|ogg|aac)$/.test(ext))
80
+ return _jsx(FileAudio, { size: size, color: "var(--muted-foreground)", "aria-hidden": true });
81
+ if (/^(zip|tar|gz|tgz|7z|rar|bz2|xz)$/.test(ext))
82
+ return _jsx(FileArchive, { size: size, color: "var(--muted-foreground)", "aria-hidden": true });
83
+ if (/^(md|txt|rtf|doc|docx|pdf|csv|xls|xlsx|ppt|pptx)$/.test(ext))
84
+ return _jsx(FileText, { size: size, color: "var(--muted-foreground)", "aria-hidden": true });
85
+ if (/^(ts|tsx|js|jsx|py|go|rs|rb|java|kt|swift|c|h|cpp|cs|sh|json|ya?ml|toml|html|css|sql)$/.test(ext))
86
+ return _jsx(FileCode, { size: size, color: "var(--muted-foreground)", "aria-hidden": true });
87
+ return _jsx(FileIcon, { size: size, color: "var(--muted-foreground)", "aria-hidden": true });
88
+ }
89
+ /** The tables' column templates, header and rows alike. */
90
+ const FILES = 'minmax(200px, 2fr) 120px 140px 100px 48px';
91
+ const PROJECTS = 'minmax(220px, 2fr) 120px 140px 100px 48px';
92
+ /** A column heading. */
93
+ const HEAD = { fontSize: '$1', fontWeight: '600', color: '$faint', letterSpacing: 0.6, textTransform: 'uppercase' };
94
+ /** A row's name: one line, cut with an ellipsis. */
95
+ const NAME = { fontSize: '$2', fontWeight: '500', color: '$ink', numberOfLines: 1 };
96
+ /** A row's kind, set as a tag. */
97
+ const CHIP = { fontSize: '$1', color: '$soft', bg: '$hover', px: 8, py: 2, rounded: 4 };
98
+ /** A quiet cell: the date and the size. */
99
+ const CELL = { fontSize: '$2', color: '$soft' };
100
+ /** A row's "more" control. */
101
+ function More({ onPress }) {
102
+ return (_jsx(Button, { variant: "ghost", size: "icon-sm", onClick: onPress, "aria-label": "More", title: "More", children: _jsx(MoreHorizontal, { size: 15 }) }));
103
+ }
104
+ /** One line of a row's menu: a glyph and a word, the whole width. */
105
+ function Pick({ icon: Icon, label, tone, onPress, }) {
106
+ return (_jsxs(Button, { variant: "ghost", size: "sm", width: "100%", justify: "flex-start", ...(tone ? { color: tone } : null), onClick: onPress, children: [_jsx(Icon, { size: 13 }), label] }));
107
+ }
108
+ /** A tooltip on a gui element: `title` is a DOM attribute gui passes through and does not type. */
109
+ const tip = (title) => ({ title });
110
+ /** A field with no chrome of its own, inside a box that draws it. */
111
+ const BARE = { flex: 1, bg: 'transparent', borderWidth: 0, outlineStyle: 'none', color: '$ink', fontSize: '$2' };
112
+ /** One line of the New menu. */
113
+ const ITEM = { variant: 'ghost', size: 'sm', width: '100%', justify: 'flex-start' };
114
+ /** A step of the breadcrumb: a link-styled Button; its ink says whether it is where you are. */
115
+ const CRUMB = { variant: 'link', size: 'sm', px: 0, fontWeight: '600' };
116
+ /** The buckets list's one line when it has no rows to show. */
117
+ const NOTE = { display: 'block', px: 16, py: 24, fontSize: '$2', color: '$faint', text: 'center' };
118
+ /** The drop target's veil: the page's own ground, nearly opaque. */
119
+ const VEIL = mix('var(--background)', 90, 'srgb');
120
+ export function Drive() {
121
+ const { client } = useAi();
122
+ const { user, isAuthenticated } = useIam();
123
+ const ready = Boolean(client && isAuthenticated);
124
+ const { bucket, openBucket } = useOpen();
125
+ const { currentOrg } = useOrganizations();
126
+ const workspace = currentOrg?.displayName || currentOrg?.name || 'hanzo';
127
+ const [navSection, setNavSection] = useState('my-drive');
128
+ const [viewMode, setViewMode] = useState('list');
129
+ const [searchQuery, setSearchQuery] = useState('');
130
+ const [buckets, setBuckets] = useState(null);
131
+ const [reload, setReload] = useState(0);
132
+ const [failed, setFailed] = useState(null);
133
+ const [entries, setEntries] = useState(null);
134
+ const [prefix, setPrefix] = useState('');
135
+ const [busy, setBusy] = useState(null);
136
+ const [over, setOver] = useState(false);
137
+ const [newMenuOpen, setNewMenuOpen] = useState(false);
138
+ // Below md the side column is a drawer over the files, opened from the bar.
139
+ const [menu, setMenu] = useState(false);
140
+ const [itemMenuKey, setItemMenuKey] = useState(null);
141
+ const [naming, setNaming] = useState(null);
142
+ const [projects, setProjects] = useState(null);
143
+ // What the drive actually holds, measured. There is no quota route, so there
144
+ // is no quota drawn: a number nobody publishes would be a number invented.
145
+ const [usage, setUsage] = useState(null);
146
+ const fileInputRef = useRef(null);
147
+ const folderInputRef = useRef(null);
148
+ const dragCounter = useRef(0);
149
+ // PROJECTS are the org's own, from the platform. `/v1/projects` answers every
150
+ // project this org owns; nothing stands in for it when it answers nothing.
151
+ useEffect(() => {
152
+ if (!ready || !client)
153
+ return;
154
+ let live = true;
155
+ client.http
156
+ .json({ path: '/v1/projects' })
157
+ .then((ps) => live && setProjects(Array.isArray(ps) ? ps : []))
158
+ .catch(() => live && setProjects([]));
159
+ return () => {
160
+ live = false;
161
+ };
162
+ }, [ready, client]);
163
+ // BUCKETS. Through the client, which carries the caller's IAM token and the
164
+ // API's own origin — a relative fetch reaches this site, not the platform,
165
+ // and answers HTML that JSON.parse then throws on.
166
+ useEffect(() => {
167
+ if (!ready || !client) {
168
+ setBuckets(null);
169
+ return;
170
+ }
171
+ let live = true;
172
+ client.http
173
+ .json({ path: '/v1/s3/buckets' })
174
+ .then((out) => live && setBuckets(out?.buckets ?? []))
175
+ .catch((e) => live && setFailed(blame(e)));
176
+ return () => {
177
+ live = false;
178
+ };
179
+ }, [ready, client, reload]);
180
+ // THE DRIVE IS A BUCKET THE ORG OWNS. It used to fall back to the literal
181
+ // "my-drive", which is a nav label rather than a name the store answers to —
182
+ // so every read 404'd and every write went nowhere.
183
+ const activeBucket = bucket || buckets?.[0]?.name || null;
184
+ // YOU LAND IN THE DRIVE, not in a chooser. The root view lists buckets and
185
+ // projects, and the toolbar's verbs all act on an OPEN bucket — so opening
186
+ // the org's first one is what makes New, upload and the listing agree about
187
+ // where they are. It is also what a drive does: the files, on arrival.
188
+ useEffect(() => {
189
+ if (!bucket && buckets && buckets.length > 0)
190
+ openBucket(buckets[0].name);
191
+ }, [bucket, buckets, openBucket]);
192
+ // WHAT IS AT THIS LEVEL. The store's listing takes `recursive`; passing
193
+ // `delimiter` (S3's own word) is silently ignored, so a flat listing came
194
+ // back and every folder read as a file.
195
+ const loadEntries = useCallback(() => {
196
+ if (!ready || !client || !activeBucket) {
197
+ setEntries(null);
198
+ return;
199
+ }
200
+ let live = true;
201
+ client.http
202
+ .collection('objects', { path: objects(activeBucket), query: { prefix } })
203
+ .then((found) => live && setEntries(found))
204
+ .catch((e) => live && setFailed(blame(e)));
205
+ return () => {
206
+ live = false;
207
+ };
208
+ }, [ready, client, activeBucket, prefix]);
209
+ useEffect(() => {
210
+ loadEntries();
211
+ }, [loadEntries, reload]);
212
+ // WHAT IT WEIGHS, measured rather than quoted: every object under the bucket,
213
+ // summed. The listing is bounded by the platform, so a bucket past that cap
214
+ // reports what was counted and says it is a floor.
215
+ useEffect(() => {
216
+ if (!ready || !client || !activeBucket) {
217
+ setUsage(null);
218
+ return;
219
+ }
220
+ let live = true;
221
+ client.http
222
+ .json({ path: objects(activeBucket), query: { recursive: 'true' } })
223
+ .then((out) => {
224
+ if (!live)
225
+ return;
226
+ const all = (out?.objects ?? []).filter((e) => !e.isDir);
227
+ setUsage({
228
+ bytes: all.reduce((n, e) => n + (e.size || 0), 0),
229
+ files: all.length,
230
+ capped: typeof out?.total === 'number' && out.total > all.length,
231
+ });
232
+ })
233
+ .catch(() => live && setUsage(null));
234
+ return () => {
235
+ live = false;
236
+ };
237
+ }, [ready, client, activeBucket, reload]);
238
+ /** One verb: say what is happening, re-read when it lands, blame when it does not. */
239
+ const run = useCallback(async (note, work) => {
240
+ if (!client || !activeBucket || busy)
241
+ return;
242
+ setBusy(note);
243
+ setFailed(null);
244
+ try {
245
+ await work(setBusy);
246
+ setReload((n) => n + 1);
247
+ }
248
+ catch (e) {
249
+ setFailed(blame(e));
250
+ }
251
+ finally {
252
+ setBusy(null);
253
+ }
254
+ }, [client, activeBucket, busy]);
255
+ // UPLOAD is two calls and the bytes skip the platform: mint a presigned PUT,
256
+ // then send the file straight to the store. A PUT at the object's own address
257
+ // is not a route the platform has — that address mints URLs and deletes.
258
+ const handleUpload = useCallback((files) => {
259
+ if (!files)
260
+ return;
261
+ const list = Array.from(files);
262
+ if (!list.length || !client || !activeBucket)
263
+ return;
264
+ void run(`Uploading ${list[0].name}`, async (say) => {
265
+ for (const file of list) {
266
+ say(`Uploading ${file.name}`);
267
+ const name = file.webkitRelativePath || file.name;
268
+ await putBytes(client, activeBucket, `${prefix}${name}`, file, file.type || undefined);
269
+ }
270
+ });
271
+ }, [client, activeBucket, prefix, run]);
272
+ const handleCreateFolder = useCallback((folderName) => {
273
+ const clean = folderName.trim().replace(/^\/+|\/+$/g, '');
274
+ if (!clean || !client || !activeBucket)
275
+ return;
276
+ void run(`Creating ${clean}`, () => putBytes(client, activeBucket, `${prefix}${clean}/`, new Blob([])));
277
+ setNaming(null);
278
+ }, [client, activeBucket, prefix, run]);
279
+ const handleRename = useCallback((entry, to) => {
280
+ const clean = to.trim().replace(/^\/+|\/+$/g, '');
281
+ if (!clean || !client || !activeBucket || entry.isDir)
282
+ return;
283
+ const from = `${prefix}${entry.key}`;
284
+ // The store has no copy, so a rename is the three verbs it does have.
285
+ void run(`Renaming ${entry.key}`, async () => {
286
+ const src = await fetch(await urlFor(client, activeBucket, from));
287
+ if (!src.ok)
288
+ throw new Error(`${entry.key} could not be read (${src.status}).`);
289
+ const body = await src.blob();
290
+ await putBytes(client, activeBucket, `${prefix}${clean}`, body, body.type || undefined);
291
+ await remove(client, activeBucket, from);
292
+ });
293
+ setNaming(null);
294
+ }, [client, activeBucket, prefix, run]);
295
+ const handleDelete = useCallback((entry) => {
296
+ if (!client || !activeBucket)
297
+ return;
298
+ const key = `${prefix}${entry.key}`;
299
+ void run(`Deleting ${entry.key}`, async (say) => {
300
+ if (!entry.isDir)
301
+ return remove(client, activeBucket, key);
302
+ const root = key.endsWith('/') ? key : `${key}/`;
303
+ const out = await client.http.json({ path: objects(activeBucket), query: { prefix: root, recursive: 'true' } });
304
+ for (const e of (out?.objects ?? []).filter((x) => !x.isDir)) {
305
+ say(`Deleting ${e.key}`);
306
+ await remove(client, activeBucket, root + e.key);
307
+ }
308
+ await remove(client, activeBucket, root).catch(() => { });
309
+ });
310
+ }, [client, activeBucket, prefix, run]);
311
+ // DOWNLOAD follows a presigned GET. The object's platform address answers a
312
+ // signed URL, not the bytes, and it needs the caller's token — which an
313
+ // anchor href does not carry.
314
+ const handleDownload = useCallback((entry) => {
315
+ if (!client || !activeBucket || entry.isDir)
316
+ return;
317
+ void run(`Fetching ${entry.key}`, async () => {
318
+ const url = await urlFor(client, activeBucket, `${prefix}${entry.key}`);
319
+ const a = document.createElement('a');
320
+ a.href = url;
321
+ a.download = entry.key.split('/').pop() || 'download';
322
+ a.rel = 'noreferrer';
323
+ a.target = '_blank';
324
+ document.body.appendChild(a);
325
+ a.click();
326
+ document.body.removeChild(a);
327
+ });
328
+ }, [client, activeBucket, prefix, run]);
329
+ const allProjects = useMemo(() => (projects ?? []).map((p) => ({
330
+ id: p.id || p.slug || p.name,
331
+ name: p.name || p.slug || p.id,
332
+ type: p.framework || 'Project',
333
+ updated: when(p.updatedAt || p.createdAt || 0),
334
+ size: '—',
335
+ })), [projects]);
336
+ const filteredProjects = useMemo(() => {
337
+ if (!searchQuery.trim())
338
+ return allProjects;
339
+ const q = searchQuery.toLowerCase();
340
+ return allProjects.filter((p) => p.name.toLowerCase().includes(q));
341
+ }, [allProjects, searchQuery]);
342
+ const filteredEntries = useMemo(() => {
343
+ if (!entries)
344
+ return null;
345
+ if (!searchQuery.trim())
346
+ return entries;
347
+ const q = searchQuery.toLowerCase();
348
+ return entries.filter((e) => e.key.toLowerCase().includes(q));
349
+ }, [entries, searchQuery]);
350
+ // Breadcrumbs
351
+ const crumbs = useMemo(() => {
352
+ if (!prefix)
353
+ return [];
354
+ return prefix.replace(/\/+$/, '').split('/');
355
+ }, [prefix]);
356
+ const userEmail = user?.email || '';
357
+ const userInitials = (user?.displayName || user?.name || userEmail)
358
+ .slice(0, 1)
359
+ .toUpperCase();
360
+ return (_jsxs(YStack, { flex: 1, height: "100%", bg: "$background", position: "relative", onDragEnter: (e) => {
361
+ e.preventDefault();
362
+ e.stopPropagation();
363
+ dragCounter.current += 1;
364
+ setOver(true);
365
+ }, onDragLeave: (e) => {
366
+ e.preventDefault();
367
+ e.stopPropagation();
368
+ dragCounter.current -= 1;
369
+ if (dragCounter.current <= 0) {
370
+ dragCounter.current = 0;
371
+ setOver(false);
372
+ }
373
+ }, onDragOver: (e) => {
374
+ e.preventDefault();
375
+ e.stopPropagation();
376
+ e.dataTransfer.dropEffect = 'copy';
377
+ setOver(true);
378
+ }, onDrop: (e) => {
379
+ e.preventDefault();
380
+ e.stopPropagation();
381
+ dragCounter.current = 0;
382
+ setOver(false);
383
+ if (e.dataTransfer.files)
384
+ handleUpload(e.dataTransfer.files);
385
+ }, children: [_jsx("input", { ref: fileInputRef, type: "file", multiple: true, hidden: true, onChange: (e) => {
386
+ handleUpload(e.target.files);
387
+ e.currentTarget.value = '';
388
+ } }), _jsx("input", { ref: folderInputRef, type: "file",
389
+ // @ts-expect-error directory support
390
+ webkitdirectory: "true", directory: "true", multiple: true, hidden: true, onChange: (e) => {
391
+ handleUpload(e.target.files);
392
+ e.currentTarget.value = '';
393
+ } }), _jsxs(XStack, { height: 56, items: "center", justify: "space-between", px: "$5", borderBottomWidth: 1, borderColor: "$borderColor", shrink: 0, children: [_jsx(XStack, { width: 220, shrink: 1, children: _jsx(View, { display: "none", "$max-md": { display: 'flex' }, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: () => setMenu((v) => !v), "aria-label": "Drive menu", "aria-expanded": menu, "aria-controls": "drive-menu", title: "Drive menu", children: _jsx(PanelLeft, { size: 16 }) }) }) }), _jsxs(XStack, { items: "center", gap: "$2", width: 460, maxW: "45%", height: 38, px: "$3", rounded: 8, bg: "$panel", borderWidth: 1, borderColor: "var(--border-control)", children: [_jsx(Search, { size: 15, color: "var(--muted-foreground)", "aria-hidden": true }), _jsx(Text, { render: _jsx("input", { value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: "Search Drive", "aria-label": "Search Drive" }), ...BARE })] }), _jsxs(XStack, { items: "center", gap: "$3", children: [_jsx(Button, { size: "icon-sm", onClick: () => fileInputRef.current?.click(), "aria-label": "Upload", title: "Upload", children: _jsx(Upload, { size: 15 }) }), _jsx(Button, { size: "icon-sm", onClick: () => setNaming({ kind: 'folder', value: '' }), "aria-label": "Add Folder", title: "Add Folder", children: _jsx(Plus, { size: 16 }) }), _jsx(Text, { ...tip(userEmail), width: 32, height: 32, rounded: 9999, bg: "$raised", borderWidth: 1, borderColor: "$borderColor", display: "flex", items: "center", justify: "center", fontSize: "$2", fontWeight: "600", color: "$ink", children: userInitials })] })] }), _jsxs(XStack, { flex: 1, minH: 0, overflow: "hidden", position: "relative", children: [_jsx(View, { display: "none", "$max-md": { display: menu ? 'flex' : 'none' }, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: "var(--z-scrim)", bg: "var(--surface-scrim)", onPress: () => setMenu(false), "aria-hidden": true }), _jsxs(YStack, { id: "drive-menu", width: 240, minW: 240, bg: "$background", borderRightWidth: 1, borderColor: "$borderColor", px: "$3", py: "$4", justify: "space-between", "$max-md": {
394
+ display: menu ? 'flex' : 'none',
395
+ position: 'absolute',
396
+ t: 0,
397
+ b: 0,
398
+ l: 0,
399
+ z: 'var(--z-drawer)',
400
+ overflowY: 'auto',
401
+ boxShadow: 'var(--shadow-lg)',
402
+ }, children: [_jsxs(YStack, { gap: "$4", children: [_jsxs(View, { position: "relative", children: [_jsxs(Button, { size: "lg", width: "100%", justify: "space-between", onClick: () => setNewMenuOpen((v) => !v), "aria-expanded": newMenuOpen, children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Plus, { size: 16 }), _jsx(Text, { fontSize: "$3", fontWeight: "500", color: "$ink", children: "New" })] }), _jsx(ChevronDown, { size: 14, opacity: 0.65 })] }), newMenuOpen ? (_jsxs(YStack, { position: "absolute", t: "100%", mt: 6, l: 0, width: "100%", bg: "var(--popover)", borderWidth: 1, borderColor: "$borderColor", rounded: 10, boxShadow: "var(--shadow-lg)", z: "var(--z-dropdown)", p: 6, gap: 2, children: [_jsxs(Button, { ...ITEM, onClick: () => {
403
+ setNewMenuOpen(false);
404
+ setMenu(false);
405
+ setNaming({ kind: 'folder', value: '' });
406
+ }, children: [_jsx(FolderPlus, { size: 14 }), "New folder"] }), _jsx(View, { height: 1, bg: "$borderColor", my: 4 }), _jsxs(Button, { ...ITEM, onClick: () => {
407
+ setNewMenuOpen(false);
408
+ setMenu(false);
409
+ fileInputRef.current?.click();
410
+ }, children: [_jsx(Upload, { size: 14 }), "File upload"] }), _jsxs(Button, { ...ITEM, onClick: () => {
411
+ setNewMenuOpen(false);
412
+ setMenu(false);
413
+ folderInputRef.current?.click();
414
+ }, children: [_jsx(FolderInput, { size: 14 }), "Folder upload"] })] })) : null] }), _jsx(YStack, { gap: "$1", children: [
415
+ { id: 'my-drive', label: 'My Drive', icon: HardDrive },
416
+ { id: 'recent', label: 'Recent', icon: Clock },
417
+ ].map((item) => {
418
+ const isActive = navSection === item.id && !bucket && !prefix;
419
+ const Icon = item.icon;
420
+ return (_jsxs(Button, { variant: isActive ? 'secondary' : 'ghost', width: "100%", justify: "flex-start", gap: 12, px: 12, color: isActive ? '$ink' : '$soft', fontWeight: isActive ? '600' : '500', onClick: () => {
421
+ setNavSection(item.id);
422
+ openBucket(null);
423
+ setPrefix('');
424
+ setMenu(false);
425
+ }, children: [_jsx(Icon, { size: 16 }), item.label] }, item.id));
426
+ }) }), _jsxs(YStack, { gap: "$2", mt: "$2", px: "$2", children: [_jsx(Text, { fontSize: 11, fontWeight: "600", color: "$faint", letterSpacing: 0.6, children: "Storage" }), _jsx(Text, { fontSize: 12, color: "$quiet", children: usage ? `${usage.capped ? 'over ' : ''}${weigh(usage.bytes)} in ${usage.files.toLocaleString()} file${usage.files === 1 ? '' : 's'}` : 'Measuring…' }), _jsx(Button, { asChild: true, size: "sm", mt: 8, children: _jsx("a", { href: checkoutUrl(), target: "_blank", rel: "noreferrer", children: "Manage storage" }) })] })] }), _jsxs(YStack, { px: 14, py: 12, rounded: 10, borderWidth: 1, borderColor: "$borderColor", bg: "$panel", gap: 6, children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Cloud, { size: 16, color: "var(--foreground)" }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: "Hanzo Drive" })] }), _jsx(Text, { fontSize: "$1", color: "$soft", children: [userEmail, activeBucket].filter(Boolean).join(' · ') }), _jsx(Button, { asChild: true, variant: "link", size: "sm", px: 0, mt: 4, justify: "flex-start", children: _jsxs("a", { href: "https://docs.hanzo.ai/docs/openapi/s3/", target: "_blank", rel: "noreferrer", children: ["Documentation", _jsx(ArrowRight, { size: 12 })] }) })] })] }), _jsxs(YStack, { flex: 1, minH: 0, px: "$6", py: "$5", overflow: "scroll", gap: "$5", children: [bucket || prefix ? (_jsxs(XStack, { items: "center", gap: "$2", pb: "$2", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(Button, { ...CRUMB, color: "$soft", onClick: () => {
427
+ openBucket(null);
428
+ setPrefix('');
429
+ }, children: "Drive" }), _jsx(ChevronRight, { size: 13, color: "var(--muted-foreground)" }), _jsx(Button, { ...CRUMB, color: crumbs.length === 0 ? '$ink' : '$soft', onClick: () => setPrefix(''), children: bucket || 'hanzo' }), crumbs.map((c, i) => (_jsxs(React.Fragment, { children: [_jsx(ChevronRight, { size: 13, color: "var(--muted-foreground)" }), _jsx(Button, { ...CRUMB, color: i === crumbs.length - 1 ? '$ink' : '$soft', onClick: () => setPrefix(crumbs.slice(0, i + 1).map((x) => `${x}/`).join('')), children: c })] }, `${i}-${c}`)))] })) : null, _jsxs(XStack, { items: "center", justify: "space-between", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(Text, { render: "h1", fontSize: 26, fontWeight: "700", color: "$ink", children: "Drive" }), _jsx(Text, { fontSize: 13, color: "$soft", children: "Secure. Private. Built for builders." })] }), _jsx(XStack, { items: "center", bg: "$panel", rounded: 8, borderWidth: 1, borderColor: "$borderColor", p: 2, gap: 2, children: [
430
+ ['list', 'List View', List],
431
+ ['grid', 'Grid View', LayoutGrid],
432
+ ['details', 'Details View', MoreHorizontal],
433
+ ].map(([mode, label, Icon]) => (_jsx(Button, { variant: viewMode === mode ? 'secondary' : 'ghost', size: "icon-sm", color: viewMode === mode ? '$ink' : '$soft', onClick: () => setViewMode(mode), "aria-label": label, "aria-pressed": viewMode === mode, title: label, children: _jsx(Icon, { size: 14 }) }, mode))) })] }), naming ? (_jsxs(XStack, { items: "center", gap: "$3", py: 10, px: 14, rounded: 8, borderWidth: 1, borderColor: "var(--border-control)", bg: "$panel", children: [_jsx(Folder, { size: 16, color: "var(--foreground)" }), _jsx(Text, { render: _jsx("input", { autoFocus: true, value: naming.value, onChange: (e) => setNaming({ ...naming, value: e.target.value }), onKeyDown: (e) => {
434
+ if (e.key === 'Escape')
435
+ setNaming(null);
436
+ if (e.key === 'Enter')
437
+ handleCreateFolder(naming.value);
438
+ }, placeholder: "Folder name", "aria-label": "Folder name" }), ...BARE }), _jsx(Button, { variant: "primary", size: "sm", onClick: () => handleCreateFolder(naming.value), children: "Create" }), _jsx(Button, { variant: "ghost", size: "sm", color: "$soft", onClick: () => setNaming(null), children: "Cancel" })] })) : null, busy ? (_jsxs(Text, { fontSize: "$2", color: "$soft", children: [busy, "\u2026"] })) : null, bucket || prefix ? (_jsx(YStack, { gap: "$2", children: _jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: 8, overflow: "hidden", children: [_jsxs(View, { display: "grid", gridTemplateColumns: FILES, px: 16, py: 10, borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { ...HEAD, children: "NAME" }), _jsx(Text, { ...HEAD, children: "TYPE" }), _jsx(Text, { ...HEAD, children: "UPDATED \u2193" }), _jsx(Text, { ...HEAD, children: "SIZE" }), _jsx(View, {})] }), !filteredEntries || filteredEntries.length === 0 ? (_jsx(Text, { py: 36, px: 16, text: "center", fontSize: "$2", color: "$faint", children: "None" })) : (filteredEntries.map((e) => {
439
+ const isFolder = e.isDir || e.key.endsWith('/');
440
+ const cleanName = e.key.replace(/\/+$/, '');
441
+ return (_jsxs(View, { display: "grid", gridTemplateColumns: FILES, px: 16, py: 11, items: "center", cursor: "pointer", borderBottomWidth: 1, borderColor: "$borderColor", transition: "100ms", hoverStyle: { bg: '$hover' }, onClick: () => {
442
+ if (isFolder) {
443
+ setPrefix(`${prefix}${cleanName}/`);
444
+ }
445
+ else {
446
+ handleDownload(e);
447
+ }
448
+ }, children: [_jsxs(XStack, { items: "center", gap: "$2.5", minW: 0, children: [_jsx(Glyph, { entry: e, size: 16 }), _jsx(Text, { ...NAME, children: cleanName })] }), _jsx(View, { items: "flex-start", children: _jsx(Text, { ...CHIP, children: isFolder ? 'Folder' : cleanName.split('.').pop()?.toUpperCase() || 'File' }) }), _jsx(Text, { ...CELL, children: when(e.lastModified) }), _jsx(Text, { ...CELL, children: weigh(e.size) }), _jsxs(View, { position: "relative", items: "flex-start", children: [_jsx(More, { onPress: (ev) => {
449
+ ev.stopPropagation();
450
+ setItemMenuKey(itemMenuKey === e.key ? null : e.key);
451
+ } }), itemMenuKey === e.key ? (_jsxs(YStack, { position: "absolute", r: 0, t: "100%", bg: "var(--popover)", borderWidth: 1, borderColor: "$borderColor", rounded: 8, boxShadow: "var(--shadow-lg)", z: "var(--z-dropdown)", p: 4, minW: 140, onClick: (ev) => ev.stopPropagation(), children: [!isFolder ? (_jsx(Pick, { icon: Download, label: "Download", onPress: () => {
452
+ setItemMenuKey(null);
453
+ handleDownload(e);
454
+ } })) : null, _jsx(Pick, { icon: Trash2, label: "Delete", tone: BAD, onPress: () => {
455
+ setItemMenuKey(null);
456
+ handleDelete(e);
457
+ } })] })) : null] })] }, e.key));
458
+ }))] }) })) : (_jsxs(_Fragment, { children: [_jsxs(YStack, { gap: "$2.5", children: [_jsx(Text, { fontSize: 12, fontWeight: "600", color: "$soft", children: "Storage Buckets" }), _jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: 8, overflow: "hidden", children: failed ? (_jsxs(Text, { ...NOTE, children: [failed, _jsx(Button, { size: "sm", mx: "auto", mt: 10, onClick: () => {
459
+ setFailed(null);
460
+ setReload((n) => n + 1);
461
+ }, children: "Try again" })] })) : !buckets ? (_jsx(Text, { ...NOTE, children: "Reading your storage\u2026" })) : buckets.length === 0 ? (_jsx(Text, { ...NOTE, children: "No buckets yet." })) : (buckets.map((b) => (_jsxs(XStack, { onClick: () => openBucket(b.name), items: "center", justify: "space-between", px: 16, py: 12, cursor: "pointer", bg: "transparent", transition: "100ms", hoverStyle: { bg: '$hover' }, children: [_jsxs(XStack, { items: "center", gap: "$2.5", children: [_jsx(Folder, { size: 17, color: "var(--foreground)" }), _jsx(Text, { fontSize: "$2", fontWeight: "500", color: "$ink", children: getBucketDisplayName(b) })] }), _jsxs(XStack, { items: "center", gap: "$4", children: [_jsx(Text, { fontSize: "$2", color: "$faint", children: when(b.createdAt) }), _jsx(More, { onPress: (e) => e.stopPropagation() })] })] }, b.name)))) })] }), _jsxs(YStack, { gap: "$2.5", children: [_jsx(Text, { fontSize: 12, fontWeight: "600", color: "$soft", children: "Projects (Unified VFS)" }), _jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: 8, overflow: "hidden", children: [_jsxs(View, { display: "grid", gridTemplateColumns: PROJECTS, px: 16, py: 10, borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { ...HEAD, children: "NAME" }), _jsx(Text, { ...HEAD, children: "TYPE" }), _jsx(Text, { ...HEAD, children: "UPDATED \u2193" }), _jsx(Text, { ...HEAD, children: "SIZE" }), _jsx(View, {})] }), filteredProjects.length === 0 ? (_jsx(Text, { py: 36, px: 16, text: "center", fontSize: "$2", color: "$faint", children: "None" })) : (filteredProjects.map((p) => (_jsxs(View, { onClick: () => {
462
+ const targetBucket = buckets[0]?.name || 'hanzo';
463
+ openBucket(targetBucket);
464
+ setPrefix(`projects/${p.name || p.id}/`);
465
+ }, display: "grid", gridTemplateColumns: PROJECTS, px: 16, py: 11, items: "center", cursor: "pointer", borderBottomWidth: 1, borderColor: "$borderColor", transition: "100ms", hoverStyle: { bg: '$hover' }, children: [_jsxs(XStack, { items: "center", gap: "$2.5", minW: 0, children: [_jsx(Folder, { size: 16, color: "var(--foreground)" }), _jsx(Text, { ...NAME, children: p.name })] }), _jsx(View, { items: "flex-start", children: _jsx(Text, { ...CHIP, children: p.type }) }), _jsx(Text, { ...CELL, children: p.updated }), _jsx(Text, { ...CELL, children: p.size }), _jsx(View, { items: "flex-start", children: _jsx(More, { onPress: (e) => e.stopPropagation() }) })] }, p.id))))] })] })] }))] })] }), over ? (_jsx(Box, { position: "absolute", t: 0, l: 0, r: 0, b: 0, z: 1000, bg: VEIL, borderWidth: 2, borderStyle: "dashed", borderColor: "var(--ring)", rounded: "$3", display: "flex", items: "center", justify: "center", pointerEvents: "none", children: _jsxs(YStack, { items: "center", gap: "$2", children: [_jsx(Upload, { size: 36, color: "currentColor" }), _jsxs(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", children: ["Drop files to upload to ", bucket || workspace] })] }) })) : null] }));
466
+ }
package/dist/Find.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { type OrgCommandItem } from "@hanzogui/shell";
2
+ export declare function Find({ doors, onClose, navigate, }: {
3
+ /** Where the shell can go without asking anyone: its rooms and its panes. */
4
+ doors: OrgCommandItem[];
5
+ onClose: () => void;
6
+ navigate?: (route: string) => void;
7
+ }): import("react").JSX.Element;
package/dist/Find.js ADDED
@@ -0,0 +1,82 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ /**
4
+ * ⌘K, in the workspace.
5
+ *
6
+ * The estate's palette — `OrgCommandPalette`, the same frame the marketing
7
+ * pages open — so the chord means one thing wherever a reader presses it. Here
8
+ * it used to focus the column's filter box, which left the shortcut that finds
9
+ * anything on every other page of this site narrowing one list in the app: a
10
+ * conversation in the room next door was unreachable from a control offering to
11
+ * search the workspace.
12
+ *
13
+ * MOUNTED ONLY WHILE OPEN. Its rows are read from the platform, and a palette
14
+ * that stayed mounted would fetch every conversation and every project on each
15
+ * load of a room that draws neither.
16
+ */
17
+ import { dev } from "./lib/host.js";
18
+ import { useEffect, useState } from "react";
19
+ import { OrgCommandPalette } from "@hanzogui/shell";
20
+ import { useThreads } from "@hanzo/ai/react";
21
+ import { useAi } from "./lib/ai.js";
22
+ import { useOpen } from "./open.js";
23
+ export function Find({ doors, onClose, navigate, }) {
24
+ const { threads } = useThreads();
25
+ const { client } = useAi();
26
+ const { open } = useOpen();
27
+ const [projects, setProjects] = useState([]);
28
+ useEffect(() => {
29
+ if (!client)
30
+ return;
31
+ let live = true;
32
+ client.http
33
+ .json({ path: "/v1/projects" })
34
+ .then((found) => live && setProjects(found))
35
+ .catch(() => {
36
+ // A palette that cannot read the projects still finds the rooms.
37
+ });
38
+ return () => {
39
+ live = false;
40
+ };
41
+ }, [client]);
42
+ const rows = [
43
+ ...doors,
44
+ // A conversation and a project are OPENED, not navigated to: the address is
45
+ // the room, and which one is open belongs to the store — so a href would
46
+ // land the reader in /chat with whatever they had before still on screen.
47
+ ...threads.map((t) => ({
48
+ id: `thread-${t.id}`,
49
+ title: t.title || "Untitled",
50
+ category: "Conversations",
51
+ action: () => {
52
+ open(t.id);
53
+ navigate?.("/chat");
54
+ },
55
+ })),
56
+ ...projects.map((p) => ({
57
+ id: `project-${p.id}`,
58
+ title: p.name,
59
+ category: "Projects",
60
+ action: () => {
61
+ navigate?.(dev(p.name));
62
+ },
63
+ })),
64
+ ];
65
+ return (_jsx(OrgCommandPalette, { open: true, currentAppId: "chat", commands: rows, onOpenChange: (next) => {
66
+ if (!next)
67
+ onClose();
68
+ }, onNavigate: (href, external) => {
69
+ if (external)
70
+ window.open(href, "_blank", "noopener");
71
+ else
72
+ navigate?.(href);
73
+ onClose();
74
+ },
75
+ // THIS ROOM IS THE AI. A question nobody indexed opens a conversation
76
+ // here, rather than a second copy of the product the reader is in.
77
+ onAsk: (question) => {
78
+ open(null);
79
+ navigate?.(`/chat?q=${encodeURIComponent(question)}`);
80
+ onClose();
81
+ } }));
82
+ }
@@ -0,0 +1,6 @@
1
+ import { type Tour } from './tours.js';
2
+ export declare function Guide({ room }: {
3
+ room?: Tour;
4
+ }): import("react").ReactPortal;
5
+ /** Let a room's guide be asked for again — a menu row can call this and reload. */
6
+ export declare function replayGuide(room?: Tour): void;