@columenlabs/lumen-ui 0.1.211 → 0.1.213
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/dist/assistant/chat-image.d.ts +4 -0
- package/dist/assistant/chat-image.js +47 -0
- package/dist/assistant/lumen-assistant.d.ts +12 -1
- package/dist/assistant/lumen-assistant.js +32 -4
- package/dist/assistant-panel.d.ts +9 -1
- package/dist/assistant-panel.js +13 -4
- package/dist/lumen-selector.js +20 -11
- package/package.json +3 -2
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// ChatImage: an image in a chat message. A thumbnail until clicked; then it opens full width in place,
|
|
3
|
+
// where the wheel or a pinch zooms at the pointer, a drag or two fingers pan and a double-click
|
|
4
|
+
// toggles fit and 2× (react-zoom-pan-pinch). Its toolbar zooms, resets, opens the image in a 90%
|
|
5
|
+
// lightbox with the same controls (the kit's Dialog: Esc, × or a click outside closes it) and
|
|
6
|
+
// shrinks it back to the thumbnail.
|
|
7
|
+
import { Maximize2Icon, Minimize2Icon, RotateCcwIcon, XIcon, ZoomInIcon, ZoomOutIcon, } from 'lucide-react';
|
|
8
|
+
import { useState } from 'react';
|
|
9
|
+
import { TransformComponent, TransformWrapper, useControls, useTransformEffect, } from 'react-zoom-pan-pinch';
|
|
10
|
+
import { IconButton } from '../icon-button.js';
|
|
11
|
+
import { cn } from '../lib/utils.js';
|
|
12
|
+
import { Dialog, DialogClose, DialogContent, DialogTitle } from '../ui/dialog.js';
|
|
13
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip.js';
|
|
14
|
+
const MAX_SCALE = 8;
|
|
15
|
+
/** The zoom buttons and the scale between them; `children` are the buttons after a divider. */
|
|
16
|
+
function ZoomBar({ children }) {
|
|
17
|
+
const { zoomIn, zoomOut, resetTransform } = useControls();
|
|
18
|
+
const [scale, setScale] = useState(1);
|
|
19
|
+
useTransformEffect(({ state }) => setScale(state.scale));
|
|
20
|
+
return (_jsxs("div", { className: "absolute top-2 right-2 z-10 flex items-center gap-0.5 rounded-lg border bg-popover/90 p-0.5 text-popover-foreground shadow-sm backdrop-blur-sm", "data-slot": "image-zoom-bar", children: [_jsx(IconButton, { icon: ZoomOutIcon, label: "Zoom out", disabled: scale <= 1.001, onClick: () => zoomOut() }), _jsxs("span", { className: "num min-w-10 text-center text-muted-foreground text-xs", "aria-live": "polite", children: [Math.round(scale * 100), "%"] }), _jsx(IconButton, { icon: ZoomInIcon, label: "Zoom in", disabled: scale >= MAX_SCALE - 0.001, onClick: () => zoomIn() }), _jsx(IconButton, { icon: RotateCcwIcon, label: "Reset zoom", onClick: () => resetTransform() }), _jsx("span", { className: "mx-0.5 h-4 w-px bg-border", "aria-hidden": true }), children] }));
|
|
21
|
+
}
|
|
22
|
+
/** The image at fit (scale 1), with zoom and pan; the wrapper takes the box its parent gives it. */
|
|
23
|
+
function Zoomable({ src, alt, children, }) {
|
|
24
|
+
return (_jsxs(TransformWrapper, { minScale: 1, maxScale: MAX_SCALE, centerZoomedOut: true, wheel: { step: 0.004 }, doubleClick: { mode: 'toggle', step: 1 }, children: [_jsx(ZoomBar, { children: children }), _jsx(TransformComponent, { wrapperClass: "!size-full cursor-grab active:cursor-grabbing", contentClass: "!size-full", children: _jsx("img", { src: src, alt: alt, draggable: false, className: "size-full object-contain" }) })] }));
|
|
25
|
+
}
|
|
26
|
+
export function ChatImage({ src, alt }) {
|
|
27
|
+
const [open, setOpen] = useState(false);
|
|
28
|
+
const [full, setFull] = useState(false);
|
|
29
|
+
const [ratio, setRatio] = useState(null);
|
|
30
|
+
if (!open) {
|
|
31
|
+
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("button", { type: "button", "aria-label": "View larger", onClick: () => setOpen(true), className: "group/thumb relative cursor-zoom-in overflow-hidden rounded-md border outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [_jsx("img", { src: src, alt: alt, className: "max-h-32 max-w-40 object-cover", onLoad: (e) => {
|
|
32
|
+
const { naturalWidth: w, naturalHeight: h } = e.currentTarget;
|
|
33
|
+
if (w && h)
|
|
34
|
+
setRatio(w / h);
|
|
35
|
+
} }), _jsx("span", { className: "absolute top-1 right-1 flex size-6 items-center justify-center rounded-md bg-background/80 opacity-0 transition-opacity group-hover/thumb:opacity-100 group-focus-visible/thumb:opacity-100", children: _jsx(Maximize2Icon, { className: "size-3.5", "aria-hidden": true }) })] }) }), _jsx(TooltipContent, { children: "View larger" })] }));
|
|
36
|
+
}
|
|
37
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "relative w-full overflow-hidden rounded-md border bg-background", style: { aspectRatio: ratio ?? 4 / 3, maxHeight: '60vh' }, "data-slot": "chat-image", children: _jsxs(Zoomable, { src: src, alt: alt, children: [_jsx(IconButton, { icon: Maximize2Icon, label: "Full screen", onClick: () => setFull(true) }), _jsx(IconButton, { icon: Minimize2Icon, label: "Shrink", onClick: () => {
|
|
38
|
+
setOpen(false);
|
|
39
|
+
setFull(false);
|
|
40
|
+
} })] }) }), _jsx(Dialog, { open: full, onOpenChange: setFull, children: _jsxs(DialogContent, { showCloseButton: false, "aria-describedby": undefined,
|
|
41
|
+
// Focus the box, not its first button: a focused button shows its tooltip, which would take
|
|
42
|
+
// the first Esc.
|
|
43
|
+
onOpenAutoFocus: (e) => {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
e.currentTarget.focus();
|
|
46
|
+
}, className: cn('h-[90vh] w-[90vw] max-w-none overflow-hidden p-0 sm:max-w-none', 'max-md:h-full max-md:w-full'), children: [_jsx(DialogTitle, { className: "sr-only", children: alt }), _jsx("div", { className: "relative size-full", children: _jsx(Zoomable, { src: src, alt: alt, children: _jsx(DialogClose, { asChild: true, children: _jsx(IconButton, { icon: XIcon, label: "Close" }) }) }) })] }) })] }));
|
|
47
|
+
}
|
|
@@ -16,6 +16,17 @@ export interface LumenAssistantProps {
|
|
|
16
16
|
onSettings?: () => void;
|
|
17
17
|
/** Days chats are kept, for the Chats menu's line. */
|
|
18
18
|
keepDays?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Chats belong to a place in the app (a workspace, a project): the Chats menu lists the
|
|
21
|
+
* `current` one's chats (rows whose `scope` matches) and a switch named `label` shows them all,
|
|
22
|
+
* each with its place's `name`. The switch is kept on this device. A row with no `scope` shows
|
|
23
|
+
* only with the switch on.
|
|
24
|
+
*/
|
|
25
|
+
chatScope?: {
|
|
26
|
+
current: string;
|
|
27
|
+
label: string;
|
|
28
|
+
name?: (scope: string) => string | undefined;
|
|
29
|
+
};
|
|
19
30
|
/** Closes the panel (its ✕). */
|
|
20
31
|
onClose?: () => void;
|
|
21
32
|
/** An outside chat state (the dock shares one between the button and the panel). */
|
|
@@ -47,7 +58,7 @@ export interface AssistantCommand {
|
|
|
47
58
|
run: (args: string) => string | undefined | Promise<string | undefined>;
|
|
48
59
|
}
|
|
49
60
|
/** The assistant's chat: header, messages and composer. */
|
|
50
|
-
export declare function LumenAssistant({ client, context, onSettings, keepDays, onClose, chat: outside, commands, sources, images, }: LumenAssistantProps): import("react").JSX.Element;
|
|
61
|
+
export declare function LumenAssistant({ client, context, onSettings, keepDays, chatScope, onClose, chat: outside, commands, sources, images, }: LumenAssistantProps): import("react").JSX.Element;
|
|
51
62
|
/** An allowance in its unit: `$4.20`, or `350 credits`. */
|
|
52
63
|
export declare const allowanceAmount: (u: AllowanceUnit, n: number) => string;
|
|
53
64
|
/** What the composer sends with the text: the tier picked with `@` and the things pointed at with `#`. */
|
|
@@ -26,6 +26,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover.js';
|
|
|
26
26
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '../ui/sheet.js';
|
|
27
27
|
import { Spinner } from '../ui/spinner.js';
|
|
28
28
|
import { Textarea } from '../ui/textarea.js';
|
|
29
|
+
import { ChatImage } from './chat-image.js';
|
|
29
30
|
import { chatMarkdown, chatTurns, copyMarkdown, turnMarkdown } from './copy.js';
|
|
30
31
|
import { composeAssistant, selectionRef, useComposeRequest, usePageSelection, useShownAssistantSources, } from './sources.js';
|
|
31
32
|
import { UsageHistoryView, tokens, usd } from './usage-history.js';
|
|
@@ -48,11 +49,12 @@ export function useAssistantStatus(client) {
|
|
|
48
49
|
export { usd };
|
|
49
50
|
const seconds = (ms) => (ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(1)} s`);
|
|
50
51
|
/** The assistant's chat: header, messages and composer. */
|
|
51
|
-
export function LumenAssistant({ client, context, onSettings, keepDays = 30, onClose, chat: outside, commands = [], sources = [], images = false, }) {
|
|
52
|
+
export function LumenAssistant({ client, context, onSettings, keepDays = 30, chatScope, onClose, chat: outside, commands = [], sources = [], images = false, }) {
|
|
52
53
|
const own = useAssistantChat(client);
|
|
53
54
|
const chat = outside ?? own;
|
|
54
55
|
const qc = useQueryClient();
|
|
55
56
|
const status = useAssistantStatus(client);
|
|
57
|
+
const [allScopes, setAllScopes] = useAllScopes(client.key);
|
|
56
58
|
const chats = useQuery({
|
|
57
59
|
queryKey: ['lumen-assistant', client.key, 'chats'],
|
|
58
60
|
queryFn: () => client.chats(),
|
|
@@ -181,11 +183,14 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, onC
|
|
|
181
183
|
allCommands.push(c);
|
|
182
184
|
const shown = useShownAssistantSources();
|
|
183
185
|
const allSources = [...shown, ...sources.filter((x) => !shown.some((y) => y.id === x.id))];
|
|
184
|
-
return (_jsxs("div", { "data-assistant": "", className: "flex h-full min-h-0 flex-col", children: [_jsxs("div", { className: "flex h-12 shrink-0 items-center gap-1 border-b px-3", children: [_jsx(SparklesIcon, { className: "size-4 text-edge" }), _jsx("span", { className: "min-w-0 flex-1 truncate font-medium", children: chat.title || s?.name || 'Assistant' }), s && s.tiers.length > 1 && (_jsx(LumenSelector, { trigger: "chip", size: "sm", label: "Tier", value: s.tier, options: s.tiers.map((t) => ({ value: t.id, label: t.label, detail: t.model_label })), onChange: (t) => void setTier(t) })), _jsx(LumenAssistantChats, { chats: chats.data
|
|
186
|
+
return (_jsxs("div", { "data-assistant": "", className: "flex h-full min-h-0 flex-col", children: [_jsxs("div", { className: "flex h-12 shrink-0 items-center gap-1 border-b px-3", children: [_jsx(SparklesIcon, { className: "size-4 text-edge" }), _jsx("span", { className: "min-w-0 flex-1 truncate font-medium", children: chat.title || s?.name || 'Assistant' }), s && s.tiers.length > 1 && (_jsx(LumenSelector, { trigger: "chip", size: "sm", label: "Tier", value: s.tier, options: s.tiers.map((t) => ({ value: t.id, label: t.label, detail: t.model_label })), onChange: (t) => void setTier(t) })), _jsx(LumenAssistantChats, { chats: chats.data
|
|
187
|
+
?.filter((c) => !chatScope || allScopes || c.scope === chatScope.current)
|
|
188
|
+
.map((c) => ({
|
|
185
189
|
id: c.id,
|
|
186
190
|
title: c.title,
|
|
187
191
|
updated_at: new Date(c.at).toISOString(),
|
|
188
|
-
|
|
192
|
+
...(chatScope && allScopes && c.scope ? { where: chatScope.name?.(c.scope) } : {}),
|
|
193
|
+
})) ?? (chats.isFetched ? [] : null), value: chat.chatId ?? undefined, toggle: chatScope && { label: chatScope.label, on: allScopes, onChange: setAllScopes }, note: `Chats are kept ${keepDays} days after their last message.`, onRefresh: () => void chats.refetch(), onOpen: (c) => void chat.open(c.id) }), _jsx(IconButton, { icon: CopyIcon, label: "Copy chat", disabled: !chat.items.length, onClick: () => copyMarkdown(chatMarkdown(chat.title, chat.items, name), 'Chat') }), _jsx(IconButton, { icon: PlusIcon, label: "New chat", disabled: !chat.items.length, onClick: chat.reset }), onClose && (_jsx(IconButton, { icon: XIcon, label: `Close ${s?.name ?? 'assistant'}`, onClick: onClose }))] }), _jsx(AssistantName.Provider, { value: name, children: _jsx(Screen.Provider, { value: controls, children: _jsx(Messages, { chat: chat, status: s, onSettings: onSettings }) }) }), _jsx(LumenComposer, { name: s?.name ?? 'the assistant', busy: chat.busy, queued: chat.queued, disabled: !s || !!s.wait, commands: allCommands, tiers: tiers, sources: allSources, page: context?.label, images: images, onSend: (text, more) => chat.send(text, { ...extra(), ...more }), onStop: chat.stop }), _jsx(UsageLine, { usage: chat.usage, client: client, spent: s?.spend.usd })] }));
|
|
189
194
|
}
|
|
190
195
|
function Messages({ chat, status, onSettings, }) {
|
|
191
196
|
const end = useRef(null);
|
|
@@ -259,7 +264,7 @@ function Item({ item, onSettle, busy, }) {
|
|
|
259
264
|
const tierName = useContext(TierNames);
|
|
260
265
|
switch (item.kind) {
|
|
261
266
|
case 'user':
|
|
262
|
-
return (_jsxs("div", { className: "ml-8 self-end rounded-lg bg-muted px-3 py-2 text-sm whitespace-pre-wrap", children: [(item.tier || item.refs?.length) && (_jsxs("span", { className: "mb-1 flex flex-wrap gap-1", children: [item.tier && _jsx(ComposerChip, { sign: "@", label: tierName(item.tier) }), item.refs?.map((r, i) => (_jsx(ComposerChip, { sign: "#", label: r.label }, `${r.kind}:${r.label}:${i}`)))] })), item.images?.length ? (_jsx("span", { className: "mb-1 flex flex-wrap gap-1.5", children: item.images.map((src, i) => (_jsx(
|
|
267
|
+
return (_jsxs("div", { className: "ml-8 self-end rounded-lg bg-muted px-3 py-2 text-sm whitespace-pre-wrap has-[[data-slot=chat-image]]:ml-0 has-[[data-slot=chat-image]]:w-full", children: [(item.tier || item.refs?.length) && (_jsxs("span", { className: "mb-1 flex flex-wrap gap-1", children: [item.tier && _jsx(ComposerChip, { sign: "@", label: tierName(item.tier) }), item.refs?.map((r, i) => (_jsx(ComposerChip, { sign: "#", label: r.label }, `${r.kind}:${r.label}:${i}`)))] })), item.images?.length ? (_jsx("span", { className: "mb-1 flex flex-wrap gap-1.5", children: item.images.map((src, i) => (_jsx(ChatImage, { src: src, alt: `Pasted ${i + 1}` }, `${i}:${src.length}`))) })) : null, item.text, item.context && (_jsxs("span", { className: "mt-1 block text-muted-foreground text-xs", children: ["on ", item.context] }))] }));
|
|
263
268
|
case 'text':
|
|
264
269
|
return (_jsxs("div", { className: "prose-sm text-sm leading-relaxed [&_code]:font-code [&_code]:text-xs [&_li]:ml-4 [&_ol]:list-decimal [&_p]:my-1.5 [&_ul]:list-disc", children: [_jsx(ChatMarkdown, { children: item.text }), item.live && (_jsx("span", { className: "ml-0.5 inline-block h-3.5 w-1.5 animate-pulse bg-foreground/60 align-middle" }))] }));
|
|
265
270
|
case 'step':
|
|
@@ -843,3 +848,26 @@ export function useLumenAssistantDock(client, o = {
|
|
|
843
848
|
panel: (_jsx(LumenAssistantPanel, { name: name, open: open, onOpenChange: setOpen, children: () => (_jsx(LumenAssistant, { client: client, chat: chat, onClose: () => setOpen(false), ...props })) })),
|
|
844
849
|
};
|
|
845
850
|
}
|
|
851
|
+
/** The Chats menu's All switch (`chatScope`), kept on this device per assistant. */
|
|
852
|
+
function useAllScopes(key) {
|
|
853
|
+
const name = `lumen-assistant:${key}:all-scopes`;
|
|
854
|
+
const [on, setOn] = useState(() => {
|
|
855
|
+
try {
|
|
856
|
+
return localStorage.getItem(name) === '1';
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
const set = (v) => {
|
|
863
|
+
setOn(v);
|
|
864
|
+
try {
|
|
865
|
+
if (v)
|
|
866
|
+
localStorage.setItem(name, '1');
|
|
867
|
+
else
|
|
868
|
+
localStorage.removeItem(name);
|
|
869
|
+
}
|
|
870
|
+
catch { }
|
|
871
|
+
};
|
|
872
|
+
return [on, set];
|
|
873
|
+
}
|
|
@@ -35,17 +35,25 @@ export interface AssistantChat {
|
|
|
35
35
|
title: string;
|
|
36
36
|
/** ISO time of its last message. */
|
|
37
37
|
updated_at: string;
|
|
38
|
+
/** Where it was started, shown before its time (`LumenAssistant` `chatScope`, its switch on). */
|
|
39
|
+
where?: string;
|
|
38
40
|
}
|
|
39
41
|
/**
|
|
40
42
|
* Chats, a dropdown from the panel header: chats grouped by when they were
|
|
41
43
|
* last used, searched by title. A row is read ahead when highlighted, so picking it opens at once.
|
|
42
44
|
*/
|
|
43
|
-
export declare function LumenAssistantChats<C extends AssistantChat>({ chats, value, note, disabled, onOpen, onRefresh, onHighlight, }: {
|
|
45
|
+
export declare function LumenAssistantChats<C extends AssistantChat>({ chats, value, note, toggle, disabled, onOpen, onRefresh, onHighlight, }: {
|
|
44
46
|
/** null while loading. */
|
|
45
47
|
chats: C[] | null;
|
|
46
48
|
value?: string;
|
|
47
49
|
/** A line above the list: how long chats are kept. */
|
|
48
50
|
note?: ReactNode;
|
|
51
|
+
/** A switch at the top of the menu, such as All workspaces (`LumenAssistant` `chatScope`). */
|
|
52
|
+
toggle?: {
|
|
53
|
+
label: string;
|
|
54
|
+
on: boolean;
|
|
55
|
+
onChange: (on: boolean) => void;
|
|
56
|
+
};
|
|
49
57
|
disabled?: boolean;
|
|
50
58
|
onOpen: (chat: C) => void;
|
|
51
59
|
/** The menu opened: read the list again. */
|
package/dist/assistant-panel.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
6
6
|
// Open or closed and the width are each browser tab's own (sessionStorage). The
|
|
7
7
|
// product fills the body (the product's assistant: its chat, composer, cards and spend).
|
|
8
8
|
import { HistoryIcon, SparklesIcon } from 'lucide-react';
|
|
9
|
-
import { useEffect, useRef, useState } from 'react';
|
|
9
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
10
10
|
import { useIsMobile } from './hooks/use-mobile.js';
|
|
11
11
|
import { cn } from './lib/utils.js';
|
|
12
12
|
import { LumenBoundary } from './lumen-boundary.js';
|
|
@@ -15,7 +15,9 @@ import { ASSISTANT_WIDTH, usePrefs } from './prefs.js';
|
|
|
15
15
|
import { ResizeEdge, tabState } from './side-dock.js';
|
|
16
16
|
import { Button } from './ui/button.js';
|
|
17
17
|
import { Kbd } from './ui/kbd.js';
|
|
18
|
+
import { Label } from './ui/label.js';
|
|
18
19
|
import { Sheet, SheetContent, SheetDescription, SheetTitle } from './ui/sheet.js';
|
|
20
|
+
import { Switch } from './ui/switch.js';
|
|
19
21
|
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
|
|
20
22
|
export { tabState };
|
|
21
23
|
/** Whether the assistant is open in this tab. */
|
|
@@ -138,8 +140,9 @@ function whenOf(at, now, group, timeZone) {
|
|
|
138
140
|
* Chats, a dropdown from the panel header: chats grouped by when they were
|
|
139
141
|
* last used, searched by title. A row is read ahead when highlighted, so picking it opens at once.
|
|
140
142
|
*/
|
|
141
|
-
export function LumenAssistantChats({ chats, value, note, disabled, onOpen, onRefresh, onHighlight, }) {
|
|
143
|
+
export function LumenAssistantChats({ chats, value, note, toggle, disabled, onOpen, onRefresh, onHighlight, }) {
|
|
142
144
|
const { timeZone } = usePrefs();
|
|
145
|
+
const toggleId = useId();
|
|
143
146
|
const now = new Date();
|
|
144
147
|
const byId = new Map((chats ?? []).map((c) => [c.id, c]));
|
|
145
148
|
const groups = GROUPS.map((group) => ({
|
|
@@ -148,11 +151,17 @@ export function LumenAssistantChats({ chats, value, note, disabled, onOpen, onRe
|
|
|
148
151
|
const at = new Date(c.updated_at);
|
|
149
152
|
const g = groupOf(at, now, timeZone);
|
|
150
153
|
return g === group
|
|
151
|
-
? [
|
|
154
|
+
? [
|
|
155
|
+
{
|
|
156
|
+
value: c.id,
|
|
157
|
+
label: c.title,
|
|
158
|
+
detail: [c.where, whenOf(at, now, g, timeZone)].filter(Boolean).join(' · '),
|
|
159
|
+
},
|
|
160
|
+
]
|
|
152
161
|
: [];
|
|
153
162
|
}),
|
|
154
163
|
})).filter((g) => g.options.length > 0);
|
|
155
|
-
return (_jsx(LumenSelector, { trigger: "icon", icon: HistoryIcon, label: "Chats", search: true, align: "end", groups: groups, empty: chats === null ? 'Loading…' : 'No chats yet', header: note && _jsx("p", { className: "px-2 pt-1 text-xs text-muted-foreground", children: note }), value: value, disabled: disabled, onOpenChange: (o) => o && onRefresh?.(), onHighlight: (id) => {
|
|
164
|
+
return (_jsx(LumenSelector, { trigger: "icon", icon: HistoryIcon, label: "Chats", search: true, align: "end", groups: groups, empty: chats === null ? 'Loading…' : 'No chats yet', header: (toggle || note) && (_jsxs(_Fragment, { children: [toggle && (_jsxs("div", { className: "flex items-center gap-2 px-2 pt-1.5", children: [_jsx(Label, { htmlFor: toggleId, className: "min-w-0 flex-1 truncate text-xs font-normal", children: toggle.label }), _jsx(Switch, { id: toggleId, checked: toggle.on, onCheckedChange: toggle.onChange, "aria-label": toggle.label })] })), note && _jsx("p", { className: "px-2 pt-1 text-xs text-muted-foreground", children: note })] })), value: value, disabled: disabled, onOpenChange: (o) => o && onRefresh?.(), onHighlight: (id) => {
|
|
156
165
|
const c = byId.get(id);
|
|
157
166
|
if (c)
|
|
158
167
|
onHighlight?.(c);
|
package/dist/lumen-selector.js
CHANGED
|
@@ -130,7 +130,7 @@ function useDuplicateWarning(all) {
|
|
|
130
130
|
}
|
|
131
131
|
}, [all]);
|
|
132
132
|
}
|
|
133
|
-
const ROW = 'relative flex w-full cursor-default items-center gap-2 rounded-md px-2 text-sm outline-hidden select-none h-8 max-md:h-11 max-md:px-4 max-md:text-[15px] data-highlighted:bg-accent data-highlighted:text-accent-foreground data-selected:bg-muted data-disabled:pointer-events-none
|
|
133
|
+
const ROW = 'relative flex w-full cursor-default items-center gap-2 rounded-md px-2 text-sm outline-hidden select-none h-8 max-md:h-11 max-md:px-4 max-md:text-[15px] data-highlighted:bg-accent data-highlighted:text-accent-foreground data-selected:bg-muted data-disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4';
|
|
134
134
|
/** A row with a description or thumbnail grows to fit it. */
|
|
135
135
|
const TALL = 'h-auto min-h-8 py-1.5 max-md:h-auto';
|
|
136
136
|
const GROUP_LABEL = 'sticky top-0 z-10 bg-popover px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground max-md:px-4';
|
|
@@ -138,12 +138,14 @@ const MORE = 'flex h-7 w-full items-center gap-1.5 rounded-md pr-2 pl-8 text-lef
|
|
|
138
138
|
/** One row's inside: box or icon, label, detail, and the check when picked. */
|
|
139
139
|
export function SelectorRowContent({ option, selected, multiple, trailing, }) {
|
|
140
140
|
const Icon = option.icon;
|
|
141
|
-
|
|
141
|
+
const off = !!option.disabled;
|
|
142
|
+
const reason = typeof option.disabled === 'string' ? option.disabled : null;
|
|
143
|
+
return (_jsxs(_Fragment, { children: [option.indent && _jsx("span", { "aria-hidden": true, className: "w-4 shrink-0" }), multiple && (_jsx("span", { "aria-hidden": true, className: cn('flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input', selected && 'border-primary bg-primary text-primary-foreground'), children: selected && _jsx(CheckIcon, { className: "size-3" }) })), option.thumb ? (_jsx("span", { "aria-hidden": true, className: "shrink-0", children: option.thumb })) : (Icon && _jsx(Icon, { className: cn('text-muted-foreground', off && 'opacity-60') })), option.description ? (_jsxs("span", { className: cn('flex min-w-0 flex-1 flex-col', off && 'text-muted-foreground'), children: [_jsx("span", { className: "truncate", children: option.content ?? option.label }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: option.description })] })) : (_jsx("span", { className: cn('min-w-0 truncate', option.detail || reason ? 'max-w-2/3 shrink-0' : 'flex-1', off && 'text-muted-foreground'), children: option.content ?? option.label })), reason ? (_jsxs("span", { className: "ml-auto inline-flex shrink-0 items-center gap-1 rounded-full border px-2 py-px text-xs text-foreground/80", children: [_jsx(CheckIcon, { className: "size-3! text-emerald-600 dark:text-emerald-400" }), reason] })) : (option.detail && (_jsx("span", { className: "min-w-0 flex-1 truncate text-right text-xs text-muted-foreground", title: option.detail, children: option.detail }))), trailing] }));
|
|
142
144
|
}
|
|
143
145
|
function Rows({ shown, picked, multiple, full, expand, }) {
|
|
144
146
|
return shown.map((g) => (_jsxs(Base.Group, { items: g.rows.map((o) => o.value), children: [g.label && _jsx(Base.GroupLabel, { className: GROUP_LABEL, children: g.label }), g.rows.map((o) => {
|
|
145
147
|
const row = (_jsx(Base.Item, { value: o.value, disabled: !!o.disabled || (full && !picked.has(o.value)), className: cn(ROW, (o.description || o.thumb) && TALL), children: _jsx(SelectorRowContent, { option: o, selected: picked.has(o.value), multiple: multiple && o.value !== CREATE }) }, o.value));
|
|
146
|
-
return
|
|
148
|
+
return row;
|
|
147
149
|
}), g.more > 0 && (_jsxs("button", { type: "button", className: MORE, onMouseDown: (e) => e.preventDefault(), onClick: () => expand(g.label), children: [_jsx(ChevronDownIcon, { className: "size-3.5" }), g.more, " more"] }))] }, g.label || '\u0000')));
|
|
148
150
|
}
|
|
149
151
|
const FIELD = "flex w-fit min-w-0 items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4";
|
|
@@ -283,12 +285,13 @@ export function LumenSelector(props) {
|
|
|
283
285
|
export function LumenSelectorList({ options, groups, query, onPick, onClose, empty, header, className, handle, label, fuzzy, }) {
|
|
284
286
|
const none = useMemo(() => new Set(), []);
|
|
285
287
|
const { shown, flat, expand } = useSelectorRows(options, groups, query, none, fuzzy);
|
|
286
|
-
const
|
|
288
|
+
const first = flat.findIndex((o) => !o.disabled);
|
|
289
|
+
const [at, setAt] = useState(first);
|
|
287
290
|
const listId = useId();
|
|
288
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: a new query starts at the first row.
|
|
289
|
-
useEffect(() => setAt(
|
|
290
|
-
const index =
|
|
291
|
-
const active = flat[index];
|
|
291
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: a new query starts at the first pickable row.
|
|
292
|
+
useEffect(() => setAt(first), [query]);
|
|
293
|
+
const index = flat[at] && !flat[at].disabled ? at : first;
|
|
294
|
+
const active = index < 0 ? undefined : flat[index];
|
|
292
295
|
useEffect(() => {
|
|
293
296
|
spotlight(active?.target, active?.targetName ?? active?.label);
|
|
294
297
|
}, [active]);
|
|
@@ -300,7 +303,13 @@ export function LumenSelectorList({ options, groups, query, onPick, onClose, emp
|
|
|
300
303
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
301
304
|
e.preventDefault();
|
|
302
305
|
const step = e.key === 'ArrowDown' ? 1 : -1;
|
|
303
|
-
|
|
306
|
+
if (index < 0)
|
|
307
|
+
return true;
|
|
308
|
+
let i = index;
|
|
309
|
+
do
|
|
310
|
+
i = (i + step + flat.length) % flat.length;
|
|
311
|
+
while (flat[i]?.disabled && i !== index);
|
|
312
|
+
setAt(i);
|
|
304
313
|
return true;
|
|
305
314
|
}
|
|
306
315
|
if ((e.key === 'Enter' && !e.shiftKey) || e.key === 'Tab') {
|
|
@@ -317,9 +326,9 @@ export function LumenSelectorList({ options, groups, query, onPick, onClose, emp
|
|
|
317
326
|
}
|
|
318
327
|
return false;
|
|
319
328
|
},
|
|
320
|
-
}), [flat, active, onPick, onClose]);
|
|
329
|
+
}), [flat, index, active, onPick, onClose]);
|
|
321
330
|
return (_jsxs("div", { "data-slot": "lumen-selector-list", className: cn('flex max-h-80 flex-col overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10', className), children: [header, _jsxs("div", { id: listId, "aria-label": label, className: "min-h-0 overflow-y-auto overscroll-contain p-1", children: [!flat.length && (_jsx("p", { className: "px-2 py-3 text-center text-sm text-muted-foreground", children: empty ?? 'Nothing matches' })), shown.map((g) => (_jsxs("section", { "aria-label": g.label || undefined, children: [g.label && _jsx("div", { className: GROUP_LABEL, children: g.label }), g.rows.map((o) => {
|
|
322
331
|
const i = flat.indexOf(o);
|
|
323
|
-
return (_jsx("button", { type: "button", tabIndex: -1, "aria-current": i === index || undefined, disabled: !!o.disabled, "data-highlighted": i === index ? '' : undefined, className: cn(ROW, 'text-left'), onMouseMove: () => i !== index && setAt(i), onMouseDown: (e) => e.preventDefault(), onClick: () => !o.disabled && onPick(o), children: _jsx(SelectorRowContent, { option: o, trailing: i ===
|
|
332
|
+
return (_jsx("button", { type: "button", tabIndex: -1, "aria-current": i === index || undefined, disabled: !!o.disabled, "data-disabled": o.disabled ? '' : undefined, "data-highlighted": i === index ? '' : undefined, className: cn(ROW, 'text-left'), onMouseMove: () => i !== index && setAt(i), onMouseDown: (e) => e.preventDefault(), onClick: () => !o.disabled && onPick(o), children: _jsx(SelectorRowContent, { option: o, trailing: i === index ? (_jsx("kbd", { className: "rounded border px-1 font-mono text-[11px] text-muted-foreground", children: "Tab" })) : undefined }) }, o.value));
|
|
324
333
|
}), g.more > 0 && (_jsxs("button", { type: "button", className: MORE, onMouseDown: (e) => e.preventDefault(), onClick: () => expand(g.label), children: [_jsx(ChevronDownIcon, { className: "size-3.5" }), g.more, " more ", g.label.toLowerCase()] }))] }, g.label || '\u0000')))] })] }));
|
|
325
334
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.213",
|
|
4
4
|
"description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@codemirror/lang-javascript": "6.2.5",
|
|
50
50
|
"@codemirror/lang-json": "6.0.2",
|
|
51
51
|
"@codemirror/lang-sql": "6.10.0",
|
|
52
|
-
"@columenlabs/lumen-ui-types": "0.1.
|
|
52
|
+
"@columenlabs/lumen-ui-types": "0.1.213",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"react-intersection-observer": "9.16.0",
|
|
76
76
|
"react-markdown": "10.1.0",
|
|
77
77
|
"react-resizable-panels": "^4.14.3",
|
|
78
|
+
"react-zoom-pan-pinch": "4.2.0",
|
|
78
79
|
"recharts": "^3.10.1",
|
|
79
80
|
"remark-gfm": "4.0.1",
|
|
80
81
|
"shiki": "4.5.0",
|