@columenlabs/lumen-ui 0.2.2 → 0.3.1
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/confirm.d.ts +4 -2
- package/dist/confirm.js +16 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/lumen-auth.d.ts +67 -0
- package/dist/lumen-auth.js +52 -0
- package/dist/lumen-choice.d.ts +18 -0
- package/dist/lumen-choice.js +12 -0
- package/dist/lumen-secret.d.ts +8 -0
- package/dist/lumen-secret.js +21 -0
- package/dist/lumen-tree.d.ts +32 -0
- package/dist/lumen-tree.js +64 -0
- package/package.json +2 -2
package/dist/confirm.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
2
|
export interface LumenConfirmProps {
|
|
3
3
|
/** The question, e.g. `Delete view Options?`. */
|
|
4
4
|
title: ReactNode;
|
|
@@ -14,5 +14,7 @@ export interface LumenConfirmProps {
|
|
|
14
14
|
onOpenChange?: (open: boolean) => void;
|
|
15
15
|
/** False for a confirm that destroys nothing (a new version): its action is not red. */
|
|
16
16
|
destructive?: boolean;
|
|
17
|
+
/** A word the person must type before the action unlocks, for a step that loses a lot (`DELETE`). */
|
|
18
|
+
typeToConfirm?: string;
|
|
17
19
|
}
|
|
18
|
-
export declare function LumenConfirm({ title, description, action, cancel, onConfirm, children, open, onOpenChange, destructive, }: LumenConfirmProps): import("react").JSX.Element;
|
|
20
|
+
export declare function LumenConfirm({ title, description, action, cancel, onConfirm, children, open, onOpenChange, destructive, typeToConfirm, }: LumenConfirmProps): import("react").JSX.Element;
|
package/dist/confirm.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Every delete asks first: one shadcn AlertDialog every app uses for any
|
|
3
|
+
// delete, remove, cancel or other step to confirm (LumenConfirm, : no raw
|
|
4
|
+
// AlertDialog in an app). Wrap the trigger button, or control `open` when it starts from a menu item.
|
|
5
|
+
import { useId, useState } from 'react';
|
|
2
6
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from './ui/alert-dialog.js';
|
|
3
|
-
|
|
4
|
-
|
|
7
|
+
import { Field, FieldLabel } from './ui/field.js';
|
|
8
|
+
import { Input } from './ui/input.js';
|
|
9
|
+
export function LumenConfirm({ title, description = 'This cannot be undone.', action = 'Delete', cancel = 'Cancel', onConfirm, children, open, onOpenChange, destructive = true, typeToConfirm, }) {
|
|
10
|
+
const id = useId();
|
|
11
|
+
const [typed, setTyped] = useState('');
|
|
12
|
+
const locked = typeToConfirm !== undefined && typed !== typeToConfirm;
|
|
13
|
+
const change = (o) => {
|
|
14
|
+
if (!o)
|
|
15
|
+
setTyped('');
|
|
16
|
+
onOpenChange?.(o);
|
|
17
|
+
};
|
|
18
|
+
return (_jsxs(AlertDialog, { open: open, onOpenChange: change, children: [children && _jsx(AlertDialogTrigger, { asChild: true, children: children }), _jsxs(AlertDialogContent, { size: "sm", children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: title }), _jsx(AlertDialogDescription, { children: description })] }), typeToConfirm !== undefined && (_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: id, children: _jsxs("span", { children: ["Type ", _jsx("code", { className: "rounded bg-muted px-1.5 font-code", children: typeToConfirm }), " to confirm"] }) }), _jsx(Input, { id: id, autoComplete: "off", className: "font-code", value: typed, onChange: (e) => setTyped(e.target.value) })] })), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: cancel }), _jsx(AlertDialogAction, { variant: destructive ? 'destructive-confirm' : 'default', disabled: locked, onClick: onConfirm, children: action })] })] })] }));
|
|
5
19
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export { LumenEChart, type EChartOption, type ChartTokens, axisStyle, baseOption
|
|
|
15
15
|
export { LumenGeoMap } from './lumen-geo-map.js';
|
|
16
16
|
export { LumenWidget, type CardControls, type CardLink, LumenSavedWidget } from './lumen-widget.js';
|
|
17
17
|
export { LumenConfirm, type LumenConfirmProps } from './confirm.js';
|
|
18
|
+
export { type LumenAuthPoint, LumenAuthHeading, LumenAuthNote, LumenAuthProblem, LumenAuthStage, type LumenAuthStageProps, LumenCodeField, LumenEmailField, LumenPasswordField, LumenStepBar, } from './lumen-auth.js';
|
|
19
|
+
export { LumenChoice, type LumenChoiceOption } from './lumen-choice.js';
|
|
20
|
+
export { LumenSecret } from './lumen-secret.js';
|
|
21
|
+
export { RankedBars, StackedBars } from './lumen-widget-charts.js';
|
|
18
22
|
export { COVERAGE_LEVELS, CoverageCell, type CoverageJobState, type CoverageLevel, CoverageLegend, coverageClass, coverageLabel, coverageLevel, } from './coverage-cells.js';
|
|
19
23
|
export { type CoverageBlock, type CoverageData, type CoverageFact, type CoveragePick, type CoverageRow, type CoverageSummary, LumenCoverageMap, type LumenCoverageMapProps, blockName, blockOf, } from './coverage-map.js';
|
|
20
24
|
export { type CalendarFact, LumenCoverageCalendarMap, type LumenCoverageCalendarMapProps, } from './coverage-calendar-map.js';
|
|
@@ -81,3 +85,4 @@ export { useSymbolSheet } from './symbol-sheet-context.js';
|
|
|
81
85
|
export { NoteLine, type NoteLineProps } from './note-line.js';
|
|
82
86
|
export { type LumenFinderConfig, type LumenFinderPageType, type LumenFinderRow, type LumenFinderScope, LumenFinderButton, useLumenFinder, useLumenFinderRows, useOpenLumenFinder, } from './lumen-finder.js';
|
|
83
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
|
@@ -22,6 +22,10 @@ export { LumenEChart, axisStyle, baseOption, useChartTokens, } from './lumen-ech
|
|
|
22
22
|
export { LumenGeoMap } from './lumen-geo-map.js';
|
|
23
23
|
export { LumenWidget, LumenSavedWidget } from './lumen-widget.js';
|
|
24
24
|
export { LumenConfirm } from './confirm.js';
|
|
25
|
+
export { LumenAuthHeading, LumenAuthNote, LumenAuthProblem, LumenAuthStage, LumenCodeField, LumenEmailField, LumenPasswordField, LumenStepBar, } from './lumen-auth.js';
|
|
26
|
+
export { LumenChoice } from './lumen-choice.js';
|
|
27
|
+
export { LumenSecret } from './lumen-secret.js';
|
|
28
|
+
export { RankedBars, StackedBars } from './lumen-widget-charts.js';
|
|
25
29
|
export { COVERAGE_LEVELS, CoverageCell, CoverageLegend, coverageClass, coverageLabel, coverageLevel, } from './coverage-cells.js';
|
|
26
30
|
export { LumenCoverageMap, blockName, blockOf, } from './coverage-map.js';
|
|
27
31
|
export { LumenCoverageCalendarMap, } from './coverage-calendar-map.js';
|
|
@@ -88,3 +92,4 @@ export { useSymbolSheet } from './symbol-sheet-context.js';
|
|
|
88
92
|
export { NoteLine } from './note-line.js';
|
|
89
93
|
export { LumenFinderButton, useLumenFinder, useLumenFinderRows, useOpenLumenFinder, } from './lumen-finder.js';
|
|
90
94
|
export { LumenAssistantButton, LumenAssistantChats, LumenAssistantPanel, assistantOpen, tabState, useAssistantHotkey, } from './assistant-panel.js';
|
|
95
|
+
export { LumenTree, LumenTreeItem, useLumenTree, } from './lumen-tree.js';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type ComponentType, type ReactNode } from 'react';
|
|
2
|
+
export interface LumenAuthPoint {
|
|
3
|
+
icon: ComponentType<{
|
|
4
|
+
className?: string;
|
|
5
|
+
}>;
|
|
6
|
+
title: string;
|
|
7
|
+
body: string;
|
|
8
|
+
}
|
|
9
|
+
export interface LumenAuthStageProps {
|
|
10
|
+
/** The app's logo and name, shown over the story and, on a phone, over the form. */
|
|
11
|
+
brand: ReactNode;
|
|
12
|
+
/** The story's headline and the line under it. */
|
|
13
|
+
pitch: {
|
|
14
|
+
title: ReactNode;
|
|
15
|
+
body?: ReactNode;
|
|
16
|
+
};
|
|
17
|
+
points?: LumenAuthPoint[];
|
|
18
|
+
/** A picture under the points, such as a diagram of what the app does. */
|
|
19
|
+
illustration?: ReactNode;
|
|
20
|
+
/** Small links under the form (privacy, terms). */
|
|
21
|
+
footer?: ReactNode;
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
/** The page a signed-out screen sits in. */
|
|
25
|
+
export declare function LumenAuthStage({ brand, pitch, points, illustration, footer, children, }: LumenAuthStageProps): import("react").JSX.Element;
|
|
26
|
+
/** A screen's title, and a line under it. */
|
|
27
|
+
export declare function LumenAuthHeading({ title, sub }: {
|
|
28
|
+
title: ReactNode;
|
|
29
|
+
sub?: ReactNode;
|
|
30
|
+
}): import("react").JSX.Element;
|
|
31
|
+
/** A red line over the form; renders nothing when empty. */
|
|
32
|
+
export declare function LumenAuthProblem({ children }: {
|
|
33
|
+
children?: ReactNode;
|
|
34
|
+
}): import("react").JSX.Element | null;
|
|
35
|
+
/** A plain note over the form, such as "New code sent"; renders nothing when empty. */
|
|
36
|
+
export declare function LumenAuthNote({ children }: {
|
|
37
|
+
children?: ReactNode;
|
|
38
|
+
}): import("react").JSX.Element | null;
|
|
39
|
+
interface TextFieldProps {
|
|
40
|
+
id?: string;
|
|
41
|
+
value: string;
|
|
42
|
+
onChange: (v: string) => void;
|
|
43
|
+
/** The browser's autocomplete hint (`username`, `email`, `current-password`, `new-password`). */
|
|
44
|
+
autoComplete: string;
|
|
45
|
+
label?: string;
|
|
46
|
+
}
|
|
47
|
+
export declare function LumenEmailField({ id, value, onChange, autoComplete, label, }: TextFieldProps): import("react").JSX.Element;
|
|
48
|
+
/** A password box with a Show switch beside it. */
|
|
49
|
+
export declare function LumenPasswordField({ id, value, onChange, autoComplete, label, }: TextFieldProps): import("react").JSX.Element;
|
|
50
|
+
/** A one-time code from an email or an authenticator app: digits only, big and spaced. */
|
|
51
|
+
export declare function LumenCodeField({ id, value, onChange, length, error, hint, onResend, resendLabel, }: {
|
|
52
|
+
id?: string;
|
|
53
|
+
value: string;
|
|
54
|
+
onChange: (v: string) => void;
|
|
55
|
+
length?: number;
|
|
56
|
+
error?: ReactNode;
|
|
57
|
+
hint?: ReactNode;
|
|
58
|
+
/** Shows a link after the hint that asks for a fresh code. */
|
|
59
|
+
onResend?: () => void;
|
|
60
|
+
resendLabel?: string;
|
|
61
|
+
}): import("react").JSX.Element;
|
|
62
|
+
/** Progress through a short flow: one bar per step, filled up to `at`, and "Step 2 of 3". */
|
|
63
|
+
export declare function LumenStepBar({ at, of }: {
|
|
64
|
+
at: number;
|
|
65
|
+
of: number;
|
|
66
|
+
}): import("react").JSX.Element;
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// The signed-out screens every app shares: a split page (the app's story on the left, hidden on
|
|
3
|
+
// a phone, the form on the right) and the fields its forms are built from. The app passes its
|
|
4
|
+
// brand, pitch, points, illustration and footer links; the kit draws the layout.
|
|
5
|
+
import { Lock, Mail } from 'lucide-react';
|
|
6
|
+
import { useId, useState } from 'react';
|
|
7
|
+
import { Alert, AlertDescription } from './ui/alert.js';
|
|
8
|
+
import { Checkbox } from './ui/checkbox.js';
|
|
9
|
+
import { Field, FieldDescription, FieldError, FieldLabel } from './ui/field.js';
|
|
10
|
+
import { Input } from './ui/input.js';
|
|
11
|
+
import { InputGroup, InputGroupAddon, InputGroupInput } from './ui/input-group.js';
|
|
12
|
+
/** The page a signed-out screen sits in. */
|
|
13
|
+
export function LumenAuthStage({ brand, pitch, points = [], illustration, footer, children, }) {
|
|
14
|
+
return (_jsxs("div", { className: "grid min-h-svh grid-cols-1 lg:grid-cols-[minmax(0,1.05fr)_minmax(0,1fr)]", children: [_jsxs("aside", { className: "hidden flex-col gap-10 border-r bg-sidebar px-10 py-8 text-sidebar-foreground lg:flex", children: [brand, _jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("h2", { className: "text-balance font-semibold text-3xl leading-tight", children: pitch.title }), pitch.body && _jsx("p", { className: "max-w-md text-muted-foreground", children: pitch.body })] }), points.length > 0 && (_jsx("ul", { className: "flex flex-col gap-4", children: points.map(({ icon: Icon, title, body }) => (_jsxs("li", { className: "flex gap-3", children: [_jsx("span", { className: "flex size-9 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground", children: _jsx(Icon, { className: "size-4.5" }) }), _jsxs("span", { className: "flex flex-col", children: [_jsx("span", { className: "font-medium text-sm", children: title }), _jsx("span", { className: "text-muted-foreground text-sm", children: body })] })] }, title))) })), illustration] }), _jsxs("main", { className: "flex flex-col bg-background px-6 py-8 sm:px-12", children: [_jsx("div", { className: "lg:hidden", children: brand }), _jsx("div", { className: "mx-auto flex w-full max-w-sm flex-1 flex-col justify-center py-8", children: children }), footer && _jsx("div", { className: "text-center text-muted-foreground text-xs", children: footer })] })] }));
|
|
15
|
+
}
|
|
16
|
+
/** A screen's title, and a line under it. */
|
|
17
|
+
export function LumenAuthHeading({ title, sub }) {
|
|
18
|
+
return (_jsxs("div", { className: "mb-6 flex flex-col gap-1.5", children: [_jsx("h1", { className: "font-semibold text-2xl", children: title }), sub && _jsx("p", { className: "text-muted-foreground text-sm", children: sub })] }));
|
|
19
|
+
}
|
|
20
|
+
/** A red line over the form; renders nothing when empty. */
|
|
21
|
+
export function LumenAuthProblem({ children }) {
|
|
22
|
+
if (!children)
|
|
23
|
+
return null;
|
|
24
|
+
return (_jsx(Alert, { variant: "destructive", className: "mb-4", children: _jsx(AlertDescription, { children: children }) }));
|
|
25
|
+
}
|
|
26
|
+
/** A plain note over the form, such as "New code sent"; renders nothing when empty. */
|
|
27
|
+
export function LumenAuthNote({ children }) {
|
|
28
|
+
if (!children)
|
|
29
|
+
return null;
|
|
30
|
+
return (_jsxs(Alert, { className: "mb-4", children: [_jsx(Mail, {}), _jsx(AlertDescription, { children: children })] }));
|
|
31
|
+
}
|
|
32
|
+
export function LumenEmailField({ id, value, onChange, autoComplete, label = 'Email', }) {
|
|
33
|
+
const own = useId();
|
|
34
|
+
const fieldId = id ?? own;
|
|
35
|
+
return (_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: fieldId, children: label }), _jsxs(InputGroup, { className: "h-10", children: [_jsx(InputGroupAddon, { children: _jsx(Mail, {}) }), _jsx(InputGroupInput, { id: fieldId, type: "email", required: true, autoComplete: autoComplete, placeholder: "you@example.com", value: value, onChange: (e) => onChange(e.target.value) })] })] }));
|
|
36
|
+
}
|
|
37
|
+
/** A password box with a Show switch beside it. */
|
|
38
|
+
export function LumenPasswordField({ id, value, onChange, autoComplete, label = 'Password', }) {
|
|
39
|
+
const own = useId();
|
|
40
|
+
const fieldId = id ?? own;
|
|
41
|
+
const [show, setShow] = useState(false);
|
|
42
|
+
return (_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: fieldId, children: label }), _jsxs(InputGroup, { className: "h-10", children: [_jsx(InputGroupAddon, { children: _jsx(Lock, {}) }), _jsx(InputGroupInput, { id: fieldId, type: show ? 'text' : 'password', required: true, autoComplete: autoComplete, value: value, onChange: (e) => onChange(e.target.value) }), _jsx(InputGroupAddon, { align: "inline-end", className: "border-l bg-muted/50 px-3", children: _jsxs("label", { htmlFor: `${fieldId}-show`, className: "flex items-center gap-1.5 text-sm", children: [_jsx(Checkbox, { id: `${fieldId}-show`, checked: show, onCheckedChange: (v) => setShow(v === true) }), ' ', "Show"] }) })] })] }));
|
|
43
|
+
}
|
|
44
|
+
/** A one-time code from an email or an authenticator app: digits only, big and spaced. */
|
|
45
|
+
export function LumenCodeField({ id, value, onChange, length = 6, error, hint, onResend, resendLabel = 'Send a new code', }) {
|
|
46
|
+
const own = useId();
|
|
47
|
+
return (_jsxs(Field, { "data-invalid": error ? true : undefined, children: [_jsx(Input, { id: id ?? own, inputMode: "numeric", autoComplete: "one-time-code", placeholder: '0'.repeat(length), maxLength: length, "aria-label": `${length}-digit code`, className: "h-14 text-center font-code text-2xl tracking-[0.6em]", value: value, onChange: (e) => onChange(e.target.value.replace(/\D/g, '').slice(0, length)), "aria-invalid": Boolean(error) }), error ? _jsx(FieldError, { children: error }) : null, (hint || onResend) && (_jsxs(FieldDescription, { className: "text-center", children: [hint, ' ', onResend && (_jsx("button", { type: "button", className: "text-primary underline-offset-4 hover:underline", onClick: onResend, children: resendLabel }))] }))] }));
|
|
48
|
+
}
|
|
49
|
+
/** Progress through a short flow: one bar per step, filled up to `at`, and "Step 2 of 3". */
|
|
50
|
+
export function LumenStepBar({ at, of }) {
|
|
51
|
+
return (_jsxs("div", { className: "mb-5 flex flex-col gap-1.5", children: [_jsx("div", { className: "flex gap-1.5", children: Array.from({ length: of }, (_, i) => i + 1).map((n) => (_jsx("span", { className: `h-1 flex-1 rounded-full ${n <= at ? 'bg-primary' : 'bg-muted'}` }, n))) }), _jsxs("span", { className: "text-center text-muted-foreground text-xs", children: ["Step ", at, " of ", of] })] }));
|
|
52
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from 'react';
|
|
2
|
+
export interface LumenChoiceOption<T extends string> {
|
|
3
|
+
value: T;
|
|
4
|
+
icon: ComponentType<{
|
|
5
|
+
className?: string;
|
|
6
|
+
}>;
|
|
7
|
+
title: ReactNode;
|
|
8
|
+
description?: ReactNode;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function LumenChoice<T extends string>({ label, value, options, onValueChange, }: {
|
|
12
|
+
/** Names the group for screen readers. */
|
|
13
|
+
label: string;
|
|
14
|
+
/** The picked option, or null before the person picks. */
|
|
15
|
+
value: T | null;
|
|
16
|
+
options: LumenChoiceOption<T>[];
|
|
17
|
+
onValueChange: (v: T) => void;
|
|
18
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// A pick-one list of cards, each with an icon, a title and a line under it: the way an app asks
|
|
3
|
+
// "how do you want to do this?" when each option needs a sentence. For a short one-of-a-few
|
|
4
|
+
// choice without notes, use CardSegments.
|
|
5
|
+
import { Check } from 'lucide-react';
|
|
6
|
+
import { Item, ItemContent, ItemDescription, ItemMedia, ItemTitle } from './ui/item.js';
|
|
7
|
+
export function LumenChoice({ label, value, options, onValueChange, }) {
|
|
8
|
+
return (_jsx("fieldset", { "aria-label": label, className: "m-0 flex min-w-0 flex-col gap-3 border-0 p-0", children: options.map(({ value: v, icon: Icon, title, description, disabled }) => {
|
|
9
|
+
const picked = v === value;
|
|
10
|
+
return (_jsx(Item, { asChild: true, variant: "outline", className: picked ? 'border-primary bg-primary/5' : 'disabled:opacity-50', children: _jsxs("button", { type: "button", "aria-pressed": picked, disabled: disabled, onClick: () => onValueChange(v), children: [_jsx(ItemMedia, { variant: "icon", children: _jsx(Icon, { className: picked ? 'text-primary' : undefined }) }), _jsxs(ItemContent, { className: "text-left", children: [_jsx(ItemTitle, { children: title }), description && _jsx(ItemDescription, { children: description })] }), picked && _jsx(Check, { className: "size-5 shrink-0 text-primary" })] }) }, v));
|
|
11
|
+
}) }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export declare function LumenSecret({ value, label, warning, hidden, }: {
|
|
3
|
+
value: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
warning?: ReactNode;
|
|
6
|
+
/** Start with the value masked. */
|
|
7
|
+
hidden?: boolean;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// A secret the app shows only once (a new API key, a signing secret): a warning to copy it now,
|
|
3
|
+
// the value in code type with a Show switch, and a Copy button that says when it worked.
|
|
4
|
+
import { Check, Copy, Eye, EyeOff, TriangleAlert } from 'lucide-react';
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
import { Alert, AlertDescription } from './ui/alert.js';
|
|
7
|
+
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from './ui/input-group.js';
|
|
8
|
+
export function LumenSecret({ value, label = 'Secret', warning = 'Copy it now. It won’t be shown again.', hidden = false, }) {
|
|
9
|
+
const [show, setShow] = useState(!hidden);
|
|
10
|
+
const [copied, setCopied] = useState(false);
|
|
11
|
+
const copy = async () => {
|
|
12
|
+
try {
|
|
13
|
+
await navigator.clipboard.writeText(value);
|
|
14
|
+
setCopied(true);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
setCopied(false);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
return (_jsxs("div", { className: "flex flex-col gap-3", children: [warning && (_jsxs(Alert, { children: [_jsx(TriangleAlert, {}), _jsx(AlertDescription, { children: warning })] })), _jsxs(InputGroup, { className: "h-10", children: [_jsx(InputGroupInput, { readOnly: true, "aria-label": label, type: show ? 'text' : 'password', value: value, className: "font-code text-xs", onFocus: (e) => e.currentTarget.select() }), _jsxs(InputGroupAddon, { align: "inline-end", children: [_jsx(InputGroupButton, { size: "icon-xs", "aria-label": show ? `Hide ${label.toLowerCase()}` : `Show ${label.toLowerCase()}`, onClick: () => setShow((s) => !s), children: show ? _jsx(EyeOff, {}) : _jsx(Eye, {}) }), _jsxs(InputGroupButton, { size: "xs", onClick: copy, children: [copied ? _jsx(Check, {}) : _jsx(Copy, {}), copied ? 'Copied' : 'Copy'] })] })] })] }));
|
|
21
|
+
}
|
|
@@ -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
|
+
"version": "0.3.1",
|
|
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.
|
|
44
|
+
"@columenlabs/lumen-ui-types": "0.3.1",
|
|
45
45
|
"@dnd-kit/core": "6.3.1",
|
|
46
46
|
"@dnd-kit/sortable": "10.0.0",
|
|
47
47
|
"@dnd-kit/utilities": "3.2.2",
|