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.
- package/dist/cli/index.js +516 -0
- package/dist/index.d.ts +67 -3
- package/dist/index.js +2588 -301
- package/dist/index.js.map +1 -1
- package/mcp/dist/index.js +5 -1
- package/package.json +18 -2
- package/registry/blocks/activity-feed.json +19 -0
- package/registry/blocks/blog-cards.json +16 -0
- package/registry/blocks/bottom-input-chat-widget.json +19 -0
- package/registry/blocks/canvas-item.json +18 -0
- package/registry/blocks/category-grid.json +16 -0
- package/registry/blocks/centered-hero.json +14 -0
- package/registry/blocks/chat-message.json +18 -0
- package/registry/blocks/circular-progress-bar-list.json +18 -0
- package/registry/blocks/component-palette.json +21 -0
- package/registry/blocks/component-search.json +19 -0
- package/registry/blocks/content-dropzone.json +16 -0
- package/registry/blocks/content-with-image.json +14 -0
- package/registry/blocks/core-values-grid.json +16 -0
- package/registry/blocks/credit-card-display.json +16 -0
- package/registry/blocks/cta-banner.json +14 -0
- package/registry/blocks/custom-component-helper.json +19 -0
- package/registry/blocks/destination-cards.json +16 -0
- package/registry/blocks/empty-state.json +16 -0
- package/registry/blocks/faq-accordion.json +16 -0
- package/registry/blocks/faqs-table.json +18 -0
- package/registry/blocks/feature-with-image.json +16 -0
- package/registry/blocks/featured-news-cards.json +16 -0
- package/registry/blocks/featured-places.json +16 -0
- package/registry/blocks/features-comparison.json +16 -0
- package/registry/blocks/filter-popover.json +28 -0
- package/registry/blocks/fixed-column-data-table.json +20 -0
- package/registry/blocks/flair-banner.json +16 -0
- package/registry/blocks/footer-navbar.json +17 -0
- package/registry/blocks/form-group.json +29 -0
- package/registry/blocks/gallery-section.json +14 -0
- package/registry/blocks/gradient-banner.json +16 -0
- package/registry/blocks/graph-metric-tiles.json +20 -0
- package/registry/blocks/grid-tiles-list.json +20 -0
- package/registry/blocks/hero-dark-centered.json +16 -0
- package/registry/blocks/hero-dark-with-image.json +16 -0
- package/registry/blocks/hero-fullwidth-image.json +16 -0
- package/registry/blocks/hero-section.json +16 -0
- package/registry/blocks/how-it-works.json +16 -0
- package/registry/blocks/image-feed-with-nested-comments.json +20 -0
- package/registry/blocks/infinity-canvas.json +58 -0
- package/registry/blocks/large-image-labels-list.json +19 -0
- package/registry/blocks/loader.json +19 -0
- package/registry/blocks/login-branding-panel.json +16 -0
- package/registry/blocks/menu-section.json +18 -0
- package/registry/blocks/menufocus-template.json +19 -0
- package/registry/blocks/messenger-sidebar.json +19 -0
- package/registry/blocks/metrics-section.json +14 -0
- package/registry/blocks/mobile-bottom-nav.json +18 -0
- package/registry/blocks/monthly-calendar-widget.json +20 -0
- package/registry/blocks/nested-comments-table.json +21 -0
- package/registry/blocks/nested-data-table.json +22 -0
- package/registry/blocks/office-locations.json +14 -0
- package/registry/blocks/page-header-section.json +17 -0
- package/registry/blocks/page-previews.json +29 -0
- package/registry/blocks/pagination.json +20 -0
- package/registry/blocks/participant-list.json +17 -0
- package/registry/blocks/persona-card.json +18 -0
- package/registry/blocks/pill-tabs.json +19 -0
- package/registry/blocks/pricing-cards.json +16 -0
- package/registry/blocks/pricing-cta.json +14 -0
- package/registry/blocks/profile-card.json +20 -0
- package/registry/blocks/profile-grid-tiles-list.json +21 -0
- package/registry/blocks/profile-image-uploader.json +19 -0
- package/registry/blocks/profile-info-cards.json +19 -0
- package/registry/blocks/progress-bar.json +16 -0
- package/registry/blocks/prompt-template.json +18 -0
- package/registry/blocks/reviews-grid.json +14 -0
- package/registry/blocks/reviews-table.json +19 -0
- package/registry/blocks/screen-flowchart.json +19 -0
- package/registry/blocks/screen-prompt-builder.json +19 -0
- package/registry/blocks/screen-prompt-template.json +18 -0
- package/registry/blocks/search-bar.json +19 -0
- package/registry/blocks/search-sidebar.json +25 -0
- package/registry/blocks/settings-list-row.json +20 -0
- package/registry/blocks/sidebar-cards.json +18 -0
- package/registry/blocks/sidebar-profile-card.json +21 -0
- package/registry/blocks/slideshow-grid-tiles.json +21 -0
- package/registry/blocks/social-feed.json +20 -0
- package/registry/blocks/social-proof.json +14 -0
- package/registry/blocks/standard-data-table.json +20 -0
- package/registry/blocks/standard-list-with-image.json +17 -0
- package/registry/blocks/step-tracker.json +16 -0
- package/registry/blocks/team-cards-grid.json +16 -0
- package/registry/blocks/team-circular-grid.json +16 -0
- package/registry/blocks/testimonial-carousel.json +16 -0
- package/registry/blocks/upvoting-posts-table.json +22 -0
- package/registry/blocks/vertical-how-it-works.json +16 -0
- package/registry/blocks/vertical-step-tracker.json +17 -0
- package/registry/blocks/video-chat-controls.json +18 -0
- package/registry/blocks/video-content-section.json +16 -0
- package/registry/blocks/video-playlist.json +18 -0
- package/registry/blocks/webcam-preview.json +18 -0
- package/registry/blocks/youtube-player.json +16 -0
- package/registry/hooks/use-css-variable-sync.json +14 -0
- package/registry/hooks/use-mobile.json +14 -0
- package/registry/index.json +730 -0
- package/registry/layout/account-settings-shell.json +20 -0
- package/registry/layout/dashboard-shell.json +23 -0
- package/registry/layout/double-sidebar-shell.json +23 -0
- package/registry/layout/double-sidebar.json +20 -0
- package/registry/layout/header.json +22 -0
- package/registry/layout/icon-sidebar-shell.json +23 -0
- package/registry/layout/icon-sidebar.json +19 -0
- package/registry/layout/mobile-menu-shell.json +19 -0
- package/registry/layout/multistep-progressbar-shell.json +23 -0
- package/registry/layout/multistep-shell.json +21 -0
- package/registry/layout/multistep-sidebar-shell.json +22 -0
- package/registry/layout/project-context-shell.json +20 -0
- package/registry/layout/search-bar-shell.json +22 -0
- package/registry/layout/sidebar-nav.json +18 -0
- package/registry/layout/sidebar.json +20 -0
- package/registry/layout/standard-page-shell.json +21 -0
- package/registry/layout/vertical-multistep-shell.json +23 -0
- package/registry/lib/utils.json +17 -0
- package/registry/ui/avatar.json +18 -0
- package/registry/ui/button.json +19 -0
- package/registry/ui/calendar.json +20 -0
- package/registry/ui/checkbox.json +19 -0
- package/registry/ui/date-input.json +18 -0
- package/registry/ui/dialog.json +19 -0
- package/registry/ui/dropdown-menu.json +19 -0
- package/registry/ui/file-uploader.json +18 -0
- package/registry/ui/image-uploader.json +18 -0
- package/registry/ui/input.json +16 -0
- package/registry/ui/label.json +18 -0
- package/registry/ui/line-tabs.json +16 -0
- package/registry/ui/multiselect-checkbox-field.json +18 -0
- package/registry/ui/multiselect-tags.json +18 -0
- package/registry/ui/popover.json +18 -0
- package/registry/ui/radio-group.json +19 -0
- package/registry/ui/range-input.json +17 -0
- package/registry/ui/scroll-area.json +18 -0
- package/registry/ui/searchbox.json +18 -0
- package/registry/ui/select.json +20 -0
- package/registry/ui/selectable-pills.json +16 -0
- package/registry/ui/separator.json +18 -0
- package/registry/ui/sheet.json +19 -0
- package/registry/ui/sidebar.json +27 -0
- package/registry/ui/skeleton.json +16 -0
- package/registry/ui/slider.json +18 -0
- package/registry/ui/switch.json +18 -0
- package/registry/ui/tabs.json +18 -0
- package/registry/ui/text-input.json +16 -0
- package/registry/ui/textarea.json +18 -0
- package/registry/ui/tooltip.json +18 -0
- package/registry/ui/typography.json +16 -0
- package/styles/tokens.reference.css +35 -3
package/mcp/dist/index.js
CHANGED
|
@@ -21756,6 +21756,7 @@ var defaultButtonSizes = {
|
|
|
21756
21756
|
"--btn-mini-radius": "4px",
|
|
21757
21757
|
"--btn-mini-font-weight": "500",
|
|
21758
21758
|
"--btn-mini-letter-spacing": "0em",
|
|
21759
|
+
"--btn-mini-font-family": "",
|
|
21759
21760
|
// Small
|
|
21760
21761
|
"--btn-small-height": "32px",
|
|
21761
21762
|
"--btn-small-font-size": "14px",
|
|
@@ -21763,6 +21764,7 @@ var defaultButtonSizes = {
|
|
|
21763
21764
|
"--btn-small-radius": "6px",
|
|
21764
21765
|
"--btn-small-font-weight": "500",
|
|
21765
21766
|
"--btn-small-letter-spacing": "0em",
|
|
21767
|
+
"--btn-small-font-family": "",
|
|
21766
21768
|
// Standard
|
|
21767
21769
|
"--btn-standard-height": "40px",
|
|
21768
21770
|
"--btn-standard-font-size": "14px",
|
|
@@ -21770,13 +21772,15 @@ var defaultButtonSizes = {
|
|
|
21770
21772
|
"--btn-standard-radius": "8px",
|
|
21771
21773
|
"--btn-standard-font-weight": "500",
|
|
21772
21774
|
"--btn-standard-letter-spacing": "0em",
|
|
21775
|
+
"--btn-standard-font-family": "",
|
|
21773
21776
|
// Large
|
|
21774
21777
|
"--btn-large-height": "48px",
|
|
21775
21778
|
"--btn-large-font-size": "16px",
|
|
21776
21779
|
"--btn-large-px": "20px",
|
|
21777
21780
|
"--btn-large-radius": "10px",
|
|
21778
21781
|
"--btn-large-font-weight": "500",
|
|
21779
|
-
"--btn-large-letter-spacing": "0em"
|
|
21782
|
+
"--btn-large-letter-spacing": "0em",
|
|
21783
|
+
"--btn-large-font-family": ""
|
|
21780
21784
|
};
|
|
21781
21785
|
var defaultButtonColors = {
|
|
21782
21786
|
// Primary Save
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canvas-ui-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A comprehensive UI component library with design tokens for building beautiful interfaces",
|
|
6
|
+
"bin": {
|
|
7
|
+
"canvas-ui": "./dist/cli/index.js"
|
|
8
|
+
},
|
|
6
9
|
"main": "./dist/index.js",
|
|
7
10
|
"module": "./dist/index.js",
|
|
8
11
|
"types": "./dist/index.d.ts",
|
|
@@ -26,10 +29,12 @@
|
|
|
26
29
|
"files": [
|
|
27
30
|
"dist",
|
|
28
31
|
"styles",
|
|
32
|
+
"registry",
|
|
29
33
|
"mcp/dist"
|
|
30
34
|
],
|
|
31
35
|
"scripts": {
|
|
32
|
-
"build": "tsup && node -e \"const fs = require('fs'); for (const f of ['dist/index.js','dist/charts.js']) { const c = fs.readFileSync(f,'utf8'); fs.writeFileSync(f, '\\\"use client\\\";\\n' + c); }\"",
|
|
36
|
+
"build": "tsup && node -e \"const fs = require('fs'); for (const f of ['dist/index.js','dist/charts.js']) { const c = fs.readFileSync(f,'utf8'); fs.writeFileSync(f, '\\\"use client\\\";\\n' + c); }\" && npm run cli:build",
|
|
37
|
+
"cli:build": "npx tsx scripts/generate-registry.ts && tsup --config tsup.cli.config.ts",
|
|
33
38
|
"dev": "tsup --watch",
|
|
34
39
|
"lint": "eslint src/",
|
|
35
40
|
"typecheck": "tsc --noEmit",
|
|
@@ -82,11 +87,22 @@
|
|
|
82
87
|
"sonner": "^2.0.7",
|
|
83
88
|
"tailwind-merge": "^3.4.0"
|
|
84
89
|
},
|
|
90
|
+
"optionalDependencies": {
|
|
91
|
+
"chalk": "^5.4.0",
|
|
92
|
+
"commander": "^13.0.0",
|
|
93
|
+
"execa": "^9.5.0",
|
|
94
|
+
"fs-extra": "^11.2.0",
|
|
95
|
+
"ora": "^8.2.0",
|
|
96
|
+
"prompts": "^2.4.2"
|
|
97
|
+
},
|
|
85
98
|
"devDependencies": {
|
|
99
|
+
"@types/fs-extra": "^11.0.4",
|
|
86
100
|
"@types/node": "^20",
|
|
101
|
+
"@types/prompts": "^2.4.9",
|
|
87
102
|
"@types/react": "^19",
|
|
88
103
|
"@types/react-dom": "^19",
|
|
89
104
|
"eslint": "^9",
|
|
105
|
+
"next": "^16.1.6",
|
|
90
106
|
"tsup": "^8.0.0",
|
|
91
107
|
"typescript": "^5"
|
|
92
108
|
},
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "activity-feed",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Timeline-style activity feed showing user actions, comments, and file attachments with connecting lines. Supports status changes, comments with reactions, and file attachments.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/activity-feed.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { Check, Heart, MessageCircle, FileText } from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface ActivityAuthor {\n id: string;\n name: string;\n avatarUrl?: string;\n}\n\nexport interface ActivityAttachment {\n id: string;\n name: string;\n size: string;\n type?: \"document\" | \"image\" | \"other\";\n}\n\nexport interface BaseActivityItem {\n id: string;\n author: ActivityAuthor;\n timestamp: string;\n}\n\nexport interface StatusChangeActivity extends BaseActivityItem {\n type: \"status_change\";\n action: \"completed\" | \"updated\" | \"started\" | \"archived\";\n projectName: string;\n}\n\nexport interface CommentActivity extends BaseActivityItem {\n type: \"comment\";\n projectName: string;\n content: string;\n likes: number;\n replies: number;\n isLiked?: boolean;\n}\n\nexport interface AttachmentActivity extends BaseActivityItem {\n type: \"attachment\";\n action: \"completed\" | \"uploaded\" | \"shared\";\n projectName: string;\n attachment: ActivityAttachment;\n}\n\nexport type ActivityItem = StatusChangeActivity | CommentActivity | AttachmentActivity;\n\nexport interface ActivityFeedProps {\n /** Section title */\n title?: string;\n /** Section subtitle */\n subtitle?: string;\n /** Activity items to display */\n items?: ActivityItem[];\n /** Callback when like button is clicked */\n onLike?: (itemId: string) => void;\n /** Callback when reply button is clicked */\n onReply?: (itemId: string) => void;\n /** Callback when attachment is clicked */\n onAttachmentClick?: (itemId: string, attachmentId: string) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultItems: ActivityItem[] = [\n {\n id: \"1\",\n type: \"status_change\",\n author: {\n id: \"raj\",\n name: \"Raj Mishra\",\n avatarUrl: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face\",\n },\n action: \"completed\",\n projectName: \"Acme Project\",\n timestamp: \"Today at 8:15 AM\",\n },\n {\n id: \"2\",\n type: \"comment\",\n author: {\n id: \"raj\",\n name: \"Raj Mishra\",\n avatarUrl: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face\",\n },\n projectName: \"Acme Project\",\n content: \"Thank you Mary, the invoice looks great! Could you email it to Jeffrey and the Acme team and ask them to please pay by tomorrow?\",\n likes: 30,\n replies: 10,\n isLiked: true,\n timestamp: \"Yesterday at 11:25 AM\",\n },\n {\n id: \"3\",\n type: \"attachment\",\n author: {\n id: \"mary\",\n name: \"Mary Trott\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n },\n action: \"completed\",\n projectName: \"Acme Project\",\n attachment: {\n id: \"inv-1\",\n name: \"Invoice #23J2KF\",\n size: \"10 MB\",\n type: \"document\",\n },\n timestamp: \"3 days ago\",\n },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface ActivityLineProps {\n showLine: boolean;\n height?: string;\n}\n\nfunction ActivityLine({ showLine, height = \"64px\" }: ActivityLineProps) {\n if (!showLine) return null;\n return (\n <div\n style={{\n width: \"1px\",\n height,\n backgroundColor: \"var(--canvas-border-disabled)\",\n }}\n />\n );\n}\n\ninterface StatusIconProps {\n status: \"completed\" | \"updated\" | \"started\" | \"archived\";\n}\n\nfunction StatusIcon({ status }: StatusIconProps) {\n if (status === \"completed\") {\n return (\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: \"48px\",\n height: \"48px\",\n backgroundColor: \"var(--canvas-success)\",\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <Check size={20} color=\"white\" strokeWidth={2.5} />\n </div>\n );\n }\n \n // Default fallback for other statuses\n return (\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: \"48px\",\n height: \"48px\",\n backgroundColor: \"var(--canvas-surface)\",\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n />\n );\n}\n\ninterface ActivityAvatarProps {\n avatarUrl?: string;\n name: string;\n}\n\nfunction ActivityAvatar({ avatarUrl, name }: ActivityAvatarProps) {\n return (\n <Avatar\n className=\"shrink-0\"\n style={{\n width: \"48px\",\n height: \"48px\",\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={avatarUrl} alt={name} />\n <AvatarFallback>\n {name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n );\n}\n\ninterface AttachmentCardProps {\n attachment: ActivityAttachment;\n onClick?: () => void;\n}\n\nfunction AttachmentCard({ attachment, onClick }: AttachmentCardProps) {\n return (\n <div\n className=\"flex items-center cursor-pointer\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-2xl)\",\n padding: \"var(--spacing-xl)\",\n boxShadow: \"0px 1px 8px 0px rgba(0, 0, 0, 0.03)\",\n gap: \"0\",\n }}\n onClick={onClick}\n >\n {/* Icon container */}\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: \"64px\",\n height: \"64px\",\n backgroundColor: \"var(--canvas-surface-brand)\",\n border: \"1px solid var(--canvas-primary)\",\n borderRadius: \"var(--radius-md)\",\n }}\n >\n <FileText size={32} style={{ color: \"var(--canvas-primary)\" }} />\n </div>\n \n {/* File info */}\n <div\n className=\"flex flex-col justify-center\"\n style={{\n paddingLeft: \"var(--spacing-xl)\",\n paddingRight: \"var(--spacing-xl)\",\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {attachment.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-placeholder)\",\n }}\n >\n {attachment.size}\n </span>\n </div>\n </div>\n );\n}\n\ninterface CommentCardProps {\n content: string;\n likes: number;\n replies: number;\n isLiked?: boolean;\n onLike?: () => void;\n onReply?: () => void;\n}\n\nfunction CommentCard({ content, likes, replies, isLiked, onLike, onReply }: CommentCardProps) {\n return (\n <div\n className=\"flex flex-col w-full\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-md)\",\n padding: \"var(--spacing-4xl)\",\n boxShadow: \"0px 1px 8px 0px rgba(0, 0, 0, 0.03)\",\n gap: \"var(--spacing-lg)\",\n maxWidth: \"580px\",\n }}\n >\n {/* Comment content */}\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 {content}\n </p>\n \n {/* Action icons */}\n <div\n className=\"flex items-center\"\n style={{\n gap: \"var(--spacing-lg)\",\n paddingTop: \"var(--spacing-xxs)\",\n paddingBottom: \"var(--spacing-xxs)\",\n }}\n >\n <button\n onClick={onLike}\n className=\"flex items-center justify-center p-0 border-0 bg-transparent cursor-pointer\"\n >\n <Heart\n size={20}\n fill={isLiked ? \"var(--canvas-destructive)\" : \"none\"}\n color={isLiked ? \"var(--canvas-destructive)\" : \"var(--canvas-text)\"}\n />\n </button>\n <button\n onClick={onReply}\n className=\"flex items-center justify-center p-0 border-0 bg-transparent cursor-pointer\"\n >\n <MessageCircle size={20} style={{ color: \"var(--canvas-text)\" }} />\n </button>\n </div>\n \n {/* Stats */}\n <div\n className=\"flex items-start\"\n style={{\n gap: \"var(--spacing-xl)\",\n paddingTop: \"var(--spacing-xs)\",\n }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {likes} likes\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: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {replies} replies\n </span>\n </div>\n </div>\n );\n}\n\nfunction getActionText(action: string): string {\n switch (action) {\n case \"completed\":\n return \"marked\";\n case \"updated\":\n return \"updated\";\n case \"started\":\n return \"started\";\n case \"uploaded\":\n return \"uploaded\";\n case \"shared\":\n return \"shared\";\n default:\n return action;\n }\n}\n\nfunction getActionSuffix(action: string): string {\n switch (action) {\n case \"completed\":\n return \"as complete\";\n case \"updated\":\n return \"\";\n case \"started\":\n return \"\";\n default:\n return \"\";\n }\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Activity Feed Block\n * \n * A timeline-style activity feed showing user actions, comments, and file\n * attachments with connecting lines. Useful for project updates, notifications,\n * and collaboration views.\n * \n * @example\n * ```tsx\n * <ActivityFeed\n * title=\"Project status\"\n * subtitle=\"Last updated today\"\n * items={activityItems}\n * onLike={(id) => console.log(\"Liked\", id)}\n * />\n * ```\n */\nexport function ActivityFeed({\n title = \"Project status\",\n subtitle = \"Last updated today\",\n items = defaultItems,\n onLike,\n onReply,\n onAttachmentClick,\n className,\n}: ActivityFeedProps) {\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 overflow-hidden\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <div \n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {subtitle}\n </p>\n </div>\n </div>\n\n {/* Activity List */}\n <div className=\"flex flex-col w-full overflow-hidden\">\n {items.map((item, index) => {\n const isLast = index === items.length - 1;\n \n return (\n <div\n key={item.id}\n className=\"flex flex-col w-full\"\n style={{\n paddingTop: index === 0 ? \"0\" : \"var(--spacing-xl)\",\n paddingBottom: isLast ? \"0\" : \"var(--spacing-xl)\",\n }}\n >\n <div\n className=\"flex w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Left column - Avatar/Icon with line */}\n <div\n className=\"flex flex-col items-center shrink-0\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {item.type === \"status_change\" ? (\n <StatusIcon status={item.action} />\n ) : (\n <ActivityAvatar\n avatarUrl={item.author.avatarUrl}\n name={item.author.name}\n />\n )}\n <ActivityLine showLine={!isLast} height={item.type === \"comment\" ? \"100%\" : \"64px\"} />\n </div>\n\n {/* Right column - Content */}\n <div\n className=\"flex flex-col flex-1 min-w-0\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n {/* Activity header row */}\n <div\n className=\"flex flex-col justify-center\"\n style={{\n minHeight: \"48px\",\n gap: \"0\",\n }}\n >\n {/* Title line */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n margin: 0,\n }}\n >\n <span\n style={{\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {item.author.name}\n </span>\n {\" \"}\n <span\n style={{\n fontWeight: \"var(--typo-body-s-weight)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {item.type === \"comment\" ? \"comments on\" : getActionText((item as StatusChangeActivity | AttachmentActivity).action)}\n </span>\n {\" \"}\n <span\n style={{\n fontWeight: \"var(--typo-body-s-weight)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.type === \"comment\" \n ? (item as CommentActivity).projectName\n : (item as StatusChangeActivity | AttachmentActivity).projectName\n }\n </span>\n {item.type !== \"comment\" && getActionSuffix((item as StatusChangeActivity | AttachmentActivity).action) && (\n <>\n {\" \"}\n <span\n style={{\n fontWeight: \"var(--typo-body-s-weight)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {getActionSuffix((item as StatusChangeActivity | AttachmentActivity).action)}\n </span>\n </>\n )}\n </p>\n \n {/* Timestamp */}\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-placeholder)\",\n margin: 0,\n }}\n >\n {item.timestamp}\n </p>\n </div>\n\n {/* Additional content based on type */}\n {item.type === \"comment\" && (\n <CommentCard\n content={(item as CommentActivity).content}\n likes={(item as CommentActivity).likes}\n replies={(item as CommentActivity).replies}\n isLiked={(item as CommentActivity).isLiked}\n onLike={() => onLike?.(item.id)}\n onReply={() => onReply?.(item.id)}\n />\n )}\n\n {item.type === \"attachment\" && (\n <AttachmentCard\n attachment={(item as AttachmentActivity).attachment}\n onClick={() => onAttachmentClick?.(item.id, (item as AttachmentActivity).attachment.id)}\n />\n )}\n </div>\n </div>\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"lucide-react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"utils",
|
|
17
|
+
"avatar"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "blog-cards",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Grid of blog post cards with images, titles, dates.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/marketing/blog-cards.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport { ArrowRight } from \"@phosphor-icons/react\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface BlogPost {\n id: string;\n title: string;\n description: string;\n image: string;\n}\n\nconst defaultPosts: BlogPost[] = [\n {\n id: \"1\",\n title: \"Top destinations to visit\",\n description: \"Discover the most exciting and must-visit travel spots around the world, from iconic cities to hidden gems.\",\n image: \"https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=400&h=340&fit=crop\",\n },\n {\n id: \"2\",\n title: \"Breathtaking natural wonders\",\n description: \"Explore the world's most awe-inspiring landscapes, from towering mountains to stunning waterfalls and surreal deserts.\",\n image: \"https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=340&fit=crop\",\n },\n {\n id: \"3\",\n title: \"Must-try dishes in Italy\",\n description: \"Indulge in Italy's most delicious and authentic dishes, from creamy pasta to crispy pizzas and decadent desserts.\",\n image: \"https://images.unsplash.com/photo-1498579150354-977475b7ea0b?w=400&h=340&fit=crop\",\n },\n];\n\ninterface BlogCardsProps {\n subtitle?: string;\n title?: string;\n posts?: BlogPost[];\n}\n\nexport function BlogCards({ \n subtitle = \"OUR BLOG\",\n title = \"Get inspired\",\n posts = defaultPosts,\n}: BlogCardsProps) {\n return (\n <section \n className=\"w-full px-4 md:px-8 lg:px-20 py-16 md:py-24\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n <div className=\"w-full max-w-[1240px] mx-auto flex flex-col gap-16\">\n {/* Header */}\n <div className=\"flex flex-col gap-3 max-w-[768px]\">\n <Typography variant=\"body-s\" as=\"p\" color=\"muted\">\n {subtitle}\n </Typography>\n <Typography variant=\"h3\" as=\"h2\">\n {title}\n </Typography>\n </div>\n\n {/* Cards Grid */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10\">\n {posts.map((post) => (\n <div \n key={post.id}\n className=\"flex flex-col overflow-hidden cursor-pointer group\"\n style={{\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-xl)\",\n }}\n >\n {/* Image */}\n <div \n className=\"w-full h-[340px] overflow-hidden\"\n style={{\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n >\n <img \n src={post.image} \n alt={post.title}\n className=\"w-full h-full object-cover group-hover:scale-105 transition-transform duration-300\"\n />\n </div>\n \n {/* Content */}\n <div \n className=\"flex flex-col gap-1 p-6\"\n >\n <Typography variant=\"body-xl\" as=\"h3\" style={{ fontWeight: 600 }}>\n {post.title}\n </Typography>\n <Typography variant=\"body-m\" color=\"muted\">\n {post.description}\n </Typography>\n \n {/* Read more */}\n <div className=\"flex items-center gap-2 pt-5\">\n <Typography variant=\"body-m\" as=\"span\" style={{ fontWeight: 500 }}>\n Read more\n </Typography>\n <div \n className=\"flex items-center justify-center\"\n style={{\n width: \"24px\",\n height: \"24px\",\n backgroundColor: \"var(--canvas-text)\",\n borderRadius: \"var(--radius-4xl)\",\n }}\n >\n <ArrowRight size={16} color=\"white\" weight=\"bold\" />\n </div>\n </div>\n </div>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"@phosphor-icons/react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": []
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bottom-input-chat-widget",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Slack-style chat widget with message threads list and conversation view. Features bottom input bar with attachment support.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/bottom-input-chat-widget.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { ChevronLeft, ChevronRight, MoreVertical, Paperclip } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar\";\nimport { cn } from \"../../lib/utils\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface ChatThread {\n id: string;\n name: string;\n avatarUrl?: string;\n preview: string;\n unreadCount?: number;\n}\n\nexport interface ChatMessageData {\n id: string;\n senderName: string;\n senderAvatar?: string;\n content: string;\n timestamp: string;\n}\n\ninterface MessageThreadItemProps {\n thread: ChatThread;\n onClick?: () => void;\n}\n\ninterface ConversationMessageProps {\n message: ChatMessageData;\n}\n\ninterface ChatInputBarProps {\n placeholder?: string;\n onSend?: (message: string) => void;\n}\n\ninterface ConversationHeaderProps {\n name: string;\n onBackClick?: () => void;\n onMenuClick?: () => void;\n}\n\nexport interface BottomInputChatWidgetProps {\n variant?: \"threads\" | \"conversation\";\n title?: string;\n threads?: ChatThread[];\n messages?: ChatMessageData[];\n conversationName?: string;\n onThreadClick?: (threadId: string) => void;\n onBackClick?: () => void;\n onSend?: (message: string) => void;\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultThreads: ChatThread[] = [\n {\n id: \"1\",\n name: \"Jeffrey Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face\",\n preview: \"That's true. Let's go ahead and book the mountain cabin then!\",\n unreadCount: 12,\n },\n {\n id: \"2\",\n name: \"Mary Trott\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n preview: \"Sounds great! Let's meet at 2PM.\",\n },\n {\n id: \"3\",\n name: \"Taylor Reed\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n preview: \"Thanks for sending the conference agenda. I'll add my comments in the document.\",\n },\n];\n\nconst defaultMessages: ChatMessageData[] = [\n {\n id: \"1\",\n senderName: \"Jeffrey Connor\",\n senderAvatar: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face\",\n content: \"That's true. Let's go ahead and book the mountain cabin then!\",\n timestamp: \"Feb 23, 1:32 PM\",\n },\n {\n id: \"2\",\n senderName: \"Mary Trott\",\n senderAvatar: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n content: \"Sounds great! Let's meet at 2PM.\",\n timestamp: \"Feb 23, 3:50 PM\",\n },\n {\n id: \"3\",\n senderName: \"Taylor Reed\",\n senderAvatar: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n content: \"Thanks for sending the conference agenda. I'll add my comments in the document.\",\n timestamp: \"Feb 23, 6:13 PM\",\n },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\nfunction MessageThreadItem({ thread, onClick }: MessageThreadItemProps) {\n const initials = thread.name\n .split(\" \")\n .map((n) => n[0])\n .join(\"\");\n\n return (\n <div\n className=\"flex items-start gap-[var(--spacing-xl)] py-[var(--spacing-2xl)] cursor-pointer hover:opacity-80 transition-opacity\"\n style={{\n borderTop: \"1px solid var(--canvas-border)\",\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n onClick={onClick}\n >\n {/* Avatar */}\n <Avatar\n className=\"size-12 shrink-0\"\n style={{\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={thread.avatarUrl} alt={thread.name} />\n <AvatarFallback\n style={{\n backgroundColor: \"var(--canvas-surface)\",\n color: \"var(--canvas-text-muted)\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n }}\n >\n {initials}\n </AvatarFallback>\n </Avatar>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\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 }}\n >\n {thread.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: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {thread.preview}\n </p>\n </div>\n\n {/* Right side: unread badge + chevron */}\n <div className=\"flex items-center gap-[var(--spacing-xl)] self-stretch shrink-0\">\n {thread.unreadCount && thread.unreadCount > 0 && (\n <div\n className=\"flex items-center justify-center size-10 rounded-full\"\n style={{\n backgroundColor: \"var(--canvas-primary)\",\n }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary-foreground)\",\n }}\n >\n {thread.unreadCount}\n </span>\n </div>\n )}\n <ChevronRight\n className=\"size-10\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n strokeWidth={1.5}\n />\n </div>\n </div>\n );\n}\n\nfunction ConversationMessage({ message }: ConversationMessageProps) {\n const initials = message.senderName\n .split(\" \")\n .map((n) => n[0])\n .join(\"\");\n\n return (\n <div className=\"flex items-start gap-[var(--spacing-xl)] py-[var(--spacing-2xl)]\">\n {/* Avatar */}\n <Avatar\n className=\"size-12 shrink-0\"\n style={{\n borderRadius: \"var(--radius-xs)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={message.senderAvatar} alt={message.senderName} />\n <AvatarFallback\n style={{\n backgroundColor: \"var(--canvas-surface)\",\n color: \"var(--canvas-text-muted)\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n borderRadius: \"var(--radius-xs)\",\n }}\n >\n {initials}\n </AvatarFallback>\n </Avatar>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-[var(--spacing-xl)]\">\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 {message.senderName}\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: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {message.timestamp}\n </span>\n </div>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {message.content}\n </p>\n </div>\n </div>\n );\n}\n\nfunction ConversationHeader({ name, onBackClick, onMenuClick }: ConversationHeaderProps) {\n return (\n <div className=\"flex items-center gap-[var(--spacing-xs)]\">\n <button\n onClick={onBackClick}\n className=\"size-10 flex items-center justify-center hover:opacity-70 transition-opacity\"\n aria-label=\"Go back\"\n >\n <ChevronLeft\n className=\"size-6\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n strokeWidth={1.5}\n />\n </button>\n <span\n className=\"flex-1\"\n style={{\n fontFamily: \"var(--typo-body-xl-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n lineHeight: \"1.5\",\n color: \"var(--canvas-text)\",\n }}\n >\n {name}\n </span>\n <button\n onClick={onMenuClick}\n className=\"size-8 flex items-center justify-center hover:opacity-70 transition-opacity rounded-full\"\n aria-label=\"More options\"\n >\n <MoreVertical\n className=\"size-5\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n />\n </button>\n </div>\n );\n}\n\nfunction ChatInputBar({ placeholder = \"Send a message\", onSend }: ChatInputBarProps) {\n const [value, setValue] = useState(\"\");\n\n const handleSend = () => {\n if (value.trim() && onSend) {\n onSend(value.trim());\n setValue(\"\");\n }\n };\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSend();\n }\n };\n\n return (\n <div\n className=\"flex items-center gap-[var(--spacing-xl)] h-20\"\n style={{\n borderTop: \"1px solid var(--canvas-border)\",\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n >\n {/* Input field */}\n <div className=\"flex-1 flex items-center gap-[var(--spacing-md)] h-11\">\n <input\n type=\"text\"\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n className=\"flex-1 bg-transparent outline-none\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n />\n <button\n className=\"size-6 flex items-center justify-center hover:opacity-70 transition-opacity\"\n aria-label=\"Attach file\"\n >\n <Paperclip\n className=\"size-6\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n />\n </button>\n </div>\n\n {/* Send button */}\n <button\n onClick={handleSend}\n className=\"h-11 px-[var(--spacing-xl)] flex items-center justify-center transition-colors hover:opacity-90\"\n style={{\n backgroundColor: \"var(--btn-primary-bg)\",\n color: \"var(--btn-primary-text)\",\n borderRadius: \"var(--radius-xs)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\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 }}\n >\n Send\n </button>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\nexport function BottomInputChatWidget({\n variant = \"threads\",\n title = \"Messages\",\n threads = defaultThreads,\n messages = defaultMessages,\n conversationName = \"Mary Trott\",\n onThreadClick,\n onBackClick,\n onSend,\n className,\n}: BottomInputChatWidgetProps) {\n if (variant === \"threads\") {\n return (\n <div\n className={cn(\n \"flex flex-col gap-[var(--spacing-3xl)] py-[var(--spacing-xl)]\",\n className\n )}\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n {/* Title */}\n <div className=\"flex flex-col gap-[var(--spacing-xl)]\">\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 lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n </div>\n\n {/* Thread list */}\n <div className=\"flex flex-col\">\n {threads.map((thread, index) => (\n <div\n key={thread.id}\n style={{\n borderTop: index === 0 ? \"1px solid var(--canvas-border)\" : \"none\",\n }}\n >\n <MessageThreadItem\n thread={thread}\n onClick={() => onThreadClick?.(thread.id)}\n />\n </div>\n ))}\n </div>\n </div>\n );\n }\n\n // Conversation variant\n return (\n <div\n className={cn(\n \"flex flex-col gap-[var(--spacing-3xl)] py-[var(--spacing-xl)]\",\n className\n )}\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n {/* Messages section */}\n <div className=\"flex flex-col gap-[var(--spacing-xl)]\">\n {/* Header */}\n <ConversationHeader\n name={conversationName}\n onBackClick={onBackClick}\n />\n\n {/* Messages list */}\n <div className=\"flex flex-col\">\n {messages.map((message, index) => (\n <div\n key={message.id}\n style={{\n borderTop: index === 0 ? \"1px solid var(--canvas-border)\" : \"none\",\n }}\n >\n <ConversationMessage message={message} />\n </div>\n ))}\n </div>\n\n {/* Input bar */}\n <ChatInputBar onSend={onSend} />\n </div>\n </div>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"lucide-react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"avatar",
|
|
17
|
+
"utils"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "canvas-item",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Wrapper for components placed on the infinity canvas. Handles positioning and selection.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/canvas-item.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { X, GripHorizontal } from \"lucide-react\";\n\nexport interface CanvasItemData {\n id: string;\n componentType: string;\n x: number;\n y: number;\n width?: number;\n height?: number;\n}\n\ninterface CanvasItemProps {\n item: CanvasItemData;\n isSelected: boolean;\n onSelect: (id: string) => void;\n onDelete: (id: string) => void;\n onDragStart: (id: string, startX: number, startY: number, itemX: number, itemY: number) => void;\n scale: number;\n children: React.ReactNode;\n}\n\n/**\n * Canvas Item - Wrapper for components placed on the infinity canvas\n * \n * Handles:\n * - Absolute positioning based on x, y coordinates\n * - Selection state with visual border\n * - Drag handle for repositioning\n * - Delete button when selected\n */\nexport function CanvasItem({\n item,\n isSelected,\n onSelect,\n onDelete,\n onDragStart,\n scale,\n children,\n}: CanvasItemProps) {\n const [isDragging, setIsDragging] = useState(false);\n\n const handleDragHandleMouseDown = (e: React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setIsDragging(true);\n onSelect(item.id);\n // Pass the mouse start position and current item position\n onDragStart(item.id, e.clientX, e.clientY, item.x, item.y);\n };\n\n const handleClick = (e: React.MouseEvent) => {\n e.stopPropagation();\n onSelect(item.id);\n };\n\n const handleDelete = (e: React.MouseEvent) => {\n e.stopPropagation();\n onDelete(item.id);\n };\n\n return (\n <div\n className={cn(\n \"absolute group\",\n \"transition-shadow duration-150\",\n isSelected && \"ring-2 ring-[var(--canvas-primary)] ring-offset-2\",\n isDragging && \"opacity-90 shadow-2xl z-50\"\n )}\n style={{\n left: item.x,\n top: item.y,\n }}\n onClick={handleClick}\n >\n {/* Control bar - visible on hover or when selected */}\n <div\n className={cn(\n \"absolute -top-9 left-0 right-0 flex items-center justify-between px-2 py-1.5\",\n \"bg-[var(--canvas-text)] rounded-t-md\",\n \"opacity-0 group-hover:opacity-100 transition-opacity\",\n isSelected && \"opacity-100\"\n )}\n >\n {/* Drag handle - this is what you grab to drag */}\n <div\n onMouseDown={handleDragHandleMouseDown}\n className=\"flex items-center gap-1.5 text-white/90 text-xs cursor-grab active:cursor-grabbing select-none\"\n >\n <GripHorizontal className=\"size-4\" />\n <span className=\"text-[11px] font-medium\">{item.componentType}</span>\n </div>\n\n {/* Delete button */}\n <button\n onClick={handleDelete}\n className=\"p-1 rounded hover:bg-white/20 text-white/80 hover:text-white transition-colors\"\n aria-label=\"Delete component\"\n >\n <X className=\"size-3.5\" />\n </button>\n </div>\n\n {/* Component content */}\n <div className=\"pointer-events-none\">\n {children}\n </div>\n </div>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"lucide-react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"utils"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "category-grid",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Grid of category cards with icons/images.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/marketing/category-grid.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport { \n Heart, Star, Sun, CurrencyDollar, Smiley, \n Image, Coffee, Moon, Clock, MapPin \n} from \"@phosphor-icons/react\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface CategoryItem {\n id: string;\n title: string;\n count: string;\n icon: React.ReactNode;\n}\n\nconst defaultCategories: CategoryItem[] = [\n { id: \"1\", title: \"Most popular\", count: \"5,000 homes\", icon: <Heart size={48} /> },\n { id: \"2\", title: \"Top rated\", count: \"5,000 homes\", icon: <Star size={48} /> },\n { id: \"3\", title: \"Unique stays\", count: \"5,000 homes\", icon: <Sun size={48} /> },\n { id: \"4\", title: \"Affordable\", count: \"5,000 homes\", icon: <CurrencyDollar size={48} /> },\n { id: \"5\", title: \"Friendly staff\", count: \"5,000 homes\", icon: <Smiley size={48} /> },\n { id: \"6\", title: \"Best views\", count: \"5,000 homes\", icon: <Image size={48} /> },\n { id: \"7\", title: \"Cafes\", count: \"5,000 homes\", icon: <Coffee size={48} /> },\n { id: \"8\", title: \"Night life\", count: \"5,000 homes\", icon: <Moon size={48} /> },\n { id: \"9\", title: \"Open 24 hours\", count: \"5,000 homes\", icon: <Clock size={48} /> },\n { id: \"10\", title: \"Best locations\", count: \"5,000 homes\", icon: <MapPin size={48} /> },\n];\n\ninterface CategoryGridProps {\n title?: string;\n categories?: CategoryItem[];\n}\n\nexport function CategoryGrid({ \n title = \"Browse by category\", \n categories = defaultCategories \n}: CategoryGridProps) {\n return (\n <section \n className=\"w-full px-4 md:px-8 lg:px-10 py-10 md:py-16\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n <div className=\"w-full max-w-[1240px] mx-auto\">\n {/* Header */}\n <Typography variant=\"h3\" as=\"h2\" style={{ marginBottom: \"var(--spacing-6xl)\" }}>\n {title}\n </Typography>\n\n {/* Categories Grid */}\n <div className=\"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-8\">\n {categories.map((category) => (\n <div \n key={category.id}\n className=\"flex flex-col items-center justify-center text-center cursor-pointer hover:shadow-md transition-shadow\"\n style={{\n height: \"158px\",\n padding: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--spacing-md)\",\n gap: \"var(--spacing-md)\",\n boxShadow: \"0px 1px 8px 0px rgba(0, 0, 0, 0.03)\",\n }}\n >\n <div style={{ color: \"var(--canvas-text)\" }}>\n {category.icon}\n </div>\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-xxs)\" }}>\n <Typography variant=\"body-xl\" className=\"font-semibold\">\n {category.title}\n </Typography>\n <Typography variant=\"body-m\" color=\"muted\">\n {category.count}\n </Typography>\n </div>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"@phosphor-icons/react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": []
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "centered-hero",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Simple centered hero for about/landing pages.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/marketing/centered-hero.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport { Typography } from \"../../ui/typography\";\n\ninterface CenteredHeroProps {\n title?: string;\n subtitle?: string;\n image?: string;\n}\n\nexport function CenteredHero({\n title = \"We're democratizing software development\",\n subtitle = \"We are committed to developing cutting-edge solutions that help our customers achieve their goals\",\n image = \"https://images.unsplash.com/photo-1497366216548-37526070297c?w=1200&h=700&fit=crop\",\n}: CenteredHeroProps) {\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 items-center gap-10 md:gap-16\">\n {/* Header Content */}\n <div className=\"flex flex-col items-center gap-4 max-w-[768px] text-center\">\n <Typography variant=\"h1\" as=\"h1\">\n {title}\n </Typography>\n <Typography variant=\"body-xl\" color=\"muted\">\n {subtitle}\n </Typography>\n </div>\n\n {/* Featured Image */}\n <div\n className=\"w-full max-w-[992px] rounded-xl overflow-hidden\"\n style={{\n border: \"1.8px solid var(--canvas-border)\",\n boxShadow: \"0px 4px 96px 0px rgba(0,0,0,0.08)\",\n }}\n >\n <img\n src={image}\n alt=\"Featured\"\n className=\"w-full h-[300px] md:h-[500px] lg:h-[590px] object-cover\"\n />\n </div>\n </div>\n </section>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [],
|
|
13
|
+
"registryDependencies": []
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chat-message",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/chat-message.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport { useRef } from \"react\";\nimport { Image, Paperclip, X } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar\";\n\nexport interface ChatBubbleMessage {\n id: string;\n content: string;\n timestamp: string;\n isSent: boolean;\n senderName?: string;\n senderAvatar?: string;\n}\n\ninterface ChatBubbleProps {\n message: ChatBubbleMessage;\n}\n\nexport function ChatBubble({ message }: ChatBubbleProps) {\n const { content, isSent, senderAvatar, senderName } = message;\n\n if (isSent) {\n return (\n <div className=\"flex justify-end\">\n <div\n className=\"max-w-[375px] p-[var(--spacing-2xl)] rounded-tl-[var(--radius-xl)] rounded-tr-[var(--radius-xl)] rounded-bl-[var(--radius-xl)]\"\n style={{\n backgroundColor: \"var(--canvas-primary)\",\n color: \"var(--canvas-primary-foreground)\",\n }}\n >\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {content}\n </p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex items-end gap-[var(--spacing-md)]\">\n <Avatar className=\"size-8 shrink-0\">\n <AvatarImage src={senderAvatar} alt={senderName} />\n <AvatarFallback\n className=\"text-[8px]\"\n style={{\n backgroundColor: \"var(--canvas-subtle)\",\n color: \"var(--canvas-muted-foreground)\",\n }}\n >\n {senderName\n ?.split(\" \")\n .map((n) => n[0])\n .join(\"\")}\n </AvatarFallback>\n </Avatar>\n <div\n className=\"max-w-[375px] p-[var(--spacing-2xl)] rounded-tl-[var(--radius-xl)] rounded-tr-[var(--radius-xl)] rounded-br-[var(--radius-xl)]\"\n style={{\n backgroundColor: \"var(--canvas-border)\",\n color: \"var(--canvas-foreground)\",\n }}\n >\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {content}\n </p>\n </div>\n </div>\n );\n}\n\ninterface ChatDateSeparatorProps {\n date: string;\n}\n\nexport function ChatDateSeparator({ date }: ChatDateSeparatorProps) {\n return (\n <div className=\"flex items-center justify-center\">\n <span\n className=\"text-xs font-medium\"\n style={{\n color: \"var(--canvas-muted-foreground)\",\n }}\n >\n {date}\n </span>\n </div>\n );\n}\n\ninterface AttachmentPill {\n id: string;\n name: string;\n type: \"image\" | \"file\";\n url?: string;\n}\n\ninterface MessengerInputProps {\n attachments?: AttachmentPill[];\n onRemoveAttachment?: (id: string) => void;\n onSend?: () => void;\n}\n\nexport function MessengerInput({\n attachments = [],\n onRemoveAttachment,\n onSend,\n}: MessengerInputProps) {\n const fileInputRef = useRef<HTMLInputElement>(null);\n\n const handleAttachmentClick = () => {\n fileInputRef.current?.click();\n };\n\n return (\n <div\n className=\"border\"\n style={{\n borderColor: \"var(--canvas-border)\",\n backgroundColor: \"var(--canvas-background)\",\n borderRadius: \"var(--radius-md)\",\n }}\n >\n {/* Textarea */}\n <textarea\n placeholder=\"Send a message\"\n rows={2}\n className=\"w-full p-3 resize-none focus:outline-none text-xs\"\n style={{\n backgroundColor: \"transparent\",\n color: \"var(--canvas-foreground)\",\n lineHeight: \"var(--typo-body-xs-line-height)\",\n }}\n />\n\n {/* Attachments Row */}\n {attachments.length > 0 && (\n <div\n className=\"flex flex-wrap gap-2 px-3 py-2 border-t\"\n style={{ borderColor: \"var(--canvas-border)\" }}\n >\n {attachments.map((attachment) => (\n <a\n key={attachment.id}\n href={attachment.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"relative flex items-center gap-[var(--spacing-md)] h-8 px-[var(--spacing-md)] rounded-[var(--radius-xs)] border cursor-pointer hover:opacity-80 transition-opacity\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n borderColor: \"var(--canvas-border)\",\n }}\n >\n {attachment.type === \"image\" ? (\n <Image\n className=\"size-5\"\n style={{ color: \"var(--canvas-primary)\" }}\n />\n ) : (\n <Paperclip\n className=\"size-5\"\n style={{ color: \"var(--canvas-primary)\" }}\n />\n )}\n <span\n className=\"text-xs\"\n style={{ color: \"var(--canvas-muted-foreground)\" }}\n >\n {attachment.name}\n </span>\n <button\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n onRemoveAttachment?.(attachment.id);\n }}\n className=\"absolute -top-1 -right-1 size-4 rounded-full flex items-center justify-center hover:opacity-70\"\n style={{ backgroundColor: \"var(--canvas-muted-foreground)\" }}\n >\n <X className=\"size-2\" style={{ color: \"white\" }} />\n </button>\n </a>\n ))}\n </div>\n )}\n\n {/* Bottom Bar */}\n <div\n className=\"flex items-center justify-between px-[var(--spacing-xl)] py-[var(--spacing-lg)] border-t\"\n style={{ borderColor: \"var(--canvas-border)\" }}\n >\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\"image/*\"\n className=\"hidden\"\n aria-hidden=\"true\"\n />\n <button\n onClick={handleAttachmentClick}\n className=\"hover:opacity-70\"\n aria-label=\"Add attachment\"\n >\n <Paperclip\n className=\"size-5\"\n style={{ color: \"var(--canvas-muted-foreground)\" }}\n />\n </button>\n <button\n onClick={onSend}\n className=\"h-10 px-[var(--spacing-lg)] rounded-[var(--radius-xs)] font-semibold text-xs transition-colors hover:opacity-90\"\n style={{\n backgroundColor: \"var(--canvas-primary)\",\n color: \"var(--canvas-primary-foreground)\",\n }}\n >\n Send\n </button>\n </div>\n </div>\n );\n}\n\n// Alias for video chat compatibility\nexport const ChatInput = MessengerInput;\n\n// Simple chat message list for video chat\ninterface ChatMessageListProps {\n messages?: ChatBubbleMessage[];\n className?: string;\n}\n\nexport function ChatMessageList({ messages = [], className }: ChatMessageListProps) {\n return (\n <div className={className}>\n {messages.map((message) => (\n <ChatBubble key={message.id} message={message} />\n ))}\n </div>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"lucide-react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"avatar"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "circular-progress-bar-list",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "List block with circular progress indicators showing completion percentages. Features header with sort/filter controls and item action menus.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/circular-progress-bar-list.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\nimport { MenufocusTemplate } from \"./menufocus-template\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface ProgressListItem {\n id: string;\n title: string;\n description: string;\n /** Progress percentage (0-100) */\n progress: number;\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 CircularProgressBarProps {\n /** Progress percentage (0-100) */\n progress: number;\n /** Size of the progress bar in pixels */\n size?: number;\n /** Stroke width of the progress arc */\n strokeWidth?: number;\n /** Additional class names */\n className?: string;\n}\n\nexport interface CircularProgressBarListProps {\n /** List 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 /** List items data */\n items?: ProgressListItem[];\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: FilterOption[];\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 item action is clicked */\n onItemAction?: (action: string, item: ProgressListItem) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultItems: ProgressListItem[] = [\n {\n id: \"1\",\n title: \"Product Design 101\",\n description: \"From ideation to prototyping, learn how to turn innovative ideas into tangible products that meet user needs and market demands.\",\n progress: 50,\n },\n {\n id: \"2\",\n title: \"Product Analytics\",\n description: \"Dive deep into understanding user behavior, optimizing features, and maximizing product performance through insightful data analysis and experimentation methodologies.\",\n progress: 75,\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"progress-asc\", label: \"Progress (Low to High)\" },\n { id: \"progress-desc\", label: \"Progress (High to Low)\" },\n { id: \"name-asc\", label: \"Name (A-Z)\" },\n { id: \"name-desc\", label: \"Name (Z-A)\" },\n];\n\nconst defaultFilterOptions: FilterOption[] = [\n { id: \"all\", label: \"All courses\" },\n { id: \"in-progress\", label: \"In progress\" },\n { id: \"completed\", label: \"Completed\" },\n { id: \"not-started\", label: \"Not started\" },\n];\n\n// ============================================\n// CircularProgressBar Component\n// ============================================\n\n/**\n * Canvas Design System - Circular Progress Bar\n * \n * An SVG-based circular progress indicator that displays a percentage.\n * Uses CSS variables for theming support.\n */\nexport function CircularProgressBar({\n progress,\n size = 80,\n strokeWidth = 8,\n className,\n}: CircularProgressBarProps) {\n // Clamp progress between 0 and 100\n const clampedProgress = Math.min(100, Math.max(0, progress));\n \n // Calculate SVG dimensions\n const radius = (size - strokeWidth) / 2;\n const circumference = 2 * Math.PI * radius;\n const offset = circumference - (clampedProgress / 100) * circumference;\n const center = size / 2;\n\n return (\n <div \n className={cn(\"relative inline-flex items-center justify-center\", className)}\n style={{ width: size, height: size }}\n >\n <svg\n width={size}\n height={size}\n viewBox={`0 0 ${size} ${size}`}\n className=\"transform -rotate-90\"\n >\n {/* Background circle (track) */}\n <circle\n cx={center}\n cy={center}\n r={radius}\n fill=\"none\"\n stroke=\"var(--progress-bar-track-color)\"\n strokeWidth={strokeWidth}\n />\n {/* Progress arc (fill) */}\n <circle\n cx={center}\n cy={center}\n r={radius}\n fill=\"none\"\n stroke=\"var(--progress-bar-fill-color)\"\n strokeWidth={strokeWidth}\n strokeDasharray={circumference}\n strokeDashoffset={offset}\n strokeLinecap=\"round\"\n style={{\n transition: \"stroke-dashoffset 0.3s ease-in-out\",\n }}\n />\n </svg>\n {/* Percentage text */}\n <span\n className=\"absolute inset-0 flex items-center justify-center\"\n style={{\n fontFamily: \"var(--typo-body-xl-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n lineHeight: 1,\n color: \"var(--progress-bar-text-color)\",\n }}\n >\n {Math.round(clampedProgress)}%\n </span>\n </div>\n );\n}\n\n// ============================================\n// CircularProgressBarList Component\n// ============================================\n\n/**\n * Canvas Design System - Circular Progress Bar List Block\n * \n * A list block displaying items with circular progress indicators.\n * Features a header section with title, result count, and sort/filter controls.\n * Each item shows a progress bar, title, description, and action menu.\n * \n * @example\n * ```tsx\n * <CircularProgressBarList\n * title=\"Courses\"\n * items={[\n * { id: \"1\", title: \"Design 101\", description: \"Learn design basics\", progress: 50 }\n * ]}\n * />\n * ```\n */\nexport function CircularProgressBarList({\n title = \"Courses\",\n resultCount,\n resultCountText,\n items = defaultItems,\n sortOptions = defaultSortOptions,\n filterOptions = defaultFilterOptions,\n onSort,\n onFilter,\n onItemAction,\n className,\n}: CircularProgressBarListProps) {\n const [sortValue, setSortValue] = useState<string>(\"\");\n const [filterValue, setFilterValue] = useState<string>(\"\");\n\n const displayResultCount = resultCount ?? items.length;\n const displayResultText = resultCountText ?? `${displayResultCount} results`;\n\n const handleSortChange = (value: string) => {\n setSortValue(value);\n onSort?.(value);\n };\n\n const handleFilterChange = (value: string) => {\n setFilterValue(value);\n onFilter?.(value);\n };\n\n return (\n <div \n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-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 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\"\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 </div>\n </div>\n\n {/* List Section */}\n <div \n className=\"flex flex-col w-full\"\n style={{ \n borderTop: \"1px solid var(--canvas-border)\",\n }}\n >\n {items.map((item, index) => (\n <div\n key={item.id}\n className=\"flex w-full\"\n style={{\n borderBottom: \"1px solid var(--canvas-border)\",\n padding: \"var(--spacing-3xl) 0\",\n gap: \"var(--spacing-3xl)\",\n }}\n >\n {/* Progress Bar */}\n <div className=\"shrink-0\">\n <CircularProgressBar\n progress={item.progress}\n size={80}\n strokeWidth={8}\n />\n </div>\n\n {/* Content */}\n <div \n className=\"flex flex-col flex-1 min-w-0\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n {/* Title Row with Actions */}\n <div \n className=\"flex items-start w-full\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n <h3\n className=\"flex-1 min-w-0\"\n style={{\n fontFamily: \"var(--typo-body-xl-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-xl-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {item.title}\n </h3>\n <div className=\"shrink-0\">\n <MenufocusTemplate\n ariaLabel=\"Item actions\"\n items={[\n { id: \"view\", label: \"View details\", onClick: () => onItemAction?.(\"view\", item) },\n { id: \"continue\", label: \"Continue\", onClick: () => onItemAction?.(\"continue\", item) },\n { id: \"reset\", label: \"Reset progress\", variant: \"destructive\", onClick: () => onItemAction?.(\"reset\", item) },\n ]}\n />\n </div>\n </div>\n\n {/* Description */}\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 {item.description}\n </p>\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [],
|
|
13
|
+
"registryDependencies": [
|
|
14
|
+
"utils",
|
|
15
|
+
"select",
|
|
16
|
+
"menufocus-template"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "component-palette",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Sidebar listing draggable components grouped by category. Used with InfinityCanvas.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/component-palette.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport { useDraggable } from \"@dnd-kit/core\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { cn } from \"../../lib/utils\";\nimport { \n ChevronDown, \n ChevronRight,\n Layout,\n LayoutGrid,\n MessageSquare,\n Megaphone,\n CreditCard,\n User,\n Table,\n List,\n Image,\n Type,\n Video,\n Search,\n Settings,\n LogIn,\n Phone,\n ShoppingCart,\n FileText,\n Square,\n CheckSquare,\n Calendar,\n ToggleLeft,\n CircleDot,\n Hash,\n SlidersHorizontal,\n Tags,\n Star,\n MapPin,\n Users,\n Play,\n Newspaper,\n Building,\n Award,\n Layers,\n} from \"lucide-react\";\nimport { ScrollArea } from \"../ui/scroll-area\";\n\n// Component definitions for the palette\nexport interface PaletteComponent {\n id: string;\n type: string;\n label: string;\n icon: React.ReactNode;\n category: string;\n}\n\nconst paletteComponents: PaletteComponent[] = [\n // =====================\n // PAGE TEMPLATES\n // =====================\n { id: \"page-about\", type: \"PageAbout\", label: \"About\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-account\", type: \"PageAccount\", label: \"Account Settings\", icon: <Settings className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-admin-portal\", type: \"PageAdminPortal\", label: \"Admin Portal\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-centered-profile\", type: \"PageCenteredProfile\", label: \"Centered Profile\", icon: <User className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-double-sidebar\", type: \"PageDoubleSidebar\", label: \"Double Sidebar\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-icon-sidebar\", type: \"PageIconSidebar\", label: \"Icon Sidebar\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-login\", type: \"PageLogin\", label: \"Login / Signup\", icon: <LogIn className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-menu-sections\", type: \"PageMenuSections\", label: \"Menu Sections\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-messenger\", type: \"PageMessenger\", label: \"Messenger\", icon: <MessageSquare className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-mobile-menu\", type: \"PageMobileMenu\", label: \"Mobile Menu\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-multistep-progressbar\", type: \"PageMultistepProgressbar\", label: \"Multistep + Progress\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-multistep-sidebar\", type: \"PageMultistepSidebar\", label: \"Multistep + Sidebar\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-pricing\", type: \"PagePricing\", label: \"Pricing\", icon: <CreditCard className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-product-homepage\", type: \"PageProductHomepage\", label: \"Product Homepage\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-reset-password\", type: \"PageResetPassword\", label: \"Reset Password\", icon: <LogIn className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-search-bar\", type: \"PageSearchBar\", label: \"Search Bar\", icon: <Search className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-sidebar-profile\", type: \"PageSidebarProfile\", label: \"Sidebar Profile\", icon: <User className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-standard\", type: \"PageStandard\", label: \"Standard Page\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-standard-multistep\", type: \"PageStandardMultistep\", label: \"Standard Multistep\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-standard-search\", type: \"PageStandardSearch\", label: \"Standard Search\", icon: <Search className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-vertical-multistep\", type: \"PageVerticalMultistep\", label: \"Vertical Multistep\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-video-chat\", type: \"PageVideoChat\", label: \"Video Chat\", icon: <Video className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-video-list\", type: \"PageVideoList\", label: \"Video List\", icon: <Play className=\"size-4\" />, category: \"Page Templates\" },\n\n // =====================\n // BLOCKS\n // =====================\n // Data & Tables\n { id: \"standard-data-table\", type: \"StandardDataTable\", label: \"Data Table\", icon: <Table className=\"size-4\" />, category: \"Blocks\" },\n \n // Cards & Profiles\n { id: \"profile-card\", type: \"ProfileCard\", label: \"Profile Card\", icon: <User className=\"size-4\" />, category: \"Blocks\" },\n { id: \"sidebar-profile-card\", type: \"SidebarProfileCard\", label: \"Sidebar Profile Card\", icon: <User className=\"size-4\" />, category: \"Blocks\" },\n { id: \"profile-info-cards\", type: \"ProfileInfoCards\", label: \"Profile Info Cards\", icon: <LayoutGrid className=\"size-4\" />, category: \"Blocks\" },\n { id: \"sidebar-cards\", type: \"SidebarCards\", label: \"Sidebar Cards\", icon: <Layers className=\"size-4\" />, category: \"Blocks\" },\n { id: \"credit-card-display\", type: \"CreditCardDisplay\", label: \"Credit Card Display\", icon: <CreditCard className=\"size-4\" />, category: \"Blocks\" },\n \n // Navigation & Progress\n { id: \"step-tracker\", type: \"StepTracker\", label: \"Step Tracker\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n { id: \"vertical-step-tracker\", type: \"VerticalStepTracker\", label: \"Vertical Step Tracker\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n { id: \"progress-bar\", type: \"ProgressBar\", label: \"Progress Bar\", icon: <SlidersHorizontal className=\"size-4\" />, category: \"Blocks\" },\n { id: \"pill-tabs\", type: \"PillTabs\", label: \"Pill Tabs\", icon: <LayoutGrid className=\"size-4\" />, category: \"Blocks\" },\n { id: \"mobile-bottom-nav\", type: \"MobileBottomNav\", label: \"Mobile Bottom Nav\", icon: <Layout className=\"size-4\" />, category: \"Blocks\" },\n \n // Banners & Headers\n { id: \"flair-banner\", type: \"FlairBanner\", label: \"Flair Banner\", icon: <Type className=\"size-4\" />, category: \"Blocks\" },\n { id: \"gradient-banner\", type: \"GradientBanner\", label: \"Gradient Banner\", icon: <Type className=\"size-4\" />, category: \"Blocks\" },\n { id: \"page-header-section\", type: \"PageHeaderSection\", label: \"Page Header Section\", icon: <FileText className=\"size-4\" />, category: \"Blocks\" },\n \n // Chat & Messaging\n { id: \"messenger-sidebar\", type: \"MessengerSidebar\", label: \"Messenger Sidebar\", icon: <MessageSquare className=\"size-4\" />, category: \"Blocks\" },\n { id: \"chat-message\", type: \"ChatMessage\", label: \"Chat Message\", icon: <MessageSquare className=\"size-4\" />, category: \"Blocks\" },\n \n // Video\n { id: \"video-chat-controls\", type: \"VideoChatControls\", label: \"Video Chat Controls\", icon: <Video className=\"size-4\" />, category: \"Blocks\" },\n { id: \"webcam-preview\", type: \"WebcamPreview\", label: \"Webcam Preview\", icon: <Video className=\"size-4\" />, category: \"Blocks\" },\n { id: \"participant-list\", type: \"ParticipantList\", label: \"Participant List\", icon: <Users className=\"size-4\" />, category: \"Blocks\" },\n { id: \"video-content-section\", type: \"VideoContentSection\", label: \"Video Content Section\", icon: <Play className=\"size-4\" />, category: \"Blocks\" },\n { id: \"video-playlist\", type: \"VideoPlaylist\", label: \"Video Playlist\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n \n // Search & Filters\n { id: \"search-bar\", type: \"SearchBar\", label: \"Search Bar\", icon: <Search className=\"size-4\" />, category: \"Blocks\" },\n { id: \"filter-popover\", type: \"FilterPopover\", label: \"Filter Popover\", icon: <SlidersHorizontal className=\"size-4\" />, category: \"Blocks\" },\n \n // Forms & Settings\n { id: \"settings-list-row\", type: \"SettingsListRow\", label: \"Settings List Row\", icon: <Settings className=\"size-4\" />, category: \"Blocks\" },\n { id: \"profile-image-uploader\", type: \"ProfileImageUploader\", label: \"Profile Image Uploader\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"login-branding-panel\", type: \"LoginBrandingPanel\", label: \"Login Branding Panel\", icon: <Layout className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Heroes\n { id: \"hero-section\", type: \"HeroSection\", label: \"Hero Section\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"hero-dark-with-image\", type: \"HeroDarkWithImage\", label: \"Hero Dark + Image\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"centered-hero\", type: \"CenteredHero\", label: \"Centered Hero\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Social Proof\n { id: \"testimonial-carousel\", type: \"TestimonialCarousel\", label: \"Testimonial Carousel\", icon: <MessageSquare className=\"size-4\" />, category: \"Blocks\" },\n { id: \"reviews-grid\", type: \"ReviewsGrid\", label: \"Reviews Grid\", icon: <Star className=\"size-4\" />, category: \"Blocks\" },\n { id: \"social-proof\", type: \"SocialProof\", label: \"Social Proof (Logos)\", icon: <Award className=\"size-4\" />, category: \"Blocks\" },\n { id: \"metrics-section\", type: \"MetricsSection\", label: \"Metrics Section\", icon: <Hash className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Features\n { id: \"feature-with-image\", type: \"FeatureWithImage\", label: \"Feature + Image\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"core-values-grid\", type: \"CoreValuesGrid\", label: \"Core Values Grid\", icon: <LayoutGrid className=\"size-4\" />, category: \"Blocks\" },\n { id: \"destination-cards\", type: \"DestinationCards\", label: \"Destination Cards\", icon: <MapPin className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Team\n { id: \"team-cards-grid\", type: \"TeamCardsGrid\", label: \"Team Cards Grid\", icon: <Users className=\"size-4\" />, category: \"Blocks\" },\n { id: \"team-circular-grid\", type: \"TeamCircularGrid\", label: \"Team Circular Grid\", icon: <Users className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - CTA & Footer\n { id: \"cta-banner\", type: \"CtaBanner\", label: \"CTA Banner\", icon: <Megaphone className=\"size-4\" />, category: \"Blocks\" },\n { id: \"footer-navbar\", type: \"FooterNavbar\", label: \"Footer Navbar\", icon: <Layout className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Other\n { id: \"featured-news-cards\", type: \"FeaturedNewsCards\", label: \"Featured News Cards\", icon: <Newspaper className=\"size-4\" />, category: \"Blocks\" },\n { id: \"office-locations\", type: \"OfficeLocations\", label: \"Office Locations\", icon: <Building className=\"size-4\" />, category: \"Blocks\" },\n \n // Pricing\n { id: \"pricing-cards\", type: \"PricingCards\", label: \"Pricing Cards\", icon: <CreditCard className=\"size-4\" />, category: \"Blocks\" },\n { id: \"faq-accordion\", type: \"FaqAccordion\", label: \"FAQ Accordion\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n { id: \"features-comparison\", type: \"FeaturesComparison\", label: \"Features Comparison\", icon: <Table className=\"size-4\" />, category: \"Blocks\" },\n\n // =====================\n // COMPONENTS (UI Primitives)\n // =====================\n { id: \"button\", type: \"Button\", label: \"Button\", icon: <Square className=\"size-4\" />, category: \"Components\" },\n { id: \"checkbox\", type: \"Checkbox\", label: \"Checkbox\", icon: <CheckSquare className=\"size-4\" />, category: \"Components\" },\n { id: \"date-input\", type: \"DateInput\", label: \"Date Input\", icon: <Calendar className=\"size-4\" />, category: \"Components\" },\n { id: \"input\", type: \"Input\", label: \"Text Input\", icon: <Type className=\"size-4\" />, category: \"Components\" },\n { id: \"select\", type: \"Select\", label: \"Select\", icon: <List className=\"size-4\" />, category: \"Components\" },\n { id: \"switch\", type: \"Switch\", label: \"Switch\", icon: <ToggleLeft className=\"size-4\" />, category: \"Components\" },\n { id: \"radio-group\", type: \"RadioGroup\", label: \"Radio Group\", icon: <CircleDot className=\"size-4\" />, category: \"Components\" },\n { id: \"multiselect-tags\", type: \"MultiselectTags\", label: \"Multiselect Tags\", icon: <Tags className=\"size-4\" />, category: \"Components\" },\n { id: \"avatar\", type: \"Avatar\", label: \"Avatar\", icon: <User className=\"size-4\" />, category: \"Components\" },\n { id: \"badge\", type: \"Badge\", label: \"Badge\", icon: <Award className=\"size-4\" />, category: \"Components\" },\n];\n\n// Group components by category\nconst componentsByCategory = paletteComponents.reduce((acc, comp) => {\n if (!acc[comp.category]) {\n acc[comp.category] = [];\n }\n acc[comp.category].push(comp);\n return acc;\n}, {} as Record<string, PaletteComponent[]>);\n\n// Define category order\nconst categoryOrder = [\"Page Templates\", \"Blocks\", \"Components\"];\n\ninterface DraggableComponentProps {\n component: PaletteComponent;\n}\n\nfunction DraggableComponent({ component }: DraggableComponentProps) {\n const [isMounted, setIsMounted] = useState(false);\n \n const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({\n id: component.id,\n data: {\n type: component.type,\n label: component.label,\n },\n });\n\n // Only apply dnd-kit attributes after hydration to prevent mismatch\n useEffect(() => {\n setIsMounted(true);\n }, []);\n\n const style = {\n transform: CSS.Transform.toString(transform),\n opacity: isDragging ? 0.5 : 1,\n };\n\n return (\n <div\n ref={setNodeRef}\n style={style}\n // Only spread dnd-kit attributes after client-side mount to avoid hydration mismatch\n {...(isMounted ? attributes : {})}\n {...(isMounted ? listeners : {})}\n className={cn(\n \"flex items-center gap-3 px-3 py-2.5 rounded-md cursor-grab active:cursor-grabbing\",\n \"border border-transparent\",\n \"hover:bg-[var(--canvas-surface)] hover:border-[var(--canvas-border)]\",\n \"transition-colors group\"\n )}\n >\n <div \n className=\"flex items-center justify-center size-8 rounded-md bg-[var(--canvas-surface-brand)] text-[var(--canvas-primary)]\"\n >\n {component.icon}\n </div>\n <span \n className=\"text-sm text-[var(--canvas-text)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n }}\n >\n {component.label}\n </span>\n </div>\n );\n}\n\ninterface CategorySectionProps {\n category: string;\n components: PaletteComponent[];\n defaultExpanded?: boolean;\n}\n\nfunction CategorySection({ category, components, defaultExpanded = true }: CategorySectionProps) {\n const [isExpanded, setIsExpanded] = useState(defaultExpanded);\n\n return (\n <div className=\"mb-2\">\n <button\n onClick={() => setIsExpanded(!isExpanded)}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-left hover:bg-[var(--canvas-surface)] rounded-md transition-colors\"\n >\n {isExpanded ? (\n <ChevronDown className=\"size-4 text-[var(--canvas-text-muted)]\" />\n ) : (\n <ChevronRight className=\"size-4 text-[var(--canvas-text-muted)]\" />\n )}\n <span \n className=\"text-xs font-semibold uppercase tracking-wider text-[var(--canvas-text-muted)]\"\n >\n {category}\n </span>\n <span className=\"ml-auto text-xs text-[var(--canvas-text-placeholder)]\">\n {components.length}\n </span>\n </button>\n \n {isExpanded && (\n <div className=\"mt-1 ml-2 space-y-0.5\">\n {components.map((component) => (\n <DraggableComponent key={component.id} component={component} />\n ))}\n </div>\n )}\n </div>\n );\n}\n\ninterface ComponentPaletteProps {\n className?: string;\n}\n\n/**\n * Component Palette - Sidebar with draggable components\n * \n * Features:\n * - Organized by category (Page Templates, Blocks, Components)\n * - Collapsible sections\n * - Drag to add to canvas\n */\nexport function ComponentPalette({ className }: ComponentPaletteProps) {\n return (\n <aside\n className={cn(\n \"w-[280px] h-full flex flex-col\",\n \"bg-white border-r border-[var(--canvas-border)]\",\n className\n )}\n >\n {/* Header */}\n <div className=\"px-4 py-4 border-b border-[var(--canvas-border)]\">\n <h2 \n className=\"text-sm font-semibold text-[var(--canvas-text)]\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n }}\n >\n Components\n </h2>\n <p \n className=\"text-xs text-[var(--canvas-text-muted)] mt-1\"\n >\n Drag components onto the canvas\n </p>\n </div>\n\n {/* Component List */}\n <ScrollArea className=\"flex-1\">\n <div className=\"p-3\">\n {categoryOrder.map((category) => {\n const components = componentsByCategory[category];\n if (!components) return null;\n return (\n <CategorySection\n key={category}\n category={category}\n components={components}\n defaultExpanded={category !== \"Page Templates\"} // Collapse templates by default\n />\n );\n })}\n </div>\n </ScrollArea>\n\n {/* Footer hint */}\n <div className=\"px-4 py-3 border-t border-[var(--canvas-border)] bg-[var(--canvas-surface)]\">\n <p className=\"text-xs text-[var(--canvas-text-placeholder)]\">\n Tip: Press <kbd className=\"px-1.5 py-0.5 bg-white rounded border border-[var(--canvas-border)] text-[10px]\">Delete</kbd> to remove selected\n </p>\n </div>\n </aside>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"@dnd-kit/core",
|
|
14
|
+
"@dnd-kit/utilities",
|
|
15
|
+
"lucide-react"
|
|
16
|
+
],
|
|
17
|
+
"registryDependencies": [
|
|
18
|
+
"utils",
|
|
19
|
+
"scroll-area"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "component-search",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/component-search.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport { Search, X, ChevronDown, ChevronUp } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n layoutShells,\n blocks,\n marketingBlocks,\n pricingBlocks,\n videoBlocks,\n pageTemplates,\n} from \"../../lib/component-registry\";\n\n// ═══════════════════════════════════════════════════════════\n// TYPES\n// ═══════════════════════════════════════════════════════════\n\nexport interface ComponentOption {\n id: string;\n name: string;\n category: string;\n path: string;\n description: string;\n}\n\ninterface ComponentSearchProps {\n selectedComponents: ComponentOption[];\n onSelectionChange: (components: ComponentOption[]) => void;\n className?: string;\n}\n\n// ═══════════════════════════════════════════════════════════\n// BUILD COMPONENT OPTIONS FROM REGISTRY\n// ═══════════════════════════════════════════════════════════\n\nfunction buildComponentOptions(): ComponentOption[] {\n const options: ComponentOption[] = [];\n\n // Layout Shells\n Object.entries(layoutShells).forEach(([name, config]) => {\n options.push({\n id: `shell-${name}`,\n name,\n category: \"Layout Shells\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Blocks\n Object.entries(blocks).forEach(([name, config]) => {\n options.push({\n id: `block-${name}`,\n name,\n category: \"Blocks\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Marketing Blocks\n Object.entries(marketingBlocks).forEach(([name, config]) => {\n options.push({\n id: `marketing-${name}`,\n name,\n category: \"Marketing\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Pricing Blocks\n Object.entries(pricingBlocks).forEach(([name, config]) => {\n options.push({\n id: `pricing-${name}`,\n name,\n category: \"Pricing\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Video Blocks\n Object.entries(videoBlocks).forEach(([name, config]) => {\n options.push({\n id: `video-${name}`,\n name,\n category: \"Video/Media\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Page Templates\n Object.entries(pageTemplates).forEach(([name, config]) => {\n options.push({\n id: `template-${name}`,\n name,\n category: \"Page Templates\",\n path: config.path,\n description: config.description,\n });\n });\n\n return options;\n}\n\nconst ALL_COMPONENTS = buildComponentOptions();\nconst CATEGORIES = [...new Set(ALL_COMPONENTS.map((c) => c.category))];\n\n// ═══════════════════════════════════════════════════════════\n// COMPONENT\n// ═══════════════════════════════════════════════════════════\n\nexport function ComponentSearch({\n selectedComponents,\n onSelectionChange,\n className,\n}: ComponentSearchProps) {\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [isOpen, setIsOpen] = useState(false);\n const [expandedCategories, setExpandedCategories] = useState<Set<string>>(\n new Set(CATEGORIES)\n );\n\n // Filter components based on search\n const filteredComponents = useMemo(() => {\n if (!searchQuery.trim()) return ALL_COMPONENTS;\n\n const query = searchQuery.toLowerCase();\n return ALL_COMPONENTS.filter(\n (c) =>\n c.name.toLowerCase().includes(query) ||\n c.description.toLowerCase().includes(query) ||\n c.category.toLowerCase().includes(query)\n );\n }, [searchQuery]);\n\n // Group by category\n const groupedComponents = useMemo(() => {\n const groups: Record<string, ComponentOption[]> = {};\n filteredComponents.forEach((c) => {\n if (!groups[c.category]) groups[c.category] = [];\n groups[c.category].push(c);\n });\n return groups;\n }, [filteredComponents]);\n\n const toggleCategory = (category: string) => {\n setExpandedCategories((prev) => {\n const next = new Set(prev);\n if (next.has(category)) {\n next.delete(category);\n } else {\n next.add(category);\n }\n return next;\n });\n };\n\n const toggleComponent = (component: ComponentOption) => {\n const isSelected = selectedComponents.some((c) => c.id === component.id);\n if (isSelected) {\n onSelectionChange(selectedComponents.filter((c) => c.id !== component.id));\n } else {\n onSelectionChange([...selectedComponents, component]);\n }\n };\n\n const removeComponent = (componentId: string) => {\n onSelectionChange(selectedComponents.filter((c) => c.id !== componentId));\n };\n\n return (\n <div className={cn(\"space-y-3\", className)}>\n {/* Selected Components Chips */}\n {selectedComponents.length > 0 && (\n <div className=\"flex flex-wrap gap-2\">\n {selectedComponents.map((component) => (\n <div\n key={component.id}\n className=\"flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-[var(--canvas-surface-brand)] text-[var(--canvas-primary)] text-sm\"\n >\n <span className=\"font-medium\">{component.name}</span>\n <button\n onClick={() => removeComponent(component.id)}\n className=\"p-0.5 rounded-full hover:bg-[var(--canvas-primary)]/20 transition-colors\"\n >\n <X className=\"size-3\" />\n </button>\n </div>\n ))}\n </div>\n )}\n\n {/* Search Input */}\n <div className=\"relative\">\n <div\n className={cn(\n \"flex items-center gap-2 px-3 py-2.5 rounded-lg border bg-[var(--canvas-background)] cursor-text\",\n isOpen\n ? \"border-[var(--canvas-primary)] ring-2 ring-[var(--canvas-primary)]/20\"\n : \"border-[var(--canvas-border)]\"\n )}\n onClick={() => setIsOpen(true)}\n >\n <Search className=\"size-4 text-[var(--canvas-text-muted)]\" />\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n onFocus={() => setIsOpen(true)}\n placeholder=\"Search components...\"\n className=\"flex-1 bg-transparent text-sm text-[var(--canvas-text)] placeholder:text-[var(--canvas-text-placeholder)] outline-none\"\n />\n <span className=\"text-xs text-[var(--canvas-text-muted)]\">\n {selectedComponents.length} selected\n </span>\n </div>\n\n {/* Dropdown */}\n {isOpen && (\n <>\n {/* Backdrop */}\n <div\n className=\"fixed inset-0 z-10\"\n onClick={() => setIsOpen(false)}\n />\n\n {/* Options List */}\n <div className=\"absolute top-full left-0 right-0 mt-1 z-20 max-h-[400px] overflow-y-auto rounded-lg border border-[var(--canvas-border)] bg-[var(--canvas-background)] shadow-lg\">\n {Object.entries(groupedComponents).map(([category, components]) => (\n <div key={category}>\n {/* Category Header */}\n <button\n onClick={() => toggleCategory(category)}\n className=\"w-full flex items-center justify-between px-3 py-2 bg-[var(--canvas-surface)] border-b border-[var(--canvas-border)] text-xs font-semibold text-[var(--canvas-text-muted)] uppercase tracking-wide hover:bg-[var(--canvas-surface-brand)]/50\"\n >\n <span>\n {category} ({components.length})\n </span>\n {expandedCategories.has(category) ? (\n <ChevronUp className=\"size-3\" />\n ) : (\n <ChevronDown className=\"size-3\" />\n )}\n </button>\n\n {/* Components in Category */}\n {expandedCategories.has(category) && (\n <div>\n {components.map((component) => {\n const isSelected = selectedComponents.some(\n (c) => c.id === component.id\n );\n return (\n <button\n key={component.id}\n onClick={() => toggleComponent(component)}\n className={cn(\n \"w-full flex items-start gap-3 px-3 py-2.5 text-left transition-colors border-b border-[var(--canvas-border)]/50 last:border-b-0\",\n isSelected\n ? \"bg-[var(--canvas-surface-brand)]/50\"\n : \"hover:bg-[var(--canvas-surface)]\"\n )}\n >\n {/* Checkbox */}\n <div\n className={cn(\n \"size-4 rounded border mt-0.5 flex items-center justify-center shrink-0\",\n isSelected\n ? \"bg-[var(--canvas-primary)] border-[var(--canvas-primary)]\"\n : \"border-[var(--canvas-border)]\"\n )}\n >\n {isSelected && (\n <svg\n className=\"size-3 text-white\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={3}\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n )}\n </div>\n\n {/* Component Info */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <span\n className={cn(\n \"text-sm font-medium\",\n isSelected\n ? \"text-[var(--canvas-primary)]\"\n : \"text-[var(--canvas-text)]\"\n )}\n >\n {component.name}\n </span>\n <span className=\"text-xs text-[var(--canvas-text-placeholder)]\">\n {component.path}\n </span>\n </div>\n <p className=\"text-xs text-[var(--canvas-text-muted)] mt-0.5 line-clamp-2\">\n {component.description}\n </p>\n </div>\n </button>\n );\n })}\n </div>\n )}\n </div>\n ))}\n\n {filteredComponents.length === 0 && (\n <div className=\"px-3 py-6 text-center text-sm text-[var(--canvas-text-muted)]\">\n No components found for \"{searchQuery}\"\n </div>\n )}\n </div>\n </>\n )}\n </div>\n </div>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"lucide-react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"utils",
|
|
17
|
+
"component-registry"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "content-dropzone",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Placeholder dropzone for content areas during development.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/content-dropzone.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "import { cn } from \"../../lib/utils\";\nimport { ReactNode, Children } from \"react\";\n\ninterface ContentDropzoneProps {\n /** Height of the dropzone (only applies when empty) */\n height?: string;\n /** Additional class names */\n className?: string;\n /** Content elements */\n children?: ReactNode;\n}\n\n/**\n * Canvas Design System - Content Dropzone\n * \n * A placeholder component representing where content blocks would be inserted.\n * When empty, shows a dashed border placeholder.\n * When children are added, becomes a flex column with 40px gap spacing.\n */\nexport function ContentDropzone({ \n height = \"480px\",\n className,\n children,\n}: ContentDropzoneProps) {\n const hasChildren = Children.count(children) > 0;\n\n if (hasChildren) {\n return (\n <div \n className={cn(\n \"flex flex-col gap-10\",\n className\n )}\n >\n {children}\n </div>\n );\n }\n\n return (\n <div \n className={cn(\n \"flex items-center justify-center\",\n \"bg-white\",\n \"border-2 border-dashed border-[var(--canvas-border)]\",\n \"rounded-[var(--radius-nav)]\",\n \"overflow-hidden\",\n className\n )}\n style={{ minHeight: height }}\n />\n );\n}\n\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [],
|
|
13
|
+
"registryDependencies": [
|
|
14
|
+
"utils"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "content-with-image",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Generic content section with image. Similar to FeatureWithImage.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/marketing/content-with-image.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport { Typography } from \"../../ui/typography\";\n\ninterface ContentWithImageProps {\n subtitle?: string;\n title?: string;\n description?: string;\n image?: string;\n imagePosition?: \"left\" | \"right\";\n}\n\nexport function ContentWithImage({\n subtitle = \"ABOUT US\",\n title = \"Helping startups succeed\",\n description = `Our company was founded with the goal of empowering people through technology, and we remain committed to this mission today.\n\nWe are driven by a passion for innovation and a desire to make a positive impact on the world. We believe that technology has the power to transform lives, and we are committed to developing cutting-edge solutions that help our customers achieve their goals.`,\n image = \"https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800&h=600&fit=crop\",\n imagePosition = \"right\",\n}: ContentWithImageProps) {\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\">\n <div\n className={`flex flex-col ${\n imagePosition === \"left\" ? \"md:flex-row-reverse\" : \"md:flex-row\"\n } gap-10 md:gap-20 items-center`}\n >\n {/* Text Content */}\n <div className=\"flex flex-col gap-6 w-full md:w-[568px] md:shrink-0\">\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 <Typography variant=\"body-l\" color=\"muted\" className=\"whitespace-pre-line\">\n {description}\n </Typography>\n </div>\n\n {/* Image */}\n <div\n className=\"w-full md:flex-1 rounded-xl overflow-hidden\"\n style={{\n border: \"1px solid var(--canvas-border)\",\n boxShadow: \"0px 0px 96px 0px rgba(13,18,28,0.08)\",\n }}\n >\n <img\n src={image}\n alt={title}\n className=\"w-full h-[280px] md:h-[420px] object-cover\"\n />\n </div>\n </div>\n </div>\n </section>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [],
|
|
13
|
+
"registryDependencies": []
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "core-values-grid",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Grid of value cards with icons and descriptions.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/marketing/core-values-grid.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport {\n Rocket,\n UserFocus,\n UsersFour,\n Eye,\n Books,\n Lightning,\n} from \"@phosphor-icons/react\";\nimport { ReactNode } from \"react\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface Value {\n icon: ReactNode;\n title: string;\n description: string;\n}\n\ninterface CoreValuesGridProps {\n variant?: \"light\" | \"dark\";\n subtitle?: string;\n title?: string;\n description?: string;\n values?: Value[];\n}\n\nconst defaultValues: Value[] = [\n {\n icon: <Rocket size={32} weight=\"duotone\" />,\n title: \"Innovation\",\n description:\n \"We are committed to constantly pushing boundaries and exploring new ideas, technologies, and solutions to drive innovation and progress\",\n },\n {\n icon: <UserFocus size={32} weight=\"duotone\" />,\n title: \"Customers first\",\n description:\n \"Our customers are at the center of everything we do, and we are dedicated to providing them with the best possible experience and support\",\n },\n {\n icon: <UsersFour size={32} weight=\"duotone\" />,\n title: \"Teamwork\",\n description:\n \"We believe in the power of collaboration and teamwork, and we strive to foster a supportive and inclusive environment\",\n },\n {\n icon: <Eye size={32} weight=\"duotone\" />,\n title: \"Accountability\",\n description:\n \"We take responsibility for our actions and decisions, and we hold ourselves accountable to the highest standards\",\n },\n {\n icon: <Books size={32} weight=\"duotone\" />,\n title: \"Continuous learning\",\n description:\n \"We believe in the importance of continuous learning and growth, and we encourage our team members to pursue development opportunities\",\n },\n {\n icon: <Lightning size={32} weight=\"duotone\" />,\n title: \"Agility\",\n description:\n \"We are adaptable and nimble, and we embrace change as an opportunity to grow and improve\",\n },\n];\n\nexport function CoreValuesGrid({\n variant = \"light\",\n subtitle = \"CORE VALUES\",\n title = \"How we work\",\n description = \"Guided by our core values, we strive to exceed expectations and create meaningful experiences\",\n values = defaultValues,\n}: CoreValuesGridProps) {\n const isDark = variant === \"dark\";\n\n return (\n <section\n className=\"w-full px-6 md:px-20 py-16 md:py-24\"\n style={{\n backgroundColor: isDark\n ? \"var(--canvas-dark-section-bg)\"\n : \"var(--canvas-background)\",\n }}\n >\n <div className=\"max-w-[1240px] mx-auto flex flex-col items-center gap-10 md:gap-12\">\n {/* Header */}\n <div className=\"flex flex-col items-center gap-4 md:gap-6 max-w-[768px] text-center\">\n <div className=\"flex flex-col items-center gap-3\">\n <Typography variant=\"body-xs\" as=\"span\" color=\"muted\" className=\"uppercase tracking-wide\">\n {subtitle}\n </Typography>\n <Typography\n variant=\"h3\"\n as=\"h2\"\n {...(isDark && { style: { color: \"white\" } })}\n >\n {title}\n </Typography>\n </div>\n <Typography\n variant=\"body-l\"\n color=\"muted\"\n className=\"max-w-[576px]\"\n {...(isDark && { style: { color: \"rgba(255, 255, 255, 0.7)\" } })}\n >\n {description}\n </Typography>\n </div>\n\n {/* Values Grid */}\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-12 md:gap-20 w-full\">\n {values.map((value, index) => (\n <div key={index} className=\"flex flex-col gap-5\">\n {/* Icon */}\n <div\n className=\"w-16 h-16 rounded-lg flex items-center justify-center\"\n style={{\n backgroundColor: \"var(--canvas-border)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {value.icon}\n </div>\n\n {/* Text */}\n <div className=\"flex flex-col gap-2\">\n <Typography\n variant=\"h6\"\n as=\"h3\"\n {...(isDark && { style: { color: \"white\" } })}\n >\n {value.title}\n </Typography>\n <Typography\n variant=\"body-m\"\n color=\"muted\"\n {...(isDark && { style: { color: \"rgba(255, 255, 255, 0.7)\" } })}\n >\n {value.description}\n </Typography>\n </div>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"@phosphor-icons/react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": []
|
|
16
|
+
}
|