@justin_evo/evo-ui 1.2.0 → 1.2.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 (77) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +70 -70
  3. package/dist/declarations.d.ts +6 -6
  4. package/package.json +52 -52
  5. package/src/Alert/Alert.tsx +49 -49
  6. package/src/AutoComplete/AutoComplete.tsx +810 -810
  7. package/src/Badge/Badge.tsx +53 -53
  8. package/src/Breadcrumb/Breadcrumb.tsx +53 -53
  9. package/src/Button/Button.tsx +125 -125
  10. package/src/Card/Card.tsx +257 -257
  11. package/src/Checkbox/Checkbox.tsx +59 -59
  12. package/src/CommandPalette/CommandPalette.tsx +185 -185
  13. package/src/Container/Container.tsx +31 -31
  14. package/src/Divider/Divider.tsx +31 -31
  15. package/src/Form/Form.tsx +185 -185
  16. package/src/Grid/Grid.tsx +66 -66
  17. package/src/ImageCropper/ImageCropper.tsx +911 -911
  18. package/src/Input/Input.tsx +74 -74
  19. package/src/Modal/Modal.tsx +77 -77
  20. package/src/Nav/Nav.tsx +708 -708
  21. package/src/Notification/Notification.tsx +1503 -1503
  22. package/src/Pagination/Pagination.tsx +76 -76
  23. package/src/Radio/Radio.tsx +69 -69
  24. package/src/RichTextArea/RichTextArea.tsx +886 -886
  25. package/src/Select/Select.tsx +515 -515
  26. package/src/Skeleton/Skeleton.tsx +70 -70
  27. package/src/Stack/Stack.tsx +52 -52
  28. package/src/Table/Table.tsx +335 -335
  29. package/src/Tabs/Tabs.tsx +90 -90
  30. package/src/Theme/ThemeProvider.tsx +253 -253
  31. package/src/Theme/ThemeToggle.tsx +79 -79
  32. package/src/Toggle/Toggle.tsx +48 -48
  33. package/src/Tooltip/Tooltip.tsx +38 -38
  34. package/src/TopNav/TopNav.tsx +1163 -1163
  35. package/src/TreeSelect/TreeSelect.tsx +825 -825
  36. package/src/css/alert.module.scss +93 -93
  37. package/src/css/autocomplete.module.scss +416 -416
  38. package/src/css/badge.module.scss +82 -82
  39. package/src/css/base/_color.scss +159 -159
  40. package/src/css/base/_theme.scss +237 -237
  41. package/src/css/base/_variables.scss +161 -161
  42. package/src/css/breadcrumb.module.scss +50 -50
  43. package/src/css/button.module.scss +385 -385
  44. package/src/css/card.module.scss +217 -217
  45. package/src/css/checkbox.module.scss +123 -123
  46. package/src/css/commandpalette.module.scss +211 -211
  47. package/src/css/container.module.scss +18 -18
  48. package/src/css/divider.module.scss +41 -41
  49. package/src/css/form.module.scss +245 -245
  50. package/src/css/imagecropper.module.scss +397 -397
  51. package/src/css/input.module.scss +89 -89
  52. package/src/css/modal.module.scss +105 -105
  53. package/src/css/nav.module.scss +494 -494
  54. package/src/css/notification.module.scss +691 -691
  55. package/src/css/pagination.module.scss +63 -63
  56. package/src/css/radio.module.scss +89 -89
  57. package/src/css/richtextarea.module.scss +307 -307
  58. package/src/css/select.module.scss +525 -525
  59. package/src/css/skeleton.module.scss +30 -30
  60. package/src/css/table.module.scss +386 -386
  61. package/src/css/tabs.module.scss +63 -63
  62. package/src/css/theme-toggle.module.scss +83 -83
  63. package/src/css/toggle.module.scss +54 -54
  64. package/src/css/tooltip.module.scss +97 -97
  65. package/src/css/topnav.module.scss +568 -568
  66. package/src/css/treeselect.module.scss +558 -558
  67. package/src/css/utilities/_borders.scss +111 -111
  68. package/src/css/utilities/_colors.scss +66 -66
  69. package/src/css/utilities/_effects.scss +216 -216
  70. package/src/css/utilities/_layout.scss +181 -181
  71. package/src/css/utilities/_position.scss +75 -75
  72. package/src/css/utilities/_sizing.scss +138 -138
  73. package/src/css/utilities/_spacing.scss +99 -99
  74. package/src/css/utilities/_typography.scss +121 -121
  75. package/src/css/utilities/index.scss +24 -24
  76. package/src/declarations.d.ts +6 -6
  77. package/src/index.ts +60 -60
@@ -1,185 +1,185 @@
1
- import React, { useState, useEffect, useRef, useCallback } from 'react';
2
- import styles from '../css/commandpalette.module.scss';
3
-
4
- export interface CommandPaletteItem {
5
- label: string;
6
- description?: string;
7
- group?: string;
8
- icon?: React.ReactNode;
9
- shortcut?: string[];
10
- onSelect: () => void;
11
- }
12
-
13
- interface EvoCommandPaletteProps {
14
- items: CommandPaletteItem[];
15
- placeholder?: string;
16
- open?: boolean;
17
- onClose?: () => void;
18
- }
19
-
20
- // Detect Mac for shortcut display only — keyboard handler uses ctrlKey||metaKey for both
21
- const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.userAgent);
22
-
23
- const SearchIcon = () => (
24
- <svg viewBox="0 0 16 16" fill="none" width="16" height="16">
25
- <circle cx="7" cy="7" r="4.5" stroke="currentColor" strokeWidth="1.5" />
26
- <path d="M10.5 10.5L13 13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
27
- </svg>
28
- );
29
-
30
- export const EvoCommandPalette = ({
31
- items,
32
- placeholder = 'Search commands…',
33
- open: controlledOpen,
34
- onClose,
35
- }: EvoCommandPaletteProps) => {
36
- const [internalOpen, setInternalOpen] = useState(false);
37
- const [query, setQuery] = useState('');
38
- const [activeIdx, setActiveIdx] = useState(0);
39
- const inputRef = useRef<HTMLInputElement>(null);
40
- const listRef = useRef<HTMLDivElement>(null);
41
-
42
- const isControlled = controlledOpen !== undefined;
43
- const isOpen = isControlled ? controlledOpen : internalOpen;
44
-
45
- const close = useCallback(() => {
46
- if (!isControlled) setInternalOpen(false);
47
- onClose?.();
48
- }, [isControlled, onClose]);
49
-
50
- // Ctrl+K (Windows/Linux) / ⌘+K (Mac)
51
- useEffect(() => {
52
- const handler = (e: KeyboardEvent) => {
53
- if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
54
- e.preventDefault();
55
- if (!isControlled) setInternalOpen(o => !o);
56
- }
57
- if (e.key === 'Escape' && isOpen) close();
58
- };
59
- document.addEventListener('keydown', handler);
60
- return () => document.removeEventListener('keydown', handler);
61
- }, [isControlled, isOpen, close]);
62
-
63
- useEffect(() => {
64
- if (isOpen) {
65
- setQuery('');
66
- setActiveIdx(0);
67
- // Small delay so the element is mounted before focus
68
- const t = setTimeout(() => inputRef.current?.focus(), 30);
69
- return () => clearTimeout(t);
70
- }
71
- }, [isOpen]);
72
-
73
- const filtered = query.trim()
74
- ? items.filter(item =>
75
- item.label.toLowerCase().includes(query.toLowerCase()) ||
76
- item.description?.toLowerCase().includes(query.toLowerCase()) ||
77
- item.group?.toLowerCase().includes(query.toLowerCase())
78
- )
79
- : items;
80
-
81
- const grouped = filtered.reduce<Record<string, CommandPaletteItem[]>>((acc, item) => {
82
- const g = item.group ?? 'Actions';
83
- if (!acc[g]) acc[g] = [];
84
- acc[g].push(item);
85
- return acc;
86
- }, {});
87
-
88
- const flat = Object.values(grouped).flat();
89
-
90
- const scrollActiveIntoView = (idx: number) => {
91
- const el = listRef.current?.querySelector(`[data-idx="${idx}"]`) as HTMLElement | null;
92
- el?.scrollIntoView({ block: 'nearest' });
93
- };
94
-
95
- const handleKeyDown = (e: React.KeyboardEvent) => {
96
- if (e.key === 'ArrowDown') {
97
- e.preventDefault();
98
- setActiveIdx(i => {
99
- const next = Math.min(i + 1, flat.length - 1);
100
- scrollActiveIntoView(next);
101
- return next;
102
- });
103
- } else if (e.key === 'ArrowUp') {
104
- e.preventDefault();
105
- setActiveIdx(i => {
106
- const next = Math.max(i - 1, 0);
107
- scrollActiveIntoView(next);
108
- return next;
109
- });
110
- } else if (e.key === 'Enter' && flat[activeIdx]) {
111
- flat[activeIdx].onSelect();
112
- close();
113
- }
114
- };
115
-
116
- if (!isOpen) return null;
117
-
118
- let globalIdx = 0;
119
-
120
- return (
121
- <div className={styles.overlay} onClick={close} role="dialog" aria-modal="true">
122
- <div
123
- className={styles.palette}
124
- onClick={e => e.stopPropagation()}
125
- onKeyDown={handleKeyDown}
126
- >
127
- <div className={styles.searchRow}>
128
- <span className={styles.searchIconWrap}><SearchIcon /></span>
129
- <input
130
- ref={inputRef}
131
- className={styles.searchInput}
132
- placeholder={placeholder}
133
- value={query}
134
- onChange={e => { setQuery(e.target.value); setActiveIdx(0); }}
135
- aria-label="Command search"
136
- />
137
- <kbd className={styles.escBadge}>Esc</kbd>
138
- </div>
139
-
140
- <div className={styles.results} ref={listRef}>
141
- {flat.length === 0 && (
142
- <div className={styles.empty}>No results for &ldquo;{query}&rdquo;</div>
143
- )}
144
- {Object.entries(grouped).map(([group, groupItems]) => (
145
- <div key={group} className={styles.group}>
146
- <div className={styles.groupLabel}>{group}</div>
147
- {groupItems.map(item => {
148
- const idx = globalIdx++;
149
- return (
150
- <button
151
- key={item.label}
152
- data-idx={idx}
153
- className={[styles.resultItem, idx === activeIdx ? styles.resultActive : '']
154
- .filter(Boolean)
155
- .join(' ')}
156
- onClick={() => { item.onSelect(); close(); }}
157
- onMouseEnter={() => setActiveIdx(idx)}
158
- >
159
- {item.icon && <span className={styles.resultIcon}>{item.icon}</span>}
160
- <span className={styles.resultLabel}>{item.label}</span>
161
- {item.description && <span className={styles.resultDesc}>{item.description}</span>}
162
- {item.shortcut && (
163
- <span className={styles.resultShortcut}>
164
- {item.shortcut.map((k, i) => <kbd key={i}>{k}</kbd>)}
165
- </span>
166
- )}
167
- </button>
168
- );
169
- })}
170
- </div>
171
- ))}
172
- </div>
173
-
174
- <div className={styles.footer}>
175
- <span><kbd>↑</kbd><kbd>↓</kbd> navigate</span>
176
- <span><kbd>↵</kbd> select</span>
177
- <span><kbd>Esc</kbd> close</span>
178
- <span className={styles.footerRight}>
179
- <kbd>{isMac ? '⌘' : 'Ctrl'}</kbd><kbd>K</kbd> toggle
180
- </span>
181
- </div>
182
- </div>
183
- </div>
184
- );
185
- };
1
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
2
+ import styles from '../css/commandpalette.module.scss';
3
+
4
+ export interface CommandPaletteItem {
5
+ label: string;
6
+ description?: string;
7
+ group?: string;
8
+ icon?: React.ReactNode;
9
+ shortcut?: string[];
10
+ onSelect: () => void;
11
+ }
12
+
13
+ interface EvoCommandPaletteProps {
14
+ items: CommandPaletteItem[];
15
+ placeholder?: string;
16
+ open?: boolean;
17
+ onClose?: () => void;
18
+ }
19
+
20
+ // Detect Mac for shortcut display only — keyboard handler uses ctrlKey||metaKey for both
21
+ const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.userAgent);
22
+
23
+ const SearchIcon = () => (
24
+ <svg viewBox="0 0 16 16" fill="none" width="16" height="16">
25
+ <circle cx="7" cy="7" r="4.5" stroke="currentColor" strokeWidth="1.5" />
26
+ <path d="M10.5 10.5L13 13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
27
+ </svg>
28
+ );
29
+
30
+ export const EvoCommandPalette = ({
31
+ items,
32
+ placeholder = 'Search commands…',
33
+ open: controlledOpen,
34
+ onClose,
35
+ }: EvoCommandPaletteProps) => {
36
+ const [internalOpen, setInternalOpen] = useState(false);
37
+ const [query, setQuery] = useState('');
38
+ const [activeIdx, setActiveIdx] = useState(0);
39
+ const inputRef = useRef<HTMLInputElement>(null);
40
+ const listRef = useRef<HTMLDivElement>(null);
41
+
42
+ const isControlled = controlledOpen !== undefined;
43
+ const isOpen = isControlled ? controlledOpen : internalOpen;
44
+
45
+ const close = useCallback(() => {
46
+ if (!isControlled) setInternalOpen(false);
47
+ onClose?.();
48
+ }, [isControlled, onClose]);
49
+
50
+ // Ctrl+K (Windows/Linux) / ⌘+K (Mac)
51
+ useEffect(() => {
52
+ const handler = (e: KeyboardEvent) => {
53
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
54
+ e.preventDefault();
55
+ if (!isControlled) setInternalOpen(o => !o);
56
+ }
57
+ if (e.key === 'Escape' && isOpen) close();
58
+ };
59
+ document.addEventListener('keydown', handler);
60
+ return () => document.removeEventListener('keydown', handler);
61
+ }, [isControlled, isOpen, close]);
62
+
63
+ useEffect(() => {
64
+ if (isOpen) {
65
+ setQuery('');
66
+ setActiveIdx(0);
67
+ // Small delay so the element is mounted before focus
68
+ const t = setTimeout(() => inputRef.current?.focus(), 30);
69
+ return () => clearTimeout(t);
70
+ }
71
+ }, [isOpen]);
72
+
73
+ const filtered = query.trim()
74
+ ? items.filter(item =>
75
+ item.label.toLowerCase().includes(query.toLowerCase()) ||
76
+ item.description?.toLowerCase().includes(query.toLowerCase()) ||
77
+ item.group?.toLowerCase().includes(query.toLowerCase())
78
+ )
79
+ : items;
80
+
81
+ const grouped = filtered.reduce<Record<string, CommandPaletteItem[]>>((acc, item) => {
82
+ const g = item.group ?? 'Actions';
83
+ if (!acc[g]) acc[g] = [];
84
+ acc[g].push(item);
85
+ return acc;
86
+ }, {});
87
+
88
+ const flat = Object.values(grouped).flat();
89
+
90
+ const scrollActiveIntoView = (idx: number) => {
91
+ const el = listRef.current?.querySelector(`[data-idx="${idx}"]`) as HTMLElement | null;
92
+ el?.scrollIntoView({ block: 'nearest' });
93
+ };
94
+
95
+ const handleKeyDown = (e: React.KeyboardEvent) => {
96
+ if (e.key === 'ArrowDown') {
97
+ e.preventDefault();
98
+ setActiveIdx(i => {
99
+ const next = Math.min(i + 1, flat.length - 1);
100
+ scrollActiveIntoView(next);
101
+ return next;
102
+ });
103
+ } else if (e.key === 'ArrowUp') {
104
+ e.preventDefault();
105
+ setActiveIdx(i => {
106
+ const next = Math.max(i - 1, 0);
107
+ scrollActiveIntoView(next);
108
+ return next;
109
+ });
110
+ } else if (e.key === 'Enter' && flat[activeIdx]) {
111
+ flat[activeIdx].onSelect();
112
+ close();
113
+ }
114
+ };
115
+
116
+ if (!isOpen) return null;
117
+
118
+ let globalIdx = 0;
119
+
120
+ return (
121
+ <div className={styles.overlay} onClick={close} role="dialog" aria-modal="true">
122
+ <div
123
+ className={styles.palette}
124
+ onClick={e => e.stopPropagation()}
125
+ onKeyDown={handleKeyDown}
126
+ >
127
+ <div className={styles.searchRow}>
128
+ <span className={styles.searchIconWrap}><SearchIcon /></span>
129
+ <input
130
+ ref={inputRef}
131
+ className={styles.searchInput}
132
+ placeholder={placeholder}
133
+ value={query}
134
+ onChange={e => { setQuery(e.target.value); setActiveIdx(0); }}
135
+ aria-label="Command search"
136
+ />
137
+ <kbd className={styles.escBadge}>Esc</kbd>
138
+ </div>
139
+
140
+ <div className={styles.results} ref={listRef}>
141
+ {flat.length === 0 && (
142
+ <div className={styles.empty}>No results for &ldquo;{query}&rdquo;</div>
143
+ )}
144
+ {Object.entries(grouped).map(([group, groupItems]) => (
145
+ <div key={group} className={styles.group}>
146
+ <div className={styles.groupLabel}>{group}</div>
147
+ {groupItems.map(item => {
148
+ const idx = globalIdx++;
149
+ return (
150
+ <button
151
+ key={item.label}
152
+ data-idx={idx}
153
+ className={[styles.resultItem, idx === activeIdx ? styles.resultActive : '']
154
+ .filter(Boolean)
155
+ .join(' ')}
156
+ onClick={() => { item.onSelect(); close(); }}
157
+ onMouseEnter={() => setActiveIdx(idx)}
158
+ >
159
+ {item.icon && <span className={styles.resultIcon}>{item.icon}</span>}
160
+ <span className={styles.resultLabel}>{item.label}</span>
161
+ {item.description && <span className={styles.resultDesc}>{item.description}</span>}
162
+ {item.shortcut && (
163
+ <span className={styles.resultShortcut}>
164
+ {item.shortcut.map((k, i) => <kbd key={i}>{k}</kbd>)}
165
+ </span>
166
+ )}
167
+ </button>
168
+ );
169
+ })}
170
+ </div>
171
+ ))}
172
+ </div>
173
+
174
+ <div className={styles.footer}>
175
+ <span><kbd>↑</kbd><kbd>↓</kbd> navigate</span>
176
+ <span><kbd>↵</kbd> select</span>
177
+ <span><kbd>Esc</kbd> close</span>
178
+ <span className={styles.footerRight}>
179
+ <kbd>{isMac ? '⌘' : 'Ctrl'}</kbd><kbd>K</kbd> toggle
180
+ </span>
181
+ </div>
182
+ </div>
183
+ </div>
184
+ );
185
+ };
@@ -1,31 +1,31 @@
1
- import React from 'react';
2
- import styles from '../css/container.module.scss';
3
-
4
- type ContainerSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';
5
-
6
- interface EvoContainerProps {
7
- children: React.ReactNode;
8
- size?: ContainerSize;
9
- centered?: boolean;
10
- className?: string;
11
- }
12
-
13
- export const EvoContainer = ({
14
- children,
15
- size = 'lg',
16
- centered = true,
17
- className = '',
18
- }: EvoContainerProps) => (
19
- <div
20
- className={[
21
- styles.container,
22
- styles[size],
23
- centered ? styles.centered : '',
24
- className,
25
- ]
26
- .filter(Boolean)
27
- .join(' ')}
28
- >
29
- {children}
30
- </div>
31
- );
1
+ import React from 'react';
2
+ import styles from '../css/container.module.scss';
3
+
4
+ type ContainerSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';
5
+
6
+ interface EvoContainerProps {
7
+ children: React.ReactNode;
8
+ size?: ContainerSize;
9
+ centered?: boolean;
10
+ className?: string;
11
+ }
12
+
13
+ export const EvoContainer = ({
14
+ children,
15
+ size = 'lg',
16
+ centered = true,
17
+ className = '',
18
+ }: EvoContainerProps) => (
19
+ <div
20
+ className={[
21
+ styles.container,
22
+ styles[size],
23
+ centered ? styles.centered : '',
24
+ className,
25
+ ]
26
+ .filter(Boolean)
27
+ .join(' ')}
28
+ >
29
+ {children}
30
+ </div>
31
+ );
@@ -1,31 +1,31 @@
1
- import styles from '../css/divider.module.scss';
2
-
3
- interface EvoDividerProps {
4
- orientation?: 'horizontal' | 'vertical';
5
- label?: string;
6
- className?: string;
7
- }
8
-
9
- export const EvoDivider = ({ orientation = 'horizontal', label, className = '' }: EvoDividerProps) => {
10
- if (label) {
11
- return (
12
- <div className={`${styles.labeled} ${className}`}>
13
- <div className={styles.line} />
14
- <span className={styles.labelText}>{label}</span>
15
- <div className={styles.line} />
16
- </div>
17
- );
18
- }
19
-
20
- return (
21
- <div
22
- className={[
23
- styles.divider,
24
- orientation === 'vertical' ? styles.vertical : styles.horizontal,
25
- className,
26
- ]
27
- .filter(Boolean)
28
- .join(' ')}
29
- />
30
- );
31
- };
1
+ import styles from '../css/divider.module.scss';
2
+
3
+ interface EvoDividerProps {
4
+ orientation?: 'horizontal' | 'vertical';
5
+ label?: string;
6
+ className?: string;
7
+ }
8
+
9
+ export const EvoDivider = ({ orientation = 'horizontal', label, className = '' }: EvoDividerProps) => {
10
+ if (label) {
11
+ return (
12
+ <div className={`${styles.labeled} ${className}`}>
13
+ <div className={styles.line} />
14
+ <span className={styles.labelText}>{label}</span>
15
+ <div className={styles.line} />
16
+ </div>
17
+ );
18
+ }
19
+
20
+ return (
21
+ <div
22
+ className={[
23
+ styles.divider,
24
+ orientation === 'vertical' ? styles.vertical : styles.horizontal,
25
+ className,
26
+ ]
27
+ .filter(Boolean)
28
+ .join(' ')}
29
+ />
30
+ );
31
+ };