@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,102 @@
1
+ 'use client';
2
+
3
+ import React, { forwardRef, useCallback, useEffect, useId, useRef } from 'react';
4
+ import './textarea.css';
5
+
6
+ export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
7
+ label?: string;
8
+ error?: string;
9
+ helperText?: string;
10
+ maxCharCount?: number;
11
+ autoResize?: boolean;
12
+ required?: boolean;
13
+ }
14
+
15
+ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(
16
+ {
17
+ label,
18
+ error,
19
+ helperText,
20
+ maxCharCount,
21
+ autoResize = false,
22
+ required,
23
+ disabled,
24
+ value,
25
+ onChange,
26
+ className = '',
27
+ id,
28
+ ...rest
29
+ },
30
+ ref
31
+ ) {
32
+ const uid = useId();
33
+ const inputId = id ?? uid;
34
+ const innerRef = useRef<HTMLTextAreaElement>(null);
35
+ const textareaRef = (ref as React.RefObject<HTMLTextAreaElement>) ?? innerRef;
36
+
37
+ const charCount = typeof value === 'string' ? value.length : 0;
38
+ const isOver = maxCharCount ? charCount > maxCharCount : false;
39
+
40
+ const adjustHeight = useCallback(() => {
41
+ const el = textareaRef.current;
42
+ if (!el || !autoResize) return;
43
+ el.style.height = 'auto';
44
+ el.style.height = `${el.scrollHeight}px`;
45
+ }, [autoResize, textareaRef]);
46
+
47
+ useEffect(() => {
48
+ adjustHeight();
49
+ }, [value, adjustHeight]);
50
+
51
+ const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
52
+ adjustHeight();
53
+ onChange?.(e);
54
+ };
55
+
56
+ return (
57
+ <div className={`gy-textarea-root ${className}`}>
58
+ {label && (
59
+ <label
60
+ className={`gy-textarea-label ${required ? 'gy-textarea-label--required' : ''}`}
61
+ htmlFor={inputId}
62
+ >
63
+ {label}
64
+ </label>
65
+ )}
66
+
67
+ <textarea
68
+ ref={textareaRef}
69
+ id={inputId}
70
+ className={[
71
+ 'gy-textarea',
72
+ error ? 'gy-textarea--error' : '',
73
+ autoResize ? 'gy-textarea--auto-resize' : '',
74
+ ]
75
+ .filter(Boolean)
76
+ .join(' ')}
77
+ disabled={disabled}
78
+ required={required}
79
+ aria-invalid={!!error}
80
+ value={value}
81
+ onChange={handleChange}
82
+ {...rest}
83
+ />
84
+
85
+ <div className="gy-textarea-footer">
86
+ {error ? (
87
+ <span className="gy-textarea-helper gy-textarea-helper--error" role="alert">{error}</span>
88
+ ) : helperText ? (
89
+ <span className="gy-textarea-helper">{helperText}</span>
90
+ ) : (
91
+ <span />
92
+ )}
93
+
94
+ {maxCharCount !== undefined && (
95
+ <span className={`gy-textarea-count ${isOver ? 'gy-textarea-count--over' : ''}`}>
96
+ {charCount}/{maxCharCount}
97
+ </span>
98
+ )}
99
+ </div>
100
+ </div>
101
+ );
102
+ });
@@ -0,0 +1,2 @@
1
+ export { Textarea } from './Textarea';
2
+ export type { TextareaProps } from './Textarea';
@@ -0,0 +1,85 @@
1
+ /* ── Textarea ─────────────────────────────────────────────────────────────── */
2
+ .gy-textarea-root {
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: var(--gy-spacing-1-5);
6
+ width: 100%;
7
+ }
8
+
9
+ .gy-textarea-label {
10
+ font-size: var(--gy-font-size-sm);
11
+ font-weight: var(--gy-font-weight-medium);
12
+ color: var(--gy-text);
13
+ }
14
+
15
+ .gy-textarea-label--required::after {
16
+ content: '*';
17
+ color: var(--gy-danger);
18
+ margin-left: 2px;
19
+ }
20
+
21
+ .gy-textarea {
22
+ width: 100%;
23
+ min-height: 80px;
24
+ padding: var(--gy-spacing-2-5) var(--gy-spacing-3);
25
+ font-family: var(--gy-font-sans);
26
+ font-size: var(--gy-font-size-sm);
27
+ line-height: var(--gy-line-height-relaxed);
28
+ color: var(--gy-text);
29
+ background: var(--gy-surface);
30
+ border: 1.5px solid var(--gy-border);
31
+ border-radius: var(--gy-radius-lg);
32
+ resize: vertical;
33
+ transition:
34
+ border-color var(--gy-duration-fast) var(--gy-ease-in-out),
35
+ box-shadow var(--gy-duration-fast) var(--gy-ease-in-out);
36
+ outline: none;
37
+ }
38
+
39
+ .gy-textarea::placeholder { color: var(--gy-text-subtle); }
40
+
41
+ .gy-textarea:focus {
42
+ border-color: var(--gy-border-focus);
43
+ box-shadow: 0 0 0 3px var(--gy-focus-ring);
44
+ }
45
+
46
+ .gy-textarea--error {
47
+ border-color: var(--gy-danger);
48
+ }
49
+
50
+ .gy-textarea--error:focus {
51
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2);
52
+ }
53
+
54
+ .gy-textarea:disabled {
55
+ background: var(--gy-disabled-bg);
56
+ color: var(--gy-text-disabled);
57
+ cursor: not-allowed;
58
+ resize: none;
59
+ }
60
+
61
+ .gy-textarea--auto-resize {
62
+ resize: none;
63
+ overflow: hidden;
64
+ }
65
+
66
+ .gy-textarea-footer {
67
+ display: flex;
68
+ justify-content: space-between;
69
+ align-items: center;
70
+ }
71
+
72
+ .gy-textarea-helper {
73
+ font-size: var(--gy-font-size-xs);
74
+ color: var(--gy-text-subtle);
75
+ }
76
+
77
+ .gy-textarea-helper--error { color: var(--gy-danger); }
78
+
79
+ .gy-textarea-count {
80
+ font-size: var(--gy-font-size-xs);
81
+ color: var(--gy-text-subtle);
82
+ margin-left: auto;
83
+ }
84
+
85
+ .gy-textarea-count--over { color: var(--gy-danger); }
@@ -0,0 +1,108 @@
1
+ 'use client';
2
+
3
+ import React, { createContext, useCallback, useContext, useId, useState } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+ import { Spinner } from '../spinner/Spinner';
6
+ import './toaster.css';
7
+
8
+ export type ToastVariant = 'success' | 'error' | 'warning' | 'info' | 'loading';
9
+ export type ToastPosition =
10
+ | 'top-right' | 'top-left' | 'top-center'
11
+ | 'bottom-right' | 'bottom-left' | 'bottom-center';
12
+
13
+ export interface Toast {
14
+ id: string;
15
+ title: string;
16
+ description?: string;
17
+ variant?: ToastVariant;
18
+ duration?: number;
19
+ dismissible?: boolean;
20
+ }
21
+
22
+ interface ToastContextValue {
23
+ toasts: Toast[];
24
+ toast: (toast: Omit<Toast, 'id'>) => string;
25
+ dismiss: (id: string) => void;
26
+ dismissAll: () => void;
27
+ }
28
+
29
+ const ToastContext = createContext<ToastContextValue | null>(null);
30
+
31
+ const icons: Record<ToastVariant, React.ReactNode> = {
32
+ success: <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="#10b981" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="8" cy="8" r="7"/><polyline points="5,8 7,10 11,6"/></svg>,
33
+ error: <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="#ef4444" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="8" cy="8" r="7"/><line x1="8" y1="5" x2="8" y2="9"/><circle cx="8" cy="11.5" r="0.5" fill="#ef4444"/></svg>,
34
+ warning: <svg width="16" height="16" viewBox="0 0 16 16" fill="#f59e0b"><path d="M8 2L1 14h14L8 2zm0 4v4m0 2v.01" stroke="white" strokeWidth="1.5" strokeLinecap="round"/></svg>,
35
+ info: <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="#0ea5e9" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="8" cy="8" r="7"/><line x1="8" y1="8" x2="8" y2="12"/><circle cx="8" cy="5" r="0.5" fill="#0ea5e9"/></svg>,
36
+ loading: <Spinner size="xs" />,
37
+ };
38
+
39
+ export function ToasterProvider({ children, position = 'top-right' }: { children: React.ReactNode; position?: ToastPosition }) {
40
+ const [toasts, setToasts] = useState<Toast[]>([]);
41
+
42
+ const dismiss = useCallback((id: string) => {
43
+ setToasts((prev) => prev.filter((t) => t.id !== id));
44
+ }, []);
45
+
46
+ const dismissAll = useCallback(() => setToasts([]), []);
47
+
48
+ const toast = useCallback((data: Omit<Toast, 'id'>) => {
49
+ const id = Math.random().toString(36).slice(2);
50
+ const duration = data.duration ?? (data.variant === 'loading' ? 0 : 4000);
51
+ const newToast: Toast = { ...data, id, duration };
52
+
53
+ setToasts((prev) => [...prev, newToast]);
54
+
55
+ if (duration > 0) {
56
+ setTimeout(() => dismiss(id), duration);
57
+ }
58
+
59
+ return id;
60
+ }, [dismiss]);
61
+
62
+ const isLeft = position.includes('left');
63
+
64
+ const toasterNode = (
65
+ <div className={`gy-toaster gy-toaster--${position}`}>
66
+ {toasts.map((t) => (
67
+ <div
68
+ key={t.id}
69
+ className={`gy-toast gy-toast--${t.variant ?? 'info'} ${isLeft ? 'gy-toast--left' : ''}`}
70
+ role="alert"
71
+ aria-live="polite"
72
+ >
73
+ <div className="gy-toast-accent" />
74
+ <span className="gy-toast-icon">{icons[t.variant ?? 'info']}</span>
75
+ <div className="gy-toast-content">
76
+ <div className="gy-toast-title">{t.title}</div>
77
+ {t.description && <div className="gy-toast-description">{t.description}</div>}
78
+ </div>
79
+ {(t.dismissible ?? true) && (
80
+ <button className="gy-toast-close" onClick={() => dismiss(t.id)} aria-label="Dismiss">
81
+ <svg width="12" height="12" viewBox="0 0 12 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
82
+ <line x1="1" y1="1" x2="11" y2="11" /><line x1="11" y1="1" x2="1" y2="11" />
83
+ </svg>
84
+ </button>
85
+ )}
86
+ {t.duration && t.duration > 0 && (
87
+ <div className="gy-toast-progress">
88
+ <div className="gy-toast-progress-bar" style={{ animationDuration: `${t.duration}ms` }} />
89
+ </div>
90
+ )}
91
+ </div>
92
+ ))}
93
+ </div>
94
+ );
95
+
96
+ return (
97
+ <ToastContext.Provider value={{ toasts, toast, dismiss, dismissAll }}>
98
+ {children}
99
+ {typeof document !== 'undefined' && createPortal(toasterNode, document.body)}
100
+ </ToastContext.Provider>
101
+ );
102
+ }
103
+
104
+ export function useToast() {
105
+ const ctx = useContext(ToastContext);
106
+ if (!ctx) throw new Error('useToast must be used inside ToasterProvider');
107
+ return ctx;
108
+ }
@@ -0,0 +1,2 @@
1
+ export { ToasterProvider, useToast } from './Toaster';
2
+ export type { Toast, ToastVariant, ToastPosition } from './Toaster';
@@ -0,0 +1,113 @@
1
+ /* ── Toaster ──────────────────────────────────────────────────────────────── */
2
+ .gy-toaster {
3
+ position: fixed;
4
+ z-index: var(--gy-z-toast);
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: var(--gy-spacing-2);
8
+ pointer-events: none;
9
+ padding: var(--gy-spacing-4);
10
+ }
11
+
12
+ .gy-toaster--top-right { top: 0; right: 0; align-items: flex-end; }
13
+ .gy-toaster--top-left { top: 0; left: 0; align-items: flex-start; }
14
+ .gy-toaster--top-center { top: 0; left: 50%; transform: translateX(-50%); align-items: center; }
15
+ .gy-toaster--bottom-right { bottom: 0; right: 0; align-items: flex-end; flex-direction: column-reverse; }
16
+ .gy-toaster--bottom-left { bottom: 0; left: 0; align-items: flex-start; flex-direction: column-reverse; }
17
+ .gy-toaster--bottom-center { bottom: 0; left: 50%; transform: translateX(-50%); align-items: center; flex-direction: column-reverse; }
18
+
19
+ .gy-toast {
20
+ display: flex;
21
+ align-items: flex-start;
22
+ gap: var(--gy-spacing-3);
23
+ padding: var(--gy-spacing-3) var(--gy-spacing-4);
24
+ background: var(--gy-surface);
25
+ border-radius: var(--gy-radius-xl);
26
+ box-shadow: var(--gy-shadow-lg);
27
+ border: 1px solid var(--gy-border);
28
+ min-width: 280px;
29
+ max-width: 380px;
30
+ pointer-events: all;
31
+ animation: gy-slide-toast-in-right var(--gy-duration-normal) var(--gy-ease-spring);
32
+ position: relative;
33
+ overflow: hidden;
34
+ }
35
+
36
+ .gy-toast--left { animation: gy-slide-in-left var(--gy-duration-normal) var(--gy-ease-spring); }
37
+
38
+ .gy-toast-accent {
39
+ position: absolute;
40
+ left: 0;
41
+ top: 0;
42
+ bottom: 0;
43
+ width: 4px;
44
+ border-radius: var(--gy-radius-lg) 0 0 var(--gy-radius-lg);
45
+ }
46
+
47
+ .gy-toast--success .gy-toast-accent { background: var(--gy-success); }
48
+ .gy-toast--error .gy-toast-accent { background: var(--gy-danger); }
49
+ .gy-toast--warning .gy-toast-accent { background: var(--gy-warning); }
50
+ .gy-toast--info .gy-toast-accent { background: var(--gy-info); }
51
+ .gy-toast--loading .gy-toast-accent { background: var(--gy-primary); }
52
+
53
+ .gy-toast-icon {
54
+ flex-shrink: 0;
55
+ margin-left: var(--gy-spacing-1);
56
+ }
57
+
58
+ .gy-toast-content { flex: 1; }
59
+
60
+ .gy-toast-title {
61
+ font-size: var(--gy-font-size-sm);
62
+ font-weight: var(--gy-font-weight-semibold);
63
+ color: var(--gy-text);
64
+ line-height: 1.4;
65
+ }
66
+
67
+ .gy-toast-description {
68
+ font-size: var(--gy-font-size-xs);
69
+ color: var(--gy-text-muted);
70
+ margin-top: 2px;
71
+ line-height: 1.5;
72
+ }
73
+
74
+ .gy-toast-close {
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ width: 1.5rem;
79
+ height: 1.5rem;
80
+ border-radius: var(--gy-radius-md);
81
+ flex-shrink: 0;
82
+ color: var(--gy-text-subtle);
83
+ cursor: pointer;
84
+ background: none;
85
+ border: none;
86
+ transition: color var(--gy-duration-fast), background var(--gy-duration-fast);
87
+ margin-top: -2px;
88
+ }
89
+
90
+ .gy-toast-close:hover { color: var(--gy-text); background: var(--gy-background-muted); }
91
+
92
+ .gy-toast-progress {
93
+ position: absolute;
94
+ bottom: 0;
95
+ left: 0;
96
+ right: 0;
97
+ height: 3px;
98
+ background: var(--gy-primary-muted);
99
+ border-radius: 0 0 var(--gy-radius-xl) var(--gy-radius-xl);
100
+ }
101
+
102
+ .gy-toast-progress-bar {
103
+ height: 100%;
104
+ background: var(--gy-primary);
105
+ border-radius: inherit;
106
+ transform-origin: left;
107
+ animation: gy-toast-progress linear forwards;
108
+ }
109
+
110
+ @keyframes gy-toast-progress {
111
+ from { width: 100%; }
112
+ to { width: 0%; }
113
+ }
@@ -0,0 +1,88 @@
1
+ 'use client';
2
+
3
+ import React, { forwardRef, useId } from 'react';
4
+ import './toggle.css';
5
+
6
+ export type ToggleSize = 'sm' | 'md' | 'lg';
7
+
8
+ export interface ToggleProps {
9
+ checked?: boolean;
10
+ defaultChecked?: boolean;
11
+ onChange?: (checked: boolean) => void;
12
+ disabled?: boolean;
13
+ label?: React.ReactNode;
14
+ labelPosition?: 'left' | 'right';
15
+ size?: ToggleSize;
16
+ id?: string;
17
+ name?: string;
18
+ className?: string;
19
+ }
20
+
21
+ export const Toggle = forwardRef<HTMLInputElement, ToggleProps>(function Toggle(
22
+ {
23
+ checked,
24
+ defaultChecked,
25
+ onChange,
26
+ disabled = false,
27
+ label,
28
+ labelPosition = 'right',
29
+ size = 'md',
30
+ id,
31
+ name,
32
+ className = '',
33
+ },
34
+ ref
35
+ ) {
36
+ const uid = useId();
37
+ const inputId = id ?? uid;
38
+ const isChecked = checked ?? defaultChecked ?? false;
39
+
40
+ const track = (
41
+ <span
42
+ className={[
43
+ 'gy-toggle-track',
44
+ `gy-toggle-track--${size}`,
45
+ isChecked ? 'gy-toggle-track--checked' : '',
46
+ ]
47
+ .filter(Boolean)
48
+ .join(' ')}
49
+ aria-hidden="true"
50
+ >
51
+ <span className="gy-toggle-thumb" />
52
+ </span>
53
+ );
54
+
55
+ const labelEl = label && (
56
+ <span className="gy-toggle-label">{label}</span>
57
+ );
58
+
59
+ return (
60
+ <label
61
+ htmlFor={inputId}
62
+ className={[
63
+ 'gy-toggle-root',
64
+ disabled ? 'gy-toggle-root--disabled' : '',
65
+ className,
66
+ ]
67
+ .filter(Boolean)
68
+ .join(' ')}
69
+ >
70
+ <input
71
+ ref={ref}
72
+ id={inputId}
73
+ type="checkbox"
74
+ role="switch"
75
+ name={name}
76
+ checked={checked}
77
+ defaultChecked={defaultChecked}
78
+ disabled={disabled}
79
+ className="gy-toggle-input"
80
+ aria-checked={isChecked}
81
+ onChange={(e) => onChange?.(e.target.checked)}
82
+ />
83
+ {labelPosition === 'left' && labelEl}
84
+ {track}
85
+ {labelPosition === 'right' && labelEl}
86
+ </label>
87
+ );
88
+ });
@@ -0,0 +1,2 @@
1
+ export { Toggle } from './Toggle';
2
+ export type { ToggleProps, ToggleSize } from './Toggle';
@@ -0,0 +1,63 @@
1
+ /* ── Toggle / Switch ──────────────────────────────────────────────────────── */
2
+ .gy-toggle-root {
3
+ display: inline-flex;
4
+ align-items: center;
5
+ gap: var(--gy-spacing-2-5);
6
+ cursor: pointer;
7
+ }
8
+
9
+ .gy-toggle-root--disabled {
10
+ cursor: not-allowed;
11
+ opacity: 0.5;
12
+ }
13
+
14
+ .gy-toggle-input {
15
+ position: absolute;
16
+ opacity: 0;
17
+ width: 0;
18
+ height: 0;
19
+ }
20
+
21
+ .gy-toggle-track {
22
+ position: relative;
23
+ background: var(--gy-border-strong);
24
+ border-radius: var(--gy-radius-full);
25
+ transition:
26
+ background-color var(--gy-duration-fast) var(--gy-ease-in-out),
27
+ box-shadow var(--gy-duration-fast) var(--gy-ease-in-out);
28
+ }
29
+
30
+ .gy-toggle-track--sm { width: 2rem; height: 1.125rem; }
31
+ .gy-toggle-track--md { width: 2.5rem; height: 1.375rem; }
32
+ .gy-toggle-track--lg { width: 3rem; height: 1.625rem; }
33
+
34
+ .gy-toggle-track--checked { background: var(--gy-primary); }
35
+
36
+ .gy-toggle-input:focus-visible ~ .gy-toggle-label,
37
+ .gy-toggle-root:has(.gy-toggle-input:focus-visible) .gy-toggle-track {
38
+ box-shadow: 0 0 0 3px var(--gy-focus-ring);
39
+ }
40
+
41
+ .gy-toggle-thumb {
42
+ position: absolute;
43
+ top: 50%;
44
+ transform: translateY(-50%) translateX(2px);
45
+ background: white;
46
+ border-radius: 50%;
47
+ box-shadow: var(--gy-shadow-sm);
48
+ transition: transform var(--gy-duration-normal) var(--gy-ease-spring);
49
+ }
50
+
51
+ .gy-toggle-track--sm .gy-toggle-thumb { width: 0.875rem; height: 0.875rem; }
52
+ .gy-toggle-track--md .gy-toggle-thumb { width: 1.0625rem; height: 1.0625rem; }
53
+ .gy-toggle-track--lg .gy-toggle-thumb { width: 1.25rem; height: 1.25rem; }
54
+
55
+ .gy-toggle-track--sm.gy-toggle-track--checked .gy-toggle-thumb { transform: translateY(-50%) translateX(calc(2rem - 0.875rem - 2px)); }
56
+ .gy-toggle-track--md.gy-toggle-track--checked .gy-toggle-thumb { transform: translateY(-50%) translateX(calc(2.5rem - 1.0625rem - 2px)); }
57
+ .gy-toggle-track--lg.gy-toggle-track--checked .gy-toggle-thumb { transform: translateY(-50%) translateX(calc(3rem - 1.25rem - 2px)); }
58
+
59
+ .gy-toggle-label {
60
+ font-size: var(--gy-font-size-sm);
61
+ font-weight: var(--gy-font-weight-medium);
62
+ color: var(--gy-text);
63
+ }
@@ -0,0 +1,58 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import './typography.css';
5
+
6
+ export type TypographyVariant =
7
+ | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
8
+ | 'body-xl' | 'body-lg' | 'body-md' | 'body-sm' | 'body-xs'
9
+ | 'caption-lg' | 'caption-md'
10
+ | 'label' | 'code';
11
+
12
+ export type TypographyColor =
13
+ | 'default' | 'muted' | 'subtle' | 'disabled' | 'inverse'
14
+ | 'primary' | 'success' | 'warning' | 'danger' | 'info';
15
+
16
+ const variantTagMap: Record<TypographyVariant, React.ElementType> = {
17
+ h1: 'h1', h2: 'h2', h3: 'h3', h4: 'h4', h5: 'h5', h6: 'h6',
18
+ 'body-xl': 'p', 'body-lg': 'p', 'body-md': 'p', 'body-sm': 'p', 'body-xs': 'p',
19
+ 'caption-lg': 'span', 'caption-md': 'span',
20
+ label: 'label',
21
+ code: 'code',
22
+ };
23
+
24
+ export interface TypographyProps {
25
+ variant?: TypographyVariant;
26
+ color?: TypographyColor;
27
+ as?: React.ElementType;
28
+ truncate?: boolean;
29
+ className?: string;
30
+ children?: React.ReactNode;
31
+ id?: string;
32
+ htmlFor?: string;
33
+ style?: React.CSSProperties;
34
+ }
35
+
36
+ export function Typography({
37
+ variant = 'body-md',
38
+ color = 'default',
39
+ as,
40
+ truncate = false,
41
+ className = '',
42
+ children,
43
+ ...rest
44
+ }: TypographyProps) {
45
+ const Tag = as ?? variantTagMap[variant];
46
+
47
+ const classes = [
48
+ 'gy-text',
49
+ `gy-text--${variant}`,
50
+ `gy-text--color-${color}`,
51
+ truncate ? 'gy-text--truncate' : '',
52
+ className,
53
+ ]
54
+ .filter(Boolean)
55
+ .join(' ');
56
+
57
+ return <Tag className={classes} {...rest}>{children}</Tag>;
58
+ }
@@ -0,0 +1,2 @@
1
+ export { Typography } from './Typography';
2
+ export type { TypographyProps, TypographyVariant, TypographyColor } from './Typography';