@stll/ui 0.59.1 → 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 (44) 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/combobox.d.ts +3 -1
  13. package/dist/components/combobox.js +3 -2
  14. package/dist/components/date-picker-popover.d.ts +2 -0
  15. package/dist/components/date-picker-popover.js +5 -3
  16. package/dist/components/details-grid.d.ts +21 -2
  17. package/dist/components/details-grid.js +75 -20
  18. package/dist/components/form.d.ts +9 -3
  19. package/dist/components/form.js +24 -3
  20. package/dist/components/input-control.d.ts +9 -0
  21. package/dist/components/input-control.js +10 -1
  22. package/dist/components/input.d.ts +7 -1
  23. package/dist/components/input.js +4 -4
  24. package/dist/components/label.d.ts +24 -2
  25. package/dist/components/label.js +21 -2
  26. package/dist/components/number-input.d.ts +9 -2
  27. package/dist/components/number-input.js +18 -6
  28. package/dist/components/textarea.d.ts +14 -1
  29. package/dist/components/textarea.js +11 -5
  30. package/dist/index.d.ts +10 -6
  31. package/dist/index.js +6 -3
  32. package/dist/kanban/index.d.ts +5 -2
  33. package/dist/kanban/index.js +4 -1
  34. package/dist/kanban/sortable-edge.d.ts +39 -0
  35. package/dist/kanban/sortable-edge.js +27 -0
  36. package/dist/kanban/sortable-interactions.d.ts +145 -0
  37. package/dist/kanban/sortable-interactions.js +503 -0
  38. package/dist/kanban/sortable-interactions.logic.d.ts +79 -0
  39. package/dist/kanban/sortable-interactions.logic.js +210 -0
  40. package/dist/kanban/subgroup-board.d.ts +27 -2
  41. package/dist/kanban/subgroup-board.js +26 -1
  42. package/dist/kanban/virtual-cell.d.ts +18 -1
  43. package/dist/kanban/virtual-cell.js +70 -17
  44. package/package.json +5 -1
@@ -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"];
@@ -5,6 +5,7 @@ import { OVERLAY_LAYER_CLASS_NAMES } from "../lib/overlay-layer.js";
5
5
  import { ScrollArea } from "./scroll-area.js";
6
6
  import { containedHandler } from "../hooks/use-contained-handler.js";
7
7
  import { CONTROL_SIZE } from "../lib/control-size.js";
8
+ import "../lib/field-trigger.js";
8
9
  import { Input } from "./input.js";
9
10
  import { jsx, jsxs } from "react/jsx-runtime";
10
11
  import * as React$1 from "react";
@@ -94,8 +95,8 @@ const COMBOBOX_ACTION_SIZE_CLASS_NAMES = {
94
95
  [CONTROL_SIZE.default]: "end-0.5",
95
96
  [CONTROL_SIZE.lg]: "end-0.5"
96
97
  };
97
- const ComboboxTrigger = ({ className, ...props }) => /* @__PURE__ */ jsx(Combobox$1.Trigger, {
98
- className,
98
+ const ComboboxTrigger = ({ className, variant = "plain", ...props }) => /* @__PURE__ */ jsx(Combobox$1.Trigger, {
99
+ className: cn(variant === "field" && "border-input bg-background text-foreground ring-ring/24 focus-visible:border-ring aria-invalid:border-destructive/36 focus-visible:aria-invalid:border-destructive/64 focus-visible:aria-invalid:ring-destructive/16 dark:bg-input/32 dark:aria-invalid:ring-destructive/24 relative inline-flex min-h-9 w-full min-w-36 items-center justify-center gap-2 rounded-lg border px-[calc(--spacing(3)-1px)] text-start text-base shadow-xs/5 outline-none select-none not-dark:bg-clip-padding before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] focus-visible:ring-[3px] data-disabled:pointer-events-none data-disabled:opacity-64 sm:min-h-8 sm:text-sm dark:not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [[data-disabled],:focus-visible,[aria-invalid],[data-pressed]]:shadow-none", className),
99
100
  "data-slot": "combobox-trigger",
100
101
  ...props
101
102
  });
@@ -39,6 +39,8 @@ type DatePickerPopoverProps = {
39
39
  * the control still has height; pass a call-to-action ("Select date…") to
40
40
  * make the empty state self-explanatory. */
41
41
  placeholderLabel?: string;
42
+ /** Replaces the formatted date in the trigger, as a week range does. */
43
+ triggerLabel?: string;
42
44
  clearLabel?: string;
43
45
  defaultOpen?: boolean;
44
46
  open?: boolean;
@@ -3,8 +3,8 @@ import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from "../icons.js";
3
3
  import { cn } from "../lib/utils.js";
4
4
  import { Button } from "./button.js";
5
5
  import { DirectionalIcon } from "./directional-icon.js";
6
- import { Input } from "./input.js";
7
6
  import { FIELD_TRIGGER_CLASS_NAME } from "../lib/field-trigger.js";
7
+ import { Input } from "./input.js";
8
8
  import { getLocaleWeekInfo, getWeekendDays } from "../lib/week.js";
9
9
  import { DATE_PICKER_MODE, formatDateTimeValue, getHourOptions, getMinuteOptions, getTimeFieldNames, joinPickerTime, localDateFromTimestamp, millisecondsUntilNextLocalDate, parseDateTimeValue, resolveCalendarViewMonth, shiftCalendarDate, splitPickerTime } from "./date-picker-popover.logic.js";
10
10
  import { Popover, PopoverContent as PopoverPopup, PopoverTrigger } from "./popover.js";
@@ -252,8 +252,10 @@ const PICKER_SIZE_CLASS_NAMES = {
252
252
  cell: "pointer-coarse:min-h-11 pointer-coarse:text-sm"
253
253
  }
254
254
  };
255
+ /** The trigger keeps its height with an em dash when nothing has a label. */
256
+ const firstLabel = (...labels) => labels.find((label) => label !== null && label !== void 0) ?? "—";
255
257
  const DatePickerPopoverContent = (props) => {
256
- const { id, labelledBy, className, disabled, hideClear, size = "compact", variant = "inline", mode = DATE_PICKER_MODE.date, value: rawValue, onChange, locale, isOverdue = false, showIcon = true, placeholderLabel, clearLabel = "Clear date", defaultOpen = false, open: controlledOpen, triggerRef, focusTriggerOnOpen = false, dateInputLabel, outOfRangeLabel, onOpenChange, todayLabel: todayLabelProp, overdueLabel, minDate, maxDate, isDateDisabled, layer = "default", today } = props;
258
+ const { id, labelledBy, className, disabled, hideClear, size = "compact", variant = "inline", mode = DATE_PICKER_MODE.date, value: rawValue, onChange, locale, isOverdue = false, showIcon = true, placeholderLabel, triggerLabel, clearLabel = "Clear date", defaultOpen = false, open: controlledOpen, triggerRef, focusTriggerOnOpen = false, dateInputLabel, outOfRangeLabel, onOpenChange, todayLabel: todayLabelProp, overdueLabel, minDate, maxDate, isDateDisabled, layer = "default", today } = props;
257
259
  const selection = resolveSelection(mode, rawValue);
258
260
  const value = selection.date;
259
261
  const labels = resolvePopupLabels(props);
@@ -300,7 +302,7 @@ const DatePickerPopoverContent = (props) => {
300
302
  isDayDisabled,
301
303
  onChange
302
304
  });
303
- const displayLabel = formatSelection(locale, selection) ?? placeholderLabel ?? "—";
305
+ const displayLabel = firstLabel(triggerLabel, formatSelection(locale, selection), placeholderLabel);
304
306
  const selectDay = (date) => {
305
307
  switch (selection.mode) {
306
308
  case DATE_PICKER_MODE.date:
@@ -1,10 +1,29 @@
1
1
  import * as React$1 from "react";
2
2
  //#region src/components/details-grid.d.ts
3
- declare const DetailsGrid: ({ className, ...props }: React$1.ComponentProps<"dl">) => React$1.JSX.Element;
3
+ /**
4
+ * - `sheet`: a fact sheet; small uppercase labels, up to three pairs per row
5
+ * as the container widens.
6
+ * - `list`: one pair per row in body type with a muted label, for a short
7
+ * list inside a card or dialog (who is signed in, which organization).
8
+ */
9
+ type DetailsVariant = "sheet" | "list";
10
+ /**
11
+ * - `horizontal`: each label beside its value. In a `list`, every label
12
+ * shares one column sized to the longest.
13
+ * - `vertical`: each label above its value, for values too long to share a
14
+ * row (a server address, a list of origins).
15
+ */
16
+ type DetailsOrientation = "horizontal" | "vertical";
17
+ type DetailsGridProps = React$1.ComponentProps<"dl"> & {
18
+ orientation?: DetailsOrientation;
19
+ variant?: DetailsVariant;
20
+ };
21
+ declare const DetailsGrid: ({ className, orientation, variant, ...props }: DetailsGridProps) => React$1.JSX.Element;
4
22
  type DetailsItemProps = React$1.ComponentProps<"div"> & {
5
23
  label: React$1.ReactNode;
24
+ /** `wide` spans every pair column of a `sheet`. */
6
25
  span?: "wide";
7
26
  };
8
27
  declare const DetailsItem: ({ label, children, span, className, ...props }: DetailsItemProps) => React$1.JSX.Element;
9
28
  //#endregion
10
- export { DetailsGrid, DetailsItem };
29
+ export { DetailsGrid, DetailsItem, type DetailsOrientation, type DetailsVariant };
@@ -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 };