@utopia-studio-design/design-system-cli 0.6.2 → 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.
- package/README.md +28 -14
- package/bin/utopia-ds-mcp.mjs +6 -2
- package/bin/utopia-ds.mjs +94 -21
- package/data/docs/arabic-friendly.md +19 -2
- package/data/docs/billing-setup.md +80 -0
- package/data/docs/cer-003-032-033-round-8-repair.md +41 -0
- package/data/docs/cer-007-checkout.md +51 -0
- package/data/docs/cer-008-011-qa-repair.md +64 -0
- package/data/docs/cer-016-023-round-6-repair.md +92 -0
- package/data/docs/cer-024-031-round-7-repair.md +51 -0
- package/data/docs/cer-033-035-036-round-12-repair.md +37 -0
- package/data/docs/cer-035-037-round-10-repair.md +34 -0
- package/data/docs/charts.md +34 -3
- package/data/docs/chat-service-methodology.md +181 -0
- package/data/docs/code-block.md +20 -0
- package/data/docs/creative-production.md +149 -0
- package/data/docs/designer-agent-backend-contract.md +552 -0
- package/data/docs/email-resend.md +29 -0
- package/data/docs/foundations.md +46 -8
- package/data/docs/guide.md +9 -9
- package/data/docs/handover-billing-aiden.md +132 -0
- package/data/docs/logo-maker-methodology.md +109 -0
- package/data/docs/presentation-asset-repository.md +53 -0
- package/data/docs/product-architecture.md +42 -0
- package/data/docs/quick-start-ai.md +4 -4
- package/data/docs/shadcn-conversion.md +1 -1
- package/data/docs/sidebar-placement.md +56 -0
- package/data/docs/theme-authoring.md +6 -3
- package/data/manifests/catalog.json +42 -46
- package/data/manifests/components.json +317 -66
- package/data/manifests/logo-skills.json +174 -0
- package/data/manifests/motion-profiles.json +2 -2
- package/data/manifests/patterns.json +201 -7
- package/data/manifests/presentation-assets.schema.json +44 -0
- package/data/manifests/templates.json +87 -1
- package/data/manifests/theme-dextrum.json +7 -7
- package/data/manifests/theme-prism-mono.json +19 -0
- package/data/manifests/theme-utopia-cloudblur.json +135 -0
- package/data/manifests/theme-utopia-default.json +31 -31
- package/data/manifests/theme-vyapti.json +17 -1
- package/data/manifests/theme-y2k-pop.json +18 -0
- package/data/manifests/themes.json +285 -57
- package/data/templates/analytics-dashboard/main.tsx +14 -1
- package/data/templates/calendar-application/main.tsx +14 -1
- package/data/templates/chat-workspace/README.md +12 -0
- package/data/templates/chat-workspace/index.html +11 -0
- package/data/templates/chat-workspace/main.tsx +97 -0
- package/data/templates/chat-workspace/styles.css +19 -0
- package/data/templates/chat-workspace/template.manifest.json +13 -0
- package/data/templates/composition-registry-lab/main.tsx +12 -1
- package/data/templates/dashboard-composer/main.tsx +14 -1
- package/data/templates/database-workspace/main.tsx +14 -1
- package/data/templates/interaction-lab/main.tsx +12 -1
- package/data/templates/quantum-verification-story/README.md +12 -0
- package/data/templates/quantum-verification-story/components.json +21 -0
- package/data/templates/quantum-verification-story/eslint.config.mjs +13 -0
- package/data/templates/quantum-verification-story/next-env.d.ts +7 -0
- package/data/templates/quantum-verification-story/next.config.mjs +11 -0
- package/data/templates/quantum-verification-story/package-lock.json +7535 -0
- package/data/templates/quantum-verification-story/package.json +46 -0
- package/data/templates/quantum-verification-story/postcss.config.js +6 -0
- package/data/templates/quantum-verification-story/src/app/assessment/page.tsx +138 -0
- package/data/templates/quantum-verification-story/src/app/ceramic-theme.css +48 -0
- package/data/templates/quantum-verification-story/src/app/deployment/page.tsx +178 -0
- package/data/templates/quantum-verification-story/src/app/fonts/IBMPlexSansArabic-Bold.ttf +0 -0
- package/data/templates/quantum-verification-story/src/app/fonts/IBMPlexSansArabic-Medium.ttf +0 -0
- package/data/templates/quantum-verification-story/src/app/fonts/IBMPlexSansArabic-Regular.ttf +0 -0
- package/data/templates/quantum-verification-story/src/app/fonts/ReadexPro-Variable.ttf +0 -0
- package/data/templates/quantum-verification-story/src/app/fonts/TWKLausanne-350.ttf +0 -0
- package/data/templates/quantum-verification-story/src/app/fonts/TWKLausanne-500.ttf +0 -0
- package/data/templates/quantum-verification-story/src/app/fonts/TWKLausanne-700.ttf +0 -0
- package/data/templates/quantum-verification-story/src/app/globals.css +175 -0
- package/data/templates/quantum-verification-story/src/app/layout.tsx +45 -0
- package/data/templates/quantum-verification-story/src/app/not-found.tsx +39 -0
- package/data/templates/quantum-verification-story/src/app/page.tsx +24 -0
- package/data/templates/quantum-verification-story/src/app/products/page.tsx +132 -0
- package/data/templates/quantum-verification-story/src/app/utopia-default.css +312 -0
- package/data/templates/quantum-verification-story/src/components/RotatingText.css +36 -0
- package/data/templates/quantum-verification-story/src/components/RotatingText.tsx +189 -0
- package/data/templates/quantum-verification-story/src/components/brand-mark.tsx +14 -0
- package/data/templates/quantum-verification-story/src/components/console-preview-section.tsx +57 -0
- package/data/templates/quantum-verification-story/src/components/console-preview.tsx +72 -0
- package/data/templates/quantum-verification-story/src/components/fade-up.tsx +54 -0
- package/data/templates/quantum-verification-story/src/components/floating-network.tsx +125 -0
- package/data/templates/quantum-verification-story/src/components/footer.tsx +83 -0
- package/data/templates/quantum-verification-story/src/components/harvest-timeline.tsx +70 -0
- package/data/templates/quantum-verification-story/src/components/lattice-diagram.tsx +73 -0
- package/data/templates/quantum-verification-story/src/components/lattice-field.tsx +129 -0
- package/data/templates/quantum-verification-story/src/components/nav.tsx +104 -0
- package/data/templates/quantum-verification-story/src/components/presentation-layout.tsx +91 -0
- package/data/templates/quantum-verification-story/src/components/readiness-estimator.tsx +135 -0
- package/data/templates/quantum-verification-story/src/components/scramble-text.tsx +60 -0
- package/data/templates/quantum-verification-story/src/components/section-backdrop.tsx +38 -0
- package/data/templates/quantum-verification-story/src/components/sections/clients.tsx +140 -0
- package/data/templates/quantum-verification-story/src/components/sections/contact.tsx +125 -0
- package/data/templates/quantum-verification-story/src/components/sections/hero.tsx +92 -0
- package/data/templates/quantum-verification-story/src/components/sections/moat.tsx +73 -0
- package/data/templates/quantum-verification-story/src/components/sections/offer.tsx +113 -0
- package/data/templates/quantum-verification-story/src/components/sections/pilot.tsx +63 -0
- package/data/templates/quantum-verification-story/src/components/sections/problem.tsx +53 -0
- package/data/templates/quantum-verification-story/src/components/text-particle.tsx +204 -0
- package/data/templates/quantum-verification-story/src/components/ticker.tsx +27 -0
- package/data/templates/quantum-verification-story/src/components/ui/badge.tsx +29 -0
- package/data/templates/quantum-verification-story/src/components/ui/button.tsx +55 -0
- package/data/templates/quantum-verification-story/src/components/ui/card.tsx +46 -0
- package/data/templates/quantum-verification-story/src/components/ui/cursor-driven-particle-typography.tsx +288 -0
- package/data/templates/quantum-verification-story/src/components/ui/input.tsx +20 -0
- package/data/templates/quantum-verification-story/src/components/ui/label.tsx +21 -0
- package/data/templates/quantum-verification-story/src/components/ui/separator.tsx +25 -0
- package/data/templates/quantum-verification-story/src/components/ui/textarea.tsx +19 -0
- package/data/templates/quantum-verification-story/src/lib/utils.ts +6 -0
- package/data/templates/quantum-verification-story/tailwind.config.js +95 -0
- package/data/templates/quantum-verification-story/template.manifest.json +23 -0
- package/data/templates/quantum-verification-story/tsconfig.json +42 -0
- package/data/templates/saas-solution-homepage/main.tsx +91 -14
- package/data/templates/saas-solution-homepage/privacy/index.html +1 -0
- package/data/templates/saas-solution-homepage/styles.css +47 -2
- package/data/templates/saas-solution-homepage/team/index.html +1 -0
- package/data/templates/saas-solution-homepage/template.manifest.json +41 -0
- package/data/templates/saas-solution-homepage/terms/index.html +1 -0
- package/lib/api.mjs +52 -3
- package/lib/template-runtime.mjs +1 -0
- 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 }
|