@djangocfg/ui-core 2.1.540 → 2.1.542
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/index.tsx +33 -6
- package/src/components/forms/input-group/index.tsx +32 -17
- 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/textarea/index.tsx +10 -4
- 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 -6
- 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
|
@@ -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]',
|
|
@@ -12,11 +12,18 @@ import type * as React from 'react';
|
|
|
12
12
|
import * as Flags from 'country-flag-icons/react/3x2';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* `country-flag-icons`
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* the
|
|
15
|
+
* `country-flag-icons` declares its components against `HTMLSVGElement` — its
|
|
16
|
+
* own `HTMLElement & SVGElement` intersection, which no DOM node satisfies. Its
|
|
17
|
+
* event-handler props are consequently uncallable with a real SVG event, so
|
|
18
|
+
* `FlagProps` exposes only the presentational attributes `Flag` forwards; a
|
|
19
|
+
* handler would have to be typed against an element that cannot exist.
|
|
19
20
|
*/
|
|
20
|
-
export type FlagSvgComponent = (
|
|
21
|
+
export type FlagSvgComponent = (typeof Flags)[keyof typeof Flags];
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Keyed by country code rather than indexed off the namespace object, because
|
|
25
|
+
* the lookup code is a runtime string and a module namespace admits only its
|
|
26
|
+
* literal export names.
|
|
27
|
+
*/
|
|
28
|
+
export const FLAG_COMPONENTS: Record<string, FlagSvgComponent> =
|
|
29
|
+
Object.fromEntries(Object.entries(Flags));
|
|
@@ -24,9 +24,12 @@ export interface ImageWithFallbackProps extends Omit<React.ImgHTMLAttributes<HTM
|
|
|
24
24
|
placeholder?: string;
|
|
25
25
|
blurDataURL?: string;
|
|
26
26
|
|
|
27
|
-
// Callbacks
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
// Callbacks. Fired from two sources with different event shapes: the
|
|
28
|
+
// preloader (`useImageLoader`) passes a DOM `Event` from a detached
|
|
29
|
+
// `new Image()`, the rendered <img> passes a React synthetic event. Narrow
|
|
30
|
+
// before touching anything beyond `type`/`target`.
|
|
31
|
+
onLoad?: (event: Event | React.SyntheticEvent<HTMLImageElement>) => void;
|
|
32
|
+
onError?: (event: Event | React.SyntheticEvent<HTMLImageElement>) => void;
|
|
30
33
|
onLoadStart?: () => void;
|
|
31
34
|
|
|
32
35
|
// Performance
|
|
@@ -55,7 +58,9 @@ export const ImageWithFallback = forwardRef<HTMLImageElement, ImageWithFallbackP
|
|
|
55
58
|
|
|
56
59
|
// Performance
|
|
57
60
|
priority = false,
|
|
58
|
-
|
|
61
|
+
// Accepted for next/image API parity; a plain <img> has nothing to
|
|
62
|
+
// optimize. Destructured so it never reaches the DOM as an attribute.
|
|
63
|
+
unoptimized: _unoptimized = false,
|
|
59
64
|
|
|
60
65
|
// Standard img attributes
|
|
61
66
|
width,
|
|
@@ -34,9 +34,8 @@ Presence.displayName = "Presence";
|
|
|
34
34
|
|
|
35
35
|
function usePresence(present: boolean) {
|
|
36
36
|
const [node, setNode] = React.useState<HTMLElement>();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
);
|
|
37
|
+
// Undefined until the node mounts; every read below already tolerates that.
|
|
38
|
+
const stylesRef = React.useRef<CSSStyleDeclaration | undefined>(undefined);
|
|
40
39
|
const prevPresentRef = React.useRef(present);
|
|
41
40
|
const prevAnimationNameRef = React.useRef<string>("none");
|
|
42
41
|
const initialState = present ? "mounted" : "unmounted";
|
|
@@ -80,25 +80,36 @@ export async function getTokensByCategory(): Promise<TokenCategory[]> {
|
|
|
80
80
|
|
|
81
81
|
export type TokenSymbol = string
|
|
82
82
|
|
|
83
|
+
/** The only props this file passes to a resolved @web3icons component. */
|
|
84
|
+
type TokenIconComponent = React.ComponentType<{
|
|
85
|
+
variant?: 'branded' | 'mono'
|
|
86
|
+
size?: number
|
|
87
|
+
}>
|
|
88
|
+
|
|
83
89
|
// Dynamic import for @web3icons/react tokens
|
|
84
|
-
const tokenIconCache = new Map<string,
|
|
90
|
+
const tokenIconCache = new Map<string, TokenIconComponent>()
|
|
85
91
|
|
|
86
|
-
async function getTokenIcon(symbol: string): Promise<
|
|
92
|
+
async function getTokenIcon(symbol: string): Promise<TokenIconComponent | null> {
|
|
87
93
|
const normalizedSymbol = symbol.toUpperCase().replace(/[^A-Z0-9]/g, '_')
|
|
88
94
|
const componentName = `Token${normalizedSymbol}`
|
|
89
95
|
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
const cached = tokenIconCache.get(componentName)
|
|
97
|
+
if (cached) {
|
|
98
|
+
return cached
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
// The export name is computed at runtime, so the module is read as an
|
|
103
|
+
// untyped key map and the value is proven to be a component (a function)
|
|
104
|
+
// before it is used.
|
|
105
|
+
const namespace: Record<string, unknown> = await import('@web3icons/react')
|
|
106
|
+
const Component = namespace[componentName]
|
|
107
|
+
if (typeof Component === 'function') {
|
|
108
|
+
const IconComponent = Component as TokenIconComponent
|
|
109
|
+
tokenIconCache.set(componentName, IconComponent)
|
|
110
|
+
return IconComponent
|
|
100
111
|
}
|
|
101
|
-
} catch (
|
|
112
|
+
} catch (_e) {
|
|
102
113
|
// Token not found
|
|
103
114
|
}
|
|
104
115
|
|
|
@@ -110,11 +121,13 @@ export function TokenIcon({
|
|
|
110
121
|
variant = 'branded',
|
|
111
122
|
size = 24,
|
|
112
123
|
className,
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
// Destructured, not used: these narrow which icon is resolved upstream
|
|
125
|
+
// and must not fall through `...props` onto the DOM element below.
|
|
126
|
+
network: _network,
|
|
127
|
+
address: _address,
|
|
115
128
|
...props
|
|
116
129
|
}: TokenIconProps) {
|
|
117
|
-
const [IconComponent, setIconComponent] = React.useState<
|
|
130
|
+
const [IconComponent, setIconComponent] = React.useState<TokenIconComponent | null>(null)
|
|
118
131
|
const [_loading, setLoading] = React.useState(true)
|
|
119
132
|
|
|
120
133
|
React.useEffect(() => {
|
|
@@ -32,6 +32,11 @@ const clamp01 = (v: number): number => {
|
|
|
32
32
|
* Per-key registry so repeated calls with the same `storageKey` return
|
|
33
33
|
* the same store instance — necessary for cross-component state sharing
|
|
34
34
|
* (e.g. ChatHeader audio toggle + ChatLauncher bus reading the same key).
|
|
35
|
+
*
|
|
36
|
+
* `E` is phantom — nothing at runtime restricts which event names the store
|
|
37
|
+
* accepts, so one instance serves callers that name different event unions.
|
|
38
|
+
* The store is therefore built and stored at the widest instantiation, and
|
|
39
|
+
* every narrower `AudioPrefsState<E>` is assignable from it.
|
|
35
40
|
*/
|
|
36
41
|
const registry = new Map<string, UseBoundStore<StoreApi<AudioPrefsState<string>>>>();
|
|
37
42
|
|
|
@@ -39,9 +44,9 @@ export function createAudioPrefsStore<E extends string = string>(
|
|
|
39
44
|
storageKey: string,
|
|
40
45
|
): UseBoundStore<StoreApi<AudioPrefsState<E>>> {
|
|
41
46
|
const cached = registry.get(storageKey);
|
|
42
|
-
if (cached) return cached
|
|
47
|
+
if (cached) return cached;
|
|
43
48
|
|
|
44
|
-
const store = create<AudioPrefsState<
|
|
49
|
+
const store = create<AudioPrefsState<string>>()(
|
|
45
50
|
persist(
|
|
46
51
|
(set) => ({
|
|
47
52
|
volume: 1,
|
|
@@ -70,7 +75,7 @@ export function createAudioPrefsStore<E extends string = string>(
|
|
|
70
75
|
),
|
|
71
76
|
);
|
|
72
77
|
|
|
73
|
-
registry.set(storageKey, store
|
|
78
|
+
registry.set(storageKey, store);
|
|
74
79
|
return store;
|
|
75
80
|
}
|
|
76
81
|
|
|
@@ -139,7 +139,11 @@ export function useBrowserDetect(): BrowserInfo {
|
|
|
139
139
|
const isEdge = ua.includes('edg/') || ua.includes('edge/');
|
|
140
140
|
|
|
141
141
|
// Brave (check for Brave-specific API)
|
|
142
|
-
|
|
142
|
+
// Boundary: `navigator.brave` is a Brave-only extension absent from lib.dom.
|
|
143
|
+
// Declaring the one optional field we read keeps the probe honest without
|
|
144
|
+
// asserting anything about the rest of Navigator.
|
|
145
|
+
const braveAware: Navigator & { brave?: unknown } = window.navigator;
|
|
146
|
+
const isBrave = braveAware.brave != null;
|
|
143
147
|
|
|
144
148
|
// Arc (check for Arc-specific markers in UA)
|
|
145
149
|
const isArc = ua.includes('arc/');
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
'use client';
|
|
8
8
|
|
|
9
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
9
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
10
10
|
|
|
11
11
|
export interface ImageLoaderState {
|
|
12
12
|
isLoading: boolean;
|
|
@@ -16,8 +16,13 @@ export interface ImageLoaderState {
|
|
|
16
16
|
|
|
17
17
|
export interface ImageLoaderCallbacks {
|
|
18
18
|
onLoadStart?: () => void;
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Receives the DOM `Event` from a detached `new Image()`, NOT a React
|
|
21
|
+
* synthetic event: there is no React element behind this load, so
|
|
22
|
+
* `persist`, `nativeEvent` and `isDefaultPrevented` do not exist on it.
|
|
23
|
+
*/
|
|
24
|
+
onLoad?: (event: Event) => void;
|
|
25
|
+
onError?: (event: Event) => void;
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
export const useImageLoader = (
|
|
@@ -30,18 +35,23 @@ export const useImageLoader = (
|
|
|
30
35
|
hasError: false,
|
|
31
36
|
});
|
|
32
37
|
|
|
38
|
+
// Callbacks are read through a ref so they never participate in the deps
|
|
39
|
+
// below. Callers pass `callbacks` as an inline object literal, so depending on
|
|
40
|
+
// it would re-run the load effect on every render — refetching the image and
|
|
41
|
+
// re-firing onLoadStart indefinitely. The ref keeps the latest handlers
|
|
42
|
+
// without tying image loading to render identity.
|
|
43
|
+
const callbacksRef = useRef(callbacks);
|
|
44
|
+
callbacksRef.current = callbacks;
|
|
45
|
+
|
|
33
46
|
const handleLoad = useCallback((event: Event) => {
|
|
34
47
|
setState({
|
|
35
48
|
isLoading: false,
|
|
36
49
|
isLoaded: true,
|
|
37
50
|
hasError: false,
|
|
38
51
|
});
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
callbacks.onLoad(event as unknown as React.SyntheticEvent<HTMLImageElement>);
|
|
43
|
-
}
|
|
44
|
-
}, [callbacks?.onLoad]);
|
|
52
|
+
|
|
53
|
+
callbacksRef.current?.onLoad?.(event);
|
|
54
|
+
}, []);
|
|
45
55
|
|
|
46
56
|
const handleError = useCallback((event: Event) => {
|
|
47
57
|
setState({
|
|
@@ -49,12 +59,9 @@ export const useImageLoader = (
|
|
|
49
59
|
isLoaded: false,
|
|
50
60
|
hasError: true,
|
|
51
61
|
});
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
callbacks.onError(event as unknown as React.SyntheticEvent<HTMLImageElement>);
|
|
56
|
-
}
|
|
57
|
-
}, [callbacks?.onError]);
|
|
62
|
+
|
|
63
|
+
callbacksRef.current?.onError?.(event);
|
|
64
|
+
}, []);
|
|
58
65
|
|
|
59
66
|
useEffect(() => {
|
|
60
67
|
if (!src) {
|
|
@@ -72,10 +79,7 @@ export const useImageLoader = (
|
|
|
72
79
|
hasError: false,
|
|
73
80
|
});
|
|
74
81
|
|
|
75
|
-
|
|
76
|
-
if (callbacks?.onLoadStart) {
|
|
77
|
-
callbacks.onLoadStart();
|
|
78
|
-
}
|
|
82
|
+
callbacksRef.current?.onLoadStart?.();
|
|
79
83
|
|
|
80
84
|
const img = new Image();
|
|
81
85
|
|
|
@@ -89,7 +93,7 @@ export const useImageLoader = (
|
|
|
89
93
|
img.removeEventListener('load', handleLoad);
|
|
90
94
|
img.removeEventListener('error', handleError);
|
|
91
95
|
};
|
|
92
|
-
}, [src, handleLoad, handleError
|
|
96
|
+
}, [src, handleLoad, handleError]);
|
|
93
97
|
|
|
94
98
|
return state;
|
|
95
99
|
};
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
* const direction = useScrollDirection();
|
|
48
48
|
*/
|
|
49
49
|
|
|
50
|
-
import { useCallback, useSyncExternalStore, type RefObject } from 'react';
|
|
50
|
+
import { useCallback, useMemo, useSyncExternalStore, type RefObject } from 'react';
|
|
51
51
|
|
|
52
52
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
53
53
|
// Types
|
|
@@ -264,8 +264,9 @@ const getServerSnapshot = (): ScrollSnapshot => SSR_SNAPSHOT;
|
|
|
264
264
|
* underlying store but let React skip re-renders when other fields change.
|
|
265
265
|
*/
|
|
266
266
|
export function useScroll(target?: ScrollTarget): ScrollSnapshot {
|
|
267
|
-
|
|
268
|
-
const
|
|
267
|
+
// useMemo, not useCallback: these are factory RESULTS, not inline functions.
|
|
268
|
+
const subscribe = useMemo(() => subscribeFactory(target), [target]);
|
|
269
|
+
const getSnapshot = useMemo(() => getSnapshotFactory(target), [target]);
|
|
269
270
|
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
270
271
|
}
|
|
271
272
|
|
|
@@ -274,7 +275,7 @@ export function useScroll(target?: ScrollTarget): ScrollSnapshot {
|
|
|
274
275
|
* change — direction flips and isScrolling transitions are ignored.
|
|
275
276
|
*/
|
|
276
277
|
export function useScrollPosition(target?: ScrollTarget): { x: number; y: number } {
|
|
277
|
-
const subscribe =
|
|
278
|
+
const subscribe = useMemo(() => subscribeFactory(target), [target]);
|
|
278
279
|
const getServerXY = useCallback(() => SSR_XY, []);
|
|
279
280
|
const getXY = useCallback(() => {
|
|
280
281
|
const snap = getSnapshotFactory(target)();
|
|
@@ -298,7 +299,7 @@ function cachePosition(snap: ScrollSnapshot): { x: number; y: number } {
|
|
|
298
299
|
* flips (e.g. user reverses scroll).
|
|
299
300
|
*/
|
|
300
301
|
export function useScrollDirection(target?: ScrollTarget): ScrollDirection {
|
|
301
|
-
const subscribe =
|
|
302
|
+
const subscribe = useMemo(() => subscribeFactory(target), [target]);
|
|
302
303
|
const getDirection = useCallback(
|
|
303
304
|
() => getSnapshotFactory(target)().direction,
|
|
304
305
|
[target]
|
|
@@ -312,7 +313,7 @@ export function useScrollDirection(target?: ScrollTarget): ScrollDirection {
|
|
|
312
313
|
* idle gap. Useful for hiding hover overlays while scrolling.
|
|
313
314
|
*/
|
|
314
315
|
export function useIsScrolling(target?: ScrollTarget): boolean {
|
|
315
|
-
const subscribe =
|
|
316
|
+
const subscribe = useMemo(() => subscribeFactory(target), [target]);
|
|
316
317
|
const getIsScrolling = useCallback(
|
|
317
318
|
() => getSnapshotFactory(target)().isScrolling,
|
|
318
319
|
[target]
|