@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.
- package/README.md +120 -4
- package/dist/components/accordion.d.ts +17 -3
- package/dist/components/accordion.js +17 -3
- package/dist/components/button.d.ts +56 -5
- package/dist/components/button.js +97 -15
- package/dist/components/button.native-classes.d.ts +17 -0
- package/dist/components/button.native-classes.js +176 -0
- package/dist/components/button.native.d.ts +5 -2
- package/dist/components/button.native.js +17 -134
- package/dist/components/button.shared.d.ts +24 -2
- package/dist/components/button.shared.js +15 -1
- package/dist/components/combobox.d.ts +3 -1
- package/dist/components/combobox.js +3 -2
- package/dist/components/date-picker-popover.d.ts +2 -0
- package/dist/components/date-picker-popover.js +5 -3
- package/dist/components/details-grid.d.ts +21 -2
- package/dist/components/details-grid.js +75 -20
- package/dist/components/form.d.ts +9 -3
- package/dist/components/form.js +24 -3
- package/dist/components/input-control.d.ts +9 -0
- package/dist/components/input-control.js +10 -1
- package/dist/components/input.d.ts +7 -1
- package/dist/components/input.js +4 -4
- package/dist/components/label.d.ts +24 -2
- package/dist/components/label.js +21 -2
- package/dist/components/number-input.d.ts +9 -2
- package/dist/components/number-input.js +18 -6
- package/dist/components/textarea.d.ts +14 -1
- package/dist/components/textarea.js +11 -5
- package/dist/index.d.ts +10 -6
- package/dist/index.js +6 -3
- package/dist/kanban/index.d.ts +5 -2
- package/dist/kanban/index.js +4 -1
- package/dist/kanban/sortable-edge.d.ts +39 -0
- package/dist/kanban/sortable-edge.js +27 -0
- package/dist/kanban/sortable-interactions.d.ts +145 -0
- package/dist/kanban/sortable-interactions.js +503 -0
- package/dist/kanban/sortable-interactions.logic.d.ts +79 -0
- package/dist/kanban/sortable-interactions.logic.js +210 -0
- package/dist/kanban/subgroup-board.d.ts +27 -2
- package/dist/kanban/subgroup-board.js +26 -1
- package/dist/kanban/virtual-cell.d.ts +18 -1
- package/dist/kanban/virtual-cell.js +70 -17
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ cannot reach into product code by accident. Lint enforces that boundary; the
|
|
|
11
11
|
export map below is what "part of the design system" means.
|
|
12
12
|
|
|
13
13
|
Peer dependencies: `react`, `react-dom`, `@base-ui/react`, `tailwindcss` (v4),
|
|
14
|
-
and `@tanstack/react-virtual`.
|
|
14
|
+
`@dnd-kit/core`, `@dnd-kit/sortable`, and `@tanstack/react-virtual`.
|
|
15
15
|
|
|
16
16
|
Derived in part from [coss ui](https://github.com/cosscom/ui) (MIT); see
|
|
17
17
|
`THIRD_PARTY_NOTICES`.
|
|
@@ -104,11 +104,101 @@ unavailable state.
|
|
|
104
104
|
minor. They will be removed after that; the export guard checks that both
|
|
105
105
|
spellings land on the same module for as long as they both exist.
|
|
106
106
|
|
|
107
|
-
##
|
|
107
|
+
## Sortable boards
|
|
108
108
|
|
|
109
109
|
`@stll/ui/kanban` provides board matrices, subgroup swimlanes, bounded virtual
|
|
110
|
-
cells, and
|
|
111
|
-
persisted mutations.
|
|
110
|
+
cells, and input/accessibility primitives. The caller keeps domain identifiers,
|
|
111
|
+
card rendering, permissions, and persisted mutations. Wrap sortable boards in
|
|
112
|
+
`KanbanSortableBoard`, render items through `useKanbanSortable`, and attach the
|
|
113
|
+
returned bindings to `KanbanDragHandle` or `KanbanCardDragSurface`. This public
|
|
114
|
+
contract supports external consumers with keyboard navigation across virtual rows
|
|
115
|
+
and matrix cells, touch scrolling and long-press dragging, and host-controlled
|
|
116
|
+
collapsed-band peeking. It remains supported independently of which interaction
|
|
117
|
+
implementation the application board uses.
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import {
|
|
121
|
+
KanbanDragHandle,
|
|
122
|
+
KanbanSortableBoard,
|
|
123
|
+
KanbanVirtualCell,
|
|
124
|
+
useKanbanSortable,
|
|
125
|
+
} from "@stll/ui/kanban";
|
|
126
|
+
|
|
127
|
+
const Card = ({ id }: { id: string }) => {
|
|
128
|
+
const { activator, setNodeRef, style } = useKanbanSortable({
|
|
129
|
+
activation: { type: "handle" },
|
|
130
|
+
id,
|
|
131
|
+
});
|
|
132
|
+
if (activator.type !== "handle") {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return (
|
|
136
|
+
<article ref={setNodeRef} style={style}>
|
|
137
|
+
<KanbanDragHandle bindings={activator.bindings} label="Move card" />
|
|
138
|
+
</article>
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
<KanbanSortableBoard onDragEnd={handleDragEnd}>
|
|
143
|
+
<KanbanVirtualCell
|
|
144
|
+
rows={cardIds}
|
|
145
|
+
getRowKey={(id) => id}
|
|
146
|
+
renderRow={(id) => <Card id={id} />}
|
|
147
|
+
pagination={{ type: "none" }}
|
|
148
|
+
sortable={{
|
|
149
|
+
dropTarget: { id: "column", position: { column: 0, lane: 0 } },
|
|
150
|
+
getRowId: (id) => id,
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
</KanbanSortableBoard>;
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
For collapsed subgroup bands, the host maps each registered drop target to its
|
|
157
|
+
band and clears the drag state on both drop and cancellation. Populate
|
|
158
|
+
`bandIdByDropTargetId` from the same cell/slot registrations that supply the
|
|
159
|
+
sortable target IDs (including folded-band slots).
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
const [dragState, setDragState] = useState<
|
|
163
|
+
{ type: "idle" } | { type: "dragging"; bandId: string | null }
|
|
164
|
+
>({ type: "idle" });
|
|
165
|
+
|
|
166
|
+
<KanbanSortableBoard
|
|
167
|
+
onDragStart={() => setDragState({ type: "dragging", bandId: null })}
|
|
168
|
+
onDragOver={({ over }) => {
|
|
169
|
+
setDragState({
|
|
170
|
+
type: "dragging",
|
|
171
|
+
bandId:
|
|
172
|
+
over === null ? null : (bandIdByDropTargetId.get(over.id) ?? null),
|
|
173
|
+
});
|
|
174
|
+
}}
|
|
175
|
+
onDragEnd={(event) => {
|
|
176
|
+
setDragState({ type: "idle" });
|
|
177
|
+
handleDragEnd(event);
|
|
178
|
+
}}
|
|
179
|
+
onDragCancel={() => setDragState({ type: "idle" })}
|
|
180
|
+
>
|
|
181
|
+
<KanbanSubgroupBoard
|
|
182
|
+
{...boardProps}
|
|
183
|
+
isDragging={dragState.type === "dragging"}
|
|
184
|
+
dragOverBandId={dragState.type === "dragging" ? dragState.bandId : null}
|
|
185
|
+
/>
|
|
186
|
+
</KanbanSortableBoard>;
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The default sensors are mouse (8px movement), touch (150ms delay with 8px
|
|
190
|
+
tolerance), and keyboard. Scroll containers retain `touch-action: auto`.
|
|
191
|
+
`KanbanDragHandle` and `{ type: "item" }` activation disable touch panning on
|
|
192
|
+
their activation surfaces. Use `{ type: "card" }` with
|
|
193
|
+
`KanbanCardDragSurface` to preserve scrolling from the card while supporting
|
|
194
|
+
long-press dragging. `KanbanSortableBoard` also accepts
|
|
195
|
+
custom `sensors`, `accessibility`, collision detection, keyboard coordinates,
|
|
196
|
+
auto-scroll options, and an `overlay` render function.
|
|
197
|
+
|
|
198
|
+
`getKanbanHorizontalEdge` takes `input: "pointer"` with a current client-x and
|
|
199
|
+
`direction: "ltr" | "rtl"` for mouse and touch input. Keyboard calls use
|
|
200
|
+
`input: "keyboard"` and require source and target indices, so every move has a
|
|
201
|
+
logical edge without relying on ambiguous geometry.
|
|
112
202
|
|
|
113
203
|
For Group/Sub-group boards, build one canonical matrix and render it with the
|
|
114
204
|
installable layout and virtual cell:
|
|
@@ -145,6 +235,32 @@ const matrix = buildKanbanBoardMatrix({
|
|
|
145
235
|
/>;
|
|
146
236
|
```
|
|
147
237
|
|
|
238
|
+
When rows inside a virtual cell are sortable, give the cell a unique drop target
|
|
239
|
+
and its ordered matrix position. The cell registers its actual scroll element,
|
|
240
|
+
including when `rows` is empty, and owns the matching `SortableContext`.
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
<KanbanVirtualCell
|
|
244
|
+
getRowKey={(row) => row.id}
|
|
245
|
+
pagination={{ type: "none" }}
|
|
246
|
+
renderRow={(row) => <Card row={row} />}
|
|
247
|
+
rows={cell.rows}
|
|
248
|
+
sortable={{
|
|
249
|
+
dropTarget: {
|
|
250
|
+
id: cellId,
|
|
251
|
+
position: { column: columnIndex, lane: laneIndex },
|
|
252
|
+
},
|
|
253
|
+
getRowId: (row) => row.id,
|
|
254
|
+
}}
|
|
255
|
+
/>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The default keyboard coordinates move through items in their cell before
|
|
259
|
+
crossing into adjacent columns or subgroup lanes; empty cells remain reachable.
|
|
260
|
+
Pointer and touch drops outside registered board targets cancel. Choose an
|
|
261
|
+
explicit `{ type: "handle" }` activation for the 44px handle, or `{ type:
|
|
262
|
+
"item" }` when the whole item is the intended activation surface.
|
|
263
|
+
|
|
148
264
|
### Column bands
|
|
149
265
|
|
|
150
266
|
Columns whose options carry a `band` (`KanbanColumnBand`, usually a status
|
|
@@ -2,11 +2,25 @@ import { Accordion as Accordion$1 } from "@base-ui/react/accordion";
|
|
|
2
2
|
//#region src/components/accordion.d.ts
|
|
3
3
|
declare const Accordion: <Value = unknown>(props: Accordion$1.Root.Props<Value>) => import("react").JSX.Element;
|
|
4
4
|
declare const AccordionItem: ({ className, ...props }: Accordion$1.Item.Props) => import("react").JSX.Element;
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* - `default`: a section heading of a page or panel.
|
|
7
|
+
* - `sm`: body-size type at a 44px row, for a disclosure inside a dialog or
|
|
8
|
+
* card ("Show all permissions", advanced options).
|
|
9
|
+
* - `compact`: small type in a dense list.
|
|
10
|
+
*/
|
|
11
|
+
type AccordionSize = "default" | "sm" | "compact";
|
|
12
|
+
/**
|
|
13
|
+
* - `default`: the trigger reads as a heading.
|
|
14
|
+
* - `muted`: a quiet "show more" line in muted body weight that brightens on
|
|
15
|
+
* hover, for a disclosure that is secondary to what is around it.
|
|
16
|
+
*/
|
|
17
|
+
type AccordionTriggerVariant = "default" | "muted";
|
|
18
|
+
declare const AccordionTrigger: ({ className, children, size, variant, ...props }: Accordion$1.Trigger.Props & {
|
|
19
|
+
size?: AccordionSize;
|
|
20
|
+
variant?: AccordionTriggerVariant;
|
|
7
21
|
}) => import("react").JSX.Element;
|
|
8
22
|
declare const AccordionPanel: ({ className, children, size, ...props }: Accordion$1.Panel.Props & {
|
|
9
|
-
size?:
|
|
23
|
+
size?: AccordionSize;
|
|
10
24
|
}) => import("react").JSX.Element;
|
|
11
25
|
//#endregion
|
|
12
26
|
export { Accordion, AccordionPanel as AccordionContent, AccordionPanel, AccordionItem, AccordionTrigger };
|
|
@@ -13,10 +13,24 @@ const AccordionItem = ({ className, ...props }) => /* @__PURE__ */ jsx(Accordion
|
|
|
13
13
|
"data-slot": "accordion-item",
|
|
14
14
|
...props
|
|
15
15
|
});
|
|
16
|
-
const
|
|
16
|
+
const TRIGGER_SIZE_CLASS_NAME = {
|
|
17
|
+
compact: "py-2 text-xs",
|
|
18
|
+
default: "py-4 text-sm",
|
|
19
|
+
sm: "min-h-11 items-center py-2 text-sm"
|
|
20
|
+
};
|
|
21
|
+
const PANEL_SIZE_CLASS_NAME = {
|
|
22
|
+
compact: "pb-3",
|
|
23
|
+
default: "pb-4",
|
|
24
|
+
sm: "pb-2"
|
|
25
|
+
};
|
|
26
|
+
const TRIGGER_VARIANT_CLASS_NAME = {
|
|
27
|
+
default: "font-medium",
|
|
28
|
+
muted: "text-muted-foreground hover:text-foreground font-normal"
|
|
29
|
+
};
|
|
30
|
+
const AccordionTrigger = ({ className, children, size = "default", variant = "default", ...props }) => /* @__PURE__ */ jsx(Accordion$1.Header, {
|
|
17
31
|
className: "flex",
|
|
18
32
|
children: /* @__PURE__ */ jsxs(Accordion$1.Trigger, {
|
|
19
|
-
className: cn("focus-visible:ring-ring flex flex-1 cursor-pointer items-start justify-between gap-4 rounded-md text-start
|
|
33
|
+
className: cn("focus-visible:ring-ring flex flex-1 cursor-pointer items-start justify-between gap-4 rounded-md text-start outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-64 data-panel-open:*:data-[slot=accordion-indicator]:rotate-180", TRIGGER_SIZE_CLASS_NAME[size], TRIGGER_VARIANT_CLASS_NAME[variant], className),
|
|
20
34
|
"data-slot": "accordion-trigger",
|
|
21
35
|
...props,
|
|
22
36
|
children: [children, /* @__PURE__ */ jsx(ChevronDownIcon, {
|
|
@@ -30,7 +44,7 @@ const AccordionPanel = ({ className, children, size = "default", ...props }) =>
|
|
|
30
44
|
"data-slot": "accordion-panel",
|
|
31
45
|
...props,
|
|
32
46
|
children: /* @__PURE__ */ jsx("div", {
|
|
33
|
-
className: cn("pt-0", size
|
|
47
|
+
className: cn("pt-0", PANEL_SIZE_CLASS_NAME[size], className),
|
|
34
48
|
children
|
|
35
49
|
})
|
|
36
50
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ButtonContract } from "./button.shared.js";
|
|
1
|
+
import { ButtonContract, ButtonSize, ButtonVariant } from "./button.shared.js";
|
|
2
2
|
import { ReactNode } from "react";
|
|
3
3
|
import { useRender } from "@base-ui/react/use-render";
|
|
4
4
|
//#region src/components/button.d.ts
|
|
@@ -12,10 +12,31 @@ import { useRender } from "@base-ui/react/use-render";
|
|
|
12
12
|
* swaps the cursor so the control still reads as unavailable on hover.
|
|
13
13
|
*/
|
|
14
14
|
declare const buttonAccessibleDisabledClass = "cursor-not-allowed opacity-64";
|
|
15
|
+
declare const BUTTON_LAYOUT_CLASS_NAMES: {
|
|
16
|
+
readonly wrap: "h-auto min-h-11 items-start justify-start py-2 text-start whitespace-normal sm:h-auto";
|
|
17
|
+
};
|
|
15
18
|
declare const buttonVariants: (props?: ({
|
|
16
|
-
size?: "chip" | "default" | "icon" | "icon-lg" | "icon-sm" | "icon-xl" | "icon-xs" | "lg" | "row" | "sm" | "xl" | "xs" | null | undefined;
|
|
17
|
-
|
|
19
|
+
size?: "chip" | "default" | "icon" | "icon-2xs" | "icon-lg" | "icon-sm" | "icon-xl" | "icon-xs" | "inline" | "lg" | "row" | "sm" | "xl" | "xs" | null | undefined;
|
|
20
|
+
layout?: "wrap" | null | undefined;
|
|
21
|
+
variant?: "default" | "destructive" | "destructive-ghost" | "destructive-outline" | "ghost" | "link" | "muted" | "muted-destructive" | "outline" | "secondary" | null | undefined;
|
|
18
22
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
23
|
+
/**
|
|
24
|
+
* The outline a plain button's focus ring follows, matching its content:
|
|
25
|
+
* `box` for a card, row or block of text (the container radius), `round` for
|
|
26
|
+
* an avatar, a dot or a pill.
|
|
27
|
+
*/
|
|
28
|
+
declare const BUTTON_PLAIN_SHAPE_CLASS: {
|
|
29
|
+
readonly box: "rounded-lg";
|
|
30
|
+
readonly round: "rounded-full";
|
|
31
|
+
};
|
|
32
|
+
type ButtonPlainShape = keyof typeof BUTTON_PLAIN_SHAPE_CLASS;
|
|
33
|
+
/**
|
|
34
|
+
* Where the focus ring is drawn. `outside` (the default) rings the box with a
|
|
35
|
+
* gap; `inside` draws it within the box, for a target that fills a clipping
|
|
36
|
+
* container (an `overflow-hidden` card, a table cell, a thumbnail grid) where
|
|
37
|
+
* an outside ring would be cut off.
|
|
38
|
+
*/
|
|
39
|
+
type ButtonFocusRing = "outside" | "inside";
|
|
19
40
|
/** What a button's box holds: a glyph alone, or a visible label. */
|
|
20
41
|
declare const BUTTON_CONTENT: {
|
|
21
42
|
readonly icon: "icon";
|
|
@@ -39,17 +60,47 @@ declare const BUTTON_SIZE_CONTENT: {
|
|
|
39
60
|
readonly "icon-lg": "icon";
|
|
40
61
|
readonly "icon-sm": "icon";
|
|
41
62
|
readonly "icon-xl": "icon";
|
|
63
|
+
readonly "icon-2xs": "icon";
|
|
42
64
|
readonly "icon-xs": "icon";
|
|
65
|
+
readonly inline: "text";
|
|
43
66
|
readonly lg: "text";
|
|
44
67
|
readonly row: "text";
|
|
45
68
|
readonly sm: "text";
|
|
46
69
|
readonly xl: "text";
|
|
47
70
|
readonly xs: "text";
|
|
48
71
|
};
|
|
72
|
+
/**
|
|
73
|
+
* `selected` (./button.shared.ts) sets `data-pressed`, which every variant
|
|
74
|
+
* paints. The ARIA state (`aria-pressed`, `aria-checked`, `aria-current` or
|
|
75
|
+
* `aria-selected`) stays the caller's. For a set of mutually exclusive
|
|
76
|
+
* options prefer `ToggleGroup`, which owns that state.
|
|
77
|
+
*/
|
|
49
78
|
type ButtonProps = Omit<ButtonContract, "children" | "disabled"> & {
|
|
79
|
+
layout?: keyof typeof BUTTON_LAYOUT_CLASS_NAMES;
|
|
50
80
|
tooltip?: ReactNode;
|
|
81
|
+
focusRing?: ButtonFocusRing | undefined;
|
|
82
|
+
/** `variant="plain"` only: the outline its focus ring follows. */
|
|
83
|
+
shape?: ButtonPlainShape | undefined;
|
|
51
84
|
"data-accessible-disabled"?: "";
|
|
52
85
|
} & useRender.ComponentProps<"button">;
|
|
53
|
-
declare function Button({ className, variant, size, render, loading, icon, children, disabled, tooltip, "data-accessible-disabled": accessibleDisabled, ...props }: ButtonProps): import("react").JSX.Element;
|
|
86
|
+
declare function Button({ className, variant: variantProp, size, render, layout, loading, icon, children, disabled, tooltip, selected, focusRing, shape, "data-accessible-disabled": accessibleDisabled, ...props }: ButtonProps): import("react").JSX.Element;
|
|
87
|
+
/**
|
|
88
|
+
* A run with no box can anchor no tooltip or popup, and carries no variant,
|
|
89
|
+
* size or icon: its text is its look.
|
|
90
|
+
*/
|
|
91
|
+
type ButtonTextRunProps = Omit<ButtonProps, "children" | "className" | "focusRing" | "icon" | "loading" | "render" | "selected" | "shape" | "size" | "tooltip" | "variant"> & {
|
|
92
|
+
children: ReactNode;
|
|
93
|
+
className?: string | undefined;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* A long run of prose that acts as one button: a citation or a
|
|
97
|
+
* cross-reference in reader text. A <button> is an atomic box even at
|
|
98
|
+
* `display: inline`, so its label cannot break across the paragraph's lines;
|
|
99
|
+
* here the text sits in a native <label>, an inline box that wraps and copies
|
|
100
|
+
* with the prose, and the label forwards a press anywhere on the run to the
|
|
101
|
+
* <button> it holds, which takes its accessible name from the label. Use
|
|
102
|
+
* `Button size="inline"` for a short action.
|
|
103
|
+
*/
|
|
104
|
+
declare const ButtonTextRun: ({ children, className, disabled, ...props }: ButtonTextRunProps) => import("react").JSX.Element;
|
|
54
105
|
//#endregion
|
|
55
|
-
export { BUTTON_CONTENT, BUTTON_SIZE_CONTENT, Button, buttonAccessibleDisabledClass, buttonVariants };
|
|
106
|
+
export { BUTTON_CONTENT, BUTTON_SIZE_CONTENT, Button, type ButtonFocusRing, type ButtonPlainShape, type ButtonProps, type ButtonSize, ButtonTextRun, type ButtonVariant, buttonAccessibleDisabledClass, buttonVariants };
|
|
@@ -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
|
|
56
|
-
destructive: `${tokens("destructive")} shadow-destructive/8 [:hover
|
|
57
|
-
"destructive-outline": `${tokens("destructive-outline")} dark:bg-input/32 [:hover
|
|
58
|
-
"destructive-ghost": `${tokens("destructive-ghost")} data-pressed:bg-destructive/
|
|
59
|
-
ghost: `${tokens("ghost")} data-pressed:bg-accent [:hover
|
|
60
|
-
link: `${tokens("link")} underline-offset-4 [:hover
|
|
61
|
-
muted: `${tokens("muted")} data-pressed:bg-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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 {};
|