panelui-web 0.2.0 → 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.
@@ -25,7 +25,10 @@ const kpiVariants = tv({
25
25
  },
26
26
  variants: {
27
27
  surface: {
28
- true: { root: 'rounded-3xl border bg-card p-5 text-card-foreground' },
28
+ true: {
29
+ root: 'rounded-3xl border bg-card p-5 text-card-foreground',
30
+ group: 'rounded-3xl border bg-card p-5 text-card-foreground',
31
+ },
29
32
  },
30
33
  color: {
31
34
  primary: { chart: '[--pu-color:var(--primary)]', progress: '[--pu-color:var(--primary)]' },
@@ -221,8 +224,8 @@ function KpiSeparator({ className, ...props }) {
221
224
  }
222
225
  KpiSeparator.displayName = 'Kpi.Separator';
223
226
  /** 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 });
227
+ function KpiGroup({ orientation = 'horizontal', surface = false, className, ...props }) {
228
+ return _jsx("div", { className: kpiVariants({ orientation, surface }).group({ className }), ...props });
226
229
  }
227
230
  KpiGroup.displayName = 'Kpi.Group';
228
231
  export const Kpi = Object.assign(KpiRoot, {
@@ -114,6 +114,13 @@ declare function LaneBoardMeta({ className, ...props }: LaneBoardMetaProps): imp
114
114
  declare namespace LaneBoardMeta {
115
115
  var displayName: string;
116
116
  }
117
+ export interface LaneBoardAvatarProps extends ComponentProps<'span'> {
118
+ }
119
+ /** Who a card belongs to, as initials in a small circle. */
120
+ declare function LaneBoardAvatar({ className, ...props }: LaneBoardAvatarProps): import("react").JSX.Element;
121
+ declare namespace LaneBoardAvatar {
122
+ var displayName: string;
123
+ }
117
124
  export declare const LaneBoard: typeof LaneBoardRoot & {
118
125
  Title: typeof LaneBoardTitle;
119
126
  Code: typeof LaneBoardCode;
@@ -121,5 +128,6 @@ export declare const LaneBoard: typeof LaneBoardRoot & {
121
128
  Steps: typeof LaneBoardSteps;
122
129
  Tag: typeof LaneBoardTag;
123
130
  Meta: typeof LaneBoardMeta;
131
+ Avatar: typeof LaneBoardAvatar;
124
132
  };
125
- export { LaneBoardTitle, LaneBoardCode, LaneBoardRow, LaneBoardSteps, LaneBoardTag, LaneBoardMeta };
133
+ export { LaneBoardTitle, LaneBoardCode, LaneBoardRow, LaneBoardSteps, LaneBoardTag, LaneBoardMeta, LaneBoardAvatar, };
@@ -89,6 +89,7 @@ const laneBoardVariants = tv({
89
89
  step: 'h-1 w-4 rounded-full',
90
90
  tag: 'inline-flex items-center rounded-full border border-[color-mix(in_oklab,var(--pu-color)_35%,var(--card))] bg-[color-mix(in_oklab,var(--pu-color)_14%,transparent)] px-2 py-0.5 text-xs font-medium text-[var(--pu-color)]',
91
91
  meta: 'font-mono text-sm tabular-nums text-foreground',
92
+ avatar: 'flex size-6 shrink-0 items-center justify-center rounded-full bg-muted text-[0.625rem] font-semibold text-muted-foreground',
92
93
  },
93
94
  variants: {
94
95
  color: {
@@ -678,6 +679,11 @@ function LaneBoardMeta({ className, ...props }) {
678
679
  return _jsx("span", { className: laneBoardVariants().meta({ className }), ...props });
679
680
  }
680
681
  LaneBoardMeta.displayName = 'LaneBoard.Meta';
682
+ /** Who a card belongs to, as initials in a small circle. */
683
+ function LaneBoardAvatar({ className, ...props }) {
684
+ return _jsx("span", { "data-slot": "lane-board-avatar", className: laneBoardVariants().avatar({ className }), ...props });
685
+ }
686
+ LaneBoardAvatar.displayName = 'LaneBoard.Avatar';
681
687
  export const LaneBoard = Object.assign(LaneBoardRoot, {
682
688
  Title: LaneBoardTitle,
683
689
  Code: LaneBoardCode,
@@ -685,7 +691,8 @@ export const LaneBoard = Object.assign(LaneBoardRoot, {
685
691
  Steps: LaneBoardSteps,
686
692
  Tag: LaneBoardTag,
687
693
  Meta: LaneBoardMeta,
694
+ Avatar: LaneBoardAvatar,
688
695
  });
689
696
  /* By name as well, for a Server Component, which cannot dot into a client
690
697
  module. */
691
- export { LaneBoardTitle, LaneBoardCode, LaneBoardRow, LaneBoardSteps, LaneBoardTag, LaneBoardMeta };
698
+ export { LaneBoardTitle, LaneBoardCode, LaneBoardRow, LaneBoardSteps, LaneBoardTag, LaneBoardMeta, LaneBoardAvatar, };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The shapes NotFound draws, as grids of cells.
3
+ *
4
+ * A digit is drawn at 5 by 7 and scaled up three times, so every stroke is
5
+ * three cells wide. Kept whole that is a solid pixel digit; reduced to the
6
+ * cells on its edge it is a digit drawn in outline, two rows of dots with a
7
+ * gap between, which is the look a stroked numeral has at this size.
8
+ *
9
+ * Digits are drawn without diagonal strokes. Two blocks that meet only at a
10
+ * corner come apart when reduced to their edges, so a slanted stroke would
11
+ * outline as a stair of separate rings.
12
+ *
13
+ * The icons Scramble turns a digit into are drawn straight onto the same 15 by
14
+ * 21 grid, from circles and lines, so they arrive at exactly the size of the
15
+ * digit they replace and need no scaling of their own.
16
+ */
17
+ /** Cells across one glyph. */
18
+ export declare const GLYPH_COLUMNS = 15;
19
+ /** Cells down one glyph. */
20
+ export declare const GLYPH_ROWS = 21;
21
+ /** A glyph: which cells are on, row by row. */
22
+ export type Glyph = boolean[][];
23
+ /** A digit, solid or as its outline. */
24
+ export declare function digitGlyph(character: string, fill: 'outline' | 'solid'): Glyph | null;
25
+ /** The icons Scramble cycles through, in order. */
26
+ export declare const ICONS: readonly Glyph[];
@@ -0,0 +1,141 @@
1
+ /**
2
+ * The shapes NotFound draws, as grids of cells.
3
+ *
4
+ * A digit is drawn at 5 by 7 and scaled up three times, so every stroke is
5
+ * three cells wide. Kept whole that is a solid pixel digit; reduced to the
6
+ * cells on its edge it is a digit drawn in outline, two rows of dots with a
7
+ * gap between, which is the look a stroked numeral has at this size.
8
+ *
9
+ * Digits are drawn without diagonal strokes. Two blocks that meet only at a
10
+ * corner come apart when reduced to their edges, so a slanted stroke would
11
+ * outline as a stair of separate rings.
12
+ *
13
+ * The icons Scramble turns a digit into are drawn straight onto the same 15 by
14
+ * 21 grid, from circles and lines, so they arrive at exactly the size of the
15
+ * digit they replace and need no scaling of their own.
16
+ */
17
+ /** Cells across one glyph. */
18
+ export const GLYPH_COLUMNS = 15;
19
+ /** Cells down one glyph. */
20
+ export const GLYPH_ROWS = 21;
21
+ const SCALE = 3;
22
+ const DIGITS = {
23
+ '0': ['.###.', '#...#', '#...#', '#...#', '#...#', '#...#', '.###.'],
24
+ '1': ['.##..', '..#..', '..#..', '..#..', '..#..', '..#..', '.###.'],
25
+ '2': ['.###.', '#...#', '....#', '.###.', '#....', '#....', '#####'],
26
+ '3': ['####.', '....#', '....#', '.###.', '....#', '....#', '####.'],
27
+ '4': ['#..#.', '#..#.', '#..#.', '#####', '...#.', '...#.', '...#.'],
28
+ '5': ['#####', '#....', '####.', '....#', '....#', '....#', '####.'],
29
+ '6': ['.###.', '#....', '#....', '####.', '#...#', '#...#', '.###.'],
30
+ '7': ['#####', '....#', '....#', '...##', '...#.', '...#.', '...#.'],
31
+ '8': ['.###.', '#...#', '#...#', '.###.', '#...#', '#...#', '.###.'],
32
+ '9': ['.###.', '#...#', '#...#', '.####', '....#', '....#', '.###.'],
33
+ };
34
+ function empty() {
35
+ return Array.from({ length: GLYPH_ROWS }, () => Array(GLYPH_COLUMNS).fill(false));
36
+ }
37
+ /** A digit as a solid grid of cells, or `null` for a character there is no drawing of. */
38
+ function solidDigit(character) {
39
+ const rows = DIGITS[character];
40
+ if (!rows)
41
+ return null;
42
+ const glyph = empty();
43
+ for (let y = 0; y < GLYPH_ROWS; y += 1) {
44
+ for (let x = 0; x < GLYPH_COLUMNS; x += 1) {
45
+ glyph[y][x] = rows[Math.floor(y / SCALE)][Math.floor(x / SCALE)] === '#';
46
+ }
47
+ }
48
+ return glyph;
49
+ }
50
+ /**
51
+ * Only the cells of a glyph that touch a cell outside it, corners included.
52
+ * Counting corners keeps a diagonal stroke as a continuous band; counting
53
+ * only sides leaves it as a scatter of single dots.
54
+ */
55
+ function outlineOf(glyph) {
56
+ const on = (x, y) => glyph[y]?.[x] === true;
57
+ return glyph.map((row, y) => row.map((cell, x) => {
58
+ if (!cell)
59
+ return false;
60
+ for (let dy = -1; dy <= 1; dy += 1) {
61
+ for (let dx = -1; dx <= 1; dx += 1)
62
+ if (!on(x + dx, y + dy))
63
+ return true;
64
+ }
65
+ return false;
66
+ }));
67
+ }
68
+ /** A digit, solid or as its outline. */
69
+ export function digitGlyph(character, fill) {
70
+ const solid = STROKED[character] ?? solidDigit(character);
71
+ if (!solid)
72
+ return null;
73
+ return fill === 'solid' ? solid : outlineOf(solid);
74
+ }
75
+ /* -------------------------------------------------------------------------- */
76
+ /* Icons */
77
+ /* -------------------------------------------------------------------------- */
78
+ /** Distance from a point to the segment from a to b. */
79
+ function toSegment(px, py, ax, ay, bx, by) {
80
+ const dx = bx - ax;
81
+ const dy = by - ay;
82
+ const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy || 1)));
83
+ return Math.hypot(px - (ax + dx * t), py - (ay + dy * t));
84
+ }
85
+ /** A glyph from a test of each cell's centre. */
86
+ function drawn(test) {
87
+ const glyph = empty();
88
+ for (let y = 0; y < GLYPH_ROWS; y += 1) {
89
+ for (let x = 0; x < GLYPH_COLUMNS; x += 1)
90
+ glyph[y][x] = test(x + 0.5, y + 0.5);
91
+ }
92
+ return glyph;
93
+ }
94
+ /** A ring: cells whose centre is within half a stroke of the circle. */
95
+ const ring = (x, y, cx, cy, r, stroke) => Math.abs(Math.hypot(x - cx, y - cy) - r) <= stroke / 2;
96
+ /** A question mark: a hook, a short stem, a dot. */
97
+ const question = drawn((x, y) => {
98
+ const angle = Math.atan2(y - 6.5, x - 7.5);
99
+ // The hook runs from the left of the circle, over the top, round to the
100
+ // lower right, and stops there so the stem can drop from it.
101
+ const onHook = ring(x, y, 7.5, 6.5, 4.6, 2) && (y < 6.5 || angle < Math.PI * 0.35);
102
+ const stem = toSegment(x, y, 9.4, 10.6, 7.5, 13.2) <= 1 || toSegment(x, y, 7.5, 13.2, 7.5, 14.5) <= 1;
103
+ const dot = Math.hypot(x - 7.5, y - 18) <= 1.3;
104
+ return onHook || stem || dot;
105
+ });
106
+ /** A magnifying glass, the lens high and the handle running to the lower right. */
107
+ const magnifier = drawn((x, y) => ring(x, y, 6.5, 7.5, 4.8, 2) || toSegment(x, y, 10.2, 11.6, 13.4, 17.6) <= 1.2);
108
+ /** A map pin: a ring with a dot in it, narrowing to a point. */
109
+ const pin = drawn((x, y) => {
110
+ const head = ring(x, y, 7.5, 7.5, 5, 2) && y < 10.5;
111
+ // The two sides run from the widest part of the ring down to the point.
112
+ const sides = toSegment(x, y, 3, 9.6, 7.5, 18.6) <= 1 || toSegment(x, y, 12, 9.6, 7.5, 18.6) <= 1;
113
+ const dot = Math.hypot(x - 7.5, y - 7.5) <= 1.5;
114
+ return head || sides || dot;
115
+ });
116
+ /*
117
+ * Digits drawn from strokes rather than scaled up, for the ones whose shape
118
+ * needs a diagonal. Each stroke is three cells wide, the same as a scaled-up
119
+ * pixel, so they sit with the others.
120
+ */
121
+ const stroke = (x, y, ax, ay, bx, by) => toSegment(x, y, ax, ay, bx, by) <= 1.5;
122
+ /** Distance from a point to an ellipse's outline, near enough for drawing at this size. */
123
+ const toEllipse = (x, y, cx, cy, rx, ry) => {
124
+ const nx = (x - cx) / rx;
125
+ const ny = (y - cy) / ry;
126
+ const r = Math.hypot(nx, ny);
127
+ // Scale the radial error back to cells by the radius in that direction.
128
+ const along = r === 0 ? Math.min(rx, ry) : Math.hypot(nx * rx, ny * ry) / r;
129
+ return Math.abs(r - 1) * along;
130
+ };
131
+ const STROKED = {
132
+ // An oval nought, not a box with its corners cut.
133
+ '0': drawn((x, y) => toEllipse(x, y, 7.5, 10.5, 6, 9) <= 1.5),
134
+ // A closed four: the diagonal runs from the top of the stem down to the
135
+ // crossbar, and the crossbar reaches a little past the stem.
136
+ '4': drawn((x, y) => stroke(x, y, 10.5, 1.5, 10.5, 19.5) ||
137
+ stroke(x, y, 10.5, 1.5, 1.5, 14.5) ||
138
+ stroke(x, y, 1.5, 14.5, 13.5, 14.5)),
139
+ };
140
+ /** The icons Scramble cycles through, in order. */
141
+ export const ICONS = [question, magnifier, pin];
@@ -0,0 +1,118 @@
1
+ /**
2
+ * NotFound — an error code drawn in pixels that come apart when the pointer
3
+ * reaches them, with a title, a line of help and a way out underneath.
4
+ *
5
+ * ```tsx
6
+ * <NotFound>
7
+ * <NotFound.Code code="404" effect="scatter" />
8
+ * <NotFound.Title>Nothing lives here</NotFound.Title>
9
+ * <NotFound.Description>The address may be mistyped, or the page has moved.</NotFound.Description>
10
+ * <NotFound.Actions>…</NotFound.Actions>
11
+ * </NotFound>
12
+ * ```
13
+ *
14
+ * ## A grid of cells, not a font
15
+ *
16
+ * Each digit is a small bitmap laid on a grid of cells, and every cell is
17
+ * drawn as a dot or a rounded square of its own size and strength. The sizes
18
+ * and strengths are fixed per cell from a seed, so the code has the uneven,
19
+ * hand-placed texture of pixel art and still looks the same on every render.
20
+ * Nothing depends on a typeface loading.
21
+ *
22
+ * ## One digit reacts at a time
23
+ *
24
+ * The digit nearest the pointer is the one that reacts, and it is put back
25
+ * when the pointer moves away. `effect` chooses how: its cells fly apart
26
+ * (`scatter`), drop out one by one (`dissolve`), or travel into a small icon
27
+ * and back (`scramble`). On a touch screen a tap does the same, and the digit
28
+ * is put back a moment after the finger lifts.
29
+ *
30
+ * ## Two colours, mixed by light
31
+ *
32
+ * Every cell is either the text colour, faint, or the accent. Which one it is
33
+ * depends on how much light falls on it: the highlighted digit is always
34
+ * partly lit, a band of light goes slowly round the whole code, and the
35
+ * pointer lights the cells near it and leaves them fading as it moves on. Each
36
+ * cell turns accent at its own level of light, so a shape in half light is
37
+ * dithered between the two colours, the way pixel art is shaded.
38
+ *
39
+ * ## What it costs
40
+ *
41
+ * The cells are drawn on a canvas, a few hundred of them in about sixteen
42
+ * fills. The band asks for 30 frames a second while the code is on screen and
43
+ * none once it is scrolled away; with `ambient={false}` a frame is only drawn
44
+ * while something is moving. The canvas reads its colours from the theme each
45
+ * time it draws, so a change of theme needs nothing from the page.
46
+ */
47
+ import { type ComponentProps } from 'react';
48
+ /** How the digit near the pointer reacts. */
49
+ export type NotFoundEffect = 'scatter' | 'dissolve' | 'scramble';
50
+ /** Which of the theme's roles the highlighted digit is drawn in. */
51
+ export type NotFoundColor = 'primary' | 'info' | 'success' | 'warning' | 'destructive';
52
+ export interface NotFoundProps extends ComponentProps<'div'> {
53
+ /** Draws a faint grid behind everything, fading out towards the edges. On by default. */
54
+ backdrop?: boolean;
55
+ }
56
+ /** The column the code, the title, the description and the actions stand in, centred. */
57
+ declare function NotFoundRoot({ backdrop, className, children, ...props }: NotFoundProps): import("react").JSX.Element;
58
+ declare namespace NotFoundRoot {
59
+ var displayName: string;
60
+ }
61
+ export interface NotFoundCodeProps extends Omit<ComponentProps<'div'>, 'children' | 'color'> {
62
+ /** The code to draw. Digits only; any other character leaves a blank the width of a digit. */
63
+ code?: string;
64
+ /** How the digit nearest the pointer reacts. */
65
+ effect?: NotFoundEffect;
66
+ /** Digits drawn with solid strokes three cells wide, or as their outline. */
67
+ fill?: 'outline' | 'solid';
68
+ /**
69
+ * A band of light that goes slowly round the code on its own. While it is
70
+ * on, the canvas draws about 30 frames a second when the code is on screen;
71
+ * turn it off and nothing is drawn until the pointer comes near.
72
+ */
73
+ ambient?: boolean;
74
+ /** The theme role of the accent: the highlighted digit, the band of light and the pointer's light. */
75
+ color?: NotFoundColor;
76
+ /**
77
+ * Which digit is lit in `color` even with nothing else lighting it, counting
78
+ * from 0. Defaults to the middle one. `null` lights none of them.
79
+ */
80
+ highlight?: number | null;
81
+ /** What a screen reader says for the code. Defaults to "Error" and the code. */
82
+ label?: string;
83
+ }
84
+ /** The code, drawn in cells on a canvas. Its box keeps the code's shape before anything is drawn. */
85
+ declare function NotFoundCode({ code, effect, fill, color, ambient, highlight, label, className, style, ...props }: NotFoundCodeProps): import("react").JSX.Element;
86
+ declare namespace NotFoundCode {
87
+ var displayName: string;
88
+ }
89
+ export interface NotFoundTitleProps extends ComponentProps<'h1'> {
90
+ /** The heading level. A page that is only this one message wants `h1`. */
91
+ as?: 'h1' | 'h2' | 'h3';
92
+ }
93
+ /** What happened, in a few words. */
94
+ declare function NotFoundTitle({ as: Tag, className, ...props }: NotFoundTitleProps): import("react").JSX.Element;
95
+ declare namespace NotFoundTitle {
96
+ var displayName: string;
97
+ }
98
+ export interface NotFoundDescriptionProps extends ComponentProps<'p'> {
99
+ }
100
+ /** A line on what to do next. */
101
+ declare function NotFoundDescription({ className, ...props }: NotFoundDescriptionProps): import("react").JSX.Element;
102
+ declare namespace NotFoundDescription {
103
+ var displayName: string;
104
+ }
105
+ export interface NotFoundActionsProps extends ComponentProps<'div'> {
106
+ }
107
+ /** The ways out: links or buttons in a row that wraps on a narrow screen. */
108
+ declare function NotFoundActions({ className, ...props }: NotFoundActionsProps): import("react").JSX.Element;
109
+ declare namespace NotFoundActions {
110
+ var displayName: string;
111
+ }
112
+ export declare const NotFound: typeof NotFoundRoot & {
113
+ Code: typeof NotFoundCode;
114
+ Title: typeof NotFoundTitle;
115
+ Description: typeof NotFoundDescription;
116
+ Actions: typeof NotFoundActions;
117
+ };
118
+ export { NotFoundCode, NotFoundTitle, NotFoundDescription, NotFoundActions };