@utopia-studio-design/design-system-cli 0.6.2 → 0.7.1
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/ceramic-release-2026-09-06.md +11 -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 +56 -0
- package/package.json +2 -2
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
4
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react';
|
|
5
|
+
import { motion, AnimatePresence } from 'motion/react';
|
|
6
|
+
|
|
7
|
+
import './RotatingText.css';
|
|
8
|
+
|
|
9
|
+
function cn(...classes: any[]) {
|
|
10
|
+
return classes.filter(Boolean).join(' ');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const RotatingText = forwardRef<any, any>((props, ref) => {
|
|
14
|
+
const {
|
|
15
|
+
texts,
|
|
16
|
+
transition = { type: 'spring', damping: 25, stiffness: 300 },
|
|
17
|
+
initial = { y: '100%', opacity: 0 },
|
|
18
|
+
animate = { y: 0, opacity: 1 },
|
|
19
|
+
exit = { y: '-120%', opacity: 0 },
|
|
20
|
+
animatePresenceMode = 'wait',
|
|
21
|
+
animatePresenceInitial = false,
|
|
22
|
+
rotationInterval = 2000,
|
|
23
|
+
staggerDuration = 0,
|
|
24
|
+
staggerFrom = 'first',
|
|
25
|
+
loop = true,
|
|
26
|
+
auto = true,
|
|
27
|
+
splitBy = 'characters',
|
|
28
|
+
onNext,
|
|
29
|
+
mainClassName,
|
|
30
|
+
splitLevelClassName,
|
|
31
|
+
elementLevelClassName,
|
|
32
|
+
...rest
|
|
33
|
+
} = props;
|
|
34
|
+
|
|
35
|
+
const [currentTextIndex, setCurrentTextIndex] = useState(0);
|
|
36
|
+
|
|
37
|
+
const splitIntoCharacters = (text: string) => {
|
|
38
|
+
if (typeof Intl !== 'undefined' && (Intl as any).Segmenter) {
|
|
39
|
+
const segmenter = new (Intl as any).Segmenter('en', { granularity: 'grapheme' });
|
|
40
|
+
return Array.from(segmenter.segment(text), (segment: any) => segment.segment);
|
|
41
|
+
}
|
|
42
|
+
return Array.from(text);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const elements = useMemo(() => {
|
|
46
|
+
const currentText = texts[currentTextIndex];
|
|
47
|
+
if (splitBy === 'characters') {
|
|
48
|
+
const words = currentText.split(' ');
|
|
49
|
+
return words.map((word: string, i: number) => ({
|
|
50
|
+
characters: splitIntoCharacters(word),
|
|
51
|
+
needsSpace: i !== words.length - 1
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
if (splitBy === 'words') {
|
|
55
|
+
return currentText.split(' ').map((word: string, i: number, arr: string[]) => ({
|
|
56
|
+
characters: [word],
|
|
57
|
+
needsSpace: i !== arr.length - 1
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
if (splitBy === 'lines') {
|
|
61
|
+
return currentText.split('\n').map((line: string, i: number, arr: string[]) => ({
|
|
62
|
+
characters: [line],
|
|
63
|
+
needsSpace: i !== arr.length - 1
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return currentText.split(splitBy).map((part: string, i: number, arr: string[]) => ({
|
|
68
|
+
characters: [part],
|
|
69
|
+
needsSpace: i !== arr.length - 1
|
|
70
|
+
}));
|
|
71
|
+
}, [texts, currentTextIndex, splitBy]);
|
|
72
|
+
|
|
73
|
+
const getStaggerDelay = useCallback(
|
|
74
|
+
(index: number, totalChars: number) => {
|
|
75
|
+
const total = totalChars;
|
|
76
|
+
if (staggerFrom === 'first') return index * staggerDuration;
|
|
77
|
+
if (staggerFrom === 'last') return (total - 1 - index) * staggerDuration;
|
|
78
|
+
if (staggerFrom === 'center') {
|
|
79
|
+
const center = Math.floor(total / 2);
|
|
80
|
+
return Math.abs(center - index) * staggerDuration;
|
|
81
|
+
}
|
|
82
|
+
if (staggerFrom === 'random') {
|
|
83
|
+
const randomIndex = Math.floor(Math.random() * total);
|
|
84
|
+
return Math.abs(randomIndex - index) * staggerDuration;
|
|
85
|
+
}
|
|
86
|
+
return Math.abs((staggerFrom as number) - index) * staggerDuration;
|
|
87
|
+
},
|
|
88
|
+
[staggerFrom, staggerDuration]
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const handleIndexChange = useCallback(
|
|
92
|
+
(newIndex: number) => {
|
|
93
|
+
setCurrentTextIndex(newIndex);
|
|
94
|
+
if (onNext) onNext(newIndex);
|
|
95
|
+
},
|
|
96
|
+
[onNext]
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const next = useCallback(() => {
|
|
100
|
+
const nextIndex = currentTextIndex === texts.length - 1 ? (loop ? 0 : currentTextIndex) : currentTextIndex + 1;
|
|
101
|
+
if (nextIndex !== currentTextIndex) {
|
|
102
|
+
handleIndexChange(nextIndex);
|
|
103
|
+
}
|
|
104
|
+
}, [currentTextIndex, texts.length, loop, handleIndexChange]);
|
|
105
|
+
|
|
106
|
+
const previous = useCallback(() => {
|
|
107
|
+
const prevIndex = currentTextIndex === 0 ? (loop ? texts.length - 1 : currentTextIndex) : currentTextIndex - 1;
|
|
108
|
+
if (prevIndex !== currentTextIndex) {
|
|
109
|
+
handleIndexChange(prevIndex);
|
|
110
|
+
}
|
|
111
|
+
}, [currentTextIndex, texts.length, loop, handleIndexChange]);
|
|
112
|
+
|
|
113
|
+
const jumpTo = useCallback(
|
|
114
|
+
(index: number) => {
|
|
115
|
+
const validIndex = Math.max(0, Math.min(index, texts.length - 1));
|
|
116
|
+
if (validIndex !== currentTextIndex) {
|
|
117
|
+
handleIndexChange(validIndex);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
[texts.length, currentTextIndex, handleIndexChange]
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const reset = useCallback(() => {
|
|
124
|
+
if (currentTextIndex !== 0) {
|
|
125
|
+
handleIndexChange(0);
|
|
126
|
+
}
|
|
127
|
+
}, [currentTextIndex, handleIndexChange]);
|
|
128
|
+
|
|
129
|
+
useImperativeHandle(
|
|
130
|
+
ref,
|
|
131
|
+
() => ({
|
|
132
|
+
next,
|
|
133
|
+
previous,
|
|
134
|
+
jumpTo,
|
|
135
|
+
reset
|
|
136
|
+
}),
|
|
137
|
+
[next, previous, jumpTo, reset]
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (!auto) return;
|
|
142
|
+
const intervalId = setInterval(next, rotationInterval);
|
|
143
|
+
return () => clearInterval(intervalId);
|
|
144
|
+
}, [next, rotationInterval, auto]);
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<motion.span className={cn('text-rotate', mainClassName)} {...rest} layout transition={transition}>
|
|
148
|
+
<span className="text-rotate-sr-only">{texts[currentTextIndex]}</span>
|
|
149
|
+
<AnimatePresence mode={animatePresenceMode} initial={animatePresenceInitial}>
|
|
150
|
+
<motion.span
|
|
151
|
+
key={currentTextIndex}
|
|
152
|
+
className={cn(splitBy === 'lines' ? 'text-rotate-lines' : 'text-rotate')}
|
|
153
|
+
layout
|
|
154
|
+
aria-hidden="true"
|
|
155
|
+
>
|
|
156
|
+
{elements.map((wordObj: any, wordIndex: number, array: any[]) => {
|
|
157
|
+
const previousCharsCount = array.slice(0, wordIndex).reduce((sum, word) => sum + word.characters.length, 0);
|
|
158
|
+
return (
|
|
159
|
+
<span key={wordIndex} className={cn('text-rotate-word', splitLevelClassName)}>
|
|
160
|
+
{wordObj.characters.map((char: string, charIndex: number) => (
|
|
161
|
+
<motion.span
|
|
162
|
+
key={charIndex}
|
|
163
|
+
initial={initial}
|
|
164
|
+
animate={animate}
|
|
165
|
+
exit={exit}
|
|
166
|
+
transition={{
|
|
167
|
+
...transition,
|
|
168
|
+
delay: getStaggerDelay(
|
|
169
|
+
previousCharsCount + charIndex,
|
|
170
|
+
array.reduce((sum, word) => sum + word.characters.length, 0)
|
|
171
|
+
)
|
|
172
|
+
}}
|
|
173
|
+
className={cn('text-rotate-element', elementLevelClassName)}
|
|
174
|
+
>
|
|
175
|
+
{char}
|
|
176
|
+
</motion.span>
|
|
177
|
+
))}
|
|
178
|
+
{wordObj.needsSpace && <span className="text-rotate-space"> </span>}
|
|
179
|
+
</span>
|
|
180
|
+
);
|
|
181
|
+
})}
|
|
182
|
+
</motion.span>
|
|
183
|
+
</AnimatePresence>
|
|
184
|
+
</motion.span>
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
RotatingText.displayName = 'RotatingText';
|
|
189
|
+
export default RotatingText;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function BrandMark({ size = 28 }: { size?: number }) {
|
|
2
|
+
return (
|
|
3
|
+
<span
|
|
4
|
+
aria-hidden="true"
|
|
5
|
+
className="inline-block shrink-0 border border-[var(--color-border-strong)]"
|
|
6
|
+
style={{
|
|
7
|
+
width: size,
|
|
8
|
+
height: size,
|
|
9
|
+
borderRadius: "var(--radius-sm)",
|
|
10
|
+
background: "linear-gradient(135deg, var(--primary) 0 42%, transparent 42% 58%, var(--foreground) 58% 100%)",
|
|
11
|
+
}}
|
|
12
|
+
/>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ScrambleText } from "@/components/scramble-text"
|
|
2
|
+
import { ConsolePreview } from "@/components/console-preview"
|
|
3
|
+
|
|
4
|
+
interface ConsolePreviewSectionProps {
|
|
5
|
+
eyebrow?: string
|
|
6
|
+
heading: string
|
|
7
|
+
body?: string
|
|
8
|
+
/** Background flavor to alternate with neighbouring sections. */
|
|
9
|
+
dark?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ConsolePreviewSection({
|
|
13
|
+
eyebrow = "Live console",
|
|
14
|
+
heading,
|
|
15
|
+
body,
|
|
16
|
+
dark = false,
|
|
17
|
+
}: ConsolePreviewSectionProps) {
|
|
18
|
+
return (
|
|
19
|
+
<section
|
|
20
|
+
className={
|
|
21
|
+
dark
|
|
22
|
+
? "section-y border-b border-white/10 bg-[var(--deep-900)]"
|
|
23
|
+
: "section-y border-b border-[var(--color-border)] bg-[var(--gray-50)]"
|
|
24
|
+
}
|
|
25
|
+
>
|
|
26
|
+
<div className="container-main">
|
|
27
|
+
<div className="max-w-xl mb-10">
|
|
28
|
+
<ScrambleText
|
|
29
|
+
text={eyebrow}
|
|
30
|
+
className={dark ? "eyebrow mb-3 block text-[var(--primary-400)]" : "eyebrow mb-3 block"}
|
|
31
|
+
/>
|
|
32
|
+
<h2
|
|
33
|
+
className={
|
|
34
|
+
"font-bold leading-[1.12] tracking-tight " +
|
|
35
|
+
(dark ? "text-white" : "text-[var(--color-text-primary)]")
|
|
36
|
+
}
|
|
37
|
+
style={{ fontSize: "var(--type-h1)" }}
|
|
38
|
+
>
|
|
39
|
+
{heading}
|
|
40
|
+
</h2>
|
|
41
|
+
{body && (
|
|
42
|
+
<p
|
|
43
|
+
className={
|
|
44
|
+
"mt-4 text-[15px] leading-[1.7] " +
|
|
45
|
+
(dark ? "text-white/60" : "text-[var(--color-text-secondary)]")
|
|
46
|
+
}
|
|
47
|
+
>
|
|
48
|
+
{body}
|
|
49
|
+
</p>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<ConsolePreview className="w-full max-w-4xl" height={420} scale={0.62} />
|
|
54
|
+
</div>
|
|
55
|
+
</section>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { ArrowUpRight } from "lucide-react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
const CONSOLE_URL = process.env.NEXT_PUBLIC_CONSOLE_URL || ""
|
|
7
|
+
const CONSOLE_HOST = CONSOLE_URL ? CONSOLE_URL.replace(/^https?:\/\//, "").replace(/\/$/, "") : "console.your-company.example"
|
|
8
|
+
|
|
9
|
+
interface ConsolePreviewProps {
|
|
10
|
+
/** Outer wrapper classes (controls max-width, margin). */
|
|
11
|
+
className?: string
|
|
12
|
+
/** Height of the visible preview viewport in px. */
|
|
13
|
+
height?: number
|
|
14
|
+
/** Scale applied to the embedded 1280px-wide console. */
|
|
15
|
+
scale?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ConsolePreview({
|
|
19
|
+
className = "mx-auto mb-9 w-full max-w-2xl",
|
|
20
|
+
height = 240,
|
|
21
|
+
scale = 0.55,
|
|
22
|
+
}: ConsolePreviewProps) {
|
|
23
|
+
return (
|
|
24
|
+
<a
|
|
25
|
+
href={CONSOLE_URL || "#"}
|
|
26
|
+
target={CONSOLE_URL ? "_blank" : undefined}
|
|
27
|
+
rel="noopener noreferrer"
|
|
28
|
+
aria-disabled={!CONSOLE_URL}
|
|
29
|
+
onClick={(event) => { if (!CONSOLE_URL) event.preventDefault() }}
|
|
30
|
+
className={cn("group block glow-border rounded-xl no-underline", className)}
|
|
31
|
+
>
|
|
32
|
+
<div className="overflow-hidden rounded-xl border border-white/10 bg-[var(--gray-950)]/85 backdrop-blur-sm">
|
|
33
|
+
{/* Browser chrome */}
|
|
34
|
+
<div className="flex items-center gap-3 px-4 py-2.5 border-b border-white/10">
|
|
35
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
36
|
+
<span className="w-2.5 h-2.5 rounded-full bg-white/20" />
|
|
37
|
+
<span className="w-2.5 h-2.5 rounded-full bg-white/20" />
|
|
38
|
+
<span className="w-2.5 h-2.5 rounded-full bg-white/20" />
|
|
39
|
+
</div>
|
|
40
|
+
<div className="flex-1 flex items-center justify-center">
|
|
41
|
+
<span className="font-mono text-[11px] text-white/45 truncate">{CONSOLE_HOST}</span>
|
|
42
|
+
</div>
|
|
43
|
+
<span className="flex items-center gap-1 font-mono text-[10px] uppercase tracking-[0.1em] text-[var(--primary-400)] shrink-0 group-hover:text-[var(--primary-300)] transition-colors">
|
|
44
|
+
Open live console
|
|
45
|
+
<ArrowUpRight size={12} />
|
|
46
|
+
</span>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{/* Live preview */}
|
|
50
|
+
<div className="relative overflow-hidden bg-white" style={{ height }}>
|
|
51
|
+
{CONSOLE_URL ? (
|
|
52
|
+
<iframe
|
|
53
|
+
src={CONSOLE_URL}
|
|
54
|
+
title="Security console preview"
|
|
55
|
+
loading="lazy"
|
|
56
|
+
tabIndex={-1}
|
|
57
|
+
aria-hidden="true"
|
|
58
|
+
className="absolute left-0 top-0 origin-top-left pointer-events-none"
|
|
59
|
+
style={{ width: `${100 / scale}%`, height: `${height / scale}px`, transform: `scale(${scale})` }}
|
|
60
|
+
/>
|
|
61
|
+
) : (
|
|
62
|
+
<div className="grid h-full place-items-center bg-[var(--surface)] font-mono text-[12px] text-[var(--muted-foreground)]">
|
|
63
|
+
Configure NEXT_PUBLIC_CONSOLE_URL
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
{/* Click-capture overlay (so the iframe stays a preview) */}
|
|
67
|
+
<div className="absolute inset-0 bg-transparent group-hover:bg-black/[0.04] transition-colors" />
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</a>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { motion } from "framer-motion"
|
|
4
|
+
import { ReactNode } from "react"
|
|
5
|
+
|
|
6
|
+
interface FadeUpProps {
|
|
7
|
+
children: ReactNode
|
|
8
|
+
delay?: number
|
|
9
|
+
className?: string
|
|
10
|
+
as?: keyof typeof motion
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function FadeUp({ children, delay = 0, className, as = "div" }: FadeUpProps) {
|
|
14
|
+
const Tag = motion[as] as typeof motion.div
|
|
15
|
+
return (
|
|
16
|
+
<Tag
|
|
17
|
+
className={className}
|
|
18
|
+
initial={{ opacity: 0, y: 28 }}
|
|
19
|
+
whileInView={{ opacity: 1, y: 0 }}
|
|
20
|
+
viewport={{ once: true, margin: "-60px" }}
|
|
21
|
+
transition={{ duration: 0.55, ease: [0.22, 1, 0.36, 1], delay }}
|
|
22
|
+
>
|
|
23
|
+
{children}
|
|
24
|
+
</Tag>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function FadeUpGroup({ children, className }: { children: ReactNode; className?: string }) {
|
|
29
|
+
return (
|
|
30
|
+
<motion.div
|
|
31
|
+
className={className}
|
|
32
|
+
initial="hidden"
|
|
33
|
+
whileInView="visible"
|
|
34
|
+
viewport={{ once: true, margin: "-60px" }}
|
|
35
|
+
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.08 } } }}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
</motion.div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function FadeUpItem({ children, className }: { children: ReactNode; className?: string }) {
|
|
43
|
+
return (
|
|
44
|
+
<motion.div
|
|
45
|
+
className={className}
|
|
46
|
+
variants={{
|
|
47
|
+
hidden: { opacity: 0, y: 24 },
|
|
48
|
+
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] } },
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
{children}
|
|
52
|
+
</motion.div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Floating node network — dots drift continuously and connect with lines
|
|
7
|
+
* whenever they pass close to one another, the links fading in and out as
|
|
8
|
+
* the distance changes. A live "constellation" / neural-network motif.
|
|
9
|
+
* Canvas-based for smooth per-frame edge recalculation; respects
|
|
10
|
+
* reduced-motion and pauses while scrolled off-screen.
|
|
11
|
+
*/
|
|
12
|
+
export function FloatingNetwork({
|
|
13
|
+
count = 18,
|
|
14
|
+
variant = "light",
|
|
15
|
+
className = "",
|
|
16
|
+
}: {
|
|
17
|
+
count?: number
|
|
18
|
+
variant?: "light" | "dark"
|
|
19
|
+
className?: string
|
|
20
|
+
}) {
|
|
21
|
+
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const canvas = canvasRef.current
|
|
25
|
+
if (!canvas) return
|
|
26
|
+
const ctx = canvas.getContext("2d")
|
|
27
|
+
if (!ctx) return
|
|
28
|
+
|
|
29
|
+
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
30
|
+
const GOLD = variant === "dark" ? "208,160,85" : "184,147,58"
|
|
31
|
+
const LINK = 150 // px: max distance to draw a connection
|
|
32
|
+
|
|
33
|
+
let w = 0, h = 0, dpr = 1
|
|
34
|
+
let nodes: { x: number; y: number; vx: number; vy: number }[] = []
|
|
35
|
+
let raf = 0
|
|
36
|
+
|
|
37
|
+
const build = () => {
|
|
38
|
+
const rect = canvas.getBoundingClientRect()
|
|
39
|
+
w = rect.width; h = rect.height
|
|
40
|
+
dpr = Math.min(window.devicePixelRatio || 1, 2)
|
|
41
|
+
canvas.width = w * dpr; canvas.height = h * dpr
|
|
42
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
43
|
+
if (nodes.length) return // keep positions across resizes
|
|
44
|
+
nodes = Array.from({ length: count }, (_, i) => {
|
|
45
|
+
// deterministic-ish spread so it looks balanced on first paint
|
|
46
|
+
const gx = (i + 0.5) / count
|
|
47
|
+
return {
|
|
48
|
+
x: gx * (w || 800),
|
|
49
|
+
y: (((i * 37) % 100) / 100) * (h || 80),
|
|
50
|
+
vx: (((i * 13) % 7) / 7 - 0.5) * 0.34,
|
|
51
|
+
vy: (((i * 29) % 5) / 5 - 0.5) * 0.28,
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const draw = () => {
|
|
57
|
+
ctx.clearRect(0, 0, w, h)
|
|
58
|
+
|
|
59
|
+
// move + bounce off bounds
|
|
60
|
+
for (const n of nodes) {
|
|
61
|
+
n.x += n.vx; n.y += n.vy
|
|
62
|
+
if (n.x < 0 || n.x > w) n.vx *= -1
|
|
63
|
+
if (n.y < 0 || n.y > h) n.vy *= -1
|
|
64
|
+
n.x = Math.max(0, Math.min(w, n.x))
|
|
65
|
+
n.y = Math.max(0, Math.min(h, n.y))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// edges — fade by proximity
|
|
69
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
70
|
+
const a = nodes[i]
|
|
71
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
72
|
+
const b = nodes[j]
|
|
73
|
+
const dx = a.x - b.x, dy = a.y - b.y
|
|
74
|
+
const d = Math.hypot(dx, dy)
|
|
75
|
+
if (d < LINK) {
|
|
76
|
+
ctx.strokeStyle = `rgba(${GOLD},${(1 - d / LINK) * 0.45})`
|
|
77
|
+
ctx.lineWidth = 0.8
|
|
78
|
+
ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// nodes
|
|
84
|
+
for (const n of nodes) {
|
|
85
|
+
ctx.fillStyle = `rgba(${GOLD},0.9)`
|
|
86
|
+
ctx.beginPath(); ctx.arc(n.x, n.y, 2.2, 0, Math.PI * 2); ctx.fill()
|
|
87
|
+
// soft halo
|
|
88
|
+
ctx.fillStyle = `rgba(${GOLD},0.12)`
|
|
89
|
+
ctx.beginPath(); ctx.arc(n.x, n.y, 5, 0, Math.PI * 2); ctx.fill()
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const loop = () => { draw(); raf = requestAnimationFrame(loop) }
|
|
94
|
+
|
|
95
|
+
let visible = true
|
|
96
|
+
const start = () => { if (!reduce && visible && !raf) raf = requestAnimationFrame(loop) }
|
|
97
|
+
const stop = () => { if (raf) { cancelAnimationFrame(raf); raf = 0 } }
|
|
98
|
+
|
|
99
|
+
build()
|
|
100
|
+
draw()
|
|
101
|
+
|
|
102
|
+
const io = new IntersectionObserver(
|
|
103
|
+
([e]) => {
|
|
104
|
+
visible = e.isIntersecting
|
|
105
|
+
if (visible) start()
|
|
106
|
+
else stop()
|
|
107
|
+
},
|
|
108
|
+
{ threshold: 0 }
|
|
109
|
+
)
|
|
110
|
+
io.observe(canvas)
|
|
111
|
+
start()
|
|
112
|
+
|
|
113
|
+
const onResize = () => { build(); draw() }
|
|
114
|
+
window.addEventListener("resize", onResize)
|
|
115
|
+
return () => { stop(); io.disconnect(); window.removeEventListener("resize", onResize) }
|
|
116
|
+
}, [count, variant])
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<canvas
|
|
120
|
+
ref={canvasRef}
|
|
121
|
+
aria-hidden
|
|
122
|
+
className={`w-full h-full pointer-events-none ${className}`}
|
|
123
|
+
/>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import Link from "next/link"
|
|
2
|
+
import { BrandMark } from "@/components/brand-mark"
|
|
3
|
+
import { LatticeField } from "@/components/lattice-field"
|
|
4
|
+
|
|
5
|
+
const cols = [
|
|
6
|
+
{
|
|
7
|
+
heading: "Services",
|
|
8
|
+
links: [
|
|
9
|
+
{ href: "/assessment", label: "PQC Assessment" },
|
|
10
|
+
{ href: "/products", label: "Migration Products" },
|
|
11
|
+
{ href: "/deployment", label: "Engineer Deployment" },
|
|
12
|
+
{ href: "/#pilot", label: "90-Day Pilot" },
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
heading: "Company",
|
|
17
|
+
links: [
|
|
18
|
+
{ href: "/#clients", label: "Who We Serve" },
|
|
19
|
+
{ href: "/#offer", label: "How It Works" },
|
|
20
|
+
{ href: "/#moat", label: "Why continuous verification" },
|
|
21
|
+
{ href: "/#contact", label: "Contact" },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
heading: "Compliance",
|
|
26
|
+
links: [
|
|
27
|
+
{ href: "#", label: "NIST PQC Standards" },
|
|
28
|
+
{ href: "#", label: "RBI Framework" },
|
|
29
|
+
{ href: "#", label: "CERT-In Guidelines" },
|
|
30
|
+
{ href: "#", label: "US Federal Mandates" },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
export function Footer() {
|
|
36
|
+
return (
|
|
37
|
+
<footer className="relative overflow-hidden bg-[var(--gray-950)] text-white">
|
|
38
|
+
<LatticeField className="opacity-25" />
|
|
39
|
+
<div className="container-main relative z-10 py-16">
|
|
40
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-10">
|
|
41
|
+
|
|
42
|
+
{/* Brand */}
|
|
43
|
+
<div className="sm:col-span-2 md:col-span-1">
|
|
44
|
+
<Link href="/" className="inline-flex items-center gap-2.5 mb-5">
|
|
45
|
+
<BrandMark />
|
|
46
|
+
<span className="text-[15px] font-semibold">Your Company</span>
|
|
47
|
+
</Link>
|
|
48
|
+
<p className="font-mono text-[11px] tracking-[0.08em] text-white/50 leading-relaxed">
|
|
49
|
+
PQC Compliance,<br />Delivered End-to-End
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{cols.map((col) => (
|
|
54
|
+
<div key={col.heading}>
|
|
55
|
+
<h4 className="text-[11px] font-semibold tracking-[0.14em] uppercase text-[var(--primary-400)] mb-5">
|
|
56
|
+
{col.heading}
|
|
57
|
+
</h4>
|
|
58
|
+
<ul className="space-y-3 list-none p-0 m-0">
|
|
59
|
+
{col.links.map((l) => (
|
|
60
|
+
<li key={l.label}>
|
|
61
|
+
<Link
|
|
62
|
+
href={l.href}
|
|
63
|
+
className="text-[13px] text-white/45 hover:text-white transition-colors"
|
|
64
|
+
>
|
|
65
|
+
{l.label}
|
|
66
|
+
</Link>
|
|
67
|
+
</li>
|
|
68
|
+
))}
|
|
69
|
+
</ul>
|
|
70
|
+
</div>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div className="h-px bg-white/10 my-10" />
|
|
75
|
+
|
|
76
|
+
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 text-[12px] text-white/50">
|
|
77
|
+
<span>© 2025 Your Company Private Limited. All rights reserved.</span>
|
|
78
|
+
<span>APAC · Middle East · United States · EU</span>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</footer>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { motion } from "framer-motion"
|
|
4
|
+
import { Lock, Unlock } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* "Harvest now, decrypt later" threat visual.
|
|
8
|
+
* Encrypted data captured today flows into adversary storage, then is
|
|
9
|
+
* retroactively broken once a cryptographically-relevant quantum computer
|
|
10
|
+
* arrives ("Q-Day"). Compact, looping, and driven by Ceramic brand tokens.
|
|
11
|
+
*/
|
|
12
|
+
export function HarvestTimeline() {
|
|
13
|
+
return (
|
|
14
|
+
<div className="relative rounded-2xl border border-white/10 bg-[var(--deep-950)]/60 p-5 overflow-hidden">
|
|
15
|
+
{/* axis labels */}
|
|
16
|
+
<div className="flex items-center justify-between mb-2 font-mono text-[10px] tracking-[0.14em] uppercase whitespace-nowrap">
|
|
17
|
+
<span className="text-[var(--deep-300)]">Today</span>
|
|
18
|
+
<span className="text-[var(--primary-400)]">Q-Day</span>
|
|
19
|
+
</div>
|
|
20
|
+
<div className="text-center mb-3 font-mono text-[9px] tracking-[0.16em] uppercase text-white/30">
|
|
21
|
+
intercept & store
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
{/* track */}
|
|
25
|
+
<div className="relative h-12">
|
|
26
|
+
{/* baseline */}
|
|
27
|
+
<div className="absolute top-1/2 left-0 right-0 h-px -translate-y-1/2 bg-gradient-to-r from-[var(--deep-400)]/60 via-white/15 to-[var(--primary-400)]/60" />
|
|
28
|
+
|
|
29
|
+
{/* left node — locked today */}
|
|
30
|
+
<div className="absolute top-1/2 left-0 -translate-y-1/2 flex items-center justify-center w-9 h-9 rounded-full bg-[var(--deep-800)] border border-[var(--deep-400)]/50 z-10">
|
|
31
|
+
<Lock size={14} className="text-[var(--deep-300)]" strokeWidth={2.2} />
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{/* right node — broken at Q-Day */}
|
|
35
|
+
<div className="absolute top-1/2 right-0 -translate-y-1/2 flex items-center justify-center w-9 h-9 rounded-full bg-[var(--primary-500)]/15 border border-[var(--primary-400)]/60 z-10">
|
|
36
|
+
<motion.div
|
|
37
|
+
animate={{ rotate: [0, 0, -12, 0], scale: [1, 1, 1.15, 1] }}
|
|
38
|
+
transition={{ duration: 3.2, repeat: Infinity, times: [0, 0.55, 0.7, 1], ease: "easeInOut" }}
|
|
39
|
+
>
|
|
40
|
+
<Unlock size={14} className="text-[var(--primary-400)]" strokeWidth={2.2} />
|
|
41
|
+
</motion.div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{/* travelling encrypted packets */}
|
|
45
|
+
{[0, 1, 2].map((i) => (
|
|
46
|
+
<motion.div
|
|
47
|
+
key={i}
|
|
48
|
+
className="absolute top-1/2 w-2 h-2 rounded-[3px] bg-[var(--primary-400)] shadow-[0_0_8px_var(--primary-500)]"
|
|
49
|
+
style={{ marginTop: -4 }}
|
|
50
|
+
initial={{ left: "10%", opacity: 0 }}
|
|
51
|
+
animate={{ left: ["10%", "88%"], opacity: [0, 1, 1, 0] }}
|
|
52
|
+
transition={{
|
|
53
|
+
duration: 3.2,
|
|
54
|
+
repeat: Infinity,
|
|
55
|
+
delay: i * 1.05,
|
|
56
|
+
ease: "linear",
|
|
57
|
+
times: [0, 0.08, 0.92, 1],
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<p className="mt-4 text-[12px] leading-[1.6] text-white/55">
|
|
64
|
+
Encrypted traffic captured <span className="text-[var(--deep-300)] font-medium">now</span> is
|
|
65
|
+
stored and broken <span className="text-[var(--primary-400)] font-medium">later</span> — your
|
|
66
|
+
long-lived secrets are already exposed.
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|