handoff-app 2.0.0-8 → 2.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 (322) hide show
  1. package/{Changelog.md → CHANGELOG.md} +85 -1
  2. package/{License.md → LICENSE.md} +1 -1
  3. package/README.md +816 -393
  4. package/RECIPES.md +975 -0
  5. package/UPGRADE.md +57 -9
  6. package/components.json +20 -20
  7. package/config/docs/foundations/colors/guidelines.md +0 -1
  8. package/config/docs/foundations/colors.md +0 -1
  9. package/config/docs/foundations/effects/guidelines.md +0 -1
  10. package/config/docs/foundations/effects/resources.md +0 -1
  11. package/config/docs/foundations/effects.md +0 -1
  12. package/config/docs/foundations/fonts.md +0 -1
  13. package/config/docs/foundations/grid.md +0 -1
  14. package/config/docs/foundations/icon.md +0 -1
  15. package/config/docs/foundations/icons/guidelines.md +0 -1
  16. package/config/docs/foundations/icons.md +0 -1
  17. package/config/docs/foundations/images/guidelines.md +0 -1
  18. package/config/docs/foundations/images/resources.md +0 -1
  19. package/config/docs/foundations/images.md +0 -1
  20. package/config/docs/foundations/logo/resources.md +0 -1
  21. package/config/docs/foundations/logo.md +0 -1
  22. package/config/docs/foundations/typography/guidelines.md +0 -1
  23. package/config/docs/foundations/typography/resources.md +0 -1
  24. package/config/docs/foundations/typography.md +0 -1
  25. package/config/docs/foundations.md +0 -1
  26. package/config/docs/guidelines.md +0 -1
  27. package/config/docs/index.md +0 -1
  28. package/config/docs/system/tokens/foundations/colors.md +0 -4
  29. package/config/docs/system/tokens/foundations/effects.md +0 -4
  30. package/config/docs/system/tokens/foundations/typography.md +0 -2
  31. package/config/docs/system.md +0 -2
  32. package/config/templates/page.md +2 -1
  33. package/config/templates/page.mdx +2 -1
  34. package/dist/ai/connections.d.ts +66 -0
  35. package/dist/ai/connections.js +149 -0
  36. package/dist/ai/define.d.ts +10 -0
  37. package/dist/ai/define.js +13 -0
  38. package/dist/ai/types.d.ts +26 -0
  39. package/dist/ai/types.js +10 -0
  40. package/dist/app-builder/build.d.ts +14 -7
  41. package/dist/app-builder/build.js +195 -66
  42. package/dist/app-builder/client-config.js +3 -2
  43. package/dist/app-builder/paths.d.ts +0 -12
  44. package/dist/app-builder/paths.js +3 -20
  45. package/dist/app-builder/static-export.js +1 -1
  46. package/dist/app-builder/vercel-output.d.ts +1 -1
  47. package/dist/app-builder/vercel-output.js +5 -4
  48. package/dist/cache/build-cache.d.ts +27 -4
  49. package/dist/cache/build-cache.js +114 -47
  50. package/dist/cache/build-inputs.d.ts +13 -0
  51. package/dist/cache/build-inputs.js +19 -0
  52. package/dist/cache/index.d.ts +2 -1
  53. package/dist/cache/index.js +5 -1
  54. package/dist/catalog/normalize.js +3 -0
  55. package/dist/catalog/previews.js +5 -4
  56. package/dist/catalog/types.d.ts +8 -0
  57. package/dist/cli/create.js +1 -2
  58. package/dist/commands/build/index.d.ts +1 -1
  59. package/dist/commands/build/index.js +1 -1
  60. package/dist/commands/utils.d.ts +3 -3
  61. package/dist/config/defaults.js +7 -4
  62. package/dist/config/from-env.d.ts +4 -0
  63. package/dist/config/from-env.js +38 -1
  64. package/dist/config/helpers.js +2 -2
  65. package/dist/config/index.d.ts +0 -1
  66. package/dist/config/index.js +1 -4
  67. package/dist/config/loader.js +28 -2
  68. package/dist/config/resolve-env.d.ts +2 -1
  69. package/dist/config/resolve-env.js +28 -12
  70. package/dist/config/runtime.js +9 -11
  71. package/dist/handlebars.d.ts +10 -2
  72. package/dist/index.d.ts +12 -7
  73. package/dist/index.js +18 -22
  74. package/dist/nav/index.d.ts +2 -2
  75. package/dist/nav/registry.d.ts +3 -3
  76. package/dist/nav/registry.js +1 -1
  77. package/dist/nav/workspace.d.ts +3 -3
  78. package/dist/nav/workspace.js +1 -1
  79. package/dist/pipeline/figma.d.ts +13 -2
  80. package/dist/pipeline/figma.js +86 -46
  81. package/dist/pipeline/styles.js +1 -1
  82. package/dist/pipeline/validation.d.ts +1 -1
  83. package/dist/pipeline/validation.js +3 -3
  84. package/dist/providers.d.ts +10 -0
  85. package/dist/providers.js +13 -0
  86. package/dist/registry/asset-storage/define.d.ts +5 -5
  87. package/dist/registry/asset-storage/define.js +4 -4
  88. package/dist/registry/asset-storage/resolve.d.ts +9 -19
  89. package/dist/registry/asset-storage/resolve.js +33 -21
  90. package/dist/registry/asset-storage/types.d.ts +37 -17
  91. package/dist/registry/asset-storage/types.js +5 -5
  92. package/dist/registry/auth/ai-keys.d.ts +32 -0
  93. package/dist/registry/auth/ai-keys.js +69 -0
  94. package/dist/registry/auth/crypto.d.ts +17 -0
  95. package/dist/registry/auth/crypto.js +61 -1
  96. package/dist/registry/auth/index.d.ts +2 -1
  97. package/dist/registry/auth/index.js +11 -2
  98. package/dist/registry/checkout/index.d.ts +2 -1
  99. package/dist/registry/checkout/index.js +57 -127
  100. package/dist/registry/client.js +53 -17
  101. package/dist/registry/db/driver.js +3 -3
  102. package/dist/registry/db/schema.d.ts +261 -3
  103. package/dist/registry/db/schema.js +29 -4
  104. package/dist/registry/email/define.d.ts +10 -0
  105. package/dist/registry/email/define.js +13 -0
  106. package/dist/registry/email/providers/smtp.d.ts +10 -0
  107. package/dist/registry/email/providers/smtp.js +73 -0
  108. package/dist/registry/email/resolve.d.ts +28 -0
  109. package/dist/registry/email/resolve.js +74 -0
  110. package/dist/registry/email/types.d.ts +24 -0
  111. package/dist/registry/email/types.js +6 -0
  112. package/dist/registry/path.d.ts +2 -0
  113. package/dist/registry/path.js +11 -1
  114. package/dist/registry/publish/package.js +8 -0
  115. package/dist/registry/transfer.d.ts +4 -0
  116. package/dist/store/registry.d.ts +3 -3
  117. package/dist/store/registry.js +4 -4
  118. package/dist/transformers/plugins/ssr-render.js +40 -15
  119. package/dist/transformers/preview/component/builder.d.ts +1 -9
  120. package/dist/transformers/preview/component/builder.js +17 -17
  121. package/dist/transformers/preview/component/css.d.ts +3 -1
  122. package/dist/transformers/preview/component/css.js +92 -51
  123. package/dist/transformers/preview/component/html.d.ts +3 -1
  124. package/dist/transformers/preview/component/html.js +6 -2
  125. package/dist/transformers/preview/component/javascript.d.ts +2 -1
  126. package/dist/transformers/preview/component/javascript.js +10 -4
  127. package/dist/transformers/preview/pattern/html.js +3 -1
  128. package/dist/transformers/preview/types.d.ts +13 -0
  129. package/dist/transformers/utils/build.d.ts +24 -0
  130. package/dist/transformers/utils/build.js +100 -1
  131. package/dist/transformers/utils/module.js +2 -1
  132. package/dist/types/config.d.ts +135 -18
  133. package/dist/utils/path.d.ts +1 -5
  134. package/dist/utils/path.js +6 -23
  135. package/docs/api.md +6 -4
  136. package/docs/cli.md +275 -54
  137. package/drizzle/0007_add_ai_keys.sql +11 -0
  138. package/drizzle/0008_add_source_dir.sql +2 -0
  139. package/drizzle/meta/0007_snapshot.json +2013 -0
  140. package/drizzle/meta/0008_snapshot.json +2025 -0
  141. package/drizzle/meta/_journal.json +14 -0
  142. package/package.json +19 -8
  143. package/src/ai/README.md +42 -0
  144. package/src/ai/connections.ts +189 -0
  145. package/src/ai/define.ts +15 -0
  146. package/src/ai/types.ts +30 -0
  147. package/src/app/components/AiAssistant/AiAssistantPanel.tsx +144 -0
  148. package/src/app/components/AiAssistant/AiAssistantProvider.tsx +215 -0
  149. package/src/app/components/AiAssistant/AiConversation.tsx +737 -0
  150. package/src/app/components/AiAssistant/AiToolResult.tsx +99 -0
  151. package/src/app/components/AiAssistant/dockState.ts +86 -0
  152. package/src/app/components/AiAssistant/toolLabel.ts +24 -0
  153. package/src/app/components/AiAssistant/useAiChat.ts +147 -0
  154. package/src/app/components/AiAssistant/useAiConnections.ts +86 -0
  155. package/src/app/components/AiAssistant/usePageContext.ts +63 -0
  156. package/src/app/components/Auth/AccountLayout.tsx +58 -19
  157. package/src/app/components/Auth/AuthShell.tsx +27 -33
  158. package/src/app/components/Auth/RegistryInstallGate.tsx +4 -8
  159. package/src/app/components/Auth/installStatus.ts +29 -0
  160. package/src/app/components/Component/PageSliceResolver.tsx +1 -0
  161. package/src/app/components/Component/Preview.tsx +383 -189
  162. package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +4 -0
  163. package/src/app/components/Foundations/ColorGrid.tsx +13 -14
  164. package/src/app/components/Layout/Header.tsx +21 -15
  165. package/src/app/components/Layout/Main.tsx +2 -2
  166. package/src/app/components/Markdown/MarkdownComponents.tsx +30 -32
  167. package/src/app/components/McpIntegration/McpConfigDialog.tsx +12 -51
  168. package/src/app/components/Navigation/MobileNav.tsx +2 -2
  169. package/src/app/components/Navigation/SideNav.tsx +1 -15
  170. package/src/app/components/context/NavProvider.tsx +7 -3
  171. package/src/app/components/ui/attachment.tsx +50 -0
  172. package/src/app/components/ui/bubble.tsx +57 -0
  173. package/src/app/components/ui/button.tsx +3 -1
  174. package/src/app/components/ui/confirm-dialog.tsx +87 -0
  175. package/src/app/components/ui/dialog.tsx +7 -2
  176. package/src/app/components/ui/marker.tsx +56 -0
  177. package/src/app/components/ui/sidebar.tsx +3 -1
  178. package/src/app/components/util/index.ts +3 -12
  179. package/src/app/css/index.css +64 -0
  180. package/src/app/lib/ai/auth.ts +33 -0
  181. package/src/app/lib/ai/context.ts +99 -0
  182. package/src/app/lib/ai/model.ts +79 -0
  183. package/src/app/lib/ai/prompt.ts +36 -0
  184. package/src/app/lib/ai/resolve.ts +97 -0
  185. package/src/app/lib/ai/tools.ts +120 -0
  186. package/src/app/lib/api/methods.ts +13 -0
  187. package/src/app/lib/asset-storage.ts +35 -36
  188. package/src/app/lib/auth/api.ts +4 -12
  189. package/src/app/lib/auth/email.ts +96 -23
  190. package/src/app/lib/docs-api/page-rendering.ts +1 -1
  191. package/src/app/lib/docs-api/registry.ts +2 -3
  192. package/src/app/lib/docs-api/runtime-config.ts +47 -21
  193. package/src/app/lib/mcp/components.ts +523 -0
  194. package/src/app/lib/mcp/page-url.ts +24 -0
  195. package/src/app/lib/mcp/pages.ts +84 -0
  196. package/src/app/lib/mcp/result.ts +33 -0
  197. package/src/app/lib/mcp/server.ts +39 -318
  198. package/src/app/lib/mcp/tokens.ts +334 -0
  199. package/src/app/lib/mcp/tool.ts +19 -0
  200. package/src/app/lib/registry-api/asset-transfer.ts +119 -11
  201. package/src/app/lib/registry-api/errors.ts +2 -0
  202. package/src/app/lib/registry-api/handler.ts +7 -1
  203. package/src/app/lib/registry-api/transfer.ts +28 -6
  204. package/src/app/lib/server-module.ts +34 -0
  205. package/src/app/lib/utils.ts +20 -0
  206. package/src/app/next.config.mjs +26 -24
  207. package/src/app/pages/[...slug]/index.tsx +2 -1
  208. package/src/app/pages/_app.tsx +6 -2
  209. package/src/app/pages/_document.tsx +6 -0
  210. package/src/app/pages/account/ai.tsx +183 -0
  211. package/src/app/pages/account/index.tsx +68 -68
  212. package/src/app/pages/account/tokens.tsx +37 -7
  213. package/src/app/pages/account/users.tsx +75 -15
  214. package/src/app/pages/api/account/ai/keys/[connectionId].ts +60 -0
  215. package/src/app/pages/api/account/ai/keys/index.ts +20 -0
  216. package/src/app/pages/api/admin/users/[id]/[action].ts +80 -11
  217. package/src/app/pages/api/admin/users/index.ts +2 -2
  218. package/src/app/pages/api/ai/chat.ts +115 -0
  219. package/src/app/pages/api/ai/connections.ts +23 -0
  220. package/src/app/pages/api/auth/request-reset.ts +14 -16
  221. package/src/app/pages/api/docs/nav.json.ts +1 -1
  222. package/src/app/pages/api/install.ts +3 -3
  223. package/src/app/pages/api/mcp.ts +6 -0
  224. package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/complete.ts +11 -0
  225. package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/upload.ts +12 -0
  226. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +1 -1
  227. package/src/app/pages/cli/device.tsx +10 -5
  228. package/src/app/pages/foundations/colors.tsx +9 -8
  229. package/src/app/pages/foundations/index.tsx +3 -3
  230. package/src/app/pages/index.tsx +2 -2
  231. package/src/app/pages/install.tsx +7 -2
  232. package/src/app/pages/login.tsx +32 -27
  233. package/src/app/pages/reset-password.tsx +21 -20
  234. package/src/app/pages/system/component/[component]/index.tsx +2 -5
  235. package/src/app/pages/system/index.tsx +11 -11
  236. package/src/app/pages/system/pattern/[pattern]/index.tsx +2 -2
  237. package/src/app/pages/system/tokens/components/[component]/index.tsx +0 -4
  238. package/src/app-builder/README.md +2 -1
  239. package/src/app-builder/build.ts +218 -73
  240. package/src/app-builder/client-config.ts +3 -2
  241. package/src/app-builder/paths.ts +3 -22
  242. package/src/app-builder/static-export.ts +1 -1
  243. package/src/app-builder/vercel-output.ts +5 -4
  244. package/src/cache/README.md +4 -1
  245. package/src/cache/build-cache.ts +128 -43
  246. package/src/cache/build-inputs.ts +28 -0
  247. package/src/cache/index.ts +4 -0
  248. package/src/catalog/README.md +3 -0
  249. package/src/catalog/normalize.ts +2 -0
  250. package/src/catalog/previews.ts +6 -3
  251. package/src/catalog/types.ts +8 -0
  252. package/src/cli/create.ts +1 -2
  253. package/src/commands/build/index.ts +1 -1
  254. package/src/config/README.md +10 -9
  255. package/src/config/defaults.ts +7 -4
  256. package/src/config/from-env.ts +33 -0
  257. package/src/config/helpers.ts +1 -3
  258. package/src/config/index.ts +0 -3
  259. package/src/config/loader.ts +27 -3
  260. package/src/config/resolve-env.ts +28 -15
  261. package/src/config/runtime.ts +10 -5
  262. package/src/handlebars.ts +8 -2
  263. package/src/index.ts +33 -29
  264. package/src/nav/index.ts +2 -2
  265. package/src/nav/registry.ts +3 -3
  266. package/src/nav/workspace.ts +3 -3
  267. package/src/pipeline/README.md +1 -1
  268. package/src/pipeline/figma.ts +98 -49
  269. package/src/pipeline/styles.ts +1 -1
  270. package/src/pipeline/validation.ts +3 -3
  271. package/src/providers.ts +18 -0
  272. package/src/registry/asset-storage/define.ts +6 -6
  273. package/src/registry/asset-storage/resolve.ts +38 -37
  274. package/src/registry/asset-storage/types.ts +39 -17
  275. package/src/registry/auth/ai-keys.ts +62 -0
  276. package/src/registry/auth/crypto.ts +63 -1
  277. package/src/registry/auth/index.ts +11 -1
  278. package/src/registry/checkout/index.ts +60 -138
  279. package/src/registry/client.ts +60 -20
  280. package/src/registry/db/driver.ts +2 -2
  281. package/src/registry/db/schema.ts +49 -22
  282. package/src/registry/email/define.ts +15 -0
  283. package/src/registry/email/providers/smtp.ts +39 -0
  284. package/src/registry/email/resolve.ts +94 -0
  285. package/src/registry/email/types.ts +28 -0
  286. package/src/registry/path.ts +11 -0
  287. package/src/registry/publish/package.ts +9 -0
  288. package/src/registry/transfer.ts +4 -0
  289. package/src/store/registry.ts +7 -7
  290. package/src/transformers/plugins/ssr-render.ts +53 -16
  291. package/src/transformers/preview/component/builder.ts +19 -25
  292. package/src/transformers/preview/component/css.ts +101 -53
  293. package/src/transformers/preview/component/html.ts +6 -2
  294. package/src/transformers/preview/component/javascript.ts +14 -4
  295. package/src/transformers/preview/pattern/html.ts +3 -1
  296. package/src/transformers/preview/types.ts +14 -0
  297. package/src/transformers/utils/build.ts +93 -0
  298. package/src/transformers/utils/module.ts +4 -2
  299. package/src/types/config.ts +148 -20
  300. package/src/utils/README.md +1 -1
  301. package/src/utils/path.ts +6 -29
  302. package/templates/default/handoff.config.js.tpl +10 -2
  303. package/templates/default/handoff.config.ts.tpl +10 -2
  304. package/templates/default/handoff.config.with-examples.js.tpl +10 -2
  305. package/templates/default/handoff.config.with-examples.ts.tpl +10 -2
  306. package/templates/default/package.json.tpl +2 -2
  307. package/.editorconfig +0 -19
  308. package/.eslintrc.json +0 -4
  309. package/.prettierignore +0 -0
  310. package/.prettierrc.json +0 -8
  311. package/.vscode/settings.json +0 -17
  312. package/dist/config/validator.d.ts +0 -9
  313. package/dist/config/validator.js +0 -24
  314. package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +0 -17
  315. package/dist/registry/asset-storage/adapters/vercel-blob.js +0 -87
  316. package/src/app/components/old/Header.tsx +0 -89
  317. package/src/app/lib/mcp/shape.ts +0 -596
  318. package/src/app/pages/assets/fonts.tsx +0 -129
  319. package/src/app/pages/assets/index.tsx +0 -91
  320. package/src/app/pages/assets/logos.tsx +0 -75
  321. package/src/config/validator.ts +0 -22
  322. package/src/registry/asset-storage/adapters/vercel-blob.ts +0 -51
@@ -1,3 +1,4 @@
1
+ import { PreviewWidth } from '@handoff/transformers/preview/types';
1
2
  import { PreviewObject } from '@handoff/types/preview';
2
3
  import { Filter, evaluateFilter } from '@handoff/utils/filter';
3
4
  import React from 'react';
@@ -13,6 +14,7 @@ export interface ComponentDisplaySliceProps {
13
14
  showPreview?: boolean;
14
15
  showCodeHighlight?: boolean;
15
16
  defaultHeight?: string;
17
+ defaultWidth?: PreviewWidth;
16
18
  filterBy?: Filter;
17
19
  codeHighlight?: boolean;
18
20
  }
@@ -38,6 +40,7 @@ const ComponentDisplaySlice: React.FC<ComponentDisplaySliceProps> = ({
38
40
  showPreview = true,
39
41
  showCodeHighlight,
40
42
  defaultHeight,
43
+ defaultWidth,
41
44
  filterBy,
42
45
  codeHighlight = true,
43
46
  }) => {
@@ -68,6 +71,7 @@ const ComponentDisplaySlice: React.FC<ComponentDisplaySliceProps> = ({
68
71
  title={title}
69
72
  component={filteredPreview}
70
73
  defaultHeight={finalDefaultHeight}
74
+ defaultWidth={defaultWidth}
71
75
  onPreviewChange={onPreviewChange}
72
76
  />
73
77
  )}
@@ -16,7 +16,7 @@ import {
16
16
  import { Separator } from '../ui/separator';
17
17
  import { Sheet, SheetClose, SheetContent, SheetHeader } from '../ui/sheet';
18
18
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
19
- import { hexToRgb } from '../util/colors';
19
+ import { hexToRgb, hexToRgbaCss } from '../util/colors';
20
20
  import ColorContrast from './ColorContrast';
21
21
  import ColorInfo from './ColorInfo';
22
22
  import ColorSpaces from './ColorSpaces';
@@ -25,7 +25,6 @@ import ColorTailwind from './ColorTailwind';
25
25
  type ColorGridProps = {
26
26
  title: string;
27
27
  group: string;
28
- description: string;
29
28
  colors: CoreTypes.IColorObject[];
30
29
  };
31
30
 
@@ -33,7 +32,7 @@ const LargeColorGrid: React.FC<{ colors: CoreTypes.IColorObject[]; setOpen: (col
33
32
  <div className="@container">
34
33
  <div className="mb-6 grid grid-cols-1 gap-6 @md:grid-cols-2">
35
34
  {colors.map((color) => (
36
- <a href="#" className="flex flex-col items-start" key={color.group + '-' + color.name}>
35
+ <div className="flex flex-col items-start" key={color.group + '-' + color.name}>
37
36
  <div
38
37
  className="group relative mb-2 block h-32 w-full rounded-lg"
39
38
  style={{ background: color.value ?? '', backgroundBlendMode: color.blend ?? '' }}
@@ -42,7 +41,7 @@ const LargeColorGrid: React.FC<{ colors: CoreTypes.IColorObject[]; setOpen: (col
42
41
  </div>
43
42
  <p className="mb-1 text-sm font-medium line-clamp-1" title={color.name}>{color.name}</p>
44
43
  <small className="font-mono text-xs font-light text-gray-400 line-clamp-1" title={color.value}>{color.value}</small>
45
- </a>
44
+ </div>
46
45
  ))}
47
46
  </div>
48
47
  </div>
@@ -52,10 +51,11 @@ const SmallColorGrid: React.FC<{ colors: CoreTypes.IColorObject[]; setOpen: (col
52
51
  // group colors by subgroup
53
52
  const groupedColors = colors.reduce(
54
53
  (acc, color) => {
55
- if (!acc[color.subgroup]) {
56
- acc[color.subgroup] = [];
54
+ const subgroup = color.subgroup ?? '';
55
+ if (!acc[subgroup]) {
56
+ acc[subgroup] = [];
57
57
  }
58
- acc[color.subgroup].push(color);
58
+ acc[subgroup].push(color);
59
59
  return acc;
60
60
  },
61
61
  {} as Record<string, CoreTypes.IColorObject[]>
@@ -64,11 +64,11 @@ const SmallColorGrid: React.FC<{ colors: CoreTypes.IColorObject[]; setOpen: (col
64
64
  <>
65
65
  {Object.entries(groupedColors).map(([subgroup]) => (
66
66
  <div className="mb-5" key={`group-${subgroup}`}>
67
- <HeadersType.H4 key={subgroup}>{startCase(subgroup.replaceAll('-', ' '))}</HeadersType.H4>
67
+ {subgroup && <HeadersType.H4>{startCase(subgroup.replaceAll('-', ' '))}</HeadersType.H4>}
68
68
  <div className="grid grid-cols-[repeat(auto-fit,minmax(60px,60px))] gap-6">
69
69
  {groupedColors[subgroup].map((color) => (
70
70
  <React.Fragment key={`group-${subgroup}-${color.name}`}>
71
- <a href="" className="flex flex-col">
71
+ <div className="flex flex-col">
72
72
  <div
73
73
  className="group relative mb-2 block h-14 rounded-lg"
74
74
  style={{ background: color.value ?? '', backgroundBlendMode: color.blend ?? '' }}
@@ -77,7 +77,7 @@ const SmallColorGrid: React.FC<{ colors: CoreTypes.IColorObject[]; setOpen: (col
77
77
  </div>
78
78
  <p className="mb-1 text-xs font-medium line-clamp-1" title={color.name}>{color.name}</p>
79
79
  <small className="font-mono text-xs font-light text-gray-400 line-clamp-1" title={color.value}>{color.value}</small>
80
- </a>
80
+ </div>
81
81
  </React.Fragment>
82
82
  ))}
83
83
  </div>
@@ -87,7 +87,7 @@ const SmallColorGrid: React.FC<{ colors: CoreTypes.IColorObject[]; setOpen: (col
87
87
  );
88
88
  };
89
89
 
90
- const ColorGrid: React.FC<ColorGridProps> = ({ title, description, colors, group }) => {
90
+ const ColorGrid: React.FC<ColorGridProps> = ({ title, colors, group }) => {
91
91
  const [open, setOpen] = React.useState(false);
92
92
  const [selectedColor, setSelectedColor] = React.useState(null);
93
93
  const openSheet = (color) => {
@@ -96,8 +96,7 @@ const ColorGrid: React.FC<ColorGridProps> = ({ title, description, colors, group
96
96
  };
97
97
  return (
98
98
  <div id={`${group}-colors`} className="scroll-mt-24 scroll-smooth pb-10">
99
- <h3 className="text-lg font-medium">{title}</h3>
100
- <p className="mb-8">{description}</p>
99
+ <h3 className="mb-8 text-lg font-medium">{title}</h3>
101
100
  {colors.length < 5 ? <LargeColorGrid colors={colors} setOpen={openSheet} /> : <SmallColorGrid colors={colors} setOpen={openSheet} />}
102
101
  <ColorSheet color={selectedColor} open={open} setOpen={setOpen} />
103
102
  </div>
@@ -122,7 +121,7 @@ const ColorDropdown: React.FC<{ color: CoreTypes.IColorObject; openSheet: (color
122
121
  <Copy className="text-gray-400" /> HEX
123
122
  <DropdownMenuShortcut className="max-w-[100px] truncate">{color.value}</DropdownMenuShortcut>
124
123
  </DropdownMenuItem>
125
- <DropdownMenuItem onClick={() => navigator.clipboard.writeText('rgba(0, 49, 82, 1)')}>
124
+ <DropdownMenuItem onClick={() => navigator.clipboard.writeText(hexToRgbaCss(color.value) ?? color.value)}>
126
125
  <Copy className="text-gray-400" /> RGBA
127
126
  <DropdownMenuShortcut className="max-w-[100px] truncate">{hexToRgb(color.value)}</DropdownMenuShortcut>
128
127
  </DropdownMenuItem>
@@ -1,5 +1,6 @@
1
1
  import Link from 'next/link';
2
2
  import { useEffect, useState } from 'react';
3
+ import { AiAssistantTrigger } from '../../components/AiAssistant/AiAssistantProvider';
3
4
  import { McpConfigDialog } from '../../components/McpIntegration/McpConfigDialog';
4
5
  import { ModeToggle } from '../../components/ModeSwitcher';
5
6
  import { MainNav } from '../../components/Navigation/MainNav';
@@ -9,7 +10,7 @@ import { useConfigContext } from '../context/ConfigContext';
9
10
  import { AuthControls } from '../Auth/AuthControls';
10
11
  import { RuntimeModeBadge } from './RuntimeModeBadge';
11
12
 
12
- export function Header() {
13
+ export function Header({ hideNav = false }: { hideNav?: boolean }) {
13
14
  const context = useConfigContext();
14
15
  const [isScrolled, setIsScrolled] = useState(false);
15
16
  const runtimeMode = context.config?.runtime?.mode ?? 'workspace';
@@ -26,30 +27,35 @@ export function Header() {
26
27
  return () => window.removeEventListener('scroll', handleScroll);
27
28
  }, []);
28
29
 
30
+ const logo = <img className="max-h-5" src={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/logo.svg`} alt={context.config?.app?.title} />;
31
+
29
32
  return (
30
33
  <div
31
34
  className={cn(
32
- 'sticky top-0 z-50 py-4 shadow-[0_0_3px_0_rgba(0,0,0,0.15)] backdrop-blur-sm transition-all duration-300',
33
- isScrolled && 'bg-background/70 py-3 shadow-[0_0_4px_0_rgba(0,0,0,0.15)]'
35
+ 'ai-dock-bleed h-17 sticky top-0 z-50 py-4 shadow-[0_0_3px_0_rgba(0,0,0,0.15)] backdrop-blur-sm transition-all duration-300',
36
+ isScrolled && 'bg-background/70 shadow-[0_0_4px_0_rgba(0,0,0,0.15)]'
34
37
  )}
35
38
  >
36
39
  <header className="border-grid container mx-auto w-full max-w-[1500px] bg-transparent px-8">
37
40
  <div className="mx-auto flex items-center justify-between @container">
38
41
  <div className="flex items-center gap-3">
39
- <Link href="/">
40
- <img className="max-h-5" src={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/logo.svg`} alt={context.config?.app?.title} />
41
- </Link>
42
+ {hideNav ? logo : <Link href="/">{logo}</Link>}
42
43
  {!isStaticSnapshot && <RuntimeModeBadge mode={runtimeMode} />}
43
44
  </div>
44
- <div className="hidden items-center gap-4 @2xl:flex">
45
- <MainNav />
46
- <McpConfigDialog />
47
- {runtimeMode === 'registry' ? <AuthControls /> : null}
48
- <ModeToggle />
49
- </div>
50
- <div className="flex items-center gap-4 @2xl:hidden">
51
- <MobileNav />
52
- </div>
45
+ {hideNav ? null : (
46
+ <>
47
+ <div className="hidden items-center gap-4 @2xl:flex">
48
+ <MainNav />
49
+ <AiAssistantTrigger />
50
+ <McpConfigDialog />
51
+ {runtimeMode === 'registry' ? <AuthControls /> : null}
52
+ <ModeToggle />
53
+ </div>
54
+ <div className="flex items-center gap-4 @2xl:hidden">
55
+ <MobileNav />
56
+ </div>
57
+ </>
58
+ )}
53
59
  </div>
54
60
  </header>
55
61
  </div>
@@ -5,7 +5,7 @@ import { Header } from '../../components/Layout/Header';
5
5
  import { ThemeProvider } from '../../components/util/theme-provider';
6
6
  import SideNav from '../Navigation/SideNav';
7
7
  import { ConfigContextProvider } from '../context/ConfigContext';
8
- import { hasRenderableNav } from '../../lib/utils';
8
+ import { hasRenderableNav, pageTitle } from '../../lib/utils';
9
9
  import { SidebarInset, SidebarProvider } from '../ui/sidebar';
10
10
  import { SectionLink } from '../util';
11
11
 
@@ -34,7 +34,7 @@ export default function Layout<LayoutComponentProps>({ children, config, menu, m
34
34
  <ConfigContextProvider defaultConfig={config} defaultMenu={menu}>
35
35
  <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
36
36
  <Head>
37
- <title>{metadata.metaTitle}</title>
37
+ <title>{pageTitle(metadata, config?.app?.title)}</title>
38
38
  <meta name="description" content={metadata.metaDescription} />
39
39
  </Head>
40
40
  <div className="absolute left-[-200px] top-[-200px] z-[-1] h-[400px] w-[600px] bg-[#111111] opacity-[0.05] blur-[350px]">
@@ -70,38 +70,36 @@ function extractText(node: any): string {
70
70
 
71
71
  const Headings = (element: any) => {
72
72
  const { children, node } = element;
73
- if (children[0]) {
74
- const type = node.tagName || 'h6';
75
-
76
- let anchor = extractText(children).toLowerCase();
77
- anchor = anchor.replace(/[^a-zA-Z0-9 ]/g, '');
78
- anchor = anchor.replace(/ /g, '-');
79
-
80
- const container = (children: React.ReactNode): React.ReactNode => (
81
- <>
82
- {children}
83
- <a id={anchor} href={`#${anchor}`} className="doc-link"></a>
84
- </>
85
- );
86
-
87
- switch (type) {
88
- case 'h1':
89
- return <h1 className="text-2xl font-bold">{container(children)}</h1>;
90
- case 'h2':
91
- return <h2 className="text-xl font-bold">{container(children)}</h2>;
92
- case 'h3':
93
- return <h3 className="text-lg font-bold">{container(children)}</h3>;
94
- case 'h4':
95
- return <h4 className="text-base font-bold">{container(children)}</h4>;
96
- case 'h5':
97
- return <h5 className="text-sm font-bold">{container(children)}</h5>;
98
- case 'h6':
99
- return <h6 className="text-xs font-bold">{container(children)}</h6>;
100
- default:
101
- return <h6 className="text-xs font-bold">{container(children)}</h6>;
102
- }
103
- } else {
104
- return <h1>children</h1>;
73
+ if (React.Children.count(children) === 0) return <></>;
74
+
75
+ const type = node.tagName || 'h6';
76
+
77
+ let anchor = extractText(children).toLowerCase();
78
+ anchor = anchor.replace(/[^a-zA-Z0-9 ]/g, '');
79
+ anchor = anchor.replace(/ /g, '-');
80
+
81
+ const container = (children: React.ReactNode): React.ReactNode => (
82
+ <>
83
+ {children}
84
+ <a id={anchor} href={`#${anchor}`} className="doc-link"></a>
85
+ </>
86
+ );
87
+
88
+ switch (type) {
89
+ case 'h1':
90
+ return <h1 className="text-2xl font-bold">{container(children)}</h1>;
91
+ case 'h2':
92
+ return <h2 className="text-xl font-bold">{container(children)}</h2>;
93
+ case 'h3':
94
+ return <h3 className="text-lg font-bold">{container(children)}</h3>;
95
+ case 'h4':
96
+ return <h4 className="text-base font-bold">{container(children)}</h4>;
97
+ case 'h5':
98
+ return <h5 className="text-sm font-bold">{container(children)}</h5>;
99
+ case 'h6':
100
+ return <h6 className="text-xs font-bold">{container(children)}</h6>;
101
+ default:
102
+ return <h6 className="text-xs font-bold">{container(children)}</h6>;
105
103
  }
106
104
  };
107
105
 
@@ -134,18 +134,13 @@ type EndpointStatus = 'checking' | 'live' | 'unreachable';
134
134
  let cachedStatus: EndpointStatus | null = null;
135
135
  let inFlight: Promise<EndpointStatus> | null = null;
136
136
 
137
- /**
138
- * Statuses that prove the route is being served. A GET is the cheapest question to ask: the handler
139
- * answers 405 without ever building an MCP server, and a registry answers 401 ahead of that because
140
- * the credential check runs first. Anything else, 404 and 5xx included, means it is not there.
141
- */
142
- const LIVE_STATUSES = new Set([200, 401, 403, 405]);
143
-
144
137
  const probeEndpoint = (url: string): Promise<EndpointStatus> => {
145
138
  if (cachedStatus) return Promise.resolve(cachedStatus);
146
139
  if (inFlight) return inFlight;
147
- inFlight = fetch(url, { method: 'GET' })
148
- .then((res): EndpointStatus => (LIVE_STATUSES.has(res.status) ? 'live' : 'unreachable'))
140
+ // The endpoint answers HEAD with 204 before the credential check. Thus a served route never logs an
141
+ // error status to the console.
142
+ inFlight = fetch(url, { method: 'HEAD' })
143
+ .then((res): EndpointStatus => (res.ok ? 'live' : 'unreachable'))
149
144
  .catch((): EndpointStatus => 'unreachable')
150
145
  .then((status) => {
151
146
  cachedStatus = status;
@@ -184,29 +179,19 @@ const StatusDot: React.FC<{ status: EndpointStatus; className?: string }> = ({ s
184
179
  <span role="status" aria-label={DOT[status].label} title={DOT[status].label} className={cn('size-1.5 rounded-full', DOT[status].className, className)} />
185
180
  );
186
181
 
187
- const Snippet: React.FC<{ label: string; value: string; compact?: boolean }> = ({ label, value, compact }) => (
182
+ const Snippet: React.FC<{ label: string; value: string }> = ({ label, value }) => (
188
183
  <div className="min-w-0 space-y-1.5">
189
184
  <div className="flex items-center justify-between gap-2">
190
185
  <code className="truncate rounded bg-muted px-1.5 py-0.5 text-[11px] text-muted-foreground">{label}</code>
191
186
  <CopyToClipboard value={value} tooltip={`Copy ${label}`} showTooltip={false} className="relative size-7 shrink-0" />
192
187
  </div>
193
- <pre
194
- className={cn(
195
- 'overflow-auto rounded-md border bg-muted/50 p-3 leading-relaxed',
196
- compact ? 'max-h-48 text-[10px]' : 'max-h-64 text-xs'
197
- )}
198
- >
188
+ <pre className="max-h-64 overflow-auto rounded-md border bg-muted/50 p-3 text-xs leading-relaxed">
199
189
  <code>{value}</code>
200
190
  </pre>
201
191
  </div>
202
192
  );
203
193
 
204
- const ClientConfig: React.FC<{ client: McpClient; name: string; target: McpTarget; compact?: boolean }> = ({
205
- client,
206
- name,
207
- target,
208
- compact,
209
- }) => {
194
+ const ClientConfig: React.FC<{ client: McpClient; name: string; target: McpTarget }> = ({ client, name, target }) => {
210
195
  // A one-click install would write the `<your-token>` placeholder verbatim and register a server
211
196
  // that cannot connect, so a registry gets the snippet and the token pointer instead.
212
197
  const deeplink = isRegistryRuntime ? undefined : client.deeplink?.(name, target);
@@ -222,8 +207,8 @@ const ClientConfig: React.FC<{ client: McpClient; name: string; target: McpTarge
222
207
  </a>
223
208
  </Button>
224
209
  )}
225
- {command && <Snippet label="command line" value={command} compact={compact} />}
226
- <Snippet label={client.configPath} value={JSON.stringify(client.file(name, target), null, 2)} compact={compact} />
210
+ {command && <Snippet label="command line" value={command} />}
211
+ <Snippet label={client.configPath} value={JSON.stringify(client.file(name, target), null, 2)} />
227
212
  <p className="text-xs text-muted-foreground">{client.note}</p>
228
213
  </div>
229
214
  );
@@ -261,8 +246,8 @@ const Credential: React.FC = () => (
261
246
  </p>
262
247
  );
263
248
 
264
- /** Endpoint, per-client config and credential guidance. Shared by the dialog and the mobile nav. */
265
- const ConnectionDetails: React.FC<{ url: string; status: EndpointStatus; compact?: boolean }> = ({ url, status, compact }) => {
249
+ /** Endpoint, per-client config and credential guidance. */
250
+ const ConnectionDetails: React.FC<{ url: string; status: EndpointStatus }> = ({ url, status }) => {
266
251
  const name = useServerName();
267
252
  const target: McpTarget = { url, headers: isRegistryRuntime ? CREDENTIAL_HEADERS : undefined };
268
253
 
@@ -279,7 +264,7 @@ const ConnectionDetails: React.FC<{ url: string; status: EndpointStatus; compact
279
264
  </TabsList>
280
265
  {CLIENTS.map((client) => (
281
266
  <TabsContent key={client.id} value={client.id} className="min-w-0">
282
- <ClientConfig client={client} name={name} target={target} compact={compact} />
267
+ <ClientConfig client={client} name={name} target={target} />
283
268
  </TabsContent>
284
269
  ))}
285
270
  </Tabs>
@@ -323,27 +308,3 @@ const McpDialog: React.FC = () => {
323
308
  </Dialog>
324
309
  );
325
310
  };
326
-
327
- /**
328
- * Inline variant for the mobile nav, which is itself a sheet, so a nested dialog would sit on top of
329
- * it. It owns its divider, so a build without the endpoint renders nothing and leaves no stray rule.
330
- */
331
- export function McpConfigMobile() {
332
- return isAvailable ? <McpMobile /> : null;
333
- }
334
-
335
- const McpMobile: React.FC = () => {
336
- const url = useMcpEndpoint();
337
- const status = useEndpointStatus(url);
338
-
339
- return (
340
- <div className="mt-4 space-y-3 border-t pt-4">
341
- <div className="flex flex-wrap items-center gap-2">
342
- <McpIcon className="h-4 w-4" />
343
- <span className="text-sm font-medium">Connect a coding agent</span>
344
- <RuntimeModeBadge mode={mode} />
345
- </div>
346
- <ConnectionDetails url={url} status={status} compact />
347
- </div>
348
- );
349
- };
@@ -3,7 +3,7 @@ import { Menu, Monitor, Moon, Sun, X } from 'lucide-react';
3
3
  import { useTheme } from 'next-themes';
4
4
  import Link from 'next/link';
5
5
  import { useRouter } from 'next/router';
6
- import { McpConfigMobile } from '../../components/McpIntegration/McpConfigDialog';
6
+ import { AiAssistantMobileTrigger } from '../../components/AiAssistant/AiAssistantProvider';
7
7
  import { Button } from '../../components/ui/button';
8
8
  import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '../../components/ui/sheet';
9
9
  import { cn } from '../../lib/utils';
@@ -52,6 +52,7 @@ export function MobileNav() {
52
52
  </SheetTrigger>
53
53
  </SheetHeader>
54
54
  <div className="mt-8 flex flex-col space-y-4">
55
+ <AiAssistantMobileTrigger />
55
56
  {menu &&
56
57
  menu.map((section) => {
57
58
  const isActive = trimSlashes(router.asPath).startsWith(trimSlashes(section.path));
@@ -70,7 +71,6 @@ export function MobileNav() {
70
71
  </Link>
71
72
  );
72
73
  })}
73
- <McpConfigMobile />
74
74
  <div className="mt-4 border-t pt-4">
75
75
  <Button variant="ghost" className="w-full justify-start font-normal" onClick={toggleTheme}>
76
76
  {theme === 'light' && (
@@ -31,23 +31,9 @@ import {
31
31
  SidebarMenuSub,
32
32
  SidebarSeparator,
33
33
  } from '../../components/ui/sidebar';
34
- import { normalizePathForMatch } from '../../lib/utils';
34
+ import { normalizePathForMatch, stripBasePath } from '../../lib/utils';
35
35
  import { useNav } from '../context/NavProvider';
36
36
 
37
- /**
38
- * `next/link` re-applies `basePath`, but the menu item paths already carry the base prefix (built by
39
- * the server navigation resolver). Strip the leading base segment once so the link is not
40
- * double-prefixed; with no base path this just returns a leading-slash route.
41
- */
42
- const stripBasePath = (p: string): string => {
43
- const base = (process.env.HANDOFF_APP_BASE_PATH ?? '').replace(/^\/+|\/+$/g, '');
44
- let rel = p.replace(/^\/+/, '');
45
- if (base && (rel === base || rel.startsWith(`${base}/`))) {
46
- rel = rel.slice(base.length).replace(/^\/+/, '');
47
- }
48
- return `/${rel}`;
49
- };
50
-
51
37
  const NormalMenuItem = ({ title, icon, path }) => {
52
38
  const router = useRouter();
53
39
  const isActive = normalizePathForMatch(path) === normalizePathForMatch(router.asPath);
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import type { NavData, SectionLink } from '@handoff/nav';
4
+ import { useRouter } from 'next/router';
4
5
  import React, { createContext, useContext, useEffect, useState } from 'react';
5
6
 
6
7
  export type { NavData, NavEntity } from '@handoff/nav';
@@ -13,6 +14,8 @@ interface INavContext {
13
14
  const EMPTY_NAV: NavData = { shell: [] };
14
15
  const NavContext = createContext<INavContext>({ nav: EMPTY_NAV, currentSectionId: '' });
15
16
  const isRegistryMode = process.env.HANDOFF_RUNTIME_MODE === 'registry';
17
+ /** Routes that render no navigation, so they do not fetch it. */
18
+ const NAVLESS_ROUTES = ['/install', '/login', '/reset-password'];
16
19
 
17
20
  // A module-level fulfilled cache and in-flight guard make the registry refresh a single hard-load
18
21
  // operation, including StrictMode remounts. A browser hard reload resets both and observes publishes.
@@ -43,9 +46,11 @@ export const NavProvider: React.FC<{
43
46
  // Registry alone owns refresh state. Workspace renders the latest page prop directly, so soft
44
47
  // navigation cannot briefly expose the preceding page's tree and triggers no provider effect.
45
48
  const [registryNav, setRegistryNav] = useState<NavData>(() => cachedNav ?? initialNav);
49
+ const { pathname } = useRouter();
50
+ const hasNav = !NAVLESS_ROUTES.includes(pathname);
46
51
 
47
52
  useEffect(() => {
48
- if (!isRegistryMode || cachedNav) return;
53
+ if (!isRegistryMode || !hasNav || cachedNav) return;
49
54
  let active = true;
50
55
  loadNav().then((data) => {
51
56
  if (active && data) setRegistryNav(data);
@@ -53,8 +58,7 @@ export const NavProvider: React.FC<{
53
58
  return () => {
54
59
  active = false;
55
60
  };
56
- // eslint-disable-next-line react-hooks/exhaustive-deps
57
- }, []);
61
+ }, [hasNav]);
58
62
 
59
63
  const nav = isRegistryMode ? registryNav : initialNav;
60
64
  return <NavContext.Provider value={{ nav, currentSectionId }}>{children}</NavContext.Provider>;
@@ -0,0 +1,50 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority';
2
+ import * as React from 'react';
3
+
4
+ import { cn } from '../../lib/utils';
5
+ import { Button } from './button';
6
+
7
+ const attachmentVariants = cva(
8
+ 'group/attachment relative flex w-fit min-w-40 max-w-full shrink-0 flex-wrap items-center rounded-xl border bg-card text-card-foreground focus-within:ring-1 focus-within:ring-ring/50',
9
+ {
10
+ variants: {
11
+ size: {
12
+ default:
13
+ 'gap-2 text-sm has-data-[slot=attachment-content]:px-2.5 has-data-[slot=attachment-content]:py-2 has-data-[slot=attachment-media]:p-2',
14
+ sm: 'gap-2.5 text-xs has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5 has-data-[slot=attachment-media]:p-1.5',
15
+ xs: 'gap-1.5 rounded-lg text-xs has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1 has-data-[slot=attachment-media]:p-1',
16
+ },
17
+ },
18
+ }
19
+ );
20
+
21
+ function Attachment({ className, size = 'default', ...props }: React.ComponentProps<'div'> & VariantProps<typeof attachmentVariants>) {
22
+ return <div data-slot="attachment" data-size={size} className={cn(attachmentVariants({ size }), className)} {...props} />;
23
+ }
24
+
25
+ function AttachmentMedia({ className, ...props }: React.ComponentProps<'div'>) {
26
+ return (
27
+ <div
28
+ data-slot="attachment-media"
29
+ className={cn(
30
+ "relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-foreground group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-md [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5",
31
+ className
32
+ )}
33
+ {...props}
34
+ />
35
+ );
36
+ }
37
+
38
+ function AttachmentContent({ className, ...props }: React.ComponentProps<'div'>) {
39
+ return <div data-slot="attachment-content" className={cn('min-w-0 max-w-full flex-1 leading-tight', className)} {...props} />;
40
+ }
41
+
42
+ function AttachmentTitle({ className, ...props }: React.ComponentProps<'span'>) {
43
+ return <span data-slot="attachment-title" className={cn('block min-w-0 max-w-full truncate font-medium', className)} {...props} />;
44
+ }
45
+
46
+ function AttachmentAction({ className, variant, size = 'icon-xs', ...props }: React.ComponentProps<typeof Button>) {
47
+ return <Button data-slot="attachment-action" variant={variant ?? 'ghost'} size={size} className={cn(className)} {...props} />;
48
+ }
49
+
50
+ export { Attachment, AttachmentAction, AttachmentContent, AttachmentMedia, AttachmentTitle };
@@ -0,0 +1,57 @@
1
+ import { Slot } from '@radix-ui/react-slot';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import * as React from 'react';
4
+
5
+ import { cn } from '../../lib/utils';
6
+
7
+ const bubbleVariants = cva(
8
+ 'group/bubble relative flex w-fit max-w-[80%] min-w-0 flex-col gap-1 data-[align=end]:self-end data-[variant=ghost]:max-w-full',
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ '*:data-[slot=bubble-content]:bg-primary *:data-[slot=bubble-content]:text-primary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-primary/80',
14
+ secondary:
15
+ '*:data-[slot=bubble-content]:bg-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-secondary/80',
16
+ muted: '*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/80',
17
+ outline:
18
+ '*:data-[slot=bubble-content]:border-border *:data-[slot=bubble-content]:bg-background [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground',
19
+ ghost:
20
+ '*:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted',
21
+ destructive:
22
+ '*:data-[slot=bubble-content]:bg-destructive/10 *:data-[slot=bubble-content]:text-destructive dark:*:data-[slot=bubble-content]:bg-destructive/20',
23
+ },
24
+ },
25
+ defaultVariants: {
26
+ variant: 'default',
27
+ },
28
+ }
29
+ );
30
+
31
+ function Bubble({
32
+ variant = 'default',
33
+ align = 'start',
34
+ className,
35
+ ...props
36
+ }: React.ComponentProps<'div'> & VariantProps<typeof bubbleVariants> & { align?: 'start' | 'end' }) {
37
+ return (
38
+ <div data-slot="bubble" data-variant={variant} data-align={align} className={cn(bubbleVariants({ variant }), className)} {...props} />
39
+ );
40
+ }
41
+
42
+ function BubbleContent({ asChild = false, className, ...props }: React.ComponentProps<'div'> & { asChild?: boolean }) {
43
+ const Comp = asChild ? Slot : 'div';
44
+
45
+ return (
46
+ <Comp
47
+ data-slot="bubble-content"
48
+ className={cn(
49
+ 'wrap-break-word w-fit min-w-0 max-w-full overflow-hidden rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed group-data-[align=end]/bubble:self-end',
50
+ className
51
+ )}
52
+ {...props}
53
+ />
54
+ );
55
+ }
56
+
57
+ export { Bubble, BubbleContent, bubbleVariants };
@@ -9,7 +9,7 @@ const buttonVariants = cva(
9
9
  {
10
10
  variants: {
11
11
  variant: {
12
- default: 'bg-gray-800 text-white shadow-sm hover:bg-gray-700',
12
+ default: 'bg-neutral-800 text-white shadow-sm hover:bg-neutral-700',
13
13
  destructive: 'bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90',
14
14
  outline: 'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
15
15
  secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
@@ -21,6 +21,8 @@ const buttonVariants = cva(
21
21
  sm: 'h-8 rounded-md px-3 text-xs',
22
22
  lg: 'h-10 rounded-md px-8',
23
23
  icon: 'h-9 w-9',
24
+ 'icon-sm': 'h-8 w-8',
25
+ 'icon-xs': 'h-6 w-6',
24
26
  },
25
27
  },
26
28
  defaultVariants: {