@udixio/ui-react 2.9.22 → 2.9.24

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 (60) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/index.cjs +3 -3
  3. package/dist/index.js +2967 -2553
  4. package/dist/lib/components/AnchorPositioner.d.ts +2 -1
  5. package/dist/lib/components/AnchorPositioner.d.ts.map +1 -1
  6. package/dist/lib/components/ContextMenu.d.ts +15 -0
  7. package/dist/lib/components/ContextMenu.d.ts.map +1 -0
  8. package/dist/lib/components/Menu.d.ts +16 -0
  9. package/dist/lib/components/Menu.d.ts.map +1 -0
  10. package/dist/lib/components/MenuGroup.d.ts +12 -0
  11. package/dist/lib/components/MenuGroup.d.ts.map +1 -0
  12. package/dist/lib/components/MenuHeadline.d.ts +7 -0
  13. package/dist/lib/components/MenuHeadline.d.ts.map +1 -0
  14. package/dist/lib/components/MenuItem.d.ts +9 -0
  15. package/dist/lib/components/MenuItem.d.ts.map +1 -0
  16. package/dist/lib/components/TextField.d.ts +5 -1
  17. package/dist/lib/components/TextField.d.ts.map +1 -1
  18. package/dist/lib/components/index.d.ts +6 -2
  19. package/dist/lib/components/index.d.ts.map +1 -1
  20. package/dist/lib/interfaces/index.d.ts +1 -0
  21. package/dist/lib/interfaces/index.d.ts.map +1 -1
  22. package/dist/lib/interfaces/menu-group.interface.d.ts +13 -0
  23. package/dist/lib/interfaces/menu-group.interface.d.ts.map +1 -0
  24. package/dist/lib/interfaces/menu-item.interface.d.ts +34 -0
  25. package/dist/lib/interfaces/menu-item.interface.d.ts.map +1 -0
  26. package/dist/lib/interfaces/menu.interface.d.ts +17 -0
  27. package/dist/lib/interfaces/menu.interface.d.ts.map +1 -0
  28. package/dist/lib/interfaces/text-field.interface.d.ts +9 -1
  29. package/dist/lib/interfaces/text-field.interface.d.ts.map +1 -1
  30. package/dist/lib/styles/index.d.ts +3 -0
  31. package/dist/lib/styles/index.d.ts.map +1 -1
  32. package/dist/lib/styles/menu-group.style.d.ts +14 -0
  33. package/dist/lib/styles/menu-group.style.d.ts.map +1 -0
  34. package/dist/lib/styles/menu-headline.style.d.ts +19 -0
  35. package/dist/lib/styles/menu-headline.style.d.ts.map +1 -0
  36. package/dist/lib/styles/menu-item.style.d.ts +39 -0
  37. package/dist/lib/styles/menu-item.style.d.ts.map +1 -0
  38. package/dist/lib/styles/menu.style.d.ts +19 -0
  39. package/dist/lib/styles/menu.style.d.ts.map +1 -0
  40. package/dist/lib/styles/text-field.style.d.ts +19 -2
  41. package/dist/lib/styles/text-field.style.d.ts.map +1 -1
  42. package/package.json +3 -3
  43. package/src/lib/components/AnchorPositioner.tsx +61 -18
  44. package/src/lib/components/ContextMenu.tsx +111 -0
  45. package/src/lib/components/Menu.tsx +113 -0
  46. package/src/lib/components/MenuGroup.tsx +34 -0
  47. package/src/lib/components/MenuHeadline.tsx +9 -0
  48. package/src/lib/components/MenuItem.tsx +197 -0
  49. package/src/lib/components/TextField.tsx +206 -42
  50. package/src/lib/components/index.ts +7 -2
  51. package/src/lib/interfaces/index.ts +1 -0
  52. package/src/lib/interfaces/menu-group.interface.ts +13 -0
  53. package/src/lib/interfaces/menu-item.interface.ts +35 -0
  54. package/src/lib/interfaces/menu.interface.ts +20 -0
  55. package/src/lib/interfaces/text-field.interface.ts +9 -1
  56. package/src/lib/styles/index.ts +3 -0
  57. package/src/lib/styles/menu-group.style.ts +34 -0
  58. package/src/lib/styles/menu-headline.style.ts +20 -0
  59. package/src/lib/styles/menu-item.style.ts +53 -0
  60. package/src/lib/styles/menu.style.ts +32 -0
@@ -3,10 +3,16 @@ import { Icon } from '../icon';
3
3
  import {
4
4
  faCalendarDays,
5
5
  faCircleExclamation,
6
+ faChevronDown,
7
+ faChevronUp,
6
8
  } from '@fortawesome/free-solid-svg-icons';
7
9
  import { motion } from 'motion/react';
8
10
  import { DatePicker } from './DatePicker';
9
11
  import { Button } from './Button';
12
+ import { Menu } from './Menu';
13
+ import { MenuItem } from './MenuItem';
14
+ import { Divider } from './Divider';
15
+ import { MenuHeadline } from './MenuHeadline';
10
16
 
11
17
  import TextareaAutosize from 'react-textarea-autosize';
12
18
  import { useTextFieldStyle } from '../styles/text-field.style';
@@ -22,6 +28,7 @@ import { TextFieldInterface } from '../interfaces';
22
28
  * @devx
23
29
  * - Supports controlled (`value`) and uncontrolled (`defaultValue`) usage.
24
30
  * - `multiline` switches to textarea mode.
31
+ * - `type="select" ` switches to select mode with `options`
25
32
  * @a11y
26
33
  * - `aria-describedby` links supporting text/error to input.
27
34
  */
@@ -50,8 +57,10 @@ export const TextField = ({
50
57
  ref,
51
58
  onFocus,
52
59
  onBlur,
60
+ options,
61
+ children,
53
62
  ...restProps
54
- }: ReactProps<TextFieldInterface>) => {
63
+ }: ReactProps<TextFieldInterface> & { children?: React.ReactNode }) => {
55
64
  const generatedId = useId();
56
65
  const id = idProp || generatedId;
57
66
  const helperTextId = `${id}-helper`;
@@ -78,20 +87,23 @@ export const TextField = ({
78
87
  }, [errorText]);
79
88
 
80
89
  const focusInput = () => {
81
- if (inputRef.current && !isFocused) {
82
- inputRef.current.focus();
90
+ if (inputRef.current && !isFocused && !disabled) {
91
+ if (type !== 'select') {
92
+ inputRef.current.focus();
93
+ }
83
94
  }
84
95
  };
85
96
 
86
97
  useEffect(() => {
87
98
  if (!autoFocus || disabled) return;
88
99
 
89
- const rafId = window.requestAnimationFrame(() => {
90
- focusInput();
91
- });
92
-
93
- return () => window.cancelAnimationFrame(rafId);
94
- }, [autoFocus, disabled, inputRef]);
100
+ if (type !== 'select') {
101
+ const rafId = window.requestAnimationFrame(() => {
102
+ focusInput();
103
+ });
104
+ return () => window.cancelAnimationFrame(rafId);
105
+ }
106
+ }, [autoFocus, disabled, inputRef, type]);
95
107
 
96
108
  useEffect(() => {
97
109
  if (isFocused) {
@@ -134,20 +146,67 @@ export const TextField = ({
134
146
  return null;
135
147
  }, [value]);
136
148
 
137
- const handleDatePickerOpen = () => {
149
+ const handleDatePickerToggle = () => {
138
150
  if (disabled) return;
139
- setTempDate(initialDateValue);
140
- setShowDatePicker(true);
151
+ if (showDatePicker) {
152
+ setShowDatePicker(false);
153
+ } else {
154
+ setTempDate(initialDateValue);
155
+ setShowDatePicker(true);
156
+ }
141
157
  };
142
158
 
143
159
  useEffect(() => {
144
160
  if (showDatePicker) {
145
161
  setIsFocused(true);
146
- } else {
162
+ } else if (!isSelectInput || !showMenu) {
147
163
  setIsFocused(false);
148
164
  }
149
165
  }, [showDatePicker]);
150
166
 
167
+ useEffect(() => {
168
+ if (!showDatePicker) return;
169
+
170
+ const isInside = (target: Node | null) => {
171
+ if (!target) return false;
172
+ return (
173
+ textFieldRef.current?.contains(target) ||
174
+ datePickerRef.current?.contains(target)
175
+ );
176
+ };
177
+
178
+ const handlePointerDown = (e: PointerEvent) => {
179
+ const target = e.target as Node;
180
+ const inDatePicker = datePickerRef.current?.contains(target);
181
+ const inCalendarTrigger = calendarTriggerRef.current?.contains(target);
182
+ if (!inDatePicker && !inCalendarTrigger) {
183
+ setShowDatePicker(false);
184
+ }
185
+ };
186
+
187
+ const handleFocusIn = (e: FocusEvent) => {
188
+ if (!isInside(e.target as Node)) {
189
+ setShowDatePicker(false);
190
+ }
191
+ };
192
+
193
+ const handleKeyDown = (e: KeyboardEvent) => {
194
+ if (e.key === 'Escape') {
195
+ setShowDatePicker(false);
196
+ }
197
+ };
198
+
199
+ document.addEventListener('pointerdown', handlePointerDown);
200
+ document.addEventListener('focusin', handleFocusIn);
201
+ document.addEventListener('keydown', handleKeyDown);
202
+
203
+ return () => {
204
+ document.removeEventListener('pointerdown', handlePointerDown);
205
+ document.removeEventListener('focusin', handleFocusIn);
206
+ document.removeEventListener('keydown', handleKeyDown);
207
+ };
208
+ }, [showDatePicker]);
209
+
151
210
  const handleDateConfirm = () => {
152
211
  const newValue = tempDate ? tempDate.toLocaleDateString('en-CA') : '';
153
212
 
@@ -156,7 +215,6 @@ export const TextField = ({
156
215
  }
157
216
 
158
217
  if (onChange) {
159
- // Create a synthetic event
160
218
  const event = {
161
219
  target: {
162
220
  value: newValue,
@@ -169,13 +227,78 @@ export const TextField = ({
169
227
  setShowDatePicker(false);
170
228
  };
171
229
 
172
- const effectiveTrailingIcon =
173
- isDateInput && !trailingIcon ? faCalendarDays : trailingIcon;
230
+ // Select Logic
231
+ const isSelectInput = type === 'select';
232
+ const [showMenu, setShowMenu] = useState(false);
233
+ const menuRef = useRef<HTMLDivElement>(null);
234
+
235
+ const displayValue = useMemo(() => {
236
+ if (isSelectInput && options) {
237
+ const selectedOption = options.find(
238
+ (o) => String(o.value) === String(value),
239
+ );
240
+ return selectedOption ? selectedOption.label : value;
241
+ }
242
+ return value;
243
+ }, [value, isSelectInput, options]);
244
+
245
+ const handleSelectToggle = () => {
246
+ if (disabled) return;
247
+ setShowMenu(!showMenu);
248
+ setIsFocused(!showMenu);
249
+ };
174
250
 
175
- // Enhance styles for date input
176
- const inputDateClass = isDateInput
177
- ? '[&::-webkit-calendar-picker-indicator]:hidden cursor-pointer'
178
- : '';
251
+ const handleSelectOption = (optionValue: string | number) => {
252
+ if (!isControlled) {
253
+ setInternalValue(String(optionValue));
254
+ }
255
+
256
+ if (onChange) {
257
+ const event = {
258
+ target: {
259
+ value: String(optionValue),
260
+ name,
261
+ type,
262
+ },
263
+ } as React.ChangeEvent<HTMLInputElement>;
264
+ onChange(event);
265
+ }
266
+ setShowMenu(false);
267
+ setIsFocused(false);
268
+ };
269
+
270
+ // Close menu on outside click
271
+ useEffect(() => {
272
+ if (!showMenu) return;
273
+ const handleClickOutside = (event: MouseEvent) => {
274
+ if (
275
+ textFieldRef.current &&
276
+ !textFieldRef.current.contains(event.target as Node) &&
277
+ menuRef.current &&
278
+ !menuRef.current.contains(event.target as Node)
279
+ ) {
280
+ setShowMenu(false);
281
+ setIsFocused(false);
282
+ }
283
+ };
284
+ document.addEventListener('mousedown', handleClickOutside);
285
+ return () => {
286
+ document.removeEventListener('mousedown', handleClickOutside);
287
+ };
288
+ }, [showMenu]);
289
+
290
+ const effectiveTrailingIcon = useMemo(() => {
291
+ if (trailingIcon) return trailingIcon;
292
+ if (isDateInput) return faCalendarDays;
293
+ if (isSelectInput) return showMenu ? faChevronUp : faChevronDown;
294
+ return undefined;
295
+ }, [trailingIcon, isDateInput, isSelectInput, showMenu]);
296
+
297
+ // Enhance styles for date input or select
298
+ const inputSpecialClass =
299
+ isDateInput || isSelectInput
300
+ ? '[&::-webkit-calendar-picker-indicator]:hidden cursor-pointer selection:bg-transparent'
301
+ : '';
179
302
 
180
303
  const styles = useTextFieldStyle({
181
304
  showSupportingText: hasSupportingText,
@@ -194,25 +317,37 @@ export const TextField = ({
194
317
  trailingIcon: effectiveTrailingIcon,
195
318
  variant,
196
319
  errorText,
197
- value: String(value),
320
+ value: String(displayValue),
198
321
  suffix,
199
322
  multiline,
200
323
  });
201
324
 
202
325
  const TextComponent = multiline ? TextareaAutosize : 'input';
203
- const textComponentProps = multiline ? {} : { type };
326
+ // For select, we want the input to be readOnly but still focusable?
327
+ // Actually, for better UX, standard select inputs are often readOnly text fields.
328
+ const textComponentProps = multiline
329
+ ? {}
330
+ : {
331
+ type: isSelectInput ? 'text' : type,
332
+ readOnly: isSelectInput,
333
+ };
204
334
 
205
335
  const isFloating =
206
336
  isFocused ||
207
337
  (typeof value === 'string' && value.length > 0) ||
208
- type == 'date'; // Float label when picker open
338
+ type == 'date' ||
339
+ (isSelectInput && showMenu);
340
+
209
341
  const showLegend = isFloating && variant === 'outlined';
210
342
  const showLabel = !showLegend;
211
343
 
212
344
  return (
213
345
  <div ref={textFieldRef} className={styles.textField} style={style}>
214
346
  <fieldset
215
- onClick={focusInput}
347
+ onClick={() => {
348
+ if (isSelectInput) handleSelectToggle();
349
+ else focusInput();
350
+ }}
216
351
  className={styles.content}
217
352
  role="presentation"
218
353
  >
@@ -282,14 +417,19 @@ export const TextField = ({
282
417
  <TextComponent
283
418
  {...(restProps as any)}
284
419
  ref={inputRef as any}
285
- value={value}
420
+ value={displayValue} // Use displayValue for select
286
421
  onChange={handleChange}
287
- className={classNames(styles.input, inputDateClass)}
422
+ className={classNames(styles.input, inputSpecialClass)}
288
423
  id={id}
289
424
  name={name}
290
425
  placeholder={isFocused ? (placeholder ?? undefined) : ''}
291
- onFocus={() => setIsFocused(true)}
292
- onBlur={() => setIsFocused(false)}
426
+ onFocus={() => {
427
+ if(!isSelectInput) setIsFocused(true)
428
+ }}
429
+ onBlur={() => {
430
+ // For select, we manage focus manually with menu state usually
431
+ if(!isSelectInput) setIsFocused(false)
432
+ }}
293
433
  disabled={disabled}
294
434
  autoComplete={autoComplete}
295
435
  aria-invalid={!!errorText?.length}
@@ -307,11 +447,12 @@ export const TextField = ({
307
447
  ref={isDateInput ? calendarTriggerRef : undefined}
308
448
  onClick={(event) => {
309
449
  event.stopPropagation();
310
- if (isDateInput) handleDatePickerOpen();
450
+ if (isDateInput) handleDatePickerToggle();
451
+ if (isSelectInput) handleSelectToggle();
311
452
  }}
312
453
  className={classNames(
313
454
  styles.trailingIcon,
314
- isDateInput && 'cursor-pointer',
455
+ (isDateInput || isSelectInput) && 'cursor-pointer',
315
456
  )}
316
457
  >
317
458
  <div className="flex items-center justify-center w-full h-full">
@@ -358,18 +499,7 @@ export const TextField = ({
358
499
 
359
500
  {isDateInput && showDatePicker && (
360
501
  <>
361
- <AnchorPositioner
362
- onBlur={(e) => {
363
- if (
364
- datePickerRef.current &&
365
- !datePickerRef.current?.contains(e.relatedTarget)
366
- ) {
367
- setShowDatePicker(false);
368
- }
369
- }}
370
- anchorRef={textFieldRef}
371
- position="bottom"
372
- >
502
+ <AnchorPositioner anchorRef={textFieldRef} position="bottom">
373
503
  <div
374
504
  ref={datePickerRef}
375
505
  className="z-50 shadow-xl rounded-[28px] bg-surface-container-high overflow-hidden"
@@ -399,6 +529,40 @@ export const TextField = ({
399
529
  </AnchorPositioner>
400
530
  </>
401
531
  )}
532
+
533
+ {isSelectInput && showMenu && (
534
+ <AnchorPositioner anchorRef={textFieldRef} position="bottom" style={{ width: textFieldRef.current?.offsetWidth }}>
535
+ <div ref={menuRef}>
536
+ <Menu
537
+ selected={value}
538
+ onItemSelect={handleSelectOption}
539
+ >
540
+ {children}
541
+ {!children && options?.map((opt, i) => {
542
+ if (opt.type === 'divider') {
543
+ return <Divider key={i} className="my-1" />
544
+ }
545
+ if (opt.type === 'headline') {
546
+ return <MenuHeadline key={i} label={opt.label} />
547
+ }
548
+ return (
549
+ <MenuItem
550
+ key={opt.value ?? i}
551
+ value={opt.value ?? ''}
552
+ label={opt.label}
553
+ leadingIcon={opt.leadingIcon}
554
+ trailingIcon={opt.trailingIcon}
555
+ disabled={opt.disabled}
556
+ >
557
+ {opt.label}
558
+ </MenuItem>
559
+ )
560
+ })}
561
+ </Menu>
562
+ </div>
563
+ </AnchorPositioner>
564
+ )}
402
565
  </div>
403
566
  );
404
567
  };
568
+
@@ -1,17 +1,22 @@
1
+
1
2
  export * from './AnchorPositioner';
2
3
  export * from './Button';
3
4
  export * from './Card';
5
+ export * from './Card';
4
6
  export * from './Carousel';
5
7
  export * from './CarouselItem';
6
- export * from './CarouselItem';
7
8
  export * from './Checkbox';
8
9
  export * from './Chip';
10
+ export * from './ContextMenu';
9
11
  export * from './Chips';
10
12
  export * from './Divider';
11
13
  export * from './Fab';
12
14
  export * from './FabMenu';
13
15
  export * from './IconButton';
14
- export * from './IconButton';
16
+ export * from './Menu';
17
+ export * from './MenuItem';
18
+ export * from './MenuGroup';
19
+ export * from './MenuHeadline';
15
20
  export * from './ProgressIndicator';
16
21
  export * from './Slider';
17
22
  export * from './SideSheet';
@@ -9,6 +9,7 @@ export * from './divider.interface';
9
9
  export * from './fab.interface';
10
10
  export * from './fab-menu.interface';
11
11
  export * from './icon-button.interface';
12
+ export * from './menu.interface';
12
13
  export * from './progress-indicator.interface';
13
14
  export * from './side-sheet.interface';
14
15
  export * from './slider.interface';
@@ -0,0 +1,13 @@
1
+ export interface MenuGroupProps {
2
+ children: React.ReactNode;
3
+ className?: string;
4
+ variant?: 'standard' | 'vibrant';
5
+ label?: string;
6
+ }
7
+
8
+ export interface MenuGroupInterface {
9
+ type: 'div';
10
+ props: MenuGroupProps;
11
+ states: object;
12
+ elements: ['menuGroup', 'groupLabel'];
13
+ }
@@ -0,0 +1,35 @@
1
+ import { ComponentInterface } from '../utils/component';
2
+
3
+ export interface MenuItemInterface extends ComponentInterface {
4
+ value: string | number;
5
+ label?: string;
6
+ children?: React.ReactNode;
7
+ leadingIcon?: any;
8
+ trailingIcon?: any;
9
+ disabled?: boolean;
10
+ selected?: boolean; // Injected by parent
11
+ variant?: 'standard' | 'vibrant'; // Injected by parent
12
+ onClick?: (e?: React.MouseEvent) => void;
13
+ // ComponentInterface implementation
14
+ type: 'div';
15
+ props: {
16
+ label?: string;
17
+ value: string | number;
18
+ leadingIcon?: any;
19
+ trailingIcon?: any;
20
+ disabled?: boolean;
21
+ selected?: boolean;
22
+ variant?: 'standard' | 'vibrant';
23
+ onItemSelect?: (value: string | number) => void; // Injected
24
+ children?: React.ReactNode;
25
+ };
26
+ states: Record<string, any>;
27
+ elements: [
28
+ 'menuItem',
29
+ 'selectedItem',
30
+ 'itemLabel',
31
+ 'itemIcon',
32
+ 'leadingIcon',
33
+ 'trailingIcon',
34
+ ];
35
+ }
@@ -0,0 +1,20 @@
1
+ export type MenuStates = Record<string, any>;
2
+
3
+ export interface MenuProps {
4
+ children: React.ReactNode;
5
+ selected?: string | number | (string | number)[];
6
+ onItemSelect?: (value: string | number) => void;
7
+ className?: string;
8
+ variant?: 'standard' | 'vibrant';
9
+ // options prop REMOVED as requested by user ("options passed as children")
10
+ // However, for backward compat or data-driven, one might want it, but I will strictly follow "options as children"
11
+ }
12
+
13
+ export interface MenuInterface {
14
+ type: 'div';
15
+ props: MenuProps;
16
+ states: {
17
+ hasGroups: boolean;
18
+ };
19
+ elements: ['menu'];
20
+ }
@@ -22,7 +22,15 @@ type Props = {
22
22
  id?: string;
23
23
  style?: React.CSSProperties;
24
24
  variant?: TextFieldVariant;
25
- type?: 'text' | 'password' | 'number' | 'date';
25
+ options?: Array<{
26
+ label: string;
27
+ value: string | number;
28
+ leadingIcon?: any;
29
+ trailingIcon?: any;
30
+ disabled?: boolean;
31
+ type?: 'divider' | 'headline';
32
+ }>;
33
+ type?: 'text' | 'password' | 'number' | 'date' | 'select';
26
34
  autoComplete?: 'on' | 'off' | string;
27
35
  autoFocus?: boolean;
28
36
  multiline?: boolean;
@@ -9,6 +9,8 @@ export * from './divider.style';
9
9
  export * from './fab.style';
10
10
  export * from './fab-menu.style';
11
11
  export * from './icon-button.style';
12
+ export * from './menu.style';
13
+ export * from './menu-group.style';
12
14
  export * from './progress-indicator.style';
13
15
  export * from './side-sheet.style';
14
16
  export * from './slider.style';
@@ -20,3 +22,4 @@ export * from './tab-panels.style';
20
22
  export * from './text-field.style';
21
23
  export * from './tooltip.style';
22
24
  export { useButtonStyle } from './button.style';
25
+ export { useMenuStyle } from './menu.style';
@@ -0,0 +1,34 @@
1
+ import {
2
+ type ClassNameComponent,
3
+ classNames,
4
+ createUseClassNames,
5
+ defaultClassNames,
6
+ } from '../utils';
7
+ import { MenuGroupInterface } from '../interfaces/menu-group.interface';
8
+
9
+ const menuGroupConfig: ClassNameComponent<MenuGroupInterface> = ({
10
+ variant,
11
+ }) => ({
12
+ menuGroup: classNames(
13
+ 'flex flex-col gap-0.5 mb-0.5 last:mb-0',
14
+ 'rounded-lg py-0.5 px-1 shadow-2 first:rounded-t-2xl last:rounded-b-2xl',
15
+ {
16
+ 'bg-surface-container': variant === 'standard',
17
+ 'bg-tertiary-container text-on-tertiary-container': variant === 'vibrant',
18
+ },
19
+ ),
20
+ groupLabel: classNames('px-3 pt-2 text-label-small tracking-wide ', {
21
+ 'text-on-surface-variant': variant === 'standard',
22
+ 'text-on-tertiary-container opacity-80': variant === 'vibrant',
23
+ }),
24
+ });
25
+
26
+ export const menuGroupStyle = defaultClassNames<MenuGroupInterface>(
27
+ 'menuGroup',
28
+ menuGroupConfig,
29
+ );
30
+
31
+ export const useMenuGroupStyle = createUseClassNames<MenuGroupInterface>(
32
+ 'menuGroup',
33
+ menuGroupConfig,
34
+ );
@@ -0,0 +1,20 @@
1
+
2
+ import { type ClassNameComponent, classNames, defaultClassNames, createUseClassNames } from '../utils';
3
+
4
+ export interface MenuHeadlineInterface {
5
+ label?: string;
6
+ variant?: 'standard' | 'vibrant';
7
+ type: 'div';
8
+ props: { label?: string; variant?: 'standard' | 'vibrant' };
9
+ states: Record<string, any>;
10
+ elements: ['headline'];
11
+ }
12
+
13
+ const menuHeadlineConfig: ClassNameComponent<MenuHeadlineInterface> = ({ props }) => ({
14
+ headline: classNames('px-3 py-1 text-label-small opacity-60 mt-1', {
15
+ 'text-on-surface-variant': !props?.variant || props.variant === 'standard',
16
+ // Vibrant treatment if different
17
+ }),
18
+ });
19
+
20
+ export const useMenuHeadlineStyle = createUseClassNames<MenuHeadlineInterface>('menu-headline', menuHeadlineConfig);
@@ -0,0 +1,53 @@
1
+ import {
2
+ type ClassNameComponent,
3
+ classNames,
4
+ createUseClassNames,
5
+ defaultClassNames,
6
+ } from '../utils';
7
+ import { MenuItemInterface } from '../interfaces/menu-item.interface';
8
+
9
+ const menuItemConfig: ClassNameComponent<MenuItemInterface> = ({
10
+ variant,
11
+ disabled,
12
+ selected,
13
+ }) => ({
14
+ menuItem: classNames(
15
+ 'group/menu-item overflow-hidden flex items-center h-12 px-3 cursor-pointer outline-none select-none shrink-0 ',
16
+ 'text-label-large',
17
+ 'transition-colors duration-200',
18
+ {
19
+ 'rounded-sm': selected,
20
+ 'rounded-xl': !selected,
21
+ },
22
+ {
23
+ 'text-on-surface': !variant || variant === 'standard',
24
+ // 'hover:bg-on-surface/[0.08] focus:bg-on-surface/[0.12]': !props?.variant || props.variant === 'standard', // Handled by State
25
+ // 'hover:bg-on-tertiary-container/[0.08] focus:bg-on-tertiary-container/[0.12]': props?.variant === 'vibrant', // Handled by State
26
+ 'opacity-38 pointer-events-none': disabled,
27
+ },
28
+ ),
29
+ selectedItem: classNames(
30
+ 'bg-secondary-container text-on-secondary-container',
31
+ // 'hover:bg-secondary-container/[0.8]',
32
+ '[&_.menu-item-icon]:text-inherit',
33
+ {
34
+ // For vibrant, selected state
35
+ '!bg-on-tertiary-container/[0.12]': variant === 'vibrant',
36
+ },
37
+ ),
38
+ itemLabel: classNames('flex-1 truncate'),
39
+ itemIcon: classNames(
40
+ 'w-6 h-6 flex items-center justify-center menu-item-icon',
41
+ ),
42
+ leadingIcon: classNames('mr-3'),
43
+ trailingIcon: classNames('ml-3'),
44
+ });
45
+
46
+ export const menuItemStyle = defaultClassNames<MenuItemInterface>(
47
+ 'menuItem',
48
+ menuItemConfig,
49
+ );
50
+ export const useMenuItemStyle = createUseClassNames<MenuItemInterface>(
51
+ 'menuItem',
52
+ menuItemConfig,
53
+ );
@@ -0,0 +1,32 @@
1
+ import {
2
+ type ClassNameComponent,
3
+ classNames,
4
+ createUseClassNames,
5
+ defaultClassNames,
6
+ } from '../utils';
7
+ import { MenuInterface } from '../interfaces';
8
+
9
+ const menuConfig: ClassNameComponent<MenuInterface> = ({
10
+ variant,
11
+ hasGroups,
12
+ }) => ({
13
+ menu: classNames(
14
+ 'z-50 min-w-[112px] max-w-[280px] max-h-[300px] ',
15
+ 'flex flex-col',
16
+ { 'overflow-y-auto': !hasGroups },
17
+ {
18
+ 'bg-surface-container': !variant || variant === 'standard',
19
+ // Vibrant uses tertiary-container (approximated) or just colored surface
20
+ 'bg-tertiary-container text-on-tertiary-container': variant === 'vibrant',
21
+ 'py-0.5 shadow-2 px-1 rounded-2xl': !hasGroups,
22
+ 'bg-transparent ': hasGroups,
23
+ },
24
+ ),
25
+ });
26
+
27
+ export const menuStyle = defaultClassNames<MenuInterface>('menu', menuConfig);
28
+
29
+ export const useMenuStyle = createUseClassNames<MenuInterface>(
30
+ 'menu',
31
+ menuConfig,
32
+ );