feat: ReactBits micro components integration
Installed 6 ReactBits micro components (src/components/): - SwipeRow: swipe-to-action cards (lead approve/reject/snooze) - CometDial: circular dial selector (future: dashboard KPI selector) - CodeSlots: code input slots (future: API token confirmation) - FuseButton: fuse-animated CTA with undo window - SlideCommit: slide-to-confirm (timesheet week lock) - HoldButton: hold-to-confirm (contract sign, dangerous actions) Integrated into views: - Timesheet: SlideCommit replaces 'Woche sperren' button - Contracts: HoldButton replaces 'E-Signatur anfordern' (hold 1.5s to sign) - Recurring: FuseButton replaces 'Jetzt ausführen' (with undo window) - Leads: SwipeableLeadCard created (swipe to approve/reject/snooze) Created wrapper components: - swipeable-lead-card.tsx: SwipeRow wrapper for lead cards - hold-to-confirm.tsx: HoldButton wrapper with loading state - fuse-cta.tsx: FuseButton wrapper with toast feedback - slide-to-lock.tsx: SlideCommit wrapper for GoBD week lock 0 TS errors, ESLint clean, no console errors.
This commit is contained in:
1 parent
21e112e5cb
commit
37e976a732
17 files changed
+2884
-25
No files matched your search
@@ -0,0 +1,530 @@
|
||||
'use client';
|
||||
|
||||
import React, { useCallback, useEffect, useId, useMemo, useRef, useState, type CSSProperties } from 'react';
|
||||
import {
|
||||
animate,
|
||||
motion,
|
||||
motionValue,
|
||||
useMotionValue,
|
||||
useReducedMotion,
|
||||
useTransform,
|
||||
type MotionValue
|
||||
} from 'motion/react';
|
||||
import { HugeiconsIcon } from '@hugeicons/react';
|
||||
import { Tick02Icon } from '@hugeicons/core-free-icons';
|
||||
|
||||
const EASE_OUT: [number, number, number, number] = [0.23, 1, 0.32, 1];
|
||||
const WASH_IN = 0.3;
|
||||
const WASH_OUT = 0.2;
|
||||
const SINK_DELAY = 0.06;
|
||||
const SINK_STEP = 0.03;
|
||||
const CHECK_DELAY = 0.28;
|
||||
const CHECK_RISE = 8;
|
||||
const SINK_FADE = 0.6;
|
||||
|
||||
export type CodeSlotsStatus = 'idle' | 'error' | 'success';
|
||||
|
||||
export interface CodeSlotsProps {
|
||||
length?: number;
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
onChange?: (code: string) => void;
|
||||
onComplete?: (code: string) => void;
|
||||
status?: CodeSlotsStatus;
|
||||
mask?: boolean;
|
||||
caret?: boolean;
|
||||
disabled?: boolean;
|
||||
autoFocus?: boolean;
|
||||
accentColor?: string;
|
||||
inkColor?: string;
|
||||
slotColor?: string;
|
||||
digitColor?: string;
|
||||
dangerColor?: string;
|
||||
slotSize?: number;
|
||||
gap?: number;
|
||||
radius?: number;
|
||||
bounce?: number;
|
||||
settle?: number;
|
||||
rise?: number;
|
||||
cascade?: number;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface Live {
|
||||
settle: number;
|
||||
bounce: number;
|
||||
cascade: number;
|
||||
reduce: boolean | null;
|
||||
}
|
||||
|
||||
interface SlotProps {
|
||||
mv: MotionValue<number>;
|
||||
drop: MotionValue<number>;
|
||||
char: string;
|
||||
active: boolean;
|
||||
rise: number;
|
||||
sink: number;
|
||||
}
|
||||
|
||||
const clamp01 = (v: number) => Math.min(1, Math.max(0, v));
|
||||
const digitsOf = (raw: string | undefined) => String(raw ?? '').replace(/\D/g, '');
|
||||
const toSlots = (raw: string | undefined, n: number) => {
|
||||
const d = digitsOf(raw).slice(0, n);
|
||||
return Array.from({ length: n }, (_, i) => d[i] ?? '');
|
||||
};
|
||||
const firstEmptyOf = (slots: string[]) => {
|
||||
const i = slots.indexOf('');
|
||||
return i === -1 ? slots.length - 1 : i;
|
||||
};
|
||||
const isFull = (slots: string[]) => slots.every(Boolean);
|
||||
const STYLE = '@keyframes code-slots-blink{0%,49.9%{opacity:1}50%,100%{opacity:0}}';
|
||||
|
||||
const CodeSlots: React.FC<CodeSlotsProps> = ({
|
||||
length = 6,
|
||||
value,
|
||||
defaultValue = '',
|
||||
onChange,
|
||||
onComplete,
|
||||
status = 'idle',
|
||||
mask = false,
|
||||
caret = true,
|
||||
disabled = false,
|
||||
autoFocus = false,
|
||||
accentColor = '#f5f5f5',
|
||||
inkColor = '#f5f5f5',
|
||||
slotColor = '#27272a',
|
||||
digitColor = '#18181b',
|
||||
dangerColor = '#ff3b30',
|
||||
slotSize = 44,
|
||||
gap = 8,
|
||||
radius = 12,
|
||||
bounce = 0.2,
|
||||
settle = 0.3,
|
||||
rise = 8,
|
||||
cascade = 20,
|
||||
ariaLabel = 'One-time code',
|
||||
className = ''
|
||||
}) => {
|
||||
const uid = useId();
|
||||
const reduce = useReducedMotion();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const rowRef = useRef<HTMLDivElement>(null);
|
||||
const [slots, setSlots] = useState(() => toSlots(value ?? defaultValue, length));
|
||||
const [active, setActive] = useState(() => firstEmptyOf(slots));
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [veiled, setVeiled] = useState(status === 'success');
|
||||
const activeMv = useMotionValue(active);
|
||||
const openMv = useMotionValue(status === 'success' ? 1 : 0);
|
||||
const checkMv = useMotionValue(status === 'success' ? 1 : 0);
|
||||
const glide = useRef<Set<number>>(new Set());
|
||||
const target = useRef<number[]>([]);
|
||||
const draining = useRef(false);
|
||||
const drainTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
const statusRef = useRef(status);
|
||||
const emitted = useRef(digitsOf(value ?? defaultValue).slice(0, length));
|
||||
const slotsRef = useRef(slots);
|
||||
slotsRef.current = slots;
|
||||
const live = useRef<Live>({} as Live);
|
||||
live.current = { settle, bounce, cascade, reduce };
|
||||
|
||||
const springs = useMemo(
|
||||
() => ({
|
||||
mvs: Array.from({ length }, (_, i) => motionValue(slotsRef.current[i] ? 1 : 0)),
|
||||
drops: Array.from({ length }, () => motionValue(statusRef.current === 'success' ? 1 : 0))
|
||||
}),
|
||||
[length]
|
||||
);
|
||||
const { mvs, drops } = springs;
|
||||
const pitch = slotSize + gap;
|
||||
const height = Math.round(slotSize * 1.18);
|
||||
const washRadius = Math.min(radius, slotSize / 2);
|
||||
|
||||
const drive = useCallback(
|
||||
(i: number, to: number, delayMs = 0) => {
|
||||
const mv = mvs[i];
|
||||
if (!mv) return;
|
||||
target.current[i] = to;
|
||||
const L = live.current;
|
||||
if (L.reduce) {
|
||||
mv.jump(to);
|
||||
return;
|
||||
}
|
||||
animate(mv, to, { type: 'spring', duration: L.settle, bounce: L.bounce, delay: delayMs / 1000 });
|
||||
},
|
||||
[mvs]
|
||||
);
|
||||
const land = useCallback(
|
||||
(i: number, delayMs = 0) => {
|
||||
if (mvs[i].get() > 0) mvs[i].jump(0);
|
||||
drive(i, 1, delayMs);
|
||||
},
|
||||
[mvs, drive]
|
||||
);
|
||||
const moveActive = useCallback(
|
||||
(next: number, crossed: number[]) => {
|
||||
crossed.forEach(j => glide.current.add(j));
|
||||
activeMv.jump(next);
|
||||
setActive(next);
|
||||
},
|
||||
[activeMv]
|
||||
);
|
||||
const jumpActive = useCallback(
|
||||
(next: number) => {
|
||||
glide.current.clear();
|
||||
activeMv.jump(next);
|
||||
setActive(next);
|
||||
},
|
||||
[activeMv]
|
||||
);
|
||||
|
||||
const caretX = useTransform(() => {
|
||||
const a = activeMv.get();
|
||||
let x = a * pitch;
|
||||
for (let j = 0; j < mvs.length; j++) {
|
||||
const h = clamp01(mvs[j].get());
|
||||
if (!glide.current.has(j)) continue;
|
||||
const to = target.current[j];
|
||||
if (to === undefined || h === clamp01(to)) {
|
||||
glide.current.delete(j);
|
||||
continue;
|
||||
}
|
||||
x += j < a ? -(1 - h) * pitch : h * pitch;
|
||||
}
|
||||
return Math.min(Math.max(x, 0), (mvs.length - 1) * pitch);
|
||||
});
|
||||
const caretTransform = useTransform(caretX, (x: number) => `translateX(${x}px)`);
|
||||
const washClip = useTransform(openMv, (o: number) => `inset(0 ${(1 - clamp01(o)) * 50}% round ${washRadius}px)`);
|
||||
const checkTransform = useTransform(
|
||||
checkMv,
|
||||
(c: number) => `translateY(${(1 - c) * CHECK_RISE}px) scale(${0.85 + 0.15 * Math.max(c, 0)})`
|
||||
);
|
||||
const checkOpacity = useTransform(checkMv, clamp01);
|
||||
|
||||
const commit = useCallback(
|
||||
(next: string[]) => {
|
||||
const prev = slotsRef.current;
|
||||
slotsRef.current = next;
|
||||
setSlots(next);
|
||||
const code = next.join('');
|
||||
emitted.current = code;
|
||||
onChange?.(code);
|
||||
if (!isFull(prev) && isFull(next)) onComplete?.(code);
|
||||
},
|
||||
[onChange, onComplete]
|
||||
);
|
||||
|
||||
const insert = (raw: string, from = active) => {
|
||||
const digits = digitsOf(raw);
|
||||
if (!digits) return;
|
||||
const next = [...slotsRef.current];
|
||||
const crossed: number[] = [];
|
||||
const step = reduce ? 0 : cascade;
|
||||
let i = from;
|
||||
for (const ch of digits) {
|
||||
if (i >= length) break;
|
||||
next[i] = ch;
|
||||
land(i, (i - from) * step);
|
||||
crossed.push(i);
|
||||
i += 1;
|
||||
}
|
||||
if (!crossed.length) return;
|
||||
commit(next);
|
||||
moveActive(Math.min(i, length - 1), crossed);
|
||||
};
|
||||
const clearSlot = (i: number, stepBack = false) => {
|
||||
if (!slotsRef.current[i]) {
|
||||
if (stepBack) jumpActive(i);
|
||||
return;
|
||||
}
|
||||
const next = [...slotsRef.current];
|
||||
next[i] = '';
|
||||
drive(i, 0);
|
||||
commit(next);
|
||||
if (stepBack) moveActive(i, [i]);
|
||||
};
|
||||
|
||||
const busy = disabled || draining.current || status === 'success';
|
||||
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (busy || e.metaKey || e.ctrlKey || e.altKey) return;
|
||||
const k = e.key;
|
||||
if (/^[0-9]$/.test(k)) {
|
||||
e.preventDefault();
|
||||
insert(k);
|
||||
} else if (k === 'Backspace') {
|
||||
e.preventDefault();
|
||||
if (slots[active]) clearSlot(active);
|
||||
else if (active > 0) clearSlot(active - 1, true);
|
||||
} else if (k === 'Delete') {
|
||||
e.preventDefault();
|
||||
clearSlot(active);
|
||||
} else if (k === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
jumpActive(Math.max(active - 1, 0));
|
||||
} else if (k === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
jumpActive(Math.min(active + 1, length - 1));
|
||||
} else if (k === 'Home') {
|
||||
e.preventDefault();
|
||||
jumpActive(0);
|
||||
} else if (k === 'End') {
|
||||
e.preventDefault();
|
||||
jumpActive(length - 1);
|
||||
}
|
||||
};
|
||||
const onPaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
||||
if (busy) return;
|
||||
e.preventDefault();
|
||||
insert(e.clipboardData.getData('text'));
|
||||
};
|
||||
const onInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (busy) return;
|
||||
const d = digitsOf(e.target.value);
|
||||
if (!d) return;
|
||||
insert(d, d.length === 1 ? active : 0);
|
||||
};
|
||||
const onRowMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (disabled) return;
|
||||
e.preventDefault();
|
||||
const row = rowRef.current;
|
||||
if (row && !draining.current && status !== 'success') {
|
||||
const rect = row.getBoundingClientRect();
|
||||
const zoom = rect.width / (row.offsetWidth || rect.width) || 1;
|
||||
const i = Math.floor((e.clientX - rect.left) / zoom / pitch);
|
||||
jumpActive(Math.max(0, Math.min(i, firstEmptyOf(slotsRef.current))));
|
||||
}
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
glide.current.clear();
|
||||
target.current = [];
|
||||
const next = Array.from({ length }, (_, i) => slotsRef.current[i] ?? '');
|
||||
slotsRef.current = next;
|
||||
setSlots(next);
|
||||
jumpActive(firstEmptyOf(next));
|
||||
const code = next.join('');
|
||||
if (code !== emitted.current) {
|
||||
emitted.current = code;
|
||||
onChange?.(code);
|
||||
}
|
||||
}, [length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (value === undefined) return;
|
||||
const clean = digitsOf(value).slice(0, length);
|
||||
if (clean === emitted.current) return;
|
||||
emitted.current = clean;
|
||||
const prev = slotsRef.current;
|
||||
const next = toSlots(clean, length);
|
||||
const hidden = statusRef.current === 'success';
|
||||
const landing: number[] = [];
|
||||
const leaving: number[] = [];
|
||||
next.forEach((ch, i) => {
|
||||
if (ch === prev[i]) return;
|
||||
(ch ? landing : leaving).push(i);
|
||||
});
|
||||
const step = live.current.reduce || hidden ? 0 : live.current.cascade;
|
||||
landing.forEach((i, k) => land(i, k * step));
|
||||
leaving.reverse().forEach((i, k) => {
|
||||
if (hidden) {
|
||||
target.current[i] = 0;
|
||||
mvs[i].jump(0);
|
||||
drops[i].jump(0);
|
||||
} else drive(i, 0, k * step);
|
||||
});
|
||||
slotsRef.current = next;
|
||||
setSlots(next);
|
||||
moveActive(firstEmptyOf(next), [...landing, ...leaving]);
|
||||
if (!isFull(prev) && isFull(next)) onComplete?.(clean);
|
||||
}, [value, length]);
|
||||
|
||||
useEffect(() => {
|
||||
const was = statusRef.current;
|
||||
const L = live.current;
|
||||
if (status === 'success') {
|
||||
setVeiled(true);
|
||||
if (L.reduce) {
|
||||
openMv.jump(1);
|
||||
drops.forEach(d => d.jump(1));
|
||||
checkMv.jump(1);
|
||||
return;
|
||||
}
|
||||
animate(openMv, 1, { duration: WASH_IN, ease: EASE_OUT });
|
||||
drops.forEach((d, k) =>
|
||||
animate(d, 1, { type: 'spring', duration: 0.3, bounce: 0, delay: SINK_DELAY + k * SINK_STEP })
|
||||
);
|
||||
animate(checkMv, 1, { type: 'spring', duration: 0.35, bounce: L.bounce, delay: CHECK_DELAY });
|
||||
return;
|
||||
}
|
||||
if (was !== 'success') return;
|
||||
if (L.reduce) {
|
||||
openMv.jump(0);
|
||||
checkMv.jump(0);
|
||||
drops.forEach(d => d.jump(0));
|
||||
setVeiled(false);
|
||||
return;
|
||||
}
|
||||
animate(checkMv, 0, { duration: 0.15, ease: EASE_OUT });
|
||||
animate(openMv, 0, { duration: WASH_OUT, ease: EASE_OUT, delay: 0.06 }).then(() => {
|
||||
if (openMv.get() === 0) setVeiled(false);
|
||||
});
|
||||
drops.forEach(d => animate(d, 0, { type: 'spring', duration: 0.3, bounce: 0, delay: 0.1 }));
|
||||
}, [status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (status !== 'error') return;
|
||||
const filled = slotsRef.current.map((c, i) => (c ? i : -1)).filter(i => i >= 0);
|
||||
if (!filled.length) return;
|
||||
filled.reverse();
|
||||
draining.current = true;
|
||||
const L = live.current;
|
||||
const step = L.reduce ? 0 : L.cascade;
|
||||
filled.forEach((i, k) => drive(i, 0, k * step));
|
||||
moveActive(
|
||||
0,
|
||||
slotsRef.current.map((_, j) => j)
|
||||
);
|
||||
clearTimeout(drainTimer.current);
|
||||
drainTimer.current = setTimeout(
|
||||
() => {
|
||||
draining.current = false;
|
||||
commit(Array.from({ length }, () => ''));
|
||||
},
|
||||
L.reduce ? 300 : (filled.length - 1) * step + L.settle * 1000
|
||||
);
|
||||
}, [status]);
|
||||
useEffect(() => {
|
||||
statusRef.current = status;
|
||||
}, [status]);
|
||||
useEffect(() => () => clearTimeout(drainTimer.current), []);
|
||||
useEffect(() => {
|
||||
if (autoFocus) inputRef.current?.focus();
|
||||
}, [autoFocus]);
|
||||
|
||||
const view = slots.length === length ? slots : Array.from({ length }, (_, i) => slots[i] ?? '');
|
||||
const showCaret =
|
||||
caret && focused && !disabled && !veiled && status !== 'success' && (status === 'error' || !view[active]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative inline-block${className ? ` ${className}` : ''}`}
|
||||
style={
|
||||
{
|
||||
'--cs-accent': accentColor,
|
||||
'--cs-ink': inkColor,
|
||||
'--cs-slot': slotColor,
|
||||
'--cs-digit': digitColor,
|
||||
'--cs-danger': dangerColor,
|
||||
'--cs-size': `${slotSize}px`,
|
||||
'--cs-height': `${height}px`,
|
||||
'--cs-gap': `${gap}px`,
|
||||
'--cs-radius': `${Math.min(radius, slotSize / 2)}px`,
|
||||
'--cs-font': `${Math.round(slotSize * 0.5)}px`
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<style>{STYLE}</style>
|
||||
<div
|
||||
ref={rowRef}
|
||||
className="group/row relative inline-flex cursor-text touch-manipulation gap-[var(--cs-gap)] [-webkit-tap-highlight-color:transparent] [transition:opacity_200ms_ease] data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50"
|
||||
data-status={status}
|
||||
data-focused={focused ? '' : undefined}
|
||||
data-disabled={disabled ? '' : undefined}
|
||||
onMouseDown={onRowMouseDown}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="absolute inset-0 z-[4] m-0 cursor-[inherit] appearance-none border-0 bg-transparent p-0 text-[16px] text-transparent opacity-0 outline-0 [caret-color:transparent]"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
autoComplete="one-time-code"
|
||||
pattern="[0-9]*"
|
||||
value=""
|
||||
maxLength={length}
|
||||
aria-label={ariaLabel}
|
||||
aria-invalid={status === 'error'}
|
||||
aria-describedby={`${uid}-count`}
|
||||
disabled={disabled}
|
||||
readOnly={status === 'success'}
|
||||
onKeyDown={onKeyDown}
|
||||
onPaste={onPaste}
|
||||
onChange={onInput}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
/>
|
||||
{view.map((ch, i) => (
|
||||
<Slot
|
||||
key={i}
|
||||
mv={mvs[i]}
|
||||
drop={drops[i]}
|
||||
char={mask && ch ? '•' : ch}
|
||||
active={focused && i === active}
|
||||
rise={rise}
|
||||
sink={Math.round(height * 0.5)}
|
||||
/>
|
||||
))}
|
||||
<motion.span
|
||||
className="pointer-events-none absolute inset-0 z-[1] grid place-items-center rounded-[var(--cs-radius)] bg-[var(--cs-accent)] [color:var(--cs-digit)] motion-reduce:opacity-0 motion-reduce:[transition:opacity_200ms_ease] motion-reduce:in-data-[status=success]:opacity-100"
|
||||
aria-hidden="true"
|
||||
style={{ clipPath: washClip }}
|
||||
>
|
||||
<motion.span
|
||||
className="grid place-items-center motion-reduce:transform-none! motion-reduce:[transition:opacity_150ms_ease]"
|
||||
style={{ transform: checkTransform, opacity: checkOpacity }}
|
||||
>
|
||||
<HugeiconsIcon icon={Tick02Icon} size={Math.round(slotSize * 0.6)} strokeWidth={2.2} />
|
||||
</motion.span>
|
||||
</motion.span>
|
||||
<motion.span
|
||||
className="pointer-events-none absolute top-1/4 left-[calc(var(--cs-size)/2_-_0.75px)] z-[3] h-1/2 w-[1.5px] opacity-0 data-[show]:opacity-100"
|
||||
aria-hidden="true"
|
||||
data-show={showCaret ? '' : undefined}
|
||||
style={{ transform: caretTransform }}
|
||||
>
|
||||
<span
|
||||
key={active}
|
||||
className="block h-full w-full bg-[var(--cs-ink)] animate-[code-slots-blink_1s_linear_infinite] motion-reduce:animate-none"
|
||||
/>
|
||||
</motion.span>
|
||||
</div>
|
||||
<span id={`${uid}-count`} className="sr-only" aria-live="polite">
|
||||
{status === 'success' ? 'Code accepted' : `${view.filter(Boolean).length} of ${length} digits entered`}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function Slot({ mv, drop, char, active, rise, sink }: SlotProps) {
|
||||
const [shown, setShown] = useState(char);
|
||||
if (char && char !== shown) setShown(char);
|
||||
const fill = useTransform(mv, (t: number) => `scale(${Math.max(t, 0)})`);
|
||||
const lift = useTransform(
|
||||
[mv, drop],
|
||||
([t, d]: number[]) => `translateY(${(1 - t) * rise + Math.max(d, 0) * sink}px)`
|
||||
);
|
||||
const ink = useTransform([mv, drop], ([t, d]: number[]) => clamp01(t) * (1 - clamp01(d / SINK_FADE)));
|
||||
return (
|
||||
<span
|
||||
className="relative h-[var(--cs-height)] w-[var(--cs-size)] overflow-hidden rounded-[var(--cs-radius)] bg-[var(--cs-slot)] select-none [transition:background-color_200ms_ease] data-[active]:[background-color:color-mix(in_srgb,var(--cs-ink)_8%,var(--cs-slot))] in-data-[status=error]:[background-color:color-mix(in_srgb,var(--cs-danger)_20%,var(--cs-slot))] in-data-[status=error]:data-[active]:[background-color:color-mix(in_srgb,var(--cs-danger)_20%,var(--cs-slot))] [@media(hover:hover)_and_(pointer:fine)]:group-hover/row:in-data-[status=idle]:not-in-data-[disabled]:not-data-[active]:[background-color:color-mix(in_srgb,var(--cs-ink)_4%,var(--cs-slot))]"
|
||||
data-active={active ? '' : undefined}
|
||||
data-filled={char ? '' : undefined}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<motion.span
|
||||
className="absolute inset-0 origin-center rounded-[inherit] bg-[var(--cs-accent)] [transition:background-color_200ms_ease] in-data-[status=error]:bg-[var(--cs-danger)]"
|
||||
style={{ transform: fill }}
|
||||
/>
|
||||
{shown ? (
|
||||
<motion.span
|
||||
className="absolute inset-0 z-[2] grid place-items-center text-[length:var(--cs-font)] leading-none font-semibold tabular-nums [font-family:inherit] [color:var(--cs-digit)] motion-reduce:transform-none! motion-reduce:[transition:opacity_150ms_ease]"
|
||||
style={{ transform: lift, opacity: ink }}
|
||||
>
|
||||
{shown}
|
||||
</motion.span>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default CodeSlots;
|
||||
Reference in new issue
Block a user