@columenlabs/lumen-ui 0.1.309 → 0.1.311
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/lumen-assistant.d.ts +10 -2
- package/dist/assistant/lumen-assistant.js +158 -15
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lumen-bottom-sheet.d.ts +6 -1
- package/dist/lumen-bottom-sheet.js +6 -3
- package/dist/lumen-floating-panel.d.ts +33 -0
- package/dist/lumen-floating-panel.js +79 -0
- package/dist/lumen-tabs.d.ts +7 -1
- package/dist/lumen-tabs.js +15 -1
- package/package.json +3 -2
|
@@ -110,7 +110,7 @@ export declare const allowanceAmount: (u: AllowanceUnit, n: number) => string;
|
|
|
110
110
|
* and a chevron that shows its steps (`children`). The chat draws each task this way; an app draws
|
|
111
111
|
* its own work the same way (an edit waiting for review) so every piece of work looks alike.
|
|
112
112
|
*/
|
|
113
|
-
export declare function WorkCard({ view, live, state, actions, onStop, steps, open: shownAt, children, }: {
|
|
113
|
+
export declare function WorkCard({ view, live, state, actions, onStop, steps, open: shownAt, rows, children, }: {
|
|
114
114
|
view: AssistantTaskView;
|
|
115
115
|
live?: boolean;
|
|
116
116
|
/** The task's own state, for `data-state`. */
|
|
@@ -121,6 +121,8 @@ export declare function WorkCard({ view, live, state, actions, onStop, steps, op
|
|
|
121
121
|
steps?: number;
|
|
122
122
|
/** Starts open; by default it is open while the work runs. */
|
|
123
123
|
open?: boolean;
|
|
124
|
+
/** `children` are rows with their own padding and lines (a task's steps), drawn edge to edge. */
|
|
125
|
+
rows?: boolean;
|
|
124
126
|
children?: ReactNode;
|
|
125
127
|
}): import("react").JSX.Element;
|
|
126
128
|
/**
|
|
@@ -153,7 +155,13 @@ export declare function readAssistantImage(file: File): Promise<AssistantImage |
|
|
|
153
155
|
* triggers list what can follow them as it is typed: `/` at the start a command, `@` a thing to
|
|
154
156
|
* point at (the `sources`), `#` a tier for this message; a picked tier or thing shows as a chip.
|
|
155
157
|
*/
|
|
156
|
-
export declare function LumenComposer({ name, busy, queued, queue, canSteer, onSteerQueued, onUnqueue, disabled, commands, tiers, sources, page, images: canAttach, modes, onSend, onStop, meter, voice, }: {
|
|
158
|
+
export declare function LumenComposer({ name, busy, queued, queue, canSteer, onSteerQueued, onUnqueue, disabled, commands, tiers, sources, page, images: canAttach, modes, onSend, onStop, meter, voice, queueShown, editQueued, }: {
|
|
159
|
+
/** The queue is drawn under the running task's card instead of above the box. */
|
|
160
|
+
queueShown?: boolean;
|
|
161
|
+
/** Gets this box's Edit, so a queued message shown elsewhere comes back here to edit. */
|
|
162
|
+
editQueued?: {
|
|
163
|
+
current: ((id: string) => void) | null;
|
|
164
|
+
};
|
|
157
165
|
/** A mic beside Send that types what the person says (`VoiceButton`); hidden where the browser can't. */
|
|
158
166
|
voice?: boolean;
|
|
159
167
|
/** Drawn in the icon row after the paperclip and the modes: `LumenAssistant` puts its context donut here. */
|
|
@@ -6,7 +6,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
6
6
|
// with Stop, a queued message and the chat's cost and context under it.
|
|
7
7
|
import { ASSISTANT_IMAGES_MAX, ASSISTANT_IMAGE_CHARS, COMPACT_AT, stepLineParts, stepLineText, } from '@columenlabs/lumen-ui-types/assistant';
|
|
8
8
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
9
|
-
import { ArrowUpIcon, CheckCheckIcon, CheckIcon, ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, CircleAlertIcon, CircleCheckIcon, CircleXIcon, CopyIcon, CornerDownRightIcon, CrosshairIcon, FileTextIcon, GlobeIcon, HandIcon, LightbulbIcon, ListPlusIcon, LoaderCircleIcon, MonitorIcon, PaperclipIcon, PencilIcon, PlusIcon, RefreshCwIcon, ReplyIcon, SearchIcon, SparklesIcon, SquareIcon, TriangleAlertIcon, Undo2Icon, WrenchIcon, XIcon, } from 'lucide-react';
|
|
9
|
+
import { ArrowUpIcon, CheckCheckIcon, CheckIcon, ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, CircleAlertIcon, CircleCheckIcon, CircleHelpIcon, CircleXIcon, CopyIcon, CornerDownRightIcon, CrosshairIcon, FileTextIcon, GlobeIcon, HandIcon, LightbulbIcon, ListPlusIcon, LoaderCircleIcon, MessageSquareTextIcon, MonitorIcon, PaperclipIcon, PencilIcon, PlusIcon, RefreshCwIcon, ReplyIcon, SearchIcon, SparklesIcon, SquareIcon, TriangleAlertIcon, Undo2Icon, WrenchIcon, XIcon, } from 'lucide-react';
|
|
10
10
|
import { Fragment, createContext, useContext, useEffect, useRef, useState, } from 'react';
|
|
11
11
|
import { LumenAssistantButton, LumenAssistantChats, LumenAssistantPanel, assistantOpen, useAssistantHotkey, } from '../assistant-panel.js';
|
|
12
12
|
import { screenMapFor, useAssistantScreen } from '../assistant-screen.js';
|
|
@@ -52,6 +52,10 @@ const seconds = (ms) => !ms ? '' : ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFix
|
|
|
52
52
|
export function LumenAssistant({ client, context, onSettings, keepDays = 30, chatScope, onClose, chat: outside, commands = [], sources = [], images = false, modes = [], imageTool, compactAt = COMPACT_AT, taskActions, taskState, taskView, }) {
|
|
53
53
|
const own = useAssistantChat(client);
|
|
54
54
|
const chat = outside ?? own;
|
|
55
|
+
// While the chat ends in an open task, its waiting messages sit under its card, not above the box.
|
|
56
|
+
const tail = chat.items[chat.items.length - 1];
|
|
57
|
+
const queueUnder = tail?.kind === 'task' && (taskState?.(tail) ?? tail.state) === 'open' && chat.queue.length > 0;
|
|
58
|
+
const editQueued = useRef(null);
|
|
55
59
|
const qc = useQueryClient();
|
|
56
60
|
const status = useAssistantStatus(client);
|
|
57
61
|
const [allScopes, setAllScopes] = useAllScopes(client.key);
|
|
@@ -195,7 +199,15 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, cha
|
|
|
195
199
|
title: c.title,
|
|
196
200
|
updated_at: new Date(c.at).toISOString(),
|
|
197
201
|
...(chatScope && allScopes && c.scope ? { where: chatScope.name?.(c.scope) } : {}),
|
|
198
|
-
})) ?? (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(ImageTool.Provider, { value: imageTool ?? null, children: _jsx(TaskSlots.Provider, { value: { actions: taskActions, state: taskState, view: taskView, stop: chat.stop }, children: _jsx(
|
|
202
|
+
})) ?? (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(ImageTool.Provider, { value: imageTool ?? null, children: _jsx(TaskSlots.Provider, { value: { actions: taskActions, state: taskState, view: taskView, stop: chat.stop }, children: _jsx(TaskAnswer.Provider, { value: s?.wait ? null : (text) => chat.send(text, extra()), children: _jsx(TaskQueue.Provider, { value: queueUnder
|
|
203
|
+
? {
|
|
204
|
+
queue: chat.queue,
|
|
205
|
+
canSteer: chat.busy && chat.canSteer,
|
|
206
|
+
onSteer: chat.steerQueued,
|
|
207
|
+
onEdit: (id) => editQueued.current?.(id),
|
|
208
|
+
onRemove: (id) => void chat.unqueue(id),
|
|
209
|
+
}
|
|
210
|
+
: null, children: _jsx(Messages, { chat: chat, status: s, onSettings: onSettings, onRetry: (at, user) => chat.retry(at, user, extra()), onFresh: (choice) => chat.fresh(choice, extra()), onAnswer: (text) => chat.send(text, extra()) }) }) }) }) }) }) }), _jsx(LumenComposer, { name: s?.name ?? 'the assistant', busy: chat.busy, queued: chat.queued, queue: chat.queue, canSteer: chat.canSteer, onSteerQueued: chat.steerQueued, onUnqueue: chat.unqueue, queueShown: queueUnder, editQueued: editQueued, disabled: !s || !!s.wait, commands: allCommands, tiers: tiers, sources: allSources, page: context?.label, images: images, modes: modes, onSend: (text, more, how) => chat.send(text, { ...extra(), ...more }, how), onStop: chat.stop, meter: _jsx(ContextDonut, { usage: chat.usage, compactAt: s?.compact_at ?? compactAt, items: chat.items }) })] }));
|
|
199
211
|
}
|
|
200
212
|
/** How near the end of the chat (px) still counts as at the end, so it keeps following. */
|
|
201
213
|
const FOLLOW_PX = 48;
|
|
@@ -239,7 +251,8 @@ function Messages({ chat, status, onSettings, onRetry, onFresh, onAnswer, }) {
|
|
|
239
251
|
: 'Open Settings' }))] })] })), chat.loading ? (_jsxs("p", { className: "flex items-center gap-2 text-muted-foreground text-sm", children: [_jsx(Spinner, {}), " Loading the chat\u2026"] })) : chat.items.length === 0 && !status?.wait ? (_jsxs("p", { className: "pt-8 text-center text-muted-foreground text-sm", children: ["Ask ", status?.name ?? 'the assistant', " anything."] })) : (_jsx(TierNames.Provider, { value: (id) => status?.tiers.find((t) => t.id === id)?.label ?? id, children: _jsx(UndoWrite.Provider, { value: chat.undo, children: _jsx(RetryTurn.Provider, { value: chat.busy || status?.wait ? null : onRetry, children: _jsx(FreshChoice.Provider, { value: chat.busy ? null : onFresh, children: _jsx(Items, { items: chat.items, onSettle: chat.settle, busy: chat.busy, turns: true }) }) }) }) })), !chat.busy &&
|
|
240
252
|
!status?.wait &&
|
|
241
253
|
(() => {
|
|
242
|
-
|
|
254
|
+
// A task's question shows its answers in its card (its Follow-up step).
|
|
255
|
+
const c = chat.items[chat.items.length - 1]?.kind === 'task' ? null : lastChoices(chat.items);
|
|
243
256
|
return c ? (_jsx("div", { className: "mt-2", children: _jsx(QuickReplies, { choices: c, onSend: onAnswer }, chat.items.length) })) : null;
|
|
244
257
|
})(), chat.busy && !chat.items.some(isLive) && (_jsxs("p", { className: "flex items-center gap-2 py-1 text-muted-foreground text-sm", children: [_jsx(Spinner, {}), " Thinking\u2026"] })), _jsx("div", { ref: end })] }));
|
|
245
258
|
}
|
|
@@ -571,6 +584,10 @@ function HandOff({ item, onSettle, busy, }) {
|
|
|
571
584
|
}
|
|
572
585
|
/** The app's parts of a task's card (`taskActions`, `taskState`, `taskView`) and the chat's Stop. */
|
|
573
586
|
const TaskSlots = createContext({});
|
|
587
|
+
/** Sends a quick reply or an answer in the person's words (null while the chat cannot take one). */
|
|
588
|
+
const TaskAnswer = createContext(null);
|
|
589
|
+
/** Messages waiting for the running task, drawn under its card: Steer now, Edit, Remove. */
|
|
590
|
+
const TaskQueue = createContext(null);
|
|
574
591
|
/** The words a card says without the app's own view. */
|
|
575
592
|
const TASK_STATE = {
|
|
576
593
|
open: 'Open',
|
|
@@ -625,22 +642,138 @@ function cardAt(items) {
|
|
|
625
642
|
* and a chevron that shows its steps (`children`). The chat draws each task this way; an app draws
|
|
626
643
|
* its own work the same way (an edit waiting for review) so every piece of work looks alike.
|
|
627
644
|
*/
|
|
628
|
-
export function WorkCard({ view, live = false, state, actions, onStop, steps = 0, open: shownAt, children, }) {
|
|
645
|
+
export function WorkCard({ view, live = false, state, actions, onStop, steps = 0, open: shownAt, rows = false, children, }) {
|
|
629
646
|
const tone = TONE[view.tone];
|
|
630
647
|
const [open, setOpen] = useState(null);
|
|
631
648
|
const shown = (open ?? (shownAt || live)) && steps > 0;
|
|
632
649
|
const Icon = tone.icon;
|
|
633
|
-
return (_jsxs("section", { "data-slot": "chat-task", "data-state": state, "data-tone": view.tone, "aria-label": view.title, className: cn('flex min-w-0 flex-col overflow-
|
|
650
|
+
return (_jsxs("section", { "data-slot": "chat-task", "data-state": state, "data-tone": view.tone, "aria-label": view.title, className: cn('flex min-w-0 flex-col overflow-clip rounded-lg border bg-card', view.tone === 'live' && 'border-primary/45', view.tone === 'ready' && 'border-gain/45', view.tone === 'failed' && 'border-loss/45', view.tone === 'ask' && 'border-warning/45'), children: [_jsxs("div", { "data-slot": "chat-task-head", className: "sticky -top-3 z-10 flex min-w-0 items-center gap-2.5 rounded-t-lg bg-card py-2 pr-1.5 pl-2.5", children: [_jsx("span", { className: cn('grid size-7 shrink-0 place-items-center rounded-md', tone.box), children: _jsx(Icon, { className: cn('size-4', tone.spin && 'animate-spin'), "aria-hidden": true }) }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate font-medium text-sm", title: view.title, children: view.title }), (view.detail || view.state || (live && view.since)) && (_jsxs("span", { className: "flex min-w-0 gap-1 text-muted-foreground text-xs", children: [view.detail && (_jsx("span", { className: "min-w-0 truncate", title: view.detail, children: view.detail })), view.state && (_jsxs("span", { className: cn('shrink-0', tone.word), children: [view.detail ? '· ' : '', view.state] })), live && view.since ? (_jsxs("span", { className: "shrink-0 tabular-nums", children: ["\u00B7 ", _jsx(Clock, { since: view.since })] })) : null] }))] }), _jsxs("span", { className: "flex shrink-0 items-center gap-1", children: [actions, onStop && (_jsx(IconButton, { icon: SquareIcon, label: "Stop", variant: "outline", size: "icon-sm", onClick: onStop })), steps > 0 && (_jsx(IconButton, { icon: shown ? ChevronUpIcon : ChevronDownIcon, label: shown ? 'Hide steps' : 'Show steps', size: "icon-sm", "aria-expanded": shown, onClick: () => setOpen(!shown) }))] })] }), live && (_jsx("div", { className: "h-0.5 w-full overflow-hidden bg-primary/15", children: _jsx("div", { className: "h-full w-1/3 animate-[lumen-progress_1.4s_ease-in-out_infinite] bg-primary" }) })), shown && _jsx("div", { className: cn('border-t', !rows && 'px-3 py-2'), children: children })] }));
|
|
651
|
+
}
|
|
652
|
+
/** A line of the assistant's words as a row's title: its first sentence, without Markdown marks. */
|
|
653
|
+
function firstLine(text) {
|
|
654
|
+
const plain = text
|
|
655
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
656
|
+
.replace(/[*_`#>[\]]|\(https?:[^)]*\)/g, '')
|
|
657
|
+
.replace(/\s+/g, ' ')
|
|
658
|
+
.trim();
|
|
659
|
+
const m = plain.match(/^(.{12,}?[.!?:])(\s|$)/);
|
|
660
|
+
return m?.[1] ?? plain;
|
|
661
|
+
}
|
|
662
|
+
/** A step that only looked something up: several in a row fold into one line. */
|
|
663
|
+
const lookup = (i) => i.kind === 'step' && i.ok !== false && !i.write && !i.choices && !i.live && !i.image;
|
|
664
|
+
/**
|
|
665
|
+
* A task's items as the rows of its card. Each turn's words are a step of their own: before its
|
|
666
|
+
* tools they are its Plan, after them its Result, and the last words of a turn that asks are the
|
|
667
|
+
* Follow-up, holding the question's answers. Lookups in a row fold into one line; the person's
|
|
668
|
+
* later messages (answers, steering) are rows too.
|
|
669
|
+
*/
|
|
670
|
+
function taskRows(items, answerable) {
|
|
671
|
+
const rows = [];
|
|
672
|
+
let looked = [];
|
|
673
|
+
let turnHasTools = false;
|
|
674
|
+
let turnStart = 0;
|
|
675
|
+
const flush = () => {
|
|
676
|
+
if (looked.length === 1)
|
|
677
|
+
rows.push({ key: `l${rows.length}`, kind: 'line', item: looked[0] });
|
|
678
|
+
else if (looked.length)
|
|
679
|
+
rows.push({ key: `k${rows.length}`, kind: 'lookups', steps: looked });
|
|
680
|
+
looked = [];
|
|
681
|
+
};
|
|
682
|
+
items.forEach((it, n) => {
|
|
683
|
+
if (it.kind === 'user') {
|
|
684
|
+
flush();
|
|
685
|
+
turnHasTools = false;
|
|
686
|
+
turnStart = rows.length + 1;
|
|
687
|
+
rows.push({ key: `u${n}`, kind: 'you', text: it.text, steered: !!it.steered });
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (it.kind === 'step' && it.choices)
|
|
691
|
+
return;
|
|
692
|
+
if (lookup(it)) {
|
|
693
|
+
looked.push(it);
|
|
694
|
+
turnHasTools = true;
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
flush();
|
|
698
|
+
if (it.kind === 'text') {
|
|
699
|
+
if (!it.text.trim())
|
|
700
|
+
return;
|
|
701
|
+
const later = items.slice(n + 1);
|
|
702
|
+
const toolsAfter = later.findIndex((x) => x.kind === 'user');
|
|
703
|
+
const rest = toolsAfter < 0 ? later : later.slice(0, toolsAfter);
|
|
704
|
+
const tag = rest.some((x) => x.kind === 'step' && !x.choices)
|
|
705
|
+
? 'Plan'
|
|
706
|
+
: turnHasTools || rows.slice(turnStart).some((r) => r.kind !== 'words')
|
|
707
|
+
? 'Result'
|
|
708
|
+
: 'Reply';
|
|
709
|
+
rows.push({ key: `t${n}`, kind: 'words', tag, text: it.text });
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (it.kind === 'step' || it.kind === 'draft')
|
|
713
|
+
turnHasTools = true;
|
|
714
|
+
rows.push({ key: `i${n}`, kind: 'line', item: it });
|
|
715
|
+
});
|
|
716
|
+
flush();
|
|
717
|
+
// The last words of the chat, while its question waits: the Follow-up, with its answers.
|
|
718
|
+
if (answerable) {
|
|
719
|
+
const last = rows.findLastIndex((r) => r.kind === 'words');
|
|
720
|
+
const r = rows[last];
|
|
721
|
+
if (r?.kind === 'words' && last >= rows.findLastIndex((x) => x.kind === 'you'))
|
|
722
|
+
rows[last] = { ...r, tag: 'Follow-up', choices: answerable };
|
|
723
|
+
}
|
|
724
|
+
return rows;
|
|
725
|
+
}
|
|
726
|
+
const ROW_ICON = {
|
|
727
|
+
line: WrenchIcon,
|
|
728
|
+
lookups: SearchIcon,
|
|
729
|
+
words: LightbulbIcon,
|
|
730
|
+
you: CornerDownRightIcon,
|
|
731
|
+
Plan: LightbulbIcon,
|
|
732
|
+
Result: CircleCheckIcon,
|
|
733
|
+
'Follow-up': CircleHelpIcon,
|
|
734
|
+
Reply: MessageSquareTextIcon,
|
|
735
|
+
};
|
|
736
|
+
/** The person's answer in their own words, under a Follow-up's quick replies. */
|
|
737
|
+
function OwnAnswer({ onSend }) {
|
|
738
|
+
const [text, setText] = useState('');
|
|
739
|
+
const send = () => {
|
|
740
|
+
if (!text.trim())
|
|
741
|
+
return;
|
|
742
|
+
onSend(text.trim());
|
|
743
|
+
setText('');
|
|
744
|
+
};
|
|
745
|
+
return (_jsxs("form", { className: "flex min-w-0 items-center gap-1 rounded-lg border bg-card py-1 pr-1 pl-2.5", onSubmit: (e) => {
|
|
746
|
+
e.preventDefault();
|
|
747
|
+
send();
|
|
748
|
+
}, children: [_jsx("input", { value: text, onChange: (e) => setText(e.target.value), placeholder: "Something else\u2026", "aria-label": "Answer in your own words", className: "min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" }), _jsx(IconButton, { icon: ArrowUpIcon, label: "Send", size: "icon-xs", type: "submit", disabled: !text.trim() })] }));
|
|
749
|
+
}
|
|
750
|
+
/** One row of a task's card: a line that opens to its whole text (one open at a time). */
|
|
751
|
+
function TaskRowView({ row, open, onToggle, onSettle, busy, }) {
|
|
752
|
+
const answer = useContext(TaskAnswer);
|
|
753
|
+
if (row.kind === 'line')
|
|
754
|
+
return (_jsx("div", { className: "border-t px-3 py-1.5 first:border-t-0", children: _jsx(Item, { item: row.item, onSettle: onSettle, busy: busy }) }));
|
|
755
|
+
const tag = row.kind === 'words' ? row.tag : row.kind === 'you' ? (row.steered ? 'Steer' : 'You') : null;
|
|
756
|
+
const Icon = ROW_ICON[row.kind === 'words' ? row.tag : row.kind];
|
|
757
|
+
const now = row.kind === 'words' && row.tag === 'Follow-up';
|
|
758
|
+
const title = row.kind === 'lookups'
|
|
759
|
+
? row.steps.map((x) => x.title).join(' · ')
|
|
760
|
+
: row.kind === 'words'
|
|
761
|
+
? firstLine(row.text)
|
|
762
|
+
: row.text;
|
|
763
|
+
return (_jsxs("div", { "data-slot": "chat-task-row", "data-open": open || undefined, className: "border-t first:border-t-0", children: [_jsxs("button", { type: "button", "aria-expanded": open, onClick: onToggle, className: cn('flex w-full min-w-0 items-center gap-2 px-3 py-1.5 text-left text-sm hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none', now && 'bg-primary/5'), children: [_jsx(Icon, { className: cn('size-3.5 shrink-0', now ? 'text-primary' : 'text-muted-foreground'), "aria-hidden": true }), tag && (_jsx("span", { className: cn('shrink-0 text-xs uppercase tracking-wide', now ? 'text-primary' : 'text-muted-foreground'), children: tag })), _jsx("span", { className: cn('min-w-0 flex-1 truncate', !open && !now && 'text-muted-foreground'), title: title, children: title }), row.kind === 'lookups' && (_jsx("span", { className: "shrink-0 text-muted-foreground text-xs tabular-nums", children: row.steps.length })), open ? (_jsx(ChevronDownIcon, { className: "size-3.5 shrink-0 text-muted-foreground", "aria-hidden": true })) : (_jsx(ChevronRightIcon, { className: "size-3.5 shrink-0 text-muted-foreground", "aria-hidden": true }))] }), open && (_jsxs("div", { className: "flex min-w-0 flex-col gap-2 px-3 pt-0.5 pb-2.5", children: [row.kind === 'lookups' ? (_jsx(Items, { items: row.steps, onSettle: onSettle, busy: busy })) : row.kind === 'words' ? (_jsx(Item, { item: { kind: 'text', text: row.text }, onSettle: onSettle, busy: busy })) : (_jsx("p", { className: "text-sm whitespace-pre-wrap [overflow-wrap:anywhere]", children: row.text })), row.kind === 'words' && row.choices && answer && (_jsxs(_Fragment, { children: [_jsx(QuickReplies, { choices: row.choices, onSend: answer }, row.key), _jsx(OwnAnswer, { onSend: answer })] }))] }))] }));
|
|
634
764
|
}
|
|
635
765
|
/**
|
|
636
766
|
* A task's turns in the chat (`TaskItem`) as one card: a tinted icon for where it stands, its title
|
|
637
767
|
* and a line under it (the app's `taskView`), the app's buttons (`taskActions`), Stop while it runs
|
|
638
|
-
* and a chevron that
|
|
639
|
-
*
|
|
640
|
-
*
|
|
768
|
+
* and a chevron that folds its steps. The person's first message sits above it; everything after,
|
|
769
|
+
* the assistant's words included, is a step inside it (`taskRows`). Only the newest step is open:
|
|
770
|
+
* when the next one starts the one before folds, and any step opens again with a tap. Messages
|
|
771
|
+
* queued while it runs wait under it (`TaskQueue`).
|
|
641
772
|
*/
|
|
642
773
|
function TaskBlock({ item, onSettle, busy, }) {
|
|
643
774
|
const slots = useContext(TaskSlots);
|
|
775
|
+
const answer = useContext(TaskAnswer);
|
|
776
|
+
const queue = useContext(TaskQueue);
|
|
644
777
|
const state = slots.state?.(item) ?? item.state;
|
|
645
778
|
const items = item.items;
|
|
646
779
|
const live = items.some(isLive);
|
|
@@ -649,11 +782,19 @@ function TaskBlock({ item, onSettle, busy, }) {
|
|
|
649
782
|
title: item.label,
|
|
650
783
|
state: live ? 'Working' : TASK_STATE[state],
|
|
651
784
|
};
|
|
652
|
-
const
|
|
653
|
-
const
|
|
654
|
-
const
|
|
655
|
-
const
|
|
656
|
-
|
|
785
|
+
const first = items.findIndex((i) => i.kind === 'user');
|
|
786
|
+
const head = items.slice(0, first + 1);
|
|
787
|
+
const inner = items.slice(first + 1);
|
|
788
|
+
const answerable = answer && !busy ? lastChoices(items) : null;
|
|
789
|
+
const rows = taskRows(inner, answerable);
|
|
790
|
+
// The newest row is open unless the person picked another since the rows last changed.
|
|
791
|
+
const [picked, setPicked] = useState(null);
|
|
792
|
+
const newest = rows.length - 1;
|
|
793
|
+
const auto = rows[newest]?.kind === 'line' ? null : newest;
|
|
794
|
+
const open = picked && picked.at === rows.length ? picked.open : auto;
|
|
795
|
+
const failed = inner.some((x) => x.kind === 'step' && (!x.ok || x.write?.status === 'failed'));
|
|
796
|
+
const card = (_jsx(WorkCard, { view: view, live: live, state: state, actions: slots.actions?.(item, live), onStop: live ? slots.stop : undefined, steps: rows.length, open: failed || !!answerable || state === 'open', rows: true, children: rows.map((r, n) => (_jsx(TaskRowView, { row: r, open: open === n, onToggle: () => setPicked({ at: rows.length, open: open === n ? null : n }), onSettle: onSettle, busy: busy }, r.key))) }));
|
|
797
|
+
return (_jsxs("div", { "data-slot": "chat-task-turns", className: "flex min-w-0 flex-col gap-3", children: [head.length > 0 && _jsx(Items, { items: head, onSettle: onSettle, busy: busy }), card, queue && state === 'open' && queue.queue.length > 0 && (_jsx(ComposerQueue, { queue: queue.queue, canSteer: queue.canSteer, onSteer: queue.onSteer, onEdit: queue.onEdit, onRemove: queue.onRemove }))] }));
|
|
657
798
|
}
|
|
658
799
|
/** The quick replies the last turn offers (`StepItem.choices`), unless the person answered since. */
|
|
659
800
|
function lastChoices(items) {
|
|
@@ -767,7 +908,7 @@ const imageUrl = (i) => `data:${i.media_type};base64,${i.data}`;
|
|
|
767
908
|
* triggers list what can follow them as it is typed: `/` at the start a command, `@` a thing to
|
|
768
909
|
* point at (the `sources`), `#` a tier for this message; a picked tier or thing shows as a chip.
|
|
769
910
|
*/
|
|
770
|
-
export function LumenComposer({ name, busy, queued, queue, canSteer = false, onSteerQueued, onUnqueue, disabled, commands = [], tiers = [], sources = [], page, images: canAttach = false, modes = [], onSend, onStop, meter, voice = true, }) {
|
|
911
|
+
export function LumenComposer({ name, busy, queued, queue, canSteer = false, onSteerQueued, onUnqueue, disabled, commands = [], tiers = [], sources = [], page, images: canAttach = false, modes = [], onSend, onStop, meter, voice = true, queueShown = false, editQueued, }) {
|
|
771
912
|
const [text, setText] = useState('');
|
|
772
913
|
const [caret, setCaret] = useState(0);
|
|
773
914
|
const [closed, setClosed] = useState(null);
|
|
@@ -905,7 +1046,9 @@ export function LumenComposer({ name, busy, queued, queue, canSteer = false, onS
|
|
|
905
1046
|
setUse(q.extra.use);
|
|
906
1047
|
requestAnimationFrame(() => box.current?.focus());
|
|
907
1048
|
};
|
|
908
|
-
|
|
1049
|
+
if (editQueued)
|
|
1050
|
+
editQueued.current = onUnqueue ? edit : null;
|
|
1051
|
+
const waiting = queueShown ? [] : (queue ?? (queued ? [{ id: 'queued', message: queued }] : []));
|
|
909
1052
|
const pickCommand = (o) => {
|
|
910
1053
|
const c = commands.find((x) => x.name === o.value);
|
|
911
1054
|
if (!c)
|
package/dist/index.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export { DetailsTable, type DetailsTableProps, FactsTable } from './details-tabl
|
|
|
41
41
|
export { DrawerSection, type DrawerSectionProps } from './drawer-section.js';
|
|
42
42
|
export { type DrawerAction, LumenDrawer, type LumenDrawerProps } from './lumen-drawer.js';
|
|
43
43
|
export { LumenBottomSheet, type LumenBottomSheetProps, type SheetSnap } from './lumen-bottom-sheet.js';
|
|
44
|
+
export { type FloatingRect, LumenFloatingPanel, type LumenFloatingPanelProps, } from './lumen-floating-panel.js';
|
|
44
45
|
export { LumenCode, type LumenCodeProps, langOfPath } from './lumen-code.js';
|
|
45
46
|
export { LumenCodeEditor, type LumenCodeEditorProps, type LumenCodeLang, } from './lumen-code-editor.js';
|
|
46
47
|
export { RenameField, type RenameFieldProps } from './rename-field.js';
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,7 @@ export { DetailsTable, FactsTable } from './details-table.js';
|
|
|
48
48
|
export { DrawerSection } from './drawer-section.js';
|
|
49
49
|
export { LumenDrawer } from './lumen-drawer.js';
|
|
50
50
|
export { LumenBottomSheet } from './lumen-bottom-sheet.js';
|
|
51
|
+
export { LumenFloatingPanel, } from './lumen-floating-panel.js';
|
|
51
52
|
export { LumenCode, langOfPath } from './lumen-code.js';
|
|
52
53
|
export { LumenCodeEditor, } from './lumen-code-editor.js';
|
|
53
54
|
export { RenameField } from './rename-field.js';
|
|
@@ -13,6 +13,11 @@ export interface LumenBottomSheetProps {
|
|
|
13
13
|
onSnap: (snap: SheetSnap) => void;
|
|
14
14
|
/** Its name for screen readers. */
|
|
15
15
|
title: string;
|
|
16
|
+
/**
|
|
17
|
+
* Its top row (tabs, a title): shown at every snap, and part of the drag area with the grab bar,
|
|
18
|
+
* so a drag that starts anywhere on it moves the sheet. Taps on its buttons still reach them.
|
|
19
|
+
*/
|
|
20
|
+
header?: React.ReactNode;
|
|
16
21
|
/** What it holds. It stays mounted at every snap, so running work inside keeps going. */
|
|
17
22
|
children: React.ReactNode;
|
|
18
23
|
className?: string;
|
|
@@ -22,4 +27,4 @@ export interface LumenBottomSheetProps {
|
|
|
22
27
|
* drags above it reach the page. The person drags its handle between `snaps` (peek, half, full);
|
|
23
28
|
* it never closes, so what it holds is never unmounted. Built on vaul's snap points.
|
|
24
29
|
*/
|
|
25
|
-
export declare function LumenBottomSheet({ snaps, snap, onSnap, title, children, className, }: LumenBottomSheetProps): React.JSX.Element;
|
|
30
|
+
export declare function LumenBottomSheet({ snaps, snap, onSnap, title, header, children, className, }: LumenBottomSheetProps): React.JSX.Element;
|
|
@@ -46,7 +46,7 @@ function useScreenHeight() {
|
|
|
46
46
|
* drags above it reach the page. The person drags its handle between `snaps` (peek, half, full);
|
|
47
47
|
* it never closes, so what it holds is never unmounted. Built on vaul's snap points.
|
|
48
48
|
*/
|
|
49
|
-
export function LumenBottomSheet({ snaps, snap, onSnap, title, children, className, }) {
|
|
49
|
+
export function LumenBottomSheet({ snaps, snap, onSnap, title, header, children, className, }) {
|
|
50
50
|
const h = useScreenHeight();
|
|
51
51
|
const inset = useSafeBottom();
|
|
52
52
|
// A px snap rests above the home bar: vaul gets it with the bar's height added.
|
|
@@ -63,7 +63,10 @@ export function LumenBottomSheet({ snaps, snap, onSnap, title, children, classNa
|
|
|
63
63
|
const [typing, setTyping] = React.useState(false);
|
|
64
64
|
const lifted = typing && kb ? Math.min(rested, kb.height) : undefined;
|
|
65
65
|
const shown = lifted ?? rested;
|
|
66
|
-
return (_jsx(Drawer.Root, { open: true, modal: false, dismissible: false,
|
|
66
|
+
return (_jsx(Drawer.Root, { open: true, modal: false, dismissible: false,
|
|
67
|
+
// The grab bar and the header drag it; the body is marked no-drag, so its lists and chat
|
|
68
|
+
// scroll and never move the sheet. (Only the 4px bar used to drag, so most swipes missed.)
|
|
69
|
+
snapPoints: rests, activeSnapPoint: rests[at] ?? rest(snap), setActiveSnapPoint: (s) => {
|
|
67
70
|
const i = s == null ? -1 : rests.indexOf(s);
|
|
68
71
|
const next = snaps[i];
|
|
69
72
|
if (next !== undefined)
|
|
@@ -75,5 +78,5 @@ export function LumenBottomSheet({ snaps, snap, onSnap, title, children, classNa
|
|
|
75
78
|
setTyping(false);
|
|
76
79
|
}, "data-lifted": lifted === undefined ? undefined : '', style: lifted === undefined || !kb
|
|
77
80
|
? undefined
|
|
78
|
-
: { top: kb.top + kb.height - lifted, bottom: 'auto', height: lifted }, className: cn('fixed inset-x-0 bottom-0 z-40 flex h-dvh flex-col rounded-t-2xl border-t bg-popover text-popover-foreground shadow-[0_-4px_16px_rgb(0_0_0/0.15)] outline-none', className), children: [_jsx(Drawer.Title, { className: "sr-only", children: title }), _jsxs("div", { className: "flex min-h-0 flex-col overflow-hidden", style: { height: shown }, children: [_jsx(Drawer.Handle, { "aria-label": "Drag to resize", className: "
|
|
81
|
+
: { top: kb.top + kb.height - lifted, bottom: 'auto', height: lifted }, className: cn('fixed inset-x-0 bottom-0 z-40 flex h-dvh flex-col rounded-t-2xl border-t bg-popover text-popover-foreground shadow-[0_-4px_16px_rgb(0_0_0/0.15)] outline-none', className), children: [_jsx(Drawer.Title, { className: "sr-only", children: title }), _jsxs("div", { className: "flex min-h-0 flex-col overflow-hidden", style: { height: shown }, children: [_jsx("div", { className: "flex h-5 shrink-0 items-center justify-center", children: _jsx(Drawer.Handle, { "aria-label": "Drag to resize", className: "m-0! h-1! w-9! shrink-0 rounded-full! bg-border! opacity-100!" }) }), header !== undefined && _jsx("div", { className: "shrink-0", children: header }), _jsx("div", { "data-vaul-no-drag": "", className: cn('flex min-h-0 flex-1 flex-col', lifted === undefined && 'pb-[env(safe-area-inset-bottom)]'), children: children })] })] }) }) }) }));
|
|
79
82
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
/** Where the panel sits in its box, and how big it is, px. */
|
|
3
|
+
export interface FloatingRect {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
export interface LumenFloatingPanelProps {
|
|
10
|
+
/** Its name for screen readers, and the fold button's tooltip. */
|
|
11
|
+
title: string;
|
|
12
|
+
/** Its top row (tabs, a title), between the grip and the fold button. Shown folded too. */
|
|
13
|
+
header?: React.ReactNode;
|
|
14
|
+
/** What it holds. It stays mounted while folded, so running work inside keeps going. */
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
/** Where it starts on a device that has not moved it yet. */
|
|
17
|
+
defaultRect?: Partial<FloatingRect>;
|
|
18
|
+
/** Kept on this device under this key: where the person left it, its size and whether it is folded. */
|
|
19
|
+
storageKey?: string;
|
|
20
|
+
/** Folded to its header, controlled (left out, it keeps its own and the device remembers it). */
|
|
21
|
+
folded?: boolean;
|
|
22
|
+
onFold?: (folded: boolean) => void;
|
|
23
|
+
minWidth?: number;
|
|
24
|
+
minHeight?: number;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A see-through panel that floats over a live page (a 3D view, a map) inside its parent: the person
|
|
29
|
+
* drags it by its grip, resizes it from its edges and corner, and folds it to its header. Its box is
|
|
30
|
+
* the parent (which must be `relative`), and it is kept inside it when the box changes size (a phone
|
|
31
|
+
* turning). Taps outside it reach the page. Built on react-rnd.
|
|
32
|
+
*/
|
|
33
|
+
export declare function LumenFloatingPanel({ title, header, children, defaultRect, storageKey, folded: foldedProp, onFold, minWidth, minHeight, className, }: LumenFloatingPanelProps): React.JSX.Element;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronDownIcon, ChevronUpIcon, GripVerticalIcon } from 'lucide-react';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Rnd } from 'react-rnd';
|
|
5
|
+
import { cn } from './lib/utils.js';
|
|
6
|
+
import { Button } from './ui/button.js';
|
|
7
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
|
|
8
|
+
const DEFAULT = { x: 12, y: 12, width: 330, height: 300 };
|
|
9
|
+
function load(key) {
|
|
10
|
+
if (!key)
|
|
11
|
+
return {};
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(localStorage.getItem(key) ?? '{}') ?? {};
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A see-through panel that floats over a live page (a 3D view, a map) inside its parent: the person
|
|
21
|
+
* drags it by its grip, resizes it from its edges and corner, and folds it to its header. Its box is
|
|
22
|
+
* the parent (which must be `relative`), and it is kept inside it when the box changes size (a phone
|
|
23
|
+
* turning). Taps outside it reach the page. Built on react-rnd.
|
|
24
|
+
*/
|
|
25
|
+
export function LumenFloatingPanel({ title, header, children, defaultRect, storageKey, folded: foldedProp, onFold, minWidth = 240, minHeight = 160, className, }) {
|
|
26
|
+
const saved = React.useMemo(() => load(storageKey), [storageKey]);
|
|
27
|
+
const [rect, setRect] = React.useState(saved.rect ?? { ...DEFAULT, ...defaultRect });
|
|
28
|
+
const [foldedOwn, setFoldedOwn] = React.useState(!!saved.folded);
|
|
29
|
+
const folded = foldedProp ?? foldedOwn;
|
|
30
|
+
const setFolded = (f) => {
|
|
31
|
+
setFoldedOwn(f);
|
|
32
|
+
onFold?.(f);
|
|
33
|
+
};
|
|
34
|
+
const box = React.useRef(null);
|
|
35
|
+
const [room, setRoom] = React.useState(null);
|
|
36
|
+
React.useLayoutEffect(() => {
|
|
37
|
+
const el = box.current;
|
|
38
|
+
if (!el)
|
|
39
|
+
return;
|
|
40
|
+
const read = () => setRoom({ w: el.clientWidth, h: el.clientHeight });
|
|
41
|
+
read();
|
|
42
|
+
const seen = new ResizeObserver(read);
|
|
43
|
+
seen.observe(el);
|
|
44
|
+
return () => seen.disconnect();
|
|
45
|
+
}, []);
|
|
46
|
+
React.useEffect(() => {
|
|
47
|
+
if (!storageKey)
|
|
48
|
+
return;
|
|
49
|
+
try {
|
|
50
|
+
localStorage.setItem(storageKey, JSON.stringify({ rect, folded }));
|
|
51
|
+
}
|
|
52
|
+
catch { }
|
|
53
|
+
}, [storageKey, rect, folded]);
|
|
54
|
+
// Inside the box whatever its size now: smaller than the box, and moved back in if it hangs out.
|
|
55
|
+
const fit = (r) => {
|
|
56
|
+
if (!room)
|
|
57
|
+
return r;
|
|
58
|
+
const width = Math.max(Math.min(minWidth, room.w), Math.min(r.width, room.w));
|
|
59
|
+
const height = Math.max(Math.min(minHeight, room.h), Math.min(r.height, room.h));
|
|
60
|
+
return {
|
|
61
|
+
width,
|
|
62
|
+
height,
|
|
63
|
+
x: Math.max(0, Math.min(r.x, room.w - width)),
|
|
64
|
+
y: Math.max(0, Math.min(r.y, room.h - height)),
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
const shown = fit(rect);
|
|
68
|
+
return (_jsx("div", { ref: box, className: "pointer-events-none absolute inset-0 z-20", children: room && (_jsx(Rnd, { bounds: "parent", position: { x: shown.x, y: shown.y }, size: { width: shown.width, height: folded ? 'auto' : shown.height }, minWidth: minWidth, minHeight: folded ? undefined : minHeight, dragHandleClassName: "lumen-float-grip", enableResizing: folded
|
|
69
|
+
? false
|
|
70
|
+
: { right: true, bottom: true, bottomRight: true, left: true, bottomLeft: true },
|
|
71
|
+
// A finger needs a fuller edge than a mouse: 12px edges, a 24px corner.
|
|
72
|
+
resizeHandleStyles: {
|
|
73
|
+
right: { width: 12, right: -6 },
|
|
74
|
+
left: { width: 12, left: -6 },
|
|
75
|
+
bottom: { height: 12, bottom: -6 },
|
|
76
|
+
bottomRight: { width: 24, height: 24, right: -6, bottom: -6 },
|
|
77
|
+
bottomLeft: { width: 24, height: 24, left: -6, bottom: -6 },
|
|
78
|
+
}, onDragStop: (_e, d) => setRect((r) => ({ ...r, x: d.x, y: d.y })), onResizeStop: (_e, _dir, el, _delta, pos) => setRect({ x: pos.x, y: pos.y, width: el.offsetWidth, height: el.offsetHeight }), className: "pointer-events-auto", children: _jsxs("section", { "data-slot": "lumen-floating-panel", "aria-label": title, className: cn('flex h-full flex-col overflow-hidden rounded-xl border bg-popover/85 text-popover-foreground shadow-lg backdrop-blur-md', className), children: [_jsxs("div", { className: "flex shrink-0 items-center gap-0.5 border-b pr-1", children: [_jsx("span", { className: "lumen-float-grip flex h-11 w-8 shrink-0 cursor-grab touch-none items-center justify-center text-muted-foreground active:cursor-grabbing", "aria-hidden": true, children: _jsx(GripVerticalIcon, { className: "size-4" }) }), _jsx("div", { className: "min-w-0 flex-1", children: header }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": folded ? `Open ${title}` : `Fold ${title}`, "aria-expanded": !folded, onClick: () => setFolded(!folded), children: folded ? _jsx(ChevronDownIcon, {}) : _jsx(ChevronUpIcon, {}) }) }), _jsx(TooltipContent, { children: folded ? `Open ${title}` : `Fold ${title}` })] })] }), _jsx("div", { className: cn('flex min-h-0 flex-1 flex-col', folded && 'hidden'), children: children })] }) })) }));
|
|
79
|
+
}
|
package/dist/lumen-tabs.d.ts
CHANGED
|
@@ -12,10 +12,16 @@ export interface LumenTab<V extends string = string> {
|
|
|
12
12
|
/** Left out of the row (a tab this page does not have). */
|
|
13
13
|
hidden?: boolean;
|
|
14
14
|
}
|
|
15
|
-
export declare function LumenTabs<V extends string>({ tabs, value, onChange, actions, }: {
|
|
15
|
+
export declare function LumenTabs<V extends string>({ tabs, value, onChange, actions, variant, }: {
|
|
16
16
|
tabs: LumenTab<V>[];
|
|
17
17
|
value: V | undefined;
|
|
18
18
|
onChange: (value: V) => void;
|
|
19
19
|
/** What only the open tab uses, at the row's right end. */
|
|
20
20
|
actions?: ReactNode;
|
|
21
|
+
/**
|
|
22
|
+
* `row` (default): icon and name side by side, the rest in More. `stacked`: a phone panel's bar,
|
|
23
|
+
* the tabs sharing the row evenly, each its icon over a short name. `icons`: icons only, each
|
|
24
|
+
* named in its tooltip, for a small floating panel.
|
|
25
|
+
*/
|
|
26
|
+
variant?: 'row' | 'stacked' | 'icons';
|
|
21
27
|
}): import("react").JSX.Element;
|
package/dist/lumen-tabs.js
CHANGED
|
@@ -10,12 +10,26 @@ import { useLayoutEffect, useRef, useState } from 'react';
|
|
|
10
10
|
import { cn } from './lib/utils.js';
|
|
11
11
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger, } from './ui/dropdown-menu.js';
|
|
12
12
|
import { Tabs, TabsList, TabsTrigger } from './ui/tabs.js';
|
|
13
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
|
|
13
14
|
/** The row's gap between tabs (`gap-6` in TabsList). */
|
|
14
15
|
const GAP = 24;
|
|
15
16
|
/** The More button, drawn like a tab. */
|
|
16
17
|
const MORE = 'relative inline-flex shrink-0 items-center gap-1.5 pb-2 text-sm leading-4 font-medium whitespace-nowrap outline-none after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:rounded-full hover:text-foreground focus-visible:rounded-sm focus-visible:ring-3 focus-visible:ring-ring/50 [&_svg]:size-4 [&_svg]:shrink-0';
|
|
17
|
-
export function LumenTabs({ tabs, value, onChange, actions, }) {
|
|
18
|
+
export function LumenTabs({ tabs, value, onChange, actions, variant = 'row', }) {
|
|
18
19
|
const shown = tabs.filter((t) => !t.hidden);
|
|
20
|
+
if (variant !== 'row')
|
|
21
|
+
return (_jsx(EvenTabs, { tabs: shown, value: value, onChange: onChange, actions: actions, icons: variant === 'icons' }));
|
|
22
|
+
return _jsx(RowTabs, { tabs: shown, value: value, onChange: onChange, actions: actions });
|
|
23
|
+
}
|
|
24
|
+
/** A bar of tabs that share the row evenly: icon over name, or icons alone with a tooltip. */
|
|
25
|
+
function EvenTabs({ tabs, value, onChange, actions, icons, }) {
|
|
26
|
+
return (_jsxs("div", { className: "flex min-w-0 items-center gap-1", children: [_jsx(Tabs, { value: value, onValueChange: (v) => onChange(v), className: "min-w-0 flex-1", children: _jsx(TabsList, { className: "w-full gap-0", children: tabs.map((t) => {
|
|
27
|
+
const name = typeof t.label === 'string' ? t.label : undefined;
|
|
28
|
+
const trigger = (_jsxs(TabsTrigger, { value: t.value, "aria-label": icons ? name : undefined, className: cn('relative min-h-11 min-w-0 flex-1 justify-center py-1 after:inset-x-3', !icons && 'flex-col gap-0.5 text-xs leading-3 [&_svg]:size-5'), children: [_jsx(t.icon, {}), !icons && _jsx("span", { className: "max-w-full truncate", children: t.label }), t.after && (_jsx("span", { className: cn('text-xs', !icons ? 'absolute top-0.5 left-1/2 ml-2' : ''), children: t.after }))] }, t.value));
|
|
29
|
+
return icons ? (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: trigger }), _jsx(TooltipContent, { children: t.label })] }, t.value)) : (trigger);
|
|
30
|
+
}) }) }), actions] }));
|
|
31
|
+
}
|
|
32
|
+
function RowTabs({ tabs: shown, value, onChange, actions, }) {
|
|
19
33
|
const box = useRef(null);
|
|
20
34
|
const ruler = useRef(null);
|
|
21
35
|
// How many tabs fit before the More menu; all of them until measured.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.311",
|
|
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.311",
|
|
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-rnd": "10.5.3",
|
|
78
79
|
"react-zoom-pan-pinch": "4.2.0",
|
|
79
80
|
"recharts": "^3.10.1",
|
|
80
81
|
"remark-gfm": "4.0.1",
|