canvas-ui-sdk 0.3.23 → 4.0.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/README.md +25 -5
- package/dist/charts.js +11 -6
- package/dist/charts.js.map +1 -1
- package/dist/index.d.ts +1233 -153
- package/dist/index.js +3562 -447
- package/dist/index.js.map +1 -1
- package/mcp/dist/index.js +1195 -149
- package/package.json +1 -1
- package/prompts/.cursorrules +96 -0
- package/prompts/.windsurfrules +96 -0
- package/prompts/CLAUDE.md +22 -0
- package/prompts/copilot-instructions.md +96 -0
- package/registry/blocks/activity-feed.json +12 -1
- package/registry/blocks/blog-cards.json +10 -2
- package/registry/blocks/bottom-action-bar.json +27 -0
- package/registry/blocks/bottom-input-chat-widget.json +9 -1
- package/registry/blocks/category-grid.json +10 -2
- package/registry/blocks/centered-hero.json +9 -1
- package/registry/blocks/chat-message.json +8 -1
- package/registry/blocks/circular-progress-bar-list.json +11 -1
- package/registry/blocks/confirmation-popup.json +10 -1
- package/registry/blocks/contact-form-popup.json +10 -1
- package/registry/blocks/content-dropzone.json +8 -0
- package/registry/blocks/content-with-image.json +9 -1
- package/registry/blocks/core-values-grid.json +10 -2
- package/registry/blocks/credit-card-display.json +9 -1
- package/registry/blocks/cta-banner.json +10 -2
- package/registry/blocks/destination-cards.json +10 -1
- package/registry/blocks/detail-drawer.json +10 -1
- package/registry/blocks/details-popup.json +10 -1
- package/registry/blocks/editable-list.json +29 -0
- package/registry/blocks/empty-state.json +10 -2
- package/registry/blocks/faq-accordion.json +9 -1
- package/registry/blocks/faqs-table.json +10 -1
- package/registry/blocks/feature-with-image.json +9 -1
- package/registry/blocks/featured-news-cards.json +10 -2
- package/registry/blocks/featured-places.json +10 -2
- package/registry/blocks/features-comparison.json +9 -1
- package/registry/blocks/feedback-popup.json +9 -1
- package/registry/blocks/filter-popover.json +8 -1
- package/registry/blocks/fixed-column-data-table.json +11 -1
- package/registry/blocks/flair-banner.json +9 -1
- package/registry/blocks/footer-navbar.json +9 -1
- package/registry/blocks/form-group.json +14 -3
- package/registry/blocks/form-popup.json +31 -0
- package/registry/blocks/gallery-section.json +10 -2
- package/registry/blocks/gradient-banner.json +10 -2
- package/registry/blocks/graph-metric-tiles.json +1 -1
- package/registry/blocks/grid-tiles-list.json +10 -1
- package/registry/blocks/hero-dark-centered.json +9 -1
- package/registry/blocks/hero-dark-with-image.json +9 -1
- package/registry/blocks/hero-fullwidth-image.json +9 -1
- package/registry/blocks/hero-section.json +9 -1
- package/registry/blocks/how-it-works.json +9 -1
- package/registry/blocks/image-feed-with-nested-comments.json +10 -1
- package/registry/blocks/image-popup.json +10 -1
- package/registry/blocks/invoice-popup.json +10 -1
- package/registry/blocks/large-image-labels-list.json +10 -1
- package/registry/blocks/list-popup.json +28 -0
- package/registry/blocks/loader.json +9 -1
- package/registry/blocks/login-branding-panel.json +10 -2
- package/registry/blocks/menu-section.json +9 -1
- package/registry/blocks/menufocus-template.json +9 -1
- package/registry/blocks/messenger-sidebar.json +11 -2
- package/registry/blocks/metrics-section.json +10 -2
- package/registry/blocks/mobile-bottom-nav.json +10 -2
- package/registry/blocks/monthly-calendar-widget.json +9 -1
- package/registry/blocks/multistep-form-popup.json +34 -0
- package/registry/blocks/nested-comments-table.json +9 -1
- package/registry/blocks/nested-data-table.json +10 -1
- package/registry/blocks/nps-survey-popup.json +27 -0
- package/registry/blocks/office-locations.json +10 -2
- package/registry/blocks/order-summary-sidebar.json +27 -0
- package/registry/blocks/page-header-section.json +9 -1
- package/registry/blocks/pagination.json +8 -1
- package/registry/blocks/participant-list.json +9 -1
- package/registry/blocks/persona-card.json +10 -1
- package/registry/blocks/personalize-feed-popup.json +27 -0
- package/registry/blocks/pill-tabs.json +9 -1
- package/registry/blocks/place-detail-panel.json +11 -1
- package/registry/blocks/pricing-cards.json +10 -2
- package/registry/blocks/pricing-cta.json +9 -1
- package/registry/blocks/pricing-plans-popup.json +10 -1
- package/registry/blocks/profile-card.json +12 -2
- package/registry/blocks/profile-grid-tiles-list.json +10 -1
- package/registry/blocks/profile-image-uploader.json +9 -1
- package/registry/blocks/profile-info-cards.json +10 -1
- package/registry/blocks/progress-bar.json +8 -1
- package/registry/blocks/prompt-template.json +1 -1
- package/registry/blocks/purchase-confirmation-popup.json +10 -1
- package/registry/blocks/reservation-card.json +26 -0
- package/registry/blocks/reviews-grid.json +10 -2
- package/registry/blocks/reviews-table.json +10 -1
- package/registry/blocks/screen-prompt-template.json +1 -1
- package/registry/blocks/search-bar.json +9 -2
- package/registry/blocks/search-sidebar.json +9 -2
- package/registry/blocks/settings-list-row.json +9 -1
- package/registry/blocks/share-project-popup.json +36 -0
- package/registry/blocks/sidebar-cards.json +10 -2
- package/registry/blocks/sidebar-profile-card.json +10 -2
- package/registry/blocks/slideshow-grid-tiles.json +10 -2
- package/registry/blocks/slideshow-popup.json +10 -1
- package/registry/blocks/small-edit-popup.json +29 -0
- package/registry/blocks/social-feed.json +10 -1
- package/registry/blocks/social-proof.json +9 -1
- package/registry/blocks/standard-data-table.json +13 -1
- package/registry/blocks/standard-list-with-image.json +10 -1
- package/registry/blocks/step-tracker.json +9 -1
- package/registry/blocks/store-location-map.json +9 -1
- package/registry/blocks/team-cards-grid.json +9 -1
- package/registry/blocks/team-circular-grid.json +9 -1
- package/registry/blocks/terms-of-service-popup.json +10 -1
- package/registry/blocks/testimonial-carousel.json +10 -2
- package/registry/blocks/tile-image-gallery.json +26 -0
- package/registry/blocks/title-group.json +10 -1
- package/registry/blocks/upvoting-posts-table.json +10 -1
- package/registry/blocks/vertical-how-it-works.json +9 -1
- package/registry/blocks/vertical-step-tracker.json +9 -1
- package/registry/blocks/video-chat-controls.json +9 -1
- package/registry/blocks/video-content-section.json +9 -1
- package/registry/blocks/video-playlist.json +9 -1
- package/registry/blocks/video-popup.json +9 -1
- package/registry/blocks/view-profile-popup.json +10 -1
- package/registry/blocks/webcam-preview.json +9 -1
- package/registry/hooks/use-css-variable-sync.json +10 -1
- package/registry/hooks/use-mobile.json +9 -1
- package/registry/index.json +1526 -147
- package/registry/layout/account-settings-shell.json +10 -1
- package/registry/layout/dashboard-shell.json +12 -1
- package/registry/layout/double-sidebar-shell.json +11 -2
- package/registry/layout/double-sidebar.json +9 -1
- package/registry/layout/header.json +10 -1
- package/registry/layout/icon-sidebar-shell.json +9 -1
- package/registry/layout/icon-sidebar.json +9 -1
- package/registry/layout/mobile-menu-shell.json +10 -1
- package/registry/layout/multistep-progressbar-shell.json +9 -1
- package/registry/layout/multistep-shell.json +11 -1
- package/registry/layout/multistep-sidebar-shell.json +10 -2
- package/registry/layout/project-context-shell.json +1 -1
- package/registry/layout/search-bar-shell.json +8 -1
- package/registry/layout/sidebar-nav.json +7 -1
- package/registry/layout/sidebar.json +9 -2
- package/registry/layout/standard-page-shell.json +10 -1
- package/registry/layout/vertical-multistep-shell.json +10 -1
- package/registry/ui/avatar.json +9 -1
- package/registry/ui/button.json +9 -1
- package/registry/ui/calendar.json +9 -1
- package/registry/ui/checkbox.json +8 -1
- package/registry/ui/date-input.json +9 -1
- package/registry/ui/dialog.json +8 -1
- package/registry/ui/dropdown-menu.json +8 -1
- package/registry/ui/file-uploader.json +9 -1
- package/registry/ui/image-uploader.json +9 -1
- package/registry/ui/input.json +8 -1
- package/registry/ui/label.json +8 -1
- package/registry/ui/line-tabs.json +9 -1
- package/registry/ui/multiselect-checkbox-field.json +9 -1
- package/registry/ui/multiselect-tags.json +9 -1
- package/registry/ui/popover.json +8 -1
- package/registry/ui/radio-group.json +9 -1
- package/registry/ui/range-input.json +8 -1
- package/registry/ui/scroll-area.json +8 -1
- package/registry/ui/searchbox.json +9 -1
- package/registry/ui/select.json +9 -1
- package/registry/ui/selectable-pills.json +11 -1
- package/registry/ui/separator.json +8 -1
- package/registry/ui/sheet.json +9 -1
- package/registry/ui/sidebar.json +8 -1
- package/registry/ui/skeleton.json +8 -1
- package/registry/ui/slider.json +10 -2
- package/registry/ui/switch.json +9 -1
- package/registry/ui/tabs.json +8 -1
- package/registry/ui/text-input.json +8 -1
- package/registry/ui/textarea.json +9 -1
- package/registry/ui/tooltip.json +8 -1
- package/registry/ui/typography.json +9 -1
- package/styles/tokens.reference.css +21 -0
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "image-feed-with-nested-comments",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Full-width image feed with large photos, social interaction buttons (like, comment, share, bookmark), and nested comment threads below each image. Use for photo galleries, social feeds, portfolio showcases, or media-rich community content.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"feed",
|
|
7
|
+
"images",
|
|
8
|
+
"social",
|
|
9
|
+
"photos",
|
|
10
|
+
"gallery",
|
|
11
|
+
"comments"
|
|
12
|
+
],
|
|
13
|
+
"visualWeight": "heavy",
|
|
5
14
|
"files": [
|
|
6
15
|
{
|
|
7
16
|
"path": "components/blocks/image-feed-with-nested-comments.tsx",
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "image-popup",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "",
|
|
4
|
+
"description": "Minimal lightbox modal for displaying a single image in full view with responsive sizing. Use for image previews, photo zoom, or any image detail view.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"image",
|
|
7
|
+
"lightbox",
|
|
8
|
+
"modal",
|
|
9
|
+
"preview",
|
|
10
|
+
"zoom",
|
|
11
|
+
"photo"
|
|
12
|
+
],
|
|
13
|
+
"visualWeight": "light",
|
|
5
14
|
"files": [
|
|
6
15
|
{
|
|
7
16
|
"path": "components/blocks/image-popup.tsx",
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "invoice-popup",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "",
|
|
4
|
+
"description": "Invoice display modal with header section (logo, title, metadata), line items table, summary calculations (subtotal, discount, total), and action button. Centered dialog (~500px tall). Use for invoice previews, receipt displays, or billing document views.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"invoice",
|
|
7
|
+
"receipt",
|
|
8
|
+
"billing",
|
|
9
|
+
"modal",
|
|
10
|
+
"payment",
|
|
11
|
+
"document"
|
|
12
|
+
],
|
|
13
|
+
"visualWeight": "medium",
|
|
5
14
|
"files": [
|
|
6
15
|
{
|
|
7
16
|
"path": "components/blocks/invoice-popup.tsx",
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "large-image-labels-list",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Vertical list of items with large landscape images, star ratings, price, and descriptive icon labels (beds, baths, area, etc.). Header with sort/filter controls and favorite button. Use for property listings, product catalogs, hotel results, or any image-prominent list.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"listings",
|
|
7
|
+
"property",
|
|
8
|
+
"images",
|
|
9
|
+
"catalog",
|
|
10
|
+
"real-estate",
|
|
11
|
+
"hotels"
|
|
12
|
+
],
|
|
13
|
+
"visualWeight": "heavy",
|
|
5
14
|
"files": [
|
|
6
15
|
{
|
|
7
16
|
"path": "components/blocks/large-image-labels-list.tsx",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "list-popup",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Editable list management modal with add/remove functionality using EditableList component. Centered dialog (~300px tall). Use for managing tag lists, skill lists, or any dynamic list that needs a dedicated editor.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"list",
|
|
7
|
+
"editable",
|
|
8
|
+
"modal",
|
|
9
|
+
"manage",
|
|
10
|
+
"add",
|
|
11
|
+
"remove"
|
|
12
|
+
],
|
|
13
|
+
"visualWeight": "light",
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/blocks/list-popup.tsx",
|
|
17
|
+
"type": "registry:block",
|
|
18
|
+
"content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { EditableList } from \"./editable-list\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ListPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Label above the list */\n listLabel?: string;\n /** Initial list items */\n items?: string[];\n /** Callback when save is clicked — receives the current list items */\n onSave?: (items: string[]) => void;\n /** Callback when cancel is clicked */\n onCancel?: () => void;\n /** Placeholder text for the add input */\n addPlaceholder?: string;\n /** Cancel button label */\n cancelLabel?: string;\n /** Save button label */\n saveLabel?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Make a list\";\nconst DEFAULT_LIST_LABEL = \"List\";\nconst DEFAULT_ITEMS = [\"Finance\", \"Technology\", \"Retail\", \"Real Estate\"];\n\n// ---------------------------------------------------------------------------\n// ListPopup\n// ---------------------------------------------------------------------------\n\nexport function ListPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n listLabel = DEFAULT_LIST_LABEL,\n items: initialItems = DEFAULT_ITEMS,\n onSave,\n onCancel,\n addPlaceholder = \"Enter category\",\n cancelLabel = \"Cancel\",\n saveLabel = \"Save changes\",\n className,\n}: ListPopupProps) {\n const [currentItems, setCurrentItems] = useState<string[]>(initialItems);\n\n // Reset items when dialog opens\n useEffect(() => {\n if (open) {\n setCurrentItems(initialItems);\n }\n }, [open, initialItems]);\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSave = () => {\n onSave?.(currentItems);\n onOpenChange?.(false);\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\n \"sm:max-w-[576px]\",\n className\n )}\n showCloseButton\n >\n {/* Title */}\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n\n {/* Editable List */}\n <EditableList\n label={listLabel}\n items={currentItems}\n onItemsChange={setCurrentItems}\n addPlaceholder={addPlaceholder}\n />\n\n {/* Actions */}\n <div className=\"flex w-full gap-[var(--spacing-3xl)] justify-end\">\n <Button variant=\"neutral\" onClick={handleCancel}>\n {cancelLabel}\n </Button>\n <Button variant=\"primary\" onClick={handleSave}>\n {saveLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"dependencies": [],
|
|
22
|
+
"registryDependencies": [
|
|
23
|
+
"lib/utils",
|
|
24
|
+
"ui/dialog",
|
|
25
|
+
"ui/button",
|
|
26
|
+
"blocks/editable-list"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loader",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "Loading feedback component with animated spinner (loading state) and checkmark (success state).
|
|
4
|
+
"description": "Loading feedback component with animated spinner (loading state) and checkmark (success state). Shows title, description, and optional action button. Use for form submissions, file uploads, or any async operation feedback.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"loading",
|
|
7
|
+
"spinner",
|
|
8
|
+
"feedback",
|
|
9
|
+
"async",
|
|
10
|
+
"success"
|
|
11
|
+
],
|
|
12
|
+
"visualWeight": "light",
|
|
5
13
|
"files": [
|
|
6
14
|
{
|
|
7
15
|
"path": "components/blocks/loader.tsx",
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "login-branding-panel",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Side panel with background image and centered logo for authentication pages. Takes up one half of a split-screen layout. Use alongside login, signup, or password reset forms.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"login",
|
|
7
|
+
"branding",
|
|
8
|
+
"auth",
|
|
9
|
+
"split-screen",
|
|
10
|
+
"signup"
|
|
11
|
+
],
|
|
12
|
+
"visualWeight": "spacer",
|
|
5
13
|
"files": [
|
|
6
14
|
{
|
|
7
15
|
"path": "components/blocks/login-branding-panel.tsx",
|
|
8
16
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\n\ninterface LoginBrandingPanelProps {\n /** Background image URL */\n backgroundImage?: string;\n /** Title text */\n title?: string;\n /** Description text */\n description?: string;\n /** Opacity of the flair color overlay (0-1, default 0.7) */\n overlayOpacity?: number;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Login Branding Panel Component\n *\n * A right-side branding panel for login/signup pages featuring:\n * - Full-height background image with semi-transparent flair color overlay\n * - Title and description text at the bottom\n *\n * The overlay uses the flair background CSS variable for live theming.\n */\nexport function LoginBrandingPanel({\n backgroundImage = \"/brand-assets/bg.jpg\",\n title = \"Title\",\n description = \"Description\",\n overlayOpacity = 0.
|
|
17
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\n\ninterface LoginBrandingPanelProps {\n /** Background image URL */\n backgroundImage?: string;\n /** Title text */\n title?: string;\n /** Description text */\n description?: string;\n /** Opacity of the flair color overlay (0-1, default 0.7) */\n overlayOpacity?: number;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Login Branding Panel Component\n *\n * A right-side branding panel for login/signup pages featuring:\n * - Full-height background image with semi-transparent flair color overlay\n * - Title and description text at the bottom\n *\n * The overlay uses the flair background CSS variable for live theming.\n */\nexport function LoginBrandingPanel({\n backgroundImage = \"/brand-assets/bg.jpg\",\n title = \"Title\",\n description = \"Description\",\n overlayOpacity = 0.85,\n className,\n}: LoginBrandingPanelProps) {\n return (\n <div\n className={cn(\n \"relative flex-1 flex flex-col overflow-hidden\",\n className\n )}\n >\n {/* Background Image */}\n <img\n src={backgroundImage}\n alt=\"\"\n className=\"absolute inset-0 w-full h-full object-cover pointer-events-none\"\n />\n\n {/* Gradient Color Overlay */}\n <div\n className=\"absolute inset-0\"\n style={{\n background: `linear-gradient(to top, var(--canvas-primary-dark) 0%, var(--canvas-primary-dark) 60%, var(--canvas-primary) 100%)`,\n opacity: overlayOpacity,\n }}\n />\n\n {/* Text Content at Bottom */}\n <div className=\"relative z-10 flex flex-col justify-end h-full p-16\">\n <div className=\"space-y-1.5\">\n <p\n className=\"text-white font-bold\"\n style={{\n fontSize: \"var(--typo-body-l-size)\",\n lineHeight: \"var(--typo-body-l-line-height)\",\n fontFamily: \"var(--typo-body-l-font, var(--typo-global-font))\",\n }}\n >\n {title}\n </p>\n <p\n className=\"text-white/90\"\n style={{\n fontSize: \"var(--typo-body-l-size)\",\n lineHeight: \"var(--typo-body-l-line-height)\",\n fontWeight: \"var(--typo-body-l-weight)\",\n fontFamily: \"var(--typo-body-l-font, var(--typo-global-font))\",\n }}\n >\n {description}\n </p>\n </div>\n </div>\n </div>\n );\n}\n\n"
|
|
10
18
|
}
|
|
11
19
|
],
|
|
12
20
|
"dependencies": [],
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "menu-section",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Collapsible navigation section with section title and list of menu items. Multiple sections stack vertically. Use for categorized navigation, settings menus, or documentation sidebars.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"menu",
|
|
7
|
+
"navigation",
|
|
8
|
+
"collapsible",
|
|
9
|
+
"sections",
|
|
10
|
+
"sidebar"
|
|
11
|
+
],
|
|
12
|
+
"visualWeight": "medium",
|
|
5
13
|
"files": [
|
|
6
14
|
{
|
|
7
15
|
"path": "components/blocks/menu-section.tsx",
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "menufocus-template",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Three-dot dropdown menu for row-level actions. Opens a list of action items on click. Use for table row actions, card actions, or any contextual menu.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"menu",
|
|
7
|
+
"dropdown",
|
|
8
|
+
"actions",
|
|
9
|
+
"context-menu",
|
|
10
|
+
"three-dot"
|
|
11
|
+
],
|
|
12
|
+
"visualWeight": "light",
|
|
5
13
|
"files": [
|
|
6
14
|
{
|
|
7
15
|
"path": "components/blocks/menufocus-template.tsx",
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "messenger-sidebar",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Vertical thread list sidebar for messaging apps. Each thread shows avatar, name, last message preview, timestamp, and unread badge. Includes search bar at top. Use for chat apps, support inboxes, or any thread-based messaging interface.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"chat",
|
|
7
|
+
"messaging",
|
|
8
|
+
"threads",
|
|
9
|
+
"inbox",
|
|
10
|
+
"conversations",
|
|
11
|
+
"support"
|
|
12
|
+
],
|
|
13
|
+
"visualWeight": "medium",
|
|
5
14
|
"files": [
|
|
6
15
|
{
|
|
7
16
|
"path": "components/blocks/messenger-sidebar.tsx",
|
|
8
17
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { MoreHorizontal, PenSquare } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar\";\nimport { Searchbox } from \"../ui/searchbox\";\nimport { AVATAR_SARAH_CHEN, AVATAR_ETHAN_BROOKS, AVATAR_JASON_MORALES } from \"./demo-avatars\";\n\
|
|
18
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { MoreHorizontal, PenSquare } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar\";\nimport { Searchbox } from \"../ui/searchbox\";\nimport { AVATAR_SARAH_CHEN, AVATAR_ETHAN_BROOKS, AVATAR_JASON_MORALES } from \"./demo-avatars\";\n\nexport interface ThreadItem {\n id: string;\n name: string;\n avatar?: string;\n lastMessage: string;\n timestamp: string;\n unreadCount?: number;\n isOnline?: boolean;\n}\n\nconst sampleThreads: ThreadItem[] = [\n {\n id: \"1\",\n name: \"Sarah Chen\",\n avatar: AVATAR_SARAH_CHEN,\n lastMessage: \"Sarah: Thank you so much for sending your...\",\n timestamp: \"Just now\",\n unreadCount: 3,\n },\n {\n id: \"2\",\n name: \"Ethan, Sarah, Marcus\",\n avatar: AVATAR_ETHAN_BROOKS,\n lastMessage: \"You: Hi Ethan, could you take a look at the doc\",\n timestamp: \"30 mins ago\",\n unreadCount: 3,\n },\n {\n id: \"3\",\n name: \"Jason Morales\",\n avatar: AVATAR_JASON_MORALES,\n lastMessage: \"You: Hi Jason, could you take a look at the doc\",\n timestamp: \"30 mins ago\",\n },\n];\n\ninterface MessengerSidebarProps {\n threads?: ThreadItem[];\n selectedThreadId?: string;\n onSelectThread?: (threadId: string) => void;\n className?: string;\n}\n\nexport function MessengerSidebar({\n threads,\n selectedThreadId,\n onSelectThread,\n className,\n}: MessengerSidebarProps) {\n const [searchValue, setSearchValue] = useState(\"\");\n\n const threadList = threads || sampleThreads;\n const filteredThreads = threadList.filter((thread) =>\n thread.name.toLowerCase().includes(searchValue.toLowerCase())\n );\n\n return (\n <aside\n className={`flex flex-col h-full border-r w-full md:w-[375px] shrink-0 ${className || \"\"}`}\n style={{\n borderColor: \"var(--canvas-border)\",\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n {/* Header */}\n <div\n className=\"flex items-center justify-between px-4 lg:px-[var(--spacing-5xl)] py-[var(--spacing-xl)] border-b shrink-0\"\n style={{ borderColor: \"var(--canvas-border)\" }}\n >\n <h1\n className=\"font-semibold\"\n style={{\n color: \"var(--canvas-foreground)\",\n fontSize: \"var(--typo-body-xl-size)\",\n lineHeight: \"28px\",\n }}\n >\n Messages\n </h1>\n <div className=\"flex items-center gap-[var(--spacing-md)]\">\n <button\n className=\"cursor-pointer flex items-center justify-center size-8 rounded-[var(--radius-xs)] border transition-colors hover:opacity-80\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n borderColor: \"var(--canvas-border)\",\n }}\n aria-label=\"More options\"\n >\n <MoreHorizontal\n className=\"size-4\"\n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n <button\n className=\"cursor-pointer flex items-center justify-center size-8 rounded-[var(--radius-xs)] border transition-colors hover:opacity-80\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n borderColor: \"var(--canvas-border)\",\n }}\n aria-label=\"Compose message\"\n >\n <PenSquare\n className=\"size-4\"\n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n </div>\n </div>\n\n {/* Search */}\n <div className=\"px-4 lg:px-[var(--spacing-5xl)] py-[var(--spacing-xl)] shrink-0 border-b\" style={{ borderColor: \"var(--canvas-border)\" }}>\n <Searchbox\n value={searchValue}\n onChange={setSearchValue}\n placeholder=\"Search messages\"\n inputSize=\"sm\"\n />\n </div>\n\n {/* Thread List */}\n <div className=\"flex-1 overflow-y-auto\">\n {filteredThreads.map((thread) => (\n <ThreadRow\n key={thread.id}\n thread={thread}\n isSelected={selectedThreadId === thread.id}\n onSelect={() => onSelectThread?.(thread.id)}\n />\n ))}\n </div>\n </aside>\n );\n}\n\ninterface ThreadRowProps {\n thread: ThreadItem;\n isSelected?: boolean;\n onSelect?: () => void;\n}\n\nfunction ThreadRow({ thread, isSelected, onSelect }: ThreadRowProps) {\n return (\n <button\n onClick={onSelect}\n className=\"cursor-pointer w-full flex items-center gap-[var(--spacing-xl)] px-4 lg:px-[var(--spacing-5xl)] py-[var(--spacing-xl)] transition-colors text-left border-b\"\n style={{\n backgroundColor: isSelected\n ? \"var(--canvas-surface)\"\n : \"var(--canvas-background)\",\n borderColor: \"var(--canvas-border)\",\n }}\n >\n {/* Avatar with unread badge */}\n <div className=\"relative shrink-0\">\n <Avatar className=\"size-12\">\n <AvatarImage src={thread.avatar} alt={thread.name} />\n <AvatarFallback\n style={{\n fontSize: \"var(--typo-body-xs-size)\",\n backgroundColor: \"var(--canvas-primary)\",\n color: \"var(--canvas-primary-foreground)\",\n }}\n >\n {thread.name\n .split(\" \")\n .map((n) => n[0])\n .join(\"\")}\n </AvatarFallback>\n </Avatar>\n {thread.unreadCount && (\n <div\n className=\"absolute size-5 rounded-full flex items-center justify-center text-[10px] font-semibold\"\n style={{\n backgroundColor: \"var(--canvas-text-placeholder)\",\n color: \"var(--canvas-primary-foreground)\",\n bottom: \"1px\",\n right: \"-2px\",\n }}\n >\n {thread.unreadCount}\n </div>\n )}\n </div>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0 flex flex-col gap-[var(--spacing-xs)]\">\n <div className=\"flex items-center justify-between gap-2\">\n <span\n className=\"font-semibold truncate\"\n style={{\n color: \"var(--canvas-foreground)\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {thread.name}\n </span>\n <span\n className=\"shrink-0 font-normal\"\n style={{\n color: \"var(--canvas-text-muted)\",\n fontSize: \"var(--typo-body-xs-size)\",\n lineHeight: \"var(--typo-body-xs-line-height)\",\n }}\n >\n {thread.timestamp}\n </span>\n </div>\n <div className=\"flex items-center gap-2\">\n <span\n className=\"truncate\"\n style={{\n color: \"var(--canvas-text-muted)\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {thread.lastMessage}\n </span>\n </div>\n </div>\n </button>\n );\n}\n\n"
|
|
10
19
|
}
|
|
11
20
|
],
|
|
12
21
|
"dependencies": [
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metrics-section",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "Row of large metric numbers with labels.",
|
|
4
|
+
"description": "Row of 2-4 large metric numbers with labels below, separated by left borders. Header with subtitle and title. Metrics grid shows 2 columns on mobile, 4 on desktop. Prefer this for marketing/landing page stat displays — includes left-border separators, h1-scale typography, and responsive grid. Use for company stats, key figures, achievement highlights, or any numeric overview section.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"metrics",
|
|
7
|
+
"stats",
|
|
8
|
+
"numbers",
|
|
9
|
+
"achievements",
|
|
10
|
+
"key-figures"
|
|
11
|
+
],
|
|
12
|
+
"visualWeight": "light",
|
|
5
13
|
"files": [
|
|
6
14
|
{
|
|
7
15
|
"path": "components/blocks/marketing/metrics-section.tsx",
|
|
8
16
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { Typography } from \"../../ui/typography\";\n\ninterface Metric {\n value: string;\n label: string;\n}\n\ninterface MetricsSectionProps {\n subtitle?: string;\n title?: string;\n metrics?: Metric[];\n}\n\nconst defaultMetrics: Metric[] = [\n { value: \"1,200+\", label: \"Team members\" },\n { value: \"2016\", label: \"Year founded\" },\n { value: \"3.5M+\", label: \"Users\" },\n { value: \"$22M\", label: \"In total funding\" },\n];\n\nexport function MetricsSection({\n subtitle = \"KEY METRICS\",\n title = \"At a glance\",\n metrics = defaultMetrics,\n}: MetricsSectionProps) {\n return (\n <section\n className=\"w-full px-6 md:px-20 py-12 md:py-16\"\n style={{ backgroundColor: \"var(--canvas-background)\" }}\n >\n <div className=\"max-w-[1240px] mx-auto flex flex-col gap-8 md:gap-12\">\n {/* Header */}\n <div className=\"flex flex-col gap-3\">\n <Typography variant=\"body-xs\" as=\"span\" className=\"uppercase tracking-wide\" color=\"muted\">\n {subtitle}\n </Typography>\n <Typography variant=\"h3\" as=\"h2\">\n {title}\n </Typography>\n </div>\n\n {/* Metrics Grid */}\n <div className=\"grid grid-cols-2
|
|
17
|
+
"content": "\"use client\";\n\nimport { Typography } from \"../../ui/typography\";\n\ninterface Metric {\n value: string;\n label: string;\n}\n\ninterface MetricsSectionProps {\n subtitle?: string;\n title?: string;\n metrics?: Metric[];\n}\n\nconst defaultMetrics: Metric[] = [\n { value: \"1,200+\", label: \"Team members\" },\n { value: \"2016\", label: \"Year founded\" },\n { value: \"3.5M+\", label: \"Users\" },\n { value: \"$22M\", label: \"In total funding\" },\n];\n\nexport function MetricsSection({\n subtitle = \"KEY METRICS\",\n title = \"At a glance\",\n metrics = defaultMetrics,\n}: MetricsSectionProps) {\n return (\n <section\n className=\"w-full px-6 md:px-20 py-12 md:py-16\"\n style={{ backgroundColor: \"var(--canvas-background)\" }}\n >\n <div className=\"max-w-[1240px] mx-auto flex flex-col gap-8 md:gap-12\">\n {/* Header */}\n <div className=\"flex flex-col gap-3\">\n <Typography variant=\"body-xs\" as=\"span\" className=\"uppercase tracking-wide\" color=\"muted\">\n {subtitle}\n </Typography>\n <Typography variant=\"h3\" as=\"h2\">\n {title}\n </Typography>\n </div>\n\n {/* Metrics Grid */}\n <div className=\"grid grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-16\">\n {metrics.map((metric, index) => (\n <div\n key={index}\n className=\"flex flex-col gap-2 pl-4 md:pl-5 py-1.5\"\n style={{\n borderLeft: \"1px solid var(--canvas-border)\",\n }}\n >\n <Typography variant=\"h1\" as=\"span\">\n {metric.value}\n </Typography>\n <Typography variant=\"body-l\" as=\"span\" color=\"muted\">\n {metric.label}\n </Typography>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n"
|
|
10
18
|
}
|
|
11
19
|
],
|
|
12
20
|
"dependencies": [],
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-bottom-nav",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "Fixed bottom navigation bar for mobile layouts.",
|
|
4
|
+
"description": "Fixed bottom navigation bar with icon tabs (Home, Search, Profile, etc.). Sticks to bottom of viewport. Use for mobile app-like experiences or responsive layouts needing bottom tab navigation.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mobile",
|
|
7
|
+
"bottom",
|
|
8
|
+
"navigation",
|
|
9
|
+
"tabs",
|
|
10
|
+
"app"
|
|
11
|
+
],
|
|
12
|
+
"visualWeight": "light",
|
|
5
13
|
"files": [
|
|
6
14
|
{
|
|
7
15
|
"path": "components/blocks/mobile-bottom-nav.tsx",
|
|
8
16
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { LucideIcon, Home, MessageSquare, Search, User } from \"lucide-react\";\n\n// ============================================\n// Mobile Nav Tab Item\n// ============================================\n\nexport interface MobileNavTabConfig {\n id: string;\n label: string;\n icon: LucideIcon;\n isActive?: boolean;\n}\n\ninterface MobileNavTabProps {\n item: MobileNavTabConfig;\n variant?: \"dark\" | \"light\";\n onClick?: () => void;\n}\n\nfunction MobileNavTab({ item, variant = \"light\", onClick }: MobileNavTabProps) {\n const Icon = item.icon;\n const isActive = item.isActive;\n const isDark = variant === \"dark\";\n\n return (\n <button\n onClick={onClick}\n className={cn(\n // Match icon-sidebar dimensions: 64px × 64px\n \"cursor-pointer relative flex flex-col items-center justify-center gap-1 w-16 h-16 rounded-[var(--radius-nav)] transition-colors\",\n // Dark variant\n isDark && isActive && \"bg-[var(--canvas-sidebar-dark-active-bg)]\",\n isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isActive && \"bg-[var(--canvas-sidebar-light-active-bg)]\",\n !isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <Icon\n className={cn(\n // Match icon-sidebar: 16px icons\n \"size-4\",\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n />\n <span\n className={cn(\n // Match icon-sidebar: 12px labels, medium weight\n \"font-medium\",\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n style={{ fontSize: \"var(--typo-sidebar-label-size)\" }}\n >\n {item.label}\n </span>\n </button>\n );\n}\n\n// ============================================\n// Default Navigation Items\n// ============================================\n\nexport const defaultMobileNavTabs: MobileNavTabConfig[] = [\n { id: \"home\", label: \"Home\", icon: Home, isActive: true },\n { id: \"messages\", label: \"Messages\", icon: MessageSquare },\n { id: \"discover\", label: \"Discover\", icon: Search },\n { id: \"account\", label: \"Account\", icon: User },\n];\n\n// ============================================\n// Mobile Bottom Navigation\n// ============================================\n\ninterface MobileBottomNavProps {\n /** Navigation tabs to display */\n tabs?: MobileNavTabConfig[];\n /** Visual variant - dark or light theme */\n variant?: \"dark\" | \"light\";\n /** Callback when a tab is clicked */\n onTabClick?: (tab: MobileNavTabConfig) => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Mobile Bottom Navigation\n * \n * A sticky bottom navigation bar with icon tabs.\n * Styling matches the icon-sidebar for consistency.\n * Supports both dark and light themes via the variant prop.\n * \n * @example\n * ```tsx\n * <MobileBottomNav\n * variant=\"light\"\n * tabs={defaultMobileNavTabs}\n * onTabClick={(tab) => console.log(tab.id)}\n * />\n * ```\n */\nexport function MobileBottomNav({\n tabs = defaultMobileNavTabs,\n variant = \"light\",\n onTabClick,\n className,\n}: MobileBottomNavProps) {\n const isDark = variant === \"dark\";\n\n return (\n <nav\n className={cn(\n \"fixed bottom-0 left-0 right-0 z-50\",\n \"flex items-center justify-center gap-5\",\n \"px-4 py-3\",\n // Dark variant\n isDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-t border-[var(--canvas-sidebar-dark-border)]\",\n isDark && \"shadow-[0px_-4px_16px_0px_rgba(0,0,0,0.2)]\",\n // Light variant\n !isDark && \"bg-[var(--canvas-sidebar-light-bg)] border-t border-[var(--canvas-sidebar-light-border)]\",\n !isDark && \"shadow-[0px_-4px_16px_0px_rgba(0,0,0,0.04)]\",\n className\n )}\n >\n {tabs.map((tab) => (\n <MobileNavTab\n key={tab.id}\n item={tab}\n variant={variant}\n onClick={() => onTabClick?.(tab)}\n />\n ))}\n </nav>\n );\n}\n"
|
|
17
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { LucideIcon, Home, MessageSquare, Search, User } from \"lucide-react\";\n\n// ============================================\n// Mobile Nav Tab Item\n// ============================================\n\nexport interface MobileNavTabConfig {\n id: string;\n label: string;\n icon: LucideIcon;\n isActive?: boolean;\n}\n\ninterface MobileNavTabProps {\n item: MobileNavTabConfig;\n variant?: \"dark\" | \"light\";\n onClick?: () => void;\n}\n\nfunction MobileNavTab({ item, variant = \"light\", onClick }: MobileNavTabProps) {\n const Icon = item.icon;\n const isActive = item.isActive;\n const isDark = variant === \"dark\";\n\n return (\n <button\n onClick={onClick}\n className={cn(\n // Match icon-sidebar dimensions: 64px × 64px\n \"cursor-pointer relative flex flex-col items-center justify-center gap-1 w-16 h-16 rounded-[var(--radius-nav)] transition-colors\",\n // Dark variant\n isDark && isActive && \"bg-[var(--canvas-sidebar-dark-active-bg)]\",\n isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isActive && \"bg-[var(--canvas-sidebar-light-active-bg)]\",\n !isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <Icon\n className={cn(\n // Match icon-sidebar: 16px icons\n \"size-4\",\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n />\n <span\n className={cn(\n // Match icon-sidebar: 12px labels, medium weight\n \"font-medium\",\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n style={{ fontSize: \"var(--typo-sidebar-label-size)\", fontFamily: \"var(--typo-sidebar-label-font, var(--typo-global-font))\" }}\n >\n {item.label}\n </span>\n </button>\n );\n}\n\n// ============================================\n// Default Navigation Items\n// ============================================\n\nexport const defaultMobileNavTabs: MobileNavTabConfig[] = [\n { id: \"home\", label: \"Home\", icon: Home, isActive: true },\n { id: \"messages\", label: \"Messages\", icon: MessageSquare },\n { id: \"discover\", label: \"Discover\", icon: Search },\n { id: \"account\", label: \"Account\", icon: User },\n];\n\n// ============================================\n// Mobile Bottom Navigation\n// ============================================\n\ninterface MobileBottomNavProps {\n /** Navigation tabs to display */\n tabs?: MobileNavTabConfig[];\n /** Visual variant - dark or light theme */\n variant?: \"dark\" | \"light\";\n /** Callback when a tab is clicked */\n onTabClick?: (tab: MobileNavTabConfig) => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Mobile Bottom Navigation\n * \n * A sticky bottom navigation bar with icon tabs.\n * Styling matches the icon-sidebar for consistency.\n * Supports both dark and light themes via the variant prop.\n * \n * @example\n * ```tsx\n * <MobileBottomNav\n * variant=\"light\"\n * tabs={defaultMobileNavTabs}\n * onTabClick={(tab) => console.log(tab.id)}\n * />\n * ```\n */\nexport function MobileBottomNav({\n tabs = defaultMobileNavTabs,\n variant = \"light\",\n onTabClick,\n className,\n}: MobileBottomNavProps) {\n const isDark = variant === \"dark\";\n\n return (\n <nav\n className={cn(\n \"fixed bottom-0 left-0 right-0 z-50\",\n \"flex items-center justify-center gap-5\",\n \"px-4 py-3\",\n // Dark variant\n isDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-t border-[var(--canvas-sidebar-dark-border)]\",\n isDark && \"shadow-[0px_-4px_16px_0px_rgba(0,0,0,0.2)]\",\n // Light variant\n !isDark && \"bg-[var(--canvas-sidebar-light-bg)] border-t border-[var(--canvas-sidebar-light-border)]\",\n !isDark && \"shadow-[0px_-4px_16px_0px_rgba(0,0,0,0.04)]\",\n className\n )}\n >\n {tabs.map((tab) => (\n <MobileNavTab\n key={tab.id}\n item={tab}\n variant={variant}\n onClick={() => onTabClick?.(tab)}\n />\n ))}\n </nav>\n );\n}\n"
|
|
10
18
|
}
|
|
11
19
|
],
|
|
12
20
|
"dependencies": [
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "monthly-calendar-widget",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "Dual-month calendar for date
|
|
4
|
+
"description": "Dual-month calendar for selecting date ranges. Shows two months side by side with price labels on dates, disabled dates, and today indicator. Use for booking flows, scheduling interfaces, or any date range selection.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"calendar",
|
|
7
|
+
"booking",
|
|
8
|
+
"dates",
|
|
9
|
+
"scheduling",
|
|
10
|
+
"date-range"
|
|
11
|
+
],
|
|
12
|
+
"visualWeight": "medium",
|
|
5
13
|
"files": [
|
|
6
14
|
{
|
|
7
15
|
"path": "components/blocks/monthly-calendar-widget.tsx",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "multistep-form-popup",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Multi-step form wizard modal with configurable form steps, billing plan selection step, and loading/success state displays. Centered dialog (~450px tall). Use for complex multi-step processes in a modal: subscription signups, onboarding wizards, or multi-page forms.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"multistep",
|
|
7
|
+
"wizard",
|
|
8
|
+
"modal",
|
|
9
|
+
"form",
|
|
10
|
+
"steps",
|
|
11
|
+
"subscription"
|
|
12
|
+
],
|
|
13
|
+
"visualWeight": "medium",
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/blocks/multistep-form-popup.tsx",
|
|
17
|
+
"type": "registry:block",
|
|
18
|
+
"content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Dialog, DialogContent } from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { Label } from \"../ui/label\";\nimport { Input } from \"../ui/input\";\nimport { Textarea } from \"../ui/textarea\";\nimport { DateInput } from \"../ui/date-input\";\nimport {\n Select,\n SelectTrigger,\n SelectContent,\n SelectItem,\n SelectValue,\n} from \"../ui/select\";\nimport { MultiselectTags } from \"../ui/multiselect-tags\";\nimport { Loader } from \"./loader\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MultistepField {\n /** Unique field identifier */\n id: string;\n /** Field label */\n label: string;\n /** Field input type */\n type:\n | \"text\"\n | \"select\"\n | \"date\"\n | \"textarea\"\n | \"multiselect-tags\";\n /** Placeholder text */\n placeholder?: string;\n /** When true, the field takes 50% width and pairs with the next half field */\n half?: boolean;\n /** Options for select or multiselect-tags */\n options?: { id: string; label: string }[];\n /** Default value */\n value?: string | string[];\n}\n\nexport interface MultistepFormStep {\n /** Step title */\n title: string;\n /** Step description shown below the title */\n description?: string;\n /** Form fields for this step */\n fields: MultistepField[];\n}\n\nexport interface BillingPlan {\n /** Unique plan identifier */\n id: string;\n /** Plan name */\n name: string;\n /** Price display text */\n price: string;\n /** Description text */\n description: string;\n /** Optional badge text */\n badge?: string;\n}\n\nexport interface MultistepFormPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Step configurations — each has title, description, and fields */\n steps?: MultistepFormStep[];\n /** Billing plan options for the final step */\n billingPlans?: BillingPlan[];\n /** Loading state title */\n loadingTitle?: string;\n /** Loading state description */\n loadingDescription?: string;\n /** Success state title */\n successTitle?: string;\n /** Success state description */\n successDescription?: string;\n /** Success action button text */\n successButtonText?: string;\n /** Callback when the form is completed (after all steps) */\n onComplete?: () => void;\n /** Callback when the success action button is clicked */\n onSuccessButtonClick?: () => void;\n /** Additional class names for the dialog content */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_STEPS: MultistepFormStep[] = [\n {\n title: \"Enter your personal information to get started\",\n fields: [\n { id: \"email\", label: \"Email\", type: \"text\", placeholder: \"jane@example.com\" },\n { id: \"company\", label: \"Company name\", type: \"text\", placeholder: \"Acme Inc.\" },\n { id: \"firstName\", label: \"First name\", type: \"text\", placeholder: \"Jane\", half: true },\n { id: \"lastName\", label: \"Last name\", type: \"text\", placeholder: \"Doe\", half: true },\n { id: \"city\", label: \"City\", type: \"text\", placeholder: \"San Francisco\" },\n {\n id: \"country\",\n label: \"Country\",\n type: \"select\",\n half: true,\n options: [\n { id: \"us\", label: \"United States\" },\n { id: \"ca\", label: \"Canada\" },\n { id: \"uk\", label: \"United Kingdom\" },\n { id: \"au\", label: \"Australia\" },\n ],\n },\n {\n id: \"state\",\n label: \"State\",\n type: \"select\",\n half: true,\n options: [\n { id: \"ca\", label: \"California\" },\n { id: \"ny\", label: \"New York\" },\n { id: \"tx\", label: \"Texas\" },\n { id: \"wa\", label: \"Washington\" },\n ],\n },\n { id: \"zip\", label: \"Zip / Postal code\", type: \"text\", placeholder: \"94102\" },\n { id: \"profession\", label: \"Profession\", type: \"text\", placeholder: \"Software Engineer\", half: true },\n { id: \"startDate\", label: \"Start date\", type: \"date\", half: true },\n ],\n },\n {\n title: \"Enter additional information below\",\n fields: [\n { id: \"address\", label: \"Address\", type: \"text\", placeholder: \"123 Main St\" },\n { id: \"stateProvince\", label: \"State / Province\", type: \"text\", placeholder: \"California\" },\n {\n id: \"occupation\",\n label: \"Occupation\",\n type: \"select\",\n half: true,\n options: [\n { id: \"eng\", label: \"Engineering\" },\n { id: \"design\", label: \"Design\" },\n { id: \"marketing\", label: \"Marketing\" },\n { id: \"sales\", label: \"Sales\" },\n ],\n },\n {\n id: \"status\",\n label: \"Status\",\n type: \"select\",\n half: true,\n options: [\n { id: \"active\", label: \"Active\" },\n { id: \"inactive\", label: \"Inactive\" },\n { id: \"pending\", label: \"Pending\" },\n ],\n },\n { id: \"phone\", label: \"Phone\", type: \"text\", placeholder: \"(555) 123-4567\", half: true },\n { id: \"fax\", label: \"Fax\", type: \"text\", placeholder: \"(555) 987-6543\", half: true },\n { id: \"step2StartDate\", label: \"Start date\", type: \"date\", half: true },\n { id: \"step2EndDate\", label: \"End date\", type: \"date\", half: true },\n { id: \"address2\", label: \"Address line 2\", type: \"text\", placeholder: \"Suite 200\" },\n {\n id: \"tags\",\n label: \"Tags\",\n type: \"multiselect-tags\",\n options: [\n { id: \"finance\", label: \"Finance\" },\n { id: \"technology\", label: \"Technology\" },\n { id: \"healthcare\", label: \"Healthcare\" },\n { id: \"education\", label: \"Education\" },\n { id: \"retail\", label: \"Retail\" },\n ],\n value: [],\n },\n ],\n },\n {\n title: \"Complete your account setup\",\n fields: [\n { id: \"step3Phone\", label: \"Phone\", type: \"text\", placeholder: \"(555) 123-4567\", half: true },\n { id: \"extension\", label: \"Extension\", type: \"text\", placeholder: \"1234\", half: true },\n { id: \"step3StartDate\", label: \"Start date\", type: \"date\", half: true },\n { id: \"step3EndDate\", label: \"End date\", type: \"date\", half: true },\n { id: \"copyrightYear\", label: \"Copyright year\", type: \"text\", placeholder: \"2026\", half: true },\n { id: \"licenseType\", label: \"License type\", type: \"text\", placeholder: \"Standard\", half: true },\n { id: \"notes\", label: \"Notes\", type: \"textarea\", placeholder: \"Add any additional notes here...\" },\n ],\n },\n];\n\nconst DEFAULT_BILLING_PLANS: BillingPlan[] = [\n {\n id: \"annual\",\n name: \"Annual billing\",\n price: \"$120/month\",\n description: \"Billed annually\",\n badge: \"Save 5%!\",\n },\n {\n id: \"monthly\",\n name: \"Monthly billing\",\n price: \"$150/month\",\n description: \"after your 30-day free trial\",\n },\n];\n\n// ---------------------------------------------------------------------------\n// Inline Sub-Components\n// ---------------------------------------------------------------------------\n\nconst fontBase = \"var(--typo-global-font)\";\n\n/** Step indicator badge + title + description */\nfunction StepHeader({\n step,\n title,\n description,\n}: {\n step: number;\n title: string;\n description?: string;\n}) {\n return (\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <div className=\"flex items-center\" style={{ gap: \"var(--spacing-xl)\" }}>\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: 40,\n height: 40,\n borderRadius: \"var(--spacing-3xl)\",\n backgroundColor: \"var(--canvas-neutral-surface)\",\n }}\n >\n <span\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n lineHeight: \"24px\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {step}\n </span>\n </div>\n <h2\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n lineHeight: \"30px\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n </div>\n {description && (\n <p\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"20px\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n paddingLeft: 56,\n }}\n >\n {description}\n </p>\n )}\n </div>\n );\n}\n\n/** Renders form fields with half-width pairing */\nfunction StepFormFields({\n fields,\n values,\n onChange,\n}: {\n fields: MultistepField[];\n values: Record<string, unknown>;\n onChange: (id: string, value: unknown) => void;\n}) {\n // Group fields into rows: half-width fields paired together\n const rows: MultistepField[][] = [];\n let i = 0;\n while (i < fields.length) {\n const field = fields[i];\n if (field.half && i + 1 < fields.length && fields[i + 1].half) {\n rows.push([field, fields[i + 1]]);\n i += 2;\n } else {\n rows.push([field]);\n i += 1;\n }\n }\n\n return (\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-3xl)\" }}>\n {rows.map((row, rowIdx) => (\n <div\n key={rowIdx}\n className={cn(\n \"flex gap-[var(--spacing-3xl)]\",\n row.length > 1 ? \"flex-col md:flex-row\" : \"flex-col\"\n )}\n >\n {row.map((field) => (\n <div\n key={field.id}\n className={cn(\n \"flex flex-col\",\n row.length > 1 ? \"flex-1\" : \"w-full\"\n )}\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <Label>{field.label}</Label>\n {renderField(field, values, onChange)}\n </div>\n ))}\n </div>\n ))}\n </div>\n );\n}\n\nfunction renderField(\n field: MultistepField,\n values: Record<string, unknown>,\n onChange: (id: string, value: unknown) => void\n) {\n switch (field.type) {\n case \"textarea\":\n return (\n <Textarea\n inputSize=\"sm\"\n value={(values[field.id] as string) ?? \"\"}\n onChange={(e) => onChange(field.id, e.target.value)}\n placeholder={field.placeholder}\n className=\"resize-none\"\n />\n );\n\n case \"select\":\n return (\n <Select\n value={(values[field.id] as string) ?? \"\"}\n onValueChange={(v) => onChange(field.id, v)}\n >\n <SelectTrigger style={{ height: \"var(--input-standard-height)\" }}>\n <SelectValue placeholder={field.placeholder ?? \"Select\"} />\n </SelectTrigger>\n <SelectContent>\n {field.options?.map((opt) => (\n <SelectItem key={opt.id} value={opt.id}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n );\n\n case \"date\":\n return (\n <DateInput\n value={(values[field.id] as string) ?? \"\"}\n onChange={(v) => onChange(field.id, v)}\n />\n );\n\n case \"multiselect-tags\":\n return (\n <MultiselectTags\n tags={(values[field.id] as string[]) ?? (field.value as string[]) ?? []}\n placeholder={field.placeholder ?? \"Add...\"}\n onAdd={(tag: string) => {\n const current = (values[field.id] as string[]) ?? (field.value as string[]) ?? [];\n onChange(field.id, [...current, tag]);\n }}\n onRemove={(tag: string) => {\n const current = (values[field.id] as string[]) ?? (field.value as string[]) ?? [];\n onChange(field.id, current.filter((t: string) => t !== tag));\n }}\n />\n );\n\n default:\n return (\n <Input\n type=\"text\"\n value={(values[field.id] as string) ?? \"\"}\n onChange={(e) => onChange(field.id, e.target.value)}\n placeholder={field.placeholder}\n />\n );\n }\n}\n\n/** Billing plan radio card */\nfunction BillingPlanCard({\n plan,\n selected,\n onClick,\n}: {\n plan: BillingPlan;\n selected: boolean;\n onClick: () => void;\n}) {\n return (\n <button\n className=\"flex items-start w-full text-left cursor-pointer\"\n onClick={onClick}\n style={{\n padding: \"var(--spacing-xl)\",\n borderRadius: \"var(--radius-md)\",\n border: selected\n ? \"2px solid var(--canvas-primary)\"\n : \"1px solid var(--canvas-border)\",\n backgroundColor: selected\n ? \"color-mix(in srgb, var(--canvas-primary) 4%, var(--canvas-background))\"\n : \"var(--canvas-background)\",\n gap: \"var(--spacing-lg)\",\n }}\n >\n {/* Radio dot */}\n <div\n className=\"shrink-0 flex items-center justify-center\"\n style={{\n width: 20,\n height: 20,\n borderRadius: \"50%\",\n marginTop: 2,\n border: selected\n ? \"2px solid var(--canvas-primary)\"\n : \"2px solid var(--canvas-border)\",\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n {selected && (\n <div\n style={{\n width: 10,\n height: 10,\n borderRadius: \"50%\",\n backgroundColor: \"var(--canvas-primary)\",\n }}\n />\n )}\n </div>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <span\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"24px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {plan.name}\n </span>\n {plan.badge && (\n <span\n className=\"shrink-0\"\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-xs-size)\",\n fontWeight: 600,\n lineHeight: \"16px\",\n padding: \"2px 8px\",\n borderRadius: \"var(--radius-sm)\",\n backgroundColor: \"var(--canvas-primary)\",\n color: \"var(--canvas-primary-foreground)\",\n }}\n >\n {plan.badge}\n </span>\n )}\n </div>\n <p\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"20px\",\n color: \"var(--canvas-text-placeholder)\",\n margin: 0,\n }}\n >\n {plan.description}\n </p>\n </div>\n\n {/* Price */}\n <span\n className=\"shrink-0\"\n style={{\n fontFamily: fontBase,\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"24px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {plan.price}\n </span>\n </button>\n );\n}\n\n/** Back / Continue navigation buttons */\nfunction StepNav({\n onBack,\n onNext,\n nextLabel = \"Continue\",\n showBack = true,\n}: {\n onBack?: () => void;\n onNext: () => void;\n nextLabel?: string;\n showBack?: boolean;\n}) {\n return (\n <div\n className=\"flex items-center justify-end\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {showBack && (\n <Button variant=\"neutral\" onClick={onBack}>\n Back\n </Button>\n )}\n <Button variant=\"primary\" onClick={onNext}>\n {nextLabel}\n </Button>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// MultistepFormPopup\n// ---------------------------------------------------------------------------\n\nexport function MultistepFormPopup({\n open,\n onOpenChange,\n steps = DEFAULT_STEPS,\n billingPlans = DEFAULT_BILLING_PLANS,\n loadingTitle = \"Please wait...\",\n loadingDescription = \"We are currently processing your submission\",\n successTitle = \"Success!\",\n successDescription = \"Your account has been set up successfully\",\n successButtonText = \"Go to portal\",\n onComplete,\n onSuccessButtonClick,\n className,\n}: MultistepFormPopupProps) {\n const [currentStep, setCurrentStep] = useState(0);\n const [phase, setPhase] = useState<\"form\" | \"loading\" | \"success\">(\"form\");\n const [values, setValues] = useState<Record<string, unknown>>({});\n const [selectedPlan, setSelectedPlan] = useState(\n billingPlans[0]?.id ?? \"\"\n );\n\n // Reset state when dialog closes\n useEffect(() => {\n if (!open) {\n setCurrentStep(0);\n setPhase(\"form\");\n setValues({});\n setSelectedPlan(billingPlans[0]?.id ?? \"\");\n }\n }, [open, billingPlans]);\n\n const totalFormSteps = steps.length;\n const totalStepsWithBilling = totalFormSteps + 1; // +1 for billing step\n\n const handleFieldChange = (id: string, value: unknown) => {\n setValues((prev) => ({ ...prev, [id]: value }));\n };\n\n const goBack = () => {\n if (currentStep > 0) {\n setCurrentStep((s) => s - 1);\n }\n };\n\n const goNext = () => {\n if (currentStep < totalStepsWithBilling - 1) {\n setCurrentStep((s) => s + 1);\n } else {\n // Final step — trigger loading → success\n setPhase(\"loading\");\n onComplete?.();\n setTimeout(() => {\n setPhase(\"success\");\n }, 2000);\n }\n };\n\n const isBillingStep = currentStep === totalFormSteps;\n const stepConfig = !isBillingStep ? steps[currentStep] : null;\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-0 gap-0 overflow-hidden\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\n \"sm:max-w-[640px]\",\n className\n )}\n showCloseButton={phase === \"form\"}\n >\n {phase === \"form\" && (\n <div\n className=\"flex flex-col overflow-y-auto\"\n style={{\n padding: \"var(--spacing-4xl)\",\n gap: \"var(--spacing-3xl)\",\n maxHeight: \"85vh\",\n }}\n >\n {/* Step Header */}\n <StepHeader\n step={currentStep + 1}\n title={\n isBillingStep\n ? \"Select a billing plan for your organization\"\n : stepConfig?.title ?? \"\"\n }\n description={\n isBillingStep ? undefined : stepConfig?.description\n }\n />\n\n {/* Step Content */}\n {isBillingStep ? (\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n {billingPlans.map((plan) => (\n <BillingPlanCard\n key={plan.id}\n plan={plan}\n selected={selectedPlan === plan.id}\n onClick={() => setSelectedPlan(plan.id)}\n />\n ))}\n </div>\n ) : (\n <StepFormFields\n fields={stepConfig?.fields ?? []}\n values={values}\n onChange={handleFieldChange}\n />\n )}\n\n {/* Navigation */}\n <StepNav\n onBack={goBack}\n onNext={goNext}\n showBack={currentStep > 0}\n nextLabel={\n isBillingStep ? \"Complete\" : \"Continue\"\n }\n />\n </div>\n )}\n\n {phase === \"loading\" && (\n <div\n className=\"flex items-center justify-center\"\n style={{\n padding: \"var(--spacing-4xl)\",\n minHeight: 320,\n }}\n >\n <Loader\n state=\"loading\"\n title={loadingTitle}\n description={loadingDescription}\n />\n </div>\n )}\n\n {phase === \"success\" && (\n <div\n className=\"flex items-center justify-center\"\n style={{\n padding: \"var(--spacing-4xl)\",\n minHeight: 320,\n }}\n >\n <Loader\n state=\"success\"\n title={successTitle}\n description={successDescription}\n buttonText={successButtonText}\n onButtonClick={() => {\n onSuccessButtonClick?.();\n onOpenChange?.(false);\n }}\n />\n </div>\n )}\n </DialogContent>\n </Dialog>\n );\n}\n"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"dependencies": [],
|
|
22
|
+
"registryDependencies": [
|
|
23
|
+
"lib/utils",
|
|
24
|
+
"ui/dialog",
|
|
25
|
+
"ui/button",
|
|
26
|
+
"ui/label",
|
|
27
|
+
"ui/input",
|
|
28
|
+
"ui/textarea",
|
|
29
|
+
"ui/date-input",
|
|
30
|
+
"ui/select",
|
|
31
|
+
"ui/multiselect-tags",
|
|
32
|
+
"blocks/loader"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nested-comments-table",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "Threaded discussion
|
|
4
|
+
"description": "Threaded discussion with nested comments, reply and like actions, and collapsible sub-threads. Each comment shows avatar, name, timestamp, and content. Use for blog comments, discussion boards, or any nested conversation interface.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"comments",
|
|
7
|
+
"discussion",
|
|
8
|
+
"threaded",
|
|
9
|
+
"replies",
|
|
10
|
+
"forum"
|
|
11
|
+
],
|
|
12
|
+
"visualWeight": "heavy",
|
|
5
13
|
"files": [
|
|
6
14
|
{
|
|
7
15
|
"path": "components/blocks/nested-comments-table.tsx",
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nested-data-table",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "Expandable data table with parent rows that reveal nested child tables. Shows hierarchical data
|
|
4
|
+
"description": "Expandable data table with parent rows that reveal nested child tables on click. Shows hierarchical data with expand/collapse toggles. Full-width block (~400-800px). Dense, text-heavy. Use for organizational hierarchies, location-based data, or any parent-child data relationships.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"table",
|
|
7
|
+
"nested",
|
|
8
|
+
"hierarchy",
|
|
9
|
+
"expandable",
|
|
10
|
+
"parent-child",
|
|
11
|
+
"tree"
|
|
12
|
+
],
|
|
13
|
+
"visualWeight": "heavy",
|
|
5
14
|
"files": [
|
|
6
15
|
{
|
|
7
16
|
"path": "components/blocks/nested-data-table.tsx",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nps-survey-popup",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Numeric rating survey modal with number buttons (configurable 1-10 range) and end-labels (e.g., 'Not likely' to 'Very likely'). Compact centered dialog (~200px tall). Use for NPS surveys, satisfaction ratings, or any numeric scale feedback.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nps",
|
|
7
|
+
"survey",
|
|
8
|
+
"rating",
|
|
9
|
+
"modal",
|
|
10
|
+
"score",
|
|
11
|
+
"feedback"
|
|
12
|
+
],
|
|
13
|
+
"visualWeight": "light",
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/blocks/nps-survey-popup.tsx",
|
|
17
|
+
"type": "registry:block",
|
|
18
|
+
"content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface NpsSurveyPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text */\n description?: string;\n /** Currently selected score (controlled) */\n value?: number | null;\n /** Callback when submit is clicked — receives the selected score */\n onSubmit?: (score: number) => void;\n /** Callback when cancel is clicked */\n onCancel?: () => void;\n /** Label for the low end of the scale */\n minLabel?: string;\n /** Label for the high end of the scale */\n maxLabel?: string;\n /** Minimum score value */\n min?: number;\n /** Maximum score value */\n max?: number;\n /** Cancel button label */\n cancelLabel?: string;\n /** Submit button label */\n submitLabel?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Your input is valuable to us\";\nconst DEFAULT_DESCRIPTION =\n \"How likely are you to recommend Sample App to a friend or colleague?\";\n\n// ---------------------------------------------------------------------------\n// NpsSurveyPopup\n// ---------------------------------------------------------------------------\n\nexport function NpsSurveyPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n value: controlledValue,\n onSubmit,\n onCancel,\n minLabel = \"Not likely\",\n maxLabel = \"Very likely\",\n min = 1,\n max = 10,\n cancelLabel = \"Cancel\",\n submitLabel = \"Submit\",\n className,\n}: NpsSurveyPopupProps) {\n const [selected, setSelected] = useState<number | null>(controlledValue ?? null);\n\n // Reset when dialog opens\n useEffect(() => {\n if (open) {\n setSelected(controlledValue ?? null);\n }\n }, [open, controlledValue]);\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSubmit = () => {\n if (selected !== null) {\n onSubmit?.(selected);\n onOpenChange?.(false);\n }\n };\n\n const scores = Array.from({ length: max - min + 1 }, (_, i) => min + i);\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\n \"sm:max-w-[576px]\",\n className\n )}\n showCloseButton\n >\n {/* Title */}\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n\n {/* Description */}\n <DialogDescription\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 color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n\n {/* Score buttons */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <div\n className=\"flex flex-wrap justify-center\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {scores.map((score) => {\n const isSelected = selected === score;\n return (\n <button\n key={score}\n type=\"button\"\n onClick={() => setSelected(score)}\n className={cn(\n \"flex items-center justify-center border rounded-[var(--radius-xs)] transition-colors cursor-pointer\",\n isSelected\n ? \"bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)] border-[var(--canvas-primary)]\"\n : \"bg-[var(--canvas-background)] text-[var(--canvas-text)] border-[var(--canvas-border)] hover:bg-[var(--canvas-surface-hover)]\"\n )}\n style={{\n width: \"var(--spacing-5xl)\",\n height: \"var(--spacing-5xl)\",\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n }}\n >\n {score}\n </button>\n );\n })}\n </div>\n\n {/* End labels */}\n <div\n className=\"flex justify-between w-full\"\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 color: \"var(--canvas-text-muted)\",\n }}\n >\n <span>{minLabel}</span>\n <span>{maxLabel}</span>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"flex w-full gap-[var(--spacing-3xl)] justify-end\">\n <Button variant=\"neutral\" onClick={handleCancel}>\n {cancelLabel}\n </Button>\n <Button\n variant=\"primary\"\n onClick={handleSubmit}\n disabled={selected === null}\n >\n {submitLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"dependencies": [],
|
|
22
|
+
"registryDependencies": [
|
|
23
|
+
"lib/utils",
|
|
24
|
+
"ui/dialog",
|
|
25
|
+
"ui/button"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "office-locations",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "3-column grid of location cards, each showing city name and multi-line address in a bordered card. Header with subtitle, title, and description. Use for office locations, store directories, branch listings, or any multi-location display.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"offices",
|
|
7
|
+
"locations",
|
|
8
|
+
"addresses",
|
|
9
|
+
"branches",
|
|
10
|
+
"stores"
|
|
11
|
+
],
|
|
12
|
+
"visualWeight": "medium",
|
|
5
13
|
"files": [
|
|
6
14
|
{
|
|
7
15
|
"path": "components/blocks/marketing/office-locations.tsx",
|
|
8
16
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { Typography } from \"../../ui/typography\";\n\ninterface Office {\n city: string;\n address: string[];\n}\n\ninterface OfficeLocationsProps {\n subtitle?: string;\n title?: string;\n description?: string;\n offices?: Office[];\n}\n\nconst defaultOffices: Office[] = [\n {\n city: \"San Francisco\",\n address: [\n \"972 Mission Street Knotel Fl 3\",\n \"San Francisco, CA, 94103\",\n \"United States\",\n ],\n },\n {\n city: \"Vancouver\",\n address: [\"1007 Hampton Orchard Rd 23\", \"Vancouver, 1607\", \"Canada\"],\n },\n {\n city: \"Sydney\",\n address: [\"304 Hampton Orchard Rd 23\", \"Sydney, NSW, 1607\", \"Australia\"],\n },\n];\n\nexport function OfficeLocations({\n subtitle = \"LOCATIONS\",\n title = \"Our offices\",\n description = \"We have global offices—reach out to us today!\",\n offices = defaultOffices,\n}: OfficeLocationsProps) {\n return (\n <section\n className=\"w-full px-6 md:px-20 py-16 md:py-24\"\n style={{ backgroundColor: \"var(--canvas-background)\" }}\n >\n <div className=\"max-w-[1240px] mx-auto flex flex-col gap-10 md:gap-12\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 md:gap-6\">\n <div className=\"flex flex-col gap-3\">\n <Typography variant=\"body-xs\" as=\"span\" color=\"muted\" className=\"uppercase tracking-wide\">\n {subtitle}\n </Typography>\n <Typography variant=\"h3\" as=\"h2\">\n {title}\n </Typography>\n </div>\n <Typography variant=\"body-l\" color=\"muted\">\n {description}\n </Typography>\n </div>\n\n {/* Office Cards */}\n <div className=\"grid grid-cols-1
|
|
17
|
+
"content": "\"use client\";\n\nimport { Typography } from \"../../ui/typography\";\n\ninterface Office {\n city: string;\n address: string[];\n}\n\ninterface OfficeLocationsProps {\n subtitle?: string;\n title?: string;\n description?: string;\n offices?: Office[];\n}\n\nconst defaultOffices: Office[] = [\n {\n city: \"San Francisco\",\n address: [\n \"972 Mission Street Knotel Fl 3\",\n \"San Francisco, CA, 94103\",\n \"United States\",\n ],\n },\n {\n city: \"Vancouver\",\n address: [\"1007 Hampton Orchard Rd 23\", \"Vancouver, 1607\", \"Canada\"],\n },\n {\n city: \"Sydney\",\n address: [\"304 Hampton Orchard Rd 23\", \"Sydney, NSW, 1607\", \"Australia\"],\n },\n];\n\nexport function OfficeLocations({\n subtitle = \"LOCATIONS\",\n title = \"Our offices\",\n description = \"We have global offices—reach out to us today!\",\n offices = defaultOffices,\n}: OfficeLocationsProps) {\n return (\n <section\n className=\"w-full px-6 md:px-20 py-16 md:py-24\"\n style={{ backgroundColor: \"var(--canvas-background)\" }}\n >\n <div className=\"max-w-[1240px] mx-auto flex flex-col gap-10 md:gap-12\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 md:gap-6\">\n <div className=\"flex flex-col gap-3\">\n <Typography variant=\"body-xs\" as=\"span\" color=\"muted\" className=\"uppercase tracking-wide\">\n {subtitle}\n </Typography>\n <Typography variant=\"h3\" as=\"h2\">\n {title}\n </Typography>\n </div>\n <Typography variant=\"body-l\" color=\"muted\">\n {description}\n </Typography>\n </div>\n\n {/* Office Cards */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8\">\n {offices.map((office, index) => (\n <div\n key={index}\n className=\"flex flex-col gap-4 p-6 md:p-8 rounded-lg\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n }}\n >\n <Typography variant=\"h6\" as=\"h3\">\n {office.city}\n </Typography>\n <div className=\"flex flex-col\">\n {office.address.map((line, lineIndex) => (\n <Typography key={lineIndex} variant=\"body-l\" as=\"span\" color=\"muted\">\n {line}\n </Typography>\n ))}\n </div>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n"
|
|
10
18
|
}
|
|
11
19
|
],
|
|
12
20
|
"dependencies": [],
|