@godxjp/ui 19.4.1 → 19.5.0

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 (93) hide show
  1. package/dist/app/app-provider.js +10 -1
  2. package/dist/app/theme-axes.d.ts +18 -3
  3. package/dist/app/theme-axes.js +15 -3
  4. package/dist/components/data-display/badge.js +4 -1
  5. package/dist/components/data-display/data-table.js +9 -2
  6. package/dist/components/data-display/list-row.d.ts +2 -0
  7. package/dist/components/data-display/list-row.js +16 -12
  8. package/dist/components/data-display/timeline-grid.js +5 -3
  9. package/dist/components/data-entry/checkbox.d.ts +12 -0
  10. package/dist/components/data-entry/checkbox.js +35 -4
  11. package/dist/components/data-entry/form-field.js +5 -5
  12. package/dist/components/data-entry/index.d.ts +2 -0
  13. package/dist/components/data-entry/index.js +2 -0
  14. package/dist/components/data-entry/input.js +1 -1
  15. package/dist/components/data-entry/radio.js +1 -1
  16. package/dist/components/data-entry/search-input.d.ts +2 -18
  17. package/dist/components/data-entry/search-input.js +3 -0
  18. package/dist/components/data-entry/segmented.d.ts +2 -0
  19. package/dist/components/data-entry/segmented.js +5 -0
  20. package/dist/components/data-entry/select.d.ts +2 -0
  21. package/dist/components/data-entry/select.js +9 -4
  22. package/dist/components/feedback/sheet.js +1 -1
  23. package/dist/components/feedback/sonner.d.ts +7 -1
  24. package/dist/components/feedback/sonner.js +27 -7
  25. package/dist/components/general/button.d.ts +2 -0
  26. package/dist/components/general/button.js +11 -2
  27. package/dist/components/general/inert-background.d.ts +8 -3
  28. package/dist/components/general/inert-background.js +28 -3
  29. package/dist/components/general/logo.d.ts +53 -0
  30. package/dist/components/general/logo.js +57 -3
  31. package/dist/components/layout/app-shell.js +1 -1
  32. package/dist/components/layout/index.d.ts +4 -0
  33. package/dist/components/layout/index.js +4 -0
  34. package/dist/components/layout/master-detail.d.ts +1 -1
  35. package/dist/components/layout/master-detail.js +47 -3
  36. package/dist/components/layout/nav-list.d.ts +26 -0
  37. package/dist/components/layout/nav-list.js +21 -0
  38. package/dist/components/layout/responsive-grid.js +7 -4
  39. package/dist/components/layout/topbar-item.d.ts +17 -0
  40. package/dist/components/layout/topbar-item.js +21 -0
  41. package/dist/components/navigation/app-setting-picker.js +31 -5
  42. package/dist/components/navigation/context-menu.js +2 -2
  43. package/dist/components/navigation/dropdown-menu.js +2 -2
  44. package/dist/components/navigation/tabs.js +42 -10
  45. package/dist/components/ui/segmented.d.ts +48 -0
  46. package/dist/components/ui/segmented.js +48 -0
  47. package/dist/email/tokens.generated.d.ts +4 -4
  48. package/dist/email/tokens.generated.js +4 -4
  49. package/dist/i18n/messages/en.json +2 -1
  50. package/dist/i18n/messages/ja.json +2 -1
  51. package/dist/i18n/messages/vi.json +2 -1
  52. package/dist/props/components/app.prop.d.ts +9 -2
  53. package/dist/props/components/data-entry.prop.d.ts +10 -5
  54. package/dist/props/components/general.prop.d.ts +4 -0
  55. package/dist/props/components/layout.prop.d.ts +54 -2
  56. package/dist/props/registry.d.ts +21 -2
  57. package/dist/props/registry.js +29 -1
  58. package/dist/props/vocabulary/shared.prop.d.ts +11 -2
  59. package/dist/styles/alert-layout.css +4 -0
  60. package/dist/styles/base.css +2 -0
  61. package/dist/styles/control.css +123 -26
  62. package/dist/styles/data-display-layout.css +24 -2
  63. package/dist/styles/dialog-layout.css +2 -1
  64. package/dist/styles/focus-ring.css +84 -22
  65. package/dist/styles/form-layout.css +6 -0
  66. package/dist/styles/layout.css +38 -2
  67. package/dist/styles/logo-layout.css +56 -0
  68. package/dist/styles/navigation-layout.css +17 -1
  69. package/dist/styles/shell-layout.css +64 -10
  70. package/dist/tokens/antd.generated.css +40 -0
  71. package/dist/tokens/axes.css +6 -0
  72. package/dist/tokens/base.css +3 -0
  73. package/dist/tokens/components/control.css +5 -1
  74. package/dist/tokens/components/data-display.css +2 -1
  75. package/dist/tokens/components/feedback.css +2 -0
  76. package/dist/tokens/components/legal-document.css +0 -2
  77. package/dist/tokens/components/logo.css +13 -0
  78. package/dist/tokens/components/navigation.css +2 -0
  79. package/dist/tokens/components/segmented.css +27 -0
  80. package/dist/tokens/components/shell.css +15 -2
  81. package/dist/tokens/foundation.css +21 -13
  82. package/docs/DESIGN-AUTHORITY.md +346 -0
  83. package/docs/FRAME-COVERAGE-REPORT.md +5 -2
  84. package/docs/README.md +14 -13
  85. package/docs/data-display/list-row.tsx +21 -0
  86. package/docs/data-entry/search-input.tsx +1 -0
  87. package/docs/data-entry/segmented.tsx +124 -0
  88. package/docs/general/button/index.md +4 -0
  89. package/docs/layout/master-detail.tsx +3 -0
  90. package/docs/layout/nav-list.tsx +63 -0
  91. package/docs/layout/responsive-grid.tsx +19 -2
  92. package/docs/layout/topbar-item.tsx +157 -0
  93. package/package.json +8 -2
@@ -16,7 +16,8 @@ import {
16
16
  } from "../lib/datetime/index.js";
17
17
  import { DEFAULT_STORAGE_KEY, readStoredPreferences, writeStoredPreferences } from "./storage.js";
18
18
  import {
19
- applyThemeAxes
19
+ applyThemeAxes,
20
+ PREFERS_DARK_SCHEME_QUERY
20
21
  } from "./theme-axes.js";
21
22
  import { resolveDefaultTimeFormat } from "./time-format-labels.js";
22
23
  import { resolveDefaultTimezone } from "./timezones.js";
@@ -174,6 +175,14 @@ function AppProvider({
174
175
  if (typeof document === "undefined") return;
175
176
  applyThemeAxes(document.documentElement, { theme, brand, density, fontSize, scaling });
176
177
  }, [theme, brand, density, fontSize, scaling]);
178
+ React.useEffect(() => {
179
+ if (theme !== "system") return;
180
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
181
+ const query = window.matchMedia(PREFERS_DARK_SCHEME_QUERY);
182
+ const reapply = () => applyThemeAxes(document.documentElement, { theme: "system" });
183
+ query.addEventListener("change", reapply);
184
+ return () => query.removeEventListener("change", reapply);
185
+ }, [theme]);
177
186
  const setLocale = React.useCallback(
178
187
  (next) => {
179
188
  prefsRef.current = { ...prefsRef.current, locale: next };
@@ -1,13 +1,19 @@
1
1
  import type { PageDensityProp } from "../props/vocabulary/layout.prop.js";
2
- /** Light / dark spine — `data-theme` (alias of the legacy `.dark` class). */
3
- export type AppTheme = "light" | "dark";
2
+ /**
3
+ * Theme spine — `data-theme` (alias of the legacy `.dark` class). `"system"` is a CHOICE, not a
4
+ * rendered value: it defers to the OS `prefers-color-scheme` and is what gets persisted, so the
5
+ * app keeps following the OS tomorrow morning. `data-theme` only ever carries `light` or `dark`.
6
+ */
7
+ export type AppTheme = "light" | "dark" | "system";
8
+ /** What `data-theme` actually carries — the resolution of an {@link AppTheme} choice. */
9
+ export type ResolvedAppTheme = Exclude<AppTheme, "system">;
4
10
  /** Primary-palette preset — `data-brand`. `null` = keep the app's own `--primary`. */
5
11
  export type AppBrand = "brand" | "crm" | "logistics" | "partner" | "slate" | "dxs";
6
12
  /** Control / table / spacing density — `data-density` (same vocab as PageContainer). */
7
13
  export type AppDensity = PageDensityProp;
8
14
  /** Base type size — `data-font-size`; a preset rescales the whole golden scale. */
9
15
  export type AppFontSize = "sm" | "default" | "lg";
10
- export declare const APP_THEMES: readonly ["light", "dark"];
16
+ export declare const APP_THEMES: readonly ["light", "dark", "system"];
11
17
  export declare const APP_BRANDS: readonly ["brand", "crm", "logistics", "partner", "slate", "dxs"];
12
18
  export declare const APP_DENSITIES: readonly ["compact", "default", "comfortable"];
13
19
  export declare const APP_FONT_SIZES: readonly ["sm", "default", "lg"];
@@ -20,6 +26,15 @@ export type AppThemeAxes = {
20
26
  scaling: number | null;
21
27
  };
22
28
  export declare const isAppTheme: (v: unknown) => v is AppTheme;
29
+ /** The media query `theme: "system"` follows. */
30
+ export declare const PREFERS_DARK_SCHEME_QUERY = "(prefers-color-scheme: dark)";
31
+ /** `false` wherever there is no `matchMedia` (SSR, jsdom without the stub) — light is the default. */
32
+ export declare function prefersDarkScheme(): boolean;
33
+ /**
34
+ * Resolve a stored CHOICE to the value `data-theme` carries. `light`/`dark` pass through
35
+ * untouched, so a preference stored before `system` existed keeps working.
36
+ */
37
+ export declare function resolveAppTheme(theme: AppTheme): ResolvedAppTheme;
23
38
  export declare const isAppBrand: (v: unknown) => v is AppBrand;
24
39
  export declare const isAppDensity: (v: unknown) => v is AppDensity;
25
40
  export declare const isAppFontSize: (v: unknown) => v is AppFontSize;
@@ -1,4 +1,4 @@
1
- const APP_THEMES = ["light", "dark"];
1
+ const APP_THEMES = ["light", "dark", "system"];
2
2
  const APP_BRANDS = [
3
3
  "brand",
4
4
  "crm",
@@ -14,11 +14,20 @@ const APP_DENSITIES = [
14
14
  ];
15
15
  const APP_FONT_SIZES = ["sm", "default", "lg"];
16
16
  const isAppTheme = (v) => APP_THEMES.includes(v);
17
+ const PREFERS_DARK_SCHEME_QUERY = "(prefers-color-scheme: dark)";
18
+ function prefersDarkScheme() {
19
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
20
+ return window.matchMedia(PREFERS_DARK_SCHEME_QUERY).matches;
21
+ }
22
+ function resolveAppTheme(theme) {
23
+ if (theme !== "system") return theme;
24
+ return prefersDarkScheme() ? "dark" : "light";
25
+ }
17
26
  const isAppBrand = (v) => APP_BRANDS.includes(v);
18
27
  const isAppDensity = (v) => APP_DENSITIES.includes(v);
19
28
  const isAppFontSize = (v) => APP_FONT_SIZES.includes(v);
20
29
  function applyThemeAxes(el, axes) {
21
- if (axes.theme !== void 0) el.dataset.theme = axes.theme;
30
+ if (axes.theme !== void 0) el.dataset.theme = resolveAppTheme(axes.theme);
22
31
  if (axes.density !== void 0) el.dataset.density = axes.density;
23
32
  if (axes.fontSize !== void 0) el.dataset.fontSize = axes.fontSize;
24
33
  if (axes.brand !== void 0) {
@@ -35,9 +44,12 @@ export {
35
44
  APP_DENSITIES,
36
45
  APP_FONT_SIZES,
37
46
  APP_THEMES,
47
+ PREFERS_DARK_SCHEME_QUERY,
38
48
  applyThemeAxes,
39
49
  isAppBrand,
40
50
  isAppDensity,
41
51
  isAppFontSize,
42
- isAppTheme
52
+ isAppTheme,
53
+ prefersDarkScheme,
54
+ resolveAppTheme
43
55
  };
@@ -50,7 +50,10 @@ const STATUS_MAP = {
50
50
  ASSIGNMENT_STATUS_TERMINATED: { tone: "destructive", icon: XCircle }
51
51
  };
52
52
  const badgeVariants = cva(
53
- "inline-flex items-center border font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring",
53
+ // `ui-focus-ring` = the system's single focus source (styles/focus-ring.css). It replaces a
54
+ // hand-rolled `focus:ring-2 focus:ring-ring`, which was token-blind (no --focus-ring-* knob
55
+ // reached it) and fired on `:focus`, so a mouse click on an interactive badge lit a ring.
56
+ "ui-focus-ring inline-flex items-center border font-medium transition-colors",
54
57
  {
55
58
  variants: {
56
59
  variant: {
@@ -683,8 +683,15 @@ DataTable.Content = function DataTableContent() {
683
683
  rowPadding,
684
684
  // Hover highlight when rows are clickable OR explicitly hoverable…
685
685
  (onRowClick || hoverable) && "hover:bg-muted/50",
686
- // …but the affordance (cursor + focus ring) only when clickable.
687
- onRowClick && "focus-visible:ring-ring cursor-pointer focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset",
686
+ // …but the affordance (cursor + focus mark) only when clickable.
687
+ //
688
+ // `ui-focus-ring` = the single focus source (styles/focus-ring.css). It
689
+ // replaces `focus-visible:ring-ring focus-visible:ring-2
690
+ // focus-visible:ring-inset`, which was a hand-rolled second ring: it read no
691
+ // `--focus-ring-*` knob, so a service could not retune it, and — being a
692
+ // utility — it painted regardless of the `--focus-outline` switch, which is
693
+ // exactly the hole the switch exists to close.
694
+ onRowClick && "ui-focus-ring cursor-pointer",
688
695
  isSelected && "bg-muted/30",
689
696
  rowClassName?.(original)
690
697
  ),
@@ -15,6 +15,8 @@ export type ListRowDensity = ListRowDensityProp;
15
15
  export interface ListRowProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
16
16
  /** Render element — `div` (default) or `li` when the parent is a `<ul>`/`<ol>`. */
17
17
  as?: "div" | "li";
18
+ /** Use the supplied link as the entire row. Do not nest interactive trailing controls. */
19
+ asChild?: boolean;
18
20
  /** Leading slot — a decorative icon or an Avatar. Mark a purely decorative icon `aria-hidden`. */
19
21
  leading?: React.ReactNode;
20
22
  /** Primary line — rendered in medium weight. */
@@ -1,12 +1,15 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
+ import { Slot } from "@radix-ui/react-slot";
4
5
  import { useTranslation } from "../../i18n/use-translation.js";
5
6
  import { cn } from "../../lib/utils.js";
6
7
  import { Text } from "../general/typography.js";
7
8
  const ListRow = React.forwardRef(
8
9
  ({
9
10
  as = "div",
11
+ asChild = false,
12
+ children,
10
13
  leading,
11
14
  title,
12
15
  description,
@@ -19,9 +22,18 @@ const ListRow = React.forwardRef(
19
22
  ...props
20
23
  }, ref) => {
21
24
  const { t } = useTranslation();
22
- const Comp = as;
25
+ const Comp = asChild ? Slot : as;
23
26
  const truncate = overflow !== "wrap";
24
- return /* @__PURE__ */ jsxs(
27
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
28
+ unread !== void 0 ? /* @__PURE__ */ jsx("span", { "data-slot": "list-row-indicator", children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: unread ? t("dataDisplay.listRow.unread") : t("dataDisplay.listRow.read") }) }) : null,
29
+ leading != null ? /* @__PURE__ */ jsx("span", { "data-slot": "list-row-leading", children: leading }) : null,
30
+ /* @__PURE__ */ jsxs("span", { "data-slot": "list-row-body", children: [
31
+ /* @__PURE__ */ jsx(Text, { weight: "medium", truncate, children: title }),
32
+ description != null ? /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", truncate, children: description }) : null
33
+ ] }),
34
+ trailing != null ? /* @__PURE__ */ jsx("span", { "data-slot": "list-row-trailing", children: trailing }) : null
35
+ ] });
36
+ return /* @__PURE__ */ jsx(
25
37
  Comp,
26
38
  {
27
39
  ref,
@@ -32,15 +44,7 @@ const ListRow = React.forwardRef(
32
44
  "data-unread": unread === true ? "" : void 0,
33
45
  className: cn("ui-list-row", className),
34
46
  ...props,
35
- children: [
36
- unread !== void 0 ? /* @__PURE__ */ jsx("span", { "data-slot": "list-row-indicator", children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: unread ? t("dataDisplay.listRow.unread") : t("dataDisplay.listRow.read") }) }) : null,
37
- leading != null ? /* @__PURE__ */ jsx("span", { "data-slot": "list-row-leading", children: leading }) : null,
38
- /* @__PURE__ */ jsxs("span", { "data-slot": "list-row-body", children: [
39
- /* @__PURE__ */ jsx(Text, { weight: "medium", truncate, children: title }),
40
- description != null ? /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", truncate, children: description }) : null
41
- ] }),
42
- trailing != null ? /* @__PURE__ */ jsx("span", { "data-slot": "list-row-trailing", children: trailing }) : null
43
- ]
47
+ children: asChild && React.isValidElement(children) ? React.cloneElement(children, void 0, content) : content
44
48
  }
45
49
  );
46
50
  }
@@ -25,7 +25,9 @@ function resolveEvent(event) {
25
25
  if (startMin === null || endMin === null) return null;
26
26
  return { event, startMin, endMin: endMin > startMin ? endMin : endMin + MINUTES_PER_DAY };
27
27
  }
28
+ const EVENT_MIN_MINUTES = 36;
28
29
  function placeColumn(events) {
30
+ const drawnEnd = (e) => Math.max(e.endMin, e.startMin + EVENT_MIN_MINUTES);
29
31
  const sorted = [...events].sort((a, b) => a.startMin - b.startMin || a.endMin - b.endMin);
30
32
  const placed = [];
31
33
  let cluster = [];
@@ -42,8 +44,8 @@ function placeColumn(events) {
42
44
  if (entry.startMin >= clusterEnd) closeCluster();
43
45
  let lane = laneEnds.findIndex((end) => end <= entry.startMin);
44
46
  if (lane === -1) lane = laneEnds.length;
45
- laneEnds[lane] = entry.endMin;
46
- clusterEnd = Math.max(clusterEnd, entry.endMin);
47
+ laneEnds[lane] = drawnEnd(entry);
48
+ clusterEnd = Math.max(clusterEnd, drawnEnd(entry));
47
49
  cluster.push({ ...entry, lane, lanes: 1 });
48
50
  }
49
51
  closeCluster();
@@ -93,7 +95,7 @@ const TimelineGrid = React.forwardRef(
93
95
  const axis = React.useMemo(() => resolveWindow(start, end, resolved), [start, end, resolved]);
94
96
  const span = axis.endMin - axis.startMin;
95
97
  const hours = span / 60;
96
- const step = Number.isFinite(interval) && interval > 0 ? interval : 1;
98
+ const step = Number.isFinite(interval) && interval > 0 ? Math.max(interval, 1 / 60) : 1;
97
99
  const byColumn = React.useMemo(() => {
98
100
  const buckets = columns.map(() => []);
99
101
  for (const entry of resolved) {
@@ -1,6 +1,18 @@
1
1
  import * as React from "react";
2
2
  import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
3
3
  import { CheckboxGroup } from "./checkbox-group.js";
4
+ /**
5
+ * Decorative checkbox glyph — a non-interactive `<span>`, NOT the real {@link Checkbox} (which is a
6
+ * `<button>`). Used where the row itself is the interactive element and a nested `<button>` would
7
+ * be invalid HTML (Cascader's option rows). Shares {@link CheckboxGlyph} so "partial" can never
8
+ * drift apart from the real control's rendering again.
9
+ */
10
+ export declare function CheckboxVisual({ checked, indeterminate, disabled, className, }: {
11
+ checked: boolean;
12
+ indeterminate?: boolean;
13
+ disabled?: boolean;
14
+ className?: string;
15
+ }): React.JSX.Element;
4
16
  /** Checkbox — dùng standalone hoặc `Checkbox.Group` với `options` (Ant Design style). */
5
17
  export declare const Checkbox: React.ForwardRefExoticComponent<Omit<CheckboxPrimitive.CheckboxProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>> & {
6
18
  Group: typeof CheckboxGroup;
@@ -2,16 +2,42 @@
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
5
- import { Check } from "lucide-react";
5
+ import { Check, Minus } from "lucide-react";
6
6
  import { cn } from "../../lib/utils.js";
7
7
  import { useFieldIdentity } from "../../lib/field-a11y.js";
8
8
  import { CheckboxGroup } from "./checkbox-group.js";
9
+ function CheckboxGlyph({ state }) {
10
+ return state === "indeterminate" ? /* @__PURE__ */ jsx(Minus, { className: "ui-checkbox-icon", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Check, { className: "ui-checkbox-icon", "aria-hidden": "true" });
11
+ }
12
+ function CheckboxVisual({
13
+ checked,
14
+ indeterminate,
15
+ disabled,
16
+ className
17
+ }) {
18
+ const state = indeterminate ? "indeterminate" : checked;
19
+ return /* @__PURE__ */ jsx(
20
+ "span",
21
+ {
22
+ "aria-hidden": "true",
23
+ "data-slot": "checkbox",
24
+ "data-state": indeterminate ? "indeterminate" : checked ? "checked" : "unchecked",
25
+ "data-disabled": disabled ? "" : void 0,
26
+ className: cn("ui-checkbox inline-flex items-center justify-center", className),
27
+ children: checked || indeterminate ? /* @__PURE__ */ jsx(CheckboxGlyph, { state }) : null
28
+ }
29
+ );
30
+ }
9
31
  const CheckboxRoot = React.forwardRef(({ className, ...props }, ref) => {
10
32
  const identity = useFieldIdentity({
11
33
  id: props.id,
12
34
  name: props.name,
13
35
  "data-field": props["data-field"]
14
36
  });
37
+ const [uncontrolled, setUncontrolled] = React.useState(
38
+ props.defaultChecked ?? false
39
+ );
40
+ const state = props.checked ?? uncontrolled;
15
41
  return /* @__PURE__ */ jsx(
16
42
  CheckboxPrimitive.Root,
17
43
  {
@@ -26,12 +52,16 @@ const CheckboxRoot = React.forwardRef(({ className, ...props }, ref) => {
26
52
  // service overriding --checkbox-checked-background got no fill change at all. Radix sets
27
53
  // `data-state`/`data-disabled` on the root, so the CSS rules match. Rendering is unchanged:
28
54
  // both knobs default to exactly the values these utilities hard-coded.
29
- "peer ui-checkbox aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground shrink-0 shadow-xs transition-shadow outline-none",
55
+ "peer ui-checkbox aria-invalid:border-destructive data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground shrink-0 shadow-xs transition-shadow outline-none",
30
56
  className
31
57
  ),
32
58
  ...props,
33
59
  ...identity,
34
- children: /* @__PURE__ */ jsx(CheckboxPrimitive.Indicator, { "data-slot": "checkbox-indicator", className: "ui-choice-indicator", children: /* @__PURE__ */ jsx(Check, { className: "ui-checkbox-icon", "aria-hidden": "true" }) })
60
+ onCheckedChange: (next) => {
61
+ setUncontrolled(next);
62
+ props.onCheckedChange?.(next);
63
+ },
64
+ children: /* @__PURE__ */ jsx(CheckboxPrimitive.Indicator, { "data-slot": "checkbox-indicator", className: "ui-choice-indicator", children: /* @__PURE__ */ jsx(CheckboxGlyph, { state }) })
35
65
  }
36
66
  );
37
67
  });
@@ -40,5 +70,6 @@ const Checkbox = Object.assign(CheckboxRoot, {
40
70
  Group: CheckboxGroup
41
71
  });
42
72
  export {
43
- Checkbox
73
+ Checkbox,
74
+ CheckboxVisual
44
75
  };
@@ -114,7 +114,7 @@ function FormField({
114
114
  asChild: true,
115
115
  id: labelId,
116
116
  className: "ui-inline-xs text-[length:var(--form-label-font-size)]",
117
- children: /* @__PURE__ */ jsxs(
117
+ children: /* @__PURE__ */ jsx(
118
118
  "span",
119
119
  {
120
120
  onClick: () => {
@@ -123,10 +123,10 @@ function FormField({
123
123
  const focusable = el.matches(FOCUSABLE_SELECTOR) ? el : el.querySelector(FOCUSABLE_SELECTOR);
124
124
  (focusable ?? el).focus();
125
125
  },
126
- children: [
127
- /* @__PURE__ */ jsx("span", { children: label }),
128
- required && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-destructive", children: "*" })
129
- ]
126
+ children: /* @__PURE__ */ jsxs("span", { children: [
127
+ label,
128
+ required && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "ui-form-field-required", children: "*" })
129
+ ] })
130
130
  }
131
131
  )
132
132
  }
@@ -63,5 +63,7 @@ export { Rating } from "./rating.js";
63
63
  export type { RatingProps } from "./rating.js";
64
64
  export { TagInput } from "./tag-input.js";
65
65
  export type { TagInputProps } from "./tag-input.js";
66
+ export { Segmented } from "./segmented.js";
67
+ export type { SegmentedOption, SegmentedProp, SegmentedProps } from "./segmented.js";
66
68
  export { BranchScopePicker } from "./branch-scope-picker.js";
67
69
  export type { BranchScopeModeProp, BranchScopeOptionProp, BranchScopePickerProp, BranchScopePickerProps, BranchScopeValueProp, } from "./branch-scope-picker.js";
@@ -51,6 +51,7 @@ import { PasswordStrength, usePasswordStrength } from "./password-strength.js";
51
51
  import { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } from "./input-otp.js";
52
52
  import { Rating } from "./rating.js";
53
53
  import { TagInput } from "./tag-input.js";
54
+ import { Segmented } from "./segmented.js";
54
55
  import { BranchScopePicker } from "./branch-scope-picker.js";
55
56
  export {
56
57
  BranchScopePicker,
@@ -93,6 +94,7 @@ export {
93
94
  SHOW_CHILD,
94
95
  SHOW_PARENT,
95
96
  SearchInput,
97
+ Segmented,
96
98
  Select,
97
99
  SelectContent,
98
100
  SelectGroup,
@@ -9,7 +9,7 @@ const inputBaseClass = [
9
9
  "ui-control ui-input border-input bg-background w-full rounded-[var(--control-radius)] transition-[color,box-shadow] outline-none",
10
10
  "selection:bg-primary selection:text-primary-foreground",
11
11
  "placeholder:text-muted-foreground",
12
- "aria-invalid:border-destructive aria-invalid:ring-destructive/20"
12
+ "aria-invalid:border-destructive"
13
13
  ];
14
14
  const Input = React.forwardRef(
15
15
  ({
@@ -27,7 +27,7 @@ const RadioItem = React.forwardRef(({ className, ...props }, ref) => /* @__PURE_
27
27
  // and reads --disabled-opacity. The utility was layered after components, so it silently
28
28
  // outranked that token — a service theme's --disabled-opacity never reached a radio.
29
29
  // Byte-identical: --disabled-opacity defaults to 0.5.
30
- "ui-radio aria-invalid:border-destructive aria-invalid:ring-destructive/20 shrink-0 shadow-xs transition-shadow outline-none",
30
+ "ui-radio aria-invalid:border-destructive shrink-0 shadow-xs transition-shadow outline-none",
31
31
  className
32
32
  ),
33
33
  ...props,
@@ -1,20 +1,4 @@
1
1
  import * as React from "react";
2
- import { type FieldA11yProps } from "../../lib/field-a11y.js";
3
- interface SearchInputProps extends FieldA11yProps {
4
- value?: string;
5
- defaultValue?: string;
6
- placeholder?: string;
7
- debounce?: number;
8
- /** Fires on EVERY keystroke (immediate) — required to keep a controlled `value` responsive. */
9
- onValueChange?: (q: string) => void;
10
- /** Fires with the DEBOUNCED term. Optional — omit it when you drive filtering off `onValueChange`. */
11
- onSearch?: (q: string) => void;
12
- label?: React.ReactNode;
13
- ariaLabel?: string;
14
- className?: string;
15
- inputClassName?: string;
16
- id?: string;
17
- disabled?: boolean;
18
- }
2
+ import type { SearchInputProp } from "../../props/components/data-entry.prop.js";
3
+ export type SearchInputProps = SearchInputProp;
19
4
  export declare function SearchInput({ value: controlledValue, defaultValue, placeholder, debounce, onValueChange, onSearch, label, ariaLabel, className, inputClassName, id, disabled, ...ariaProps }: SearchInputProps): React.JSX.Element;
20
- export {};
@@ -37,7 +37,10 @@ function SearchInput({
37
37
  React.useEffect(() => {
38
38
  onSearchRef.current = onSearch;
39
39
  });
40
+ const lastSearch = React.useRef(debounced);
40
41
  React.useEffect(() => {
42
+ if (lastSearch.current === debounced) return;
43
+ lastSearch.current = debounced;
41
44
  onSearchRef.current?.(debounced);
42
45
  }, [debounced]);
43
46
  const setValue = (v) => {
@@ -0,0 +1,2 @@
1
+ export { Segmented } from "../ui/segmented.js";
2
+ export type { SegmentedOption, SegmentedProp, SegmentedProps } from "../ui/segmented.js";
@@ -0,0 +1,5 @@
1
+ "use client";
2
+ import { Segmented } from "../ui/segmented.js";
3
+ export {
4
+ Segmented
5
+ };
@@ -25,6 +25,8 @@ export declare const SelectTrigger: React.ForwardRefExoticComponent<Omit<SelectP
25
25
  * `full` (default) fills the field column — right inside a FormField. `auto` sizes to the
26
26
  * selected label — right in a PageContainer `extra` slot, a toolbar or a footer row, where a
27
27
  * full-width trigger swallows the row and starves its siblings (text truncated to "UA…").
28
+ * `bounded` holds one width from `--control-bounded-width`, for a trigger whose VALUE varies
29
+ * in length and must not move its neighbours when it changes (gh#375).
28
30
  */
29
31
  width?: ControlWidthProp;
30
32
  /**
@@ -98,8 +98,13 @@ const SelectTrigger = React.forwardRef(({ className, children, size = "md", widt
98
98
  "data-width": width,
99
99
  className: cn(
100
100
  controlTriggerClass,
101
- width === "auto" ? "w-auto" : "w-full",
102
- "aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground whitespace-nowrap transition-[color,box-shadow] outline-none *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center",
101
+ // `bounded` deliberately emits NO width utility: its width is owned by the
102
+ // `[data-width="bounded"]` rule in control.css. A utility here would win the layer order
103
+ // and make that rule — and therefore the token — dead, the way `w-full` did to
104
+ // `.ui-app-setting-picker-icon` (gh#366, gh#371).
105
+ width === "auto" && "w-auto",
106
+ width === "full" && "w-full",
107
+ "aria-invalid:border-destructive data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground whitespace-nowrap transition-[color,box-shadow] outline-none *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center",
103
108
  className
104
109
  ),
105
110
  ...props,
@@ -143,11 +148,11 @@ const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) =
143
148
  ));
144
149
  SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
145
150
  const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => {
146
- useInertHiddenBackground();
151
+ const contentRef = useInertHiddenBackground(ref);
147
152
  return /* @__PURE__ */ jsx(SelectPrimitive.Portal, { children: /* @__PURE__ */ jsxs(
148
153
  SelectPrimitive.Content,
149
154
  {
150
- ref,
155
+ ref: contentRef,
151
156
  "data-slot": "select-content",
152
157
  className: cn(
153
158
  "ui-select-content data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
@@ -127,7 +127,7 @@ const SheetContent = React.forwardRef(
127
127
  DialogPrimitive.Close,
128
128
  {
129
129
  "data-slot": "sheet-close",
130
- className: "ui-sheet-close ring-offset-background focus:ring-ring focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none",
130
+ className: "ui-sheet-close ui-focus-ring disabled:pointer-events-none",
131
131
  children: [
132
132
  /* @__PURE__ */ jsx(X, { className: "ui-sheet-close-icon", "aria-hidden": "true" }),
133
133
  /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("feedback.alert.dismiss") })
@@ -1,4 +1,10 @@
1
1
  import * as React from "react";
2
2
  import { type ToasterProps } from "sonner";
3
- declare function Toaster({ style, ...props }: ToasterProps): React.JSX.Element;
3
+ /**
4
+ * `richColors` defaults ON: it is the switch that makes sonner apply the per-type properties at
5
+ * all (`[data-rich-colors='true'][data-type='success'] { … }`), so without it the tokens above
6
+ * are declared and never read. A consumer can still pass `richColors={false}` for a deliberately
7
+ * monochrome stack.
8
+ */
9
+ declare function Toaster({ style, richColors, ...props }: ToasterProps): React.JSX.Element;
4
10
  export { Toaster };
@@ -10,22 +10,42 @@ function useDocumentTheme() {
10
10
  const mq = window.matchMedia("(prefers-color-scheme: dark)");
11
11
  mq.addEventListener("change", onStoreChange);
12
12
  const obs = new MutationObserver(onStoreChange);
13
- obs.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
13
+ obs.observe(document.documentElement, {
14
+ attributes: true,
15
+ attributeFilter: ["class", "data-theme"]
16
+ });
14
17
  return () => {
15
18
  mq.removeEventListener("change", onStoreChange);
16
19
  obs.disconnect();
17
20
  };
18
21
  },
19
- () => document.documentElement.classList.contains("dark") ? "dark" : "light",
22
+ () => document.documentElement.dataset.theme === "dark" || document.documentElement.classList.contains("dark") ? "dark" : "light",
20
23
  () => "light"
21
24
  );
22
25
  }
23
- function Toaster({ style, ...props }) {
26
+ const TOAST_TONES = [
27
+ { type: "success", hue: "success", ink: "text-success" },
28
+ { type: "error", hue: "destructive", ink: "text-error" },
29
+ { type: "warning", hue: "warning", ink: "text-warning" },
30
+ { type: "info", hue: "info", ink: "text-info" }
31
+ ];
32
+ const toneVars = Object.fromEntries(
33
+ TOAST_TONES.flatMap(({ type, hue, ink }) => [
34
+ [
35
+ `--${type}-bg`,
36
+ `color-mix(in srgb, hsl(var(--${hue})) calc(var(--alert-bg-alpha) * 100%), hsl(var(--popover)))`
37
+ ],
38
+ [`--${type}-border`, `hsl(var(--${hue}) / var(--alert-border-alpha))`],
39
+ [`--${type}-text`, `hsl(var(--${ink}))`]
40
+ ])
41
+ );
42
+ function Toaster({ style, richColors = true, ...props }) {
24
43
  const theme = useDocumentTheme();
25
44
  return /* @__PURE__ */ jsx(
26
45
  Sonner,
27
46
  {
28
47
  theme,
48
+ richColors,
29
49
  className: "toaster group",
30
50
  icons: {
31
51
  success: /* @__PURE__ */ jsx(CheckCircle2, { className: "ui-toast-icon", "aria-hidden": "true" }),
@@ -43,10 +63,10 @@ function Toaster({ style, ...props }) {
43
63
  "--normal-text": "hsl(var(--popover-foreground))",
44
64
  "--normal-border": "hsl(var(--border))",
45
65
  "--border-radius": "var(--radius)",
46
- // MERGED, never spread over.
47
- // object and wiped all four vars and an unset `--normal-bg` renders the toast
48
- // transparent.
49
- // var by name.
66
+ ...toneVars,
67
+ // MERGED LAST, never spread over: a consumer's `style` is an override of these
68
+ // defaults, one custom property at a time. Assigning it instead of merging would
69
+ // wipe every var above, and an unset `--normal-bg` renders the toast transparent.
50
70
  ...style
51
71
  },
52
72
  position: "bottom-right",
@@ -10,6 +10,8 @@ export declare const Button: React.ForwardRefExoticComponent<React.ButtonHTMLAtt
10
10
  size?: import("../../props/index.js").ButtonSizeProp;
11
11
  shape?: import("../../props/index.js").ShapeProp;
12
12
  fullWidth?: boolean;
13
+ wrap?: boolean;
14
+ align?: import("../../props/index.js").TextAlignProp;
13
15
  asChild?: import("../../props/index.js").AsChildProp;
14
16
  onClick?: import("../../props/index.js").OnClickProp;
15
17
  disabled?: import("../../props/index.js").DisabledProp;
@@ -9,7 +9,12 @@ import { useTranslation } from "../../i18n/use-translation.js";
9
9
  const buttonVariants = cva("ui-button", {
10
10
  variants: {
11
11
  variant: {
12
- default: "ui-button--default bg-primary text-primary-foreground hover:bg-primary/90",
12
+ // Colour is the TOKEN LAYER's (`.ui-button--default` in styles/control.css), not a utility's.
13
+ // The three utilities that used to sit here — `bg-primary text-primary-foreground
14
+ // hover:bg-primary/90` — restated the fill the components layer already declares and, being
15
+ // utilities, out-ranked it: the hover step this library generates from antd's algorithm
16
+ // (`--primary-hover`) could never take effect while `hover:bg-primary/90` was emitted here.
17
+ default: "ui-button--default",
13
18
  destructive: "ui-button--destructive bg-destructive text-destructive-foreground",
14
19
  outline: "ui-button--outline border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
15
20
  dashed: "ui-button--dashed border border-dashed bg-background hover:bg-accent hover:text-accent-foreground",
@@ -47,6 +52,8 @@ const Button = React.forwardRef(
47
52
  size,
48
53
  shape,
49
54
  fullWidth = false,
55
+ wrap = false,
56
+ align = "center",
50
57
  asChild = false,
51
58
  loading = false,
52
59
  loadingText,
@@ -76,13 +83,15 @@ const Button = React.forwardRef(
76
83
  "data-size": size ?? "default",
77
84
  "data-shape": shape ?? "default",
78
85
  "data-full-width": fullWidth ? "" : void 0,
86
+ "data-wrap": wrap ? "" : void 0,
87
+ "data-align": align === "center" ? void 0 : align,
79
88
  "data-loading": isLoading ? "" : void 0,
80
89
  "aria-busy": isLoading || void 0,
81
90
  disabled: isLoading || disabled,
82
91
  type: asChild ? void 0 : type ?? "button",
83
92
  className: cn(
84
93
  fullWidth && "w-full",
85
- "aria-invalid:border-destructive aria-invalid:ring-destructive/20",
94
+ "aria-invalid:border-destructive",
86
95
  "[&_svg]:pointer-events-none [&_svg]:shrink-0",
87
96
  buttonVariants({ variant, size, shape, className })
88
97
  ),