@djangocfg/ui-core 2.1.541 → 2.1.543

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 (48) hide show
  1. package/README.md +3 -1
  2. package/package.json +12 -9
  3. package/src/components/data/BalancedText/hooks/useMaxLinesWidth.ts +4 -25
  4. package/src/components/forms/button-download/index.tsx +1 -1
  5. package/src/components/forms/datetime-field/date-time-field.tsx +1 -1
  6. package/src/components/forms/editable/index.tsx +7 -3
  7. package/src/components/forms/input-group/index.tsx +12 -0
  8. package/src/components/forms/mask-input/index.tsx +7 -3
  9. package/src/components/forms/money-field/README.md +79 -0
  10. package/src/components/forms/money-field/index.tsx +288 -0
  11. package/src/components/forms/otp/use-otp-input.ts +1 -1
  12. package/src/components/forms/tags-input/index.tsx +55 -41
  13. package/src/components/forms/time-picker/index.tsx +7 -3
  14. package/src/components/index.ts +4 -0
  15. package/src/components/layout/key-value/index.tsx +9 -7
  16. package/src/components/layout/resizable/index.tsx +6 -1
  17. package/src/components/navigation/command/index.tsx +24 -6
  18. package/src/components/navigation/link/LinkContext.tsx +3 -1
  19. package/src/components/navigation/pagination/pagination-static.tsx +1 -1
  20. package/src/components/navigation/tabs/index.tsx +30 -9
  21. package/src/components/overlay/responsive-sheet/index.tsx +4 -4
  22. package/src/components/select/helpers.tsx +1 -1
  23. package/src/components/select/multi-select-pro-async.tsx +13 -6
  24. package/src/components/select/multi-select-pro.tsx +3 -4
  25. package/src/components/specialized/flag/Flag.tsx +11 -5
  26. package/src/components/specialized/flag/flag-map.ts +13 -7
  27. package/src/components/specialized/image-with-fallback/index.tsx +9 -4
  28. package/src/components/specialized/presence/index.tsx +2 -3
  29. package/src/components/specialized/token-icon/index.tsx +26 -13
  30. package/src/hooks/audio/useAudioPrefs.ts +8 -3
  31. package/src/hooks/device/useBrowserDetect.ts +5 -1
  32. package/src/hooks/dom/useImageLoader.ts +24 -20
  33. package/src/hooks/dom/useScroll.ts +7 -6
  34. package/src/hooks/events/useEventsBus.ts +19 -5
  35. package/src/hooks/hotkey/useHotkeyChord.ts +11 -4
  36. package/src/hooks/hotkey/useHotkeyHelp.ts +8 -3
  37. package/src/hooks/router/adapter.tsx +3 -1
  38. package/src/hooks/state/storage-quota.ts +27 -0
  39. package/src/hooks/state/useDebouncedCallback.ts +26 -20
  40. package/src/hooks/state/useLocalStorage.ts +7 -13
  41. package/src/hooks/state/useSessionStorage.ts +7 -9
  42. package/src/lib/compose-event-handlers.ts +5 -5
  43. package/src/lib/dialog-service/getDialog.ts +1 -1
  44. package/src/lib/get-element-ref.ts +9 -6
  45. package/src/lib/pretext/pretext.types.ts +25 -70
  46. package/src/lib/pretext/use-pretext.ts +8 -12
  47. package/src/snippets/LazyComponent.tsx +9 -9
  48. package/src/styles/palette/useThemePalette.ts +7 -0
@@ -56,8 +56,7 @@ export interface TagsInputRootProps
56
56
  children?: React.ReactNode | ((context: { value: TagValue[] }) => React.ReactNode);
57
57
  }
58
58
 
59
- export interface TagsInputInputProps
60
- extends Omit<React.ComponentPropsWithoutRef<"input">, "value" | "defaultValue"> {}
59
+ export type TagsInputInputProps = Omit<React.ComponentPropsWithoutRef<"input">, "value" | "defaultValue">;
61
60
 
62
61
  export interface TagsInputItemProps extends React.ComponentPropsWithoutRef<"div"> {
63
62
  /** The value of the item. */
@@ -66,11 +65,9 @@ export interface TagsInputItemProps extends React.ComponentPropsWithoutRef<"div"
66
65
  disabled?: boolean;
67
66
  }
68
67
 
69
- export interface TagsInputItemTextProps
70
- extends React.ComponentPropsWithoutRef<"span"> {}
68
+ export type TagsInputItemTextProps = React.ComponentPropsWithoutRef<"span">;
71
69
 
72
- export interface TagsInputItemDeleteProps
73
- extends React.ComponentPropsWithoutRef<"button"> {}
70
+ export type TagsInputItemDeleteProps = React.ComponentPropsWithoutRef<"button">;
74
71
 
75
72
  // =============================================================================
76
73
  // Context
@@ -169,7 +166,16 @@ const TagsInput = React.forwardRef<HTMLDivElement, TagsInputRootProps>(
169
166
  } = props;
170
167
 
171
168
  const [value = [], setValue] = React.useState<TagValue[] | undefined>(defaultValue);
172
- const resolvedValue = valueProp !== undefined ? valueProp : (value ?? []);
169
+ /*
170
+ * Memoised because the `?? []` branch mints a NEW array each render, and
171
+ * four `useCallback`s below take `resolvedValue` as a dependency — so an
172
+ * uncontrolled, empty TagsInput rebuilt all four on every render and the
173
+ * memoisation bought nothing. Identity is what those hooks compare.
174
+ */
175
+ const resolvedValue = React.useMemo(
176
+ () => (valueProp !== undefined ? valueProp : (value ?? [])),
177
+ [valueProp, value],
178
+ );
173
179
 
174
180
  const [highlightedIndex, setHighlightedIndex] = React.useState<number | null>(null);
175
181
  const [editingIndex, setEditingIndex] = React.useState<number | null>(null);
@@ -531,46 +537,47 @@ const TagsInputInput = React.forwardRef<HTMLInputElement, TagsInputInputProps>(
531
537
  (props, ref) => {
532
538
  const { autoFocus, ...inputProps } = props;
533
539
  const context = useTagsInput("TagsInputInput");
540
+ const { onItemAdd, setHighlightedIndex, addOnTab, inputRef } = context;
534
541
 
535
542
  const onCustomKeydown = React.useCallback(
536
543
  (event: React.KeyboardEvent<HTMLInputElement>) => {
537
544
  if (event.defaultPrevented) return;
538
545
  const value = event.currentTarget.value;
539
546
  if (!value) return;
540
- const isAdded = context.onItemAdd(value);
547
+ const isAdded = onItemAdd(value);
541
548
  if (isAdded) {
542
549
  event.currentTarget.value = "";
543
- context.setHighlightedIndex(null);
550
+ setHighlightedIndex(null);
544
551
  }
545
552
  event.preventDefault();
546
553
  },
547
- [context.onItemAdd, context.setHighlightedIndex]
554
+ [onItemAdd, setHighlightedIndex]
548
555
  );
549
556
 
550
557
  const onTab = React.useCallback(
551
558
  (event: React.KeyboardEvent<HTMLInputElement>) => {
552
- if (!context.addOnTab) return;
559
+ if (!addOnTab) return;
553
560
  onCustomKeydown(event);
554
561
  },
555
- [context.addOnTab, onCustomKeydown]
562
+ [addOnTab, onCustomKeydown]
556
563
  );
557
564
 
558
565
  React.useEffect(() => {
559
566
  if (!autoFocus) return;
560
- const id = requestAnimationFrame(() => context.inputRef.current?.focus());
567
+ const id = requestAnimationFrame(() => inputRef.current?.focus());
561
568
  return () => cancelAnimationFrame(id);
562
- }, [autoFocus, context.inputRef]);
569
+ }, [autoFocus, inputRef]);
563
570
 
564
571
  const composedRef = React.useCallback(
565
572
  (node: HTMLInputElement | null) => {
566
- context.inputRef.current = node;
573
+ inputRef.current = node;
567
574
  if (typeof ref === "function") {
568
575
  ref(node);
569
576
  } else if (ref) {
570
577
  (ref as React.MutableRefObject<HTMLInputElement | null>).current = node;
571
578
  }
572
579
  },
573
- [ref, context.inputRef]
580
+ [ref, inputRef]
574
581
  );
575
582
 
576
583
  return (
@@ -664,10 +671,11 @@ const TagsInputItem = React.forwardRef<HTMLDivElement, TagsInputItemProps>(
664
671
  const itemDisabled = itemDisabledProp || context.disabled;
665
672
  const displayValue = context.displayValue(value);
666
673
 
674
+ const { setHighlightedIndex, inputRef } = context;
667
675
  const onItemSelect = React.useCallback(() => {
668
- context.setHighlightedIndex(index);
669
- context.inputRef.current?.focus();
670
- }, [context.setHighlightedIndex, context.inputRef, index]);
676
+ setHighlightedIndex(index);
677
+ inputRef.current?.focus();
678
+ }, [setHighlightedIndex, inputRef, index]);
671
679
 
672
680
  return (
673
681
  <TagsInputItemContext.Provider
@@ -694,6 +702,10 @@ const TagsInputItem = React.forwardRef<HTMLDivElement, TagsInputItemProps>(
694
702
  data-editing={isEditing ? "" : undefined}
695
703
  data-editable={context.editable ? "" : undefined}
696
704
  data-disabled={itemDisabled ? "" : undefined}
705
+ {...itemProps}
706
+ // Spread stays ABOVE className and the handlers: below them, a consumer
707
+ // that passes any of these props replaces the composed behaviour
708
+ // outright — a tag with an onClick stops being selectable.
697
709
  className={cn(
698
710
  "inline-flex items-center gap-1 rounded-[var(--radius)] border bg-secondary px-2 py-0.5 text-sm text-secondary-foreground transition-colors",
699
711
  isHighlighted && "ring-1 ring-ring",
@@ -707,14 +719,14 @@ const TagsInputItem = React.forwardRef<HTMLDivElement, TagsInputItemProps>(
707
719
  onItemSelect();
708
720
  }
709
721
  }}
710
- onDoubleClick={() => {
711
- itemProps.onDoubleClick?.(undefined as unknown as React.MouseEvent<HTMLDivElement>);
722
+ onDoubleClick={(event) => {
723
+ itemProps.onDoubleClick?.(event);
712
724
  if (context.editable && !itemDisabled) {
713
725
  requestAnimationFrame(() => context.setEditingIndex(index));
714
726
  }
715
727
  }}
716
- onPointerUp={() => {
717
- itemProps.onPointerUp?.(undefined as unknown as React.PointerEvent<HTMLDivElement>);
728
+ onPointerUp={(event) => {
729
+ itemProps.onPointerUp?.(event);
718
730
  if (pointerTypeRef.current === "mouse") onItemSelect();
719
731
  }}
720
732
  onPointerDown={(event) => {
@@ -736,7 +748,6 @@ const TagsInputItem = React.forwardRef<HTMLDivElement, TagsInputItemProps>(
736
748
  context.onItemLeave();
737
749
  }
738
750
  }}
739
- {...itemProps}
740
751
  />
741
752
  </TagsInputItemContext.Provider>
742
753
  );
@@ -754,10 +765,13 @@ function TagsInputEditableItemText() {
754
765
  const itemContext = useTagsInputItem("TagsInputEditableItemText");
755
766
  const [editValue, setEditValue] = React.useState(itemContext.displayValue);
756
767
 
768
+ const { setEditingIndex, setHighlightedIndex, onItemUpdate, inputRef, value: contextValue } = context;
769
+ const { displayValue, value: itemValue, index: itemIndex } = itemContext;
770
+
757
771
  const onBlur = React.useCallback(() => {
758
- setEditValue(itemContext.displayValue);
759
- context.setEditingIndex(null);
760
- }, [context.setEditingIndex, itemContext.displayValue]);
772
+ setEditValue(displayValue);
773
+ setEditingIndex(null);
774
+ }, [setEditingIndex, displayValue]);
761
775
 
762
776
  const onChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
763
777
  const target = event.target;
@@ -775,26 +789,26 @@ function TagsInputEditableItemText() {
775
789
  const onKeyDown = React.useCallback(
776
790
  (event: React.KeyboardEvent<HTMLInputElement>) => {
777
791
  if (event.key === "Enter") {
778
- const index = context.value.indexOf(itemContext.value);
779
- context.onItemUpdate(index, editValue);
792
+ const index = contextValue.indexOf(itemValue);
793
+ onItemUpdate(index, editValue);
780
794
  } else if (event.key === "Escape") {
781
- setEditValue(itemContext.displayValue);
782
- context.setEditingIndex(null);
783
- context.setHighlightedIndex(itemContext.index);
784
- context.inputRef.current?.focus();
795
+ setEditValue(displayValue);
796
+ setEditingIndex(null);
797
+ setHighlightedIndex(itemIndex);
798
+ inputRef.current?.focus();
785
799
  }
786
800
  event.stopPropagation();
787
801
  },
788
802
  [
789
- context.value,
790
- context.onItemUpdate,
791
- context.setEditingIndex,
792
- itemContext.displayValue,
803
+ contextValue,
804
+ onItemUpdate,
805
+ setEditingIndex,
806
+ displayValue,
793
807
  editValue,
794
- itemContext.value,
795
- context.setHighlightedIndex,
796
- itemContext.index,
797
- context.inputRef,
808
+ itemValue,
809
+ setHighlightedIndex,
810
+ itemIndex,
811
+ inputRef,
798
812
  ]
799
813
  );
800
814
 
@@ -3,6 +3,7 @@
3
3
  import { Clock } from "lucide-react";
4
4
  import * as React from "react";
5
5
 
6
+ import { useComposedRefs } from "../../../lib/compose-refs";
6
7
  import { cn } from "../../../lib/utils";
7
8
  import { Button } from "../../forms/button";
8
9
  import { Popover, PopoverContent, PopoverTrigger } from "../../overlay/popover";
@@ -166,8 +167,11 @@ const TimePicker = React.forwardRef<HTMLButtonElement, TimePickerProps>(
166
167
  }, [minuteStep]);
167
168
 
168
169
  const isFormControl = React.useRef(false);
169
- const rootRef = React.useRef<HTMLDivElement>(null);
170
- React.useImperativeHandle(ref, () => rootRef.current as unknown as HTMLButtonElement);
170
+ // The trigger is a <button>, and the caller's ref must see `null` before it
171
+ // mounts. useImperativeHandle cannot express that — it must produce a
172
+ // handle — so the two refs are composed onto the element instead.
173
+ const rootRef = React.useRef<HTMLButtonElement | null>(null);
174
+ const composedRef = useComposedRefs(rootRef, ref);
171
175
 
172
176
  React.useEffect(() => {
173
177
  if (rootRef.current) {
@@ -180,7 +184,7 @@ const TimePicker = React.forwardRef<HTMLButtonElement, TimePickerProps>(
180
184
  <Popover open={open} onOpenChange={setOpen}>
181
185
  <PopoverTrigger asChild>
182
186
  <Button
183
- ref={rootRef as unknown as React.Ref<HTMLButtonElement>}
187
+ ref={composedRef}
184
188
  variant={variant}
185
189
  disabled={disabled}
186
190
  className={cn(
@@ -31,6 +31,10 @@ export type { DownloadButtonProps } from './forms/button-download';
31
31
  export { PopoverActionButton } from './forms/popover-action-button';
32
32
  export type { PopoverActionButtonProps } from './forms/popover-action-button';
33
33
 
34
+ // Money Field — amount in, minor units out.
35
+ export { MoneyField, minorUnitFactor, currencyFractionDigits } from './forms/money-field';
36
+ export type { MoneyFieldProps } from './forms/money-field';
37
+
34
38
  // Mask Input
35
39
  export { MaskInput } from './forms/mask-input';
36
40
  export type { MaskInputProps, MaskDefinition } from './forms/mask-input';
@@ -408,7 +408,10 @@ function KeyValueKeyInput(props: KeyValueKeyInputProps) {
408
408
  const {
409
409
  onChange: onChangeProp,
410
410
  onPaste: onPasteProp,
411
- asChild,
411
+ // Declared on the props type but not honoured here: unlike the `div`-based
412
+ // parts of this component, this one renders a concrete <Input>, so there is
413
+ // no element to Slot into. Destructured only to keep it off the DOM.
414
+ asChild: _asChild,
412
415
  disabled,
413
416
  readOnly,
414
417
  required,
@@ -565,10 +568,7 @@ function KeyValueKeyInput(props: KeyValueKeyInputProps) {
565
568
  store.setState("value", newValue)
566
569
 
567
570
  if (context.onPaste) {
568
- context.onPaste(
569
- event.nativeEvent as unknown as ClipboardEvent,
570
- parsed,
571
- )
571
+ context.onPaste(event.nativeEvent, parsed)
572
572
  }
573
573
  }
574
574
  }
@@ -608,7 +608,9 @@ interface KeyValueValueInputProps
608
608
  function KeyValueValueInput(props: KeyValueValueInputProps) {
609
609
  const {
610
610
  onChange: onChangeProp,
611
- asChild,
611
+ // Same as KeyValueKeyInput: renders a concrete <Textarea>, nothing to Slot
612
+ // into. Destructured only to keep it off the DOM.
613
+ asChild: _asChild,
612
614
  disabled,
613
615
  readOnly,
614
616
  required,
@@ -724,7 +726,7 @@ function KeyValueValueInput(props: KeyValueValueInputProps) {
724
726
  )
725
727
  }
726
728
 
727
- interface KeyValueRemoveProps extends React.ComponentProps<typeof Button> {}
729
+ type KeyValueRemoveProps = React.ComponentProps<typeof Button>;
728
730
 
729
731
  function KeyValueRemove(props: KeyValueRemoveProps) {
730
732
  const { onClick: onClickProp, children, ...removeProps } = props
@@ -66,7 +66,12 @@ const ResizablePanel = React.forwardRef<
66
66
 
67
67
  // SSR fallback - render static div with default size
68
68
  if (!mounted) {
69
- const direction = (props as any)['data-panel-group-direction']
69
+ // `data-*` attributes are not part of the typed prop surface, so the read
70
+ // needs a cast. Narrowed to the one attribute rather than `any` on `props`:
71
+ // this opens exactly the key it uses and leaves the rest checked.
72
+ const direction = (props as { 'data-panel-group-direction'?: string })[
73
+ 'data-panel-group-direction'
74
+ ]
70
75
  const sizeStyle = defaultSize
71
76
  ? direction === 'vertical'
72
77
  ? { height: `${defaultSize}%` }
@@ -28,7 +28,7 @@ const CommandDialog = ({ children, ...props }: DialogProps) => {
28
28
  return (
29
29
  <Dialog {...props}>
30
30
  <DialogContent className="overflow-hidden p-0">
31
- <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
31
+ <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[data-cmdk-input-wrapper]_svg]:h-5 [&_[data-cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
32
32
  {children}
33
33
  </Command>
34
34
  </DialogContent>
@@ -36,11 +36,15 @@ const CommandDialog = ({ children, ...props }: DialogProps) => {
36
36
  )
37
37
  }
38
38
 
39
+ // The wrapper is marked with `data-cmdk-input-wrapper` rather than the bare
40
+ // `cmdk-input-wrapper`: this div is ours, not cmdk's, and React only forwards
41
+ // unknown attributes to the DOM under a `data-` prefix. CommandDialog's
42
+ // `[&_[data-cmdk-input-wrapper]_svg]` sizing selector matches this same name.
39
43
  const CommandInput = React.forwardRef<
40
44
  React.ElementRef<typeof CommandPrimitive.Input>,
41
45
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
42
46
  >(({ className, ...props }, ref) => (
43
- <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
47
+ <div className="flex items-center border-b px-3" data-cmdk-input-wrapper="">
44
48
  <MagnifyingGlassIcon className="mr-2 h-4 w-4 shrink-0 opacity-50" />
45
49
  <CommandPrimitive.Input
46
50
  ref={ref}
@@ -59,9 +63,23 @@ const CommandList = React.forwardRef<
59
63
  React.ElementRef<typeof CommandPrimitive.List>,
60
64
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
61
65
  >(({ className, style, ...props }, ref) => {
62
- const listRef = React.useRef<HTMLDivElement>(null)
63
-
64
- React.useImperativeHandle(ref, () => listRef.current as HTMLDivElement)
66
+ const listRef = React.useRef<React.ElementRef<typeof CommandPrimitive.List> | null>(null)
67
+
68
+ // The element is needed locally (for the cssText fix below) AND by the
69
+ // caller, so it is captured and forwarded in one callback. useImperativeHandle
70
+ // cannot be used here: it must produce a non-null handle, and the element is
71
+ // genuinely null until the primitive mounts.
72
+ const setRef = React.useCallback(
73
+ (node: React.ElementRef<typeof CommandPrimitive.List> | null) => {
74
+ listRef.current = node
75
+ if (typeof ref === 'function') {
76
+ ref(node)
77
+ } else if (ref) {
78
+ ref.current = node
79
+ }
80
+ },
81
+ [ref]
82
+ )
65
83
 
66
84
  React.useEffect(() => {
67
85
  if (listRef.current) {
@@ -71,7 +89,7 @@ const CommandList = React.forwardRef<
71
89
 
72
90
  return (
73
91
  <CommandPrimitive.List
74
- ref={listRef as any}
92
+ ref={setRef}
75
93
  className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
76
94
  style={{
77
95
  maxHeight: '300px',
@@ -46,7 +46,9 @@ export type LinkComponent = ComponentType<LinkComponentProps>;
46
46
  * makes the two instances agree whatever the bundler decides.
47
47
  */
48
48
  const CONTEXT_KEY = Symbol.for('@djangocfg/ui-core.LinkComponentContext');
49
- const globalScope = globalThis as unknown as Record<symbol, unknown>;
49
+ // `typeof globalThis` carries no index signature, but the object genuinely
50
+ // accepts symbol keys — this asserts a property the runtime has.
51
+ const globalScope = globalThis as Record<symbol, unknown>;
50
52
 
51
53
  export const LinkComponentContext =
52
54
  (globalScope[CONTEXT_KEY] as ReturnType<typeof createContext<LinkComponent | null>> | undefined) ??
@@ -18,7 +18,7 @@ import {
18
18
  Pagination, PaginationContent, PaginationEllipsis, PaginationItem
19
19
  } from './pagination';
20
20
 
21
- export interface DRFPaginatedResponse<T = any> {
21
+ export interface DRFPaginatedResponse<T = unknown> {
22
22
  count: number;
23
23
  page: number;
24
24
  pages: number;
@@ -50,6 +50,17 @@ export interface TabsProps extends React.ComponentPropsWithoutRef<typeof TabsPri
50
50
  storageType?: StorageType
51
51
  }
52
52
 
53
+ /**
54
+ * The part of a trigger's props the mobile sheet re-writes when it clones one.
55
+ *
56
+ * Not the trigger's full prop type: everything else passes through untouched,
57
+ * and naming more than is read would be a claim this file cannot check.
58
+ */
59
+ type TriggerProps = {
60
+ onClick?: (event: React.MouseEvent) => void
61
+ className?: string
62
+ }
63
+
53
64
  const Tabs = React.forwardRef<
54
65
  React.ElementRef<typeof TabsPrimitive.Root>,
55
66
  TabsProps
@@ -67,12 +78,13 @@ const Tabs = React.forwardRef<
67
78
  );
68
79
 
69
80
  // Wrap onValueChange to persist tab changes
81
+ const { onValueChange } = props;
70
82
  const handleValueChange = React.useCallback(
71
83
  (newValue: string) => {
72
84
  if (storageKey) setStoredTab(newValue);
73
- props.onValueChange?.(newValue);
85
+ onValueChange?.(newValue);
74
86
  },
75
- [storageKey, setStoredTab, props.onValueChange],
87
+ [storageKey, setStoredTab, onValueChange],
76
88
  );
77
89
 
78
90
  // Build enhanced props: inject stored defaultValue when no value/defaultValue given
@@ -123,9 +135,14 @@ const Tabs = React.forwardRef<
123
135
  (child) => React.isValidElement(child) && child.type === TabsContent
124
136
  )
125
137
 
126
- // Extract triggers from TabsList children
127
- const triggers = React.isValidElement(tabsList)
128
- ? React.Children.toArray((tabsList.props as any).children)
138
+ // Extract triggers from TabsList children.
139
+ //
140
+ // `isValidElement` narrows to `ReactElement<unknown>` unless it is told the
141
+ // prop shape, which is why reading `.props.children` used to need a cast.
142
+ // Naming the shape is the same assertion, made once and checked: this branch
143
+ // only runs for an element whose `type === TabsList`.
144
+ const triggers = React.isValidElement<{ children?: React.ReactNode }>(tabsList)
145
+ ? React.Children.toArray(tabsList.props.children)
129
146
  : []
130
147
 
131
148
  // Mobile Sheet Navigation
@@ -155,11 +172,15 @@ const Tabs = React.forwardRef<
155
172
  <nav className="flex flex-col gap-2 mt-6">
156
173
  <TabsPrimitive.List className="flex flex-col gap-2">
157
174
  {triggers.map((trigger, index) => {
158
- if (!React.isValidElement(trigger)) return null
175
+ // Typed here rather than cast at each use: the two props
176
+ // this clone actually touches are the two it declares, so
177
+ // `cloneElement` checks the override instead of taking it
178
+ // on faith.
179
+ if (!React.isValidElement<TriggerProps>(trigger)) return null
159
180
 
160
181
  // Clone trigger and wrap in mobile-friendly container
161
- const triggerProps = (trigger as any).props || {};
162
- return React.cloneElement(trigger as React.ReactElement, {
182
+ const triggerProps = trigger.props
183
+ return React.cloneElement(trigger, {
163
184
  key: index,
164
185
  onClick: (e: React.MouseEvent) => {
165
186
  setOpen(false)
@@ -171,7 +192,7 @@ const Tabs = React.forwardRef<
171
192
  "hover:bg-muted",
172
193
  triggerProps.className
173
194
  ),
174
- } as any)
195
+ })
175
196
  })}
176
197
  </TabsPrimitive.List>
177
198
  </nav>
@@ -168,7 +168,7 @@ ResponsiveSheetContent.displayName = "ResponsiveSheetContent"
168
168
  // Header
169
169
  // ─────────────────────────────────────────────────────────────────────────────
170
170
 
171
- interface ResponsiveSheetHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
171
+ type ResponsiveSheetHeaderProps = React.HTMLAttributes<HTMLDivElement>;
172
172
 
173
173
  function ResponsiveSheetHeader({ className, ...props }: ResponsiveSheetHeaderProps) {
174
174
  const { isMobile } = React.useContext(ResponsiveSheetContext);
@@ -185,7 +185,7 @@ ResponsiveSheetHeader.displayName = "ResponsiveSheetHeader"
185
185
  // Title
186
186
  // ─────────────────────────────────────────────────────────────────────────────
187
187
 
188
- interface ResponsiveSheetTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {}
188
+ type ResponsiveSheetTitleProps = React.HTMLAttributes<HTMLHeadingElement>;
189
189
 
190
190
  function ResponsiveSheetTitle({ className, ...props }: ResponsiveSheetTitleProps) {
191
191
  const { isMobile } = React.useContext(ResponsiveSheetContext);
@@ -202,7 +202,7 @@ ResponsiveSheetTitle.displayName = "ResponsiveSheetTitle"
202
202
  // Description
203
203
  // ─────────────────────────────────────────────────────────────────────────────
204
204
 
205
- interface ResponsiveSheetDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {}
205
+ type ResponsiveSheetDescriptionProps = React.HTMLAttributes<HTMLParagraphElement>;
206
206
 
207
207
  function ResponsiveSheetDescription({ className, ...props }: ResponsiveSheetDescriptionProps) {
208
208
  const { isMobile } = React.useContext(ResponsiveSheetContext);
@@ -219,7 +219,7 @@ ResponsiveSheetDescription.displayName = "ResponsiveSheetDescription"
219
219
  // Footer
220
220
  // ─────────────────────────────────────────────────────────────────────────────
221
221
 
222
- interface ResponsiveSheetFooterProps extends React.HTMLAttributes<HTMLDivElement> {}
222
+ type ResponsiveSheetFooterProps = React.HTMLAttributes<HTMLDivElement>;
223
223
 
224
224
  function ResponsiveSheetFooter({ className, ...props }: ResponsiveSheetFooterProps) {
225
225
  const { isMobile } = React.useContext(ResponsiveSheetContext);
@@ -10,7 +10,7 @@ import type { MultiSelectProAsyncOption } from './multi-select-pro-async'
10
10
  /**
11
11
  * Generic option builder config
12
12
  */
13
- export interface OptionBuilderConfig<T = any> {
13
+ export interface OptionBuilderConfig<T = unknown> {
14
14
  /** Extract unique ID/value from item */
15
15
  getValue: (item: T) => string
16
16
  /** Extract main label text */
@@ -248,12 +248,19 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
248
248
  [animation, animationConfig]
249
249
  )
250
250
 
251
- // Reset on defaultValue change
251
+ // Reset on defaultValue change.
252
+ // Keyed on the serialized contents, not the array identity: callers commonly
253
+ // pass an inline literal, which would otherwise reset the user's selection on
254
+ // every render. `defaultValue` itself is read through a ref for the same reason.
255
+ const defaultValueKey = JSON.stringify(defaultValue)
256
+ const defaultValueRef = React.useRef(defaultValue)
257
+ defaultValueRef.current = defaultValue
258
+
252
259
  React.useEffect(() => {
253
260
  if (resetOnDefaultValueChange) {
254
- setSelectedValues(defaultValue)
261
+ setSelectedValues(defaultValueRef.current)
255
262
  }
256
- }, [JSON.stringify(defaultValue), resetOnDefaultValueChange])
263
+ }, [defaultValueKey, resetOnDefaultValueChange])
257
264
 
258
265
  // Announce changes for screen readers
259
266
  const announce = React.useCallback((message: string) => {
@@ -347,7 +354,7 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
347
354
  )
348
355
 
349
356
  // Render badge with custom styles
350
- const renderBadge = (option: MultiSelectProAsyncOption, index: number) => {
357
+ const renderBadge = React.useCallback((option: MultiSelectProAsyncOption, index: number) => {
351
358
  const { style, icon: Icon } = option
352
359
  const badgeStyle: React.CSSProperties = {}
353
360
 
@@ -393,7 +400,7 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
393
400
  )}
394
401
  </Badge>
395
402
  )
396
- }
403
+ }, [animConfig, variant, disabled, toggleOption, translations])
397
404
 
398
405
  // Display value
399
406
  const displayValue = React.useMemo(() => {
@@ -414,7 +421,7 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
414
421
  )}
415
422
  </div>
416
423
  )
417
- }, [selectedOptions, maxCount, translations, singleLine, variant, disabled, animConfig])
424
+ }, [selectedOptions, maxCount, translations, singleLine, renderBadge])
418
425
 
419
426
  // Render options
420
427
  const renderOptions = () => {
@@ -214,7 +214,6 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
214
214
  }, [flatOptions, selectedValues])
215
215
 
216
216
  // Responsive configuration (reserved for future use)
217
- // @ts-ignore reserved for future use
218
217
  const _responsiveConfig = React.useMemo((): ResponsiveConfig => {
219
218
  if (typeof responsive === 'boolean') {
220
219
  return responsive
@@ -364,7 +363,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
364
363
  )
365
364
 
366
365
  // Render badge with custom styles
367
- const renderBadge = (option: MultiSelectProOption, index: number) => {
366
+ const renderBadge = React.useCallback((option: MultiSelectProOption, index: number) => {
368
367
  const { style, icon: Icon } = option
369
368
  const badgeStyle: React.CSSProperties = {}
370
369
 
@@ -410,7 +409,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
410
409
  )}
411
410
  </Badge>
412
411
  )
413
- }
412
+ }, [animConfig, variant, disabled, toggleOption, translations])
414
413
 
415
414
  // Display value
416
415
  const displayValue = React.useMemo(() => {
@@ -431,7 +430,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
431
430
  )}
432
431
  </div>
433
432
  )
434
- }, [selectedOptions, maxCount, translations, singleLine, variant, disabled, animConfig])
433
+ }, [selectedOptions, maxCount, translations, singleLine, renderBadge])
435
434
 
436
435
  // Render options
437
436
  const renderOptions = () => {
@@ -7,11 +7,20 @@ import { cn } from '../../../lib/utils';
7
7
  import { FLAG_COMPONENTS } from './flag-map';
8
8
  import { getLanguageCountryCode } from './language-to-country';
9
9
 
10
- export interface FlagProps extends Omit<React.SVGAttributes<SVGSVGElement>, 'children'> {
10
+ /**
11
+ * Presentational attributes only. The underlying `country-flag-icons`
12
+ * components type their handlers against an element that cannot exist, so no
13
+ * event handler passed here could be given an honest signature.
14
+ */
15
+ export interface FlagProps {
11
16
  /** ISO 3166-1 alpha-2 country code (e.g. `'US'`, `'JP'`). Case-insensitive. */
12
17
  countryCode: string;
13
18
  /** Optional rounded corners — common for tile / chip presentations. */
14
19
  rounded?: boolean;
20
+ className?: string;
21
+ width?: number | string;
22
+ height?: number | string;
23
+ title?: string;
15
24
  }
16
25
 
17
26
  /**
@@ -22,11 +31,8 @@ export function Flag({ countryCode, rounded, className, ...props }: FlagProps) {
22
31
  const code = countryCode?.toUpperCase();
23
32
  const Component = code ? FLAG_COMPONENTS[code] : undefined;
24
33
  if (!Component) return null;
25
- // `country-flag-icons` types its components against an HTML+SVG intersection;
26
- // we narrow to plain `SVGAttributes` for our consumers.
27
- const Tag = Component as unknown as React.ComponentType<React.SVGAttributes<SVGSVGElement>>;
28
34
  return (
29
- <Tag
35
+ <Component
30
36
  className={cn(
31
37
  'inline-block shrink-0 select-none',
32
38
  rounded && 'overflow-hidden rounded-[2px]',