canvas-ui-sdk 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/dist/cli/index.js +516 -0
  2. package/package.json +18 -2
  3. package/registry/blocks/activity-feed.json +19 -0
  4. package/registry/blocks/blog-cards.json +16 -0
  5. package/registry/blocks/bottom-input-chat-widget.json +19 -0
  6. package/registry/blocks/canvas-item.json +18 -0
  7. package/registry/blocks/category-grid.json +16 -0
  8. package/registry/blocks/centered-hero.json +14 -0
  9. package/registry/blocks/chat-message.json +18 -0
  10. package/registry/blocks/circular-progress-bar-list.json +18 -0
  11. package/registry/blocks/component-palette.json +21 -0
  12. package/registry/blocks/component-search.json +19 -0
  13. package/registry/blocks/content-dropzone.json +16 -0
  14. package/registry/blocks/content-with-image.json +14 -0
  15. package/registry/blocks/core-values-grid.json +16 -0
  16. package/registry/blocks/credit-card-display.json +16 -0
  17. package/registry/blocks/cta-banner.json +14 -0
  18. package/registry/blocks/custom-component-helper.json +19 -0
  19. package/registry/blocks/destination-cards.json +16 -0
  20. package/registry/blocks/empty-state.json +16 -0
  21. package/registry/blocks/faq-accordion.json +16 -0
  22. package/registry/blocks/faqs-table.json +18 -0
  23. package/registry/blocks/feature-with-image.json +16 -0
  24. package/registry/blocks/featured-news-cards.json +16 -0
  25. package/registry/blocks/featured-places.json +16 -0
  26. package/registry/blocks/features-comparison.json +16 -0
  27. package/registry/blocks/filter-popover.json +28 -0
  28. package/registry/blocks/fixed-column-data-table.json +20 -0
  29. package/registry/blocks/flair-banner.json +16 -0
  30. package/registry/blocks/footer-navbar.json +17 -0
  31. package/registry/blocks/form-group.json +29 -0
  32. package/registry/blocks/gallery-section.json +14 -0
  33. package/registry/blocks/gradient-banner.json +16 -0
  34. package/registry/blocks/graph-metric-tiles.json +20 -0
  35. package/registry/blocks/grid-tiles-list.json +20 -0
  36. package/registry/blocks/hero-dark-centered.json +16 -0
  37. package/registry/blocks/hero-dark-with-image.json +16 -0
  38. package/registry/blocks/hero-fullwidth-image.json +16 -0
  39. package/registry/blocks/hero-section.json +16 -0
  40. package/registry/blocks/how-it-works.json +16 -0
  41. package/registry/blocks/image-feed-with-nested-comments.json +20 -0
  42. package/registry/blocks/infinity-canvas.json +58 -0
  43. package/registry/blocks/large-image-labels-list.json +19 -0
  44. package/registry/blocks/loader.json +19 -0
  45. package/registry/blocks/login-branding-panel.json +16 -0
  46. package/registry/blocks/menu-section.json +18 -0
  47. package/registry/blocks/menufocus-template.json +19 -0
  48. package/registry/blocks/messenger-sidebar.json +19 -0
  49. package/registry/blocks/metrics-section.json +14 -0
  50. package/registry/blocks/mobile-bottom-nav.json +18 -0
  51. package/registry/blocks/monthly-calendar-widget.json +20 -0
  52. package/registry/blocks/nested-comments-table.json +21 -0
  53. package/registry/blocks/nested-data-table.json +22 -0
  54. package/registry/blocks/office-locations.json +14 -0
  55. package/registry/blocks/page-header-section.json +17 -0
  56. package/registry/blocks/page-previews.json +29 -0
  57. package/registry/blocks/pagination.json +20 -0
  58. package/registry/blocks/participant-list.json +17 -0
  59. package/registry/blocks/persona-card.json +18 -0
  60. package/registry/blocks/pill-tabs.json +19 -0
  61. package/registry/blocks/pricing-cards.json +16 -0
  62. package/registry/blocks/pricing-cta.json +14 -0
  63. package/registry/blocks/profile-card.json +20 -0
  64. package/registry/blocks/profile-grid-tiles-list.json +21 -0
  65. package/registry/blocks/profile-image-uploader.json +19 -0
  66. package/registry/blocks/profile-info-cards.json +19 -0
  67. package/registry/blocks/progress-bar.json +16 -0
  68. package/registry/blocks/prompt-template.json +18 -0
  69. package/registry/blocks/reviews-grid.json +14 -0
  70. package/registry/blocks/reviews-table.json +19 -0
  71. package/registry/blocks/screen-flowchart.json +19 -0
  72. package/registry/blocks/screen-prompt-builder.json +19 -0
  73. package/registry/blocks/screen-prompt-template.json +18 -0
  74. package/registry/blocks/search-bar.json +19 -0
  75. package/registry/blocks/search-sidebar.json +25 -0
  76. package/registry/blocks/settings-list-row.json +20 -0
  77. package/registry/blocks/sidebar-cards.json +18 -0
  78. package/registry/blocks/sidebar-profile-card.json +21 -0
  79. package/registry/blocks/slideshow-grid-tiles.json +21 -0
  80. package/registry/blocks/social-feed.json +20 -0
  81. package/registry/blocks/social-proof.json +14 -0
  82. package/registry/blocks/standard-data-table.json +20 -0
  83. package/registry/blocks/standard-list-with-image.json +17 -0
  84. package/registry/blocks/step-tracker.json +16 -0
  85. package/registry/blocks/team-cards-grid.json +16 -0
  86. package/registry/blocks/team-circular-grid.json +16 -0
  87. package/registry/blocks/testimonial-carousel.json +16 -0
  88. package/registry/blocks/upvoting-posts-table.json +22 -0
  89. package/registry/blocks/vertical-how-it-works.json +16 -0
  90. package/registry/blocks/vertical-step-tracker.json +17 -0
  91. package/registry/blocks/video-chat-controls.json +18 -0
  92. package/registry/blocks/video-content-section.json +16 -0
  93. package/registry/blocks/video-playlist.json +18 -0
  94. package/registry/blocks/webcam-preview.json +18 -0
  95. package/registry/blocks/youtube-player.json +16 -0
  96. package/registry/hooks/use-css-variable-sync.json +14 -0
  97. package/registry/hooks/use-mobile.json +14 -0
  98. package/registry/index.json +730 -0
  99. package/registry/layout/account-settings-shell.json +20 -0
  100. package/registry/layout/dashboard-shell.json +23 -0
  101. package/registry/layout/double-sidebar-shell.json +23 -0
  102. package/registry/layout/double-sidebar.json +20 -0
  103. package/registry/layout/header.json +22 -0
  104. package/registry/layout/icon-sidebar-shell.json +23 -0
  105. package/registry/layout/icon-sidebar.json +19 -0
  106. package/registry/layout/mobile-menu-shell.json +19 -0
  107. package/registry/layout/multistep-progressbar-shell.json +23 -0
  108. package/registry/layout/multistep-shell.json +21 -0
  109. package/registry/layout/multistep-sidebar-shell.json +22 -0
  110. package/registry/layout/project-context-shell.json +20 -0
  111. package/registry/layout/search-bar-shell.json +22 -0
  112. package/registry/layout/sidebar-nav.json +18 -0
  113. package/registry/layout/sidebar.json +20 -0
  114. package/registry/layout/standard-page-shell.json +21 -0
  115. package/registry/layout/vertical-multistep-shell.json +23 -0
  116. package/registry/lib/utils.json +17 -0
  117. package/registry/ui/avatar.json +18 -0
  118. package/registry/ui/button.json +19 -0
  119. package/registry/ui/calendar.json +20 -0
  120. package/registry/ui/checkbox.json +19 -0
  121. package/registry/ui/date-input.json +18 -0
  122. package/registry/ui/dialog.json +19 -0
  123. package/registry/ui/dropdown-menu.json +19 -0
  124. package/registry/ui/file-uploader.json +18 -0
  125. package/registry/ui/image-uploader.json +18 -0
  126. package/registry/ui/input.json +16 -0
  127. package/registry/ui/label.json +18 -0
  128. package/registry/ui/line-tabs.json +16 -0
  129. package/registry/ui/multiselect-checkbox-field.json +18 -0
  130. package/registry/ui/multiselect-tags.json +18 -0
  131. package/registry/ui/popover.json +18 -0
  132. package/registry/ui/radio-group.json +19 -0
  133. package/registry/ui/range-input.json +17 -0
  134. package/registry/ui/scroll-area.json +18 -0
  135. package/registry/ui/searchbox.json +18 -0
  136. package/registry/ui/select.json +20 -0
  137. package/registry/ui/selectable-pills.json +16 -0
  138. package/registry/ui/separator.json +18 -0
  139. package/registry/ui/sheet.json +19 -0
  140. package/registry/ui/sidebar.json +27 -0
  141. package/registry/ui/skeleton.json +16 -0
  142. package/registry/ui/slider.json +18 -0
  143. package/registry/ui/switch.json +18 -0
  144. package/registry/ui/tabs.json +18 -0
  145. package/registry/ui/text-input.json +16 -0
  146. package/registry/ui/textarea.json +18 -0
  147. package/registry/ui/tooltip.json +18 -0
  148. package/registry/ui/typography.json +16 -0
@@ -0,0 +1,516 @@
1
+ // cli/src/index.ts
2
+ import { Command } from "commander";
3
+
4
+ // cli/src/commands/init.ts
5
+ import fs2 from "fs-extra";
6
+ import path2 from "path";
7
+ import chalk from "chalk";
8
+ import ora from "ora";
9
+ import prompts from "prompts";
10
+
11
+ // cli/src/types.ts
12
+ var DEFAULT_CONFIG = {
13
+ aliases: {
14
+ components: "@/components",
15
+ ui: "@/components/ui",
16
+ lib: "@/lib",
17
+ hooks: "@/hooks"
18
+ },
19
+ tailwind: {
20
+ css: "src/app/globals.css"
21
+ }
22
+ };
23
+
24
+ // cli/src/utils/installer.ts
25
+ import { execa } from "execa";
26
+ import fs from "fs-extra";
27
+ import path from "path";
28
+ function detectPackageManager(cwd) {
29
+ if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock"))) return "bun";
30
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
31
+ if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
32
+ return "npm";
33
+ }
34
+ async function installPackages(packages, cwd) {
35
+ if (packages.length === 0) return;
36
+ const pm = detectPackageManager(cwd);
37
+ const args = pm === "npm" ? ["install", ...packages] : pm === "yarn" ? ["add", ...packages] : pm === "pnpm" ? ["add", ...packages] : ["add", ...packages];
38
+ await execa(pm, args, { cwd, stdio: "pipe" });
39
+ }
40
+
41
+ // cli/src/commands/init.ts
42
+ var CONFIG_FILE = "canvas-ui.json";
43
+ async function init() {
44
+ const cwd = process.cwd();
45
+ if (fs2.existsSync(path2.join(cwd, CONFIG_FILE))) {
46
+ const { overwrite } = await prompts({
47
+ type: "confirm",
48
+ name: "overwrite",
49
+ message: `${CONFIG_FILE} already exists. Overwrite?`,
50
+ initial: false
51
+ });
52
+ if (!overwrite) {
53
+ console.log("Aborted.");
54
+ return;
55
+ }
56
+ }
57
+ if (!fs2.existsSync(path2.join(cwd, "package.json"))) {
58
+ console.log(
59
+ chalk.red("No package.json found. Please run this in a project directory.")
60
+ );
61
+ process.exit(1);
62
+ }
63
+ console.log(chalk.bold("\nInitializing Canvas UI SDK...\n"));
64
+ const answers = await prompts([
65
+ {
66
+ type: "text",
67
+ name: "cssPath",
68
+ message: "Where is your global CSS file?",
69
+ initial: DEFAULT_CONFIG.tailwind.css
70
+ },
71
+ {
72
+ type: "text",
73
+ name: "componentsAlias",
74
+ message: "Components import alias:",
75
+ initial: DEFAULT_CONFIG.aliases.components
76
+ },
77
+ {
78
+ type: "text",
79
+ name: "libAlias",
80
+ message: "Lib/utils import alias:",
81
+ initial: DEFAULT_CONFIG.aliases.lib
82
+ },
83
+ {
84
+ type: "text",
85
+ name: "hooksAlias",
86
+ message: "Hooks import alias:",
87
+ initial: DEFAULT_CONFIG.aliases.hooks
88
+ }
89
+ ]);
90
+ if (!answers.cssPath) {
91
+ console.log("Aborted.");
92
+ return;
93
+ }
94
+ const config = {
95
+ aliases: {
96
+ components: answers.componentsAlias,
97
+ ui: `${answers.componentsAlias}/ui`,
98
+ lib: answers.libAlias,
99
+ hooks: answers.hooksAlias
100
+ },
101
+ tailwind: {
102
+ css: answers.cssPath
103
+ }
104
+ };
105
+ const spinner = ora("Writing configuration...").start();
106
+ await fs2.writeJSON(path2.join(cwd, CONFIG_FILE), config, { spaces: 2 });
107
+ spinner.succeed("Created canvas-ui.json");
108
+ const pm = detectPackageManager(cwd);
109
+ spinner.start(`Installing canvas-ui-sdk via ${pm}...`);
110
+ try {
111
+ await installPackages(["canvas-ui-sdk"], cwd);
112
+ spinner.succeed("Installed canvas-ui-sdk");
113
+ } catch {
114
+ spinner.warn(
115
+ "Could not install canvas-ui-sdk automatically. Run: npm install canvas-ui-sdk"
116
+ );
117
+ }
118
+ spinner.start("Installing base dependencies...");
119
+ try {
120
+ await installPackages(["clsx", "tailwind-merge"], cwd);
121
+ spinner.succeed("Installed clsx and tailwind-merge");
122
+ } catch {
123
+ spinner.warn(
124
+ "Could not install base deps. Run: npm install clsx tailwind-merge"
125
+ );
126
+ }
127
+ spinner.start("Creating directory structure...");
128
+ const dirs = [
129
+ resolveAlias(config.aliases.ui, cwd),
130
+ path2.join(resolveAlias(config.aliases.components, cwd), "blocks"),
131
+ path2.join(resolveAlias(config.aliases.components, cwd), "layout"),
132
+ resolveAlias(config.aliases.lib, cwd),
133
+ resolveAlias(config.aliases.hooks, cwd)
134
+ ];
135
+ for (const dir of dirs) {
136
+ await fs2.ensureDir(dir);
137
+ }
138
+ spinner.succeed("Created component directories");
139
+ const cssPath = path2.join(cwd, config.tailwind.css);
140
+ if (fs2.existsSync(cssPath)) {
141
+ spinner.start("Updating globals.css...");
142
+ let css = await fs2.readFile(cssPath, "utf-8");
143
+ const importsToAdd = [
144
+ '@import "canvas-ui-sdk/tailwind";',
145
+ '@import "canvas-ui-sdk/styles";'
146
+ ];
147
+ let modified = false;
148
+ for (const imp of importsToAdd) {
149
+ if (!css.includes(imp)) {
150
+ const tailwindImport = '@import "tailwindcss"';
151
+ if (css.includes(tailwindImport)) {
152
+ const idx = css.indexOf(tailwindImport);
153
+ const lineEnd = css.indexOf("\n", idx);
154
+ css = css.slice(0, lineEnd + 1) + imp + "\n" + css.slice(lineEnd + 1);
155
+ } else {
156
+ css = imp + "\n" + css;
157
+ }
158
+ modified = true;
159
+ }
160
+ }
161
+ const sourceDirective = '@source "./components";';
162
+ if (!css.includes(sourceDirective) && !css.includes("@source")) {
163
+ const lastImport = css.lastIndexOf("@import");
164
+ if (lastImport !== -1) {
165
+ const lineEnd = css.indexOf("\n", css.indexOf(";", lastImport));
166
+ css = css.slice(0, lineEnd + 1) + sourceDirective + "\n" + css.slice(lineEnd + 1);
167
+ } else {
168
+ css = sourceDirective + "\n" + css;
169
+ }
170
+ modified = true;
171
+ }
172
+ if (modified) {
173
+ await fs2.writeFile(cssPath, css);
174
+ spinner.succeed("Updated globals.css with Canvas UI imports");
175
+ } else {
176
+ spinner.succeed("globals.css already has Canvas UI imports");
177
+ }
178
+ } else {
179
+ spinner.info(
180
+ `globals.css not found at ${config.tailwind.css}. You'll need to add the imports manually.`
181
+ );
182
+ }
183
+ spinner.start("Copying lib/utils.ts...");
184
+ const utilsContent = `import { clsx, type ClassValue } from "clsx"
185
+ import { twMerge } from "tailwind-merge"
186
+
187
+ export function cn(...inputs: ClassValue[]) {
188
+ return twMerge(clsx(inputs))
189
+ }
190
+ `;
191
+ const utilsPath = path2.join(
192
+ resolveAlias(config.aliases.lib, cwd),
193
+ "utils.ts"
194
+ );
195
+ await fs2.writeFile(utilsPath, utilsContent);
196
+ spinner.succeed("Created lib/utils.ts");
197
+ console.log(
198
+ chalk.green("\n\u2713 Canvas UI SDK initialized successfully!\n")
199
+ );
200
+ console.log("Next steps:");
201
+ console.log(
202
+ ` ${chalk.cyan("npx canvas-ui add button")} Add a component`
203
+ );
204
+ console.log(
205
+ ` ${chalk.cyan("npx canvas-ui add dashboard-shell")} Add a layout shell`
206
+ );
207
+ console.log();
208
+ }
209
+ function resolveAlias(alias, cwd) {
210
+ const stripped = alias.replace(/^@\//, "").replace(/^~\//, "");
211
+ return path2.join(cwd, "src", stripped);
212
+ }
213
+
214
+ // cli/src/commands/add.ts
215
+ import fs4 from "fs-extra";
216
+ import path4 from "path";
217
+ import chalk2 from "chalk";
218
+ import ora2 from "ora";
219
+
220
+ // cli/src/utils/registry.ts
221
+ import fs3 from "fs-extra";
222
+ import path3 from "path";
223
+ function getRegistryDir() {
224
+ const candidates = [
225
+ path3.resolve(import.meta.dirname, "../../registry"),
226
+ path3.resolve(import.meta.dirname, "../../../registry"),
227
+ path3.resolve(process.cwd(), "node_modules/canvas-ui-sdk/registry")
228
+ ];
229
+ for (const candidate of candidates) {
230
+ if (fs3.existsSync(path3.join(candidate, "index.json"))) {
231
+ return candidate;
232
+ }
233
+ }
234
+ throw new Error(
235
+ "Could not find canvas-ui registry. Make sure canvas-ui-sdk is installed."
236
+ );
237
+ }
238
+ async function loadRegistryIndex() {
239
+ const registryDir = getRegistryDir();
240
+ const indexPath = path3.join(registryDir, "index.json");
241
+ return fs3.readJSON(indexPath);
242
+ }
243
+ async function loadRegistryItem(name) {
244
+ const registryDir = getRegistryDir();
245
+ for (const subdir of ["ui", "blocks", "layout", "hooks", "lib"]) {
246
+ const itemPath = path3.join(registryDir, subdir, `${name}.json`);
247
+ if (fs3.existsSync(itemPath)) {
248
+ return fs3.readJSON(itemPath);
249
+ }
250
+ }
251
+ return null;
252
+ }
253
+
254
+ // cli/src/utils/resolver.ts
255
+ async function resolveAll(names) {
256
+ const resolved = /* @__PURE__ */ new Map();
257
+ const npmDeps = /* @__PURE__ */ new Set();
258
+ async function resolve(name) {
259
+ if (resolved.has(name)) return;
260
+ const item = await loadRegistryItem(name);
261
+ if (!item) {
262
+ console.warn(` Warning: component "${name}" not found in registry, skipping`);
263
+ return;
264
+ }
265
+ resolved.set(name, item);
266
+ for (const dep of item.dependencies) {
267
+ npmDeps.add(dep);
268
+ }
269
+ for (const dep of item.registryDependencies) {
270
+ await resolve(dep);
271
+ }
272
+ }
273
+ for (const name of names) {
274
+ await resolve(name);
275
+ }
276
+ if (!resolved.has("utils")) {
277
+ await resolve("utils");
278
+ }
279
+ return {
280
+ items: [...resolved.values()],
281
+ npmDeps: [...npmDeps]
282
+ };
283
+ }
284
+
285
+ // cli/src/utils/transform.ts
286
+ var NPM_PACKAGE_REWRITES = {
287
+ "../../context/theme-context": {
288
+ pkg: "canvas-ui-sdk",
289
+ named: true
290
+ },
291
+ "../../lib/theme-defaults": {
292
+ pkg: "canvas-ui-sdk",
293
+ named: true
294
+ },
295
+ "../../lib/css-variable-sync": {
296
+ pkg: "canvas-ui-sdk",
297
+ named: true
298
+ }
299
+ };
300
+ function transformSource(content, filePath, config) {
301
+ let result = content;
302
+ for (const [from, to] of Object.entries(NPM_PACKAGE_REWRITES)) {
303
+ result = result.replace(
304
+ new RegExp(`from\\s+["']${escapeRegex(from)}["']`, "g"),
305
+ `from "${to.pkg}"`
306
+ );
307
+ }
308
+ result = result.replace(
309
+ /from\s+["']\.\.\/\.\.\/lib\/(\w+)["']/g,
310
+ `from "${config.aliases.lib}/$1"`
311
+ );
312
+ result = result.replace(
313
+ /from\s+["']\.\.\/\.\.\/hooks\/([\w-]+)["']/g,
314
+ `from "${config.aliases.hooks}/$1"`
315
+ );
316
+ result = result.replace(
317
+ /from\s+["']\.\.\/ui\/([\w-]+)["']/g,
318
+ `from "${config.aliases.ui}/$1"`
319
+ );
320
+ result = result.replace(
321
+ /from\s+["']\.\.\/blocks\/([\w-]+)["']/g,
322
+ `from "${config.aliases.components}/blocks/$1"`
323
+ );
324
+ if (filePath.startsWith("components/layout/")) {
325
+ result = result.replace(
326
+ /from\s+["']\.\/([\w-]+)["']/g,
327
+ `from "${config.aliases.components}/layout/$1"`
328
+ );
329
+ } else if (filePath.startsWith("components/blocks/marketing/")) {
330
+ result = result.replace(
331
+ /from\s+["']\.\/([\w-]+)["']/g,
332
+ `from "${config.aliases.components}/blocks/marketing/$1"`
333
+ );
334
+ } else if (filePath.startsWith("components/blocks/pricing/")) {
335
+ result = result.replace(
336
+ /from\s+["']\.\/([\w-]+)["']/g,
337
+ `from "${config.aliases.components}/blocks/pricing/$1"`
338
+ );
339
+ } else if (filePath.startsWith("components/blocks/")) {
340
+ result = result.replace(
341
+ /from\s+["']\.\/([\w-]+)["']/g,
342
+ `from "${config.aliases.components}/blocks/$1"`
343
+ );
344
+ }
345
+ return result;
346
+ }
347
+ function escapeRegex(str) {
348
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
349
+ }
350
+
351
+ // cli/src/commands/add.ts
352
+ var CONFIG_FILE2 = "canvas-ui.json";
353
+ async function add(componentNames) {
354
+ const cwd = process.cwd();
355
+ const configPath = path4.join(cwd, CONFIG_FILE2);
356
+ if (!fs4.existsSync(configPath)) {
357
+ console.log(
358
+ chalk2.red(
359
+ `No ${CONFIG_FILE2} found. Run ${chalk2.cyan("npx canvas-ui init")} first.`
360
+ )
361
+ );
362
+ process.exit(1);
363
+ }
364
+ const config = await fs4.readJSON(configPath);
365
+ if (componentNames.length === 0) {
366
+ console.log(chalk2.red("Please specify at least one component to add."));
367
+ console.log(`
368
+ Usage: ${chalk2.cyan("npx canvas-ui add <component> [component...]")}`);
369
+ console.log(`
370
+ Run ${chalk2.cyan("npx canvas-ui list")} to see available components.`);
371
+ process.exit(1);
372
+ }
373
+ const spinner = ora2("Loading registry...").start();
374
+ const index = await loadRegistryIndex();
375
+ const validNames = new Set(index.items.map((i) => i.name));
376
+ const invalid = componentNames.filter((n) => !validNames.has(n));
377
+ if (invalid.length > 0) {
378
+ spinner.fail(`Unknown components: ${invalid.join(", ")}`);
379
+ console.log(`
380
+ Run ${chalk2.cyan("npx canvas-ui list")} to see available components.`);
381
+ process.exit(1);
382
+ }
383
+ spinner.succeed("Registry loaded");
384
+ spinner.start("Resolving dependencies...");
385
+ const { items, npmDeps } = await resolveAll(componentNames);
386
+ spinner.succeed(
387
+ `Resolved ${items.length} components (${items.length - componentNames.length} dependencies)`
388
+ );
389
+ const existing = [];
390
+ const toWrite = [];
391
+ for (const item of items) {
392
+ for (const file of item.files) {
393
+ const targetPath = resolveFilePath(file.path, config, cwd);
394
+ if (fs4.existsSync(targetPath)) {
395
+ existing.push(file.path);
396
+ }
397
+ toWrite.push({ item, targetPath });
398
+ }
399
+ }
400
+ if (existing.length > 0) {
401
+ console.log(
402
+ chalk2.yellow(`
403
+ Files that already exist (will be overwritten):`)
404
+ );
405
+ for (const f of existing) {
406
+ console.log(chalk2.dim(` ${f}`));
407
+ }
408
+ console.log();
409
+ }
410
+ spinner.start("Writing component files...");
411
+ let filesWritten = 0;
412
+ for (const { item, targetPath } of toWrite) {
413
+ for (const file of item.files) {
414
+ const transformed = transformSource(file.content, file.path, config);
415
+ await fs4.ensureDir(path4.dirname(targetPath));
416
+ await fs4.writeFile(targetPath, transformed);
417
+ filesWritten++;
418
+ }
419
+ }
420
+ spinner.succeed(`Wrote ${filesWritten} files`);
421
+ if (npmDeps.length > 0) {
422
+ const pkgJsonPath = path4.join(cwd, "package.json");
423
+ let installed = /* @__PURE__ */ new Set();
424
+ if (fs4.existsSync(pkgJsonPath)) {
425
+ const pkgJson = await fs4.readJSON(pkgJsonPath);
426
+ installed = /* @__PURE__ */ new Set([
427
+ ...Object.keys(pkgJson.dependencies || {}),
428
+ ...Object.keys(pkgJson.devDependencies || {})
429
+ ]);
430
+ }
431
+ const toInstall = npmDeps.filter((dep) => !installed.has(dep));
432
+ if (toInstall.length > 0) {
433
+ spinner.start(`Installing ${toInstall.length} npm packages...`);
434
+ try {
435
+ await installPackages(toInstall, cwd);
436
+ spinner.succeed(`Installed: ${toInstall.join(", ")}`);
437
+ } catch (err) {
438
+ spinner.warn(
439
+ `Could not auto-install packages. Run manually:
440
+ npm install ${toInstall.join(" ")}`
441
+ );
442
+ }
443
+ }
444
+ }
445
+ console.log(chalk2.green("\n\u2713 Components added successfully!\n"));
446
+ const requested = new Set(componentNames);
447
+ const deps = items.filter((i) => !requested.has(i.name));
448
+ for (const name of componentNames) {
449
+ const item = items.find((i) => i.name === name);
450
+ if (item) {
451
+ console.log(
452
+ ` ${chalk2.green("+")} ${chalk2.bold(name)} ${chalk2.dim(`\u2192 ${item.files[0]?.path}`)}`
453
+ );
454
+ }
455
+ }
456
+ if (deps.length > 0) {
457
+ console.log(chalk2.dim("\n Dependencies:"));
458
+ for (const dep of deps) {
459
+ console.log(
460
+ ` ${chalk2.green("+")} ${dep.name} ${chalk2.dim(`\u2192 ${dep.files[0]?.path}`)}`
461
+ );
462
+ }
463
+ }
464
+ console.log();
465
+ }
466
+ async function list() {
467
+ const spinner = ora2("Loading registry...").start();
468
+ const index = await loadRegistryIndex();
469
+ spinner.stop();
470
+ const byType = {};
471
+ for (const item of index.items) {
472
+ const type = item.type.replace("registry:", "");
473
+ if (!byType[type]) byType[type] = [];
474
+ byType[type].push(item);
475
+ }
476
+ const typeLabels = {
477
+ ui: "UI Primitives",
478
+ block: "Blocks",
479
+ layout: "Layout Shells",
480
+ hook: "Hooks",
481
+ lib: "Utilities"
482
+ };
483
+ for (const [type, items] of Object.entries(byType)) {
484
+ console.log(chalk2.bold(`
485
+ ${typeLabels[type] || type} (${items.length})`));
486
+ for (const item of items) {
487
+ const desc = item.description ? chalk2.dim(` \u2014 ${item.description.slice(0, 60)}`) : "";
488
+ console.log(` ${chalk2.cyan(item.name)}${desc}`);
489
+ }
490
+ }
491
+ console.log();
492
+ }
493
+ function resolveFilePath(registryPath, config, cwd) {
494
+ const aliasPrefix = config.aliases.components.replace(/^@\//, "").replace(/^~\//, "");
495
+ const libPrefix = config.aliases.lib.replace(/^@\//, "").replace(/^~\//, "");
496
+ const hooksPrefix = config.aliases.hooks.replace(/^@\//, "").replace(/^~\//, "");
497
+ let resolved;
498
+ if (registryPath.startsWith("components/")) {
499
+ resolved = registryPath.replace("components/", `${aliasPrefix}/`);
500
+ } else if (registryPath.startsWith("lib/")) {
501
+ resolved = registryPath.replace("lib/", `${libPrefix}/`);
502
+ } else if (registryPath.startsWith("hooks/")) {
503
+ resolved = registryPath.replace("hooks/", `${hooksPrefix}/`);
504
+ } else {
505
+ resolved = registryPath;
506
+ }
507
+ return path4.join(cwd, "src", resolved);
508
+ }
509
+
510
+ // cli/src/index.ts
511
+ var program = new Command();
512
+ program.name("canvas-ui").description("CLI for adding Canvas UI SDK components to your project").version("0.1.0");
513
+ program.command("init").description("Initialize Canvas UI SDK in your project").action(init);
514
+ program.command("add").description("Add components to your project").argument("<components...>", "Component names to add").action(add);
515
+ program.command("list").description("List all available components").action(list);
516
+ program.parse();
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "canvas-ui-sdk",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "description": "A comprehensive UI component library with design tokens for building beautiful interfaces",
6
+ "bin": {
7
+ "canvas-ui": "./dist/cli/index.js"
8
+ },
6
9
  "main": "./dist/index.js",
7
10
  "module": "./dist/index.js",
8
11
  "types": "./dist/index.d.ts",
@@ -26,10 +29,12 @@
26
29
  "files": [
27
30
  "dist",
28
31
  "styles",
32
+ "registry",
29
33
  "mcp/dist"
30
34
  ],
31
35
  "scripts": {
32
- "build": "tsup && node -e \"const fs = require('fs'); for (const f of ['dist/index.js','dist/charts.js']) { const c = fs.readFileSync(f,'utf8'); fs.writeFileSync(f, '\\\"use client\\\";\\n' + c); }\"",
36
+ "build": "tsup && node -e \"const fs = require('fs'); for (const f of ['dist/index.js','dist/charts.js']) { const c = fs.readFileSync(f,'utf8'); fs.writeFileSync(f, '\\\"use client\\\";\\n' + c); }\" && npm run cli:build",
37
+ "cli:build": "npx tsx scripts/generate-registry.ts && tsup --config tsup.cli.config.ts",
33
38
  "dev": "tsup --watch",
34
39
  "lint": "eslint src/",
35
40
  "typecheck": "tsc --noEmit",
@@ -82,11 +87,22 @@
82
87
  "sonner": "^2.0.7",
83
88
  "tailwind-merge": "^3.4.0"
84
89
  },
90
+ "optionalDependencies": {
91
+ "chalk": "^5.4.0",
92
+ "commander": "^13.0.0",
93
+ "execa": "^9.5.0",
94
+ "fs-extra": "^11.2.0",
95
+ "ora": "^8.2.0",
96
+ "prompts": "^2.4.2"
97
+ },
85
98
  "devDependencies": {
99
+ "@types/fs-extra": "^11.0.4",
86
100
  "@types/node": "^20",
101
+ "@types/prompts": "^2.4.9",
87
102
  "@types/react": "^19",
88
103
  "@types/react-dom": "^19",
89
104
  "eslint": "^9",
105
+ "next": "^16.1.6",
90
106
  "tsup": "^8.0.0",
91
107
  "typescript": "^5"
92
108
  },
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "activity-feed",
3
+ "type": "registry:block",
4
+ "description": "Timeline-style activity feed showing user actions, comments, and file attachments with connecting lines. Supports status changes, comments with reactions, and file attachments.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/activity-feed.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { Check, Heart, MessageCircle, FileText } from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface ActivityAuthor {\n id: string;\n name: string;\n avatarUrl?: string;\n}\n\nexport interface ActivityAttachment {\n id: string;\n name: string;\n size: string;\n type?: \"document\" | \"image\" | \"other\";\n}\n\nexport interface BaseActivityItem {\n id: string;\n author: ActivityAuthor;\n timestamp: string;\n}\n\nexport interface StatusChangeActivity extends BaseActivityItem {\n type: \"status_change\";\n action: \"completed\" | \"updated\" | \"started\" | \"archived\";\n projectName: string;\n}\n\nexport interface CommentActivity extends BaseActivityItem {\n type: \"comment\";\n projectName: string;\n content: string;\n likes: number;\n replies: number;\n isLiked?: boolean;\n}\n\nexport interface AttachmentActivity extends BaseActivityItem {\n type: \"attachment\";\n action: \"completed\" | \"uploaded\" | \"shared\";\n projectName: string;\n attachment: ActivityAttachment;\n}\n\nexport type ActivityItem = StatusChangeActivity | CommentActivity | AttachmentActivity;\n\nexport interface ActivityFeedProps {\n /** Section title */\n title?: string;\n /** Section subtitle */\n subtitle?: string;\n /** Activity items to display */\n items?: ActivityItem[];\n /** Callback when like button is clicked */\n onLike?: (itemId: string) => void;\n /** Callback when reply button is clicked */\n onReply?: (itemId: string) => void;\n /** Callback when attachment is clicked */\n onAttachmentClick?: (itemId: string, attachmentId: string) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultItems: ActivityItem[] = [\n {\n id: \"1\",\n type: \"status_change\",\n author: {\n id: \"raj\",\n name: \"Raj Mishra\",\n avatarUrl: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face\",\n },\n action: \"completed\",\n projectName: \"Acme Project\",\n timestamp: \"Today at 8:15 AM\",\n },\n {\n id: \"2\",\n type: \"comment\",\n author: {\n id: \"raj\",\n name: \"Raj Mishra\",\n avatarUrl: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face\",\n },\n projectName: \"Acme Project\",\n content: \"Thank you Mary, the invoice looks great! Could you email it to Jeffrey and the Acme team and ask them to please pay by tomorrow?\",\n likes: 30,\n replies: 10,\n isLiked: true,\n timestamp: \"Yesterday at 11:25 AM\",\n },\n {\n id: \"3\",\n type: \"attachment\",\n author: {\n id: \"mary\",\n name: \"Mary Trott\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n },\n action: \"completed\",\n projectName: \"Acme Project\",\n attachment: {\n id: \"inv-1\",\n name: \"Invoice #23J2KF\",\n size: \"10 MB\",\n type: \"document\",\n },\n timestamp: \"3 days ago\",\n },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface ActivityLineProps {\n showLine: boolean;\n height?: string;\n}\n\nfunction ActivityLine({ showLine, height = \"64px\" }: ActivityLineProps) {\n if (!showLine) return null;\n return (\n <div\n style={{\n width: \"1px\",\n height,\n backgroundColor: \"var(--canvas-border-disabled)\",\n }}\n />\n );\n}\n\ninterface StatusIconProps {\n status: \"completed\" | \"updated\" | \"started\" | \"archived\";\n}\n\nfunction StatusIcon({ status }: StatusIconProps) {\n if (status === \"completed\") {\n return (\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: \"48px\",\n height: \"48px\",\n backgroundColor: \"var(--canvas-success)\",\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <Check size={20} color=\"white\" strokeWidth={2.5} />\n </div>\n );\n }\n \n // Default fallback for other statuses\n return (\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: \"48px\",\n height: \"48px\",\n backgroundColor: \"var(--canvas-surface)\",\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n />\n );\n}\n\ninterface ActivityAvatarProps {\n avatarUrl?: string;\n name: string;\n}\n\nfunction ActivityAvatar({ avatarUrl, name }: ActivityAvatarProps) {\n return (\n <Avatar\n className=\"shrink-0\"\n style={{\n width: \"48px\",\n height: \"48px\",\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={avatarUrl} alt={name} />\n <AvatarFallback>\n {name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n );\n}\n\ninterface AttachmentCardProps {\n attachment: ActivityAttachment;\n onClick?: () => void;\n}\n\nfunction AttachmentCard({ attachment, onClick }: AttachmentCardProps) {\n return (\n <div\n className=\"flex items-center cursor-pointer\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-2xl)\",\n padding: \"var(--spacing-xl)\",\n boxShadow: \"0px 1px 8px 0px rgba(0, 0, 0, 0.03)\",\n gap: \"0\",\n }}\n onClick={onClick}\n >\n {/* Icon container */}\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: \"64px\",\n height: \"64px\",\n backgroundColor: \"var(--canvas-surface-brand)\",\n border: \"1px solid var(--canvas-primary)\",\n borderRadius: \"var(--radius-md)\",\n }}\n >\n <FileText size={32} style={{ color: \"var(--canvas-primary)\" }} />\n </div>\n \n {/* File info */}\n <div\n className=\"flex flex-col justify-center\"\n style={{\n paddingLeft: \"var(--spacing-xl)\",\n paddingRight: \"var(--spacing-xl)\",\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {attachment.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {attachment.size}\n </span>\n </div>\n </div>\n );\n}\n\ninterface CommentCardProps {\n content: string;\n likes: number;\n replies: number;\n isLiked?: boolean;\n onLike?: () => void;\n onReply?: () => void;\n}\n\nfunction CommentCard({ content, likes, replies, isLiked, onLike, onReply }: CommentCardProps) {\n return (\n <div\n className=\"flex flex-col w-full\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-md)\",\n padding: \"var(--spacing-4xl)\",\n boxShadow: \"0px 1px 8px 0px rgba(0, 0, 0, 0.03)\",\n gap: \"var(--spacing-lg)\",\n maxWidth: \"580px\",\n }}\n >\n {/* Comment content */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {content}\n </p>\n \n {/* Action icons */}\n <div\n className=\"flex items-center\"\n style={{\n gap: \"var(--spacing-lg)\",\n paddingTop: \"var(--spacing-xxs)\",\n paddingBottom: \"var(--spacing-xxs)\",\n }}\n >\n <button\n onClick={onLike}\n className=\"flex items-center justify-center p-0 border-0 bg-transparent cursor-pointer\"\n >\n <Heart\n size={20}\n fill={isLiked ? \"var(--canvas-destructive)\" : \"none\"}\n color={isLiked ? \"var(--canvas-destructive)\" : \"var(--canvas-text)\"}\n />\n </button>\n <button\n onClick={onReply}\n className=\"flex items-center justify-center p-0 border-0 bg-transparent cursor-pointer\"\n >\n <MessageCircle size={20} style={{ color: \"var(--canvas-text)\" }} />\n </button>\n </div>\n \n {/* Stats */}\n <div\n className=\"flex items-start\"\n style={{\n gap: \"var(--spacing-xl)\",\n paddingTop: \"var(--spacing-xs)\",\n }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {likes} likes\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {replies} replies\n </span>\n </div>\n </div>\n );\n}\n\nfunction getActionText(action: string): string {\n switch (action) {\n case \"completed\":\n return \"marked\";\n case \"updated\":\n return \"updated\";\n case \"started\":\n return \"started\";\n case \"uploaded\":\n return \"uploaded\";\n case \"shared\":\n return \"shared\";\n default:\n return action;\n }\n}\n\nfunction getActionSuffix(action: string): string {\n switch (action) {\n case \"completed\":\n return \"as complete\";\n case \"updated\":\n return \"\";\n case \"started\":\n return \"\";\n default:\n return \"\";\n }\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Activity Feed Block\n * \n * A timeline-style activity feed showing user actions, comments, and file\n * attachments with connecting lines. Useful for project updates, notifications,\n * and collaboration views.\n * \n * @example\n * ```tsx\n * <ActivityFeed\n * title=\"Project status\"\n * subtitle=\"Last updated today\"\n * items={activityItems}\n * onLike={(id) => console.log(\"Liked\", id)}\n * />\n * ```\n */\nexport function ActivityFeed({\n title = \"Project status\",\n subtitle = \"Last updated today\",\n items = defaultItems,\n onLike,\n onReply,\n onAttachmentClick,\n className,\n}: ActivityFeedProps) {\n return (\n <div \n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Header Section */}\n <div \n className=\"flex flex-wrap items-start w-full overflow-hidden\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <div \n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {subtitle}\n </p>\n </div>\n </div>\n\n {/* Activity List */}\n <div className=\"flex flex-col w-full overflow-hidden\">\n {items.map((item, index) => {\n const isLast = index === items.length - 1;\n \n return (\n <div\n key={item.id}\n className=\"flex flex-col w-full\"\n style={{\n paddingTop: index === 0 ? \"0\" : \"var(--spacing-xl)\",\n paddingBottom: isLast ? \"0\" : \"var(--spacing-xl)\",\n }}\n >\n <div\n className=\"flex w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Left column - Avatar/Icon with line */}\n <div\n className=\"flex flex-col items-center shrink-0\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {item.type === \"status_change\" ? (\n <StatusIcon status={item.action} />\n ) : (\n <ActivityAvatar\n avatarUrl={item.author.avatarUrl}\n name={item.author.name}\n />\n )}\n <ActivityLine showLine={!isLast} height={item.type === \"comment\" ? \"100%\" : \"64px\"} />\n </div>\n\n {/* Right column - Content */}\n <div\n className=\"flex flex-col flex-1 min-w-0\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n {/* Activity header row */}\n <div\n className=\"flex flex-col justify-center\"\n style={{\n minHeight: \"48px\",\n gap: \"0\",\n }}\n >\n {/* Title line */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n margin: 0,\n }}\n >\n <span\n style={{\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {item.author.name}\n </span>\n {\" \"}\n <span\n style={{\n fontWeight: \"var(--typo-body-s-weight)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {item.type === \"comment\" ? \"comments on\" : getActionText((item as StatusChangeActivity | AttachmentActivity).action)}\n </span>\n {\" \"}\n <span\n style={{\n fontWeight: \"var(--typo-body-s-weight)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.type === \"comment\" \n ? (item as CommentActivity).projectName\n : (item as StatusChangeActivity | AttachmentActivity).projectName\n }\n </span>\n {item.type !== \"comment\" && getActionSuffix((item as StatusChangeActivity | AttachmentActivity).action) && (\n <>\n {\" \"}\n <span\n style={{\n fontWeight: \"var(--typo-body-s-weight)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {getActionSuffix((item as StatusChangeActivity | AttachmentActivity).action)}\n </span>\n </>\n )}\n </p>\n \n {/* Timestamp */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n margin: 0,\n }}\n >\n {item.timestamp}\n </p>\n </div>\n\n {/* Additional content based on type */}\n {item.type === \"comment\" && (\n <CommentCard\n content={(item as CommentActivity).content}\n likes={(item as CommentActivity).likes}\n replies={(item as CommentActivity).replies}\n isLiked={(item as CommentActivity).isLiked}\n onLike={() => onLike?.(item.id)}\n onReply={() => onReply?.(item.id)}\n />\n )}\n\n {item.type === \"attachment\" && (\n <AttachmentCard\n attachment={(item as AttachmentActivity).attachment}\n onClick={() => onAttachmentClick?.(item.id, (item as AttachmentActivity).attachment.id)}\n />\n )}\n </div>\n </div>\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "registryDependencies": [
16
+ "utils",
17
+ "avatar"
18
+ ]
19
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "blog-cards",
3
+ "type": "registry:block",
4
+ "description": "Grid of blog post cards with images, titles, dates.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/marketing/blog-cards.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { ArrowRight } from \"@phosphor-icons/react\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface BlogPost {\n id: string;\n title: string;\n description: string;\n image: string;\n}\n\nconst defaultPosts: BlogPost[] = [\n {\n id: \"1\",\n title: \"Top destinations to visit\",\n description: \"Discover the most exciting and must-visit travel spots around the world, from iconic cities to hidden gems.\",\n image: \"https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=400&h=340&fit=crop\",\n },\n {\n id: \"2\",\n title: \"Breathtaking natural wonders\",\n description: \"Explore the world's most awe-inspiring landscapes, from towering mountains to stunning waterfalls and surreal deserts.\",\n image: \"https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=340&fit=crop\",\n },\n {\n id: \"3\",\n title: \"Must-try dishes in Italy\",\n description: \"Indulge in Italy's most delicious and authentic dishes, from creamy pasta to crispy pizzas and decadent desserts.\",\n image: \"https://images.unsplash.com/photo-1498579150354-977475b7ea0b?w=400&h=340&fit=crop\",\n },\n];\n\ninterface BlogCardsProps {\n subtitle?: string;\n title?: string;\n posts?: BlogPost[];\n}\n\nexport function BlogCards({ \n subtitle = \"OUR BLOG\",\n title = \"Get inspired\",\n posts = defaultPosts,\n}: BlogCardsProps) {\n return (\n <section \n className=\"w-full px-4 md:px-8 lg:px-20 py-16 md:py-24\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n <div className=\"w-full max-w-[1240px] mx-auto flex flex-col gap-16\">\n {/* Header */}\n <div className=\"flex flex-col gap-3 max-w-[768px]\">\n <Typography variant=\"body-s\" as=\"p\" color=\"muted\">\n {subtitle}\n </Typography>\n <Typography variant=\"h3\" as=\"h2\">\n {title}\n </Typography>\n </div>\n\n {/* Cards Grid */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10\">\n {posts.map((post) => (\n <div \n key={post.id}\n className=\"flex flex-col overflow-hidden cursor-pointer group\"\n style={{\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-xl)\",\n }}\n >\n {/* Image */}\n <div \n className=\"w-full h-[340px] overflow-hidden\"\n style={{\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n >\n <img \n src={post.image} \n alt={post.title}\n className=\"w-full h-full object-cover group-hover:scale-105 transition-transform duration-300\"\n />\n </div>\n \n {/* Content */}\n <div \n className=\"flex flex-col gap-1 p-6\"\n >\n <Typography variant=\"body-xl\" as=\"h3\" style={{ fontWeight: 600 }}>\n {post.title}\n </Typography>\n <Typography variant=\"body-m\" color=\"muted\">\n {post.description}\n </Typography>\n \n {/* Read more */}\n <div className=\"flex items-center gap-2 pt-5\">\n <Typography variant=\"body-m\" as=\"span\" style={{ fontWeight: 500 }}>\n Read more\n </Typography>\n <div \n className=\"flex items-center justify-center\"\n style={{\n width: \"24px\",\n height: \"24px\",\n backgroundColor: \"var(--canvas-text)\",\n borderRadius: \"var(--radius-4xl)\",\n }}\n >\n <ArrowRight size={16} color=\"white\" weight=\"bold\" />\n </div>\n </div>\n </div>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "@phosphor-icons/react"
14
+ ],
15
+ "registryDependencies": []
16
+ }