@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
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
- ## Kanban boards
107
+ ## Sortable boards
108
108
 
109
109
  `@stll/ui/kanban` provides board matrices, subgroup swimlanes, bounded virtual
110
- cells, and column bands. The caller owns card rendering, permissions, 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
@@ -9,18 +9,28 @@ export type DayCalendarTone = {
9
9
  } | {
10
10
  type: "neutral";
11
11
  };
12
- /** Whether an entry is still provisional (drawn dashed) or final. */
13
- export type DayCalendarEntryState = "draft" | "final";
12
+ /**
13
+ * Whether an entry is final, still provisional (dashed), or only proposed:
14
+ * a suggestion is not an entry yet, so it is drawn lighter, dashed, and is a
15
+ * button the caller acts on.
16
+ */
17
+ export type DayCalendarEntryState = "draft" | "final" | "suggestion";
14
18
  type DayCalendarBlockFields = DayCalendarRange & {
15
19
  accessibleLabel: string;
16
- detail?: ReactNode;
20
+ caption?: ReactNode;
17
21
  id: string;
18
22
  title: ReactNode;
19
23
  tone: DayCalendarTone;
24
+ /** Shown on hover, for example the apps a `mixed` block merged. */
25
+ tooltip?: ReactNode;
20
26
  };
21
27
  export type DayCalendarBlock = (DayCalendarBlockFields & {
22
28
  kind: "entry";
23
- state: DayCalendarEntryState;
29
+ state: Exclude<DayCalendarEntryState, "suggestion">;
30
+ }) | (DayCalendarBlockFields & {
31
+ kind: "entry";
32
+ onActivate: () => void;
33
+ state: "suggestion";
24
34
  }) | (DayCalendarBlockFields & {
25
35
  kind: "mixed";
26
36
  }) | (DayCalendarBlockFields & {
@@ -1,7 +1,8 @@
1
1
  import { cn } from "../lib/utils.js";
2
+ import { Tooltip, TooltipContent as TooltipPopup, TooltipTrigger } from "../components/tooltip.js";
2
3
  import { DAY_CALENDAR_SLOT_HEIGHT_PX, boundsHeight, extendRange, getDayBounds, getHourTicks, isRepeatedWallTime, layoutOverlaps, moveRange, offsetToTime, rangeFromDrag, slotAt, timeToOffset } from "./day-calendar.logic.js";
3
4
  import { resolveMatterColor } from "../lib/matter-colors.js";
4
- import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
6
  import { useState } from "react";
6
7
  import { panic } from "better-result";
7
8
  import { Temporal } from "temporal-polyfill/full";
@@ -305,7 +306,7 @@ const ColumnBlocks = ({ blocks, grid }) => {
305
306
  }, block.id);
306
307
  });
307
308
  };
308
- /** Below this height a block puts its title and detail on one line. */
309
+ /** Below this height a block puts its title and caption on one line. */
309
310
  const TWO_LINE_BLOCK_MIN_PX = 40;
310
311
  /** A brief block paints at least this tall, unless the grid ends sooner. */
311
312
  const BLOCK_MIN_PX = 6;
@@ -316,7 +317,8 @@ const BLOCK_KIND_CLASSES = {
316
317
  };
317
318
  const ENTRY_STATE_CLASSES = {
318
319
  draft: "border border-dashed border-current/40",
319
- final: ""
320
+ final: "",
321
+ suggestion: "border border-dashed border-current/45 outline-none hover:ring-1 hover:ring-ring/50 focus-visible:ring-2 focus-visible:ring-ring"
320
322
  };
321
323
  const DayCalendarBlockView = ({ block, grid, lane, range }) => {
322
324
  const top = timeToOffset(range.startMs, grid);
@@ -327,36 +329,66 @@ const DayCalendarBlockView = ({ block, grid, lane, range }) => {
327
329
  insetBlockStart: top,
328
330
  insetInlineStart: `${String(lane.lane * 100 / lane.laneCount)}%`
329
331
  };
332
+ const className = cn("text-foreground flex h-full w-full min-w-0 overflow-hidden rounded-md px-2.5 text-start text-xs", blockSize < TWO_LINE_BLOCK_MIN_PX ? "flex-row items-center gap-2" : "flex-col gap-0.5 py-1", BLOCK_KIND_CLASSES[block.kind], block.kind === "entry" && ENTRY_STATE_CLASSES[block.state], block.tone.type === "matter" && "border-s-[3px]", block.tone.type === "neutral" && "bg-muted");
333
+ const content = blockSize < 16 ? null : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
334
+ className: "min-w-0 shrink-0 truncate font-medium",
335
+ children: block.title
336
+ }), block.caption === void 0 ? null : /* @__PURE__ */ jsx("span", {
337
+ className: "text-muted-foreground min-w-0 truncate tabular-nums",
338
+ children: block.caption
339
+ })] });
340
+ const shared = {
341
+ "aria-label": block.accessibleLabel,
342
+ className,
343
+ "data-block-id": block.id,
344
+ "data-kind": block.kind,
345
+ "data-slot": "day-calendar-block",
346
+ style: blockSurfaceStyle(block)
347
+ };
348
+ const surface = block.kind === "entry" && block.state === "suggestion" ? /* @__PURE__ */ jsx("button", {
349
+ ...shared,
350
+ "data-state": block.state,
351
+ onClick: block.onActivate,
352
+ onKeyDown: (event) => event.stopPropagation(),
353
+ onPointerDown: (event) => event.stopPropagation(),
354
+ type: "button",
355
+ children: content
356
+ }) : /* @__PURE__ */ jsx("article", {
357
+ ...shared,
358
+ "data-state": block.kind === "entry" ? block.state : void 0,
359
+ children: content
360
+ });
330
361
  return /* @__PURE__ */ jsx("div", {
331
- className: "pointer-events-none absolute px-1.5 py-px",
362
+ className: cn("absolute px-1.5 py-px", block.tooltip === void 0 && !(block.kind === "entry" && block.state === "suggestion") && "pointer-events-none"),
332
363
  style: placement,
333
- children: /* @__PURE__ */ jsxs("article", {
334
- "aria-label": block.accessibleLabel,
335
- className: cn("text-foreground flex h-full min-w-0 overflow-hidden rounded-md px-2.5 text-start text-xs", blockSize < TWO_LINE_BLOCK_MIN_PX ? "flex-row items-center gap-2" : "flex-col gap-0.5 py-1", BLOCK_KIND_CLASSES[block.kind], block.kind === "entry" && ENTRY_STATE_CLASSES[block.state], block.tone.type === "matter" && "border-s-[3px]", block.tone.type === "neutral" && "bg-muted"),
336
- "data-kind": block.kind,
337
- "data-slot": "day-calendar-block",
338
- style: blockSurfaceStyle(block),
339
- children: [/* @__PURE__ */ jsx("span", {
340
- className: "min-w-0 shrink-0 truncate font-medium",
341
- children: block.title
342
- }), block.detail === void 0 ? null : /* @__PURE__ */ jsx("span", {
343
- className: "text-muted-foreground min-w-0 truncate tabular-nums",
344
- children: block.detail
345
- })]
346
- })
364
+ children: block.tooltip === void 0 ? surface : /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, { render: surface }), /* @__PURE__ */ jsx(TooltipPopup, {
365
+ className: "max-w-72",
366
+ children: block.tooltip
367
+ })] })
347
368
  });
348
369
  };
349
370
  const MIXED_STRIPES = "repeating-linear-gradient(135deg, var(--muted) 0 6px, transparent 6px 12px)";
371
+ /** A suggestion's tint is half an entry's, so proposed time reads lighter. */
372
+ const TONE_TINT_PERCENT = {
373
+ entry: 16,
374
+ suggestion: 8
375
+ };
350
376
  function blockSurfaceStyle(block) {
351
377
  const tone = block.tone;
352
378
  const stripes = block.kind === "mixed" ? MIXED_STRIPES : void 0;
379
+ const suggestion = block.kind === "entry" && block.state === "suggestion";
353
380
  switch (tone.type) {
354
- case "neutral": return { backgroundImage: stripes };
381
+ case "neutral": return {
382
+ backgroundColor: suggestion ? "transparent" : void 0,
383
+ backgroundImage: stripes
384
+ };
355
385
  case "matter": {
356
386
  const color = resolveMatterColor(tone.matterId, tone.color);
387
+ const tint = suggestion ? TONE_TINT_PERCENT.suggestion : TONE_TINT_PERCENT.entry;
357
388
  return {
358
- backgroundColor: `color-mix(in srgb, ${color} 16%, var(--card))`,
389
+ backgroundColor: `color-mix(in srgb, ${color} ${String(tint)}%, var(--card))`,
359
390
  backgroundImage: stripes,
391
+ borderColor: suggestion ? color : void 0,
360
392
  borderInlineStartColor: color
361
393
  };
362
394
  }
@@ -1,6 +1,9 @@
1
1
  //#region src/calendar/day-calendar.logic.d.ts
2
2
  export declare const DAY_CALENDAR_GRANULARITIES: readonly [15, 30, 60];
3
3
  export type DayCalendarGranularity = (typeof DAY_CALENDAR_GRANULARITIES)[number];
4
+ /** Below this height a block cannot hold one line of its label, so it draws
5
+ * its surface only; its accessible label and tooltip still carry it. */
6
+ export declare const DAY_CALENDAR_ONE_LINE_BLOCK_MIN_PX = 16;
4
7
  /**
5
8
  * Height of one grid slot. Coarser grids get taller slots, but not
6
9
  * proportionally: an hour stays tall enough for a two-line block.
@@ -6,6 +6,9 @@ const DAY_CALENDAR_GRANULARITIES = [
6
6
  30,
7
7
  60
8
8
  ];
9
+ /** Below this height a block cannot hold one line of its label, so it draws
10
+ * its surface only; its accessible label and tooltip still carry it. */
11
+ const DAY_CALENDAR_ONE_LINE_BLOCK_MIN_PX = 16;
9
12
  /**
10
13
  * Height of one grid slot. Coarser grids get taller slots, but not
11
14
  * proportionally: an hour stays tall enough for a two-line block.
@@ -232,4 +235,4 @@ function compareIds(left, right) {
232
235
  return left > right ? 1 : 0;
233
236
  }
234
237
  //#endregion
235
- export { DAY_CALENDAR_GRANULARITIES, DAY_CALENDAR_SLOT_HEIGHT_PX, boundsHeight, clampToBounds, extendRange, getDayBounds, getHourTicks, granularityMs, isRepeatedWallTime, layoutOverlaps, mergeShortSegments, moveRange, offsetToTime, rangeFromDrag, slotAt, snapToGrid, timeToOffset };
238
+ export { DAY_CALENDAR_GRANULARITIES, DAY_CALENDAR_ONE_LINE_BLOCK_MIN_PX, DAY_CALENDAR_SLOT_HEIGHT_PX, boundsHeight, clampToBounds, extendRange, getDayBounds, getHourTicks, granularityMs, isRepeatedWallTime, layoutOverlaps, mergeShortSegments, moveRange, offsetToTime, rangeFromDrag, slotAt, snapToGrid, timeToOffset };
@@ -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
- declare const AccordionTrigger: ({ className, children, size, ...props }: Accordion$1.Trigger.Props & {
6
- size?: "default" | "compact";
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?: "default" | "compact";
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 AccordionTrigger = ({ className, children, size = "default", ...props }) => /* @__PURE__ */ jsx(Accordion$1.Header, {
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 font-medium outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-64 data-panel-open:*:data-[slot=accordion-indicator]:rotate-180", size === "compact" ? "py-2 text-xs" : "py-4 text-sm", className),
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 === "compact" ? "pb-3" : "pb-4", className),
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
- variant?: "default" | "destructive" | "destructive-ghost" | "destructive-outline" | "ghost" | "link" | "muted" | "outline" | "secondary" | null | undefined;
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 };