@columenlabs/lumen-ui 0.1.121 → 0.1.122
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/lumen-tabs.d.ts +1 -1
- package/dist/lumen-tabs.js +60 -4
- package/dist/scroll-fade.js +3 -2
- package/package.json +2 -2
package/dist/lumen-tabs.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
2
|
import type { NavIcon } from './nav.js';
|
|
3
3
|
/**
|
|
4
4
|
* One tab: its icon, its name and a count or mark after it. Every tab has an icon; a tab the person edits takes theirs through `iconOf` and LumenTabDrawer.
|
package/dist/lumen-tabs.js
CHANGED
|
@@ -1,8 +1,64 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// LumenTabs: a page's
|
|
3
|
+
// tabs, each tab its own LumenLayout. The tabs sit on the left of one row and what only the open tab
|
|
4
|
+
// uses (its inputs, its Run or Cancel) at the right end, so the page header keeps only what acts on
|
|
5
|
+
// the whole page. Tabs and actions share one row on a phone too ("it splits into 2 rows"); only
|
|
6
|
+
// actions so wide that the tabs would get under 12rem wrap to a line under them. Tabs that do not
|
|
7
|
+
// fit what the actions leave fold into a More menu at the row's end, never a cut-off tab.
|
|
8
|
+
import { ChevronDownIcon } from 'lucide-react';
|
|
9
|
+
import { useLayoutEffect, useRef, useState } from 'react';
|
|
10
|
+
import { cn } from './lib/utils.js';
|
|
11
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger, } from './ui/dropdown-menu.js';
|
|
3
12
|
import { Tabs, TabsList, TabsTrigger } from './ui/tabs.js';
|
|
13
|
+
/** The row's gap between tabs (`gap-6` in TabsList). */
|
|
14
|
+
const GAP = 24;
|
|
15
|
+
/** The More button, drawn like a tab. */
|
|
16
|
+
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';
|
|
4
17
|
export function LumenTabs({ tabs, value, onChange, actions, }) {
|
|
5
18
|
const shown = tabs.filter((t) => !t.hidden);
|
|
6
|
-
const
|
|
7
|
-
|
|
19
|
+
const box = useRef(null);
|
|
20
|
+
const ruler = useRef(null);
|
|
21
|
+
// How many tabs fit before the More menu; all of them until measured.
|
|
22
|
+
const [fit, setFit] = useState(shown.length);
|
|
23
|
+
// The More button names the open tab when it is folded, so its width moves with the open tab.
|
|
24
|
+
const sig = `${shown.map((t) => t.value).join('|')}#${value}`;
|
|
25
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: re-measure when the tab set changes
|
|
26
|
+
useLayoutEffect(() => {
|
|
27
|
+
const el = box.current;
|
|
28
|
+
const r = ruler.current;
|
|
29
|
+
if (!el || !r)
|
|
30
|
+
return;
|
|
31
|
+
const measure = () => {
|
|
32
|
+
const widths = [...r.children].map((c) => c.offsetWidth);
|
|
33
|
+
const more = widths.pop() ?? 0;
|
|
34
|
+
// A few pixels spare for rounding, so the row never needs to scroll.
|
|
35
|
+
const room = el.clientWidth - 4;
|
|
36
|
+
const all = widths.reduce((a, w, i) => a + w + (i ? GAP : 0), 0);
|
|
37
|
+
if (all <= room)
|
|
38
|
+
return setFit(widths.length);
|
|
39
|
+
let used = more;
|
|
40
|
+
let n = 0;
|
|
41
|
+
for (const w of widths) {
|
|
42
|
+
if (used + GAP + w > room)
|
|
43
|
+
break;
|
|
44
|
+
used += GAP + w;
|
|
45
|
+
n++;
|
|
46
|
+
}
|
|
47
|
+
setFit(Math.max(1, n));
|
|
48
|
+
};
|
|
49
|
+
measure();
|
|
50
|
+
// The row's room, and the tabs' own widths (a count that arrives after the first paint).
|
|
51
|
+
const seen = new ResizeObserver(measure);
|
|
52
|
+
seen.observe(el);
|
|
53
|
+
seen.observe(r);
|
|
54
|
+
return () => seen.disconnect();
|
|
55
|
+
}, [sig]);
|
|
56
|
+
const inRow = shown.slice(0, fit);
|
|
57
|
+
const folded = shown.slice(fit);
|
|
58
|
+
const openFolded = folded.find((t) => t.value === value);
|
|
59
|
+
const trigger = (t) => (_jsxs(_Fragment, { children: [_jsx(t.icon, {}), t.label, t.after] }));
|
|
60
|
+
return (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs("div", { ref: box, className: "relative min-w-48 max-w-full flex-1", children: [_jsx(Tabs, { "aria-hidden": true, className: "pointer-events-none invisible absolute h-0 overflow-hidden", children: _jsxs(TabsList, { ref: ruler, className: "w-max", children: [shown.map((t) => (_jsx(TabsTrigger, { value: t.value, tabIndex: -1, children: trigger(t) }, t.value))), _jsxs("span", { "data-slot": "tabs-trigger", className: MORE, children: [openFolded ? trigger(openFolded) : 'More', _jsx(ChevronDownIcon, {})] })] }) }), shown.length ? (_jsx(Tabs, { value: value, onValueChange: (v) => onChange(v), children: _jsxs(TabsList, { className: "max-w-full overflow-x-hidden", children: [inRow.map((t) => (_jsx(TabsTrigger, { value: t.value, children: trigger(t) }, t.value))), folded.length > 0 && (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { "data-slot": "tabs-trigger", "data-state": openFolded ? 'active' : 'inactive', className: cn(MORE, openFolded &&
|
|
61
|
+
'text-foreground after:bg-primary [&_svg:first-child]:text-primary'), children: [openFolded ? trigger(openFolded) : 'More', _jsx(ChevronDownIcon, { className: "text-muted-foreground" })] }), _jsx(DropdownMenuContent, { align: "end", className: "w-auto min-w-44",
|
|
62
|
+
// A pick from the menu moves the tab; no keyboard ring back on the button.
|
|
63
|
+
onCloseAutoFocus: (e) => e.preventDefault(), children: _jsx(DropdownMenuRadioGroup, { value: value, onValueChange: (v) => onChange(v), children: folded.map((t) => (_jsx(DropdownMenuRadioItem, { value: t.value, children: trigger(t) }, t.value))) }) })] }))] }) })) : null] }), actions ? _jsx("div", { className: "ml-auto flex shrink-0 items-center gap-0.5", children: actions }) : null] }));
|
|
8
64
|
}
|
package/dist/scroll-fade.js
CHANGED
|
@@ -12,8 +12,9 @@ export function useScrollFade(given) {
|
|
|
12
12
|
const el = ref.current;
|
|
13
13
|
if (!el)
|
|
14
14
|
return;
|
|
15
|
-
|
|
16
|
-
const
|
|
15
|
+
// A couple of pixels past the edge (rounding, a border) is not "more beyond it".
|
|
16
|
+
const start = el.scrollLeft > 2;
|
|
17
|
+
const end = el.scrollLeft + el.clientWidth < el.scrollWidth - 2;
|
|
17
18
|
setSide(start && end ? 'both' : start ? 'start' : end ? 'end' : undefined);
|
|
18
19
|
}, [ref]);
|
|
19
20
|
useEffect(() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.122",
|
|
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.122",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|