@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.
- package/README.md +85 -2
- package/package.json +105 -4
- package/src/blocks/agent-chat-workspace/agent-chat-workspace.tsx +162 -0
- package/src/blocks/agent-chat-workspace/index.ts +2 -0
- package/src/blocks/agent-provider-settings/agent-provider-settings.tsx +183 -0
- package/src/blocks/agent-provider-settings/index.ts +2 -0
- package/src/blocks/agent-task-workspace/agent-task-workspace.tsx +48 -0
- package/src/blocks/agent-task-workspace/index.ts +2 -0
- package/src/blocks/analysis-context/analysis-context.tsx +6 -0
- package/src/blocks/analysis-context/index.ts +1 -0
- package/src/blocks/app-header/app-header.tsx +159 -0
- package/src/blocks/app-header/index.ts +2 -0
- package/src/blocks/archived-chat-workspace/archived-chat-workspace.tsx +29 -0
- package/src/blocks/archived-chat-workspace/index.ts +2 -0
- package/src/blocks/auth/README.md +32 -0
- package/src/blocks/auth/admin-login-page.tsx +15 -0
- package/src/blocks/auth/admin-password-forgot-page.tsx +5 -0
- package/src/blocks/auth/auth-page-layout.tsx +90 -0
- package/src/blocks/auth/client-login-page.tsx +20 -0
- package/src/blocks/auth/client-password-forgot-page.tsx +5 -0
- package/src/blocks/auth/identity-management-desk.tsx +530 -0
- package/src/blocks/auth/index.ts +12 -0
- package/src/blocks/auth/login-page.tsx +206 -0
- package/src/blocks/auth/password-forgot-page.tsx +60 -0
- package/src/blocks/auth/portal-pages.tsx +32 -0
- package/src/blocks/auth/privileged-desk.tsx +55 -0
- package/src/blocks/auth/register-page.tsx +117 -0
- package/src/blocks/auth/session-boundary.tsx +296 -0
- package/src/blocks/auth/super-admin-login-page.tsx +15 -0
- package/src/blocks/auth/super-admin-password-forgot-page.tsx +5 -0
- package/src/blocks/blog/blog-card.tsx +156 -0
- package/src/blocks/blog/blog-reader.tsx +175 -0
- package/src/blocks/blog/blog-types.ts +47 -0
- package/src/blocks/blog/index.ts +3 -0
- package/src/blocks/chat-composer/chat-composer.tsx +140 -0
- package/src/blocks/chat-composer/index.ts +1 -0
- package/src/blocks/chat-history/chat-history.tsx +59 -0
- package/src/blocks/chat-history/index.ts +1 -0
- package/src/blocks/chat-response-progress/chat-response-progress.tsx +34 -0
- package/src/blocks/chat-response-progress/index.ts +2 -0
- package/src/blocks/chat-runtime-controls/chat-runtime-controls.tsx +56 -0
- package/src/blocks/chat-runtime-controls/index.ts +2 -0
- package/src/blocks/chat-runtime-trace/chat-runtime-trace.tsx +71 -0
- package/src/blocks/chat-runtime-trace/index.ts +2 -0
- package/src/blocks/codex-connection-settings/codex-connection-settings.tsx +42 -0
- package/src/blocks/codex-connection-settings/index.ts +1 -0
- package/src/blocks/dropzone/dropzone-file-item.tsx +113 -0
- package/src/blocks/dropzone/dropzone-types.ts +32 -0
- package/src/blocks/dropzone/dropzone.tsx +216 -0
- package/src/blocks/dropzone/index.ts +3 -0
- package/src/blocks/ecommerce/cart/cart-drawer.tsx +228 -0
- package/src/blocks/ecommerce/cart/cart-types.ts +27 -0
- package/src/blocks/ecommerce/cart/index.ts +2 -0
- package/src/blocks/ecommerce/categories/categories-types.ts +23 -0
- package/src/blocks/ecommerce/categories/category-showcase.tsx +125 -0
- package/src/blocks/ecommerce/categories/index.ts +2 -0
- package/src/blocks/ecommerce/checkout/checkout-types.ts +48 -0
- package/src/blocks/ecommerce/checkout/checkout-wizard.tsx +468 -0
- package/src/blocks/ecommerce/checkout/index.ts +2 -0
- package/src/blocks/ecommerce/comparison/comparison-types.ts +28 -0
- package/src/blocks/ecommerce/comparison/index.ts +2 -0
- package/src/blocks/ecommerce/comparison/product-comparison.tsx +174 -0
- package/src/blocks/ecommerce/coupon-wallet/coupon-wallet-types.ts +21 -0
- package/src/blocks/ecommerce/coupon-wallet/coupon-wallet.tsx +154 -0
- package/src/blocks/ecommerce/coupon-wallet/index.ts +2 -0
- package/src/blocks/ecommerce/delivery-tracker/delivery-tracker-types.ts +20 -0
- package/src/blocks/ecommerce/delivery-tracker/delivery-tracker.tsx +169 -0
- package/src/blocks/ecommerce/delivery-tracker/index.ts +2 -0
- package/src/blocks/ecommerce/index.ts +12 -0
- package/src/blocks/ecommerce/payment-methods/index.ts +2 -0
- package/src/blocks/ecommerce/payment-methods/payment-method-selector.tsx +224 -0
- package/src/blocks/ecommerce/payment-methods/payment-methods-types.ts +21 -0
- package/src/blocks/ecommerce/price-history/index.ts +2 -0
- package/src/blocks/ecommerce/price-history/price-history-chart.tsx +217 -0
- package/src/blocks/ecommerce/price-history/price-history-types.ts +16 -0
- package/src/blocks/ecommerce/reviews/index.ts +2 -0
- package/src/blocks/ecommerce/reviews/reviews-section.tsx +309 -0
- package/src/blocks/ecommerce/reviews/reviews-types.ts +26 -0
- package/src/blocks/ecommerce/wishlist/index.ts +2 -0
- package/src/blocks/ecommerce/wishlist/wishlist-grid.tsx +149 -0
- package/src/blocks/ecommerce/wishlist/wishlist-types.ts +19 -0
- package/src/blocks/execution-status/README.md +25 -0
- package/src/blocks/execution-status/execution-status.tsx +135 -0
- package/src/blocks/execution-status/index.ts +2 -0
- package/src/blocks/file-tree/file-icons.tsx +84 -0
- package/src/blocks/file-tree/file-tree-node.tsx +158 -0
- package/src/blocks/file-tree/file-tree-types.ts +33 -0
- package/src/blocks/file-tree/file-tree.tsx +163 -0
- package/src/blocks/file-tree/index.ts +5 -0
- package/src/blocks/filter-builder/filter-builder.tsx +169 -0
- package/src/blocks/filter-builder/filter-row.tsx +164 -0
- package/src/blocks/filter-builder/filter-types.ts +61 -0
- package/src/blocks/filter-builder/index.ts +12 -0
- package/src/blocks/footer/footer-types.ts +35 -0
- package/src/blocks/footer/index.ts +2 -0
- package/src/blocks/footer/site-footer.tsx +212 -0
- package/src/blocks/form/form-block.tsx +113 -0
- package/src/blocks/form/form-lookup-field.tsx +60 -0
- package/src/blocks/form/index.ts +4 -0
- package/src/blocks/handover-stack/handover-stack.tsx +41 -0
- package/src/blocks/handover-stack/index.ts +1 -0
- package/src/blocks/idea-handover/idea-handover-readiness.ts +41 -0
- package/src/blocks/idea-handover/idea-handover-workspace.tsx +150 -0
- package/src/blocks/idea-handover/index.ts +4 -0
- package/src/blocks/kanban/index.ts +11 -0
- package/src/blocks/kanban/kanban-board.tsx +146 -0
- package/src/blocks/kanban/kanban-card.tsx +149 -0
- package/src/blocks/kanban/kanban-column.tsx +102 -0
- package/src/blocks/kanban/kanban-types.ts +38 -0
- package/src/blocks/loader/global-loader.tsx +90 -0
- package/src/blocks/loader/index.ts +1 -0
- package/src/blocks/mascot/README.md +21 -0
- package/src/blocks/mascot/index.ts +2 -0
- package/src/blocks/mascot/mascot.tsx +202 -0
- package/src/blocks/master-list/README.md +21 -0
- package/src/blocks/master-list/index.ts +12 -0
- package/src/blocks/master-list/master-form.tsx +157 -0
- package/src/blocks/master-list/master-list-desk.tsx +370 -0
- package/src/blocks/master-list/master-list.tsx +189 -0
- package/src/blocks/master-list/types.ts +18 -0
- package/src/blocks/mermaid-preview/index.ts +1 -0
- package/src/blocks/mermaid-preview/mermaid-preview.tsx +164 -0
- package/src/blocks/notifications/README.md +7 -0
- package/src/blocks/notifications/index.ts +1 -0
- package/src/blocks/notifications/notification-center-page.tsx +73 -0
- package/src/blocks/pricing/index.ts +7 -0
- package/src/blocks/pricing/pricing-table.tsx +173 -0
- package/src/blocks/pricing/pricing-types.ts +34 -0
- package/src/blocks/product-card/index.ts +2 -0
- package/src/blocks/product-card/product-card-types.ts +35 -0
- package/src/blocks/product-card/product-card.tsx +183 -0
- package/src/blocks/table/data-table-block.tsx +313 -0
- package/src/blocks/table/data-table-filter-menu.tsx +48 -0
- package/src/blocks/table/data-table-icon-menu-trigger.tsx +44 -0
- package/src/blocks/table/data-table-menu-content.tsx +32 -0
- package/src/blocks/table/data-table-pagination.tsx +149 -0
- package/src/blocks/table/data-table-row-actions.tsx +53 -0
- package/src/blocks/table/data-table-status.tsx +42 -0
- package/src/blocks/table/data-table-toolbar.tsx +124 -0
- package/src/blocks/table/data-table-totals.tsx +31 -0
- package/src/blocks/table/index.ts +13 -0
- package/src/blocks/workspace/index.ts +19 -0
- package/src/blocks/workspace/workspace-action-card.tsx +73 -0
- package/src/blocks/workspace/workspace-application-header.tsx +67 -0
- package/src/blocks/workspace/workspace-entity-card.tsx +48 -0
- package/src/blocks/workspace/workspace-health-summary.tsx +15 -0
- package/src/blocks/workspace/workspace-metric-card.tsx +97 -0
- package/src/blocks/workspace/workspace-page-header.tsx +43 -0
- package/src/blocks/workspace/workspace-publish-status.tsx +15 -0
- package/src/blocks/workspace/workspace-route-checklist.tsx +28 -0
- package/src/blocks/workspace/workspace-runtime-status.tsx +48 -0
- package/src/blocks/workspace/workspace-section-card.tsx +42 -0
- package/src/components/accordion.tsx +65 -0
- package/src/components/alert-dialog.tsx +160 -0
- package/src/components/alert.tsx +68 -0
- package/src/components/aspect-ratio.tsx +22 -0
- package/src/components/attachment.tsx +195 -0
- package/src/components/avatar.tsx +90 -0
- package/src/components/badge.tsx +55 -0
- package/src/components/breadcrumb.tsx +100 -0
- package/src/components/bubble.tsx +123 -0
- package/src/components/button-group.tsx +79 -0
- package/src/components/button.tsx +71 -0
- package/src/components/calendar.tsx +185 -0
- package/src/components/card.tsx +100 -0
- package/src/components/carousel.tsx +229 -0
- package/src/components/chart.tsx +338 -0
- package/src/components/checkbox.tsx +27 -0
- package/src/components/collapsible.tsx +15 -0
- package/src/components/combobox.tsx +269 -0
- package/src/components/command.tsx +181 -0
- package/src/components/compact-model-switcher.tsx +128 -0
- package/src/components/context-menu.tsx +244 -0
- package/src/components/dialog.tsx +138 -0
- package/src/components/direction.tsx +1 -0
- package/src/components/drawer.tsx +208 -0
- package/src/components/dropdown-menu.tsx +257 -0
- package/src/components/empty.tsx +93 -0
- package/src/components/field.tsx +232 -0
- package/src/components/hover-card.tsx +45 -0
- package/src/components/input-group.tsx +146 -0
- package/src/components/input-otp.tsx +83 -0
- package/src/components/input.tsx +19 -0
- package/src/components/item.tsx +188 -0
- package/src/components/kbd.tsx +26 -0
- package/src/components/label.tsx +17 -0
- package/src/components/markdown-content.tsx +113 -0
- package/src/components/marker.tsx +67 -0
- package/src/components/menubar.tsx +267 -0
- package/src/components/message-scroller.tsx +91 -0
- package/src/components/message.tsx +84 -0
- package/src/components/native-select.tsx +54 -0
- package/src/components/navigation-menu.tsx +160 -0
- package/src/components/pagination.tsx +118 -0
- package/src/components/popover.tsx +74 -0
- package/src/components/progress.tsx +82 -0
- package/src/components/questionnaire.tsx +150 -0
- package/src/components/radio-group.tsx +35 -0
- package/src/components/resizable.tsx +41 -0
- package/src/components/rich-text-editor.tsx +604 -0
- package/src/components/scroll-area.tsx +47 -0
- package/src/components/select.tsx +189 -0
- package/src/components/separator.tsx +18 -0
- package/src/components/sheet.tsx +125 -0
- package/src/components/sidebar.tsx +693 -0
- package/src/components/skeleton.tsx +13 -0
- package/src/components/slider.tsx +51 -0
- package/src/components/sonner.tsx +45 -0
- package/src/components/sparkline.tsx +231 -0
- package/src/components/spinner.tsx +20 -0
- package/src/components/status-badge.tsx +114 -0
- package/src/components/switch.tsx +31 -0
- package/src/components/table.tsx +91 -0
- package/src/components/tabs.tsx +72 -0
- package/src/components/textarea.tsx +17 -0
- package/src/components/toast.tsx +222 -0
- package/src/components/toggle-group.tsx +85 -0
- package/src/components/toggle.tsx +42 -0
- package/src/components/tooltip.tsx +53 -0
- package/src/design-system/README.md +55 -0
- package/src/design-system/action-intents.ts +21 -0
- package/src/design-system/agent-manifest.ts +83 -0
- package/src/design-system/block-registry.ts +272 -0
- package/src/design-system/component-registry.ts +142 -0
- package/src/design-system/contracts.ts +69 -0
- package/src/design-system/defaults.ts +3 -0
- package/src/design-system/index.ts +29 -0
- package/src/design-system/mcp-server.ts +156 -0
- package/src/design-system/page-registry.ts +74 -0
- package/src/design-system/selection.ts +67 -0
- package/src/design-system/template-registry.ts +33 -0
- package/src/features/interface-topology/index.ts +14 -0
- package/src/features/interface-topology/interface-topology.types.ts +40 -0
- package/src/features/interface-topology/topology-control.tsx +18 -0
- package/src/features/interface-topology/topology-inspector.tsx +228 -0
- package/src/features/interface-topology/topology-marker.tsx +47 -0
- package/src/features/interface-topology/topology-region.tsx +44 -0
- package/src/features/interface-topology/use-interface-topology.ts +178 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-scroll-follow.ts +22 -0
- package/src/index.ts +11 -0
- package/src/layouts/README.md +65 -0
- package/src/layouts/agent-workspace/agent-activity-rail.tsx +101 -0
- package/src/layouts/agent-workspace/agent-workspace.tsx +27 -0
- package/src/layouts/agent-workspace/agent-workspace.types.ts +28 -0
- package/src/layouts/agent-workspace/index.ts +7 -0
- package/src/layouts/blog-header/blog-header-types.ts +25 -0
- package/src/layouts/blog-header/blog-header.tsx +172 -0
- package/src/layouts/blog-header/index.ts +2 -0
- package/src/layouts/documentation-workspace/README.md +8 -0
- package/src/layouts/documentation-workspace/documentation-workspace.tsx +34 -0
- package/src/layouts/documentation-workspace/index.ts +1 -0
- package/src/layouts/ecommerce-header/ecommerce-actions.tsx +89 -0
- package/src/layouts/ecommerce-header/ecommerce-announcement.tsx +43 -0
- package/src/layouts/ecommerce-header/ecommerce-drawer.tsx +97 -0
- package/src/layouts/ecommerce-header/ecommerce-header-types.ts +59 -0
- package/src/layouts/ecommerce-header/ecommerce-header.tsx +47 -0
- package/src/layouts/ecommerce-header/ecommerce-navigation.tsx +98 -0
- package/src/layouts/ecommerce-header/ecommerce-search.tsx +86 -0
- package/src/layouts/ecommerce-header/index.ts +2 -0
- package/src/layouts/main-workspace/index.ts +28 -0
- package/src/layouts/mdi/index.ts +1 -0
- package/src/layouts/mdi/mdi.tsx +20 -0
- package/src/layouts/mdi-main/index.ts +27 -0
- package/src/layouts/mdi-main/mdi-app-catalog.generated.ts +159 -0
- package/src/layouts/mdi-main/mdi-app-catalog.ts +76 -0
- package/src/layouts/mdi-main/mdi-app-switcher.tsx +107 -0
- package/src/layouts/mdi-main/mdi-feature-settings.tsx +135 -0
- package/src/layouts/mdi-main/mdi-global-search.tsx +192 -0
- package/src/layouts/mdi-main/mdi-main.tsx +241 -0
- package/src/layouts/mdi-main/mdi-notifications-menu.tsx +197 -0
- package/src/layouts/mdi-main/mdi-profile-menu.tsx +79 -0
- package/src/layouts/mdi-main/mdi-sidebar-navigation.tsx +226 -0
- package/src/layouts/mdi-main/mdi-sidebar.tsx +110 -0
- package/src/layouts/mdi-main/mdi-status-bar.tsx +26 -0
- package/src/layouts/mdi-main/mdi-top-menu-control.ts +4 -0
- package/src/layouts/mdi-main/mdi-top-menu.tsx +117 -0
- package/src/layouts/mdi-main/mdi-topology.tsx +297 -0
- package/src/layouts/mdi-main/mdi-tweak-panel.tsx +104 -0
- package/src/layouts/mdi-main/mdi-types.ts +131 -0
- package/src/layouts/mdi-main/use-mdi-features.ts +36 -0
- package/src/layouts/mdi-main/use-mdi-sidebar-state.ts +59 -0
- package/src/layouts/site-header/announcement-bar.tsx +55 -0
- package/src/layouts/site-header/category-strip.tsx +41 -0
- package/src/layouts/site-header/index.ts +16 -0
- package/src/layouts/site-header/main-navbar.tsx +179 -0
- package/src/layouts/site-header/mobile-drawer.tsx +140 -0
- package/src/layouts/site-header/site-header-types.ts +63 -0
- package/src/layouts/site-header/site-header.tsx +58 -0
- package/src/lib/random-id.ts +19 -0
- package/src/lib/utils.ts +6 -0
- package/src/providers/pdf-provider.ts +137 -0
- package/src/styles/globals.css +374 -0
- package/src/templates/README.md +12 -0
- package/src/templates/dashboard-01/components/app-sidebar.tsx +177 -0
- package/src/templates/dashboard-01/components/chart-area-interactive.tsx +276 -0
- package/src/templates/dashboard-01/components/data-table-cell-viewer.tsx +213 -0
- package/src/templates/dashboard-01/components/data-table.tsx +577 -0
- package/src/templates/dashboard-01/components/nav-documents.tsx +79 -0
- package/src/templates/dashboard-01/components/nav-main.tsx +55 -0
- package/src/templates/dashboard-01/components/nav-secondary.tsx +39 -0
- package/src/templates/dashboard-01/components/nav-user.tsx +97 -0
- package/src/templates/dashboard-01/components/section-cards.tsx +99 -0
- package/src/templates/dashboard-01/components/site-header.tsx +14 -0
- package/src/templates/dashboard-01/data.json +614 -0
- package/src/templates/dashboard-01/index.ts +7 -0
- package/src/templates/documentation-sidebar/app-sidebar.tsx +95 -0
- package/src/templates/documentation-sidebar/index.ts +2 -0
- package/src/templates/sidebar-07/components/app-sidebar.tsx +175 -0
- package/src/templates/sidebar-07/components/nav-main.tsx +64 -0
- package/src/templates/sidebar-07/components/nav-projects.tsx +79 -0
- package/src/templates/sidebar-07/components/nav-user.tsx +105 -0
- package/src/templates/sidebar-07/components/team-switcher.tsx +94 -0
- package/src/templates/sidebar-07/index.ts +1 -0
- package/src/templates/ui-page/index.ts +7 -0
- package/src/templates/ui-page/ui-browser-frame.tsx +27 -0
- package/src/templates/ui-page/ui-template-code.tsx +48 -0
- package/src/templates/ui-page/ui-template-header.tsx +87 -0
- package/src/templates/ui-page/ui-template-navigation.tsx +60 -0
- package/src/templates/ui-page/ui-template-page.tsx +97 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-config.ts +30 -0
- package/src/theme/theme-provider.tsx +142 -0
- package/src/theme/theme-selector.tsx +75 -0
- package/src/tokens/index.ts +15 -0
- 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,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,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
|
+
}
|