canvas-ui-sdk 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/dist/cli/index.js +516 -0
  2. package/dist/index.d.ts +67 -3
  3. package/dist/index.js +2588 -301
  4. package/dist/index.js.map +1 -1
  5. package/mcp/dist/index.js +5 -1
  6. package/package.json +18 -2
  7. package/registry/blocks/activity-feed.json +19 -0
  8. package/registry/blocks/blog-cards.json +16 -0
  9. package/registry/blocks/bottom-input-chat-widget.json +19 -0
  10. package/registry/blocks/canvas-item.json +18 -0
  11. package/registry/blocks/category-grid.json +16 -0
  12. package/registry/blocks/centered-hero.json +14 -0
  13. package/registry/blocks/chat-message.json +18 -0
  14. package/registry/blocks/circular-progress-bar-list.json +18 -0
  15. package/registry/blocks/component-palette.json +21 -0
  16. package/registry/blocks/component-search.json +19 -0
  17. package/registry/blocks/content-dropzone.json +16 -0
  18. package/registry/blocks/content-with-image.json +14 -0
  19. package/registry/blocks/core-values-grid.json +16 -0
  20. package/registry/blocks/credit-card-display.json +16 -0
  21. package/registry/blocks/cta-banner.json +14 -0
  22. package/registry/blocks/custom-component-helper.json +19 -0
  23. package/registry/blocks/destination-cards.json +16 -0
  24. package/registry/blocks/empty-state.json +16 -0
  25. package/registry/blocks/faq-accordion.json +16 -0
  26. package/registry/blocks/faqs-table.json +18 -0
  27. package/registry/blocks/feature-with-image.json +16 -0
  28. package/registry/blocks/featured-news-cards.json +16 -0
  29. package/registry/blocks/featured-places.json +16 -0
  30. package/registry/blocks/features-comparison.json +16 -0
  31. package/registry/blocks/filter-popover.json +28 -0
  32. package/registry/blocks/fixed-column-data-table.json +20 -0
  33. package/registry/blocks/flair-banner.json +16 -0
  34. package/registry/blocks/footer-navbar.json +17 -0
  35. package/registry/blocks/form-group.json +29 -0
  36. package/registry/blocks/gallery-section.json +14 -0
  37. package/registry/blocks/gradient-banner.json +16 -0
  38. package/registry/blocks/graph-metric-tiles.json +20 -0
  39. package/registry/blocks/grid-tiles-list.json +20 -0
  40. package/registry/blocks/hero-dark-centered.json +16 -0
  41. package/registry/blocks/hero-dark-with-image.json +16 -0
  42. package/registry/blocks/hero-fullwidth-image.json +16 -0
  43. package/registry/blocks/hero-section.json +16 -0
  44. package/registry/blocks/how-it-works.json +16 -0
  45. package/registry/blocks/image-feed-with-nested-comments.json +20 -0
  46. package/registry/blocks/infinity-canvas.json +58 -0
  47. package/registry/blocks/large-image-labels-list.json +19 -0
  48. package/registry/blocks/loader.json +19 -0
  49. package/registry/blocks/login-branding-panel.json +16 -0
  50. package/registry/blocks/menu-section.json +18 -0
  51. package/registry/blocks/menufocus-template.json +19 -0
  52. package/registry/blocks/messenger-sidebar.json +19 -0
  53. package/registry/blocks/metrics-section.json +14 -0
  54. package/registry/blocks/mobile-bottom-nav.json +18 -0
  55. package/registry/blocks/monthly-calendar-widget.json +20 -0
  56. package/registry/blocks/nested-comments-table.json +21 -0
  57. package/registry/blocks/nested-data-table.json +22 -0
  58. package/registry/blocks/office-locations.json +14 -0
  59. package/registry/blocks/page-header-section.json +17 -0
  60. package/registry/blocks/page-previews.json +29 -0
  61. package/registry/blocks/pagination.json +20 -0
  62. package/registry/blocks/participant-list.json +17 -0
  63. package/registry/blocks/persona-card.json +18 -0
  64. package/registry/blocks/pill-tabs.json +19 -0
  65. package/registry/blocks/pricing-cards.json +16 -0
  66. package/registry/blocks/pricing-cta.json +14 -0
  67. package/registry/blocks/profile-card.json +20 -0
  68. package/registry/blocks/profile-grid-tiles-list.json +21 -0
  69. package/registry/blocks/profile-image-uploader.json +19 -0
  70. package/registry/blocks/profile-info-cards.json +19 -0
  71. package/registry/blocks/progress-bar.json +16 -0
  72. package/registry/blocks/prompt-template.json +18 -0
  73. package/registry/blocks/reviews-grid.json +14 -0
  74. package/registry/blocks/reviews-table.json +19 -0
  75. package/registry/blocks/screen-flowchart.json +19 -0
  76. package/registry/blocks/screen-prompt-builder.json +19 -0
  77. package/registry/blocks/screen-prompt-template.json +18 -0
  78. package/registry/blocks/search-bar.json +19 -0
  79. package/registry/blocks/search-sidebar.json +25 -0
  80. package/registry/blocks/settings-list-row.json +20 -0
  81. package/registry/blocks/sidebar-cards.json +18 -0
  82. package/registry/blocks/sidebar-profile-card.json +21 -0
  83. package/registry/blocks/slideshow-grid-tiles.json +21 -0
  84. package/registry/blocks/social-feed.json +20 -0
  85. package/registry/blocks/social-proof.json +14 -0
  86. package/registry/blocks/standard-data-table.json +20 -0
  87. package/registry/blocks/standard-list-with-image.json +17 -0
  88. package/registry/blocks/step-tracker.json +16 -0
  89. package/registry/blocks/team-cards-grid.json +16 -0
  90. package/registry/blocks/team-circular-grid.json +16 -0
  91. package/registry/blocks/testimonial-carousel.json +16 -0
  92. package/registry/blocks/upvoting-posts-table.json +22 -0
  93. package/registry/blocks/vertical-how-it-works.json +16 -0
  94. package/registry/blocks/vertical-step-tracker.json +17 -0
  95. package/registry/blocks/video-chat-controls.json +18 -0
  96. package/registry/blocks/video-content-section.json +16 -0
  97. package/registry/blocks/video-playlist.json +18 -0
  98. package/registry/blocks/webcam-preview.json +18 -0
  99. package/registry/blocks/youtube-player.json +16 -0
  100. package/registry/hooks/use-css-variable-sync.json +14 -0
  101. package/registry/hooks/use-mobile.json +14 -0
  102. package/registry/index.json +730 -0
  103. package/registry/layout/account-settings-shell.json +20 -0
  104. package/registry/layout/dashboard-shell.json +23 -0
  105. package/registry/layout/double-sidebar-shell.json +23 -0
  106. package/registry/layout/double-sidebar.json +20 -0
  107. package/registry/layout/header.json +22 -0
  108. package/registry/layout/icon-sidebar-shell.json +23 -0
  109. package/registry/layout/icon-sidebar.json +19 -0
  110. package/registry/layout/mobile-menu-shell.json +19 -0
  111. package/registry/layout/multistep-progressbar-shell.json +23 -0
  112. package/registry/layout/multistep-shell.json +21 -0
  113. package/registry/layout/multistep-sidebar-shell.json +22 -0
  114. package/registry/layout/project-context-shell.json +20 -0
  115. package/registry/layout/search-bar-shell.json +22 -0
  116. package/registry/layout/sidebar-nav.json +18 -0
  117. package/registry/layout/sidebar.json +20 -0
  118. package/registry/layout/standard-page-shell.json +21 -0
  119. package/registry/layout/vertical-multistep-shell.json +23 -0
  120. package/registry/lib/utils.json +17 -0
  121. package/registry/ui/avatar.json +18 -0
  122. package/registry/ui/button.json +19 -0
  123. package/registry/ui/calendar.json +20 -0
  124. package/registry/ui/checkbox.json +19 -0
  125. package/registry/ui/date-input.json +18 -0
  126. package/registry/ui/dialog.json +19 -0
  127. package/registry/ui/dropdown-menu.json +19 -0
  128. package/registry/ui/file-uploader.json +18 -0
  129. package/registry/ui/image-uploader.json +18 -0
  130. package/registry/ui/input.json +16 -0
  131. package/registry/ui/label.json +18 -0
  132. package/registry/ui/line-tabs.json +16 -0
  133. package/registry/ui/multiselect-checkbox-field.json +18 -0
  134. package/registry/ui/multiselect-tags.json +18 -0
  135. package/registry/ui/popover.json +18 -0
  136. package/registry/ui/radio-group.json +19 -0
  137. package/registry/ui/range-input.json +17 -0
  138. package/registry/ui/scroll-area.json +18 -0
  139. package/registry/ui/searchbox.json +18 -0
  140. package/registry/ui/select.json +20 -0
  141. package/registry/ui/selectable-pills.json +16 -0
  142. package/registry/ui/separator.json +18 -0
  143. package/registry/ui/sheet.json +19 -0
  144. package/registry/ui/sidebar.json +27 -0
  145. package/registry/ui/skeleton.json +16 -0
  146. package/registry/ui/slider.json +18 -0
  147. package/registry/ui/switch.json +18 -0
  148. package/registry/ui/tabs.json +18 -0
  149. package/registry/ui/text-input.json +16 -0
  150. package/registry/ui/textarea.json +18 -0
  151. package/registry/ui/tooltip.json +18 -0
  152. package/registry/ui/typography.json +16 -0
  153. package/styles/tokens.reference.css +35 -3
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "nested-data-table",
3
+ "type": "registry:block",
4
+ "description": "Expandable data table with parent rows that reveal nested child tables. Shows hierarchical data like locations with employees.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/nested-data-table.tsx",
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\";\nimport { ChevronRight, ChevronDown, Eye } from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface ChildRow {\n id: string;\n name: string;\n avatarUrl?: string;\n email: string;\n phone: string;\n}\n\nexport interface ParentRow {\n id: string;\n location: string;\n ftes: number;\n contractors: number;\n hrContact: {\n name: string;\n phone: string;\n };\n children: ChildRow[];\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 NestedDataTableProps {\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?: ParentRow[];\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 parent row action is clicked */\n onRowAction?: (action: string, row: ParentRow) => void;\n /** Callback when child row action is clicked */\n onChildAction?: (action: string, child: ChildRow, parent: ParentRow) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultData: ParentRow[] = [\n {\n id: \"1\",\n location: \"San Francisco, CA\",\n ftes: 320,\n contractors: 66,\n hrContact: {\n name: \"Mary Trott\",\n phone: \"415-232-3434\",\n },\n children: [\n {\n id: \"1-1\",\n name: \"Jeff Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n email: \"jconner@gmail.com\",\n phone: \"508-343-5334\",\n },\n {\n id: \"1-2\",\n name: \"Aya Williams\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n email: \"eperez@gmail.com\",\n phone: \"234-989-6675\",\n },\n {\n id: \"1-3\",\n name: \"Lily Sun\",\n avatarUrl: \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face\",\n email: \"rmishra@gmail.com\",\n phone: \"205-443-4324\",\n },\n ],\n },\n {\n id: \"2\",\n location: \"New York, NY\",\n ftes: 80,\n contractors: 8,\n hrContact: {\n name: \"Raj Mishra\",\n phone: \"206-646-9834\",\n },\n children: [],\n },\n {\n id: \"3\",\n location: \"Seattle, WA\",\n ftes: 98,\n contractors: 5,\n hrContact: {\n name: \"James Clayton\",\n phone: \"312-687-8675\",\n },\n children: [],\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"location-asc\", label: \"Location (A-Z)\" },\n { id: \"location-desc\", label: \"Location (Z-A)\" },\n { id: \"ftes-high\", label: \"FTEs (High-Low)\" },\n { id: \"ftes-low\", label: \"FTEs (Low-High)\" },\n];\n\nconst defaultFilterOptions: FilterOption[] = [\n { id: \"all\", label: \"All locations\" },\n { id: \"us-west\", label: \"US West\" },\n { id: \"us-east\", label: \"US East\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface ExpandButtonProps {\n expanded: boolean;\n onClick: () => void;\n disabled?: boolean;\n}\n\nfunction ExpandButton({ expanded, onClick, disabled }: ExpandButtonProps) {\n return (\n <button\n onClick={onClick}\n disabled={disabled}\n className={cn(\n \"flex items-center justify-center shrink-0 transition-colors\",\n disabled ? \"opacity-40 cursor-not-allowed\" : \"cursor-pointer hover:bg-[var(--canvas-surface)]\"\n )}\n style={{\n width: \"32px\",\n height: \"32px\",\n borderRadius: \"var(--radius-xs)\",\n border: \"1px solid var(--canvas-border)\",\n backgroundColor: \"var(--canvas-background)\",\n }}\n aria-label={expanded ? \"Collapse row\" : \"Expand row\"}\n >\n {expanded ? (\n <ChevronDown \n size={20} \n style={{ color: \"var(--canvas-text)\" }}\n />\n ) : (\n <ChevronRight \n size={20} \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n )}\n </button>\n );\n}\n\ninterface TableHeaderCellProps {\n children: React.ReactNode;\n className?: string;\n}\n\nfunction TableHeaderCell({ children, className }: TableHeaderCellProps) {\n return (\n <div\n className={cn(\n \"flex items-center h-8 overflow-hidden\",\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 }}\n >\n {children}\n </div>\n );\n}\n\ninterface TableCellProps {\n children: React.ReactNode;\n className?: string;\n}\n\nfunction TableCell({ children, className }: TableCellProps) {\n return (\n <div\n className={cn(\n \"flex items-center gap-2 overflow-hidden\",\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 }}\n >\n {children}\n </div>\n );\n}\n\ninterface NestedTableCellProps {\n children: React.ReactNode;\n className?: string;\n}\n\nfunction NestedTableCell({ children, className }: NestedTableCellProps) {\n return (\n <div\n className={cn(\n \"flex items-center gap-2 h-8 overflow-hidden\",\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 }}\n >\n {children}\n </div>\n );\n}\n\ninterface NestedTableProps {\n children: ChildRow[];\n onChildAction?: (action: string, child: ChildRow) => void;\n}\n\nfunction NestedTable({ children, onChildAction }: NestedTableProps) {\n if (children.length === 0) return null;\n\n return (\n <div\n className=\"w-full overflow-hidden\"\n style={{\n borderRadius: \"var(--radius-md)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n {/* Nested Table Header */}\n <div\n className=\"grid items-center\"\n style={{\n backgroundColor: \"var(--canvas-surface)\",\n borderBottom: \"1px solid var(--canvas-border)\",\n gridTemplateColumns: \"minmax(160px, 1fr) minmax(160px, 1fr) minmax(120px, 1fr) 56px\",\n }}\n >\n <div style={{ padding: \"0 var(--spacing-lg)\" }}>\n <TableHeaderCell>Name</TableHeaderCell>\n </div>\n <div style={{ padding: \"0 var(--spacing-lg)\" }}>\n <TableHeaderCell>Email</TableHeaderCell>\n </div>\n <div style={{ padding: \"0 var(--spacing-lg)\" }}>\n <TableHeaderCell>Phone number</TableHeaderCell>\n </div>\n <div style={{ padding: \"0 var(--spacing-lg)\" }}>\n <TableHeaderCell>&nbsp;</TableHeaderCell>\n </div>\n </div>\n\n {/* Nested Table Rows */}\n {children.map((child, index) => (\n <div\n key={child.id}\n className=\"grid items-center\"\n style={{\n borderBottom: index < children.length - 1 ? \"1px solid var(--canvas-border)\" : \"none\",\n padding: \"var(--spacing-md) 0\",\n gridTemplateColumns: \"minmax(160px, 1fr) minmax(160px, 1fr) minmax(120px, 1fr) 56px\",\n }}\n >\n <div style={{ padding: \"0 var(--spacing-lg)\" }}>\n <NestedTableCell>\n <Avatar className=\"size-8 border border-[var(--canvas-border)]\">\n <AvatarImage src={child.avatarUrl} alt={child.name} />\n <AvatarFallback>\n {child.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <span className=\"whitespace-nowrap\">{child.name}</span>\n </NestedTableCell>\n </div>\n <div style={{ padding: \"0 var(--spacing-lg)\" }}>\n <NestedTableCell>\n <span className=\"whitespace-nowrap\">{child.email}</span>\n </NestedTableCell>\n </div>\n <div style={{ padding: \"0 var(--spacing-lg)\" }}>\n <NestedTableCell>\n <span className=\"whitespace-nowrap\">{child.phone}</span>\n </NestedTableCell>\n </div>\n <div className=\"flex justify-center\" style={{ padding: \"0 var(--spacing-lg)\" }}>\n <button\n onClick={() => onChildAction?.(\"view\", child)}\n className=\"flex items-center justify-center size-8 rounded-full hover:bg-[var(--canvas-surface)] transition-colors\"\n aria-label={`View ${child.name}`}\n >\n <Eye size={20} style={{ color: \"var(--canvas-text-muted)\" }} />\n </button>\n </div>\n </div>\n ))}\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Nested Data Table Block\n * \n * An expandable data table with parent rows that reveal nested child tables.\n * Ideal for displaying hierarchical data like locations with employees,\n * departments with team members, or categories with items.\n * \n * @example\n * ```tsx\n * <NestedDataTable\n * title=\"FTEs & Contractors by Location\"\n * data={locationData}\n * onAddNew={() => console.log(\"Add new\")}\n * />\n * ```\n */\nexport function NestedDataTable({\n title = \"FTEs & Contractors by Location\",\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 onChildAction,\n className,\n}: NestedDataTableProps) {\n const [sortValue, setSortValue] = useState<string>(\"\");\n const [filterValue, setFilterValue] = useState<string>(\"\");\n const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set([\"1\"])); // First row expanded by default\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 const toggleRow = (rowId: string) => {\n setExpandedRows(prev => {\n const next = new Set(prev);\n if (next.has(rowId)) {\n next.delete(rowId);\n } else {\n next.add(rowId);\n }\n return next;\n });\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 */}\n <div className=\"w-full overflow-x-auto\">\n <div className=\"min-w-[800px]\">\n {/* Table Header */}\n <div\n className=\"grid items-center\"\n style={{ \n borderBottom: \"1px solid var(--canvas-border)\",\n gridTemplateColumns: \"minmax(220px, 1.5fr) minmax(80px, 1fr) minmax(100px, 1fr) minmax(160px, 1.2fr) 40px\",\n }}\n >\n <div style={{ paddingLeft: \"var(--spacing-6xl)\" }}>\n <TableHeaderCell>Location</TableHeaderCell>\n </div>\n <div>\n <TableHeaderCell>FTEs</TableHeaderCell>\n </div>\n <div>\n <TableHeaderCell>Contractors</TableHeaderCell>\n </div>\n <div>\n <TableHeaderCell>HR Contact</TableHeaderCell>\n </div>\n <div>\n <TableHeaderCell>&nbsp;</TableHeaderCell>\n </div>\n </div>\n\n {/* Table Rows */}\n {data.map((row, index) => {\n const isExpanded = expandedRows.has(row.id);\n const hasChildren = row.children && row.children.length > 0;\n\n return (\n <div\n key={row.id}\n className=\"flex flex-col\"\n style={{\n borderBottom: index < data.length - 1 ? \"1px solid var(--canvas-border)\" : \"none\",\n paddingBottom: isExpanded ? \"var(--spacing-xl)\" : \"0\",\n }}\n >\n {/* Parent Row */}\n <div\n className=\"grid items-center\"\n style={{\n padding: \"var(--spacing-md) 0\",\n minHeight: \"64px\",\n gridTemplateColumns: \"minmax(220px, 1.5fr) minmax(80px, 1fr) minmax(100px, 1fr) minmax(160px, 1.2fr) 40px\",\n }}\n >\n {/* Location with Expand Button */}\n <div className=\"flex items-center\" style={{ gap: \"var(--spacing-xl)\" }}>\n <ExpandButton\n expanded={isExpanded}\n onClick={() => toggleRow(row.id)}\n disabled={!hasChildren}\n />\n <TableCell>\n <span className=\"whitespace-nowrap\">{row.location}</span>\n </TableCell>\n </div>\n\n {/* FTEs */}\n <div>\n <TableCell>\n <span className=\"whitespace-nowrap\">{row.ftes}</span>\n </TableCell>\n </div>\n\n {/* Contractors */}\n <div>\n <TableCell>\n <span className=\"whitespace-nowrap\">{row.contractors}</span>\n </TableCell>\n </div>\n\n {/* HR Contact */}\n <div>\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-xxs, 2px)\" }}>\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: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n whiteSpace: \"nowrap\",\n }}\n >\n {row.hrContact.name}\n </span>\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: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n whiteSpace: \"nowrap\",\n }}\n >\n {row.hrContact.phone}\n </span>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"flex justify-center\">\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: \"delete\", label: \"Delete\", variant: \"destructive\", onClick: () => onRowAction?.(\"delete\", row) },\n ]}\n />\n </div>\n </div>\n\n {/* Nested Table */}\n {isExpanded && hasChildren && (\n <div\n style={{\n paddingLeft: \"var(--spacing-6xl)\",\n paddingRight: \"var(--spacing-lg)\",\n }}\n >\n <NestedTable\n children={row.children}\n onChildAction={(action, child) => onChildAction?.(action, child, row)}\n />\n </div>\n )}\n </div>\n );\n })}\n </div>\n </div>\n </div>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "registryDependencies": [
16
+ "utils",
17
+ "button",
18
+ "select",
19
+ "avatar",
20
+ "menufocus-template"
21
+ ]
22
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "office-locations",
3
+ "type": "registry:block",
4
+ "description": "Grid of office location cards with addresses.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/marketing/office-locations.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { Typography } from \"../../ui/typography\";\n\ninterface Office {\n city: string;\n address: string[];\n}\n\ninterface OfficeLocationsProps {\n subtitle?: string;\n title?: string;\n description?: string;\n offices?: Office[];\n}\n\nconst defaultOffices: Office[] = [\n {\n city: \"San Francisco\",\n address: [\n \"972 Mission Street Knotel Fl 3\",\n \"San Francisco, CA, 94103\",\n \"United States\",\n ],\n },\n {\n city: \"Vancouver\",\n address: [\"1007 Hampton Orchard Rd 23\", \"Vancouver, 1607\", \"Canada\"],\n },\n {\n city: \"Sydney\",\n address: [\"304 Hampton Orchard Rd 23\", \"Sydney, NSW, 1607\", \"Australia\"],\n },\n];\n\nexport function OfficeLocations({\n subtitle = \"LOCATIONS\",\n title = \"Our offices\",\n description = \"We have global offices—reach out to us today!\",\n offices = defaultOffices,\n}: OfficeLocationsProps) {\n return (\n <section\n className=\"w-full px-6 md:px-20 py-16 md:py-24\"\n style={{ backgroundColor: \"var(--canvas-background)\" }}\n >\n <div className=\"max-w-[1240px] mx-auto flex flex-col gap-10 md:gap-12\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 md:gap-6\">\n <div className=\"flex flex-col gap-3\">\n <Typography variant=\"body-xs\" as=\"span\" color=\"muted\" className=\"uppercase tracking-wide\">\n {subtitle}\n </Typography>\n <Typography variant=\"h3\" as=\"h2\">\n {title}\n </Typography>\n </div>\n <Typography variant=\"body-l\" color=\"muted\">\n {description}\n </Typography>\n </div>\n\n {/* Office Cards */}\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-8\">\n {offices.map((office, index) => (\n <div\n key={index}\n className=\"flex flex-col gap-4 p-6 md:p-8 rounded-lg\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n }}\n >\n <Typography variant=\"h6\" as=\"h3\">\n {office.city}\n </Typography>\n <div className=\"flex flex-col\">\n {office.address.map((line, lineIndex) => (\n <Typography key={lineIndex} variant=\"body-l\" as=\"span\" color=\"muted\">\n {line}\n </Typography>\n ))}\n </div>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [],
13
+ "registryDependencies": []
14
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "page-header-section",
3
+ "type": "registry:block",
4
+ "description": "Page title, description, and optional line tabs. Used below banners.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/page-header-section.tsx",
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 \n className=\"text-4xl\"\n style={{\n fontFamily: \"var(--typo-page-title-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-page-title-size)\",\n fontWeight: \"var(--typo-page-title-weight)\",\n letterSpacing: \"var(--typo-page-title-spacing)\",\n lineHeight: \"var(--typo-page-title-line-height)\",\n color: \"var(--typo-page-title-color)\",\n }}\n >\n {title}\n </h2>\n {/* Description - Uses typography variables with muted color */}\n <p \n style={{\n fontFamily: \"var(--typo-page-desc-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-page-desc-size)\",\n fontWeight: \"var(--typo-page-desc-weight)\",\n letterSpacing: \"var(--typo-page-desc-spacing)\",\n lineHeight: \"var(--typo-page-desc-line-height)\",\n color: \"var(--typo-page-desc-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
+ }
11
+ ],
12
+ "dependencies": [],
13
+ "registryDependencies": [
14
+ "utils",
15
+ "line-tabs"
16
+ ]
17
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "page-previews",
3
+ "type": "registry:block",
4
+ "description": "",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/page-previews.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { Header } from \"../layout\";\nimport {\n CenteredHero,\n MetricsSection,\n CoreValuesGrid,\n TeamCardsGrid,\n CtaBanner,\n FooterNavbar,\n HeroSection,\n TestimonialCarousel,\n SocialProof,\n FeatureWithImage,\n} from \"./marketing\";\nimport { PricingCards } from \"./pricing\";\nimport { StandardDataTable } from \"./standard-data-table\";\nimport { DashboardShell, IconSidebarShell, DoubleSidebarShell } from \"../layout\";\nimport { MessengerSidebar } from \"./messenger-sidebar\";\nimport { ChatMessageList } from \"./chat-message\";\nimport { LoginBrandingPanel } from \"./login-branding-panel\";\nimport { ProfileCard } from \"./profile-card\";\nimport { StepTracker, defaultSteps } from \"./step-tracker\";\nimport { VerticalStepTracker } from \"./vertical-step-tracker\";\nimport { SearchBar } from \"./search-bar\";\nimport { VideoChatControls } from \"./video-chat-controls\";\nimport { VideoContentSection } from \"./video-content-section\";\nimport { Input } from \"../ui/input\";\nimport { Button } from \"../ui/button\";\n\n/**\n * Scaled Preview Wrapper\n * Renders content at a scaled-down size for canvas thumbnails\n */\ninterface ScaledPreviewProps {\n children: React.ReactNode;\n width?: number;\n height?: number;\n scale?: number;\n}\n\nexport function ScaledPreview({ \n children, \n width = 400, \n height = 280,\n scale = 0.25 \n}: ScaledPreviewProps) {\n const innerWidth = width / scale;\n const innerHeight = height / scale;\n \n return (\n <div \n className=\"overflow-hidden rounded-lg border border-[var(--canvas-border)] shadow-md bg-white\"\n style={{ width, height }}\n >\n <div\n style={{\n width: innerWidth,\n height: innerHeight,\n transform: `scale(${scale})`,\n transformOrigin: \"top left\",\n }}\n >\n {children}\n </div>\n </div>\n );\n}\n\n// Sample navigation for previews\nconst previewNav = [\n { id: \"home\", label: \"Home\", href: \"#\" },\n { id: \"about\", label: \"About\", href: \"#\" },\n];\n\nconst sampleSidebarSections = [\n {\n items: [\n { id: \"dashboard\", label: \"Dashboard\", icon: \"home\" as const, href: \"#\" },\n { id: \"analytics\", label: \"Analytics\", icon: \"chart\" as const, href: \"#\" },\n { id: \"settings\", label: \"Settings\", icon: \"settings\" as const, href: \"#\" },\n ],\n },\n];\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.\",\n tags: [{ label: \"Design\" }],\n};\n\n// =====================\n// PAGE TEMPLATE PREVIEWS\n// =====================\n\nexport function PageAboutPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <CenteredHero title=\"About Us\" subtitle=\"Our story\" />\n <MetricsSection />\n <CoreValuesGrid variant=\"light\" />\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageAccountPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DashboardShell navigation={sampleSidebarSections}>\n <div className=\"p-6 space-y-4\">\n <h1 className=\"text-2xl font-bold\">Account Settings</h1>\n <div className=\"bg-white rounded-lg border p-4 space-y-4\">\n <div className=\"flex items-center gap-4\">\n <div className=\"w-16 h-16 rounded-full bg-gray-200\" />\n <div>\n <p className=\"font-semibold\">Jeff Connor</p>\n <p className=\"text-sm text-gray-500\">jeff@example.com</p>\n </div>\n </div>\n </div>\n </div>\n </DashboardShell>\n </ScaledPreview>\n );\n}\n\nexport function PageAdminPortalPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DashboardShell navigation={sampleSidebarSections}>\n <div className=\"p-6\">\n <StandardDataTable title=\"Team Members\" />\n </div>\n </DashboardShell>\n </ScaledPreview>\n );\n}\n\nexport function PageCenteredProfilePreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)] flex flex-col\">\n <Header showDesktopLogo navItems={previewNav} />\n <div className=\"flex-1 flex items-center justify-center p-8\">\n <div className=\"w-[400px]\">\n <ProfileCard {...sampleProfileData} />\n </div>\n </div>\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageDoubleSidebarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DoubleSidebarShell>\n <div className=\"p-6\">\n <h1 className=\"text-xl font-bold mb-4\">Dashboard</h1>\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"bg-white rounded-lg border p-4 h-32\" />\n <div className=\"bg-white rounded-lg border p-4 h-32\" />\n </div>\n </div>\n </DoubleSidebarShell>\n </ScaledPreview>\n );\n}\n\nexport function PageIconSidebarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <IconSidebarShell>\n <div className=\"p-6\">\n <h1 className=\"text-xl font-bold mb-4\">Dashboard</h1>\n <div className=\"grid grid-cols-3 gap-4\">\n <div className=\"bg-white rounded-lg border p-4 h-24\" />\n <div className=\"bg-white rounded-lg border p-4 h-24\" />\n <div className=\"bg-white rounded-lg border p-4 h-24\" />\n </div>\n </div>\n </IconSidebarShell>\n </ScaledPreview>\n );\n}\n\nexport function PageLoginPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"flex h-screen\">\n <div className=\"flex-1 flex items-center justify-center p-8\">\n <div className=\"w-[360px] space-y-4 bg-white rounded-lg border p-6\">\n <h2 className=\"text-xl font-bold\">Log in</h2>\n <Input placeholder=\"Email\" />\n <Input placeholder=\"Password\" type=\"password\" />\n <Button className=\"w-full\">Log in</Button>\n </div>\n </div>\n <LoginBrandingPanel className=\"hidden lg:flex w-1/2\" />\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageMenuSectionsPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DashboardShell navigation={sampleSidebarSections}>\n <div className=\"p-6 space-y-6\">\n <div className=\"bg-white rounded-lg border p-4\">\n <h2 className=\"font-semibold mb-2\">Section 1</h2>\n <div className=\"space-y-2\">\n <div className=\"h-8 bg-gray-100 rounded\" />\n <div className=\"h-8 bg-gray-100 rounded\" />\n </div>\n </div>\n </div>\n </DashboardShell>\n </ScaledPreview>\n );\n}\n\nexport function PageMessengerPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"flex h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} className=\"absolute top-0 left-0 right-0\" />\n <div className=\"flex flex-1 pt-16\">\n <MessengerSidebar className=\"w-[320px]\" />\n <div className=\"flex-1 flex flex-col\">\n <ChatMessageList />\n </div>\n </div>\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageMobileMenuPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"h-screen bg-[var(--canvas-background)] flex flex-col\">\n <Header showDesktopLogo navItems={previewNav} />\n <div className=\"flex-1 p-4\">\n <div className=\"bg-white rounded-lg border p-4 space-y-2\">\n <div className=\"h-10 bg-gray-100 rounded flex items-center px-3\">Menu Item 1</div>\n <div className=\"h-10 bg-gray-100 rounded flex items-center px-3\">Menu Item 2</div>\n <div className=\"h-10 bg-gray-100 rounded flex items-center px-3\">Menu Item 3</div>\n </div>\n </div>\n <div className=\"h-16 border-t bg-white flex items-center justify-around\">\n <div className=\"w-8 h-8 bg-gray-200 rounded\" />\n <div className=\"w-8 h-8 bg-gray-200 rounded\" />\n <div className=\"w-8 h-8 bg-gray-200 rounded\" />\n </div>\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageMultistepProgressbarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)] flex flex-col\">\n <Header showDesktopLogo navItems={previewNav} />\n <div className=\"flex-1 flex flex-col items-center justify-center p-8\">\n <div className=\"w-[600px]\">\n <StepTracker steps={defaultSteps} currentStep={1} />\n <div className=\"mt-8 bg-white rounded-lg border p-6\">\n <h2 className=\"text-xl font-bold mb-4\">Step 1: Basic Info</h2>\n <Input placeholder=\"Enter details...\" />\n </div>\n </div>\n </div>\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageMultistepSidebarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"flex h-screen\">\n <div className=\"w-64 border-r bg-white p-6\">\n <VerticalStepTracker steps={defaultSteps} currentStep={1} />\n </div>\n <div className=\"flex-1 p-8\">\n <h2 className=\"text-xl font-bold mb-4\">Step 1</h2>\n <div className=\"bg-white rounded-lg border p-6\">\n <Input placeholder=\"Enter details...\" />\n </div>\n </div>\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PagePricingPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <div className=\"py-12\">\n <PricingCards />\n </div>\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageProductHomepagePreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <HeroSection title=\"Build amazing products\" subtitle=\"The platform for modern teams\" />\n <SocialProof />\n <FeatureWithImage />\n <TestimonialCarousel />\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageResetPasswordPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"flex h-screen\">\n <div className=\"flex-1 flex items-center justify-center p-8\">\n <div className=\"w-[360px] space-y-4 bg-white rounded-lg border p-6\">\n <h2 className=\"text-xl font-bold\">Reset Password</h2>\n <p className=\"text-sm text-gray-500\">Enter your new password</p>\n <Input placeholder=\"New password\" type=\"password\" />\n <Input placeholder=\"Confirm password\" type=\"password\" />\n <Button className=\"w-full\">Reset Password</Button>\n </div>\n </div>\n <LoginBrandingPanel className=\"hidden lg:flex w-1/2\" />\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageSearchBarPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <div className=\"p-8\">\n <SearchBar placeholder=\"Search...\" className=\"max-w-2xl mx-auto\" />\n <div className=\"mt-8 grid grid-cols-3 gap-4 max-w-4xl mx-auto\">\n <div className=\"bg-white rounded-lg border p-4 h-32\" />\n <div className=\"bg-white rounded-lg border p-4 h-32\" />\n <div className=\"bg-white rounded-lg border p-4 h-32\" />\n </div>\n </div>\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageSidebarProfilePreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DashboardShell navigation={sampleSidebarSections}>\n <div className=\"p-6 flex gap-6\">\n <div className=\"w-[300px]\">\n <ProfileCard {...sampleProfileData} />\n </div>\n <div className=\"flex-1 space-y-4\">\n <div className=\"bg-white rounded-lg border p-4 h-40\" />\n <div className=\"bg-white rounded-lg border p-4 h-40\" />\n </div>\n </div>\n </DashboardShell>\n </ScaledPreview>\n );\n}\n\nexport function PageStandardPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <div className=\"max-w-4xl mx-auto p-8\">\n <h1 className=\"text-3xl font-bold mb-4\">Page Title</h1>\n <p className=\"text-gray-600 mb-8\">Page description text goes here.</p>\n <div className=\"bg-white rounded-lg border p-6\">\n <div className=\"space-y-4\">\n <div className=\"h-8 bg-gray-100 rounded w-3/4\" />\n <div className=\"h-8 bg-gray-100 rounded w-1/2\" />\n <div className=\"h-8 bg-gray-100 rounded w-2/3\" />\n </div>\n </div>\n </div>\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageStandardMultistepPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <div className=\"max-w-2xl mx-auto p-8\">\n <StepTracker steps={defaultSteps} currentStep={2} />\n <div className=\"mt-8 bg-white rounded-lg border p-6 space-y-4\">\n <Input placeholder=\"Field 1\" />\n <Input placeholder=\"Field 2\" />\n <div className=\"flex gap-4\">\n <Button variant=\"outline\">Back</Button>\n <Button>Continue</Button>\n </div>\n </div>\n </div>\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageStandardSearchPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <DashboardShell navigation={sampleSidebarSections}>\n <div className=\"p-6\">\n <SearchBar placeholder=\"Search...\" className=\"mb-6\" />\n <StandardDataTable title=\"Search Results\" />\n </div>\n </DashboardShell>\n </ScaledPreview>\n );\n}\n\nexport function PageVerticalMultistepPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <div className=\"flex max-w-4xl mx-auto p-8 gap-8\">\n <div className=\"w-48\">\n <VerticalStepTracker steps={defaultSteps} currentStep={1} />\n </div>\n <div className=\"flex-1 bg-white rounded-lg border p-6\">\n <h2 className=\"text-xl font-bold mb-4\">Step Content</h2>\n <Input placeholder=\"Enter details...\" />\n </div>\n </div>\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageVideoChatPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"h-screen bg-gray-900 flex flex-col\">\n <div className=\"flex-1 grid grid-cols-2 gap-4 p-4\">\n <div className=\"bg-gray-800 rounded-lg flex items-center justify-center\">\n <div className=\"w-24 h-24 bg-gray-700 rounded-full\" />\n </div>\n <div className=\"bg-gray-800 rounded-lg flex items-center justify-center\">\n <div className=\"w-24 h-24 bg-gray-700 rounded-full\" />\n </div>\n </div>\n <div className=\"p-4\">\n <VideoChatControls />\n </div>\n </div>\n </ScaledPreview>\n );\n}\n\nexport function PageVideoListPreview() {\n return (\n <ScaledPreview width={400} height={280} scale={0.22}>\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n <Header showDesktopLogo navItems={previewNav} />\n <div className=\"flex p-6 gap-6\">\n <div className=\"flex-1\">\n <VideoContentSection />\n </div>\n <div className=\"w-80 bg-white rounded-lg border p-4 space-y-2\">\n <div className=\"h-16 bg-gray-100 rounded\" />\n <div className=\"h-16 bg-gray-100 rounded\" />\n <div className=\"h-16 bg-gray-100 rounded\" />\n </div>\n </div>\n </div>\n </ScaledPreview>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [],
13
+ "registryDependencies": [
14
+ "marketing",
15
+ "pricing",
16
+ "standard-data-table",
17
+ "messenger-sidebar",
18
+ "chat-message",
19
+ "login-branding-panel",
20
+ "profile-card",
21
+ "step-tracker",
22
+ "vertical-step-tracker",
23
+ "search-bar",
24
+ "video-chat-controls",
25
+ "video-content-section",
26
+ "input",
27
+ "button"
28
+ ]
29
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "pagination",
3
+ "type": "registry:block",
4
+ "description": "Responsive pagination with results summary, items per page dropdown, and page navigation. Shows 'Viewing X-Y of Z results' on desktop, compact navigation on mobile.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/pagination.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { useIsMobile } from \"../../hooks/use-mobile\";\nimport {\n ChevronLeft,\n ChevronRight,\n ChevronsLeft,\n ChevronsRight,\n MoreHorizontal,\n} from \"lucide-react\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PaginationProps {\n /** Current active page (1-indexed) */\n currentPage?: number;\n /** Total number of pages */\n totalPages?: number;\n /** Total number of items (for results text) */\n totalItems?: number;\n /** Items displayed per page */\n itemsPerPage?: number;\n /** Available items per page options */\n itemsPerPageOptions?: number[];\n /** Callback when page changes */\n onPageChange?: (page: number) => void;\n /** Callback when items per page changes */\n onItemsPerPageChange?: (itemsPerPage: number) => void;\n /** Show \"Viewing X-Y of Z results\" text (desktop only) */\n showResultsText?: boolean;\n /** Show \"Show per page\" dropdown (desktop only) */\n showItemsPerPage?: boolean;\n /** Maximum number of visible page buttons on desktop */\n maxVisiblePages?: number;\n /** Maximum number of visible page buttons on mobile (default: 3) */\n mobileMaxVisiblePages?: number;\n /** Force compact mode (fewer visible pages) regardless of viewport */\n compact?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface PaginationButtonProps {\n page: number;\n isSelected?: boolean;\n onClick?: () => void;\n className?: string;\n}\n\n/**\n * Individual page number button\n */\nfunction PaginationButton({\n page,\n isSelected = false,\n onClick,\n className,\n}: PaginationButtonProps) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className={cn(\n \"flex items-center justify-center shrink-0 transition-colors\",\n \"font-semibold text-[14px] leading-[20px]\",\n \"size-[32px] rounded-[var(--radius-xs)]\",\n \"border bg-[var(--canvas-background)]\",\n isSelected\n ? \"border-[var(--canvas-primary)] text-[var(--canvas-primary)]\"\n : \"border-[var(--canvas-border)] text-[var(--canvas-text)] hover:bg-[var(--canvas-surface)]\",\n className\n )}\n aria-current={isSelected ? \"page\" : undefined}\n >\n {page}\n </button>\n );\n}\n\ninterface NavigationButtonProps {\n type: \"first\" | \"prev\" | \"next\" | \"last\";\n disabled?: boolean;\n onClick?: () => void;\n className?: string;\n}\n\n/**\n * Navigation button (First, Prev, Next, Last)\n */\nfunction NavigationButton({\n type,\n disabled = false,\n onClick,\n className,\n}: NavigationButtonProps) {\n const Icon = {\n first: ChevronsLeft,\n prev: ChevronLeft,\n next: ChevronRight,\n last: ChevronsRight,\n }[type];\n\n const label = {\n first: null,\n prev: \"Prev\",\n next: \"Next\",\n last: null,\n }[type];\n\n const isIconOnly = type === \"first\" || type === \"last\";\n\n return (\n <button\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n className={cn(\n \"flex items-center justify-center shrink-0 transition-colors\",\n \"h-[32px] rounded-[var(--radius-xs)]\",\n \"border bg-[var(--canvas-background)] border-[var(--canvas-border)]\",\n isIconOnly ? \"w-[32px]\" : \"px-[var(--spacing-md)] gap-[var(--spacing-xs)]\",\n disabled\n ? \"text-[var(--canvas-text-placeholder)] cursor-not-allowed\"\n : \"text-[var(--canvas-text)] hover:bg-[var(--canvas-surface)]\",\n className\n )}\n aria-label={type}\n >\n {(type === \"first\" || type === \"prev\") && (\n <Icon className=\"size-[20px]\" />\n )}\n {label && (\n <span className=\"font-semibold text-[14px] leading-[20px]\">{label}</span>\n )}\n {(type === \"next\" || type === \"last\") && (\n <Icon className=\"size-[20px]\" />\n )}\n </button>\n );\n}\n\n/**\n * Ellipsis indicator for truncated pages\n */\nfunction PaginationEllipsis({ className }: { className?: string }) {\n return (\n <div\n className={cn(\n \"flex items-center justify-center\",\n \"w-[24px] h-[32px] rounded-[var(--radius-xs)]\",\n className\n )}\n aria-hidden=\"true\"\n >\n <MoreHorizontal className=\"size-[20px] text-[var(--canvas-text-muted)]\" />\n </div>\n );\n}\n\n// ============================================\n// Helper Functions\n// ============================================\n\n/**\n * Generate array of page numbers to display with ellipsis\n */\nfunction generatePageNumbers(\n currentPage: number,\n totalPages: number,\n maxVisible: number\n): (number | \"ellipsis\")[] {\n if (totalPages <= maxVisible) {\n return Array.from({ length: totalPages }, (_, i) => i + 1);\n }\n\n const pages: (number | \"ellipsis\")[] = [];\n const halfVisible = Math.floor((maxVisible - 2) / 2);\n\n // Always show first page\n pages.push(1);\n\n // Calculate start and end of visible range\n let start = Math.max(2, currentPage - halfVisible);\n let end = Math.min(totalPages - 1, currentPage + halfVisible);\n\n // Adjust if we're near the start\n if (currentPage <= halfVisible + 2) {\n end = Math.min(totalPages - 1, maxVisible - 1);\n start = 2;\n }\n\n // Adjust if we're near the end\n if (currentPage >= totalPages - halfVisible - 1) {\n start = Math.max(2, totalPages - maxVisible + 2);\n end = totalPages - 1;\n }\n\n // Add ellipsis before middle section if needed\n if (start > 2) {\n pages.push(\"ellipsis\");\n }\n\n // Add middle pages\n for (let i = start; i <= end; i++) {\n pages.push(i);\n }\n\n // Add ellipsis after middle section if needed\n if (end < totalPages - 1) {\n pages.push(\"ellipsis\");\n }\n\n // Always show last page\n if (totalPages > 1) {\n pages.push(totalPages);\n }\n\n return pages;\n}\n\n/**\n * Format number with commas (e.g., 2500 -> 2,500)\n */\nfunction formatNumber(num: number): string {\n return num.toLocaleString();\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Pagination Component\n *\n * A responsive pagination component with results text, items per page dropdown,\n * and page navigation. Automatically adjusts layout for mobile screens.\n *\n * @example\n * ```tsx\n * // Basic usage\n * <Pagination\n * currentPage={1}\n * totalPages={10}\n * onPageChange={(page) => setPage(page)}\n * />\n *\n * // With all features\n * <Pagination\n * currentPage={1}\n * totalPages={50}\n * totalItems={2500}\n * itemsPerPage={50}\n * itemsPerPageOptions={[30, 50, 100]}\n * showResultsText\n * showItemsPerPage\n * onPageChange={(page) => setPage(page)}\n * onItemsPerPageChange={(perPage) => setPerPage(perPage)}\n * />\n * ```\n */\nexport function Pagination({\n currentPage = 1,\n totalPages = 10,\n totalItems = 2500,\n itemsPerPage = 50,\n itemsPerPageOptions = [30, 50, 100],\n onPageChange,\n onItemsPerPageChange,\n showResultsText = true,\n showItemsPerPage = true,\n maxVisiblePages = 5,\n mobileMaxVisiblePages = 3,\n compact = false,\n className,\n}: PaginationProps) {\n const isMobile = useIsMobile();\n \n // Use fewer visible pages on mobile or when compact mode is forced\n const effectiveMaxVisiblePages = (isMobile || compact) ? mobileMaxVisiblePages : maxVisiblePages;\n \n // Calculate visible page range\n const pageNumbers = useMemo(\n () => generatePageNumbers(currentPage, totalPages, effectiveMaxVisiblePages),\n [currentPage, totalPages, effectiveMaxVisiblePages]\n );\n\n // Calculate results range\n const startItem = (currentPage - 1) * itemsPerPage + 1;\n const endItem = Math.min(currentPage * itemsPerPage, totalItems);\n\n // Navigation handlers\n const goToPage = (page: number) => {\n if (page >= 1 && page <= totalPages && page !== currentPage) {\n onPageChange?.(page);\n }\n };\n\n const goToFirst = () => goToPage(1);\n const goToPrev = () => goToPage(currentPage - 1);\n const goToNext = () => goToPage(currentPage + 1);\n const goToLast = () => goToPage(totalPages);\n\n const isFirstPage = currentPage === 1;\n const isLastPage = currentPage === totalPages;\n\n return (\n <div\n className={cn(\n \"flex items-center justify-between\",\n \"h-[56px] py-[var(--spacing-lg)]\",\n \"w-full\",\n className\n )}\n >\n {/* Left section: Results text & Items per page (desktop only) */}\n <div className=\"hidden md:flex items-center gap-[var(--spacing-2xl)]\">\n {/* Results text */}\n {showResultsText && (\n <span\n className=\"text-[14px] leading-[20px] text-[var(--canvas-text)] whitespace-nowrap\"\n style={{ fontFamily: \"var(--typo-global-font)\" }}\n >\n Viewing {formatNumber(startItem)}–{formatNumber(endItem)} of{\" \"}\n {formatNumber(totalItems)} results\n </span>\n )}\n\n {/* Items per page dropdown */}\n {showItemsPerPage && (\n <div className=\"flex items-center gap-[var(--spacing-sm)]\">\n <span\n className=\"text-[14px] leading-[20px] text-[var(--canvas-text)] whitespace-nowrap\"\n style={{ fontFamily: \"var(--typo-global-font)\" }}\n >\n Show per page\n </span>\n <Select\n value={String(itemsPerPage)}\n onValueChange={(value) => onItemsPerPageChange?.(Number(value))}\n >\n <SelectTrigger\n inputSize=\"sm\"\n className=\"w-[70px]\"\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent position=\"popper\" side=\"bottom\" sideOffset={4}>\n {itemsPerPageOptions.map((option) => (\n <SelectItem key={option} value={String(option)}>\n {option}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n </div>\n\n {/* Right section: Navigation (always visible, centered on mobile) */}\n <div className=\"flex items-center gap-[var(--spacing-sm)] mx-auto md:mx-0\">\n {/* First & Prev buttons */}\n <div className=\"flex items-center gap-[var(--spacing-sm)]\">\n {!compact && (\n <NavigationButton\n type=\"first\"\n disabled={isFirstPage}\n onClick={goToFirst}\n />\n )}\n <NavigationButton\n type=\"prev\"\n disabled={isFirstPage}\n onClick={goToPrev}\n />\n </div>\n\n {/* Page numbers */}\n <div className=\"flex items-center gap-[var(--spacing-sm)]\">\n {pageNumbers.map((item, index) =>\n item === \"ellipsis\" ? (\n <PaginationEllipsis key={`ellipsis-${index}`} />\n ) : (\n <PaginationButton\n key={item}\n page={item}\n isSelected={item === currentPage}\n onClick={() => goToPage(item)}\n />\n )\n )}\n </div>\n\n {/* Next & Last buttons */}\n <div className=\"flex items-center gap-[var(--spacing-sm)]\">\n <NavigationButton\n type=\"next\"\n disabled={isLastPage}\n onClick={goToNext}\n />\n {!compact && (\n <NavigationButton\n type=\"last\"\n disabled={isLastPage}\n onClick={goToLast}\n />\n )}\n </div>\n </div>\n </div>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "registryDependencies": [
16
+ "utils",
17
+ "use-mobile",
18
+ "select"
19
+ ]
20
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "participant-list",
3
+ "type": "registry:block",
4
+ "description": "List of video call participants with avatars and status.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/participant-list.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar\";\n\ninterface Participant {\n id: string;\n name: string;\n role: string;\n avatarUrl?: string;\n}\n\ninterface ParticipantItemProps {\n /** Participant name */\n name: string;\n /** Participant role/title */\n role: string;\n /** Avatar image URL */\n avatarUrl?: string;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Individual participant row with avatar, name, and role\n */\nexport function ParticipantItem({\n name,\n role,\n avatarUrl,\n className,\n}: ParticipantItemProps) {\n // Get initials from name\n const initials = name\n .split(\" \")\n .map((n) => n[0])\n .join(\"\")\n .toUpperCase()\n .slice(0, 2);\n\n return (\n <div className={cn(\"flex items-center gap-3 py-3\", className)}>\n <Avatar className=\"w-10 h-10 shrink-0\">\n <AvatarImage src={avatarUrl} alt={name} />\n <AvatarFallback \n className=\"bg-[var(--canvas-surface)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n color: \"var(--typo-body-s-color)\",\n }}\n >\n {initials}\n </AvatarFallback>\n </Avatar>\n <div className=\"flex-1 min-w-0\">\n <p \n className=\"truncate\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n letterSpacing: \"var(--typo-body-s-spacing)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--typo-body-s-color)\",\n }}\n >\n {name}\n </p>\n <p \n className=\"truncate\"\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 letterSpacing: \"var(--typo-body-s-spacing)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--typo-body-s-color-muted)\",\n }}\n >\n {role}\n </p>\n </div>\n </div>\n );\n}\n\ninterface ParticipantListProps {\n /** Array of participants */\n participants: Participant[];\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Participant List Component\n * \n * Displays a list of video chat participants with avatars.\n * \n * @example\n * ```tsx\n * <ParticipantList\n * participants={[\n * { id: \"1\", name: \"John Doe\", role: \"Developer\", avatarUrl: \"/avatar.jpg\" },\n * ]}\n * />\n * ```\n */\nexport function ParticipantList({\n participants,\n className,\n}: ParticipantListProps) {\n return (\n <div className={cn(\"flex flex-col\", className)}>\n {participants.map((participant) => (\n <ParticipantItem\n key={participant.id}\n name={participant.name}\n role={participant.role}\n avatarUrl={participant.avatarUrl}\n />\n ))}\n </div>\n );\n}\n\n"
10
+ }
11
+ ],
12
+ "dependencies": [],
13
+ "registryDependencies": [
14
+ "utils",
15
+ "avatar"
16
+ ]
17
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "persona-card",
3
+ "type": "registry:block",
4
+ "description": "",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/persona-card.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport type { Persona } from \"../../types/project\";\nimport { Target, AlertCircle, Quote } from \"lucide-react\";\n\ninterface PersonaCardProps {\n persona: Persona;\n className?: string;\n}\n\nexport function PersonaCard({ persona, className }: PersonaCardProps) {\n return (\n <div\n className={cn(\n \"rounded-xl border border-[var(--canvas-border)] bg-[var(--canvas-background)]\",\n \"p-5 hover:shadow-md transition-shadow\",\n className\n )}\n >\n {/* Header */}\n <div className=\"flex items-start gap-3 mb-4\">\n <div className=\"text-3xl\">{persona.avatar || \"👤\"}</div>\n <div>\n <h3 className=\"font-semibold text-[var(--canvas-text)]\">\n {persona.name}\n </h3>\n <p className=\"text-sm text-[var(--canvas-text-muted)]\">\n {persona.role}\n </p>\n </div>\n </div>\n\n {/* Goals */}\n <div className=\"mb-4\">\n <div className=\"flex items-center gap-1.5 text-xs font-medium text-[var(--canvas-text-muted)] mb-2\">\n <Target className=\"size-3\" />\n Goals\n </div>\n <ul className=\"space-y-1\">\n {persona.goals.map((goal, i) => (\n <li\n key={i}\n className=\"text-sm text-[var(--canvas-text)] flex items-start gap-2\"\n >\n <span className=\"text-[var(--canvas-primary)] mt-1.5\">•</span>\n {goal}\n </li>\n ))}\n </ul>\n </div>\n\n {/* Pain Points */}\n <div className=\"mb-4\">\n <div className=\"flex items-center gap-1.5 text-xs font-medium text-[var(--canvas-text-muted)] mb-2\">\n <AlertCircle className=\"size-3\" />\n Pain Points\n </div>\n <ul className=\"space-y-1\">\n {persona.painPoints.map((point, i) => (\n <li\n key={i}\n className=\"text-sm text-[var(--canvas-text)] flex items-start gap-2\"\n >\n <span className=\"text-red-500 mt-1.5\">•</span>\n {point}\n </li>\n ))}\n </ul>\n </div>\n\n {/* Quote */}\n <div className=\"border-t border-[var(--canvas-border)] pt-4 mt-4\">\n <div className=\"flex items-start gap-2\">\n <Quote className=\"size-4 text-[var(--canvas-text-muted)] shrink-0 mt-0.5\" />\n <p className=\"text-sm italic text-[var(--canvas-text-muted)]\">\n \"{persona.quote}\"\n </p>\n </div>\n </div>\n </div>\n );\n}\n\n// ═══════════════════════════════════════════════════════════\n// PERSONA GRID\n// ═══════════════════════════════════════════════════════════\n\ninterface PersonaGridProps {\n personas: Persona[];\n}\n\nexport function PersonaGrid({ personas }: PersonaGridProps) {\n if (personas.length === 0) {\n return null;\n }\n\n return (\n <div className=\"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4\">\n {personas.map((persona) => (\n <PersonaCard key={persona.id} persona={persona} />\n ))}\n </div>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "registryDependencies": [
16
+ "utils"
17
+ ]
18
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "pill-tabs",
3
+ "type": "registry:block",
4
+ "description": "Horizontal pill-style tab navigation.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/pill-tabs.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Typography } from \"../ui/typography\";\nimport {\n Briefcase,\n Star,\n User,\n type LucideIcon,\n} from \"lucide-react\";\n\ntype TabIconType = \"briefcase\" | \"star\" | \"user\";\n\ninterface PillTab {\n id: string;\n label: string;\n icon?: TabIconType;\n}\n\nconst tabIcons: Record<TabIconType, LucideIcon> = {\n briefcase: Briefcase,\n star: Star,\n user: User,\n};\n\nexport interface PillTabsProps {\n tabs: PillTab[];\n activeTab?: string;\n onTabChange?: (tabId: string) => void;\n className?: string;\n}\n\n/**\n * Canvas Design System - Pill Tabs Component\n *\n * A horizontal tab navigation with pill-shaped tabs.\n * Each tab can have an optional icon.\n */\nexport function PillTabs({\n tabs,\n activeTab,\n onTabChange,\n className,\n}: PillTabsProps) {\n return (\n <div className={cn(\"flex gap-[var(--spacing-xl)]\", className)}>\n {tabs.map((tab) => {\n const isActive = activeTab === tab.id;\n const IconComponent = tab.icon ? tabIcons[tab.icon] : null;\n\n return (\n <button\n key={tab.id}\n onClick={() => onTabChange?.(tab.id)}\n className={cn(\n \"flex items-center gap-[var(--spacing-md)] h-10 px-[var(--spacing-xl)] rounded-full transition-colors\",\n \"bg-[var(--canvas-border)]\",\n isActive && \"bg-[var(--canvas-surface-brand)]\"\n )}\n >\n {IconComponent && (\n <IconComponent\n className={cn(\n \"size-4\",\n isActive\n ? \"text-[var(--canvas-text)]\"\n : \"text-[var(--canvas-text)]\"\n )}\n />\n )}\n <Typography\n variant=\"body-s\"\n className={cn(\n isActive\n ? \"text-[var(--canvas-text)]\"\n : \"text-[var(--canvas-text)]\"\n )}\n >\n {tab.label}\n </Typography>\n </button>\n );\n })}\n </div>\n );\n}\n\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "registryDependencies": [
16
+ "utils",
17
+ "typography"
18
+ ]
19
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "pricing-cards",
3
+ "type": "registry:block",
4
+ "description": "Three-tier pricing cards with monthly/annual toggle, features list, and CTA buttons.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/pricing/pricing-cards.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Check, Info, Sparkle } from \"@phosphor-icons/react\";\nimport { Button } from \"../../ui/button\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface PlanFeature {\n text: string;\n hasInfo?: boolean;\n}\n\ninterface PricingPlan {\n name: string;\n price: number;\n period: string;\n description: string;\n features: PlanFeature[];\n isPopular?: boolean;\n hasAI?: boolean;\n}\n\nconst plans: PricingPlan[] = [\n {\n name: \"Starter\",\n price: 25,\n period: \"/month\",\n description: \"Best for hobbyists or individuals\",\n features: [\n { text: \"1 TB storage\", hasInfo: true },\n { text: \"Up to 5 apps and integrations\" },\n { text: \"2 collaborators\" },\n ],\n },\n {\n name: \"Deluxe\",\n price: 70,\n period: \"/month\",\n description: \"Best for small teams\",\n isPopular: true,\n hasAI: true,\n features: [\n { text: \"50 TB storage\" },\n { text: \"Up to 20 apps and integrations\" },\n { text: \"5 collaborators\" },\n { text: \"Dedicated support\" },\n { text: \"Unlimited workspace\" },\n { text: \"Unlimited access\" },\n ],\n },\n {\n name: \"Professional\",\n price: 120,\n period: \"/month\",\n description: \"Best for large teams or enterprises\",\n hasAI: true,\n features: [\n { text: \"Unlimited storage\" },\n { text: \"Up to 50 apps and integrations\" },\n { text: \"12 collaborators\" },\n { text: \"Dedicated support\" },\n { text: \"Unlimited workspace\" },\n { text: \"Unlimited access\" },\n { text: \"Unlimited version history\" },\n ],\n },\n];\n\nexport function PricingCards() {\n const [isAnnual, setIsAnnual] = useState(false);\n\n return (\n <section\n className=\"w-full px-4 md:px-8 lg:px-20 py-16 md:py-24\"\n style={{ backgroundColor: \"var(--canvas-background)\" }}\n >\n <div className=\"w-full max-w-[1240px] mx-auto flex flex-col items-center gap-12\">\n {/* Header */}\n <div className=\"flex flex-col items-center gap-3 text-center\">\n <Typography variant=\"body-s\" as=\"p\" color=\"muted\">\n PRICING\n </Typography>\n <Typography variant=\"h3\" as=\"h1\">\n Choose the best plan for your team\n </Typography>\n <Typography variant=\"body-l\" color=\"muted\">\n Pay by the month or the year, and cancel at any time\n </Typography>\n\n {/* Billing Toggle */}\n <div className=\"flex items-center gap-2.5 mt-2\">\n <button\n onClick={() => setIsAnnual(!isAnnual)}\n className=\"relative w-20 h-11 rounded-full transition-colors\"\n style={{\n backgroundColor: isAnnual\n ? \"var(--canvas-primary)\"\n : \"var(--canvas-border)\",\n }}\n >\n <div\n className=\"absolute top-1 w-9 h-9 bg-white rounded-full shadow transition-all\"\n style={{\n left: isAnnual ? \"calc(100% - 40px)\" : \"4px\",\n }}\n />\n </button>\n <Typography variant=\"body-m\" as=\"span\">\n Billed annually\n </Typography>\n <span\n className=\"px-2 py-0.5 rounded\"\n style={{\n backgroundColor: \"var(--canvas-surface-brand)\",\n fontFamily: \"var(--typo-global-font)\",\n fontSize: \"12px\",\n fontWeight: 600,\n color: \"var(--canvas-primary)\",\n }}\n >\n SAVE 20%\n </span>\n </div>\n </div>\n\n {/* Pricing Cards */}\n <div className=\"w-full flex flex-col gap-8\">\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-8\">\n {plans.map((plan) => (\n <div\n key={plan.name}\n className=\"flex flex-col rounded-xl overflow-hidden\"\n style={{\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n }}\n >\n {/* Popular Badge */}\n {plan.isPopular ? (\n <div\n className=\"w-full py-3 text-center\"\n style={{\n backgroundColor: \"var(--canvas-text)\",\n }}\n >\n <Typography variant=\"body-xs\" as=\"span\" style={{ color: \"white\", fontWeight: 600 }}>\n MOST POPULAR\n </Typography>\n </div>\n ) : (\n <div className=\"h-11\" />\n )}\n\n {/* Card Content */}\n <div\n className=\"flex-1 flex flex-col gap-6 p-8\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: plan.isPopular\n ? \"0 0 var(--radius-md) var(--radius-md)\"\n : \"var(--radius-md)\",\n }}\n >\n {/* Plan Info */}\n <div className=\"flex flex-col gap-3\">\n <Typography variant=\"h5\" as=\"h3\">\n {plan.name}\n </Typography>\n <div className=\"flex items-end gap-1\">\n <Typography variant=\"h2\" as=\"span\">\n ${isAnnual ? Math.round(plan.price * 0.8) : plan.price}\n </Typography>\n <Typography variant=\"body-l\" as=\"span\" color=\"muted\" className=\"pb-2.5\">\n {plan.period}\n </Typography>\n </div>\n <Typography variant=\"body-m\" color=\"muted\">\n {plan.description}\n </Typography>\n </div>\n\n {/* CTA Button */}\n <Button variant=\"primary\" size=\"lg\" className=\"w-full\">\n Select plan\n </Button>\n\n {/* Features List */}\n <div className=\"flex flex-col\">\n {/* AI Badge */}\n {plan.hasAI && (\n <div className=\"flex items-center gap-2 py-2\">\n <Sparkle\n size={20}\n weight=\"fill\"\n style={{ color: \"var(--canvas-primary)\" }}\n />\n <Typography\n variant=\"body-m\"\n as=\"span\"\n style={{ fontWeight: 500, color: \"var(--canvas-primary-hover)\" }}\n >\n AI add-on available\n </Typography>\n <span\n className=\"px-2 py-0.5 rounded\"\n style={{\n backgroundColor: \"var(--canvas-surface-brand)\",\n fontFamily: \"var(--typo-global-font)\",\n fontSize: \"12px\",\n fontWeight: 600,\n color: \"var(--canvas-primary)\",\n }}\n >\n NEW\n </span>\n </div>\n )}\n\n {plan.features.map((feature, idx) => (\n <div\n key={idx}\n className=\"flex items-center gap-2 py-2\"\n >\n <Check\n size={20}\n weight=\"bold\"\n style={{ color: \"var(--canvas-primary-hover)\" }}\n />\n <Typography variant=\"body-m\" as=\"span\" color=\"muted\" className=\"flex-1\">\n {feature.text}\n </Typography>\n {feature.hasInfo && (\n <Info\n size={20}\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n />\n )}\n </div>\n ))}\n </div>\n </div>\n </div>\n ))}\n </div>\n\n {/* Contact Us Banner */}\n <div\n className=\"w-full flex flex-col md:flex-row items-start md:items-end justify-between gap-4 p-8 rounded-xl\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n boxShadow: \"0px 1px 8px 0px rgba(14,30,47,0.03)\",\n }}\n >\n <div className=\"flex flex-col gap-4 flex-1\">\n <Typography variant=\"h5\" as=\"h3\">\n Contact us\n </Typography>\n <Typography variant=\"body-m\" color=\"muted\">\n For advanced security and more flexible controls, speak to\n someone from our team who will help you scale your business\n quickly with custom add-on features.\n </Typography>\n </div>\n <Button variant=\"primary\" size=\"lg\">\n Talk to us\n </Button>\n </div>\n </div>\n </div>\n </section>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "@phosphor-icons/react"
14
+ ],
15
+ "registryDependencies": []
16
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "pricing-cta",
3
+ "type": "registry:block",
4
+ "description": "CTA section for pricing page (contact sales, etc.).",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/pricing/pricing-cta.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { Button } from \"../../ui/button\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface PricingCtaProps {\n title?: string;\n primaryButtonText?: string;\n secondaryButtonText?: string;\n onPrimaryClick?: () => void;\n onSecondaryClick?: () => void;\n}\n\nexport function PricingCta({\n title = \"Get started for free\",\n primaryButtonText = \"Open account\",\n secondaryButtonText = \"Explore demo\",\n onPrimaryClick,\n onSecondaryClick,\n}: PricingCtaProps) {\n return (\n <section className=\"w-full px-4 md:px-8 lg:px-20\">\n <div\n className=\"w-full flex flex-col items-center justify-center text-center gap-8 py-12 md:py-16 px-10 overflow-hidden\"\n style={{\n backgroundColor: \"var(--canvas-dark-section-bg)\",\n borderRadius: \"var(--radius-3xl)\",\n }}\n >\n <Typography\n variant=\"h3\"\n as=\"h2\"\n style={{ color: \"white\" }}\n >\n {title}\n </Typography>\n\n <div className=\"flex flex-col sm:flex-row items-center gap-6\">\n <Button variant=\"primary\" size=\"lg\" onClick={onPrimaryClick}>\n {primaryButtonText}\n </Button>\n <Button variant=\"secondary\" size=\"lg\" onClick={onSecondaryClick}>\n {secondaryButtonText}\n </Button>\n </div>\n </div>\n </section>\n );\n}\n\n\n\n\n\n\n"
10
+ }
11
+ ],
12
+ "dependencies": [],
13
+ "registryDependencies": []
14
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "profile-card",
3
+ "type": "registry:block",
4
+ "description": "Centered profile card with avatar overlapping banner, stats, bio, tags, and social links.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/profile-card.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { Typography } from \"../ui/typography\";\nimport {\n MapPin,\n Calendar,\n Globe,\n Star,\n Facebook,\n Twitter,\n Linkedin,\n Instagram,\n MoreHorizontal,\n} from \"lucide-react\";\n\ninterface ProfileStat {\n value: string;\n label: string;\n}\n\ninterface ProfileTag {\n label: string;\n}\n\ninterface SocialLink {\n type: \"website\" | \"facebook\" | \"twitter\" | \"linkedin\" | \"instagram\";\n label: string;\n href?: string;\n}\n\nexport interface ProfileCardProps {\n /** Profile avatar image URL */\n avatarUrl?: string;\n /** Avatar fallback initials */\n avatarFallback?: string;\n /** Whether to show online status indicator */\n showStatus?: boolean;\n /** User's display name */\n name: string;\n /** User's username (with @) */\n username: string;\n /** Star rating (0-5) */\n rating?: number;\n /** Location text */\n location?: string;\n /** Join date text */\n joinDate?: string;\n /** Stats to display */\n stats?: ProfileStat[];\n /** Bio text */\n bio?: string;\n /** Tags/skills */\n tags?: ProfileTag[];\n /** Social links */\n socialLinks?: SocialLink[];\n /** Additional class names */\n className?: string;\n /** Show menu button */\n showMenu?: boolean;\n /** Menu click handler */\n onMenuClick?: () => void;\n}\n\nconst socialIcons = {\n website: Globe,\n facebook: Facebook,\n twitter: Twitter,\n linkedin: Linkedin,\n instagram: Instagram,\n};\n\n/**\n * Canvas Design System - Profile Card Component\n *\n * A centered profile card with avatar overlapping banner, stats, tags, and social links.\n * Uses CSS variables for theming to support live preview.\n */\nexport function ProfileCard({\n avatarUrl,\n avatarFallback = \"JC\",\n showStatus = true,\n name,\n username,\n rating = 5,\n location,\n joinDate,\n stats = [],\n bio,\n tags = [],\n socialLinks = [],\n className,\n showMenu = true,\n onMenuClick,\n}: ProfileCardProps) {\n return (\n <div\n className={cn(\n \"flex flex-col items-center w-full max-w-[992px] mx-auto\",\n className\n )}\n >\n {/* Avatar Section - Overlaps banner via negative margin in parent */}\n <div className=\"relative flex flex-col items-center w-full\">\n {/* Avatar with Status */}\n <div className=\"relative -mt-16\">\n <Avatar className=\"size-32 border-4 border-white\">\n <AvatarImage src={avatarUrl} alt={name} />\n <AvatarFallback\n className=\"text-xl font-semibold bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)]\"\n >\n {avatarFallback}\n </AvatarFallback>\n </Avatar>\n {showStatus && (\n <div className=\"absolute bottom-[9px] right-[9px] size-5 rounded-full bg-emerald-500 border-[3px] border-white\" />\n )}\n </div>\n\n {/* Menu Button - Positioned to the right */}\n {showMenu && (\n <button\n onClick={onMenuClick}\n className=\"absolute top-4 right-0 flex items-center justify-center size-7 rounded-[var(--radius-xs)] border border-[var(--canvas-border)] bg-[var(--canvas-background)] hover:bg-[var(--canvas-surface)] transition-colors\"\n aria-label=\"More options\"\n >\n <MoreHorizontal className=\"size-4 text-[var(--canvas-text-muted)]\" />\n </button>\n )}\n </div>\n\n {/* Name & Username - Centered */}\n <div className=\"flex flex-col items-center gap-1 mt-4\">\n <Typography variant=\"body-xl\" className=\"text-center\" style={{ fontWeight: 600 }}>\n {name}\n </Typography>\n <Typography variant=\"body-s\" color=\"muted\" className=\"text-center\">\n {username}\n </Typography>\n </div>\n\n {/* Star Rating - Centered */}\n {rating > 0 && (\n <div className=\"flex items-center gap-0.5 mt-3\">\n {[...Array(5)].map((_, i) => (\n <Star\n key={i}\n className={cn(\n \"size-4\",\n i < rating\n ? \"fill-[var(--canvas-primary)] text-[var(--canvas-primary)]\"\n : \"fill-[var(--canvas-border)] text-[var(--canvas-border)]\"\n )}\n />\n ))}\n </div>\n )}\n\n {/* Location & Join Date - Centered */}\n <div className=\"flex flex-col items-center gap-2 mt-4\">\n {location && (\n <div className=\"flex items-center gap-[var(--spacing-sm)]\">\n <MapPin className=\"size-4 text-[var(--canvas-text-muted)]\" />\n <Typography variant=\"body-s\" color=\"muted\">\n {location}\n </Typography>\n </div>\n )}\n {joinDate && (\n <div className=\"flex items-center gap-[var(--spacing-sm)]\">\n <Calendar className=\"size-4 text-[var(--canvas-text-muted)]\" />\n <Typography variant=\"body-s\" color=\"muted\">\n {joinDate}\n </Typography>\n </div>\n )}\n </div>\n\n {/* Stats Section with top border */}\n {stats.length > 0 && (\n <div className=\"flex items-center justify-center gap-[var(--spacing-3xl)] w-full mt-6 pt-6 border-t border-[var(--canvas-border)]\">\n {stats.map((stat, index) => (\n <div key={index} className=\"flex flex-col items-center gap-0.5\">\n <Typography variant=\"body-l\" style={{ fontWeight: 600 }}>\n {stat.value}\n </Typography>\n <Typography variant=\"body-s\" color=\"muted\" className=\"text-center\">\n {stat.label}\n </Typography>\n </div>\n ))}\n </div>\n )}\n\n {/* Bio - Centered */}\n {bio && (\n <div className=\"w-full max-w-[576px] mt-6\">\n <Typography variant=\"body-s\" className=\"text-center\">\n {bio}\n </Typography>\n </div>\n )}\n\n {/* Tags - Centered */}\n {tags.length > 0 && (\n <div className=\"flex flex-wrap justify-center gap-[var(--spacing-md)] mt-6\">\n {tags.map((tag, index) => (\n <span\n key={index}\n className=\"px-[var(--spacing-lg)] py-[var(--spacing-xs)] h-7 flex items-center rounded-[var(--radius-xs)] bg-[var(--canvas-surface-brand)] text-[var(--canvas-primary)]\"\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: \"var(--typo-body-s-line-height)\",\n }}\n >\n {tag.label}\n </span>\n ))}\n </div>\n )}\n\n {/* Social Links - Top border only */}\n {socialLinks.length > 0 && (\n <div className=\"flex items-center justify-center w-full mt-6 pt-6 border-t border-[var(--canvas-border)]\">\n <div className=\"flex items-center justify-between w-full max-w-[700px]\">\n {socialLinks.map((link, index) => {\n const IconComponent = socialIcons[link.type];\n return (\n <a\n key={index}\n href={link.href || \"#\"}\n className=\"flex items-center gap-[var(--spacing-sm)] text-[var(--canvas-text-muted)] hover:text-[var(--canvas-text)] transition-colors\"\n >\n <IconComponent className=\"size-4\" />\n <Typography variant=\"body-s\" color=\"muted\" as=\"span\">\n {link.label}\n </Typography>\n </a>\n );\n })}\n </div>\n </div>\n )}\n </div>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "registryDependencies": [
16
+ "utils",
17
+ "avatar",
18
+ "typography"
19
+ ]
20
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "profile-grid-tiles-list",
3
+ "type": "registry:block",
4
+ "description": "Responsive grid of profile cards with avatar, ratings, certifications, and metadata. Configurable 2-5 columns with mobile responsiveness. Ideal for tutors, team members, or user directories.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/profile-grid-tiles-list.tsx",
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 { MapPin, BookOpen, Video, DollarSign, Star } from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface ProfileTileItem {\n id: string;\n name: string;\n avatarUrl?: string;\n isOnline?: boolean;\n subject: string;\n pricePerHour: number;\n rating: number;\n reviewCount: string;\n certification?: string;\n location: string;\n education: string;\n sessionsCount: string;\n earnings: 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 ProfileGridTilesListProps {\n /** Block title */\n title?: string;\n /** Subtitle text (e.g., \"23 english tutors near you\") */\n subtitle?: string;\n /** Array of profile tile items */\n items?: ProfileTileItem[];\n /** Number of columns in the grid (2-5) */\n columns?: 2 | 3 | 4 | 5;\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 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 a profile is clicked */\n onItemClick?: (item: ProfileTileItem) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultItems: ProfileTileItem[] = [\n {\n id: \"1\",\n name: \"Jeffrey Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n isOnline: true,\n subject: \"English\",\n pricePerHour: 80,\n rating: 4.3,\n reviewCount: \"2.4k\",\n certification: \"TEFL certification\",\n location: \"San Francisco\",\n education: \"UCLA\",\n sessionsCount: \"105 sessions\",\n earnings: \"5.2k earned\",\n },\n {\n id: \"2\",\n name: \"Stacy Jones\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n isOnline: true,\n subject: \"English\",\n pricePerHour: 75,\n rating: 4.3,\n reviewCount: \"2.4k\",\n certification: \"Native speaker\",\n location: \"New York\",\n education: \"Columbia\",\n sessionsCount: \"23 sessions\",\n earnings: \"2k earned\",\n },\n {\n id: \"3\",\n name: \"Raj Mishra\",\n avatarUrl: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face\",\n isOnline: true,\n subject: \"English\",\n pricePerHour: 75,\n rating: 4.3,\n reviewCount: \"2.4k\",\n certification: \"TEFL certification\",\n location: \"Newark\",\n education: \"Rutgers\",\n sessionsCount: \"34 sessions\",\n earnings: \"2.1k earned\",\n },\n {\n id: \"4\",\n name: \"Mary Trott\",\n avatarUrl: \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face\",\n isOnline: true,\n subject: \"English\",\n pricePerHour: 75,\n rating: 4.3,\n reviewCount: \"2.4k\",\n certification: \"TEFL certification\",\n location: \"Connecticut\",\n education: \"Yale\",\n sessionsCount: \"75 sessions\",\n earnings: \"91k earned\",\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"rating\", label: \"Highest Rated\" },\n { id: \"price-low\", label: \"Price: Low to High\" },\n { id: \"price-high\", label: \"Price: High to Low\" },\n { id: \"sessions\", label: \"Most Sessions\" },\n];\n\nconst defaultFilterOptions: FilterOption[] = [\n { id: \"all\", label: \"All Tutors\" },\n { id: \"online\", label: \"Online Now\" },\n { id: \"native\", label: \"Native Speaker\" },\n { id: \"certified\", label: \"Certified\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface ProfileTileCardProps {\n item: ProfileTileItem;\n onClick?: (item: ProfileTileItem) => void;\n}\n\nfunction ProfileTileCard({ item, onClick }: ProfileTileCardProps) {\n return (\n <div\n className=\"flex flex-col\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-md, 8px)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n overflow: \"hidden\",\n }}\n >\n {/* Main Content */}\n <div\n className=\"flex flex-col items-center w-full\"\n style={{\n padding: \"var(--spacing-4xl) var(--spacing-4xl) 0\",\n gap: \"var(--spacing-2xl)\",\n }}\n >\n {/* Avatar Section */}\n <div className=\"flex flex-col items-center w-full\" style={{ gap: \"var(--radius-md, 8px)\" }}>\n {/* Avatar with Online Indicator */}\n <div className=\"relative shrink-0\" style={{ width: \"120px\", height: \"120px\" }}>\n <Avatar\n className=\"w-full h-full\"\n style={{\n width: \"120px\",\n height: \"120px\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={item.avatarUrl} alt={item.name} />\n <AvatarFallback style={{ fontSize: \"var(--typo-h4-size)\" }}>\n {item.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n {item.isOnline && (\n <div\n className=\"absolute\"\n style={{\n width: \"20px\",\n height: \"20px\",\n right: \"4px\",\n bottom: \"7px\",\n backgroundColor: \"var(--canvas-success)\",\n borderRadius: \"var(--radius-full, 50%)\",\n border: \"2px solid var(--canvas-background)\",\n }}\n />\n )}\n </div>\n\n {/* Name, Subject, Price */}\n <div\n className=\"flex flex-col items-center text-center\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <p\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 margin: 0,\n }}\n >\n {item.name}\n </p>\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)\",\n margin: 0,\n }}\n >\n {item.subject}\n </p>\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)\",\n margin: 0,\n }}\n >\n <span style={{ fontWeight: 600 }}>${item.pricePerHour}</span> / hour\n </p>\n </div>\n </div>\n\n {/* Rating Row */}\n <div\n className=\"flex items-center justify-center w-full\"\n style={{ gap: \"4px\" }}\n >\n <Star\n className=\"w-5 h-5\"\n style={{ color: \"var(--canvas-primary)\", fill: \"var(--canvas-primary)\" }}\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: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.rating}\n </span>\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: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n ({item.reviewCount})\n </span>\n </div>\n\n {/* Divider */}\n <div\n className=\"w-full\"\n style={{\n height: \"1px\",\n backgroundColor: \"var(--canvas-border)\",\n }}\n />\n\n {/* Certification Badge */}\n {item.certification && (\n <p\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: \"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 {item.certification}\n </p>\n )}\n\n {/* Metadata Rows */}\n <div\n className=\"flex flex-col w-full\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n {/* Location */}\n <div className=\"flex items-center\" style={{ gap: \"var(--spacing-sm)\" }}>\n <MapPin\n className=\"w-5 h-5 shrink-0\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\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: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {item.location}\n </span>\n </div>\n\n {/* Education */}\n <div className=\"flex items-center\" style={{ gap: \"var(--spacing-sm)\" }}>\n <BookOpen\n className=\"w-5 h-5 shrink-0\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\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: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {item.education}\n </span>\n </div>\n\n {/* Sessions */}\n <div className=\"flex items-center\" style={{ gap: \"var(--spacing-sm)\" }}>\n <Video\n className=\"w-5 h-5 shrink-0\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\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: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {item.sessionsCount}\n </span>\n </div>\n\n {/* Earnings */}\n <div className=\"flex items-center\" style={{ gap: \"var(--spacing-sm)\" }}>\n <DollarSign\n className=\"w-5 h-5 shrink-0\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\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: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {item.earnings}\n </span>\n </div>\n </div>\n </div>\n\n {/* Footer - View Profile Button */}\n <div\n className=\"flex items-center justify-center w-full cursor-pointer\"\n style={{\n borderTop: \"1px solid var(--canvas-border)\",\n padding: \"var(--spacing-xl)\",\n marginTop: \"var(--spacing-2xl)\",\n }}\n onClick={() => onClick?.(item)}\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: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n View profile &gt;\n </span>\n </div>\n </div>\n );\n}\n\n// ============================================\n// Grid Constants\n// ============================================\n\nconst CARD_MIN_WIDTH = 300;\nconst GRID_GAP_PX = 32; // --spacing-4xl = 32px\n\n/**\n * Get inline styles for the grid based on columns configuration\n * Uses CSS Grid with auto-fill and minmax for:\n * - Minimum card width of 300px\n * - Equal width for all cards (via 1fr)\n * - Automatic wrapping when container is narrower\n * - Maximum columns limited by the columns prop\n */\nfunction getGridStyle(columns: 2 | 3 | 4 | 5): React.CSSProperties {\n // Calculate max-width to limit the number of columns\n // Formula: columns * minWidth + (columns - 1) * gap\n const maxGridWidth = columns * CARD_MIN_WIDTH + (columns - 1) * GRID_GAP_PX;\n \n return {\n display: 'grid',\n gridTemplateColumns: `repeat(auto-fill, minmax(${CARD_MIN_WIDTH}px, 1fr))`,\n gap: 'var(--spacing-4xl)',\n width: '100%',\n maxWidth: `${maxGridWidth}px`,\n };\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Profile Grid Tiles List Block\n *\n * A responsive grid of profile cards with avatar, ratings, certifications,\n * and metadata. Supports 2-5 column layouts that adapt to screen size.\n *\n * @example\n * ```tsx\n * <ProfileGridTilesList\n * title=\"Tutors near you\"\n * subtitle=\"23 english tutors near you\"\n * columns={4}\n * onItemClick={(item) => console.log(\"Clicked\", item)}\n * />\n * ```\n */\nexport function ProfileGridTilesList({\n title = \"Tutors near you\",\n subtitle,\n items = defaultItems,\n columns = 4,\n sortOptions = defaultSortOptions,\n filterOptions = defaultFilterOptions,\n actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onFilter,\n onItemClick,\n className,\n}: ProfileGridTilesListProps) {\n const [sortValue, setSortValue] = useState<string>(\"\");\n const [filterValue, setFilterValue] = useState<string>(\"\");\n\n const displaySubtitle = subtitle ?? `${items.length} tutors available`;\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-xl)\" }}\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 Subtitle */}\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 {displaySubtitle}\n </p>\n </div>\n\n {/* Controls */}\n <div\n className=\"flex flex-wrap items-start justify-end shrink-0\"\n style={{ gap: \"var(--spacing-3xl)\" }}\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 {/* Grid Section */}\n <div style={getGridStyle(columns)}>\n {items.map((item) => (\n <ProfileTileCard\n key={item.id}\n item={item}\n onClick={onItemClick}\n />\n ))}\n </div>\n </div>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "registryDependencies": [
16
+ "utils",
17
+ "button",
18
+ "select",
19
+ "avatar"
20
+ ]
21
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "profile-image-uploader",
3
+ "type": "registry:block",
4
+ "description": "Avatar upload component with preview and edit button.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/profile-image-uploader.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { useState, useRef } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { User, Camera } from \"lucide-react\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\n\ninterface ProfileImageUploaderProps {\n /** Current image URL */\n imageUrl?: string;\n /** Callback when image is selected */\n onImageChange?: (file: File) => void;\n /** Size of the uploader */\n size?: number;\n /** Class name for customization */\n className?: string;\n}\n\n/**\n * Profile Image Uploader\n * \n * A circular avatar with upload functionality.\n * Shows a placeholder user icon when no image is set.\n * Hover state shows camera overlay for upload interaction.\n */\nexport function ProfileImageUploader({\n imageUrl,\n onImageChange,\n size = 132,\n className,\n}: ProfileImageUploaderProps) {\n const [isHovered, setIsHovered] = useState(false);\n const [previewUrl, setPreviewUrl] = useState<string | undefined>(imageUrl);\n const inputRef = useRef<HTMLInputElement>(null);\n\n const handleClick = () => {\n inputRef.current?.click();\n };\n\n const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const file = e.target.files?.[0];\n if (file) {\n // Create preview URL\n const objectUrl = URL.createObjectURL(file);\n setPreviewUrl(objectUrl);\n onImageChange?.(file);\n }\n };\n\n return (\n <div className={cn(\"relative\", className)}>\n <button\n type=\"button\"\n onClick={handleClick}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n className=\"relative rounded-full overflow-hidden border border-[var(--canvas-neutral-border)] transition-all hover:border-[var(--canvas-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:ring-offset-2\"\n style={{ width: size, height: size }}\n >\n <Avatar className=\"size-full\">\n {previewUrl ? (\n <AvatarImage src={previewUrl} alt=\"Profile\" className=\"object-cover\" />\n ) : null}\n <AvatarFallback className=\"bg-[var(--canvas-neutral-surface)] size-full flex items-center justify-center\">\n <User \n className=\"text-[var(--canvas-neutral-text)]\" \n style={{ width: size * 0.4, height: size * 0.4 }} \n />\n </AvatarFallback>\n </Avatar>\n\n {/* Hover overlay */}\n <div\n className={cn(\n \"absolute inset-0 bg-black/50 flex items-center justify-center transition-opacity\",\n isHovered ? \"opacity-100\" : \"opacity-0\"\n )}\n >\n <Camera className=\"text-white\" style={{ width: size * 0.25, height: size * 0.25 }} />\n </div>\n </button>\n\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/*\"\n onChange={handleFileChange}\n className=\"hidden\"\n aria-label=\"Upload profile image\"\n />\n </div>\n );\n}\n\n\n\n\n\n\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "registryDependencies": [
16
+ "utils",
17
+ "avatar"
18
+ ]
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "profile-info-cards",
3
+ "type": "registry:block",
4
+ "description": "Grid of info cards for profile pages (contact, stats, etc.).",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/profile-info-cards.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Typography } from \"../ui/typography\";\nimport {\n Globe,\n Facebook,\n Twitter,\n Instagram,\n Zap,\n Star,\n CheckCircle,\n} from \"lucide-react\";\n\n// Base card wrapper component\ninterface CardWrapperProps {\n children: React.ReactNode;\n className?: string;\n}\n\nfunction CardWrapper({ children, className }: CardWrapperProps) {\n return (\n <div\n className={cn(\n \"bg-[var(--canvas-background)] border border-[var(--canvas-border)] rounded-[var(--radius-md)]\",\n \"w-full\",\n className\n )}\n >\n {children}\n </div>\n );\n}\n\n// Stats Card\ninterface Stat {\n value: string;\n label: string;\n}\n\nexport interface StatsCardProps {\n stats: Stat[];\n className?: string;\n}\n\nexport function StatsCard({ stats, className }: StatsCardProps) {\n return (\n <CardWrapper className={cn(\"p-[var(--spacing-3xl)]\", className)}>\n <div className=\"flex gap-[var(--spacing-md)]\">\n {stats.map((stat, index) => (\n <div\n key={index}\n className=\"flex-1 flex flex-col items-center gap-[var(--spacing-xxs)]\"\n >\n <Typography variant=\"body-l\" style={{ fontWeight: 600 }}>\n {stat.value}\n </Typography>\n <Typography variant=\"body-s\" color=\"muted\" className=\"text-center\">\n {stat.label}\n </Typography>\n </div>\n ))}\n </div>\n </CardWrapper>\n );\n}\n\n// Portfolio Card\ninterface PortfolioImage {\n src: string;\n alt?: string;\n}\n\nexport interface PortfolioCardProps {\n title?: string;\n viewAllHref?: string;\n images: PortfolioImage[];\n className?: string;\n}\n\nexport function PortfolioCard({\n title = \"Portfolio\",\n viewAllHref,\n images,\n className,\n}: PortfolioCardProps) {\n return (\n <CardWrapper className={cn(\"p-[var(--spacing-4xl)] overflow-hidden\", className)}>\n <div className=\"flex flex-col gap-[var(--spacing-xl)]\">\n {/* Header */}\n <div className=\"flex items-center justify-between\">\n <Typography variant=\"body-l\" style={{ fontWeight: 600 }}>\n {title}\n </Typography>\n {viewAllHref && (\n <a\n href={viewAllHref}\n className=\"text-[var(--canvas-primary)] hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n View all\n </a>\n )}\n </div>\n\n {/* Image Grid */}\n <div className=\"flex flex-col gap-[var(--spacing-xs)]\">\n {/* First row */}\n <div className=\"flex gap-[var(--spacing-xs)]\">\n {images.slice(0, 2).map((image, index) => (\n <div\n key={index}\n className=\"flex-1 h-20 bg-[var(--canvas-surface)] overflow-hidden\"\n >\n <img\n src={image.src}\n alt={image.alt || \"Portfolio image\"}\n className=\"w-full h-full object-cover\"\n />\n </div>\n ))}\n </div>\n {/* Second row */}\n {images.length > 2 && (\n <div className=\"flex gap-[var(--spacing-xs)]\">\n {images.slice(2, 4).map((image, index) => (\n <div\n key={index}\n className=\"flex-1 h-20 bg-[var(--canvas-surface)] overflow-hidden\"\n >\n <img\n src={image.src}\n alt={image.alt || \"Portfolio image\"}\n className=\"w-full h-full object-cover\"\n />\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n </CardWrapper>\n );\n}\n\n// Description Card\nexport interface DescriptionCardProps {\n title?: string;\n content: string;\n className?: string;\n}\n\nexport function DescriptionCard({\n title = \"Description\",\n content,\n className,\n}: DescriptionCardProps) {\n return (\n <CardWrapper className={cn(\"p-[var(--spacing-4xl)]\", className)}>\n <div className=\"flex flex-col gap-[var(--spacing-xl)]\">\n <Typography variant=\"body-l\" style={{ fontWeight: 600 }}>\n {title}\n </Typography>\n <Typography variant=\"body-s\" color=\"muted\" className=\"whitespace-pre-line\">\n {content}\n </Typography>\n </div>\n </CardWrapper>\n );\n}\n\n// Skills Card\ninterface Skill {\n label: string;\n}\n\nexport interface SkillsCardProps {\n title?: string;\n skills: Skill[];\n className?: string;\n}\n\nexport function SkillsCard({\n title = \"Skills\",\n skills,\n className,\n}: SkillsCardProps) {\n return (\n <CardWrapper className={cn(\"p-[var(--spacing-4xl)]\", className)}>\n <div className=\"flex flex-col gap-[var(--spacing-xl)]\">\n <Typography variant=\"body-l\" style={{ fontWeight: 600 }}>\n {title}\n </Typography>\n <div className=\"flex flex-wrap gap-[var(--spacing-sm)]\">\n {skills.map((skill, index) => (\n <span\n key={index}\n className=\"h-7 px-[var(--spacing-lg)] flex items-center rounded-full border border-[var(--canvas-border)] bg-[var(--canvas-background)]\"\n style={{\n fontFamily: \"var(--typo-body-xs-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xs-size)\",\n fontWeight: 500,\n }}\n >\n {skill.label}\n </span>\n ))}\n </div>\n </div>\n </CardWrapper>\n );\n}\n\n// Links Card\ninterface LinkItem {\n type: \"website\" | \"facebook\" | \"twitter\" | \"instagram\";\n url: string;\n}\n\nconst linkIcons = {\n website: Globe,\n facebook: Facebook,\n twitter: Twitter,\n instagram: Instagram,\n};\n\nexport interface LinksCardProps {\n title?: string;\n links: LinkItem[];\n className?: string;\n}\n\nexport function LinksCard({\n title = \"Links\",\n links,\n className,\n}: LinksCardProps) {\n return (\n <CardWrapper className={cn(\"p-[var(--spacing-4xl)]\", className)}>\n <div className=\"flex flex-col gap-[var(--spacing-xl)]\">\n <Typography variant=\"body-m\" style={{ fontWeight: 600 }}>\n {title}\n </Typography>\n <div className=\"flex flex-col gap-[var(--spacing-xl)]\">\n {links.map((link, index) => {\n const IconComponent = linkIcons[link.type];\n return (\n <a\n key={index}\n href={`https://${link.url}`}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-[var(--spacing-sm)] text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)] transition-colors\"\n >\n <IconComponent className=\"size-4\" />\n <Typography variant=\"body-s\" color=\"muted\" as=\"span\">\n {link.url}\n </Typography>\n </a>\n );\n })}\n </div>\n </div>\n </CardWrapper>\n );\n}\n\n// Badges Card\ninterface Badge {\n icon: \"zap\" | \"star\" | \"check\";\n title: string;\n description: string;\n iconColor?: string;\n}\n\nconst badgeIcons = {\n zap: Zap,\n star: Star,\n check: CheckCircle,\n};\n\nconst badgeColors = {\n zap: \"text-[var(--canvas-primary)]\",\n star: \"text-amber-600\",\n check: \"text-emerald-600\",\n};\n\nexport interface BadgesCardProps {\n badges: Badge[];\n className?: string;\n}\n\nexport function BadgesCard({ badges, className }: BadgesCardProps) {\n return (\n <CardWrapper className={cn(\"p-[var(--spacing-4xl)]\", className)}>\n <div className=\"flex flex-col gap-[var(--spacing-xl)]\">\n {badges.map((badge, index) => {\n const IconComponent = badgeIcons[badge.icon];\n const iconColor = badgeColors[badge.icon];\n return (\n <div\n key={index}\n className=\"flex items-center gap-[var(--spacing-xl)]\"\n >\n <IconComponent className={cn(\"size-4\", iconColor)} />\n <div className=\"flex-1 flex flex-col\">\n <Typography variant=\"body-s\" className=\"font-semibold\">\n {badge.title}\n </Typography>\n <Typography variant=\"body-xs\" color=\"muted\">\n {badge.description}\n </Typography>\n </div>\n </div>\n );\n })}\n </div>\n </CardWrapper>\n );\n}\n\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "registryDependencies": [
16
+ "utils",
17
+ "typography"
18
+ ]
19
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "progress-bar",
3
+ "type": "registry:block",
4
+ "description": "Horizontal progress bar. Can use percentage or currentStep/totalSteps.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/progress-bar.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\n\nexport interface ProgressBarProps {\n /** Progress value (0-100) */\n progress?: number;\n /** Total number of steps */\n totalSteps?: number;\n /** Current step (0-indexed) - alternative to progress */\n currentStep?: number;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Canvas Design System - Progress Bar\n * \n * A horizontal progress indicator with:\n * - 8px height\n * - Pill-shaped (fully rounded)\n * - Background: --canvas-border\n * - Fill: --canvas-primary\n * \n * Can be controlled via either:\n * - `progress` prop (0-100 percentage)\n * - `currentStep` and `totalSteps` props (calculates percentage automatically)\n * \n * @example\n * ```tsx\n * // Using percentage\n * <ProgressBar progress={33} />\n * \n * // Using steps\n * <ProgressBar currentStep={1} totalSteps={3} />\n * ```\n */\nexport function ProgressBar({\n progress,\n totalSteps = 3,\n currentStep = 0,\n className,\n}: ProgressBarProps) {\n // Calculate progress percentage\n // If progress prop is provided, use it directly\n // Otherwise calculate from currentStep/totalSteps\n const progressPercent = progress !== undefined \n ? Math.min(100, Math.max(0, progress))\n : totalSteps > 0 \n ? Math.min(100, Math.max(0, ((currentStep + 1) / totalSteps) * 100))\n : 0;\n\n return (\n <div \n className={cn(\n \"w-full h-2 rounded-full overflow-hidden\",\n \"bg-[var(--canvas-border)]\",\n className\n )}\n role=\"progressbar\"\n aria-valuenow={progressPercent}\n aria-valuemin={0}\n aria-valuemax={100}\n >\n <div \n className=\"h-full bg-[var(--canvas-primary)] rounded-full transition-all duration-300 ease-out\"\n style={{ width: `${progressPercent}%` }}\n />\n </div>\n );\n}\n\n\n\n\n\n\n\n\n"
10
+ }
11
+ ],
12
+ "dependencies": [],
13
+ "registryDependencies": [
14
+ "utils"
15
+ ]
16
+ }