@columenlabs/lumen-ui 0.1.112 → 0.1.114
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/color-dot.js +7 -1
- package/dist/hooks/use-hover-open.d.ts +23 -0
- package/dist/hooks/use-hover-open.js +32 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lumen-links.js +1 -1
- package/dist/lumen-view.js +46 -36
- package/dist/status-health.d.ts +1 -1
- package/dist/status-health.js +3 -19
- package/dist/status-messages.d.ts +25 -0
- package/dist/status-messages.js +72 -0
- package/dist/theme.css +4 -0
- package/dist/ui/alert-dialog.js +2 -1
- package/dist/ui/badge.js +4 -4
- package/dist/ui/dialog.js +2 -1
- package/dist/ui/hover-card.d.ts +2 -2
- package/dist/ui/hover-card.js +7 -4
- package/dist/ui/overlay-guard.d.ts +2 -0
- package/dist/ui/overlay-guard.js +14 -0
- package/dist/ui/sheet.js +2 -1
- package/package.json +2 -2
package/dist/color-dot.js
CHANGED
|
@@ -33,5 +33,11 @@ export function tintedIcon(icon, color) {
|
|
|
33
33
|
}
|
|
34
34
|
/** A value as a chip in its colour (a label): a tint behind, the colour as text, its icon first. */
|
|
35
35
|
export function ColorChip({ color, icon: Icon, children, }) {
|
|
36
|
-
return (_jsxs("span", { className: "inline-flex h-5 shrink-0 items-center gap-1 rounded-sm bg-secondary px-1.5 text-xs font-medium whitespace-nowrap text-secondary-foreground", style: color
|
|
36
|
+
return (_jsxs("span", { className: "inline-flex h-5 shrink-0 items-center gap-1 rounded-sm bg-secondary px-1.5 text-xs font-medium whitespace-nowrap text-secondary-foreground", style: color
|
|
37
|
+
? {
|
|
38
|
+
background: `color-mix(in oklab, ${color} 18%, transparent)`,
|
|
39
|
+
// The colour pulled toward the text colour, so a light hue (yellow, sky) still reads.
|
|
40
|
+
color: `color-mix(in oklab, ${color} 55%, var(--foreground))`,
|
|
41
|
+
}
|
|
42
|
+
: undefined, children: [Icon && _jsx(Icon, { className: "size-3 shrink-0" }), children] }));
|
|
37
43
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Focus stays on the trigger: moving it into the pop-up, or back on close, would dismiss a
|
|
2
|
+
* neighbour's pop-up the mouse just opened (Radix closes a pop-up on focus outside). */
|
|
3
|
+
declare const keepFocus: (e: Event) => void;
|
|
4
|
+
/**
|
|
5
|
+
* Open state for a status bar pop-up that opens on hover with a mouse and on tap with a finger or
|
|
6
|
+
* keyboard (a `Popover`'s own trigger). Spread `hover` on the trigger and `content` on the
|
|
7
|
+
* `PopoverContent`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useHoverOpen(): {
|
|
10
|
+
open: boolean;
|
|
11
|
+
setOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
12
|
+
hover: {
|
|
13
|
+
onPointerEnter: (e: React.PointerEvent) => void;
|
|
14
|
+
onPointerLeave: (e: React.PointerEvent) => void;
|
|
15
|
+
};
|
|
16
|
+
content: {
|
|
17
|
+
onPointerEnter: (e: React.PointerEvent) => void;
|
|
18
|
+
onPointerLeave: (e: React.PointerEvent) => void;
|
|
19
|
+
onOpenAutoFocus: typeof keepFocus;
|
|
20
|
+
onCloseAutoFocus: typeof keepFocus;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
/** How long a pop-up stays after the mouse leaves, so it can travel from trigger to pop-up. */
|
|
3
|
+
const HOVER_CLOSE_MS = 150;
|
|
4
|
+
/** Focus stays on the trigger: moving it into the pop-up, or back on close, would dismiss a
|
|
5
|
+
* neighbour's pop-up the mouse just opened (Radix closes a pop-up on focus outside). */
|
|
6
|
+
const keepFocus = (e) => e.preventDefault();
|
|
7
|
+
/**
|
|
8
|
+
* Open state for a status bar pop-up that opens on hover with a mouse and on tap with a finger or
|
|
9
|
+
* keyboard (a `Popover`'s own trigger). Spread `hover` on the trigger and `content` on the
|
|
10
|
+
* `PopoverContent`.
|
|
11
|
+
*/
|
|
12
|
+
export function useHoverOpen() {
|
|
13
|
+
const [open, setOpen] = useState(false);
|
|
14
|
+
const timer = useRef(undefined);
|
|
15
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
16
|
+
const hover = (on) => (e) => {
|
|
17
|
+
if (e.pointerType !== 'mouse')
|
|
18
|
+
return;
|
|
19
|
+
clearTimeout(timer.current);
|
|
20
|
+
if (on)
|
|
21
|
+
setOpen(true);
|
|
22
|
+
else
|
|
23
|
+
timer.current = setTimeout(() => setOpen(false), HOVER_CLOSE_MS);
|
|
24
|
+
};
|
|
25
|
+
const handlers = { onPointerEnter: hover(true), onPointerLeave: hover(false) };
|
|
26
|
+
return {
|
|
27
|
+
open,
|
|
28
|
+
setOpen,
|
|
29
|
+
hover: handlers,
|
|
30
|
+
content: { ...handlers, onOpenAutoFocus: keepFocus, onCloseAutoFocus: keepFocus },
|
|
31
|
+
};
|
|
32
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -78,6 +78,7 @@ export { LumenShell, type LumenShellProps, type ShellUser, StatusItem, firstName
|
|
|
78
78
|
export { apiJson } from './api-fetch.js';
|
|
79
79
|
export { cn } from './lib/utils.js';
|
|
80
80
|
export { type HealthProbe, StatusHealth } from './status-health.js';
|
|
81
|
+
export { type StatusMessage, type StatusMessageTone, StatusMessages, groupStatusMessages, } from './status-messages.js';
|
|
81
82
|
export { TimeZoneSelect } from './status.js';
|
|
82
83
|
export { type AccountFacts, type JoinReaders, JoinsProvider, SourceStatusItems, YOURS_COLOR, editedCells, editedKey, frameColor, useFrameEdits, useJoins, usedJoinKeys, } from './joins.js';
|
|
83
84
|
export { OriginLegend, originsOf } from './origin-legend.js';
|
package/dist/index.js
CHANGED
|
@@ -85,6 +85,7 @@ export { LumenShell, StatusItem, firstName, initials, maskAmount, useAmountsHidd
|
|
|
85
85
|
export { apiJson } from './api-fetch.js';
|
|
86
86
|
export { cn } from './lib/utils.js';
|
|
87
87
|
export { StatusHealth } from './status-health.js';
|
|
88
|
+
export { StatusMessages, groupStatusMessages, } from './status-messages.js';
|
|
88
89
|
export { TimeZoneSelect } from './status.js';
|
|
89
90
|
export { JoinsProvider, SourceStatusItems, YOURS_COLOR, editedCells, editedKey, frameColor, useFrameEdits, useJoins, usedJoinKeys, } from './joins.js';
|
|
90
91
|
export { OriginLegend, originsOf } from './origin-legend.js';
|
package/dist/lumen-links.js
CHANGED
|
@@ -172,7 +172,7 @@ export function LinkPicker({ color, onPick, label, className, }) {
|
|
|
172
172
|
onPick(c);
|
|
173
173
|
setOpen(false);
|
|
174
174
|
};
|
|
175
|
-
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-xs", className: cn('size-5 shrink-0', className), "aria-label": `${label} link colour: ${color ? LINK_NAMES[color] : 'None'}`, title: color ? `Linked: ${LINK_NAMES[color]}` : 'Link colour', children: _jsx(LinkDot, { color: color }) }) }), _jsxs(PopoverContent, { align: "start", className: "flex w-auto flex-col gap-2 p-2", children: [_jsx("p", { className: "text-xs text-muted-foreground", children: "Link colour" }), _jsxs("div", { className: "flex items-center gap-1", children: [LINK_COLORS.map((c) => (_jsx(Button, { variant: c === color ? 'secondary' : 'ghost', size: "icon-sm", "aria-label": LINK_NAMES[c], "aria-pressed": c === color, title: LINK_NAMES[c], onClick: () => pick(c), children: _jsx(LinkDot, { color: c, className: "size-3.5" }) }, c))), _jsx(Button, { variant: color === null ? 'secondary' : 'ghost', size: "sm", className: "h-8 px-2 text-xs", "aria-pressed": color === null, onClick: () => pick(null), children: "None" })] })] })] }));
|
|
175
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-xs", className: cn('size-5 shrink-0 max-md:size-6', className), "aria-label": `${label} link colour: ${color ? LINK_NAMES[color] : 'None'}`, title: color ? `Linked: ${LINK_NAMES[color]}` : 'Link colour', children: _jsx(LinkDot, { color: color }) }) }), _jsxs(PopoverContent, { align: "start", className: "flex w-auto flex-col gap-2 p-2", children: [_jsx("p", { className: "text-xs text-muted-foreground", children: "Link colour" }), _jsxs("div", { className: "flex items-center gap-1", children: [LINK_COLORS.map((c) => (_jsx(Button, { variant: c === color ? 'secondary' : 'ghost', size: "icon-sm", "aria-label": LINK_NAMES[c], "aria-pressed": c === color, title: LINK_NAMES[c], onClick: () => pick(c), children: _jsx(LinkDot, { color: c, className: "size-3.5" }) }, c))), _jsx(Button, { variant: color === null ? 'secondary' : 'ghost', size: "sm", className: "h-8 px-2 text-xs", "aria-pressed": color === null, onClick: () => pick(null), children: "None" })] })] })] }));
|
|
176
176
|
}
|
|
177
177
|
/**
|
|
178
178
|
* Row-tap filtering (keyed): in a coloured card a row tap picks the
|
package/dist/lumen-view.js
CHANGED
|
@@ -8,7 +8,7 @@ import { bytesParts, controlOn, isCountAgg, isDollarFormat, viewPaginationSchema
|
|
|
8
8
|
// details drawer; LumenLayout (lumen-layout.tsx) is the page around it. Rows are the ones the app
|
|
9
9
|
// already loaded; everything here runs on the client.
|
|
10
10
|
import { createPaginatedRowModel, createSortedRowModel, flexRender, rowPaginationFeature, rowSortingFeature, sortFns, tableFeatures, useTable, } from '@tanstack/react-table';
|
|
11
|
-
import { ArrowDownIcon, ArrowUpIcon, ChartCandlestick, ChartSpline, ChevronRightIcon, ChevronsDownIcon, ChevronsDownUpIcon, ChevronsUpDownIcon, Columns3CogIcon, CornerDownRightIcon, DownloadIcon, ListCollapseIcon, PlusIcon, SigmaIcon, UserPenIcon, } from 'lucide-react';
|
|
11
|
+
import { ArrowDownIcon, ArrowDownWideNarrowIcon, ArrowUpIcon, ArrowUpNarrowWideIcon, ChartCandlestick, ChartSpline, ChevronRightIcon, ChevronsDownIcon, ChevronsDownUpIcon, ChevronsUpDownIcon, Columns3CogIcon, CornerDownRightIcon, DownloadIcon, ListCollapseIcon, PlusIcon, SigmaIcon, UserPenIcon, } from 'lucide-react';
|
|
12
12
|
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
13
13
|
import { ColorChip, ColorDot } from './color-dot.js';
|
|
14
14
|
import { DrawerSection } from './drawer-section.js';
|
|
@@ -558,6 +558,16 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
558
558
|
},
|
|
559
559
|
autoResetPageIndex: false,
|
|
560
560
|
});
|
|
561
|
+
// A phone's cards hide the header row, so a Sort by picker and a direction toggle stand in for
|
|
562
|
+
// the header clicks (numbers and dates start high to low, text A to Z, as a header click does).
|
|
563
|
+
const sortBy = sorting[0];
|
|
564
|
+
const sortable = shown.filter((f) => !serverSorts || serverSorts.includes(f.key));
|
|
565
|
+
const cardSort = sortable.length > 1 && (_jsxs("div", { "data-slot": "card-sort", className: "hidden items-center justify-end gap-1 px-(--cell-x) py-1 max-sm:flex", children: [_jsx(LumenSelector, { label: "Sort by", size: "sm", placeholder: "Sort by", options: sortable.map((f) => ({ value: f.key, label: f.label, icon: f.icon })), value: sortBy?.id ?? null, onChange: (id) => table.setSorting([
|
|
566
|
+
{
|
|
567
|
+
id,
|
|
568
|
+
desc: sortBy?.id === id ? sortBy.desc : sortable.some((f) => f.key === id && short(f)),
|
|
569
|
+
},
|
|
570
|
+
]) }), sortBy && (_jsx(IconButton, { icon: sortBy.desc ? ArrowDownWideNarrowIcon : ArrowUpNarrowWideIcon, label: sortBy.desc ? 'High to low' : 'Low to high', onClick: () => table.setSorting([{ id: sortBy.id, desc: !sortBy.desc }]) }))] }));
|
|
561
571
|
// Phone: a long press on a header opens the Columns picker as a sheet.
|
|
562
572
|
const [fieldOpen, setFieldOpen] = useState(false);
|
|
563
573
|
const press = useRef(null);
|
|
@@ -731,41 +741,41 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
731
741
|
};
|
|
732
742
|
const box = useRef(null);
|
|
733
743
|
useFloatingHead(box, !inView);
|
|
734
|
-
return (
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
744
|
+
return (_jsxs("div", { ref: box, className: cn(inView ? IN_VIEW : 'overflow-x-auto overflow-y-hidden [--pin-top:0px]', fit && FIT), children: [cardSort, _jsx(UnitColumns.Provider, { value: markUnits, children: _jsxs(Table, { "data-cards": "", children: [_jsx(TableHeader, { children: table.getHeaderGroups().map((hg) => (_jsxs(TableRow, { children: [selection && (_jsx(TableHead, { className: cn('w-8 pr-0', PINNED_HEAD), children: _jsx(Checkbox, { checked: allOn ? true : someOn ? 'indeterminate' : false, onCheckedChange: (on) => toggle(allKeys, on === true), "aria-label": "Select every row" }) })), hg.headers.map((h, i) => (_jsxs(TableHead, { ...col(shown[i], PINNED_HEAD), style: ruleOf(colorOf(domain, shown[i])), onPointerDown: pressStart, onPointerUp: pressEnd, onPointerLeave: pressEnd, onPointerCancel: pressEnd, onContextMenu: (e) => {
|
|
745
|
+
if (press.current)
|
|
746
|
+
e.preventDefault();
|
|
747
|
+
}, "aria-sort": h.column.getIsSorted() === 'asc'
|
|
748
|
+
? 'ascending'
|
|
749
|
+
: h.column.getIsSorted() === 'desc'
|
|
750
|
+
? 'descending'
|
|
751
|
+
: undefined, children: [i === 0 && groups && folds && !serverGroups && (_jsxs(DropdownMenu, { modal: false, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(IconButton, { icon: anyClosed || anyFoldClosed ? ChevronsUpDownIcon : ChevronsDownUpIcon, size: "icon-xs", className: "-ml-2 mr-1 align-middle", label: "Expand or collapse rows" }) }), _jsxs(DropdownMenuContent, { align: "start", className: "w-52", children: [_jsxs(DropdownMenuItem, { onSelect: () => {
|
|
752
|
+
setAll(true);
|
|
753
|
+
setAllParts(true);
|
|
754
|
+
}, children: [_jsx(ChevronsUpDownIcon, {}), " Expand all"] }), _jsxs(DropdownMenuItem, { onSelect: () => {
|
|
755
|
+
setAll(true);
|
|
756
|
+
setAllParts(false);
|
|
757
|
+
}, children: [_jsx(ListCollapseIcon, {}), " Close every ", subgroup?.label.toLowerCase()] }), _jsxs(DropdownMenuItem, { onSelect: () => setAll(false), children: [_jsx(ChevronsDownUpIcon, {}), " Collapse all"] })] })] })), i === 0 &&
|
|
758
|
+
groups &&
|
|
759
|
+
!(folds && !serverGroups) &&
|
|
760
|
+
(!serverGroups || serverGroups.onToggleAll) && (_jsx(IconButton, { icon: anyClosed ? ChevronsUpDownIcon : ChevronsDownUpIcon, size: "icon-xs", className: "-ml-2 mr-1 align-middle", label: anyClosed ? 'Expand all groups' : 'Collapse all groups', onClick: () => setAll(anyClosed) })), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "xs", className: cn('font-medium', !(i === 0 && groups) && '-mx-2'), style: { color: colorOf(domain, shown[i]) ?? undefined }, onClick: h.column.getCanSort() ? h.column.getToggleSortingHandler() : undefined, children: [shown[i] ? _jsx(FreshnessMark, { domain: domain, field: shown[i] }) : null, flexRender(h.column.columnDef.header, h.getContext()), _jsx(SortIcon, { dir: h.column.getIsSorted() })] }) }), shown[i] && (domain.groups?.length || freshnessOf(domain, shown[i])) ? (_jsxs(TooltipContent, { children: [domain.groups?.length ? _jsx("div", { children: pathOf(domain, shown[i]) }) : null, freshnessLine(domain, shown[i]) ? (_jsx("div", { children: freshnessLine(domain, shown[i]) })) : null] })) : null] })] }, h.id))), add && (_jsx(TableHead, { className: cn('w-8 pl-1', PINNED_HEAD), children: _jsx(AddColumnMenu, { add: add, fieldOpen: fieldOpen, onFieldOpen: setFieldOpen }) }))] }, hg.id))) }), _jsxs(TableBody, { children: [!rows.length && !serverGroups?.list?.length && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: span, className: "py-6 text-center text-muted-foreground", children: "No rows match." }) })), groups && groupBy
|
|
761
|
+
? groups.map(([key, members], gi) => {
|
|
762
|
+
const open = isOpen(key, gi);
|
|
763
|
+
return (_jsxs(Fragment, { children: [_jsxs(TableRow, { className: ROLLUP, "data-group": key, "data-rollup": "", children: [selection && (_jsx(TableCell, { "data-card": "select", className: cn('w-8 pr-0', PINNED_GROUP), children: _jsx(Checkbox, { checked: members.every((m) => selection.keys.has(keyOf(m.original)))
|
|
764
|
+
? true
|
|
765
|
+
: members.some((m) => selection.keys.has(keyOf(m.original)))
|
|
766
|
+
? 'indeterminate'
|
|
767
|
+
: false, onCheckedChange: (on) => toggle(members.map((m) => keyOf(m.original)), on === true), "aria-label": `Select every row of ${key}` }) })), shown.slice(lead - 1).map((f, i) => i === 0 ? (_jsx(TableCell, { colSpan: leadSpan, "data-card": "lead", className: PINNED_GROUP, children: _jsxs("span", { className: "flex max-w-(--lead-col-max) min-w-0 items-center whitespace-nowrap", children: [_jsxs(Button, { variant: "ghost", size: "xs", title: key, className: "-mx-2 min-w-0 hover:bg-transparent aria-expanded:bg-transparent", "aria-expanded": open, "aria-label": `${open ? 'Collapse' : 'Expand'} ${key}`, onClick: () => serverGroups
|
|
768
|
+
? serverGroups.onToggle(key)
|
|
769
|
+
: setFlipped((c) => {
|
|
770
|
+
const n = new Set(c);
|
|
771
|
+
n.has(key) ? n.delete(key) : n.add(key);
|
|
772
|
+
return n;
|
|
773
|
+
}), children: [_jsx(ChevronRightIcon, { className: cn('transition-transform', open && 'rotate-90') }), _jsx(GroupMark, { field: groupBy, value: key }), _jsx("span", { className: "truncate", children: key }), _jsx("span", { className: "num shrink-0 font-normal text-muted-foreground", children: serverGroups
|
|
774
|
+
? (served.get(key)?.n?.toLocaleString() ?? '')
|
|
775
|
+
: members.length })] }), groupActions?.(key, members.map((m) => m.original))] }) }, f.key)) : (_jsx(TableCell, { ...col(f, PINNED_GROUP), children: !serverGroups &&
|
|
776
|
+
rollCell(f, members.map((m) => m.original), 'group') }, f.key))), add && _jsx(TableCell, { className: PINNED_GROUP })] }), open && (serverGroups ? servedRows(members, key) : drawn(members, key))] }, key));
|
|
777
|
+
})
|
|
778
|
+
: drawn(sorted, ''), serverGroups?.more && !serverGroups.pages && (_jsx(TableRow, { "data-more": true, children: _jsx(TableCell, { colSpan: span, className: "py-1", children: _jsxs(Button, { variant: "ghost", size: "xs", onClick: serverGroups.more, children: [_jsx(ChevronsDownIcon, {}), "More groups"] }) }) })), hasTotal && rows.length > 0 && (_jsxs(TableRow, { className: cn(ROLLUP, 'border-t-2'), "data-total": true, "data-rollup": "", children: [selection && _jsx(TableCell, {}), rollCells(rows).map((cell, i) => i === 0 && !(shown[0] && rollsUp(shown[0])) ? (_jsx(TableCell, { "data-card": "lead", children: "Total" }, "total")) : (cell)), add && _jsx(TableCell, {})] }))] })] }) })] }));
|
|
769
779
|
}
|
|
770
780
|
/** One value rolled up into a rows × columns grid, with totals where the roll-up adds. */
|
|
771
781
|
function PivotTable({ domain, rows, pivot, }) {
|
package/dist/status-health.d.ts
CHANGED
package/dist/status-health.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
|
|
3
|
-
// pop-up"): one chip with the worst state of every service the app checks, opening a list of each
|
|
4
|
-
// service and its latency, on hover with a mouse and on tap with a finger or keyboard.
|
|
5
|
-
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useHoverOpen } from './hooks/use-hover-open.js';
|
|
6
3
|
import { cn } from './lib/utils.js';
|
|
7
4
|
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover.js';
|
|
8
|
-
/** How long the list stays after the mouse leaves, so it can travel from chip to list. */
|
|
9
|
-
const HOVER_CLOSE_MS = 150;
|
|
10
5
|
/** Keeps the list off the window's edge on a narrow screen. */
|
|
11
6
|
const COLLISION_PAD = 8;
|
|
12
7
|
const dot = (ok) => cn('size-2 shrink-0 rounded-full', ok === null ? 'bg-muted-foreground/40' : ok ? 'bg-gain' : 'bg-destructive');
|
|
@@ -22,20 +17,9 @@ function summary(probes) {
|
|
|
22
17
|
return { ok: true, text: 'All up' };
|
|
23
18
|
}
|
|
24
19
|
export function StatusHealth({ probes, checked, error, }) {
|
|
25
|
-
const
|
|
26
|
-
const timer = useRef(undefined);
|
|
27
|
-
useEffect(() => () => clearTimeout(timer.current), []);
|
|
28
|
-
const hover = (on) => (e) => {
|
|
29
|
-
if (e.pointerType !== 'mouse')
|
|
30
|
-
return;
|
|
31
|
-
clearTimeout(timer.current);
|
|
32
|
-
if (on)
|
|
33
|
-
setOpen(true);
|
|
34
|
-
else
|
|
35
|
-
timer.current = setTimeout(() => setOpen(false), HOVER_CLOSE_MS);
|
|
36
|
-
};
|
|
20
|
+
const { open, setOpen, hover, content: pop } = useHoverOpen();
|
|
37
21
|
if (!probes.length && !error)
|
|
38
22
|
return null;
|
|
39
23
|
const s = error ? { ok: false, text: 'Check failed' } : summary(probes);
|
|
40
|
-
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsxs(PopoverTrigger, { "aria-label": `Services: ${s.text}`, className: "flex h-6 shrink-0 items-center gap-1.5 rounded-md px-1.5 hover:bg-muted hover:text-foreground",
|
|
24
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsxs(PopoverTrigger, { "aria-label": `Services: ${s.text}`, className: "flex h-6 shrink-0 items-center gap-1.5 rounded-md px-1.5 hover:bg-muted hover:text-foreground", ...hover, children: [_jsx("span", { "aria-hidden": true, className: dot(s.ok) }), "Services", _jsx("span", { className: cn('num', s.ok === false ? 'text-destructive' : 'text-foreground/80'), children: s.text })] }), _jsxs(PopoverContent, { side: "top", align: "end", collisionPadding: COLLISION_PAD, className: "w-auto min-w-56 gap-1.5 text-xs", ...pop, children: [_jsx("ul", { "aria-label": "Services", className: "flex flex-col gap-1", children: probes.map((p) => (_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { "aria-hidden": true, className: dot(p.ok) }), _jsxs("span", { className: "flex-1", children: [p.name, p.note && _jsx("span", { className: "ml-1.5 text-muted-foreground", children: p.note })] }), _jsx("span", { className: cn('num', p.ok === false ? 'text-destructive' : 'text-foreground/80'), children: p.ok === false ? 'down' : p.ms != null ? `${p.ms} ms` : '—' })] }, p.name))) }), error && _jsx("p", { className: "text-destructive", children: error }), checked && (_jsxs("p", { className: "border-t pt-1.5 text-muted-foreground", children: ["Checked ", _jsx("span", { className: "num", children: checked })] }))] })] }));
|
|
41
25
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type StatusMessageTone = 'info' | 'success' | 'warning' | 'error';
|
|
2
|
+
export interface StatusMessage {
|
|
3
|
+
text: string;
|
|
4
|
+
/** Colours the text and its dot: error red, warning amber, success green; info has no dot. */
|
|
5
|
+
tone?: StatusMessageTone;
|
|
6
|
+
/** How many times it was posted, for an app that counts repeats itself; repeats in the list add up too. */
|
|
7
|
+
count?: number;
|
|
8
|
+
}
|
|
9
|
+
interface Group extends Required<StatusMessage> {
|
|
10
|
+
count: number;
|
|
11
|
+
}
|
|
12
|
+
/** One entry per distinct message (same tone, same words), counted, most severe first. */
|
|
13
|
+
export declare function groupStatusMessages(messages: readonly StatusMessage[]): Group[];
|
|
14
|
+
/**
|
|
15
|
+
* The app's messages in the status bar. Each message is one line that ends in an ellipsis past
|
|
16
|
+
* `--status-message-max`, with its full text on hover or tap; a repeated message shows once with
|
|
17
|
+
* `×N`; past `max` messages (one on a phone), a `+N` chip lists the rest.
|
|
18
|
+
*/
|
|
19
|
+
export declare function StatusMessages({ messages, max, className, }: {
|
|
20
|
+
messages: readonly StatusMessage[];
|
|
21
|
+
/** How many messages show at once on a wider screen; the rest go behind the `+N` chip. */
|
|
22
|
+
max?: number;
|
|
23
|
+
className?: string;
|
|
24
|
+
}): import("react").JSX.Element | null;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useHoverOpen } from './hooks/use-hover-open.js';
|
|
3
|
+
import { useIsMobile } from './hooks/use-mobile.js';
|
|
4
|
+
import { cn } from './lib/utils.js';
|
|
5
|
+
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover.js';
|
|
6
|
+
/** Errors first, then warnings, so the message that matters most is the one in view. */
|
|
7
|
+
const RANK = { error: 0, warning: 1, success: 2, info: 3 };
|
|
8
|
+
const TEXT = {
|
|
9
|
+
error: 'text-destructive',
|
|
10
|
+
warning: 'text-warning',
|
|
11
|
+
success: undefined,
|
|
12
|
+
info: undefined,
|
|
13
|
+
};
|
|
14
|
+
const DOT = {
|
|
15
|
+
error: 'bg-destructive',
|
|
16
|
+
warning: 'bg-warning',
|
|
17
|
+
success: 'bg-gain',
|
|
18
|
+
info: undefined,
|
|
19
|
+
};
|
|
20
|
+
/** One entry per distinct message (same tone, same words), counted, most severe first. */
|
|
21
|
+
export function groupStatusMessages(messages) {
|
|
22
|
+
const groups = new Map();
|
|
23
|
+
for (const m of messages) {
|
|
24
|
+
const text = m.text.replace(/\s+/g, ' ').trim();
|
|
25
|
+
if (!text)
|
|
26
|
+
continue;
|
|
27
|
+
const tone = m.tone ?? 'info';
|
|
28
|
+
const key = `${tone}\u0000${text}`;
|
|
29
|
+
const count = Math.max(1, m.count ?? 1);
|
|
30
|
+
const g = groups.get(key);
|
|
31
|
+
if (g)
|
|
32
|
+
g.count += count;
|
|
33
|
+
else
|
|
34
|
+
groups.set(key, { text, tone, count });
|
|
35
|
+
}
|
|
36
|
+
// Map keeps first-seen order; the sort is stable, so equal tones keep it.
|
|
37
|
+
return [...groups.values()].sort((a, b) => RANK[a.tone] - RANK[b.tone]);
|
|
38
|
+
}
|
|
39
|
+
const label = (g) => (g.count > 1 ? `${g.text} (×${g.count})` : g.text);
|
|
40
|
+
function Dot({ tone }) {
|
|
41
|
+
const color = DOT[tone];
|
|
42
|
+
return color ? _jsx("span", { "aria-hidden": true, className: cn('size-2 shrink-0 rounded-full', color) }) : null;
|
|
43
|
+
}
|
|
44
|
+
/** Keeps the pop-up off the window's edge on a narrow screen. */
|
|
45
|
+
const COLLISION_PAD = 8;
|
|
46
|
+
/**
|
|
47
|
+
* A status bar trigger whose pop-up opens on hover with a mouse and on tap with a finger or
|
|
48
|
+
* keyboard, like the services chip, so a phone can read the full text too (a tooltip never opens
|
|
49
|
+
* on touch). Styled on its own classes only: a trigger owns `data-state`.
|
|
50
|
+
*/
|
|
51
|
+
function Pop({ label: name, className, children, content, }) {
|
|
52
|
+
const { open, setOpen, hover, content: pop } = useHoverOpen();
|
|
53
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { "aria-label": name, className: className, ...hover, children: children }), _jsx(PopoverContent, { side: "top", align: "start", collisionPadding: COLLISION_PAD, className: "w-auto max-w-sm gap-1.5 text-xs break-words", ...pop, children: content })] }));
|
|
54
|
+
}
|
|
55
|
+
const TRIGGER = 'flex h-6 items-center gap-1.5 rounded-md px-1.5 hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none';
|
|
56
|
+
function Line({ group }) {
|
|
57
|
+
return (_jsxs(Pop, { className: cn(TRIGGER, 'min-w-0 max-w-(--status-message-max)'), content: _jsx("p", { className: TEXT[group.tone], children: label(group) }), children: [_jsx(Dot, { tone: group.tone }), _jsx("span", { className: cn('min-w-0 truncate', TEXT[group.tone]), children: group.text }), group.count > 1 && (_jsxs("span", { className: "num shrink-0 text-foreground/80", "aria-label": `${group.count} times`, children: ["\u00D7", group.count] }))] }));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The app's messages in the status bar. Each message is one line that ends in an ellipsis past
|
|
61
|
+
* `--status-message-max`, with its full text on hover or tap; a repeated message shows once with
|
|
62
|
+
* `×N`; past `max` messages (one on a phone), a `+N` chip lists the rest.
|
|
63
|
+
*/
|
|
64
|
+
export function StatusMessages({ messages, max = 2, className, }) {
|
|
65
|
+
const phone = useIsMobile();
|
|
66
|
+
const groups = groupStatusMessages(messages);
|
|
67
|
+
if (!groups.length)
|
|
68
|
+
return null;
|
|
69
|
+
const shown = groups.slice(0, phone ? 1 : Math.max(1, max));
|
|
70
|
+
const rest = groups.slice(shown.length);
|
|
71
|
+
return (_jsxs("span", { "aria-live": "polite", className: cn('flex min-w-0 items-center gap-2', className), children: [shown.map((g) => (_jsx(Line, { group: g }, `${g.tone}:${g.text}`))), rest.length > 0 && (_jsxs(Pop, { label: `${rest.length} more messages`, className: cn(TRIGGER, 'num shrink-0 text-foreground/80'), content: _jsx("ul", { className: "flex flex-col gap-1", children: rest.map((g) => (_jsxs("li", { className: "flex items-start gap-1.5", children: [_jsx("span", { className: "flex h-4 items-center", children: _jsx(Dot, { tone: g.tone }) }), _jsx("span", { className: TEXT[g.tone], children: label(g) })] }, `${g.tone}:${g.text}`))) }), children: ["+", rest.length] }))] }));
|
|
72
|
+
}
|
package/dist/theme.css
CHANGED
|
@@ -88,6 +88,9 @@
|
|
|
88
88
|
--shell-assistant-width: 26rem;
|
|
89
89
|
--shell-header-height: 48px;
|
|
90
90
|
--shell-status-height: 32px;
|
|
91
|
+
/* A status bar message ends in an ellipsis here (StatusMessages), so it fits beside the bar's
|
|
92
|
+
other items; the full text opens on hover or tap. A phone shows one message, a wider screen two. */
|
|
93
|
+
--status-message-max: 45vw;
|
|
91
94
|
--tile-width: 8.5rem;
|
|
92
95
|
--shell-gutter: 12px;
|
|
93
96
|
--shell-section-gap: 12px;
|
|
@@ -209,6 +212,7 @@
|
|
|
209
212
|
@media (min-width: 768px) {
|
|
210
213
|
:root {
|
|
211
214
|
--shell-gutter: 16px;
|
|
215
|
+
--status-message-max: min(28rem, 30vw);
|
|
212
216
|
}
|
|
213
217
|
}
|
|
214
218
|
|
package/dist/ui/alert-dialog.js
CHANGED
|
@@ -4,6 +4,7 @@ import { AlertDialog as AlertDialogPrimitive } from 'radix-ui';
|
|
|
4
4
|
import { useOverlayOpen } from '../hooks/use-back-close.js';
|
|
5
5
|
import { usePhoneScreen } from '../hooks/use-keyboard-fit.js';
|
|
6
6
|
import { Button } from './button.js';
|
|
7
|
+
import { escapeToPicker } from './overlay-guard.js';
|
|
7
8
|
function AlertDialog({ open: given, defaultOpen, onOpenChange: changed, ...props }) {
|
|
8
9
|
// Phone back closes it, as its Cancel does (useBackClose).
|
|
9
10
|
const state = useOverlayOpen({ open: given, defaultOpen, onOpenChange: changed });
|
|
@@ -21,7 +22,7 @@ function AlertDialogOverlay({ className, ...props }) {
|
|
|
21
22
|
function AlertDialogContent({ className, size = 'default', ...props }) {
|
|
22
23
|
// On a phone it is a full screen pinned to the top, its footer a bar at the top (usePhoneScreen).
|
|
23
24
|
const style = usePhoneScreen(props.style);
|
|
24
|
-
return (_jsxs(AlertDialogPortal, { children: [_jsx(AlertDialogOverlay, {}), _jsx(AlertDialogPrimitive.Content, { "data-slot": "alert-dialog-content", "data-size": size, className: cn('data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-popover text-popover-foreground ring-foreground/10 gap-4 rounded-xl p-4 ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none max-md:inset-0 max-md:h-full max-md:w-full max-md:max-w-none! max-md:translate-x-0 max-md:translate-y-0 max-md:content-start max-md:overflow-y-auto max-md:rounded-none max-md:pt-[calc(1rem+env(safe-area-inset-top))] max-md:ring-0 max-md:end-fade', className), ...props, style: style })] }));
|
|
25
|
+
return (_jsxs(AlertDialogPortal, { children: [_jsx(AlertDialogOverlay, {}), _jsx(AlertDialogPrimitive.Content, { "data-slot": "alert-dialog-content", "data-size": size, className: cn('data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-popover text-popover-foreground ring-foreground/10 gap-4 rounded-xl p-4 ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none max-md:inset-0 max-md:h-full max-md:w-full max-md:max-w-none! max-md:translate-x-0 max-md:translate-y-0 max-md:content-start max-md:overflow-y-auto max-md:rounded-none max-md:pt-[calc(1rem+env(safe-area-inset-top))] max-md:ring-0 max-md:end-fade', className), ...props, onEscapeKeyDown: escapeToPicker(props.onEscapeKeyDown), style: style })] }));
|
|
25
26
|
}
|
|
26
27
|
function AlertDialogHeader({ className, ...props }) {
|
|
27
28
|
return (_jsx("div", { "data-slot": "alert-dialog-header", className: cn('grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]', className), ...props }));
|
package/dist/ui/badge.js
CHANGED
|
@@ -7,13 +7,13 @@ const badgeVariants = cva('group/badge inline-flex h-5 w-fit shrink-0 items-cent
|
|
|
7
7
|
variant: {
|
|
8
8
|
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
|
|
9
9
|
secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
|
|
10
|
-
destructive: 'bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20',
|
|
10
|
+
destructive: 'bg-destructive/10 text-[color-mix(in_oklab,var(--destructive)_80%,var(--foreground))] focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20',
|
|
11
11
|
outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
|
|
12
12
|
ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
|
|
13
13
|
link: 'text-primary underline-offset-4 hover:underline',
|
|
14
|
-
success: 'bg-success/10 text-success dark:bg-success/20',
|
|
15
|
-
warning: 'bg-warning/10 text-warning dark:bg-warning/20',
|
|
16
|
-
info: 'bg-info/10 text-info dark:bg-info/20',
|
|
14
|
+
success: 'bg-success/10 text-[color-mix(in_oklab,var(--success)_80%,var(--foreground))] dark:bg-success/20',
|
|
15
|
+
warning: 'bg-warning/10 text-[color-mix(in_oklab,var(--warning)_80%,var(--foreground))] dark:bg-warning/20',
|
|
16
|
+
info: 'bg-info/10 text-[color-mix(in_oklab,var(--info)_80%,var(--foreground))] dark:bg-info/20',
|
|
17
17
|
},
|
|
18
18
|
},
|
|
19
19
|
defaultVariants: {
|
package/dist/ui/dialog.js
CHANGED
|
@@ -5,6 +5,7 @@ import { Button } from './button.js';
|
|
|
5
5
|
import { XIcon } from 'lucide-react';
|
|
6
6
|
import { useOverlayOpen } from '../hooks/use-back-close.js';
|
|
7
7
|
import { usePhoneScreen } from '../hooks/use-keyboard-fit.js';
|
|
8
|
+
import { escapeToPicker } from './overlay-guard.js';
|
|
8
9
|
function Dialog({ open: given, defaultOpen, onOpenChange: changed, ...props }) {
|
|
9
10
|
// Phone back closes it (useBackClose).
|
|
10
11
|
const state = useOverlayOpen({ open: given, defaultOpen, onOpenChange: changed });
|
|
@@ -25,7 +26,7 @@ function DialogOverlay({ className, ...props }) {
|
|
|
25
26
|
function DialogContent({ className, children, showCloseButton = true, ...props }) {
|
|
26
27
|
// On a phone it is a full screen pinned to the top, its footer a bar at the top (usePhoneScreen).
|
|
27
28
|
const style = usePhoneScreen(props.style);
|
|
28
|
-
return (_jsxs(DialogPortal, { children: [_jsx(DialogOverlay, {}), _jsxs(DialogPrimitive.Content, { "data-slot": "dialog-content", "data-close": showCloseButton || undefined, className: cn('group/dialog fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-(--dialog-pad) text-sm [--dialog-pad:--spacing(4)] text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm max-md:inset-0 max-md:h-full max-md:max-h-none max-md:w-full max-md:max-w-none max-md:translate-x-0 max-md:translate-y-0 max-md:content-start max-md:overflow-y-auto max-md:rounded-none max-md:pt-[calc(var(--dialog-pad)+env(safe-area-inset-top))] max-md:ring-0 max-md:end-fade max-md:[--end-fade:var(--dialog-pad)] data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95', className), ...props, style: style, children: [children, showCloseButton && (_jsx(DialogPrimitive.Close, { "data-slot": "dialog-close", asChild: true, children: _jsxs(Button, { variant: "ghost", className: "absolute top-2 right-2 max-md:top-[calc(1.25rem+env(safe-area-inset-top))]", size: "icon-sm", children: [_jsx(XIcon, {}), _jsx("span", { className: "sr-only", children: "Close" })] }) }))] })] }));
|
|
29
|
+
return (_jsxs(DialogPortal, { children: [_jsx(DialogOverlay, {}), _jsxs(DialogPrimitive.Content, { "data-slot": "dialog-content", "data-close": showCloseButton || undefined, className: cn('group/dialog fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-(--dialog-pad) text-sm [--dialog-pad:--spacing(4)] text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm max-md:inset-0 max-md:h-full max-md:max-h-none max-md:w-full max-md:max-w-none max-md:translate-x-0 max-md:translate-y-0 max-md:content-start max-md:overflow-y-auto max-md:rounded-none max-md:pt-[calc(var(--dialog-pad)+env(safe-area-inset-top))] max-md:ring-0 max-md:end-fade max-md:[--end-fade:var(--dialog-pad)] data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95', className), ...props, onEscapeKeyDown: escapeToPicker(props.onEscapeKeyDown), style: style, children: [children, showCloseButton && (_jsx(DialogPrimitive.Close, { "data-slot": "dialog-close", asChild: true, children: _jsxs(Button, { variant: "ghost", className: "absolute top-2 right-2 max-md:top-[calc(1.25rem+env(safe-area-inset-top))]", size: "icon-sm", children: [_jsx(XIcon, {}), _jsx("span", { className: "sr-only", children: "Close" })] }) }))] })] }));
|
|
29
30
|
}
|
|
30
31
|
function DialogHeader({ className, ...props }) {
|
|
31
32
|
return (_jsx("div", { "data-slot": "dialog-header",
|
package/dist/ui/hover-card.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HoverCard as HoverCardPrimitive } from 'radix-ui';
|
|
2
2
|
import type * as React from 'react';
|
|
3
3
|
declare function HoverCard({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Root>): React.JSX.Element;
|
|
4
|
-
declare function HoverCardTrigger({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>): React.JSX.Element;
|
|
5
|
-
declare function HoverCardContent({ className, align, sideOffset, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Content>): React.JSX.Element;
|
|
4
|
+
declare function HoverCardTrigger({ className, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>): React.JSX.Element;
|
|
5
|
+
declare function HoverCardContent({ className, align, sideOffset, collisionPadding, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Content>): React.JSX.Element;
|
|
6
6
|
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
package/dist/ui/hover-card.js
CHANGED
|
@@ -4,10 +4,13 @@ import { cn } from '../lib/utils.js';
|
|
|
4
4
|
function HoverCard({ ...props }) {
|
|
5
5
|
return _jsx(HoverCardPrimitive.Root, { "data-slot": "hover-card", ...props });
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// Keyboard users reach a hover card by Tab, so its trigger shows the focus ring like a button.
|
|
8
|
+
function HoverCardTrigger({ className, ...props }) {
|
|
9
|
+
return (_jsx(HoverCardPrimitive.Trigger, { "data-slot": "hover-card-trigger", className: cn('rounded-sm outline-none focus-visible:ring-3 focus-visible:ring-ring/50', className), ...props }));
|
|
9
10
|
}
|
|
10
|
-
function HoverCardContent({ className, align = 'center', sideOffset = 4, ...props }) {
|
|
11
|
-
return (_jsx(HoverCardPrimitive.Portal, { "data-slot": "hover-card-portal", children: _jsx(HoverCardPrimitive.Content, { "data-slot": "hover-card-content", align: align, sideOffset: sideOffset,
|
|
11
|
+
function HoverCardContent({ className, align = 'center', sideOffset = 4, collisionPadding = 8, ...props }) {
|
|
12
|
+
return (_jsx(HoverCardPrimitive.Portal, { "data-slot": "hover-card-portal", children: _jsx(HoverCardPrimitive.Content, { "data-slot": "hover-card-content", align: align, sideOffset: sideOffset,
|
|
13
|
+
// Never flush against (or past) the screen's edge on a phone.
|
|
14
|
+
collisionPadding: collisionPadding, className: cn('bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 z-[70] w-72 origin-(--radix-hover-card-content-transform-origin) rounded-lg p-3 text-sm shadow-md ring-1 outline-hidden duration-100', className), ...props }) }));
|
|
12
15
|
}
|
|
13
16
|
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Esc closes the topmost thing only. A Radix overlay (Dialog, Sheet, AlertDialog) hears Esc on the
|
|
2
|
+
// document before a base-ui picker open inside it (LumenSelector, Combobox), which isn't one of
|
|
3
|
+
// Radix's layers, so one Esc closed both. While such a picker is open the overlay leaves Esc to it.
|
|
4
|
+
const OPEN_PICKER = '[data-slot="lumen-selector"][data-open], [data-slot="combobox-content"][data-open]';
|
|
5
|
+
/** The overlay's `onEscapeKeyDown`: ignores Esc while a picker is open, else runs the caller's. */
|
|
6
|
+
export function escapeToPicker(own) {
|
|
7
|
+
return (e) => {
|
|
8
|
+
if (document.querySelector(OPEN_PICKER)) {
|
|
9
|
+
e.preventDefault();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
own?.(e);
|
|
13
|
+
};
|
|
14
|
+
}
|
package/dist/ui/sheet.js
CHANGED
|
@@ -7,6 +7,7 @@ import * as React from 'react';
|
|
|
7
7
|
import { useOverlayOpen } from '../hooks/use-back-close.js';
|
|
8
8
|
import { useKeyboardFit } from '../hooks/use-keyboard-fit.js';
|
|
9
9
|
import { useIsMobile } from '../hooks/use-mobile.js';
|
|
10
|
+
import { escapeToPicker } from './overlay-guard.js';
|
|
10
11
|
/** Closes the sheet from inside it (the grab handle's swipe). */
|
|
11
12
|
const SheetCloseContext = React.createContext(() => undefined);
|
|
12
13
|
function Sheet({ open: given, defaultOpen, onOpenChange: changed, ...props }) {
|
|
@@ -101,7 +102,7 @@ function SheetContent({ className, children, side = 'right', showCloseButton = t
|
|
|
101
102
|
return (_jsxs(SheetPortal, { children: [_jsx(SheetOverlay, {}), _jsxs(SheetPrimitive.Content, { "data-slot": "sheet-content", "data-side": side, "data-card": card || undefined, "data-close": closable || undefined, className: cn('group/sheet bg-popover text-popover-foreground fixed z-50 flex flex-col gap-4 bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-(--assistant-docked,0px) data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10 data-[side=left]:pt-[env(safe-area-inset-top)] data-[side=right]:pt-[env(safe-area-inset-top)]', className,
|
|
102
103
|
// After the caller's classes: a phone's bottom sheet always keeps its shape and room.
|
|
103
104
|
card &&
|
|
104
|
-
'max-h-[calc(100dvh-env(safe-area-inset-top)-2.75rem)] rounded-t-2xl border-t-0 pt-5'), ...props, style: style,
|
|
105
|
+
'max-h-[calc(100dvh-env(safe-area-inset-top)-2.75rem)] rounded-t-2xl border-t-0 pt-5'), ...props, onEscapeKeyDown: escapeToPicker(props.onEscapeKeyDown), style: style,
|
|
105
106
|
// A click or focus in the docked the assistant panel leaves the sheet open, so the person can ask
|
|
106
107
|
// about what it shows (LumenShell).
|
|
107
108
|
onInteractOutside: (e) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.114",
|
|
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.114",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|