@lerianstudio/sindarian-ui 1.2.0-beta.14 → 1.2.0-beta.15

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.
Files changed (71) hide show
  1. package/dist/components/form/radio-group-field/index.d.ts +1 -1
  2. package/dist/components/form/radio-group-field/index.d.ts.map +1 -1
  3. package/dist/components/form/radio-group-field/index.js +8 -1
  4. package/dist/components/ui/alert-dialog/index.d.ts.map +1 -1
  5. package/dist/components/ui/alert-dialog/index.js +1 -1
  6. package/dist/components/ui/copy-field/index.d.ts.map +1 -1
  7. package/dist/components/ui/copy-field/index.js +6 -1
  8. package/dist/components/ui/file-upload/index.d.ts.map +1 -1
  9. package/dist/components/ui/file-upload/index.js +8 -1
  10. package/dist/domain/delinquency-aging/aging-buckets.d.ts +22 -2
  11. package/dist/domain/delinquency-aging/aging-buckets.d.ts.map +1 -1
  12. package/dist/domain/delinquency-aging/aging-buckets.js +17 -4
  13. package/dist/domain/delinquency-aging/index.d.ts +17 -2
  14. package/dist/domain/delinquency-aging/index.d.ts.map +1 -1
  15. package/dist/domain/delinquency-aging/index.js +4 -2
  16. package/dist/domain/index.d.ts +2 -1
  17. package/dist/domain/index.d.ts.map +1 -1
  18. package/dist/domain/money-diff.d.ts +9 -4
  19. package/dist/domain/money-diff.d.ts.map +1 -1
  20. package/dist/domain/section-label/index.d.ts +3 -3
  21. package/dist/domain/section-label/index.d.ts.map +1 -1
  22. package/dist/domain/section-label/index.js +6 -2
  23. package/dist/domain/status-rail/index.d.ts.map +1 -1
  24. package/dist/domain/status-rail/index.js +9 -1
  25. package/dist/domain/threshold-gauge/index.d.ts.map +1 -1
  26. package/dist/domain/threshold-gauge/index.js +1 -1
  27. package/dist/enterprise/date-range-picker/index.d.ts.map +1 -1
  28. package/dist/enterprise/date-range-picker/index.js +28 -10
  29. package/dist/enterprise/search-input/index.d.ts.map +1 -1
  30. package/dist/enterprise/search-input/index.js +13 -6
  31. package/dist/enterprise/use-is-mobile.d.ts +13 -4
  32. package/dist/enterprise/use-is-mobile.d.ts.map +1 -1
  33. package/dist/enterprise/use-is-mobile.js +15 -6
  34. package/dist/esm/components/form/radio-group-field/index.d.ts +1 -1
  35. package/dist/esm/components/form/radio-group-field/index.d.ts.map +1 -1
  36. package/dist/esm/components/form/radio-group-field/index.js +8 -1
  37. package/dist/esm/components/ui/alert-dialog/index.d.ts.map +1 -1
  38. package/dist/esm/components/ui/alert-dialog/index.js +1 -1
  39. package/dist/esm/components/ui/copy-field/index.d.ts.map +1 -1
  40. package/dist/esm/components/ui/copy-field/index.js +6 -1
  41. package/dist/esm/components/ui/file-upload/index.d.ts.map +1 -1
  42. package/dist/esm/components/ui/file-upload/index.js +8 -1
  43. package/dist/esm/domain/delinquency-aging/aging-buckets.d.ts +22 -2
  44. package/dist/esm/domain/delinquency-aging/aging-buckets.d.ts.map +1 -1
  45. package/dist/esm/domain/delinquency-aging/aging-buckets.js +17 -4
  46. package/dist/esm/domain/delinquency-aging/index.d.ts +17 -2
  47. package/dist/esm/domain/delinquency-aging/index.d.ts.map +1 -1
  48. package/dist/esm/domain/delinquency-aging/index.js +4 -2
  49. package/dist/esm/domain/index.d.ts +2 -1
  50. package/dist/esm/domain/index.d.ts.map +1 -1
  51. package/dist/esm/domain/money-diff.d.ts +9 -4
  52. package/dist/esm/domain/money-diff.d.ts.map +1 -1
  53. package/dist/esm/domain/section-label/index.d.ts +3 -3
  54. package/dist/esm/domain/section-label/index.d.ts.map +1 -1
  55. package/dist/esm/domain/section-label/index.js +6 -2
  56. package/dist/esm/domain/status-rail/index.d.ts.map +1 -1
  57. package/dist/esm/domain/status-rail/index.js +9 -1
  58. package/dist/esm/domain/threshold-gauge/index.d.ts.map +1 -1
  59. package/dist/esm/domain/threshold-gauge/index.js +1 -1
  60. package/dist/esm/enterprise/date-range-picker/index.d.ts.map +1 -1
  61. package/dist/esm/enterprise/date-range-picker/index.js +29 -11
  62. package/dist/esm/enterprise/search-input/index.d.ts.map +1 -1
  63. package/dist/esm/enterprise/search-input/index.js +13 -6
  64. package/dist/esm/enterprise/use-is-mobile.d.ts +13 -4
  65. package/dist/esm/enterprise/use-is-mobile.d.ts.map +1 -1
  66. package/dist/esm/enterprise/use-is-mobile.js +13 -5
  67. package/dist/esm/theme/theme-provider.d.ts.map +1 -1
  68. package/dist/esm/theme/theme-provider.js +15 -1
  69. package/dist/theme/theme-provider.d.ts.map +1 -1
  70. package/dist/theme/theme-provider.js +15 -1
  71. package/package.json +1 -1
@@ -1,14 +1,24 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  /**
4
- * DateRangePicker — a single popover range control.
4
+ * DateRangePicker — a two-segment popover range control.
5
5
  *
6
- * Two labelled mono trigger segments (from / to) share one two-month
7
- * react-day-picker range calendar. The external contract is plain YYYY-MM-DD
8
- * strings (empty string = unset) so state shapes that speak `date_from`/
9
- * `date_to` stay untouched. The picker guarantees from <= to (react-day-picker
10
- * reorders inverted picks); aria-invalid/aria-describedby pass-throughs keep
11
- * external validation wiring alive. i18n is via plain string/locale props.
6
+ * Two labelled mono trigger segments (from / to) each open the SAME two-month
7
+ * react-day-picker range calendar over the same range selection. The external
8
+ * contract is plain YYYY-MM-DD strings (empty string = unset) so state shapes
9
+ * that speak `date_from`/`date_to` stay untouched. The picker guarantees
10
+ * from <= to (react-day-picker reorders inverted picks);
11
+ * aria-invalid/aria-describedby pass-throughs keep external validation wiring
12
+ * alive. i18n is via plain string/locale props.
13
+ *
14
+ * ONE POPOVER ROOT PER SEGMENT, deliberately. A single `Popover.Root` holds ONE
15
+ * trigger ref and ONE open state, so wrapping the whole two-button row in a
16
+ * single `PopoverTrigger asChild` put Radix's dialog attributes
17
+ * (`aria-haspopup`, `aria-expanded`, `aria-controls`) on a plain, non-focusable
18
+ * `div` — an aria-allowed-attr violation, two buttons advertising the same
19
+ * expanded state, and focus that Radix could drop on `document.body` when the
20
+ * popover closed. Each segment is now a real `PopoverTrigger` button: focusable,
21
+ * correctly named, and the element Radix returns focus to on dismissal.
12
22
  */
13
23
  import { useState } from 'react';
14
24
  import { CalendarDays } from 'lucide-react';
@@ -51,7 +61,11 @@ function toDayString(value) {
51
61
  return `${year}-${month}-${day}`;
52
62
  }
53
63
  export function DateRangePicker({ value, onValueChange, fromId, toId, fromLabel, toLabel, invalid = false, errorId, placeholder = 'Any date', ariaLabel = 'Select date range', clearLabel = 'Clear', locale = enUS }) {
54
- const [open, setOpen] = useState(false);
64
+ // Which segment's popover is open, if any. One root per segment means one open
65
+ // state per segment; holding the identity of the open one here keeps them
66
+ // mutually exclusive, so clicking "to" while "from" is open swaps the anchor
67
+ // instead of stacking two calendars.
68
+ const [openSegment, setOpenSegment] = useState(null);
55
69
  const fromDate = parseDay(value.from);
56
70
  const toDate = parseDay(value.to);
57
71
  const selected = fromDate || toDate ? { from: fromDate, to: toDate } : undefined;
@@ -66,6 +80,10 @@ export function DateRangePicker({ value, onValueChange, fromId, toId, fromLabel,
66
80
  });
67
81
  };
68
82
  const triggerClass = cn('border-input bg-card hover:bg-secondary focus-visible:ring-ring flex h-9 items-center gap-2 rounded-md border px-3 text-left font-mono text-sm tabular-nums transition-colors duration-150 ease-out focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:outline-none sm:w-40', invalid && 'border-destructive');
69
- const renderTrigger = (id, label, dateValue) => (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: id, className: SECTION_LABEL_CLASS, children: label }), _jsxs("button", { type: "button", id: id, className: triggerClass, "aria-haspopup": "dialog", "aria-expanded": open, "aria-invalid": invalid || undefined, "aria-describedby": invalid ? errorId : undefined, children: [_jsx(CalendarDays, { className: "text-muted-foreground size-4 shrink-0", "aria-hidden": true }), dateValue ? (_jsx("span", { children: dateValue })) : (_jsx("span", { className: "text-muted-foreground font-sans", children: placeholder }))] })] }));
70
- return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-end", children: [renderTrigger(fromId, fromLabel, value.from), renderTrigger(toId, toLabel, value.to)] }) }), _jsxs(PopoverContent, { align: "start", className: "w-auto p-0", "aria-label": ariaLabel, children: [_jsx(Calendar, { mode: "range", numberOfMonths: 2, defaultMonth: fromDate ?? toDate ?? new Date(), selected: selected, onSelect: onSelect, locale: locale }), _jsx("div", { className: "border-border flex items-center justify-end border-t px-3 py-2", children: _jsx(Button, { type: "button", variant: "plain", size: "small", disabled: !hasValue, onClick: () => onValueChange({ from: '', to: '' }), children: clearLabel }) })] })] }));
83
+ // The one calendar, rendered inside whichever segment's popover is open. Both
84
+ // segments read and write the SAME range selection, so which one you opened
85
+ // changes only the anchor, never the state.
86
+ const calendar = (_jsxs(_Fragment, { children: [_jsx(Calendar, { mode: "range", numberOfMonths: 2, defaultMonth: fromDate ?? toDate ?? new Date(), selected: selected, onSelect: onSelect, locale: locale }), _jsx("div", { className: "border-border flex items-center justify-end border-t px-3 py-2", children: _jsx(Button, { type: "button", variant: "plain", size: "small", disabled: !hasValue, onClick: () => onValueChange({ from: '', to: '' }), children: clearLabel }) })] }));
87
+ const renderSegment = (segment, id, label, dateValue) => (_jsxs(Popover, { open: openSegment === segment, onOpenChange: (next) => setOpenSegment(next ? segment : null), children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: id, className: SECTION_LABEL_CLASS, children: label }), _jsxs(PopoverTrigger, { type: "button", id: id, className: triggerClass, "aria-invalid": invalid || undefined, "aria-describedby": invalid ? errorId : undefined, children: [_jsx(CalendarDays, { className: "text-muted-foreground size-4 shrink-0", "aria-hidden": true }), dateValue ? (_jsx("span", { children: dateValue })) : (_jsx("span", { className: "text-muted-foreground font-sans", children: placeholder }))] })] }), _jsx(PopoverContent, { align: "start", className: "w-auto p-0", "aria-label": ariaLabel, children: calendar })] }, segment));
88
+ return (_jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-end", children: [renderSegment('from', fromId, fromLabel, value.from), renderSegment('to', toId, toLabel, value.to)] }));
71
89
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/enterprise/search-input/index.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAa9B,MAAM,MAAM,gBAAgB,GAAG;IAC7B,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAA;IACb,8CAA8C;IAC9C,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,aAAa,EACb,UAAgB,EAChB,WAAuB,EACvB,YAAY,EAAE,SAAS,EACvB,UAA2B,EAC3B,QAAgB,EAChB,SAAS,EACT,EAAE,EACH,EAAE,gBAAgB,qBAgGlB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/enterprise/search-input/index.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAa9B,MAAM,MAAM,gBAAgB,GAAG;IAC7B,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAA;IACb,8CAA8C;IAC9C,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,aAAa,EACb,UAAgB,EAChB,WAAuB,EACvB,YAAY,EAAE,SAAS,EACvB,UAA2B,EAC3B,QAAgB,EAChB,SAAS,EACT,EAAE,EACH,EAAE,gBAAgB,qBAsGlB"}
@@ -36,13 +36,20 @@ export function SearchInput({ value, onValueChange, debounceMs = 250, placeholde
36
36
  // initial value is never mistaken for an echo of something we sent.
37
37
  const lastEmitted = React.useRef(NO_PENDING_ECHO);
38
38
  React.useEffect(() => {
39
- if (lastEmitted.current === value) {
40
- // This is the echo of our own emission. Consume the token: a LATER
41
- // external change back to this same text (foo -> bar -> foo) is a real
42
- // change and must reach the draft.
43
- lastEmitted.current = NO_PENDING_ECHO;
39
+ const isEcho = lastEmitted.current === value;
40
+ // ANY external change consumes the token, echo or not. Consuming it only on
41
+ // a match left a DROPPED emission pending forever: emit "pix", the parent
42
+ // rejects it and holds "", the parent later sets "foo" (draft follows, token
43
+ // still holding "pix"), the parent then sets "pix" — read as our own echo,
44
+ // so the draft stayed on "foo" and the visible field disagreed with the
45
+ // controlled value. A later external change is proof the parent moved on,
46
+ // which invalidates any echo still outstanding.
47
+ lastEmitted.current = NO_PENDING_ECHO;
48
+ // A genuine echo must NOT reach the draft: the parent echoes one render
49
+ // later, by which time the user may have typed further, and syncing would
50
+ // rewind the field and swallow those keystrokes.
51
+ if (isEcho)
44
52
  return;
45
- }
46
53
  setDraft(value);
47
54
  }, [value]);
48
55
  const clearTimer = React.useCallback(() => {
@@ -1,11 +1,20 @@
1
+ /** The one breakpoint the hook subscribes at. Exported so its tests can bind to
2
+ * it instead of re-stating the number. */
3
+ export declare const MOBILE_BREAKPOINT = 768;
1
4
  /**
2
5
  * True while the viewport is narrower than the 768px mobile breakpoint.
3
6
  *
4
7
  * Always false on the FIRST render, server and client alike, so a narrow
5
- * viewport can never produce a hydration mismatch; the effect corrects it
6
- * before paint. Every reading comes from the media query itself rather than a
7
- * mix of `matchMedia` and `window.innerWidth`, so the initial value and the
8
- * change events can never disagree at the breakpoint boundary.
8
+ * viewport can never produce a hydration mismatch. The correction lands in a
9
+ * `useEffect`, which runs AFTER the browser paints: on a narrow viewport the
10
+ * first painted frame therefore shows the desktop branch and the mobile branch
11
+ * appears on the next frame. That one frame is the deliberate price of
12
+ * hydration safety — a layout effect would remove it but would also change the
13
+ * render timing every existing consumer is built on.
14
+ *
15
+ * Every reading comes from the media query itself rather than a mix of
16
+ * `matchMedia` and `window.innerWidth`, so the initial value and the change
17
+ * events can never disagree at the breakpoint boundary.
9
18
  */
10
19
  export declare function useIsMobile(): boolean;
11
20
  //# sourceMappingURL=use-is-mobile.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-is-mobile.d.ts","sourceRoot":"","sources":["../../../src/enterprise/use-is-mobile.ts"],"names":[],"mappings":"AAMA;;;;;;;;GAQG;AACH,wBAAgB,WAAW,YAY1B"}
1
+ {"version":3,"file":"use-is-mobile.d.ts","sourceRoot":"","sources":["../../../src/enterprise/use-is-mobile.ts"],"names":[],"mappings":"AAIA;2CAC2C;AAC3C,eAAO,MAAM,iBAAiB,MAAM,CAAA;AAEpC;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,YAY1B"}
@@ -1,14 +1,22 @@
1
1
  'use client';
2
2
  import * as React from 'react';
3
- const MOBILE_BREAKPOINT = 768;
3
+ /** The one breakpoint the hook subscribes at. Exported so its tests can bind to
4
+ * it instead of re-stating the number. */
5
+ export const MOBILE_BREAKPOINT = 768;
4
6
  /**
5
7
  * True while the viewport is narrower than the 768px mobile breakpoint.
6
8
  *
7
9
  * Always false on the FIRST render, server and client alike, so a narrow
8
- * viewport can never produce a hydration mismatch; the effect corrects it
9
- * before paint. Every reading comes from the media query itself rather than a
10
- * mix of `matchMedia` and `window.innerWidth`, so the initial value and the
11
- * change events can never disagree at the breakpoint boundary.
10
+ * viewport can never produce a hydration mismatch. The correction lands in a
11
+ * `useEffect`, which runs AFTER the browser paints: on a narrow viewport the
12
+ * first painted frame therefore shows the desktop branch and the mobile branch
13
+ * appears on the next frame. That one frame is the deliberate price of
14
+ * hydration safety — a layout effect would remove it but would also change the
15
+ * render timing every existing consumer is built on.
16
+ *
17
+ * Every reading comes from the media query itself rather than a mix of
18
+ * `matchMedia` and `window.innerWidth`, so the initial value and the change
19
+ * events can never disagree at the breakpoint boundary.
12
20
  */
13
21
  export function useIsMobile() {
14
22
  const [isMobile, setIsMobile] = React.useState(false);
@@ -1 +1 @@
1
- {"version":3,"file":"theme-provider.d.ts","sourceRoot":"","sources":["../../../src/theme/theme-provider.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAA;AAgBzD,KAAK,iBAAiB,GAAG;IACvB,oEAAoE;IACpE,KAAK,EAAE,eAAe,CAAA;IACtB,yEAAyE;IACzE,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IACzC,mDAAmD;IACnD,aAAa,EAAE,OAAO,GAAG,MAAM,CAAA;CAChC,CAAA;AAID,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iEAAiE;IACjE,YAAY,CAAC,EAAE,eAAe,CAAA;CAC/B;AAED,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,UAA8B,EAC9B,YAAuB,EACxB,EAAE,kBAAkB,qBAwEpB;AAED,uEAAuE;AACvE,wBAAgB,QAAQ,IAAI,iBAAiB,CAM5C"}
1
+ {"version":3,"file":"theme-provider.d.ts","sourceRoot":"","sources":["../../../src/theme/theme-provider.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAA;AAgBzD,KAAK,iBAAiB,GAAG;IACvB,oEAAoE;IACpE,KAAK,EAAE,eAAe,CAAA;IACtB,yEAAyE;IACzE,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IACzC,mDAAmD;IACnD,aAAa,EAAE,OAAO,GAAG,MAAM,CAAA;CAChC,CAAA;AAID,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iEAAiE;IACjE,YAAY,CAAC,EAAE,eAAe,CAAA;CAC/B;AAED,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,UAA8B,EAC9B,YAAuB,EACxB,EAAE,kBAAkB,qBAwFpB;AAED,uEAAuE;AACvE,wBAAgB,QAAQ,IAAI,iBAAiB,CAM5C"}
@@ -78,7 +78,21 @@ export function ThemeProvider({ children, storageKey = 'sindarian.theme', defaul
78
78
  if (typeof window === 'undefined')
79
79
  return;
80
80
  const onStorage = (event) => {
81
- if (event.key !== storageKey)
81
+ // WHICH STORE first, then which key. Only the store the preference lives
82
+ // in may change it: a sessionStorage event carrying the SAME key would
83
+ // otherwise mutate a localStorage-backed preference. `storageArea` is
84
+ // absent only on a hand-dispatched synthetic event — apps use those for
85
+ // same-tab sync — and one of those is not attributable to any store, so it
86
+ // is allowed through rather than silently dropped.
87
+ if (event.storageArea != null &&
88
+ event.storageArea !== window.localStorage)
89
+ return;
90
+ // A null key means another tab called `clear()` on the whole store, so the
91
+ // stored preference is gone even though it is not named. Returning early
92
+ // on that left the provider holding a deleted preference — and the wrong
93
+ // `dark` class on the document. Fall through with newValue null, which
94
+ // lands on the fallback.
95
+ if (event.key !== null && event.key !== storageKey)
82
96
  return;
83
97
  setThemeState(isThemePreference(event.newValue) ? event.newValue : defaultTheme);
84
98
  };
@@ -1 +1 @@
1
- {"version":3,"file":"theme-provider.d.ts","sourceRoot":"","sources":["../../src/theme/theme-provider.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAA;AAgBzD,KAAK,iBAAiB,GAAG;IACvB,oEAAoE;IACpE,KAAK,EAAE,eAAe,CAAA;IACtB,yEAAyE;IACzE,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IACzC,mDAAmD;IACnD,aAAa,EAAE,OAAO,GAAG,MAAM,CAAA;CAChC,CAAA;AAID,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iEAAiE;IACjE,YAAY,CAAC,EAAE,eAAe,CAAA;CAC/B;AAED,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,UAA8B,EAC9B,YAAuB,EACxB,EAAE,kBAAkB,qBAwEpB;AAED,uEAAuE;AACvE,wBAAgB,QAAQ,IAAI,iBAAiB,CAM5C"}
1
+ {"version":3,"file":"theme-provider.d.ts","sourceRoot":"","sources":["../../src/theme/theme-provider.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAA;AAgBzD,KAAK,iBAAiB,GAAG;IACvB,oEAAoE;IACpE,KAAK,EAAE,eAAe,CAAA;IACtB,yEAAyE;IACzE,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IACzC,mDAAmD;IACnD,aAAa,EAAE,OAAO,GAAG,MAAM,CAAA;CAChC,CAAA;AAID,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iEAAiE;IACjE,YAAY,CAAC,EAAE,eAAe,CAAA;CAC/B;AAED,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,UAA8B,EAC9B,YAAuB,EACxB,EAAE,kBAAkB,qBAwFpB;AAED,uEAAuE;AACvE,wBAAgB,QAAQ,IAAI,iBAAiB,CAM5C"}
@@ -115,7 +115,21 @@ function ThemeProvider({ children, storageKey = 'sindarian.theme', defaultTheme
115
115
  if (typeof window === 'undefined')
116
116
  return;
117
117
  const onStorage = (event) => {
118
- if (event.key !== storageKey)
118
+ // WHICH STORE first, then which key. Only the store the preference lives
119
+ // in may change it: a sessionStorage event carrying the SAME key would
120
+ // otherwise mutate a localStorage-backed preference. `storageArea` is
121
+ // absent only on a hand-dispatched synthetic event — apps use those for
122
+ // same-tab sync — and one of those is not attributable to any store, so it
123
+ // is allowed through rather than silently dropped.
124
+ if (event.storageArea != null &&
125
+ event.storageArea !== window.localStorage)
126
+ return;
127
+ // A null key means another tab called `clear()` on the whole store, so the
128
+ // stored preference is gone even though it is not named. Returning early
129
+ // on that left the provider holding a deleted preference — and the wrong
130
+ // `dark` class on the document. Fall through with newValue null, which
131
+ // lands on the fallback.
132
+ if (event.key !== null && event.key !== storageKey)
119
133
  return;
120
134
  setThemeState(isThemePreference(event.newValue) ? event.newValue : defaultTheme);
121
135
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lerianstudio/sindarian-ui",
3
- "version": "1.2.0-beta.14",
3
+ "version": "1.2.0-beta.15",
4
4
  "description": "Sindarian UI - A UI library for Midaz Console",
5
5
  "license": "ISC",
6
6
  "author": {