@stll/ui 0.59.0 → 0.60.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 (48) hide show
  1. package/README.md +120 -4
  2. package/dist/calendar/day-calendar.d.ts +14 -4
  3. package/dist/calendar/day-calendar.js +52 -20
  4. package/dist/calendar/day-calendar.logic.d.ts +3 -0
  5. package/dist/calendar/day-calendar.logic.js +4 -1
  6. package/dist/components/accordion.d.ts +17 -3
  7. package/dist/components/accordion.js +17 -3
  8. package/dist/components/button.d.ts +56 -5
  9. package/dist/components/button.js +97 -15
  10. package/dist/components/button.native-classes.d.ts +17 -0
  11. package/dist/components/button.native-classes.js +176 -0
  12. package/dist/components/button.native.d.ts +5 -2
  13. package/dist/components/button.native.js +17 -134
  14. package/dist/components/button.shared.d.ts +24 -2
  15. package/dist/components/button.shared.js +15 -1
  16. package/dist/components/combobox.d.ts +3 -1
  17. package/dist/components/combobox.js +3 -2
  18. package/dist/components/date-picker-popover.d.ts +2 -0
  19. package/dist/components/date-picker-popover.js +5 -3
  20. package/dist/components/details-grid.d.ts +21 -2
  21. package/dist/components/details-grid.js +75 -20
  22. package/dist/components/form.d.ts +9 -3
  23. package/dist/components/form.js +24 -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/textarea.d.ts +14 -1
  33. package/dist/components/textarea.js +11 -5
  34. package/dist/index.d.ts +10 -6
  35. package/dist/index.js +6 -3
  36. package/dist/kanban/index.d.ts +5 -2
  37. package/dist/kanban/index.js +4 -1
  38. package/dist/kanban/sortable-edge.d.ts +39 -0
  39. package/dist/kanban/sortable-edge.js +27 -0
  40. package/dist/kanban/sortable-interactions.d.ts +145 -0
  41. package/dist/kanban/sortable-interactions.js +503 -0
  42. package/dist/kanban/sortable-interactions.logic.d.ts +79 -0
  43. package/dist/kanban/sortable-interactions.logic.js +210 -0
  44. package/dist/kanban/subgroup-board.d.ts +27 -2
  45. package/dist/kanban/subgroup-board.js +26 -1
  46. package/dist/kanban/virtual-cell.d.ts +18 -1
  47. package/dist/kanban/virtual-cell.js +70 -17
  48. package/package.json +5 -1
@@ -3,7 +3,7 @@ import { cn } from "../lib/utils.js";
3
3
  import { renderTooltipTrigger } from "./tooltip-trigger-helper.js";
4
4
  import { BUTTON_DISPOSITION, blockDisabledActivation, blockDisabledKeyActivation, resolveButtonDisposition } from "../lib/button-disposition.js";
5
5
  import { MENU_ROW_CLASS_NAME } from "../lib/menu-row.js";
6
- import { BUTTON_VARIANT_TOKENS } from "./button.shared.js";
6
+ import { BUTTON_PLAIN_VARIANT, BUTTON_VARIANT_TOKENS } from "./button.shared.js";
7
7
  import { Loader } from "./loader.js";
8
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
9
  import { mergeProps } from "@base-ui/react/merge-props";
@@ -44,7 +44,9 @@ const BUTTON_SIZE_CLASS_NAMES = {
44
44
  "icon-lg": "size-10 sm:size-9",
45
45
  "icon-sm": "size-8 sm:size-7",
46
46
  "icon-xl": "size-11 sm:size-10 [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5",
47
+ "icon-2xs": "size-5 rounded-md before:rounded-[calc(var(--radius-md)-1px)] sm:size-4 [&_svg:not([class*='size-'])]:size-3.5 sm:[&_svg:not([class*='size-'])]:size-3",
47
48
  "icon-xs": "size-7 rounded-md before:rounded-[calc(var(--radius-md)-1px)] sm:size-6 not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-4 sm:not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-3.5",
49
+ inline: "inline h-auto rounded-sm border-0 p-0 text-start text-[length:inherit] leading-[inherit] font-normal whitespace-normal select-text before:rounded-[calc(var(--radius-sm)-1px)] sm:text-[length:inherit] [&_svg]:mx-0 [&_svg]:inline [&_svg]:align-[-0.125em] [&_svg:not(:last-child)]:me-1",
48
50
  lg: "h-10 px-[calc(--spacing(3.5)-1px)] sm:h-9",
49
51
  row: `${MENU_ROW_CLASS_NAME} [&_svg]:mx-0`,
50
52
  sm: "h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] text-sm sm:h-7 sm:text-xs",
@@ -52,26 +54,68 @@ const BUTTON_SIZE_CLASS_NAMES = {
52
54
  xs: "h-7 gap-1 rounded-md px-[calc(--spacing(2)-1px)] text-sm before:rounded-[calc(var(--radius-md)-1px)] sm:h-6 sm:text-xs [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5"
53
55
  };
54
56
  const BUTTON_VARIANT_CLASS_NAMES = {
55
- default: `${tokens("default")} shadow-primary/24 [:hover,[data-pressed]]:bg-primary/90 shadow-xs not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none`,
56
- destructive: `${tokens("destructive")} shadow-destructive/8 [:hover,[data-pressed]]:border-destructive/40 [:hover,[data-pressed]]:bg-destructive/14 [:active,[data-pressed]]:bg-destructive/18 shadow-xs not-dark:bg-clip-padding not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] [:disabled,:active,[data-pressed]]:shadow-none`,
57
- "destructive-outline": `${tokens("destructive-outline")} dark:bg-input/32 [:hover,[data-pressed]]:border-destructive/32 [:hover,[data-pressed]]:bg-destructive/4 shadow-xs/5 not-dark:bg-clip-padding not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none`,
58
- "destructive-ghost": `${tokens("destructive-ghost")} data-pressed:bg-destructive/10 [:hover,[data-pressed]]:bg-destructive/10`,
59
- ghost: `${tokens("ghost")} data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent`,
60
- link: `${tokens("link")} underline-offset-4 [:hover,[data-pressed]]:underline`,
61
- muted: `${tokens("muted")} data-pressed:bg-accent data-pressed:text-foreground [:hover,[data-pressed]]:bg-accent [:hover,[data-pressed]]:text-foreground`,
62
- outline: `${tokens("outline")} dark:bg-input/32 [:hover,[data-pressed]]:bg-accent/50 dark:[:hover,[data-pressed]]:bg-input/64 shadow-xs/5 not-dark:bg-clip-padding not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none`,
63
- secondary: `${tokens("secondary")} [:active,[data-pressed]]:bg-secondary/80 [:hover,[data-pressed]]:bg-secondary/90`
57
+ default: `${tokens("default")} shadow-primary/24 data-pressed:bg-primary/80 [:hover:not([data-pressed])]:bg-primary/90 shadow-xs not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none`,
58
+ destructive: `${tokens("destructive")} shadow-destructive/8 data-pressed:border-destructive/56 data-pressed:bg-destructive/20 [:hover:not([data-pressed])]:border-destructive/40 [:hover:not([data-pressed])]:bg-destructive/14 active:bg-destructive/18 shadow-xs not-dark:bg-clip-padding not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] [:disabled,:active,[data-pressed]]:shadow-none`,
59
+ "destructive-outline": `${tokens("destructive-outline")} dark:bg-input/32 data-pressed:border-destructive/56 data-pressed:bg-destructive/8 [:hover:not([data-pressed])]:border-destructive/32 [:hover:not([data-pressed])]:bg-destructive/4 shadow-xs/5 not-dark:bg-clip-padding not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none`,
60
+ "destructive-ghost": `${tokens("destructive-ghost")} data-pressed:bg-destructive/14 data-pressed:inset-ring-destructive/32 [:hover:not([data-pressed])]:bg-destructive/10 data-pressed:inset-ring`,
61
+ ghost: `${tokens("ghost")} data-pressed:bg-accent data-pressed:inset-ring-input [:hover:not([data-pressed])]:bg-accent data-pressed:shadow-xs/5 data-pressed:inset-ring`,
62
+ link: `${tokens("link")} underline-offset-4 data-pressed:underline data-pressed:decoration-2 [:hover:not([data-pressed])]:underline`,
63
+ "muted-destructive": `${tokens("muted-destructive")} data-pressed:bg-destructive/14 data-pressed:text-destructive data-pressed:inset-ring-destructive/32 [:hover:not([data-pressed])]:bg-destructive/10 [:hover:not([data-pressed])]:text-destructive data-pressed:inset-ring`,
64
+ muted: `${tokens("muted")} data-pressed:bg-accent data-pressed:text-foreground data-pressed:inset-ring-input [:hover:not([data-pressed])]:bg-accent [:hover:not([data-pressed])]:text-foreground data-pressed:shadow-xs/5 data-pressed:inset-ring`,
65
+ outline: `${tokens("outline")} dark:bg-input/32 data-pressed:border-primary/48 data-pressed:bg-accent dark:data-pressed:bg-input/64 [:hover:not([data-pressed])]:bg-accent/50 dark:[:hover:not([data-pressed])]:bg-input/64 shadow-xs/5 not-dark:bg-clip-padding not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none`,
66
+ secondary: `${tokens("secondary")} data-pressed:bg-background data-pressed:inset-ring-input active:bg-secondary/80 [:hover:not([data-pressed])]:bg-secondary/90 data-pressed:shadow-xs/5 data-pressed:inset-ring`
64
67
  };
65
68
  const buttonVariants = cva("focus-visible:ring-ring focus-visible:ring-offset-background relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-lg border text-base font-medium whitespace-nowrap outline-none select-none before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] focus-visible:ring-2 focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-64 sm:text-sm pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 [&_svg]:pointer-events-none [&_svg]:-mx-0.5 [&_svg]:shrink-0 [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4", {
69
+ compoundVariants: [{
70
+ className: "[:hover:not([data-pressed])]:bg-transparent",
71
+ size: "inline",
72
+ variant: [
73
+ "destructive-ghost",
74
+ "ghost",
75
+ "link",
76
+ "muted",
77
+ "muted-destructive"
78
+ ]
79
+ }, {
80
+ className: "underline",
81
+ size: "inline",
82
+ variant: "link"
83
+ }],
66
84
  defaultVariants: {
67
85
  size: BUTTON_DEFAULT_SIZE,
68
86
  variant: "default"
69
87
  },
70
88
  variants: {
71
89
  size: BUTTON_SIZE_CLASS_NAMES,
90
+ layout: { wrap: "h-auto min-h-11 items-start justify-start py-2 text-start whitespace-normal sm:h-auto" },
72
91
  variant: BUTTON_VARIANT_CLASS_NAMES
73
92
  }
74
93
  });
94
+ /**
95
+ * `plain` (./button.shared.ts) on the web. It bypasses `buttonVariants` and
96
+ * its box, border, type and press motion instead of undoing them class by
97
+ * class, and keeps the disabled disposition, the tooltip and the focus ring.
98
+ * It lays out like a native button (`inline-block`, content flowing from the
99
+ * start), so a card's children stack as they would in a `div`; `className`
100
+ * places it (`w-full`, `block`). `shape` gives it the outline its focus ring
101
+ * follows. Like a native button it does not dim when disabled: the content
102
+ * says it is unavailable.
103
+ *
104
+ * Selected, it draws the inset edge the other variants draw, as an outline
105
+ * inside the box: an outline paints above the content, so a card's own
106
+ * surface cannot cover it the way it would cover an inset shadow.
107
+ */
108
+ const BUTTON_PLAIN_CLASS = "focus-visible:ring-ring focus-visible:ring-offset-background data-pressed:outline-primary/48 relative inline-block cursor-pointer text-start outline-none focus-visible:ring-2 focus-visible:ring-offset-1 disabled:cursor-default data-pressed:outline-2 data-pressed:-outline-offset-2 data-pressed:outline-solid";
109
+ /**
110
+ * The outline a plain button's focus ring follows, matching its content:
111
+ * `box` for a card, row or block of text (the container radius), `round` for
112
+ * an avatar, a dot or a pill.
113
+ */
114
+ const BUTTON_PLAIN_SHAPE_CLASS = {
115
+ box: "rounded-lg",
116
+ round: "rounded-full"
117
+ };
118
+ const BUTTON_FOCUS_RING_INSIDE_CLASS = "focus-visible:ring-inset focus-visible:ring-offset-0";
75
119
  /** What a button's box holds: a glyph alone, or a visible label. */
76
120
  const BUTTON_CONTENT = {
77
121
  icon: "icon",
@@ -95,7 +139,9 @@ const BUTTON_SIZE_CONTENT = {
95
139
  "icon-lg": BUTTON_CONTENT.icon,
96
140
  "icon-sm": BUTTON_CONTENT.icon,
97
141
  "icon-xl": BUTTON_CONTENT.icon,
142
+ "icon-2xs": BUTTON_CONTENT.icon,
98
143
  "icon-xs": BUTTON_CONTENT.icon,
144
+ inline: BUTTON_CONTENT.text,
99
145
  lg: BUTTON_CONTENT.text,
100
146
  row: BUTTON_CONTENT.text,
101
147
  sm: BUTTON_CONTENT.text,
@@ -110,7 +156,8 @@ const implicitTooltip = (size, accessibleName) => {
110
156
  default: return panic(`Unhandled button content: ${String(content)}`);
111
157
  }
112
158
  };
113
- function Button({ className, variant, size, render, loading, icon, children, disabled, tooltip, "data-accessible-disabled": accessibleDisabled, ...props }) {
159
+ function Button({ className, variant: variantProp, size, render, layout, loading, icon, children, disabled, tooltip, selected, focusRing = "outside", shape = "box", "data-accessible-disabled": accessibleDisabled, ...props }) {
160
+ const variant = variantProp ?? (size === "inline" ? "link" : void 0);
114
161
  const typeValue = render ? void 0 : "button";
115
162
  const tooltipContent = tooltip ?? implicitTooltip(size, props["aria-label"] ?? props.title);
116
163
  const disposition = resolveButtonDisposition({
@@ -128,12 +175,14 @@ function Button({ className, variant, size, render, loading, icon, children, dis
128
175
  size: "sm",
129
176
  variant: "decorative"
130
177
  }) : icon, children] }),
131
- className: cn(disposition === BUTTON_DISPOSITION.enabled && !isActivationPreservingDisabled && size !== "row" && variant !== "link" && buttonPressClass, buttonVariants({
178
+ className: cn(variant === "plain" ? cn(BUTTON_PLAIN_CLASS, BUTTON_PLAIN_SHAPE_CLASS[shape], className) : cn(disposition === BUTTON_DISPOSITION.enabled && !isActivationPreservingDisabled && size !== "row" && size !== "inline" && variant !== "link" && buttonPressClass, buttonVariants({
132
179
  className,
133
180
  size,
134
- variant
135
- }), (isAccessibleDisabled || isActivationPreservingDisabled) && "cursor-not-allowed opacity-64"),
181
+ variant,
182
+ layout
183
+ })), focusRing === "inside" && BUTTON_FOCUS_RING_INSIDE_CLASS, (isAccessibleDisabled || isActivationPreservingDisabled) && "cursor-not-allowed opacity-64"),
136
184
  "data-slot": "button",
185
+ "data-pressed": selected === true ? "" : void 0,
137
186
  "aria-busy": loading || void 0,
138
187
  ...isAccessibleDisabled || isActivationPreservingDisabled ? {
139
188
  "aria-disabled": true,
@@ -159,5 +208,38 @@ function Button({ className, variant, size, render, loading, icon, children, dis
159
208
  trigger: button
160
209
  });
161
210
  }
211
+ /**
212
+ * The run is the visible target: underlined like a link, ringed while its
213
+ * button has keyboard focus (per line, so a wrapped run rings each line).
214
+ */
215
+ const BUTTON_TEXT_RUN_CLASS = "has-focus-visible:ring-ring cursor-pointer rounded-xs underline underline-offset-2 box-decoration-clone select-text hover:decoration-2 has-focus-visible:ring-2 has-disabled:cursor-not-allowed has-disabled:opacity-64";
216
+ const BUTTON_TEXT_RUN_TARGET_CLASS = "absolute size-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]";
217
+ /**
218
+ * A long run of prose that acts as one button: a citation or a
219
+ * cross-reference in reader text. A <button> is an atomic box even at
220
+ * `display: inline`, so its label cannot break across the paragraph's lines;
221
+ * here the text sits in a native <label>, an inline box that wraps and copies
222
+ * with the prose, and the label forwards a press anywhere on the run to the
223
+ * <button> it holds, which takes its accessible name from the label. Use
224
+ * `Button size="inline"` for a short action.
225
+ */
226
+ const ButtonTextRun = ({ children, className, disabled, ...props }) => /* @__PURE__ */ jsxs("label", {
227
+ className: cn(BUTTON_TEXT_RUN_CLASS, className),
228
+ "data-slot": "button-text-run",
229
+ children: [/* @__PURE__ */ jsx(Button, {
230
+ ...props,
231
+ className: BUTTON_TEXT_RUN_TARGET_CLASS,
232
+ disabled,
233
+ focusRing: "outside",
234
+ icon: void 0,
235
+ loading: false,
236
+ render: void 0,
237
+ selected: false,
238
+ shape: "box",
239
+ size: void 0,
240
+ tooltip: void 0,
241
+ variant: BUTTON_PLAIN_VARIANT
242
+ }), children]
243
+ });
162
244
  //#endregion
163
- export { BUTTON_CONTENT, BUTTON_SIZE_CONTENT, Button, buttonAccessibleDisabledClass, buttonVariants };
245
+ export { BUTTON_CONTENT, BUTTON_SIZE_CONTENT, Button, ButtonTextRun, buttonAccessibleDisabledClass, buttonVariants };
@@ -0,0 +1,17 @@
1
+ import { ButtonSize, ButtonVariant } from "./button.shared.js";
2
+ //#region src/components/button.native-classes.d.ts
3
+ type NativeButtonClassNamesOptions = {
4
+ className?: string | undefined;
5
+ inactive: boolean;
6
+ selected: boolean;
7
+ size?: ButtonSize | null | undefined;
8
+ variant?: ButtonVariant | null | undefined;
9
+ };
10
+ /** The Pressable's and the label's classes, and the touch slop, for a button. */
11
+ export declare const nativeButtonClassNames: ({ className, inactive, selected, size, variant }: NativeButtonClassNamesOptions) => {
12
+ label: string;
13
+ pressable: string;
14
+ slop: 0 | 6 | 8 | 12;
15
+ };
16
+ //#endregion
17
+ export {};
@@ -0,0 +1,176 @@
1
+ import { BUTTON_PLAIN_VARIANT, BUTTON_VARIANT_TOKENS } from "./button.shared.js";
2
+ //#region src/components/button.native-classes.ts
3
+ /** A control's box: centred content inside the variant's border. */
4
+ const CONTROL = "items-center justify-center border";
5
+ /**
6
+ * Every shared size in a phone's metrics. A text size grows to the platform's
7
+ * 44-point touch target where the web relies on a coarse-pointer hit area; a
8
+ * smaller control keeps its look and reaches the target through `slop`.
9
+ */
10
+ const SIZES = {
11
+ chip: {
12
+ box: `${CONTROL} min-h-7 gap-1.5 rounded-full px-2.5`,
13
+ label: "text-xs",
14
+ slop: 8
15
+ },
16
+ default: {
17
+ box: `${CONTROL} min-h-11 gap-2 rounded-lg px-4`,
18
+ label: "",
19
+ slop: 0
20
+ },
21
+ icon: {
22
+ box: `${CONTROL} size-11 rounded-lg`,
23
+ label: "",
24
+ slop: 0
25
+ },
26
+ "icon-lg": {
27
+ box: `${CONTROL} size-12 rounded-lg`,
28
+ label: "",
29
+ slop: 0
30
+ },
31
+ "icon-sm": {
32
+ box: `${CONTROL} size-8 rounded-lg`,
33
+ label: "",
34
+ slop: 6
35
+ },
36
+ "icon-xl": {
37
+ box: `${CONTROL} size-11 rounded-full`,
38
+ label: "",
39
+ slop: 0
40
+ },
41
+ "icon-2xs": {
42
+ box: `${CONTROL} size-5 rounded-md`,
43
+ label: "",
44
+ slop: 12
45
+ },
46
+ "icon-xs": {
47
+ box: `${CONTROL} size-7 rounded-md`,
48
+ label: "",
49
+ slop: 8
50
+ },
51
+ inline: {
52
+ box: "items-center gap-1",
53
+ label: "",
54
+ slop: 8
55
+ },
56
+ lg: {
57
+ box: `${CONTROL} min-h-12 gap-2 rounded-lg px-5`,
58
+ label: "",
59
+ slop: 0
60
+ },
61
+ row: {
62
+ box: "min-h-11 w-full items-start gap-3 py-3",
63
+ label: "",
64
+ slop: 0
65
+ },
66
+ sm: {
67
+ box: `${CONTROL} min-h-9 gap-1.5 rounded-lg px-3`,
68
+ label: "text-sm",
69
+ slop: 0
70
+ },
71
+ xl: {
72
+ box: `${CONTROL} min-h-14 gap-2 rounded-lg px-6`,
73
+ label: "text-lg",
74
+ slop: 0
75
+ },
76
+ xs: {
77
+ box: `${CONTROL} min-h-7 gap-1 rounded-md px-2`,
78
+ label: "text-sm",
79
+ slop: 8
80
+ }
81
+ };
82
+ /**
83
+ * A phone has no hover: a pressed button answers with the fill the web shows
84
+ * on hover, or dims when it already has one. `selected` is the pressed state
85
+ * held (a row open beside the list, a chosen option).
86
+ */
87
+ const PRESS = {
88
+ default: {
89
+ pressed: "active:opacity-80",
90
+ selected: ""
91
+ },
92
+ destructive: {
93
+ pressed: "active:bg-destructive/18",
94
+ selected: ""
95
+ },
96
+ "destructive-outline": {
97
+ pressed: "active:bg-destructive/4",
98
+ selected: ""
99
+ },
100
+ "destructive-ghost": {
101
+ pressed: "active:bg-destructive/10",
102
+ selected: "bg-destructive/10"
103
+ },
104
+ ghost: {
105
+ pressed: "active:bg-accent",
106
+ selected: "bg-accent"
107
+ },
108
+ link: {
109
+ pressed: "active:opacity-60",
110
+ selected: ""
111
+ },
112
+ muted: {
113
+ pressed: "active:bg-accent",
114
+ selected: "bg-accent"
115
+ },
116
+ "muted-destructive": {
117
+ pressed: "active:bg-destructive/10",
118
+ selected: "bg-destructive/10"
119
+ },
120
+ outline: {
121
+ pressed: "active:bg-accent",
122
+ selected: "bg-accent"
123
+ },
124
+ secondary: {
125
+ pressed: "active:bg-secondary/80",
126
+ selected: ""
127
+ },
128
+ plain: {
129
+ pressed: "active:opacity-80",
130
+ selected: "border-primary"
131
+ }
132
+ };
133
+ /**
134
+ * `plain` has no fill, no label colour and no size; like the web's, it does
135
+ * not dim when disabled, because its content says it is unavailable. It keeps
136
+ * a transparent border that only `selected` colours, the phone's stand-in for
137
+ * the web's inset ring.
138
+ */
139
+ const PLAIN = {
140
+ shape: {
141
+ box: "rounded-lg border-2 border-transparent",
142
+ label: "",
143
+ slop: 0
144
+ },
145
+ tokens: {
146
+ surface: "",
147
+ label: ""
148
+ }
149
+ };
150
+ /** The Pressable's and the label's classes, and the touch slop, for a button. */
151
+ const nativeButtonClassNames = ({ className, inactive, selected, size, variant }) => {
152
+ const plain = variant === BUTTON_PLAIN_VARIANT;
153
+ const shape = plain ? PLAIN.shape : SIZES[size ?? "default"];
154
+ const tokens = plain ? PLAIN.tokens : BUTTON_VARIANT_TOKENS[variant ?? "default"];
155
+ const press = PRESS[variant ?? "default"];
156
+ const dimmed = plain ? "" : "opacity-64";
157
+ const join = (names) => names.filter((name) => name !== "").join(" ");
158
+ return {
159
+ label: join([
160
+ tokens.label,
161
+ variant === "link" ? "text-foreground underline" : "",
162
+ shape.label
163
+ ]),
164
+ pressable: join([
165
+ plain ? "" : "flex-row",
166
+ tokens.surface,
167
+ shape.box,
168
+ inactive ? dimmed : press.pressed,
169
+ selected ? press.selected : "",
170
+ className ?? ""
171
+ ]),
172
+ slop: shape.slop
173
+ };
174
+ };
175
+ //#endregion
176
+ export { nativeButtonClassNames };
@@ -8,8 +8,11 @@ type ButtonProps = Omit<PressableProps, "children" | "disabled"> & ButtonContrac
8
8
  * The web's `Button` (./button.tsx) on a phone: the same variants and sizes,
9
9
  * read from ./button.shared.ts, on a Pressable. A string child is set as the
10
10
  * button's label in its variant's colour; any other child is laid out as
11
- * given, which is how a row holds a glyph beside two lines of text.
11
+ * given, which is how a row holds a glyph beside two lines of text. A
12
+ * `plain` button paints no fill or label colour and ignores `size`: its
13
+ * children are its look, stacked as in a View, inside a border that shows
14
+ * only when it is selected. Its classes come from ./button.native-classes.ts.
12
15
  */
13
- export declare const Button: ({ accessibilityState, children, className, disabled, hitSlop, icon, loading, size, variant, ...props }: ButtonProps) => import("react").JSX.Element;
16
+ export declare const Button: ({ accessibilityState, children, className, disabled, hitSlop, icon, loading, selected: selectedProp, size, variant: variantProp, ...props }: ButtonProps) => import("react").JSX.Element;
14
17
  //#endregion
15
18
  export {};
@@ -1,138 +1,28 @@
1
- import { BUTTON_VARIANT_TOKENS } from "./button.shared.js";
1
+ import { nativeButtonClassNames } from "./button.native-classes.js";
2
2
  import { Loader } from "./loader.native.js";
3
3
  import { Text as Text$1 } from "./text.native.js";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  import { Pressable } from "react-native";
6
6
  //#region src/components/button.native.tsx
7
- /** A control's box: centred content inside the variant's border. */
8
- const CONTROL = "items-center justify-center border";
9
- /**
10
- * Every shared size in a phone's metrics. A text size grows to the platform's
11
- * 44-point touch target where the web relies on a coarse-pointer hit area; a
12
- * smaller control keeps its look and reaches the target through `slop`.
13
- */
14
- const SIZES = {
15
- chip: {
16
- box: `${CONTROL} min-h-7 gap-1.5 rounded-full px-2.5`,
17
- label: "text-xs",
18
- slop: 8
19
- },
20
- default: {
21
- box: `${CONTROL} min-h-11 gap-2 rounded-lg px-4`,
22
- label: "",
23
- slop: 0
24
- },
25
- icon: {
26
- box: `${CONTROL} size-11 rounded-lg`,
27
- label: "",
28
- slop: 0
29
- },
30
- "icon-lg": {
31
- box: `${CONTROL} size-12 rounded-lg`,
32
- label: "",
33
- slop: 0
34
- },
35
- "icon-sm": {
36
- box: `${CONTROL} size-8 rounded-lg`,
37
- label: "",
38
- slop: 6
39
- },
40
- "icon-xl": {
41
- box: `${CONTROL} size-11 rounded-full`,
42
- label: "",
43
- slop: 0
44
- },
45
- "icon-xs": {
46
- box: `${CONTROL} size-7 rounded-md`,
47
- label: "",
48
- slop: 8
49
- },
50
- lg: {
51
- box: `${CONTROL} min-h-12 gap-2 rounded-lg px-5`,
52
- label: "",
53
- slop: 0
54
- },
55
- row: {
56
- box: "min-h-11 w-full items-start gap-3 py-3",
57
- label: "",
58
- slop: 0
59
- },
60
- sm: {
61
- box: `${CONTROL} min-h-9 gap-1.5 rounded-lg px-3`,
62
- label: "text-sm",
63
- slop: 0
64
- },
65
- xl: {
66
- box: `${CONTROL} min-h-14 gap-2 rounded-lg px-6`,
67
- label: "text-lg",
68
- slop: 0
69
- },
70
- xs: {
71
- box: `${CONTROL} min-h-7 gap-1 rounded-md px-2`,
72
- label: "text-sm",
73
- slop: 8
74
- }
75
- };
76
- /**
77
- * A phone has no hover: a pressed button answers with the fill the web shows
78
- * on hover, or dims when it already has one. `selected` is the pressed state
79
- * held (a row open beside the list, a chosen option).
80
- */
81
- const PRESS = {
82
- default: {
83
- pressed: "active:opacity-80",
84
- selected: ""
85
- },
86
- destructive: {
87
- pressed: "active:bg-destructive/18",
88
- selected: ""
89
- },
90
- "destructive-outline": {
91
- pressed: "active:bg-destructive/4",
92
- selected: ""
93
- },
94
- "destructive-ghost": {
95
- pressed: "active:bg-destructive/10",
96
- selected: "bg-destructive/10"
97
- },
98
- ghost: {
99
- pressed: "active:bg-accent",
100
- selected: "bg-accent"
101
- },
102
- link: {
103
- pressed: "active:opacity-60",
104
- selected: ""
105
- },
106
- muted: {
107
- pressed: "active:bg-accent",
108
- selected: "bg-accent"
109
- },
110
- outline: {
111
- pressed: "active:bg-accent",
112
- selected: "bg-accent"
113
- },
114
- secondary: {
115
- pressed: "active:bg-secondary/80",
116
- selected: ""
117
- }
118
- };
119
7
  /**
120
8
  * The web's `Button` (./button.tsx) on a phone: the same variants and sizes,
121
9
  * read from ./button.shared.ts, on a Pressable. A string child is set as the
122
10
  * button's label in its variant's colour; any other child is laid out as
123
- * given, which is how a row holds a glyph beside two lines of text.
11
+ * given, which is how a row holds a glyph beside two lines of text. A
12
+ * `plain` button paints no fill or label colour and ignores `size`: its
13
+ * children are its look, stacked as in a View, inside a border that shows
14
+ * only when it is selected. Its classes come from ./button.native-classes.ts.
124
15
  */
125
- const Button = ({ accessibilityState, children, className, disabled = false, hitSlop, icon, loading = false, size, variant, ...props }) => {
126
- const shape = SIZES[size ?? "default"];
127
- const tokens = BUTTON_VARIANT_TOKENS[variant ?? "default"];
128
- const press = PRESS[variant ?? "default"];
16
+ const Button = ({ accessibilityState, children, className, disabled = false, hitSlop, icon, loading = false, selected: selectedProp, size, variant: variantProp, ...props }) => {
17
+ const variant = variantProp ?? (size === "inline" ? "link" : void 0);
129
18
  const inactive = disabled || loading;
130
- const selected = accessibilityState?.selected === true;
131
- const label = [
132
- tokens.label,
133
- variant === "link" ? "text-foreground underline" : "",
134
- shape.label
135
- ].filter((name) => name !== "").join(" ");
19
+ const look = nativeButtonClassNames({
20
+ className,
21
+ inactive,
22
+ selected: selectedProp === true || accessibilityState?.selected === true,
23
+ size,
24
+ variant
25
+ });
136
26
  return /* @__PURE__ */ jsxs(Pressable, {
137
27
  accessibilityRole: "button",
138
28
  accessibilityState: {
@@ -140,22 +30,15 @@ const Button = ({ accessibilityState, children, className, disabled = false, hit
140
30
  busy: loading,
141
31
  disabled: inactive
142
32
  },
143
- className: [
144
- "flex-row",
145
- tokens.surface,
146
- shape.box,
147
- inactive ? "opacity-64" : press.pressed,
148
- selected ? press.selected : "",
149
- className ?? ""
150
- ].filter((name) => name !== "").join(" "),
33
+ className: look.pressable,
151
34
  disabled: inactive,
152
- hitSlop: hitSlop ?? (shape.slop === 0 ? void 0 : shape.slop),
35
+ hitSlop: hitSlop ?? (look.slop === 0 ? void 0 : look.slop),
153
36
  ...props,
154
37
  children: [loading ? /* @__PURE__ */ jsx(Loader, {
155
38
  size: "sm",
156
39
  variant: "decorative"
157
40
  }) : icon, typeof children === "string" || typeof children === "number" ? /* @__PURE__ */ jsx(Text$1, {
158
- className: label,
41
+ className: look.label,
159
42
  variant: "button",
160
43
  children
161
44
  }) : children]
@@ -34,6 +34,10 @@ export declare const BUTTON_VARIANT_TOKENS: {
34
34
  readonly surface: "border-transparent";
35
35
  readonly label: "text-muted-foreground";
36
36
  };
37
+ readonly "muted-destructive": {
38
+ readonly surface: "border-transparent";
39
+ readonly label: "text-muted-foreground";
40
+ };
37
41
  readonly outline: {
38
42
  readonly surface: "border-input bg-popover";
39
43
  readonly label: "text-foreground";
@@ -43,9 +47,19 @@ export declare const BUTTON_VARIANT_TOKENS: {
43
47
  readonly label: "text-secondary-foreground";
44
48
  };
45
49
  };
46
- export type ButtonVariant = keyof typeof BUTTON_VARIANT_TOKENS;
50
+ /** A variant painted from its shared tokens above. */
51
+ export type ButtonTokenVariant = keyof typeof BUTTON_VARIANT_TOKENS;
52
+ /**
53
+ * `plain`: a click target whose look is its content (a card, a thumbnail, a
54
+ * cell, a passage). It has no tokens: neither renderer paints a box, border,
55
+ * type or press motion for it, and each keeps everything else its button
56
+ * owns (the disabled disposition, and on the web the tooltip and the focus
57
+ * ring). A `size` does not apply to it.
58
+ */
59
+ export declare const BUTTON_PLAIN_VARIANT = "plain";
60
+ export type ButtonVariant = ButtonTokenVariant | typeof BUTTON_PLAIN_VARIANT;
47
61
  /** Every size a button comes in, on every platform. */
48
- export declare const BUTTON_SIZES: readonly ["chip", "default", "icon", "icon-lg", "icon-sm", "icon-xl", "icon-xs", "lg", "row", "sm", "xl", "xs"];
62
+ export declare const BUTTON_SIZES: readonly ["chip", "default", "icon", "icon-lg", "icon-sm", "icon-xl", "icon-2xs", "icon-xs", "inline", "lg", "row", "sm", "xl", "xs"];
49
63
  export type ButtonSize = (typeof BUTTON_SIZES)[number];
50
64
  /**
51
65
  * The props every button renderer accepts, with one meaning. A renderer adds
@@ -60,6 +74,14 @@ export type ButtonContract = {
60
74
  disabled?: boolean;
61
75
  /** A glyph before the label. */
62
76
  icon?: ReactNode;
77
+ /**
78
+ * The current choice in a set (a picked option, the active filter tab, the
79
+ * open item of a list): shows the variant's selected treatment, which is
80
+ * distinct from its hover or press. The accessibility state stays the
81
+ * caller's, because which one is right depends on the widget the button is
82
+ * part of.
83
+ */
84
+ selected?: boolean | undefined;
63
85
  children?: ReactNode;
64
86
  };
65
87
  //#endregion
@@ -33,6 +33,10 @@ const BUTTON_VARIANT_TOKENS = {
33
33
  surface: "border-transparent",
34
34
  label: "text-muted-foreground"
35
35
  },
36
+ "muted-destructive": {
37
+ surface: "border-transparent",
38
+ label: "text-muted-foreground"
39
+ },
36
40
  outline: {
37
41
  surface: "border-input bg-popover",
38
42
  label: "text-foreground"
@@ -42,6 +46,14 @@ const BUTTON_VARIANT_TOKENS = {
42
46
  label: "text-secondary-foreground"
43
47
  }
44
48
  };
49
+ /**
50
+ * `plain`: a click target whose look is its content (a card, a thumbnail, a
51
+ * cell, a passage). It has no tokens: neither renderer paints a box, border,
52
+ * type or press motion for it, and each keeps everything else its button
53
+ * owns (the disabled disposition, and on the web the tooltip and the focus
54
+ * ring). A `size` does not apply to it.
55
+ */
56
+ const BUTTON_PLAIN_VARIANT = "plain";
45
57
  /** Every size a button comes in, on every platform. */
46
58
  const BUTTON_SIZES = [
47
59
  "chip",
@@ -50,7 +62,9 @@ const BUTTON_SIZES = [
50
62
  "icon-lg",
51
63
  "icon-sm",
52
64
  "icon-xl",
65
+ "icon-2xs",
53
66
  "icon-xs",
67
+ "inline",
54
68
  "lg",
55
69
  "row",
56
70
  "sm",
@@ -58,4 +72,4 @@ const BUTTON_SIZES = [
58
72
  "xs"
59
73
  ];
60
74
  //#endregion
61
- export { BUTTON_SIZES, BUTTON_VARIANT_TOKENS };
75
+ export { BUTTON_PLAIN_VARIANT, BUTTON_SIZES, BUTTON_VARIANT_TOKENS };
@@ -14,7 +14,9 @@ declare const ComboboxInput: ({ className, showTrigger, showClear, startAddon, s
14
14
  size?: ControlSize | number;
15
15
  ref?: React$1.Ref<HTMLInputElement>;
16
16
  }) => React$1.JSX.Element;
17
- declare const ComboboxTrigger: ({ className, ...props }: Combobox$1.Trigger.Props) => React$1.JSX.Element;
17
+ declare const ComboboxTrigger: ({ className, variant, ...props }: Combobox$1.Trigger.Props & {
18
+ variant?: "plain" | "field";
19
+ }) => React$1.JSX.Element;
18
20
  declare const ComboboxPopup: ({ className, children, side, sideOffset, alignOffset, align, ...props }: Combobox$1.Popup.Props & {
19
21
  align?: Combobox$1.Positioner.Props["align"];
20
22
  sideOffset?: Combobox$1.Positioner.Props["sideOffset"];