@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.
- package/README.md +3 -1
- package/package.json +12 -9
- package/src/components/data/BalancedText/hooks/useMaxLinesWidth.ts +4 -25
- package/src/components/forms/button-download/index.tsx +1 -1
- package/src/components/forms/datetime-field/date-time-field.tsx +1 -1
- package/src/components/forms/editable/index.tsx +7 -3
- package/src/components/forms/input-group/index.tsx +12 -0
- package/src/components/forms/mask-input/index.tsx +7 -3
- package/src/components/forms/money-field/README.md +79 -0
- package/src/components/forms/money-field/index.tsx +288 -0
- package/src/components/forms/otp/use-otp-input.ts +1 -1
- package/src/components/forms/tags-input/index.tsx +55 -41
- package/src/components/forms/time-picker/index.tsx +7 -3
- package/src/components/index.ts +4 -0
- package/src/components/layout/key-value/index.tsx +9 -7
- package/src/components/layout/resizable/index.tsx +6 -1
- package/src/components/navigation/command/index.tsx +24 -6
- package/src/components/navigation/link/LinkContext.tsx +3 -1
- package/src/components/navigation/pagination/pagination-static.tsx +1 -1
- package/src/components/navigation/tabs/index.tsx +30 -9
- package/src/components/overlay/responsive-sheet/index.tsx +4 -4
- package/src/components/select/helpers.tsx +1 -1
- package/src/components/select/multi-select-pro-async.tsx +13 -6
- package/src/components/select/multi-select-pro.tsx +3 -4
- package/src/components/specialized/flag/Flag.tsx +11 -5
- package/src/components/specialized/flag/flag-map.ts +13 -7
- package/src/components/specialized/image-with-fallback/index.tsx +9 -4
- package/src/components/specialized/presence/index.tsx +2 -3
- package/src/components/specialized/token-icon/index.tsx +26 -13
- package/src/hooks/audio/useAudioPrefs.ts +8 -3
- package/src/hooks/device/useBrowserDetect.ts +5 -1
- package/src/hooks/dom/useImageLoader.ts +24 -20
- package/src/hooks/dom/useScroll.ts +7 -6
- package/src/hooks/events/useEventsBus.ts +19 -5
- package/src/hooks/hotkey/useHotkeyChord.ts +11 -4
- package/src/hooks/hotkey/useHotkeyHelp.ts +8 -3
- package/src/hooks/router/adapter.tsx +3 -1
- package/src/hooks/state/storage-quota.ts +27 -0
- package/src/hooks/state/useDebouncedCallback.ts +26 -20
- package/src/hooks/state/useLocalStorage.ts +7 -13
- package/src/hooks/state/useSessionStorage.ts +7 -9
- package/src/lib/compose-event-handlers.ts +5 -5
- package/src/lib/dialog-service/getDialog.ts +1 -1
- package/src/lib/get-element-ref.ts +9 -6
- package/src/lib/pretext/pretext.types.ts +25 -70
- package/src/lib/pretext/use-pretext.ts +8 -12
- package/src/snippets/LazyComponent.tsx +9 -9
- 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
|
|
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
|
|
70
|
-
extends React.ComponentPropsWithoutRef<"span"> {}
|
|
68
|
+
export type TagsInputItemTextProps = React.ComponentPropsWithoutRef<"span">;
|
|
71
69
|
|
|
72
|
-
export
|
|
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
|
-
|
|
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 =
|
|
547
|
+
const isAdded = onItemAdd(value);
|
|
541
548
|
if (isAdded) {
|
|
542
549
|
event.currentTarget.value = "";
|
|
543
|
-
|
|
550
|
+
setHighlightedIndex(null);
|
|
544
551
|
}
|
|
545
552
|
event.preventDefault();
|
|
546
553
|
},
|
|
547
|
-
[
|
|
554
|
+
[onItemAdd, setHighlightedIndex]
|
|
548
555
|
);
|
|
549
556
|
|
|
550
557
|
const onTab = React.useCallback(
|
|
551
558
|
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
552
|
-
if (!
|
|
559
|
+
if (!addOnTab) return;
|
|
553
560
|
onCustomKeydown(event);
|
|
554
561
|
},
|
|
555
|
-
[
|
|
562
|
+
[addOnTab, onCustomKeydown]
|
|
556
563
|
);
|
|
557
564
|
|
|
558
565
|
React.useEffect(() => {
|
|
559
566
|
if (!autoFocus) return;
|
|
560
|
-
const id = requestAnimationFrame(() =>
|
|
567
|
+
const id = requestAnimationFrame(() => inputRef.current?.focus());
|
|
561
568
|
return () => cancelAnimationFrame(id);
|
|
562
|
-
}, [autoFocus,
|
|
569
|
+
}, [autoFocus, inputRef]);
|
|
563
570
|
|
|
564
571
|
const composedRef = React.useCallback(
|
|
565
572
|
(node: HTMLInputElement | null) => {
|
|
566
|
-
|
|
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,
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
}, [
|
|
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?.(
|
|
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?.(
|
|
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(
|
|
759
|
-
|
|
760
|
-
}, [
|
|
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 =
|
|
779
|
-
|
|
792
|
+
const index = contextValue.indexOf(itemValue);
|
|
793
|
+
onItemUpdate(index, editValue);
|
|
780
794
|
} else if (event.key === "Escape") {
|
|
781
|
-
setEditValue(
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
795
|
+
setEditValue(displayValue);
|
|
796
|
+
setEditingIndex(null);
|
|
797
|
+
setHighlightedIndex(itemIndex);
|
|
798
|
+
inputRef.current?.focus();
|
|
785
799
|
}
|
|
786
800
|
event.stopPropagation();
|
|
787
801
|
},
|
|
788
802
|
[
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
803
|
+
contextValue,
|
|
804
|
+
onItemUpdate,
|
|
805
|
+
setEditingIndex,
|
|
806
|
+
displayValue,
|
|
793
807
|
editValue,
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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={
|
|
187
|
+
ref={composedRef}
|
|
184
188
|
variant={variant}
|
|
185
189
|
disabled={disabled}
|
|
186
190
|
className={cn(
|
package/src/components/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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<
|
|
63
|
-
|
|
64
|
-
|
|
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={
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
85
|
+
onValueChange?.(newValue);
|
|
74
86
|
},
|
|
75
|
-
[storageKey, setStoredTab,
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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 =
|
|
162
|
-
return React.cloneElement(trigger
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
261
|
+
setSelectedValues(defaultValueRef.current)
|
|
255
262
|
}
|
|
256
|
-
}, [
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
<
|
|
35
|
+
<Component
|
|
30
36
|
className={cn(
|
|
31
37
|
'inline-block shrink-0 select-none',
|
|
32
38
|
rounded && 'overflow-hidden rounded-[2px]',
|