@utopia-studio-design/design-system-cli 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +28 -10
  2. package/bin/utopia-ds-mcp.mjs +6 -2
  3. package/bin/utopia-ds.mjs +94 -21
  4. package/data/docs/arabic-friendly.md +19 -2
  5. package/data/docs/billing-setup.md +80 -0
  6. package/data/docs/cer-003-032-033-round-8-repair.md +41 -0
  7. package/data/docs/cer-007-checkout.md +51 -0
  8. package/data/docs/cer-008-011-qa-repair.md +64 -0
  9. package/data/docs/cer-016-023-round-6-repair.md +92 -0
  10. package/data/docs/cer-024-031-round-7-repair.md +51 -0
  11. package/data/docs/cer-033-035-036-round-12-repair.md +37 -0
  12. package/data/docs/cer-035-037-round-10-repair.md +34 -0
  13. package/data/docs/charts.md +34 -3
  14. package/data/docs/chat-service-methodology.md +181 -0
  15. package/data/docs/code-block.md +20 -0
  16. package/data/docs/creative-production.md +149 -0
  17. package/data/docs/designer-agent-backend-contract.md +552 -0
  18. package/data/docs/email-resend.md +29 -0
  19. package/data/docs/foundations.md +46 -8
  20. package/data/docs/guide.md +9 -9
  21. package/data/docs/handover-billing-aiden.md +132 -0
  22. package/data/docs/logo-maker-methodology.md +109 -0
  23. package/data/docs/presentation-asset-repository.md +53 -0
  24. package/data/docs/product-architecture.md +42 -0
  25. package/data/docs/quick-start-ai.md +4 -4
  26. package/data/docs/shadcn-conversion.md +1 -1
  27. package/data/docs/sidebar-placement.md +56 -0
  28. package/data/docs/theme-authoring.md +6 -3
  29. package/data/manifests/catalog.json +42 -46
  30. package/data/manifests/components.json +317 -66
  31. package/data/manifests/logo-skills.json +174 -0
  32. package/data/manifests/motion-profiles.json +2 -2
  33. package/data/manifests/patterns.json +201 -7
  34. package/data/manifests/presentation-assets.schema.json +44 -0
  35. package/data/manifests/templates.json +87 -1
  36. package/data/manifests/theme-dextrum.json +7 -7
  37. package/data/manifests/theme-prism-mono.json +19 -0
  38. package/data/manifests/theme-utopia-cloudblur.json +135 -0
  39. package/data/manifests/theme-utopia-default.json +31 -31
  40. package/data/manifests/theme-vyapti.json +17 -1
  41. package/data/manifests/theme-y2k-pop.json +18 -0
  42. package/data/manifests/themes.json +285 -57
  43. package/data/templates/analytics-dashboard/main.tsx +14 -1
  44. package/data/templates/calendar-application/main.tsx +14 -1
  45. package/data/templates/chat-workspace/README.md +12 -0
  46. package/data/templates/chat-workspace/index.html +11 -0
  47. package/data/templates/chat-workspace/main.tsx +97 -0
  48. package/data/templates/chat-workspace/styles.css +19 -0
  49. package/data/templates/chat-workspace/template.manifest.json +13 -0
  50. package/data/templates/composition-registry-lab/main.tsx +12 -1
  51. package/data/templates/dashboard-composer/main.tsx +14 -1
  52. package/data/templates/database-workspace/main.tsx +14 -1
  53. package/data/templates/interaction-lab/main.tsx +12 -1
  54. package/data/templates/quantum-verification-story/README.md +12 -0
  55. package/data/templates/quantum-verification-story/components.json +21 -0
  56. package/data/templates/quantum-verification-story/eslint.config.mjs +13 -0
  57. package/data/templates/quantum-verification-story/next-env.d.ts +7 -0
  58. package/data/templates/quantum-verification-story/next.config.mjs +11 -0
  59. package/data/templates/quantum-verification-story/package-lock.json +7535 -0
  60. package/data/templates/quantum-verification-story/package.json +46 -0
  61. package/data/templates/quantum-verification-story/postcss.config.js +6 -0
  62. package/data/templates/quantum-verification-story/src/app/assessment/page.tsx +138 -0
  63. package/data/templates/quantum-verification-story/src/app/ceramic-theme.css +48 -0
  64. package/data/templates/quantum-verification-story/src/app/deployment/page.tsx +178 -0
  65. package/data/templates/quantum-verification-story/src/app/fonts/IBMPlexSansArabic-Bold.ttf +0 -0
  66. package/data/templates/quantum-verification-story/src/app/fonts/IBMPlexSansArabic-Medium.ttf +0 -0
  67. package/data/templates/quantum-verification-story/src/app/fonts/IBMPlexSansArabic-Regular.ttf +0 -0
  68. package/data/templates/quantum-verification-story/src/app/fonts/ReadexPro-Variable.ttf +0 -0
  69. package/data/templates/quantum-verification-story/src/app/fonts/TWKLausanne-350.ttf +0 -0
  70. package/data/templates/quantum-verification-story/src/app/fonts/TWKLausanne-500.ttf +0 -0
  71. package/data/templates/quantum-verification-story/src/app/fonts/TWKLausanne-700.ttf +0 -0
  72. package/data/templates/quantum-verification-story/src/app/globals.css +175 -0
  73. package/data/templates/quantum-verification-story/src/app/layout.tsx +45 -0
  74. package/data/templates/quantum-verification-story/src/app/not-found.tsx +39 -0
  75. package/data/templates/quantum-verification-story/src/app/page.tsx +24 -0
  76. package/data/templates/quantum-verification-story/src/app/products/page.tsx +132 -0
  77. package/data/templates/quantum-verification-story/src/app/utopia-default.css +312 -0
  78. package/data/templates/quantum-verification-story/src/components/RotatingText.css +36 -0
  79. package/data/templates/quantum-verification-story/src/components/RotatingText.tsx +189 -0
  80. package/data/templates/quantum-verification-story/src/components/brand-mark.tsx +14 -0
  81. package/data/templates/quantum-verification-story/src/components/console-preview-section.tsx +57 -0
  82. package/data/templates/quantum-verification-story/src/components/console-preview.tsx +72 -0
  83. package/data/templates/quantum-verification-story/src/components/fade-up.tsx +54 -0
  84. package/data/templates/quantum-verification-story/src/components/floating-network.tsx +125 -0
  85. package/data/templates/quantum-verification-story/src/components/footer.tsx +83 -0
  86. package/data/templates/quantum-verification-story/src/components/harvest-timeline.tsx +70 -0
  87. package/data/templates/quantum-verification-story/src/components/lattice-diagram.tsx +73 -0
  88. package/data/templates/quantum-verification-story/src/components/lattice-field.tsx +129 -0
  89. package/data/templates/quantum-verification-story/src/components/nav.tsx +104 -0
  90. package/data/templates/quantum-verification-story/src/components/presentation-layout.tsx +91 -0
  91. package/data/templates/quantum-verification-story/src/components/readiness-estimator.tsx +135 -0
  92. package/data/templates/quantum-verification-story/src/components/scramble-text.tsx +60 -0
  93. package/data/templates/quantum-verification-story/src/components/section-backdrop.tsx +38 -0
  94. package/data/templates/quantum-verification-story/src/components/sections/clients.tsx +140 -0
  95. package/data/templates/quantum-verification-story/src/components/sections/contact.tsx +125 -0
  96. package/data/templates/quantum-verification-story/src/components/sections/hero.tsx +92 -0
  97. package/data/templates/quantum-verification-story/src/components/sections/moat.tsx +73 -0
  98. package/data/templates/quantum-verification-story/src/components/sections/offer.tsx +113 -0
  99. package/data/templates/quantum-verification-story/src/components/sections/pilot.tsx +63 -0
  100. package/data/templates/quantum-verification-story/src/components/sections/problem.tsx +53 -0
  101. package/data/templates/quantum-verification-story/src/components/text-particle.tsx +204 -0
  102. package/data/templates/quantum-verification-story/src/components/ticker.tsx +27 -0
  103. package/data/templates/quantum-verification-story/src/components/ui/badge.tsx +29 -0
  104. package/data/templates/quantum-verification-story/src/components/ui/button.tsx +55 -0
  105. package/data/templates/quantum-verification-story/src/components/ui/card.tsx +46 -0
  106. package/data/templates/quantum-verification-story/src/components/ui/cursor-driven-particle-typography.tsx +288 -0
  107. package/data/templates/quantum-verification-story/src/components/ui/input.tsx +20 -0
  108. package/data/templates/quantum-verification-story/src/components/ui/label.tsx +21 -0
  109. package/data/templates/quantum-verification-story/src/components/ui/separator.tsx +25 -0
  110. package/data/templates/quantum-verification-story/src/components/ui/textarea.tsx +19 -0
  111. package/data/templates/quantum-verification-story/src/lib/utils.ts +6 -0
  112. package/data/templates/quantum-verification-story/tailwind.config.js +95 -0
  113. package/data/templates/quantum-verification-story/template.manifest.json +23 -0
  114. package/data/templates/quantum-verification-story/tsconfig.json +42 -0
  115. package/data/templates/saas-solution-homepage/main.tsx +91 -14
  116. package/data/templates/saas-solution-homepage/privacy/index.html +1 -0
  117. package/data/templates/saas-solution-homepage/styles.css +47 -2
  118. package/data/templates/saas-solution-homepage/team/index.html +1 -0
  119. package/data/templates/saas-solution-homepage/template.manifest.json +41 -0
  120. package/data/templates/saas-solution-homepage/terms/index.html +1 -0
  121. package/lib/api.mjs +52 -3
  122. package/lib/template-runtime.mjs +1 -0
  123. package/package.json +2 -2
@@ -0,0 +1,204 @@
1
+ "use client"
2
+
3
+ import { useEffect, useRef } from "react"
4
+
5
+ const CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
6
+
7
+ type DrawFn = (ctx: CanvasRenderingContext2D, W: number, H: number) => void
8
+
9
+ interface TextParticleProps {
10
+ text?: string
11
+ /** Custom draw function to define the shape (overrides text) */
12
+ drawShape?: DrawFn
13
+ className?: string
14
+ /** CSS color string for particles */
15
+ color?: string
16
+ /** Font size as fraction of canvas height (0–1) */
17
+ fontScale?: number
18
+ /** Pixel spacing between sampled points */
19
+ spacing?: number
20
+ }
21
+
22
+ // ── Preset icon draw functions ──────────────────────────────────────────────
23
+
24
+ export function drawEye(ctx: CanvasRenderingContext2D, W: number, H: number) {
25
+ const lw = Math.min(W, H) * 0.09
26
+ ctx.strokeStyle = "white"
27
+ ctx.lineWidth = lw
28
+ ctx.lineCap = "round"
29
+ ctx.lineJoin = "round"
30
+ // Outer eye lens shape
31
+ ctx.beginPath()
32
+ ctx.moveTo(W * 0.06, H * 0.5)
33
+ ctx.quadraticCurveTo(W * 0.5, H * 0.1, W * 0.94, H * 0.5)
34
+ ctx.quadraticCurveTo(W * 0.5, H * 0.9, W * 0.06, H * 0.5)
35
+ ctx.stroke()
36
+ // Iris/pupil circle
37
+ ctx.beginPath()
38
+ ctx.arc(W * 0.5, H * 0.5, Math.min(W, H) * 0.16, 0, Math.PI * 2)
39
+ ctx.stroke()
40
+ }
41
+
42
+ export function drawActivity(ctx: CanvasRenderingContext2D, W: number, H: number) {
43
+ const midY = H * 0.5
44
+ const lw = Math.min(W, H) * 0.09
45
+ ctx.strokeStyle = "white"
46
+ ctx.lineWidth = lw
47
+ ctx.lineJoin = "round"
48
+ ctx.lineCap = "round"
49
+ ctx.beginPath()
50
+ ctx.moveTo(W * 0.03, midY)
51
+ ctx.lineTo(W * 0.22, midY)
52
+ ctx.lineTo(W * 0.35, H * 0.14)
53
+ ctx.lineTo(W * 0.50, H * 0.83)
54
+ ctx.lineTo(W * 0.63, H * 0.24)
55
+ ctx.lineTo(W * 0.76, midY)
56
+ ctx.lineTo(W * 0.97, midY)
57
+ ctx.stroke()
58
+ }
59
+
60
+ export function drawBadgeCheck(ctx: CanvasRenderingContext2D, W: number, H: number) {
61
+ const cx = W * 0.5, cy = H * 0.5
62
+ const r = Math.min(W, H) * 0.37
63
+ const lw = r * 0.22
64
+ ctx.strokeStyle = "white"
65
+ ctx.lineWidth = lw
66
+ ctx.lineCap = "round"
67
+ ctx.lineJoin = "round"
68
+ ctx.beginPath()
69
+ for (let i = 0; i < 8; i++) {
70
+ const angle = (i / 8) * Math.PI * 2 - Math.PI / 8
71
+ const x = cx + r * Math.cos(angle)
72
+ const y = cy + r * Math.sin(angle)
73
+ if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y)
74
+ }
75
+ ctx.closePath()
76
+ ctx.stroke()
77
+ ctx.lineWidth = lw * 0.8
78
+ ctx.beginPath()
79
+ ctx.moveTo(cx - r * 0.30, cy + r * 0.05)
80
+ ctx.lineTo(cx - r * 0.03, cy + r * 0.30)
81
+ ctx.lineTo(cx + r * 0.36, cy - r * 0.22)
82
+ ctx.stroke()
83
+ }
84
+
85
+ export function TextParticle({
86
+ text = "PQC",
87
+ drawShape,
88
+ className = "",
89
+ color = "rgb(196,146,58)",
90
+ fontScale = 0.62,
91
+ spacing = 12,
92
+ }: TextParticleProps) {
93
+ const canvasRef = useRef<HTMLCanvasElement>(null)
94
+
95
+ useEffect(() => {
96
+ if (typeof window === "undefined") return
97
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return
98
+
99
+ const canvas = canvasRef.current!
100
+ if (!canvas) return
101
+ const ctx = canvas.getContext("2d")!
102
+
103
+ let raf: number
104
+ let frame = 0
105
+ let particles: { x: number; y: number; char: string; alpha: number }[] = []
106
+
107
+ function buildParticles(W: number, H: number) {
108
+ const off = document.createElement("canvas")
109
+ off.width = W
110
+ off.height = H
111
+ const offCtx = off.getContext("2d")!
112
+ if (drawShape) {
113
+ drawShape(offCtx, W, H)
114
+ } else {
115
+ offCtx.fillStyle = "white"
116
+ offCtx.font = `bold ${Math.floor(H * fontScale)}px 'Courier New', monospace`
117
+ offCtx.textAlign = "center"
118
+ offCtx.textBaseline = "middle"
119
+ offCtx.fillText(text, W / 2, H / 2)
120
+ }
121
+
122
+ const { data } = offCtx.getImageData(0, 0, W, H)
123
+ particles = []
124
+ for (let y = spacing; y < H - spacing / 2; y += spacing) {
125
+ for (let x = spacing / 2; x < W - spacing / 2; x += spacing) {
126
+ if (data[(y * W + x) * 4 + 3] > 100) {
127
+ particles.push({
128
+ x,
129
+ y,
130
+ char: CHARS[Math.floor(Math.random() * CHARS.length)],
131
+ alpha: 0.4 + Math.random() * 0.55,
132
+ })
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ function resize() {
139
+ canvas.width = canvas.offsetWidth
140
+ canvas.height = canvas.offsetHeight
141
+ if (canvas.width > 0 && canvas.height > 0) {
142
+ buildParticles(canvas.width, canvas.height)
143
+ }
144
+ }
145
+
146
+ function draw() {
147
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
148
+
149
+ // Scramble ~10% of particles per frame
150
+ if (frame % 3 === 0) {
151
+ const n = Math.max(1, Math.floor(particles.length * 0.10))
152
+ for (let i = 0; i < n; i++) {
153
+ const idx = Math.floor(Math.random() * particles.length)
154
+ particles[idx].char = CHARS[Math.floor(Math.random() * CHARS.length)]
155
+ particles[idx].alpha = 0.35 + Math.random() * 0.6
156
+ }
157
+ }
158
+
159
+ const fontSize = Math.round(spacing * 1.8)
160
+ ctx.font = `bold ${fontSize}px 'Courier New', monospace`
161
+ ctx.fillStyle = color
162
+
163
+ for (const p of particles) {
164
+ ctx.globalAlpha = p.alpha
165
+ ctx.fillText(p.char, p.x, p.y)
166
+ }
167
+ ctx.globalAlpha = 1
168
+
169
+ frame++
170
+ raf = requestAnimationFrame(draw)
171
+ }
172
+
173
+ const ro = new ResizeObserver(resize)
174
+ ro.observe(canvas)
175
+ resize()
176
+
177
+ const io = new IntersectionObserver(
178
+ ([e]) => {
179
+ if (e.isIntersecting) {
180
+ raf = requestAnimationFrame(draw)
181
+ } else {
182
+ cancelAnimationFrame(raf)
183
+ }
184
+ },
185
+ { threshold: 0.1 }
186
+ )
187
+ io.observe(canvas)
188
+ raf = requestAnimationFrame(draw)
189
+
190
+ return () => {
191
+ cancelAnimationFrame(raf)
192
+ ro.disconnect()
193
+ io.disconnect()
194
+ }
195
+ }, [text, drawShape, color, fontScale, spacing])
196
+
197
+ return (
198
+ <canvas
199
+ ref={canvasRef}
200
+ className={className}
201
+ style={{ display: "block", width: "100%", height: "100%" }}
202
+ />
203
+ )
204
+ }
@@ -0,0 +1,27 @@
1
+ const items = [
2
+ "NIST PQC STANDARDS FINALIZED — COMPLIANCE WINDOW NOW OPEN",
3
+ "HARVEST-NOW-DECRYPT-LATER ATTACKS DOCUMENTED BY NATION-STATE ACTORS",
4
+ "GOVERNMENT CONTRACTORS: PQC MANDATES EFFECTIVE 2025",
5
+ "RBI CYBERSECURITY FRAMEWORK UPDATE — Q4 2024",
6
+ "NIST PQC STANDARDS FINALIZED — COMPLIANCE WINDOW NOW OPEN",
7
+ "HARVEST-NOW-DECRYPT-LATER ATTACKS DOCUMENTED BY NATION-STATE ACTORS",
8
+ "GOVERNMENT CONTRACTORS: PQC MANDATES EFFECTIVE 2025",
9
+ "RBI CYBERSECURITY FRAMEWORK UPDATE — Q4 2024",
10
+ ]
11
+
12
+ export function Ticker() {
13
+ return (
14
+ <div className="bg-[var(--deep-900)] overflow-hidden whitespace-nowrap py-2 border-b border-white/10">
15
+ <div className="inline-block" style={{ animation: "ticker 35s linear infinite" }}>
16
+ {items.map((item, i) => (
17
+ <span
18
+ key={i}
19
+ className="mx-10 font-mono text-[10.5px] tracking-[0.12em] uppercase text-[var(--primary-400)]"
20
+ >
21
+ {item}
22
+ </span>
23
+ ))}
24
+ </div>
25
+ </div>
26
+ )
27
+ }
@@ -0,0 +1,29 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const badgeVariants = cva(
6
+ "inline-flex items-center border px-2.5 py-0.5 text-[10px] font-semibold tracking-[0.12em] uppercase transition-colors",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "border-transparent bg-ink text-[var(--primary-foreground)]",
11
+ secondary: "border-transparent bg-[#ede9df] text-muted2",
12
+ outline: "border-rule text-ink",
13
+ primary: "border-transparent bg-[var(--primary)] text-[var(--primary-foreground)]",
14
+ deep: "border-transparent bg-[var(--deep-900)] text-[var(--primary-foreground)]",
15
+ },
16
+ },
17
+ defaultVariants: { variant: "default" },
18
+ }
19
+ )
20
+
21
+ export interface BadgeProps
22
+ extends React.HTMLAttributes<HTMLDivElement>,
23
+ VariantProps<typeof badgeVariants> {}
24
+
25
+ function Badge({ className, variant, ...props }: BadgeProps) {
26
+ return <div className={cn(badgeVariants({ variant }), className)} {...props} />
27
+ }
28
+
29
+ export { Badge, badgeVariants }
@@ -0,0 +1,55 @@
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const buttonVariants = cva(
7
+ "inline-flex items-center justify-center whitespace-nowrap text-[13px] font-semibold transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default:
12
+ "rounded-full bg-[var(--deep-900)] text-white hover:bg-[var(--deep-800)]",
13
+ outline:
14
+ "rounded-full border border-[var(--color-border)] bg-transparent text-[var(--color-text-primary)] hover:border-[var(--color-border-strong)] hover:bg-[var(--gray-50)]",
15
+ ghost: "rounded-md hover:bg-[var(--gray-100)] text-[var(--color-text-primary)]",
16
+ link: "text-[var(--primary-500)] underline-offset-4 hover:underline",
17
+ primary: "rounded-full bg-[var(--primary-500)] text-[var(--primary-foreground)] hover:bg-[var(--primary-400)]",
18
+ secondary: "rounded-full bg-[var(--gray-100)] text-[var(--color-text-primary)] hover:bg-[var(--gray-200)]",
19
+ destructive: "rounded-full bg-destructive text-destructive-foreground hover:bg-destructive/90",
20
+ },
21
+ size: {
22
+ default: "h-10 px-6",
23
+ sm: "h-8 px-4 text-[12px]",
24
+ lg: "h-11 px-8 text-[15px]",
25
+ icon: "h-9 w-9",
26
+ },
27
+ },
28
+ defaultVariants: {
29
+ variant: "default",
30
+ size: "default",
31
+ },
32
+ }
33
+ )
34
+
35
+ export interface ButtonProps
36
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
37
+ VariantProps<typeof buttonVariants> {
38
+ asChild?: boolean
39
+ }
40
+
41
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
42
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
43
+ const Comp = asChild ? Slot : "button"
44
+ return (
45
+ <Comp
46
+ className={cn(buttonVariants({ variant, size, className }))}
47
+ ref={ref}
48
+ {...props}
49
+ />
50
+ )
51
+ }
52
+ )
53
+ Button.displayName = "Button"
54
+
55
+ export { Button, buttonVariants }
@@ -0,0 +1,46 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
5
+ ({ className, ...props }, ref) => (
6
+ <div ref={ref} className={cn("bg-paper border border-rule transition-colors duration-200", className)} {...props} />
7
+ )
8
+ )
9
+ Card.displayName = "Card"
10
+
11
+ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
12
+ ({ className, ...props }, ref) => (
13
+ <div ref={ref} className={cn("flex flex-col p-9 pb-0", className)} {...props} />
14
+ )
15
+ )
16
+ CardHeader.displayName = "CardHeader"
17
+
18
+ const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
19
+ ({ className, ...props }, ref) => (
20
+ <h3 ref={ref} className={cn("font-display text-2xl font-bold leading-tight", className)} {...props} />
21
+ )
22
+ )
23
+ CardTitle.displayName = "CardTitle"
24
+
25
+ const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
26
+ ({ className, ...props }, ref) => (
27
+ <p ref={ref} className={cn("text-sm leading-7 text-muted2 mt-3", className)} {...props} />
28
+ )
29
+ )
30
+ CardDescription.displayName = "CardDescription"
31
+
32
+ const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
33
+ ({ className, ...props }, ref) => (
34
+ <div ref={ref} className={cn("p-9 pt-6", className)} {...props} />
35
+ )
36
+ )
37
+ CardContent.displayName = "CardContent"
38
+
39
+ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
40
+ ({ className, ...props }, ref) => (
41
+ <div ref={ref} className={cn("flex items-center p-9 pt-0", className)} {...props} />
42
+ )
43
+ )
44
+ CardFooter.displayName = "CardFooter"
45
+
46
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
@@ -0,0 +1,288 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/utils";
4
+ import React, { useEffect, useRef } from "react";
5
+
6
+ export interface CursorDrivenParticleTypographyProps {
7
+ /** Additional CSS classes */
8
+ className?: string;
9
+ /** The text to render */
10
+ text: string;
11
+ /** Font size in pixels */
12
+ fontSize?: number;
13
+ /** Font family */
14
+ fontFamily?: string;
15
+ /** Size of each particle */
16
+ particleSize?: number;
17
+ /** Density of particles (lower number = more particles, minimum 1) */
18
+ particleDensity?: number;
19
+ /** How strongly the cursor pushes particles away */
20
+ dispersionStrength?: number;
21
+ /** Speed at which particles return to origin */
22
+ returnSpeed?: number;
23
+ /** Custom color for particles. Overrides inherited text color if set. */
24
+ color?: string;
25
+ }
26
+
27
+ class Particle {
28
+ x: number;
29
+ y: number;
30
+ originX: number;
31
+ originY: number;
32
+ vx: number;
33
+ vy: number;
34
+ size: number;
35
+ color: string;
36
+ dispersion: number;
37
+ returnSpd: number;
38
+
39
+ constructor(
40
+ x: number,
41
+ y: number,
42
+ size: number,
43
+ color: string,
44
+ dispersion: number,
45
+ returnSpd: number
46
+ ) {
47
+ this.x = x + (Math.random() - 0.5) * 10; // start with slight randomness
48
+ this.y = y + (Math.random() - 0.5) * 10;
49
+ this.originX = x;
50
+ this.originY = y;
51
+ this.vx = (Math.random() - 0.5) * 5;
52
+ this.vy = (Math.random() - 0.5) * 5;
53
+ this.size = size;
54
+ this.color = color;
55
+ this.dispersion = dispersion;
56
+ this.returnSpd = returnSpd;
57
+ }
58
+
59
+ update(mouseX: number, mouseY: number) {
60
+ const dx = mouseX - this.x;
61
+ const dy = mouseY - this.y;
62
+ const distance = Math.sqrt(dx * dx + dy * dy);
63
+
64
+ // Physics interaction with mouse
65
+ const interactionRadius = 120; // 120px interaction radius
66
+
67
+ if (distance < interactionRadius && mouseX !== -1000 && mouseY !== -1000) {
68
+ const forceDirectionX = dx / distance;
69
+ const forceDirectionY = dy / distance;
70
+
71
+ const force = (interactionRadius - distance) / interactionRadius;
72
+
73
+ // Calculate repulsion
74
+ const repulsionX = forceDirectionX * force * this.dispersion;
75
+ const repulsionY = forceDirectionY * force * this.dispersion;
76
+
77
+ this.vx -= repulsionX;
78
+ this.vy -= repulsionY;
79
+ }
80
+
81
+ // Return to origin (spring physics)
82
+ this.vx += (this.originX - this.x) * this.returnSpd;
83
+ this.vy += (this.originY - this.y) * this.returnSpd;
84
+
85
+ // Friction
86
+ this.vx *= 0.85;
87
+ this.vy *= 0.85;
88
+
89
+ // Add subtle noise/jitter when close to origin
90
+ const distToOrigin = Math.sqrt(
91
+ Math.pow(this.x - this.originX, 2) + Math.pow(this.y - this.originY, 2)
92
+ );
93
+ if (distToOrigin < 1 && Math.random() > 0.95) {
94
+ this.vx += (Math.random() - 0.5) * 0.2;
95
+ this.vy += (Math.random() - 0.5) * 0.2;
96
+ }
97
+
98
+ this.x += this.vx;
99
+ this.y += this.vy;
100
+ }
101
+
102
+ draw(ctx: CanvasRenderingContext2D) {
103
+ ctx.fillStyle = this.color;
104
+ ctx.beginPath();
105
+ ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
106
+ ctx.fill();
107
+ }
108
+ }
109
+
110
+ export function CursorDrivenParticleTypography({
111
+ className,
112
+ text,
113
+ fontSize = 120,
114
+ fontFamily = "Inter, sans-serif",
115
+ particleSize = 1.5,
116
+ particleDensity = 6,
117
+ dispersionStrength = 15,
118
+ returnSpeed = 0.08,
119
+ color,
120
+ }: CursorDrivenParticleTypographyProps) {
121
+ const canvasRef = useRef<HTMLCanvasElement>(null);
122
+ const containerRef = useRef<HTMLDivElement>(null);
123
+
124
+ useEffect(() => {
125
+ const canvas = canvasRef.current;
126
+ if (!canvas) return;
127
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
128
+ if (!ctx) return;
129
+
130
+ let animationFrameId: number;
131
+ let particles: Particle[] = [];
132
+
133
+ let mouseX = -1000;
134
+ let mouseY = -1000;
135
+
136
+ let containerWidth = 0;
137
+ let containerHeight = 0;
138
+
139
+ const init = () => {
140
+ const container = containerRef.current;
141
+ if (!container) return;
142
+
143
+ containerWidth = container.clientWidth;
144
+ containerHeight = container.clientHeight;
145
+
146
+ const dpr = window.devicePixelRatio || 1;
147
+ canvas.width = containerWidth * dpr;
148
+ canvas.height = containerHeight * dpr;
149
+ canvas.style.width = `${containerWidth}px`;
150
+ canvas.style.height = `${containerHeight}px`;
151
+
152
+ ctx.scale(dpr, dpr);
153
+
154
+ // Determine text color
155
+ const computedStyle = window.getComputedStyle(container);
156
+ const textColor = color || computedStyle.color || "#000000";
157
+
158
+ ctx.clearRect(0, 0, containerWidth, containerHeight);
159
+
160
+ // Draw text to generate pixel map
161
+ ctx.fillStyle = textColor;
162
+ // Fit text within container: start large, shrink until text width fits
163
+ let effectiveFontSize = Math.min(fontSize, containerHeight * 0.55);
164
+ ctx.font = `bold ${effectiveFontSize}px ${fontFamily}`;
165
+ while (ctx.measureText(text).width > containerWidth * 0.88 && effectiveFontSize > 8) {
166
+ effectiveFontSize -= 2;
167
+ ctx.font = `bold ${effectiveFontSize}px ${fontFamily}`;
168
+ }
169
+ ctx.textAlign = "center";
170
+ ctx.textBaseline = "middle";
171
+
172
+ // Draw standard text first to measure it
173
+ ctx.fillText(text, containerWidth / 2, containerHeight / 2);
174
+
175
+ // Get pixel data
176
+ const textCoordinates = ctx.getImageData(0, 0, canvas.width, canvas.height);
177
+ particles = [];
178
+
179
+ // Create particles from text pixels
180
+ // Step by density multiplied by dpr
181
+ const step = Math.max(1, Math.floor(particleDensity * dpr));
182
+ for (let y = 0; y < textCoordinates.height; y += step) {
183
+ for (let x = 0; x < textCoordinates.width; x += step) {
184
+ const index = (y * textCoordinates.width + x) * 4;
185
+ const alpha = textCoordinates.data[index + 3] || 0;
186
+
187
+ if (alpha > 128) {
188
+ particles.push(
189
+ new Particle(
190
+ x / dpr,
191
+ y / dpr,
192
+ particleSize,
193
+ textColor,
194
+ dispersionStrength,
195
+ returnSpeed
196
+ )
197
+ );
198
+ }
199
+ }
200
+ }
201
+ };
202
+
203
+ const animate = () => {
204
+ ctx.clearRect(0, 0, containerWidth, containerHeight);
205
+
206
+ particles.forEach((particle) => {
207
+ particle.update(mouseX, mouseY);
208
+ particle.draw(ctx);
209
+ });
210
+ animationFrameId = requestAnimationFrame(animate);
211
+ };
212
+
213
+ const handleMouseMove = (e: MouseEvent) => {
214
+ const rect = canvas.getBoundingClientRect();
215
+ mouseX = e.clientX - rect.left;
216
+ mouseY = e.clientY - rect.top;
217
+ };
218
+
219
+ const handleMouseLeave = () => {
220
+ mouseX = -1000;
221
+ mouseY = -1000;
222
+ };
223
+
224
+ const handleResize = () => {
225
+ init();
226
+ };
227
+
228
+ // Initialize with a short delay to ensure fonts/layout are ready
229
+ const timeoutId = setTimeout(() => {
230
+ init();
231
+ animate();
232
+ }, 100);
233
+
234
+ const resizeObserver = new ResizeObserver(() => {
235
+ handleResize();
236
+ });
237
+
238
+ if (containerRef.current) {
239
+ resizeObserver.observe(containerRef.current);
240
+ }
241
+
242
+ // Re-initialize particles when the theme changes (detects class changes on html tag)
243
+ const themeObserver = new MutationObserver(() => {
244
+ init();
245
+ });
246
+ themeObserver.observe(document.documentElement, {
247
+ attributes: true,
248
+ attributeFilter: ["class"]
249
+ });
250
+
251
+ canvas.addEventListener("mousemove", handleMouseMove);
252
+ canvas.addEventListener("mouseleave", handleMouseLeave);
253
+ canvas.addEventListener("touchstart", (e) => {
254
+ if (!e.touches[0]) return;
255
+ const rect = canvas.getBoundingClientRect();
256
+ mouseX = e.touches[0].clientX - rect.left;
257
+ mouseY = e.touches[0].clientY - rect.top;
258
+ });
259
+ canvas.addEventListener("touchmove", (e) => {
260
+ if (!e.touches[0]) return;
261
+ const rect = canvas.getBoundingClientRect();
262
+ mouseX = e.touches[0].clientX - rect.left;
263
+ mouseY = e.touches[0].clientY - rect.top;
264
+ });
265
+ canvas.addEventListener("touchend", handleMouseLeave);
266
+
267
+ return () => {
268
+ clearTimeout(timeoutId);
269
+ resizeObserver.disconnect();
270
+ themeObserver.disconnect();
271
+ canvas.removeEventListener("mousemove", handleMouseMove);
272
+ canvas.removeEventListener("mouseleave", handleMouseLeave);
273
+ cancelAnimationFrame(animationFrameId);
274
+ };
275
+ }, [text, fontSize, fontFamily, particleSize, particleDensity, dispersionStrength, returnSpeed, color]);
276
+
277
+ return (
278
+ <div
279
+ ref={containerRef}
280
+ className={cn("w-full h-full min-h-[400px] flex items-center justify-center relative touch-none", className)}
281
+ >
282
+ <canvas
283
+ ref={canvasRef}
284
+ className="block w-full h-full"
285
+ />
286
+ </div>
287
+ );
288
+ }
@@ -0,0 +1,20 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ export type InputProps = React.InputHTMLAttributes<HTMLInputElement>
5
+
6
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
7
+ ({ className, type, ...props }, ref) => (
8
+ <input
9
+ type={type}
10
+ className={cn(
11
+ "flex h-11 w-full bg-[var(--surface)] border border-[var(--border)] px-4 py-3 text-sm font-sans text-[var(--foreground)] placeholder:text-[var(--muted-foreground)] focus-visible:outline-none focus-visible:border-[var(--primary)] transition-colors duration-200 disabled:cursor-not-allowed disabled:opacity-50",
12
+ className
13
+ )}
14
+ ref={ref}
15
+ {...props}
16
+ />
17
+ )
18
+ )
19
+ Input.displayName = "Input"
20
+ export { Input }
@@ -0,0 +1,21 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as LabelPrimitive from "@radix-ui/react-label"
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const Label = React.forwardRef<
8
+ React.ElementRef<typeof LabelPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <LabelPrimitive.Root
12
+ ref={ref}
13
+ className={cn(
14
+ "block font-mono-brand text-[10px] tracking-[0.15em] uppercase text-[#6b6b5e] mb-2",
15
+ className
16
+ )}
17
+ {...props}
18
+ />
19
+ ))
20
+ Label.displayName = LabelPrimitive.Root.displayName
21
+ export { Label }