@godxjp/ui 13.11.1 → 13.11.2

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.
@@ -3,6 +3,7 @@ import * as React from "react";
3
3
  import { CalendarIcon, X } from "lucide-react";
4
4
  import { usePickerLocales, useTranslation } from "../../i18n/use-translation";
5
5
  import { parseDateInput, toIsoDate } from "../../lib/datetime/parse";
6
+ import { useControlledLatch } from "../../lib/hooks";
6
7
  import { cn } from "../../lib/utils";
7
8
  import { Button } from "../general/button";
8
9
  import { Input } from "./input";
@@ -26,7 +27,7 @@ function DatePicker({
26
27
  const { t } = useTranslation();
27
28
  const { dayPickerLocale } = usePickerLocales(localeProp);
28
29
  const [open, setOpen] = React.useState(false);
29
- const isControlled = React.useRef(valueProp !== void 0).current;
30
+ const isControlled = useControlledLatch(valueProp !== void 0);
30
31
  const [internalValue, setInternalValue] = React.useState(defaultValue);
31
32
  const value = isControlled ? valueProp : internalValue;
32
33
  const emit = (next) => {
@@ -3,6 +3,7 @@ import * as React from "react";
3
3
  import { ArrowRight, CalendarIcon, X } from "lucide-react";
4
4
  import { usePickerLocales, useTranslation } from "../../i18n/use-translation";
5
5
  import { parseDateInput, toIsoDate } from "../../lib/datetime";
6
+ import { useControlledLatch } from "../../lib/hooks";
6
7
  import { cn } from "../../lib/utils";
7
8
  import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../data-display/popover";
8
9
  import { Calendar } from "./calendar";
@@ -27,7 +28,7 @@ function DateRangePicker({
27
28
  const autoId = React.useId();
28
29
  const fromId = id ?? autoId;
29
30
  const toId = `${fromId}-to`;
30
- const isControlled = React.useRef(valueProp !== void 0).current;
31
+ const isControlled = useControlledLatch(valueProp !== void 0);
31
32
  const [internalValue, setInternalValue] = React.useState(defaultValue);
32
33
  const value = isControlled ? valueProp : internalValue;
33
34
  const [fromText, setFromText] = React.useState(() => toIsoDate(value?.from));
@@ -2,6 +2,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { CalendarIcon, ChevronLeft, ChevronRight, X } from "lucide-react";
4
4
  import { usePickerLocales, useTranslation } from "../../i18n/use-translation";
5
+ import { useControlledLatch } from "../../lib/hooks";
5
6
  import { cn } from "../../lib/utils";
6
7
  import { Button } from "../general/button";
7
8
  import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../data-display/popover";
@@ -32,7 +33,7 @@ function MonthPicker({
32
33
  const [open, setOpen] = React.useState(false);
33
34
  const autoId = React.useId();
34
35
  const inputId = id ?? autoId;
35
- const isControlled = React.useRef(valueProp !== void 0).current;
36
+ const isControlled = useControlledLatch(valueProp !== void 0);
36
37
  const [internalValue, setInternalValue] = React.useState(defaultValue);
37
38
  const value = isControlled ? valueProp : internalValue;
38
39
  const [text, setText] = React.useState(() => toYmText(value));
@@ -2,6 +2,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { ArrowRight, CalendarIcon, ChevronLeft, ChevronRight, X } from "lucide-react";
4
4
  import { usePickerLocales, useTranslation } from "../../i18n/use-translation";
5
+ import { useControlledLatch } from "../../lib/hooks";
5
6
  import { cn } from "../../lib/utils";
6
7
  import { Button } from "../general/button";
7
8
  import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../data-display/popover";
@@ -34,7 +35,7 @@ function MonthRangePicker({
34
35
  const autoId = React.useId();
35
36
  const fromId = id ?? autoId;
36
37
  const toId = `${fromId}-to`;
37
- const isControlled = React.useRef(valueProp !== void 0).current;
38
+ const isControlled = useControlledLatch(valueProp !== void 0);
38
39
  const [internalValue, setInternalValue] = React.useState(defaultValue);
39
40
  const value = isControlled ? valueProp : internalValue;
40
41
  const [fromText, setFromText] = React.useState(() => toYmText(value?.from));
@@ -11,5 +11,20 @@ export declare function useDebouncedValue<T>(value: T, delay?: number): T;
11
11
  * setState is scheduled asynchronously (setTimeout 0 / ms) — Rules of React safe.
12
12
  */
13
13
  export declare function useTimeoutFlag(signal: unknown, ms?: number): boolean;
14
+ /**
15
+ * Controlled-ness latch for `value`/`defaultValue`/`onValueChange` controls
16
+ * whose empty state is `undefined` (pickers carrying `Date`/`DateRange`).
17
+ *
18
+ * A control counts as controlled once a DEFINED `value` has EVER been passed:
19
+ * - mounted with a defined `value` → controlled, and a later `value={undefined}`
20
+ * stays controlled-EMPTY (not mistaken for uncontrolled);
21
+ * - mounted with `value={undefined}` (an empty form that later restores a
22
+ * saved value) → uncontrolled until the first defined value arrives, then
23
+ * PROMOTES to controlled for good.
24
+ *
25
+ * Fixing controlled-ness at mount (the previous behaviour) silently ignored
26
+ * every later controlled value in the second case.
27
+ */
28
+ export declare function useControlledLatch(valueIsDefined: boolean): boolean;
14
29
  export declare function useMediaQuery(query: string): boolean;
15
30
  export declare function useIsMobile(): boolean;
package/dist/lib/hooks.js CHANGED
@@ -35,6 +35,11 @@ function useTimeoutFlag(signal, ms = 2e3) {
35
35
  }, [signal, ms]);
36
36
  return Boolean(signal) && active;
37
37
  }
38
+ function useControlledLatch(valueIsDefined) {
39
+ const [latched, setLatched] = useState(valueIsDefined);
40
+ if (valueIsDefined && !latched) setLatched(true);
41
+ return valueIsDefined || latched;
42
+ }
38
43
  function useMediaQuery(query) {
39
44
  const isBrowser = typeof window !== "undefined";
40
45
  const getMatch = () => isBrowser ? window.matchMedia(query).matches : false;
@@ -63,6 +68,7 @@ function useIsMobile() {
63
68
  return useMediaQuery("(max-width: 767px)");
64
69
  }
65
70
  export {
71
+ useControlledLatch,
66
72
  useDebouncedValue,
67
73
  useIsMobile,
68
74
  useMediaQuery,
@@ -39,6 +39,14 @@
39
39
  min-width: 0;
40
40
  /* Cap the control width when a controlWidth is set; otherwise fill the column. */
41
41
  max-inline-size: var(--form-control-width, none);
42
+ /* Reserve the control-row height and centre the content so a SHORT control
43
+ * (switch, slider, rating, a bare checkbox/radio — anything thinner than an
44
+ * input) lines up with the label in a horizontal field instead of sitting
45
+ * high. `align-content: center` centres the content row without stretching
46
+ * the control; full-width inputs still fill via the 1fr column, and taller
47
+ * controls (textarea) simply grow past the floor. */
48
+ min-height: var(--control-height);
49
+ align-content: center;
42
50
  }
43
51
 
44
52
  /* Let controls (and helper/error) shrink within the cell instead of forcing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "13.11.1",
3
+ "version": "13.11.2",
4
4
  "type": "module",
5
5
  "packageManager": "pnpm@10.29.1",
6
6
  "sideEffects": false,