@oalacea/chaosui 0.4.0 → 0.5.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 (68) hide show
  1. package/bin/cli.js +32 -2
  2. package/components/buttons/cta-brutal/cta-brutal.module.css +81 -0
  3. package/components/buttons/cta-brutal/index.tsx +56 -0
  4. package/components/buttons/dead-button/dead-button.module.css +111 -0
  5. package/components/buttons/dead-button/index.tsx +47 -0
  6. package/components/buttons/deeper-button/deeper-button.module.css +76 -0
  7. package/components/buttons/deeper-button/index.tsx +51 -0
  8. package/components/buttons/dual-choice/dual-choice.module.css +90 -0
  9. package/components/buttons/dual-choice/index.tsx +54 -0
  10. package/components/buttons/tension-bar/index.tsx +79 -0
  11. package/components/buttons/tension-bar/tension-bar.module.css +105 -0
  12. package/components/decorative/coffee-stain/coffee-stain.module.css +24 -0
  13. package/components/decorative/coffee-stain/index.tsx +55 -0
  14. package/components/decorative/ornaments/index.tsx +51 -0
  15. package/components/decorative/ornaments/ornaments.module.css +33 -0
  16. package/components/decorative/rune-symbols/index.tsx +55 -0
  17. package/components/decorative/rune-symbols/rune-symbols.module.css +22 -0
  18. package/components/layout/horizontal-scroll/horizontal-scroll.module.css +30 -0
  19. package/components/layout/horizontal-scroll/index.tsx +78 -0
  20. package/components/layout/spec-grid/index.tsx +56 -0
  21. package/components/layout/spec-grid/spec-grid.module.css +21 -0
  22. package/components/layout/tower-pricing/index.tsx +56 -0
  23. package/components/layout/tower-pricing/tower-pricing.module.css +27 -0
  24. package/components/layout/tracklist/index.tsx +45 -0
  25. package/components/layout/tracklist/tracklist.module.css +24 -0
  26. package/components/layout/void-frame/index.tsx +32 -0
  27. package/components/layout/void-frame/void-frame.module.css +38 -0
  28. package/components/navigation/brutal-nav/brutal-nav.module.css +85 -0
  29. package/components/navigation/brutal-nav/index.tsx +71 -0
  30. package/components/navigation/progress-dots/index.tsx +55 -0
  31. package/components/navigation/progress-dots/progress-dots.module.css +91 -0
  32. package/components/navigation/scattered-nav/index.tsx +59 -0
  33. package/components/navigation/scattered-nav/scattered-nav.module.css +113 -0
  34. package/components/navigation/scroll-indicator/index.tsx +58 -0
  35. package/components/navigation/scroll-indicator/scroll-indicator.module.css +82 -0
  36. package/components/navigation/vertical-nav/index.tsx +59 -0
  37. package/components/navigation/vertical-nav/vertical-nav.module.css +98 -0
  38. package/components/text/ascii-art/css/ascii-art.module.css +173 -0
  39. package/components/text/ascii-art/css/index.tsx +116 -0
  40. package/components/text/ascii-art/tailwind/index.tsx +124 -0
  41. package/components/text/blood-drip/css/blood-drip.module.css +142 -0
  42. package/components/text/blood-drip/css/index.tsx +113 -0
  43. package/components/text/blood-drip/tailwind/index.tsx +133 -0
  44. package/components/text/char-glitch/css/char-glitch.module.css +124 -0
  45. package/components/text/char-glitch/css/index.tsx +153 -0
  46. package/components/text/char-glitch/tailwind/index.tsx +126 -0
  47. package/components/text/countdown-display/css/countdown-display.module.css +179 -0
  48. package/components/text/countdown-display/css/index.tsx +190 -0
  49. package/components/text/countdown-display/tailwind/index.tsx +155 -0
  50. package/components/text/giant-layers/css/giant-layers.module.css +156 -0
  51. package/components/text/giant-layers/css/index.tsx +97 -0
  52. package/components/text/giant-layers/tailwind/index.tsx +111 -0
  53. package/components/text/reveal-text/css/index.tsx +180 -0
  54. package/components/text/reveal-text/css/reveal-text.module.css +129 -0
  55. package/components/text/reveal-text/tailwind/index.tsx +135 -0
  56. package/components/text/rotate-text/css/index.tsx +139 -0
  57. package/components/text/rotate-text/css/rotate-text.module.css +162 -0
  58. package/components/text/rotate-text/tailwind/index.tsx +127 -0
  59. package/components/text/strike-reveal/css/index.tsx +124 -0
  60. package/components/text/strike-reveal/css/strike-reveal.module.css +139 -0
  61. package/components/text/strike-reveal/tailwind/index.tsx +138 -0
  62. package/components/text/terminal-output/css/index.tsx +179 -0
  63. package/components/text/terminal-output/css/terminal-output.module.css +203 -0
  64. package/components/text/terminal-output/tailwind/index.tsx +174 -0
  65. package/components/text/typing-text/css/index.tsx +115 -0
  66. package/components/text/typing-text/css/typing-text.module.css +84 -0
  67. package/components/text/typing-text/tailwind/index.tsx +126 -0
  68. package/package.json +1 -1
@@ -0,0 +1,79 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes } from 'react';
4
+ import styles from './tension-bar.module.css';
5
+
6
+ export interface TensionBarProps extends HTMLAttributes<HTMLDivElement> {
7
+ value: number;
8
+ max?: number;
9
+ labelLeft?: string;
10
+ labelRight?: string;
11
+ showPercentage?: boolean;
12
+ showMarkers?: boolean;
13
+ markerCount?: number;
14
+ variant?: 'default' | 'gold' | 'danger' | 'segmented';
15
+ size?: 'sm' | 'md' | 'lg';
16
+ innerText?: string;
17
+ animated?: boolean;
18
+ dangerThreshold?: number;
19
+ }
20
+
21
+ export const TensionBar = forwardRef<HTMLDivElement, TensionBarProps>(
22
+ ({ value, max = 100, labelLeft, labelRight, showPercentage = false, showMarkers = false, markerCount = 10, variant = 'default', size = 'md', innerText, animated = false, dangerThreshold = 80, className, ...props }, ref) => {
23
+ const percentage = Math.min(100, Math.max(0, (value / max) * 100));
24
+ const isHigh = variant === 'danger' && percentage >= dangerThreshold;
25
+
26
+ const containerClasses = [
27
+ styles.container,
28
+ variant === 'gold' && styles.variantGold,
29
+ variant === 'danger' && styles.variantDanger,
30
+ variant === 'segmented' && styles.variantSegmented,
31
+ size === 'sm' && styles.sizeSm,
32
+ size === 'lg' && styles.sizeLg,
33
+ innerText && styles.withText,
34
+ animated && styles.animated,
35
+ isHigh && styles.high,
36
+ className,
37
+ ].filter(Boolean).join(' ');
38
+
39
+ const renderSegmented = () => {
40
+ const segments = [];
41
+ const filledCount = Math.floor((percentage / 100) * markerCount);
42
+ for (let i = 0; i < markerCount; i++) {
43
+ segments.push(
44
+ <div key={i} className={`${styles.segment} ${i < filledCount ? styles.segmentFilled : ''}`} />
45
+ );
46
+ }
47
+ return segments;
48
+ };
49
+
50
+ return (
51
+ <div ref={ref} className={containerClasses} {...props}>
52
+ {(labelLeft || labelRight) && (
53
+ <div className={styles.labels}>
54
+ <span className={styles.labelLeft}>{labelLeft}</span>
55
+ <span className={styles.labelRight}>{labelRight}</span>
56
+ </div>
57
+ )}
58
+
59
+ <div className={styles.track}>
60
+ {innerText && <span className={styles.innerText}>{innerText}</span>}
61
+ {variant === 'segmented' ? renderSegmented() : <div className={styles.fill} style={{ width: `${percentage}%` }} />}
62
+ {showPercentage && <span className={styles.percentage}>{Math.round(percentage)}%</span>}
63
+ </div>
64
+
65
+ {showMarkers && variant !== 'segmented' && (
66
+ <div className={styles.markers}>
67
+ {Array.from({ length: markerCount + 1 }).map((_, i) => {
68
+ const markerPercent = (i / markerCount) * 100;
69
+ return <div key={i} className={`${styles.marker} ${markerPercent <= percentage ? styles.markerActive : ''}`} />;
70
+ })}
71
+ </div>
72
+ )}
73
+ </div>
74
+ );
75
+ }
76
+ );
77
+
78
+ TensionBar.displayName = 'TensionBar';
79
+ export default TensionBar;
@@ -0,0 +1,105 @@
1
+ /* tension-bar - Barre de tension/progression dramatique */
2
+ .container {
3
+ width: 100%;
4
+ position: relative;
5
+ font-family: var(--chaos-font-mono, 'Space Mono', monospace);
6
+ }
7
+
8
+ .labels {
9
+ display: flex;
10
+ justify-content: space-between;
11
+ margin-bottom: 0.5rem;
12
+ font-size: 0.65rem;
13
+ letter-spacing: 0.2em;
14
+ text-transform: uppercase;
15
+ }
16
+
17
+ .labelLeft { color: var(--chaos-bone-dark, #666); }
18
+ .labelRight { color: var(--chaos-blood, #ff0040); }
19
+
20
+ .track {
21
+ height: 8px;
22
+ background: var(--chaos-iron, #222);
23
+ position: relative;
24
+ overflow: hidden;
25
+ }
26
+
27
+ .fill {
28
+ height: 100%;
29
+ background: var(--chaos-blood, #ff0040);
30
+ transition: width 0.5s ease-out;
31
+ position: relative;
32
+ }
33
+
34
+ .fill::after {
35
+ content: '';
36
+ position: absolute;
37
+ right: 0;
38
+ top: 0;
39
+ bottom: 0;
40
+ width: 20px;
41
+ background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.3));
42
+ animation: shimmer 1.5s infinite;
43
+ }
44
+
45
+ @keyframes shimmer {
46
+ 0% { opacity: 0; }
47
+ 50% { opacity: 1; }
48
+ 100% { opacity: 0; }
49
+ }
50
+
51
+ .percentage {
52
+ position: absolute;
53
+ right: 0;
54
+ top: -1.5rem;
55
+ font-size: 0.7rem;
56
+ color: var(--chaos-blood, #ff0040);
57
+ font-weight: bold;
58
+ }
59
+
60
+ .markers { display: flex; justify-content: space-between; margin-top: 0.3rem; }
61
+ .marker { width: 1px; height: 6px; background: var(--chaos-iron, #333); }
62
+ .markerActive { background: var(--chaos-blood, #ff0040); }
63
+
64
+ .variantGold .fill { background: linear-gradient(to right, var(--chaos-gold-dark, #8b7017), var(--chaos-gold, #c9a227)); }
65
+ .variantGold .labelRight, .variantGold .percentage { color: var(--chaos-gold, #c9a227); }
66
+ .variantGold .markerActive { background: var(--chaos-gold, #c9a227); }
67
+
68
+ .variantDanger .fill { background: var(--chaos-blood, #ff0040); }
69
+ .variantDanger.high .fill { animation: dangerPulse 0.5s ease-in-out infinite; }
70
+ .variantDanger.high .track { animation: dangerGlow 0.5s ease-in-out infinite; }
71
+
72
+ @keyframes dangerPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
73
+ @keyframes dangerGlow {
74
+ 0%, 100% { box-shadow: 0 0 5px var(--chaos-blood, #ff0040); }
75
+ 50% { box-shadow: 0 0 15px var(--chaos-blood, #ff0040); }
76
+ }
77
+
78
+ .variantSegmented .track { display: flex; gap: 2px; background: transparent; }
79
+ .segment { flex: 1; height: 8px; background: var(--chaos-iron, #222); transition: background 0.3s; }
80
+ .segmentFilled { background: var(--chaos-blood, #ff0040); }
81
+
82
+ .sizeSm .track, .sizeSm .segment { height: 4px; }
83
+ .sizeLg .track, .sizeLg .segment { height: 16px; }
84
+
85
+ .withText .track { height: 30px; display: flex; align-items: center; padding: 0 1rem; }
86
+ .innerText {
87
+ position: absolute;
88
+ left: 1rem;
89
+ font-size: 0.7rem;
90
+ color: var(--chaos-bone, #e8e8e8);
91
+ z-index: 1;
92
+ text-transform: uppercase;
93
+ letter-spacing: 0.1em;
94
+ }
95
+
96
+ .animated .fill {
97
+ background: repeating-linear-gradient(45deg, var(--chaos-blood, #ff0040), var(--chaos-blood, #ff0040) 10px, #cc0033 10px, #cc0033 20px);
98
+ background-size: 200% 100%;
99
+ animation: stripeMove 1s linear infinite;
100
+ }
101
+
102
+ @keyframes stripeMove {
103
+ 0% { background-position: 0 0; }
104
+ 100% { background-position: 28px 0; }
105
+ }
@@ -0,0 +1,24 @@
1
+ .container { position: relative; pointer-events: none; }
2
+ .overlay { position: fixed; inset: 0; z-index: 1; }
3
+ .inline { position: relative; width: 100%; height: 100%; }
4
+ .stain { position: absolute; border-radius: 50%; pointer-events: none; }
5
+ .stain1 { width: 150px; height: 140px; background: radial-gradient(ellipse at 40% 40%, rgba(61, 43, 31, 0.25) 0%, rgba(61, 43, 31, 0.12) 40%, transparent 70%); transform: rotate(15deg); }
6
+ .stain2 { width: 100px; height: 90px; background: radial-gradient(ellipse at 60% 50%, rgba(45, 31, 21, 0.25) 0%, rgba(45, 31, 21, 0.12) 50%, transparent 75%); transform: rotate(-20deg); }
7
+ .stain3 { width: 80px; height: 85px; background: radial-gradient(ellipse at 50% 50%, rgba(61, 43, 31, 0.18) 0%, transparent 60%); }
8
+ .stain4 { width: 120px; height: 110px; background: radial-gradient(ellipse at 45% 55%, rgba(50, 35, 25, 0.2) 0%, rgba(50, 35, 25, 0.1) 45%, transparent 70%); transform: rotate(10deg); }
9
+ .stain5 { width: 60px; height: 55px; background: radial-gradient(circle, rgba(61, 43, 31, 0.15) 0%, transparent 65%); }
10
+ .ring { border-radius: 50%; background: transparent; box-shadow: inset 0 0 10px rgba(61, 43, 31, 0.2), 0 0 15px rgba(61, 43, 31, 0.15); border: 2px solid rgba(61, 43, 31, 0.15); }
11
+ .ring1 { width: 80px; height: 80px; }
12
+ .ring2 { width: 70px; height: 70px; }
13
+ .agedPaper { position: absolute; inset: 0; background: linear-gradient(135deg, transparent 0%, rgba(139, 115, 85, 0.03) 25%, transparent 50%, rgba(139, 115, 85, 0.05) 75%, transparent 100%); pointer-events: none; }
14
+ .paperTexture { position: absolute; inset: 0; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"); opacity: 0.05; mix-blend-mode: overlay; pointer-events: none; }
15
+ .burnEdges { position: absolute; inset: 0; box-shadow: inset 0 0 100px rgba(0, 0, 0, 0.3); pointer-events: none; }
16
+ .edgeLeft { position: absolute; left: 0; top: 0; bottom: 0; width: 60px; background: linear-gradient(90deg, rgba(26, 21, 16, 0.8) 0%, transparent 100%); }
17
+ .edgeRight { position: absolute; right: 0; top: 0; bottom: 0; width: 60px; background: linear-gradient(-90deg, rgba(26, 21, 16, 0.8) 0%, transparent 100%); }
18
+ .light .stain { opacity: 0.5; }
19
+ .medium .stain { opacity: 0.75; }
20
+ .heavy .stain { opacity: 1; }
21
+ .coffee { --stain-color: rgba(61, 43, 31, 1); }
22
+ .tea { --stain-color: rgba(139, 115, 85, 1); }
23
+ .wine { --stain-color: rgba(100, 30, 40, 1); }
24
+ .ink { --stain-color: rgba(20, 20, 40, 1); }
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes, useMemo } from 'react';
4
+ import styles from './coffee-stain.module.css';
5
+
6
+ export interface StainConfig {
7
+ type?: 'stain' | 'ring';
8
+ size?: 'sm' | 'md' | 'lg';
9
+ position: { top?: string; right?: string; bottom?: string; left?: string };
10
+ rotation?: number;
11
+ }
12
+
13
+ export interface CoffeeStainProps extends HTMLAttributes<HTMLDivElement> {
14
+ mode?: 'overlay' | 'inline';
15
+ intensity?: 'light' | 'medium' | 'heavy';
16
+ variant?: 'coffee' | 'tea' | 'wine' | 'ink';
17
+ count?: number;
18
+ stains?: StainConfig[];
19
+ agedPaper?: boolean;
20
+ paperTexture?: boolean;
21
+ burnEdges?: boolean;
22
+ edgeDarkening?: boolean;
23
+ }
24
+
25
+ const generateRandomStains = (count: number): StainConfig[] => {
26
+ return Array.from({ length: count }, () => ({
27
+ type: Math.random() > 0.7 ? 'ring' : 'stain',
28
+ size: ['sm', 'md', 'lg'][Math.floor(Math.random() * 3)] as 'sm' | 'md' | 'lg',
29
+ position: { top: `${Math.random() * 80 + 10}%`, left: `${Math.random() * 80 + 10}%` },
30
+ rotation: Math.random() * 360,
31
+ }));
32
+ };
33
+
34
+ export const CoffeeStain = forwardRef<HTMLDivElement, CoffeeStainProps>(
35
+ ({ mode = 'overlay', intensity = 'medium', variant = 'coffee', count = 3, stains: customStains, agedPaper = false, paperTexture = false, burnEdges = false, edgeDarkening = false, className, ...props }, ref) => {
36
+ const stains = useMemo(() => customStains || generateRandomStains(count), [customStains, count]);
37
+ const stainStyles = ['stain1', 'stain2', 'stain3', 'stain4', 'stain5'];
38
+
39
+ return (
40
+ <div ref={ref} className={`${styles.container} ${styles[mode]} ${styles[intensity]} ${styles[variant]} ${className || ''}`} {...props}>
41
+ {stains.map((stain, i) => {
42
+ const styleClass = stain.type === 'ring' ? `${styles.ring} ${stain.size === 'lg' ? styles.ring1 : styles.ring2}` : `${styles.stain} ${styles[stainStyles[i % stainStyles.length]]}`;
43
+ return <div key={i} className={styleClass} style={{ ...stain.position, transform: `rotate(${stain.rotation || 0}deg)` }} />;
44
+ })}
45
+ {agedPaper && <div className={styles.agedPaper} />}
46
+ {paperTexture && <div className={styles.paperTexture} />}
47
+ {burnEdges && <div className={styles.burnEdges} />}
48
+ {edgeDarkening && <><div className={styles.edgeLeft} /><div className={styles.edgeRight} /></>}
49
+ </div>
50
+ );
51
+ }
52
+ );
53
+
54
+ CoffeeStain.displayName = 'CoffeeStain';
55
+ export default CoffeeStain;
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes } from 'react';
4
+ import styles from './ornaments.module.css';
5
+
6
+ export const ORNAMENT_SYMBOLS = {
7
+ cross: '✝', maltese: '✠', orthodox: '☦', fleurDeLis: '⚜', star: '✦',
8
+ diamond: '◆', heart: '❧', leaf: '❦', dagger: '†', doubleDagger: '‡',
9
+ asterisk: '✽', florette: '✿', skull: '☠', crown: '♔', swords: '⚔',
10
+ };
11
+
12
+ export interface OrnamentsProps extends HTMLAttributes<HTMLDivElement> {
13
+ type?: 'divider' | 'corner' | 'frame' | 'fleuron' | 'symbols';
14
+ symbol?: keyof typeof ORNAMENT_SYMBOLS | string;
15
+ symbols?: (keyof typeof ORNAMENT_SYMBOLS | string)[];
16
+ variant?: 'gold' | 'bone' | 'blood' | 'iron';
17
+ size?: 'sm' | 'md' | 'lg';
18
+ animated?: boolean;
19
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'all';
20
+ }
21
+
22
+ const getSymbol = (s: keyof typeof ORNAMENT_SYMBOLS | string): string => ORNAMENT_SYMBOLS[s as keyof typeof ORNAMENT_SYMBOLS] || s;
23
+
24
+ export const Ornaments = forwardRef<HTMLDivElement, OrnamentsProps>(
25
+ ({ type = 'divider', symbol = 'cross', symbols, variant = 'gold', size = 'md', animated = false, position = 'all', className, ...props }, ref) => {
26
+ const baseClasses = `${styles.ornament} ${styles[variant]} ${styles[size]} ${animated ? styles.animated : ''}`;
27
+ const sym = getSymbol(symbol);
28
+
29
+ if (type === 'divider') {
30
+ return (<div ref={ref} className={`${baseClasses} ${styles.divider} ${className || ''}`} {...props}><span className={styles.line} /><span className={styles.symbol}>{sym}</span><span className={styles.line} /></div>);
31
+ }
32
+ if (type === 'fleuron') {
33
+ return (<div ref={ref} className={`${baseClasses} ${className || ''}`} {...props}><span className={styles.fleuron}>{sym}</span></div>);
34
+ }
35
+ if (type === 'symbols') {
36
+ const syms = symbols || ['star', 'diamond', 'star'];
37
+ return (<div ref={ref} className={`${baseClasses} ${styles.symbols} ${className || ''}`} {...props}>{syms.map((s, i) => (<span key={i} className={styles.symbol}>{getSymbol(s)}</span>))}</div>);
38
+ }
39
+ if (type === 'corner') {
40
+ const corners = position === 'all' ? ['TopLeft', 'TopRight', 'BottomLeft', 'BottomRight'] : [position.split('-').map((p, i) => i === 0 ? p.charAt(0).toUpperCase() + p.slice(1) : p.charAt(0).toUpperCase() + p.slice(1)).join('')];
41
+ return (<>{corners.map((pos) => (<div key={pos} ref={pos === corners[0] ? ref : undefined} className={`${styles.corner} ${styles[`corner${pos}`]} ${styles[variant]} ${className || ''}`} {...props}><span className={styles.cornerInner}><span className={styles.cornerSymbol}>{sym}</span></span></div>))}</>);
42
+ }
43
+ if (type === 'frame') {
44
+ return (<div ref={ref} className={`${styles.frame} ${styles[variant]} ${className || ''}`} {...props}><div className={styles.frameTop}><span className={styles.frameLine} /><span className={styles.frameSymbol}>{sym}</span><span className={styles.frameLine} /></div><div className={styles.frameBottom}><span className={styles.frameLine} /><span className={styles.frameSymbol}>{sym}</span><span className={styles.frameLine} /></div></div>);
45
+ }
46
+ return null;
47
+ }
48
+ );
49
+
50
+ Ornaments.displayName = 'Ornaments';
51
+ export default Ornaments;
@@ -0,0 +1,33 @@
1
+ .ornament { --ornament-color: #c9a227; display: flex; align-items: center; justify-content: center; color: var(--ornament-color); }
2
+ .divider { gap: 1rem; width: 100%; }
3
+ .line { flex: 1; height: 1px; background: linear-gradient(90deg, transparent, var(--ornament-color), transparent); }
4
+ .symbol { font-size: 1.25rem; opacity: 0.8; }
5
+ .corner { position: absolute; width: 40px; height: 40px; }
6
+ .cornerTopLeft { top: 0; left: 0; }
7
+ .cornerTopRight { top: 0; right: 0; transform: scaleX(-1); }
8
+ .cornerBottomLeft { bottom: 0; left: 0; transform: scaleY(-1); }
9
+ .cornerBottomRight { bottom: 0; right: 0; transform: scale(-1); }
10
+ .cornerInner { position: absolute; top: 0; left: 0; }
11
+ .cornerInner::before { content: ''; position: absolute; top: 8px; left: 0; width: 30px; height: 1px; background: var(--ornament-color); }
12
+ .cornerInner::after { content: ''; position: absolute; top: 0; left: 8px; width: 1px; height: 30px; background: var(--ornament-color); }
13
+ .cornerSymbol { position: absolute; top: 4px; left: 4px; font-size: 0.75rem; }
14
+ .fleuron { font-family: 'Times New Roman', serif; font-size: 2rem; opacity: 0.7; }
15
+ .frame { position: absolute; inset: 0; pointer-events: none; }
16
+ .frameTop, .frameBottom { position: absolute; left: 50%; transform: translateX(-50%); display: flex; align-items: center; gap: 0.5rem; }
17
+ .frameTop { top: -0.5em; }
18
+ .frameBottom { bottom: -0.5em; }
19
+ .frameLine { width: 60px; height: 1px; background: var(--ornament-color); }
20
+ .frameSymbol { font-size: 0.875rem; }
21
+ .gold { --ornament-color: #c9a227; }
22
+ .bone { --ornament-color: #d4c5a9; }
23
+ .blood { --ornament-color: #8b1a1a; }
24
+ .iron { --ornament-color: #4a4a4a; }
25
+ .symbols { display: flex; gap: 0.5rem; opacity: 0.6; }
26
+ .animated .symbol, .animated .fleuron { animation: ornamentPulse 3s ease-in-out infinite; }
27
+ @keyframes ornamentPulse { 0%, 100% { opacity: 0.5; } 50% { opacity: 0.9; } }
28
+ .sm .symbol { font-size: 0.875rem; }
29
+ .sm .fleuron { font-size: 1.25rem; }
30
+ .md .symbol { font-size: 1.25rem; }
31
+ .md .fleuron { font-size: 2rem; }
32
+ .lg .symbol { font-size: 1.75rem; }
33
+ .lg .fleuron { font-size: 3rem; }
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes } from 'react';
4
+ import styles from './rune-symbols.module.css';
5
+
6
+ export const RUNES = {
7
+ fehu: 'ᚠ', uruz: 'ᚢ', thurisaz: 'ᚦ', ansuz: 'ᚨ', raidho: 'ᚱ', kenaz: 'ᚲ',
8
+ gebo: 'ᚷ', wunjo: 'ᚹ', hagalaz: 'ᚺ', nauthiz: 'ᚾ', isaz: 'ᛁ', jera: 'ᛃ',
9
+ eihwaz: 'ᛇ', perthro: 'ᛈ', algiz: 'ᛉ', sowilo: 'ᛊ', tiwaz: 'ᛏ', berkano: 'ᛒ',
10
+ ehwaz: 'ᛖ', mannaz: 'ᛗ', laguz: 'ᛚ', ingwaz: 'ᛝ', dagaz: 'ᛞ', othala: 'ᛟ',
11
+ };
12
+
13
+ export interface RuneSymbolsProps extends HTMLAttributes<HTMLDivElement> {
14
+ runes?: (keyof typeof RUNES | string)[];
15
+ count?: number;
16
+ variant?: 'gold' | 'blood' | 'bone' | 'iron' | 'cyan';
17
+ animation?: 'glow' | 'floating' | 'pulsing' | 'flickering' | 'none';
18
+ direction?: 'horizontal' | 'vertical';
19
+ size?: 'sm' | 'md' | 'lg' | 'xl';
20
+ scattered?: boolean;
21
+ }
22
+
23
+ const getRandomRunes = (count: number): string[] => {
24
+ const runeValues = Object.values(RUNES);
25
+ return Array.from({ length: count }, () => runeValues[Math.floor(Math.random() * runeValues.length)]);
26
+ };
27
+
28
+ export const RuneSymbols = forwardRef<HTMLDivElement, RuneSymbolsProps>(
29
+ ({ runes, count = 6, variant = 'gold', animation = 'glow', direction = 'horizontal', size = 'md', scattered = false, className, style, ...props }, ref) => {
30
+ const resolvedRunes = runes ? runes.map(r => RUNES[r as keyof typeof RUNES] || r) : getRandomRunes(count);
31
+ const animationClass = animation !== 'none' ? styles[animation] : '';
32
+
33
+ return (
34
+ <div
35
+ ref={ref}
36
+ className={`${styles.container} ${styles[variant]} ${styles[size]} ${direction === 'vertical' ? styles.vertical : ''} ${scattered ? styles.scattered : ''} ${animationClass} ${className || ''}`}
37
+ style={style}
38
+ {...props}
39
+ >
40
+ {resolvedRunes.map((rune, i) => (
41
+ <span
42
+ key={i}
43
+ className={styles.rune}
44
+ style={{ '--delay': `${i * 0.5}s`, ...(scattered ? { left: `${Math.random() * 80 + 10}%`, top: `${Math.random() * 80 + 10}%` } : {}) } as React.CSSProperties}
45
+ >
46
+ {rune}
47
+ </span>
48
+ ))}
49
+ </div>
50
+ );
51
+ }
52
+ );
53
+
54
+ RuneSymbols.displayName = 'RuneSymbols';
55
+ export default RuneSymbols;
@@ -0,0 +1,22 @@
1
+ .container { --rune-color: #c9a227; --rune-glow: rgba(201, 162, 39, 0.5); display: flex; gap: 1.5rem; font-size: 2rem; }
2
+ .rune { color: var(--rune-color); opacity: 0.6; transition: all 0.4s ease; animation: runeGlow 4s ease-in-out infinite; animation-delay: var(--delay, 0s); }
3
+ .rune:hover { opacity: 1; text-shadow: 0 0 20px var(--rune-glow); transform: scale(1.1); }
4
+ @keyframes runeGlow { 0%, 100% { opacity: 0.4; text-shadow: none; } 50% { opacity: 0.8; text-shadow: 0 0 15px var(--rune-glow); } }
5
+ .floating .rune { animation: runeFloat 6s ease-in-out infinite; }
6
+ @keyframes runeFloat { 0%, 100% { transform: translateY(0) rotate(0deg); opacity: 0.4; } 25% { transform: translateY(-10px) rotate(5deg); opacity: 0.7; } 50% { transform: translateY(0) rotate(0deg); opacity: 0.5; } 75% { transform: translateY(10px) rotate(-5deg); opacity: 0.7; } }
7
+ .pulsing .rune { animation: runePulse 2s ease-in-out infinite; }
8
+ @keyframes runePulse { 0%, 100% { transform: scale(1); opacity: 0.5; } 50% { transform: scale(1.15); opacity: 1; text-shadow: 0 0 25px var(--rune-glow); } }
9
+ .flickering .rune { animation: runeFlicker 0.5s steps(2) infinite; }
10
+ @keyframes runeFlicker { 0% { opacity: 0.3; } 25% { opacity: 0.8; } 50% { opacity: 0.5; } 75% { opacity: 0.9; } 100% { opacity: 0.3; } }
11
+ .scattered { position: relative; width: 100%; height: 100%; }
12
+ .scattered .rune { position: absolute; }
13
+ .gold { --rune-color: #c9a227; --rune-glow: rgba(201, 162, 39, 0.5); }
14
+ .blood { --rune-color: #8b1a1a; --rune-glow: rgba(139, 26, 26, 0.5); }
15
+ .bone { --rune-color: #d4c5a9; --rune-glow: rgba(212, 197, 169, 0.5); }
16
+ .iron { --rune-color: #4a4a4a; --rune-glow: rgba(74, 74, 74, 0.5); }
17
+ .cyan { --rune-color: #00f0ff; --rune-glow: rgba(0, 240, 255, 0.5); }
18
+ .vertical { flex-direction: column; }
19
+ .sm { font-size: 1rem; gap: 0.75rem; }
20
+ .md { font-size: 2rem; gap: 1.5rem; }
21
+ .lg { font-size: 3rem; gap: 2rem; }
22
+ .xl { font-size: 4rem; gap: 3rem; }
@@ -0,0 +1,30 @@
1
+ .container {
2
+ --scroll-accent: #00f0ff;
3
+ --scroll-bg: rgba(0, 0, 0, 0.3);
4
+ --panel-gap: 2rem;
5
+ width: 100%;
6
+ overflow-x: auto;
7
+ overflow-y: hidden;
8
+ scroll-snap-type: x mandatory;
9
+ scroll-behavior: smooth;
10
+ -webkit-overflow-scrolling: touch;
11
+ scrollbar-width: thin;
12
+ scrollbar-color: var(--scroll-accent) var(--scroll-bg);
13
+ }
14
+ .container::-webkit-scrollbar { height: 4px; }
15
+ .container::-webkit-scrollbar-track { background: var(--scroll-bg); }
16
+ .container::-webkit-scrollbar-thumb { background: var(--scroll-accent); border-radius: 2px; }
17
+ .track { display: flex; gap: var(--panel-gap); padding: 1rem 0; width: max-content; }
18
+ .panel { flex: 0 0 auto; scroll-snap-align: start; }
19
+ .cyan { --scroll-accent: #00f0ff; }
20
+ .green { --scroll-accent: #00ff88; }
21
+ .amber { --scroll-accent: #ffaa00; }
22
+ .blood { --scroll-accent: #8b1a1a; }
23
+ .panelFull { width: 100vw; }
24
+ .panelLarge { width: 80vw; }
25
+ .panelMedium { width: 60vw; }
26
+ .panelSmall { width: 40vw; }
27
+ .fadeEdges { mask-image: linear-gradient(90deg, transparent 0%, black 5%, black 95%, transparent 100%); -webkit-mask-image: linear-gradient(90deg, transparent 0%, black 5%, black 95%, transparent 100%); }
28
+ .indicators { display: flex; justify-content: center; gap: 0.5rem; margin-top: 1rem; }
29
+ .indicator { width: 8px; height: 8px; border-radius: 50%; background: var(--scroll-bg); border: 1px solid var(--scroll-accent); cursor: pointer; transition: all 0.3s ease; }
30
+ .indicator:hover, .indicatorActive { background: var(--scroll-accent); box-shadow: 0 0 10px var(--scroll-accent); }
@@ -0,0 +1,78 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes, ReactNode, useRef, useState, useEffect } from 'react';
4
+ import styles from './horizontal-scroll.module.css';
5
+
6
+ export interface HorizontalScrollProps extends HTMLAttributes<HTMLDivElement> {
7
+ children: ReactNode;
8
+ variant?: 'cyan' | 'green' | 'amber' | 'blood';
9
+ panelSize?: 'full' | 'large' | 'medium' | 'small';
10
+ fadeEdges?: boolean;
11
+ showIndicators?: boolean;
12
+ gap?: number;
13
+ }
14
+
15
+ export const HorizontalScroll = forwardRef<HTMLDivElement, HorizontalScrollProps>(
16
+ ({ children, variant = 'cyan', panelSize = 'large', fadeEdges = false, showIndicators = false, gap = 2, className, style, ...props }, ref) => {
17
+ const containerRef = useRef<HTMLDivElement>(null);
18
+ const [activeIndex, setActiveIndex] = useState(0);
19
+ const [panelCount, setPanelCount] = useState(0);
20
+
21
+ useEffect(() => {
22
+ const container = containerRef.current;
23
+ if (!container) return;
24
+ const panels = container.querySelectorAll(`.${styles.panel}`);
25
+ setPanelCount(panels.length);
26
+
27
+ const handleScroll = () => {
28
+ if (!container) return;
29
+ const panelWidth = container.scrollWidth / panels.length;
30
+ setActiveIndex(Math.round(container.scrollLeft / panelWidth));
31
+ };
32
+
33
+ container.addEventListener('scroll', handleScroll, { passive: true });
34
+ return () => container.removeEventListener('scroll', handleScroll);
35
+ }, [children]);
36
+
37
+ const scrollToPanel = (index: number) => {
38
+ const container = containerRef.current;
39
+ if (!container) return;
40
+ const panelWidth = container.scrollWidth / panelCount;
41
+ container.scrollTo({ left: panelWidth * index, behavior: 'smooth' });
42
+ };
43
+
44
+ const panelSizeClass = { full: styles.panelFull, large: styles.panelLarge, medium: styles.panelMedium, small: styles.panelSmall }[panelSize];
45
+
46
+ return (
47
+ <div className={className}>
48
+ <div
49
+ ref={(node) => {
50
+ (containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
51
+ if (typeof ref === 'function') ref(node);
52
+ else if (ref) ref.current = node;
53
+ }}
54
+ className={`${styles.container} ${styles[variant]} ${fadeEdges ? styles.fadeEdges : ''}`}
55
+ style={{ '--panel-gap': `${gap}rem`, ...style } as React.CSSProperties}
56
+ {...props}
57
+ >
58
+ <div className={styles.track}>
59
+ {Array.isArray(children)
60
+ ? children.map((child, i) => <div key={i} className={`${styles.panel} ${panelSizeClass}`}>{child}</div>)
61
+ : <div className={`${styles.panel} ${panelSizeClass}`}>{children}</div>
62
+ }
63
+ </div>
64
+ </div>
65
+ {showIndicators && panelCount > 1 && (
66
+ <div className={styles.indicators}>
67
+ {Array.from({ length: panelCount }).map((_, i) => (
68
+ <button key={i} className={`${styles.indicator} ${i === activeIndex ? styles.indicatorActive : ''}`} onClick={() => scrollToPanel(i)} aria-label={`Go to panel ${i + 1}`} />
69
+ ))}
70
+ </div>
71
+ )}
72
+ </div>
73
+ );
74
+ }
75
+ );
76
+
77
+ HorizontalScroll.displayName = 'HorizontalScroll';
78
+ export default HorizontalScroll;
@@ -0,0 +1,56 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes } from 'react';
4
+ import styles from './spec-grid.module.css';
5
+
6
+ export interface SpecItem {
7
+ label: string;
8
+ value: string | number;
9
+ unit?: string;
10
+ description?: string;
11
+ icon?: string;
12
+ highlighted?: boolean;
13
+ }
14
+
15
+ export interface SpecGridProps extends HTMLAttributes<HTMLDivElement> {
16
+ specs: SpecItem[];
17
+ variant?: 'cyan' | 'green' | 'amber' | 'blood';
18
+ columns?: number;
19
+ showHeader?: boolean;
20
+ headerTitle?: string;
21
+ compact?: boolean;
22
+ striped?: boolean;
23
+ }
24
+
25
+ export const SpecGrid = forwardRef<HTMLDivElement, SpecGridProps>(
26
+ ({ specs, variant = 'cyan', columns, showHeader = false, headerTitle = 'SYSTEM SPECS', compact = false, striped = false, className, style, ...props }, ref) => {
27
+ return (
28
+ <div
29
+ ref={ref}
30
+ className={`${styles.grid} ${styles[variant]} ${compact ? styles.compact : ''} ${striped ? styles.striped : ''} ${className || ''}`}
31
+ style={{ gridTemplateColumns: columns ? `repeat(${columns}, 1fr)` : undefined, ...style }}
32
+ {...props}
33
+ >
34
+ {showHeader && (
35
+ <div className={styles.header}>
36
+ <span>{headerTitle}</span>
37
+ <div className={styles.headerDots}><span className={styles.dot} /><span className={styles.dot} /><span className={styles.dot} /></div>
38
+ </div>
39
+ )}
40
+ {specs.map((spec, index) => (
41
+ <div key={index} className={`${styles.item} ${spec.icon ? styles.hasIcon : ''} ${spec.highlighted ? styles.highlighted : ''}`}>
42
+ {spec.icon && <span className={styles.icon}>{spec.icon}</span>}
43
+ <div className={styles.content}>
44
+ <span className={styles.label}>{spec.label}</span>
45
+ <span className={styles.value}>{spec.value}{spec.unit && <span className={styles.unit}>{spec.unit}</span>}</span>
46
+ {spec.description && <span className={styles.description}>{spec.description}</span>}
47
+ </div>
48
+ </div>
49
+ ))}
50
+ </div>
51
+ );
52
+ }
53
+ );
54
+
55
+ SpecGrid.displayName = 'SpecGrid';
56
+ export default SpecGrid;
@@ -0,0 +1,21 @@
1
+ .grid { --spec-accent: #00f0ff; --spec-bg: rgba(0, 0, 0, 0.5); --spec-border: rgba(255, 255, 255, 0.1); display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1px; background: var(--spec-border); border: 1px solid var(--spec-border); font-family: 'Share Tech Mono', monospace; }
2
+ .item { padding: 1.5rem; background: var(--spec-bg); display: flex; flex-direction: column; gap: 0.5rem; transition: all 0.3s ease; }
3
+ .item:hover { background: rgba(0, 240, 255, 0.05); }
4
+ .label { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.2em; color: var(--spec-accent); opacity: 0.8; }
5
+ .value { font-size: 1.5rem; font-weight: 700; color: #fff; line-height: 1.2; }
6
+ .unit { font-size: 0.75rem; color: rgba(255, 255, 255, 0.4); margin-left: 0.25rem; }
7
+ .description { font-size: 0.75rem; color: rgba(255, 255, 255, 0.5); margin-top: 0.5rem; }
8
+ .hasIcon { flex-direction: row; align-items: flex-start; gap: 1rem; }
9
+ .icon { font-size: 1.5rem; color: var(--spec-accent); opacity: 0.6; }
10
+ .content { flex: 1; display: flex; flex-direction: column; gap: 0.25rem; }
11
+ .highlighted { background: rgba(0, 240, 255, 0.1); border-left: 2px solid var(--spec-accent); }
12
+ .header { grid-column: 1 / -1; padding: 0.75rem 1.5rem; background: var(--spec-accent); color: #000; font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.2em; display: flex; justify-content: space-between; align-items: center; }
13
+ .headerDots { display: flex; gap: 0.5rem; }
14
+ .dot { width: 8px; height: 8px; border-radius: 50%; background: rgba(0, 0, 0, 0.3); }
15
+ .cyan { --spec-accent: #00f0ff; }
16
+ .green { --spec-accent: #00ff88; }
17
+ .amber { --spec-accent: #ffaa00; }
18
+ .blood { --spec-accent: #8b1a1a; }
19
+ .compact .item { padding: 1rem; }
20
+ .compact .value { font-size: 1.25rem; }
21
+ .striped .item:nth-child(even) { background: rgba(255, 255, 255, 0.02); }