@galyan/ui 0.0.1

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 (95) hide show
  1. package/.turbo/turbo-build.log +20 -0
  2. package/dist/index.cjs +2291 -0
  3. package/dist/index.css +2558 -0
  4. package/dist/index.d.cts +498 -0
  5. package/dist/index.d.ts +498 -0
  6. package/dist/index.js +2235 -0
  7. package/eslint.config.mjs +4 -0
  8. package/package.json +43 -0
  9. package/src/accordion/Accordion.tsx +88 -0
  10. package/src/accordion/accordion.css +78 -0
  11. package/src/accordion/index.ts +2 -0
  12. package/src/animatednumber/AnimatedNumber.tsx +71 -0
  13. package/src/animatednumber/index.ts +2 -0
  14. package/src/banner/Banner.tsx +84 -0
  15. package/src/banner/banner.css +56 -0
  16. package/src/banner/index.ts +2 -0
  17. package/src/breadcrumb/Breadcrumb.tsx +94 -0
  18. package/src/breadcrumb/breadcrumb.css +58 -0
  19. package/src/breadcrumb/index.ts +2 -0
  20. package/src/button/Button.tsx +98 -0
  21. package/src/button/button.css +190 -0
  22. package/src/button/index.ts +2 -0
  23. package/src/button.tsx +20 -0
  24. package/src/calendar/Calendar.tsx +190 -0
  25. package/src/calendar/calendar.css +142 -0
  26. package/src/calendar/index.ts +2 -0
  27. package/src/card/Card.tsx +88 -0
  28. package/src/card/card.css +115 -0
  29. package/src/card/index.ts +2 -0
  30. package/src/card.tsx +27 -0
  31. package/src/chart/Chart.tsx +186 -0
  32. package/src/chart/index.ts +2 -0
  33. package/src/checkbox/Checkbox.tsx +101 -0
  34. package/src/checkbox/checkbox.css +76 -0
  35. package/src/checkbox/index.ts +2 -0
  36. package/src/chips/Chips.tsx +146 -0
  37. package/src/chips/chips.css +90 -0
  38. package/src/chips/index.ts +2 -0
  39. package/src/code.tsx +11 -0
  40. package/src/datepicker/DatePicker.tsx +104 -0
  41. package/src/datepicker/datepicker.css +22 -0
  42. package/src/datepicker/index.ts +2 -0
  43. package/src/dragdrop/DragDrop.tsx +168 -0
  44. package/src/dragdrop/dragdrop.css +122 -0
  45. package/src/dragdrop/index.ts +2 -0
  46. package/src/dropdown/Dropdown.tsx +176 -0
  47. package/src/dropdown/dropdown.css +118 -0
  48. package/src/dropdown/index.ts +2 -0
  49. package/src/fileupload/FileUpload.tsx +144 -0
  50. package/src/fileupload/fileupload.css +118 -0
  51. package/src/fileupload/index.ts +2 -0
  52. package/src/index.ts +28 -0
  53. package/src/input/Input.tsx +121 -0
  54. package/src/input/index.ts +2 -0
  55. package/src/input/input.css +120 -0
  56. package/src/menu/Menu.tsx +118 -0
  57. package/src/menu/index.ts +2 -0
  58. package/src/menu/menu.css +117 -0
  59. package/src/modal/Modal.tsx +103 -0
  60. package/src/modal/index.ts +2 -0
  61. package/src/modal/modal.css +79 -0
  62. package/src/progressbar/ProgressBar.tsx +68 -0
  63. package/src/progressbar/index.ts +2 -0
  64. package/src/progressbar/progressbar.css +73 -0
  65. package/src/radiogroup/RadioGroup.tsx +73 -0
  66. package/src/radiogroup/index.ts +2 -0
  67. package/src/radiogroup/radiogroup.css +80 -0
  68. package/src/skeleton/Skeleton.tsx +61 -0
  69. package/src/skeleton/index.ts +2 -0
  70. package/src/skeleton/skeleton.css +46 -0
  71. package/src/spinner/Spinner.tsx +29 -0
  72. package/src/spinner/index.ts +2 -0
  73. package/src/spinner/spinner.css +39 -0
  74. package/src/stepper/Stepper.tsx +127 -0
  75. package/src/stepper/index.ts +2 -0
  76. package/src/stepper/stepper.css +141 -0
  77. package/src/tab/Tabs.tsx +71 -0
  78. package/src/tab/index.ts +2 -0
  79. package/src/tab/tab.css +92 -0
  80. package/src/table/Table.tsx +205 -0
  81. package/src/table/index.ts +2 -0
  82. package/src/table/table.css +119 -0
  83. package/src/textarea/Textarea.tsx +102 -0
  84. package/src/textarea/index.ts +2 -0
  85. package/src/textarea/textarea.css +85 -0
  86. package/src/toaster/Toaster.tsx +108 -0
  87. package/src/toaster/index.ts +2 -0
  88. package/src/toaster/toaster.css +113 -0
  89. package/src/toggle/Toggle.tsx +88 -0
  90. package/src/toggle/index.ts +2 -0
  91. package/src/toggle/toggle.css +63 -0
  92. package/src/typography/Typography.tsx +58 -0
  93. package/src/typography/index.ts +2 -0
  94. package/src/typography/typography.css +102 -0
  95. package/tsconfig.json +13 -0
@@ -0,0 +1,68 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import './progressbar.css';
5
+
6
+ export type ProgressSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
7
+ export type ProgressColor = 'primary' | 'success' | 'warning' | 'danger' | 'gradient';
8
+
9
+ export interface ProgressBarProps {
10
+ value?: number;
11
+ max?: number;
12
+ size?: ProgressSize;
13
+ color?: ProgressColor;
14
+ label?: string;
15
+ showValue?: boolean;
16
+ striped?: boolean;
17
+ indeterminate?: boolean;
18
+ className?: string;
19
+ }
20
+
21
+ export function ProgressBar({
22
+ value = 0,
23
+ max = 100,
24
+ size = 'md',
25
+ color = 'primary',
26
+ label,
27
+ showValue = false,
28
+ striped = false,
29
+ indeterminate = false,
30
+ className = '',
31
+ }: ProgressBarProps) {
32
+ const pct = Math.min(Math.max((value / max) * 100, 0), 100);
33
+
34
+ const fillClasses = [
35
+ 'gy-progress-fill',
36
+ color !== 'primary' ? `gy-progress-fill--${color}` : '',
37
+ striped ? 'gy-progress-fill--striped' : '',
38
+ indeterminate ? 'gy-progress-fill--indeterminate' : '',
39
+ ]
40
+ .filter(Boolean)
41
+ .join(' ');
42
+
43
+ return (
44
+ <div className={`gy-progress ${className}`}>
45
+ {(label || showValue) && (
46
+ <div className="gy-progress-header">
47
+ {label && <span className="gy-progress-label">{label}</span>}
48
+ {showValue && !indeterminate && (
49
+ <span className="gy-progress-value">{Math.round(pct)}%</span>
50
+ )}
51
+ </div>
52
+ )}
53
+ <div
54
+ className={`gy-progress-track gy-progress-track--${size}`}
55
+ role="progressbar"
56
+ aria-valuenow={indeterminate ? undefined : value}
57
+ aria-valuemin={0}
58
+ aria-valuemax={max}
59
+ aria-label={label ?? 'Progress'}
60
+ >
61
+ <div
62
+ className={fillClasses}
63
+ style={{ width: indeterminate ? undefined : `${pct}%` }}
64
+ />
65
+ </div>
66
+ </div>
67
+ );
68
+ }
@@ -0,0 +1,2 @@
1
+ export { ProgressBar } from './ProgressBar';
2
+ export type { ProgressBarProps, ProgressSize, ProgressColor } from './ProgressBar';
@@ -0,0 +1,73 @@
1
+ /* ── ProgressBar ─────────────────────────────────────────────────────────── */
2
+ .gy-progress {
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: var(--gy-spacing-1-5);
6
+ width: 100%;
7
+ }
8
+
9
+ .gy-progress-header {
10
+ display: flex;
11
+ justify-content: space-between;
12
+ align-items: center;
13
+ }
14
+
15
+ .gy-progress-label {
16
+ font-size: var(--gy-font-size-sm);
17
+ font-weight: var(--gy-font-weight-medium);
18
+ color: var(--gy-text);
19
+ }
20
+
21
+ .gy-progress-value {
22
+ font-size: var(--gy-font-size-sm);
23
+ color: var(--gy-text-muted);
24
+ font-weight: var(--gy-font-weight-medium);
25
+ }
26
+
27
+ .gy-progress-track {
28
+ background: var(--gy-background-muted);
29
+ border-radius: var(--gy-radius-full);
30
+ overflow: hidden;
31
+ }
32
+
33
+ .gy-progress-track--xs { height: 0.25rem; }
34
+ .gy-progress-track--sm { height: 0.375rem; }
35
+ .gy-progress-track--md { height: 0.5rem; }
36
+ .gy-progress-track--lg { height: 0.75rem; }
37
+ .gy-progress-track--xl { height: 1rem; }
38
+
39
+ .gy-progress-fill {
40
+ height: 100%;
41
+ border-radius: var(--gy-radius-full);
42
+ background: var(--gy-primary);
43
+ transition: width var(--gy-duration-slow) var(--gy-ease-out);
44
+ }
45
+
46
+ .gy-progress-fill--success { background: var(--gy-success); }
47
+ .gy-progress-fill--warning { background: var(--gy-warning); }
48
+ .gy-progress-fill--danger { background: var(--gy-danger); }
49
+
50
+ .gy-progress-fill--gradient {
51
+ background: var(--gy-gradient-primary);
52
+ }
53
+
54
+ .gy-progress-fill--striped {
55
+ background-image: repeating-linear-gradient(
56
+ -45deg,
57
+ transparent,
58
+ transparent 8px,
59
+ rgba(255,255,255,0.2) 8px,
60
+ rgba(255,255,255,0.2) 16px
61
+ );
62
+ background-size: 32px 32px;
63
+ }
64
+
65
+ .gy-progress-fill--indeterminate {
66
+ width: 40% !important;
67
+ animation: gy-progress-indeterminate 1.5s var(--gy-ease-in-out) infinite;
68
+ }
69
+
70
+ @keyframes gy-progress-indeterminate {
71
+ 0% { transform: translateX(-100%); }
72
+ 100% { transform: translateX(250%); }
73
+ }
@@ -0,0 +1,73 @@
1
+ 'use client';
2
+
3
+ import React, { useId } from 'react';
4
+ import './radiogroup.css';
5
+
6
+ export interface RadioOption {
7
+ value: string;
8
+ label: React.ReactNode;
9
+ disabled?: boolean;
10
+ }
11
+
12
+ export interface RadioGroupProps {
13
+ options: RadioOption[];
14
+ value?: string;
15
+ defaultValue?: string;
16
+ onChange?: (value: string) => void;
17
+ orientation?: 'horizontal' | 'vertical';
18
+ groupLabel?: string;
19
+ name?: string;
20
+ disabled?: boolean;
21
+ className?: string;
22
+ }
23
+
24
+ export function RadioGroup({
25
+ options,
26
+ value,
27
+ defaultValue,
28
+ onChange,
29
+ orientation = 'vertical',
30
+ groupLabel,
31
+ name,
32
+ disabled = false,
33
+ className = '',
34
+ }: RadioGroupProps) {
35
+ const uid = useId();
36
+ const groupName = name ?? uid;
37
+
38
+ return (
39
+ <div
40
+ role="radiogroup"
41
+ aria-label={typeof groupLabel === 'string' ? groupLabel : undefined}
42
+ className={`gy-radio-group ${orientation === 'horizontal' ? 'gy-radio-group--horizontal' : ''} ${className}`}
43
+ >
44
+ {groupLabel && <div className="gy-radio-group-label">{groupLabel}</div>}
45
+ {options.map((opt) => {
46
+ const isChecked = value !== undefined ? value === opt.value : undefined;
47
+ const isDisabled = disabled || opt.disabled;
48
+
49
+ return (
50
+ <label
51
+ key={opt.value}
52
+ className={`gy-radio ${isDisabled ? 'gy-radio--disabled' : ''}`}
53
+ >
54
+ <input
55
+ type="radio"
56
+ name={groupName}
57
+ value={opt.value}
58
+ checked={isChecked}
59
+ defaultChecked={defaultValue === opt.value}
60
+ disabled={isDisabled}
61
+ className="gy-radio-input"
62
+ onChange={() => onChange?.(opt.value)}
63
+ />
64
+ <span className={`gy-radio-circle ${isChecked ? 'gy-radio-circle--checked' : ''}`}>
65
+ {isChecked && <span className="gy-radio-dot" />}
66
+ </span>
67
+ <span className="gy-radio-label">{opt.label}</span>
68
+ </label>
69
+ );
70
+ })}
71
+ </div>
72
+ );
73
+ }
@@ -0,0 +1,2 @@
1
+ export { RadioGroup } from './RadioGroup';
2
+ export type { RadioGroupProps, RadioOption } from './RadioGroup';
@@ -0,0 +1,80 @@
1
+ /* ── RadioGroup ───────────────────────────────────────────────────────────── */
2
+ .gy-radio-group {
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: var(--gy-spacing-2);
6
+ }
7
+
8
+ .gy-radio-group--horizontal {
9
+ flex-direction: row;
10
+ flex-wrap: wrap;
11
+ }
12
+
13
+ .gy-radio-group-label {
14
+ font-size: var(--gy-font-size-sm);
15
+ font-weight: var(--gy-font-weight-semibold);
16
+ color: var(--gy-text);
17
+ margin-bottom: var(--gy-spacing-1);
18
+ }
19
+
20
+ .gy-radio {
21
+ display: inline-flex;
22
+ align-items: center;
23
+ gap: var(--gy-spacing-2);
24
+ cursor: pointer;
25
+ }
26
+
27
+ .gy-radio--disabled {
28
+ cursor: not-allowed;
29
+ opacity: 0.5;
30
+ }
31
+
32
+ .gy-radio-input {
33
+ position: absolute;
34
+ opacity: 0;
35
+ width: 0;
36
+ height: 0;
37
+ }
38
+
39
+ .gy-radio-circle {
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: center;
43
+ width: 1.125rem;
44
+ height: 1.125rem;
45
+ border-radius: 50%;
46
+ border: 2px solid var(--gy-border-strong);
47
+ background: var(--gy-surface);
48
+ flex-shrink: 0;
49
+ transition:
50
+ border-color var(--gy-duration-fast) var(--gy-ease-in-out),
51
+ box-shadow var(--gy-duration-fast) var(--gy-ease-in-out);
52
+ }
53
+
54
+ .gy-radio:not(.gy-radio--disabled):hover .gy-radio-circle {
55
+ border-color: var(--gy-primary);
56
+ }
57
+
58
+ .gy-radio-input:focus-visible + .gy-radio-circle {
59
+ box-shadow: 0 0 0 3px var(--gy-focus-ring);
60
+ border-color: var(--gy-primary);
61
+ }
62
+
63
+ .gy-radio-circle--checked {
64
+ border-color: var(--gy-primary);
65
+ }
66
+
67
+ .gy-radio-dot {
68
+ width: 0.5rem;
69
+ height: 0.5rem;
70
+ border-radius: 50%;
71
+ background: var(--gy-primary);
72
+ animation: gy-zoom-in var(--gy-duration-fast) var(--gy-ease-spring);
73
+ }
74
+
75
+ .gy-radio-label {
76
+ font-size: var(--gy-font-size-sm);
77
+ font-weight: var(--gy-font-weight-medium);
78
+ color: var(--gy-text);
79
+ line-height: 1.4;
80
+ }
@@ -0,0 +1,61 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import './skeleton.css';
5
+
6
+ export type SkeletonShape = 'text' | 'circle' | 'rect' | 'card';
7
+ export type SkeletonAnimation = 'wave' | 'pulse' | 'none';
8
+
9
+ export interface SkeletonProps {
10
+ shape?: SkeletonShape;
11
+ animation?: SkeletonAnimation;
12
+ width?: string | number;
13
+ height?: string | number;
14
+ className?: string;
15
+ lines?: number;
16
+ gap?: string;
17
+ }
18
+
19
+ export function Skeleton({
20
+ shape = 'rect',
21
+ animation = 'wave',
22
+ width,
23
+ height,
24
+ className = '',
25
+ lines,
26
+ gap = '0.5rem',
27
+ }: SkeletonProps) {
28
+ const classes = [
29
+ 'gy-skeleton',
30
+ `gy-skeleton--${shape}`,
31
+ animation !== 'none' ? `gy-skeleton--${animation}` : '',
32
+ !width ? 'gy-skeleton--w-full' : '',
33
+ className,
34
+ ]
35
+ .filter(Boolean)
36
+ .join(' ');
37
+
38
+ const style: React.CSSProperties = {
39
+ width: width,
40
+ height: height,
41
+ };
42
+
43
+ if (lines) {
44
+ return (
45
+ <div style={{ display: 'flex', flexDirection: 'column', gap }}>
46
+ {Array.from({ length: lines }).map((_, i) => (
47
+ <div
48
+ key={i}
49
+ className={`gy-skeleton gy-skeleton--text ${animation !== 'none' ? `gy-skeleton--${animation}` : ''} ${
50
+ i === lines - 1 ? 'gy-skeleton--w-3-4' : 'gy-skeleton--w-full'
51
+ }`}
52
+ style={{ height: height ?? '1em' }}
53
+ aria-hidden="true"
54
+ />
55
+ ))}
56
+ </div>
57
+ );
58
+ }
59
+
60
+ return <div className={classes} style={style} aria-hidden="true" />;
61
+ }
@@ -0,0 +1,2 @@
1
+ export { Skeleton } from './Skeleton';
2
+ export type { SkeletonProps, SkeletonShape, SkeletonAnimation } from './Skeleton';
@@ -0,0 +1,46 @@
1
+ /* ── Skeleton ─────────────────────────────────────────────────────────────── */
2
+ .gy-skeleton {
3
+ background: var(--gy-skeleton-base);
4
+ border-radius: var(--gy-radius-md);
5
+ overflow: hidden;
6
+ position: relative;
7
+ }
8
+
9
+ /* Wave animation */
10
+ .gy-skeleton--wave::after {
11
+ content: '';
12
+ position: absolute;
13
+ inset: 0;
14
+ background: linear-gradient(90deg, transparent 25%, var(--gy-border) 50%, transparent 75%);
15
+ background-size: 200% 100%;
16
+ animation: gy-wave 1.5s var(--gy-ease) infinite;
17
+ }
18
+
19
+ /* Pulse animation */
20
+ .gy-skeleton--pulse {
21
+ animation: gy-pulse 1.5s var(--gy-ease-in-out) infinite;
22
+ }
23
+
24
+ /* Shapes */
25
+ .gy-skeleton--text {
26
+ height: 1em;
27
+ border-radius: var(--gy-radius-sm);
28
+ }
29
+
30
+ .gy-skeleton--circle {
31
+ border-radius: 50%;
32
+ }
33
+
34
+ .gy-skeleton--rect {
35
+ border-radius: var(--gy-radius-md);
36
+ }
37
+
38
+ .gy-skeleton--card {
39
+ border-radius: var(--gy-radius-xl);
40
+ }
41
+
42
+ /* Width presets */
43
+ .gy-skeleton--w-full { width: 100%; }
44
+ .gy-skeleton--w-3-4 { width: 75%; }
45
+ .gy-skeleton--w-1-2 { width: 50%; }
46
+ .gy-skeleton--w-1-4 { width: 25%; }
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import './spinner.css';
5
+
6
+ export type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
7
+ export type SpinnerColor = 'primary' | 'white' | 'neutral';
8
+
9
+ export interface SpinnerProps {
10
+ size?: SpinnerSize;
11
+ color?: SpinnerColor;
12
+ label?: string;
13
+ className?: string;
14
+ }
15
+
16
+ export function Spinner({ size = 'md', color = 'primary', label, className = '' }: SpinnerProps) {
17
+ return (
18
+ <div className={`gy-spinner-container ${className}`}>
19
+ <span
20
+ className={`gy-spinner gy-spinner--${size} gy-spinner--${color}`}
21
+ role="status"
22
+ aria-label={label ?? 'Loading...'}
23
+ >
24
+ <span className="gy-spinner__circle" aria-hidden="true" />
25
+ </span>
26
+ {label && <span className="gy-spinner__label">{label}</span>}
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,2 @@
1
+ export { Spinner } from './Spinner';
2
+ export type { SpinnerProps, SpinnerSize, SpinnerColor } from './Spinner';
@@ -0,0 +1,39 @@
1
+ /* ── Spinner ──────────────────────────────────────────────────────────────── */
2
+ .gy-spinner {
3
+ display: inline-flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ flex-shrink: 0;
7
+ }
8
+
9
+ .gy-spinner__circle {
10
+ border-radius: 50%;
11
+ border-style: solid;
12
+ border-color: var(--gy-primary-muted);
13
+ border-top-color: var(--gy-primary);
14
+ animation: gy-spin var(--gy-duration-slower) linear infinite;
15
+ }
16
+
17
+ .gy-spinner--xs .gy-spinner__circle { width: 1rem; height: 1rem; border-width: 2px; }
18
+ .gy-spinner--sm .gy-spinner__circle { width: 1.25rem; height: 1.25rem; border-width: 2px; }
19
+ .gy-spinner--md .gy-spinner__circle { width: 1.5rem; height: 1.5rem; border-width: 2.5px; }
20
+ .gy-spinner--lg .gy-spinner__circle { width: 2rem; height: 2rem; border-width: 3px; }
21
+ .gy-spinner--xl .gy-spinner__circle { width: 3rem; height: 3rem; border-width: 4px; }
22
+
23
+ /* Color variants */
24
+ .gy-spinner--primary .gy-spinner__circle { border-color: var(--gy-primary-muted); border-top-color: var(--gy-primary); }
25
+ .gy-spinner--white .gy-spinner__circle { border-color: rgba(255,255,255,0.3); border-top-color: #ffffff; }
26
+ .gy-spinner--neutral .gy-spinner__circle { border-color: var(--gy-border); border-top-color: var(--gy-text-muted); }
27
+
28
+ .gy-spinner__label {
29
+ margin-top: var(--gy-spacing-2);
30
+ font-size: var(--gy-font-size-xs);
31
+ color: var(--gy-text-subtle);
32
+ text-align: center;
33
+ }
34
+
35
+ .gy-spinner-container {
36
+ display: inline-flex;
37
+ flex-direction: column;
38
+ align-items: center;
39
+ }
@@ -0,0 +1,127 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import './stepper.css';
5
+
6
+ export type StepStatus = 'upcoming' | 'active' | 'completed' | 'error';
7
+
8
+ export interface Step {
9
+ id: string;
10
+ label: string;
11
+ description?: string;
12
+ content?: React.ReactNode;
13
+ optional?: boolean;
14
+ }
15
+
16
+ export interface StepperProps {
17
+ steps: Step[];
18
+ activeStep?: number;
19
+ defaultStep?: number;
20
+ /** 'linear' = must complete in order; 'free' = can jump to any step */
21
+ variant?: 'linear' | 'free';
22
+ orientation?: 'horizontal' | 'vertical';
23
+ onStepChange?: (index: number) => void;
24
+ onComplete?: () => void;
25
+ className?: string;
26
+ }
27
+
28
+ const CheckIcon = () => (
29
+ <svg width="14" height="11" viewBox="0 0 14 11" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
30
+ <polyline points="1,5 5,9 13,1" />
31
+ </svg>
32
+ );
33
+
34
+ const ErrorIcon = () => (
35
+ <svg width="12" height="12" viewBox="0 0 12 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
36
+ <line x1="2" y1="2" x2="10" y2="10" /><line x1="10" y1="2" x2="2" y2="10" />
37
+ </svg>
38
+ );
39
+
40
+ export function Stepper({
41
+ steps,
42
+ activeStep: controlledStep,
43
+ defaultStep = 0,
44
+ variant = 'linear',
45
+ orientation = 'horizontal',
46
+ onStepChange,
47
+ onComplete,
48
+ className = '',
49
+ }: StepperProps) {
50
+ const [internalStep, setInternalStep] = useState(defaultStep);
51
+ const current = controlledStep ?? internalStep;
52
+
53
+ const goTo = (idx: number) => {
54
+ if (variant === 'linear' && idx > current + 1) return;
55
+ if (idx < 0 || idx >= steps.length) return;
56
+ setInternalStep(idx);
57
+ onStepChange?.(idx);
58
+ };
59
+
60
+ const next = () => {
61
+ if (current === steps.length - 1) {
62
+ onComplete?.();
63
+ } else {
64
+ goTo(current + 1);
65
+ }
66
+ };
67
+
68
+ const prev = () => goTo(current - 1);
69
+
70
+ const getStatus = (idx: number): StepStatus => {
71
+ if (idx < current) return 'completed';
72
+ if (idx === current) return 'active';
73
+ return 'upcoming';
74
+ };
75
+
76
+ return (
77
+ <div className={`gy-stepper ${orientation === 'vertical' ? 'gy-stepper--vertical' : ''} ${className}`}>
78
+ {/* Step Indicators */}
79
+ <div className={`gy-stepper ${orientation === 'vertical' ? 'gy-stepper--vertical' : ''}`} style={{ width: '100%' }}>
80
+ {steps.map((step, idx) => {
81
+ const status = getStatus(idx);
82
+ const isClickable = variant === 'free' || idx <= current;
83
+
84
+ return (
85
+ <React.Fragment key={step.id}>
86
+ <div className="gy-stepper-item">
87
+ <span
88
+ className={[
89
+ 'gy-stepper-indicator',
90
+ `gy-stepper-indicator--${status}`,
91
+ isClickable ? 'gy-stepper-indicator--clickable' : '',
92
+ ]
93
+ .filter(Boolean)
94
+ .join(' ')}
95
+ onClick={() => isClickable && goTo(idx)}
96
+ role={isClickable ? 'button' : undefined}
97
+ tabIndex={isClickable ? 0 : undefined}
98
+ onKeyDown={isClickable ? (e) => { if (e.key === 'Enter') goTo(idx); } : undefined}
99
+ aria-label={`Step ${idx + 1}: ${step.label}`}
100
+ aria-current={status === 'active' ? 'step' : undefined}
101
+ >
102
+ {status === 'completed' ? <CheckIcon /> : status === 'error' ? <ErrorIcon /> : idx + 1}
103
+ </span>
104
+ <div>
105
+ <div className={`gy-stepper-label gy-stepper-label--${status}`}>{step.label}</div>
106
+ {step.optional && <div className="gy-stepper-description">Optional</div>}
107
+ {step.description && <div className="gy-stepper-description">{step.description}</div>}
108
+ </div>
109
+ </div>
110
+
111
+ {idx < steps.length - 1 && (
112
+ <div className={`gy-stepper-connector ${idx < current ? 'gy-stepper-connector--completed' : ''}`} />
113
+ )}
114
+ </React.Fragment>
115
+ );
116
+ })}
117
+ </div>
118
+
119
+ {/* Active Panel */}
120
+ {steps[current]?.content && (
121
+ <div key={current} className="gy-stepper-panel">
122
+ {steps[current].content}
123
+ </div>
124
+ )}
125
+ </div>
126
+ );
127
+ }
@@ -0,0 +1,2 @@
1
+ export { Stepper } from './Stepper';
2
+ export type { StepperProps, Step, StepStatus } from './Stepper';