@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 +40 -75
- package/dist/components/button.d.ts +1 -1
- package/dist/components/composer.d.ts +9 -1
- package/dist/components/composer.js +19 -2
- package/dist/components/copy-button.d.ts +19 -0
- package/dist/components/copy-button.js +64 -0
- package/dist/components/list.d.ts +58 -0
- package/dist/components/list.js +160 -0
- package/dist/components/menu.js +3 -3
- package/dist/components/search-field.d.ts +20 -0
- package/dist/components/search-field.js +53 -0
- package/dist/components/sidebar.d.ts +2 -2
- package/dist/components/workspace-shell.js +4 -4
- package/dist/icons.d.ts +2 -2
- package/dist/icons.js +2 -2
- package/dist/index.d.ts +5 -5
- package/dist/index.js +4 -4
- package/dist/kanban/index.d.ts +2 -5
- package/dist/kanban/index.js +1 -4
- package/dist/kanban/subgroup-board.d.ts +2 -27
- package/dist/kanban/subgroup-board.js +1 -25
- package/dist/kanban/virtual-cell.d.ts +1 -18
- package/dist/kanban/virtual-cell.js +5 -47
- package/dist/lib/control-size.d.ts +1 -1
- package/dist/styles/theme.css +7 -0
- package/package.json +13 -5
- package/dist/kanban/sortable-edge.d.ts +0 -38
- package/dist/kanban/sortable-edge.js +0 -27
- package/dist/kanban/sortable-interactions.d.ts +0 -151
- package/dist/kanban/sortable-interactions.js +0 -527
- package/dist/kanban/sortable-interactions.logic.d.ts +0 -78
- package/dist/kanban/sortable-interactions.logic.js +0 -205
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
|
-
|
|
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
|
-
##
|
|
104
|
+
## Kanban boards
|
|
105
105
|
|
|
106
106
|
`@stll/ui/kanban` provides board matrices, subgroup swimlanes, bounded virtual
|
|
107
|
-
cells, and
|
|
108
|
-
|
|
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" | "
|
|
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 };
|
package/dist/components/menu.js
CHANGED
|
@@ -20,13 +20,13 @@ function MenuTrigger({ tooltip, ...props }) {
|
|
|
20
20
|
})
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
|
-
const MenuPopup = ({ children, className, sideOffset = 4, align
|
|
24
|
-
align: anchor ? "start" :
|
|
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
|
|
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 |
|
|
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?: "
|
|
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 {
|
|
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(
|
|
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(
|
|
102
|
+
children: /* @__PURE__ */ jsx(NewChatIcon, {
|
|
103
103
|
"aria-hidden": "true",
|
|
104
104
|
className: "size-4"
|
|
105
105
|
})
|