UNPKG

react-split-flap

Version:
294 lines (278 loc) 15.3 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var jsxRuntime = require('react/jsx-runtime'); var React = require('react'); function styleInject(css, ref) { if ( ref === void 0 ) ref = {}; var insertAt = ref.insertAt; if (typeof document === 'undefined') { return; } var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; if (insertAt === 'top') { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } } var css_248z = "@keyframes flapDownTop{0%{transform:rotateX(0deg) translateZ(0)}50%,to{transform:rotateX(90deg) translateZ(0)}}@keyframes flapDownBottom{0%,50%{transform:rotateX(90deg) translateZ(0)}90%{transform:rotateX(20deg) translateZ(0)}80%,to{transform:rotateX(0deg) translateZ(0)}}.split-flap-display{align-items:center;display:flex;flex-direction:row;gap:2px;justify-content:center;perspective:1000px}.split-flap-display.words-mode{gap:0}.split-flap-digit{background-color:#182216;border-radius:4px;box-sizing:content-box;color:var(--flap-font-color,#fff);display:inline-block;font-family:Monaco,Menlo,Ubuntu Mono,monospace,sans-serif;font-weight:700;height:1em;line-height:1em;overflow:hidden;position:relative;text-align:center;transform-style:preserve-3d;width:1.1ch}.split-flap-digit[data-mode=alpha]{width:1.7ch}.split-flap-digit[data-mode=words]{min-width:2ch;width:100%}.split-flap-part{align-items:center;animation-fill-mode:forwards;backface-visibility:hidden;background-color:inherit;background:var(--flap-background,linear-gradient(145deg,#1a1a1a 0,#191919 15%));border:1px solid #333;border-radius:inherit;box-shadow:0 1px 3px rgba(0,0,0,.3);box-sizing:border-box;display:flex;height:100%;justify-content:center;position:absolute;transform-origin:center;width:100%;z-index:1}.split-flap-part.animated{animation-duration:.3s;z-index:2}.split-flap-part.top{align-items:flex-start;clip-path:polygon(0 50%,100% 50%,100% 0,0 0);padding-bottom:.05em}.split-flap-part.top.animated{opacity:.4;transform:rotateX(50deg)}.split-flap-part.top.animated.final{animation-name:flapDownTop;animation-timing-function:ease-in;opacity:1}.split-flap-part.bottom{align-items:flex-end;clip-path:polygon(0 100%,100% 100%,100% 50%,0 50%)}.split-flap-part.bottom.animated.final{animation-name:flapDownBottom;animation-timing-function:ease-out}.split-flap-char{display:block;font-size:inherit;line-height:1}.split-flap-hinge{background:#000;box-shadow:0 0 2px rgba(0,0,0,.3);box-sizing:border-box;height:1px;left:0;position:absolute;right:0;top:50%;transform:translateY(-50%);z-index:3}.split-flap-display.light .split-flap-digit{background:#f0f0f0;color:var(--flap-font-color,#333)}.split-flap-display.light .split-flap-part{background:var(--flap-background,linear-gradient(145deg,#f8f8f8,#e8e8e8));border:1px solid #ccc;box-shadow:inset 2px 2px 4px hsla(0,0%,100%,.5),inset -2px -2px 10px rgba(0,0,0,.1),0 1px 3px rgba(0,0,0,.2)}.split-flap-display.light .split-flap-hinge{background:linear-gradient(90deg,#b9b9b9,#cfcfcf 50%,#b9b9b9);box-shadow:0 0 2px rgba(0,0,0,.2)}.split-flap-display.dark .split-flap-digit{background:#1a1a1a;color:var(--flap-font-color,#fff)}.split-flap-display.dark .split-flap-part{background:var(--flap-background,linear-gradient(184deg,#212121 0,#191919 25%));border:1px solid #222}.split-flap-display.dark .split-flap-hinge{background:linear-gradient(90deg,#333,#222 50%,#333)}.split-flap-display.small{font-size:20px}.split-flap-display.medium{font-size:36px}.split-flap-display.large{font-size:54px}.split-flap-display.xlarge{font-size:84px}.split-flap-display.long-flap-mode{font-size:inherit}.split-flap-digit:before{background:linear-gradient(135deg,hsla(0,0%,100%,.1),hsla(0,0%,100%,.05) 25%,hsla(0,0%,100%,0) 50%,rgba(0,0,0,.05) 75%,rgba(0,0,0,.1));border-radius:inherit;bottom:0;content:\"\";left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:4}@media (prefers-reduced-motion:reduce){.split-flap-part.animated{animation-duration:50ms}.split-flap-part.top.animated{opacity:1;transform:none}}@media (prefers-contrast:high){.split-flap-digit{border:2px solid}.split-flap-display.light .split-flap-digit{background:#fff;color:#000}.split-flap-display.dark .split-flap-digit{background:#000;color:#fff}}.split-flap-display.words-mode .split-flap-digit{border-radius:0;margin:0}.split-flap-display.words-mode .split-flap-digit:first-child{border-bottom-left-radius:4px;border-top-left-radius:4px}.split-flap-display.words-mode .split-flap-digit:last-child{border-bottom-right-radius:4px;border-top-right-radius:4px}.split-flap-display.words-mode .split-flap-digit:only-child{border-radius:4px}.split-flap-display:focus-within .split-flap-digit{box-shadow:0 0 0 2px rgba(59,130,246,.5)}.split-flap-part:not(.animated){transition:background-color .2s ease,border-color .2s ease}.split-flap-display.long-flap-mode .split-flap-digit{height:var(--digit-height,auto);min-height:var(--digit-height,auto);min-width:var(--digit-width,auto);width:var(--digit-width,auto)}.split-flap-display.long-flap-mode .split-flap-char{align-items:center;display:flex;height:100%;justify-content:center;width:100%}"; styleInject(css_248z); const Flap = ({ bottom, animated, final, hinge, children }) => { const classes = ['split-flap-part', bottom ? 'bottom' : 'top', animated ? 'animated' : '', final ? 'final' : ''] .filter(Boolean) .join(' '); return (jsxRuntime.jsxs("div", { className: classes, children: [jsxRuntime.jsx("span", { className: "split-flap-char", children: children }), hinge && jsxRuntime.jsx("div", { className: "split-flap-hinge", "data-kind": "hinge" })] })); }; const FlapDigit = ({ value, prevValue, final, mode, hinge, digitWidth }) => { const digitStyle = digitWidth ? { width: `${digitWidth}px` } : {}; return (jsxRuntime.jsxs("div", { className: "split-flap-digit", "data-kind": "digit", "data-mode": mode, style: digitStyle, children: [jsxRuntime.jsx(Flap, { hinge: hinge, children: value }), jsxRuntime.jsx(Flap, { bottom: true, hinge: hinge, children: prevValue }), jsxRuntime.jsx(Flap, { animated: true, final: final, hinge: hinge, children: prevValue }, `top-${(prevValue === null || prevValue === void 0 ? void 0 : prevValue.key) || String(prevValue)}`), final && (jsxRuntime.jsx(Flap, { bottom: true, animated: true, final: true, hinge: hinge, children: value }, `bottom-${(value === null || value === void 0 ? void 0 : value.key) || String(value)}`))] })); }; const FlapStack = ({ stack, value, timing, mode, hinge, digitWidth }) => { const [cursor, setCursor] = React.useState({ current: -1, previous: -1, target: 0, }); // Reset cursor when stack changes React.useEffect(() => { setCursor({ current: -1, previous: -1, target: 0, }); }, [stack]); // Animation sequence when value changes React.useEffect(() => { // Validate index and set target const target = Math.max(0, Math.min(value, stack.length - 1)); const currentStack = stack; let { current } = cursor; // Continue animation if not at target if (current !== target) { const timer = setInterval(() => { setCursor((prevCursor) => { const newPrevious = prevCursor.current; const newCurrent = prevCursor.current >= currentStack.length - 1 ? 0 : prevCursor.current + 1; if (newCurrent === target) { clearInterval(timer); } return { current: newCurrent, previous: newPrevious, target, }; }); }, timing); return () => { clearInterval(timer); }; } }, [stack, value, timing]); const { current, previous, target } = cursor; return (jsxRuntime.jsx(FlapDigit, { value: stack[current] || '', prevValue: stack[previous] || '', final: current === target, mode: mode, hinge: hinge, digitWidth: digitWidth })); }; const Presets = { NUM: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], ALPHANUM: [ ' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ], SPECIAL: [ ' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '.', ',', '?', '-', '_', ], }; // Main display component const SplitFlap = ({ value, chars = Presets.NUM, length, padChar = ' ', padMode = 'auto', digitWidth, timing = 60, hinge = true, theme = 'default', size = 'medium', className = '', style, background, fontColor, render, }) => { const [displayValue, setDisplayValue] = React.useState(''); const [mode, setMode] = React.useState('num'); // Determine mode based on chars content React.useEffect(() => { // Check if chars contains only numbers const isNumeric = chars.every((char) => /^[\s0-9]$/.test(char)); if (isNumeric) { setMode('num'); } else { setMode('alpha'); } }, [chars]); // Process value with padding React.useEffect(() => { let processedValue = value; // Handle padding if (processedValue.length < length) { const padCount = length - processedValue.length; const padding = padChar.repeat(padCount); const padEnd = padMode === 'auto' ? /^[0-9.,+-]*$/.test(processedValue) : padMode === 'end'; processedValue = padEnd ? padding + processedValue : processedValue + padding; } else if (processedValue.length > length) { processedValue = processedValue.substring(0, length); } // Convert to uppercase for string chars processedValue = processedValue.toUpperCase(); setDisplayValue(processedValue); }, [value, length, padChar, padMode, mode]); // Helper function to find index of char in stack const findCharIndexInStack = (char) => { // First try exact match const index = chars.findIndex((item) => item === char); if (index !== -1) { return { index, stack: chars }; } // If no exact match, create extended stack with the missing character // This will cause a full rotation through all chars before showing the fallback const extendedStack = [...chars, char]; return { index: extendedStack.length - 1, stack: extendedStack }; }; const renderDigits = React.useCallback(() => { const digits = []; // When length=1, display the entire value as a single flap if (length === 1) { // Use displayValue which already includes padding and case conversion const { index: stackIndex, stack } = findCharIndexInStack(displayValue); digits.push(jsxRuntime.jsx(FlapStack, { stack: stack, value: stackIndex, mode: "words", timing: timing, hinge: hinge, digitWidth: digitWidth }, 0)); } else { // When length>1, display each character separately for (let i = 0; i < length; i++) { const char = displayValue[i] || padChar; const { index: stackIndex, stack } = findCharIndexInStack(char); digits.push(jsxRuntime.jsx(FlapStack, { stack: stack, value: stackIndex, mode: mode, timing: timing, hinge: hinge, digitWidth: digitWidth }, i)); } } return digits; }, [displayValue, chars, mode, length, padChar, timing, hinge, digitWidth, value]); const displayClasses = [ 'split-flap-display', theme !== 'default' ? theme : '', size, // Always include the size class length === 1 ? 'words-mode' : '', className, ] .filter(Boolean) .join(' '); // Combine styles with custom background and font color const combinedStyle = Object.assign(Object.assign(Object.assign({}, style), (background && { '--flap-background': background })), (fontColor && { '--flap-font-color': fontColor })); const content = (jsxRuntime.jsx("div", { className: displayClasses, style: combinedStyle, "aria-hidden": "true", "aria-label": value, children: renderDigits() })); return render ? render(content) : content; }; const LongFlap = ({ flaps, displayId, digitWidth, digitHeight = 50, timing = 60, hinge = true, theme = 'default', size = 'medium', className = '', style, background, fontColor, render, }) => { // Memoize the stack to maintain stable references const flapStack = React.useMemo(() => flaps.map((f) => jsxRuntime.jsx(React.Fragment, { children: f.component }, f.id)), [flaps]); // Memoize the current index to maintain stable reference const currentIndex = React.useMemo(() => { const flapIndex = flaps.findIndex((f) => f.id === displayId); return flapIndex !== -1 ? flapIndex : 0; }, [flaps, displayId]); const displayClasses = ['split-flap-display', theme !== 'default' ? theme : '', size, 'long-flap-mode', className] .filter(Boolean) .join(' '); // Add custom styles for LongFlap to ensure proper display const longFlapStyle = Object.assign(Object.assign(Object.assign(Object.assign({}, style), { '--digit-width': digitWidth ? `${digitWidth}px` : 'auto', '--digit-height': digitHeight ? `${digitHeight}px` : 'auto' }), (background && { '--flap-background': background })), (fontColor && { '--flap-font-color': fontColor })); const content = (jsxRuntime.jsx("div", { className: displayClasses, style: longFlapStyle, "aria-hidden": "true", children: jsxRuntime.jsx(FlapStack, { stack: flapStack, value: currentIndex, timing: timing, mode: "words", hinge: hinge, digitWidth: digitWidth }) })); return render ? render(content) : content; }; exports.Flap = Flap; exports.FlapDigit = FlapDigit; exports.FlapStack = FlapStack; exports.LongFlap = LongFlap; exports.Presets = Presets; exports.SplitFlap = SplitFlap; exports.default = SplitFlap; //# sourceMappingURL=index.js.map