canvas-ui-sdk 0.3.13 → 0.3.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvas-ui-sdk",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "type": "module",
5
5
  "description": "A comprehensive UI component library with design tokens for building beautiful interfaces",
6
6
  "bin": {
@@ -30,7 +30,8 @@
30
30
  "dist",
31
31
  "styles",
32
32
  "registry",
33
- "mcp/dist"
33
+ "mcp/dist",
34
+ "prompts"
34
35
  ],
35
36
  "scripts": {
36
37
  "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",
@@ -0,0 +1,85 @@
1
+ # Canvas Consumer App
2
+
3
+ This is a Next.js app that uses the Canvas UI SDK component library.
4
+
5
+ ## Adding Components
6
+
7
+ This project uses a **shadcn-style CLI** to copy component source files from `canvas-ui-sdk` into the project for full customization.
8
+
9
+ ### Available commands
10
+
11
+ ```bash
12
+ # See all available components (145 total: layouts, blocks, UI primitives, hooks)
13
+ npx canvas-ui list
14
+
15
+ # Copy components into the project (auto-resolves dependencies)
16
+ npx canvas-ui add <component-name> [component-name...]
17
+
18
+ # Examples:
19
+ npx canvas-ui add button # Copies button + utils
20
+ npx canvas-ui add dashboard-shell # Copies shell + sidebar + header + 15 deps
21
+ npx canvas-ui add standard-data-table # Copies table + pagination + deps
22
+ ```
23
+
24
+ ### How it works
25
+
26
+ - Components are copied as **editable source files** into `src/components/`
27
+ - UI primitives go to `src/components/ui/`
28
+ - Layout shells go to `src/components/layout/`
29
+ - Block components go to `src/components/blocks/<category>/`
30
+ - Hooks go to `src/hooks/`
31
+ - Utilities go to `src/lib/`
32
+ - Import paths use `@/` aliases (e.g. `@/components/ui/button`, `@/lib/utils`)
33
+ - npm dependencies (like `@radix-ui/*`, `class-variance-authority`) are auto-installed
34
+
35
+ ### When building a page
36
+
37
+ 1. Decide which layout shell fits (run `npx canvas-ui list` to see options)
38
+ 2. Add the layout: `npx canvas-ui add <shell-name>`
39
+ 3. Add any blocks needed: `npx canvas-ui add <block-name> [block-name...]`
40
+ 4. Import components from their local paths (e.g. `import { Button } from "@/components/ui/button"`)
41
+ 5. Customize the copied source files as needed
42
+
43
+ ### What stays in the npm package (do NOT copy these)
44
+
45
+ - Design tokens / CSS variables (`canvas-ui-sdk/styles`)
46
+ - Tailwind theme mappings (`canvas-ui-sdk/tailwind`)
47
+ - Theme context and providers (`canvas-ui-sdk`)
48
+
49
+ These are imported directly from the npm package, not copied.
50
+
51
+ ## Design Tokens
52
+
53
+ When creating or modifying components, always use the canvas design token CSS variables. Never use raw Tailwind color classes (`bg-white`, `text-gray-500`, `border-gray-200`) or hardcoded pixel values for font sizes. Use the token system instead:
54
+
55
+ - **Colors**: `var(--canvas-background)`, `var(--canvas-text)`, `var(--canvas-primary)`, `var(--canvas-border)`, etc.
56
+ - **Typography**: `var(--typo-body-s-size)`, `var(--typo-body-m-size)`, `var(--typo-h4-size)`, `var(--typo-global-font)`, etc.
57
+ - **Spacing**: `var(--spacing-sm)`, `var(--spacing-md)`, `var(--spacing-lg)`, etc.
58
+ - **Radii**: `var(--radius-xs)`, `var(--radius-sm)`, `var(--radius-md)`, etc.
59
+ - **Inputs**: `var(--canvas-border-input)`, `var(--input-standard-height)`, `var(--input-standard-radius)`, etc.
60
+ - **Buttons**: `var(--btn-primary-bg)`, `var(--btn-standard-height)`, etc.
61
+
62
+ Reference the SDK's `styles/tokens.reference.css` for the full list of available tokens.
63
+
64
+ ## CSS Setup
65
+
66
+ The app's `src/app/globals.css` imports:
67
+ ```css
68
+ @import "tailwindcss";
69
+ @import "canvas-ui-sdk/tailwind";
70
+ @import "canvas-ui-sdk/styles";
71
+ @source "../../node_modules/canvas-ui-sdk/dist";
72
+ ```
73
+
74
+ ## Project Structure
75
+
76
+ ```
77
+ src/
78
+ app/ # Next.js app router pages
79
+ components/ # Copied Canvas UI components (editable)
80
+ ui/ # Primitives (button, input, dialog, etc.)
81
+ layout/ # Layout shells (dashboard-shell, sidebar, etc.)
82
+ blocks/ # Feature blocks (data-tables, forms, cards, etc.)
83
+ hooks/ # Copied hooks
84
+ lib/ # Utilities (utils.ts with cn() helper)
85
+ ```
@@ -0,0 +1,194 @@
1
+ # Bake Theme Into Source Code
2
+
3
+ ## What this does
4
+
5
+ Reads the saved theme from `.data/theme.json` and applies it as source-code defaults
6
+ so the app loads with the correct styling on first paint — no flash of unstyled content.
7
+
8
+ After baking, `.data/theme.json` is deleted and the ThemeDrawer starts fresh.
9
+
10
+ ---
11
+
12
+ ## Steps
13
+
14
+ ### 1. Read the saved theme
15
+
16
+ Read `.data/theme.json`. It has this shape:
17
+
18
+ ```json
19
+ {
20
+ "overrides": { "--canvas-primary": "#ee4c11", "--typo-global-font": "Lato", ... },
21
+ "branding": { "iconShape": "rounded", "iconName": "WifiHigh", ... },
22
+ "images": { "logoLight": "", "logoDark": "", ... },
23
+ "customButtonStyles": []
24
+ }
25
+ ```
26
+
27
+ If the file doesn't exist or is empty, stop — there's nothing to bake.
28
+
29
+ ### 2. Update `src/app/globals.css`
30
+
31
+ For each entry in `overrides`, add or update the CSS variable in the correct block:
32
+
33
+ | Variable pattern | Block | Notes |
34
+ |-----------------|-------|-------|
35
+ | `--canvas-*` (colors) | `:root { }` | e.g. `--canvas-primary: #ee4c11;` |
36
+ | `--typo-*-size`, `--typo-*-weight`, `--typo-*-spacing`, `--typo-*-height` | `body { }` | e.g. `--typo-menu-label-size: 40;` |
37
+ | `--typo-global-font` | `body { }` | Set as: `--typo-global-font: var(--font-FONTNAME), "FontName", sans-serif;` where FONTNAME is the lowercased, hyphenated font name. Also update the existing `--typo-global-font` line if present. |
38
+ | `--typo-*-font` (per-component) | `body { }` | Only if the value is a real font name (not empty). Set as: `--typo-COMPONENT-font: "FontName", sans-serif;`. Remove the corresponding `--typo-*-font: initial;` line from `:root` if present. |
39
+ | `--btn-*` (button styles) | `body { }` | e.g. `--btn-border-radius: 8;` |
40
+
41
+ **Important:** Keep any existing variables that aren't in the overrides. Only add/update the ones from the saved theme.
42
+
43
+ ### 3. Add Google Font imports in `src/app/layout.tsx`
44
+
45
+ For each unique font name found in the overrides (values of `--typo-*-font` variables):
46
+
47
+ 1. Add an import from `next/font/google` at the top of the file:
48
+ ```typescript
49
+ import { ExistingFont, NewFont } from "next/font/google";
50
+ ```
51
+
52
+ 2. Create the font instance:
53
+ ```typescript
54
+ const newFont = NewFont({
55
+ variable: "--font-new-font",
56
+ subsets: ["latin"],
57
+ weight: ["400", "500", "600", "700"],
58
+ });
59
+ ```
60
+
61
+ 3. Add the font's variable class to the `<body>` className:
62
+ ```typescript
63
+ <body className={`${existingFont.variable} ${newFont.variable} antialiased`}>
64
+ ```
65
+
66
+ **Variable naming convention:** The CSS variable name is `--font-` followed by the font name in lowercase with spaces replaced by hyphens. For example, "Open Sans" becomes `--font-open-sans`.
67
+
68
+ **Keep existing fonts.** Don't remove Inter, Geist, or Geist_Mono — they're still needed.
69
+
70
+ ### 4. Update `src/lib/theme-config.ts`
71
+
72
+ Write the `branding` and `images` objects from the saved theme into this file:
73
+
74
+ ```typescript
75
+ import type { BrandingState, ImageKey } from "canvas-ui-sdk";
76
+
77
+ export const savedBranding: BrandingState = {
78
+ // paste values from theme.json branding
79
+ };
80
+
81
+ export const savedImages: Record<ImageKey, string> = {
82
+ // paste values from theme.json images
83
+ };
84
+ ```
85
+
86
+ ### 5. Delete the saved theme file
87
+
88
+ Remove `.data/theme.json` — the overrides are now baked into the source code.
89
+
90
+ ```bash
91
+ rm .data/theme.json
92
+ ```
93
+
94
+ ### 6. Verify
95
+
96
+ Run `npm run dev` and confirm:
97
+ - The page loads with the correct colors, fonts, and branding on first paint
98
+ - No flash of default styles
99
+ - The theme drawer opens with no unsaved changes
100
+
101
+ ---
102
+
103
+ ## Example
104
+
105
+ Given this `.data/theme.json`:
106
+
107
+ ```json
108
+ {
109
+ "overrides": {
110
+ "--canvas-primary": "#ee4c11",
111
+ "--canvas-primary-dark": "#772609",
112
+ "--typo-global-font": "Lato",
113
+ "--typo-menu-label-font": "Open Sans",
114
+ "--typo-menu-label-size": "40",
115
+ "--typo-menu-label-weight": "700"
116
+ },
117
+ "branding": {
118
+ "iconShape": "rounded",
119
+ "iconName": "WifiHigh",
120
+ "bgColor": "var(--canvas-primary)",
121
+ "iconColor": "var(--canvas-primary-foreground)",
122
+ "wordmark": "ohhh"
123
+ },
124
+ "images": {
125
+ "logoLight": "",
126
+ "logoDark": "",
127
+ "faviconLight": "",
128
+ "faviconDark": ""
129
+ },
130
+ "customButtonStyles": []
131
+ }
132
+ ```
133
+
134
+ **globals.css** `:root` block gets:
135
+ ```css
136
+ :root {
137
+ --canvas-primary: #ee4c11;
138
+ --canvas-primary-dark: #772609;
139
+ /* ... keep other existing variables ... */
140
+
141
+ /* Remove --typo-menu-label-font: initial; since it now has a real value */
142
+ }
143
+ ```
144
+
145
+ **globals.css** `body` block gets:
146
+ ```css
147
+ body {
148
+ --typo-global-font: var(--font-lato), "Lato", sans-serif;
149
+ --typo-menu-label-font: "Open Sans", sans-serif;
150
+ --typo-menu-label-size: 40;
151
+ --typo-menu-label-weight: 700;
152
+ /* ... keep existing body styles ... */
153
+ }
154
+ ```
155
+
156
+ **layout.tsx** gets new font imports:
157
+ ```typescript
158
+ import { Inter, Geist, Geist_Mono, Lato, Open_Sans } from "next/font/google";
159
+
160
+ const lato = Lato({
161
+ variable: "--font-lato",
162
+ subsets: ["latin"],
163
+ weight: ["400", "700"],
164
+ });
165
+
166
+ const openSans = Open_Sans({
167
+ variable: "--font-open-sans",
168
+ subsets: ["latin"],
169
+ weight: ["400", "500", "600", "700"],
170
+ });
171
+
172
+ // In the body className:
173
+ <body className={`${inter.variable} ${geistSans.variable} ${geistMono.variable} ${lato.variable} ${openSans.variable} antialiased`}>
174
+ ```
175
+
176
+ **theme-config.ts** gets:
177
+ ```typescript
178
+ import type { BrandingState, ImageKey } from "canvas-ui-sdk";
179
+
180
+ export const savedBranding: BrandingState = {
181
+ iconShape: "rounded",
182
+ iconName: "WifiHigh",
183
+ bgColor: "var(--canvas-primary)",
184
+ iconColor: "var(--canvas-primary-foreground)",
185
+ wordmark: "ohhh",
186
+ };
187
+
188
+ export const savedImages: Record<ImageKey, string> = {
189
+ logoLight: "",
190
+ logoDark: "",
191
+ faviconLight: "",
192
+ faviconDark: "",
193
+ };
194
+ ```
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/activity-feed.tsx",
8
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"
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-xl)\" }}\n >\n {/* Header Section */}\n <div \n className=\"flex flex-wrap items-end 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
10
  }
11
11
  ],
12
12
  "dependencies": [
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/circular-progress-bar-list.tsx",
8
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"
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-end 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-end justify-end shrink-0 max-sm:w-full max-sm:flex-wrap\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Sort Dropdown */}\n <div className=\"w-[120px] max-sm:flex-1 max-sm:min-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] max-sm:flex-1 max-sm:min-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
10
  }
11
11
  ],
12
12
  "dependencies": [],
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/faqs-table.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Plus, Minus } from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface FAQItem {\n id: string;\n question: string;\n answer: string;\n}\n\nexport interface FaqsTableProps {\n /** Table title */\n title?: string;\n /** FAQ items to display */\n items?: FAQItem[];\n /** ID of the item that should be expanded by default */\n defaultExpandedId?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultItems: FAQItem[] = [\n {\n id: \"1\",\n question: \"What is Canvas?\",\n answer:\n \"Canvas uses a modular approach that allows you to easily plug in standardized components for most of your application's UX. That allows you to not reinvent the wheel and focus most of your efforts on your product's unique parts.\",\n },\n {\n id: \"2\",\n question: \"How can I purchase a license?\",\n answer:\n \"You can purchase a license directly from our website. We offer individual, team, and enterprise plans to suit your needs. All plans include access to our full component library and design system.\",\n },\n {\n id: \"3\",\n question: \"Do you offer refunds?\",\n answer:\n \"Yes, we offer a 30-day money-back guarantee. If you're not satisfied with your purchase, contact our support team for a full refund.\",\n },\n {\n id: \"4\",\n question: \"Can I use Canvas for commercial projects?\",\n answer:\n \"Absolutely! All our licenses allow for unlimited commercial projects. You can use Canvas components in client work, SaaS products, and internal tools.\",\n },\n];\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - FAQs Table Block\n *\n * An expandable FAQ list with plus/minus toggle icons.\n * Displays one item expanded at a time (accordion behavior).\n *\n * @example\n * ```tsx\n * <FaqsTable\n * title=\"Common questions\"\n * items={[\n * { id: \"1\", question: \"What is Canvas?\", answer: \"...\" }\n * ]}\n * />\n * ```\n */\nexport function FaqsTable({\n title = \"Common questions\",\n items = defaultItems,\n defaultExpandedId,\n className,\n}: FaqsTableProps) {\n const [expandedId, setExpandedId] = useState<string | null>(\n defaultExpandedId ?? items[0]?.id ?? null\n );\n\n const toggleItem = (id: string) => {\n setExpandedId((current) => (current === id ? null : id));\n };\n\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title Section */}\n <div\n className=\"flex flex-wrap items-start w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <div\n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n </div>\n </div>\n\n {/* FAQ List */}\n <div className=\"flex flex-col w-full\">\n {items.map((item, index) => {\n const isExpanded = expandedId === item.id;\n const isFirst = index === 0;\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 borderTop: isFirst ? \"1px solid var(--canvas-border)\" : \"none\",\n borderBottom: \"1px solid var(--canvas-border)\",\n paddingTop: \"var(--spacing-xl)\",\n paddingBottom: \"var(--spacing-xl)\",\n }}\n >\n {/* Question Row */}\n <button\n onClick={() => toggleItem(item.id)}\n className=\"cursor-pointer flex items-center justify-between w-full text-left\"\n style={{ gap: \"var(--spacing-xl)\" }}\n aria-expanded={isExpanded}\n aria-controls={`faq-answer-${item.id}`}\n >\n <span\n className=\"flex-1\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.question}\n </span>\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: \"32px\",\n height: \"32px\",\n padding: \"var(--spacing-md)\",\n borderRadius: \"var(--radius-full)\",\n }}\n >\n {isExpanded ? (\n <Minus\n size={20}\n style={{ color: \"var(--canvas-text)\" }}\n strokeWidth={1.5}\n />\n ) : (\n <Plus\n size={20}\n style={{ color: \"var(--canvas-text)\" }}\n strokeWidth={1.5}\n />\n )}\n </div>\n </button>\n\n {/* Answer (collapsible) */}\n {isExpanded && (\n <div\n id={`faq-answer-${item.id}`}\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 paddingTop: \"var(--spacing-md)\",\n }}\n >\n {item.answer}\n </div>\n )}\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Plus, Minus } from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface FAQItem {\n id: string;\n question: string;\n answer: string;\n}\n\nexport interface FaqsTableProps {\n /** Table title */\n title?: string;\n /** FAQ items to display */\n items?: FAQItem[];\n /** ID of the item that should be expanded by default */\n defaultExpandedId?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultItems: FAQItem[] = [\n {\n id: \"1\",\n question: \"What is Canvas?\",\n answer:\n \"Canvas uses a modular approach that allows you to easily plug in standardized components for most of your application's UX. That allows you to not reinvent the wheel and focus most of your efforts on your product's unique parts.\",\n },\n {\n id: \"2\",\n question: \"How can I purchase a license?\",\n answer:\n \"You can purchase a license directly from our website. We offer individual, team, and enterprise plans to suit your needs. All plans include access to our full component library and design system.\",\n },\n {\n id: \"3\",\n question: \"Do you offer refunds?\",\n answer:\n \"Yes, we offer a 30-day money-back guarantee. If you're not satisfied with your purchase, contact our support team for a full refund.\",\n },\n {\n id: \"4\",\n question: \"Can I use Canvas for commercial projects?\",\n answer:\n \"Absolutely! All our licenses allow for unlimited commercial projects. You can use Canvas components in client work, SaaS products, and internal tools.\",\n },\n];\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - FAQs Table Block\n *\n * An expandable FAQ list with plus/minus toggle icons.\n * Displays one item expanded at a time (accordion behavior).\n *\n * @example\n * ```tsx\n * <FaqsTable\n * title=\"Common questions\"\n * items={[\n * { id: \"1\", question: \"What is Canvas?\", answer: \"...\" }\n * ]}\n * />\n * ```\n */\nexport function FaqsTable({\n title = \"Common questions\",\n items = defaultItems,\n defaultExpandedId,\n className,\n}: FaqsTableProps) {\n const [expandedId, setExpandedId] = useState<string | null>(\n defaultExpandedId ?? items[0]?.id ?? null\n );\n\n const toggleItem = (id: string) => {\n setExpandedId((current) => (current === id ? null : id));\n };\n\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title Section */}\n <div\n className=\"flex flex-wrap items-end w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <div\n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n </div>\n </div>\n\n {/* FAQ List */}\n <div className=\"flex flex-col w-full\">\n {items.map((item, index) => {\n const isExpanded = expandedId === item.id;\n const isFirst = index === 0;\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 borderTop: isFirst ? \"1px solid var(--canvas-border)\" : \"none\",\n borderBottom: \"1px solid var(--canvas-border)\",\n paddingTop: \"var(--spacing-xl)\",\n paddingBottom: \"var(--spacing-xl)\",\n }}\n >\n {/* Question Row */}\n <button\n onClick={() => toggleItem(item.id)}\n className=\"cursor-pointer flex items-center justify-between w-full text-left\"\n style={{ gap: \"var(--spacing-xl)\" }}\n aria-expanded={isExpanded}\n aria-controls={`faq-answer-${item.id}`}\n >\n <span\n className=\"flex-1\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.question}\n </span>\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: \"32px\",\n height: \"32px\",\n padding: \"var(--spacing-md)\",\n borderRadius: \"var(--radius-full)\",\n }}\n >\n {isExpanded ? (\n <Minus\n size={20}\n style={{ color: \"var(--canvas-text)\" }}\n strokeWidth={1.5}\n />\n ) : (\n <Plus\n size={20}\n style={{ color: \"var(--canvas-text)\" }}\n strokeWidth={1.5}\n />\n )}\n </div>\n </button>\n\n {/* Answer (collapsible) */}\n {isExpanded && (\n <div\n id={`faq-answer-${item.id}`}\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 paddingTop: \"var(--spacing-md)\",\n }}\n >\n {item.answer}\n </div>\n )}\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/fixed-column-data-table.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { MenufocusTemplate } from \"./menufocus-template\";\n\n// ============================================\n// Types\n// ============================================\n\nexport type FixedColumnTableStatus = \"pending\" | \"paid\" | \"overdue\";\n\nexport interface FixedColumnTableRow {\n id: string;\n name: string;\n avatarUrl?: string;\n amount: string;\n status: FixedColumnTableStatus;\n logoUrl?: string;\n company: string;\n dateSent: string;\n}\n\nexport interface FixedColumnTableColumn {\n id: string;\n label: string;\n width?: string;\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface FilterOption {\n id: string;\n label: string;\n}\n\nexport interface FixedColumnDataTableProps {\n /** Table title */\n title?: string;\n /** Number of results to display */\n resultCount?: number;\n /** Custom result count text (overrides default \"{count} results\") */\n resultCountText?: string;\n /** Table data rows */\n data?: FixedColumnTableRow[];\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: FilterOption[];\n /** Primary action button text */\n actionButtonText?: string;\n /** Callback when add/action button is clicked */\n onAddNew?: () => void;\n /** Callback when sort value changes */\n onSort?: (value: string) => void;\n /** Callback when filter value changes */\n onFilter?: (value: string) => void;\n /** Callback when row action is clicked */\n onRowAction?: (action: string, row: FixedColumnTableRow) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultData: FixedColumnTableRow[] = [\n {\n id: \"1\",\n name: \"Jeff Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n amount: \"$3,200\",\n status: \"pending\",\n logoUrl: \"\",\n company: \"Airdev\",\n dateSent: \"5/23/2024\",\n },\n {\n id: \"2\",\n name: \"Aya Williams\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n amount: \"$2,400\",\n status: \"paid\",\n logoUrl: \"\",\n company: \"Airdev\",\n dateSent: \"2/19/2024\",\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"name-asc\", label: \"Name (A-Z)\" },\n { id: \"name-desc\", label: \"Name (Z-A)\" },\n { id: \"date-newest\", label: \"Newest first\" },\n { id: \"date-oldest\", label: \"Oldest first\" },\n { id: \"amount-high\", label: \"Amount (High)\" },\n { id: \"amount-low\", label: \"Amount (Low)\" },\n];\n\nconst defaultFilterOptions: FilterOption[] = [\n { id: \"all\", label: \"All statuses\" },\n { id: \"pending\", label: \"Pending\" },\n { id: \"paid\", label: \"Paid\" },\n { id: \"overdue\", label: \"Overdue\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface TableHeaderCellProps {\n children: React.ReactNode;\n className?: string;\n isFixed?: boolean;\n}\n\nfunction TableHeaderCell({ children, className, isFixed }: TableHeaderCellProps) {\n return (\n <th\n className={cn(\n \"text-left h-8\",\n isFixed && \"sticky left-0 z-10\",\n className\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n backgroundColor: isFixed ? \"var(--canvas-background)\" : undefined,\n }}\n >\n {children}\n </th>\n );\n}\n\ninterface TableCellProps {\n children: React.ReactNode;\n className?: string;\n isFixed?: boolean;\n}\n\nfunction TableCell({ children, className, isFixed }: TableCellProps) {\n return (\n <td\n className={cn(\n \"h-12\",\n isFixed && \"sticky left-0 z-10\",\n className\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n backgroundColor: isFixed ? \"var(--canvas-background)\" : undefined,\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n {children}\n </td>\n );\n}\n\ninterface StatusBadgeProps {\n status: FixedColumnTableStatus;\n}\n\nfunction StatusBadge({ status }: StatusBadgeProps) {\n const statusConfig: Record<FixedColumnTableStatus, { label: string; bgColor: string; textColor: string }> = {\n pending: {\n label: \"Pending\",\n bgColor: \"var(--canvas-surface-brand)\",\n textColor: \"var(--canvas-primary)\",\n },\n paid: {\n label: \"Paid\",\n bgColor: \"var(--canvas-success-surface)\",\n textColor: \"var(--canvas-success)\",\n },\n overdue: {\n label: \"Overdue\",\n bgColor: \"var(--canvas-destructive-surface)\",\n textColor: \"var(--canvas-destructive)\",\n },\n };\n\n const config = statusConfig[status];\n\n return (\n <span\n className=\"inline-flex items-center justify-center whitespace-nowrap\"\n style={{\n backgroundColor: config.bgColor,\n color: config.textColor,\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n padding: \"var(--spacing-md) var(--spacing-xl)\",\n borderRadius: \"var(--spacing-3xl)\",\n height: \"32px\",\n }}\n >\n {config.label}\n </span>\n );\n}\n\ninterface CompanyLogoProps {\n logoUrl?: string;\n company: string;\n}\n\nfunction CompanyLogo({ logoUrl, company }: CompanyLogoProps) {\n // Default favicon-style logo if no URL provided\n if (!logoUrl) {\n return (\n <div\n className=\"flex items-center justify-center\"\n style={{\n width: \"32px\",\n height: \"32px\",\n backgroundColor: \"var(--canvas-primary)\",\n borderRadius: \"var(--radius-xs)\",\n }}\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"var(--canvas-primary-foreground)\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\" />\n <polyline points=\"9,22 9,12 15,12 15,22\" />\n </svg>\n </div>\n );\n }\n\n return (\n <img\n src={logoUrl}\n alt={`${company} logo`}\n className=\"object-contain\"\n style={{\n width: \"32px\",\n height: \"32px\",\n borderRadius: \"var(--radius-xs)\",\n }}\n />\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Fixed Column Data Table Block\n * \n * A data table with a fixed first column (Name with avatar) that stays\n * visible during horizontal scrolling. Ideal for invoice-style tables\n * with many columns that need horizontal scrolling on smaller screens.\n * \n * @example\n * ```tsx\n * <FixedColumnDataTable\n * title=\"Invoices\"\n * data={[\n * { id: \"1\", name: \"John\", amount: \"$1,000\", status: \"paid\", company: \"Acme\", dateSent: \"1/1/2024\" }\n * ]}\n * onAddNew={() => console.log(\"Add new\")}\n * />\n * ```\n */\nexport function FixedColumnDataTable({\n title = \"Invoices\",\n resultCount,\n resultCountText,\n data = defaultData,\n sortOptions = defaultSortOptions,\n filterOptions = defaultFilterOptions,\n actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onFilter,\n onRowAction,\n className,\n}: FixedColumnDataTableProps) {\n const [sortValue, setSortValue] = useState<string>(\"\");\n const [filterValue, setFilterValue] = useState<string>(\"\");\n\n const displayResultCount = resultCount ?? data.length;\n const displayResultText = resultCountText ?? `${displayResultCount} results`;\n\n const handleSortChange = (value: string) => {\n setSortValue(value);\n onSort?.(value);\n };\n\n const handleFilterChange = (value: string) => {\n setFilterValue(value);\n onFilter?.(value);\n };\n\n return (\n <div \n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Header Section */}\n <div \n className=\"flex flex-wrap items-start w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title and Count */}\n <div \n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {displayResultText}\n </p>\n </div>\n\n {/* Controls */}\n <div \n className=\"flex items-start justify-end shrink-0 gap-3\"\n >\n {/* Sort Dropdown */}\n <div className=\"w-[120px]\">\n <Select value={sortValue || undefined} onValueChange={handleSortChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Sort\" />\n </SelectTrigger>\n <SelectContent>\n {sortOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Filter Dropdown */}\n <div className=\"w-[120px]\">\n <Select value={filterValue || undefined} onValueChange={handleFilterChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Filter\" />\n </SelectTrigger>\n <SelectContent>\n {filterOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Action Button */}\n <Button\n variant=\"primary\"\n size=\"sm\"\n onClick={onAddNew}\n style={{\n height: \"var(--btn-small-height)\",\n paddingLeft: \"var(--btn-small-px)\",\n paddingRight: \"var(--btn-small-px)\",\n fontSize: \"var(--btn-small-font-size)\",\n borderRadius: \"var(--btn-small-radius)\",\n backgroundColor: \"var(--btn-primary-bg)\",\n color: \"var(--btn-primary-text)\",\n borderColor: \"var(--btn-primary-border)\",\n }}\n >\n {actionButtonText}\n </Button>\n </div>\n </div>\n\n {/* Table Section with horizontal scroll */}\n <div className=\"w-full overflow-x-auto\">\n <table className=\"w-full min-w-max border-collapse\">\n <thead>\n <tr style={{ borderBottom: \"1px solid var(--canvas-border)\" }}>\n {/* Fixed Name Column Header */}\n <TableHeaderCell isFixed className=\"pr-8 min-w-[200px]\">\n Name\n </TableHeaderCell>\n {/* Scrollable Column Headers */}\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Amount\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Status\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[120px]\">\n Logo\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Company\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Date Sent\n </TableHeaderCell>\n <TableHeaderCell className=\"w-12 px-4\">\n &nbsp;\n </TableHeaderCell>\n </tr>\n </thead>\n <tbody>\n {data.map((row) => (\n <tr\n key={row.id}\n style={{ borderBottom: \"1px solid var(--canvas-border)\" }}\n >\n {/* Fixed Name Column */}\n <TableCell isFixed className=\"pr-8\">\n <div className=\"flex items-center gap-2\">\n <Avatar className=\"size-8 border border-[var(--canvas-border)]\">\n <AvatarImage src={row.avatarUrl} alt={row.name} />\n <AvatarFallback>\n {row.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <span className=\"whitespace-nowrap\">{row.name}</span>\n </div>\n </TableCell>\n {/* Scrollable Columns */}\n <TableCell className=\"px-6\">\n <span className=\"whitespace-nowrap\">{row.amount}</span>\n </TableCell>\n <TableCell className=\"px-6\">\n <StatusBadge status={row.status} />\n </TableCell>\n <TableCell className=\"px-6\">\n <CompanyLogo logoUrl={row.logoUrl} company={row.company} />\n </TableCell>\n <TableCell className=\"px-6\">\n <span className=\"whitespace-nowrap\">{row.company}</span>\n </TableCell>\n <TableCell className=\"px-6\">\n <span className=\"whitespace-nowrap\">{row.dateSent}</span>\n </TableCell>\n <TableCell className=\"px-4\">\n <MenufocusTemplate\n ariaLabel=\"Row actions\"\n items={[\n { id: \"edit\", label: \"Edit\", onClick: () => onRowAction?.(\"edit\", row) },\n { id: \"view\", label: \"View details\", onClick: () => onRowAction?.(\"view\", row) },\n { id: \"download\", label: \"Download\", onClick: () => onRowAction?.(\"download\", row) },\n { id: \"delete\", label: \"Delete\", variant: \"destructive\", onClick: () => onRowAction?.(\"delete\", row) },\n ]}\n />\n </TableCell>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </div>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { MenufocusTemplate } from \"./menufocus-template\";\n\n// ============================================\n// Types\n// ============================================\n\nexport type FixedColumnTableStatus = \"pending\" | \"paid\" | \"overdue\";\n\nexport interface FixedColumnTableRow {\n id: string;\n name: string;\n avatarUrl?: string;\n amount: string;\n status: FixedColumnTableStatus;\n logoUrl?: string;\n company: string;\n dateSent: string;\n}\n\nexport interface FixedColumnTableColumn {\n id: string;\n label: string;\n width?: string;\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface FilterOption {\n id: string;\n label: string;\n}\n\nexport interface FixedColumnDataTableProps {\n /** Table title */\n title?: string;\n /** Number of results to display */\n resultCount?: number;\n /** Custom result count text (overrides default \"{count} results\") */\n resultCountText?: string;\n /** Table data rows */\n data?: FixedColumnTableRow[];\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: FilterOption[];\n /** Primary action button text */\n actionButtonText?: string;\n /** Callback when add/action button is clicked */\n onAddNew?: () => void;\n /** Callback when sort value changes */\n onSort?: (value: string) => void;\n /** Callback when filter value changes */\n onFilter?: (value: string) => void;\n /** Callback when row action is clicked */\n onRowAction?: (action: string, row: FixedColumnTableRow) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultData: FixedColumnTableRow[] = [\n {\n id: \"1\",\n name: \"Jeff Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n amount: \"$3,200\",\n status: \"pending\",\n logoUrl: \"\",\n company: \"Airdev\",\n dateSent: \"5/23/2024\",\n },\n {\n id: \"2\",\n name: \"Aya Williams\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n amount: \"$2,400\",\n status: \"paid\",\n logoUrl: \"\",\n company: \"Airdev\",\n dateSent: \"2/19/2024\",\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"name-asc\", label: \"Name (A-Z)\" },\n { id: \"name-desc\", label: \"Name (Z-A)\" },\n { id: \"date-newest\", label: \"Newest first\" },\n { id: \"date-oldest\", label: \"Oldest first\" },\n { id: \"amount-high\", label: \"Amount (High)\" },\n { id: \"amount-low\", label: \"Amount (Low)\" },\n];\n\nconst defaultFilterOptions: FilterOption[] = [\n { id: \"all\", label: \"All statuses\" },\n { id: \"pending\", label: \"Pending\" },\n { id: \"paid\", label: \"Paid\" },\n { id: \"overdue\", label: \"Overdue\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface TableHeaderCellProps {\n children: React.ReactNode;\n className?: string;\n isFixed?: boolean;\n}\n\nfunction TableHeaderCell({ children, className, isFixed }: TableHeaderCellProps) {\n return (\n <th\n className={cn(\n \"text-left h-8\",\n isFixed && \"sticky left-0 z-10\",\n className\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n backgroundColor: isFixed ? \"var(--canvas-background)\" : undefined,\n }}\n >\n {children}\n </th>\n );\n}\n\ninterface TableCellProps {\n children: React.ReactNode;\n className?: string;\n isFixed?: boolean;\n}\n\nfunction TableCell({ children, className, isFixed }: TableCellProps) {\n return (\n <td\n className={cn(\n \"h-12\",\n isFixed && \"sticky left-0 z-10\",\n className\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n backgroundColor: isFixed ? \"var(--canvas-background)\" : undefined,\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n {children}\n </td>\n );\n}\n\ninterface StatusBadgeProps {\n status: FixedColumnTableStatus;\n}\n\nfunction StatusBadge({ status }: StatusBadgeProps) {\n const statusConfig: Record<FixedColumnTableStatus, { label: string; bgColor: string; textColor: string }> = {\n pending: {\n label: \"Pending\",\n bgColor: \"var(--canvas-surface-brand)\",\n textColor: \"var(--canvas-primary)\",\n },\n paid: {\n label: \"Paid\",\n bgColor: \"var(--canvas-success-surface)\",\n textColor: \"var(--canvas-success)\",\n },\n overdue: {\n label: \"Overdue\",\n bgColor: \"var(--canvas-destructive-surface)\",\n textColor: \"var(--canvas-destructive)\",\n },\n };\n\n const config = statusConfig[status];\n\n return (\n <span\n className=\"inline-flex items-center justify-center whitespace-nowrap\"\n style={{\n backgroundColor: config.bgColor,\n color: config.textColor,\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n padding: \"var(--spacing-md) var(--spacing-xl)\",\n borderRadius: \"var(--spacing-3xl)\",\n height: \"32px\",\n }}\n >\n {config.label}\n </span>\n );\n}\n\ninterface CompanyLogoProps {\n logoUrl?: string;\n company: string;\n}\n\nfunction CompanyLogo({ logoUrl, company }: CompanyLogoProps) {\n // Default favicon-style logo if no URL provided\n if (!logoUrl) {\n return (\n <div\n className=\"flex items-center justify-center\"\n style={{\n width: \"32px\",\n height: \"32px\",\n backgroundColor: \"var(--canvas-primary)\",\n borderRadius: \"var(--radius-xs)\",\n }}\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"var(--canvas-primary-foreground)\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\" />\n <polyline points=\"9,22 9,12 15,12 15,22\" />\n </svg>\n </div>\n );\n }\n\n return (\n <img\n src={logoUrl}\n alt={`${company} logo`}\n className=\"object-contain\"\n style={{\n width: \"32px\",\n height: \"32px\",\n borderRadius: \"var(--radius-xs)\",\n }}\n />\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Fixed Column Data Table Block\n * \n * A data table with a fixed first column (Name with avatar) that stays\n * visible during horizontal scrolling. Ideal for invoice-style tables\n * with many columns that need horizontal scrolling on smaller screens.\n * \n * @example\n * ```tsx\n * <FixedColumnDataTable\n * title=\"Invoices\"\n * data={[\n * { id: \"1\", name: \"John\", amount: \"$1,000\", status: \"paid\", company: \"Acme\", dateSent: \"1/1/2024\" }\n * ]}\n * onAddNew={() => console.log(\"Add new\")}\n * />\n * ```\n */\nexport function FixedColumnDataTable({\n title = \"Invoices\",\n resultCount,\n resultCountText,\n data = defaultData,\n sortOptions = defaultSortOptions,\n filterOptions = defaultFilterOptions,\n actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onFilter,\n onRowAction,\n className,\n}: FixedColumnDataTableProps) {\n const [sortValue, setSortValue] = useState<string>(\"\");\n const [filterValue, setFilterValue] = useState<string>(\"\");\n\n const displayResultCount = resultCount ?? data.length;\n const displayResultText = resultCountText ?? `${displayResultCount} results`;\n\n const handleSortChange = (value: string) => {\n setSortValue(value);\n onSort?.(value);\n };\n\n const handleFilterChange = (value: string) => {\n setFilterValue(value);\n onFilter?.(value);\n };\n\n return (\n <div \n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Header Section */}\n <div \n className=\"flex flex-wrap items-end 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-end justify-end shrink-0 gap-3 max-sm:w-full max-sm:flex-wrap\"\n >\n {/* Sort Dropdown */}\n <div className=\"w-[120px] max-sm:flex-1 max-sm:min-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] max-sm:flex-1 max-sm:min-w-[120px]\">\n <Select value={filterValue || undefined} onValueChange={handleFilterChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Filter\" />\n </SelectTrigger>\n <SelectContent>\n {filterOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Action Button */}\n <Button\n variant=\"primary\"\n size=\"sm\"\n onClick={onAddNew}\n style={{\n height: \"var(--btn-small-height)\",\n paddingLeft: \"var(--btn-small-px)\",\n paddingRight: \"var(--btn-small-px)\",\n fontSize: \"var(--btn-small-font-size)\",\n borderRadius: \"var(--btn-small-radius)\",\n backgroundColor: \"var(--btn-primary-bg)\",\n color: \"var(--btn-primary-text)\",\n borderColor: \"var(--btn-primary-border)\",\n }}\n >\n {actionButtonText}\n </Button>\n </div>\n </div>\n\n {/* Table Section with horizontal scroll */}\n <div className=\"w-full overflow-x-auto\">\n <table className=\"w-full min-w-max border-collapse\">\n <thead>\n <tr style={{ borderBottom: \"1px solid var(--canvas-border)\" }}>\n {/* Fixed Name Column Header */}\n <TableHeaderCell isFixed className=\"pr-8 min-w-[200px]\">\n Name\n </TableHeaderCell>\n {/* Scrollable Column Headers */}\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Amount\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Status\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[120px]\">\n Logo\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Company\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Date Sent\n </TableHeaderCell>\n <TableHeaderCell className=\"w-12 px-4\">\n &nbsp;\n </TableHeaderCell>\n </tr>\n </thead>\n <tbody>\n {data.map((row) => (\n <tr\n key={row.id}\n style={{ borderBottom: \"1px solid var(--canvas-border)\" }}\n >\n {/* Fixed Name Column */}\n <TableCell isFixed className=\"pr-8\">\n <div className=\"flex items-center gap-2\">\n <Avatar className=\"size-8 border border-[var(--canvas-border)]\">\n <AvatarImage src={row.avatarUrl} alt={row.name} />\n <AvatarFallback>\n {row.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <span className=\"whitespace-nowrap\">{row.name}</span>\n </div>\n </TableCell>\n {/* Scrollable Columns */}\n <TableCell className=\"px-6\">\n <span className=\"whitespace-nowrap\">{row.amount}</span>\n </TableCell>\n <TableCell className=\"px-6\">\n <StatusBadge status={row.status} />\n </TableCell>\n <TableCell className=\"px-6\">\n <CompanyLogo logoUrl={row.logoUrl} company={row.company} />\n </TableCell>\n <TableCell className=\"px-6\">\n <span className=\"whitespace-nowrap\">{row.company}</span>\n </TableCell>\n <TableCell className=\"px-6\">\n <span className=\"whitespace-nowrap\">{row.dateSent}</span>\n </TableCell>\n <TableCell className=\"px-4\">\n <MenufocusTemplate\n ariaLabel=\"Row actions\"\n items={[\n { id: \"edit\", label: \"Edit\", onClick: () => onRowAction?.(\"edit\", row) },\n { id: \"view\", label: \"View details\", onClick: () => onRowAction?.(\"view\", row) },\n { id: \"download\", label: \"Download\", onClick: () => onRowAction?.(\"download\", row) },\n { id: \"delete\", label: \"Delete\", variant: \"destructive\", onClick: () => onRowAction?.(\"delete\", row) },\n ]}\n />\n </TableCell>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </div>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [],