@classytic/fluid 0.3.3 → 0.3.6
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/dist/client/hooks.d.mts +6 -2
- package/dist/client/hooks.mjs +71 -31
- package/dist/dashboard.d.mts +2 -0
- package/dist/dashboard.mjs +1 -1
- package/dist/forms.mjs +21 -6
- package/package.json +1 -1
package/dist/client/hooks.d.mts
CHANGED
|
@@ -76,14 +76,18 @@ declare function useCopyToClipboard(resetDelay?: number): UseCopyToClipboardRetu
|
|
|
76
76
|
/**
|
|
77
77
|
* useLocalStorage — Persist state in localStorage with type safety and optional expiry.
|
|
78
78
|
*
|
|
79
|
-
*
|
|
79
|
+
* Uses `useSyncExternalStore` (React 18+) so the component always reflects
|
|
80
|
+
* the current localStorage value — even when the key changes dynamically,
|
|
81
|
+
* the component re-mounts, or another tab writes to the same key.
|
|
82
|
+
*
|
|
83
|
+
* @param key - The localStorage key (can change dynamically)
|
|
80
84
|
* @param initialValue - Default value if no stored value exists
|
|
81
85
|
* @param ttl - Time to live in milliseconds (optional)
|
|
82
86
|
*
|
|
83
87
|
* @example
|
|
84
88
|
* ```tsx
|
|
85
89
|
* const [theme, setTheme] = useLocalStorage("theme", "light");
|
|
86
|
-
* const [cache, setCache] = useLocalStorage("api-cache", {}, 60000); // 1 min TTL
|
|
90
|
+
* const [cache, setCache, clearCache] = useLocalStorage("api-cache", {}, 60000); // 1 min TTL
|
|
87
91
|
* ```
|
|
88
92
|
*/
|
|
89
93
|
declare function useLocalStorage<T>(key: string, initialValue: T, ttl?: number): [T, (value: T | ((prev: T) => T)) => void, () => void];
|
package/dist/client/hooks.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { t as useMediaQuery } from "../use-media-query-BnVNIKT4.mjs";
|
|
|
6
6
|
import { t as useScrollDetection } from "../use-scroll-detection-CsgsQYvy.mjs";
|
|
7
7
|
import { n as useDebouncedCallback, t as useDebounce } from "../use-debounce-xmZucz5e.mjs";
|
|
8
8
|
import { t as useKeyboardShortcut } from "../use-keyboard-shortcut-Bl6YM5Q7.mjs";
|
|
9
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
9
|
+
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
10
10
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
11
11
|
|
|
12
12
|
//#region src/hooks/use-base-search.ts
|
|
@@ -391,53 +391,93 @@ const storage = {
|
|
|
391
391
|
|
|
392
392
|
//#endregion
|
|
393
393
|
//#region src/hooks/use-local-storage.ts
|
|
394
|
+
function parseSnapshot(rawValue, initialValue) {
|
|
395
|
+
try {
|
|
396
|
+
const parsed = JSON.parse(rawValue);
|
|
397
|
+
if (parsed && typeof parsed === "object" && "__expiresAt" in parsed && typeof parsed.__expiresAt === "number") {
|
|
398
|
+
if (Date.now() > parsed.__expiresAt) return {
|
|
399
|
+
value: initialValue,
|
|
400
|
+
expired: true
|
|
401
|
+
};
|
|
402
|
+
return {
|
|
403
|
+
value: parsed.value ?? initialValue,
|
|
404
|
+
expired: false
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
value: parsed ?? initialValue,
|
|
409
|
+
expired: false
|
|
410
|
+
};
|
|
411
|
+
} catch {
|
|
412
|
+
return {
|
|
413
|
+
value: initialValue,
|
|
414
|
+
expired: false
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
}
|
|
394
418
|
/**
|
|
395
419
|
* useLocalStorage — Persist state in localStorage with type safety and optional expiry.
|
|
396
420
|
*
|
|
397
|
-
*
|
|
421
|
+
* Uses `useSyncExternalStore` (React 18+) so the component always reflects
|
|
422
|
+
* the current localStorage value — even when the key changes dynamically,
|
|
423
|
+
* the component re-mounts, or another tab writes to the same key.
|
|
424
|
+
*
|
|
425
|
+
* @param key - The localStorage key (can change dynamically)
|
|
398
426
|
* @param initialValue - Default value if no stored value exists
|
|
399
427
|
* @param ttl - Time to live in milliseconds (optional)
|
|
400
428
|
*
|
|
401
429
|
* @example
|
|
402
430
|
* ```tsx
|
|
403
431
|
* const [theme, setTheme] = useLocalStorage("theme", "light");
|
|
404
|
-
* const [cache, setCache] = useLocalStorage("api-cache", {}, 60000); // 1 min TTL
|
|
432
|
+
* const [cache, setCache, clearCache] = useLocalStorage("api-cache", {}, 60000); // 1 min TTL
|
|
405
433
|
* ```
|
|
406
434
|
*/
|
|
407
435
|
function useLocalStorage(key, initialValue, ttl) {
|
|
408
|
-
const [storedValue, setStoredValue] = useState(() => {
|
|
409
|
-
const item = storage.get(key, initialValue);
|
|
410
|
-
return item !== null ? item : initialValue;
|
|
411
|
-
});
|
|
412
|
-
const keyRef = useRef(key);
|
|
413
436
|
const ttlRef = useRef(ttl);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
});
|
|
424
|
-
}, []);
|
|
425
|
-
const removeValue = useCallback(() => {
|
|
426
|
-
storage.remove(keyRef.current);
|
|
427
|
-
setStoredValue(initialValue);
|
|
428
|
-
}, [initialValue]);
|
|
429
|
-
useEffect(() => {
|
|
437
|
+
ttlRef.current = ttl;
|
|
438
|
+
const initialValueRef = useRef(initialValue);
|
|
439
|
+
const serializedInitialRef = useRef(() => JSON.stringify(initialValue));
|
|
440
|
+
const getSnapshot = useCallback(() => {
|
|
441
|
+
if (typeof window === "undefined") return serializedInitialRef.current();
|
|
442
|
+
return window.localStorage.getItem(key) ?? serializedInitialRef.current();
|
|
443
|
+
}, [key]);
|
|
444
|
+
const getServerSnapshot = useCallback(() => serializedInitialRef.current(), []);
|
|
445
|
+
const rawValue = useSyncExternalStore(useCallback((onStoreChange) => {
|
|
430
446
|
const handleStorage = (e) => {
|
|
431
|
-
if (e.key ===
|
|
432
|
-
const item = storage.get(keyRef.current, initialValue);
|
|
433
|
-
setStoredValue(item !== null ? item : initialValue);
|
|
434
|
-
}
|
|
447
|
+
if (e.key === key || e.key === null) onStoreChange();
|
|
435
448
|
};
|
|
436
449
|
window.addEventListener("storage", handleStorage);
|
|
437
|
-
|
|
438
|
-
|
|
450
|
+
const handleLocal = (e) => {
|
|
451
|
+
if (e.detail?.key === key) onStoreChange();
|
|
452
|
+
};
|
|
453
|
+
window.addEventListener("local-storage-change", handleLocal);
|
|
454
|
+
return () => {
|
|
455
|
+
window.removeEventListener("storage", handleStorage);
|
|
456
|
+
window.removeEventListener("local-storage-change", handleLocal);
|
|
457
|
+
};
|
|
458
|
+
}, [key]), getSnapshot, getServerSnapshot);
|
|
459
|
+
const parsedSnapshot = useMemo(() => parseSnapshot(rawValue, initialValueRef.current), [rawValue]);
|
|
460
|
+
useEffect(() => {
|
|
461
|
+
if (!parsedSnapshot.expired || typeof window === "undefined") return;
|
|
462
|
+
storage.remove(key);
|
|
463
|
+
window.dispatchEvent(new CustomEvent("local-storage-change", { detail: { key } }));
|
|
464
|
+
}, [key, parsedSnapshot.expired]);
|
|
465
|
+
const notifyChange = useCallback(() => {
|
|
466
|
+
window.dispatchEvent(new CustomEvent("local-storage-change", { detail: { key } }));
|
|
467
|
+
}, [key]);
|
|
468
|
+
const setValue = useCallback((updater) => {
|
|
469
|
+
const init = initialValueRef.current;
|
|
470
|
+
const current = parseSnapshot(typeof window === "undefined" ? JSON.stringify(init) : window.localStorage.getItem(key) ?? JSON.stringify(init), init).value;
|
|
471
|
+
const nextValue = updater instanceof Function ? updater(current) : updater;
|
|
472
|
+
storage.set(key, nextValue, ttlRef.current);
|
|
473
|
+
notifyChange();
|
|
474
|
+
}, [key, notifyChange]);
|
|
475
|
+
const removeValue = useCallback(() => {
|
|
476
|
+
storage.remove(key);
|
|
477
|
+
notifyChange();
|
|
478
|
+
}, [key, notifyChange]);
|
|
439
479
|
return [
|
|
440
|
-
|
|
480
|
+
parsedSnapshot.value,
|
|
441
481
|
setValue,
|
|
442
482
|
removeValue
|
|
443
483
|
];
|
package/dist/dashboard.d.mts
CHANGED
|
@@ -49,6 +49,8 @@ interface NavItem extends NavPermissions {
|
|
|
49
49
|
url: string;
|
|
50
50
|
icon?: LucideIcon;
|
|
51
51
|
isActive?: boolean;
|
|
52
|
+
/** When true, the collapsible sub-menu is expanded by default (even when not active). */
|
|
53
|
+
defaultOpen?: boolean;
|
|
52
54
|
badge?: string | number;
|
|
53
55
|
items?: NavSubItem[];
|
|
54
56
|
}
|
package/dist/dashboard.mjs
CHANGED
|
@@ -379,7 +379,7 @@ function SidebarBrand({ title, icon, href = "/", className, tooltip }) {
|
|
|
379
379
|
function SidebarNavItem({ item, onClick }) {
|
|
380
380
|
const hasSubItems = item.items && item.items.length > 0;
|
|
381
381
|
const Icon = item.icon;
|
|
382
|
-
const [open, setOpen] = useState(item.isActive ?? false);
|
|
382
|
+
const [open, setOpen] = useState(item.defaultOpen ?? item.isActive ?? false);
|
|
383
383
|
if (hasSubItems) return /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsxs(Collapsible, {
|
|
384
384
|
open,
|
|
385
385
|
onOpenChange: setOpen,
|
package/dist/forms.mjs
CHANGED
|
@@ -372,6 +372,7 @@ function SelectInput({ control, items = [], groups = [], name, label, placeholde
|
|
|
372
372
|
const rawValue = field ? field.value?.toString() : localValue;
|
|
373
373
|
const handleChange = (newValue) => {
|
|
374
374
|
const actualValue = newValue === CLEAR_VALUE ? "" : newValue;
|
|
375
|
+
if ((field ? String(field.value ?? "") : localValue) === actualValue) return;
|
|
375
376
|
if (field) field.onChange(actualValue);
|
|
376
377
|
else setLocalValue(actualValue);
|
|
377
378
|
onValueChange?.(actualValue);
|
|
@@ -1475,18 +1476,32 @@ TagChoiceInput.displayName = "TagChoiceInput";
|
|
|
1475
1476
|
*/
|
|
1476
1477
|
function ComboboxInput({ control, name, label, placeholder = "Select...", emptyText = "No items found.", description, helperText, required, disabled, items = [], className, labelClassName, inputClassName, onValueChange, renderOption, value: propValue, onChange: propOnChange }) {
|
|
1477
1478
|
const descriptionText = description || helperText;
|
|
1478
|
-
const
|
|
1479
|
+
const resolvedMapRef = useRef(/* @__PURE__ */ new Map());
|
|
1480
|
+
const currentValueRef = useRef("");
|
|
1481
|
+
useEffect(() => {
|
|
1482
|
+
resolvedMapRef.current = new Map(items.map((item) => [item.value, item]));
|
|
1483
|
+
}, [items]);
|
|
1484
|
+
const onValueChangeRef = useRef(onValueChange);
|
|
1485
|
+
onValueChangeRef.current = onValueChange;
|
|
1486
|
+
const propOnChangeRef = useRef(propOnChange);
|
|
1487
|
+
propOnChangeRef.current = propOnChange;
|
|
1488
|
+
const fieldRef = useRef();
|
|
1489
|
+
const handleValueChange = useCallback((newItem) => {
|
|
1479
1490
|
const safeValue = newItem?.value || "";
|
|
1491
|
+
if (safeValue === currentValueRef.current) return;
|
|
1492
|
+
const field = fieldRef.current;
|
|
1480
1493
|
if (field) field.onChange(safeValue);
|
|
1481
|
-
else if (
|
|
1482
|
-
|
|
1483
|
-
}, [
|
|
1494
|
+
else if (propOnChangeRef.current) propOnChangeRef.current(safeValue);
|
|
1495
|
+
onValueChangeRef.current?.(safeValue);
|
|
1496
|
+
}, []);
|
|
1484
1497
|
const renderCombobox = (currentValue, field, isDisabled, fieldState) => {
|
|
1498
|
+
fieldRef.current = field;
|
|
1499
|
+
currentValueRef.current = currentValue ?? "";
|
|
1485
1500
|
const ariaDescribedBy = [descriptionText ? `${name}-description` : void 0, fieldState?.invalid ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0;
|
|
1486
1501
|
return /* @__PURE__ */ jsxs(Combobox, {
|
|
1487
1502
|
items,
|
|
1488
|
-
value: currentValue ? items.find((item) => item.value === currentValue) ?? null : null,
|
|
1489
|
-
onValueChange:
|
|
1503
|
+
value: currentValue ? resolvedMapRef.current.get(currentValue) ?? items.find((item) => item.value === currentValue) ?? null : null,
|
|
1504
|
+
onValueChange: handleValueChange,
|
|
1490
1505
|
disabled: isDisabled,
|
|
1491
1506
|
children: [/* @__PURE__ */ jsx(ComboboxInput$1, {
|
|
1492
1507
|
placeholder,
|