@classytic/fluid 0.3.4 → 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.
@@ -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, useSyncExternalStore } 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,6 +391,30 @@ 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
  *
@@ -411,12 +435,14 @@ const storage = {
411
435
  function useLocalStorage(key, initialValue, ttl) {
412
436
  const ttlRef = useRef(ttl);
413
437
  ttlRef.current = ttl;
438
+ const initialValueRef = useRef(initialValue);
439
+ const serializedInitialRef = useRef(() => JSON.stringify(initialValue));
414
440
  const getSnapshot = useCallback(() => {
415
- if (typeof window === "undefined") return JSON.stringify(initialValue);
416
- return window.localStorage.getItem(key) ?? JSON.stringify(initialValue);
417
- }, [key, initialValue]);
418
- const getServerSnapshot = useCallback(() => JSON.stringify(initialValue), [initialValue]);
419
- useSyncExternalStore(useCallback((onStoreChange) => {
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) => {
420
446
  const handleStorage = (e) => {
421
447
  if (e.key === key || e.key === null) onStoreChange();
422
448
  };
@@ -430,30 +456,30 @@ function useLocalStorage(key, initialValue, ttl) {
430
456
  window.removeEventListener("local-storage-change", handleLocal);
431
457
  };
432
458
  }, [key]), getSnapshot, getServerSnapshot);
433
- const value = (() => {
434
- const parsed = storage.get(key, initialValue);
435
- return parsed !== null ? parsed : initialValue;
436
- })();
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]);
437
465
  const notifyChange = useCallback(() => {
438
466
  window.dispatchEvent(new CustomEvent("local-storage-change", { detail: { key } }));
439
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]);
440
479
  return [
441
- value,
442
- useCallback((updater) => {
443
- const currentValue = storage.get(key, initialValue);
444
- const current = currentValue !== null ? currentValue : initialValue;
445
- const nextValue = updater instanceof Function ? updater(current) : updater;
446
- storage.set(key, nextValue, ttlRef.current);
447
- notifyChange();
448
- }, [
449
- key,
450
- initialValue,
451
- notifyChange
452
- ]),
453
- useCallback(() => {
454
- storage.remove(key);
455
- notifyChange();
456
- }, [key, notifyChange])
480
+ parsedSnapshot.value,
481
+ setValue,
482
+ removeValue
457
483
  ];
458
484
  }
459
485
 
@@ -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
  }
@@ -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 handleValueChange = useCallback((newItem, field) => {
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 (propOnChange) propOnChange(safeValue);
1482
- onValueChange?.(safeValue);
1483
- }, [propOnChange, onValueChange]);
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: (item) => handleValueChange(item, field),
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/fluid",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "type": "module",
5
5
  "description": "Fluid UI - Custom components built on shadcn/ui and base ui by Classytic",
6
6
  "main": "./dist/index.mjs",