canvas-ui-sdk 0.3.6 → 0.3.8
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/dist/index.js +272 -225
- package/dist/index.js.map +1 -1
- package/mcp/dist/index.js +14 -2
- package/package.json +1 -1
- package/registry/blocks/canvas-item.json +1 -1
- package/registry/blocks/chat-message.json +1 -1
- package/registry/blocks/component-palette.json +1 -1
- package/registry/blocks/component-search.json +1 -1
- package/registry/blocks/content-dropzone.json +1 -1
- package/registry/blocks/credit-card-display.json +1 -1
- package/registry/blocks/custom-component-helper.json +1 -1
- package/registry/blocks/empty-state.json +1 -1
- package/registry/blocks/filter-popover.json +1 -1
- package/registry/blocks/fixed-column-data-table.json +1 -1
- package/registry/blocks/infinity-canvas.json +1 -1
- package/registry/blocks/menu-section.json +1 -1
- package/registry/blocks/messenger-sidebar.json +1 -1
- package/registry/blocks/mobile-bottom-nav.json +1 -1
- package/registry/blocks/monthly-calendar-widget.json +1 -1
- package/registry/blocks/page-header-section.json +1 -1
- package/registry/blocks/page-previews.json +1 -1
- package/registry/blocks/pagination.json +1 -1
- package/registry/blocks/persona-card.json +1 -1
- package/registry/blocks/pricing-cards.json +1 -1
- package/registry/blocks/profile-card.json +1 -1
- package/registry/blocks/profile-info-cards.json +1 -1
- package/registry/blocks/prompt-template.json +1 -1
- package/registry/blocks/screen-flowchart.json +1 -1
- package/registry/blocks/screen-prompt-builder.json +1 -1
- package/registry/blocks/screen-prompt-template.json +1 -1
- package/registry/blocks/search-bar.json +1 -1
- package/registry/blocks/sidebar-cards.json +1 -1
- package/registry/blocks/sidebar-profile-card.json +1 -1
- package/registry/blocks/step-tracker.json +1 -1
- package/registry/blocks/vertical-step-tracker.json +1 -1
- package/registry/blocks/video-chat-controls.json +1 -1
- package/registry/layout/account-settings-shell.json +1 -1
- package/registry/layout/dashboard-shell.json +1 -1
- package/registry/layout/double-sidebar-shell.json +1 -1
- package/registry/layout/double-sidebar.json +1 -1
- package/registry/layout/header.json +1 -1
- package/registry/layout/icon-sidebar-shell.json +1 -1
- package/registry/layout/icon-sidebar.json +1 -1
- package/registry/layout/mobile-menu-shell.json +1 -1
- package/registry/layout/multistep-progressbar-shell.json +1 -1
- package/registry/layout/multistep-shell.json +1 -1
- package/registry/layout/multistep-sidebar-shell.json +1 -1
- package/registry/layout/project-context-shell.json +1 -1
- package/registry/layout/search-bar-shell.json +1 -1
- package/registry/layout/sidebar.json +1 -1
- package/registry/layout/standard-page-shell.json +1 -1
- package/registry/layout/vertical-multistep-shell.json +1 -1
- package/registry/ui/avatar.json +1 -1
- package/registry/ui/checkbox.json +1 -1
- package/registry/ui/date-input.json +1 -1
- package/registry/ui/image-uploader.json +1 -1
- package/registry/ui/multiselect-checkbox-field.json +1 -1
- package/registry/ui/multiselect-tags.json +1 -1
- package/registry/ui/radio-group.json +1 -1
- package/registry/ui/searchbox.json +1 -1
- package/registry/ui/select.json +1 -1
- package/registry/ui/selectable-pills.json +1 -1
- package/registry/ui/slider.json +1 -1
- package/registry/ui/switch.json +1 -1
- package/registry/ui/text-input.json +1 -1
- package/registry/ui/textarea.json +1 -1
- package/styles/tokens.reference.css +9 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/fixed-column-data-table.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { MenufocusTemplate } from \"./menufocus-template\";\n\n// ============================================\n// Types\n// ============================================\n\nexport type FixedColumnTableStatus = \"pending\" | \"paid\" | \"overdue\";\n\nexport interface FixedColumnTableRow {\n id: string;\n name: string;\n avatarUrl?: string;\n amount: string;\n status: FixedColumnTableStatus;\n logoUrl?: string;\n company: string;\n dateSent: string;\n}\n\nexport interface FixedColumnTableColumn {\n id: string;\n label: string;\n width?: string;\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface FilterOption {\n id: string;\n label: string;\n}\n\nexport interface FixedColumnDataTableProps {\n /** Table title */\n title?: string;\n /** Number of results to display */\n resultCount?: number;\n /** Custom result count text (overrides default \"{count} results\") */\n resultCountText?: string;\n /** Table data rows */\n data?: FixedColumnTableRow[];\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: FilterOption[];\n /** Primary action button text */\n actionButtonText?: string;\n /** Callback when add/action button is clicked */\n onAddNew?: () => void;\n /** Callback when sort value changes */\n onSort?: (value: string) => void;\n /** Callback when filter value changes */\n onFilter?: (value: string) => void;\n /** Callback when row action is clicked */\n onRowAction?: (action: string, row: FixedColumnTableRow) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultData: FixedColumnTableRow[] = [\n {\n id: \"1\",\n name: \"Jeff Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n amount: \"$3,200\",\n status: \"pending\",\n logoUrl: \"\",\n company: \"Airdev\",\n dateSent: \"5/23/2024\",\n },\n {\n id: \"2\",\n name: \"Aya Williams\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n amount: \"$2,400\",\n status: \"paid\",\n logoUrl: \"\",\n company: \"Airdev\",\n dateSent: \"2/19/2024\",\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"name-asc\", label: \"Name (A-Z)\" },\n { id: \"name-desc\", label: \"Name (Z-A)\" },\n { id: \"date-newest\", label: \"Newest first\" },\n { id: \"date-oldest\", label: \"Oldest first\" },\n { id: \"amount-high\", label: \"Amount (High)\" },\n { id: \"amount-low\", label: \"Amount (Low)\" },\n];\n\nconst defaultFilterOptions: FilterOption[] = [\n { id: \"all\", label: \"All statuses\" },\n { id: \"pending\", label: \"Pending\" },\n { id: \"paid\", label: \"Paid\" },\n { id: \"overdue\", label: \"Overdue\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface TableHeaderCellProps {\n children: React.ReactNode;\n className?: string;\n isFixed?: boolean;\n}\n\nfunction TableHeaderCell({ children, className, isFixed }: TableHeaderCellProps) {\n return (\n <th\n className={cn(\n \"text-left h-8\",\n isFixed && \"sticky left-0 z-10\",\n className\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n backgroundColor: isFixed ? \"var(--canvas-background)\" : undefined,\n }}\n >\n {children}\n </th>\n );\n}\n\ninterface TableCellProps {\n children: React.ReactNode;\n className?: string;\n isFixed?: boolean;\n}\n\nfunction TableCell({ children, className, isFixed }: TableCellProps) {\n return (\n <td\n className={cn(\n \"h-12\",\n isFixed && \"sticky left-0 z-10\",\n className\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n backgroundColor: isFixed ? \"var(--canvas-background)\" : undefined,\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n {children}\n </td>\n );\n}\n\ninterface StatusBadgeProps {\n status: FixedColumnTableStatus;\n}\n\nfunction StatusBadge({ status }: StatusBadgeProps) {\n const statusConfig: Record<FixedColumnTableStatus, { label: string; bgColor: string; textColor: string }> = {\n pending: {\n label: \"Pending\",\n bgColor: \"var(--canvas-surface-brand)\",\n textColor: \"var(--canvas-primary)\",\n },\n paid: {\n label: \"Paid\",\n bgColor: \"var(--canvas-success-surface, #edfdf8)\",\n textColor: \"var(--canvas-success)\",\n },\n overdue: {\n label: \"Overdue\",\n bgColor: \"var(--canvas-destructive-surface, #fef2f2)\",\n textColor: \"var(--canvas-destructive)\",\n },\n };\n\n const config = statusConfig[status];\n\n return (\n <span\n className=\"inline-flex items-center justify-center whitespace-nowrap\"\n style={{\n backgroundColor: config.bgColor,\n color: config.textColor,\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n padding: \"var(--spacing-md) var(--spacing-xl)\",\n borderRadius: \"var(--spacing-3xl)\",\n height: \"32px\",\n }}\n >\n {config.label}\n </span>\n );\n}\n\ninterface CompanyLogoProps {\n logoUrl?: string;\n company: string;\n}\n\nfunction CompanyLogo({ logoUrl, company }: CompanyLogoProps) {\n // Default favicon-style logo if no URL provided\n if (!logoUrl) {\n return (\n <div\n className=\"flex items-center justify-center\"\n style={{\n width: \"32px\",\n height: \"32px\",\n backgroundColor: \"var(--canvas-primary)\",\n borderRadius: \"var(--radius-xs)\",\n }}\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"var(--canvas-primary-foreground)\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\" />\n <polyline points=\"9,22 9,12 15,12 15,22\" />\n </svg>\n </div>\n );\n }\n\n return (\n <img\n src={logoUrl}\n alt={`${company} logo`}\n className=\"object-contain\"\n style={{\n width: \"32px\",\n height: \"32px\",\n borderRadius: \"var(--radius-xs)\",\n }}\n />\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Fixed Column Data Table Block\n * \n * A data table with a fixed first column (Name with avatar) that stays\n * visible during horizontal scrolling. Ideal for invoice-style tables\n * with many columns that need horizontal scrolling on smaller screens.\n * \n * @example\n * ```tsx\n * <FixedColumnDataTable\n * title=\"Invoices\"\n * data={[\n * { id: \"1\", name: \"John\", amount: \"$1,000\", status: \"paid\", company: \"Acme\", dateSent: \"1/1/2024\" }\n * ]}\n * onAddNew={() => console.log(\"Add new\")}\n * />\n * ```\n */\nexport function FixedColumnDataTable({\n title = \"Invoices\",\n resultCount,\n resultCountText,\n data = defaultData,\n sortOptions = defaultSortOptions,\n filterOptions = defaultFilterOptions,\n actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onFilter,\n onRowAction,\n className,\n}: FixedColumnDataTableProps) {\n const [sortValue, setSortValue] = useState<string>(\"\");\n const [filterValue, setFilterValue] = useState<string>(\"\");\n\n const displayResultCount = resultCount ?? data.length;\n const displayResultText = resultCountText ?? `${displayResultCount} results`;\n\n const handleSortChange = (value: string) => {\n setSortValue(value);\n onSort?.(value);\n };\n\n const handleFilterChange = (value: string) => {\n setFilterValue(value);\n onFilter?.(value);\n };\n\n return (\n <div \n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Header Section */}\n <div \n className=\"flex flex-wrap items-start w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title and Count */}\n <div \n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {displayResultText}\n </p>\n </div>\n\n {/* Controls */}\n <div \n className=\"flex items-start justify-end shrink-0 gap-3\"\n >\n {/* Sort Dropdown */}\n <div className=\"w-[120px]\">\n <Select value={sortValue || undefined} onValueChange={handleSortChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Sort\" />\n </SelectTrigger>\n <SelectContent>\n {sortOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Filter Dropdown */}\n <div className=\"w-[120px]\">\n <Select value={filterValue || undefined} onValueChange={handleFilterChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Filter\" />\n </SelectTrigger>\n <SelectContent>\n {filterOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Action Button */}\n <Button\n variant=\"primary\"\n size=\"sm\"\n onClick={onAddNew}\n style={{\n height: \"var(--btn-small-height)\",\n paddingLeft: \"var(--btn-small-px)\",\n paddingRight: \"var(--btn-small-px)\",\n fontSize: \"var(--btn-small-font-size)\",\n borderRadius: \"var(--btn-small-radius)\",\n backgroundColor: \"var(--btn-primary-bg)\",\n color: \"var(--btn-primary-text)\",\n borderColor: \"var(--btn-primary-border)\",\n }}\n >\n {actionButtonText}\n </Button>\n </div>\n </div>\n\n {/* Table Section with horizontal scroll */}\n <div className=\"w-full overflow-x-auto\">\n <table className=\"w-full min-w-max border-collapse\">\n <thead>\n <tr style={{ borderBottom: \"1px solid var(--canvas-border)\" }}>\n {/* Fixed Name Column Header */}\n <TableHeaderCell isFixed className=\"pr-8 min-w-[200px]\">\n Name\n </TableHeaderCell>\n {/* Scrollable Column Headers */}\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Amount\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Status\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[120px]\">\n Logo\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Company\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Date Sent\n </TableHeaderCell>\n <TableHeaderCell className=\"w-12 px-4\">\n \n </TableHeaderCell>\n </tr>\n </thead>\n <tbody>\n {data.map((row) => (\n <tr\n key={row.id}\n style={{ borderBottom: \"1px solid var(--canvas-border)\" }}\n >\n {/* Fixed Name Column */}\n <TableCell isFixed className=\"pr-8\">\n <div className=\"flex items-center gap-2\">\n <Avatar className=\"size-8 border border-[var(--canvas-border)]\">\n <AvatarImage src={row.avatarUrl} alt={row.name} />\n <AvatarFallback>\n {row.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <span className=\"whitespace-nowrap\">{row.name}</span>\n </div>\n </TableCell>\n {/* Scrollable Columns */}\n <TableCell className=\"px-6\">\n <span className=\"whitespace-nowrap\">{row.amount}</span>\n </TableCell>\n <TableCell className=\"px-6\">\n <StatusBadge status={row.status} />\n </TableCell>\n <TableCell className=\"px-6\">\n <CompanyLogo logoUrl={row.logoUrl} company={row.company} />\n </TableCell>\n <TableCell className=\"px-6\">\n <span className=\"whitespace-nowrap\">{row.company}</span>\n </TableCell>\n <TableCell className=\"px-6\">\n <span className=\"whitespace-nowrap\">{row.dateSent}</span>\n </TableCell>\n <TableCell className=\"px-4\">\n <MenufocusTemplate\n ariaLabel=\"Row actions\"\n items={[\n { id: \"edit\", label: \"Edit\", onClick: () => onRowAction?.(\"edit\", row) },\n { id: \"view\", label: \"View details\", onClick: () => onRowAction?.(\"view\", row) },\n { id: \"download\", label: \"Download\", onClick: () => onRowAction?.(\"download\", row) },\n { id: \"delete\", label: \"Delete\", variant: \"destructive\", onClick: () => onRowAction?.(\"delete\", row) },\n ]}\n />\n </TableCell>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </div>\n );\n}\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { MenufocusTemplate } from \"./menufocus-template\";\n\n// ============================================\n// Types\n// ============================================\n\nexport type FixedColumnTableStatus = \"pending\" | \"paid\" | \"overdue\";\n\nexport interface FixedColumnTableRow {\n id: string;\n name: string;\n avatarUrl?: string;\n amount: string;\n status: FixedColumnTableStatus;\n logoUrl?: string;\n company: string;\n dateSent: string;\n}\n\nexport interface FixedColumnTableColumn {\n id: string;\n label: string;\n width?: string;\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface FilterOption {\n id: string;\n label: string;\n}\n\nexport interface FixedColumnDataTableProps {\n /** Table title */\n title?: string;\n /** Number of results to display */\n resultCount?: number;\n /** Custom result count text (overrides default \"{count} results\") */\n resultCountText?: string;\n /** Table data rows */\n data?: FixedColumnTableRow[];\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: FilterOption[];\n /** Primary action button text */\n actionButtonText?: string;\n /** Callback when add/action button is clicked */\n onAddNew?: () => void;\n /** Callback when sort value changes */\n onSort?: (value: string) => void;\n /** Callback when filter value changes */\n onFilter?: (value: string) => void;\n /** Callback when row action is clicked */\n onRowAction?: (action: string, row: FixedColumnTableRow) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultData: FixedColumnTableRow[] = [\n {\n id: \"1\",\n name: \"Jeff Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n amount: \"$3,200\",\n status: \"pending\",\n logoUrl: \"\",\n company: \"Airdev\",\n dateSent: \"5/23/2024\",\n },\n {\n id: \"2\",\n name: \"Aya Williams\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n amount: \"$2,400\",\n status: \"paid\",\n logoUrl: \"\",\n company: \"Airdev\",\n dateSent: \"2/19/2024\",\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"name-asc\", label: \"Name (A-Z)\" },\n { id: \"name-desc\", label: \"Name (Z-A)\" },\n { id: \"date-newest\", label: \"Newest first\" },\n { id: \"date-oldest\", label: \"Oldest first\" },\n { id: \"amount-high\", label: \"Amount (High)\" },\n { id: \"amount-low\", label: \"Amount (Low)\" },\n];\n\nconst defaultFilterOptions: FilterOption[] = [\n { id: \"all\", label: \"All statuses\" },\n { id: \"pending\", label: \"Pending\" },\n { id: \"paid\", label: \"Paid\" },\n { id: \"overdue\", label: \"Overdue\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface TableHeaderCellProps {\n children: React.ReactNode;\n className?: string;\n isFixed?: boolean;\n}\n\nfunction TableHeaderCell({ children, className, isFixed }: TableHeaderCellProps) {\n return (\n <th\n className={cn(\n \"text-left h-8\",\n isFixed && \"sticky left-0 z-10\",\n className\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n backgroundColor: isFixed ? \"var(--canvas-background)\" : undefined,\n }}\n >\n {children}\n </th>\n );\n}\n\ninterface TableCellProps {\n children: React.ReactNode;\n className?: string;\n isFixed?: boolean;\n}\n\nfunction TableCell({ children, className, isFixed }: TableCellProps) {\n return (\n <td\n className={cn(\n \"h-12\",\n isFixed && \"sticky left-0 z-10\",\n className\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n backgroundColor: isFixed ? \"var(--canvas-background)\" : undefined,\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n {children}\n </td>\n );\n}\n\ninterface StatusBadgeProps {\n status: FixedColumnTableStatus;\n}\n\nfunction StatusBadge({ status }: StatusBadgeProps) {\n const statusConfig: Record<FixedColumnTableStatus, { label: string; bgColor: string; textColor: string }> = {\n pending: {\n label: \"Pending\",\n bgColor: \"var(--canvas-surface-brand)\",\n textColor: \"var(--canvas-primary)\",\n },\n paid: {\n label: \"Paid\",\n bgColor: \"var(--canvas-success-surface)\",\n textColor: \"var(--canvas-success)\",\n },\n overdue: {\n label: \"Overdue\",\n bgColor: \"var(--canvas-destructive-surface)\",\n textColor: \"var(--canvas-destructive)\",\n },\n };\n\n const config = statusConfig[status];\n\n return (\n <span\n className=\"inline-flex items-center justify-center whitespace-nowrap\"\n style={{\n backgroundColor: config.bgColor,\n color: config.textColor,\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n padding: \"var(--spacing-md) var(--spacing-xl)\",\n borderRadius: \"var(--spacing-3xl)\",\n height: \"32px\",\n }}\n >\n {config.label}\n </span>\n );\n}\n\ninterface CompanyLogoProps {\n logoUrl?: string;\n company: string;\n}\n\nfunction CompanyLogo({ logoUrl, company }: CompanyLogoProps) {\n // Default favicon-style logo if no URL provided\n if (!logoUrl) {\n return (\n <div\n className=\"flex items-center justify-center\"\n style={{\n width: \"32px\",\n height: \"32px\",\n backgroundColor: \"var(--canvas-primary)\",\n borderRadius: \"var(--radius-xs)\",\n }}\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"var(--canvas-primary-foreground)\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\" />\n <polyline points=\"9,22 9,12 15,12 15,22\" />\n </svg>\n </div>\n );\n }\n\n return (\n <img\n src={logoUrl}\n alt={`${company} logo`}\n className=\"object-contain\"\n style={{\n width: \"32px\",\n height: \"32px\",\n borderRadius: \"var(--radius-xs)\",\n }}\n />\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Fixed Column Data Table Block\n * \n * A data table with a fixed first column (Name with avatar) that stays\n * visible during horizontal scrolling. Ideal for invoice-style tables\n * with many columns that need horizontal scrolling on smaller screens.\n * \n * @example\n * ```tsx\n * <FixedColumnDataTable\n * title=\"Invoices\"\n * data={[\n * { id: \"1\", name: \"John\", amount: \"$1,000\", status: \"paid\", company: \"Acme\", dateSent: \"1/1/2024\" }\n * ]}\n * onAddNew={() => console.log(\"Add new\")}\n * />\n * ```\n */\nexport function FixedColumnDataTable({\n title = \"Invoices\",\n resultCount,\n resultCountText,\n data = defaultData,\n sortOptions = defaultSortOptions,\n filterOptions = defaultFilterOptions,\n actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onFilter,\n onRowAction,\n className,\n}: FixedColumnDataTableProps) {\n const [sortValue, setSortValue] = useState<string>(\"\");\n const [filterValue, setFilterValue] = useState<string>(\"\");\n\n const displayResultCount = resultCount ?? data.length;\n const displayResultText = resultCountText ?? `${displayResultCount} results`;\n\n const handleSortChange = (value: string) => {\n setSortValue(value);\n onSort?.(value);\n };\n\n const handleFilterChange = (value: string) => {\n setFilterValue(value);\n onFilter?.(value);\n };\n\n return (\n <div \n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Header Section */}\n <div \n className=\"flex flex-wrap items-start w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title and Count */}\n <div \n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {displayResultText}\n </p>\n </div>\n\n {/* Controls */}\n <div \n className=\"flex items-start justify-end shrink-0 gap-3\"\n >\n {/* Sort Dropdown */}\n <div className=\"w-[120px]\">\n <Select value={sortValue || undefined} onValueChange={handleSortChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Sort\" />\n </SelectTrigger>\n <SelectContent>\n {sortOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Filter Dropdown */}\n <div className=\"w-[120px]\">\n <Select value={filterValue || undefined} onValueChange={handleFilterChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Filter\" />\n </SelectTrigger>\n <SelectContent>\n {filterOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Action Button */}\n <Button\n variant=\"primary\"\n size=\"sm\"\n onClick={onAddNew}\n style={{\n height: \"var(--btn-small-height)\",\n paddingLeft: \"var(--btn-small-px)\",\n paddingRight: \"var(--btn-small-px)\",\n fontSize: \"var(--btn-small-font-size)\",\n borderRadius: \"var(--btn-small-radius)\",\n backgroundColor: \"var(--btn-primary-bg)\",\n color: \"var(--btn-primary-text)\",\n borderColor: \"var(--btn-primary-border)\",\n }}\n >\n {actionButtonText}\n </Button>\n </div>\n </div>\n\n {/* Table Section with horizontal scroll */}\n <div className=\"w-full overflow-x-auto\">\n <table className=\"w-full min-w-max border-collapse\">\n <thead>\n <tr style={{ borderBottom: \"1px solid var(--canvas-border)\" }}>\n {/* Fixed Name Column Header */}\n <TableHeaderCell isFixed className=\"pr-8 min-w-[200px]\">\n Name\n </TableHeaderCell>\n {/* Scrollable Column Headers */}\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Amount\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Status\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[120px]\">\n Logo\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Company\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Date Sent\n </TableHeaderCell>\n <TableHeaderCell className=\"w-12 px-4\">\n \n </TableHeaderCell>\n </tr>\n </thead>\n <tbody>\n {data.map((row) => (\n <tr\n key={row.id}\n style={{ borderBottom: \"1px solid var(--canvas-border)\" }}\n >\n {/* Fixed Name Column */}\n <TableCell isFixed className=\"pr-8\">\n <div className=\"flex items-center gap-2\">\n <Avatar className=\"size-8 border border-[var(--canvas-border)]\">\n <AvatarImage src={row.avatarUrl} alt={row.name} />\n <AvatarFallback>\n {row.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <span className=\"whitespace-nowrap\">{row.name}</span>\n </div>\n </TableCell>\n {/* Scrollable Columns */}\n <TableCell className=\"px-6\">\n <span className=\"whitespace-nowrap\">{row.amount}</span>\n </TableCell>\n <TableCell className=\"px-6\">\n <StatusBadge status={row.status} />\n </TableCell>\n <TableCell className=\"px-6\">\n <CompanyLogo logoUrl={row.logoUrl} company={row.company} />\n </TableCell>\n <TableCell className=\"px-6\">\n <span className=\"whitespace-nowrap\">{row.company}</span>\n </TableCell>\n <TableCell className=\"px-6\">\n <span className=\"whitespace-nowrap\">{row.dateSent}</span>\n </TableCell>\n <TableCell className=\"px-4\">\n <MenufocusTemplate\n ariaLabel=\"Row actions\"\n items={[\n { id: \"edit\", label: \"Edit\", onClick: () => onRowAction?.(\"edit\", row) },\n { id: \"view\", label: \"View details\", onClick: () => onRowAction?.(\"view\", row) },\n { id: \"download\", label: \"Download\", onClick: () => onRowAction?.(\"download\", row) },\n { id: \"delete\", label: \"Delete\", variant: \"destructive\", onClick: () => onRowAction?.(\"delete\", row) },\n ]}\n />\n </TableCell>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </div>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/infinity-canvas.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState, useCallback, useRef, useEffect } from \"react\";\nimport { TransformWrapper, TransformComponent, ReactZoomPanPinchRef } from \"react-zoom-pan-pinch\";\nimport { useDroppable } from \"@dnd-kit/core\";\nimport { cn } from \"../../lib/utils\";\nimport { CanvasItem, CanvasItemData } from \"./canvas-item\";\nimport { ZoomIn, ZoomOut, Maximize2, Grid3X3 } from \"lucide-react\";\n\n// =====================\n// Block Component Imports\n// =====================\nimport { ProfileCard } from \"./profile-card\";\nimport { StandardDataTable } from \"./standard-data-table\";\nimport { StepTracker, defaultSteps } from \"./step-tracker\";\nimport { VerticalStepTracker } from \"./vertical-step-tracker\";\nimport { ProgressBar } from \"./progress-bar\";\nimport { FlairBanner } from \"./flair-banner\";\nimport { GradientBanner } from \"./gradient-banner\";\nimport { MessengerSidebar } from \"./messenger-sidebar\";\nimport { PillTabs } from \"./pill-tabs\";\nimport { ChatBubble } from \"./chat-message\";\nimport { VideoChatControls } from \"./video-chat-controls\";\nimport { WebcamPreview } from \"./webcam-preview\";\nimport { ParticipantList } from \"./participant-list\";\nimport { VideoContentSection } from \"./video-content-section\";\nimport { VideoPlaylistCard } from \"./video-playlist\";\nimport { SearchBar } from \"./search-bar\";\nimport { FilterPopover } from \"./filter-popover\";\nimport { SettingsListRow } from \"./settings-list-row\";\nimport { ProfileImageUploader } from \"./profile-image-uploader\";\nimport { LoginBrandingPanel } from \"./login-branding-panel\";\nimport { SidebarProfileCard } from \"./sidebar-profile-card\";\nimport { StatsCard, PortfolioCard } from \"./profile-info-cards\";\nimport { LinksCard, InfoCard } from \"./sidebar-cards\";\nimport { CreditCardDisplay } from \"./credit-card-display\";\nimport { PageHeaderSection } from \"./page-header-section\";\nimport { MobileBottomNav } from \"./mobile-bottom-nav\";\n\n// Marketing blocks\nimport { \n HeroSection,\n HeroDarkWithImage,\n CenteredHero,\n TestimonialCarousel,\n ReviewsGrid,\n SocialProof,\n MetricsSection,\n FeatureWithImage,\n CoreValuesGrid,\n DestinationCards,\n TeamCardsGrid,\n TeamCircularGrid,\n CtaBanner,\n FooterNavbar,\n FeaturedNewsCards,\n OfficeLocations,\n} from \"./marketing\";\n\n// Pricing blocks\nimport { PricingCards, FaqAccordion, FeaturesComparison } from \"./pricing\";\n\n// Page Template Previews\nimport {\n PageAboutPreview,\n PageAccountPreview,\n PageAdminPortalPreview,\n PageCenteredProfilePreview,\n PageDoubleSidebarPreview,\n PageIconSidebarPreview,\n PageLoginPreview,\n PageMenuSectionsPreview,\n PageMessengerPreview,\n PageMobileMenuPreview,\n PageMultistepProgressbarPreview,\n PageMultistepSidebarPreview,\n PagePricingPreview,\n PageProductHomepagePreview,\n PageResetPasswordPreview,\n PageSearchBarPreview,\n PageSidebarProfilePreview,\n PageStandardPreview,\n PageStandardMultistepPreview,\n PageStandardSearchPreview,\n PageVerticalMultistepPreview,\n PageVideoChatPreview,\n PageVideoListPreview,\n} from \"./page-previews\";\n\n// UI Components\nimport { Button } from \"../ui/button\";\nimport { Checkbox } from \"../ui/checkbox\";\nimport { Input } from \"../ui/input\";\nimport { Switch } from \"../ui/switch\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { RadioGroup, RadioGroupItem } from \"../ui/radio-group\";\nimport { Label } from \"../ui/label\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\n\n// =====================\n// Sample Data\n// =====================\nconst sampleProfileData = {\n name: \"Jeff Connor\",\n username: \"@jconnor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n rating: 5,\n location: \"San Francisco, CA\",\n joinDate: \"Joined January 2020\",\n stats: [\n { value: \"234\", label: \"Posts\" },\n { value: \"12.5k\", label: \"Followers\" },\n { value: \"892\", label: \"Following\" },\n ],\n bio: \"Product designer and creative director. Building beautiful digital experiences.\",\n tags: [{ label: \"Design\" }, { label: \"Product\" }, { label: \"Strategy\" }],\n};\n\nconst samplePillTabs = [\n { id: \"all\", label: \"All\" },\n { id: \"design\", label: \"Design\" },\n { id: \"dev\", label: \"Development\" },\n];\n\nconst sampleStats = [\n { value: \"234\", label: \"Posts\" },\n { value: \"12.5k\", label: \"Followers\" },\n { value: \"892\", label: \"Following\" },\n];\n\nconst sampleMobileNavItems = [\n { id: \"home\", label: \"Home\", icon: \"home\" as const },\n { id: \"search\", label: \"Search\", icon: \"search\" as const },\n { id: \"profile\", label: \"Profile\", icon: \"user\" as const },\n];\n\n\n// =====================\n// Component Renderers\n// =====================\nconst componentRenderers: Record<string, () => React.ReactNode> = {\n // =====================\n // PAGE TEMPLATES (scaled previews)\n // =====================\n PageAbout: () => <PageAboutPreview />,\n PageAccount: () => <PageAccountPreview />,\n PageAdminPortal: () => <PageAdminPortalPreview />,\n PageCenteredProfile: () => <PageCenteredProfilePreview />,\n PageDoubleSidebar: () => <PageDoubleSidebarPreview />,\n PageIconSidebar: () => <PageIconSidebarPreview />,\n PageLogin: () => <PageLoginPreview />,\n PageMenuSections: () => <PageMenuSectionsPreview />,\n PageMessenger: () => <PageMessengerPreview />,\n PageMobileMenu: () => <PageMobileMenuPreview />,\n PageMultistepProgressbar: () => <PageMultistepProgressbarPreview />,\n PageMultistepSidebar: () => <PageMultistepSidebarPreview />,\n PagePricing: () => <PagePricingPreview />,\n PageProductHomepage: () => <PageProductHomepagePreview />,\n PageResetPassword: () => <PageResetPasswordPreview />,\n PageSearchBar: () => <PageSearchBarPreview />,\n PageSidebarProfile: () => <PageSidebarProfilePreview />,\n PageStandard: () => <PageStandardPreview />,\n PageStandardMultistep: () => <PageStandardMultistepPreview />,\n PageStandardSearch: () => <PageStandardSearchPreview />,\n PageVerticalMultistep: () => <PageVerticalMultistepPreview />,\n PageVideoChat: () => <PageVideoChatPreview />,\n PageVideoList: () => <PageVideoListPreview />,\n\n // =====================\n // BLOCKS - Data & Tables\n // =====================\n StandardDataTable: () => (\n <div className=\"w-[600px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <StandardDataTable title=\"Team Members\" />\n </div>\n ),\n\n // =====================\n // BLOCKS - Cards & Profiles\n // =====================\n ProfileCard: () => (\n <div className=\"w-[400px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <ProfileCard {...sampleProfileData} showMenu={false} />\n </div>\n ),\n SidebarProfileCard: () => (\n <div className=\"w-[280px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <SidebarProfileCard \n name=\"Jeff Connor\" \n role=\"Product Designer\"\n avatarUrl={sampleProfileData.avatarUrl}\n />\n </div>\n ),\n ProfileInfoCards: () => (\n <div className=\"w-[400px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <StatsCard stats={sampleStats} />\n </div>\n ),\n SidebarCards: () => (\n <div className=\"w-[280px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <LinksCard />\n </div>\n ),\n CreditCardDisplay: () => (\n <div className=\"w-[360px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <CreditCardDisplay \n cardNumber=\"4242 4242 4242 4242\"\n cardHolder=\"JEFF CONNOR\"\n expiry=\"12/28\"\n />\n </div>\n ),\n\n // =====================\n // BLOCKS - Navigation & Progress\n // =====================\n StepTracker: () => (\n <div className=\"w-[500px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <StepTracker steps={defaultSteps} currentStep={1} />\n </div>\n ),\n VerticalStepTracker: () => (\n <div className=\"w-[280px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <VerticalStepTracker steps={defaultSteps} currentStep={1} />\n </div>\n ),\n ProgressBar: () => (\n <div className=\"w-[300px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <ProgressBar progress={65} />\n </div>\n ),\n PillTabs: () => (\n <div className=\"w-[300px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <PillTabs tabs={samplePillTabs} activeTab=\"all\" />\n </div>\n ),\n MobileBottomNav: () => (\n <div className=\"w-[375px] bg-white rounded-lg shadow-sm border border-[var(--canvas-border)] overflow-hidden\">\n <MobileBottomNav items={sampleMobileNavItems} activeItem=\"home\" />\n </div>\n ),\n\n // =====================\n // BLOCKS - Banners & Headers\n // =====================\n FlairBanner: () => (\n <div className=\"w-[500px] overflow-hidden rounded-lg shadow-sm\">\n <FlairBanner title=\"Welcome\" />\n </div>\n ),\n GradientBanner: () => (\n <div className=\"w-[500px] overflow-hidden rounded-lg shadow-sm\">\n <GradientBanner title=\"Welcome\" subtitle=\"Get started with your dashboard\" />\n </div>\n ),\n PageHeaderSection: () => (\n <div className=\"w-[600px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <PageHeaderSection \n title=\"Dashboard\" \n description=\"Manage your account and settings\"\n showTabs={false}\n />\n </div>\n ),\n\n // =====================\n // BLOCKS - Chat & Messaging\n // =====================\n MessengerSidebar: () => (\n <div className=\"w-[375px] h-[500px] overflow-hidden rounded-lg shadow-sm border border-[var(--canvas-border)]\">\n <MessengerSidebar />\n </div>\n ),\n ChatMessage: () => (\n <div className=\"w-[400px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <ChatBubble \n message={{\n id: \"1\",\n content: \"Hey! How's the project going?\",\n sender: \"Jeff Connor\",\n timestamp: \"2:30 PM\",\n isOwn: false,\n }}\n />\n </div>\n ),\n\n // =====================\n // BLOCKS - Video\n // =====================\n VideoChatControls: () => (\n <div className=\"w-[400px] bg-[var(--canvas-text)] rounded-lg p-4 shadow-sm\">\n <VideoChatControls />\n </div>\n ),\n WebcamPreview: () => (\n <div className=\"w-[320px] h-[240px] overflow-hidden rounded-lg shadow-sm border border-[var(--canvas-border)]\">\n <WebcamPreview />\n </div>\n ),\n ParticipantList: () => (\n <div className=\"w-[280px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <ParticipantList />\n </div>\n ),\n VideoContentSection: () => (\n <div className=\"w-[600px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <VideoContentSection />\n </div>\n ),\n VideoPlaylist: () => (\n <div className=\"w-[320px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <VideoPlaylistCard />\n </div>\n ),\n\n // =====================\n // BLOCKS - Search & Filters\n // =====================\n SearchBar: () => (\n <div className=\"w-[400px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <SearchBar placeholder=\"Search...\" />\n </div>\n ),\n FilterPopover: () => (\n <div className=\"w-[300px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <FilterPopover />\n </div>\n ),\n\n // =====================\n // BLOCKS - Forms & Settings\n // =====================\n SettingsListRow: () => (\n <div className=\"w-[400px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <SettingsListRow label=\"Email\" value=\"jeff@example.com\" />\n </div>\n ),\n ProfileImageUploader: () => (\n <div className=\"w-[200px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <ProfileImageUploader currentImage={sampleProfileData.avatarUrl} />\n </div>\n ),\n LoginBrandingPanel: () => (\n <div className=\"w-[400px] h-[300px] overflow-hidden rounded-lg shadow-sm\">\n <LoginBrandingPanel />\n </div>\n ),\n\n // =====================\n // BLOCKS - Marketing Heroes\n // =====================\n HeroSection: () => (\n <div className=\"w-[600px] overflow-hidden rounded-lg shadow-sm\">\n <HeroSection \n title=\"Plan your next adventure\" \n subtitle=\"Live like locals from anywhere\"\n />\n </div>\n ),\n HeroDarkWithImage: () => (\n <div className=\"w-[700px] overflow-hidden rounded-lg shadow-sm\">\n <HeroDarkWithImage />\n </div>\n ),\n CenteredHero: () => (\n <div className=\"w-[600px] overflow-hidden rounded-lg shadow-sm\">\n <CenteredHero \n title=\"Build something amazing\"\n subtitle=\"The platform for modern developers\"\n />\n </div>\n ),\n\n // =====================\n // BLOCKS - Marketing Social Proof\n // =====================\n TestimonialCarousel: () => (\n <div className=\"w-[700px] overflow-hidden rounded-lg shadow-sm\">\n <TestimonialCarousel />\n </div>\n ),\n ReviewsGrid: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <ReviewsGrid />\n </div>\n ),\n SocialProof: () => (\n <div className=\"w-[600px] overflow-hidden rounded-lg shadow-sm bg-white\">\n <SocialProof label=\"TRUSTED BY\" />\n </div>\n ),\n MetricsSection: () => (\n <div className=\"w-[700px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <MetricsSection />\n </div>\n ),\n\n // =====================\n // BLOCKS - Marketing Features\n // =====================\n FeatureWithImage: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <FeatureWithImage \n title=\"Powerful analytics\"\n description=\"Get insights into your data with our advanced analytics tools.\"\n />\n </div>\n ),\n CoreValuesGrid: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <CoreValuesGrid />\n </div>\n ),\n DestinationCards: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <DestinationCards />\n </div>\n ),\n\n // =====================\n // BLOCKS - Marketing Team\n // =====================\n TeamCardsGrid: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <TeamCardsGrid />\n </div>\n ),\n TeamCircularGrid: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <TeamCircularGrid />\n </div>\n ),\n\n // =====================\n // BLOCKS - Marketing CTA & Footer\n // =====================\n CtaBanner: () => (\n <div className=\"w-[500px] overflow-hidden rounded-lg shadow-sm\">\n <CtaBanner title=\"Ready to get started?\" buttonText=\"Sign up\" />\n </div>\n ),\n FooterNavbar: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm\">\n <FooterNavbar />\n </div>\n ),\n\n // =====================\n // BLOCKS - Marketing Other\n // =====================\n FeaturedNewsCards: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <FeaturedNewsCards />\n </div>\n ),\n OfficeLocations: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <OfficeLocations />\n </div>\n ),\n\n // =====================\n // BLOCKS - Pricing\n // =====================\n PricingCards: () => (\n <div className=\"w-[900px] overflow-hidden rounded-lg shadow-sm\">\n <PricingCards />\n </div>\n ),\n FaqAccordion: () => (\n <div className=\"w-[600px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <FaqAccordion />\n </div>\n ),\n FeaturesComparison: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <FeaturesComparison />\n </div>\n ),\n\n // =====================\n // UI COMPONENTS\n // =====================\n Button: () => (\n <div className=\"w-[200px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)] flex flex-col gap-3\">\n <Button>Primary Button</Button>\n <Button variant=\"outline\">Outline Button</Button>\n <Button variant=\"ghost\">Ghost Button</Button>\n </div>\n ),\n Checkbox: () => (\n <div className=\"w-[200px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <div className=\"flex items-center space-x-2\">\n <Checkbox id=\"terms\" defaultChecked />\n <Label htmlFor=\"terms\">Accept terms</Label>\n </div>\n </div>\n ),\n DateInput: () => (\n <div className=\"w-[250px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <Input type=\"date\" />\n </div>\n ),\n Input: () => (\n <div className=\"w-[300px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <Input placeholder=\"Enter your email...\" />\n </div>\n ),\n Select: () => (\n <div className=\"w-[250px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <Select>\n <SelectTrigger>\n <SelectValue placeholder=\"Select option\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"1\">Option 1</SelectItem>\n <SelectItem value=\"2\">Option 2</SelectItem>\n <SelectItem value=\"3\">Option 3</SelectItem>\n </SelectContent>\n </Select>\n </div>\n ),\n Switch: () => (\n <div className=\"w-[200px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <div className=\"flex items-center space-x-2\">\n <Switch id=\"notifications\" defaultChecked />\n <Label htmlFor=\"notifications\">Notifications</Label>\n </div>\n </div>\n ),\n RadioGroup: () => (\n <div className=\"w-[200px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <RadioGroup defaultValue=\"option-1\">\n <div className=\"flex items-center space-x-2\">\n <RadioGroupItem value=\"option-1\" id=\"option-1\" />\n <Label htmlFor=\"option-1\">Option 1</Label>\n </div>\n <div className=\"flex items-center space-x-2\">\n <RadioGroupItem value=\"option-2\" id=\"option-2\" />\n <Label htmlFor=\"option-2\">Option 2</Label>\n </div>\n </RadioGroup>\n </div>\n ),\n MultiselectTags: () => (\n <div className=\"w-[300px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <div className=\"flex flex-wrap gap-2\">\n <span className=\"px-2 py-1 bg-[var(--canvas-primary)] text-white text-xs rounded-full\">Design</span>\n <span className=\"px-2 py-1 bg-[var(--canvas-primary)] text-white text-xs rounded-full\">Development</span>\n <span className=\"px-2 py-1 border border-[var(--canvas-border)] text-[var(--canvas-text-muted)] text-xs rounded-full\">+ Add tag</span>\n </div>\n </div>\n ),\n Avatar: () => (\n <div className=\"w-[200px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)] flex gap-3\">\n <Avatar>\n <AvatarImage src={sampleProfileData.avatarUrl} />\n <AvatarFallback>JC</AvatarFallback>\n </Avatar>\n <Avatar>\n <AvatarFallback>AB</AvatarFallback>\n </Avatar>\n </div>\n ),\n Badge: () => (\n <div className=\"w-[250px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)] flex flex-wrap gap-2\">\n <span className=\"px-2 py-1 bg-[var(--canvas-primary)] text-white text-xs rounded-full\">Default</span>\n <span className=\"px-2 py-1 bg-[var(--canvas-surface)] text-[var(--canvas-text)] text-xs rounded-full\">Secondary</span>\n <span className=\"px-2 py-1 border border-[var(--canvas-border)] text-[var(--canvas-text-muted)] text-xs rounded-full\">Outline</span>\n <span className=\"px-2 py-1 bg-red-500 text-white text-xs rounded-full\">Destructive</span>\n </div>\n ),\n};\n\n// =====================\n// InfinityCanvas Component\n// =====================\ninterface InfinityCanvasProps {\n items: CanvasItemData[];\n onItemsChange: (items: CanvasItemData[]) => void;\n selectedId: string | null;\n onSelectItem: (id: string | null) => void;\n}\n\n/**\n * Infinity Canvas - Pannable, zoomable canvas for placing components\n * \n * Features:\n * - Pan by dragging empty space\n * - Zoom with scroll wheel or controls\n * - Drop zone for components from palette\n * - Grid background for visual reference\n */\nexport function InfinityCanvas({\n items,\n onItemsChange,\n selectedId,\n onSelectItem,\n}: InfinityCanvasProps) {\n const [showGrid, setShowGrid] = useState(true);\n const transformRef = useRef<ReactZoomPanPinchRef>(null);\n const [scale, setScale] = useState(1);\n \n // Drag state - stores starting positions\n const dragStateRef = useRef<{\n itemId: string;\n startMouseX: number;\n startMouseY: number;\n startItemX: number;\n startItemY: number;\n } | null>(null);\n const [isDragging, setIsDragging] = useState(false);\n\n // Set up droppable area\n const { setNodeRef, isOver } = useDroppable({\n id: \"canvas-drop-zone\",\n });\n\n // Handle item deletion\n const handleDeleteItem = useCallback((id: string) => {\n onItemsChange(items.filter(item => item.id !== id));\n if (selectedId === id) {\n onSelectItem(null);\n }\n }, [items, onItemsChange, selectedId, onSelectItem]);\n\n // Handle item drag start - receives mouse position and item position\n const handleDragStart = useCallback((id: string, startMouseX: number, startMouseY: number, startItemX: number, startItemY: number) => {\n dragStateRef.current = {\n itemId: id,\n startMouseX,\n startMouseY,\n startItemX,\n startItemY,\n };\n setIsDragging(true);\n }, []);\n\n // Handle mouse move for dragging\n useEffect(() => {\n if (!isDragging) return;\n\n const handleMouseMove = (e: MouseEvent) => {\n const dragState = dragStateRef.current;\n if (!dragState || !transformRef.current) return;\n\n const state = transformRef.current.instance.transformState;\n \n // Calculate delta from start position, accounting for zoom\n const deltaX = (e.clientX - dragState.startMouseX) / state.scale;\n const deltaY = (e.clientY - dragState.startMouseY) / state.scale;\n \n // New position = original position + delta\n const newX = dragState.startItemX + deltaX;\n const newY = dragState.startItemY + deltaY;\n\n onItemsChange(items.map(item => \n item.id === dragState.itemId \n ? { ...item, x: Math.max(0, newX), y: Math.max(0, newY) }\n : item\n ));\n };\n\n const handleMouseUp = () => {\n dragStateRef.current = null;\n setIsDragging(false);\n };\n\n window.addEventListener(\"mousemove\", handleMouseMove);\n window.addEventListener(\"mouseup\", handleMouseUp);\n\n return () => {\n window.removeEventListener(\"mousemove\", handleMouseMove);\n window.removeEventListener(\"mouseup\", handleMouseUp);\n };\n }, [isDragging, items, onItemsChange]);\n\n // Handle keyboard shortcuts\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Delete\" || e.key === \"Backspace\") {\n if (selectedId && document.activeElement?.tagName !== \"INPUT\") {\n handleDeleteItem(selectedId);\n }\n }\n if (e.key === \"Escape\") {\n onSelectItem(null);\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [selectedId, handleDeleteItem, onSelectItem]);\n\n // Click on empty canvas to deselect\n const handleCanvasClick = () => {\n onSelectItem(null);\n };\n\n return (\n <div \n ref={setNodeRef}\n className={cn(\n \"relative flex-1 overflow-hidden\",\n \"bg-[#f8f9fa]\",\n isOver && \"ring-2 ring-inset ring-[var(--canvas-primary)]\"\n )}\n >\n {/* Zoom Controls */}\n <div className=\"absolute top-4 right-4 z-20 flex items-center gap-2\">\n <button\n onClick={() => setShowGrid(!showGrid)}\n className={cn(\n \"p-2 rounded-md border shadow-sm transition-colors\",\n showGrid \n ? \"bg-[var(--canvas-primary)] text-white border-[var(--canvas-primary)]\" \n : \"bg-white text-[var(--canvas-text-muted)] border-[var(--canvas-border)] hover:bg-[var(--canvas-surface)]\"\n )}\n aria-label=\"Toggle grid\"\n >\n <Grid3X3 className=\"size-4\" />\n </button>\n <div className=\"flex items-center bg-white rounded-md border border-[var(--canvas-border)] shadow-sm\">\n <button\n onClick={() => transformRef.current?.zoomOut()}\n className=\"p-2 hover:bg-[var(--canvas-surface)] rounded-l-md transition-colors\"\n aria-label=\"Zoom out\"\n >\n <ZoomOut className=\"size-4 text-[var(--canvas-text-muted)]\" />\n </button>\n <span className=\"px-3 text-sm text-[var(--canvas-text-muted)] border-x border-[var(--canvas-border)] min-w-[60px] text-center\">\n {Math.round(scale * 100)}%\n </span>\n <button\n onClick={() => transformRef.current?.zoomIn()}\n className=\"p-2 hover:bg-[var(--canvas-surface)] transition-colors\"\n aria-label=\"Zoom in\"\n >\n <ZoomIn className=\"size-4 text-[var(--canvas-text-muted)]\" />\n </button>\n <button\n onClick={() => transformRef.current?.resetTransform()}\n className=\"p-2 hover:bg-[var(--canvas-surface)] rounded-r-md transition-colors border-l border-[var(--canvas-border)]\"\n aria-label=\"Reset view\"\n >\n <Maximize2 className=\"size-4 text-[var(--canvas-text-muted)]\" />\n </button>\n </div>\n </div>\n\n {/* Canvas */}\n <TransformWrapper\n ref={transformRef}\n initialScale={1}\n minScale={0.1}\n maxScale={2}\n limitToBounds={false}\n onTransformed={(_, state) => setScale(state.scale)}\n panning={{\n disabled: isDragging,\n velocityDisabled: true,\n }}\n wheel={{\n smoothStep: 0.05,\n }}\n >\n <TransformComponent\n wrapperStyle={{\n width: \"100%\",\n height: \"100%\",\n }}\n contentStyle={{\n width: \"5000px\",\n height: \"5000px\",\n }}\n >\n <div\n className={cn(\n \"w-full h-full relative\",\n showGrid && \"bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImdyaWQiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTSAwIDEwIEwgNDAgMTAgTSAxMCAwIEwgMTAgNDAgTSAwIDIwIEwgNDAgMjAgTSAyMCAwIEwgMjAgNDAgTSAwIDMwIEwgNDAgMzAgTSAzMCAwIEwgMzAgNDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2UwZTBlMCIgc3Ryb2tlLXdpZHRoPSIxIi8+PHBhdGggZD0iTSA0MCAwIEwgMCA0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJub25lIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2dyaWQpIi8+PC9zdmc+')]\"\n )}\n onClick={handleCanvasClick}\n >\n {/* Render canvas items */}\n {items.map((item) => {\n const renderer = componentRenderers[item.componentType];\n if (!renderer) return null;\n\n return (\n <CanvasItem\n key={item.id}\n item={item}\n isSelected={selectedId === item.id}\n onSelect={onSelectItem}\n onDelete={handleDeleteItem}\n onDragStart={handleDragStart}\n scale={scale}\n >\n {renderer()}\n </CanvasItem>\n );\n })}\n\n {/* Empty state hint */}\n {items.length === 0 && (\n <div className=\"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center pointer-events-none\">\n <p className=\"text-[var(--canvas-text-muted)] text-lg\">\n Drag components from the sidebar to get started\n </p>\n <p className=\"text-[var(--canvas-text-placeholder)] text-sm mt-2\">\n Scroll to zoom • Drag to pan\n </p>\n </div>\n )}\n </div>\n </TransformComponent>\n </TransformWrapper>\n </div>\n );\n}\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState, useCallback, useRef, useEffect } from \"react\";\nimport { TransformWrapper, TransformComponent, ReactZoomPanPinchRef } from \"react-zoom-pan-pinch\";\nimport { useDroppable } from \"@dnd-kit/core\";\nimport { cn } from \"../../lib/utils\";\nimport { CanvasItem, CanvasItemData } from \"./canvas-item\";\nimport { ZoomIn, ZoomOut, Maximize2, Grid3X3 } from \"lucide-react\";\n\n// =====================\n// Block Component Imports\n// =====================\nimport { ProfileCard } from \"./profile-card\";\nimport { StandardDataTable } from \"./standard-data-table\";\nimport { StepTracker, defaultSteps } from \"./step-tracker\";\nimport { VerticalStepTracker } from \"./vertical-step-tracker\";\nimport { ProgressBar } from \"./progress-bar\";\nimport { FlairBanner } from \"./flair-banner\";\nimport { GradientBanner } from \"./gradient-banner\";\nimport { MessengerSidebar } from \"./messenger-sidebar\";\nimport { PillTabs } from \"./pill-tabs\";\nimport { ChatBubble } from \"./chat-message\";\nimport { VideoChatControls } from \"./video-chat-controls\";\nimport { WebcamPreview } from \"./webcam-preview\";\nimport { ParticipantList } from \"./participant-list\";\nimport { VideoContentSection } from \"./video-content-section\";\nimport { VideoPlaylistCard } from \"./video-playlist\";\nimport { SearchBar } from \"./search-bar\";\nimport { FilterPopover } from \"./filter-popover\";\nimport { SettingsListRow } from \"./settings-list-row\";\nimport { ProfileImageUploader } from \"./profile-image-uploader\";\nimport { LoginBrandingPanel } from \"./login-branding-panel\";\nimport { SidebarProfileCard } from \"./sidebar-profile-card\";\nimport { StatsCard, PortfolioCard } from \"./profile-info-cards\";\nimport { LinksCard, InfoCard } from \"./sidebar-cards\";\nimport { CreditCardDisplay } from \"./credit-card-display\";\nimport { PageHeaderSection } from \"./page-header-section\";\nimport { MobileBottomNav } from \"./mobile-bottom-nav\";\n\n// Marketing blocks\nimport { \n HeroSection,\n HeroDarkWithImage,\n CenteredHero,\n TestimonialCarousel,\n ReviewsGrid,\n SocialProof,\n MetricsSection,\n FeatureWithImage,\n CoreValuesGrid,\n DestinationCards,\n TeamCardsGrid,\n TeamCircularGrid,\n CtaBanner,\n FooterNavbar,\n FeaturedNewsCards,\n OfficeLocations,\n} from \"./marketing\";\n\n// Pricing blocks\nimport { PricingCards, FaqAccordion, FeaturesComparison } from \"./pricing\";\n\n// Page Template Previews\nimport {\n PageAboutPreview,\n PageAccountPreview,\n PageAdminPortalPreview,\n PageCenteredProfilePreview,\n PageDoubleSidebarPreview,\n PageIconSidebarPreview,\n PageLoginPreview,\n PageMenuSectionsPreview,\n PageMessengerPreview,\n PageMobileMenuPreview,\n PageMultistepProgressbarPreview,\n PageMultistepSidebarPreview,\n PagePricingPreview,\n PageProductHomepagePreview,\n PageResetPasswordPreview,\n PageSearchBarPreview,\n PageSidebarProfilePreview,\n PageStandardPreview,\n PageStandardMultistepPreview,\n PageStandardSearchPreview,\n PageVerticalMultistepPreview,\n PageVideoChatPreview,\n PageVideoListPreview,\n} from \"./page-previews\";\n\n// UI Components\nimport { Button } from \"../ui/button\";\nimport { Checkbox } from \"../ui/checkbox\";\nimport { Input } from \"../ui/input\";\nimport { Switch } from \"../ui/switch\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { RadioGroup, RadioGroupItem } from \"../ui/radio-group\";\nimport { Label } from \"../ui/label\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\n\n// =====================\n// Sample Data\n// =====================\nconst sampleProfileData = {\n name: \"Jeff Connor\",\n username: \"@jconnor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n rating: 5,\n location: \"San Francisco, CA\",\n joinDate: \"Joined January 2020\",\n stats: [\n { value: \"234\", label: \"Posts\" },\n { value: \"12.5k\", label: \"Followers\" },\n { value: \"892\", label: \"Following\" },\n ],\n bio: \"Product designer and creative director. Building beautiful digital experiences.\",\n tags: [{ label: \"Design\" }, { label: \"Product\" }, { label: \"Strategy\" }],\n};\n\nconst samplePillTabs = [\n { id: \"all\", label: \"All\" },\n { id: \"design\", label: \"Design\" },\n { id: \"dev\", label: \"Development\" },\n];\n\nconst sampleStats = [\n { value: \"234\", label: \"Posts\" },\n { value: \"12.5k\", label: \"Followers\" },\n { value: \"892\", label: \"Following\" },\n];\n\nconst sampleMobileNavItems = [\n { id: \"home\", label: \"Home\", icon: \"home\" as const },\n { id: \"search\", label: \"Search\", icon: \"search\" as const },\n { id: \"profile\", label: \"Profile\", icon: \"user\" as const },\n];\n\n\n// =====================\n// Component Renderers\n// =====================\nconst componentRenderers: Record<string, () => React.ReactNode> = {\n // =====================\n // PAGE TEMPLATES (scaled previews)\n // =====================\n PageAbout: () => <PageAboutPreview />,\n PageAccount: () => <PageAccountPreview />,\n PageAdminPortal: () => <PageAdminPortalPreview />,\n PageCenteredProfile: () => <PageCenteredProfilePreview />,\n PageDoubleSidebar: () => <PageDoubleSidebarPreview />,\n PageIconSidebar: () => <PageIconSidebarPreview />,\n PageLogin: () => <PageLoginPreview />,\n PageMenuSections: () => <PageMenuSectionsPreview />,\n PageMessenger: () => <PageMessengerPreview />,\n PageMobileMenu: () => <PageMobileMenuPreview />,\n PageMultistepProgressbar: () => <PageMultistepProgressbarPreview />,\n PageMultistepSidebar: () => <PageMultistepSidebarPreview />,\n PagePricing: () => <PagePricingPreview />,\n PageProductHomepage: () => <PageProductHomepagePreview />,\n PageResetPassword: () => <PageResetPasswordPreview />,\n PageSearchBar: () => <PageSearchBarPreview />,\n PageSidebarProfile: () => <PageSidebarProfilePreview />,\n PageStandard: () => <PageStandardPreview />,\n PageStandardMultistep: () => <PageStandardMultistepPreview />,\n PageStandardSearch: () => <PageStandardSearchPreview />,\n PageVerticalMultistep: () => <PageVerticalMultistepPreview />,\n PageVideoChat: () => <PageVideoChatPreview />,\n PageVideoList: () => <PageVideoListPreview />,\n\n // =====================\n // BLOCKS - Data & Tables\n // =====================\n StandardDataTable: () => (\n <div className=\"w-[600px] bg-[var(--canvas-background)] rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <StandardDataTable title=\"Team Members\" />\n </div>\n ),\n\n // =====================\n // BLOCKS - Cards & Profiles\n // =====================\n ProfileCard: () => (\n <div className=\"w-[400px] bg-[var(--canvas-background)] rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <ProfileCard {...sampleProfileData} showMenu={false} />\n </div>\n ),\n SidebarProfileCard: () => (\n <div className=\"w-[280px] bg-[var(--canvas-background)] rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <SidebarProfileCard \n name=\"Jeff Connor\" \n role=\"Product Designer\"\n avatarUrl={sampleProfileData.avatarUrl}\n />\n </div>\n ),\n ProfileInfoCards: () => (\n <div className=\"w-[400px] bg-[var(--canvas-background)] rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <StatsCard stats={sampleStats} />\n </div>\n ),\n SidebarCards: () => (\n <div className=\"w-[280px] bg-[var(--canvas-background)] rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <LinksCard />\n </div>\n ),\n CreditCardDisplay: () => (\n <div className=\"w-[360px] bg-[var(--canvas-background)] rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <CreditCardDisplay \n cardNumber=\"4242 4242 4242 4242\"\n cardHolder=\"JEFF CONNOR\"\n expiry=\"12/28\"\n />\n </div>\n ),\n\n // =====================\n // BLOCKS - Navigation & Progress\n // =====================\n StepTracker: () => (\n <div className=\"w-[500px] bg-[var(--canvas-background)] rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <StepTracker steps={defaultSteps} currentStep={1} />\n </div>\n ),\n VerticalStepTracker: () => (\n <div className=\"w-[280px] bg-[var(--canvas-background)] rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <VerticalStepTracker steps={defaultSteps} currentStep={1} />\n </div>\n ),\n ProgressBar: () => (\n <div className=\"w-[300px] bg-[var(--canvas-background)] rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <ProgressBar progress={65} />\n </div>\n ),\n PillTabs: () => (\n <div className=\"w-[300px] bg-[var(--canvas-background)] rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <PillTabs tabs={samplePillTabs} activeTab=\"all\" />\n </div>\n ),\n MobileBottomNav: () => (\n <div className=\"w-[375px] bg-[var(--canvas-background)] rounded-lg shadow-sm border border-[var(--canvas-border)] overflow-hidden\">\n <MobileBottomNav items={sampleMobileNavItems} activeItem=\"home\" />\n </div>\n ),\n\n // =====================\n // BLOCKS - Banners & Headers\n // =====================\n FlairBanner: () => (\n <div className=\"w-[500px] overflow-hidden rounded-lg shadow-sm\">\n <FlairBanner title=\"Welcome\" />\n </div>\n ),\n GradientBanner: () => (\n <div className=\"w-[500px] overflow-hidden rounded-lg shadow-sm\">\n <GradientBanner title=\"Welcome\" subtitle=\"Get started with your dashboard\" />\n </div>\n ),\n PageHeaderSection: () => (\n <div className=\"w-[600px] bg-[var(--canvas-background)] rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <PageHeaderSection \n title=\"Dashboard\" \n description=\"Manage your account and settings\"\n showTabs={false}\n />\n </div>\n ),\n\n // =====================\n // BLOCKS - Chat & Messaging\n // =====================\n MessengerSidebar: () => (\n <div className=\"w-[375px] h-[500px] overflow-hidden rounded-lg shadow-sm border border-[var(--canvas-border)]\">\n <MessengerSidebar />\n </div>\n ),\n ChatMessage: () => (\n <div className=\"w-[400px] bg-[var(--canvas-background)] rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <ChatBubble \n message={{\n id: \"1\",\n content: \"Hey! How's the project going?\",\n sender: \"Jeff Connor\",\n timestamp: \"2:30 PM\",\n isOwn: false,\n }}\n />\n </div>\n ),\n\n // =====================\n // BLOCKS - Video\n // =====================\n VideoChatControls: () => (\n <div className=\"w-[400px] bg-[var(--canvas-text)] rounded-lg p-4 shadow-sm\">\n <VideoChatControls />\n </div>\n ),\n WebcamPreview: () => (\n <div className=\"w-[320px] h-[240px] overflow-hidden rounded-lg shadow-sm border border-[var(--canvas-border)]\">\n <WebcamPreview />\n </div>\n ),\n ParticipantList: () => (\n <div className=\"w-[280px] bg-[var(--canvas-background)] rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <ParticipantList />\n </div>\n ),\n VideoContentSection: () => (\n <div className=\"w-[600px] bg-[var(--canvas-background)] rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <VideoContentSection />\n </div>\n ),\n VideoPlaylist: () => (\n <div className=\"w-[320px] bg-[var(--canvas-background)] rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <VideoPlaylistCard />\n </div>\n ),\n\n // =====================\n // BLOCKS - Search & Filters\n // =====================\n SearchBar: () => (\n <div className=\"w-[400px] bg-[var(--canvas-background)] rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <SearchBar placeholder=\"Search...\" />\n </div>\n ),\n FilterPopover: () => (\n <div className=\"w-[300px] bg-[var(--canvas-background)] rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <FilterPopover />\n </div>\n ),\n\n // =====================\n // BLOCKS - Forms & Settings\n // =====================\n SettingsListRow: () => (\n <div className=\"w-[400px] bg-[var(--canvas-background)] rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <SettingsListRow label=\"Email\" value=\"jeff@example.com\" />\n </div>\n ),\n ProfileImageUploader: () => (\n <div className=\"w-[200px] bg-[var(--canvas-background)] rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <ProfileImageUploader currentImage={sampleProfileData.avatarUrl} />\n </div>\n ),\n LoginBrandingPanel: () => (\n <div className=\"w-[400px] h-[300px] overflow-hidden rounded-lg shadow-sm\">\n <LoginBrandingPanel />\n </div>\n ),\n\n // =====================\n // BLOCKS - Marketing Heroes\n // =====================\n HeroSection: () => (\n <div className=\"w-[600px] overflow-hidden rounded-lg shadow-sm\">\n <HeroSection \n title=\"Plan your next adventure\" \n subtitle=\"Live like locals from anywhere\"\n />\n </div>\n ),\n HeroDarkWithImage: () => (\n <div className=\"w-[700px] overflow-hidden rounded-lg shadow-sm\">\n <HeroDarkWithImage />\n </div>\n ),\n CenteredHero: () => (\n <div className=\"w-[600px] overflow-hidden rounded-lg shadow-sm\">\n <CenteredHero \n title=\"Build something amazing\"\n subtitle=\"The platform for modern developers\"\n />\n </div>\n ),\n\n // =====================\n // BLOCKS - Marketing Social Proof\n // =====================\n TestimonialCarousel: () => (\n <div className=\"w-[700px] overflow-hidden rounded-lg shadow-sm\">\n <TestimonialCarousel />\n </div>\n ),\n ReviewsGrid: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] p-6\">\n <ReviewsGrid />\n </div>\n ),\n SocialProof: () => (\n <div className=\"w-[600px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)]\">\n <SocialProof label=\"TRUSTED BY\" />\n </div>\n ),\n MetricsSection: () => (\n <div className=\"w-[700px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] p-6\">\n <MetricsSection />\n </div>\n ),\n\n // =====================\n // BLOCKS - Marketing Features\n // =====================\n FeatureWithImage: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] p-6\">\n <FeatureWithImage \n title=\"Powerful analytics\"\n description=\"Get insights into your data with our advanced analytics tools.\"\n />\n </div>\n ),\n CoreValuesGrid: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] p-6\">\n <CoreValuesGrid />\n </div>\n ),\n DestinationCards: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] p-6\">\n <DestinationCards />\n </div>\n ),\n\n // =====================\n // BLOCKS - Marketing Team\n // =====================\n TeamCardsGrid: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] p-6\">\n <TeamCardsGrid />\n </div>\n ),\n TeamCircularGrid: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] p-6\">\n <TeamCircularGrid />\n </div>\n ),\n\n // =====================\n // BLOCKS - Marketing CTA & Footer\n // =====================\n CtaBanner: () => (\n <div className=\"w-[500px] overflow-hidden rounded-lg shadow-sm\">\n <CtaBanner title=\"Ready to get started?\" buttonText=\"Sign up\" />\n </div>\n ),\n FooterNavbar: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm\">\n <FooterNavbar />\n </div>\n ),\n\n // =====================\n // BLOCKS - Marketing Other\n // =====================\n FeaturedNewsCards: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] p-6\">\n <FeaturedNewsCards />\n </div>\n ),\n OfficeLocations: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] p-6\">\n <OfficeLocations />\n </div>\n ),\n\n // =====================\n // BLOCKS - Pricing\n // =====================\n PricingCards: () => (\n <div className=\"w-[900px] overflow-hidden rounded-lg shadow-sm\">\n <PricingCards />\n </div>\n ),\n FaqAccordion: () => (\n <div className=\"w-[600px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] p-6\">\n <FaqAccordion />\n </div>\n ),\n FeaturesComparison: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] p-6\">\n <FeaturesComparison />\n </div>\n ),\n\n // =====================\n // UI COMPONENTS\n // =====================\n Button: () => (\n <div className=\"w-[200px] bg-[var(--canvas-background)] rounded-lg p-6 shadow-sm border border-[var(--canvas-border)] flex flex-col gap-3\">\n <Button>Primary Button</Button>\n <Button variant=\"outline\">Outline Button</Button>\n <Button variant=\"ghost\">Ghost Button</Button>\n </div>\n ),\n Checkbox: () => (\n <div className=\"w-[200px] bg-[var(--canvas-background)] rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <div className=\"flex items-center space-x-2\">\n <Checkbox id=\"terms\" defaultChecked />\n <Label htmlFor=\"terms\">Accept terms</Label>\n </div>\n </div>\n ),\n DateInput: () => (\n <div className=\"w-[250px] bg-[var(--canvas-background)] rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <Input type=\"date\" />\n </div>\n ),\n Input: () => (\n <div className=\"w-[300px] bg-[var(--canvas-background)] rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <Input placeholder=\"Enter your email...\" />\n </div>\n ),\n Select: () => (\n <div className=\"w-[250px] bg-[var(--canvas-background)] rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <Select>\n <SelectTrigger>\n <SelectValue placeholder=\"Select option\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"1\">Option 1</SelectItem>\n <SelectItem value=\"2\">Option 2</SelectItem>\n <SelectItem value=\"3\">Option 3</SelectItem>\n </SelectContent>\n </Select>\n </div>\n ),\n Switch: () => (\n <div className=\"w-[200px] bg-[var(--canvas-background)] rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <div className=\"flex items-center space-x-2\">\n <Switch id=\"notifications\" defaultChecked />\n <Label htmlFor=\"notifications\">Notifications</Label>\n </div>\n </div>\n ),\n RadioGroup: () => (\n <div className=\"w-[200px] bg-[var(--canvas-background)] rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <RadioGroup defaultValue=\"option-1\">\n <div className=\"flex items-center space-x-2\">\n <RadioGroupItem value=\"option-1\" id=\"option-1\" />\n <Label htmlFor=\"option-1\">Option 1</Label>\n </div>\n <div className=\"flex items-center space-x-2\">\n <RadioGroupItem value=\"option-2\" id=\"option-2\" />\n <Label htmlFor=\"option-2\">Option 2</Label>\n </div>\n </RadioGroup>\n </div>\n ),\n MultiselectTags: () => (\n <div className=\"w-[300px] bg-[var(--canvas-background)] rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <div className=\"flex flex-wrap gap-2\">\n <span className=\"px-2 py-1 bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)] text-xs rounded-full\">Design</span>\n <span className=\"px-2 py-1 bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)] text-xs rounded-full\">Development</span>\n <span className=\"px-2 py-1 border border-[var(--canvas-border)] text-[var(--canvas-text-muted)] text-xs rounded-full\">+ Add tag</span>\n </div>\n </div>\n ),\n Avatar: () => (\n <div className=\"w-[200px] bg-[var(--canvas-background)] rounded-lg p-6 shadow-sm border border-[var(--canvas-border)] flex gap-3\">\n <Avatar>\n <AvatarImage src={sampleProfileData.avatarUrl} />\n <AvatarFallback>JC</AvatarFallback>\n </Avatar>\n <Avatar>\n <AvatarFallback>AB</AvatarFallback>\n </Avatar>\n </div>\n ),\n Badge: () => (\n <div className=\"w-[250px] bg-[var(--canvas-background)] rounded-lg p-6 shadow-sm border border-[var(--canvas-border)] flex flex-wrap gap-2\">\n <span className=\"px-2 py-1 bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)] text-xs rounded-full\">Default</span>\n <span className=\"px-2 py-1 bg-[var(--canvas-surface)] text-[var(--canvas-text)] text-xs rounded-full\">Secondary</span>\n <span className=\"px-2 py-1 border border-[var(--canvas-border)] text-[var(--canvas-text-muted)] text-xs rounded-full\">Outline</span>\n <span className=\"px-2 py-1 bg-[var(--canvas-destructive)] text-[var(--canvas-primary-foreground)] text-xs rounded-full\">Destructive</span>\n </div>\n ),\n};\n\n// =====================\n// InfinityCanvas Component\n// =====================\ninterface InfinityCanvasProps {\n items: CanvasItemData[];\n onItemsChange: (items: CanvasItemData[]) => void;\n selectedId: string | null;\n onSelectItem: (id: string | null) => void;\n}\n\n/**\n * Infinity Canvas - Pannable, zoomable canvas for placing components\n * \n * Features:\n * - Pan by dragging empty space\n * - Zoom with scroll wheel or controls\n * - Drop zone for components from palette\n * - Grid background for visual reference\n */\nexport function InfinityCanvas({\n items,\n onItemsChange,\n selectedId,\n onSelectItem,\n}: InfinityCanvasProps) {\n const [showGrid, setShowGrid] = useState(true);\n const transformRef = useRef<ReactZoomPanPinchRef>(null);\n const [scale, setScale] = useState(1);\n \n // Drag state - stores starting positions\n const dragStateRef = useRef<{\n itemId: string;\n startMouseX: number;\n startMouseY: number;\n startItemX: number;\n startItemY: number;\n } | null>(null);\n const [isDragging, setIsDragging] = useState(false);\n\n // Set up droppable area\n const { setNodeRef, isOver } = useDroppable({\n id: \"canvas-drop-zone\",\n });\n\n // Handle item deletion\n const handleDeleteItem = useCallback((id: string) => {\n onItemsChange(items.filter(item => item.id !== id));\n if (selectedId === id) {\n onSelectItem(null);\n }\n }, [items, onItemsChange, selectedId, onSelectItem]);\n\n // Handle item drag start - receives mouse position and item position\n const handleDragStart = useCallback((id: string, startMouseX: number, startMouseY: number, startItemX: number, startItemY: number) => {\n dragStateRef.current = {\n itemId: id,\n startMouseX,\n startMouseY,\n startItemX,\n startItemY,\n };\n setIsDragging(true);\n }, []);\n\n // Handle mouse move for dragging\n useEffect(() => {\n if (!isDragging) return;\n\n const handleMouseMove = (e: MouseEvent) => {\n const dragState = dragStateRef.current;\n if (!dragState || !transformRef.current) return;\n\n const state = transformRef.current.instance.transformState;\n \n // Calculate delta from start position, accounting for zoom\n const deltaX = (e.clientX - dragState.startMouseX) / state.scale;\n const deltaY = (e.clientY - dragState.startMouseY) / state.scale;\n \n // New position = original position + delta\n const newX = dragState.startItemX + deltaX;\n const newY = dragState.startItemY + deltaY;\n\n onItemsChange(items.map(item => \n item.id === dragState.itemId \n ? { ...item, x: Math.max(0, newX), y: Math.max(0, newY) }\n : item\n ));\n };\n\n const handleMouseUp = () => {\n dragStateRef.current = null;\n setIsDragging(false);\n };\n\n window.addEventListener(\"mousemove\", handleMouseMove);\n window.addEventListener(\"mouseup\", handleMouseUp);\n\n return () => {\n window.removeEventListener(\"mousemove\", handleMouseMove);\n window.removeEventListener(\"mouseup\", handleMouseUp);\n };\n }, [isDragging, items, onItemsChange]);\n\n // Handle keyboard shortcuts\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Delete\" || e.key === \"Backspace\") {\n if (selectedId && document.activeElement?.tagName !== \"INPUT\") {\n handleDeleteItem(selectedId);\n }\n }\n if (e.key === \"Escape\") {\n onSelectItem(null);\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [selectedId, handleDeleteItem, onSelectItem]);\n\n // Click on empty canvas to deselect\n const handleCanvasClick = () => {\n onSelectItem(null);\n };\n\n return (\n <div \n ref={setNodeRef}\n className={cn(\n \"relative flex-1 overflow-hidden\",\n \"bg-[#f8f9fa]\",\n isOver && \"ring-2 ring-inset ring-[var(--canvas-primary)]\"\n )}\n >\n {/* Zoom Controls */}\n <div className=\"absolute top-4 right-4 z-20 flex items-center gap-2\">\n <button\n onClick={() => setShowGrid(!showGrid)}\n className={cn(\n \"p-2 rounded-md border shadow-sm transition-colors\",\n showGrid \n ? \"bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)] border-[var(--canvas-primary)]\"\n : \"bg-[var(--canvas-background)] text-[var(--canvas-text-muted)] border-[var(--canvas-border)] hover:bg-[var(--canvas-surface)]\"\n )}\n aria-label=\"Toggle grid\"\n >\n <Grid3X3 className=\"size-4\" />\n </button>\n <div className=\"flex items-center bg-[var(--canvas-background)] rounded-md border border-[var(--canvas-border)] shadow-sm\">\n <button\n onClick={() => transformRef.current?.zoomOut()}\n className=\"p-2 hover:bg-[var(--canvas-surface)] rounded-l-md transition-colors\"\n aria-label=\"Zoom out\"\n >\n <ZoomOut className=\"size-4 text-[var(--canvas-text-muted)]\" />\n </button>\n <span className=\"px-3 text-[var(--canvas-text-muted)] border-x border-[var(--canvas-border)] min-w-[60px] text-center\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n {Math.round(scale * 100)}%\n </span>\n <button\n onClick={() => transformRef.current?.zoomIn()}\n className=\"p-2 hover:bg-[var(--canvas-surface)] transition-colors\"\n aria-label=\"Zoom in\"\n >\n <ZoomIn className=\"size-4 text-[var(--canvas-text-muted)]\" />\n </button>\n <button\n onClick={() => transformRef.current?.resetTransform()}\n className=\"p-2 hover:bg-[var(--canvas-surface)] rounded-r-md transition-colors border-l border-[var(--canvas-border)]\"\n aria-label=\"Reset view\"\n >\n <Maximize2 className=\"size-4 text-[var(--canvas-text-muted)]\" />\n </button>\n </div>\n </div>\n\n {/* Canvas */}\n <TransformWrapper\n ref={transformRef}\n initialScale={1}\n minScale={0.1}\n maxScale={2}\n limitToBounds={false}\n onTransformed={(_, state) => setScale(state.scale)}\n panning={{\n disabled: isDragging,\n velocityDisabled: true,\n }}\n wheel={{\n smoothStep: 0.05,\n }}\n >\n <TransformComponent\n wrapperStyle={{\n width: \"100%\",\n height: \"100%\",\n }}\n contentStyle={{\n width: \"5000px\",\n height: \"5000px\",\n }}\n >\n <div\n className={cn(\n \"w-full h-full relative\",\n showGrid && \"bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImdyaWQiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTSAwIDEwIEwgNDAgMTAgTSAxMCAwIEwgMTAgNDAgTSAwIDIwIEwgNDAgMjAgTSAyMCAwIEwgMjAgNDAgTSAwIDMwIEwgNDAgMzAgTSAzMCAwIEwgMzAgNDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2UwZTBlMCIgc3Ryb2tlLXdpZHRoPSIxIi8+PHBhdGggZD0iTSA0MCAwIEwgMCA0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJub25lIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2dyaWQpIi8+PC9zdmc+')]\"\n )}\n onClick={handleCanvasClick}\n >\n {/* Render canvas items */}\n {items.map((item) => {\n const renderer = componentRenderers[item.componentType];\n if (!renderer) return null;\n\n return (\n <CanvasItem\n key={item.id}\n item={item}\n isSelected={selectedId === item.id}\n onSelect={onSelectItem}\n onDelete={handleDeleteItem}\n onDragStart={handleDragStart}\n scale={scale}\n >\n {renderer()}\n </CanvasItem>\n );\n })}\n\n {/* Empty state hint */}\n {items.length === 0 && (\n <div className=\"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center pointer-events-none\">\n <p className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-l-size)\" }}>\n Drag components from the sidebar to get started\n </p>\n <p className=\"text-[var(--canvas-text-placeholder)] mt-2\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n Scroll to zoom • Drag to pan\n </p>\n </div>\n )}\n </div>\n </TransformComponent>\n </TransformWrapper>\n </div>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/menu-section.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { ChevronRight } from \"lucide-react\";\n\ninterface MenuSectionItemProps {\n /** Menu item label */\n label: string;\n /** Optional children content shown when expanded */\n children?: React.ReactNode;\n /** Whether the item starts expanded */\n defaultExpanded?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Menu Section Item Component\n * \n * An individual expandable menu item with a chevron icon that rotates when expanded.\n * \n * @example\n * ```tsx\n * <MenuSectionItem label=\"Menu tab\">\n * <p>Expanded content goes here</p>\n * </MenuSectionItem>\n * ```\n */\nexport function MenuSectionItem({\n label,\n children,\n defaultExpanded = false,\n className,\n}: MenuSectionItemProps) {\n const [expanded, setExpanded] = useState(defaultExpanded);\n\n return (\n <div className={cn(\"w-full\", className)}>\n {/* Clickable header */}\n <button\n type=\"button\"\n onClick={() => setExpanded(!expanded)}\n className=\"w-full flex items-center gap-4 py-6 text-left group\"\n >\n {/* Circular icon button with chevron */}\n <div \n className={cn(\n \"size-8 rounded-full flex items-center justify-center shrink-0\",\n \"bg-[var(--canvas-surface)] border border-[var(--canvas-border-disabled)]\",\n \"transition-colors group-hover:bg-[var(--canvas-surface-brand)]\"\n )}\n >\n <ChevronRight \n className={cn(\n \"size-5 text-[var(--canvas-text-placeholder)] transition-transform duration-200\",\n expanded && \"rotate-90\"\n )}\n />\n </div>\n \n {/* Label - Uses typography variables */}\n <span \n className=\"text-[var(--canvas-text)]\"\n style={{\n fontFamily: \"var(--typo-menu-label-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-menu-label-size)\",\n fontWeight: \"var(--typo-menu-label-weight)\",\n letterSpacing: \"var(--typo-menu-label-spacing)\",\n lineHeight: \"var(--typo-menu-label-line-height)\",\n }}\n >\n {label}\n </span>\n </button>\n\n {/* Expandable content */}\n {children && (\n <div\n className={cn(\n \"overflow-hidden transition-all duration-200 ease-in-out\",\n expanded ? \"max-h-[1000px] opacity-100\" : \"max-h-0 opacity-0\"\n )}\n >\n <div className=\"pl-12 pb-6\">\n {children}\n </div>\n </div>\n )}\n </div>\n );\n}\n\ninterface MenuSectionProps {\n /** Array of menu items */\n items: Array<{\n id: string;\n label: string;\n children?: React.ReactNode;\n defaultExpanded?: boolean;\n }>;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Menu Section Component\n * \n * A group of expandable menu items with border separators.\n * \n * @example\n * ```tsx\n * <MenuSection\n * items={[\n * { id: \"1\", label: \"Menu tab\" },\n * { id: \"2\", label: \"Another menu tab\" },\n * ]}\n * />\n * ```\n */\nexport function MenuSection({\n items,\n className,\n}: MenuSectionProps) {\n return (\n <div className={cn(\"w-full\", className)}>\n {items.map((item, index) => (\n <div\n key={item.id}\n className={cn(\n \"border-[var(--canvas-border)]\",\n // All items get bottom border only\n \"border-b\"\n )}\n >\n <MenuSectionItem\n label={item.label}\n defaultExpanded={item.defaultExpanded}\n >\n {item.children}\n </MenuSectionItem>\n </div>\n ))}\n </div>\n );\n}\n\ninterface SectionHeaderProps {\n /** Section title */\n title: string;\n /** Section description */\n description?: string;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Section Header Component\n * \n * A simple title and description header for content sections.\n * \n * @example\n * ```tsx\n * <SectionHeader title=\"Title\" description=\"Description\" />\n * ```\n */\nexport function SectionHeader({\n title,\n description,\n className,\n}: SectionHeaderProps) {\n return (\n <div className={cn(\"flex flex-col gap-1\", className)}>\n {/* Title - 24px semibold */}\n <h3 className=\"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { ChevronRight } from \"lucide-react\";\n\ninterface MenuSectionItemProps {\n /** Menu item label */\n label: string;\n /** Optional children content shown when expanded */\n children?: React.ReactNode;\n /** Whether the item starts expanded */\n defaultExpanded?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Menu Section Item Component\n * \n * An individual expandable menu item with a chevron icon that rotates when expanded.\n * \n * @example\n * ```tsx\n * <MenuSectionItem label=\"Menu tab\">\n * <p>Expanded content goes here</p>\n * </MenuSectionItem>\n * ```\n */\nexport function MenuSectionItem({\n label,\n children,\n defaultExpanded = false,\n className,\n}: MenuSectionItemProps) {\n const [expanded, setExpanded] = useState(defaultExpanded);\n\n return (\n <div className={cn(\"w-full\", className)}>\n {/* Clickable header */}\n <button\n type=\"button\"\n onClick={() => setExpanded(!expanded)}\n className=\"w-full flex items-center gap-4 py-6 text-left group\"\n >\n {/* Circular icon button with chevron */}\n <div \n className={cn(\n \"size-8 rounded-full flex items-center justify-center shrink-0\",\n \"bg-[var(--canvas-surface)] border border-[var(--canvas-border-disabled)]\",\n \"transition-colors group-hover:bg-[var(--canvas-surface-brand)]\"\n )}\n >\n <ChevronRight \n className={cn(\n \"size-5 text-[var(--canvas-text-placeholder)] transition-transform duration-200\",\n expanded && \"rotate-90\"\n )}\n />\n </div>\n \n {/* Label - Uses typography variables */}\n <span \n className=\"text-[var(--canvas-text)]\"\n style={{\n fontFamily: \"var(--typo-menu-label-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-menu-label-size)\",\n fontWeight: \"var(--typo-menu-label-weight)\",\n letterSpacing: \"var(--typo-menu-label-spacing)\",\n lineHeight: \"var(--typo-menu-label-line-height)\",\n }}\n >\n {label}\n </span>\n </button>\n\n {/* Expandable content */}\n {children && (\n <div\n className={cn(\n \"overflow-hidden transition-all duration-200 ease-in-out\",\n expanded ? \"max-h-[1000px] opacity-100\" : \"max-h-0 opacity-0\"\n )}\n >\n <div className=\"pl-12 pb-6\">\n {children}\n </div>\n </div>\n )}\n </div>\n );\n}\n\ninterface MenuSectionProps {\n /** Array of menu items */\n items: Array<{\n id: string;\n label: string;\n children?: React.ReactNode;\n defaultExpanded?: boolean;\n }>;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Menu Section Component\n * \n * A group of expandable menu items with border separators.\n * \n * @example\n * ```tsx\n * <MenuSection\n * items={[\n * { id: \"1\", label: \"Menu tab\" },\n * { id: \"2\", label: \"Another menu tab\" },\n * ]}\n * />\n * ```\n */\nexport function MenuSection({\n items,\n className,\n}: MenuSectionProps) {\n return (\n <div className={cn(\"w-full\", className)}>\n {items.map((item, index) => (\n <div\n key={item.id}\n className={cn(\n \"border-[var(--canvas-border)]\",\n // All items get bottom border only\n \"border-b\"\n )}\n >\n <MenuSectionItem\n label={item.label}\n defaultExpanded={item.defaultExpanded}\n >\n {item.children}\n </MenuSectionItem>\n </div>\n ))}\n </div>\n );\n}\n\ninterface SectionHeaderProps {\n /** Section title */\n title: string;\n /** Section description */\n description?: string;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Section Header Component\n * \n * A simple title and description header for content sections.\n * \n * @example\n * ```tsx\n * <SectionHeader title=\"Title\" description=\"Description\" />\n * ```\n */\nexport function SectionHeader({\n title,\n description,\n className,\n}: SectionHeaderProps) {\n return (\n <div className={cn(\"flex flex-col gap-1\", className)}>\n {/* Title - 24px semibold */}\n <h3 className=\"font-semibold text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-h6-size)\", lineHeight: \"var(--typo-h6-line-height)\" }}>\n {title}\n </h3>\n {/* Description - 16px regular */}\n {description && (\n <p className=\"font-normal text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-m-size)\", lineHeight: \"var(--typo-body-m-line-height)\" }}>\n {description}\n </p>\n )}\n </div>\n );\n}\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/messenger-sidebar.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { MoreHorizontal, PenSquare } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar\";\nimport { Searchbox } from \"../ui/searchbox\";\n\ninterface ThreadItem {\n id: string;\n name: string;\n avatar?: string;\n lastMessage: string;\n timestamp: string;\n unreadCount?: number;\n isOnline?: boolean;\n}\n\nconst sampleThreads: ThreadItem[] = [\n {\n id: \"1\",\n name: \"Mary Trott\",\n avatar: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop\",\n lastMessage: \"Mary: Thank you so much for sending your...\",\n timestamp: \"Just now\",\n unreadCount: 3,\n },\n {\n id: \"2\",\n name: \"Raj, Mary, Jeff\",\n avatar: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop\",\n lastMessage: \"You: Hi Raj, could you take a look at the doc\",\n timestamp: \"30 mins ago\",\n unreadCount: 3,\n },\n {\n id: \"3\",\n name: \"Raj Mishra\",\n avatar: \"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100&h=100&fit=crop\",\n lastMessage: \"You: Hi Raj, could you take a look at the doc\",\n timestamp: \"30 mins ago\",\n },\n];\n\ninterface MessengerSidebarProps {\n selectedThreadId?: string;\n onSelectThread?: (threadId: string) => void;\n className?: string;\n}\n\nexport function MessengerSidebar({\n selectedThreadId,\n onSelectThread,\n className,\n}: MessengerSidebarProps) {\n const [searchValue, setSearchValue] = useState(\"\");\n\n const filteredThreads = sampleThreads.filter((thread) =>\n thread.name.toLowerCase().includes(searchValue.toLowerCase())\n );\n\n return (\n <aside\n className={`flex flex-col h-full border-r w-full md:w-[375px] shrink-0 ${className || \"\"}`}\n style={{\n borderColor: \"var(--canvas-border)\",\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n {/* Header */}\n <div\n className=\"flex items-center justify-between px-4 lg:px-[var(--spacing-5xl)] py-[var(--spacing-xl)] border-b shrink-0\"\n style={{ borderColor: \"var(--canvas-border)\" }}\n >\n <h1\n className=\"font-semibold\"\n style={{\n color: \"var(--canvas-foreground)\",\n fontSize: \"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { MoreHorizontal, PenSquare } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar\";\nimport { Searchbox } from \"../ui/searchbox\";\n\ninterface ThreadItem {\n id: string;\n name: string;\n avatar?: string;\n lastMessage: string;\n timestamp: string;\n unreadCount?: number;\n isOnline?: boolean;\n}\n\nconst sampleThreads: ThreadItem[] = [\n {\n id: \"1\",\n name: \"Mary Trott\",\n avatar: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop\",\n lastMessage: \"Mary: Thank you so much for sending your...\",\n timestamp: \"Just now\",\n unreadCount: 3,\n },\n {\n id: \"2\",\n name: \"Raj, Mary, Jeff\",\n avatar: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop\",\n lastMessage: \"You: Hi Raj, could you take a look at the doc\",\n timestamp: \"30 mins ago\",\n unreadCount: 3,\n },\n {\n id: \"3\",\n name: \"Raj Mishra\",\n avatar: \"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100&h=100&fit=crop\",\n lastMessage: \"You: Hi Raj, could you take a look at the doc\",\n timestamp: \"30 mins ago\",\n },\n];\n\ninterface MessengerSidebarProps {\n selectedThreadId?: string;\n onSelectThread?: (threadId: string) => void;\n className?: string;\n}\n\nexport function MessengerSidebar({\n selectedThreadId,\n onSelectThread,\n className,\n}: MessengerSidebarProps) {\n const [searchValue, setSearchValue] = useState(\"\");\n\n const filteredThreads = sampleThreads.filter((thread) =>\n thread.name.toLowerCase().includes(searchValue.toLowerCase())\n );\n\n return (\n <aside\n className={`flex flex-col h-full border-r w-full md:w-[375px] shrink-0 ${className || \"\"}`}\n style={{\n borderColor: \"var(--canvas-border)\",\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n {/* Header */}\n <div\n className=\"flex items-center justify-between px-4 lg:px-[var(--spacing-5xl)] py-[var(--spacing-xl)] border-b shrink-0\"\n style={{ borderColor: \"var(--canvas-border)\" }}\n >\n <h1\n className=\"font-semibold\"\n style={{\n color: \"var(--canvas-foreground)\",\n fontSize: \"var(--typo-body-xl-size)\",\n lineHeight: \"28px\",\n }}\n >\n Messages\n </h1>\n <div className=\"flex items-center gap-[var(--spacing-md)]\">\n <button\n className=\"flex items-center justify-center size-8 rounded-[var(--radius-xs)] border transition-colors hover:opacity-80\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n borderColor: \"var(--canvas-border)\",\n }}\n aria-label=\"More options\"\n >\n <MoreHorizontal\n className=\"size-4\"\n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n <button\n className=\"flex items-center justify-center size-8 rounded-[var(--radius-xs)] border transition-colors hover:opacity-80\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n borderColor: \"var(--canvas-border)\",\n }}\n aria-label=\"Compose message\"\n >\n <PenSquare\n className=\"size-4\"\n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n </div>\n </div>\n\n {/* Search */}\n <div className=\"px-4 lg:px-[var(--spacing-5xl)] py-[var(--spacing-xl)] shrink-0 border-b\" style={{ borderColor: \"var(--canvas-border)\" }}>\n <Searchbox\n value={searchValue}\n onChange={setSearchValue}\n placeholder=\"Search messages\"\n inputSize=\"sm\"\n />\n </div>\n\n {/* Thread List */}\n <div className=\"flex-1 overflow-y-auto\">\n {filteredThreads.map((thread) => (\n <ThreadRow\n key={thread.id}\n thread={thread}\n isSelected={selectedThreadId === thread.id}\n onSelect={() => onSelectThread?.(thread.id)}\n />\n ))}\n </div>\n </aside>\n );\n}\n\ninterface ThreadRowProps {\n thread: ThreadItem;\n isSelected?: boolean;\n onSelect?: () => void;\n}\n\nfunction ThreadRow({ thread, isSelected, onSelect }: ThreadRowProps) {\n return (\n <button\n onClick={onSelect}\n className=\"w-full flex items-center gap-[var(--spacing-xl)] px-4 lg:px-[var(--spacing-5xl)] py-[var(--spacing-xl)] transition-colors text-left border-b\"\n style={{\n backgroundColor: isSelected\n ? \"var(--canvas-surface)\"\n : \"var(--canvas-background)\",\n borderColor: \"var(--canvas-border)\",\n }}\n >\n {/* Avatar with unread badge */}\n <div className=\"relative shrink-0\">\n <Avatar className=\"size-12\">\n <AvatarImage src={thread.avatar} alt={thread.name} />\n <AvatarFallback\n style={{\n fontSize: \"var(--typo-body-xs-size)\",\n backgroundColor: \"var(--canvas-primary)\",\n color: \"var(--canvas-primary-foreground)\",\n }}\n >\n {thread.name\n .split(\" \")\n .map((n) => n[0])\n .join(\"\")}\n </AvatarFallback>\n </Avatar>\n {thread.unreadCount && (\n <div\n className=\"absolute size-5 rounded-full flex items-center justify-center text-[10px] font-semibold\"\n style={{\n backgroundColor: \"var(--canvas-text-placeholder)\",\n color: \"var(--canvas-primary-foreground)\",\n bottom: \"1px\",\n right: \"-2px\",\n }}\n >\n {thread.unreadCount}\n </div>\n )}\n </div>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0 flex flex-col gap-[var(--spacing-xs)]\">\n <div className=\"flex items-center justify-between gap-2\">\n <span\n className=\"font-semibold truncate\"\n style={{\n color: \"var(--canvas-foreground)\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {thread.name}\n </span>\n <span\n className=\"shrink-0 font-normal\"\n style={{\n color: \"var(--canvas-text-muted)\",\n fontSize: \"var(--typo-body-xs-size)\",\n lineHeight: \"var(--typo-body-xs-line-height)\",\n }}\n >\n {thread.timestamp}\n </span>\n </div>\n <div className=\"flex items-center gap-2\">\n <span\n className=\"truncate\"\n style={{\n color: \"var(--canvas-text-muted)\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {thread.lastMessage}\n </span>\n </div>\n </div>\n </button>\n );\n}\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/mobile-bottom-nav.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { LucideIcon, Home, MessageSquare, Search, User } from \"lucide-react\";\n\n// ============================================\n// Mobile Nav Tab Item\n// ============================================\n\nexport interface MobileNavTabConfig {\n id: string;\n label: string;\n icon: LucideIcon;\n isActive?: boolean;\n}\n\ninterface MobileNavTabProps {\n item: MobileNavTabConfig;\n variant?: \"dark\" | \"light\";\n onClick?: () => void;\n}\n\nfunction MobileNavTab({ item, variant = \"light\", onClick }: MobileNavTabProps) {\n const Icon = item.icon;\n const isActive = item.isActive;\n const isDark = variant === \"dark\";\n\n return (\n <button\n onClick={onClick}\n className={cn(\n // Match icon-sidebar dimensions: 64px × 64px\n \"relative flex flex-col items-center justify-center gap-1 w-16 h-16 rounded-[var(--radius-nav)] transition-colors\",\n // Dark variant\n isDark && isActive && \"bg-[var(--canvas-sidebar-dark-active-bg)]\",\n isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isActive && \"bg-[var(--canvas-sidebar-light-active-bg)]\",\n !isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <Icon\n className={cn(\n // Match icon-sidebar: 16px icons\n \"size-4\",\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n />\n <span\n className={cn(\n // Match icon-sidebar: 12px labels, medium weight\n \"
|
|
9
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { LucideIcon, Home, MessageSquare, Search, User } from \"lucide-react\";\n\n// ============================================\n// Mobile Nav Tab Item\n// ============================================\n\nexport interface MobileNavTabConfig {\n id: string;\n label: string;\n icon: LucideIcon;\n isActive?: boolean;\n}\n\ninterface MobileNavTabProps {\n item: MobileNavTabConfig;\n variant?: \"dark\" | \"light\";\n onClick?: () => void;\n}\n\nfunction MobileNavTab({ item, variant = \"light\", onClick }: MobileNavTabProps) {\n const Icon = item.icon;\n const isActive = item.isActive;\n const isDark = variant === \"dark\";\n\n return (\n <button\n onClick={onClick}\n className={cn(\n // Match icon-sidebar dimensions: 64px × 64px\n \"relative flex flex-col items-center justify-center gap-1 w-16 h-16 rounded-[var(--radius-nav)] transition-colors\",\n // Dark variant\n isDark && isActive && \"bg-[var(--canvas-sidebar-dark-active-bg)]\",\n isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isActive && \"bg-[var(--canvas-sidebar-light-active-bg)]\",\n !isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <Icon\n className={cn(\n // Match icon-sidebar: 16px icons\n \"size-4\",\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n />\n <span\n className={cn(\n // Match icon-sidebar: 12px labels, medium weight\n \"font-medium\",\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n style={{ fontSize: \"var(--typo-sidebar-label-size)\" }}\n >\n {item.label}\n </span>\n </button>\n );\n}\n\n// ============================================\n// Default Navigation Items\n// ============================================\n\nexport const defaultMobileNavTabs: MobileNavTabConfig[] = [\n { id: \"home\", label: \"Home\", icon: Home, isActive: true },\n { id: \"messages\", label: \"Messages\", icon: MessageSquare },\n { id: \"discover\", label: \"Discover\", icon: Search },\n { id: \"account\", label: \"Account\", icon: User },\n];\n\n// ============================================\n// Mobile Bottom Navigation\n// ============================================\n\ninterface MobileBottomNavProps {\n /** Navigation tabs to display */\n tabs?: MobileNavTabConfig[];\n /** Visual variant - dark or light theme */\n variant?: \"dark\" | \"light\";\n /** Callback when a tab is clicked */\n onTabClick?: (tab: MobileNavTabConfig) => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Mobile Bottom Navigation\n * \n * A sticky bottom navigation bar with icon tabs.\n * Styling matches the icon-sidebar for consistency.\n * Supports both dark and light themes via the variant prop.\n * \n * @example\n * ```tsx\n * <MobileBottomNav\n * variant=\"light\"\n * tabs={defaultMobileNavTabs}\n * onTabClick={(tab) => console.log(tab.id)}\n * />\n * ```\n */\nexport function MobileBottomNav({\n tabs = defaultMobileNavTabs,\n variant = \"light\",\n onTabClick,\n className,\n}: MobileBottomNavProps) {\n const isDark = variant === \"dark\";\n\n return (\n <nav\n className={cn(\n \"fixed bottom-0 left-0 right-0 z-50\",\n \"flex items-center justify-center gap-5\",\n \"px-4 py-3\",\n // Dark variant\n isDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-t border-[var(--canvas-sidebar-dark-border)]\",\n isDark && \"shadow-[0px_-4px_16px_0px_rgba(0,0,0,0.2)]\",\n // Light variant\n !isDark && \"bg-[var(--canvas-sidebar-light-bg)] border-t border-[var(--canvas-sidebar-light-border)]\",\n !isDark && \"shadow-[0px_-4px_16px_0px_rgba(0,0,0,0.04)]\",\n className\n )}\n >\n {tabs.map((tab) => (\n <MobileNavTab\n key={tab.id}\n item={tab}\n variant={variant}\n onClick={() => onTabClick?.(tab)}\n />\n ))}\n </nav>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/monthly-calendar-widget.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { ChevronLeft, ChevronRight, ArrowRight } from \"lucide-react\";\nimport {\n format,\n startOfMonth,\n endOfMonth,\n startOfWeek,\n endOfWeek,\n addDays,\n addMonths,\n subMonths,\n isSameMonth,\n isSameDay,\n isWithinInterval,\n isBefore,\n isAfter,\n} from \"date-fns\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PricedDate {\n date: Date;\n price: string;\n}\n\nexport interface DateRange {\n start: Date | null;\n end: Date | null;\n}\n\nexport interface MonthlyCalendarWidgetProps {\n /** Widget title */\n title?: string;\n /** Widget subtitle */\n subtitle?: string;\n /** Initial month to display (defaults to current month) */\n initialMonth?: Date;\n /** Currently selected date range */\n selectedRange?: DateRange;\n /** Array of dates that should be disabled/unavailable */\n disabledDates?: Date[];\n /** Array of dates with prices to display */\n pricedDates?: PricedDate[];\n /** Override for \"today\" (useful for demos) */\n todayDate?: Date;\n /** Callback when a date is selected */\n onDateSelect?: (date: Date) => void;\n /** Callback when the date range changes */\n onRangeChange?: (range: DateRange) => void;\n /** Callback when Confirm button is clicked */\n onConfirm?: () => void;\n /** Callback when Cancel button is clicked */\n onCancel?: () => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Helper Functions\n// ============================================\n\nfunction isDateDisabled(date: Date, disabledDates: Date[]): boolean {\n return disabledDates.some((d) => isSameDay(d, date));\n}\n\nfunction isDateInRange(date: Date, range: DateRange): boolean {\n if (!range.start || !range.end) return false;\n return isWithinInterval(date, { start: range.start, end: range.end });\n}\n\nfunction isRangeStart(date: Date, range: DateRange): boolean {\n return range.start ? isSameDay(date, range.start) : false;\n}\n\nfunction isRangeEnd(date: Date, range: DateRange): boolean {\n return range.end ? isSameDay(date, range.end) : false;\n}\n\nfunction getDatePrice(date: Date, pricedDates: PricedDate[]): string | null {\n const priced = pricedDates.find((p) => isSameDay(p.date, date));\n return priced ? priced.price : null;\n}\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface DateCellProps {\n date: Date;\n currentMonth: Date;\n today: Date;\n selectedRange: DateRange;\n disabledDates: Date[];\n pricedDates: PricedDate[];\n onSelect: (date: Date) => void;\n}\n\nfunction DateCell({\n date,\n currentMonth,\n today,\n selectedRange,\n disabledDates,\n pricedDates,\n onSelect,\n}: DateCellProps) {\n const isCurrentMonth = isSameMonth(date, currentMonth);\n const isToday = isSameDay(date, today);\n const isDisabled = isDateDisabled(date, disabledDates);\n const isInRange = isDateInRange(date, selectedRange);\n const isStart = isRangeStart(date, selectedRange);\n const isEnd = isRangeEnd(date, selectedRange);\n const isSelected = isStart || isEnd;\n const price = getDatePrice(date, pricedDates);\n\n // Determine if this is a past date (before today, should show as disabled)\n const isPast = isBefore(date, today) && !isSameDay(date, today);\n\n // Don't render dates from other months\n if (!isCurrentMonth) {\n return (\n <div className=\"flex justify-center items-center\">\n <div className=\"size-12\" />\n </div>\n );\n }\n\n const handleClick = () => {\n if (!isDisabled && !isPast) {\n onSelect(date);\n }\n };\n\n // Determine styling based on state\n let bgColor = \"transparent\";\n let textColor = \"var(--canvas-text-placeholder)\";\n let showStrikethrough = false;\n let priceTextColor = \"var(--canvas-text)\";\n\n if (isDisabled || isPast) {\n textColor = \"var(--canvas-border-disabled)\";\n showStrikethrough = true;\n } else if (isSelected) {\n bgColor = \"var(--canvas-primary)\";\n textColor = \"var(--canvas-primary-foreground)\";\n priceTextColor = \"var(--canvas-primary-foreground)\";\n } else if (isInRange) {\n bgColor = \"var(--canvas-surface-brand)\";\n textColor = \"var(--canvas-primary)\";\n }\n\n return (\n <div className=\"flex justify-center items-center\">\n <button\n type=\"button\"\n onClick={handleClick}\n disabled={isDisabled || isPast}\n className={cn(\n \"relative flex flex-col items-center justify-center rounded-full size-12 transition-colors\",\n !isDisabled && !isPast && \"hover:bg-[var(--canvas-surface)] cursor-pointer\",\n (isDisabled || isPast) && \"cursor-not-allowed\"\n )}\n style={{\n backgroundColor: bgColor,\n }}\n >\n {/* Today indicator */}\n {isToday && !isSelected && (\n <div\n className=\"absolute top-1.5 rounded-full\"\n style={{\n width: \"var(--spacing-sm)\",\n height: \"var(--spacing-sm)\",\n backgroundColor: \"var(--canvas-primary)\",\n }}\n />\n )}\n\n {/* Date number */}\n <span\n className={cn(\n \"font-semibold text-base leading-6\",\n showStrikethrough && \"line-through\"\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n color: textColor,\n }}\n >\n {format(date, \"d\")}\n </span>\n\n {/* Price label */}\n {price && !isDisabled && !isPast && (\n <span\n className=\"absolute text-[6px] font-normal\"\n style={{\n bottom: \"4px\",\n color: priceTextColor,\n fontFamily: \"var(--typo-body-xs-font, var(--typo-global-font))\",\n }}\n >\n {price}\n </span>\n )}\n </button>\n </div>\n );\n}\n\ninterface MonthCalendarProps {\n month: Date;\n today: Date;\n selectedRange: DateRange;\n disabledDates: Date[];\n pricedDates: PricedDate[];\n onDateSelect: (date: Date) => void;\n onPrevMonth?: () => void;\n onNextMonth?: () => void;\n showPrevArrow?: boolean;\n showNextArrow?: boolean;\n}\n\nfunction MonthCalendar({\n month,\n today,\n selectedRange,\n disabledDates,\n pricedDates,\n onDateSelect,\n onPrevMonth,\n onNextMonth,\n showPrevArrow = false,\n showNextArrow = false,\n}: MonthCalendarProps) {\n const dayHeaders = [\"SUN\", \"MON\", \"TUE\", \"WED\", \"THU\", \"FRI\", \"SAT\"];\n\n // Generate calendar grid\n const monthStart = startOfMonth(month);\n const monthEnd = endOfMonth(month);\n const calendarStart = startOfWeek(monthStart, { weekStartsOn: 0 });\n const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 0 });\n\n const weeks: Date[][] = [];\n let currentDate = calendarStart;\n\n while (currentDate <= calendarEnd) {\n const week: Date[] = [];\n for (let i = 0; i < 7; i++) {\n week.push(currentDate);\n currentDate = addDays(currentDate, 1);\n }\n weeks.push(week);\n }\n\n return (\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-lg)\", minWidth: \"336px\" }}\n >\n {/* Month Header */}\n <div\n className=\"flex items-center justify-center\"\n style={{\n paddingLeft: showPrevArrow ? \"0\" : \"var(--spacing-4xl)\",\n paddingRight: showNextArrow ? \"0\" : \"var(--spacing-4xl)\",\n }}\n >\n {showPrevArrow && (\n <button\n type=\"button\"\n onClick={onPrevMonth}\n className=\"size-8 flex items-center justify-center hover:bg-[var(--canvas-surface)] rounded-md transition-colors shrink-0\"\n >\n <ChevronLeft\n className=\"size-4\"\n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n )}\n <span\n className=\"flex-1 text-center font-medium\"\n style={{\n fontFamily: \"var(--typo-body-xl-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n lineHeight: \"30px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {format(month, \"MMMM\")}\n </span>\n {showNextArrow && (\n <button\n type=\"button\"\n onClick={onNextMonth}\n className=\"size-8 flex items-center justify-center hover:bg-[var(--canvas-surface)] rounded-md transition-colors shrink-0\"\n >\n <ChevronRight\n className=\"size-4\"\n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n )}\n </div>\n\n {/* Day Headers */}\n <div\n className=\"grid grid-cols-7\"\n style={{\n paddingLeft: showPrevArrow ? \"0\" : \"var(--spacing-4xl)\",\n paddingRight: showNextArrow ? \"0\" : \"var(--spacing-4xl)\",\n }}\n >\n {dayHeaders.map((day) => (\n <div\n key={day}\n className=\"text-center\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"20px\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {day}\n </div>\n ))}\n </div>\n\n {/* Week Rows */}\n {weeks.map((week, weekIndex) => (\n <div\n key={weekIndex}\n className=\"grid grid-cols-7\"\n style={{\n paddingLeft: showPrevArrow ? \"0\" : \"var(--spacing-4xl)\",\n paddingRight: showNextArrow ? \"0\" : \"var(--spacing-4xl)\",\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n {week.map((date, dateIndex) => (\n <DateCell\n key={dateIndex}\n date={date}\n currentMonth={month}\n today={today}\n selectedRange={selectedRange}\n disabledDates={disabledDates}\n pricedDates={pricedDates}\n onSelect={onDateSelect}\n />\n ))}\n </div>\n ))}\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Monthly Calendar Widget Block\n *\n * A dual-month calendar widget for date range selection with support for\n * disabled dates, price labels, and today indicator. Commonly used for\n * booking and scheduling interfaces.\n *\n * @example\n * ```tsx\n * <MonthlyCalendarWidget\n * title=\"Browse availability\"\n * subtitle=\"Book your stay\"\n * onRangeChange={(range) => console.log(range)}\n * onConfirm={() => console.log(\"Confirmed\")}\n * />\n * ```\n */\nexport function MonthlyCalendarWidget({\n title = \"Browse availability\",\n subtitle = \"Book your stay\",\n initialMonth,\n selectedRange: controlledRange,\n disabledDates = [],\n pricedDates = [],\n todayDate,\n onDateSelect,\n onRangeChange,\n onConfirm,\n onCancel,\n className,\n}: MonthlyCalendarWidgetProps) {\n const today = todayDate || new Date();\n const [currentMonth, setCurrentMonth] = useState<Date>(\n initialMonth || startOfMonth(today)\n );\n const [internalRange, setInternalRange] = useState<DateRange>({\n start: null,\n end: null,\n });\n\n const selectedRange = controlledRange ?? internalRange;\n const nextMonth = addMonths(currentMonth, 1);\n\n const handleDateSelect = (date: Date) => {\n onDateSelect?.(date);\n\n let newRange: DateRange;\n\n if (!selectedRange.start || (selectedRange.start && selectedRange.end)) {\n // Start new selection\n newRange = { start: date, end: null };\n } else {\n // Complete the selection\n if (isBefore(date, selectedRange.start)) {\n newRange = { start: date, end: selectedRange.start };\n } else {\n newRange = { start: selectedRange.start, end: date };\n }\n }\n\n if (!controlledRange) {\n setInternalRange(newRange);\n }\n onRangeChange?.(newRange);\n };\n\n const handlePrevMonth = () => {\n setCurrentMonth((prev) => subMonths(prev, 1));\n };\n\n const handleNextMonth = () => {\n setCurrentMonth((prev) => addMonths(prev, 1));\n };\n\n const formatInputDate = (date: Date | null): string => {\n if (!date) return \"\";\n return format(date, \"MMM d, yyyy\");\n };\n\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title Section */}\n <div\n className=\"flex flex-wrap items-start w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <div\n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {subtitle}\n </p>\n </div>\n </div>\n\n {/* Calendar Content */}\n <div\n className=\"flex flex-col w-full\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Dual Month Grid */}\n <div className=\"flex items-start justify-between w-full gap-4 overflow-x-auto\">\n <MonthCalendar\n month={currentMonth}\n today={today}\n selectedRange={selectedRange}\n disabledDates={disabledDates}\n pricedDates={pricedDates}\n onDateSelect={handleDateSelect}\n onPrevMonth={handlePrevMonth}\n showPrevArrow={true}\n showNextArrow={false}\n />\n <MonthCalendar\n month={nextMonth}\n today={today}\n selectedRange={selectedRange}\n disabledDates={disabledDates}\n pricedDates={pricedDates}\n onDateSelect={handleDateSelect}\n onNextMonth={handleNextMonth}\n showPrevArrow={false}\n showNextArrow={true}\n />\n </div>\n\n {/* Footer Section */}\n <div className=\"flex items-start justify-between w-full\">\n {/* Date Inputs */}\n <div\n className=\"flex items-center justify-center\"\n style={{ gap: \"var(--spacing-md)\", width: \"286px\" }}\n >\n {/* Start Date Input */}\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-xs)\", width: \"128px\" }}\n >\n <div\n className=\"flex items-center h-11 rounded\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-xs)\",\n paddingLeft: \"var(--spacing-xl)\",\n paddingRight: \"var(--spacing-xl)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"24px\",\n color: selectedRange.start\n ? \"var(--canvas-text)\"\n : \"var(--canvas-text-placeholder)\",\n }}\n >\n {selectedRange.start\n ? formatInputDate(selectedRange.start)\n : \"Start date\"}\n </span>\n </div>\n </div>\n\n {/* Arrow */}\n <ArrowRight\n className=\"size-5 shrink-0\"\n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n\n {/* End Date Input */}\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-xs)\", width: \"128px\" }}\n >\n <div\n className=\"flex items-center h-11 rounded\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-xs)\",\n paddingLeft: \"var(--spacing-xl)\",\n paddingRight: \"var(--spacing-xl)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"24px\",\n color: selectedRange.end\n ? \"var(--canvas-text)\"\n : \"var(--canvas-text-placeholder)\",\n }}\n >\n {selectedRange.end\n ? formatInputDate(selectedRange.end)\n : \"End date\"}\n </span>\n </div>\n </div>\n </div>\n\n {/* Action Buttons */}\n <div\n className=\"flex items-center justify-center\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n <Button variant=\"outline\" size=\"lg\" onClick={onCancel}>\n Cancel\n </Button>\n <Button variant=\"primary\" size=\"lg\" onClick={onConfirm}>\n Confirm\n </Button>\n </div>\n </div>\n </div>\n </div>\n );\n}\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { ChevronLeft, ChevronRight, ArrowRight } from \"lucide-react\";\nimport {\n format,\n startOfMonth,\n endOfMonth,\n startOfWeek,\n endOfWeek,\n addDays,\n addMonths,\n subMonths,\n isSameMonth,\n isSameDay,\n isWithinInterval,\n isBefore,\n isAfter,\n} from \"date-fns\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PricedDate {\n date: Date;\n price: string;\n}\n\nexport interface DateRange {\n start: Date | null;\n end: Date | null;\n}\n\nexport interface MonthlyCalendarWidgetProps {\n /** Widget title */\n title?: string;\n /** Widget subtitle */\n subtitle?: string;\n /** Initial month to display (defaults to current month) */\n initialMonth?: Date;\n /** Currently selected date range */\n selectedRange?: DateRange;\n /** Array of dates that should be disabled/unavailable */\n disabledDates?: Date[];\n /** Array of dates with prices to display */\n pricedDates?: PricedDate[];\n /** Override for \"today\" (useful for demos) */\n todayDate?: Date;\n /** Callback when a date is selected */\n onDateSelect?: (date: Date) => void;\n /** Callback when the date range changes */\n onRangeChange?: (range: DateRange) => void;\n /** Callback when Confirm button is clicked */\n onConfirm?: () => void;\n /** Callback when Cancel button is clicked */\n onCancel?: () => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Helper Functions\n// ============================================\n\nfunction isDateDisabled(date: Date, disabledDates: Date[]): boolean {\n return disabledDates.some((d) => isSameDay(d, date));\n}\n\nfunction isDateInRange(date: Date, range: DateRange): boolean {\n if (!range.start || !range.end) return false;\n return isWithinInterval(date, { start: range.start, end: range.end });\n}\n\nfunction isRangeStart(date: Date, range: DateRange): boolean {\n return range.start ? isSameDay(date, range.start) : false;\n}\n\nfunction isRangeEnd(date: Date, range: DateRange): boolean {\n return range.end ? isSameDay(date, range.end) : false;\n}\n\nfunction getDatePrice(date: Date, pricedDates: PricedDate[]): string | null {\n const priced = pricedDates.find((p) => isSameDay(p.date, date));\n return priced ? priced.price : null;\n}\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface DateCellProps {\n date: Date;\n currentMonth: Date;\n today: Date;\n selectedRange: DateRange;\n disabledDates: Date[];\n pricedDates: PricedDate[];\n onSelect: (date: Date) => void;\n}\n\nfunction DateCell({\n date,\n currentMonth,\n today,\n selectedRange,\n disabledDates,\n pricedDates,\n onSelect,\n}: DateCellProps) {\n const isCurrentMonth = isSameMonth(date, currentMonth);\n const isToday = isSameDay(date, today);\n const isDisabled = isDateDisabled(date, disabledDates);\n const isInRange = isDateInRange(date, selectedRange);\n const isStart = isRangeStart(date, selectedRange);\n const isEnd = isRangeEnd(date, selectedRange);\n const isSelected = isStart || isEnd;\n const price = getDatePrice(date, pricedDates);\n\n // Determine if this is a past date (before today, should show as disabled)\n const isPast = isBefore(date, today) && !isSameDay(date, today);\n\n // Don't render dates from other months\n if (!isCurrentMonth) {\n return (\n <div className=\"flex justify-center items-center\">\n <div className=\"size-12\" />\n </div>\n );\n }\n\n const handleClick = () => {\n if (!isDisabled && !isPast) {\n onSelect(date);\n }\n };\n\n // Determine styling based on state\n let bgColor = \"transparent\";\n let textColor = \"var(--canvas-text-placeholder)\";\n let showStrikethrough = false;\n let priceTextColor = \"var(--canvas-text)\";\n\n if (isDisabled || isPast) {\n textColor = \"var(--canvas-border-disabled)\";\n showStrikethrough = true;\n } else if (isSelected) {\n bgColor = \"var(--canvas-primary)\";\n textColor = \"var(--canvas-primary-foreground)\";\n priceTextColor = \"var(--canvas-primary-foreground)\";\n } else if (isInRange) {\n bgColor = \"var(--canvas-surface-brand)\";\n textColor = \"var(--canvas-primary)\";\n }\n\n return (\n <div className=\"flex justify-center items-center\">\n <button\n type=\"button\"\n onClick={handleClick}\n disabled={isDisabled || isPast}\n className={cn(\n \"relative flex flex-col items-center justify-center rounded-full size-12 transition-colors\",\n !isDisabled && !isPast && \"hover:bg-[var(--canvas-surface)] cursor-pointer\",\n (isDisabled || isPast) && \"cursor-not-allowed\"\n )}\n style={{\n backgroundColor: bgColor,\n }}\n >\n {/* Today indicator */}\n {isToday && !isSelected && (\n <div\n className=\"absolute top-1.5 rounded-full\"\n style={{\n width: \"var(--spacing-sm)\",\n height: \"var(--spacing-sm)\",\n backgroundColor: \"var(--canvas-primary)\",\n }}\n />\n )}\n\n {/* Date number */}\n <span\n className={cn(\n \"font-semibold leading-6\",\n showStrikethrough && \"line-through\"\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n color: textColor,\n }}\n >\n {format(date, \"d\")}\n </span>\n\n {/* Price label */}\n {price && !isDisabled && !isPast && (\n <span\n className=\"absolute text-[6px] font-normal\"\n style={{\n bottom: \"4px\",\n color: priceTextColor,\n fontFamily: \"var(--typo-body-xs-font, var(--typo-global-font))\",\n }}\n >\n {price}\n </span>\n )}\n </button>\n </div>\n );\n}\n\ninterface MonthCalendarProps {\n month: Date;\n today: Date;\n selectedRange: DateRange;\n disabledDates: Date[];\n pricedDates: PricedDate[];\n onDateSelect: (date: Date) => void;\n onPrevMonth?: () => void;\n onNextMonth?: () => void;\n showPrevArrow?: boolean;\n showNextArrow?: boolean;\n}\n\nfunction MonthCalendar({\n month,\n today,\n selectedRange,\n disabledDates,\n pricedDates,\n onDateSelect,\n onPrevMonth,\n onNextMonth,\n showPrevArrow = false,\n showNextArrow = false,\n}: MonthCalendarProps) {\n const dayHeaders = [\"SUN\", \"MON\", \"TUE\", \"WED\", \"THU\", \"FRI\", \"SAT\"];\n\n // Generate calendar grid\n const monthStart = startOfMonth(month);\n const monthEnd = endOfMonth(month);\n const calendarStart = startOfWeek(monthStart, { weekStartsOn: 0 });\n const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 0 });\n\n const weeks: Date[][] = [];\n let currentDate = calendarStart;\n\n while (currentDate <= calendarEnd) {\n const week: Date[] = [];\n for (let i = 0; i < 7; i++) {\n week.push(currentDate);\n currentDate = addDays(currentDate, 1);\n }\n weeks.push(week);\n }\n\n return (\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-lg)\", minWidth: \"336px\" }}\n >\n {/* Month Header */}\n <div\n className=\"flex items-center justify-center\"\n style={{\n paddingLeft: showPrevArrow ? \"0\" : \"var(--spacing-4xl)\",\n paddingRight: showNextArrow ? \"0\" : \"var(--spacing-4xl)\",\n }}\n >\n {showPrevArrow && (\n <button\n type=\"button\"\n onClick={onPrevMonth}\n className=\"size-8 flex items-center justify-center hover:bg-[var(--canvas-surface)] rounded-md transition-colors shrink-0\"\n >\n <ChevronLeft\n className=\"size-4\"\n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n )}\n <span\n className=\"flex-1 text-center font-medium\"\n style={{\n fontFamily: \"var(--typo-body-xl-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n lineHeight: \"30px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {format(month, \"MMMM\")}\n </span>\n {showNextArrow && (\n <button\n type=\"button\"\n onClick={onNextMonth}\n className=\"size-8 flex items-center justify-center hover:bg-[var(--canvas-surface)] rounded-md transition-colors shrink-0\"\n >\n <ChevronRight\n className=\"size-4\"\n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n )}\n </div>\n\n {/* Day Headers */}\n <div\n className=\"grid grid-cols-7\"\n style={{\n paddingLeft: showPrevArrow ? \"0\" : \"var(--spacing-4xl)\",\n paddingRight: showNextArrow ? \"0\" : \"var(--spacing-4xl)\",\n }}\n >\n {dayHeaders.map((day) => (\n <div\n key={day}\n className=\"text-center\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"20px\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {day}\n </div>\n ))}\n </div>\n\n {/* Week Rows */}\n {weeks.map((week, weekIndex) => (\n <div\n key={weekIndex}\n className=\"grid grid-cols-7\"\n style={{\n paddingLeft: showPrevArrow ? \"0\" : \"var(--spacing-4xl)\",\n paddingRight: showNextArrow ? \"0\" : \"var(--spacing-4xl)\",\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n {week.map((date, dateIndex) => (\n <DateCell\n key={dateIndex}\n date={date}\n currentMonth={month}\n today={today}\n selectedRange={selectedRange}\n disabledDates={disabledDates}\n pricedDates={pricedDates}\n onSelect={onDateSelect}\n />\n ))}\n </div>\n ))}\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Monthly Calendar Widget Block\n *\n * A dual-month calendar widget for date range selection with support for\n * disabled dates, price labels, and today indicator. Commonly used for\n * booking and scheduling interfaces.\n *\n * @example\n * ```tsx\n * <MonthlyCalendarWidget\n * title=\"Browse availability\"\n * subtitle=\"Book your stay\"\n * onRangeChange={(range) => console.log(range)}\n * onConfirm={() => console.log(\"Confirmed\")}\n * />\n * ```\n */\nexport function MonthlyCalendarWidget({\n title = \"Browse availability\",\n subtitle = \"Book your stay\",\n initialMonth,\n selectedRange: controlledRange,\n disabledDates = [],\n pricedDates = [],\n todayDate,\n onDateSelect,\n onRangeChange,\n onConfirm,\n onCancel,\n className,\n}: MonthlyCalendarWidgetProps) {\n const today = todayDate || new Date();\n const [currentMonth, setCurrentMonth] = useState<Date>(\n initialMonth || startOfMonth(today)\n );\n const [internalRange, setInternalRange] = useState<DateRange>({\n start: null,\n end: null,\n });\n\n const selectedRange = controlledRange ?? internalRange;\n const nextMonth = addMonths(currentMonth, 1);\n\n const handleDateSelect = (date: Date) => {\n onDateSelect?.(date);\n\n let newRange: DateRange;\n\n if (!selectedRange.start || (selectedRange.start && selectedRange.end)) {\n // Start new selection\n newRange = { start: date, end: null };\n } else {\n // Complete the selection\n if (isBefore(date, selectedRange.start)) {\n newRange = { start: date, end: selectedRange.start };\n } else {\n newRange = { start: selectedRange.start, end: date };\n }\n }\n\n if (!controlledRange) {\n setInternalRange(newRange);\n }\n onRangeChange?.(newRange);\n };\n\n const handlePrevMonth = () => {\n setCurrentMonth((prev) => subMonths(prev, 1));\n };\n\n const handleNextMonth = () => {\n setCurrentMonth((prev) => addMonths(prev, 1));\n };\n\n const formatInputDate = (date: Date | null): string => {\n if (!date) return \"\";\n return format(date, \"MMM d, yyyy\");\n };\n\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title Section */}\n <div\n className=\"flex flex-wrap items-start w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <div\n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {subtitle}\n </p>\n </div>\n </div>\n\n {/* Calendar Content */}\n <div\n className=\"flex flex-col w-full\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Dual Month Grid */}\n <div className=\"flex items-start justify-between w-full gap-4 overflow-x-auto\">\n <MonthCalendar\n month={currentMonth}\n today={today}\n selectedRange={selectedRange}\n disabledDates={disabledDates}\n pricedDates={pricedDates}\n onDateSelect={handleDateSelect}\n onPrevMonth={handlePrevMonth}\n showPrevArrow={true}\n showNextArrow={false}\n />\n <MonthCalendar\n month={nextMonth}\n today={today}\n selectedRange={selectedRange}\n disabledDates={disabledDates}\n pricedDates={pricedDates}\n onDateSelect={handleDateSelect}\n onNextMonth={handleNextMonth}\n showPrevArrow={false}\n showNextArrow={true}\n />\n </div>\n\n {/* Footer Section */}\n <div className=\"flex items-start justify-between w-full\">\n {/* Date Inputs */}\n <div\n className=\"flex items-center justify-center\"\n style={{ gap: \"var(--spacing-md)\", width: \"286px\" }}\n >\n {/* Start Date Input */}\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-xs)\", width: \"128px\" }}\n >\n <div\n className=\"flex items-center h-11 rounded\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-xs)\",\n paddingLeft: \"var(--spacing-xl)\",\n paddingRight: \"var(--spacing-xl)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"24px\",\n color: selectedRange.start\n ? \"var(--canvas-text)\"\n : \"var(--canvas-text-placeholder)\",\n }}\n >\n {selectedRange.start\n ? formatInputDate(selectedRange.start)\n : \"Start date\"}\n </span>\n </div>\n </div>\n\n {/* Arrow */}\n <ArrowRight\n className=\"size-5 shrink-0\"\n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n\n {/* End Date Input */}\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-xs)\", width: \"128px\" }}\n >\n <div\n className=\"flex items-center h-11 rounded\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-xs)\",\n paddingLeft: \"var(--spacing-xl)\",\n paddingRight: \"var(--spacing-xl)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"24px\",\n color: selectedRange.end\n ? \"var(--canvas-text)\"\n : \"var(--canvas-text-placeholder)\",\n }}\n >\n {selectedRange.end\n ? formatInputDate(selectedRange.end)\n : \"End date\"}\n </span>\n </div>\n </div>\n </div>\n\n {/* Action Buttons */}\n <div\n className=\"flex items-center justify-center\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n <Button variant=\"outline\" size=\"lg\" onClick={onCancel}>\n Cancel\n </Button>\n <Button variant=\"primary\" size=\"lg\" onClick={onConfirm}>\n Confirm\n </Button>\n </div>\n </div>\n </div>\n </div>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/page-header-section.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { LineTabs, LineTab } from \"../ui/line-tabs\";\n\ninterface PageHeaderSectionProps {\n /** Page title */\n title?: string;\n /** Page description */\n description?: string;\n /** Array of tab items */\n tabs?: LineTab[];\n /** ID of the currently active tab */\n activeTab?: string;\n /** Callback when a tab is clicked */\n onTabChange?: (tabId: string) => void;\n /** Whether to show the tabs */\n showTabs?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Page Header Section Component\n * \n * A section combining page title, description, and line tabs.\n * Centered with max-width constraint.\n * \n * @example\n * ```tsx\n * <PageHeaderSection \n * title=\"Page title\"\n * description=\"Description\"\n * tabs={[\n * { id: \"tab1\", label: \"Tab 1\" },\n * { id: \"tab2\", label: \"Tab 2\" },\n * ]}\n * />\n * ```\n */\nexport function PageHeaderSection({\n title = \"Page title\",\n description = \"Description\",\n tabs = [\n { id: \"tab1\", label: \"Tab 1\" },\n { id: \"tab2\", label: \"Tab 2\" },\n { id: \"tab3\", label: \"Tab 3\" },\n { id: \"tab4\", label: \"Tab 4\" },\n ],\n activeTab,\n onTabChange,\n showTabs = true,\n className,\n}: PageHeaderSectionProps) {\n return (\n <div className={cn(\"flex flex-col w-full border-b border-[var(--canvas-border)]\", className)}>\n {/* Page Title and Description */}\n <div\n className={cn(\n \"flex flex-col gap-1 w-full max-w-[var(--content-max-width)] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-[var(--spacing-6xl)]\"\n )}\n >\n {/* Title - Uses typography variables */}\n <h2
|
|
9
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { LineTabs, LineTab } from \"../ui/line-tabs\";\n\ninterface PageHeaderSectionProps {\n /** Page title */\n title?: string;\n /** Page description */\n description?: string;\n /** Array of tab items */\n tabs?: LineTab[];\n /** ID of the currently active tab */\n activeTab?: string;\n /** Callback when a tab is clicked */\n onTabChange?: (tabId: string) => void;\n /** Whether to show the tabs */\n showTabs?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Page Header Section Component\n * \n * A section combining page title, description, and line tabs.\n * Centered with max-width constraint.\n * \n * @example\n * ```tsx\n * <PageHeaderSection \n * title=\"Page title\"\n * description=\"Description\"\n * tabs={[\n * { id: \"tab1\", label: \"Tab 1\" },\n * { id: \"tab2\", label: \"Tab 2\" },\n * ]}\n * />\n * ```\n */\nexport function PageHeaderSection({\n title = \"Page title\",\n description = \"Description\",\n tabs = [\n { id: \"tab1\", label: \"Tab 1\" },\n { id: \"tab2\", label: \"Tab 2\" },\n { id: \"tab3\", label: \"Tab 3\" },\n { id: \"tab4\", label: \"Tab 4\" },\n ],\n activeTab,\n onTabChange,\n showTabs = true,\n className,\n}: PageHeaderSectionProps) {\n return (\n <div className={cn(\"flex flex-col w-full border-b border-[var(--canvas-border)]\", className)}>\n {/* Page Title and Description */}\n <div\n className={cn(\n \"flex flex-col gap-1 w-full max-w-[var(--content-max-width)] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-[var(--spacing-6xl)]\"\n )}\n >\n {/* Title - Uses typography variables */}\n <h2\n style={{\n fontFamily: \"var(--typo-h4-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h4-size)\",\n fontWeight: \"var(--typo-h4-weight)\",\n letterSpacing: \"var(--typo-h4-spacing)\",\n lineHeight: \"var(--typo-h4-line-height)\",\n color: \"var(--typo-h4-color)\",\n }}\n >\n {title}\n </h2>\n {/* Description - Uses typography variables with muted color */}\n <p\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: \"var(--typo-body-m-weight)\",\n letterSpacing: \"var(--typo-body-m-spacing)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--typo-body-m-color-muted)\",\n }}\n >\n {description}\n </p>\n </div>\n\n {/* Line Tabs */}\n {showTabs && (\n <div className=\"w-full max-w-[var(--content-max-width)] mx-auto px-[var(--spacing-xl)] lg:px-0\">\n <LineTabs\n tabs={tabs}\n activeTab={activeTab}\n onTabChange={onTabChange}\n />\n </div>\n )}\n </div>\n );\n}\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|