@columenlabs/lumen-ui 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -85,3 +85,4 @@ export { useSymbolSheet } from './symbol-sheet-context.js';
85
85
  export { NoteLine, type NoteLineProps } from './note-line.js';
86
86
  export { type LumenFinderConfig, type LumenFinderPageType, type LumenFinderRow, type LumenFinderScope, LumenFinderButton, useLumenFinder, useLumenFinderRows, useOpenLumenFinder, } from './lumen-finder.js';
87
87
  export { type AssistantChat, LumenAssistantButton, LumenAssistantChats, LumenAssistantPanel, assistantOpen, tabState, useAssistantHotkey, } from './assistant-panel.js';
88
+ export { LumenTree, LumenTreeItem, type LumenTreeItemProps, type LumenTreeProps, useLumenTree, } from './lumen-tree.js';
package/dist/index.js CHANGED
@@ -92,3 +92,4 @@ export { useSymbolSheet } from './symbol-sheet-context.js';
92
92
  export { NoteLine } from './note-line.js';
93
93
  export { LumenFinderButton, useLumenFinder, useLumenFinderRows, useOpenLumenFinder, } from './lumen-finder.js';
94
94
  export { LumenAssistantButton, LumenAssistantChats, LumenAssistantPanel, assistantOpen, tabState, useAssistantHotkey, } from './assistant-panel.js';
95
+ export { LumenTree, LumenTreeItem, useLumenTree, } from './lumen-tree.js';
@@ -0,0 +1,32 @@
1
+ import { type ReactNode } from 'react';
2
+ interface TreeContext {
3
+ /** Opens every item above the row `id`, scrolls to it and flashes it. */
4
+ reveal: (id: string) => void;
5
+ /** Items told to open by the last reveal, and a count so the same set opens again. */
6
+ opening: {
7
+ ids: Set<string>;
8
+ n: number;
9
+ };
10
+ }
11
+ /** The tree around the rows: `reveal` from any row inside it. */
12
+ export declare function useLumenTree(): TreeContext;
13
+ export interface LumenTreeProps {
14
+ children: ReactNode;
15
+ 'aria-label'?: string;
16
+ className?: string;
17
+ }
18
+ export declare function LumenTree({ children, className, ...aria }: LumenTreeProps): import("react").JSX.Element;
19
+ export interface LumenTreeItemProps {
20
+ /** What `reveal` finds this row by. */
21
+ id?: string;
22
+ /** The row itself, laid out as a wrapping line beside the chevron. */
23
+ row: ReactNode;
24
+ /** Rows under this one: LumenTreeItems. */
25
+ children?: ReactNode;
26
+ defaultOpen?: boolean;
27
+ /** `positive`: the guide under it is the colour of a path taken; `muted`: the row is dimmed. */
28
+ tone?: 'default' | 'positive' | 'muted';
29
+ className?: string;
30
+ }
31
+ export declare function LumenTreeItem({ id, row, children, defaultOpen, tone, className, }: LumenTreeItemProps): import("react").JSX.Element;
32
+ export {};
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // LumenTree: rows that nest, read top to bottom. Each item is one row; an item with children has a
3
+ // chevron and folds, and its children hang off a guide line (green when `tone` is positive: the path
4
+ // that was taken). Nothing about what the rows mean lives here: an app fills `row` with its own
5
+ // words. Children stay mounted while folded, so `reveal` can find a row inside a closed item, open
6
+ // every item above it, scroll to it and flash it (a link from one row to another).
7
+ import { ChevronRightIcon } from 'lucide-react';
8
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
9
+ import { cn } from './lib/utils.js';
10
+ import { Collapsible, CollapsibleContent } from './ui/collapsible.js';
11
+ const Ctx = createContext(null);
12
+ /** The tree around the rows: `reveal` from any row inside it. */
13
+ export function useLumenTree() {
14
+ const c = useContext(Ctx);
15
+ if (!c)
16
+ throw new Error('useLumenTree outside LumenTree');
17
+ return c;
18
+ }
19
+ export function LumenTree({ children, className, ...aria }) {
20
+ const root = useRef(null);
21
+ const [opening, setOpening] = useState({ ids: new Set(), n: 0 });
22
+ const reveal = useCallback((id) => {
23
+ const el = root.current?.querySelector(`[data-tree-id="${CSS.escape(id)}"]`);
24
+ if (!el)
25
+ return;
26
+ const ids = new Set();
27
+ for (let p = el.parentElement; p && p !== root.current; p = p.parentElement) {
28
+ const k = p.getAttribute('data-tree-id');
29
+ if (k)
30
+ ids.add(k);
31
+ }
32
+ setOpening((o) => ({ ids, n: o.n + 1 }));
33
+ requestAnimationFrame(() => {
34
+ const row = el.querySelector(':scope > [data-tree-row]') ?? el;
35
+ const still = matchMedia('(prefers-reduced-motion: reduce)').matches;
36
+ row.scrollIntoView({ behavior: still ? 'auto' : 'smooth', block: 'center' });
37
+ row.animate(still
38
+ ? [{ outline: '1px solid var(--primary)' }, { outline: '1px solid transparent' }]
39
+ : [
40
+ { backgroundColor: 'color-mix(in oklab, var(--primary) 35%, transparent)' },
41
+ { backgroundColor: 'transparent' },
42
+ ], { duration: 1600, easing: 'ease-out' });
43
+ });
44
+ }, []);
45
+ const ctx = useMemo(() => ({ reveal, opening }), [reveal, opening]);
46
+ return (_jsx(Ctx.Provider, { value: ctx, children: _jsx("ul", { ref: root, className: cn('flex flex-col', className), ...aria, children: children }) }));
47
+ }
48
+ const isControl = (t) => t instanceof Element && t.closest('button, a, input, [role="button"], [data-tree-skip]') !== null;
49
+ export function LumenTreeItem({ id, row, children, defaultOpen = false, tone = 'default', className, }) {
50
+ const [open, setOpen] = useState(defaultOpen);
51
+ const ctx = useContext(Ctx);
52
+ const n = ctx?.opening.n ?? 0;
53
+ const told = id !== undefined && (ctx?.opening.ids.has(id) ?? false);
54
+ useEffect(() => {
55
+ if (told && n > 0)
56
+ setOpen(true);
57
+ }, [told, n]);
58
+ const has = children !== undefined && children !== null && children !== false;
59
+ const line = (_jsxs("div", { "data-tree-row": true, className: cn('flex min-h-7 flex-wrap items-baseline gap-x-2 gap-y-0.5 rounded-md px-1.5 py-0.5 hover:bg-muted/60', has && 'cursor-pointer', tone === 'muted' && 'text-muted-foreground'), onClick: has ? (e) => !isControl(e.target) && setOpen((o) => !o) : undefined, children: [has ? (_jsx("button", { type: "button", "aria-expanded": open, "aria-label": open ? 'Fold' : 'Unfold', className: "grid size-4 shrink-0 self-center place-items-center text-muted-foreground", onClick: (e) => {
60
+ e.stopPropagation();
61
+ setOpen((o) => !o);
62
+ }, children: _jsx(ChevronRightIcon, { className: cn('size-3.5 transition-transform', open && 'rotate-90') }) })) : (_jsx("span", { className: "w-4 shrink-0", "aria-hidden": true })), row] }));
63
+ return (_jsx("li", { "data-tree-id": id, className: className, children: has ? (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, children: [line, _jsx(CollapsibleContent, { forceMount: true, className: "data-[state=closed]:hidden", children: _jsx("ul", { className: cn('ml-3 flex flex-col border-l pl-2.5 max-sm:ml-2 max-sm:pl-1.5', tone === 'positive' && 'border-rule-yes/40'), children: children }) })] })) : (line) }));
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  "@codemirror/lang-javascript": "6.2.5",
42
42
  "@codemirror/lang-json": "6.0.2",
43
43
  "@codemirror/lang-sql": "6.10.0",
44
- "@columenlabs/lumen-ui-types": "0.3.0",
44
+ "@columenlabs/lumen-ui-types": "0.3.2",
45
45
  "@dnd-kit/core": "6.3.1",
46
46
  "@dnd-kit/sortable": "10.0.0",
47
47
  "@dnd-kit/utilities": "3.2.2",