@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.
Files changed (123) hide show
  1. package/README.md +28 -14
  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,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 &amp; 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
+ }