@djangocfg/ui-core 2.1.480 → 2.1.481
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-core",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.481",
|
|
4
4
|
"description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-components",
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
"check:contrast": "node scripts/check-preset-contrast.mjs"
|
|
129
129
|
},
|
|
130
130
|
"peerDependencies": {
|
|
131
|
-
"@djangocfg/i18n": "^2.1.
|
|
131
|
+
"@djangocfg/i18n": "^2.1.481",
|
|
132
132
|
"consola": "^3.4.2",
|
|
133
133
|
"lucide-react": "^0.545.0",
|
|
134
134
|
"moment": "^2.30.1",
|
|
@@ -206,8 +206,8 @@
|
|
|
206
206
|
"@chenglou/pretext": "^0.0.8"
|
|
207
207
|
},
|
|
208
208
|
"devDependencies": {
|
|
209
|
-
"@djangocfg/i18n": "^2.1.
|
|
210
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
209
|
+
"@djangocfg/i18n": "^2.1.481",
|
|
210
|
+
"@djangocfg/typescript-config": "^2.1.481",
|
|
211
211
|
"@types/node": "^24.13.3",
|
|
212
212
|
"@types/react": "19.2.15",
|
|
213
213
|
"@types/react-dom": "19.2.3",
|
|
@@ -177,7 +177,11 @@ const TagsInput = React.forwardRef<HTMLDivElement, TagsInputRootProps>(
|
|
|
177
177
|
const collectionRef = React.useRef<HTMLDivElement>(null);
|
|
178
178
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
// useId must be called unconditionally — `idProp ?? React.useId()` skips the
|
|
181
|
+
// hook whenever a caller passes `id`, so the hook count differs between
|
|
182
|
+
// renders/instances and React crashes (#310). Call it, then prefer idProp.
|
|
183
|
+
const generatedId = React.useId();
|
|
184
|
+
const id = idProp ?? generatedId;
|
|
181
185
|
const inputId = React.useId();
|
|
182
186
|
const labelId = React.useId();
|
|
183
187
|
|
|
@@ -195,6 +195,56 @@ export function CountrySelect({
|
|
|
195
195
|
onChange?.(value.filter((v) => v !== code))
|
|
196
196
|
}, [value, onChange])
|
|
197
197
|
|
|
198
|
+
// Used only by the dropdown variant below, but MUST be declared before the
|
|
199
|
+
// `inline` early return — a hook after a conditional return changes the hook
|
|
200
|
+
// count when `variant` differs between renders and crashes React (#310).
|
|
201
|
+
const displayValue = React.useMemo(() => {
|
|
202
|
+
if (selectedCountries.length === 0) {
|
|
203
|
+
return <span className="text-muted-foreground">{resolvedPlaceholder}</span>
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!multiple && selectedCountries.length === 1) {
|
|
207
|
+
const country = selectedCountries[0]!;
|
|
208
|
+
return (
|
|
209
|
+
<div className="flex items-center gap-2">
|
|
210
|
+
<Flag countryCode={country.code} rounded className="h-3 w-4 shrink-0" />
|
|
211
|
+
<span>{country.name}</span>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const displayed = selectedCountries.slice(0, maxDisplay)
|
|
217
|
+
const remaining = selectedCountries.length - maxDisplay
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<div className="flex flex-wrap gap-1">
|
|
221
|
+
{displayed.map((country) => (
|
|
222
|
+
<Badge
|
|
223
|
+
key={country.code}
|
|
224
|
+
variant="secondary"
|
|
225
|
+
className="mr-1 text-xs"
|
|
226
|
+
>
|
|
227
|
+
<Flag countryCode={country.code} rounded className="mr-1 h-3 w-4 shrink-0" />
|
|
228
|
+
{country.name}
|
|
229
|
+
<button
|
|
230
|
+
className="ml-1 rounded-full hover:bg-muted-foreground/20"
|
|
231
|
+
onClick={(e) => handleRemove(country.code, e)}
|
|
232
|
+
disabled={disabled}
|
|
233
|
+
aria-label={`Remove ${country.name}`}
|
|
234
|
+
>
|
|
235
|
+
<X className="h-3 w-3" />
|
|
236
|
+
</button>
|
|
237
|
+
</Badge>
|
|
238
|
+
))}
|
|
239
|
+
{remaining > 0 && (
|
|
240
|
+
<Badge variant="outline" className="text-xs">
|
|
241
|
+
{moreItemsLabel(remaining)}
|
|
242
|
+
</Badge>
|
|
243
|
+
)}
|
|
244
|
+
</div>
|
|
245
|
+
)
|
|
246
|
+
}, [selectedCountries, maxDisplay, resolvedPlaceholder, disabled, multiple, handleRemove, moreItemsLabel])
|
|
247
|
+
|
|
198
248
|
// Inline variant
|
|
199
249
|
if (variant === 'inline') {
|
|
200
250
|
return (
|
|
@@ -276,54 +326,7 @@ export function CountrySelect({
|
|
|
276
326
|
);
|
|
277
327
|
}
|
|
278
328
|
|
|
279
|
-
// Dropdown variant
|
|
280
|
-
const displayValue = React.useMemo(() => {
|
|
281
|
-
if (selectedCountries.length === 0) {
|
|
282
|
-
return <span className="text-muted-foreground">{resolvedPlaceholder}</span>
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (!multiple && selectedCountries.length === 1) {
|
|
286
|
-
const country = selectedCountries[0]!;
|
|
287
|
-
return (
|
|
288
|
-
<div className="flex items-center gap-2">
|
|
289
|
-
<Flag countryCode={country.code} rounded className="h-3 w-4 shrink-0" />
|
|
290
|
-
<span>{country.name}</span>
|
|
291
|
-
</div>
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const displayed = selectedCountries.slice(0, maxDisplay)
|
|
296
|
-
const remaining = selectedCountries.length - maxDisplay
|
|
297
|
-
|
|
298
|
-
return (
|
|
299
|
-
<div className="flex flex-wrap gap-1">
|
|
300
|
-
{displayed.map((country) => (
|
|
301
|
-
<Badge
|
|
302
|
-
key={country.code}
|
|
303
|
-
variant="secondary"
|
|
304
|
-
className="mr-1 text-xs"
|
|
305
|
-
>
|
|
306
|
-
<Flag countryCode={country.code} rounded className="mr-1 h-3 w-4 shrink-0" />
|
|
307
|
-
{country.name}
|
|
308
|
-
<button
|
|
309
|
-
className="ml-1 rounded-full hover:bg-muted-foreground/20"
|
|
310
|
-
onClick={(e) => handleRemove(country.code, e)}
|
|
311
|
-
disabled={disabled}
|
|
312
|
-
aria-label={`Remove ${country.name}`}
|
|
313
|
-
>
|
|
314
|
-
<X className="h-3 w-3" />
|
|
315
|
-
</button>
|
|
316
|
-
</Badge>
|
|
317
|
-
))}
|
|
318
|
-
{remaining > 0 && (
|
|
319
|
-
<Badge variant="outline" className="text-xs">
|
|
320
|
-
{moreItemsLabel(remaining)}
|
|
321
|
-
</Badge>
|
|
322
|
-
)}
|
|
323
|
-
</div>
|
|
324
|
-
)
|
|
325
|
-
}, [selectedCountries, maxDisplay, resolvedPlaceholder, disabled, multiple, handleRemove, moreItemsLabel])
|
|
326
|
-
|
|
329
|
+
// Dropdown variant (displayValue is memoized above, before the inline guard)
|
|
327
330
|
return (
|
|
328
331
|
<Popover
|
|
329
332
|
open={open}
|
|
@@ -209,6 +209,58 @@ export function LanguageSelect({
|
|
|
209
209
|
onChange?.(value.filter((v) => v !== code))
|
|
210
210
|
}, [value, onChange])
|
|
211
211
|
|
|
212
|
+
// Used only by the dropdown variant below, but MUST be declared before the
|
|
213
|
+
// `inline` early return — a hook after a conditional return changes the hook
|
|
214
|
+
// count when `variant` differs between renders and crashes React (#310).
|
|
215
|
+
const displayValue = React.useMemo(() => {
|
|
216
|
+
if (selectedLanguages.length === 0) {
|
|
217
|
+
return <span className="text-muted-foreground">{resolvedPlaceholder}</span>
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!multiple && selectedLanguages.length === 1) {
|
|
221
|
+
const language = selectedLanguages[0]!;
|
|
222
|
+
return (
|
|
223
|
+
<div className="flex items-center gap-2">
|
|
224
|
+
{showFlag && <LanguageFlag code={language.code} className="h-3 w-4" rounded />}
|
|
225
|
+
{showCode && <span className="text-xs text-muted-foreground uppercase">{language.code}</span>}
|
|
226
|
+
<span>{language.name}</span>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const displayed = selectedLanguages.slice(0, maxDisplay)
|
|
232
|
+
const remaining = selectedLanguages.length - maxDisplay
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<div className="flex flex-wrap gap-1">
|
|
236
|
+
{displayed.map((language) => (
|
|
237
|
+
<Badge
|
|
238
|
+
key={language.code}
|
|
239
|
+
variant="secondary"
|
|
240
|
+
className="mr-1 text-xs"
|
|
241
|
+
>
|
|
242
|
+
{showFlag && <LanguageFlag code={language.code} className="mr-1 h-3 w-4" rounded />}
|
|
243
|
+
{showCode && <span className="mr-1 text-muted-foreground uppercase">{language.code}</span>}
|
|
244
|
+
{language.name}
|
|
245
|
+
<button
|
|
246
|
+
className="ml-1 rounded-full hover:bg-muted-foreground/20"
|
|
247
|
+
onClick={(e) => handleRemove(language.code, e)}
|
|
248
|
+
disabled={disabled}
|
|
249
|
+
aria-label={`Remove ${language.name}`}
|
|
250
|
+
>
|
|
251
|
+
<X className="h-3 w-3" />
|
|
252
|
+
</button>
|
|
253
|
+
</Badge>
|
|
254
|
+
))}
|
|
255
|
+
{remaining > 0 && (
|
|
256
|
+
<Badge variant="outline" className="text-xs">
|
|
257
|
+
{moreItemsLabel(remaining)}
|
|
258
|
+
</Badge>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
)
|
|
262
|
+
}, [selectedLanguages, maxDisplay, resolvedPlaceholder, disabled, multiple, handleRemove, moreItemsLabel, showFlag, showCode])
|
|
263
|
+
|
|
212
264
|
// Inline variant
|
|
213
265
|
if (variant === 'inline') {
|
|
214
266
|
return (
|
|
@@ -307,56 +359,7 @@ export function LanguageSelect({
|
|
|
307
359
|
);
|
|
308
360
|
}
|
|
309
361
|
|
|
310
|
-
// Dropdown variant
|
|
311
|
-
const displayValue = React.useMemo(() => {
|
|
312
|
-
if (selectedLanguages.length === 0) {
|
|
313
|
-
return <span className="text-muted-foreground">{resolvedPlaceholder}</span>
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (!multiple && selectedLanguages.length === 1) {
|
|
317
|
-
const language = selectedLanguages[0]!;
|
|
318
|
-
return (
|
|
319
|
-
<div className="flex items-center gap-2">
|
|
320
|
-
{showFlag && <LanguageFlag code={language.code} className="h-3 w-4" rounded />}
|
|
321
|
-
{showCode && <span className="text-xs text-muted-foreground uppercase">{language.code}</span>}
|
|
322
|
-
<span>{language.name}</span>
|
|
323
|
-
</div>
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const displayed = selectedLanguages.slice(0, maxDisplay)
|
|
328
|
-
const remaining = selectedLanguages.length - maxDisplay
|
|
329
|
-
|
|
330
|
-
return (
|
|
331
|
-
<div className="flex flex-wrap gap-1">
|
|
332
|
-
{displayed.map((language) => (
|
|
333
|
-
<Badge
|
|
334
|
-
key={language.code}
|
|
335
|
-
variant="secondary"
|
|
336
|
-
className="mr-1 text-xs"
|
|
337
|
-
>
|
|
338
|
-
{showFlag && <LanguageFlag code={language.code} className="mr-1 h-3 w-4" rounded />}
|
|
339
|
-
{showCode && <span className="mr-1 text-muted-foreground uppercase">{language.code}</span>}
|
|
340
|
-
{language.name}
|
|
341
|
-
<button
|
|
342
|
-
className="ml-1 rounded-full hover:bg-muted-foreground/20"
|
|
343
|
-
onClick={(e) => handleRemove(language.code, e)}
|
|
344
|
-
disabled={disabled}
|
|
345
|
-
aria-label={`Remove ${language.name}`}
|
|
346
|
-
>
|
|
347
|
-
<X className="h-3 w-3" />
|
|
348
|
-
</button>
|
|
349
|
-
</Badge>
|
|
350
|
-
))}
|
|
351
|
-
{remaining > 0 && (
|
|
352
|
-
<Badge variant="outline" className="text-xs">
|
|
353
|
-
{moreItemsLabel(remaining)}
|
|
354
|
-
</Badge>
|
|
355
|
-
)}
|
|
356
|
-
</div>
|
|
357
|
-
)
|
|
358
|
-
}, [selectedLanguages, maxDisplay, resolvedPlaceholder, disabled, multiple, handleRemove, moreItemsLabel, showFlag, showCode])
|
|
359
|
-
|
|
362
|
+
// Dropdown variant (displayValue is memoized above, before the inline guard)
|
|
360
363
|
return (
|
|
361
364
|
<Popover
|
|
362
365
|
open={open}
|
|
@@ -5,33 +5,41 @@ import { useDebugValue } from 'react';
|
|
|
5
5
|
type DebugValue = Record<string, unknown> | unknown[] | null | undefined;
|
|
6
6
|
|
|
7
7
|
export function useDebugTools(values: DebugValue, prefix = '') {
|
|
8
|
+
// useDebugValue must be called EXACTLY ONCE and UNCONDITIONALLY. The prior
|
|
9
|
+
// version called it inside branches and per-array-element/object-key loops,
|
|
10
|
+
// so the hook count varied with the shape of `values` between renders —
|
|
11
|
+
// a rules-of-hooks violation (React #310 territory). Instead, call it once
|
|
12
|
+
// with a lazy formatter that builds the full multi-line label; the formatter
|
|
13
|
+
// still only runs when React DevTools inspects the hook, so nothing is
|
|
14
|
+
// eagerly serialized in normal renders.
|
|
15
|
+
useDebugValue(values, (v) => formatDebugValues(v, prefix));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatDebugValues(values: DebugValue, prefix: string): string {
|
|
8
19
|
if (values === null || values === undefined) {
|
|
9
|
-
|
|
10
|
-
return;
|
|
20
|
+
return `${prefix}: ${values === null ? 'null' : 'undefined'}`;
|
|
11
21
|
}
|
|
12
22
|
|
|
13
23
|
if (Array.isArray(values)) {
|
|
14
|
-
values
|
|
15
|
-
|
|
24
|
+
return values
|
|
25
|
+
.map((value, index) => {
|
|
16
26
|
const label = prefix ? `${prefix}[${index}]` : `[${index}]`;
|
|
17
|
-
return `${label}: ${formatValue(
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
return;
|
|
27
|
+
return `${label}: ${formatValue(value)}`;
|
|
28
|
+
})
|
|
29
|
+
.join('\n');
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
if (typeof values === 'object') {
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
return Object.entries(values)
|
|
34
|
+
.map(([key, value]) => {
|
|
26
35
|
const label = prefix ? `${prefix}.${key}` : key;
|
|
27
|
-
return `${label}: ${formatValue(
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
return;
|
|
36
|
+
return `${label}: ${formatValue(value)}`;
|
|
37
|
+
})
|
|
38
|
+
.join('\n');
|
|
31
39
|
}
|
|
32
40
|
|
|
33
|
-
//
|
|
34
|
-
|
|
41
|
+
// Primitive
|
|
42
|
+
return `${prefix}: ${formatValue(values)}`;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
function formatValue(value: unknown): string {
|