@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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useRef } from 'react';
|
|
4
4
|
|
|
5
|
-
export type FormEvent<T extends string = string, P =
|
|
5
|
+
export type FormEvent<T extends string = string, P = unknown> = {
|
|
6
6
|
type: T;
|
|
7
7
|
payload?: P;
|
|
8
8
|
timestamp?: number;
|
|
@@ -11,7 +11,9 @@ export type FormEvent<T extends string = string, P = any> = {
|
|
|
11
11
|
type EventListener<T extends FormEvent> = (event: T) => void;
|
|
12
12
|
|
|
13
13
|
class EventBus {
|
|
14
|
-
|
|
14
|
+
// Subscribers each declare their own event type; the bus only ever hands
|
|
15
|
+
// them a FormEvent, so it stores the widest listener it can actually call.
|
|
16
|
+
private listeners: Set<EventListener<FormEvent>> = new Set();
|
|
15
17
|
|
|
16
18
|
publish<T extends FormEvent>(event: T) {
|
|
17
19
|
this.listeners.forEach(listener => listener({
|
|
@@ -20,7 +22,10 @@ class EventBus {
|
|
|
20
22
|
}));
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
// Not generic on purpose: the bus broadcasts EVERY event to EVERY listener,
|
|
26
|
+
// so a listener declaring a narrower event type would be handed events that
|
|
27
|
+
// do not match it. Filtering by `type` is the subscriber's job.
|
|
28
|
+
subscribe(listener: EventListener<FormEvent>) {
|
|
24
29
|
this.listeners.add(listener);
|
|
25
30
|
return () => {
|
|
26
31
|
this.listeners.delete(listener);
|
|
@@ -30,9 +35,18 @@ class EventBus {
|
|
|
30
35
|
|
|
31
36
|
export const events = new EventBus();
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Subscribe to one event type on the bus.
|
|
40
|
+
*
|
|
41
|
+
* The payload arrives from an untyped publisher, so it is delivered as
|
|
42
|
+
* `unknown`: this is the boundary, and the handler narrows it. Callers that
|
|
43
|
+
* know the publisher's shape should narrow with a type guard rather than
|
|
44
|
+
* declaring the shape and hoping — nothing at runtime enforces the pairing of
|
|
45
|
+
* `eventType` and payload.
|
|
46
|
+
*/
|
|
47
|
+
export function useEventListener<T extends string>(
|
|
34
48
|
eventType: T,
|
|
35
|
-
handler: (payload:
|
|
49
|
+
handler: (payload: unknown) => void
|
|
36
50
|
) {
|
|
37
51
|
const savedHandler = useRef(handler);
|
|
38
52
|
|
|
@@ -48,12 +48,19 @@ export function useHotkeyChord(
|
|
|
48
48
|
const callbackRef = useRef(callback);
|
|
49
49
|
callbackRef.current = callback;
|
|
50
50
|
|
|
51
|
+
// The listener is rebound on the chord's CONTENTS, not the array's identity:
|
|
52
|
+
// callers write `useHotkeyChord(['g', 't'], …)` with a fresh literal each
|
|
53
|
+
// render, which would otherwise tear down and re-add the keydown listener
|
|
54
|
+
// continuously and reset any chord in progress.
|
|
55
|
+
const keysKey = keys.join('|');
|
|
56
|
+
const keysRef = useRef(keys);
|
|
57
|
+
keysRef.current = keys;
|
|
58
|
+
|
|
51
59
|
useEffect(() => {
|
|
52
60
|
if (!enabled) return;
|
|
53
61
|
if (typeof window === 'undefined') return;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const sequence = keys.map((k) => k.toLowerCase());
|
|
62
|
+
const sequence = keysRef.current.map((k) => k.toLowerCase());
|
|
63
|
+
if (!sequence.length) return;
|
|
57
64
|
let progress = 0;
|
|
58
65
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
59
66
|
|
|
@@ -92,5 +99,5 @@ export function useHotkeyChord(
|
|
|
92
99
|
window.removeEventListener('keydown', onKey);
|
|
93
100
|
if (timer) clearTimeout(timer);
|
|
94
101
|
};
|
|
95
|
-
}, [
|
|
102
|
+
}, [keysKey, enabled, chordWindow, enableOnFormTags, preventDefault]);
|
|
96
103
|
}
|
|
@@ -61,8 +61,13 @@ export function getRegisteredHotkeys(): RegisteredHotkey[] {
|
|
|
61
61
|
* still want to appear in the cheat sheet).
|
|
62
62
|
*/
|
|
63
63
|
export function useRegisterHotkey(entry: RegisteredHotkey | null): void {
|
|
64
|
+
// Registration is keyed on the entry's FIELDS, not its identity: callers pass
|
|
65
|
+
// an inline object, so depending on `entry` would unregister and re-register
|
|
66
|
+
// on every render and notify every cheat-sheet subscriber each time.
|
|
67
|
+
const { combo, description, scope } = entry ?? {};
|
|
68
|
+
|
|
64
69
|
useEffect(() => {
|
|
65
|
-
if (!
|
|
66
|
-
return registerHotkey(
|
|
67
|
-
}, [
|
|
70
|
+
if (!combo || !description) return;
|
|
71
|
+
return registerHotkey({ combo, description, scope });
|
|
72
|
+
}, [combo, description, scope]);
|
|
68
73
|
}
|
|
@@ -113,7 +113,9 @@ export const defaultAdapter: RouterAdapter = Object.freeze({
|
|
|
113
113
|
// every hook reads another — falling back to the History API, which changes the
|
|
114
114
|
// URL without telling the host router.
|
|
115
115
|
const CONTEXT_KEY = Symbol.for('@djangocfg/ui-core.RouterAdapterContext');
|
|
116
|
-
|
|
116
|
+
// `typeof globalThis` carries no index signature, but the object genuinely
|
|
117
|
+
// accepts symbol keys — this asserts a property the runtime has.
|
|
118
|
+
const globalScope = globalThis as Record<symbol, unknown>;
|
|
117
119
|
|
|
118
120
|
export const RouterAdapterContext =
|
|
119
121
|
(globalScope[CONTEXT_KEY] as ReturnType<typeof createContext<RouterAdapter | null>> | undefined) ??
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared quota detection for the localStorage / sessionStorage hooks.
|
|
3
|
+
*
|
|
4
|
+
* Internal — not exported from the package entry point.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Is this thrown value the browser's "storage is full" signal?
|
|
9
|
+
*
|
|
10
|
+
* Boundary: a `catch` binding is `unknown`, and browsers disagree on how they
|
|
11
|
+
* report a full quota — Chrome/Safari throw `QuotaExceededError`, older engines
|
|
12
|
+
* use the legacy DOMException code 22, and some report it only in the message.
|
|
13
|
+
* Each field is therefore probed rather than assumed to exist.
|
|
14
|
+
*/
|
|
15
|
+
export function isQuotaExceededError(error: unknown): boolean {
|
|
16
|
+
if (typeof error !== 'object' || error === null) return false;
|
|
17
|
+
|
|
18
|
+
const { name, code, message } = error as {
|
|
19
|
+
name?: unknown;
|
|
20
|
+
code?: unknown;
|
|
21
|
+
message?: unknown;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (name === 'QuotaExceededError') return true;
|
|
25
|
+
if (code === 22) return true;
|
|
26
|
+
return typeof message === 'string' && message.includes('quota');
|
|
27
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect, useRef } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Creates a debounced version of a callback function.
|
|
@@ -9,10 +9,15 @@ import { useCallback, useEffect, useRef } from 'react';
|
|
|
9
9
|
* @param delay The debounce delay in milliseconds.
|
|
10
10
|
* @returns A debounced callback function.
|
|
11
11
|
*/
|
|
12
|
-
|
|
12
|
+
/** The debounced function, plus the `cancel` handle this hook attaches to it. */
|
|
13
|
+
export type DebouncedCallback<T extends (...args: never[]) => unknown> = ((
|
|
14
|
+
...args: Parameters<T>
|
|
15
|
+
) => void) & { cancel: () => void };
|
|
16
|
+
|
|
17
|
+
export function useDebouncedCallback<T extends (...args: never[]) => unknown>(
|
|
13
18
|
callback: T,
|
|
14
19
|
delay: number
|
|
15
|
-
):
|
|
20
|
+
): DebouncedCallback<T> {
|
|
16
21
|
const callbackRef = useRef(callback);
|
|
17
22
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
18
23
|
|
|
@@ -30,29 +35,30 @@ export function useDebouncedCallback<T extends (...args: any[]) => any>(
|
|
|
30
35
|
};
|
|
31
36
|
}, []);
|
|
32
37
|
|
|
33
|
-
const
|
|
34
|
-
(...args: Parameters<T>) => {
|
|
35
|
-
if (timeoutRef.current) {
|
|
36
|
-
clearTimeout(timeoutRef.current);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
timeoutRef.current = setTimeout(() => {
|
|
40
|
-
callbackRef.current(...args);
|
|
41
|
-
}, delay);
|
|
42
|
-
},
|
|
43
|
-
[delay]
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
// Add a cancel method to the debounced function
|
|
47
|
-
// We attach it directly to the function object
|
|
48
|
-
(debouncedCallback as any).cancel = useCallback(() => {
|
|
38
|
+
const cancel = useCallback(() => {
|
|
49
39
|
if (timeoutRef.current) {
|
|
50
40
|
clearTimeout(timeoutRef.current);
|
|
51
41
|
timeoutRef.current = null;
|
|
52
42
|
}
|
|
53
43
|
}, []);
|
|
54
44
|
|
|
55
|
-
|
|
45
|
+
// `cancel` is part of the returned type, so it is assembled with the
|
|
46
|
+
// function rather than bolted on afterwards.
|
|
47
|
+
return useMemo(() => {
|
|
48
|
+
const debounced: DebouncedCallback<T> = Object.assign(
|
|
49
|
+
(...args: Parameters<T>) => {
|
|
50
|
+
if (timeoutRef.current) {
|
|
51
|
+
clearTimeout(timeoutRef.current);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
timeoutRef.current = setTimeout(() => {
|
|
55
|
+
callbackRef.current(...args);
|
|
56
|
+
}, delay);
|
|
57
|
+
},
|
|
58
|
+
{ cancel }
|
|
59
|
+
);
|
|
60
|
+
return debounced;
|
|
61
|
+
}, [delay, cancel]);
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
export default useDebouncedCallback;
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
4
|
|
|
5
|
+
import { isQuotaExceededError } from './storage-quota';
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Storage wrapper format with metadata
|
|
7
9
|
* Used when TTL is specified
|
|
@@ -254,7 +256,7 @@ export function useLocalStorage<T>(
|
|
|
254
256
|
}, [key, parseRaw]);
|
|
255
257
|
|
|
256
258
|
// Check data size and limit
|
|
257
|
-
const checkDataSize = (data:
|
|
259
|
+
const checkDataSize = (data: unknown): boolean => {
|
|
258
260
|
try {
|
|
259
261
|
const jsonString = JSON.stringify(data);
|
|
260
262
|
const sizeInBytes = new Blob([jsonString]).size;
|
|
@@ -338,12 +340,8 @@ export function useLocalStorage<T>(
|
|
|
338
340
|
(dataToStore: string): void => {
|
|
339
341
|
try {
|
|
340
342
|
window.localStorage.setItem(key, dataToStore);
|
|
341
|
-
} catch (storageError
|
|
342
|
-
if (
|
|
343
|
-
storageError.name === 'QuotaExceededError' ||
|
|
344
|
-
storageError.code === 22 ||
|
|
345
|
-
storageError.message?.includes('quota')
|
|
346
|
-
) {
|
|
343
|
+
} catch (storageError) {
|
|
344
|
+
if (isQuotaExceededError(storageError)) {
|
|
347
345
|
console.warn('localStorage quota exceeded, clearing old data...');
|
|
348
346
|
clearOldData();
|
|
349
347
|
try {
|
|
@@ -481,12 +479,8 @@ export function useLocalStorage<T>(
|
|
|
481
479
|
if (typeof window !== 'undefined') {
|
|
482
480
|
try {
|
|
483
481
|
window.localStorage.removeItem(key);
|
|
484
|
-
} catch (removeError
|
|
485
|
-
if (
|
|
486
|
-
removeError.name === 'QuotaExceededError' ||
|
|
487
|
-
removeError.code === 22 ||
|
|
488
|
-
removeError.message?.includes('quota')
|
|
489
|
-
) {
|
|
482
|
+
} catch (removeError) {
|
|
483
|
+
if (isQuotaExceededError(removeError)) {
|
|
490
484
|
console.warn('localStorage quota exceeded during removal, clearing old data...');
|
|
491
485
|
clearOldData();
|
|
492
486
|
try {
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
|
|
5
|
+
import { isQuotaExceededError } from './storage-quota';
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Storage wrapper format with metadata
|
|
7
9
|
* Used when TTL is specified
|
|
@@ -131,7 +133,7 @@ export function useSessionStorage<T>(
|
|
|
131
133
|
});
|
|
132
134
|
|
|
133
135
|
// Check data size and limit
|
|
134
|
-
const checkDataSize = (data:
|
|
136
|
+
const checkDataSize = (data: unknown): boolean => {
|
|
135
137
|
try {
|
|
136
138
|
const jsonString = JSON.stringify(data);
|
|
137
139
|
const sizeInBytes = new Blob([jsonString]).size;
|
|
@@ -236,11 +238,9 @@ export function useSessionStorage<T>(
|
|
|
236
238
|
// Try to set the value
|
|
237
239
|
try {
|
|
238
240
|
window.sessionStorage.setItem(key, dataToStore);
|
|
239
|
-
} catch (storageError
|
|
241
|
+
} catch (storageError) {
|
|
240
242
|
// If quota exceeded, clear old data and try again
|
|
241
|
-
if (storageError
|
|
242
|
-
storageError.code === 22 ||
|
|
243
|
-
storageError.message?.includes('quota')) {
|
|
243
|
+
if (isQuotaExceededError(storageError)) {
|
|
244
244
|
console.warn('sessionStorage quota exceeded, clearing old data...');
|
|
245
245
|
clearOldData();
|
|
246
246
|
|
|
@@ -279,11 +279,9 @@ export function useSessionStorage<T>(
|
|
|
279
279
|
if (typeof window !== 'undefined') {
|
|
280
280
|
try {
|
|
281
281
|
window.sessionStorage.removeItem(key);
|
|
282
|
-
} catch (removeError
|
|
282
|
+
} catch (removeError) {
|
|
283
283
|
// If removal fails due to quota, try to clear some data first
|
|
284
|
-
if (removeError
|
|
285
|
-
removeError.code === 22 ||
|
|
286
|
-
removeError.message?.includes('quota')) {
|
|
284
|
+
if (isQuotaExceededError(removeError)) {
|
|
287
285
|
console.warn('sessionStorage quota exceeded during removal, clearing old data...');
|
|
288
286
|
clearOldData();
|
|
289
287
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Constrained to values that actually carry `defaultPrevented` — both DOM
|
|
4
|
+
// events and React synthetic events qualify. Without it the guard below reads a
|
|
5
|
+
// property the type never promised.
|
|
6
|
+
function composeEventHandlers<E extends { defaultPrevented: boolean }>(
|
|
4
7
|
originalEventHandler?: (event: E) => void,
|
|
5
8
|
ourEventHandler?: (event: E) => void,
|
|
6
9
|
{ checkForDefaultPrevented = true } = {},
|
|
@@ -8,10 +11,7 @@ function composeEventHandlers<E>(
|
|
|
8
11
|
return function handleEvent(event: E) {
|
|
9
12
|
originalEventHandler?.(event);
|
|
10
13
|
|
|
11
|
-
if (
|
|
12
|
-
checkForDefaultPrevented &&
|
|
13
|
-
(event as unknown as Event).defaultPrevented
|
|
14
|
-
) {
|
|
14
|
+
if (checkForDefaultPrevented && event.defaultPrevented) {
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -30,7 +30,7 @@ export function getDialog(): DialogAPI | null {
|
|
|
30
30
|
if (!api) {
|
|
31
31
|
if (process.env.NODE_ENV !== 'production' && !warnedMissing) {
|
|
32
32
|
warnedMissing = true;
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
console.warn(
|
|
35
35
|
'[getDialog] window.dialog is not available — mount <DialogProvider /> at the app root.',
|
|
36
36
|
);
|
|
@@ -3,17 +3,21 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* React 18 exposed `ref` on the element itself; React 19 moved it into `props`.
|
|
7
|
+
* Neither placement is in `ReactElement`'s public type, so the legacy one is
|
|
8
|
+
* declared here rather than asserted away.
|
|
7
9
|
*/
|
|
8
|
-
|
|
10
|
+
type ElementWithLegacyRef = React.ReactElement & { ref?: React.Ref<unknown> };
|
|
11
|
+
|
|
12
|
+
/** Get the ref from a React element without throwing warnings. */
|
|
13
|
+
function getElementRef(element: ElementWithLegacyRef) {
|
|
9
14
|
if (!React.isValidElement(element)) return undefined;
|
|
10
15
|
|
|
11
16
|
// React <=18 in DEV
|
|
12
17
|
let getter = Object.getOwnPropertyDescriptor(element.props, "ref")?.get;
|
|
13
18
|
let mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
|
|
14
19
|
if (mayWarn) {
|
|
15
|
-
|
|
16
|
-
return (element as any).ref;
|
|
20
|
+
return element.ref;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
// React 19 in DEV
|
|
@@ -25,8 +29,7 @@ function getElementRef(element: React.ReactElement) {
|
|
|
25
29
|
|
|
26
30
|
// Not DEV
|
|
27
31
|
return (
|
|
28
|
-
|
|
29
|
-
(element.props as { ref?: React.Ref<unknown> }).ref || (element as any).ref
|
|
32
|
+
(element.props as { ref?: React.Ref<unknown> }).ref || element.ref
|
|
30
33
|
);
|
|
31
34
|
}
|
|
32
35
|
|
|
@@ -1,72 +1,27 @@
|
|
|
1
1
|
// Adapted from jalcoui (MIT) — github.com/jal-co/ui
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
height: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface LayoutLine {
|
|
32
|
-
text: string;
|
|
33
|
-
width: number;
|
|
34
|
-
start: number;
|
|
35
|
-
end: number;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface LayoutLinesResult {
|
|
39
|
-
lineCount: number;
|
|
40
|
-
height: number;
|
|
41
|
-
lines: LayoutLine[];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface WalkLineRange {
|
|
45
|
-
width: number;
|
|
46
|
-
start: number;
|
|
47
|
-
end: number;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface PretextModule {
|
|
51
|
-
prepare(text: string, font: string, options?: PrepareOptions): PreparedText;
|
|
52
|
-
prepareWithSegments(
|
|
53
|
-
text: string,
|
|
54
|
-
font: string,
|
|
55
|
-
options?: PrepareOptions,
|
|
56
|
-
): PreparedTextWithSegments;
|
|
57
|
-
layout(
|
|
58
|
-
prepared: PreparedText,
|
|
59
|
-
maxWidth: number,
|
|
60
|
-
lineHeight: number,
|
|
61
|
-
): LayoutResult;
|
|
62
|
-
layoutWithLines(
|
|
63
|
-
prepared: PreparedTextWithSegments,
|
|
64
|
-
maxWidth: number,
|
|
65
|
-
lineHeight: number,
|
|
66
|
-
): LayoutLinesResult;
|
|
67
|
-
walkLineRanges(
|
|
68
|
-
prepared: PreparedTextWithSegments,
|
|
69
|
-
maxWidth: number,
|
|
70
|
-
visit: (line: WalkLineRange) => void,
|
|
71
|
-
): void;
|
|
72
|
-
}
|
|
3
|
+
// Re-export of the @chenglou/pretext public types. A hand-written mirror used
|
|
4
|
+
// to live here to keep `pnpm check` green while the runtime was an optional
|
|
5
|
+
// dependency; it is a hard `dependencies` entry now, and the mirror had already
|
|
6
|
+
// drifted — it declared `LayoutLine.start/end` as `number` where upstream
|
|
7
|
+
// returns a `{ segmentIndex, graphemeIndex }` cursor, so every consumer of
|
|
8
|
+
// `usePretextLines` was typed against a shape the runtime never produces.
|
|
9
|
+
|
|
10
|
+
export type {
|
|
11
|
+
PrepareOptions,
|
|
12
|
+
PreparedText,
|
|
13
|
+
PreparedTextWithSegments,
|
|
14
|
+
LayoutResult,
|
|
15
|
+
LayoutLine,
|
|
16
|
+
LayoutLineRange,
|
|
17
|
+
LayoutLinesResult,
|
|
18
|
+
LayoutCursor,
|
|
19
|
+
} from '@chenglou/pretext';
|
|
20
|
+
|
|
21
|
+
import type * as pretext from '@chenglou/pretext';
|
|
22
|
+
|
|
23
|
+
/** The subset of the module surface the hooks in this folder call. */
|
|
24
|
+
export type PretextModule = Pick<
|
|
25
|
+
typeof pretext,
|
|
26
|
+
'prepare' | 'prepareWithSegments' | 'layout' | 'layoutWithLines' | 'walkLineRanges'
|
|
27
|
+
>;
|
|
@@ -6,15 +6,16 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Powered by Pretext by Cheng Lou — github.com/chenglou/pretext
|
|
8
8
|
//
|
|
9
|
-
// `@chenglou/pretext` is
|
|
10
|
-
// `
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
9
|
+
// `@chenglou/pretext` is imported statically because it is ESM-only
|
|
10
|
+
// (`"type": "module"`, and its `exports` map offers only an `import`
|
|
11
|
+
// condition). The previous lazy `require('@chenglou/pretext')` could not
|
|
12
|
+
// resolve it in any runtime: under Vite/ESM `require` is not defined at all,
|
|
13
|
+
// so the first render of `BalancedText` threw `require is not defined`
|
|
14
|
+
// rather than degrading gracefully.
|
|
15
15
|
|
|
16
16
|
'use client';
|
|
17
17
|
|
|
18
|
+
import * as pretext from '@chenglou/pretext';
|
|
18
19
|
import * as React from 'react';
|
|
19
20
|
import type {
|
|
20
21
|
PreparedText,
|
|
@@ -35,13 +36,8 @@ export type {
|
|
|
35
36
|
|
|
36
37
|
const isBrowser = typeof window !== 'undefined';
|
|
37
38
|
|
|
38
|
-
let cachedPretext: PretextModule | null = null;
|
|
39
|
-
|
|
40
39
|
function getPretext(): PretextModule {
|
|
41
|
-
|
|
42
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
43
|
-
cachedPretext = require('@chenglou/pretext') as PretextModule;
|
|
44
|
-
return cachedPretext;
|
|
40
|
+
return pretext;
|
|
45
41
|
}
|
|
46
42
|
|
|
47
43
|
const EMPTY_LAYOUT: LayoutResult = { lineCount: 0, height: 0 };
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
'use client';
|
|
9
9
|
|
|
10
|
-
import React, { Suspense, ReactNode, ComponentType, lazy } from 'react';
|
|
10
|
+
import React, { Suspense, ReactNode, ComponentType, LazyExoticComponent, lazy } from 'react';
|
|
11
11
|
|
|
12
12
|
// Default loading spinner
|
|
13
13
|
export const DefaultLoader = () => (
|
|
@@ -67,13 +67,13 @@ export function LazyWrapper({ children, fallback }: LazyWrapperProps) {
|
|
|
67
67
|
* <HeavyComponent someProps={value} />
|
|
68
68
|
* ```
|
|
69
69
|
*/
|
|
70
|
-
export function createLazyComponent<
|
|
71
|
-
importFn: () => Promise<{ default:
|
|
70
|
+
export function createLazyComponent<P extends object>(
|
|
71
|
+
importFn: () => Promise<{ default: ComponentType<P> }>,
|
|
72
72
|
fallback?: ReactNode
|
|
73
73
|
) {
|
|
74
74
|
const LazyComponent = lazy(importFn);
|
|
75
75
|
|
|
76
|
-
return function WrappedLazyComponent(props:
|
|
76
|
+
return function WrappedLazyComponent(props: P) {
|
|
77
77
|
return (
|
|
78
78
|
<Suspense fallback={fallback ?? <DefaultLoader />}>
|
|
79
79
|
<LazyComponent {...props} />
|
|
@@ -95,18 +95,18 @@ export function createLazyComponent<T extends ComponentType<any>>(
|
|
|
95
95
|
* ```
|
|
96
96
|
*/
|
|
97
97
|
export function createLazyNamedComponent<
|
|
98
|
-
|
|
99
|
-
K extends
|
|
98
|
+
P extends object,
|
|
99
|
+
K extends string = string
|
|
100
100
|
>(
|
|
101
|
-
importFn: () => Promise<
|
|
101
|
+
importFn: () => Promise<Record<K, ComponentType<P>>>,
|
|
102
102
|
exportName: K,
|
|
103
103
|
fallback?: ReactNode
|
|
104
104
|
) {
|
|
105
|
-
const LazyComponent = lazy(() =>
|
|
105
|
+
const LazyComponent: LazyExoticComponent<ComponentType<P>> = lazy(() =>
|
|
106
106
|
importFn().then((mod) => ({ default: mod[exportName] }))
|
|
107
107
|
);
|
|
108
108
|
|
|
109
|
-
return function WrappedLazyComponent(props:
|
|
109
|
+
return function WrappedLazyComponent(props: P) {
|
|
110
110
|
return (
|
|
111
111
|
<Suspense fallback={fallback ?? <DefaultLoader />}>
|
|
112
112
|
<LazyComponent {...props} />
|
|
@@ -101,6 +101,13 @@ export function useThemePalette(): ThemePalette {
|
|
|
101
101
|
const theme = useResolvedTheme();
|
|
102
102
|
|
|
103
103
|
return useMemo(() => {
|
|
104
|
+
// `theme` is read here so the dependency is real to the linter as well as to
|
|
105
|
+
// us: every getCssColorAsHex call below resolves against the live DOM, whose
|
|
106
|
+
// values change when the theme flips, but nothing in that call chain
|
|
107
|
+
// mentions `theme`. Without this read the memo looks free of it and would be
|
|
108
|
+
// cached across a theme change, returning the previous theme's colors.
|
|
109
|
+
void theme;
|
|
110
|
+
|
|
104
111
|
return {
|
|
105
112
|
// Base colors
|
|
106
113
|
background: getCssColorAsHex('background'),
|