@launchui/launch-ui 1.0.0 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/registry/components/3d-card/3d-card-2.tsx +258 -0
- package/registry/components/3d-card/{index.tsx → 3d-card.tsx} +33 -12
- package/registry/components/3d-pin-card/3d-pin-card.tsx +172 -0
- package/registry/components/Keyboard/Keyboard.tsx +885 -0
- package/registry/components/comet-card/comet-card.tsx +122 -0
- package/registry/components/macbook-scroll/macbook-scroll.tsx +611 -0
- package/registry/components/text-flipping-board/text-flipping-board.tsx +449 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useRef, useState, useMemo } from "react";
|
|
4
|
+
import { motion } from "motion/react";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal utility replacing external lib requirements ensuring zero-friction installations.
|
|
8
|
+
*/
|
|
9
|
+
const cn = (...classes: any[]) => {
|
|
10
|
+
return classes.filter(Boolean).join(" ");
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const FLAP_CHARS = " ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$()-+&=;:'\"%,./?°";
|
|
14
|
+
|
|
15
|
+
const BOARD_ROWS = 6;
|
|
16
|
+
const BOARD_COLS = 22;
|
|
17
|
+
|
|
18
|
+
const BASE_COL_DELAY = 30;
|
|
19
|
+
const BASE_ROW_DELAY = 20;
|
|
20
|
+
const BASE_STEP_MS = 55;
|
|
21
|
+
const BASE_FLIP_S = 0.35;
|
|
22
|
+
const BASE_TOTAL_S =
|
|
23
|
+
((BOARD_COLS - 1) * BASE_COL_DELAY +
|
|
24
|
+
(BOARD_ROWS - 1) * BASE_ROW_DELAY +
|
|
25
|
+
8 * BASE_STEP_MS) /
|
|
26
|
+
1000;
|
|
27
|
+
|
|
28
|
+
type AccentColor = {
|
|
29
|
+
top: string;
|
|
30
|
+
bottom: string;
|
|
31
|
+
text: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// CUSTOMIZED PALETTE: Elevated vivid color spectrum slightly shifted from the original.
|
|
35
|
+
const ACCENT_COLORS: AccentColor[] = [
|
|
36
|
+
{ top: "bg-rose-600", bottom: "bg-rose-700", text: "text-white" },
|
|
37
|
+
{ top: "bg-amber-500", bottom: "bg-amber-600", text: "text-white" },
|
|
38
|
+
{ top: "bg-yellow-300", bottom: "bg-yellow-400", text: "text-black" },
|
|
39
|
+
{ top: "bg-emerald-600", bottom: "bg-emerald-700", text: "text-white" },
|
|
40
|
+
{ top: "bg-cyan-600", bottom: "bg-cyan-700", text: "text-white" },
|
|
41
|
+
{ top: "bg-indigo-600", bottom: "bg-indigo-700", text: "text-white" },
|
|
42
|
+
{ top: "bg-slate-50", bottom: "bg-slate-200", text: "text-slate-900" },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const CELL_TEXT_STYLE: React.CSSProperties = {
|
|
46
|
+
fontSize: "clamp(6px, 2vw, 22px)",
|
|
47
|
+
lineHeight: 1,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ── Individual Split-Flap Character ───────────────────────────────────
|
|
51
|
+
|
|
52
|
+
const FlapCell = React.memo(function FlapCell({
|
|
53
|
+
target,
|
|
54
|
+
delay,
|
|
55
|
+
stepMs,
|
|
56
|
+
flipDuration,
|
|
57
|
+
}: {
|
|
58
|
+
target: string;
|
|
59
|
+
delay: number;
|
|
60
|
+
stepMs: number;
|
|
61
|
+
flipDuration: number;
|
|
62
|
+
}) {
|
|
63
|
+
const [current, setCurrent] = useState(" ");
|
|
64
|
+
const [prev, setPrev] = useState(" ");
|
|
65
|
+
const [flipId, setFlipId] = useState(0);
|
|
66
|
+
const [accent, setAccent] = useState<AccentColor | null>(null);
|
|
67
|
+
const [prevAccent, setPrevAccent] = useState<AccentColor | null>(null);
|
|
68
|
+
const curRef = useRef(" ");
|
|
69
|
+
const tgtRef = useRef<string | null>(null);
|
|
70
|
+
const accentRef = useRef<AccentColor | null>(null);
|
|
71
|
+
const startTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
72
|
+
const stepTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (startTimer.current) clearTimeout(startTimer.current);
|
|
76
|
+
if (stepTimer.current) clearTimeout(stepTimer.current);
|
|
77
|
+
startTimer.current = null;
|
|
78
|
+
stepTimer.current = null;
|
|
79
|
+
|
|
80
|
+
const normalized = FLAP_CHARS.includes(target.toUpperCase())
|
|
81
|
+
? target.toUpperCase()
|
|
82
|
+
: " ";
|
|
83
|
+
if (normalized === tgtRef.current) return;
|
|
84
|
+
tgtRef.current = normalized;
|
|
85
|
+
|
|
86
|
+
if (normalized === " " && curRef.current === " ") return;
|
|
87
|
+
|
|
88
|
+
const scrambleCount =
|
|
89
|
+
normalized === " "
|
|
90
|
+
? 8 + Math.floor(Math.random() * 8)
|
|
91
|
+
: 25 + Math.floor(Math.random() * 15);
|
|
92
|
+
|
|
93
|
+
const runStep = (i: number) => {
|
|
94
|
+
const isLast = i === scrambleCount;
|
|
95
|
+
const ch = isLast
|
|
96
|
+
? normalized
|
|
97
|
+
: FLAP_CHARS[1 + Math.floor(Math.random() * (FLAP_CHARS.length - 1))];
|
|
98
|
+
|
|
99
|
+
const newAccent = isLast
|
|
100
|
+
? null
|
|
101
|
+
: Math.random() < 0.2
|
|
102
|
+
? ACCENT_COLORS[Math.floor(Math.random() * ACCENT_COLORS.length)]
|
|
103
|
+
: null;
|
|
104
|
+
|
|
105
|
+
setPrev(curRef.current);
|
|
106
|
+
setPrevAccent(accentRef.current);
|
|
107
|
+
curRef.current = ch;
|
|
108
|
+
accentRef.current = newAccent;
|
|
109
|
+
setCurrent(ch);
|
|
110
|
+
setAccent(newAccent);
|
|
111
|
+
setFlipId((n) => n + 1);
|
|
112
|
+
|
|
113
|
+
if (!isLast) {
|
|
114
|
+
stepTimer.current = setTimeout(() => runStep(i + 1), stepMs);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
startTimer.current = setTimeout(() => runStep(1), delay);
|
|
119
|
+
|
|
120
|
+
return () => {
|
|
121
|
+
if (startTimer.current) clearTimeout(startTimer.current);
|
|
122
|
+
if (stepTimer.current) clearTimeout(stepTimer.current);
|
|
123
|
+
startTimer.current = null;
|
|
124
|
+
stepTimer.current = null;
|
|
125
|
+
tgtRef.current = null;
|
|
126
|
+
};
|
|
127
|
+
}, [target, delay, stepMs]);
|
|
128
|
+
|
|
129
|
+
const show = current === " " ? "\u00A0" : current;
|
|
130
|
+
const showPrev = prev === " " ? "\u00A0" : prev;
|
|
131
|
+
|
|
132
|
+
const textCx =
|
|
133
|
+
"absolute inset-x-0 flex select-none items-center justify-center font-mono font-bold tracking-wide";
|
|
134
|
+
|
|
135
|
+
// MODIFIED: Swapped neutral scaling for rich slate/black high-contrast mapping
|
|
136
|
+
const topBg = accent?.top ?? "bg-slate-100 dark:bg-black";
|
|
137
|
+
const bottomBg = accent?.bottom ?? "bg-slate-200 dark:bg-zinc-900";
|
|
138
|
+
const textColor = accent?.text ?? "text-slate-800 dark:text-zinc-100";
|
|
139
|
+
|
|
140
|
+
const flapTopBg = prevAccent?.top ?? "bg-slate-50 dark:bg-zinc-800";
|
|
141
|
+
const flapTextColor = prevAccent?.text ?? "text-slate-800 dark:text-zinc-100";
|
|
142
|
+
|
|
143
|
+
const bottomDelay = flipDuration * 0.5;
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className="flex aspect-3/6 flex-col overflow-hidden rounded-[2px] border border-slate-300 md:rounded-[3px] md:border-2 dark:border-black">
|
|
147
|
+
{/* Flap content area */}
|
|
148
|
+
<div className="relative flex-1 [perspective:800px] [transform-style:preserve-3d]">
|
|
149
|
+
<div className="absolute inset-0 z-40 hidden flex-row items-center justify-center md:flex">
|
|
150
|
+
<div className="h-1/2 w-px rounded-tr-sm rounded-br-sm bg-slate-300 dark:bg-black" />
|
|
151
|
+
<div className="flex h-px flex-1 bg-slate-300 dark:bg-black" />
|
|
152
|
+
<div className="h-1/2 w-px rounded-tl-sm rounded-bl-sm bg-slate-300 dark:bg-black" />
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Static top – new character top half */}
|
|
156
|
+
<div
|
|
157
|
+
className={cn(
|
|
158
|
+
"absolute inset-x-0 top-0 h-[calc(50%-0.5px)] overflow-hidden rounded-t-[3px]",
|
|
159
|
+
topBg,
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
<div
|
|
163
|
+
className={cn(textCx, textColor, "top-0 h-[200%]")}
|
|
164
|
+
style={CELL_TEXT_STYLE}
|
|
165
|
+
>
|
|
166
|
+
{show}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{/* Static bottom – new character bottom half */}
|
|
171
|
+
<div
|
|
172
|
+
className={cn(
|
|
173
|
+
"absolute inset-x-0 bottom-0 h-[calc(50%-0.5px)] overflow-hidden rounded-b-[3px]",
|
|
174
|
+
bottomBg,
|
|
175
|
+
)}
|
|
176
|
+
>
|
|
177
|
+
<div
|
|
178
|
+
className={cn(textCx, textColor, "bottom-0 h-[200%]")}
|
|
179
|
+
style={CELL_TEXT_STYLE}
|
|
180
|
+
>
|
|
181
|
+
{show}
|
|
182
|
+
</div>
|
|
183
|
+
{flipId > 0 && (
|
|
184
|
+
<motion.div
|
|
185
|
+
key={`s${flipId}`}
|
|
186
|
+
className="pointer-events-none absolute inset-0 bg-[linear-gradient(to_bottom,rgba(255,255,255,0.8),transparent_60%)] dark:bg-[linear-gradient(to_bottom,rgba(0,0,0,0.8),transparent_60%)]"
|
|
187
|
+
initial={{ opacity: 0.5 }}
|
|
188
|
+
animate={{ opacity: 0 }}
|
|
189
|
+
transition={{ duration: flipDuration * 1.3, ease: "easeOut" }}
|
|
190
|
+
/>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{/* Flipping top flap – old character top half, drops down */}
|
|
195
|
+
{flipId > 0 && (
|
|
196
|
+
<motion.div
|
|
197
|
+
key={flipId}
|
|
198
|
+
className={cn(
|
|
199
|
+
"absolute inset-x-0 top-0 z-10 h-[calc(50%-0.5px)] origin-bottom overflow-hidden rounded-t-[3px] [backface-visibility:hidden] [transform-style:preserve-3d]",
|
|
200
|
+
flapTopBg,
|
|
201
|
+
)}
|
|
202
|
+
initial={{ rotateX: 0 }}
|
|
203
|
+
animate={{ rotateX: -100 }}
|
|
204
|
+
transition={{
|
|
205
|
+
duration: flipDuration,
|
|
206
|
+
ease: [0.55, 0.055, 0.675, 0.19],
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
<div
|
|
210
|
+
className={cn(textCx, flapTextColor, "top-0 h-[200%]")}
|
|
211
|
+
style={CELL_TEXT_STYLE}
|
|
212
|
+
>
|
|
213
|
+
{showPrev}
|
|
214
|
+
</div>
|
|
215
|
+
<motion.div
|
|
216
|
+
className="pointer-events-none absolute inset-0 bg-[linear-gradient(to_bottom,rgba(255,255,255,0),rgba(255,255,255,1))] dark:bg-[linear-gradient(to_bottom,rgba(0,0,0,0),rgba(0,0,0,1))]"
|
|
217
|
+
initial={{ opacity: 0 }}
|
|
218
|
+
animate={{ opacity: 0.6 }}
|
|
219
|
+
transition={{ duration: flipDuration }}
|
|
220
|
+
/>
|
|
221
|
+
</motion.div>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{/* Flipping bottom flap – new character bottom half, rises up */}
|
|
225
|
+
{flipId > 0 && (
|
|
226
|
+
<motion.div
|
|
227
|
+
key={`b${flipId}`}
|
|
228
|
+
className={cn(
|
|
229
|
+
"absolute inset-x-0 bottom-0 z-10 h-[calc(50%-0.5px)] origin-top overflow-hidden rounded-b-[3px] [backface-visibility:hidden] [transform-style:preserve-3d]",
|
|
230
|
+
bottomBg,
|
|
231
|
+
)}
|
|
232
|
+
initial={{ rotateX: 90 }}
|
|
233
|
+
animate={{ rotateX: 0 }}
|
|
234
|
+
transition={{
|
|
235
|
+
duration: flipDuration * 0.85,
|
|
236
|
+
delay: bottomDelay,
|
|
237
|
+
ease: [0.33, 1.55, 0.64, 1],
|
|
238
|
+
}}
|
|
239
|
+
>
|
|
240
|
+
<div
|
|
241
|
+
className={cn(textCx, textColor, "bottom-0 h-[200%]")}
|
|
242
|
+
style={CELL_TEXT_STYLE}
|
|
243
|
+
>
|
|
244
|
+
{show}
|
|
245
|
+
</div>
|
|
246
|
+
<motion.div
|
|
247
|
+
className="pointer-events-none absolute inset-0 bg-[linear-gradient(to_top,rgba(255,255,255,0),rgba(255,255,255,0.6))] dark:bg-[linear-gradient(to_top,rgba(0,0,0,0),rgba(0,0,0,0.6))]"
|
|
248
|
+
initial={{ opacity: 0.4 }}
|
|
249
|
+
animate={{ opacity: 0 }}
|
|
250
|
+
transition={{
|
|
251
|
+
duration: flipDuration * 0.85,
|
|
252
|
+
delay: bottomDelay,
|
|
253
|
+
}}
|
|
254
|
+
/>
|
|
255
|
+
</motion.div>
|
|
256
|
+
)}
|
|
257
|
+
|
|
258
|
+
{/* Split line */}
|
|
259
|
+
<div className="pointer-events-none absolute inset-x-0 top-1/2 z-20 h-px -translate-y-[0.5px] bg-slate-400/50 dark:bg-black/50" />
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
{/* Bottom stripes – decorative, outside the flap area */}
|
|
263
|
+
<div className="h-2 w-full bg-[repeating-linear-gradient(to_bottom,currentColor_0,currentColor_1px,transparent_1px,transparent_0.15rem)] text-slate-400 opacity-20 md:h-4 md:bg-[repeating-linear-gradient(to_bottom,currentColor_0,currentColor_1px,transparent_1px,transparent_0.2rem)] dark:text-black dark:opacity-100" />
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
},
|
|
267
|
+
(prevProps, nextProps) =>
|
|
268
|
+
prevProps.target === nextProps.target &&
|
|
269
|
+
prevProps.delay === nextProps.delay &&
|
|
270
|
+
prevProps.stepMs === nextProps.stepMs &&
|
|
271
|
+
prevProps.flipDuration === nextProps.flipDuration,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// ── Color Tile ────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
const COLOR_MAP: Record<string, string> = {
|
|
277
|
+
"{R}": "#E11D48", // rose 600
|
|
278
|
+
"{O}": "#D97706", // amber 600
|
|
279
|
+
"{Y}": "#FBBF24", // yellow 400
|
|
280
|
+
"{G}": "#059669", // emerald 600
|
|
281
|
+
"{B}": "#0891B2", // cyan 600
|
|
282
|
+
"{V}": "#4F46E5", // indigo 600
|
|
283
|
+
"{W}": "#F8FAFC", // slate 50
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const ColorCell = React.memo(function ColorCell({ color }: { color: string }) {
|
|
287
|
+
return (
|
|
288
|
+
<div
|
|
289
|
+
className="aspect-3/5 rounded-[3px] border-2 border-slate-300 dark:border-black"
|
|
290
|
+
style={{ backgroundColor: color }}
|
|
291
|
+
/>
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ── Row Parser ────────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
type ParsedCell =
|
|
298
|
+
| { type: "char"; value: string }
|
|
299
|
+
| { type: "color"; hex: string };
|
|
300
|
+
|
|
301
|
+
function parseRow(row: string): ParsedCell[] {
|
|
302
|
+
const cells: ParsedCell[] = [];
|
|
303
|
+
let i = 0;
|
|
304
|
+
while (i < row.length) {
|
|
305
|
+
if (row[i] === "{" && i + 2 < row.length && row[i + 2] === "}") {
|
|
306
|
+
const code = row.substring(i, i + 3);
|
|
307
|
+
if (COLOR_MAP[code]) {
|
|
308
|
+
cells.push({ type: "color", hex: COLOR_MAP[code] });
|
|
309
|
+
i += 3;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
cells.push({ type: "char", value: row[i] });
|
|
314
|
+
i++;
|
|
315
|
+
}
|
|
316
|
+
return cells;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── Word Wrap ─────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
function wrapParagraph(paragraph: string, maxCols: number): string[] {
|
|
322
|
+
const lines: string[] = [];
|
|
323
|
+
const words = paragraph.split(/[ \t]+/).filter(Boolean);
|
|
324
|
+
let currentLine = "";
|
|
325
|
+
|
|
326
|
+
for (const word of words) {
|
|
327
|
+
if (word.length > maxCols) {
|
|
328
|
+
if (currentLine) {
|
|
329
|
+
lines.push(currentLine);
|
|
330
|
+
currentLine = "";
|
|
331
|
+
}
|
|
332
|
+
lines.push(word.slice(0, maxCols));
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!currentLine) {
|
|
337
|
+
currentLine = word;
|
|
338
|
+
} else if (currentLine.length + 1 + word.length <= maxCols) {
|
|
339
|
+
currentLine += " " + word;
|
|
340
|
+
} else {
|
|
341
|
+
lines.push(currentLine);
|
|
342
|
+
currentLine = word;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (currentLine) lines.push(currentLine);
|
|
347
|
+
return lines;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function wrapText(input: string, maxCols: number): string[] {
|
|
351
|
+
return input
|
|
352
|
+
.split("\n")
|
|
353
|
+
.flatMap((paragraph) =>
|
|
354
|
+
paragraph.trim() === "" ? [""] : wrapParagraph(paragraph, maxCols),
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ── Main TextFlippingBoard Component ──────────────────────────────────
|
|
359
|
+
|
|
360
|
+
export interface TextFlippingBoardProps {
|
|
361
|
+
rows?: string[];
|
|
362
|
+
text?: string;
|
|
363
|
+
className?: string;
|
|
364
|
+
/** Total animation duration in seconds. Defaults to ~1.2s. */
|
|
365
|
+
duration?: number;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function TextFlippingBoard({
|
|
369
|
+
rows,
|
|
370
|
+
text,
|
|
371
|
+
className,
|
|
372
|
+
duration = BASE_TOTAL_S,
|
|
373
|
+
}: TextFlippingBoardProps) {
|
|
374
|
+
const scale = duration / BASE_TOTAL_S;
|
|
375
|
+
const colDelay = BASE_COL_DELAY * scale;
|
|
376
|
+
const rowDelay = BASE_ROW_DELAY * scale;
|
|
377
|
+
const stepMs = BASE_STEP_MS * scale;
|
|
378
|
+
const flipDur = Math.min(0.6, Math.max(0.15, BASE_FLIP_S * scale));
|
|
379
|
+
|
|
380
|
+
const board = useMemo(() => {
|
|
381
|
+
const grid: ParsedCell[][] = Array.from({ length: BOARD_ROWS }, () =>
|
|
382
|
+
Array.from({ length: BOARD_COLS }, () => ({
|
|
383
|
+
type: "char" as const,
|
|
384
|
+
value: " ",
|
|
385
|
+
})),
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
if (text) {
|
|
389
|
+
const lines = wrapText(text, BOARD_COLS).slice(0, BOARD_ROWS);
|
|
390
|
+
const startRow = Math.max(0, Math.floor((BOARD_ROWS - lines.length) / 2));
|
|
391
|
+
lines.forEach((line, i) => {
|
|
392
|
+
const row = startRow + i;
|
|
393
|
+
if (row >= BOARD_ROWS) return;
|
|
394
|
+
const parsed = parseRow(line);
|
|
395
|
+
const startCol = Math.max(
|
|
396
|
+
0,
|
|
397
|
+
Math.floor((BOARD_COLS - parsed.length) / 2),
|
|
398
|
+
);
|
|
399
|
+
parsed.forEach((cell, c) => {
|
|
400
|
+
if (startCol + c < BOARD_COLS) {
|
|
401
|
+
grid[row][startCol + c] = cell;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
} else if (rows) {
|
|
406
|
+
rows.forEach((row, r) => {
|
|
407
|
+
if (r >= BOARD_ROWS) return;
|
|
408
|
+
const parsed = parseRow(row);
|
|
409
|
+
parsed.forEach((cell, c) => {
|
|
410
|
+
if (c < BOARD_COLS) {
|
|
411
|
+
grid[r][c] = cell;
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return grid;
|
|
418
|
+
}, [rows, text]);
|
|
419
|
+
|
|
420
|
+
return (
|
|
421
|
+
<div
|
|
422
|
+
className={cn(
|
|
423
|
+
"relative mx-auto w-full max-w-3xl rounded-xl bg-slate-100 p-2 shadow-xl md:rounded-2xl md:p-4 dark:bg-zinc-950 dark:shadow-[0_20px_70px_-15px_rgba(0,0,0,0.6)]",
|
|
424
|
+
className,
|
|
425
|
+
)}
|
|
426
|
+
>
|
|
427
|
+
<div
|
|
428
|
+
className="grid gap-px md:gap-[3px]"
|
|
429
|
+
style={{ gridTemplateColumns: `repeat(${BOARD_COLS}, 1fr)` }}
|
|
430
|
+
>
|
|
431
|
+
{board.map((row, r) =>
|
|
432
|
+
row.map((cell, c) =>
|
|
433
|
+
cell.type === "color" ? (
|
|
434
|
+
<ColorCell key={`${r}-${c}`} color={cell.hex} />
|
|
435
|
+
) : (
|
|
436
|
+
<FlapCell
|
|
437
|
+
key={`${r}-${c}`}
|
|
438
|
+
target={cell.value}
|
|
439
|
+
delay={c * colDelay + r * rowDelay}
|
|
440
|
+
stepMs={stepMs}
|
|
441
|
+
flipDuration={flipDur}
|
|
442
|
+
/>
|
|
443
|
+
),
|
|
444
|
+
),
|
|
445
|
+
)}
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
);
|
|
449
|
+
}
|