@galyan/ui 0.0.1 → 0.0.2

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 (90) hide show
  1. package/package.json +53 -22
  2. package/.turbo/turbo-build.log +0 -20
  3. package/eslint.config.mjs +0 -4
  4. package/src/accordion/Accordion.tsx +0 -88
  5. package/src/accordion/accordion.css +0 -78
  6. package/src/accordion/index.ts +0 -2
  7. package/src/animatednumber/AnimatedNumber.tsx +0 -71
  8. package/src/animatednumber/index.ts +0 -2
  9. package/src/banner/Banner.tsx +0 -84
  10. package/src/banner/banner.css +0 -56
  11. package/src/banner/index.ts +0 -2
  12. package/src/breadcrumb/Breadcrumb.tsx +0 -94
  13. package/src/breadcrumb/breadcrumb.css +0 -58
  14. package/src/breadcrumb/index.ts +0 -2
  15. package/src/button/Button.tsx +0 -98
  16. package/src/button/button.css +0 -190
  17. package/src/button/index.ts +0 -2
  18. package/src/button.tsx +0 -20
  19. package/src/calendar/Calendar.tsx +0 -190
  20. package/src/calendar/calendar.css +0 -142
  21. package/src/calendar/index.ts +0 -2
  22. package/src/card/Card.tsx +0 -88
  23. package/src/card/card.css +0 -115
  24. package/src/card/index.ts +0 -2
  25. package/src/card.tsx +0 -27
  26. package/src/chart/Chart.tsx +0 -186
  27. package/src/chart/index.ts +0 -2
  28. package/src/checkbox/Checkbox.tsx +0 -101
  29. package/src/checkbox/checkbox.css +0 -76
  30. package/src/checkbox/index.ts +0 -2
  31. package/src/chips/Chips.tsx +0 -146
  32. package/src/chips/chips.css +0 -90
  33. package/src/chips/index.ts +0 -2
  34. package/src/code.tsx +0 -11
  35. package/src/datepicker/DatePicker.tsx +0 -104
  36. package/src/datepicker/datepicker.css +0 -22
  37. package/src/datepicker/index.ts +0 -2
  38. package/src/dragdrop/DragDrop.tsx +0 -168
  39. package/src/dragdrop/dragdrop.css +0 -122
  40. package/src/dragdrop/index.ts +0 -2
  41. package/src/dropdown/Dropdown.tsx +0 -176
  42. package/src/dropdown/dropdown.css +0 -118
  43. package/src/dropdown/index.ts +0 -2
  44. package/src/fileupload/FileUpload.tsx +0 -144
  45. package/src/fileupload/fileupload.css +0 -118
  46. package/src/fileupload/index.ts +0 -2
  47. package/src/index.ts +0 -28
  48. package/src/input/Input.tsx +0 -121
  49. package/src/input/index.ts +0 -2
  50. package/src/input/input.css +0 -120
  51. package/src/menu/Menu.tsx +0 -118
  52. package/src/menu/index.ts +0 -2
  53. package/src/menu/menu.css +0 -117
  54. package/src/modal/Modal.tsx +0 -103
  55. package/src/modal/index.ts +0 -2
  56. package/src/modal/modal.css +0 -79
  57. package/src/progressbar/ProgressBar.tsx +0 -68
  58. package/src/progressbar/index.ts +0 -2
  59. package/src/progressbar/progressbar.css +0 -73
  60. package/src/radiogroup/RadioGroup.tsx +0 -73
  61. package/src/radiogroup/index.ts +0 -2
  62. package/src/radiogroup/radiogroup.css +0 -80
  63. package/src/skeleton/Skeleton.tsx +0 -61
  64. package/src/skeleton/index.ts +0 -2
  65. package/src/skeleton/skeleton.css +0 -46
  66. package/src/spinner/Spinner.tsx +0 -29
  67. package/src/spinner/index.ts +0 -2
  68. package/src/spinner/spinner.css +0 -39
  69. package/src/stepper/Stepper.tsx +0 -127
  70. package/src/stepper/index.ts +0 -2
  71. package/src/stepper/stepper.css +0 -141
  72. package/src/tab/Tabs.tsx +0 -71
  73. package/src/tab/index.ts +0 -2
  74. package/src/tab/tab.css +0 -92
  75. package/src/table/Table.tsx +0 -205
  76. package/src/table/index.ts +0 -2
  77. package/src/table/table.css +0 -119
  78. package/src/textarea/Textarea.tsx +0 -102
  79. package/src/textarea/index.ts +0 -2
  80. package/src/textarea/textarea.css +0 -85
  81. package/src/toaster/Toaster.tsx +0 -108
  82. package/src/toaster/index.ts +0 -2
  83. package/src/toaster/toaster.css +0 -113
  84. package/src/toggle/Toggle.tsx +0 -88
  85. package/src/toggle/index.ts +0 -2
  86. package/src/toggle/toggle.css +0 -63
  87. package/src/typography/Typography.tsx +0 -58
  88. package/src/typography/index.ts +0 -2
  89. package/src/typography/typography.css +0 -102
  90. package/tsconfig.json +0 -13
@@ -1,121 +0,0 @@
1
- 'use client';
2
-
3
- import React, { forwardRef, useId } from 'react';
4
- import './input.css';
5
-
6
- export type InputSize = 'sm' | 'md' | 'lg';
7
-
8
- export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
9
- label?: string;
10
- size?: InputSize;
11
- error?: string;
12
- helperText?: string;
13
- leftIcon?: React.ReactNode;
14
- rightIcon?: React.ReactNode;
15
- clearable?: boolean;
16
- onClear?: () => void;
17
- required?: boolean;
18
- fullWidth?: boolean;
19
- }
20
-
21
- export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
22
- {
23
- label,
24
- size = 'md',
25
- error,
26
- helperText,
27
- leftIcon,
28
- rightIcon,
29
- clearable,
30
- onClear,
31
- required,
32
- fullWidth = true,
33
- disabled,
34
- className = '',
35
- id,
36
- value,
37
- onChange,
38
- ...rest
39
- },
40
- ref
41
- ) {
42
- const uid = useId();
43
- const inputId = id ?? uid;
44
-
45
- const wrapperClasses = [
46
- 'gy-input-wrapper',
47
- `gy-input-wrapper--${size}`,
48
- error ? 'gy-input-wrapper--error' : '',
49
- disabled ? 'gy-input-wrapper--disabled' : '',
50
- ]
51
- .filter(Boolean)
52
- .join(' ');
53
-
54
- const showClear = clearable && value && !disabled;
55
-
56
- return (
57
- <div className={`gy-input-root ${fullWidth ? '' : 'gy-input-root--inline'} ${className}`}>
58
- {label && (
59
- <label
60
- className={`gy-input-label ${required ? 'gy-input-label--required' : ''}`}
61
- htmlFor={inputId}
62
- >
63
- {label}
64
- </label>
65
- )}
66
-
67
- <div className={wrapperClasses}>
68
- {leftIcon && (
69
- <span className="gy-input-addon gy-input-addon--left" aria-hidden="true">
70
- {leftIcon}
71
- </span>
72
- )}
73
-
74
- <input
75
- ref={ref}
76
- id={inputId}
77
- className="gy-input"
78
- disabled={disabled}
79
- required={required}
80
- aria-invalid={!!error}
81
- aria-describedby={error ? `${inputId}-error` : helperText ? `${inputId}-helper` : undefined}
82
- value={value}
83
- onChange={onChange}
84
- {...rest}
85
- />
86
-
87
- {showClear && (
88
- <button
89
- type="button"
90
- className="gy-input-clear"
91
- onClick={onClear}
92
- aria-label="Clear input"
93
- tabIndex={-1}
94
- >
95
- <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="2">
96
- <line x1="1" y1="1" x2="13" y2="13" />
97
- <line x1="13" y1="1" x2="1" y2="13" />
98
- </svg>
99
- </button>
100
- )}
101
-
102
- {rightIcon && !showClear && (
103
- <span className="gy-input-addon gy-input-addon--right" aria-hidden="true">
104
- {rightIcon}
105
- </span>
106
- )}
107
- </div>
108
-
109
- {error && (
110
- <span id={`${inputId}-error`} className="gy-input-helper gy-input-helper--error" role="alert">
111
- {error}
112
- </span>
113
- )}
114
- {!error && helperText && (
115
- <span id={`${inputId}-helper`} className="gy-input-helper">
116
- {helperText}
117
- </span>
118
- )}
119
- </div>
120
- );
121
- });
@@ -1,2 +0,0 @@
1
- export { Input } from './Input';
2
- export type { InputProps, InputSize } from './Input';
@@ -1,120 +0,0 @@
1
- /* ── Input Component ──────────────────────────────────────────────────────── */
2
- .gy-input-root {
3
- display: flex;
4
- flex-direction: column;
5
- gap: var(--gy-spacing-1-5);
6
- width: 100%;
7
- }
8
-
9
- .gy-input-label {
10
- font-size: var(--gy-font-size-sm);
11
- font-weight: var(--gy-font-weight-medium);
12
- color: var(--gy-text);
13
- display: flex;
14
- align-items: center;
15
- gap: var(--gy-spacing-1);
16
- }
17
-
18
- .gy-input-label--required::after {
19
- content: '*';
20
- color: var(--gy-danger);
21
- margin-left: 2px;
22
- }
23
-
24
- .gy-input-wrapper {
25
- position: relative;
26
- display: flex;
27
- align-items: center;
28
- background: var(--gy-surface);
29
- border: 1.5px solid var(--gy-border);
30
- border-radius: var(--gy-radius-lg);
31
- transition:
32
- border-color var(--gy-duration-fast) var(--gy-ease-in-out),
33
- box-shadow var(--gy-duration-fast) var(--gy-ease-in-out);
34
- }
35
-
36
- .gy-input-wrapper:focus-within {
37
- border-color: var(--gy-border-focus);
38
- box-shadow: 0 0 0 3px var(--gy-focus-ring);
39
- }
40
-
41
- .gy-input-wrapper--error {
42
- border-color: var(--gy-danger) !important;
43
- box-shadow: none !important;
44
- }
45
-
46
- .gy-input-wrapper--error:focus-within {
47
- box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2) !important;
48
- }
49
-
50
- .gy-input-wrapper--disabled {
51
- background: var(--gy-disabled-bg);
52
- border-color: var(--gy-disabled-border);
53
- cursor: not-allowed;
54
- }
55
-
56
- .gy-input-addon {
57
- display: flex;
58
- align-items: center;
59
- justify-content: center;
60
- color: var(--gy-text-subtle);
61
- flex-shrink: 0;
62
- }
63
-
64
- .gy-input-addon--left { padding-left: var(--gy-spacing-3); }
65
- .gy-input-addon--right { padding-right: var(--gy-spacing-3); }
66
-
67
- .gy-input {
68
- flex: 1;
69
- border: none;
70
- outline: none;
71
- background: transparent;
72
- color: var(--gy-text);
73
- font-family: var(--gy-font-sans);
74
- width: 100%;
75
- min-width: 0;
76
- }
77
-
78
- .gy-input::placeholder { color: var(--gy-text-subtle); }
79
-
80
- .gy-input:disabled {
81
- cursor: not-allowed;
82
- color: var(--gy-text-disabled);
83
- }
84
-
85
- /* Sizes */
86
- .gy-input-wrapper--sm .gy-input { padding: var(--gy-spacing-1-5) var(--gy-spacing-3); font-size: var(--gy-font-size-sm); }
87
- .gy-input-wrapper--md .gy-input { padding: var(--gy-spacing-2-5) var(--gy-spacing-3); font-size: var(--gy-font-size-sm); }
88
- .gy-input-wrapper--lg .gy-input { padding: var(--gy-spacing-3) var(--gy-spacing-4); font-size: var(--gy-font-size-md); }
89
-
90
- .gy-input-wrapper--sm { min-height: 2rem; }
91
- .gy-input-wrapper--md { min-height: 2.5rem; }
92
- .gy-input-wrapper--lg { min-height: 3rem; }
93
-
94
- /* Left addon spacing fix */
95
- .gy-input-wrapper .gy-input-addon--left + .gy-input { padding-left: var(--gy-spacing-2); }
96
- .gy-input-wrapper .gy-input:has(+ .gy-input-addon--right) { padding-right: var(--gy-spacing-2); }
97
-
98
- /* Clear button */
99
- .gy-input-clear {
100
- display: flex;
101
- align-items: center;
102
- justify-content: center;
103
- padding: var(--gy-spacing-1);
104
- margin-right: var(--gy-spacing-1);
105
- color: var(--gy-text-subtle);
106
- border-radius: var(--gy-radius-full);
107
- cursor: pointer;
108
- transition: color var(--gy-duration-fast), background var(--gy-duration-fast);
109
- }
110
-
111
- .gy-input-clear:hover { color: var(--gy-text); background: var(--gy-background-muted); }
112
-
113
- /* Helper / Error text */
114
- .gy-input-helper {
115
- font-size: var(--gy-font-size-xs);
116
- color: var(--gy-text-subtle);
117
- margin-top: var(--gy-spacing-0-5);
118
- }
119
-
120
- .gy-input-helper--error { color: var(--gy-danger); }
package/src/menu/Menu.tsx DELETED
@@ -1,118 +0,0 @@
1
- 'use client';
2
-
3
- import React, { useEffect, useRef, useState } from 'react';
4
- import './menu.css';
5
-
6
- // ── Tooltip ───────────────────────────────────────────────────────────────────
7
- export type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';
8
-
9
- export interface TooltipProps {
10
- content: React.ReactNode;
11
- children: React.ReactElement;
12
- position?: TooltipPosition;
13
- delay?: number;
14
- disabled?: boolean;
15
- }
16
-
17
- export function Tooltip({ content, children, position = 'top', delay = 300, disabled = false }: TooltipProps) {
18
- const [visible, setVisible] = useState(false);
19
- const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
20
-
21
- const show = () => { timerRef.current = setTimeout(() => setVisible(true), delay); };
22
- const hide = () => { clearTimeout(timerRef.current); setVisible(false); };
23
-
24
- return (
25
- <span className="gy-tooltip-wrapper" onMouseEnter={show} onMouseLeave={hide} onFocus={show} onBlur={hide}>
26
- {children}
27
- {visible && !disabled && content && (
28
- <span className={`gy-tooltip gy-tooltip--${position}`} role="tooltip">
29
- {content}
30
- </span>
31
- )}
32
- </span>
33
- );
34
- }
35
-
36
- // ── Menu ──────────────────────────────────────────────────────────────────────
37
- export interface MenuItemDef {
38
- id: string;
39
- label: React.ReactNode;
40
- icon?: React.ReactNode;
41
- onClick?: () => void;
42
- disabled?: boolean;
43
- danger?: boolean;
44
- badge?: string;
45
- separator?: never;
46
- groupLabel?: never;
47
- }
48
-
49
- export interface MenuSeparator { separator: true; id: string; }
50
- export interface MenuGroupLabel { groupLabel: string; id: string; }
51
-
52
- export type MenuEntry = MenuItemDef | MenuSeparator | MenuGroupLabel;
53
-
54
- export interface MenuProps {
55
- items: MenuEntry[];
56
- trigger: React.ReactElement;
57
- position?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
58
- className?: string;
59
- }
60
-
61
- const posStyle: Record<string, React.CSSProperties> = {
62
- 'bottom-left': { top: 'calc(100% + 4px)', left: 0 },
63
- 'bottom-right': { top: 'calc(100% + 4px)', right: 0 },
64
- 'top-left': { bottom: 'calc(100% + 4px)', left: 0 },
65
- 'top-right': { bottom: 'calc(100% + 4px)', right: 0 },
66
- };
67
-
68
- export function Menu({ items, trigger, position = 'bottom-left', className = '' }: MenuProps) {
69
- const [open, setOpen] = useState(false);
70
- const rootRef = useRef<HTMLDivElement>(null);
71
-
72
- useEffect(() => {
73
- const handler = (e: MouseEvent) => {
74
- if (!rootRef.current?.contains(e.target as Node)) setOpen(false);
75
- };
76
- document.addEventListener('mousedown', handler);
77
- return () => document.removeEventListener('mousedown', handler);
78
- }, []);
79
-
80
- return (
81
- <div ref={rootRef} className="gy-menu-trigger" style={{ position: 'relative' }}>
82
- {React.cloneElement(trigger as React.ReactElement<any>, { onClick: () => setOpen((o) => !o) })}
83
- {open && (
84
- <div className={`gy-menu gy-menu--absolute ${className}`} style={posStyle[position]} role="menu">
85
- {items.map((entry) => {
86
- if ('separator' in entry && entry.separator) {
87
- return <div key={entry.id} className="gy-menu-separator" role="separator" />;
88
- }
89
- if ('groupLabel' in entry && entry.groupLabel) {
90
- return <div key={entry.id} className="gy-menu-group-label">{entry.groupLabel}</div>;
91
- }
92
- const item = entry as MenuItemDef;
93
- return (
94
- <button
95
- key={item.id}
96
- type="button"
97
- className={[
98
- 'gy-menu-item',
99
- item.danger ? 'gy-menu-item--danger' : '',
100
- item.disabled ? 'gy-menu-item--disabled' : '',
101
- ]
102
- .filter(Boolean)
103
- .join(' ')}
104
- role="menuitem"
105
- disabled={item.disabled}
106
- onClick={() => { item.onClick?.(); setOpen(false); }}
107
- >
108
- {item.icon && <span className="gy-menu-item__icon" aria-hidden="true">{item.icon}</span>}
109
- {item.label}
110
- {item.badge && <span className="gy-menu-item__badge">{item.badge}</span>}
111
- </button>
112
- );
113
- })}
114
- </div>
115
- )}
116
- </div>
117
- );
118
- }
package/src/menu/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export { Tooltip, Menu } from './Menu';
2
- export type { TooltipProps, TooltipPosition, MenuProps, MenuItemDef, MenuEntry } from './Menu';
package/src/menu/menu.css DELETED
@@ -1,117 +0,0 @@
1
- /* ── Tooltip ─────────────────────────────────────────────────────────────── */
2
- .gy-tooltip-wrapper {
3
- position: relative;
4
- display: inline-flex;
5
- }
6
-
7
- .gy-tooltip {
8
- position: absolute;
9
- z-index: var(--gy-z-tooltip);
10
- background: var(--gy-text);
11
- color: var(--gy-text-inverse);
12
- font-size: var(--gy-font-size-xs);
13
- font-weight: var(--gy-font-weight-medium);
14
- padding: var(--gy-spacing-1-5) var(--gy-spacing-2-5);
15
- border-radius: var(--gy-radius-md);
16
- white-space: nowrap;
17
- pointer-events: none;
18
- box-shadow: var(--gy-shadow-sm);
19
- animation: gy-fade-in var(--gy-duration-fast) var(--gy-ease-out);
20
- max-width: 240px;
21
- word-wrap: break-word;
22
- white-space: normal;
23
- text-align: center;
24
- line-height: 1.4;
25
- }
26
-
27
- /* Arrow */
28
- .gy-tooltip::after {
29
- content: '';
30
- position: absolute;
31
- border: 5px solid transparent;
32
- }
33
-
34
- .gy-tooltip--top { bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%); }
35
- .gy-tooltip--top::after { top: 100%; left: 50%; transform: translateX(-50%); border-top-color: var(--gy-text); }
36
-
37
- .gy-tooltip--bottom { top: calc(100% + 8px); left: 50%; transform: translateX(-50%); }
38
- .gy-tooltip--bottom::after { bottom: 100%; left: 50%; transform: translateX(-50%); border-bottom-color: var(--gy-text); }
39
-
40
- .gy-tooltip--left { right: calc(100% + 8px); top: 50%; transform: translateY(-50%); }
41
- .gy-tooltip--left::after { left: 100%; top: 50%; transform: translateY(-50%); border-left-color: var(--gy-text); }
42
-
43
- .gy-tooltip--right { left: calc(100% + 8px); top: 50%; transform: translateY(-50%); }
44
- .gy-tooltip--right::after { right: 100%; top: 50%; transform: translateY(-50%); border-right-color: var(--gy-text); }
45
-
46
- /* ── Menu ────────────────────────────────────────────────────────────────── */
47
- .gy-menu {
48
- background: var(--gy-surface);
49
- border: 1px solid var(--gy-border);
50
- border-radius: var(--gy-radius-xl);
51
- box-shadow: var(--gy-shadow-dropdown);
52
- padding: var(--gy-spacing-1) 0;
53
- min-width: 180px;
54
- z-index: var(--gy-z-dropdown);
55
- animation: gy-slide-in-up var(--gy-duration-fast) var(--gy-ease-out);
56
- }
57
-
58
- .gy-menu--absolute {
59
- position: absolute;
60
- }
61
-
62
- .gy-menu-item {
63
- display: flex;
64
- align-items: center;
65
- gap: var(--gy-spacing-2-5);
66
- padding: var(--gy-spacing-2) var(--gy-spacing-4);
67
- font-size: var(--gy-font-size-sm);
68
- color: var(--gy-text);
69
- cursor: pointer;
70
- transition: background-color var(--gy-duration-fast);
71
- border: none;
72
- background: none;
73
- width: 100%;
74
- text-align: left;
75
- border-radius: 0;
76
- }
77
-
78
- .gy-menu-item:hover { background: var(--gy-background-subtle); }
79
- .gy-menu-item:focus-visible { outline: none; background: var(--gy-background-muted); }
80
-
81
- .gy-menu-item--danger { color: var(--gy-danger); }
82
- .gy-menu-item--danger:hover { background: var(--gy-danger-subtle); }
83
-
84
- .gy-menu-item--disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
85
-
86
- .gy-menu-item__icon {
87
- display: flex;
88
- align-items: center;
89
- flex-shrink: 0;
90
- color: var(--gy-text-subtle);
91
- }
92
-
93
- .gy-menu-item--danger .gy-menu-item__icon { color: var(--gy-danger); }
94
-
95
- .gy-menu-item__badge {
96
- margin-left: auto;
97
- font-size: var(--gy-font-size-xs);
98
- color: var(--gy-text-subtle);
99
- }
100
-
101
- .gy-menu-separator {
102
- height: 1px;
103
- background: var(--gy-divider);
104
- margin: var(--gy-spacing-1) 0;
105
- }
106
-
107
- .gy-menu-group-label {
108
- padding: var(--gy-spacing-2) var(--gy-spacing-4) var(--gy-spacing-1);
109
- font-size: var(--gy-font-size-xs);
110
- font-weight: var(--gy-font-weight-semibold);
111
- color: var(--gy-text-subtle);
112
- letter-spacing: var(--gy-letter-spacing-wider);
113
- text-transform: uppercase;
114
- }
115
-
116
- /* Context menu trigger */
117
- .gy-menu-trigger { position: relative; display: inline-flex; }
@@ -1,103 +0,0 @@
1
- 'use client';
2
-
3
- import React, { useEffect, useRef } from 'react';
4
- import { createPortal } from 'react-dom';
5
- import './modal.css';
6
-
7
- export type ModalSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
8
-
9
- export interface ModalProps {
10
- open: boolean;
11
- onClose: () => void;
12
- title?: React.ReactNode;
13
- size?: ModalSize;
14
- children?: React.ReactNode;
15
- footer?: React.ReactNode;
16
- closeOnBackdrop?: boolean;
17
- closeOnEscape?: boolean;
18
- className?: string;
19
- }
20
-
21
- export function Modal({
22
- open,
23
- onClose,
24
- title,
25
- size = 'md',
26
- children,
27
- footer,
28
- closeOnBackdrop = true,
29
- closeOnEscape = true,
30
- className = '',
31
- }: ModalProps) {
32
- const dialogRef = useRef<HTMLDivElement>(null);
33
-
34
- // Focus trap and keyboard handling
35
- useEffect(() => {
36
- if (!open) return;
37
-
38
- const previous = document.activeElement as HTMLElement;
39
- dialogRef.current?.focus();
40
-
41
- const handleKey = (e: KeyboardEvent) => {
42
- if (e.key === 'Escape' && closeOnEscape) onClose();
43
- if (e.key === 'Tab') {
44
- const focusable = dialogRef.current?.querySelectorAll<HTMLElement>(
45
- 'a, button:not(:disabled), textarea, input, select, [tabindex]:not([tabindex="-1"])'
46
- );
47
- if (!focusable?.length) return;
48
- const first = focusable[0]!;
49
- const last = focusable[focusable.length - 1]!;
50
- if (e.shiftKey && document.activeElement === first) {
51
- e.preventDefault();
52
- last.focus();
53
- } else if (!e.shiftKey && document.activeElement === last) {
54
- e.preventDefault();
55
- first.focus();
56
- }
57
- }
58
- };
59
-
60
- document.addEventListener('keydown', handleKey);
61
- document.body.style.overflow = 'hidden';
62
-
63
- return () => {
64
- document.removeEventListener('keydown', handleKey);
65
- document.body.style.overflow = '';
66
- previous?.focus();
67
- };
68
- }, [open, onClose, closeOnEscape]);
69
-
70
- if (!open) return null;
71
-
72
- const content = (
73
- <div
74
- className="gy-modal-backdrop"
75
- onClick={closeOnBackdrop ? (e) => { if (e.target === e.currentTarget) onClose(); } : undefined}
76
- aria-modal="true"
77
- >
78
- <div
79
- ref={dialogRef}
80
- role="dialog"
81
- aria-labelledby={title ? 'gy-modal-title' : undefined}
82
- className={`gy-modal gy-modal--${size} ${className}`}
83
- tabIndex={-1}
84
- >
85
- {title && (
86
- <div className="gy-modal-header">
87
- <h2 id="gy-modal-title" className="gy-modal-title">{title}</h2>
88
- <button className="gy-modal-close" onClick={onClose} aria-label="Close dialog">
89
- <svg width="16" height="16" viewBox="0 0 16 16" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
90
- <line x1="1" y1="1" x2="15" y2="15" />
91
- <line x1="15" y1="1" x2="1" y2="15" />
92
- </svg>
93
- </button>
94
- </div>
95
- )}
96
- <div className="gy-modal-body">{children}</div>
97
- {footer && <div className="gy-modal-footer">{footer}</div>}
98
- </div>
99
- </div>
100
- );
101
-
102
- return createPortal(content, document.body);
103
- }
@@ -1,2 +0,0 @@
1
- export { Modal } from './Modal';
2
- export type { ModalProps, ModalSize } from './Modal';
@@ -1,79 +0,0 @@
1
- /* ── Modal ────────────────────────────────────────────────────────────────── */
2
- .gy-modal-backdrop {
3
- position: fixed;
4
- inset: 0;
5
- background: var(--gy-overlay);
6
- backdrop-filter: blur(4px);
7
- z-index: var(--gy-z-modal);
8
- display: flex;
9
- align-items: center;
10
- justify-content: center;
11
- padding: var(--gy-spacing-4);
12
- animation: gy-fade-in var(--gy-duration-normal) var(--gy-ease-out);
13
- }
14
-
15
- .gy-modal {
16
- background: var(--gy-surface);
17
- border-radius: var(--gy-radius-2xl);
18
- box-shadow: var(--gy-shadow-modal);
19
- display: flex;
20
- flex-direction: column;
21
- max-height: calc(100vh - 2rem);
22
- width: 100%;
23
- animation: gy-zoom-in var(--gy-duration-normal) var(--gy-ease-spring);
24
- outline: none;
25
- }
26
-
27
- .gy-modal--xs { max-width: 20rem; }
28
- .gy-modal--sm { max-width: 28rem; }
29
- .gy-modal--md { max-width: 36rem; }
30
- .gy-modal--lg { max-width: 48rem; }
31
- .gy-modal--xl { max-width: 64rem; }
32
- .gy-modal--full { max-width: calc(100vw - 2rem); max-height: calc(100vh - 2rem); }
33
-
34
- .gy-modal-header {
35
- display: flex;
36
- align-items: center;
37
- justify-content: space-between;
38
- padding: var(--gy-spacing-5) var(--gy-spacing-6);
39
- border-bottom: 1px solid var(--gy-border);
40
- flex-shrink: 0;
41
- }
42
-
43
- .gy-modal-title {
44
- font-size: var(--gy-font-size-lg);
45
- font-weight: var(--gy-font-weight-semibold);
46
- color: var(--gy-text);
47
- }
48
-
49
- .gy-modal-close {
50
- display: flex;
51
- align-items: center;
52
- justify-content: center;
53
- width: 2rem;
54
- height: 2rem;
55
- border-radius: var(--gy-radius-md);
56
- color: var(--gy-text-subtle);
57
- cursor: pointer;
58
- background: none;
59
- border: none;
60
- transition: color var(--gy-duration-fast), background var(--gy-duration-fast);
61
- }
62
-
63
- .gy-modal-close:hover { color: var(--gy-text); background: var(--gy-background-muted); }
64
-
65
- .gy-modal-body {
66
- flex: 1;
67
- overflow-y: auto;
68
- padding: var(--gy-spacing-6);
69
- }
70
-
71
- .gy-modal-footer {
72
- display: flex;
73
- align-items: center;
74
- justify-content: flex-end;
75
- gap: var(--gy-spacing-3);
76
- padding: var(--gy-spacing-4) var(--gy-spacing-6);
77
- border-top: 1px solid var(--gy-border);
78
- flex-shrink: 0;
79
- }