apex-ui-kit 2.1.0 → 2.1.2

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.
@@ -63,7 +63,7 @@ export default function CardStack({
63
63
  return (
64
64
  <div
65
65
  ref={containerRef}
66
- className="relative flex justify-center items-end h-[440px] w-full max-w-lg mx-auto pb-2"
66
+ className="relative flex justify-center items-end h-[27.5rem] w-full max-w-lg mx-auto pb-2"
67
67
  onMouseEnter={() => setIsHovered(true)}
68
68
  onMouseLeave={() => setIsHovered(false)}
69
69
  >
@@ -78,7 +78,7 @@ export default function CardStack({
78
78
  <div
79
79
  key={idx}
80
80
  onClick={isTop ? next : undefined}
81
- className={`card-item group absolute bottom-2 left-0 right-0 mx-auto h-[360px] w-[280px] max-w-[calc(100%-1rem)] origin-bottom overflow-hidden rounded-[1.75rem] border border-white/15 bg-slate-950 shadow-[0_24px_60px_-20px_rgba(0,0,0,0.8)] select-none ${isTop ? "cursor-pointer" : "pointer-events-none"}`}
81
+ className={`card-item group absolute bottom-2 left-0 right-0 mx-auto h-[22.5rem] w-[17.5rem] max-w-[calc(100%-1rem)] origin-bottom overflow-hidden rounded-[1.75rem] border border-white/15 bg-slate-950 shadow-[0_24px_60px_-20px_rgba(0,0,0,0.8)] select-none ${isTop ? "cursor-pointer" : "pointer-events-none"}`}
82
82
  style={{ opacity: 0 }}
83
83
  >
84
84
  {/* Full-bleed image */}
@@ -9,10 +9,10 @@ const ProfileCard = ({ title, summary, image, link }) => {
9
9
  initial={{ y: 20, opacity: 0 }}
10
10
  animate={{ y: 0, opacity: 1 }}
11
11
  transition={{ type: "spring", stiffness: 300, damping: 20 }}
12
- className="w-[280px] max-w-full rounded-2xl bg-slate-950 border border-gray-800 shadow-2xl overflow-hidden p-2 shadow-lime-700/20"
12
+ className="w-[17.5rem] max-w-full rounded-2xl bg-slate-950 border border-gray-800 shadow-2xl overflow-hidden p-2 shadow-lime-700/20"
13
13
  >
14
- {/* Image: fixed px size so the card keeps its shape on every screen */}
15
- <div className="h-[200px] w-full rounded-b-4xl rounded-t-lg overflow-hidden">
14
+ {/* Image: fixed size (in rem, like the text) so the card keeps its shape on every screen */}
15
+ <div className="h-[12.5rem] w-full rounded-b-4xl rounded-t-lg overflow-hidden">
16
16
  <motion.img
17
17
  src={image}
18
18
  alt={title}
@@ -69,7 +69,8 @@ const FlipProfile = ({
69
69
  return (
70
70
  <div className="w-full flex items-center justify-center p-4" style={{ perspective: "1200px" }}>
71
71
  <motion.div
72
- className="relative w-[300px] max-w-full h-[420px] cursor-pointer"
72
+ // Sized in rem (300×420 at a 16px root) so the card grows with its rem-based content.
73
+ className="relative w-[18.75rem] max-w-full h-[26.25rem] cursor-pointer"
73
74
  style={{ transformStyle: "preserve-3d" }}
74
75
  variants={cardVariants}
75
76
  animate={isFlipped ? "flipped" : "initial"}
@@ -42,7 +42,7 @@ const MeteorCard = ({ data, particleNumber, particleColor, particleSpeed }) => {
42
42
  return (
43
43
  <div className="flex flex-wrap justify-center gap-6">
44
44
  {data.map((item, index) => (
45
- <div key={index} className="relative w-[320px] max-w-full bg-slate-800/80 p-0.5 rounded-2xl moving-border-card">
45
+ <div key={index} className="relative w-[20rem] max-w-full bg-slate-800/80 p-0.5 rounded-2xl moving-border-card">
46
46
  {/* Card ka content container */}
47
47
  <div className="relative bg-slate-900 rounded-[calc(1rem-2px)] p-3 flex flex-col overflow-hidden">
48
48
  {/* Background Effects */}
@@ -59,7 +59,7 @@ const MeteorCard = ({ data, particleNumber, particleColor, particleSpeed }) => {
59
59
  <div className="absolute -top-1/4 -left-1/4 w-3/4 h-1/2 bg-blue-400/60 rounded-full blur-[90px] pointer-events-none z-0"></div>
60
60
 
61
61
  {/* Image */}
62
- <div className="relative h-[220px] w-full rounded-xl overflow-hidden z-10 ring-1 ring-white/10">
62
+ <div className="relative h-[13.75rem] w-full rounded-xl overflow-hidden z-10 ring-1 ring-white/10">
63
63
  <img src={item.imageUrl} alt={item.title} className="w-full h-full object-cover object-[center_25%]" />
64
64
  </div>
65
65
 
@@ -1,8 +1,9 @@
1
1
  /* eslint-disable no-unused-vars */
2
- import React, { useState, useEffect } from 'react';
2
+ import React, { useState, useEffect, useMemo, useRef } from 'react';
3
3
  import { motion } from 'framer-motion';
4
4
 
5
5
  // --- FloatingOrbsBackground Component (Optimized for Mobile) ---
6
+ // Orbs start together in the centre and drift outwards inside the container, then glide back.
6
7
  const FloatingOrbsBackground = ({
7
8
  children,
8
9
  className,
@@ -11,78 +12,77 @@ const FloatingOrbsBackground = ({
11
12
  desktopOrbs = 20, // Orbs for desktop
12
13
  mobileOrbs = 10, // Orbs for mobile
13
14
  }) => {
14
- const [numberOfOrbs, setNumberOfOrbs] = useState(desktopOrbs);
15
- const [isMobile, setIsMobile] = useState(false);
15
+ const boxRef = useRef(null);
16
+ const [box, setBox] = useState({ w: 0, h: 0 });
17
+ const isMobile = box.w > 0 && box.w < 768;
18
+ const numberOfOrbs = isMobile ? mobileOrbs : desktopOrbs;
16
19
 
17
- // Screen size check karne ke liye
20
+ // Measure the container so the orbs roam inside it (not the whole window).
18
21
  useEffect(() => {
19
- const checkScreenSize = () => {
20
- const isMobileDevice = window.innerWidth < 768;
21
- setIsMobile(isMobileDevice);
22
- setNumberOfOrbs(isMobileDevice ? mobileOrbs : desktopOrbs);
23
- };
24
-
25
- checkScreenSize(); // Pehli baar check karein
26
- window.addEventListener('resize', checkScreenSize);
27
-
28
- // Cleanup
29
- return () => window.removeEventListener('resize', checkScreenSize);
30
- }, [desktopOrbs, mobileOrbs]);
22
+ const el = boxRef.current;
23
+ if (!el) return;
24
+ const measure = () => setBox({ w: el.offsetWidth, h: el.offsetHeight });
25
+ measure();
26
+ const ro = new ResizeObserver(measure);
27
+ ro.observe(el);
28
+ return () => ro.disconnect();
29
+ }, []);
31
30
 
32
31
  const orbColors = colors ?? [
33
32
  "#10b981", "#06b6d4", "#6366f1", "#a855f7",
34
33
  "#d946ef", "#ec4899", "#f43f5e", "#f97316",
35
34
  ];
36
35
 
36
+ // Random paths are picked once per size change, so re-renders don't make the orbs jump.
37
+ const orbs = useMemo(() => {
38
+ const rangeX = box.w * 0.42;
39
+ const rangeY = box.h * 0.38;
40
+ const rand = (range) => Math.round((Math.random() * 2 - 1) * range);
41
+ return Array.from({ length: numberOfOrbs }, () => ({
42
+ size: Math.random() * 160 + (isMobile ? 90 : 130),
43
+ duration: Math.random() * 14 + 12,
44
+ x: [0, rand(rangeX), rand(rangeX), rand(rangeX)],
45
+ y: [0, rand(rangeY), rand(rangeY), rand(rangeY)],
46
+ }));
47
+ }, [numberOfOrbs, box.w, box.h, isMobile]);
48
+
37
49
  return (
38
50
  <div
51
+ ref={boxRef}
39
52
  className={`relative h-[60vh] lg:h-[80vh] w-full flex flex-col items-center justify-center overflow-hidden bg-slate-950 ${containerClassName}`}
40
53
  >
41
54
  {/* Orbs Container */}
42
55
  <div className="absolute inset-0 z-0 w-full h-full">
43
- {Array.from({ length: numberOfOrbs }).map((_, index) => {
44
- const size = Math.random() * 200 + 100;
45
- const duration = Math.random() * 20 + 15;
46
- // Mobile ke liye blur kam kar diya hai
47
- const blur = isMobile ? '60px' : '80px';
48
-
49
- return (
50
- <motion.div
51
- key={index}
52
- className="absolute rounded-full"
53
- style={{
54
- width: size,
55
- height: size,
56
- backgroundColor: orbColors[index % orbColors.length],
57
- filter: `blur(${blur})`,
58
- opacity: 0.4,
59
- // Performance behtar karne ke liye browser ko hint dena
60
- willChange: 'transform, opacity',
61
- }}
62
- animate={{
63
- x: [
64
- // Movement range ko badha diya hai taaki yeh poori screen cover kare
65
- `${Math.random() * 150 - 75}vw`,
66
- `${Math.random() * 150 - 75}vw`,
67
- `${Math.random() * 150 - 75}vw`,
68
- ],
69
- y: [
70
- // Movement range ko badha diya hai taaki yeh poori screen cover kare
71
- `${Math.random() * 150 - 75}vh`,
72
- `${Math.random() * 150 - 75}vh`,
73
- `${Math.random() * 150 - 75}vh`,
74
- ],
75
- scale: [1, 1.2, 1],
76
- }}
77
- transition={{
78
- duration: duration,
79
- repeat: Infinity,
80
- ease: 'easeInOut',
81
- repeatType: 'mirror',
82
- }}
83
- />
84
- );
85
- })}
56
+ {box.w > 0 && orbs.map((orb, index) => (
57
+ <motion.div
58
+ key={index}
59
+ className="absolute left-1/2 top-1/2 rounded-full"
60
+ style={{
61
+ width: orb.size,
62
+ height: orb.size,
63
+ marginLeft: -orb.size / 2,
64
+ marginTop: -orb.size / 2,
65
+ backgroundColor: orbColors[index % orbColors.length],
66
+ filter: `blur(${isMobile ? 45 : 60}px)`,
67
+ mixBlendMode: 'screen',
68
+ // Performance behtar karne ke liye browser ko hint dena
69
+ willChange: 'transform, opacity',
70
+ }}
71
+ initial={{ x: 0, y: 0, scale: 0.6, opacity: 0 }}
72
+ animate={{
73
+ x: orb.x,
74
+ y: orb.y,
75
+ scale: [0.8, 1.15, 0.95, 1.1],
76
+ opacity: [0.85, 0.7, 0.8, 0.7],
77
+ }}
78
+ transition={{
79
+ duration: orb.duration,
80
+ repeat: Infinity,
81
+ ease: 'easeInOut',
82
+ repeatType: 'mirror',
83
+ }}
84
+ />
85
+ ))}
86
86
  </div>
87
87
 
88
88
  <div className={`relative z-10 ${className}`}>
@@ -57,7 +57,7 @@ const ParallaxProfileCard = ({ profile }) => {
57
57
  return (
58
58
  <div
59
59
  ref={cardRef}
60
- className="relative w-[300px] max-w-full bg-[#1D2B3A] rounded-3xl p-6 border border-cyan-400/30 shadow-2xl shadow-cyan-900/40"
60
+ className="relative w-[18.75rem] max-w-full bg-[#1D2B3A] rounded-3xl p-6 border border-cyan-400/30 shadow-2xl shadow-cyan-900/40"
61
61
  style={{ transformStyle: "preserve-3d" }}
62
62
  >
63
63
  <div className="absolute inset-0 bg-gradient-to-br from-[#1D2B3A] to-[#0b1220] rounded-3xl"></div>
@@ -0,0 +1,304 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from "react";
2
+ import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
3
+ import { CircleCheck, LoaderCircle, Printer, RotateCcw } from "lucide-react";
4
+
5
+ /*
6
+ * Receipt Printer: a little thermal printer that processes a payment, feeds a
7
+ * receipt out of its slot and settles on "Payment complete".
8
+ *
9
+ * Runs on its own (autoPlay) or follows a controlled `stage`:
10
+ * "processing" → "printing" → "complete"
11
+ */
12
+
13
+ const defaultItems = [
14
+ { name: "Iced Latte", qty: 2, price: 4.5 },
15
+ { name: "Blueberry Muffin", qty: 1, price: 3.25 },
16
+ { name: "Avocado Toast", qty: 1, price: 8.9 },
17
+ { name: "Sparkling Water", qty: 1, price: 2 },
18
+ ];
19
+
20
+ const STATUS = {
21
+ processing: { label: "Processing payment", Icon: LoaderCircle },
22
+ printing: { label: "Printing receipt", Icon: Printer },
23
+ complete: { label: "Payment complete", Icon: CircleCheck },
24
+ };
25
+
26
+ const ease = [0.22, 1, 0.36, 1];
27
+
28
+ /*
29
+ * Keyframes for a "stepped" feed: the paper advances in short bursts with a
30
+ * pause after each one, like a thermal printer printing line by line.
31
+ */
32
+ function steppedFeed(steps = 9, moveShare = 0.62) {
33
+ const y = ["-100%"];
34
+ const times = [0];
35
+ for (let i = 1; i <= steps; i++) {
36
+ // Ease the step sizes so the paper starts slow and speeds up a little.
37
+ const progress = 1 - Math.pow(1 - i / steps, 1.35);
38
+ const at = -100 + progress * 100;
39
+ const start = (i - 1) / steps;
40
+ const end = i / steps;
41
+ y.push(`${at.toFixed(2)}%`, `${at.toFixed(2)}%`);
42
+ times.push(start + (end - start) * moveShare, end);
43
+ }
44
+ return { y, times };
45
+ }
46
+
47
+ // Bar widths derived from the order id, so the same order always gets the same barcode.
48
+ function barcodeBars(code, count = 38) {
49
+ const seed = [...String(code)].reduce((sum, ch, i) => sum + ch.charCodeAt(0) * (i + 7), 0);
50
+ return Array.from({ length: count }, (_, i) => 1 + ((seed * (i + 3) * 2654435761) >>> 0) % 3);
51
+ }
52
+
53
+ const money = (value, currency) => `${currency}${value.toFixed(2)}`;
54
+
55
+ // Zig-zag tear along the bottom edge of the paper.
56
+ const tearMask = {
57
+ WebkitMask:
58
+ "linear-gradient(#000 0 0) top / 100% calc(100% - 7px) no-repeat, conic-gradient(from -45deg at bottom, #0000, #000 1deg 89deg, #0000 90deg) bottom / 14px 7px repeat-x",
59
+ mask: "linear-gradient(#000 0 0) top / 100% calc(100% - 7px) no-repeat, conic-gradient(from -45deg at bottom, #0000, #000 1deg 89deg, #0000 90deg) bottom / 14px 7px repeat-x",
60
+ };
61
+
62
+ function Receipt({ store, address, items, currency, taxRate, orderId, date }) {
63
+ const subtotal = items.reduce((sum, item) => sum + item.qty * item.price, 0);
64
+ const tax = subtotal * taxRate;
65
+ const bars = useMemo(() => barcodeBars(orderId), [orderId]);
66
+
67
+ return (
68
+ <article
69
+ className="relative bg-[#fbfaf5] px-5 pb-8 pt-5 font-mono text-[0.72rem] leading-relaxed text-zinc-800 shadow-[0_18px_30px_-18px_rgba(0,0,0,0.6)]"
70
+ style={tearMask}
71
+ >
72
+ {/* faint thermal-paper grain */}
73
+ <div
74
+ aria-hidden="true"
75
+ className="pointer-events-none absolute inset-0 opacity-[0.35]"
76
+ style={{ backgroundImage: "repeating-linear-gradient(0deg, rgba(0,0,0,0.035) 0 1px, transparent 1px 3px)" }}
77
+ />
78
+ <div className="relative">
79
+ <p className="text-center text-[0.85rem] font-bold uppercase tracking-[0.18em]">{store}</p>
80
+ {address && <p className="mt-0.5 text-center text-[0.65rem] text-zinc-500">{address}</p>}
81
+ <p className="mt-0.5 text-center text-[0.65rem] text-zinc-500">{date}</p>
82
+
83
+ <div className="my-3 border-t border-dashed border-zinc-400" />
84
+
85
+ <ul className="space-y-1">
86
+ {items.map((item, i) => (
87
+ <li key={i} className="flex justify-between gap-3">
88
+ <span className="min-w-0 truncate">
89
+ {item.qty} × {item.name}
90
+ </span>
91
+ <span className="shrink-0 tabular-nums">{money(item.qty * item.price, currency)}</span>
92
+ </li>
93
+ ))}
94
+ </ul>
95
+
96
+ <div className="my-3 border-t border-dashed border-zinc-400" />
97
+
98
+ <div className="space-y-0.5 text-zinc-600">
99
+ <p className="flex justify-between"><span>Subtotal</span><span className="tabular-nums">{money(subtotal, currency)}</span></p>
100
+ <p className="flex justify-between"><span>Tax {Math.round(taxRate * 100)}%</span><span className="tabular-nums">{money(tax, currency)}</span></p>
101
+ </div>
102
+ <p className="mt-2 flex justify-between text-[0.85rem] font-bold">
103
+ <span>TOTAL</span>
104
+ <span className="tabular-nums">{money(subtotal + tax, currency)}</span>
105
+ </p>
106
+
107
+ <div className="my-3 border-t border-dashed border-zinc-400" />
108
+
109
+ <div aria-hidden="true" className="flex h-9 items-stretch justify-center gap-[1.5px]">
110
+ {bars.map((w, i) => (
111
+ <span key={i} className="bg-zinc-800" style={{ width: w }} />
112
+ ))}
113
+ </div>
114
+ <p className="mt-1 text-center text-[0.62rem] tracking-[0.3em] text-zinc-500">{orderId}</p>
115
+ <p className="mt-3 text-center text-[0.65rem] font-semibold uppercase tracking-[0.25em]">Thank you</p>
116
+ </div>
117
+ </article>
118
+ );
119
+ }
120
+
121
+ const ReceiptPrinter = ({
122
+ store = "ApexUI Café",
123
+ address = "221B Baker Street",
124
+ items = defaultItems,
125
+ currency = "$",
126
+ taxRate = 0.08,
127
+ orderId = "AX-20491",
128
+ date,
129
+ stage,
130
+ autoPlay = true,
131
+ feed = "stepped",
132
+ processingTime = 1600,
133
+ printTime = 2200,
134
+ accentColor = "#a3e635",
135
+ onComplete,
136
+ className = "",
137
+ }) => {
138
+ const controlled = stage !== undefined;
139
+ const [ownStage, setOwnStage] = useState(autoPlay ? "processing" : "complete");
140
+ const [run, setRun] = useState(0);
141
+ const current = controlled ? stage : ownStage;
142
+ const reduceMotion = useReducedMotion();
143
+ const onCompleteRef = useRef(onComplete);
144
+ onCompleteRef.current = onComplete;
145
+
146
+ // Self-running sequence (skipped when the parent controls the stage).
147
+ useEffect(() => {
148
+ if (controlled || (!autoPlay && run === 0)) return undefined;
149
+ setOwnStage("processing");
150
+ const toPrint = setTimeout(() => setOwnStage("printing"), processingTime);
151
+ const toDone = setTimeout(() => {
152
+ setOwnStage("complete");
153
+ onCompleteRef.current?.();
154
+ }, processingTime + printTime);
155
+ return () => {
156
+ clearTimeout(toPrint);
157
+ clearTimeout(toDone);
158
+ };
159
+ }, [controlled, autoPlay, run, processingTime, printTime]);
160
+
161
+ const printedAt = useMemo(
162
+ () =>
163
+ date ??
164
+ new Date().toLocaleString("en-US", { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" }),
165
+ // A fresh timestamp for every print run.
166
+ // eslint-disable-next-line react-hooks/exhaustive-deps
167
+ [date, run]
168
+ );
169
+
170
+ const stepped = useMemo(() => steppedFeed(), []);
171
+ const moving = !reduceMotion;
172
+ const paperY =
173
+ current === "processing"
174
+ ? "-100%"
175
+ : current === "printing" && moving
176
+ ? feed === "stepped"
177
+ ? stepped.y
178
+ : "0%"
179
+ : "0%";
180
+ const paperTransition =
181
+ current === "printing" && moving
182
+ ? feed === "stepped"
183
+ ? { duration: printTime / 1000, ease: "linear", times: stepped.times }
184
+ : { duration: printTime / 1000, ease: [0.65, 0, 0.35, 1] }
185
+ : { duration: 0 };
186
+
187
+ const { label, Icon } = STATUS[current] ?? STATUS.processing;
188
+ const itemCount = items.reduce((sum, item) => sum + item.qty, 0);
189
+
190
+ return (
191
+ <section aria-label="Receipt printer" className={`relative isolate flex w-[20rem] max-w-full flex-col items-center ${className}`}>
192
+ {/* Machine */}
193
+ <div
194
+ className="relative z-20 w-full rounded-[1.75rem] border border-white/10 bg-[#1b1b20] p-3 pb-8 shadow-[0_30px_60px_-30px_rgba(0,0,0,0.85),inset_0_1px_0_rgba(255,255,255,0.08),inset_0_-2px_0_rgba(0,0,0,0.5)]"
195
+ >
196
+ {/* matte plastic texture */}
197
+ <div
198
+ aria-hidden="true"
199
+ className="pointer-events-none absolute inset-0 rounded-[inherit] opacity-[0.12]"
200
+ style={{ backgroundImage: "radial-gradient(rgba(255,255,255,0.7) 0.5px, transparent 0.6px)", backgroundSize: "4px 4px" }}
201
+ />
202
+
203
+ {/* Top row: brand, LED and reprint */}
204
+ <div className="relative flex h-10 items-center justify-between px-2">
205
+ <span className="font-mono text-[0.6rem] uppercase tracking-[0.3em] text-white/40">Apex · Print</span>
206
+ <div className="flex items-center gap-2">
207
+ <span
208
+ aria-hidden="true"
209
+ className={`h-2 w-2 rounded-full ${current === "printing" && moving ? "animate-pulse" : ""}`}
210
+ style={{
211
+ backgroundColor: current === "complete" ? accentColor : current === "printing" ? accentColor : "#f59e0b",
212
+ boxShadow: `0 0 10px ${current === "processing" ? "#f59e0b" : accentColor}`,
213
+ }}
214
+ />
215
+ {!controlled && (
216
+ <button
217
+ type="button"
218
+ onClick={() => setRun((r) => r + 1)}
219
+ disabled={current !== "complete"}
220
+ aria-label="Print again"
221
+ title="Print again"
222
+ className="flex h-7 w-7 items-center justify-center rounded-full border border-white/10 bg-white/5 text-white/60 transition hover:text-white disabled:pointer-events-none disabled:opacity-30"
223
+ >
224
+ <RotateCcw className="h-3.5 w-3.5" />
225
+ </button>
226
+ )}
227
+ </div>
228
+ </div>
229
+
230
+ {/* Screen */}
231
+ <div className="relative overflow-hidden rounded-[1.1rem] border border-black/60 bg-[#0b0e0c] px-4 py-3.5 shadow-[inset_0_2px_12px_rgba(0,0,0,0.8)]">
232
+ <div className="flex items-center gap-2.5" role="status" aria-live="polite">
233
+ <span className="relative grid h-5 w-5 shrink-0 place-items-center" style={{ color: current === "processing" ? "#f59e0b" : accentColor }}>
234
+ <AnimatePresence initial={false} mode="popLayout">
235
+ <motion.span
236
+ key={current}
237
+ initial={moving ? { opacity: 0, scale: 0.7 } : false}
238
+ animate={{ opacity: 1, scale: 1 }}
239
+ exit={moving ? { opacity: 0, scale: 0.7 } : { opacity: 0 }}
240
+ transition={{ duration: 0.2, ease }}
241
+ className="grid place-items-center"
242
+ >
243
+ <Icon className={`h-[18px] w-[18px] ${current === "processing" && moving ? "animate-spin" : ""}`} strokeWidth={2.2} />
244
+ </motion.span>
245
+ </AnimatePresence>
246
+ </span>
247
+ <div className="relative h-4 min-w-0 flex-1 overflow-hidden">
248
+ <AnimatePresence initial={false} mode="popLayout">
249
+ <motion.p
250
+ key={current}
251
+ initial={moving ? { opacity: 0, y: 8 } : false}
252
+ animate={{ opacity: 1, y: 0 }}
253
+ exit={moving ? { opacity: 0, y: -8 } : { opacity: 0 }}
254
+ transition={{ duration: 0.22, ease }}
255
+ className="truncate text-xs font-medium leading-4 text-zinc-200"
256
+ >
257
+ {label}
258
+ </motion.p>
259
+ </AnimatePresence>
260
+ </div>
261
+ </div>
262
+ <div className="mt-2.5 flex items-center justify-between font-mono text-[0.62rem] text-zinc-500">
263
+ <span>#{orderId}</span>
264
+ <span>{itemCount} items</span>
265
+ </div>
266
+ {/* Progress while printing */}
267
+ <div className="mt-2 h-1 overflow-hidden rounded-full bg-white/5">
268
+ <motion.div
269
+ key={`${run}-${current}`}
270
+ className="h-full rounded-full"
271
+ style={{ backgroundColor: current === "processing" ? "#f59e0b" : accentColor }}
272
+ initial={{ width: current === "printing" ? "0%" : current === "complete" ? "100%" : "35%" }}
273
+ animate={{ width: current === "processing" ? ["15%", "60%", "15%"] : "100%" }}
274
+ transition={
275
+ current === "processing"
276
+ ? { duration: 1.4, repeat: Infinity, ease: "easeInOut" }
277
+ : { duration: current === "printing" && moving ? printTime / 1000 : 0, ease: "linear" }
278
+ }
279
+ />
280
+ </div>
281
+ </div>
282
+
283
+ {/* Paper slot */}
284
+ <div aria-hidden="true" className="absolute inset-x-7 bottom-3 h-2 rounded-full bg-black shadow-[inset_0_2px_3px_rgba(0,0,0,0.9),0_1px_0_rgba(255,255,255,0.06)]" />
285
+ </div>
286
+
287
+ {/* Paper coming out of the slot */}
288
+ <div className="relative z-10 -mt-4 w-[86%] overflow-hidden px-1 pb-4 pt-0">
289
+ <div aria-hidden="true" className="pointer-events-none absolute inset-x-1 top-0 z-10 h-3 bg-gradient-to-b from-black/45 to-transparent" />
290
+ <motion.div
291
+ key={run}
292
+ initial={{ y: "-100%" }}
293
+ animate={{ y: paperY }}
294
+ transition={paperTransition}
295
+ aria-hidden={current !== "complete"}
296
+ >
297
+ <Receipt store={store} address={address} items={items} currency={currency} taxRate={taxRate} orderId={orderId} date={printedAt} />
298
+ </motion.div>
299
+ </div>
300
+ </section>
301
+ );
302
+ };
303
+
304
+ export default ReceiptPrinter;
@@ -9,8 +9,8 @@ import {
9
9
  LaptopMinimal,
10
10
  } from "lucide-react";
11
11
 
12
- const CARD_HEIGHT = 250;
13
- const CARD_EXPANDED_HEIGHT = 510;
12
+ const CARD_HEIGHT = 264;
13
+ const CARD_EXPANDED_HEIGHT = 2 * CARD_HEIGHT + 10; // two rows plus the grid gap
14
14
 
15
15
  const SmartGridCard = ({
16
16
  cards = defaultCards(),
@@ -157,20 +157,21 @@ const SmartGridCard = ({
157
157
  />
158
158
 
159
159
  {/* Card Content */}
160
- <div className="relative z-10 flex flex-col items-start justify-between h-full w-full p-6 rounded-2xl bg-zinc-900 text-white space-y-4">
161
- <div className="text-4xl border border-dashed p-2 rounded-lg border-purple-600">{card.icon}</div>
162
- <div>
163
- <h3 className="text-xl md:text-xl font-bold mb-2 break-words leading-tight">{card.title}</h3>
164
- <p className="text-sm md:text-base w-full text-white/70 break-words">{card.description}</p>
160
+ {/* Compact enough that the button always fits inside the fixed card height */}
161
+ <div className="relative z-10 flex flex-col items-start h-full w-full p-5 rounded-2xl bg-zinc-900 text-white gap-3">
162
+ <div className="flex h-11 w-11 shrink-0 items-center justify-center border border-dashed rounded-lg border-purple-600">{card.icon}</div>
163
+ <div className="min-h-0">
164
+ <h3 className="text-lg font-bold mb-1.5 break-words leading-tight">{card.title}</h3>
165
+ <p className="text-sm leading-relaxed w-full text-white/70 break-words line-clamp-3">{card.description}</p>
165
166
  </div>
166
167
  <button
167
168
  type="button"
168
- className="mt-auto relative overflow-hidden whitespace-nowrap px-4 py-3 rounded-lg border border-purple-400/60 text-white transition hover:text-white bg-black/20 group flex items-center gap-2 text-xs md:text-base"
169
+ className="mt-auto shrink-0 relative overflow-hidden whitespace-nowrap px-4 py-2 rounded-lg border border-purple-400/60 text-white transition hover:text-white bg-black/20 group flex items-center gap-2 text-sm"
169
170
  >
170
171
  <span className="relative z-10 flex items-center gap-2">
171
172
  {card.buttonText}
172
173
  <span className="arrow-icon transition-transform duration-200 group-hover:translate-x-2 flex items-center">
173
- <svg width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
174
+ <svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
174
175
  <path d="M5 12h14M13 6l6 6-6 6" />
175
176
  </svg>
176
177
  </span>
package/dist/cli/index.js CHANGED
@@ -47,6 +47,7 @@ const componentMap = {
47
47
  "theme-toggle": "ThemeToggle",
48
48
  "testimonials": "Testimonials",
49
49
  "mask-view": "MaskView",
50
+ "receipt-printer": "ReceiptPrinter",
50
51
  "water-drop-reveal": "MaskView",
51
52
  };
52
53
  // Packages only some components need, installed on top of the shared ones.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apex-ui-kit",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "license": "MIT",
5
5
  "description": "Apex UI Kit - A React component library for building modern user interfaces",
6
6
  "keywords": [