@asharca/ui 0.2.1 → 0.2.2

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
@@ -1,14 +1,30 @@
1
1
  # @asharca/ui
2
2
 
3
+ `ChatComposerToolbar` (`@asharca/ui/chat-composer-toolbar`) provides a unified
4
+ tool menu and configurable, ordered shortcuts. Pass `tools`, `pinnedIds`, and
5
+ `onPinnedIdsChange`; the consuming app owns actions and preference persistence.
6
+ Use it in `ChatThread.composerTools` with `showAttachmentPicker={false}` to
7
+ replace the default attachment button. The default remains unchanged.
8
+
3
9
  Reusable React controls, chat thread, conversation sidebar, and responsive shell extracted from ToolPlane. Routing, persistence, authentication, and API handlers stay in the host application.
4
10
 
5
11
  ## Install
6
12
 
7
13
  ```bash
8
- pnpm add @asharca/ui @assistant-ui/react@0.15.18
9
- # or: npm install @asharca/ui @assistant-ui/react@0.15.18
14
+ pnpm add @asharca/ui
15
+ # or: npm install @asharca/ui
16
+ ```
17
+
18
+ pnpm installs missing peer dependencies by default. If automatic peer installation
19
+ is disabled or your package manager leaves peers missing, install them explicitly:
20
+
21
+ ```bash
22
+ pnpm add @asharca/ui @assistant-ui/react@0.15.18 react@^19 react-dom@^19 tailwindcss@^4
10
23
  ```
11
24
 
25
+ For version conflicts, first check that your application and other dependencies
26
+ support these versions. Installing the package does not configure your CSS build.
27
+
12
28
  The package uses React 19 and Tailwind CSS 4. `ChatThread` additionally accepts an assistant-ui `AssistantRuntime`, so transport and persistence stay in the host application.
13
29
 
14
30
  The runtime peer is pinned to `@assistant-ui/react@0.15.18`, paired with
@@ -55,9 +71,24 @@ export function Toolbar() {
55
71
 
56
72
  ## Modules
57
73
 
74
+ - `@asharca/ui/workspace-sidebar` provides `WorkspaceSidebar`: controlled `collapsed`
75
+ and `mobileOpen` states, `items`, `activeId`, `onSelect`, brand/workspace content
76
+ and a `footer` slot. Items accept `icon`, `label`, `badge`, and `disabled`.
77
+ Widths use `--workspace-sidebar-width` and `--workspace-sidebar-collapsed-width`.
78
+ The mobile breakpoint is 850px. Hosts own routing, persistence, the open trigger,
79
+ scrim, focus trapping/restoration, and background inertness for modal drawers;
80
+ `mobileCloseRef` exposes the built-in close button for focus management.
81
+
58
82
  - `@asharca/ui/controls` — buttons and native form controls.
83
+ - `@asharca/ui/controls` also provides `Switch`, native `Slider`, and an additive `outline` button variant.
84
+ - `@asharca/ui/avatar` — `Avatar`, `AvatarImage`, and `AvatarFallback`.
85
+ - `@asharca/ui/workspace-tab-bar` — ToolPlane's `WorkspaceTabBar` with host-controlled selection, closing, pinning, drag reordering, and new-window callbacks. The showcase wires these actions to its sidebar and pages; route state stays in the host application.
86
+ - `@asharca/ui/accordion` — `Accordion`, `AccordionItem`, `AccordionTrigger`, and `AccordionContent`.
87
+ - `@asharca/ui/navigation` also provides Radix-backed `Tabs`, `TabsList`, `TabsTrigger`, and `TabsContent` with keyboard navigation. Existing `Tab` / `TabList` exports are unchanged.
88
+ - `@asharca/ui/feedback` also provides native `Progress` and decorative `Skeleton`. Give progress bars and sliders an accessible label; label switches via `htmlFor` or `aria-label`.
89
+ - `@asharca/ui/overlays` also provides `DropdownMenu`, its trigger, portal, content, item, group, label, and separator.
59
90
  - `@asharca/ui/forms` — submit, confirm-submit, and copy actions.
60
- - `@asharca/ui/layout` — page, header, toolbar, section, panel, card, empty state, entity, and data table.
91
+ - `@asharca/ui/layout` — page, header, toolbar, section, panel, card, empty state, entity, and data table. `DataTable` supports controlled multi-select with `selectable`, `rowIds`, `selectedRowIds`, and `onSelectedRowIdsChange`.
61
92
  - `@asharca/ui/navigation` — tabs, chips, and pagination layout.
62
93
  - `@asharca/ui/feedback` — badges, status, alerts, and spinners.
63
94
  - `@asharca/ui/dialog` and `@asharca/ui/overlays` — Dialog, Popover, Tooltip, Context Menu, and Hover Card primitives.
@@ -115,8 +146,28 @@ Build the runtime with AI SDK, assistant-ui local runtime, or another adapter in
115
146
 
116
147
  `ConversationSidebar` conversations may also provide optional `meta` content and a `deleting` state, so hosts can keep domain badges and show async delete progress without replacing the list layout.
117
148
 
149
+ Pass `onConversationOrderChange(groupId, conversationIds)` to enable same-group
150
+ conversation drag ordering and accessible move-up/down buttons. The callback
151
+ returns the full group order, including items hidden by search. Hosts own the
152
+ reordered state and persistence; selection is unchanged. Disabled/deleting
153
+ conversations cannot be moved. Omitting the callback preserves the default list.
154
+
118
155
  Theme defaults are scoped to package component roots. Override them with HSL-channel variables such as `--toolplane-ui-background`, `--toolplane-ui-foreground`, and `--toolplane-ui-brand`. The older `--chat-ui-*` variables remain supported; `--chat-ui-sidebar-width` and `--chat-ui-right-panel-width` still control chat layout. Add a `.dark` class to an ancestor, or `data-theme="dark"` to a component, to use the dark defaults.
119
156
 
157
+ ## Component workbench
158
+
159
+ Run `pnpm dev` and open the local URL printed by Vite. The standalone showcase
160
+ includes interactive controls, forms, tables, overlays, light/dark themes, and
161
+ a local chat demo (no AI service or credentials required). Demo data is kept in
162
+ memory and resets on reload.
163
+
164
+ `pnpm build:showcase` type-checks and builds the site into `showcase-dist/`.
165
+ The showcase is development-only and is not included in the npm package.
166
+ It includes nine interactive examples with copyable code snippets. Profile photos
167
+ in `showcase/public/avatars` are demo assets from Unsplash (photo IDs
168
+ `1494790108377-be9c29b29330`, `1506794778202-cad84cf45f1d`, and
169
+ `1534528741775-53994a69daeb`); member names and emails are fictional.
170
+
120
171
  ## Build and pack
121
172
 
122
173
  ```bash
@@ -0,0 +1,5 @@
1
+ import { Accordion as AccordionPrimitive } from 'radix-ui';
2
+ export declare const Accordion: import("react").ForwardRefExoticComponent<(AccordionPrimitive.AccordionSingleProps | AccordionPrimitive.AccordionMultipleProps) & import("react").RefAttributes<HTMLDivElement>>;
3
+ export declare const AccordionItem: import("react").ForwardRefExoticComponent<Omit<AccordionPrimitive.AccordionItemProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
4
+ export declare const AccordionTrigger: import("react").ForwardRefExoticComponent<Omit<AccordionPrimitive.AccordionTriggerProps & import("react").RefAttributes<HTMLButtonElement>, "ref"> & import("react").RefAttributes<HTMLButtonElement>>;
5
+ export declare const AccordionContent: import("react").ForwardRefExoticComponent<Omit<AccordionPrimitive.AccordionContentProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
13
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
+ import { forwardRef } from 'react';
15
+ import { Accordion as AccordionPrimitive } from 'radix-ui';
16
+ import { ChevronDown } from 'lucide-react';
17
+ export const Accordion = AccordionPrimitive.Root;
18
+ export const AccordionItem = forwardRef(function AccordionItem(_a, ref) {
19
+ var { className } = _a, props = __rest(_a, ["className"]);
20
+ return _jsx(AccordionPrimitive.Item, Object.assign({}, props, { ref: ref, "data-toolplane-ui": "accordion-item", className: `ui-accordion-item ${className !== null && className !== void 0 ? className : ''}`.trim() }));
21
+ });
22
+ export const AccordionTrigger = forwardRef(function AccordionTrigger(_a, ref) {
23
+ var { children, className } = _a, props = __rest(_a, ["children", "className"]);
24
+ return _jsx(AccordionPrimitive.Header, { className: "ui-accordion-header", children: _jsxs(AccordionPrimitive.Trigger, Object.assign({}, props, { ref: ref, className: `ui-accordion-trigger ${className !== null && className !== void 0 ? className : ''}`.trim(), children: [children, _jsx(ChevronDown, { "aria-hidden": "true", className: "ui-accordion-chevron" })] })) });
25
+ });
26
+ export const AccordionContent = forwardRef(function AccordionContent(_a, ref) {
27
+ var { className } = _a, props = __rest(_a, ["className"]);
28
+ return _jsx(AccordionPrimitive.Content, Object.assign({}, props, { ref: ref, className: `ui-accordion-content ${className !== null && className !== void 0 ? className : ''}`.trim() }));
29
+ });
@@ -0,0 +1,4 @@
1
+ import { Avatar as AvatarPrimitive } from 'radix-ui';
2
+ export declare const Avatar: import("react").ForwardRefExoticComponent<Omit<AvatarPrimitive.AvatarProps & import("react").RefAttributes<HTMLSpanElement>, "ref"> & import("react").RefAttributes<HTMLSpanElement>>;
3
+ export declare const AvatarImage: import("react").ForwardRefExoticComponent<Omit<AvatarPrimitive.AvatarImageProps & import("react").RefAttributes<HTMLImageElement>, "ref"> & import("react").RefAttributes<HTMLImageElement>>;
4
+ export declare const AvatarFallback: import("react").ForwardRefExoticComponent<Omit<AvatarPrimitive.AvatarFallbackProps & import("react").RefAttributes<HTMLSpanElement>, "ref"> & import("react").RefAttributes<HTMLSpanElement>>;
package/dist/Avatar.js ADDED
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
13
+ import { jsx as _jsx } from "react/jsx-runtime";
14
+ import { forwardRef } from 'react';
15
+ import { Avatar as AvatarPrimitive } from 'radix-ui';
16
+ export const Avatar = forwardRef(function Avatar(_a, ref) {
17
+ var { className } = _a, props = __rest(_a, ["className"]);
18
+ return _jsx(AvatarPrimitive.Root, Object.assign({}, props, { ref: ref, "data-toolplane-ui": "avatar", className: `ui-avatar ${className !== null && className !== void 0 ? className : ''}`.trim() }));
19
+ });
20
+ export const AvatarImage = forwardRef(function AvatarImage(_a, ref) {
21
+ var { className } = _a, props = __rest(_a, ["className"]);
22
+ return _jsx(AvatarPrimitive.Image, Object.assign({}, props, { ref: ref, className: `ui-avatar-image ${className !== null && className !== void 0 ? className : ''}`.trim() }));
23
+ });
24
+ export const AvatarFallback = forwardRef(function AvatarFallback(_a, ref) {
25
+ var { className } = _a, props = __rest(_a, ["className"]);
26
+ return _jsx(AvatarPrimitive.Fallback, Object.assign({}, props, { ref: ref, className: `ui-avatar-fallback ${className !== null && className !== void 0 ? className : ''}`.trim() }));
27
+ });
@@ -0,0 +1,26 @@
1
+ import { type ReactNode } from 'react';
2
+ export type ChatComposerTool = {
3
+ id: string;
4
+ label: string;
5
+ icon: ReactNode;
6
+ description?: string;
7
+ disabled?: boolean;
8
+ pressed?: boolean;
9
+ onSelect: () => void;
10
+ };
11
+ export type ChatComposerToolbarLabels = {
12
+ open: string;
13
+ customize: string;
14
+ close: string;
15
+ reset: string;
16
+ moveUp: (label: string) => string;
17
+ moveDown: (label: string) => string;
18
+ };
19
+ export type ChatComposerToolbarProps = {
20
+ tools: readonly ChatComposerTool[];
21
+ pinnedIds: readonly string[];
22
+ onPinnedIdsChange: (ids: string[]) => void;
23
+ disabled?: boolean;
24
+ labels?: Partial<ChatComposerToolbarLabels>;
25
+ };
26
+ export declare function ChatComposerToolbar({ tools, pinnedIds, onPinnedIdsChange, disabled, labels }: ChatComposerToolbarProps): import("react").JSX.Element;
@@ -0,0 +1,24 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useState } from 'react';
4
+ import { ArrowDown, ArrowUp, Plus, RotateCcw, SlidersHorizontal, X } from 'lucide-react';
5
+ import { Button, IconButton } from "./Controls.js";
6
+ import { Dialog, DialogClose, DialogContent, DialogOverlay, DialogPortal, DialogTitle } from "./Dialog.js";
7
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuSeparator, DropdownMenuTrigger } from "./Overlays.js";
8
+ export function ChatComposerToolbar({ tools, pinnedIds, onPinnedIdsChange, disabled, labels }) {
9
+ const [customizing, setCustomizing] = useState(false);
10
+ const copy = Object.assign({ open: 'Open tools', customize: 'Customize toolbar', close: 'Close', reset: 'Reset toolbar', moveUp: (label) => `Move ${label} up`, moveDown: (label) => `Move ${label} down` }, labels);
11
+ const pinned = [...new Set(pinnedIds)].flatMap((id) => { var _a; return (_a = tools.find((tool) => tool.id === id)) !== null && _a !== void 0 ? _a : []; });
12
+ const ids = pinned.map((tool) => tool.id);
13
+ const unpinned = tools.filter((tool) => !ids.includes(tool.id));
14
+ const move = (index, offset) => {
15
+ const next = [...ids];
16
+ const [id] = next.splice(index, 1);
17
+ next.splice(index + offset, 0, id);
18
+ onPinnedIdsChange(next);
19
+ };
20
+ return _jsxs(_Fragment, { children: [_jsxs("div", { className: "ui-composer-toolbar", "data-chat-ui": "composer-toolbar", children: [_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("button", { type: "button", className: "ui-composer-tool", "aria-label": copy.open, title: copy.open, disabled: disabled, children: _jsx(Plus, { size: 18 }) }) }), _jsx(DropdownMenuPortal, { children: _jsxs(DropdownMenuContent, { side: "top", align: "start", sideOffset: 8, className: "ui-composer-menu", "aria-label": copy.open, children: [unpinned.map((tool) => _jsxs(DropdownMenuItem, { disabled: disabled || tool.disabled, onSelect: tool.onSelect, children: [tool.icon, _jsxs("span", { children: [tool.label, tool.description && _jsx("small", { children: tool.description })] })] }, tool.id)), unpinned.length > 0 && _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { disabled: disabled, onSelect: () => setCustomizing(true), children: [_jsx(SlidersHorizontal, { size: 16 }), copy.customize] })] }) })] }), _jsx("div", { className: "ui-composer-shortcuts", children: pinned.map((tool) => _jsx("button", { type: "button", className: "ui-composer-tool", "data-composer-shortcut": tool.id, disabled: disabled || tool.disabled, "aria-label": tool.label, title: tool.label, "aria-pressed": tool.pressed, onClick: tool.onSelect, children: tool.icon }, tool.id)) })] }), _jsx(Dialog, { open: customizing, onOpenChange: setCustomizing, children: _jsxs(DialogPortal, { children: [_jsx(DialogOverlay, {}), _jsxs(DialogContent, { className: "ui-composer-customize", "aria-describedby": undefined, children: [_jsxs("header", { children: [_jsx(DialogTitle, { children: copy.customize }), _jsx(DialogClose, { asChild: true, children: _jsx(IconButton, { variant: "ghost", label: copy.close, icon: _jsx(X, { size: 16 }) }) })] }), _jsx("div", { children: [...pinned, ...unpinned].map((tool) => {
21
+ const index = ids.indexOf(tool.id);
22
+ return _jsxs("div", { className: "ui-composer-option", children: [_jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: index >= 0, disabled: disabled, onChange: (event) => onPinnedIdsChange(event.target.checked ? [...ids, tool.id] : ids.filter((id) => id !== tool.id)) }), tool.icon, _jsx("span", { children: tool.label })] }), index >= 0 && _jsxs(_Fragment, { children: [_jsx(IconButton, { variant: "ghost", label: copy.moveUp(tool.label), icon: _jsx(ArrowUp, { size: 14 }), disabled: disabled || index === 0, onClick: () => move(index, -1) }), _jsx(IconButton, { variant: "ghost", label: copy.moveDown(tool.label), icon: _jsx(ArrowDown, { size: 14 }), disabled: disabled || index === ids.length - 1, onClick: () => move(index, 1) })] })] }, tool.id);
23
+ }) }), _jsx("footer", { children: _jsxs(Button, { variant: "outline", disabled: disabled || !ids.length, onClick: () => onPinnedIdsChange([]), children: [_jsx(RotateCcw, { size: 14 }), copy.reset] }) })] })] }) })] });
24
+ }
@@ -69,6 +69,7 @@ export type ChatThreadProps = {
69
69
  composerEnd?: ReactNode;
70
70
  composerStatus?: ReactNode;
71
71
  composerTools?: ReactNode;
72
+ showAttachmentPicker?: boolean;
72
73
  disabled?: boolean;
73
74
  emptyState?: ReactNode;
74
75
  error?: ReactNode;
@@ -18,7 +18,7 @@ import { code } from '@streamdown/code';
18
18
  import { Popover } from 'radix-ui';
19
19
  import remarkBreaks from 'remark-breaks';
20
20
  import { defaultRemarkPlugins } from 'streamdown';
21
- import { Bot, Box, Brain, Check, CheckCircle2, ChevronDown, ChevronLeft, ChevronRight, CircleAlert, CirclePause, Copy, Globe2, Loader2, Maximize2, Minimize2, Paperclip, Pencil, Plug, Plus, RefreshCw, Send, Split, UserRound, Wrench, X, } from 'lucide-react';
21
+ import { Bot, Box, Brain, Check, CheckCircle2, ChevronDown, ChevronLeft, ChevronRight, CircleAlert, CirclePause, Copy, ArrowUp, Globe2, Loader2, Maximize2, Minimize2, Paperclip, Pencil, Plug, Plus, RefreshCw, Split, UserRound, Wrench, X, } from 'lucide-react';
22
22
  import { Button, IconButton } from "./Controls.js";
23
23
  import { hasMermaidFence } from "./chat-markdown.js";
24
24
  const MermaidAssistantText = lazy(() => import("./MermaidAssistantText.js"));
@@ -259,7 +259,7 @@ function ComposerExpand({ expanded, onToggle }) {
259
259
  const label = expanded ? labels.restoreComposer : labels.expandComposer;
260
260
  return (_jsx("div", { className: "absolute right-px top-px z-10 size-8", children: _jsx("button", { type: "button", onClick: onToggle, "aria-label": label, title: label, "aria-pressed": expanded, className: "pointer-events-none absolute right-1 top-1 flex size-[22px] -translate-y-2.5 translate-x-2.5 rotate-[-8deg] scale-80 items-center justify-center rounded-full bg-transparent text-muted-foreground opacity-0 transition-all duration-300 hover:bg-muted hover:text-foreground focus-visible:pointer-events-auto focus-visible:translate-x-0 focus-visible:translate-y-0 focus-visible:rotate-0 focus-visible:scale-100 focus-visible:opacity-100 group-focus-within/composer:pointer-events-auto group-focus-within/composer:translate-x-0 group-focus-within/composer:translate-y-0 group-focus-within/composer:rotate-0 group-focus-within/composer:scale-100 group-focus-within/composer:opacity-100 group-hover/composer:pointer-events-auto group-hover/composer:translate-x-0 group-hover/composer:translate-y-0 group-hover/composer:rotate-0 group-hover/composer:scale-100 group-hover/composer:opacity-100", children: _jsx(Icon, { className: "size-3" }) }) }));
261
261
  }
262
- function ChatThreadContent({ assistantName, allowAttachments, allowEdit, allowRegenerate, branchNavigation, busy, className, composerEnd, composerStatus, composerTools, components, disabled, emptyState, error, onBranchSelect, onBranchStart, onRegenerateMessage, }) {
262
+ function ChatThreadContent({ assistantName, allowAttachments, allowEdit, allowRegenerate, branchNavigation, busy, className, composerEnd, composerStatus, composerTools, showAttachmentPicker = true, components, disabled, emptyState, error, onBranchSelect, onBranchStart, onRegenerateMessage, }) {
263
263
  const labels = useLabels();
264
264
  const [composerRows, setComposerRows] = useState(2);
265
265
  const composerInputRef = useRef(null);
@@ -276,7 +276,7 @@ function ChatThreadContent({ assistantName, allowAttachments, allowEdit, allowRe
276
276
  const blocked = disabled || busy;
277
277
  return (_jsxs(ThreadPrimitive.Root, { "data-chat-ui": "chat-thread", className: cx('flex min-h-0 flex-1 flex-col', className), children: [_jsxs(ThreadPrimitive.Viewport, { className: "relative flex min-h-0 flex-1 flex-col overflow-y-auto bg-background", children: [_jsxs("div", { className: "flex-1 py-1.5", children: [_jsx(ThreadPrimitive.Empty, { children: _jsx("div", { className: "flex min-h-full items-center justify-center px-6 pb-24", children: emptyState !== null && emptyState !== void 0 ? emptyState : (_jsxs("div", { className: "max-w-md text-center", children: [_jsx("div", { className: "mx-auto mb-4 flex size-12 items-center justify-center rounded-full bg-muted text-muted-foreground", children: _jsx(Bot, { className: "size-6" }) }), _jsx("h3", { className: "text-lg font-medium text-foreground", children: labels.startConversation })] })) }) }), _jsx("div", { className: "mx-auto flex w-full max-w-[53rem] flex-col gap-0 px-6", children: _jsx(ThreadPrimitive.Messages, { children: ({ message }) => message.role === 'user'
278
278
  ? (_jsx(UserMessage, { allowEdit: allowEdit, branch: branchByMessageId.get(message.id), busy: blocked, components: components, messageId: message.id, onBranchSelect: onBranchSelect }))
279
- : (_jsx(AssistantMessage, { agentName: assistantName, allowRegenerate: allowRegenerate, branch: branchByMessageId.get(message.id), busy: blocked, components: components, messageId: message.id, onBranchSelect: onBranchSelect, onBranchStart: onBranchStart, onRegenerateMessage: onRegenerateMessage })) }) })] }), _jsx(ThreadPrimitive.ScrollToBottom, { "aria-label": labels.scrollToLatestMessage, title: labels.scrollToLatestMessage, className: "sticky bottom-3 z-10 mx-auto mb-3 flex size-9 shrink-0 items-center justify-center rounded-full border border-border bg-card text-muted-foreground hover:text-foreground disabled:invisible", children: _jsx(ChevronDown, { className: "size-4" }) })] }), _jsx("div", { className: "shrink-0 bg-background pb-3", children: _jsxs("div", { className: "mx-auto w-full max-w-[53rem] px-6", children: [error ? (_jsx("div", { role: "alert", className: "mb-2 rounded-md border border-red-500/20 bg-red-500/10 px-3 py-2 text-sm text-red-700 dark:text-red-300", children: error })) : null, _jsx(ComposerPrimitive.Root, { "data-ui": "chat.composer", className: "group/composer relative rounded-[20px] border-[0.5px] border-border bg-card pt-2 shadow-sm transition-all duration-200 ease-in-out hover:border-foreground/25 focus-within:border-foreground/25", children: _jsx(ComposerPrimitive.AttachmentDropzone, { asChild: true, children: _jsxs("div", { className: "contents", children: [_jsx(ComposerExpand, { expanded: composerExpanded, onToggle: toggleComposer }), _jsx("div", { className: "flex flex-wrap gap-1.5 px-[15px] empty:hidden", children: _jsx(ComposerPrimitive.Attachments, { children: ({ attachment }) => _jsx(ComposerAttachment, { attachment: attachment }) }) }), _jsx(ComposerPrimitive.Input, { ref: composerInputRef, placeholder: labels.messagePlaceholder, disabled: blocked, rows: 2, minRows: composerRows, submitMode: "enter", className: cx('block min-h-[46px] w-full resize-none overflow-y-auto bg-transparent pb-0 pl-[15px] pr-11 pt-1.5 text-sm leading-[1.4] text-foreground outline-none placeholder:text-muted-foreground disabled:opacity-60 [&::-webkit-scrollbar]:w-[3px]', composerExpanded ? 'max-h-[max(220px,50vh)]' : 'max-h-[max(220px,40vh)]') }), _jsxs("div", { className: "relative z-[2] flex min-h-10 items-center justify-between gap-4 px-2 py-[5px]", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx(AttachmentPicker, { disabled: blocked, enabled: allowAttachments }), composerTools, composerStatus ? _jsx("div", { className: "min-w-0 text-[11px] text-muted-foreground", children: composerStatus }) : null] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [composerEnd, _jsx(ThreadPrimitive.If, { running: false, children: _jsx(ComposerPrimitive.Send, { disabled: blocked || attachmentUploading, "aria-label": labels.send, title: attachmentUploading ? labels.processing : labels.send, className: "mr-0.5 mt-px flex size-[30px] shrink-0 items-center justify-center text-brand transition-all duration-200 disabled:cursor-not-allowed disabled:text-muted-foreground/50", children: _jsx(Send, { className: "size-[22px]" }) }) }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(ComposerPrimitive.Cancel, { "aria-label": labels.stop, title: labels.stop, className: "flex size-[30px] shrink-0 items-center justify-center rounded-full text-destructive hover:bg-muted", children: _jsx(CirclePause, { className: "size-5" }) }) })] })] })] }) }) })] }) })] }));
279
+ : (_jsx(AssistantMessage, { agentName: assistantName, allowRegenerate: allowRegenerate, branch: branchByMessageId.get(message.id), busy: blocked, components: components, messageId: message.id, onBranchSelect: onBranchSelect, onBranchStart: onBranchStart, onRegenerateMessage: onRegenerateMessage })) }) })] }), _jsx(ThreadPrimitive.ScrollToBottom, { "aria-label": labels.scrollToLatestMessage, title: labels.scrollToLatestMessage, className: "sticky bottom-3 z-10 mx-auto mb-3 flex size-9 shrink-0 items-center justify-center rounded-full border border-border bg-card text-muted-foreground hover:text-foreground disabled:invisible", children: _jsx(ChevronDown, { className: "size-4" }) })] }), _jsx("div", { className: "shrink-0 bg-background pb-3", children: _jsxs("div", { className: "mx-auto w-full max-w-[53rem] px-6", children: [error ? (_jsx("div", { role: "alert", className: "mb-2 rounded-md border border-red-500/20 bg-red-500/10 px-3 py-2 text-sm text-red-700 dark:text-red-300", children: error })) : null, _jsx(ComposerPrimitive.Root, { "data-ui": "chat.composer", className: "group/composer relative rounded-[20px] border-[0.5px] border-border bg-card pt-2 shadow-sm transition-all duration-200 ease-in-out hover:border-foreground/25 focus-within:border-foreground/25", children: _jsx(ComposerPrimitive.AttachmentDropzone, { asChild: true, children: _jsxs("div", { className: "contents", children: [_jsx(ComposerExpand, { expanded: composerExpanded, onToggle: toggleComposer }), _jsx("div", { className: "flex flex-wrap gap-1.5 px-[15px] empty:hidden", children: _jsx(ComposerPrimitive.Attachments, { children: ({ attachment }) => _jsx(ComposerAttachment, { attachment: attachment }) }) }), _jsx(ComposerPrimitive.Input, { ref: composerInputRef, placeholder: labels.messagePlaceholder, disabled: blocked, rows: 2, minRows: composerRows, submitMode: "enter", className: cx('block min-h-[46px] w-full resize-none overflow-y-auto bg-transparent pb-0 pl-[15px] pr-11 pt-1.5 text-sm leading-[1.4] text-foreground outline-none placeholder:text-muted-foreground disabled:opacity-60 [&::-webkit-scrollbar]:w-[3px]', composerExpanded ? 'max-h-[max(220px,50vh)]' : 'max-h-[max(220px,40vh)]') }), _jsxs("div", { className: "relative z-[2] flex min-h-10 items-center justify-between gap-4 px-2 py-[5px]", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-1.5", children: [showAttachmentPicker && _jsx(AttachmentPicker, { disabled: blocked, enabled: allowAttachments }), composerTools, composerStatus ? _jsx("div", { className: "min-w-0 text-[11px] text-muted-foreground", children: composerStatus }) : null] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [composerEnd, _jsx(ThreadPrimitive.If, { running: false, children: _jsx(ComposerPrimitive.Send, { disabled: blocked || attachmentUploading, "aria-label": labels.send, title: attachmentUploading ? labels.processing : labels.send, className: "mr-0.5 mt-px flex size-[30px] shrink-0 items-center justify-center rounded-full bg-brand text-brand-foreground transition-all duration-200 hover:bg-brand/85 disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground/50", children: _jsx(ArrowUp, { className: "size-[17px]", strokeWidth: 2.5 }) }) }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(ComposerPrimitive.Cancel, { "aria-label": labels.stop, title: labels.stop, className: "flex size-[30px] shrink-0 items-center justify-center rounded-full text-destructive hover:bg-muted", children: _jsx(CirclePause, { className: "size-5" }) }) })] })] })] }) }) })] }) })] }));
280
280
  }
281
281
  export function ChatThread(_a) {
282
282
  var { runtime, assistantName, allowAttachments = false, allowEdit = false, allowRegenerate = true, branchNavigation = [], busy = false, disabled = false, labels: labelOverrides, transformUserText = (text) => text } = _a, props = __rest(_a, ["runtime", "assistantName", "allowAttachments", "allowEdit", "allowRegenerate", "branchNavigation", "busy", "disabled", "labels", "transformUserText"]);
@@ -1,5 +1,6 @@
1
1
  import { type ButtonHTMLAttributes, type ComponentPropsWithoutRef, type InputHTMLAttributes, type ReactNode, type SelectHTMLAttributes, type TextareaHTMLAttributes } from 'react';
2
- export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger' | 'danger-secondary';
2
+ import { Switch as SwitchPrimitive } from 'radix-ui';
3
+ export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'danger-secondary';
3
4
  export type ControlSize = 'sm' | 'md' | 'lg';
4
5
  export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
5
6
  asChild?: boolean;
@@ -44,6 +45,10 @@ export declare const NativeSelect: import("react").ForwardRefExoticComponent<Sel
44
45
  controlSize?: ControlSize;
45
46
  wrapperClassName?: string;
46
47
  } & import("react").RefAttributes<HTMLSelectElement>>;
48
+ export type SwitchProps = ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>;
49
+ export declare const Switch: import("react").ForwardRefExoticComponent<Omit<SwitchPrimitive.SwitchProps & import("react").RefAttributes<HTMLButtonElement>, "ref"> & import("react").RefAttributes<HTMLButtonElement>>;
50
+ export type SliderProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'type'>;
51
+ export declare const Slider: import("react").ForwardRefExoticComponent<SliderProps & import("react").RefAttributes<HTMLInputElement>>;
47
52
  export type CheckboxProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'type'>;
48
53
  export declare const Checkbox: import("react").ForwardRefExoticComponent<CheckboxProps & import("react").RefAttributes<HTMLInputElement>>;
49
54
  export type RadioProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'type'>;
@@ -59,7 +64,7 @@ export type SearchInputProps = Omit<InputProps, 'defaultValue' | 'type' | 'value
59
64
  value: string;
60
65
  wrapperClassName?: string;
61
66
  };
62
- export declare const SearchInput: import("react").ForwardRefExoticComponent<Omit<InputProps, "type" | "value" | "defaultValue"> & {
67
+ export declare const SearchInput: import("react").ForwardRefExoticComponent<Omit<InputProps, "type" | "defaultValue" | "value"> & {
63
68
  clearLabel?: string;
64
69
  label: string;
65
70
  onClear: () => void;
package/dist/Controls.js CHANGED
@@ -13,13 +13,14 @@ var __rest = (this && this.__rest) || function (s, e) {
13
13
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
14
  import { forwardRef, useImperativeHandle, useRef, } from 'react';
15
15
  import { ChevronDown, Loader2, Search, X } from 'lucide-react';
16
- import { Slot } from 'radix-ui';
16
+ import { Slot, Switch as SwitchPrimitive } from 'radix-ui';
17
17
  function cx(...classes) {
18
18
  return classes.filter(Boolean).join(' ');
19
19
  }
20
20
  const buttonVariants = {
21
21
  primary: 'ui-button-primary',
22
22
  secondary: 'ui-button-secondary',
23
+ outline: 'ui-button-secondary ui-button-outline',
23
24
  ghost: 'ui-button-ghost',
24
25
  danger: 'ui-button-primary ui-button-danger',
25
26
  'danger-secondary': 'ui-button-secondary ui-button-danger-secondary',
@@ -63,6 +64,14 @@ export const Select = forwardRef(function Select(_a, ref) {
63
64
  return (_jsxs("span", { "data-toolplane-ui": "select", className: cx('ui-select', wrapperClassName), children: [_jsx("select", Object.assign({}, props, { ref: ref, disabled: disabled, className: cx('ui-input ui-select-control', controlSize === 'sm' && 'ui-input-sm', controlSize === 'lg' && 'ui-input-lg', className), children: children })), _jsx(ChevronDown, { "aria-hidden": "true", className: cx('ui-select-chevron', disabled && 'opacity-40') })] }));
64
65
  });
65
66
  export const NativeSelect = Select;
67
+ export const Switch = forwardRef(function Switch(_a, ref) {
68
+ var { className } = _a, props = __rest(_a, ["className"]);
69
+ return (_jsx(SwitchPrimitive.Root, Object.assign({}, props, { ref: ref, "data-toolplane-ui": "switch", className: cx('ui-switch', className), children: _jsx(SwitchPrimitive.Thumb, { className: "ui-switch-thumb" }) })));
70
+ });
71
+ export const Slider = forwardRef(function Slider(_a, ref) {
72
+ var { className } = _a, props = __rest(_a, ["className"]);
73
+ return _jsx("input", Object.assign({}, props, { ref: ref, type: "range", "data-toolplane-ui": "slider", className: cx('ui-slider', className) }));
74
+ });
66
75
  export const Checkbox = forwardRef(function Checkbox(_a, ref) {
67
76
  var { className } = _a, props = __rest(_a, ["className"]);
68
77
  return (_jsx("input", Object.assign({}, props, { ref: ref, type: "checkbox", "data-toolplane-ui": "checkbox", className: cx('ui-checkbox', className) })));
@@ -25,6 +25,8 @@ export type ConversationSidebarLabels = {
25
25
  deleteConversation: string;
26
26
  showConversations: string;
27
27
  hideConversations: string;
28
+ moveUp?: string;
29
+ moveDown?: string;
28
30
  };
29
31
  export declare const conversationSidebarDefaultLabels: ConversationSidebarLabels;
30
32
  export type ConversationSidebarProps = {
@@ -33,6 +35,7 @@ export type ConversationSidebarProps = {
33
35
  activeConversationId?: string | null;
34
36
  labels?: Partial<ConversationSidebarLabels>;
35
37
  className?: string;
38
+ onConversationOrderChange?: (groupId: string, conversationIds: string[]) => void;
36
39
  onSelectGroup?: (group: ConversationSidebarGroup) => void;
37
40
  onSelectConversation: (conversation: ConversationSidebarConversation, group: ConversationSidebarGroup) => void;
38
41
  onCreateGroup?: () => void;
@@ -40,4 +43,4 @@ export type ConversationSidebarProps = {
40
43
  onRenameConversation?: (conversation: ConversationSidebarConversation, group: ConversationSidebarGroup) => void;
41
44
  onDeleteConversation?: (conversation: ConversationSidebarConversation, group: ConversationSidebarGroup) => void;
42
45
  };
43
- export declare function ConversationSidebar({ groups, activeGroupId, activeConversationId, labels: labelsOverride, className, onSelectGroup, onSelectConversation, onCreateGroup, onCreateConversation, onRenameConversation, onDeleteConversation, }: ConversationSidebarProps): import("react").JSX.Element;
46
+ export declare function ConversationSidebar({ groups, activeGroupId, activeConversationId, labels: labelsOverride, className, onSelectGroup, onSelectConversation, onCreateGroup, onCreateConversation, onRenameConversation, onDeleteConversation, onConversationOrderChange, }: ConversationSidebarProps): import("react").JSX.Element;
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useId, useMemo, useState } from 'react';
4
- import { Bot, ChevronRight, MessageSquare, Pencil, Plus, Trash2, } from 'lucide-react';
3
+ import { useId, useMemo, useRef, useState } from 'react';
4
+ import { Bot, ChevronRight, Pencil, Plus, Trash2, } from 'lucide-react';
5
5
  import { IconButton, SearchInput } from "./Controls.js";
6
6
  import { SidebarActionRail } from "./Sidebar.js";
7
7
  export const conversationSidebarDefaultLabels = {
@@ -18,14 +18,32 @@ export const conversationSidebarDefaultLabels = {
18
18
  deleteConversation: 'Delete conversation',
19
19
  showConversations: 'Show conversations',
20
20
  hideConversations: 'Hide conversations',
21
+ moveUp: 'Move up',
22
+ moveDown: 'Move down',
21
23
  };
22
24
  function cx(...classes) {
23
25
  return classes.filter(Boolean).join(' ');
24
26
  }
25
- export function ConversationSidebar({ groups, activeGroupId, activeConversationId, labels: labelsOverride, className, onSelectGroup, onSelectConversation, onCreateGroup, onCreateConversation, onRenameConversation, onDeleteConversation, }) {
27
+ export function ConversationSidebar({ groups, activeGroupId, activeConversationId, labels: labelsOverride, className, onSelectGroup, onSelectConversation, onCreateGroup, onCreateConversation, onRenameConversation, onDeleteConversation, onConversationOrderChange, }) {
26
28
  const labels = Object.assign(Object.assign({}, conversationSidebarDefaultLabels), labelsOverride);
27
29
  const controlsId = useId();
28
30
  const [query, setQuery] = useState('');
31
+ const dragging = useRef(null);
32
+ const [dropRow, setDropRow] = useState(null);
33
+ function edgeFor(event) {
34
+ const rect = event.currentTarget.getBoundingClientRect();
35
+ return event.clientY < rect.top + rect.height / 2 ? 'before' : 'after';
36
+ }
37
+ function move(sourceId, targetId, group, edge) {
38
+ const source = group.conversations.find((item) => item.id === sourceId);
39
+ const target = group.conversations.find((item) => item.id === targetId);
40
+ if (!onConversationOrderChange || !source || !target || sourceId === targetId || source.disabled || source.deleting || target.disabled || target.deleting)
41
+ return;
42
+ const ids = group.conversations.map((item) => item.id).filter((id) => id !== sourceId);
43
+ ids.splice(ids.indexOf(targetId) + (edge === 'after' ? 1 : 0), 0, sourceId);
44
+ if (ids.some((id, index) => group.conversations[index].id !== id))
45
+ onConversationOrderChange(group.id, ids);
46
+ }
29
47
  const [collapsedGroupIds, setCollapsedGroupIds] = useState(() => new Set());
30
48
  const needle = query.trim().toLocaleLowerCase();
31
49
  const visibleGroups = useMemo(() => groups.flatMap((group) => {
@@ -59,9 +77,48 @@ export function ConversationSidebar({ groups, activeGroupId, activeConversationI
59
77
  const title = conversation.title || labels.untitledConversation;
60
78
  const selected = conversation.id === activeConversationId;
61
79
  const hasActions = Boolean(onRenameConversation || onDeleteConversation);
62
- return (_jsx("li", { className: "relative py-0.5", "data-chat-ui": "sidebar-conversation", children: _jsxs("div", { className: cx('group group/sidebar-conversation flex h-8 min-w-0 items-center gap-1.5 rounded-lg px-2 transition-colors', selected
80
+ const index = group.conversations.findIndex((item) => item.id === conversation.id);
81
+ const canMove = Boolean(onConversationOrderChange && !conversation.disabled && !conversation.deleting);
82
+ return (_jsx("li", { className: "relative py-0.5", "data-chat-ui": "sidebar-conversation", "data-conversation-id": conversation.id, "data-drop-edge": (dropRow === null || dropRow === void 0 ? void 0 : dropRow.id) === conversation.id && dropRow.groupId === group.id ? dropRow.edge : undefined, draggable: canMove, onDragStart: (event) => {
83
+ if (!canMove) {
84
+ event.preventDefault();
85
+ return;
86
+ }
87
+ event.stopPropagation();
88
+ dragging.current = { id: conversation.id, groupId: group.id };
89
+ event.dataTransfer.effectAllowed = 'move';
90
+ event.dataTransfer.setData('text/plain', conversation.id);
91
+ }, onDragEnd: () => { dragging.current = null; setDropRow(null); }, onDragOver: (event) => {
92
+ var _a;
93
+ if (!canMove || ((_a = dragging.current) === null || _a === void 0 ? void 0 : _a.groupId) !== group.id || dragging.current.id === conversation.id)
94
+ return;
95
+ event.preventDefault();
96
+ event.stopPropagation();
97
+ event.dataTransfer.dropEffect = 'move';
98
+ setDropRow({ id: conversation.id, groupId: group.id, edge: edgeFor(event) });
99
+ }, onDragLeave: (event) => {
100
+ if (!event.currentTarget.contains(event.relatedTarget))
101
+ setDropRow(null);
102
+ }, onDrop: (event) => {
103
+ event.preventDefault();
104
+ event.stopPropagation();
105
+ const source = dragging.current;
106
+ dragging.current = null;
107
+ setDropRow(null);
108
+ if ((source === null || source === void 0 ? void 0 : source.groupId) === group.id && canMove)
109
+ move(source.id, conversation.id, group, edgeFor(event));
110
+ }, children: _jsxs("div", { className: cx('group group/sidebar-conversation flex h-8 min-w-0 items-center gap-1.5 rounded-lg px-2 transition-colors', selected
63
111
  ? 'bg-muted font-medium text-foreground'
64
- : 'text-foreground/75 hover:bg-muted/60 hover:text-foreground'), children: [_jsxs("button", { type: "button", disabled: conversation.disabled, onClick: () => onSelectConversation(conversation, group), "aria-current": selected ? 'page' : undefined, title: title, className: "flex h-full min-w-0 flex-1 items-center gap-1.5 text-left text-[13px] disabled:cursor-not-allowed disabled:opacity-50", children: [_jsx(MessageSquare, { className: "size-3 shrink-0 text-muted-foreground" }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: title }), conversation.meta ? (_jsx("span", { "aria-hidden": "true", className: "shrink-0", children: conversation.meta })) : null] }), hasActions ? (_jsxs(SidebarActionRail, { active: conversation.deleting, children: [onRenameConversation ? (_jsx(IconButton, { icon: _jsx(Pencil, { className: "size-3" }), label: `${labels.renameConversation}: ${title}`, size: "sm", variant: "ghost", onClick: () => onRenameConversation(conversation, group), title: labels.renameConversation, className: "min-h-6 w-6 rounded-md text-muted-foreground hover:bg-background hover:text-foreground" })) : null, onDeleteConversation ? (_jsx(IconButton, { icon: _jsx(Trash2, { className: "size-3" }), label: `${labels.deleteConversation}: ${title}`, size: "sm", variant: "ghost", loading: conversation.deleting, disabled: conversation.deleting, onClick: () => onDeleteConversation(conversation, group), title: labels.deleteConversation, className: "min-h-6 w-6 rounded-md text-muted-foreground hover:bg-background hover:text-destructive" })) : null] })) : null] }) }, conversation.id));
112
+ : 'text-foreground/75 hover:bg-muted/60 hover:text-foreground'), children: [_jsxs("button", { type: "button", disabled: conversation.disabled, onKeyDown: (event) => {
113
+ if (!event.altKey || !['ArrowUp', 'ArrowDown'].includes(event.key) || !canMove)
114
+ return;
115
+ const target = group.conversations[index + (event.key === 'ArrowUp' ? -1 : 1)];
116
+ if (!target || target.disabled || target.deleting)
117
+ return;
118
+ event.preventDefault();
119
+ move(conversation.id, target.id, group, event.key === 'ArrowUp' ? 'before' : 'after');
120
+ }, onClick: () => { if (!dragging.current)
121
+ onSelectConversation(conversation, group); }, "aria-current": selected ? 'page' : undefined, title: title, className: "flex h-full min-w-0 flex-1 items-center gap-1.5 text-left text-[13px] disabled:cursor-not-allowed disabled:opacity-50", children: [_jsx("span", { className: "min-w-0 flex-1 truncate", children: title }), conversation.meta ? (_jsx("span", { "aria-hidden": "true", className: "shrink-0", children: conversation.meta })) : null] }), hasActions ? (_jsxs(SidebarActionRail, { active: conversation.deleting, children: [onRenameConversation ? (_jsx(IconButton, { icon: _jsx(Pencil, { className: "size-3" }), label: `${labels.renameConversation}: ${title}`, size: "sm", variant: "ghost", onClick: () => onRenameConversation(conversation, group), title: labels.renameConversation, className: "min-h-6 w-6 rounded-md text-muted-foreground hover:bg-background hover:text-foreground" })) : null, onDeleteConversation ? (_jsx(IconButton, { icon: _jsx(Trash2, { className: "size-3" }), label: `${labels.deleteConversation}: ${title}`, size: "sm", variant: "ghost", loading: conversation.deleting, disabled: conversation.deleting, onClick: () => onDeleteConversation(conversation, group), title: labels.deleteConversation, className: "min-h-6 w-6 rounded-md text-muted-foreground hover:bg-background hover:text-destructive" })) : null] })) : null] }) }, conversation.id));
65
122
  }) : (_jsx("li", { className: "flex h-8 items-center px-2 text-xs text-muted-foreground", children: labels.noConversations })) })) : null] }, group.id));
66
123
  }) }), !visibleGroups.length ? (_jsx("p", { className: "px-3 py-8 text-center text-xs text-muted-foreground", children: needle ? labels.noResults : labels.noGroups })) : null] })] }));
67
124
  }
@@ -1,4 +1,8 @@
1
1
  import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
+ export type ProgressProps = ComponentPropsWithoutRef<'progress'>;
3
+ export declare function Progress({ className, max, ...props }: ProgressProps): import("react").JSX.Element;
4
+ export type SkeletonProps = ComponentPropsWithoutRef<'div'>;
5
+ export declare function Skeleton({ className, ...props }: SkeletonProps): import("react").JSX.Element;
2
6
  export type FeedbackTone = 'neutral' | 'brand' | 'success' | 'warning' | 'danger';
3
7
  export type BadgeProps = ComponentPropsWithoutRef<'span'> & {
4
8
  tone?: FeedbackTone;
package/dist/Feedback.js CHANGED
@@ -14,6 +14,14 @@ import { Loader2 } from 'lucide-react';
14
14
  function cx(...classes) {
15
15
  return classes.filter(Boolean).join(' ');
16
16
  }
17
+ export function Progress(_a) {
18
+ var { className, max = 100 } = _a, props = __rest(_a, ["className", "max"]);
19
+ return _jsx("progress", Object.assign({}, props, { max: max, "data-toolplane-ui": "progress", className: cx('ui-progress', className) }));
20
+ }
21
+ export function Skeleton(_a) {
22
+ var { className } = _a, props = __rest(_a, ["className"]);
23
+ return _jsx("div", Object.assign({}, props, { "aria-hidden": "true", "data-toolplane-ui": "skeleton", className: cx('ui-skeleton', className) }));
24
+ }
17
25
  const badgeTones = {
18
26
  neutral: 'bg-muted text-muted-foreground',
19
27
  brand: 'bg-brand-soft text-accent-foreground',
package/dist/Layout.d.ts CHANGED
@@ -55,9 +55,14 @@ export type DataTableProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> &
55
55
  label?: string;
56
56
  minWidth?: string;
57
57
  panel?: boolean;
58
+ rowIds?: readonly string[];
59
+ rowLabels?: readonly ReactNode[];
60
+ selectedRowIds?: readonly string[];
61
+ onSelectedRowIdsChange?: (rowIds: string[]) => void;
62
+ selectable?: boolean;
58
63
  tableClassName?: string;
59
64
  };
60
- export declare function DataTable({ children, className, headers, label, minWidth, panel, tableClassName, ...props }: DataTableProps): import("react").JSX.Element;
65
+ export declare function DataTable({ children, className, headers, label, minWidth, panel, rowIds, rowLabels, selectedRowIds, onSelectedRowIdsChange, selectable, tableClassName, ...props }: DataTableProps): import("react").JSX.Element;
61
66
  export type EntityProps = Omit<ComponentPropsWithoutRef<'div'>, 'title'> & {
62
67
  description?: ReactNode;
63
68
  initials?: string;
package/dist/Layout.js CHANGED
@@ -10,6 +10,7 @@ var __rest = (this && this.__rest) || function (s, e) {
10
10
  return t;
11
11
  };
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ import { Children, cloneElement, isValidElement, useEffect, useRef } from 'react';
13
14
  function cx(...classes) {
14
15
  return classes.filter(Boolean).join(' ');
15
16
  }
@@ -39,9 +40,49 @@ export function EmptyState(_a) {
39
40
  var { actions, children, className, description, icon: IconComponent, title } = _a, props = __rest(_a, ["actions", "children", "className", "description", "icon", "title"]);
40
41
  return (_jsxs("div", Object.assign({}, props, { "data-toolplane-ui": "empty-state", className: cx('ui-empty', className), children: [IconComponent ? _jsx(IconComponent, { className: "mb-3 size-8 text-muted-foreground" }) : null, title ? _jsx("h2", { className: "text-lg font-semibold text-foreground", children: title }) : null, description ? (_jsx("div", { className: cx('text-sm text-muted-foreground', Boolean(title) && 'mt-1'), children: description })) : null, children ? _jsx("div", { className: "mt-6 w-full", children: children }) : null, actions ? _jsx("div", { className: "mt-5 flex flex-wrap items-center justify-center gap-2", children: actions }) : null] })));
41
42
  }
43
+ function SelectionCheckbox(_a) {
44
+ var { indeterminate = false } = _a, props = __rest(_a, ["indeterminate"]);
45
+ const ref = useRef(null);
46
+ useEffect(() => {
47
+ if (ref.current)
48
+ ref.current.indeterminate = indeterminate;
49
+ }, [indeterminate]);
50
+ return _jsx("input", Object.assign({}, props, { ref: ref, type: "checkbox", className: cx('ui-checkbox ui-table__checkbox', props.className) }));
51
+ }
42
52
  export function DataTable(_a) {
43
- var { children, className, headers, label, minWidth = '40rem', panel = true, tableClassName } = _a, props = __rest(_a, ["children", "className", "headers", "label", "minWidth", "panel", "tableClassName"]);
44
- return (_jsx("div", Object.assign({}, props, { "data-toolplane-ui": "data-table", role: label ? 'region' : undefined, "aria-label": label, tabIndex: label ? 0 : undefined, className: cx(panel && 'ui-panel', 'relative overflow-x-auto overscroll-x-contain focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring', className), children: _jsxs("table", { className: cx('ui-table', tableClassName), style: { minWidth }, children: [_jsx("thead", { children: _jsx("tr", { children: headers.map((header, index) => (_jsx("th", { scope: "col", colSpan: header.colSpan, className: cx('px-4 py-3 font-medium', header.align === 'right' && 'text-right', header.className), children: header.label }, index))) }) }), _jsx("tbody", { className: "divide-y divide-border", children: children })] }) })));
53
+ var { children, className, headers, label, minWidth = '40rem', panel = true, rowIds, rowLabels, selectedRowIds = [], onSelectedRowIdsChange, selectable = false, tableClassName } = _a, props = __rest(_a, ["children", "className", "headers", "label", "minWidth", "panel", "rowIds", "rowLabels", "selectedRowIds", "onSelectedRowIdsChange", "selectable", "tableClassName"]);
54
+ const childList = Children.toArray(children);
55
+ const bodyChild = childList.length === 1 && isValidElement(childList[0]) && childList[0].type === 'tbody'
56
+ ? childList[0]
57
+ : undefined;
58
+ const rows = bodyChild ? bodyChild.props.children : children;
59
+ const selectableRows = selectable ? Children.toArray(rows).flatMap((child) => {
60
+ if (!isValidElement(child) || child.type !== 'tr')
61
+ return [];
62
+ return [child];
63
+ }) : [];
64
+ const ids = selectableRows.map((_, index) => { var _a; return (_a = rowIds === null || rowIds === void 0 ? void 0 : rowIds[index]) !== null && _a !== void 0 ? _a : String(index); });
65
+ const selected = new Set(selectedRowIds);
66
+ const allSelected = ids.length > 0 && ids.every((id) => selected.has(id));
67
+ const someSelected = ids.some((id) => selected.has(id));
68
+ const toggle = (id) => onSelectedRowIdsChange === null || onSelectedRowIdsChange === void 0 ? void 0 : onSelectedRowIdsChange(selected.has(id) ? selectedRowIds.filter((value) => value !== id) : [...selectedRowIds, id]);
69
+ const toggleAll = () => onSelectedRowIdsChange === null || onSelectedRowIdsChange === void 0 ? void 0 : onSelectedRowIdsChange(allSelected ? selectedRowIds.filter((id) => !ids.includes(id)) : [...new Set([...selectedRowIds, ...ids])]);
70
+ let rowIndex = 0;
71
+ const tableChildren = selectable
72
+ ? Children.map(rows, (child) => {
73
+ var _a;
74
+ if (!isValidElement(child) || child.type !== 'tr')
75
+ return child;
76
+ const row = child;
77
+ const id = ids[rowIndex];
78
+ const index = rowIndex++;
79
+ return cloneElement(row, {}, [
80
+ _jsx("td", { className: "ui-table__selection", children: _jsx(SelectionCheckbox, { checked: selected.has(id), onChange: () => toggle(id), "aria-label": `选择${(_a = rowLabels === null || rowLabels === void 0 ? void 0 : rowLabels[index]) !== null && _a !== void 0 ? _a : `第 ${index + 1} 行`}` }) }, "selection"),
81
+ ...Children.toArray(row.props.children),
82
+ ]);
83
+ })
84
+ : rows;
85
+ return (_jsx("div", Object.assign({}, props, { "data-toolplane-ui": "data-table", role: label ? 'region' : undefined, "aria-label": label, tabIndex: label ? 0 : undefined, className: cx(panel && 'ui-panel', 'relative overflow-x-auto overscroll-x-contain focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring', className), children: _jsxs("table", { className: cx('ui-table', tableClassName), style: { minWidth }, children: [_jsx("thead", { children: _jsxs("tr", { children: [selectable ? (_jsx("th", { scope: "col", className: "ui-table__selection", children: _jsx(SelectionCheckbox, { checked: allSelected, indeterminate: !allSelected && someSelected, onChange: toggleAll, "aria-label": "\u5168\u9009\u884C" }) })) : null, headers.map((header, index) => (_jsx("th", { scope: "col", colSpan: header.colSpan, className: cx('px-4 py-3 font-medium', header.align === 'right' && 'text-right', header.className), children: header.label }, index)))] }) }), _jsx("tbody", { className: cx('divide-y divide-border', bodyChild === null || bodyChild === void 0 ? void 0 : bodyChild.props.className), children: tableChildren })] }) })));
45
86
  }
46
87
  export function Entity(_a) {
47
88
  var { className, description, initials, mono = false, title } = _a, props = __rest(_a, ["className", "description", "initials", "mono", "title"]);
@@ -1,4 +1,9 @@
1
+ import { Tabs as TabsPrimitive } from 'radix-ui';
1
2
  import type { ButtonHTMLAttributes, ComponentPropsWithoutRef, ReactNode } from 'react';
3
+ export declare const Tabs: import("react").ForwardRefExoticComponent<TabsPrimitive.TabsProps & import("react").RefAttributes<HTMLDivElement>>;
4
+ export declare const TabsList: import("react").ForwardRefExoticComponent<Omit<TabsPrimitive.TabsListProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
5
+ export declare const TabsTrigger: import("react").ForwardRefExoticComponent<Omit<TabsPrimitive.TabsTriggerProps & import("react").RefAttributes<HTMLButtonElement>, "ref"> & import("react").RefAttributes<HTMLButtonElement>>;
6
+ export declare const TabsContent: import("react").ForwardRefExoticComponent<Omit<TabsPrimitive.TabsContentProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
2
7
  export type ChipProps = ComponentPropsWithoutRef<'span'> & {
3
8
  active?: boolean;
4
9
  asChild?: boolean;
@@ -11,10 +11,24 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  return t;
12
12
  };
13
13
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
- import { Slot } from 'radix-ui';
14
+ import { Slot, Tabs as TabsPrimitive } from 'radix-ui';
15
+ import { forwardRef } from 'react';
15
16
  function cx(...classes) {
16
17
  return classes.filter(Boolean).join(' ');
17
18
  }
19
+ export const Tabs = TabsPrimitive.Root;
20
+ export const TabsList = forwardRef(function TabsList(_a, ref) {
21
+ var { className } = _a, props = __rest(_a, ["className"]);
22
+ return _jsx(TabsPrimitive.List, Object.assign({}, props, { ref: ref, "data-toolplane-ui": "tabs-list", className: cx('ui-tabs-list', className) }));
23
+ });
24
+ export const TabsTrigger = forwardRef(function TabsTrigger(_a, ref) {
25
+ var { className } = _a, props = __rest(_a, ["className"]);
26
+ return _jsx(TabsPrimitive.Trigger, Object.assign({}, props, { ref: ref, "data-toolplane-ui": "tabs-trigger", className: cx('ui-tabs-trigger', className) }));
27
+ });
28
+ export const TabsContent = forwardRef(function TabsContent(_a, ref) {
29
+ var { className } = _a, props = __rest(_a, ["className"]);
30
+ return _jsx(TabsPrimitive.Content, Object.assign({}, props, { ref: ref, "data-toolplane-ui": "tabs-content", className: cx('ui-tabs-content', className) }));
31
+ });
18
32
  export function Chip(_a) {
19
33
  var { active = false, asChild = false, className } = _a, props = __rest(_a, ["active", "asChild", "className"]);
20
34
  const Component = asChild ? Slot.Root : 'span';
@@ -1,4 +1,12 @@
1
- import { ContextMenu as ContextMenuPrimitive, HoverCard as HoverCardPrimitive, Popover as PopoverPrimitive, Tooltip as TooltipPrimitive } from 'radix-ui';
1
+ import { ContextMenu as ContextMenuPrimitive, DropdownMenu as DropdownMenuPrimitive, HoverCard as HoverCardPrimitive, Popover as PopoverPrimitive, Tooltip as TooltipPrimitive } from 'radix-ui';
2
+ export declare const DropdownMenu: import("react").FC<DropdownMenuPrimitive.DropdownMenuProps>;
3
+ export declare const DropdownMenuTrigger: import("react").ForwardRefExoticComponent<DropdownMenuPrimitive.DropdownMenuTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
4
+ export declare const DropdownMenuPortal: import("react").FC<DropdownMenuPrimitive.DropdownMenuPortalProps>;
5
+ export declare const DropdownMenuGroup: import("react").ForwardRefExoticComponent<DropdownMenuPrimitive.DropdownMenuGroupProps & import("react").RefAttributes<HTMLDivElement>>;
6
+ export declare const DropdownMenuContent: import("react").ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuContentProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
7
+ export declare const DropdownMenuItem: import("react").ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuItemProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
8
+ export declare const DropdownMenuLabel: import("react").ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuLabelProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
9
+ export declare const DropdownMenuSeparator: import("react").ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuSeparatorProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
2
10
  export declare const Popover: import("react").FC<PopoverPrimitive.PopoverProps>;
3
11
  export declare const PopoverTrigger: import("react").ForwardRefExoticComponent<PopoverPrimitive.PopoverTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
4
12
  export declare const PopoverPortal: import("react").FC<PopoverPrimitive.PopoverPortalProps>;
package/dist/Overlays.js CHANGED
@@ -12,7 +12,27 @@ var __rest = (this && this.__rest) || function (s, e) {
12
12
  };
13
13
  import { jsx as _jsx } from "react/jsx-runtime";
14
14
  import { forwardRef, } from 'react';
15
- import { ContextMenu as ContextMenuPrimitive, HoverCard as HoverCardPrimitive, Popover as PopoverPrimitive, Tooltip as TooltipPrimitive, } from 'radix-ui';
15
+ import { ContextMenu as ContextMenuPrimitive, DropdownMenu as DropdownMenuPrimitive, HoverCard as HoverCardPrimitive, Popover as PopoverPrimitive, Tooltip as TooltipPrimitive, } from 'radix-ui';
16
+ export const DropdownMenu = DropdownMenuPrimitive.Root;
17
+ export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
18
+ export const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
19
+ export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
20
+ export const DropdownMenuContent = forwardRef(function DropdownMenuContent(_a, ref) {
21
+ var { className, sideOffset = 6, collisionPadding = 8 } = _a, props = __rest(_a, ["className", "sideOffset", "collisionPadding"]);
22
+ return _jsx(DropdownMenuPrimitive.Content, Object.assign({}, props, { ref: ref, sideOffset: sideOffset, collisionPadding: collisionPadding, "data-toolplane-ui": "dropdown-menu", className: `ui-dropdown-menu ${className !== null && className !== void 0 ? className : ''}`.trim() }));
23
+ });
24
+ export const DropdownMenuItem = forwardRef(function DropdownMenuItem(_a, ref) {
25
+ var { className } = _a, props = __rest(_a, ["className"]);
26
+ return _jsx(DropdownMenuPrimitive.Item, Object.assign({}, props, { ref: ref, className: `ui-dropdown-item ${className !== null && className !== void 0 ? className : ''}`.trim() }));
27
+ });
28
+ export const DropdownMenuLabel = forwardRef(function DropdownMenuLabel(_a, ref) {
29
+ var { className } = _a, props = __rest(_a, ["className"]);
30
+ return _jsx(DropdownMenuPrimitive.Label, Object.assign({}, props, { ref: ref, className: `ui-dropdown-label ${className !== null && className !== void 0 ? className : ''}`.trim() }));
31
+ });
32
+ export const DropdownMenuSeparator = forwardRef(function DropdownMenuSeparator(_a, ref) {
33
+ var { className } = _a, props = __rest(_a, ["className"]);
34
+ return _jsx(DropdownMenuPrimitive.Separator, Object.assign({}, props, { ref: ref, className: `ui-dropdown-separator ${className !== null && className !== void 0 ? className : ''}`.trim() }));
35
+ });
16
36
  export const Popover = PopoverPrimitive.Root;
17
37
  export const PopoverTrigger = PopoverPrimitive.Trigger;
18
38
  export const PopoverPortal = PopoverPrimitive.Portal;
@@ -0,0 +1,64 @@
1
+ import { type HTMLAttributes, type ReactNode, type Ref } from 'react';
2
+ export type WorkspaceSidebarItem = {
3
+ id: string;
4
+ label: string;
5
+ icon: ReactNode;
6
+ badge?: ReactNode;
7
+ disabled?: boolean;
8
+ };
9
+ export type WorkspaceSidebarProps = Omit<HTMLAttributes<HTMLElement>, 'children' | 'onSelect'> & {
10
+ brand: ReactNode;
11
+ brandIcon: ReactNode;
12
+ brandLabel: string;
13
+ onBrandClick?: () => void;
14
+ workspace?: {
15
+ icon: ReactNode;
16
+ name: string;
17
+ description?: string;
18
+ badge?: ReactNode;
19
+ };
20
+ footer?: ReactNode;
21
+ items: readonly WorkspaceSidebarItem[];
22
+ activeId?: string;
23
+ onSelect: (id: string) => void;
24
+ collapsed: boolean;
25
+ onCollapsedChange: (collapsed: boolean) => void;
26
+ mobileOpen?: boolean;
27
+ onMobileOpenChange?: (open: boolean) => void;
28
+ mobileCloseRef?: Ref<HTMLButtonElement>;
29
+ navigationLabel?: string;
30
+ sectionLabel?: string;
31
+ labels?: {
32
+ collapse?: string;
33
+ expand?: string;
34
+ close?: string;
35
+ };
36
+ };
37
+ export declare const WorkspaceSidebar: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLElement>, "children" | "onSelect"> & {
38
+ brand: ReactNode;
39
+ brandIcon: ReactNode;
40
+ brandLabel: string;
41
+ onBrandClick?: () => void;
42
+ workspace?: {
43
+ icon: ReactNode;
44
+ name: string;
45
+ description?: string;
46
+ badge?: ReactNode;
47
+ };
48
+ footer?: ReactNode;
49
+ items: readonly WorkspaceSidebarItem[];
50
+ activeId?: string;
51
+ onSelect: (id: string) => void;
52
+ collapsed: boolean;
53
+ onCollapsedChange: (collapsed: boolean) => void;
54
+ mobileOpen?: boolean;
55
+ onMobileOpenChange?: (open: boolean) => void;
56
+ mobileCloseRef?: Ref<HTMLButtonElement>;
57
+ navigationLabel?: string;
58
+ sectionLabel?: string;
59
+ labels?: {
60
+ collapse?: string;
61
+ expand?: string;
62
+ close?: string;
63
+ };
64
+ } & import("react").RefAttributes<HTMLElement>>;
@@ -0,0 +1,24 @@
1
+ 'use client';
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
13
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
+ import { forwardRef, useId } from 'react';
15
+ import { PanelLeftClose, PanelLeftOpen, X } from 'lucide-react';
16
+ import { Tooltip, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger } from "./Overlays.js";
17
+ export const WorkspaceSidebar = forwardRef(function WorkspaceSidebar(_a, ref) {
18
+ var _b, _c, _d, _e;
19
+ var { brand, brandIcon, brandLabel, onBrandClick, workspace, footer, items, activeId, onSelect, collapsed, onCollapsedChange, mobileOpen = false, onMobileOpenChange, mobileCloseRef, navigationLabel = 'Workspace navigation', sectionLabel, labels, className, id } = _a, props = __rest(_a, ["brand", "brandIcon", "brandLabel", "onBrandClick", "workspace", "footer", "items", "activeId", "onSelect", "collapsed", "onCollapsedChange", "mobileOpen", "onMobileOpenChange", "mobileCloseRef", "navigationLabel", "sectionLabel", "labels", "className", "id"]);
20
+ const generatedId = useId();
21
+ const sidebarId = id !== null && id !== void 0 ? id : generatedId;
22
+ const toggleLabel = collapsed ? (_b = labels === null || labels === void 0 ? void 0 : labels.expand) !== null && _b !== void 0 ? _b : 'Expand sidebar' : (_c = labels === null || labels === void 0 ? void 0 : labels.collapse) !== null && _c !== void 0 ? _c : 'Collapse sidebar';
23
+ return _jsxs("aside", Object.assign({}, props, { id: sidebarId, ref: ref, className: `tp-workspace-sidebar ${className !== null && className !== void 0 ? className : ''}`, "data-toolplane-ui": "workspace-sidebar", "data-collapsed": collapsed, "data-mobile-open": mobileOpen, children: [_jsxs("div", { className: "tp-workspace-sidebar__header", children: [_jsxs("button", { type: "button", className: "tp-workspace-sidebar__brand", "aria-label": brandLabel, onClick: onBrandClick, children: [brandIcon, _jsx("span", { children: brand })] }), _jsx("button", { type: "button", className: "tp-workspace-sidebar__toggle", "aria-label": toggleLabel, title: toggleLabel, "aria-controls": sidebarId, "aria-expanded": !collapsed, onClick: () => onCollapsedChange(!collapsed), children: collapsed ? _jsxs("span", { className: "tp-workspace-sidebar__expand", children: [brandIcon, _jsx(PanelLeftOpen, { size: 18 })] }) : _jsx(PanelLeftClose, { size: 18 }) }), _jsx("button", { type: "button", ref: mobileCloseRef, className: "tp-workspace-sidebar__mobile-close", "aria-label": (_d = labels === null || labels === void 0 ? void 0 : labels.close) !== null && _d !== void 0 ? _d : 'Close navigation', title: (_e = labels === null || labels === void 0 ? void 0 : labels.close) !== null && _e !== void 0 ? _e : 'Close navigation', onClick: () => onMobileOpenChange === null || onMobileOpenChange === void 0 ? void 0 : onMobileOpenChange(false), children: _jsx(X, { size: 18 }) })] }), sectionLabel && _jsx("div", { className: "tp-workspace-sidebar__section", children: sectionLabel }), _jsx(TooltipProvider, { children: _jsx("nav", { className: "tp-workspace-sidebar__nav", "aria-label": navigationLabel, children: items.map((item) => _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "tp-workspace-sidebar__item", disabled: item.disabled, "aria-label": item.label, "aria-current": activeId === item.id ? 'page' : undefined, onClick: () => onSelect(item.id), children: [_jsx("span", { className: "tp-workspace-sidebar__icon", children: item.icon }), _jsx("span", { className: "tp-workspace-sidebar__label", children: item.label }), _jsx("span", { className: "tp-workspace-sidebar__badge", children: item.badge })] }) }), collapsed && _jsx(TooltipPortal, { children: _jsx(TooltipContent, { side: "right", children: item.label }) })] }, item.id)) }) }), workspace && _jsxs("div", { className: "tp-workspace-sidebar__workspace", children: [_jsx("span", { className: "tp-workspace-sidebar__workspace-icon", children: workspace.icon }), _jsxs("span", { className: "tp-workspace-sidebar__workspace-text", children: [_jsx("strong", { children: workspace.name }), _jsx("small", { children: workspace.description })] }), _jsx("span", { className: "tp-workspace-sidebar__workspace-badge", children: workspace.badge })] }), footer && _jsx("div", { className: "tp-workspace-sidebar__footer", children: footer })] }));
24
+ });
@@ -18,7 +18,7 @@ export function WorkspaceTabBar({ actions, activeTabId, labels: labelsOverride,
18
18
  var _a, _b;
19
19
  (_b = (_a = activeRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView) === null || _b === void 0 ? void 0 : _b.call(_a, { block: 'nearest', inline: 'nearest' });
20
20
  }, [activeTabId, tabs.length]);
21
- return (_jsxs("nav", { "aria-label": labels.navigation, "data-toolplane-ui": "workspace-tab-bar", className: "flex h-11 shrink-0 bg-shell px-2", children: [_jsxs("ol", { className: "flex min-w-0 flex-1 items-center gap-1 overflow-x-auto py-1 [&::-webkit-scrollbar]:hidden", children: [tabs.map((tab) => (_jsx(WorkspaceTabButton, { tab: tab, active: tab.id === activeTabId, canClose: tabs.length > 1, draggingId: draggingId, labels: labels, onClose: onClose, onDragChange: setDraggingId, onNewWindow: onOpenInNewWindow, onReorder: onReorder, onSelect: onSelect, onTogglePinned: onTogglePinned, activeButtonRef: tab.id === activeTabId ? activeRef : undefined }, tab.id))), _jsx("li", { className: "shrink-0", children: _jsx("button", { type: "button", "aria-label": labels.newTab, title: labels.newTab, onClick: onNewTab, className: "ui-button-ghost ui-icon-button", children: _jsx(Plus, { className: "size-4" }) }) })] }), actions] }));
21
+ return (_jsxs("nav", { "aria-label": labels.navigation, "data-toolplane-ui": "workspace-tab-bar", className: "flex h-11 shrink-0 bg-shell px-2", children: [_jsxs("ol", { className: "flex min-w-0 flex-1 items-center gap-1 overflow-x-auto overflow-y-hidden py-1 [&::-webkit-scrollbar]:hidden", children: [tabs.map((tab) => (_jsx(WorkspaceTabButton, { tab: tab, active: tab.id === activeTabId, canClose: tabs.length > 1, draggingId: draggingId, labels: labels, onClose: onClose, onDragChange: setDraggingId, onNewWindow: onOpenInNewWindow, onReorder: onReorder, onSelect: onSelect, onTogglePinned: onTogglePinned, activeButtonRef: tab.id === activeTabId ? activeRef : undefined }, tab.id))), _jsx("li", { className: "shrink-0", children: _jsx("button", { type: "button", "aria-label": labels.newTab, title: labels.newTab, onClick: onNewTab, className: "ui-button-ghost ui-icon-button", children: _jsx(Plus, { className: "size-4" }) }) })] }), actions] }));
22
22
  }
23
23
  function WorkspaceTabButton({ active, activeButtonRef, canClose, draggingId, labels, onClose, onDragChange, onNewWindow, onReorder, onSelect, onTogglePinned, tab, }) {
24
24
  const dragAllowed = Boolean(draggingId && draggingId !== tab.id);
@@ -41,9 +41,13 @@ function WorkspaceTabButton({ active, activeButtonRef, canClose, draggingId, lab
41
41
  onReorder(draggingId, tab.id);
42
42
  onDragChange(null);
43
43
  }, className: `group flex h-[30px] min-w-24 max-w-56 shrink-0 items-center rounded-[10px] transition-[background-color,color,transform] duration-150 ${active ? 'bg-background text-foreground ring-1 ring-border/70' : 'text-muted-foreground hover:bg-accent/70 hover:text-foreground'} ${draggingId === tab.id ? 'scale-[0.98] opacity-50' : ''}`, children: [_jsxs("button", { ref: activeButtonRef, type: "button", "aria-current": active ? 'page' : undefined, title: tab.label, onAuxClick: (event) => {
44
- if (event.button === 1)
44
+ if (event.button !== 1)
45
+ return;
46
+ event.preventDefault();
47
+ if (canClose)
45
48
  onClose(tab.id);
46
- }, onClick: () => onSelect(tab.id), onDoubleClick: () => onClose(tab.id), className: "flex min-w-0 flex-1 items-center gap-1.5 px-2 text-left text-xs", children: [_jsx(Icon, { className: "size-3.5 shrink-0" }), _jsx("span", { className: "truncate", children: tab.label })] }), _jsxs("div", { className: "mr-1 flex shrink-0 items-center", children: [_jsx("button", { type: "button", "aria-pressed": tab.pinned, "aria-label": tab.pinned ? labels.unpin(tab.label) : labels.pin(tab.label), title: tab.pinned ? labels.unpin(tab.label) : labels.pin(tab.label), onClick: (event) => {
49
+ }, onClick: () => onSelect(tab.id), onDoubleClick: () => { if (canClose)
50
+ onClose(tab.id); }, className: "flex h-full min-w-0 flex-1 items-center gap-1.5 px-2 text-left text-xs", children: [_jsx(Icon, { className: "size-3.5 shrink-0" }), _jsx("span", { className: "truncate", children: tab.label })] }), _jsxs("div", { className: "mr-1 flex shrink-0 items-center", children: [_jsx("button", { type: "button", "aria-pressed": tab.pinned, "aria-label": tab.pinned ? labels.unpin(tab.label) : labels.pin(tab.label), title: tab.pinned ? labels.unpin(tab.label) : labels.pin(tab.label), onClick: (event) => {
47
51
  stop(event);
48
52
  onTogglePinned(tab.id);
49
53
  }, className: `flex size-[18px] items-center justify-center rounded-sm transition-colors hover:bg-foreground/10 ${tab.pinned ? (controls ? 'bg-brand-soft text-accent-foreground' : 'bg-brand-soft/50 text-accent-foreground/60 group-hover:bg-brand-soft group-hover:text-accent-foreground group-focus-within:bg-brand-soft group-focus-within:text-accent-foreground') : (controls ? '' : 'opacity-0 group-hover:opacity-100 group-focus-within:opacity-100')}`, children: _jsx(Pin, { className: `size-3 ${tab.pinned ? 'fill-current' : ''}` }) }), _jsxs("div", { className: `flex items-center transition-opacity ${controls ? 'opacity-100' : 'opacity-0 group-hover:opacity-100 group-focus-within:opacity-100'}`, children: [_jsx("button", { type: "button", "aria-label": labels.openInNewWindow(tab.label), title: labels.openInNewWindow(tab.label), onClick: (event) => {
package/dist/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export * from './Brand.js';
2
+ export * from './Accordion.js';
3
+ export * from './Avatar.js';
2
4
  export * from './ChatShell.js';
3
5
  export * from './ChatThread.js';
6
+ export * from './ChatComposerToolbar.js';
4
7
  export * from './chat-markdown.js';
5
8
  export * from './ContentPage.js';
6
9
  export * from './Controls.js';
@@ -15,3 +18,4 @@ export * from './RotatingHeadline.js';
15
18
  export * from './SafeStreamdown.js';
16
19
  export * from './Sidebar.js';
17
20
  export * from './WorkspaceTabBar.js';
21
+ export * from './WorkspaceSidebar.js';
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  export * from "./Brand.js";
2
+ export * from "./Accordion.js";
3
+ export * from "./Avatar.js";
2
4
  export * from "./ChatShell.js";
3
5
  export * from "./ChatThread.js";
6
+ export * from "./ChatComposerToolbar.js";
4
7
  export * from "./chat-markdown.js";
5
8
  export * from "./ContentPage.js";
6
9
  export * from "./Controls.js";
@@ -15,3 +18,4 @@ export * from "./RotatingHeadline.js";
15
18
  export * from "./SafeStreamdown.js";
16
19
  export * from "./Sidebar.js";
17
20
  export * from "./WorkspaceTabBar.js";
21
+ export * from "./WorkspaceSidebar.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asharca/ui",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Reusable ToolPlane components and chat UI for React applications.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -44,6 +44,22 @@
44
44
  "types": "./dist/Controls.d.ts",
45
45
  "import": "./dist/Controls.js"
46
46
  },
47
+ "./accordion": {
48
+ "types": "./dist/Accordion.d.ts",
49
+ "import": "./dist/Accordion.js"
50
+ },
51
+ "./avatar": {
52
+ "types": "./dist/Avatar.d.ts",
53
+ "import": "./dist/Avatar.js"
54
+ },
55
+ "./workspace-tab-bar": {
56
+ "types": "./dist/WorkspaceTabBar.d.ts",
57
+ "import": "./dist/WorkspaceTabBar.js"
58
+ },
59
+ "./workspace-sidebar": {
60
+ "types": "./dist/WorkspaceSidebar.d.ts",
61
+ "import": "./dist/WorkspaceSidebar.js"
62
+ },
47
63
  "./dialog": {
48
64
  "types": "./dist/Dialog.d.ts",
49
65
  "import": "./dist/Dialog.js"
@@ -72,6 +88,10 @@
72
88
  "types": "./dist/ChatShell.d.ts",
73
89
  "import": "./dist/ChatShell.js"
74
90
  },
91
+ "./chat-composer-toolbar": {
92
+ "types": "./dist/ChatComposerToolbar.d.ts",
93
+ "import": "./dist/ChatComposerToolbar.js"
94
+ },
75
95
  "./chat-thread": {
76
96
  "types": "./dist/ChatThread.d.ts",
77
97
  "import": "./dist/ChatThread.js"
@@ -83,7 +103,9 @@
83
103
  "./styles.css": "./src/styles.css"
84
104
  },
85
105
  "scripts": {
86
- "lint": "eslint src tests",
106
+ "dev": "vite --config showcase/vite.config.ts",
107
+ "build:showcase": "tsc -p showcase/tsconfig.json && vite build --config showcase/vite.config.ts",
108
+ "lint": "eslint src tests showcase",
87
109
  "test": "vitest run",
88
110
  "test:watch": "vitest",
89
111
  "test:package": "node scripts/check-package.mjs",
@@ -128,6 +150,7 @@
128
150
  "tailwindcss": "^4",
129
151
  "typescript": "^5",
130
152
  "typescript-eslint": "^8",
153
+ "vite": "^8.2.2",
131
154
  "vitest": "^4.1.9"
132
155
  }
133
156
  }
package/src/styles.css CHANGED
@@ -7,6 +7,171 @@
7
7
  @source "../../../@streamdown/mermaid/dist/*.js";
8
8
  @source "./**/*.{ts,tsx}";
9
9
 
10
+ @layer components {
11
+ .tp-workspace-sidebar { display: flex; flex-direction: column; flex-shrink: 0; position: relative; height: 100%; width: var(--workspace-sidebar-width, 256px); overflow: hidden; padding: 0 12px 14px; background: hsl(var(--shell)); color: hsl(var(--foreground)); transition: width 200ms ease-out, padding 200ms ease-out, transform 200ms ease-out; }
12
+ .tp-workspace-sidebar__header { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; height: 56px; gap: 4px; }
13
+ .tp-workspace-sidebar__brand { display: flex; align-items: center; gap: 9px; min-width: 0; white-space: nowrap; font-weight: 600; font-size: 20px; }
14
+ .tp-workspace-sidebar__brand svg { flex-shrink: 0; }
15
+ .tp-workspace-sidebar__toggle, .tp-workspace-sidebar__mobile-close { display: grid; place-items: center; width: 36px; height: 36px; flex-shrink: 0; border-radius: 6px; color: hsl(var(--muted-foreground)); }
16
+ .tp-workspace-sidebar__toggle:hover, .tp-workspace-sidebar__mobile-close:hover { background: hsl(var(--muted)); }
17
+ .tp-workspace-sidebar__mobile-close { display: none; }
18
+ .tp-workspace-sidebar__expand { display: grid; place-items: center; width: 24px; height: 24px; }
19
+ .tp-workspace-sidebar__expand > svg { grid-area: 1 / 1; transition: opacity 150ms ease-out, transform 150ms ease-out; }
20
+ .tp-workspace-sidebar__expand > svg:last-child { opacity: 0; transform: scale(.75); }
21
+ .tp-workspace-sidebar__toggle:is(:hover,:focus-visible) .tp-workspace-sidebar__expand > svg:first-child { opacity: 0; transform: scale(.9); }
22
+ .tp-workspace-sidebar__toggle:is(:hover,:focus-visible) .tp-workspace-sidebar__expand > svg:last-child { opacity: 1; transform: scale(1); }
23
+ .tp-workspace-sidebar__workspace { height: 70px; display: flex; flex-shrink: 0; align-items: center; gap: 8px; margin-top: auto; padding: 12px 7px; overflow: hidden; transition: padding 200ms ease-out, gap 200ms ease-out; }
24
+ .tp-workspace-sidebar__workspace-icon { display: grid; place-items: center; width: 30px; height: 30px; flex-shrink: 0; border: 1px solid hsl(var(--border)); border-radius: 6px; background: hsl(var(--background)); font-size: 12px; }
25
+ .tp-workspace-sidebar__workspace-text { min-width: 0; flex: 1; }
26
+ .tp-workspace-sidebar__workspace-text strong { display: block; font-size: 11px; }
27
+ .tp-workspace-sidebar__workspace-text small { display: block; font-size: 10px; color: hsl(var(--muted-foreground)); }
28
+ .tp-workspace-sidebar__section { height: 28px; padding: 4px 12px; flex-shrink: 0; font-size: 10px; color: hsl(var(--muted-foreground)); }
29
+ .tp-workspace-sidebar__nav { display: grid; align-content: start; gap: 3px; overflow-y: auto; overflow-x: hidden; min-height: 0; }
30
+ .tp-workspace-sidebar__item { display: grid; grid-template-columns: 18px minmax(0,1fr) auto; align-items: center; column-gap: 10px; width: 100%; height: 40px; padding: 0 12px; text-align: left; border-radius: 8px; color: hsl(var(--muted-foreground)); font-size: 12px; overflow: hidden; transition: height 200ms ease-out, border-radius 200ms ease-out, padding 200ms ease-out, column-gap 200ms ease-out; }
31
+ .tp-workspace-sidebar__icon { display: grid; place-items: center; width: 18px; height: 18px; }
32
+ .tp-workspace-sidebar__label, .tp-workspace-sidebar__badge, .tp-workspace-sidebar__workspace-text, .tp-workspace-sidebar__workspace-badge { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: opacity 150ms ease-out, transform 150ms ease-out; }
33
+ .tp-workspace-sidebar__badge { font-size: 10px; }
34
+ .tp-workspace-sidebar__item:hover { background: hsl(var(--muted)); color: hsl(var(--foreground)); }
35
+ .tp-workspace-sidebar__item[aria-current="page"] { background: hsl(var(--brand-soft)); color: hsl(var(--foreground)); font-weight: 550; }
36
+ .tp-workspace-sidebar__item:disabled { opacity: .5; cursor: not-allowed; }
37
+ .tp-workspace-sidebar button:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: -2px; }
38
+ .tp-workspace-sidebar__footer { padding: 14px 7px 0; display: flex; align-items: center; gap: 9px; overflow: hidden; }
39
+ .tp-workspace-sidebar__footer > :first-child { flex-shrink: 0; }
40
+ .tp-workspace-sidebar__footer > :not(:first-child) { min-width: 0; white-space: nowrap; transition: opacity 150ms ease-out; }
41
+ .tp-workspace-sidebar__footer strong, .tp-workspace-sidebar__footer small { display: block; font-size: 11px; }
42
+ .tp-workspace-sidebar__footer small { color: hsl(var(--muted-foreground)); }
43
+ @media (min-width: 851px) {
44
+ .tp-workspace-sidebar[data-collapsed="true"] { width: var(--workspace-sidebar-collapsed-width, 64px); padding-inline: 8px; }
45
+ .tp-workspace-sidebar[data-collapsed="true"] .tp-workspace-sidebar__brand { display: none; }
46
+ .tp-workspace-sidebar[data-collapsed="true"] .tp-workspace-sidebar__header { justify-content: center; }
47
+ .tp-workspace-sidebar[data-collapsed="true"] .tp-workspace-sidebar__item { grid-template-columns: 18px 0px 0px; column-gap: 0; width: 36px; height: 36px; padding-inline: 9px; margin-inline: auto; border-radius: 50%; }
48
+ .tp-workspace-sidebar[data-collapsed="true"] :is(.tp-workspace-sidebar__label,.tp-workspace-sidebar__badge,.tp-workspace-sidebar__workspace-text,.tp-workspace-sidebar__workspace-badge,.tp-workspace-sidebar__section) { opacity: 0; transform: translateX(4px); }
49
+ .tp-workspace-sidebar[data-collapsed="true"] .tp-workspace-sidebar__workspace { gap: 0; padding-inline: 9px; }
50
+ .tp-workspace-sidebar[data-collapsed="true"] .tp-workspace-sidebar__workspace-badge { width: 0; }
51
+ .tp-workspace-sidebar[data-collapsed="true"] .tp-workspace-sidebar__footer { gap: 0; padding-inline: 9px; }
52
+ .tp-workspace-sidebar[data-collapsed="true"] .tp-workspace-sidebar__footer > :not(:first-child) { width: 0; opacity: 0; overflow: hidden; }
53
+ }
54
+ @media (max-width: 850px) {
55
+ .tp-workspace-sidebar { position: fixed; inset: 0 auto 0 0; z-index: 30; transform: translateX(-100%); visibility: hidden; }
56
+ .tp-workspace-sidebar[data-mobile-open="true"] { transform: translateX(0); visibility: visible; }
57
+ .tp-workspace-sidebar__toggle { display: none; }
58
+ .tp-workspace-sidebar__mobile-close { display: grid; }
59
+ }
60
+ @media (prefers-reduced-motion: reduce) { .tp-workspace-sidebar, .tp-workspace-sidebar * { transition-duration: 0.01ms !important; } }
61
+ [data-chat-ui="sidebar-conversation"][data-drop-edge]::after { content: ""; position: absolute; left: .5rem; right: .5rem; height: 2px; background: hsl(var(--brand)); pointer-events: none; }
62
+ [data-chat-ui="sidebar-conversation"][data-drop-edge="before"]::after { top: 0; }
63
+ [data-chat-ui="sidebar-conversation"][data-drop-edge="after"]::after { bottom: 0; }
64
+ .ui-composer-toolbar { display: flex; align-items: center; gap: .375rem; min-width: 0; }
65
+ .ui-composer-tool { display: inline-flex; width: 30px; height: 30px; flex-shrink: 0; align-items: center; justify-content: center; border-radius: 50%; color: hsl(var(--muted-foreground)); }
66
+ .ui-composer-tool svg, .ui-composer-menu svg, .ui-composer-option label svg { width: 17px; height: 17px; flex-shrink: 0; }
67
+ .ui-composer-tool:hover, .ui-composer-tool[aria-pressed="true"] { background: hsl(var(--muted)); color: hsl(var(--foreground)); }
68
+ .ui-composer-tool:disabled { opacity: .4; cursor: not-allowed; }
69
+ .ui-composer-tool:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; }
70
+ .ui-composer-shortcuts { display: flex; align-items: center; gap: .25rem; min-width: 0; overflow-x: auto; padding: 3px; }
71
+ .ui-composer-menu { width: 16rem; border-radius: .5rem; }
72
+ .ui-composer-menu .ui-dropdown-item { gap: .625rem; padding: .5rem .625rem; }
73
+ .ui-composer-menu small { display: block; margin-top: .125rem; color: hsl(var(--muted-foreground)); font-size: .6875rem; }
74
+ .ui-composer-customize { max-width: 28rem; border-radius: .5rem; }
75
+ .ui-composer-customize header, .ui-composer-customize footer { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
76
+ .ui-composer-customize footer { justify-content: flex-end; }
77
+ .ui-composer-option { display: flex; align-items: center; gap: .5rem; min-height: 44px; padding-block: .25rem; border-bottom: 1px solid hsl(var(--border)); }
78
+ .ui-composer-option label { display: flex; align-items: center; gap: .625rem; min-width: 0; flex: 1; font-size: .875rem; overflow-wrap: anywhere; }
79
+ .ui-composer-option input { width: 16px; height: 16px; flex-shrink: 0; accent-color: hsl(var(--brand)); }
80
+ .ui-switch {
81
+ display: inline-flex;
82
+ width: 2.25rem;
83
+ height: 1.25rem;
84
+ flex: none;
85
+ align-items: center;
86
+ padding: 2px;
87
+ border: 0;
88
+ border-radius: 999px;
89
+ background: hsl(var(--input));
90
+ cursor: pointer;
91
+ transition: background-color 150ms ease;
92
+ }
93
+ .ui-switch[data-state="checked"] { background: hsl(var(--primary)); }
94
+ .ui-switch-thumb {
95
+ width: 1rem;
96
+ height: 1rem;
97
+ border-radius: 50%;
98
+ background: hsl(var(--background));
99
+ box-shadow: 0 1px 2px #0002;
100
+ transition: transform 150ms ease;
101
+ }
102
+ .ui-switch-thumb[data-state="checked"] { transform: translateX(1rem); }
103
+ .ui-switch[dir="rtl"] .ui-switch-thumb[data-state="checked"] { transform: translateX(-1rem); }
104
+ .ui-slider {
105
+ width: 100%;
106
+ min-width: 0;
107
+ height: 1.5rem;
108
+ accent-color: hsl(var(--primary));
109
+ cursor: pointer;
110
+ }
111
+ .ui-switch:disabled, .ui-slider:disabled { cursor: not-allowed; opacity: .5; }
112
+ .ui-progress {
113
+ display: block;
114
+ width: 100%;
115
+ height: .375rem;
116
+ overflow: hidden;
117
+ border: 0;
118
+ border-radius: 999px;
119
+ appearance: none;
120
+ background: hsl(var(--muted));
121
+ color: hsl(var(--primary));
122
+ accent-color: hsl(var(--primary));
123
+ }
124
+ .ui-progress::-webkit-progress-bar { border-radius: inherit; background: hsl(var(--muted)); }
125
+ .ui-progress::-webkit-progress-value { border-radius: inherit; background: hsl(var(--primary)); }
126
+ .ui-progress::-moz-progress-bar { border-radius: inherit; background: hsl(var(--primary)); }
127
+ .ui-progress:indeterminate { appearance: auto; }
128
+ .ui-skeleton { min-height: 1rem; border-radius: .375rem; background: hsl(var(--muted)); animation: tp-skeleton-pulse 2s ease-in-out infinite; }
129
+ .ui-avatar {
130
+ display: inline-flex;
131
+ position: relative;
132
+ flex: none;
133
+ width: 2.25rem;
134
+ height: 2.25rem;
135
+ overflow: hidden;
136
+ border-radius: 50%;
137
+ vertical-align: middle;
138
+ }
139
+ .ui-avatar-image { width: 100%; height: 100%; object-fit: cover; }
140
+ .ui-avatar-fallback { display: grid; place-items: center; width: 100%; height: 100%; background: hsl(var(--muted)); color: hsl(var(--muted-foreground)); font-size: .75rem; font-weight: 500; }
141
+ .ui-tabs-list { display: inline-flex; max-width: 100%; align-items: center; gap: .25rem; padding: .25rem; border-radius: .5rem; background: hsl(var(--muted)); overflow-x: auto; }
142
+ .ui-tabs-trigger { flex: 1 0 auto; border: 0; border-radius: .375rem; padding: .375rem .75rem; background: transparent; color: hsl(var(--muted-foreground)); font-size: .875rem; font-weight: 500; cursor: pointer; }
143
+ .ui-tabs-trigger[data-state="active"] { background: hsl(var(--background)); color: hsl(var(--foreground)); box-shadow: 0 1px 3px #0001; }
144
+ .ui-tabs-trigger:disabled { opacity: .5; cursor: not-allowed; }
145
+ .ui-tabs-content { margin-top: 1rem; min-width: 0; }
146
+ .ui-accordion-item { border-bottom: 1px solid hsl(var(--border)); }
147
+ .ui-accordion-item:last-child { border-bottom: 0; }
148
+ .ui-accordion-header { margin: 0; }
149
+ .ui-accordion-trigger { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 1rem; padding: 1rem 0; border: 0; background: transparent; color: hsl(var(--foreground)); font-size: .875rem; font-weight: 500; text-align: start; cursor: pointer; }
150
+ .ui-accordion-trigger:hover { text-decoration: underline; text-underline-offset: 4px; }
151
+ .ui-accordion-trigger:disabled { opacity: .5; cursor: not-allowed; }
152
+ .ui-accordion-chevron { width: 1rem; height: 1rem; flex: none; color: hsl(var(--muted-foreground)); transition: transform 150ms ease; }
153
+ .ui-accordion-trigger[data-state="open"] .ui-accordion-chevron { transform: rotate(180deg); }
154
+ .ui-accordion-content { padding-bottom: 1rem; color: hsl(var(--muted-foreground)); font-size: .875rem; line-height: 1.65; overflow-wrap: anywhere; }
155
+ .ui-dropdown-menu { z-index: 60; min-width: 12rem; max-width: calc(100vw - 1rem); max-height: var(--radix-dropdown-menu-content-available-height); overflow-y: auto; border: 1px solid hsl(var(--border)); border-radius: .5rem; background: hsl(var(--popover)); color: hsl(var(--popover-foreground)); padding: .25rem; box-shadow: 0 4px 12px #0002; }
156
+ .ui-dropdown-item { display: flex; min-height: 2rem; align-items: center; gap: .5rem; padding: .375rem .5rem; border-radius: .25rem; font-size: .875rem; outline: none; cursor: default; user-select: none; }
157
+ .ui-dropdown-item[data-highlighted] { background: hsl(var(--accent)); }
158
+ .ui-dropdown-item[data-disabled] { opacity: .5; pointer-events: none; }
159
+ .ui-dropdown-label { padding: .375rem .5rem; font-size: .75rem; font-weight: 500; color: hsl(var(--muted-foreground)); }
160
+ .ui-dropdown-separator { height: 1px; margin: .25rem -.25rem; background: hsl(var(--border)); }
161
+ :is(.ui-switch, .ui-slider, .ui-tabs-trigger, .ui-tabs-content, .ui-accordion-trigger):focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 3px; }
162
+ @media (pointer: coarse) {
163
+ .ui-switch { position: relative; }
164
+ .ui-switch::before { content: ""; position: absolute; inset: -.75rem -.25rem; }
165
+ .ui-slider, .ui-tabs-trigger, .ui-dropdown-item { min-height: 2.75rem; }
166
+ }
167
+ @media (prefers-reduced-motion: reduce) {
168
+ .ui-skeleton { animation: none; }
169
+ .ui-switch, .ui-switch-thumb, .ui-accordion-chevron { transition: none; }
170
+ }
171
+ }
172
+
173
+ @keyframes tp-skeleton-pulse { 50% { opacity: .45; } }
174
+
10
175
  @custom-variant dark (&:is(.dark *));
11
176
 
12
177
  @theme inline {
@@ -360,6 +525,16 @@
360
525
  background: hsl(var(--accent) / 0.62);
361
526
  }
362
527
 
528
+ .ui-table__selection {
529
+ width: 2.75rem;
530
+ padding: 0.75rem 0.875rem;
531
+ text-align: center;
532
+ }
533
+
534
+ .ui-table__checkbox {
535
+ margin: 0 auto;
536
+ }
537
+
363
538
  .ui-button-primary,
364
539
  .ui-button-secondary,
365
540
  .ui-button-ghost {
@@ -420,6 +595,16 @@
420
595
  color: hsl(var(--destructive-foreground));
421
596
  }
422
597
 
598
+ .ui-button-outline {
599
+ border-color: hsl(var(--border));
600
+ background: hsl(var(--background));
601
+ }
602
+
603
+ .ui-button-outline:hover:not(:disabled) {
604
+ border-color: hsl(var(--border));
605
+ background: hsl(var(--accent));
606
+ }
607
+
423
608
  .ui-button-danger:hover:not(:disabled) {
424
609
  border-color: color-mix(in hsl, hsl(var(--destructive)) 88%, black 12%);
425
610
  background: color-mix(in hsl, hsl(var(--destructive)) 88%, black 12%);