@fragments-sdk/ui 0.8.6 → 0.8.8
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.
- package/fragments.json +1 -1
- package/package.json +2 -2
- package/src/assets/fragments-logo.tsx +9 -8
- package/src/blocks/AccountSettings.block.ts +1 -1
- package/src/blocks/ActivityFeed.block.ts +1 -1
- package/src/blocks/ChatInterface.block.ts +1 -1
- package/src/blocks/ChatMessages.block.ts +1 -1
- package/src/blocks/CheckoutForm.block.ts +1 -1
- package/src/blocks/CommandPalette.block.ts +34 -0
- package/src/blocks/ContactForm.block.ts +1 -1
- package/src/blocks/DashboardLayout.block.ts +1 -1
- package/src/blocks/DashboardPage.block.ts +1 -1
- package/src/blocks/DataTable.block.ts +1 -1
- package/src/blocks/EmptyState.block.ts +1 -1
- package/src/blocks/FAQSection.block.ts +1 -1
- package/src/blocks/FeatureGrid.block.ts +1 -1
- package/src/blocks/HeroSection.block.ts +1 -1
- package/src/blocks/LoginForm.block.ts +1 -1
- package/src/blocks/NavigationHeader.block.ts +1 -1
- package/src/blocks/PaginatedTable.block.ts +36 -0
- package/src/blocks/PricingComparison.block.ts +1 -1
- package/src/blocks/ProductCard.block.ts +1 -1
- package/src/blocks/RegistrationForm.block.ts +1 -1
- package/src/blocks/SettingsDrawer.block.ts +47 -0
- package/src/blocks/SettingsPanel.block.ts +1 -1
- package/src/blocks/ShoppingCart.block.ts +1 -1
- package/src/blocks/StatsCard.block.ts +1 -1
- package/src/blocks/ThinkingStates.block.ts +1 -1
- package/src/components/Accordion/Accordion.fragment.tsx +1 -1
- package/src/components/Alert/Alert.fragment.tsx +1 -1
- package/src/components/AppShell/AppShell.fragment.tsx +11 -11
- package/src/components/Avatar/Avatar.fragment.tsx +1 -1
- package/src/components/Badge/Badge.fragment.tsx +1 -1
- package/src/components/Box/Box.fragment.tsx +1 -1
- package/src/components/Breadcrumbs/Breadcrumbs.fragment.tsx +1 -1
- package/src/components/Button/Button.fragment.tsx +1 -1
- package/src/components/ButtonGroup/ButtonGroup.fragment.tsx +1 -1
- package/src/components/Card/Card.fragment.tsx +1 -1
- package/src/components/Chart/Chart.fragment.tsx +1 -1
- package/src/components/Checkbox/Checkbox.fragment.tsx +1 -1
- package/src/components/Chip/Chip.fragment.tsx +1 -1
- package/src/components/CodeBlock/CodeBlock.fragment.tsx +1 -1
- package/src/components/Collapsible/Collapsible.fragment.tsx +1 -1
- package/src/components/ColorPicker/ColorPicker.fragment.tsx +1 -1
- package/src/components/Combobox/Combobox.fragment.tsx +1 -1
- package/src/components/Command/Command.fragment.tsx +237 -0
- package/src/components/Command/Command.module.scss +153 -0
- package/src/components/Command/Command.test.tsx +363 -0
- package/src/components/Command/index.tsx +502 -0
- package/src/components/ConversationList/ConversationList.fragment.tsx +1 -1
- package/src/components/DatePicker/DatePicker.fragment.tsx +10 -9
- package/src/components/Dialog/Dialog.fragment.tsx +1 -1
- package/src/components/Drawer/Drawer.fragment.tsx +206 -0
- package/src/components/Drawer/Drawer.module.scss +215 -0
- package/src/components/Drawer/Drawer.test.tsx +227 -0
- package/src/components/Drawer/index.tsx +239 -0
- package/src/components/EmptyState/EmptyState.fragment.tsx +1 -1
- package/src/components/Field/Field.fragment.tsx +1 -1
- package/src/components/Fieldset/Fieldset.fragment.tsx +1 -1
- package/src/components/Form/Form.fragment.tsx +1 -1
- package/src/components/Grid/Grid.fragment.tsx +1 -1
- package/src/components/Header/Header.fragment.tsx +1 -1
- package/src/components/Icon/Icon.fragment.tsx +1 -1
- package/src/components/Image/Image.fragment.tsx +1 -1
- package/src/components/Input/Input.fragment.tsx +1 -1
- package/src/components/Link/Link.fragment.tsx +1 -1
- package/src/components/List/List.fragment.tsx +1 -1
- package/src/components/Listbox/Listbox.fragment.tsx +1 -1
- package/src/components/Loading/Loading.fragment.tsx +1 -1
- package/src/components/Markdown/Markdown.fragment.tsx +1 -1
- package/src/components/Menu/Menu.fragment.tsx +55 -5
- package/src/components/Menu/Menu.module.scss +21 -10
- package/src/components/Menu/Menu.test.tsx +126 -3
- package/src/components/Menu/index.tsx +85 -11
- package/src/components/Message/Message.fragment.tsx +1 -1
- package/src/components/Message/Message.module.scss +2 -1
- package/src/components/NavigationMenu/NavigationMenu.fragment.tsx +1 -1
- package/src/components/Pagination/Pagination.fragment.tsx +152 -0
- package/src/components/Pagination/Pagination.module.scss +109 -0
- package/src/components/Pagination/Pagination.test.tsx +171 -0
- package/src/components/Pagination/index.tsx +360 -0
- package/src/components/Popover/Popover.fragment.tsx +1 -1
- package/src/components/Progress/Progress.fragment.tsx +1 -1
- package/src/components/Prompt/Prompt.fragment.tsx +1 -1
- package/src/components/RadioGroup/RadioGroup.fragment.tsx +1 -1
- package/src/components/ScrollArea/ScrollArea.fragment.tsx +1 -1
- package/src/components/Select/Select.fragment.tsx +1 -1
- package/src/components/Separator/Separator.fragment.tsx +1 -1
- package/src/components/Sidebar/Sidebar.fragment.tsx +2 -2
- package/src/components/Skeleton/Skeleton.fragment.tsx +1 -1
- package/src/components/Slider/Slider.fragment.tsx +1 -1
- package/src/components/Stack/Stack.fragment.tsx +1 -1
- package/src/components/Table/Table.fragment.tsx +1 -1
- package/src/components/TableOfContents/TableOfContents.fragment.tsx +1 -1
- package/src/components/Tabs/Tabs.fragment.tsx +1 -1
- package/src/components/Text/Text.fragment.tsx +1 -1
- package/src/components/Textarea/Textarea.fragment.tsx +1 -1
- package/src/components/Theme/Theme.fragment.tsx +1 -1
- package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +1 -1
- package/src/components/Toast/Toast.fragment.tsx +1 -1
- package/src/components/Toggle/Toggle.fragment.tsx +1 -1
- package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +1 -1
- package/src/components/Tooltip/Tooltip.fragment.tsx +1 -1
- package/src/components/Tooltip/index.tsx +25 -1
- package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +1 -1
- package/src/index.ts +34 -1
- package/src/styles/globals.scss +65 -7
- package/src/tokens/_computed.scss +1 -1
- package/src/tokens/_density.scss +1 -1
- package/src/tokens/_derive.scss +1 -1
- package/src/tokens/_index.scss +1 -1
- package/src/tokens/_mixins.scss +1 -1
- package/src/tokens/_palettes.scss +1 -1
- package/src/tokens/_radius.scss +1 -1
- package/src/tokens/_seeds.scss +1 -1
- package/src/tokens/_variables.scss +2 -2
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import styles from './Command.module.scss';
|
|
5
|
+
import '../../styles/globals.scss';
|
|
6
|
+
|
|
7
|
+
// ============================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================
|
|
10
|
+
|
|
11
|
+
export interface CommandProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
/** Controlled search value */
|
|
14
|
+
search?: string;
|
|
15
|
+
/** Default search value */
|
|
16
|
+
defaultSearch?: string;
|
|
17
|
+
/** Called when search input changes */
|
|
18
|
+
onSearchChange?: (search: string) => void;
|
|
19
|
+
/** Custom filter function. Return 0 to hide, >0 to show (higher = better match).
|
|
20
|
+
Default: case-insensitive substring match on value + keywords */
|
|
21
|
+
filter?: (value: string, search: string, keywords?: string[]) => number;
|
|
22
|
+
/** Whether to loop keyboard navigation. Default: true */
|
|
23
|
+
loop?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CommandInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CommandListProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
31
|
+
children: React.ReactNode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CommandItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect'> {
|
|
35
|
+
children: React.ReactNode;
|
|
36
|
+
/** Value used for filtering (falls back to text content) */
|
|
37
|
+
value?: string;
|
|
38
|
+
/** Extra keywords for filtering */
|
|
39
|
+
keywords?: string[];
|
|
40
|
+
/** Whether this item is disabled */
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
/** Called when item is selected (Enter or click) */
|
|
43
|
+
onItemSelect?: () => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CommandGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
47
|
+
children: React.ReactNode;
|
|
48
|
+
/** Group heading text */
|
|
49
|
+
heading?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CommandEmptyProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
53
|
+
children: React.ReactNode;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface CommandSeparatorProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
57
|
+
|
|
58
|
+
// ============================================
|
|
59
|
+
// Default filter
|
|
60
|
+
// ============================================
|
|
61
|
+
|
|
62
|
+
function defaultFilter(value: string, search: string, keywords?: string[]): number {
|
|
63
|
+
if (!search) return 1;
|
|
64
|
+
const searchLower = search.toLowerCase();
|
|
65
|
+
const valueLower = value.toLowerCase();
|
|
66
|
+
|
|
67
|
+
if (valueLower.includes(searchLower)) return 1;
|
|
68
|
+
|
|
69
|
+
if (keywords) {
|
|
70
|
+
for (const keyword of keywords) {
|
|
71
|
+
if (keyword.toLowerCase().includes(searchLower)) return 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ============================================
|
|
79
|
+
// Context
|
|
80
|
+
// ============================================
|
|
81
|
+
|
|
82
|
+
interface ItemRegistration {
|
|
83
|
+
value: string;
|
|
84
|
+
keywords?: string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface CommandContextValue {
|
|
88
|
+
search: string;
|
|
89
|
+
setSearch: (search: string) => void;
|
|
90
|
+
filter: (value: string, search: string, keywords?: string[]) => number;
|
|
91
|
+
scores: Map<string, number>;
|
|
92
|
+
registerItem: (id: string, registration: ItemRegistration) => void;
|
|
93
|
+
unregisterItem: (id: string) => void;
|
|
94
|
+
activeId: string | null;
|
|
95
|
+
setActiveId: (id: string | null) => void;
|
|
96
|
+
loop: boolean;
|
|
97
|
+
listRef: React.RefObject<HTMLDivElement | null>;
|
|
98
|
+
visibleCount: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const CommandContext = React.createContext<CommandContextValue | null>(null);
|
|
102
|
+
|
|
103
|
+
function useCommandContext() {
|
|
104
|
+
const ctx = React.useContext(CommandContext);
|
|
105
|
+
if (!ctx) throw new Error('Command sub-components must be used within <Command>');
|
|
106
|
+
return ctx;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ============================================
|
|
110
|
+
// Search Icon
|
|
111
|
+
// ============================================
|
|
112
|
+
|
|
113
|
+
function SearchIcon() {
|
|
114
|
+
return (
|
|
115
|
+
<svg
|
|
116
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
117
|
+
width="16"
|
|
118
|
+
height="16"
|
|
119
|
+
viewBox="0 0 24 24"
|
|
120
|
+
fill="none"
|
|
121
|
+
stroke="currentColor"
|
|
122
|
+
strokeWidth="2"
|
|
123
|
+
strokeLinecap="round"
|
|
124
|
+
strokeLinejoin="round"
|
|
125
|
+
aria-hidden="true"
|
|
126
|
+
>
|
|
127
|
+
<circle cx="11" cy="11" r="8" />
|
|
128
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
129
|
+
</svg>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ============================================
|
|
134
|
+
// Components
|
|
135
|
+
// ============================================
|
|
136
|
+
|
|
137
|
+
function CommandRoot({
|
|
138
|
+
children,
|
|
139
|
+
search: controlledSearch,
|
|
140
|
+
defaultSearch = '',
|
|
141
|
+
onSearchChange,
|
|
142
|
+
filter = defaultFilter,
|
|
143
|
+
loop = true,
|
|
144
|
+
className,
|
|
145
|
+
...htmlProps
|
|
146
|
+
}: CommandProps) {
|
|
147
|
+
const [uncontrolledSearch, setUncontrolledSearch] = React.useState(defaultSearch);
|
|
148
|
+
const isControlled = controlledSearch !== undefined;
|
|
149
|
+
const search = isControlled ? controlledSearch : uncontrolledSearch;
|
|
150
|
+
|
|
151
|
+
const [items, setItems] = React.useState<Map<string, ItemRegistration>>(new Map());
|
|
152
|
+
const [activeId, setActiveId] = React.useState<string | null>(null);
|
|
153
|
+
const listRef = React.useRef<HTMLDivElement | null>(null);
|
|
154
|
+
|
|
155
|
+
const setSearch = React.useCallback(
|
|
156
|
+
(value: string) => {
|
|
157
|
+
if (!isControlled) {
|
|
158
|
+
setUncontrolledSearch(value);
|
|
159
|
+
}
|
|
160
|
+
onSearchChange?.(value);
|
|
161
|
+
},
|
|
162
|
+
[isControlled, onSearchChange]
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const registerItem = React.useCallback((id: string, registration: ItemRegistration) => {
|
|
166
|
+
setItems((prev) => {
|
|
167
|
+
const next = new Map(prev);
|
|
168
|
+
next.set(id, registration);
|
|
169
|
+
return next;
|
|
170
|
+
});
|
|
171
|
+
}, []);
|
|
172
|
+
|
|
173
|
+
const unregisterItem = React.useCallback((id: string) => {
|
|
174
|
+
setItems((prev) => {
|
|
175
|
+
const next = new Map(prev);
|
|
176
|
+
next.delete(id);
|
|
177
|
+
return next;
|
|
178
|
+
});
|
|
179
|
+
}, []);
|
|
180
|
+
|
|
181
|
+
// Compute scores for all items
|
|
182
|
+
const scores = React.useMemo(() => {
|
|
183
|
+
const result = new Map<string, number>();
|
|
184
|
+
for (const [id, registration] of items) {
|
|
185
|
+
const score = filter(registration.value, search, registration.keywords);
|
|
186
|
+
result.set(id, score);
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}, [items, search, filter]);
|
|
190
|
+
|
|
191
|
+
const visibleCount = React.useMemo(() => {
|
|
192
|
+
let count = 0;
|
|
193
|
+
for (const score of scores.values()) {
|
|
194
|
+
if (score > 0) count++;
|
|
195
|
+
}
|
|
196
|
+
return count;
|
|
197
|
+
}, [scores]);
|
|
198
|
+
|
|
199
|
+
// Reset active when search changes
|
|
200
|
+
React.useEffect(() => {
|
|
201
|
+
setActiveId(null);
|
|
202
|
+
}, [search]);
|
|
203
|
+
|
|
204
|
+
const contextValue = React.useMemo<CommandContextValue>(
|
|
205
|
+
() => ({
|
|
206
|
+
search,
|
|
207
|
+
setSearch,
|
|
208
|
+
filter,
|
|
209
|
+
scores,
|
|
210
|
+
registerItem,
|
|
211
|
+
unregisterItem,
|
|
212
|
+
activeId,
|
|
213
|
+
setActiveId,
|
|
214
|
+
loop,
|
|
215
|
+
listRef,
|
|
216
|
+
visibleCount,
|
|
217
|
+
}),
|
|
218
|
+
[search, setSearch, filter, scores, registerItem, unregisterItem, activeId, loop, visibleCount]
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<CommandContext.Provider value={contextValue}>
|
|
223
|
+
<div
|
|
224
|
+
{...htmlProps}
|
|
225
|
+
className={[styles.command, className].filter(Boolean).join(' ')}
|
|
226
|
+
role="search"
|
|
227
|
+
>
|
|
228
|
+
{children}
|
|
229
|
+
</div>
|
|
230
|
+
</CommandContext.Provider>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function CommandInput({ className, ...htmlProps }: CommandInputProps) {
|
|
235
|
+
const { search, setSearch, listRef, setActiveId, activeId, loop } = useCommandContext();
|
|
236
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
237
|
+
|
|
238
|
+
const getEnabledItems = React.useCallback(() => {
|
|
239
|
+
const list = listRef.current;
|
|
240
|
+
if (!list) return [];
|
|
241
|
+
return Array.from(
|
|
242
|
+
list.querySelectorAll<HTMLElement>('[data-command-item]:not([data-disabled="true"])')
|
|
243
|
+
).filter((el) => el.style.display !== 'none');
|
|
244
|
+
}, [listRef]);
|
|
245
|
+
|
|
246
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
247
|
+
htmlProps.onKeyDown?.(event);
|
|
248
|
+
if (event.defaultPrevented) return;
|
|
249
|
+
|
|
250
|
+
const items = getEnabledItems();
|
|
251
|
+
if (items.length === 0) return;
|
|
252
|
+
|
|
253
|
+
const currentIndex = activeId
|
|
254
|
+
? items.findIndex((item) => item.id === activeId)
|
|
255
|
+
: -1;
|
|
256
|
+
|
|
257
|
+
switch (event.key) {
|
|
258
|
+
case 'ArrowDown': {
|
|
259
|
+
event.preventDefault();
|
|
260
|
+
if (currentIndex < 0) {
|
|
261
|
+
setActiveId(items[0].id);
|
|
262
|
+
} else if (currentIndex < items.length - 1) {
|
|
263
|
+
setActiveId(items[currentIndex + 1].id);
|
|
264
|
+
} else if (loop) {
|
|
265
|
+
setActiveId(items[0].id);
|
|
266
|
+
}
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
case 'ArrowUp': {
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
if (currentIndex < 0) {
|
|
272
|
+
setActiveId(items[items.length - 1].id);
|
|
273
|
+
} else if (currentIndex > 0) {
|
|
274
|
+
setActiveId(items[currentIndex - 1].id);
|
|
275
|
+
} else if (loop) {
|
|
276
|
+
setActiveId(items[items.length - 1].id);
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
case 'Home': {
|
|
281
|
+
event.preventDefault();
|
|
282
|
+
setActiveId(items[0].id);
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
case 'End': {
|
|
286
|
+
event.preventDefault();
|
|
287
|
+
setActiveId(items[items.length - 1].id);
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
case 'Enter': {
|
|
291
|
+
event.preventDefault();
|
|
292
|
+
if (activeId) {
|
|
293
|
+
const activeItem = items.find((item) => item.id === activeId);
|
|
294
|
+
if (activeItem) {
|
|
295
|
+
activeItem.click();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<div className={styles.inputWrapper}>
|
|
305
|
+
<SearchIcon />
|
|
306
|
+
<input
|
|
307
|
+
ref={inputRef}
|
|
308
|
+
type="text"
|
|
309
|
+
role="combobox"
|
|
310
|
+
aria-expanded={true}
|
|
311
|
+
aria-controls="command-list"
|
|
312
|
+
aria-autocomplete="list"
|
|
313
|
+
aria-activedescendant={activeId ?? undefined}
|
|
314
|
+
autoComplete="off"
|
|
315
|
+
autoCorrect="off"
|
|
316
|
+
spellCheck={false}
|
|
317
|
+
value={search}
|
|
318
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
319
|
+
onKeyDown={handleKeyDown}
|
|
320
|
+
className={[styles.input, className].filter(Boolean).join(' ')}
|
|
321
|
+
{...htmlProps}
|
|
322
|
+
/>
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function CommandList({ children, className, ...htmlProps }: CommandListProps) {
|
|
328
|
+
const { listRef } = useCommandContext();
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<div
|
|
332
|
+
ref={listRef}
|
|
333
|
+
id="command-list"
|
|
334
|
+
role="listbox"
|
|
335
|
+
className={[styles.list, className].filter(Boolean).join(' ')}
|
|
336
|
+
{...htmlProps}
|
|
337
|
+
>
|
|
338
|
+
{children}
|
|
339
|
+
</div>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function CommandItem({
|
|
344
|
+
children,
|
|
345
|
+
value: valueProp,
|
|
346
|
+
keywords,
|
|
347
|
+
disabled = false,
|
|
348
|
+
onItemSelect,
|
|
349
|
+
className,
|
|
350
|
+
...htmlProps
|
|
351
|
+
}: CommandItemProps) {
|
|
352
|
+
const { scores, registerItem, unregisterItem, activeId, setActiveId } = useCommandContext();
|
|
353
|
+
const generatedId = React.useId();
|
|
354
|
+
const itemId = (htmlProps.id as string | undefined) ?? `command-item-${generatedId}`;
|
|
355
|
+
const itemRef = React.useRef<HTMLDivElement>(null);
|
|
356
|
+
|
|
357
|
+
// Extract text content for filtering if no value prop
|
|
358
|
+
const textValue = React.useMemo(() => {
|
|
359
|
+
if (valueProp) return valueProp;
|
|
360
|
+
if (typeof children === 'string') return children;
|
|
361
|
+
return '';
|
|
362
|
+
}, [valueProp, children]);
|
|
363
|
+
|
|
364
|
+
// Register with context
|
|
365
|
+
React.useEffect(() => {
|
|
366
|
+
registerItem(itemId, { value: textValue, keywords });
|
|
367
|
+
return () => unregisterItem(itemId);
|
|
368
|
+
}, [itemId, textValue, keywords, registerItem, unregisterItem]);
|
|
369
|
+
|
|
370
|
+
const score = scores.get(itemId) ?? 1;
|
|
371
|
+
const isVisible = score > 0;
|
|
372
|
+
const isActive = activeId === itemId;
|
|
373
|
+
|
|
374
|
+
// Scroll active item into view
|
|
375
|
+
React.useEffect(() => {
|
|
376
|
+
if (isActive && itemRef.current) {
|
|
377
|
+
itemRef.current.scrollIntoView({ block: 'nearest' });
|
|
378
|
+
}
|
|
379
|
+
}, [isActive]);
|
|
380
|
+
|
|
381
|
+
const handleClick = () => {
|
|
382
|
+
if (disabled) return;
|
|
383
|
+
onItemSelect?.();
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const handleMouseEnter = () => {
|
|
387
|
+
if (!disabled) {
|
|
388
|
+
setActiveId(itemId);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
return (
|
|
393
|
+
<div
|
|
394
|
+
ref={itemRef}
|
|
395
|
+
{...htmlProps}
|
|
396
|
+
id={itemId}
|
|
397
|
+
role="option"
|
|
398
|
+
aria-selected={isActive}
|
|
399
|
+
aria-disabled={disabled}
|
|
400
|
+
data-command-item=""
|
|
401
|
+
data-active={isActive || undefined}
|
|
402
|
+
data-disabled={disabled || undefined}
|
|
403
|
+
onClick={handleClick}
|
|
404
|
+
onMouseEnter={handleMouseEnter}
|
|
405
|
+
className={[
|
|
406
|
+
styles.item,
|
|
407
|
+
isActive && styles.itemActive,
|
|
408
|
+
disabled && styles.itemDisabled,
|
|
409
|
+
className,
|
|
410
|
+
]
|
|
411
|
+
.filter(Boolean)
|
|
412
|
+
.join(' ')}
|
|
413
|
+
style={{ display: isVisible ? undefined : 'none' }}
|
|
414
|
+
>
|
|
415
|
+
{children}
|
|
416
|
+
</div>
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function CommandGroup({ children, heading, className, ...htmlProps }: CommandGroupProps) {
|
|
421
|
+
const labelId = React.useId();
|
|
422
|
+
const groupRef = React.useRef<HTMLDivElement>(null);
|
|
423
|
+
const { scores } = useCommandContext();
|
|
424
|
+
const [hasVisibleChildren, setHasVisibleChildren] = React.useState(true);
|
|
425
|
+
|
|
426
|
+
// Check if any children are visible after each score update
|
|
427
|
+
React.useEffect(() => {
|
|
428
|
+
if (!groupRef.current) return;
|
|
429
|
+
const items = groupRef.current.querySelectorAll<HTMLElement>('[data-command-item]');
|
|
430
|
+
const anyVisible = Array.from(items).some((item) => item.style.display !== 'none');
|
|
431
|
+
setHasVisibleChildren(anyVisible);
|
|
432
|
+
}, [scores]);
|
|
433
|
+
|
|
434
|
+
return (
|
|
435
|
+
<div
|
|
436
|
+
ref={groupRef}
|
|
437
|
+
{...htmlProps}
|
|
438
|
+
role="group"
|
|
439
|
+
aria-labelledby={heading ? labelId : undefined}
|
|
440
|
+
className={[styles.group, className].filter(Boolean).join(' ')}
|
|
441
|
+
style={{ display: hasVisibleChildren ? undefined : 'none' }}
|
|
442
|
+
>
|
|
443
|
+
{heading && (
|
|
444
|
+
<div id={labelId} className={styles.groupHeading}>
|
|
445
|
+
{heading}
|
|
446
|
+
</div>
|
|
447
|
+
)}
|
|
448
|
+
{children}
|
|
449
|
+
</div>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function CommandEmpty({ children, className, ...htmlProps }: CommandEmptyProps) {
|
|
454
|
+
const { visibleCount } = useCommandContext();
|
|
455
|
+
|
|
456
|
+
if (visibleCount > 0) return null;
|
|
457
|
+
|
|
458
|
+
return (
|
|
459
|
+
<div
|
|
460
|
+
{...htmlProps}
|
|
461
|
+
role="option"
|
|
462
|
+
aria-disabled="true"
|
|
463
|
+
aria-selected="false"
|
|
464
|
+
className={[styles.empty, className].filter(Boolean).join(' ')}
|
|
465
|
+
>
|
|
466
|
+
{children}
|
|
467
|
+
</div>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function CommandSeparator({ className, ...htmlProps }: CommandSeparatorProps) {
|
|
472
|
+
return (
|
|
473
|
+
<div
|
|
474
|
+
{...htmlProps}
|
|
475
|
+
role="separator"
|
|
476
|
+
className={[styles.separator, className].filter(Boolean).join(' ')}
|
|
477
|
+
/>
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ============================================
|
|
482
|
+
// Export compound component
|
|
483
|
+
// ============================================
|
|
484
|
+
|
|
485
|
+
export const Command = Object.assign(CommandRoot, {
|
|
486
|
+
Input: CommandInput,
|
|
487
|
+
List: CommandList,
|
|
488
|
+
Item: CommandItem,
|
|
489
|
+
Group: CommandGroup,
|
|
490
|
+
Empty: CommandEmpty,
|
|
491
|
+
Separator: CommandSeparator,
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
export {
|
|
495
|
+
CommandRoot,
|
|
496
|
+
CommandInput,
|
|
497
|
+
CommandList,
|
|
498
|
+
CommandItem,
|
|
499
|
+
CommandGroup,
|
|
500
|
+
CommandEmpty,
|
|
501
|
+
CommandSeparator,
|
|
502
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
|
-
import { defineFragment } from '@fragments/core';
|
|
2
|
+
import { defineFragment } from '@fragments-sdk/cli/core';
|
|
3
3
|
import { DatePicker } from '.';
|
|
4
4
|
import type { DateRange } from '.';
|
|
5
5
|
import { subDays } from 'date-fns';
|
|
@@ -87,17 +87,18 @@ export default defineFragment({
|
|
|
87
87
|
required: true,
|
|
88
88
|
},
|
|
89
89
|
mode: {
|
|
90
|
-
type:
|
|
90
|
+
type: 'enum',
|
|
91
|
+
values: ['single', 'range'],
|
|
91
92
|
description: 'Selection mode',
|
|
92
93
|
default: "'single'",
|
|
93
94
|
},
|
|
94
95
|
selected: {
|
|
95
|
-
type: '
|
|
96
|
-
description: 'Controlled date (single mode)',
|
|
96
|
+
type: 'custom',
|
|
97
|
+
description: 'Controlled date (single mode). Type: Date | null',
|
|
97
98
|
},
|
|
98
99
|
selectedRange: {
|
|
99
|
-
type: '
|
|
100
|
-
description: 'Controlled range (range mode)',
|
|
100
|
+
type: 'custom',
|
|
101
|
+
description: 'Controlled range (range mode). Type: DateRange | null',
|
|
101
102
|
},
|
|
102
103
|
onSelect: {
|
|
103
104
|
type: 'function',
|
|
@@ -118,8 +119,8 @@ export default defineFragment({
|
|
|
118
119
|
default: 'false',
|
|
119
120
|
},
|
|
120
121
|
disabledDates: {
|
|
121
|
-
type: '
|
|
122
|
-
description: 'Dates to disable (react-day-picker Matcher)',
|
|
122
|
+
type: 'custom',
|
|
123
|
+
description: 'Dates to disable (react-day-picker Matcher | Matcher[])',
|
|
123
124
|
},
|
|
124
125
|
placeholder: {
|
|
125
126
|
type: 'string',
|
|
@@ -130,7 +131,7 @@ export default defineFragment({
|
|
|
130
131
|
relations: [
|
|
131
132
|
{ component: 'Select', relationship: 'alternative', note: 'Use Select for choosing from a list of options' },
|
|
132
133
|
{ component: 'Input', relationship: 'sibling', note: 'Use Input for free-form text date entry' },
|
|
133
|
-
{ component: 'Popover', relationship: '
|
|
134
|
+
{ component: 'Popover', relationship: 'composition', note: 'DatePicker uses Popover for the calendar dropdown' },
|
|
134
135
|
],
|
|
135
136
|
|
|
136
137
|
contract: {
|