@columenlabs/lumen-ui 0.3.2 → 0.4.0
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 +17 -2
- package/dist/confirm.js +43 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lumen-widget-charts.d.ts +24 -0
- package/dist/lumen-widget-charts.js +18 -1
- package/package.json +2 -2
package/dist/confirm.d.ts
CHANGED
|
@@ -7,7 +7,8 @@ export interface LumenConfirmProps {
|
|
|
7
7
|
action?: ReactNode;
|
|
8
8
|
/** The button that backs out (Keep, Keep running). */
|
|
9
9
|
cancel?: string;
|
|
10
|
-
|
|
10
|
+
/** Return a promise to keep the dialog open, busy, until it settles. */
|
|
11
|
+
onConfirm: () => unknown;
|
|
11
12
|
/** The button that opens the dialog; leave it out and control `open` instead. */
|
|
12
13
|
children?: ReactNode;
|
|
13
14
|
open?: boolean;
|
|
@@ -16,5 +17,19 @@ export interface LumenConfirmProps {
|
|
|
16
17
|
destructive?: boolean;
|
|
17
18
|
/** A word the person must type before the action unlocks, for a step that loses a lot (`DELETE`). */
|
|
18
19
|
typeToConfirm?: string;
|
|
20
|
+
/** The action's label while the promise runs. */
|
|
21
|
+
busyAction?: ReactNode;
|
|
22
|
+
/** Turns a failed promise's error into what the dialog says (a string, or the error itself). */
|
|
23
|
+
problem?: (error: unknown) => unknown;
|
|
24
|
+
/** What the dialog says after the promise succeeds; leave it out to close instead. */
|
|
25
|
+
done?: {
|
|
26
|
+
title: ReactNode;
|
|
27
|
+
description?: ReactNode;
|
|
28
|
+
action?: ReactNode;
|
|
29
|
+
};
|
|
30
|
+
/** Runs when the person dismisses the `done` state. */
|
|
31
|
+
onDone?: () => void;
|
|
32
|
+
/** `default` is wider, for a longer description. */
|
|
33
|
+
size?: 'sm' | 'default';
|
|
19
34
|
}
|
|
20
|
-
export declare function LumenConfirm({ title, description, action, cancel, onConfirm, children, open, onOpenChange, destructive, typeToConfirm, }: LumenConfirmProps): import("react").JSX.Element;
|
|
35
|
+
export declare function LumenConfirm({ title, description, action, cancel, onConfirm, children, open, onOpenChange, destructive, typeToConfirm, busyAction, problem, done, onDone, size, }: LumenConfirmProps): import("react").JSX.Element;
|
package/dist/confirm.js
CHANGED
|
@@ -1,19 +1,58 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// Every delete asks first: one shadcn AlertDialog every app uses for any
|
|
3
3
|
// delete, remove, cancel or other step to confirm (LumenConfirm, : no raw
|
|
4
4
|
// AlertDialog in an app). Wrap the trigger button, or control `open` when it starts from a menu item.
|
|
5
|
+
// When `onConfirm` returns a promise the dialog waits for it: the action shows `busyAction` and
|
|
6
|
+
// nothing closes it until the server answers; a failure stays in the dialog, a success closes it
|
|
7
|
+
// or shows `done`.
|
|
5
8
|
import { useId, useState } from 'react';
|
|
9
|
+
import { ErrorLine } from './integrations.js';
|
|
6
10
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from './ui/alert-dialog.js';
|
|
7
11
|
import { Field, FieldLabel } from './ui/field.js';
|
|
8
12
|
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, }) {
|
|
13
|
+
export function LumenConfirm({ title, description = 'This cannot be undone.', action = 'Delete', cancel = 'Cancel', onConfirm, children, open, onOpenChange, destructive = true, typeToConfirm, busyAction = 'Working…', problem = (e) => e, done, onDone, size = 'sm', }) {
|
|
10
14
|
const id = useId();
|
|
15
|
+
const [inner, setInner] = useState(false);
|
|
11
16
|
const [typed, setTyped] = useState('');
|
|
17
|
+
const [busy, setBusy] = useState(false);
|
|
18
|
+
const [error, setError] = useState(null);
|
|
19
|
+
const [finished, setFinished] = useState(false);
|
|
12
20
|
const locked = typeToConfirm !== undefined && typed !== typeToConfirm;
|
|
13
21
|
const change = (o) => {
|
|
14
|
-
if (
|
|
22
|
+
if (busy)
|
|
23
|
+
return;
|
|
24
|
+
if (!o && finished)
|
|
25
|
+
onDone?.();
|
|
26
|
+
if (!o) {
|
|
15
27
|
setTyped('');
|
|
28
|
+
setError(null);
|
|
29
|
+
setFinished(false);
|
|
30
|
+
}
|
|
31
|
+
setInner(o);
|
|
16
32
|
onOpenChange?.(o);
|
|
17
33
|
};
|
|
18
|
-
|
|
34
|
+
const confirm = async (e) => {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
const run = onConfirm();
|
|
37
|
+
if (!(run instanceof Promise))
|
|
38
|
+
return change(false);
|
|
39
|
+
setBusy(true);
|
|
40
|
+
setError(null);
|
|
41
|
+
try {
|
|
42
|
+
await run;
|
|
43
|
+
setBusy(false);
|
|
44
|
+
if (done)
|
|
45
|
+
setFinished(true);
|
|
46
|
+
else {
|
|
47
|
+
setTyped('');
|
|
48
|
+
setInner(false);
|
|
49
|
+
onOpenChange?.(false);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
setBusy(false);
|
|
54
|
+
setError(problem(err));
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
return (_jsxs(AlertDialog, { open: open ?? inner, onOpenChange: change, children: [children && _jsx(AlertDialogTrigger, { asChild: true, children: children }), _jsx(AlertDialogContent, { size: size, children: finished && done ? (_jsxs(_Fragment, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: done.title }), done.description && (_jsx(AlertDialogDescription, { children: done.description }))] }), _jsx(AlertDialogFooter, { className: "group-data-[size=sm]/alert-dialog-content:grid-cols-1", children: _jsx(AlertDialogAction, { children: done.action ?? 'Done' }) })] })) : (_jsxs(_Fragment, { 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", autoCapitalize: typeToConfirm === typeToConfirm.toUpperCase() ? 'characters' : 'none', spellCheck: false, className: "font-code", disabled: busy, value: typed, onChange: (e) => setTyped(e.target.value) })] })), _jsx(ErrorLine, { error: error }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: busy, children: cancel }), _jsx(AlertDialogAction, { variant: destructive ? 'destructive-confirm' : 'default', disabled: locked || busy, "aria-busy": busy, onClick: confirm, children: busy ? busyAction : action })] })] })) })] }));
|
|
19
58
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export { LumenConfirm, type LumenConfirmProps } from './confirm.js';
|
|
|
18
18
|
export { type LumenAuthPoint, LumenAuthHeading, LumenAuthNote, LumenAuthProblem, LumenAuthStage, type LumenAuthStageProps, LumenCodeField, LumenEmailField, LumenPasswordField, LumenStepBar, } from './lumen-auth.js';
|
|
19
19
|
export { LumenChoice, type LumenChoiceOption } from './lumen-choice.js';
|
|
20
20
|
export { LumenSecret } from './lumen-secret.js';
|
|
21
|
-
export { RankedBars, StackedBars } from './lumen-widget-charts.js';
|
|
21
|
+
export { RankedBars, StackedBars, StackedColumns } from './lumen-widget-charts.js';
|
|
22
22
|
export { COVERAGE_LEVELS, CoverageCell, type CoverageJobState, type CoverageLevel, CoverageLegend, coverageClass, coverageLabel, coverageLevel, } from './coverage-cells.js';
|
|
23
23
|
export { type CoverageBlock, type CoverageData, type CoverageFact, type CoveragePick, type CoverageRow, type CoverageSummary, LumenCoverageMap, type LumenCoverageMapProps, blockName, blockOf, } from './coverage-map.js';
|
|
24
24
|
export { type CalendarFact, LumenCoverageCalendarMap, type LumenCoverageCalendarMapProps, } from './coverage-calendar-map.js';
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@ export { LumenConfirm } from './confirm.js';
|
|
|
25
25
|
export { LumenAuthHeading, LumenAuthNote, LumenAuthProblem, LumenAuthStage, LumenCodeField, LumenEmailField, LumenPasswordField, LumenStepBar, } from './lumen-auth.js';
|
|
26
26
|
export { LumenChoice } from './lumen-choice.js';
|
|
27
27
|
export { LumenSecret } from './lumen-secret.js';
|
|
28
|
-
export { RankedBars, StackedBars } from './lumen-widget-charts.js';
|
|
28
|
+
export { RankedBars, StackedBars, StackedColumns } from './lumen-widget-charts.js';
|
|
29
29
|
export { COVERAGE_LEVELS, CoverageCell, CoverageLegend, coverageClass, coverageLabel, coverageLevel, } from './coverage-cells.js';
|
|
30
30
|
export { LumenCoverageMap, blockName, blockOf, } from './coverage-map.js';
|
|
31
31
|
export { LumenCoverageCalendarMap, } from './coverage-calendar-map.js';
|
|
@@ -26,6 +26,30 @@ export declare function StackedBars({ rows, parts, fmt, exact, share, }: {
|
|
|
26
26
|
exact: Fmt;
|
|
27
27
|
share: (v: number) => string;
|
|
28
28
|
}): import("react").JSX.Element;
|
|
29
|
+
/**
|
|
30
|
+
* Columns over time (one per day, week or month), each split into stacked parts: what worked and
|
|
31
|
+
* what failed per day, spend per category per month. Oldest on the left; the top part gets the
|
|
32
|
+
* rounded cap; the tooltip lists every part and the column's total.
|
|
33
|
+
*/
|
|
34
|
+
export declare function StackedColumns({ rows, x, parts, label, height, tick, fmt, legend, }: {
|
|
35
|
+
/** One row per period, oldest first: the `x` field plus a number for each part's key. */
|
|
36
|
+
rows: Record<string, number | string>[];
|
|
37
|
+
/** The field that names each column (a date, a month). */
|
|
38
|
+
x: string;
|
|
39
|
+
parts: {
|
|
40
|
+
name: string;
|
|
41
|
+
key: string;
|
|
42
|
+
fill: string;
|
|
43
|
+
}[];
|
|
44
|
+
/** What the chart shows, for screen readers. */
|
|
45
|
+
label: string;
|
|
46
|
+
height?: number;
|
|
47
|
+
/** How a column's name reads under it (a date as `10-22`). */
|
|
48
|
+
tick?: (v: string) => string;
|
|
49
|
+
fmt?: Fmt;
|
|
50
|
+
/** The parts' names under the columns. */
|
|
51
|
+
legend?: boolean;
|
|
52
|
+
}): import("react").JSX.Element;
|
|
29
53
|
/** Horizontal bars as a ranked list: a full track per row, each bar its own colour. */
|
|
30
54
|
export declare function RankedBars({ id, items, signed, fmt, share, }: {
|
|
31
55
|
id: string;
|
|
@@ -3,11 +3,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// list on full tracks (or split into parts by a text field), the donut carries its total in the
|
|
4
4
|
// middle, allocation names its wide slices. Flat marks, no glow. Recharts only.
|
|
5
5
|
import { useState } from 'react';
|
|
6
|
-
import { Bar, BarChart, Cell, Label, LabelList, Pie, PieChart, XAxis, YAxis, usePlotArea, } from 'recharts';
|
|
6
|
+
import { Bar, BarChart, CartesianGrid, Cell, Label, LabelList, Pie, PieChart, Rectangle, XAxis, YAxis, usePlotArea, } from 'recharts';
|
|
7
7
|
import { cn } from './lib/utils.js';
|
|
8
8
|
import { ChartContainer, ChartTooltip, ChartTooltipContent } from './ui/chart.js';
|
|
9
9
|
/** Parts a split bar shows on their own; the rest fold into Other. */
|
|
10
10
|
export const STACK_MAX = 5;
|
|
11
|
+
/** A column's rounded top. */
|
|
12
|
+
const CAP = [3, 3, 0, 0];
|
|
11
13
|
/** The widget's lead number over its chart: total and a short caption. */
|
|
12
14
|
export function Headline({ value, caption, tone, }) {
|
|
13
15
|
return (_jsxs("div", { className: "mb-3 flex items-baseline gap-2", children: [_jsx("span", { className: cn('num font-semibold tracking-tight', 'text-2xl', tone === 'gain' && 'text-gain', tone === 'loss' && 'text-loss'), children: value }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: caption })] }));
|
|
@@ -30,6 +32,21 @@ export function StackedBars({ rows, parts, fmt, exact, share, }) {
|
|
|
30
32
|
return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("div", { className: "flex items-baseline gap-2 text-xs", children: [_jsx("span", { className: "truncate font-medium", children: String(r.name) }), _jsx("span", { className: "num ml-auto text-[11px] text-muted-foreground", children: share(total) }), _jsx("span", { className: "num", children: fmt(total) })] }), _jsx("div", { className: "h-2.5 overflow-hidden rounded-full bg-muted", children: _jsx(ChartContainer, { config: config, className: "aspect-auto h-full w-full", children: _jsxs(BarChart, { data: [r], layout: "vertical", margin: { left: 0, right: 0, top: 0, bottom: 0 }, barCategoryGap: 0, children: [_jsx(XAxis, { type: "number", hide: true, domain: [0, hi] }), _jsx(YAxis, { type: "category", hide: true }), _jsx(ChartTooltip, { cursor: false, shared: false, allowEscapeViewBox: { x: false, y: true }, wrapperStyle: { zIndex: 50 }, content: _jsx(ChartTooltipContent, { hideLabel: true, formatter: (v, name) => (_jsxs("span", { className: "flex w-full items-center gap-2", children: [_jsx("span", { className: "size-2 shrink-0 rounded-full", style: { background: config[String(name)]?.color } }), _jsx("span", { children: config[String(name)]?.label }), _jsxs("span", { className: "num ml-auto pl-3 whitespace-nowrap", children: [exact(Number(v)), " \u00B7", ' ', total ? ((Number(v) / total) * 100).toFixed(1) : 0, "%"] })] })) }) }), parts.map((p) => (_jsx(Bar, { dataKey: p.key, stackId: "a", fill: p.fill, stroke: "var(--card)", strokeWidth: 1, isAnimationActive: false }, p.key)))] }) }) })] }, String(r.name)));
|
|
31
33
|
}), _jsx("ul", { className: "flex flex-wrap gap-x-3 gap-y-1 pt-1 text-xs text-muted-foreground", children: parts.map((p) => (_jsxs("li", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "size-2 rounded-sm", style: { background: p.fill } }), p.name] }, p.key))) })] }));
|
|
32
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Columns over time (one per day, week or month), each split into stacked parts: what worked and
|
|
37
|
+
* what failed per day, spend per category per month. Oldest on the left; the top part gets the
|
|
38
|
+
* rounded cap; the tooltip lists every part and the column's total.
|
|
39
|
+
*/
|
|
40
|
+
export function StackedColumns({ rows, x, parts, label, height = 220, tick = String, fmt = (v) => v.toLocaleString(), legend = true, }) {
|
|
41
|
+
const config = Object.fromEntries(parts.map((p) => [p.key, { label: p.name, color: p.fill }]));
|
|
42
|
+
const total = (r) => parts.reduce((n, p) => n + (Number(r[p.key]) || 0), 0);
|
|
43
|
+
// The highest part with a value carries the column's rounded cap.
|
|
44
|
+
const top = (r) => parts.findLast((p) => Number(r[p.key]) > 0)?.key;
|
|
45
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(ChartContainer, { config: config, role: "img", "aria-label": label, className: "aspect-auto w-full", style: { height }, children: _jsxs(BarChart, { data: rows, margin: { left: 0, right: 0, top: 8, bottom: 0 }, children: [_jsx(CartesianGrid, { vertical: false, strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: x, tickLine: false, axisLine: false, tickMargin: 8, minTickGap: 12, tickFormatter: (v) => tick(String(v)) }), _jsx(YAxis, { tickLine: false, axisLine: false, width: 36, allowDecimals: false, tickFormatter: (v) => fmt(Number(v)) }), _jsx(ChartTooltip, { cursor: { fill: 'var(--muted)', opacity: 0.5 }, content: _jsx(ChartTooltipContent, { labelFormatter: (_, payload) => {
|
|
46
|
+
const r = payload?.[0]?.payload;
|
|
47
|
+
return r ? (_jsxs("span", { className: "flex w-full gap-3", children: [_jsx("span", { children: tick(String(r[x])) }), _jsx("span", { className: "num ml-auto", children: fmt(total(r)) })] })) : null;
|
|
48
|
+
}, formatter: (v, name) => (_jsxs("span", { className: "flex w-full items-center gap-2", children: [_jsx("span", { className: "size-2 shrink-0 rounded-sm", style: { background: config[String(name)]?.color } }), _jsx("span", { children: config[String(name)]?.label }), _jsx("span", { className: "num ml-auto pl-3", children: fmt(Number(v)) })] })) }) }), parts.map((p) => (_jsx(Bar, { dataKey: p.key, stackId: "a", fill: p.fill, isAnimationActive: false, shape: (b) => (_jsx(Rectangle, { ...b, radius: top(b.payload) === p.key ? CAP : 0 })) }, p.key)))] }) }), legend && (_jsx("ul", { className: "flex flex-wrap justify-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: parts.map((p) => (_jsxs("li", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "size-2 rounded-sm", style: { background: p.fill } }), p.name] }, p.key))) }))] }));
|
|
49
|
+
}
|
|
33
50
|
/** Horizontal bars as a ranked list: a full track per row, each bar its own colour. */
|
|
34
51
|
export function RankedBars({ id, items, signed, fmt, share, }) {
|
|
35
52
|
const ROW = 34;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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.4.0",
|
|
45
45
|
"@dnd-kit/core": "6.3.1",
|
|
46
46
|
"@dnd-kit/sortable": "10.0.0",
|
|
47
47
|
"@dnd-kit/utilities": "3.2.2",
|