@stll/ui 0.37.1 → 0.39.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 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
- `@dnd-kit/core`, `@dnd-kit/sortable`, and `@tanstack/react-virtual`.
14
+ and `@tanstack/react-virtual`.
15
15
 
16
16
  ## Import
17
17
 
@@ -101,56 +101,11 @@ unavailable state.
101
101
  minor. They will be removed after that; the export guard checks that both
102
102
  spellings land on the same module for as long as they both exist.
103
103
 
104
- ## Sortable boards
104
+ ## Kanban boards
105
105
 
106
106
  `@stll/ui/kanban` provides board matrices, subgroup swimlanes, bounded virtual
107
- cells, and input/accessibility primitives. The caller keeps domain identifiers,
108
- card rendering, permissions, and persisted mutations. Wrap sortable boards in
109
- `KanbanSortableBoard`, render items through `useKanbanSortable`, and attach the
110
- returned bindings to `KanbanDragHandle`.
111
-
112
- ```tsx
113
- import {
114
- KanbanDragHandle,
115
- KanbanSortableBoard,
116
- KanbanSortableList,
117
- useKanbanSortable,
118
- } from "@stll/ui/kanban";
119
-
120
- const Card = ({ id }: { id: string }) => {
121
- const { activator, setNodeRef } = useKanbanSortable({
122
- activation: { type: "handle" },
123
- id,
124
- });
125
- if (activator.type !== "handle") {
126
- return null;
127
- }
128
- return (
129
- <article ref={setNodeRef}>
130
- <KanbanDragHandle bindings={activator.bindings} label="Move card" />
131
- </article>
132
- );
133
- };
134
-
135
- <KanbanSortableBoard onDragEnd={handleDragEnd}>
136
- <KanbanSortableList items={cardIds}>
137
- {cardIds.map((id) => (
138
- <Card id={id} key={id} />
139
- ))}
140
- </KanbanSortableList>
141
- </KanbanSortableBoard>;
142
- ```
143
-
144
- The default sensors are mouse (8px movement), touch (150ms delay with 8px
145
- tolerance), and keyboard. Scroll containers retain `touch-action: auto`; only
146
- `KanbanDragHandle` disables touch panning. `KanbanSortableBoard` also accepts
147
- custom `sensors`, `accessibility`, collision detection, keyboard coordinates,
148
- auto-scroll options, and an `overlay` render function.
149
-
150
- `getKanbanHorizontalEdge` takes `input: "pointer"` with a current client-x and
151
- `direction: "ltr" | "rtl"` for mouse and touch input. Keyboard calls use
152
- `input: "keyboard"` and require source and target indices, so every move has a
153
- logical edge without relying on ambiguous geometry.
107
+ cells, and column bands. The caller owns card rendering, permissions, and
108
+ persisted mutations.
154
109
 
155
110
  For Group/Sub-group boards, build one canonical matrix and render it with the
156
111
  installable layout and virtual cell:
@@ -187,32 +142,6 @@ const matrix = buildKanbanBoardMatrix({
187
142
  />;
188
143
  ```
189
144
 
190
- When rows inside a virtual cell are sortable, give the cell a unique drop target
191
- and its ordered matrix position. The cell registers its actual scroll element,
192
- including when `rows` is empty, and owns the matching `SortableContext`.
193
-
194
- ```tsx
195
- <KanbanVirtualCell
196
- getRowKey={(row) => row.id}
197
- pagination={{ type: "none" }}
198
- renderRow={(row) => <Card row={row} />}
199
- rows={cell.rows}
200
- sortable={{
201
- dropTarget: {
202
- id: cellId,
203
- position: { column: columnIndex, lane: laneIndex },
204
- },
205
- getRowId: (row) => row.id,
206
- }}
207
- />
208
- ```
209
-
210
- The default keyboard coordinates move through items in their cell before
211
- crossing into adjacent columns or subgroup lanes; empty cells remain reachable.
212
- Pointer and touch drops outside registered board targets cancel. Choose an
213
- explicit `{ type: "handle" }` activation for the 44px handle, or `{ type:
214
- "item" }` when the whole item is the intended activation surface.
215
-
216
145
  ### Column bands
217
146
 
218
147
  Columns whose options carry a `band` (`KanbanColumnBand`, usually a status
@@ -273,6 +202,42 @@ renderCollapsedBandCell={({ band, cells, count }) => (
273
202
  )}
274
203
  ```
275
204
 
205
+ ## Grouped lists and search
206
+
207
+ `@stll/ui/list` renders inventories of named things (connections, sessions,
208
+ keys) as titled groups of hairline-divided rows. Rows truncate their content
209
+ column and keep their trailing column, so they never scroll sideways.
210
+
211
+ ```tsx
212
+ <ListGroup aria-labelledby="apps">
213
+ <ListGroupHeader>
214
+ <ListGroupHeading>
215
+ <ListGroupTitle id="apps">
216
+ Apps with access<ListGroupCount>3</ListGroupCount>
217
+ </ListGroupTitle>
218
+ <ListGroupDescription>Apps you authorized.</ListGroupDescription>
219
+ </ListGroupHeading>
220
+ </ListGroupHeader>
221
+ <List>
222
+ <ListItem>
223
+ <ListItemMedia>C</ListItemMedia>
224
+ <ListItemContent>
225
+ <ListItemTitle>Claude</ListItemTitle>
226
+ <ListItemDescription>Connected 3 days ago</ListItemDescription>
227
+ </ListItemContent>
228
+ <ListItemActions>
229
+ <ListItemStatus tone="success">Connected</ListItemStatus>
230
+ </ListItemActions>
231
+ </ListItem>
232
+ </List>
233
+ </ListGroup>
234
+ ```
235
+
236
+ Pass `render={<a href="…" />}` to `ListItem` to make the whole row a link.
237
+ `@stll/ui/search-field` is the matching filter box: leading magnifier, clear
238
+ button, and Escape clears the query. The caller owns the query and the
239
+ matching.
240
+
276
241
  ## Styles
277
242
 
278
243
  No compiled CSS ships. The components carry Tailwind class names, so the
@@ -13,7 +13,7 @@ import { VariantProps } from "class-variance-authority";
13
13
  */
14
14
  declare const buttonAccessibleDisabledClass = "cursor-not-allowed opacity-64";
15
15
  declare const buttonVariants: (props?: ({
16
- size?: "default" | "row" | "chip" | "icon" | "icon-lg" | "icon-sm" | "icon-xl" | "icon-xs" | "lg" | "sm" | "xl" | "xs" | null | undefined;
16
+ size?: "sm" | "default" | "lg" | "row" | "chip" | "icon" | "icon-lg" | "icon-sm" | "icon-xl" | "icon-xs" | "xl" | "xs" | null | undefined;
17
17
  variant?: "link" | "default" | "destructive" | "destructive-outline" | "destructive-ghost" | "ghost" | "muted" | "outline" | "secondary" | null | undefined;
18
18
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
19
19
  type ButtonProps = {
@@ -75,6 +75,14 @@ type ComposerStatusRowProps = {
75
75
  start?: ReactNode | undefined;
76
76
  /** End slot, pinned to the far edge (e.g. the context meter). */
77
77
  end?: ReactNode | undefined;
78
+ /**
79
+ * Middle slot, centered on the row (e.g. scroll to the latest message).
80
+ * With it the row becomes three columns whose sides share the remaining
81
+ * width equally, so the middle stays centered and never covers a control.
82
+ * The row is a size container, so the middle can drop its label when the
83
+ * row is narrow (`@md:` variants).
84
+ */
85
+ center?: ReactNode | undefined;
78
86
  className?: string | undefined;
79
87
  };
80
88
  /**
@@ -83,7 +91,7 @@ type ComposerStatusRowProps = {
83
91
  * surface so the shell can never drift. Renders nothing when both slots are
84
92
  * empty, so a surface with no per-send controls shows no row.
85
93
  */
86
- export declare const ComposerStatusRow: ({ start, end, className }: ComposerStatusRowProps) => import("react").JSX.Element | null;
94
+ export declare const ComposerStatusRow: ({ start, end, center, className }: ComposerStatusRowProps) => import("react").JSX.Element | null;
87
95
  export type ComposerPickerOption<Value extends string> = {
88
96
  label: string;
89
97
  value: Value;
@@ -80,8 +80,25 @@ const COMPOSER_PICKER_TRIGGER_CLASS = "text-muted-foreground hover:text-foregrou
80
80
  * surface so the shell can never drift. Renders nothing when both slots are
81
81
  * empty, so a surface with no per-send controls shows no row.
82
82
  */
83
- const ComposerStatusRow = ({ start, end, className }) => {
84
- if (start === void 0 && end === void 0) return null;
83
+ const ComposerStatusRow = ({ start, end, center, className }) => {
84
+ if (start === void 0 && end === void 0 && center === void 0) return null;
85
+ if (center !== void 0) return /* @__PURE__ */ jsxs("div", {
86
+ className: cn("text-muted-foreground @container mt-1.5 grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-2 px-1 text-xs", className),
87
+ children: [
88
+ /* @__PURE__ */ jsx("div", {
89
+ className: "flex min-w-0 items-center",
90
+ children: start
91
+ }),
92
+ /* @__PURE__ */ jsx("div", {
93
+ className: "flex items-center justify-center",
94
+ children: center
95
+ }),
96
+ /* @__PURE__ */ jsx("div", {
97
+ className: "flex min-w-0 items-center justify-end",
98
+ children: end
99
+ })
100
+ ]
101
+ });
85
102
  return /* @__PURE__ */ jsxs("div", {
86
103
  className: cn("text-muted-foreground mt-1.5 flex items-center justify-between gap-2 px-1 text-xs", className),
87
104
  children: [start, end !== void 0 && /* @__PURE__ */ jsx("div", {
@@ -0,0 +1,19 @@
1
+ import { Button } from "./button.js";
2
+ import { ComponentProps } from "react";
3
+ //#region src/components/copy-button.d.ts
4
+ type CopyButtonProps = Omit<ComponentProps<typeof Button>, "children" | "onClick"> & {
5
+ /** Copies and reports whether it worked; failures stay with the caller. */
6
+ onCopy: () => Promise<boolean>;
7
+ label: string;
8
+ copiedLabel: string;
9
+ /** Renders the glyph alone, with the labels as its accessible name. */
10
+ iconOnly?: boolean;
11
+ iconClassName?: string;
12
+ };
13
+ /**
14
+ * A copy action that confirms in place: the glyph turns into a check and the
15
+ * label into `copiedLabel`, then both return. The confirmation lands where
16
+ * the user is looking, so callers raise no success toast.
17
+ */
18
+ export declare const CopyButton: ({ onCopy, label, copiedLabel, iconOnly, iconClassName, className, ...buttonProps }: CopyButtonProps) => import("react").JSX.Element;
19
+ //#endregion
@@ -0,0 +1,64 @@
1
+ "use client";
2
+ import { CheckIcon, CopyIcon } from "../icons.js";
3
+ import { cn } from "../lib/utils.js";
4
+ import { Button } from "./button.js";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { useRef, useState } from "react";
7
+ //#region src/components/copy-button.tsx
8
+ const COPIED_RESET_MS = 1500;
9
+ const SWAP_CLASS = "col-start-1 row-start-1 transition-[opacity,scale] duration-200 ease-out motion-reduce:transition-none";
10
+ /**
11
+ * A copy action that confirms in place: the glyph turns into a check and the
12
+ * label into `copiedLabel`, then both return. The confirmation lands where
13
+ * the user is looking, so callers raise no success toast.
14
+ */
15
+ const CopyButton = ({ onCopy, label, copiedLabel, iconOnly = false, iconClassName = "size-3.5", className, ...buttonProps }) => {
16
+ const [copied, setCopied] = useState(false);
17
+ const resetTimerRef = useRef(null);
18
+ const handleClick = async () => {
19
+ if (!await onCopy()) return;
20
+ if (resetTimerRef.current !== null) clearTimeout(resetTimerRef.current);
21
+ setCopied(true);
22
+ resetTimerRef.current = setTimeout(() => {
23
+ resetTimerRef.current = null;
24
+ setCopied(false);
25
+ }, COPIED_RESET_MS);
26
+ };
27
+ const currentLabel = copied ? copiedLabel : label;
28
+ const shown = (isCopiedSlot) => copied === isCopiedSlot ? "opacity-100 scale-100" : "opacity-0 scale-50";
29
+ return /* @__PURE__ */ jsxs(Button, {
30
+ ...buttonProps,
31
+ "aria-label": currentLabel,
32
+ className,
33
+ "data-copied": copied ? "" : void 0,
34
+ onClick: () => {
35
+ handleClick().catch(reportError);
36
+ },
37
+ children: [
38
+ /* @__PURE__ */ jsxs("span", {
39
+ "aria-hidden": "true",
40
+ className: "grid place-items-center",
41
+ children: [/* @__PURE__ */ jsx(CopyIcon, { className: cn(SWAP_CLASS, shown(false), iconClassName) }), /* @__PURE__ */ jsx(CheckIcon, { className: cn(SWAP_CLASS, shown(true), iconClassName) })]
42
+ }),
43
+ !iconOnly && /* @__PURE__ */ jsxs("span", {
44
+ className: "grid",
45
+ children: [/* @__PURE__ */ jsx("span", {
46
+ "aria-hidden": copied,
47
+ className: cn(SWAP_CLASS, copied ? "opacity-0" : "opacity-100"),
48
+ children: label
49
+ }), /* @__PURE__ */ jsx("span", {
50
+ "aria-hidden": !copied,
51
+ className: cn(SWAP_CLASS, copied ? "opacity-100" : "opacity-0"),
52
+ children: copiedLabel
53
+ })]
54
+ }),
55
+ /* @__PURE__ */ jsx("span", {
56
+ "aria-live": "polite",
57
+ className: "sr-only",
58
+ children: copied ? copiedLabel : ""
59
+ })
60
+ ]
61
+ });
62
+ };
63
+ //#endregion
64
+ export { CopyButton };
@@ -0,0 +1,58 @@
1
+ import * as React$1 from "react";
2
+ import { useRender } from "@base-ui/react/use-render";
3
+ import { VariantProps } from "class-variance-authority";
4
+ //#region src/components/list.d.ts
5
+ /**
6
+ * Grouped list: a titled section holding one surface of hairline-divided
7
+ * rows. Use it for settings and inventories of named things (connections,
8
+ * sessions, members, keys) where each row is icon + name + one quiet line +
9
+ * trailing status or action. Rows never scroll sideways: the content column
10
+ * truncates and the trailing column keeps its width, so actions stay on
11
+ * screen at any viewport.
12
+ */
13
+ declare const ListGroup: ({ className, ...props }: React$1.ComponentProps<"section">) => React$1.JSX.Element;
14
+ declare const ListGroupHeader: ({ className, ...props }: React$1.ComponentProps<"header">) => React$1.JSX.Element;
15
+ /** Title and description stack; takes the space the header action leaves. */
16
+ declare const ListGroupHeading: ({ className, ...props }: React$1.ComponentProps<"div">) => React$1.JSX.Element;
17
+ declare const ListGroupTitle: ({ className, children, ...props }: React$1.ComponentProps<"h2">) => React$1.JSX.Element;
18
+ /** Quiet item count after the title; shows matches while a search filters. */
19
+ declare const ListGroupCount: ({ className, ...props }: React$1.ComponentProps<"span">) => React$1.JSX.Element;
20
+ declare const ListGroupDescription: ({ className, ...props }: React$1.ComponentProps<"p">) => React$1.JSX.Element;
21
+ /** Quiet trailing affordance on the header row (a link or ghost button). */
22
+ declare const ListGroupAction: ({ className, ...props }: React$1.ComponentProps<"div">) => React$1.JSX.Element;
23
+ /** The surface. Depth comes from a ring plus a soft shadow, not a frame. */
24
+ declare const List: ({ className, ...props }: React$1.ComponentProps<"ul">) => React$1.JSX.Element;
25
+ /**
26
+ * One row. Pass `render` (a link or button element) to make the whole row the
27
+ * target; then keep other interactive elements out of it. Rows with their own
28
+ * trailing buttons stay a plain `li` and put the actions in `ListItemActions`.
29
+ */
30
+ declare const ListItem: ({ className, render, children, ...props }: useRender.ComponentProps<"div">) => React$1.JSX.Element;
31
+ declare const listItemMediaVariants: (props?: ({
32
+ variant?: "icon" | "initial" | null | undefined;
33
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
34
+ /** Fixed 32px tile for an icon, logo or initial. */
35
+ declare const ListItemMedia: ({ className, variant, ...props }: React$1.ComponentProps<"div"> & VariantProps<typeof listItemMediaVariants>) => React$1.JSX.Element;
36
+ declare const ListItemContent: ({ className, ...props }: React$1.ComponentProps<"div">) => React$1.JSX.Element;
37
+ declare const ListItemTitle: ({ className, ...props }: React$1.ComponentProps<"span">) => React$1.JSX.Element;
38
+ declare const listItemDescriptionVariants: (props?: ({
39
+ variant?: "code" | "text" | null | undefined;
40
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
41
+ /**
42
+ * Secondary line. Pass `line-clamp-2` when two lines are genuinely needed;
43
+ * never `whitespace-nowrap`.
44
+ */
45
+ declare const ListItemDescription: ({ className, variant, ...props }: React$1.ComponentProps<"span"> & VariantProps<typeof listItemDescriptionVariants>) => React$1.JSX.Element;
46
+ declare const ListItemActions: ({ className, ...props }: React$1.ComponentProps<"div">) => React$1.JSX.Element;
47
+ /** Single-line empty row inside a `List`. Phrase it as the next action. */
48
+ declare const ListEmpty: ({ className, ...props }: React$1.ComponentProps<"li">) => React$1.JSX.Element;
49
+ type StatusTone = "neutral" | "success" | "warning" | "destructive";
50
+ /**
51
+ * Dot + label status. The label always carries the meaning; the dot only
52
+ * reinforces it, so colour is never the sole signal.
53
+ */
54
+ declare const ListItemStatus: ({ tone, className, children, ...props }: React$1.ComponentProps<"span"> & {
55
+ tone?: StatusTone;
56
+ }) => React$1.JSX.Element;
57
+ //#endregion
58
+ export { List, ListEmpty, ListGroup, ListGroupAction, ListGroupCount, ListGroupDescription, ListGroupHeader, ListGroupHeading, ListGroupTitle, ListItem, ListItemActions, ListItemContent, ListItemDescription, ListItemMedia, ListItemStatus, ListItemTitle, type StatusTone };
@@ -0,0 +1,160 @@
1
+ "use client";
2
+ import { cn } from "../lib/utils.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { mergeProps } from "@base-ui/react/merge-props";
5
+ import { useRender } from "@base-ui/react/use-render";
6
+ import { cva } from "class-variance-authority";
7
+ //#region src/components/list.tsx
8
+ /**
9
+ * Grouped list: a titled section holding one surface of hairline-divided
10
+ * rows. Use it for settings and inventories of named things (connections,
11
+ * sessions, members, keys) where each row is icon + name + one quiet line +
12
+ * trailing status or action. Rows never scroll sideways: the content column
13
+ * truncates and the trailing column keeps its width, so actions stay on
14
+ * screen at any viewport.
15
+ */
16
+ const ListGroup = ({ className, ...props }) => /* @__PURE__ */ jsx("section", {
17
+ className: cn("flex flex-col gap-2.5", className),
18
+ "data-slot": "list-group",
19
+ ...props
20
+ });
21
+ const ListGroupHeader = ({ className, ...props }) => /* @__PURE__ */ jsx("header", {
22
+ className: cn("flex items-end gap-4 px-1", className),
23
+ "data-slot": "list-group-header",
24
+ ...props
25
+ });
26
+ /** Title and description stack; takes the space the header action leaves. */
27
+ const ListGroupHeading = ({ className, ...props }) => /* @__PURE__ */ jsx("div", {
28
+ className: cn("flex min-w-0 flex-1 flex-col gap-0.5", className),
29
+ "data-slot": "list-group-heading",
30
+ ...props
31
+ });
32
+ const ListGroupTitle = ({ className, children, ...props }) => /* @__PURE__ */ jsx("h2", {
33
+ className: cn("text-foreground text-sm font-medium text-balance", className),
34
+ "data-slot": "list-group-title",
35
+ ...props,
36
+ children
37
+ });
38
+ /** Quiet item count after the title; shows matches while a search filters. */
39
+ const ListGroupCount = ({ className, ...props }) => /* @__PURE__ */ jsx("span", {
40
+ className: cn("text-muted-foreground ms-1.5 font-normal tabular-nums", className),
41
+ "data-slot": "list-group-count",
42
+ ...props
43
+ });
44
+ const ListGroupDescription = ({ className, ...props }) => /* @__PURE__ */ jsx("p", {
45
+ className: cn("text-muted-foreground text-xs text-pretty", className),
46
+ "data-slot": "list-group-description",
47
+ ...props
48
+ });
49
+ /** Quiet trailing affordance on the header row (a link or ghost button). */
50
+ const ListGroupAction = ({ className, ...props }) => /* @__PURE__ */ jsx("div", {
51
+ className: cn("flex shrink-0 items-center gap-1", className),
52
+ "data-slot": "list-group-action",
53
+ ...props
54
+ });
55
+ /** The surface. Depth comes from a ring plus a soft shadow, not a frame. */
56
+ const List = ({ className, ...props }) => /* @__PURE__ */ jsx("ul", {
57
+ className: cn("bg-background ring-border divide-border flex flex-col divide-y overflow-hidden rounded-xl shadow-xs/5 ring-1", className),
58
+ "data-slot": "list",
59
+ ...props
60
+ });
61
+ /**
62
+ * One row. Pass `render` (a link or button element) to make the whole row the
63
+ * target; then keep other interactive elements out of it. Rows with their own
64
+ * trailing buttons stay a plain `li` and put the actions in `ListItemActions`.
65
+ */
66
+ const ListItem = ({ className, render, children, ...props }) => {
67
+ const defaultProps = {
68
+ className: cn("flex min-h-14 w-full items-center gap-3 px-4 py-2.5 text-start", render !== void 0 && "hover:bg-muted/50 focus-visible:bg-muted/50 focus-visible:ring-ring cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-inset", className),
69
+ "data-slot": "list-item",
70
+ children
71
+ };
72
+ const row = useRender({
73
+ defaultTagName: "div",
74
+ props: mergeProps(defaultProps, props),
75
+ render
76
+ });
77
+ return /* @__PURE__ */ jsx("li", {
78
+ className: "flex",
79
+ children: row
80
+ });
81
+ };
82
+ const listItemMediaVariants = cva("bg-muted grid size-8 shrink-0 place-items-center overflow-hidden rounded-lg outline-1 -outline-offset-1 outline-black/5 dark:outline-white/10", {
83
+ defaultVariants: { variant: "icon" },
84
+ variants: { variant: {
85
+ /** A glyph or logo, drawn quietly. */
86
+ icon: "text-muted-foreground [&_svg:not([class*='size-'])]:size-4",
87
+ /** A letter standing in for a name that has no logo. */
88
+ initial: "text-foreground text-xs font-medium"
89
+ } }
90
+ });
91
+ /** Fixed 32px tile for an icon, logo or initial. */
92
+ const ListItemMedia = ({ className, variant, ...props }) => /* @__PURE__ */ jsx("div", {
93
+ "aria-hidden": "true",
94
+ className: cn(listItemMediaVariants({ variant }), className),
95
+ "data-slot": "list-item-media",
96
+ ...props
97
+ });
98
+ const ListItemContent = ({ className, ...props }) => /* @__PURE__ */ jsx("div", {
99
+ className: cn("flex min-w-0 flex-1 flex-col gap-0.5", className),
100
+ "data-slot": "list-item-content",
101
+ ...props
102
+ });
103
+ const ListItemTitle = ({ className, ...props }) => /* @__PURE__ */ jsx("span", {
104
+ className: cn("text-foreground truncate text-sm font-medium", className),
105
+ "data-slot": "list-item-title",
106
+ dir: "auto",
107
+ ...props
108
+ });
109
+ const listItemDescriptionVariants = cva("text-muted-foreground text-xs", {
110
+ defaultVariants: { variant: "text" },
111
+ variants: { variant: {
112
+ /** Prose, clamped to one line so long provider copy cannot widen the row. */
113
+ text: "line-clamp-1 wrap-break-word",
114
+ /** A URL or command: monospace, left-to-right, truncated at the end. */
115
+ code: "truncate font-mono"
116
+ } }
117
+ });
118
+ /**
119
+ * Secondary line. Pass `line-clamp-2` when two lines are genuinely needed;
120
+ * never `whitespace-nowrap`.
121
+ */
122
+ const ListItemDescription = ({ className, variant, ...props }) => /* @__PURE__ */ jsx("span", {
123
+ className: cn(listItemDescriptionVariants({ variant }), className),
124
+ "data-slot": "list-item-description",
125
+ dir: variant === "code" ? "ltr" : "auto",
126
+ ...props
127
+ });
128
+ const ListItemActions = ({ className, ...props }) => /* @__PURE__ */ jsx("div", {
129
+ className: cn("flex shrink-0 items-center gap-2", className),
130
+ "data-slot": "list-item-actions",
131
+ ...props
132
+ });
133
+ /** Single-line empty row inside a `List`. Phrase it as the next action. */
134
+ const ListEmpty = ({ className, ...props }) => /* @__PURE__ */ jsx("li", {
135
+ className: cn("text-muted-foreground flex min-h-14 items-center gap-2 px-4 py-2.5 text-sm", className),
136
+ "data-slot": "list-empty",
137
+ ...props
138
+ });
139
+ const STATUS_DOT_CLASS = {
140
+ neutral: "bg-foreground-disabled",
141
+ success: "bg-success",
142
+ warning: "bg-warning",
143
+ destructive: "bg-destructive"
144
+ };
145
+ /**
146
+ * Dot + label status. The label always carries the meaning; the dot only
147
+ * reinforces it, so colour is never the sole signal.
148
+ */
149
+ const ListItemStatus = ({ tone = "neutral", className, children, ...props }) => /* @__PURE__ */ jsxs("span", {
150
+ className: cn("text-muted-foreground inline-flex items-center gap-1.5 text-xs whitespace-nowrap", className),
151
+ "data-slot": "list-item-status",
152
+ "data-tone": tone,
153
+ ...props,
154
+ children: [/* @__PURE__ */ jsx("span", {
155
+ "aria-hidden": "true",
156
+ className: cn("size-1.5 shrink-0 rounded-full", STATUS_DOT_CLASS[tone])
157
+ }), children]
158
+ });
159
+ //#endregion
160
+ export { List, ListEmpty, ListGroup, ListGroupAction, ListGroupCount, ListGroupDescription, ListGroupHeader, ListGroupHeading, ListGroupTitle, ListItem, ListItemActions, ListItemContent, ListItemDescription, ListItemMedia, ListItemStatus, ListItemTitle };
@@ -20,13 +20,13 @@ function MenuTrigger({ tooltip, ...props }) {
20
20
  })
21
21
  });
22
22
  }
23
- const MenuPopup = ({ children, className, sideOffset = 4, align = "center", alignOffset, side = "bottom", anchor, ...props }) => /* @__PURE__ */ jsx(Menu$1.Portal, { children: /* @__PURE__ */ jsx(Menu$1.Positioner, {
24
- align: anchor ? "start" : align,
23
+ const MenuPopup = ({ children, className, sideOffset = 4, align, alignOffset, side = "bottom", anchor, ...props }) => /* @__PURE__ */ jsx(Menu$1.Portal, { children: /* @__PURE__ */ jsx(Menu$1.Positioner, {
24
+ align: align ?? (anchor ? "start" : "center"),
25
25
  alignOffset,
26
26
  anchor,
27
27
  className: OVERLAY_LAYER_CLASS_NAMES.popup,
28
28
  "data-slot": "menu-positioner",
29
- side: anchor ? "bottom" : side,
29
+ side,
30
30
  sideOffset,
31
31
  children: /* @__PURE__ */ jsx(Menu$1.Popup, {
32
32
  className: cn("bg-popover relative flex origin-(--transform-origin) rounded-lg border shadow-lg/5 outline-none not-dark:bg-clip-padding not-[class*='w-']:min-w-32 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] focus:outline-none dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", className),
@@ -0,0 +1,20 @@
1
+ import { InputGroupInput } from "./input-group.js";
2
+ import * as React$1 from "react";
3
+ //#region src/components/search-field.d.ts
4
+ type SearchFieldProps = Omit<React$1.ComponentProps<typeof InputGroupInput>, "onChange" | "type" | "value"> & {
5
+ value: string;
6
+ onValueChange: (value: string) => void;
7
+ /** Accessible name of the clear button; the package ships no copy. */
8
+ clearLabel: string;
9
+ /** Class for the outer group (width, margins). */
10
+ groupClassName?: string;
11
+ };
12
+ /**
13
+ * Filter-as-you-type search box: leading magnifier, clear button once there is
14
+ * a query, and Escape clears before it blurs. The caller owns the query state
15
+ * and the matching; keep matching local and instant for lists under a few
16
+ * hundred rows.
17
+ */
18
+ declare const SearchField: ({ value, onValueChange, clearLabel, groupClassName, className, onKeyDown, ...props }: SearchFieldProps) => React$1.JSX.Element;
19
+ //#endregion
20
+ export { SearchField, type SearchFieldProps };
@@ -0,0 +1,53 @@
1
+ "use client";
2
+ import { SearchIcon, XIcon } from "../icons.js";
3
+ import { cn } from "../lib/utils.js";
4
+ import { Button } from "./button.js";
5
+ import { InputGroup, InputGroupAddon, InputGroupInput } from "./input-group.js";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ //#region src/components/search-field.tsx
8
+ /**
9
+ * Filter-as-you-type search box: leading magnifier, clear button once there is
10
+ * a query, and Escape clears before it blurs. The caller owns the query state
11
+ * and the matching; keep matching local and instant for lists under a few
12
+ * hundred rows.
13
+ */
14
+ const SearchField = ({ value, onValueChange, clearLabel, groupClassName, className, onKeyDown, ...props }) => /* @__PURE__ */ jsxs(InputGroup, {
15
+ className: cn("min-h-11 sm:min-h-0", groupClassName),
16
+ children: [
17
+ /* @__PURE__ */ jsx(InputGroupAddon, { children: /* @__PURE__ */ jsx(SearchIcon, {}) }),
18
+ /* @__PURE__ */ jsx(InputGroupInput, {
19
+ autoComplete: "off",
20
+ spellCheck: false,
21
+ ...props,
22
+ className: cn("max-sm:h-11 max-sm:leading-11", className),
23
+ onChange: (event) => {
24
+ onValueChange(event.target.value);
25
+ },
26
+ onKeyDown: (event) => {
27
+ if (event.key === "Escape" && value.length > 0) {
28
+ event.preventDefault();
29
+ event.stopPropagation();
30
+ onValueChange("");
31
+ }
32
+ onKeyDown?.(event);
33
+ },
34
+ type: "search",
35
+ value
36
+ }),
37
+ value.length > 0 && /* @__PURE__ */ jsx(InputGroupAddon, {
38
+ align: "inline-end",
39
+ children: /* @__PURE__ */ jsx(Button, {
40
+ "aria-label": clearLabel,
41
+ onClick: () => {
42
+ onValueChange("");
43
+ },
44
+ size: "icon-xs",
45
+ type: "button",
46
+ variant: "ghost",
47
+ children: /* @__PURE__ */ jsx(XIcon, {})
48
+ })
49
+ })
50
+ ]
51
+ });
52
+ //#endregion
53
+ export { SearchField };
@@ -33,7 +33,7 @@ declare function useSidebarLayout(): SidebarLayout;
33
33
  * left for the content column. Mobile reports 0: there the sidebar is an
34
34
  * overlay sheet and occupies no layout width.
35
35
  */
36
- declare function useSidebarInlineSize(): 0 | 256 | 48;
36
+ declare function useSidebarInlineSize(): 0 | 48 | 256;
37
37
  declare const SidebarProvider: ({ defaultOpen, forceCollapsed, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }: React.ComponentProps<"div"> & {
38
38
  defaultOpen?: boolean;
39
39
  forceCollapsed?: boolean;
@@ -86,7 +86,7 @@ declare const SidebarMenu: ({ className, ...props }: React.ComponentProps<"ul">)
86
86
  declare const SidebarMenuItem: ({ className, ...props }: React.ComponentProps<"li">) => import("react").JSX.Element;
87
87
  declare const sidebarMenuButtonVariants: (props?: ({
88
88
  variant?: "default" | "outline" | null | undefined;
89
- size?: "default" | "lg" | "sm" | "rail" | null | undefined;
89
+ size?: "sm" | "default" | "lg" | "rail" | null | undefined;
90
90
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
91
91
  /**
92
92
  * The footer's avatar trigger, signed in (user menu) or not (sign-in): a
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { MessageSquarePlusIcon } from "../icons.js";
2
+ import { NewChatIcon } from "../icons.js";
3
3
  import { cn } from "../lib/utils.js";
4
4
  import { SHELL_CHROME_LAYER_CLASS_NAME } from "../lib/overlay-layer.js";
5
5
  import { useIsMobile } from "../hooks/use-mobile.js";
@@ -48,7 +48,7 @@ const WorkspaceShell = ({ children, endDock, navigation, topBar }) => {
48
48
  "data-slot": "workspace-shell-top-bar",
49
49
  children: topBar({ compactNavigationTrigger })
50
50
  }), /* @__PURE__ */ jsx("div", {
51
- className: "flex min-h-0 flex-1 flex-col overflow-auto overscroll-contain",
51
+ className: "flex min-h-0 flex-1 flex-col overflow-auto overscroll-contain [&>*:first-child]:border-t-0",
52
52
  "data-slot": "workspace-shell-content",
53
53
  children
54
54
  })]
@@ -91,7 +91,7 @@ const WorkspaceEndRail = ({ chatAction, children, className, label, overlay, top
91
91
  "aria-label": chatAction.label,
92
92
  onClick: chatAction.onActivate,
93
93
  title: chatAction.label,
94
- children: /* @__PURE__ */ jsx(MessageSquarePlusIcon, {
94
+ children: /* @__PURE__ */ jsx(NewChatIcon, {
95
95
  "aria-hidden": "true",
96
96
  className: "size-4"
97
97
  })
@@ -99,7 +99,7 @@ const WorkspaceEndRail = ({ chatAction, children, className, label, overlay, top
99
99
  "aria-label": `${chatAction.label}: ${chatAction.reason}`,
100
100
  disabled: true,
101
101
  title: chatAction.reason,
102
- children: /* @__PURE__ */ jsx(MessageSquarePlusIcon, {
102
+ children: /* @__PURE__ */ jsx(NewChatIcon, {
103
103
  "aria-hidden": "true",
104
104
  className: "size-4"
105
105
  })