@stll/ui 0.59.1 → 0.61.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 (60) hide show
  1. package/README.md +120 -4
  2. package/dist/components/accordion.d.ts +17 -3
  3. package/dist/components/accordion.js +17 -3
  4. package/dist/components/button.d.ts +56 -5
  5. package/dist/components/button.js +97 -15
  6. package/dist/components/button.native-classes.d.ts +17 -0
  7. package/dist/components/button.native-classes.js +176 -0
  8. package/dist/components/button.native.d.ts +5 -2
  9. package/dist/components/button.native.js +17 -134
  10. package/dist/components/button.shared.d.ts +24 -2
  11. package/dist/components/button.shared.js +15 -1
  12. package/dist/components/collapsible.d.ts +26 -0
  13. package/dist/components/collapsible.js +40 -0
  14. package/dist/components/combobox.d.ts +3 -1
  15. package/dist/components/combobox.js +3 -2
  16. package/dist/components/date-picker-popover.d.ts +2 -0
  17. package/dist/components/date-picker-popover.js +5 -3
  18. package/dist/components/details-grid.d.ts +21 -2
  19. package/dist/components/details-grid.js +75 -20
  20. package/dist/components/field.d.ts +6 -0
  21. package/dist/components/field.js +6 -0
  22. package/dist/components/form.d.ts +43 -3
  23. package/dist/components/form.js +53 -3
  24. package/dist/components/input-control.d.ts +9 -0
  25. package/dist/components/input-control.js +10 -1
  26. package/dist/components/input.d.ts +7 -1
  27. package/dist/components/input.js +4 -4
  28. package/dist/components/label.d.ts +24 -2
  29. package/dist/components/label.js +21 -2
  30. package/dist/components/number-input.d.ts +9 -2
  31. package/dist/components/number-input.js +18 -6
  32. package/dist/components/search-field.js +1 -0
  33. package/dist/components/segmented-icon-toggle.d.ts +2 -1
  34. package/dist/components/segmented-icon-toggle.js +18 -19
  35. package/dist/components/switch.d.ts +21 -2
  36. package/dist/components/switch.js +44 -5
  37. package/dist/components/table.d.ts +16 -2
  38. package/dist/components/table.js +5 -4
  39. package/dist/components/textarea.d.ts +14 -1
  40. package/dist/components/textarea.js +11 -5
  41. package/dist/components/toggle-group.d.ts +32 -0
  42. package/dist/components/toggle-group.js +45 -0
  43. package/dist/components/toggle.d.ts +26 -0
  44. package/dist/components/toggle.js +26 -0
  45. package/dist/index.d.ts +14 -7
  46. package/dist/index.js +9 -3
  47. package/dist/kanban/column-bands.js +1 -1
  48. package/dist/kanban/index.d.ts +5 -2
  49. package/dist/kanban/index.js +4 -1
  50. package/dist/kanban/sortable-edge.d.ts +39 -0
  51. package/dist/kanban/sortable-edge.js +27 -0
  52. package/dist/kanban/sortable-interactions.d.ts +145 -0
  53. package/dist/kanban/sortable-interactions.js +503 -0
  54. package/dist/kanban/sortable-interactions.logic.d.ts +79 -0
  55. package/dist/kanban/sortable-interactions.logic.js +210 -0
  56. package/dist/kanban/subgroup-board.d.ts +27 -2
  57. package/dist/kanban/subgroup-board.js +26 -1
  58. package/dist/kanban/virtual-cell.d.ts +18 -1
  59. package/dist/kanban/virtual-cell.js +70 -17
  60. package/package.json +17 -1
@@ -1,26 +1,81 @@
1
1
  import { cn } from "../lib/utils.js";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { createContext, useContext } from "react";
3
4
  //#region src/components/details-grid.tsx
4
- const DetailsGrid = ({ className, ...props }) => /* @__PURE__ */ jsx("div", {
5
- className: "@container min-w-0 [contain-intrinsic-inline-size:20rem]",
6
- "data-slot": "details-grid-container",
7
- children: /* @__PURE__ */ jsx("dl", {
8
- className: cn("grid grid-cols-[repeat(auto-fit,minmax(min(100%,20rem),1fr))] gap-x-6 gap-y-2 font-sans @min-[64rem]:grid-cols-3", className),
9
- "data-slot": "details-grid",
10
- ...props
11
- })
12
- });
13
- const DetailsItem = ({ label, children, span, className, ...props }) => /* @__PURE__ */ jsxs("div", {
14
- className: cn("grid min-w-0 grid-cols-[minmax(0,7rem)_minmax(0,1fr)] items-start gap-x-4 text-xs", span === "wide" && "col-span-full", className),
15
- "data-slot": "details-item",
16
- ...props,
17
- children: [/* @__PURE__ */ jsx("dt", {
18
- className: "text-foreground-disabled min-w-0 font-medium tracking-wide wrap-anywhere uppercase",
19
- children: label
20
- }), /* @__PURE__ */ jsx("dd", {
21
- className: "text-foreground-strong-muted min-w-0 wrap-anywhere",
22
- children
23
- })]
5
+ const DetailsShapeContext = createContext({
6
+ orientation: "horizontal",
7
+ variant: "sheet"
24
8
  });
9
+ const LIST_CLASS_NAME = {
10
+ horizontal: "grid grid-cols-[auto_minmax(0,1fr)] items-center gap-x-4 gap-y-1 text-sm",
11
+ vertical: "flex min-w-0 flex-col gap-2 text-sm"
12
+ };
13
+ const ITEM_CLASS_NAME = {
14
+ list: {
15
+ horizontal: "col-span-full grid grid-cols-subgrid items-center",
16
+ vertical: "flex min-w-0 flex-col gap-2"
17
+ },
18
+ sheet: {
19
+ horizontal: "grid min-w-0 grid-cols-[minmax(0,7rem)_minmax(0,1fr)] items-start gap-x-4 text-xs",
20
+ vertical: "flex min-w-0 flex-col gap-1 text-xs"
21
+ }
22
+ };
23
+ const LABEL_CLASS_NAME = {
24
+ list: {
25
+ horizontal: "text-muted-foreground min-w-0",
26
+ vertical: "font-medium"
27
+ },
28
+ sheet: {
29
+ horizontal: "text-foreground-disabled min-w-0 font-medium tracking-wide wrap-anywhere uppercase",
30
+ vertical: "text-foreground-disabled min-w-0 font-medium tracking-wide wrap-anywhere uppercase"
31
+ }
32
+ };
33
+ const VALUE_CLASS_NAME = {
34
+ list: "min-w-0 wrap-anywhere",
35
+ sheet: "text-foreground-strong-muted min-w-0 wrap-anywhere"
36
+ };
37
+ const DetailsGrid = ({ className, orientation = "horizontal", variant = "sheet", ...props }) => {
38
+ const shape = {
39
+ orientation,
40
+ variant
41
+ };
42
+ if (variant === "list") return /* @__PURE__ */ jsx(DetailsShapeContext.Provider, {
43
+ value: shape,
44
+ children: /* @__PURE__ */ jsx("dl", {
45
+ className: cn(LIST_CLASS_NAME[orientation], className),
46
+ "data-orientation": orientation,
47
+ "data-slot": "details-grid",
48
+ "data-variant": variant,
49
+ ...props
50
+ })
51
+ });
52
+ return /* @__PURE__ */ jsx(DetailsShapeContext.Provider, {
53
+ value: shape,
54
+ children: /* @__PURE__ */ jsx("div", {
55
+ className: "@container min-w-0 [contain-intrinsic-inline-size:20rem]",
56
+ "data-slot": "details-grid-container",
57
+ children: /* @__PURE__ */ jsx("dl", {
58
+ className: cn("grid grid-cols-[repeat(auto-fit,minmax(min(100%,20rem),1fr))] gap-x-6 gap-y-2 font-sans @min-[64rem]:grid-cols-3", className),
59
+ "data-slot": "details-grid",
60
+ ...props
61
+ })
62
+ })
63
+ });
64
+ };
65
+ const DetailsItem = ({ label, children, span, className, ...props }) => {
66
+ const { orientation, variant } = useContext(DetailsShapeContext);
67
+ return /* @__PURE__ */ jsxs("div", {
68
+ className: cn(ITEM_CLASS_NAME[variant][orientation], variant === "sheet" && span === "wide" && "col-span-full", className),
69
+ "data-slot": "details-item",
70
+ ...props,
71
+ children: [/* @__PURE__ */ jsx("dt", {
72
+ className: LABEL_CLASS_NAME[variant][orientation],
73
+ children: label
74
+ }), /* @__PURE__ */ jsx("dd", {
75
+ className: VALUE_CLASS_NAME[variant],
76
+ children
77
+ })]
78
+ });
79
+ };
25
80
  //#endregion
26
81
  export { DetailsGrid, DetailsItem };
@@ -4,6 +4,12 @@ declare const Field: ({ className, ...props }: Field$1.Root.Props) => import("re
4
4
  declare const FieldLabel: ({ className, ...props }: Field$1.Label.Props) => import("react").JSX.Element;
5
5
  declare const FieldItem: ({ className, ...props }: Field$1.Item.Props) => import("react").JSX.Element;
6
6
  declare const FieldDescription: ({ className, ...props }: Field$1.Description.Props) => import("react").JSX.Element;
7
+ /**
8
+ * The message for an invalid field. With no `match` it shows whatever error
9
+ * the field has, including one the `Form` received in `errors` from the
10
+ * server; with a validity key (`match="valueMissing"`) it shows only for that
11
+ * constraint, so each rule can be worded on its own. See `Form`.
12
+ */
7
13
  declare const FieldError: ({ className, ...props }: Field$1.Error.Props) => import("react").JSX.Element;
8
14
  declare const FieldControl: import("react").ForwardRefExoticComponent<Omit<import("@base-ui/react").FieldControlProps, "ref"> & import("react").RefAttributes<HTMLElement>>;
9
15
  declare const FieldValidity: import("react").FC<import("@base-ui/react").FieldValidityProps>;
@@ -23,6 +23,12 @@ const FieldDescription = ({ className, ...props }) => /* @__PURE__ */ jsx(Field$
23
23
  "data-slot": "field-description",
24
24
  ...props
25
25
  });
26
+ /**
27
+ * The message for an invalid field. With no `match` it shows whatever error
28
+ * the field has, including one the `Form` received in `errors` from the
29
+ * server; with a validity key (`match="valueMissing"`) it shows only for that
30
+ * constraint, so each rule can be worded on its own. See `Form`.
31
+ */
26
32
  const FieldError = ({ className, ...props }) => /* @__PURE__ */ jsx(Field$1.Error, {
27
33
  className: cn("text-destructive-foreground text-xs", className),
28
34
  "data-slot": "field-error",
@@ -1,8 +1,48 @@
1
1
  import { Form as Form$1 } from "@base-ui/react/form";
2
+ import { VariantProps } from "class-variance-authority";
2
3
  //#region src/components/form.d.ts
3
- declare const Form: ({ className, dirty, onDiscard, children, ...props }: Form$1.Props & {
4
+ declare const formVariants: (props?: ({
5
+ density?: "compact" | "default" | "snug" | null | undefined;
6
+ layout?: "inline" | "stack" | null | undefined;
7
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
+ /**
9
+ * Errors a server returned for the last submit, keyed by each `Field`'s
10
+ * `name`: one message or several.
11
+ */
12
+ type FormErrors = NonNullable<Form$1.Props["errors"]>;
13
+ type FormProps = Form$1.Props & VariantProps<typeof formVariants> & {
4
14
  dirty?: boolean;
5
15
  onDiscard?: (() => void) | undefined;
6
- }) => import("react").JSX.Element;
16
+ };
17
+ /**
18
+ * A form whose fields report their own errors.
19
+ *
20
+ * Validation, in the order a reader meets it:
21
+ *
22
+ * - **In the browser.** A `Field` reads its control's constraints
23
+ * (`required`, `type`, `pattern`, `min`/`max`, a `validate` function) and
24
+ * shows a `FieldError` for the ones that fail. `validationMode` stays
25
+ * Base UI's `onSubmit`: nothing is flagged while a person tabs through an
26
+ * empty form, the first submit marks every invalid field at once, and from
27
+ * then on each field re-validates as it changes. `onBlur` would flag a
28
+ * required field the moment focus passes it, before anyone tried to submit;
29
+ * pass it only where checking a value as soon as it is left is the point
30
+ * (an availability check, a long identifier).
31
+ * - **From the server.** Pass what the server rejected as `errors`, keyed by
32
+ * field `name` (`{ email: "Already invited" }`). The field turns invalid,
33
+ * its control gets `aria-invalid`, and its `FieldError` shows the message.
34
+ * Base UI drops a field's server error as soon as its value changes, so a
35
+ * correction clears the message without a round trip. Keep the errors in
36
+ * state and replace them on the next submit.
37
+ *
38
+ * In a `FieldError`, leave `match` unset to show whichever error the field
39
+ * has, the server's included; `match="valueMissing"` (or another validity
40
+ * key) words one constraint in the product's voice; `match` set to `true`
41
+ * hands visibility to the caller.
42
+ *
43
+ * `dirty` and `onDiscard` register the form with an enclosing `Dialog`'s
44
+ * unsaved-changes guard.
45
+ */
46
+ declare const Form: ({ className, density, layout, dirty, onDiscard, children, ...props }: FormProps) => import("react").JSX.Element;
7
47
  //#endregion
8
- export { Form };
48
+ export { Form, type FormErrors, type FormProps, formVariants };
@@ -2,10 +2,60 @@
2
2
  import { cn } from "../lib/utils.js";
3
3
  import { DialogFormState } from "./dialog-form-state.js";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { cva } from "class-variance-authority";
5
6
  import { Form as Form$1 } from "@base-ui/react/form";
6
7
  //#region src/components/form.tsx
7
- const Form = ({ className, dirty = false, onDiscard, children, ...props }) => /* @__PURE__ */ jsxs(Form$1, {
8
- className: cn("flex w-full flex-col gap-4", className),
8
+ const formVariants = cva("flex w-full", {
9
+ defaultVariants: {
10
+ density: "default",
11
+ layout: "stack"
12
+ },
13
+ variants: {
14
+ density: {
15
+ default: "gap-4",
16
+ snug: "gap-3",
17
+ compact: "gap-2"
18
+ },
19
+ layout: {
20
+ stack: "flex-col",
21
+ inline: "flex-row items-center"
22
+ }
23
+ }
24
+ });
25
+ /**
26
+ * A form whose fields report their own errors.
27
+ *
28
+ * Validation, in the order a reader meets it:
29
+ *
30
+ * - **In the browser.** A `Field` reads its control's constraints
31
+ * (`required`, `type`, `pattern`, `min`/`max`, a `validate` function) and
32
+ * shows a `FieldError` for the ones that fail. `validationMode` stays
33
+ * Base UI's `onSubmit`: nothing is flagged while a person tabs through an
34
+ * empty form, the first submit marks every invalid field at once, and from
35
+ * then on each field re-validates as it changes. `onBlur` would flag a
36
+ * required field the moment focus passes it, before anyone tried to submit;
37
+ * pass it only where checking a value as soon as it is left is the point
38
+ * (an availability check, a long identifier).
39
+ * - **From the server.** Pass what the server rejected as `errors`, keyed by
40
+ * field `name` (`{ email: "Already invited" }`). The field turns invalid,
41
+ * its control gets `aria-invalid`, and its `FieldError` shows the message.
42
+ * Base UI drops a field's server error as soon as its value changes, so a
43
+ * correction clears the message without a round trip. Keep the errors in
44
+ * state and replace them on the next submit.
45
+ *
46
+ * In a `FieldError`, leave `match` unset to show whichever error the field
47
+ * has, the server's included; `match="valueMissing"` (or another validity
48
+ * key) words one constraint in the product's voice; `match` set to `true`
49
+ * hands visibility to the caller.
50
+ *
51
+ * `dirty` and `onDiscard` register the form with an enclosing `Dialog`'s
52
+ * unsaved-changes guard.
53
+ */
54
+ const Form = ({ className, density, layout, dirty = false, onDiscard, children, ...props }) => /* @__PURE__ */ jsxs(Form$1, {
55
+ className: cn(formVariants({
56
+ density,
57
+ layout
58
+ }), className),
9
59
  "data-slot": "form",
10
60
  ...props,
11
61
  children: [/* @__PURE__ */ jsx(DialogFormState, {
@@ -14,4 +64,4 @@ const Form = ({ className, dirty = false, onDiscard, children, ...props }) => /*
14
64
  }), children]
15
65
  });
16
66
  //#endregion
17
- export { Form };
67
+ export { Form, formVariants };
@@ -16,4 +16,13 @@ export declare const INPUT_SIZE_CLASS_NAMES: {
16
16
  readonly default: undefined;
17
17
  readonly lg: "min-h-9.5 leading-9.5 sm:min-h-8.5 sm:leading-8.5";
18
18
  };
19
+ /**
20
+ * A field edited where it is read: an option's name in a list, a page
21
+ * heading, the summary under it. No box until focused, the type, leading and
22
+ * colour of the text around it, so the row keeps the height of its text; a
23
+ * focus ring marks it while editing. A slim inline inset gives the ring and
24
+ * the caret room, and a matching negative margin keeps the text in line with
25
+ * its neighbours.
26
+ */
27
+ export declare const INLINE_FIELD_CLASS_NAME = "placeholder:text-foreground-placeholder focus-visible:ring-ring/30 -mx-1 min-h-0 w-full min-w-0 rounded-sm bg-transparent px-1 py-0 font-[inherit] text-[length:inherit] leading-[inherit] text-inherit outline-none focus-visible:ring-2";
19
28
  //#endregion
@@ -17,5 +17,14 @@ const INPUT_SIZE_CLASS_NAMES = {
17
17
  [CONTROL_SIZE.default]: void 0,
18
18
  [CONTROL_SIZE.lg]: "min-h-9.5 leading-9.5 sm:min-h-8.5 sm:leading-8.5"
19
19
  };
20
+ /**
21
+ * A field edited where it is read: an option's name in a list, a page
22
+ * heading, the summary under it. No box until focused, the type, leading and
23
+ * colour of the text around it, so the row keeps the height of its text; a
24
+ * focus ring marks it while editing. A slim inline inset gives the ring and
25
+ * the caret room, and a matching negative margin keeps the text in line with
26
+ * its neighbours.
27
+ */
28
+ const INLINE_FIELD_CLASS_NAME = "placeholder:text-foreground-placeholder focus-visible:ring-ring/30 -mx-1 min-h-0 w-full min-w-0 rounded-sm bg-transparent px-1 py-0 font-[inherit] text-[length:inherit] leading-[inherit] text-inherit outline-none focus-visible:ring-2";
20
29
  //#endregion
21
- export { INPUT_CONTROL_CLASS_NAME, INPUT_ELEMENT_CLASS_NAME, INPUT_SIZE_CLASS_NAMES };
30
+ export { INLINE_FIELD_CLASS_NAME, INPUT_CONTROL_CLASS_NAME, INPUT_ELEMENT_CLASS_NAME, INPUT_SIZE_CLASS_NAMES };
@@ -7,8 +7,14 @@ type InputProps = Omit<Input$1.Props & React$1.RefAttributes<HTMLInputElement>,
7
7
  size?: ControlSize | number;
8
8
  style?: React$1.CSSProperties;
9
9
  unstyled?: boolean;
10
+ /**
11
+ * A borderless field in a line of text: it edits a value where it is read,
12
+ * with no box until focused and the parent's type, leading and colour. See
13
+ * `INLINE_FIELD_CLASS_NAME`.
14
+ */
15
+ inline?: boolean;
10
16
  nativeInput?: boolean;
11
17
  };
12
- declare const Input: ({ className, size, unstyled, nativeInput, dir, onChange, ...props }: InputProps) => React$1.JSX.Element;
18
+ declare const Input: ({ className, size, unstyled, inline, nativeInput, dir, onChange, ...props }: InputProps) => React$1.JSX.Element;
13
19
  //#endregion
14
20
  export { Input, type InputProps };
@@ -3,11 +3,11 @@ import { SearchIcon } from "../icons.js";
3
3
  import { cn } from "../lib/utils.js";
4
4
  import { CONTROL_SIZE } from "../lib/control-size.js";
5
5
  import { isStructuredInputType, useContentDir } from "../hooks/use-content-dir.js";
6
- import { INPUT_ELEMENT_CLASS_NAME, INPUT_SIZE_CLASS_NAMES } from "./input-control.js";
6
+ import { INLINE_FIELD_CLASS_NAME, INPUT_ELEMENT_CLASS_NAME, INPUT_SIZE_CLASS_NAMES } from "./input-control.js";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
8
  import { Input as Input$1 } from "@base-ui/react/input";
9
9
  //#region src/components/input.tsx
10
- const Input = ({ className, size = CONTROL_SIZE.default, unstyled = false, nativeInput = false, dir, onChange, ...props }) => {
10
+ const Input = ({ className, size = CONTROL_SIZE.default, unstyled = false, inline = false, nativeInput = false, dir, onChange, ...props }) => {
11
11
  const controlSize = typeof size === "number" ? CONTROL_SIZE.default : size;
12
12
  const contentDir = useContentDir({
13
13
  dir: dir ?? (isStructuredInputType(props.type) ? "ltr" : void 0),
@@ -18,9 +18,9 @@ const Input = ({ className, size = CONTROL_SIZE.default, unstyled = false, nativ
18
18
  contentDir.trackValue(event.currentTarget.value);
19
19
  onChange?.(event);
20
20
  };
21
- const inputClassName = cn(INPUT_ELEMENT_CLASS_NAME, INPUT_SIZE_CLASS_NAMES[controlSize], unstyled && "px-0", props.type === "search" && "ps-8 [&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none");
21
+ const inputClassName = cn(inline ? INLINE_FIELD_CLASS_NAME : INPUT_ELEMENT_CLASS_NAME, !inline && INPUT_SIZE_CLASS_NAMES[controlSize], unstyled && !inline && "px-0", props.type === "search" && "ps-8 [&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none");
22
22
  return /* @__PURE__ */ jsxs("span", {
23
- className: cn(!unstyled && "border-input bg-background text-foreground ring-ring/24 has-autofill:bg-foreground/4 has-focus-visible:border-ring has-aria-invalid:border-destructive/36 has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-aria-invalid:ring-destructive/24 relative inline-flex w-full rounded-lg border text-base shadow-xs/5 not-dark:bg-clip-padding before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] has-focus-visible:ring-[3px] has-disabled:opacity-64 has-[:disabled,:focus-visible,[aria-invalid]]:shadow-none sm:text-sm dark:not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)] pointer-coarse:min-h-11", className) || void 0,
23
+ className: cn(inline ? "relative inline-flex w-full" : !unstyled && "border-input bg-background text-foreground ring-ring/24 has-autofill:bg-foreground/4 has-focus-visible:border-ring has-aria-invalid:border-destructive/36 has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-aria-invalid:ring-destructive/24 relative inline-flex w-full rounded-lg border text-base shadow-xs/5 not-dark:bg-clip-padding before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] has-focus-visible:ring-[3px] has-disabled:opacity-64 has-[:disabled,:focus-visible,[aria-invalid]]:shadow-none sm:text-sm dark:not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)] pointer-coarse:min-h-11", className) || void 0,
24
24
  "data-size": size,
25
25
  "data-slot": "input-control",
26
26
  dir: contentDir.dir,
@@ -1,5 +1,27 @@
1
1
  import { useRender } from "@base-ui/react/use-render";
2
2
  //#region src/components/label.d.ts
3
- declare function Label({ className, render, ...props }: useRender.ComponentProps<"label">): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
3
+ /**
4
+ * - `default`: the caption above or beside a control.
5
+ * - `choice`: one row of a list of choices, wrapping its radio or checkbox so
6
+ * the whole row picks it; it highlights on hover and rings while the
7
+ * control inside has keyboard focus.
8
+ * - `choice-card`: a bordered card wrapping its radio, for choices that carry
9
+ * a description; the chosen card takes the primary border.
10
+ * - `muted`: a quiet caption in small type, for a filter bar or a dense side
11
+ * form where the controls, not their names, carry the eye.
12
+ *
13
+ * The choice variants dim and stop inviting a click while their control is
14
+ * disabled.
15
+ */
16
+ declare const LABEL_VARIANT_CLASS_NAME: {
17
+ readonly default: "text-foreground inline-flex items-center gap-2 text-base/4.5 font-medium sm:text-sm/4";
18
+ readonly choice: "hover:bg-muted/60 has-focus-visible:ring-ring flex min-h-11 cursor-pointer items-center gap-2 rounded-sm px-1 py-1.5 text-sm has-focus-visible:ring-2 has-disabled:cursor-not-allowed has-disabled:opacity-64";
19
+ readonly "choice-card": "has-checked:border-primary has-checked:bg-muted/50 flex min-h-11 cursor-pointer items-start gap-3 rounded-md border p-3 has-disabled:cursor-not-allowed has-disabled:opacity-50";
20
+ readonly muted: "text-muted-foreground inline-flex items-center gap-2 text-xs/4 font-medium";
21
+ };
22
+ type LabelVariant = keyof typeof LABEL_VARIANT_CLASS_NAME;
23
+ declare function Label({ className, render, variant, ...props }: useRender.ComponentProps<"label"> & {
24
+ variant?: LabelVariant;
25
+ }): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
4
26
  //#endregion
5
- export { Label };
27
+ export { Label, type LabelVariant };
@@ -3,9 +3,28 @@ import { cn } from "../lib/utils.js";
3
3
  import { mergeProps } from "@base-ui/react/merge-props";
4
4
  import { useRender } from "@base-ui/react/use-render";
5
5
  //#region src/components/label.tsx
6
- function Label({ className, render, ...props }) {
6
+ /**
7
+ * - `default`: the caption above or beside a control.
8
+ * - `choice`: one row of a list of choices, wrapping its radio or checkbox so
9
+ * the whole row picks it; it highlights on hover and rings while the
10
+ * control inside has keyboard focus.
11
+ * - `choice-card`: a bordered card wrapping its radio, for choices that carry
12
+ * a description; the chosen card takes the primary border.
13
+ * - `muted`: a quiet caption in small type, for a filter bar or a dense side
14
+ * form where the controls, not their names, carry the eye.
15
+ *
16
+ * The choice variants dim and stop inviting a click while their control is
17
+ * disabled.
18
+ */
19
+ const LABEL_VARIANT_CLASS_NAME = {
20
+ default: "text-foreground inline-flex items-center gap-2 text-base/4.5 font-medium sm:text-sm/4",
21
+ choice: "hover:bg-muted/60 has-focus-visible:ring-ring flex min-h-11 cursor-pointer items-center gap-2 rounded-sm px-1 py-1.5 text-sm has-focus-visible:ring-2 has-disabled:cursor-not-allowed has-disabled:opacity-64",
22
+ "choice-card": "has-checked:border-primary has-checked:bg-muted/50 flex min-h-11 cursor-pointer items-start gap-3 rounded-md border p-3 has-disabled:cursor-not-allowed has-disabled:opacity-50",
23
+ muted: "text-muted-foreground inline-flex items-center gap-2 text-xs/4 font-medium"
24
+ };
25
+ function Label({ className, render, variant = "default", ...props }) {
7
26
  const defaultProps = {
8
- className: cn("text-foreground inline-flex items-center gap-2 text-base/4.5 font-medium sm:text-sm/4", className),
27
+ className: cn(LABEL_VARIANT_CLASS_NAME[variant], className),
9
28
  "data-slot": "label"
10
29
  };
11
30
  return useRender({
@@ -1,17 +1,24 @@
1
1
  import { ControlSize } from "../lib/control-size.js";
2
2
  import { NumberField } from "@base-ui/react/number-field";
3
+ import { ReactNode } from "react";
3
4
  //#region src/components/number-input.d.ts
4
5
  type NumberInputProps = Omit<NumberField.Root.Props, "children" | "className" | "render"> & {
5
6
  className?: string;
6
7
  inputClassName?: string;
7
- inputProps?: Omit<NumberField.Input.Props, "className" | "defaultValue" | "id" | "onChange" | "type" | "value">;
8
+ inputProps?: Omit<NumberField.Input.Props, "className" | "defaultValue" | "id" | "onChange" | "render" | "type" | "value">;
8
9
  size?: ControlSize;
10
+ /**
11
+ * The unit the value is in ("pt", "%"), shown muted at the end of the
12
+ * control so the number reads with its unit while the typed text stays a
13
+ * bare number.
14
+ */
15
+ suffix?: ReactNode;
9
16
  };
10
17
  /**
11
18
  * A numeric control that keeps the user's editable text separate from its
12
19
  * canonical numeric value. Parsed changes are reported through `onValueChange`;
13
20
  * formatting and clamping happen only at Base UI's commit boundaries.
14
21
  */
15
- declare const NumberInput: ({ className, inputClassName, inputProps, dir, size, ...props }: NumberInputProps) => import("react").JSX.Element;
22
+ declare const NumberInput: ({ className, inputClassName, inputProps, dir, size, suffix, ...props }: NumberInputProps) => import("react").JSX.Element;
16
23
  //#endregion
17
24
  export { NumberInput, type NumberInputProps };
@@ -2,7 +2,7 @@
2
2
  import { cn } from "../lib/utils.js";
3
3
  import { CONTROL_SIZE } from "../lib/control-size.js";
4
4
  import { INPUT_CONTROL_CLASS_NAME, INPUT_ELEMENT_CLASS_NAME, INPUT_SIZE_CLASS_NAMES } from "./input-control.js";
5
- import { jsx } from "react/jsx-runtime";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
6
  import { NumberField } from "@base-ui/react/number-field";
7
7
  //#region src/components/number-input.tsx
8
8
  /**
@@ -10,18 +10,30 @@ import { NumberField } from "@base-ui/react/number-field";
10
10
  * canonical numeric value. Parsed changes are reported through `onValueChange`;
11
11
  * formatting and clamping happen only at Base UI's commit boundaries.
12
12
  */
13
- const NumberInput = ({ className, inputClassName, inputProps, dir = "ltr", size = CONTROL_SIZE.default, ...props }) => /* @__PURE__ */ jsx(NumberField.Root, {
13
+ const NumberInput = ({ className, inputClassName, inputProps, dir = "ltr", size = CONTROL_SIZE.default, suffix, ...props }) => /* @__PURE__ */ jsxs(NumberField.Root, {
14
14
  ...props,
15
15
  className: cn(INPUT_CONTROL_CLASS_NAME, className),
16
16
  "data-size": size,
17
17
  "data-slot": "number-input-control",
18
18
  dir,
19
19
  render: void 0,
20
- children: /* @__PURE__ */ jsx(NumberField.Input, {
20
+ children: [/* @__PURE__ */ jsx(NumberField.Input, {
21
21
  ...inputProps,
22
- className: cn(INPUT_ELEMENT_CLASS_NAME, INPUT_SIZE_CLASS_NAMES[size], inputClassName),
23
- "data-slot": "number-input"
24
- })
22
+ className: cn(INPUT_ELEMENT_CLASS_NAME, INPUT_SIZE_CLASS_NAMES[size], "tabular-nums", suffix !== void 0 && "pe-1", inputClassName),
23
+ "data-slot": "number-input",
24
+ render: (inputElementProps, state) => /* @__PURE__ */ jsx("input", {
25
+ ...inputElementProps,
26
+ "aria-valuemax": props.max,
27
+ "aria-valuemin": props.min,
28
+ "aria-valuenow": state.value ?? void 0,
29
+ dir,
30
+ role: "spinbutton"
31
+ })
32
+ }), suffix === void 0 ? null : /* @__PURE__ */ jsx("span", {
33
+ className: "text-muted-foreground pointer-events-none flex shrink-0 items-center pe-2.5 text-xs",
34
+ "data-slot": "number-input-suffix",
35
+ children: suffix
36
+ })]
25
37
  });
26
38
  //#endregion
27
39
  export { NumberInput };
@@ -35,6 +35,7 @@ const SearchField = ({ value, onValueChange, clearLabel, groupClassName, classNa
35
35
  align: "inline-end",
36
36
  children: /* @__PURE__ */ jsx(Button, {
37
37
  "aria-label": clearLabel,
38
+ disabled: props.disabled,
38
39
  onClick: () => {
39
40
  onValueChange("");
40
41
  },
@@ -16,7 +16,8 @@ type SegmentedIconToggleProps<T extends string> = {
16
16
  };
17
17
  /** Compact segmented control of icon buttons (e.g. table row wrap, list
18
18
  * density). One shared shell so every density/mode switch looks and behaves
19
- * the same across the app. */
19
+ * the same across the app. A `ToggleGroup` of icon items that always holds
20
+ * one value: clicking the chosen option keeps it. */
20
21
  export declare const SegmentedIconToggle: <T extends string>({ value, options, onChange, size }: SegmentedIconToggleProps<T>) => import("react").JSX.Element;
21
22
  //#endregion
22
23
  export {};
@@ -1,31 +1,30 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/utils.js";
3
- import { Tooltip, TooltipContent as TooltipPopup, TooltipTrigger } from "./tooltip.js";
4
- import { Button } from "./button.js";
5
- import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { ToggleGroup, ToggleGroupItem } from "./toggle-group.js";
4
+ import { jsx } from "react/jsx-runtime";
6
5
  //#region src/components/segmented-icon-toggle.tsx
7
6
  /** Compact segmented control of icon buttons (e.g. table row wrap, list
8
7
  * density). One shared shell so every density/mode switch looks and behaves
9
- * the same across the app. */
10
- const SegmentedIconToggle = ({ value, options, onChange, size = "compact" }) => /* @__PURE__ */ jsx("div", {
11
- className: cn("border-border/70 bg-muted/30 inline-flex shrink-0 items-center overflow-hidden rounded-md border p-0.5", size === "touch" ? "h-7 [@media(any-pointer:coarse)]:h-12" : "h-7"),
8
+ * the same across the app. A `ToggleGroup` of icon items that always holds
9
+ * one value: clicking the chosen option keeps it. */
10
+ const SegmentedIconToggle = ({ value, options, onChange, size = "compact" }) => /* @__PURE__ */ jsx(ToggleGroup, {
11
+ className: cn("rounded-md", size === "touch" ? "h-7 [@media(any-pointer:coarse)]:h-12" : "h-7"),
12
12
  "data-slot": "segmented-icon-toggle",
13
+ onValueChange: (next) => {
14
+ const [chosen] = next;
15
+ if (chosen !== void 0 && chosen !== value) onChange(chosen);
16
+ },
17
+ value: [value],
13
18
  children: options.map((option) => {
14
19
  const Icon = option.icon;
15
- const isActive = value === option.value;
16
- return /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
17
- render: /* @__PURE__ */ jsx(Button, {
18
- "aria-label": option.label,
19
- "aria-pressed": isActive,
20
- className: cn("text-muted-foreground h-6 min-h-0 w-7 rounded-[4px] p-0", size === "touch" && "[@media(any-pointer:coarse)]:h-11 [@media(any-pointer:coarse)]:min-h-11 [@media(any-pointer:coarse)]:w-11", isActive && "bg-muted text-foreground ring-border/80 hover:bg-muted hover:text-foreground shadow-xs ring-1"),
21
- "data-control-value": option.value,
22
- onClick: () => onChange(option.value),
23
- size: "icon-xs",
24
- type: "button",
25
- variant: "ghost"
26
- }),
20
+ return /* @__PURE__ */ jsx(ToggleGroupItem, {
21
+ "aria-label": option.label,
22
+ className: cn("h-6 w-7 rounded-[4px] sm:h-6", size === "touch" && "[@media(any-pointer:coarse)]:h-11 [@media(any-pointer:coarse)]:w-11"),
23
+ "data-control-value": option.value,
24
+ size: "icon-xs",
25
+ value: option.value,
27
26
  children: /* @__PURE__ */ jsx(Icon, { className: "size-3.5" })
28
- }), /* @__PURE__ */ jsx(TooltipPopup, { children: option.label })] }, option.value);
27
+ }, option.value);
29
28
  })
30
29
  });
31
30
  //#endregion
@@ -1,5 +1,24 @@
1
+ import * as React$1 from "react";
1
2
  import { Switch as Switch$1 } from "@base-ui/react/switch";
2
3
  //#region src/components/switch.d.ts
3
- declare const Switch: ({ className, ...props }: Switch$1.Root.Props) => import("react").JSX.Element;
4
+ /**
5
+ * `default` is a setting's own control; `sm` sits in a dense row or a menu
6
+ * beside small type.
7
+ */
8
+ type SwitchSize = "sm" | "default";
9
+ type SwitchProps = Switch$1.Root.Props & {
10
+ size?: SwitchSize | undefined;
11
+ };
12
+ declare const Switch: ({ className, size, ...props }: SwitchProps) => React$1.JSX.Element;
13
+ /**
14
+ * A setting row: its label and an optional description at the start, the
15
+ * switch at the end. The whole row is the label, so a click anywhere on it
16
+ * flips the switch, and the row is at least 44px tall for touch. The
17
+ * description is linked to the switch, so it is read after the name.
18
+ */
19
+ declare const SwitchField: ({ className, description, label, size, ...props }: SwitchProps & {
20
+ label: React$1.ReactNode;
21
+ description?: React$1.ReactNode;
22
+ }) => React$1.JSX.Element;
4
23
  //#endregion
5
- export { Switch };
24
+ export { Switch, SwitchField, type SwitchProps, type SwitchSize };
@@ -1,16 +1,55 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/utils.js";
3
- import { jsx } from "react/jsx-runtime";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { useId } from "react";
4
5
  import { Switch as Switch$1 } from "@base-ui/react/switch";
5
6
  //#region src/components/switch.tsx
6
- const Switch = ({ className, ...props }) => /* @__PURE__ */ jsx(Switch$1.Root, {
7
- className: cn("focus-visible:ring-ring focus-visible:ring-offset-background data-checked:bg-primary data-unchecked:bg-input relative inline-flex h-5 w-9 shrink-0 items-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-offset-1 data-disabled:opacity-64", className),
7
+ const SWITCH_ROOT_SIZE_CLASS_NAME = {
8
+ default: "h-5 w-9",
9
+ sm: "h-4 w-7"
10
+ };
11
+ const SWITCH_THUMB_SIZE_CLASS_NAME = {
12
+ default: "size-4 ltr:data-checked:translate-x-4.5 ltr:data-unchecked:translate-x-0.5 rtl:data-checked:-translate-x-4.5 rtl:data-unchecked:-translate-x-0.5",
13
+ sm: "size-3 ltr:data-checked:translate-x-3.5 ltr:data-unchecked:translate-x-0.5 rtl:data-checked:-translate-x-3.5 rtl:data-unchecked:-translate-x-0.5"
14
+ };
15
+ const Switch = ({ className, size = "default", ...props }) => /* @__PURE__ */ jsx(Switch$1.Root, {
16
+ className: cn("focus-visible:ring-ring focus-visible:ring-offset-background data-checked:bg-primary data-unchecked:bg-input relative inline-flex shrink-0 items-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-offset-1 data-disabled:opacity-64", SWITCH_ROOT_SIZE_CLASS_NAME[size], className),
17
+ "data-size": size,
8
18
  "data-slot": "switch",
9
19
  ...props,
10
20
  children: /* @__PURE__ */ jsx(Switch$1.Thumb, {
11
- className: "bg-background pointer-events-none block size-4 rounded-full shadow-xs transition-transform ltr:data-checked:translate-x-4.5 ltr:data-unchecked:translate-x-0.5 rtl:data-checked:-translate-x-4.5 rtl:data-unchecked:-translate-x-0.5",
21
+ className: cn("bg-background pointer-events-none block rounded-full shadow-xs transition-transform", SWITCH_THUMB_SIZE_CLASS_NAME[size]),
12
22
  "data-slot": "switch-thumb"
13
23
  })
14
24
  });
25
+ /**
26
+ * A setting row: its label and an optional description at the start, the
27
+ * switch at the end. The whole row is the label, so a click anywhere on it
28
+ * flips the switch, and the row is at least 44px tall for touch. The
29
+ * description is linked to the switch, so it is read after the name.
30
+ */
31
+ const SwitchField = ({ className, description, label, size, ...props }) => {
32
+ const descriptionId = useId();
33
+ return /* @__PURE__ */ jsxs("label", {
34
+ className: cn("flex min-h-11 cursor-pointer items-center justify-between gap-4 py-2 has-data-disabled:cursor-not-allowed", className),
35
+ "data-slot": "switch-field",
36
+ children: [/* @__PURE__ */ jsxs("span", {
37
+ className: "flex min-w-0 flex-col gap-0.5 in-[label:has([data-disabled])]:opacity-64",
38
+ children: [/* @__PURE__ */ jsx("span", {
39
+ className: "text-foreground text-sm font-medium",
40
+ children: label
41
+ }), description === void 0 ? null : /* @__PURE__ */ jsx("span", {
42
+ className: "text-muted-foreground text-xs",
43
+ "data-slot": "switch-field-description",
44
+ id: descriptionId,
45
+ children: description
46
+ })]
47
+ }), /* @__PURE__ */ jsx(Switch, {
48
+ "aria-describedby": description === void 0 ? void 0 : descriptionId,
49
+ size,
50
+ ...props
51
+ })]
52
+ });
53
+ };
15
54
  //#endregion
16
- export { Switch };
55
+ export { Switch, SwitchField };