@devxcrew/react-ui 0.0.0-stage → 0.1.7

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 (326) hide show
  1. package/README.md +85 -2
  2. package/package.json +105 -4
  3. package/src/blocks/agent-chat-workspace/agent-chat-workspace.tsx +162 -0
  4. package/src/blocks/agent-chat-workspace/index.ts +2 -0
  5. package/src/blocks/agent-provider-settings/agent-provider-settings.tsx +183 -0
  6. package/src/blocks/agent-provider-settings/index.ts +2 -0
  7. package/src/blocks/agent-task-workspace/agent-task-workspace.tsx +48 -0
  8. package/src/blocks/agent-task-workspace/index.ts +2 -0
  9. package/src/blocks/analysis-context/analysis-context.tsx +6 -0
  10. package/src/blocks/analysis-context/index.ts +1 -0
  11. package/src/blocks/app-header/app-header.tsx +159 -0
  12. package/src/blocks/app-header/index.ts +2 -0
  13. package/src/blocks/archived-chat-workspace/archived-chat-workspace.tsx +29 -0
  14. package/src/blocks/archived-chat-workspace/index.ts +2 -0
  15. package/src/blocks/auth/README.md +32 -0
  16. package/src/blocks/auth/admin-login-page.tsx +15 -0
  17. package/src/blocks/auth/admin-password-forgot-page.tsx +5 -0
  18. package/src/blocks/auth/auth-page-layout.tsx +90 -0
  19. package/src/blocks/auth/client-login-page.tsx +20 -0
  20. package/src/blocks/auth/client-password-forgot-page.tsx +5 -0
  21. package/src/blocks/auth/identity-management-desk.tsx +530 -0
  22. package/src/blocks/auth/index.ts +12 -0
  23. package/src/blocks/auth/login-page.tsx +206 -0
  24. package/src/blocks/auth/password-forgot-page.tsx +60 -0
  25. package/src/blocks/auth/portal-pages.tsx +32 -0
  26. package/src/blocks/auth/privileged-desk.tsx +55 -0
  27. package/src/blocks/auth/register-page.tsx +117 -0
  28. package/src/blocks/auth/session-boundary.tsx +296 -0
  29. package/src/blocks/auth/super-admin-login-page.tsx +15 -0
  30. package/src/blocks/auth/super-admin-password-forgot-page.tsx +5 -0
  31. package/src/blocks/blog/blog-card.tsx +156 -0
  32. package/src/blocks/blog/blog-reader.tsx +175 -0
  33. package/src/blocks/blog/blog-types.ts +47 -0
  34. package/src/blocks/blog/index.ts +3 -0
  35. package/src/blocks/chat-composer/chat-composer.tsx +140 -0
  36. package/src/blocks/chat-composer/index.ts +1 -0
  37. package/src/blocks/chat-history/chat-history.tsx +59 -0
  38. package/src/blocks/chat-history/index.ts +1 -0
  39. package/src/blocks/chat-response-progress/chat-response-progress.tsx +34 -0
  40. package/src/blocks/chat-response-progress/index.ts +2 -0
  41. package/src/blocks/chat-runtime-controls/chat-runtime-controls.tsx +56 -0
  42. package/src/blocks/chat-runtime-controls/index.ts +2 -0
  43. package/src/blocks/chat-runtime-trace/chat-runtime-trace.tsx +71 -0
  44. package/src/blocks/chat-runtime-trace/index.ts +2 -0
  45. package/src/blocks/codex-connection-settings/codex-connection-settings.tsx +42 -0
  46. package/src/blocks/codex-connection-settings/index.ts +1 -0
  47. package/src/blocks/dropzone/dropzone-file-item.tsx +113 -0
  48. package/src/blocks/dropzone/dropzone-types.ts +32 -0
  49. package/src/blocks/dropzone/dropzone.tsx +216 -0
  50. package/src/blocks/dropzone/index.ts +3 -0
  51. package/src/blocks/ecommerce/cart/cart-drawer.tsx +228 -0
  52. package/src/blocks/ecommerce/cart/cart-types.ts +27 -0
  53. package/src/blocks/ecommerce/cart/index.ts +2 -0
  54. package/src/blocks/ecommerce/categories/categories-types.ts +23 -0
  55. package/src/blocks/ecommerce/categories/category-showcase.tsx +125 -0
  56. package/src/blocks/ecommerce/categories/index.ts +2 -0
  57. package/src/blocks/ecommerce/checkout/checkout-types.ts +48 -0
  58. package/src/blocks/ecommerce/checkout/checkout-wizard.tsx +468 -0
  59. package/src/blocks/ecommerce/checkout/index.ts +2 -0
  60. package/src/blocks/ecommerce/comparison/comparison-types.ts +28 -0
  61. package/src/blocks/ecommerce/comparison/index.ts +2 -0
  62. package/src/blocks/ecommerce/comparison/product-comparison.tsx +174 -0
  63. package/src/blocks/ecommerce/coupon-wallet/coupon-wallet-types.ts +21 -0
  64. package/src/blocks/ecommerce/coupon-wallet/coupon-wallet.tsx +154 -0
  65. package/src/blocks/ecommerce/coupon-wallet/index.ts +2 -0
  66. package/src/blocks/ecommerce/delivery-tracker/delivery-tracker-types.ts +20 -0
  67. package/src/blocks/ecommerce/delivery-tracker/delivery-tracker.tsx +169 -0
  68. package/src/blocks/ecommerce/delivery-tracker/index.ts +2 -0
  69. package/src/blocks/ecommerce/index.ts +12 -0
  70. package/src/blocks/ecommerce/payment-methods/index.ts +2 -0
  71. package/src/blocks/ecommerce/payment-methods/payment-method-selector.tsx +224 -0
  72. package/src/blocks/ecommerce/payment-methods/payment-methods-types.ts +21 -0
  73. package/src/blocks/ecommerce/price-history/index.ts +2 -0
  74. package/src/blocks/ecommerce/price-history/price-history-chart.tsx +217 -0
  75. package/src/blocks/ecommerce/price-history/price-history-types.ts +16 -0
  76. package/src/blocks/ecommerce/reviews/index.ts +2 -0
  77. package/src/blocks/ecommerce/reviews/reviews-section.tsx +309 -0
  78. package/src/blocks/ecommerce/reviews/reviews-types.ts +26 -0
  79. package/src/blocks/ecommerce/wishlist/index.ts +2 -0
  80. package/src/blocks/ecommerce/wishlist/wishlist-grid.tsx +149 -0
  81. package/src/blocks/ecommerce/wishlist/wishlist-types.ts +19 -0
  82. package/src/blocks/execution-status/README.md +25 -0
  83. package/src/blocks/execution-status/execution-status.tsx +135 -0
  84. package/src/blocks/execution-status/index.ts +2 -0
  85. package/src/blocks/file-tree/file-icons.tsx +84 -0
  86. package/src/blocks/file-tree/file-tree-node.tsx +158 -0
  87. package/src/blocks/file-tree/file-tree-types.ts +33 -0
  88. package/src/blocks/file-tree/file-tree.tsx +163 -0
  89. package/src/blocks/file-tree/index.ts +5 -0
  90. package/src/blocks/filter-builder/filter-builder.tsx +169 -0
  91. package/src/blocks/filter-builder/filter-row.tsx +164 -0
  92. package/src/blocks/filter-builder/filter-types.ts +61 -0
  93. package/src/blocks/filter-builder/index.ts +12 -0
  94. package/src/blocks/footer/footer-types.ts +35 -0
  95. package/src/blocks/footer/index.ts +2 -0
  96. package/src/blocks/footer/site-footer.tsx +212 -0
  97. package/src/blocks/form/form-block.tsx +113 -0
  98. package/src/blocks/form/form-lookup-field.tsx +60 -0
  99. package/src/blocks/form/index.ts +4 -0
  100. package/src/blocks/handover-stack/handover-stack.tsx +41 -0
  101. package/src/blocks/handover-stack/index.ts +1 -0
  102. package/src/blocks/idea-handover/idea-handover-readiness.ts +41 -0
  103. package/src/blocks/idea-handover/idea-handover-workspace.tsx +150 -0
  104. package/src/blocks/idea-handover/index.ts +4 -0
  105. package/src/blocks/kanban/index.ts +11 -0
  106. package/src/blocks/kanban/kanban-board.tsx +146 -0
  107. package/src/blocks/kanban/kanban-card.tsx +149 -0
  108. package/src/blocks/kanban/kanban-column.tsx +102 -0
  109. package/src/blocks/kanban/kanban-types.ts +38 -0
  110. package/src/blocks/loader/global-loader.tsx +90 -0
  111. package/src/blocks/loader/index.ts +1 -0
  112. package/src/blocks/mascot/README.md +21 -0
  113. package/src/blocks/mascot/index.ts +2 -0
  114. package/src/blocks/mascot/mascot.tsx +202 -0
  115. package/src/blocks/master-list/README.md +21 -0
  116. package/src/blocks/master-list/index.ts +12 -0
  117. package/src/blocks/master-list/master-form.tsx +157 -0
  118. package/src/blocks/master-list/master-list-desk.tsx +370 -0
  119. package/src/blocks/master-list/master-list.tsx +189 -0
  120. package/src/blocks/master-list/types.ts +18 -0
  121. package/src/blocks/mermaid-preview/index.ts +1 -0
  122. package/src/blocks/mermaid-preview/mermaid-preview.tsx +164 -0
  123. package/src/blocks/notifications/README.md +7 -0
  124. package/src/blocks/notifications/index.ts +1 -0
  125. package/src/blocks/notifications/notification-center-page.tsx +73 -0
  126. package/src/blocks/pricing/index.ts +7 -0
  127. package/src/blocks/pricing/pricing-table.tsx +173 -0
  128. package/src/blocks/pricing/pricing-types.ts +34 -0
  129. package/src/blocks/product-card/index.ts +2 -0
  130. package/src/blocks/product-card/product-card-types.ts +35 -0
  131. package/src/blocks/product-card/product-card.tsx +183 -0
  132. package/src/blocks/table/data-table-block.tsx +313 -0
  133. package/src/blocks/table/data-table-filter-menu.tsx +48 -0
  134. package/src/blocks/table/data-table-icon-menu-trigger.tsx +44 -0
  135. package/src/blocks/table/data-table-menu-content.tsx +32 -0
  136. package/src/blocks/table/data-table-pagination.tsx +149 -0
  137. package/src/blocks/table/data-table-row-actions.tsx +53 -0
  138. package/src/blocks/table/data-table-status.tsx +42 -0
  139. package/src/blocks/table/data-table-toolbar.tsx +124 -0
  140. package/src/blocks/table/data-table-totals.tsx +31 -0
  141. package/src/blocks/table/index.ts +13 -0
  142. package/src/blocks/workspace/index.ts +19 -0
  143. package/src/blocks/workspace/workspace-action-card.tsx +73 -0
  144. package/src/blocks/workspace/workspace-application-header.tsx +67 -0
  145. package/src/blocks/workspace/workspace-entity-card.tsx +48 -0
  146. package/src/blocks/workspace/workspace-health-summary.tsx +15 -0
  147. package/src/blocks/workspace/workspace-metric-card.tsx +97 -0
  148. package/src/blocks/workspace/workspace-page-header.tsx +43 -0
  149. package/src/blocks/workspace/workspace-publish-status.tsx +15 -0
  150. package/src/blocks/workspace/workspace-route-checklist.tsx +28 -0
  151. package/src/blocks/workspace/workspace-runtime-status.tsx +48 -0
  152. package/src/blocks/workspace/workspace-section-card.tsx +42 -0
  153. package/src/components/accordion.tsx +65 -0
  154. package/src/components/alert-dialog.tsx +160 -0
  155. package/src/components/alert.tsx +68 -0
  156. package/src/components/aspect-ratio.tsx +22 -0
  157. package/src/components/attachment.tsx +195 -0
  158. package/src/components/avatar.tsx +90 -0
  159. package/src/components/badge.tsx +55 -0
  160. package/src/components/breadcrumb.tsx +100 -0
  161. package/src/components/bubble.tsx +123 -0
  162. package/src/components/button-group.tsx +79 -0
  163. package/src/components/button.tsx +71 -0
  164. package/src/components/calendar.tsx +185 -0
  165. package/src/components/card.tsx +100 -0
  166. package/src/components/carousel.tsx +229 -0
  167. package/src/components/chart.tsx +338 -0
  168. package/src/components/checkbox.tsx +27 -0
  169. package/src/components/collapsible.tsx +15 -0
  170. package/src/components/combobox.tsx +269 -0
  171. package/src/components/command.tsx +181 -0
  172. package/src/components/compact-model-switcher.tsx +128 -0
  173. package/src/components/context-menu.tsx +244 -0
  174. package/src/components/dialog.tsx +138 -0
  175. package/src/components/direction.tsx +1 -0
  176. package/src/components/drawer.tsx +208 -0
  177. package/src/components/dropdown-menu.tsx +257 -0
  178. package/src/components/empty.tsx +93 -0
  179. package/src/components/field.tsx +232 -0
  180. package/src/components/hover-card.tsx +45 -0
  181. package/src/components/input-group.tsx +146 -0
  182. package/src/components/input-otp.tsx +83 -0
  183. package/src/components/input.tsx +19 -0
  184. package/src/components/item.tsx +188 -0
  185. package/src/components/kbd.tsx +26 -0
  186. package/src/components/label.tsx +17 -0
  187. package/src/components/markdown-content.tsx +113 -0
  188. package/src/components/marker.tsx +67 -0
  189. package/src/components/menubar.tsx +267 -0
  190. package/src/components/message-scroller.tsx +91 -0
  191. package/src/components/message.tsx +84 -0
  192. package/src/components/native-select.tsx +54 -0
  193. package/src/components/navigation-menu.tsx +160 -0
  194. package/src/components/pagination.tsx +118 -0
  195. package/src/components/popover.tsx +74 -0
  196. package/src/components/progress.tsx +82 -0
  197. package/src/components/questionnaire.tsx +150 -0
  198. package/src/components/radio-group.tsx +35 -0
  199. package/src/components/resizable.tsx +41 -0
  200. package/src/components/rich-text-editor.tsx +604 -0
  201. package/src/components/scroll-area.tsx +47 -0
  202. package/src/components/select.tsx +189 -0
  203. package/src/components/separator.tsx +18 -0
  204. package/src/components/sheet.tsx +125 -0
  205. package/src/components/sidebar.tsx +693 -0
  206. package/src/components/skeleton.tsx +13 -0
  207. package/src/components/slider.tsx +51 -0
  208. package/src/components/sonner.tsx +45 -0
  209. package/src/components/sparkline.tsx +231 -0
  210. package/src/components/spinner.tsx +20 -0
  211. package/src/components/status-badge.tsx +114 -0
  212. package/src/components/switch.tsx +31 -0
  213. package/src/components/table.tsx +91 -0
  214. package/src/components/tabs.tsx +72 -0
  215. package/src/components/textarea.tsx +17 -0
  216. package/src/components/toast.tsx +222 -0
  217. package/src/components/toggle-group.tsx +85 -0
  218. package/src/components/toggle.tsx +42 -0
  219. package/src/components/tooltip.tsx +53 -0
  220. package/src/design-system/README.md +55 -0
  221. package/src/design-system/action-intents.ts +21 -0
  222. package/src/design-system/agent-manifest.ts +83 -0
  223. package/src/design-system/block-registry.ts +272 -0
  224. package/src/design-system/component-registry.ts +142 -0
  225. package/src/design-system/contracts.ts +69 -0
  226. package/src/design-system/defaults.ts +3 -0
  227. package/src/design-system/index.ts +29 -0
  228. package/src/design-system/mcp-server.ts +156 -0
  229. package/src/design-system/page-registry.ts +74 -0
  230. package/src/design-system/selection.ts +67 -0
  231. package/src/design-system/template-registry.ts +33 -0
  232. package/src/features/interface-topology/index.ts +14 -0
  233. package/src/features/interface-topology/interface-topology.types.ts +40 -0
  234. package/src/features/interface-topology/topology-control.tsx +18 -0
  235. package/src/features/interface-topology/topology-inspector.tsx +228 -0
  236. package/src/features/interface-topology/topology-marker.tsx +47 -0
  237. package/src/features/interface-topology/topology-region.tsx +44 -0
  238. package/src/features/interface-topology/use-interface-topology.ts +178 -0
  239. package/src/hooks/use-mobile.ts +19 -0
  240. package/src/hooks/use-scroll-follow.ts +22 -0
  241. package/src/index.ts +11 -0
  242. package/src/layouts/README.md +65 -0
  243. package/src/layouts/agent-workspace/agent-activity-rail.tsx +101 -0
  244. package/src/layouts/agent-workspace/agent-workspace.tsx +27 -0
  245. package/src/layouts/agent-workspace/agent-workspace.types.ts +28 -0
  246. package/src/layouts/agent-workspace/index.ts +7 -0
  247. package/src/layouts/blog-header/blog-header-types.ts +25 -0
  248. package/src/layouts/blog-header/blog-header.tsx +172 -0
  249. package/src/layouts/blog-header/index.ts +2 -0
  250. package/src/layouts/documentation-workspace/README.md +8 -0
  251. package/src/layouts/documentation-workspace/documentation-workspace.tsx +34 -0
  252. package/src/layouts/documentation-workspace/index.ts +1 -0
  253. package/src/layouts/ecommerce-header/ecommerce-actions.tsx +89 -0
  254. package/src/layouts/ecommerce-header/ecommerce-announcement.tsx +43 -0
  255. package/src/layouts/ecommerce-header/ecommerce-drawer.tsx +97 -0
  256. package/src/layouts/ecommerce-header/ecommerce-header-types.ts +59 -0
  257. package/src/layouts/ecommerce-header/ecommerce-header.tsx +47 -0
  258. package/src/layouts/ecommerce-header/ecommerce-navigation.tsx +98 -0
  259. package/src/layouts/ecommerce-header/ecommerce-search.tsx +86 -0
  260. package/src/layouts/ecommerce-header/index.ts +2 -0
  261. package/src/layouts/main-workspace/index.ts +28 -0
  262. package/src/layouts/mdi/index.ts +1 -0
  263. package/src/layouts/mdi/mdi.tsx +20 -0
  264. package/src/layouts/mdi-main/index.ts +27 -0
  265. package/src/layouts/mdi-main/mdi-app-catalog.generated.ts +159 -0
  266. package/src/layouts/mdi-main/mdi-app-catalog.ts +76 -0
  267. package/src/layouts/mdi-main/mdi-app-switcher.tsx +107 -0
  268. package/src/layouts/mdi-main/mdi-feature-settings.tsx +135 -0
  269. package/src/layouts/mdi-main/mdi-global-search.tsx +192 -0
  270. package/src/layouts/mdi-main/mdi-main.tsx +241 -0
  271. package/src/layouts/mdi-main/mdi-notifications-menu.tsx +197 -0
  272. package/src/layouts/mdi-main/mdi-profile-menu.tsx +79 -0
  273. package/src/layouts/mdi-main/mdi-sidebar-navigation.tsx +226 -0
  274. package/src/layouts/mdi-main/mdi-sidebar.tsx +110 -0
  275. package/src/layouts/mdi-main/mdi-status-bar.tsx +26 -0
  276. package/src/layouts/mdi-main/mdi-top-menu-control.ts +4 -0
  277. package/src/layouts/mdi-main/mdi-top-menu.tsx +117 -0
  278. package/src/layouts/mdi-main/mdi-topology.tsx +297 -0
  279. package/src/layouts/mdi-main/mdi-tweak-panel.tsx +104 -0
  280. package/src/layouts/mdi-main/mdi-types.ts +131 -0
  281. package/src/layouts/mdi-main/use-mdi-features.ts +36 -0
  282. package/src/layouts/mdi-main/use-mdi-sidebar-state.ts +59 -0
  283. package/src/layouts/site-header/announcement-bar.tsx +55 -0
  284. package/src/layouts/site-header/category-strip.tsx +41 -0
  285. package/src/layouts/site-header/index.ts +16 -0
  286. package/src/layouts/site-header/main-navbar.tsx +179 -0
  287. package/src/layouts/site-header/mobile-drawer.tsx +140 -0
  288. package/src/layouts/site-header/site-header-types.ts +63 -0
  289. package/src/layouts/site-header/site-header.tsx +58 -0
  290. package/src/lib/random-id.ts +19 -0
  291. package/src/lib/utils.ts +6 -0
  292. package/src/providers/pdf-provider.ts +137 -0
  293. package/src/styles/globals.css +374 -0
  294. package/src/templates/README.md +12 -0
  295. package/src/templates/dashboard-01/components/app-sidebar.tsx +177 -0
  296. package/src/templates/dashboard-01/components/chart-area-interactive.tsx +276 -0
  297. package/src/templates/dashboard-01/components/data-table-cell-viewer.tsx +213 -0
  298. package/src/templates/dashboard-01/components/data-table.tsx +577 -0
  299. package/src/templates/dashboard-01/components/nav-documents.tsx +79 -0
  300. package/src/templates/dashboard-01/components/nav-main.tsx +55 -0
  301. package/src/templates/dashboard-01/components/nav-secondary.tsx +39 -0
  302. package/src/templates/dashboard-01/components/nav-user.tsx +97 -0
  303. package/src/templates/dashboard-01/components/section-cards.tsx +99 -0
  304. package/src/templates/dashboard-01/components/site-header.tsx +14 -0
  305. package/src/templates/dashboard-01/data.json +614 -0
  306. package/src/templates/dashboard-01/index.ts +7 -0
  307. package/src/templates/documentation-sidebar/app-sidebar.tsx +95 -0
  308. package/src/templates/documentation-sidebar/index.ts +2 -0
  309. package/src/templates/sidebar-07/components/app-sidebar.tsx +175 -0
  310. package/src/templates/sidebar-07/components/nav-main.tsx +64 -0
  311. package/src/templates/sidebar-07/components/nav-projects.tsx +79 -0
  312. package/src/templates/sidebar-07/components/nav-user.tsx +105 -0
  313. package/src/templates/sidebar-07/components/team-switcher.tsx +94 -0
  314. package/src/templates/sidebar-07/index.ts +1 -0
  315. package/src/templates/ui-page/index.ts +7 -0
  316. package/src/templates/ui-page/ui-browser-frame.tsx +27 -0
  317. package/src/templates/ui-page/ui-template-code.tsx +48 -0
  318. package/src/templates/ui-page/ui-template-header.tsx +87 -0
  319. package/src/templates/ui-page/ui-template-navigation.tsx +60 -0
  320. package/src/templates/ui-page/ui-template-page.tsx +97 -0
  321. package/src/theme/index.ts +12 -0
  322. package/src/theme/theme-config.ts +30 -0
  323. package/src/theme/theme-provider.tsx +142 -0
  324. package/src/theme/theme-selector.tsx +75 -0
  325. package/src/tokens/index.ts +15 -0
  326. package/src/vite-env.d.ts +3 -0
@@ -0,0 +1,164 @@
1
+ import { Download, Minus, Pencil, Plus, RotateCcw } from 'lucide-react'
2
+ import { useEffect, useId, useState } from 'react'
3
+ import { Button } from '../../components/button'
4
+ import { Textarea } from '../../components/textarea'
5
+
6
+ type MermaidRuntime = {
7
+ initialize: (config: Record<string, unknown>) => void
8
+ render: (id: string, source: string) => Promise<{ svg: string }>
9
+ }
10
+
11
+ let runtimePromise: Promise<MermaidRuntime> | undefined
12
+
13
+ function loadRuntime() {
14
+ runtimePromise ??= import('mermaid').then(({ default: runtime }) => runtime as MermaidRuntime)
15
+ return runtimePromise
16
+ }
17
+
18
+ export function MermaidPreview({
19
+ interactive = false,
20
+ source,
21
+ }: {
22
+ interactive?: boolean
23
+ source: string
24
+ }) {
25
+ const id = useId().replace(/:/g, '-')
26
+ const [draft, setDraft] = useState(source)
27
+ const [editing, setEditing] = useState(false)
28
+ const [result, setResult] = useState<{ error?: string; svg?: string }>({})
29
+ const [zoom, setZoom] = useState(1)
30
+
31
+ useEffect(() => setDraft(source), [source])
32
+
33
+ useEffect(() => {
34
+ const value = draft.trim()
35
+ if (!value) {
36
+ setResult({})
37
+ return
38
+ }
39
+ let active = true
40
+ void loadRuntime()
41
+ .then((runtime) => {
42
+ runtime.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'neutral' })
43
+ return runtime.render(`mermaid-${id}`, value)
44
+ })
45
+ .then(({ svg }) => active && setResult({ svg }))
46
+ .catch(() => active && setResult({ error: 'Mermaid could not render this diagram.' }))
47
+ return () => {
48
+ active = false
49
+ }
50
+ }, [draft, id])
51
+
52
+ if (!draft.trim()) return null
53
+ if (result.error) return <p className="text-sm text-destructive">{result.error}</p>
54
+ if (!result.svg) return <p className="text-sm text-muted-foreground">Rendering diagram…</p>
55
+ return (
56
+ <section
57
+ aria-label="Mermaid diagram"
58
+ className="group/mermaid relative space-y-3 rounded-lg border bg-muted/20 p-3"
59
+ >
60
+ {interactive ? (
61
+ <div className="absolute right-3 top-3 z-10 flex gap-1 rounded-md border bg-background/95 p-1 opacity-0 shadow-sm transition-opacity group-focus-within/mermaid:opacity-100 group-hover/mermaid:opacity-100">
62
+ <Button
63
+ aria-label="Zoom out"
64
+ disabled={zoom <= 0.5}
65
+ onClick={() => setZoom((value) => Math.max(0.5, value - 0.25))}
66
+ size="icon-xs"
67
+ type="button"
68
+ variant="ghost"
69
+ >
70
+ <Minus />
71
+ </Button>
72
+ <Button
73
+ aria-label="Reset diagram zoom"
74
+ onClick={() => setZoom(1)}
75
+ size="icon-xs"
76
+ type="button"
77
+ variant="ghost"
78
+ >
79
+ <RotateCcw />
80
+ </Button>
81
+ <Button
82
+ aria-label="Zoom in"
83
+ disabled={zoom >= 2}
84
+ onClick={() => setZoom((value) => Math.min(2, value + 0.25))}
85
+ size="icon-xs"
86
+ type="button"
87
+ variant="ghost"
88
+ >
89
+ <Plus />
90
+ </Button>
91
+ <Button
92
+ aria-label="Download diagram as PNG"
93
+ onClick={() => void downloadPng(result.svg!, id)}
94
+ size="icon-xs"
95
+ type="button"
96
+ variant="ghost"
97
+ >
98
+ <Download />
99
+ </Button>
100
+ <Button
101
+ aria-label="Open Mermaid editor"
102
+ onClick={() => setEditing((value) => !value)}
103
+ size="icon-xs"
104
+ type="button"
105
+ variant="ghost"
106
+ >
107
+ <Pencil />
108
+ </Button>
109
+ </div>
110
+ ) : null}
111
+ {editing ? (
112
+ <div className="space-y-2">
113
+ <p className="text-xs text-muted-foreground">
114
+ Local draft. The saved chat response is unchanged.
115
+ </p>
116
+ <Textarea
117
+ aria-label="Mermaid source editor"
118
+ className="min-h-36 font-mono text-xs"
119
+ onChange={(event) => setDraft(event.target.value)}
120
+ value={draft}
121
+ />
122
+ </div>
123
+ ) : null}
124
+ <div
125
+ className="overflow-auto rounded-md"
126
+ style={{ maxHeight: interactive ? '34rem' : undefined }}
127
+ >
128
+ <div
129
+ className="min-w-max origin-top-left"
130
+ style={{ transform: `scale(${zoom})`, width: `${100 / zoom}%` }}
131
+ dangerouslySetInnerHTML={{ __html: result.svg }}
132
+ />
133
+ </div>
134
+ </section>
135
+ )
136
+ }
137
+
138
+ async function downloadPng(svg: string, id: string) {
139
+ const sourceUrl = URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }))
140
+ try {
141
+ const image = new Image()
142
+ await new Promise<void>((resolve, reject) => {
143
+ image.onload = () => resolve()
144
+ image.onerror = () => reject(new Error('The diagram could not be exported.'))
145
+ image.src = sourceUrl
146
+ })
147
+ const canvas = document.createElement('canvas')
148
+ canvas.width = Math.max(1, image.width * 2)
149
+ canvas.height = Math.max(1, image.height * 2)
150
+ const context = canvas.getContext('2d')
151
+ if (!context) throw new Error('The diagram could not be exported.')
152
+ context.scale(2, 2)
153
+ context.drawImage(image, 0, 0)
154
+ const png = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, 'image/png'))
155
+ if (!png) throw new Error('The diagram could not be exported.')
156
+ const link = document.createElement('a')
157
+ link.download = `mermaid-${id}.png`
158
+ link.href = URL.createObjectURL(png)
159
+ link.click()
160
+ URL.revokeObjectURL(link.href)
161
+ } finally {
162
+ URL.revokeObjectURL(sourceUrl)
163
+ }
164
+ }
@@ -0,0 +1,7 @@
1
+ # Notification Blocks
2
+
3
+ This folder owns the reusable, presentation-only notification center page. Applications supply
4
+ records, read state, navigation actions, and persistence callbacks.
5
+
6
+ The block does not fetch notifications, change server state, or own product routes. It renders a
7
+ responsive inbox surface with unread indicators and an optional mark-all-read action.
@@ -0,0 +1 @@
1
+ export { NotificationCenterPage, type NotificationCenterItem } from './notification-center-page'
@@ -0,0 +1,73 @@
1
+ import { BellIcon, CheckCheckIcon } from 'lucide-react'
2
+ import { Badge } from '../../components/badge'
3
+ import { Button } from '../../components/button'
4
+ import { cn } from '../../lib/utils'
5
+
6
+ export type NotificationCenterItem = {
7
+ description: string
8
+ id: string
9
+ read?: boolean
10
+ time: string
11
+ title: string
12
+ onSelect?: () => void
13
+ }
14
+
15
+ export function NotificationCenterPage({
16
+ embedded = false,
17
+ items,
18
+ onMarkAllRead,
19
+ }: {
20
+ embedded?: boolean
21
+ items: readonly NotificationCenterItem[]
22
+ onMarkAllRead?: () => void
23
+ }) {
24
+ const unreadCount = items.filter(({ read }) => !read).length
25
+
26
+ return (
27
+ <main className={cn('bg-background px-6 py-10', embedded ? 'min-h-[620px]' : 'min-h-screen')}>
28
+ <div className="mx-auto grid w-full max-w-3xl gap-8">
29
+ <header className="flex flex-wrap items-start justify-between gap-4 border-b pb-6">
30
+ <div className="grid gap-2">
31
+ <div className="flex items-center gap-2">
32
+ <BellIcon className="size-5" />
33
+ <h1 className="text-2xl font-semibold tracking-tight">Notifications</h1>
34
+ {unreadCount > 0 ? <Badge>{unreadCount} new</Badge> : null}
35
+ </div>
36
+ <p className="text-sm text-muted-foreground">
37
+ Review activity from your connected CODEXSUN applications.
38
+ </p>
39
+ </div>
40
+ <Button onClick={onMarkAllRead} variant="outline">
41
+ <CheckCheckIcon /> Mark all as read
42
+ </Button>
43
+ </header>
44
+
45
+ <section className="divide-y" aria-label="Recent notifications">
46
+ {items.map((item) => (
47
+ <button
48
+ className="flex w-full cursor-pointer items-start gap-4 px-2 py-5 text-left transition-colors hover:bg-muted/50"
49
+ key={item.id}
50
+ onClick={item.onSelect}
51
+ type="button"
52
+ >
53
+ <span
54
+ aria-hidden="true"
55
+ className={cn(
56
+ 'mt-2 size-2 shrink-0 rounded-full',
57
+ item.read ? 'bg-muted' : 'bg-primary',
58
+ )}
59
+ />
60
+ <span className="min-w-0 flex-1">
61
+ <span className="block font-medium">{item.title}</span>
62
+ <span className="mt-1 block text-sm leading-6 text-muted-foreground">
63
+ {item.description}
64
+ </span>
65
+ </span>
66
+ <span className="shrink-0 text-sm text-muted-foreground">{item.time}</span>
67
+ </button>
68
+ ))}
69
+ </section>
70
+ </div>
71
+ </main>
72
+ )
73
+ }
@@ -0,0 +1,7 @@
1
+ export { PricingTable } from './pricing-table'
2
+ export type {
3
+ BillingInterval,
4
+ PricingFeatureItem,
5
+ PricingTableProps,
6
+ PricingTierItem,
7
+ } from './pricing-types'
@@ -0,0 +1,173 @@
1
+ import { Check, Sparkles, X } from 'lucide-react'
2
+ import { useState } from 'react'
3
+ import { Badge } from '../../components/badge'
4
+ import { Button } from '../../components/button'
5
+ import { cn } from '../../lib/utils'
6
+ import type { BillingInterval, PricingTableProps, PricingTierItem } from './pricing-types'
7
+
8
+ export function PricingTable({
9
+ allowIntervalToggle = true,
10
+ annualDiscountLabel = 'Save 20%',
11
+ className,
12
+ defaultInterval = 'monthly',
13
+ onSelectTier,
14
+ subtitle = 'Simple, transparent pricing. No hidden fees.',
15
+ tiers,
16
+ title = 'Choose your plan',
17
+ }: PricingTableProps) {
18
+ const [interval, setInterval] = useState<BillingInterval>(defaultInterval)
19
+
20
+ return (
21
+ <div className={cn('flex flex-col items-center gap-8 py-4', className)}>
22
+ {/* Header */}
23
+ <div className="flex flex-col items-center text-center max-w-xl gap-2">
24
+ <h3 className="text-2xl font-bold tracking-tight text-foreground sm:text-3xl">{title}</h3>
25
+ {subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
26
+
27
+ {/* Interval Switcher */}
28
+ {allowIntervalToggle && (
29
+ <div className="mt-4 flex items-center gap-1 rounded-xl border border-border/80 bg-muted/40 p-1">
30
+ <button
31
+ className={cn(
32
+ 'rounded-lg px-3 py-1.5 text-xs font-semibold transition-all',
33
+ interval === 'monthly'
34
+ ? 'bg-background text-foreground shadow-2xs'
35
+ : 'text-muted-foreground hover:text-foreground',
36
+ )}
37
+ type="button"
38
+ onClick={() => setInterval('monthly')}
39
+ >
40
+ Monthly billing
41
+ </button>
42
+ <button
43
+ className={cn(
44
+ 'flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-semibold transition-all',
45
+ interval === 'annual'
46
+ ? 'bg-background text-foreground shadow-2xs'
47
+ : 'text-muted-foreground hover:text-foreground',
48
+ )}
49
+ type="button"
50
+ onClick={() => setInterval('annual')}
51
+ >
52
+ <span>Annual billing</span>
53
+ <span className="rounded-full bg-emerald-500/15 px-1.5 py-0.2 text-[10px] font-bold text-emerald-600 dark:text-emerald-400">
54
+ {annualDiscountLabel}
55
+ </span>
56
+ </button>
57
+ </div>
58
+ )}
59
+ </div>
60
+
61
+ {/* Tiers Grid */}
62
+ <div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3 items-stretch">
63
+ {tiers.map((tier) => (
64
+ <PricingTierCard
65
+ interval={interval}
66
+ key={tier.id}
67
+ tier={tier}
68
+ onSelect={() => onSelectTier?.(tier, interval)}
69
+ />
70
+ ))}
71
+ </div>
72
+ </div>
73
+ )
74
+ }
75
+
76
+ function PricingTierCard({
77
+ interval,
78
+ onSelect,
79
+ tier,
80
+ }: {
81
+ interval: BillingInterval
82
+ onSelect?: () => void
83
+ tier: PricingTierItem
84
+ }) {
85
+ const currency = tier.currency ?? '$'
86
+ const isAnnual = interval === 'annual'
87
+ const price = isAnnual && tier.annualPrice !== undefined ? tier.annualPrice : tier.monthlyPrice
88
+ const period = tier.periodLabel ?? (isAnnual ? '/yr' : '/mo')
89
+
90
+ return (
91
+ <div
92
+ className={cn(
93
+ 'relative flex flex-col justify-between rounded-2xl border bg-card p-6 shadow-2xs transition-all duration-200 hover:shadow-md',
94
+ tier.isPopular
95
+ ? 'border-primary ring-2 ring-primary/20 shadow-sm'
96
+ : 'border-border/80 hover:border-foreground/25',
97
+ )}
98
+ >
99
+ {tier.isPopular && (
100
+ <div className="absolute -top-3 left-1/2 -translate-x-1/2">
101
+ <Badge className="bg-primary text-primary-foreground font-bold shadow-xs px-3 py-0.5">
102
+ <Sparkles className="size-3 mr-1" />
103
+ {tier.badge ?? 'Most Popular'}
104
+ </Badge>
105
+ </div>
106
+ )}
107
+
108
+ <div>
109
+ <div className="flex items-center justify-between gap-2">
110
+ <h4 className="text-lg font-bold tracking-tight text-foreground">{tier.name}</h4>
111
+ {!tier.isPopular && tier.badge && <Badge variant="secondary">{tier.badge}</Badge>}
112
+ </div>
113
+
114
+ <p className="mt-1 text-xs text-muted-foreground leading-relaxed">{tier.description}</p>
115
+
116
+ {/* Price display */}
117
+ <div className="mt-5 flex items-baseline gap-1">
118
+ <span className="text-3xl font-extrabold text-foreground sm:text-4xl">
119
+ {currency}
120
+ {price}
121
+ </span>
122
+ <span className="text-xs font-medium text-muted-foreground">{period}</span>
123
+ </div>
124
+
125
+ {/* Features List */}
126
+ <div className="mt-6 flex flex-col gap-2.5 border-t border-border/40 pt-5">
127
+ <span className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider">
128
+ Features included:
129
+ </span>
130
+ <ul className="flex flex-col gap-2">
131
+ {tier.features.map((feat, idx) => (
132
+ <li className="flex items-start gap-2 text-xs" key={idx}>
133
+ {feat.excluded ? (
134
+ <X className="size-3.5 text-muted-foreground/50 shrink-0 mt-0.5" />
135
+ ) : (
136
+ <Check
137
+ className={cn(
138
+ 'size-3.5 shrink-0 mt-0.5',
139
+ feat.highlight ? 'text-primary font-bold' : 'text-emerald-500',
140
+ )}
141
+ />
142
+ )}
143
+ <span
144
+ className={cn(
145
+ feat.excluded
146
+ ? 'text-muted-foreground/60 line-through'
147
+ : feat.highlight
148
+ ? 'text-foreground font-semibold'
149
+ : 'text-foreground/90',
150
+ )}
151
+ >
152
+ {feat.label}
153
+ </span>
154
+ </li>
155
+ ))}
156
+ </ul>
157
+ </div>
158
+ </div>
159
+
160
+ {/* CTA Button */}
161
+ <div className="mt-8 pt-2">
162
+ <Button
163
+ className="w-full rounded-xl font-semibold"
164
+ render={tier.ctaHref ? <a href={tier.ctaHref} /> : undefined}
165
+ variant={tier.ctaVariant ?? (tier.isPopular ? 'default' : 'outline')}
166
+ onClick={onSelect}
167
+ >
168
+ {tier.ctaLabel ?? 'Get Started'}
169
+ </Button>
170
+ </div>
171
+ </div>
172
+ )
173
+ }
@@ -0,0 +1,34 @@
1
+ export type BillingInterval = 'monthly' | 'annual'
2
+
3
+ export interface PricingFeatureItem {
4
+ excluded?: boolean
5
+ highlight?: boolean
6
+ label: string
7
+ }
8
+
9
+ export interface PricingTierItem {
10
+ annualPrice?: number
11
+ badge?: string
12
+ ctaHref?: string
13
+ ctaLabel?: string
14
+ ctaVariant?: 'default' | 'outline' | 'secondary'
15
+ currency?: string
16
+ description: string
17
+ features: readonly PricingFeatureItem[]
18
+ id: string
19
+ isPopular?: boolean
20
+ monthlyPrice: number
21
+ name: string
22
+ periodLabel?: string
23
+ }
24
+
25
+ export interface PricingTableProps {
26
+ allowIntervalToggle?: boolean
27
+ annualDiscountLabel?: string
28
+ className?: string
29
+ defaultInterval?: BillingInterval
30
+ onSelectTier?: (tier: PricingTierItem, interval: BillingInterval) => void
31
+ subtitle?: string
32
+ tiers: readonly PricingTierItem[]
33
+ title?: string
34
+ }
@@ -0,0 +1,2 @@
1
+ export { ProductCard } from './product-card'
2
+ export type { ProductCardProps, ProductColorSwatch, ProductItem } from './product-card-types'
@@ -0,0 +1,35 @@
1
+ export interface ProductColorSwatch {
2
+ hex: string
3
+ id: string
4
+ name: string
5
+ }
6
+
7
+ export interface ProductItem {
8
+ badge?: string
9
+ brand?: string
10
+ category?: string
11
+ colors?: readonly ProductColorSwatch[]
12
+ currency?: string
13
+ description?: string
14
+ discountPercent?: number
15
+ id: string
16
+ imageUrl: string
17
+ inStock?: boolean
18
+ isNew?: boolean
19
+ originalPrice?: number
20
+ price: number
21
+ rating?: number
22
+ reviewCount?: number
23
+ secondaryImageUrl?: string
24
+ title: string
25
+ }
26
+
27
+ export interface ProductCardProps {
28
+ className?: string
29
+ onAddToCart?: (product: ProductItem, selectedColor?: ProductColorSwatch) => void
30
+ onQuickView?: (product: ProductItem) => void
31
+ onToggleWishlist?: (product: ProductItem, isWishlisted: boolean) => void
32
+ product: ProductItem
33
+ showQuickView?: boolean
34
+ showWishlist?: boolean
35
+ }
@@ -0,0 +1,183 @@
1
+ import { Eye, Heart, ShoppingBag, Star } from 'lucide-react'
2
+ import { useState } from 'react'
3
+ import { Badge } from '../../components/badge'
4
+ import { Button } from '../../components/button'
5
+ import { cn } from '../../lib/utils'
6
+ import type { ProductCardProps, ProductColorSwatch } from './product-card-types'
7
+
8
+ export function ProductCard({
9
+ className,
10
+ onAddToCart,
11
+ onQuickView,
12
+ onToggleWishlist,
13
+ product,
14
+ showQuickView = true,
15
+ showWishlist = true,
16
+ }: ProductCardProps) {
17
+ const [isHovered, setIsHovered] = useState(false)
18
+ const [isWishlisted, setIsWishlisted] = useState(false)
19
+ const [selectedColor, setSelectedColor] = useState<ProductColorSwatch | undefined>(
20
+ product.colors?.[0],
21
+ )
22
+
23
+ const currency = product.currency ?? '$'
24
+ const inStock = product.inStock ?? true
25
+
26
+ function handleWishlistToggle(e: React.MouseEvent) {
27
+ e.stopPropagation()
28
+ const next = !isWishlisted
29
+ setIsWishlisted(next)
30
+ onToggleWishlist?.(product, next)
31
+ }
32
+
33
+ function handleAddToCart(e: React.MouseEvent) {
34
+ e.stopPropagation()
35
+ if (!inStock) return
36
+ onAddToCart?.(product, selectedColor)
37
+ }
38
+
39
+ return (
40
+ <div
41
+ className={cn(
42
+ 'group relative flex flex-col overflow-hidden rounded-2xl border border-border/80 bg-card text-card-foreground shadow-2xs transition-all duration-200 hover:-translate-y-1 hover:border-foreground/25 hover:shadow-md select-none',
43
+ className,
44
+ )}
45
+ onMouseEnter={() => setIsHovered(true)}
46
+ onMouseLeave={() => setIsHovered(false)}
47
+ >
48
+ {/* Product Image Area */}
49
+ <div className="relative aspect-square w-full overflow-hidden bg-muted/40">
50
+ <img
51
+ alt={product.title}
52
+ className="size-full object-cover transition-transform duration-300 group-hover:scale-105"
53
+ loading="lazy"
54
+ src={
55
+ isHovered && product.secondaryImageUrl ? product.secondaryImageUrl : product.imageUrl
56
+ }
57
+ />
58
+
59
+ {/* Badges */}
60
+ <div className="absolute left-2.5 top-2.5 flex flex-col gap-1">
61
+ {product.discountPercent ? (
62
+ <Badge className="bg-destructive text-destructive-foreground font-bold shadow-xs">
63
+ -{product.discountPercent}%
64
+ </Badge>
65
+ ) : product.isNew ? (
66
+ <Badge className="bg-primary text-primary-foreground font-bold shadow-xs">NEW</Badge>
67
+ ) : product.badge ? (
68
+ <Badge variant="secondary">{product.badge}</Badge>
69
+ ) : null}
70
+
71
+ {!inStock && (
72
+ <Badge className="bg-muted text-muted-foreground font-medium" variant="outline">
73
+ Out of stock
74
+ </Badge>
75
+ )}
76
+ </div>
77
+
78
+ {/* Floating Actions */}
79
+ <div className="absolute right-2.5 top-2.5 flex flex-col gap-1.5">
80
+ {showWishlist && (
81
+ <button
82
+ aria-label={isWishlisted ? 'Remove from wishlist' : 'Add to wishlist'}
83
+ className={cn(
84
+ 'flex size-8 items-center justify-center rounded-full bg-background/85 backdrop-blur-xs text-foreground shadow-xs transition-colors hover:bg-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
85
+ isWishlisted && 'text-destructive',
86
+ )}
87
+ type="button"
88
+ onClick={handleWishlistToggle}
89
+ >
90
+ <Heart
91
+ className={cn('size-4', isWishlisted && 'fill-destructive text-destructive')}
92
+ />
93
+ </button>
94
+ )}
95
+
96
+ {showQuickView && onQuickView && (
97
+ <button
98
+ aria-label="Quick view product"
99
+ className="flex size-8 items-center justify-center rounded-full bg-background/85 backdrop-blur-xs text-muted-foreground shadow-xs transition-colors hover:bg-background hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
100
+ type="button"
101
+ onClick={() => onQuickView(product)}
102
+ >
103
+ <Eye className="size-4" />
104
+ </button>
105
+ )}
106
+ </div>
107
+ </div>
108
+
109
+ {/* Product Content Details */}
110
+ <div className="flex flex-1 flex-col p-4">
111
+ {product.brand && (
112
+ <span className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider">
113
+ {product.brand}
114
+ </span>
115
+ )}
116
+
117
+ <h4 className="mt-0.5 line-clamp-1 text-sm font-semibold tracking-tight text-foreground">
118
+ {product.title}
119
+ </h4>
120
+
121
+ {/* Rating */}
122
+ {product.rating !== undefined && (
123
+ <div className="mt-1.5 flex items-center gap-1 text-xs text-muted-foreground">
124
+ <div className="flex items-center text-amber-500">
125
+ <Star className="size-3.5 fill-amber-500 text-amber-500" />
126
+ </div>
127
+ <span className="font-semibold text-foreground text-[11px]">{product.rating}</span>
128
+ {product.reviewCount !== undefined && (
129
+ <span className="text-[11px] text-muted-foreground">({product.reviewCount})</span>
130
+ )}
131
+ </div>
132
+ )}
133
+
134
+ {/* Color Swatches */}
135
+ {product.colors && product.colors.length > 0 && (
136
+ <div className="mt-2.5 flex items-center gap-1.5">
137
+ {product.colors.map((color) => (
138
+ <button
139
+ aria-label={`Select color ${color.name}`}
140
+ className={cn(
141
+ 'size-4.5 rounded-full border border-border/80 transition-transform hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
142
+ selectedColor?.id === color.id && 'ring-2 ring-primary ring-offset-1',
143
+ )}
144
+ key={color.id}
145
+ style={{ backgroundColor: color.hex }}
146
+ title={color.name}
147
+ type="button"
148
+ onClick={() => setSelectedColor(color)}
149
+ />
150
+ ))}
151
+ </div>
152
+ )}
153
+
154
+ {/* Price & Add to Cart */}
155
+ <div className="mt-auto pt-3 flex items-center justify-between gap-2 border-t border-border/40">
156
+ <div className="flex items-baseline gap-1.5">
157
+ <span className="text-base font-bold text-foreground">
158
+ {currency}
159
+ {product.price.toFixed(2)}
160
+ </span>
161
+ {product.originalPrice && product.originalPrice > product.price && (
162
+ <span className="text-xs text-muted-foreground line-through">
163
+ {currency}
164
+ {product.originalPrice.toFixed(2)}
165
+ </span>
166
+ )}
167
+ </div>
168
+
169
+ <Button
170
+ className="rounded-xl px-2.5 py-1 text-xs font-semibold"
171
+ disabled={!inStock}
172
+ size="sm"
173
+ variant="secondary"
174
+ onClick={handleAddToCart}
175
+ >
176
+ <ShoppingBag className="size-3.5" />
177
+ <span className="hidden xs:inline">Add</span>
178
+ </Button>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ )
183
+ }