@udixio/ui-react 2.9.23 → 2.10.0

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 (62) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/index.cjs +3 -3
  3. package/dist/index.js +2906 -2498
  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/icon/icon.d.ts.map +1 -1
  21. package/dist/lib/interfaces/index.d.ts +1 -0
  22. package/dist/lib/interfaces/index.d.ts.map +1 -1
  23. package/dist/lib/interfaces/menu-group.interface.d.ts +13 -0
  24. package/dist/lib/interfaces/menu-group.interface.d.ts.map +1 -0
  25. package/dist/lib/interfaces/menu-item.interface.d.ts +34 -0
  26. package/dist/lib/interfaces/menu-item.interface.d.ts.map +1 -0
  27. package/dist/lib/interfaces/menu.interface.d.ts +17 -0
  28. package/dist/lib/interfaces/menu.interface.d.ts.map +1 -0
  29. package/dist/lib/interfaces/text-field.interface.d.ts +9 -1
  30. package/dist/lib/interfaces/text-field.interface.d.ts.map +1 -1
  31. package/dist/lib/styles/index.d.ts +3 -0
  32. package/dist/lib/styles/index.d.ts.map +1 -1
  33. package/dist/lib/styles/menu-group.style.d.ts +14 -0
  34. package/dist/lib/styles/menu-group.style.d.ts.map +1 -0
  35. package/dist/lib/styles/menu-headline.style.d.ts +19 -0
  36. package/dist/lib/styles/menu-headline.style.d.ts.map +1 -0
  37. package/dist/lib/styles/menu-item.style.d.ts +39 -0
  38. package/dist/lib/styles/menu-item.style.d.ts.map +1 -0
  39. package/dist/lib/styles/menu.style.d.ts +19 -0
  40. package/dist/lib/styles/menu.style.d.ts.map +1 -0
  41. package/dist/lib/styles/text-field.style.d.ts +19 -2
  42. package/dist/lib/styles/text-field.style.d.ts.map +1 -1
  43. package/package.json +1 -1
  44. package/src/lib/components/AnchorPositioner.tsx +61 -18
  45. package/src/lib/components/ContextMenu.tsx +111 -0
  46. package/src/lib/components/Menu.tsx +113 -0
  47. package/src/lib/components/MenuGroup.tsx +34 -0
  48. package/src/lib/components/MenuHeadline.tsx +9 -0
  49. package/src/lib/components/MenuItem.tsx +197 -0
  50. package/src/lib/components/TextField.tsx +154 -26
  51. package/src/lib/components/index.ts +7 -2
  52. package/src/lib/icon/icon.tsx +10 -6
  53. package/src/lib/interfaces/index.ts +1 -0
  54. package/src/lib/interfaces/menu-group.interface.ts +13 -0
  55. package/src/lib/interfaces/menu-item.interface.ts +35 -0
  56. package/src/lib/interfaces/menu.interface.ts +20 -0
  57. package/src/lib/interfaces/text-field.interface.ts +9 -1
  58. package/src/lib/styles/index.ts +3 -0
  59. package/src/lib/styles/menu-group.style.ts +34 -0
  60. package/src/lib/styles/menu-headline.style.ts +20 -0
  61. package/src/lib/styles/menu-item.style.ts +53 -0
  62. 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) {
@@ -147,7 +159,7 @@ export const TextField = ({
147
159
  useEffect(() => {
148
160
  if (showDatePicker) {
149
161
  setIsFocused(true);
150
- } else {
162
+ } else if (!isSelectInput || !showMenu) {
151
163
  setIsFocused(false);
152
164
  }
153
165
  }, [showDatePicker]);
@@ -203,7 +215,6 @@ export const TextField = ({
203
215
  }
204
216
 
205
217
  if (onChange) {
206
- // Create a synthetic event
207
218
  const event = {
208
219
  target: {
209
220
  value: newValue,
@@ -216,13 +227,78 @@ export const TextField = ({
216
227
  setShowDatePicker(false);
217
228
  };
218
229
 
219
- const effectiveTrailingIcon =
220
- 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]);
221
244
 
222
- // Enhance styles for date input
223
- const inputDateClass = isDateInput
224
- ? '[&::-webkit-calendar-picker-indicator]:hidden cursor-pointer'
225
- : '';
245
+ const handleSelectToggle = () => {
246
+ if (disabled) return;
247
+ setShowMenu(!showMenu);
248
+ setIsFocused(!showMenu);
249
+ };
250
+
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
+ : '';
226
302
 
227
303
  const styles = useTextFieldStyle({
228
304
  showSupportingText: hasSupportingText,
@@ -241,25 +317,37 @@ export const TextField = ({
241
317
  trailingIcon: effectiveTrailingIcon,
242
318
  variant,
243
319
  errorText,
244
- value: String(value),
320
+ value: String(displayValue),
245
321
  suffix,
246
322
  multiline,
247
323
  });
248
324
 
249
325
  const TextComponent = multiline ? TextareaAutosize : 'input';
250
- 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
+ };
251
334
 
252
335
  const isFloating =
253
336
  isFocused ||
254
337
  (typeof value === 'string' && value.length > 0) ||
255
- type == 'date'; // Float label when picker open
338
+ type == 'date' ||
339
+ (isSelectInput && showMenu);
340
+
256
341
  const showLegend = isFloating && variant === 'outlined';
257
342
  const showLabel = !showLegend;
258
343
 
259
344
  return (
260
345
  <div ref={textFieldRef} className={styles.textField} style={style}>
261
346
  <fieldset
262
- onClick={focusInput}
347
+ onClick={() => {
348
+ if (isSelectInput) handleSelectToggle();
349
+ else focusInput();
350
+ }}
263
351
  className={styles.content}
264
352
  role="presentation"
265
353
  >
@@ -329,14 +417,19 @@ export const TextField = ({
329
417
  <TextComponent
330
418
  {...(restProps as any)}
331
419
  ref={inputRef as any}
332
- value={value}
420
+ value={displayValue} // Use displayValue for select
333
421
  onChange={handleChange}
334
- className={classNames(styles.input, inputDateClass)}
422
+ className={classNames(styles.input, inputSpecialClass)}
335
423
  id={id}
336
424
  name={name}
337
425
  placeholder={isFocused ? (placeholder ?? undefined) : ''}
338
- onFocus={() => setIsFocused(true)}
339
- 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
+ }}
340
433
  disabled={disabled}
341
434
  autoComplete={autoComplete}
342
435
  aria-invalid={!!errorText?.length}
@@ -355,10 +448,11 @@ export const TextField = ({
355
448
  onClick={(event) => {
356
449
  event.stopPropagation();
357
450
  if (isDateInput) handleDatePickerToggle();
451
+ if (isSelectInput) handleSelectToggle();
358
452
  }}
359
453
  className={classNames(
360
454
  styles.trailingIcon,
361
- isDateInput && 'cursor-pointer',
455
+ (isDateInput || isSelectInput) && 'cursor-pointer',
362
456
  )}
363
457
  >
364
458
  <div className="flex items-center justify-center w-full h-full">
@@ -435,6 +529,40 @@ export const TextField = ({
435
529
  </AnchorPositioner>
436
530
  </>
437
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
+ )}
438
565
  </div>
439
566
  );
440
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';
@@ -25,14 +25,12 @@ export const Icon: React.FC<Props> = ({
25
25
  }) => {
26
26
  // Si c'est une chaîne de caractères (SVG raw)
27
27
  if (typeof icon === 'string') {
28
- // Modifier la couleur du SVG en remplaçant les attributs fill/stroke
29
28
  let svgContent = icon;
29
+
30
+ let additionalAttrs = ' width="100%" height="100%"';
31
+
30
32
  if (colors[0]) {
31
- // Remplacer ou ajouter des attributs de couleur
32
- svgContent = svgContent.replace(
33
- /<svg([^>]*)>/,
34
- `<svg$1 fill="${colors[0]}" color="${colors[0]}">`,
35
- );
33
+ additionalAttrs += ` fill="${colors[0]}" color="${colors[0]}"`;
36
34
  // Remplacer les paths existants pour utiliser currentColor
37
35
  svgContent = svgContent.replace(
38
36
  /<path([^>]*?)>/g,
@@ -40,6 +38,12 @@ export const Icon: React.FC<Props> = ({
40
38
  );
41
39
  }
42
40
 
41
+ // Injecter les attributs (taille + couleur éventuelle) dans la balise <svg>
42
+ svgContent = svgContent.replace(
43
+ /<svg([^>]*)>/,
44
+ `<svg$1${additionalAttrs}>`,
45
+ );
46
+
43
47
  return (
44
48
  <div
45
49
  className={classNames(
@@ -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
+ );