panelui-web 0.0.0-stage → 0.1.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/README.md +43 -2
- package/dist/components/bar-picker/index.d.ts +61 -0
- package/dist/components/bar-picker/index.js +139 -0
- package/dist/components/command-dock/index.d.ts +118 -0
- package/dist/components/command-dock/index.js +239 -0
- package/dist/components/day-grid/index.d.ts +87 -0
- package/dist/components/day-grid/index.js +251 -0
- package/dist/components/feedback/index.d.ts +135 -0
- package/dist/components/feedback/index.js +283 -0
- package/dist/components/flow/flow-paths.d.ts +94 -0
- package/dist/components/flow/flow-paths.js +270 -0
- package/dist/components/flow/index.d.ts +235 -0
- package/dist/components/flow/index.js +597 -0
- package/dist/components/kpi/index.d.ts +211 -0
- package/dist/components/kpi/index.js +243 -0
- package/dist/components/lane-board/index.d.ts +125 -0
- package/dist/components/lane-board/index.js +691 -0
- package/dist/components/planet/index.d.ts +132 -0
- package/dist/components/planet/index.js +653 -0
- package/dist/components/planet/land.d.ts +8 -0
- package/dist/components/planet/land.js +122 -0
- package/dist/components/range-meter/index.d.ts +70 -0
- package/dist/components/range-meter/index.js +170 -0
- package/dist/components/rolling-number/index.d.ts +46 -0
- package/dist/components/rolling-number/index.js +81 -0
- package/dist/components/tilt-card/index.d.ts +60 -0
- package/dist/components/tilt-card/index.js +114 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/lib/cn.d.ts +3 -0
- package/dist/lib/cn.js +6 -0
- package/package.json +72 -4
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kpi — one number, and what it is doing.
|
|
3
|
+
*
|
|
4
|
+
* ```tsx
|
|
5
|
+
* <Kpi>
|
|
6
|
+
* <Kpi.Header>
|
|
7
|
+
* <Kpi.Title>Subscribers</Kpi.Title>
|
|
8
|
+
* </Kpi.Header>
|
|
9
|
+
* <Kpi.Content>
|
|
10
|
+
* <Kpi.Value>18,240</Kpi.Value>
|
|
11
|
+
* <Kpi.Trend value={6.4} />
|
|
12
|
+
* </Kpi.Content>
|
|
13
|
+
* <Kpi.Chart data={month} />
|
|
14
|
+
* </Kpi>
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* ## The number is the message
|
|
18
|
+
*
|
|
19
|
+
* A metric card is not a chart with a caption. The parts are sized and ordered
|
|
20
|
+
* round the number: a title that stays quiet, a value that does not, a trend
|
|
21
|
+
* that says which way and by how much, and a sparkline that is allowed to be
|
|
22
|
+
* small because nobody reads values off it.
|
|
23
|
+
*
|
|
24
|
+
* ## The trend is given a number
|
|
25
|
+
*
|
|
26
|
+
* `Kpi.Trend` takes the change as a number and decides the sign and the colour
|
|
27
|
+
* itself, so a minus sign and a red cannot fall out of step at the call site.
|
|
28
|
+
* Where a rise is the bad news, such as churn, latency or cost, say so with
|
|
29
|
+
* `goodDirection="down"` and the colour follows the meaning and not the sign.
|
|
30
|
+
*
|
|
31
|
+
* ## The same parts as the app
|
|
32
|
+
*
|
|
33
|
+
* The parts and their names are the app library's Kpi, so a metric written for
|
|
34
|
+
* one reads the same in the other. Two things differ because the platform
|
|
35
|
+
* does: the chart takes a plain list of numbers, and colours are named by the
|
|
36
|
+
* theme's roles as everywhere in this collection.
|
|
37
|
+
*
|
|
38
|
+
* ## Not a client module
|
|
39
|
+
*
|
|
40
|
+
* It holds no state and handles no events, so there is no `'use client'` at
|
|
41
|
+
* the top. A Server Component can render it, dotted parts and all, and it
|
|
42
|
+
* adds nothing to the JavaScript a page sends.
|
|
43
|
+
*/
|
|
44
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
45
|
+
/** Which of the theme's roles a chart or a bar is drawn in. */
|
|
46
|
+
export type KpiColor = 'primary' | 'info' | 'success' | 'warning' | 'destructive';
|
|
47
|
+
/** Which direction of movement is the good news for this metric. */
|
|
48
|
+
export type KpiGoodDirection = 'up' | 'down' | 'none';
|
|
49
|
+
/** How a part is coloured once the metric's polarity has been applied. */
|
|
50
|
+
export type KpiTone = 'good' | 'bad' | 'flat' | 'neutral';
|
|
51
|
+
export interface KpiProps extends ComponentProps<'div'> {
|
|
52
|
+
/** Draws the metric as a card of its own. Leave it off inside a frame or a group. */
|
|
53
|
+
surface?: boolean;
|
|
54
|
+
}
|
|
55
|
+
declare function KpiRoot({ surface, className, ...props }: KpiProps): import("react").JSX.Element;
|
|
56
|
+
declare namespace KpiRoot {
|
|
57
|
+
var displayName: string;
|
|
58
|
+
}
|
|
59
|
+
export interface KpiHeaderProps extends ComponentProps<'div'> {
|
|
60
|
+
}
|
|
61
|
+
/** The row over the number: an icon, the title, and anything at the end. */
|
|
62
|
+
declare function KpiHeader({ className, ...props }: KpiHeaderProps): import("react").JSX.Element;
|
|
63
|
+
declare namespace KpiHeader {
|
|
64
|
+
var displayName: string;
|
|
65
|
+
}
|
|
66
|
+
export interface KpiIconProps extends ComponentProps<'span'> {
|
|
67
|
+
/** How the tile is coloured: by good or bad news, or left neutral. */
|
|
68
|
+
tone?: KpiTone;
|
|
69
|
+
}
|
|
70
|
+
/** A small tile for an icon, at the start of the header. */
|
|
71
|
+
declare function KpiIcon({ tone, className, ...props }: KpiIconProps): import("react").JSX.Element;
|
|
72
|
+
declare namespace KpiIcon {
|
|
73
|
+
var displayName: string;
|
|
74
|
+
}
|
|
75
|
+
export interface KpiTitleProps extends ComponentProps<'div'> {
|
|
76
|
+
}
|
|
77
|
+
/** What the number measures. */
|
|
78
|
+
declare function KpiTitle({ className, ...props }: KpiTitleProps): import("react").JSX.Element;
|
|
79
|
+
declare namespace KpiTitle {
|
|
80
|
+
var displayName: string;
|
|
81
|
+
}
|
|
82
|
+
export interface KpiActionsProps extends ComponentProps<'div'> {
|
|
83
|
+
}
|
|
84
|
+
/** Whatever belongs at the end of the header: a period, a badge, a button. */
|
|
85
|
+
declare function KpiActions({ className, ...props }: KpiActionsProps): import("react").JSX.Element;
|
|
86
|
+
declare namespace KpiActions {
|
|
87
|
+
var displayName: string;
|
|
88
|
+
}
|
|
89
|
+
export interface KpiContentProps extends ComponentProps<'div'> {
|
|
90
|
+
/** `inline` puts a chart beside the number in a fixed box; `below` leaves it under everything. */
|
|
91
|
+
layout?: 'below' | 'inline';
|
|
92
|
+
}
|
|
93
|
+
/** The row the number is in, with a trend or a chart at its other end. */
|
|
94
|
+
declare function KpiContent({ layout, className, ...props }: KpiContentProps): import("react").JSX.Element;
|
|
95
|
+
declare namespace KpiContent {
|
|
96
|
+
var displayName: string;
|
|
97
|
+
}
|
|
98
|
+
export interface KpiStatProps extends ComponentProps<'div'> {
|
|
99
|
+
}
|
|
100
|
+
/** Title, value and trend stacked as one block, for a row with a chart beside it. */
|
|
101
|
+
declare function KpiStat({ className, ...props }: KpiStatProps): import("react").JSX.Element;
|
|
102
|
+
declare namespace KpiStat {
|
|
103
|
+
var displayName: string;
|
|
104
|
+
}
|
|
105
|
+
export interface KpiValueProps extends ComponentProps<'div'> {
|
|
106
|
+
}
|
|
107
|
+
/** The number. */
|
|
108
|
+
declare function KpiValue({ className, ...props }: KpiValueProps): import("react").JSX.Element;
|
|
109
|
+
declare namespace KpiValue {
|
|
110
|
+
var displayName: string;
|
|
111
|
+
}
|
|
112
|
+
export interface KpiTrendProps extends Omit<ComponentProps<'span'>, 'children'> {
|
|
113
|
+
/** The change. Its sign decides the direction. */
|
|
114
|
+
value: number;
|
|
115
|
+
/** Writes the change. Defaults to a signed percentage with one decimal: "+7.8%". */
|
|
116
|
+
format?: (value: number) => string;
|
|
117
|
+
/** Which direction is the good news. `down` for churn, latency, cost; `none` for no colour. */
|
|
118
|
+
goodDirection?: KpiGoodDirection;
|
|
119
|
+
/** `text` is a line of colour; `badge` puts a pill and an arrow round it. */
|
|
120
|
+
variant?: 'text' | 'badge';
|
|
121
|
+
/** Quiet words after the change: "this month". */
|
|
122
|
+
caption?: string;
|
|
123
|
+
/** A change this small or smaller is drawn as no change. */
|
|
124
|
+
threshold?: number;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The change. Colour comes from what the movement means, not from its sign: a
|
|
128
|
+
* fall in churn is good news and is drawn as good news.
|
|
129
|
+
*/
|
|
130
|
+
declare function KpiTrend({ value, format, goodDirection, variant, caption, threshold, className, ...props }: KpiTrendProps): import("react").JSX.Element;
|
|
131
|
+
declare namespace KpiTrend {
|
|
132
|
+
var displayName: string;
|
|
133
|
+
}
|
|
134
|
+
export interface KpiChartProps extends Omit<ComponentProps<'svg'>, 'children' | 'color'> {
|
|
135
|
+
/** The values, oldest first. */
|
|
136
|
+
data: number[];
|
|
137
|
+
/** Which of the theme's roles the line is drawn in. */
|
|
138
|
+
color?: KpiColor;
|
|
139
|
+
/** Fills under the line with a wash of the same colour. */
|
|
140
|
+
filled?: boolean;
|
|
141
|
+
/** How tall the chart is, in pixels. */
|
|
142
|
+
height?: number;
|
|
143
|
+
/**
|
|
144
|
+
* A fixed box for the end of a `layout="inline"` row. Charts beside numbers
|
|
145
|
+
* of different lengths are then all one width, and line up down a column.
|
|
146
|
+
*/
|
|
147
|
+
inline?: boolean;
|
|
148
|
+
/** How thick the line is, in pixels. */
|
|
149
|
+
strokeWidth?: number;
|
|
150
|
+
}
|
|
151
|
+
/** The sparkline: the shape of the number over time, with no axis and no values. */
|
|
152
|
+
declare function KpiChart({ data, color, filled, height, inline, strokeWidth, className, style, ...props }: KpiChartProps): import("react").JSX.Element;
|
|
153
|
+
declare namespace KpiChart {
|
|
154
|
+
var displayName: string;
|
|
155
|
+
}
|
|
156
|
+
export interface KpiProgressProps extends Omit<ComponentProps<'div'>, 'children' | 'color'> {
|
|
157
|
+
/** How far along. */
|
|
158
|
+
value: number;
|
|
159
|
+
/** What `value` is out of. */
|
|
160
|
+
maxValue?: number;
|
|
161
|
+
/** Words at the start of the row over the bar: "of 5,000". */
|
|
162
|
+
label?: ReactNode;
|
|
163
|
+
/** Writes the percentage at the end of the row over the bar. */
|
|
164
|
+
showValueLabel?: boolean;
|
|
165
|
+
/** Which of the theme's roles the bar is drawn in. */
|
|
166
|
+
color?: KpiColor;
|
|
167
|
+
}
|
|
168
|
+
/** Progress towards a target, under the number. */
|
|
169
|
+
declare function KpiProgress({ value, maxValue, label, showValueLabel, color, className, ...props }: KpiProgressProps): import("react").JSX.Element;
|
|
170
|
+
declare namespace KpiProgress {
|
|
171
|
+
var displayName: string;
|
|
172
|
+
}
|
|
173
|
+
export interface KpiFooterProps extends ComponentProps<'div'> {
|
|
174
|
+
}
|
|
175
|
+
/** A quiet line under everything: when it was measured, how long is left. */
|
|
176
|
+
declare function KpiFooter({ className, ...props }: KpiFooterProps): import("react").JSX.Element;
|
|
177
|
+
declare namespace KpiFooter {
|
|
178
|
+
var displayName: string;
|
|
179
|
+
}
|
|
180
|
+
export interface KpiSeparatorProps extends ComponentProps<'div'> {
|
|
181
|
+
}
|
|
182
|
+
/** A rule between two parts of one card. */
|
|
183
|
+
declare function KpiSeparator({ className, ...props }: KpiSeparatorProps): import("react").JSX.Element;
|
|
184
|
+
declare namespace KpiSeparator {
|
|
185
|
+
var displayName: string;
|
|
186
|
+
}
|
|
187
|
+
export interface KpiGroupProps extends ComponentProps<'div'> {
|
|
188
|
+
/** Side by side with upright rules, or stacked with level ones. */
|
|
189
|
+
orientation?: 'horizontal' | 'vertical';
|
|
190
|
+
}
|
|
191
|
+
/** Several metrics as one panel, with a rule between each and the next. */
|
|
192
|
+
declare function KpiGroup({ orientation, className, ...props }: KpiGroupProps): import("react").JSX.Element;
|
|
193
|
+
declare namespace KpiGroup {
|
|
194
|
+
var displayName: string;
|
|
195
|
+
}
|
|
196
|
+
export declare const Kpi: typeof KpiRoot & {
|
|
197
|
+
Header: typeof KpiHeader;
|
|
198
|
+
Icon: typeof KpiIcon;
|
|
199
|
+
Title: typeof KpiTitle;
|
|
200
|
+
Actions: typeof KpiActions;
|
|
201
|
+
Content: typeof KpiContent;
|
|
202
|
+
Stat: typeof KpiStat;
|
|
203
|
+
Value: typeof KpiValue;
|
|
204
|
+
Trend: typeof KpiTrend;
|
|
205
|
+
Chart: typeof KpiChart;
|
|
206
|
+
Progress: typeof KpiProgress;
|
|
207
|
+
Footer: typeof KpiFooter;
|
|
208
|
+
Separator: typeof KpiSeparator;
|
|
209
|
+
Group: typeof KpiGroup;
|
|
210
|
+
};
|
|
211
|
+
export { KpiHeader, KpiIcon, KpiTitle, KpiActions, KpiContent, KpiStat, KpiValue, KpiTrend, KpiChart, KpiProgress, KpiFooter, KpiSeparator, KpiGroup, };
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { tv } from 'tailwind-variants';
|
|
3
|
+
const kpiVariants = tv({
|
|
4
|
+
slots: {
|
|
5
|
+
root: 'flex w-full flex-col gap-2 text-foreground',
|
|
6
|
+
// A title in a header takes the room between the icon and the actions.
|
|
7
|
+
header: 'flex items-center gap-2 [&>[data-kpi-title]]:flex-1',
|
|
8
|
+
icon: 'flex size-8 shrink-0 items-center justify-center rounded-xl [&_svg]:size-4',
|
|
9
|
+
title: 'min-w-0 truncate text-sm font-medium text-muted-foreground',
|
|
10
|
+
actions: 'ms-auto flex shrink-0 items-center gap-2',
|
|
11
|
+
content: 'flex items-end justify-between gap-3',
|
|
12
|
+
// The stacked title, value and trend, tight enough to read as one thing.
|
|
13
|
+
stat: 'flex min-w-0 flex-1 flex-col items-start gap-1',
|
|
14
|
+
value: 'text-2xl font-bold tabular-nums tracking-tight text-foreground',
|
|
15
|
+
trend: 'inline-flex shrink-0 items-center gap-1 font-medium',
|
|
16
|
+
caption: 'font-normal text-muted-foreground',
|
|
17
|
+
chart: 'block w-full overflow-visible',
|
|
18
|
+
progress: 'flex w-full flex-col gap-2',
|
|
19
|
+
progressHeader: 'flex items-baseline justify-between gap-3 text-sm',
|
|
20
|
+
progressTrack: 'h-2 w-full overflow-hidden rounded-full bg-[color-mix(in_oklab,var(--pu-color)_16%,transparent)]',
|
|
21
|
+
progressFill: 'h-full rounded-full bg-[var(--pu-color)]',
|
|
22
|
+
footer: 'flex items-center gap-2 text-sm text-muted-foreground',
|
|
23
|
+
separator: 'h-px w-full shrink-0 bg-border',
|
|
24
|
+
group: 'flex w-full divide-border',
|
|
25
|
+
},
|
|
26
|
+
variants: {
|
|
27
|
+
surface: {
|
|
28
|
+
true: { root: 'rounded-3xl border bg-card p-5 text-card-foreground' },
|
|
29
|
+
},
|
|
30
|
+
color: {
|
|
31
|
+
primary: { chart: '[--pu-color:var(--primary)]', progress: '[--pu-color:var(--primary)]' },
|
|
32
|
+
info: { chart: '[--pu-color:var(--info)]', progress: '[--pu-color:var(--info)]' },
|
|
33
|
+
success: { chart: '[--pu-color:var(--success)]', progress: '[--pu-color:var(--success)]' },
|
|
34
|
+
warning: { chart: '[--pu-color:var(--warning)]', progress: '[--pu-color:var(--warning)]' },
|
|
35
|
+
destructive: {
|
|
36
|
+
chart: '[--pu-color:var(--destructive)]',
|
|
37
|
+
progress: '[--pu-color:var(--destructive)]',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
/** Which way the number moved, after the metric's own polarity. */
|
|
41
|
+
tone: {
|
|
42
|
+
good: {
|
|
43
|
+
trend: 'text-[var(--success)]',
|
|
44
|
+
icon: 'bg-[color-mix(in_oklab,var(--success)_14%,transparent)] text-[var(--success)]',
|
|
45
|
+
},
|
|
46
|
+
bad: {
|
|
47
|
+
trend: 'text-destructive',
|
|
48
|
+
icon: 'bg-[color-mix(in_oklab,var(--destructive)_14%,transparent)] text-destructive',
|
|
49
|
+
},
|
|
50
|
+
flat: { trend: 'text-muted-foreground', icon: 'bg-muted text-muted-foreground' },
|
|
51
|
+
neutral: { trend: 'text-muted-foreground', icon: 'bg-secondary text-muted-foreground' },
|
|
52
|
+
},
|
|
53
|
+
/*
|
|
54
|
+
* `text` is the quieter of the two and the default: a line of colour under
|
|
55
|
+
* the number, which is where the eye already is. `badge` puts a pill round
|
|
56
|
+
* it for a card dense enough that a bare line of colour gets lost.
|
|
57
|
+
*/
|
|
58
|
+
trendVariant: {
|
|
59
|
+
text: { trend: 'text-sm' },
|
|
60
|
+
badge: { trend: 'rounded-full px-2 py-0.5 text-xs' },
|
|
61
|
+
},
|
|
62
|
+
/** Where the chart sits relative to the number. */
|
|
63
|
+
layout: {
|
|
64
|
+
below: {},
|
|
65
|
+
// Beside the number: the text takes the width and the chart a fixed
|
|
66
|
+
// box on the end, so charts line up down a column of cards.
|
|
67
|
+
inline: { content: 'items-center gap-4' },
|
|
68
|
+
},
|
|
69
|
+
orientation: {
|
|
70
|
+
// The padding is on the children and not the rule, so the rule reaches
|
|
71
|
+
// the full height or width of the panel.
|
|
72
|
+
horizontal: {
|
|
73
|
+
group: 'flex-row items-stretch divide-x *:min-w-0 *:flex-1 *:px-4 *:first:ps-0 *:last:pe-0',
|
|
74
|
+
},
|
|
75
|
+
vertical: { group: 'flex-col divide-y *:py-4 *:first:pt-0 *:last:pb-0' },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
compoundVariants: [
|
|
79
|
+
{
|
|
80
|
+
trendVariant: 'badge',
|
|
81
|
+
tone: 'good',
|
|
82
|
+
class: { trend: 'bg-[color-mix(in_oklab,var(--success)_14%,transparent)]' },
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
trendVariant: 'badge',
|
|
86
|
+
tone: 'bad',
|
|
87
|
+
class: { trend: 'bg-[color-mix(in_oklab,var(--destructive)_14%,transparent)]' },
|
|
88
|
+
},
|
|
89
|
+
{ trendVariant: 'badge', tone: 'flat', class: { trend: 'bg-muted' } },
|
|
90
|
+
{ trendVariant: 'badge', tone: 'neutral', class: { trend: 'bg-muted' } },
|
|
91
|
+
],
|
|
92
|
+
defaultVariants: { tone: 'neutral', trendVariant: 'text', layout: 'below', color: 'primary' },
|
|
93
|
+
});
|
|
94
|
+
function KpiRoot({ surface = false, className, ...props }) {
|
|
95
|
+
return _jsx("div", { className: kpiVariants({ surface }).root({ className }), ...props });
|
|
96
|
+
}
|
|
97
|
+
KpiRoot.displayName = 'Kpi';
|
|
98
|
+
/** The row over the number: an icon, the title, and anything at the end. */
|
|
99
|
+
function KpiHeader({ className, ...props }) {
|
|
100
|
+
return _jsx("div", { className: kpiVariants().header({ className }), ...props });
|
|
101
|
+
}
|
|
102
|
+
KpiHeader.displayName = 'Kpi.Header';
|
|
103
|
+
/** A small tile for an icon, at the start of the header. */
|
|
104
|
+
function KpiIcon({ tone = 'neutral', className, ...props }) {
|
|
105
|
+
return _jsx("span", { "aria-hidden": "true", className: kpiVariants({ tone }).icon({ className }), ...props });
|
|
106
|
+
}
|
|
107
|
+
KpiIcon.displayName = 'Kpi.Icon';
|
|
108
|
+
/** What the number measures. */
|
|
109
|
+
function KpiTitle({ className, ...props }) {
|
|
110
|
+
return _jsx("div", { "data-kpi-title": "", className: kpiVariants().title({ className }), ...props });
|
|
111
|
+
}
|
|
112
|
+
KpiTitle.displayName = 'Kpi.Title';
|
|
113
|
+
/** Whatever belongs at the end of the header: a period, a badge, a button. */
|
|
114
|
+
function KpiActions({ className, ...props }) {
|
|
115
|
+
return _jsx("div", { className: kpiVariants().actions({ className }), ...props });
|
|
116
|
+
}
|
|
117
|
+
KpiActions.displayName = 'Kpi.Actions';
|
|
118
|
+
/** The row the number is in, with a trend or a chart at its other end. */
|
|
119
|
+
function KpiContent({ layout = 'below', className, ...props }) {
|
|
120
|
+
return _jsx("div", { className: kpiVariants({ layout }).content({ className }), ...props });
|
|
121
|
+
}
|
|
122
|
+
KpiContent.displayName = 'Kpi.Content';
|
|
123
|
+
/** Title, value and trend stacked as one block, for a row with a chart beside it. */
|
|
124
|
+
function KpiStat({ className, ...props }) {
|
|
125
|
+
return _jsx("div", { className: kpiVariants().stat({ className }), ...props });
|
|
126
|
+
}
|
|
127
|
+
KpiStat.displayName = 'Kpi.Stat';
|
|
128
|
+
/** The number. */
|
|
129
|
+
function KpiValue({ className, ...props }) {
|
|
130
|
+
return _jsx("div", { className: kpiVariants().value({ className }), ...props });
|
|
131
|
+
}
|
|
132
|
+
KpiValue.displayName = 'Kpi.Value';
|
|
133
|
+
/**
|
|
134
|
+
* The change. Colour comes from what the movement means, not from its sign: a
|
|
135
|
+
* fall in churn is good news and is drawn as good news.
|
|
136
|
+
*/
|
|
137
|
+
function KpiTrend({ value, format, goodDirection = 'up', variant = 'text', caption, threshold = 0, className, ...props }) {
|
|
138
|
+
const flat = Math.abs(value) <= threshold;
|
|
139
|
+
const rising = value > 0;
|
|
140
|
+
const tone = flat
|
|
141
|
+
? 'flat'
|
|
142
|
+
: goodDirection === 'none'
|
|
143
|
+
? 'neutral'
|
|
144
|
+
: (rising && goodDirection === 'up') || (!rising && goodDirection === 'down')
|
|
145
|
+
? 'good'
|
|
146
|
+
: 'bad';
|
|
147
|
+
const slots = kpiVariants({ tone, trendVariant: variant });
|
|
148
|
+
const label = format
|
|
149
|
+
? format(value)
|
|
150
|
+
: // A true minus sign and not a hyphen: at this size a hyphen reads as a
|
|
151
|
+
// dash between two words, and the sign is half the meaning.
|
|
152
|
+
`${rising ? '+' : value < 0 ? '−' : ''}${Math.abs(value).toFixed(1)}%`;
|
|
153
|
+
const spoken = `${flat ? 'No change' : rising ? 'Up' : 'Down'} ${Math.abs(value).toFixed(1)} percent${caption ? `, ${caption}` : ''}`;
|
|
154
|
+
return (_jsxs("span", { role: "img", "aria-label": spoken, className: slots.trend({ className }), ...props, children: [variant === 'badge' ? (_jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", className: "size-3", children: flat ? (_jsx("path", { d: "M5 12h14" })) : rising ? (_jsx("path", { d: "M12 19V5M5 12l7-7 7 7" })) : (_jsx("path", { d: "M12 5v14M19 12l-7 7-7-7" })) })) : null, _jsxs("span", { "aria-hidden": "true", children: [label, caption ? _jsxs("span", { className: slots.caption(), children: [" ", caption] }) : null] })] }));
|
|
155
|
+
}
|
|
156
|
+
KpiTrend.displayName = 'Kpi.Trend';
|
|
157
|
+
/** The box an inline chart is given, in pixels. */
|
|
158
|
+
const INLINE_WIDTH = 128;
|
|
159
|
+
const INLINE_HEIGHT = 56;
|
|
160
|
+
/** The grid the line is drawn on. It is stretched to whatever size the chart is. */
|
|
161
|
+
const GRID_WIDTH = 100;
|
|
162
|
+
const GRID_HEIGHT = 40;
|
|
163
|
+
const round = (value) => Number(value.toFixed(2));
|
|
164
|
+
/** A smooth line through the points, as a path. */
|
|
165
|
+
function linePath(points) {
|
|
166
|
+
if (points.length === 0)
|
|
167
|
+
return '';
|
|
168
|
+
const [first, ...rest] = points;
|
|
169
|
+
if (!first)
|
|
170
|
+
return '';
|
|
171
|
+
let path = `M${round(first[0])} ${round(first[1])}`;
|
|
172
|
+
rest.forEach((point, index) => {
|
|
173
|
+
const before = points[index] ?? point;
|
|
174
|
+
// Control points a third of the way along, level with each end, which
|
|
175
|
+
// rounds the corners without the line ever overshooting a value.
|
|
176
|
+
const reach = (point[0] - before[0]) / 3;
|
|
177
|
+
path += ` C${round(before[0] + reach)} ${round(before[1])} ${round(point[0] - reach)} ${round(point[1])} ${round(point[0])} ${round(point[1])}`;
|
|
178
|
+
});
|
|
179
|
+
return path;
|
|
180
|
+
}
|
|
181
|
+
/** The sparkline: the shape of the number over time, with no axis and no values. */
|
|
182
|
+
function KpiChart({ data, color = 'primary', filled = false, height, inline = false, strokeWidth = 2, className, style, ...props }) {
|
|
183
|
+
const slots = kpiVariants({ color });
|
|
184
|
+
const low = Math.min(...data);
|
|
185
|
+
const high = Math.max(...data);
|
|
186
|
+
const span = high - low || 1;
|
|
187
|
+
// A little room top and bottom, so the line's own thickness is not cut off
|
|
188
|
+
// at its highest and lowest values.
|
|
189
|
+
const pad = 3;
|
|
190
|
+
const points = data.map((value, index) => [
|
|
191
|
+
data.length === 1 ? GRID_WIDTH / 2 : (index / (data.length - 1)) * GRID_WIDTH,
|
|
192
|
+
pad + (1 - (value - low) / span) * (GRID_HEIGHT - pad * 2),
|
|
193
|
+
]);
|
|
194
|
+
const line = linePath(points);
|
|
195
|
+
return (_jsxs("svg", { role: "img", "aria-label": "Trend", viewBox: `0 0 ${GRID_WIDTH} ${GRID_HEIGHT}`, preserveAspectRatio: "none", className: slots.chart({ className }), style: {
|
|
196
|
+
height: height ?? (inline ? INLINE_HEIGHT : 56),
|
|
197
|
+
...(inline ? { width: INLINE_WIDTH, flex: 'none' } : null),
|
|
198
|
+
...style,
|
|
199
|
+
}, ...props, children: [filled && line ? (_jsx("path", { d: `${line} L${GRID_WIDTH} ${GRID_HEIGHT} L0 ${GRID_HEIGHT} Z`, fill: "var(--pu-color)", opacity: "0.12" })) : null, _jsx("path", { d: line, fill: "none", stroke: "var(--pu-color)", strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round",
|
|
200
|
+
// The grid is stretched to the chart's size. Without this the line
|
|
201
|
+
// would be stretched with it, thick across and thin up and down.
|
|
202
|
+
vectorEffect: "non-scaling-stroke" })] }));
|
|
203
|
+
}
|
|
204
|
+
KpiChart.displayName = 'Kpi.Chart';
|
|
205
|
+
/** Progress towards a target, under the number. */
|
|
206
|
+
function KpiProgress({ value, maxValue = 100, label, showValueLabel = false, color = 'primary', className, ...props }) {
|
|
207
|
+
const slots = kpiVariants({ color });
|
|
208
|
+
const share = Math.min(1, Math.max(0, maxValue === 0 ? 0 : value / maxValue));
|
|
209
|
+
const percent = Math.round(share * 100);
|
|
210
|
+
return (_jsxs("div", { className: slots.progress({ className }), ...props, children: [label !== undefined || showValueLabel ? (_jsxs("div", { className: slots.progressHeader(), children: [_jsx("span", { className: "font-medium", children: label }), showValueLabel ? _jsxs("span", { className: "text-muted-foreground", children: [percent, "%"] }) : null] })) : null, _jsx("div", { role: "progressbar", "aria-valuemin": 0, "aria-valuemax": maxValue, "aria-valuenow": value, "aria-label": typeof label === 'string' ? label : 'Progress', className: slots.progressTrack(), children: _jsx("div", { className: slots.progressFill(), style: { width: `${percent}%` } }) })] }));
|
|
211
|
+
}
|
|
212
|
+
KpiProgress.displayName = 'Kpi.Progress';
|
|
213
|
+
/** A quiet line under everything: when it was measured, how long is left. */
|
|
214
|
+
function KpiFooter({ className, ...props }) {
|
|
215
|
+
return _jsx("div", { className: kpiVariants().footer({ className }), ...props });
|
|
216
|
+
}
|
|
217
|
+
KpiFooter.displayName = 'Kpi.Footer';
|
|
218
|
+
/** A rule between two parts of one card. */
|
|
219
|
+
function KpiSeparator({ className, ...props }) {
|
|
220
|
+
return _jsx("div", { role: "separator", className: kpiVariants().separator({ className }), ...props });
|
|
221
|
+
}
|
|
222
|
+
KpiSeparator.displayName = 'Kpi.Separator';
|
|
223
|
+
/** Several metrics as one panel, with a rule between each and the next. */
|
|
224
|
+
function KpiGroup({ orientation = 'horizontal', className, ...props }) {
|
|
225
|
+
return _jsx("div", { className: kpiVariants({ orientation }).group({ className }), ...props });
|
|
226
|
+
}
|
|
227
|
+
KpiGroup.displayName = 'Kpi.Group';
|
|
228
|
+
export const Kpi = Object.assign(KpiRoot, {
|
|
229
|
+
Header: KpiHeader,
|
|
230
|
+
Icon: KpiIcon,
|
|
231
|
+
Title: KpiTitle,
|
|
232
|
+
Actions: KpiActions,
|
|
233
|
+
Content: KpiContent,
|
|
234
|
+
Stat: KpiStat,
|
|
235
|
+
Value: KpiValue,
|
|
236
|
+
Trend: KpiTrend,
|
|
237
|
+
Chart: KpiChart,
|
|
238
|
+
Progress: KpiProgress,
|
|
239
|
+
Footer: KpiFooter,
|
|
240
|
+
Separator: KpiSeparator,
|
|
241
|
+
Group: KpiGroup,
|
|
242
|
+
});
|
|
243
|
+
export { KpiHeader, KpiIcon, KpiTitle, KpiActions, KpiContent, KpiStat, KpiValue, KpiTrend, KpiChart, KpiProgress, KpiFooter, KpiSeparator, KpiGroup, };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode } from 'react';
|
|
2
|
+
/** Which of the theme's roles a part is drawn in. */
|
|
3
|
+
export type LaneBoardColor = 'primary' | 'info' | 'success' | 'warning' | 'destructive';
|
|
4
|
+
/** A card is anything with an id that is its own across the whole board. */
|
|
5
|
+
export interface LaneBoardCard {
|
|
6
|
+
id: string;
|
|
7
|
+
}
|
|
8
|
+
export interface LaneBoardLane<T extends LaneBoardCard = LaneBoardCard> {
|
|
9
|
+
id: string;
|
|
10
|
+
/** Written at the head of the lane. */
|
|
11
|
+
title: ReactNode;
|
|
12
|
+
cards: T[];
|
|
13
|
+
/** One of the theme's roles, drawn as a dot before the title. */
|
|
14
|
+
color?: LaneBoardColor;
|
|
15
|
+
/**
|
|
16
|
+
* How many cards the lane is meant to hold. The count is written against
|
|
17
|
+
* it, and drawn as a warning once the lane holds more. It is advice: the
|
|
18
|
+
* board does not refuse a card. Do that in `onLanesChange`.
|
|
19
|
+
*/
|
|
20
|
+
limit?: number;
|
|
21
|
+
}
|
|
22
|
+
/** One card going from one place to another. */
|
|
23
|
+
export interface LaneBoardMove<T extends LaneBoardCard = LaneBoardCard> {
|
|
24
|
+
card: T;
|
|
25
|
+
from: {
|
|
26
|
+
lane: string;
|
|
27
|
+
index: number;
|
|
28
|
+
};
|
|
29
|
+
to: {
|
|
30
|
+
lane: string;
|
|
31
|
+
index: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export interface LaneBoardProps<T extends LaneBoardCard> extends Omit<ComponentProps<'div'>, 'children' | 'onChange'> {
|
|
35
|
+
/** The lanes and their cards, when the caller holds them. */
|
|
36
|
+
lanes?: LaneBoardLane<T>[];
|
|
37
|
+
/** The lanes to begin with, when the caller does not hold them. */
|
|
38
|
+
defaultLanes?: LaneBoardLane<T>[];
|
|
39
|
+
/** Called with the lanes as they should now be, when a card is set down somewhere new. */
|
|
40
|
+
onLanesChange?: (lanes: LaneBoardLane<T>[]) => void;
|
|
41
|
+
/** Called with the one card that moved, and where from and to. */
|
|
42
|
+
onMove?: (move: LaneBoardMove<T>) => void;
|
|
43
|
+
/** Draws the inside of a card. */
|
|
44
|
+
renderCard: (card: T, state: {
|
|
45
|
+
lane: LaneBoardLane<T>;
|
|
46
|
+
carried: boolean;
|
|
47
|
+
}) => ReactNode;
|
|
48
|
+
/** Draws something under a lane's cards, such as a button that adds one. */
|
|
49
|
+
renderLaneFooter?: (lane: LaneBoardLane<T>) => ReactNode;
|
|
50
|
+
/** What a screen reader calls a card, where its id would not mean much. */
|
|
51
|
+
cardName?: (card: T) => string;
|
|
52
|
+
/** What a screen reader calls the board. */
|
|
53
|
+
label?: string;
|
|
54
|
+
/**
|
|
55
|
+
* How tall the board is: a number of pixels, or any CSS length. Lanes fill
|
|
56
|
+
* it, and one with more cards than fit scrolls. `'auto'` lets the board
|
|
57
|
+
* grow with its longest lane.
|
|
58
|
+
*/
|
|
59
|
+
height?: number | string;
|
|
60
|
+
}
|
|
61
|
+
declare function LaneBoardRoot<T extends LaneBoardCard>({ lanes: controlled, defaultLanes, onLanesChange, onMove, renderCard, renderLaneFooter, cardName, label, height, className, style, ...props }: LaneBoardProps<T>): import("react").JSX.Element;
|
|
62
|
+
declare namespace LaneBoardRoot {
|
|
63
|
+
var displayName: string;
|
|
64
|
+
}
|
|
65
|
+
export interface LaneBoardTitleProps extends ComponentProps<'div'> {
|
|
66
|
+
}
|
|
67
|
+
/** A card's name. */
|
|
68
|
+
declare function LaneBoardTitle({ className, ...props }: LaneBoardTitleProps): import("react").JSX.Element;
|
|
69
|
+
declare namespace LaneBoardTitle {
|
|
70
|
+
var displayName: string;
|
|
71
|
+
}
|
|
72
|
+
export interface LaneBoardCodeProps extends ComponentProps<'div'> {
|
|
73
|
+
}
|
|
74
|
+
/** A card's reference, in a fixed-width face so a column of them lines up. */
|
|
75
|
+
declare function LaneBoardCode({ className, ...props }: LaneBoardCodeProps): import("react").JSX.Element;
|
|
76
|
+
declare namespace LaneBoardCode {
|
|
77
|
+
var displayName: string;
|
|
78
|
+
}
|
|
79
|
+
export interface LaneBoardRowProps extends ComponentProps<'div'> {
|
|
80
|
+
}
|
|
81
|
+
/** The last line of a card: something at each end. */
|
|
82
|
+
declare function LaneBoardRow({ className, ...props }: LaneBoardRowProps): import("react").JSX.Element;
|
|
83
|
+
declare namespace LaneBoardRow {
|
|
84
|
+
var displayName: string;
|
|
85
|
+
}
|
|
86
|
+
export interface LaneBoardStepsProps extends Omit<ComponentProps<'div'>, 'children' | 'color'> {
|
|
87
|
+
/** How many steps are done. */
|
|
88
|
+
value: number;
|
|
89
|
+
/** How many steps there are. */
|
|
90
|
+
max?: number;
|
|
91
|
+
/** Which of the theme's roles the done steps are drawn in. */
|
|
92
|
+
color?: LaneBoardColor;
|
|
93
|
+
/** What a screen reader calls it. */
|
|
94
|
+
label?: string;
|
|
95
|
+
}
|
|
96
|
+
/** How far along a card is, as a row of dashes with the done ones lit. */
|
|
97
|
+
declare function LaneBoardSteps({ value, max, color, label, className, ...props }: LaneBoardStepsProps): import("react").JSX.Element;
|
|
98
|
+
declare namespace LaneBoardSteps {
|
|
99
|
+
var displayName: string;
|
|
100
|
+
}
|
|
101
|
+
export interface LaneBoardTagProps extends Omit<ComponentProps<'span'>, 'color'> {
|
|
102
|
+
/** Which of the theme's roles the tag is drawn in. */
|
|
103
|
+
color?: LaneBoardColor;
|
|
104
|
+
}
|
|
105
|
+
/** A word in a pill, for a card's state. */
|
|
106
|
+
declare function LaneBoardTag({ color, className, ...props }: LaneBoardTagProps): import("react").JSX.Element;
|
|
107
|
+
declare namespace LaneBoardTag {
|
|
108
|
+
var displayName: string;
|
|
109
|
+
}
|
|
110
|
+
export interface LaneBoardMetaProps extends ComponentProps<'span'> {
|
|
111
|
+
}
|
|
112
|
+
/** A short figure at the end of a card's last line: a time, a count. */
|
|
113
|
+
declare function LaneBoardMeta({ className, ...props }: LaneBoardMetaProps): import("react").JSX.Element;
|
|
114
|
+
declare namespace LaneBoardMeta {
|
|
115
|
+
var displayName: string;
|
|
116
|
+
}
|
|
117
|
+
export declare const LaneBoard: typeof LaneBoardRoot & {
|
|
118
|
+
Title: typeof LaneBoardTitle;
|
|
119
|
+
Code: typeof LaneBoardCode;
|
|
120
|
+
Row: typeof LaneBoardRow;
|
|
121
|
+
Steps: typeof LaneBoardSteps;
|
|
122
|
+
Tag: typeof LaneBoardTag;
|
|
123
|
+
Meta: typeof LaneBoardMeta;
|
|
124
|
+
};
|
|
125
|
+
export { LaneBoardTitle, LaneBoardCode, LaneBoardRow, LaneBoardSteps, LaneBoardTag, LaneBoardMeta };
|