panelui-web 0.0.0-stage → 0.2.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/card/index.d.ts +107 -0
- package/dist/components/card/index.js +88 -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/frame/index.d.ts +189 -0
- package/dist/components/frame/index.js +181 -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/sheet/index.d.ts +148 -0
- package/dist/components/sheet/index.js +330 -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 +15 -0
- package/dist/index.js +15 -0
- package/dist/lib/cn.d.ts +3 -0
- package/dist/lib/cn.js +6 -0
- package/package.json +72 -4
package/README.md
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
-
#
|
|
1
|
+
# panelui-web
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
PanelUI for the web: animated, data-rich React components, styled with Tailwind CSS on the
|
|
4
|
+
tokens a shadcn project already defines.
|
|
5
|
+
|
|
6
|
+
Documentation: [panelui.dev/docs/web](https://panelui.dev/docs/web)
|
|
7
|
+
|
|
8
|
+
## Two ways to use it
|
|
9
|
+
|
|
10
|
+
**Copy a component into your project** with the shadcn CLI. The source lands in
|
|
11
|
+
`components/panelui/` and is yours to edit:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx shadcn@latest add https://panelui.dev/r/web/planet.json
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Or install the package** and import from it:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install panelui-web
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```css
|
|
24
|
+
/* app/globals.css — let Tailwind see the classes the package uses */
|
|
25
|
+
@import "tailwindcss";
|
|
26
|
+
@source "../node_modules/panelui-web/dist";
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { Planet } from 'panelui-web/planet';
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Both read the same source, so a component looks and behaves the same either way.
|
|
34
|
+
|
|
35
|
+
## Requirements
|
|
36
|
+
|
|
37
|
+
- React 18 or newer
|
|
38
|
+
- Tailwind CSS 4
|
|
39
|
+
- The shadcn theme tokens (`--background`, `--foreground`, `--card`, `--muted`, `--border`,
|
|
40
|
+
`--primary` and their `-foreground` pairs). `npx shadcn@latest init` writes them.
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BarPicker — a short run of values as columns of lights, one column picked.
|
|
3
|
+
*
|
|
4
|
+
* ```tsx
|
|
5
|
+
* <BarPicker data={week} value={day} onValueChange={setDay} />
|
|
6
|
+
* ```
|
|
7
|
+
*
|
|
8
|
+
* ## A column is a stack of lights
|
|
9
|
+
*
|
|
10
|
+
* Each value is drawn as a column of segments, lit from the bottom up to the
|
|
11
|
+
* value, the way a level meter shows loudness. The unlit segments stay as
|
|
12
|
+
* faint outlines of themselves, so every column is the same height and how
|
|
13
|
+
* full it is can be judged against its own top without an axis.
|
|
14
|
+
*
|
|
15
|
+
* ## The picked column is the bright one
|
|
16
|
+
*
|
|
17
|
+
* It is lit at full strength with a glow on its highest segment, and it is
|
|
18
|
+
* wider than the others. The rest are the same colour, held back. Picking
|
|
19
|
+
* another column lights it from the bottom, one segment after the next.
|
|
20
|
+
*
|
|
21
|
+
* ## A choice, not a chart
|
|
22
|
+
*
|
|
23
|
+
* It behaves like a row of radio buttons. Exactly one column is picked, and
|
|
24
|
+
* the arrow keys walk along the row. The figure for the picked column is
|
|
25
|
+
* yours to show beside it, large: a column of eight lights cannot be read to
|
|
26
|
+
* the unit, but one number can.
|
|
27
|
+
*/
|
|
28
|
+
import { type ComponentProps, type ReactNode } from 'react';
|
|
29
|
+
export interface BarPickerBar {
|
|
30
|
+
/** Written under the column: "Mon", "W3". Keep it to a few characters. */
|
|
31
|
+
label: ReactNode;
|
|
32
|
+
value: number;
|
|
33
|
+
/** The long form, for a screen reader: "Monday". Defaults to the label. */
|
|
34
|
+
name?: string;
|
|
35
|
+
}
|
|
36
|
+
/** Which of the theme's roles the lights are drawn in. */
|
|
37
|
+
export type BarPickerColor = 'primary' | 'info' | 'success' | 'warning' | 'destructive';
|
|
38
|
+
export interface BarPickerProps extends Omit<ComponentProps<'div'>, 'children' | 'onChange' | 'defaultValue' | 'color'> {
|
|
39
|
+
/** The columns, left to right. */
|
|
40
|
+
data: BarPickerBar[];
|
|
41
|
+
/** The value a fully lit column stands for. Defaults to the largest in `data`. */
|
|
42
|
+
max?: number;
|
|
43
|
+
/** The index of the picked column, when the caller holds it. */
|
|
44
|
+
value?: number;
|
|
45
|
+
/** The index picked to begin with, when the caller does not hold it. */
|
|
46
|
+
defaultValue?: number;
|
|
47
|
+
/** Called with the index of a column when it is picked. */
|
|
48
|
+
onValueChange?: (index: number) => void;
|
|
49
|
+
/** How many lights a column is made of. */
|
|
50
|
+
segments?: number;
|
|
51
|
+
/** Which of the theme's roles the lights are drawn in. */
|
|
52
|
+
color?: BarPickerColor;
|
|
53
|
+
/** Writes a value for a screen reader: "17 deploys". */
|
|
54
|
+
formatValue?: (value: number) => string;
|
|
55
|
+
/** What a screen reader calls the row. */
|
|
56
|
+
label?: string;
|
|
57
|
+
}
|
|
58
|
+
export declare function BarPicker({ data, max, value, defaultValue, onValueChange, segments, color, formatValue, label, className, ...props }: BarPickerProps): import("react").JSX.Element;
|
|
59
|
+
export declare namespace BarPicker {
|
|
60
|
+
var displayName: string;
|
|
61
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* BarPicker — a short run of values as columns of lights, one column picked.
|
|
5
|
+
*
|
|
6
|
+
* ```tsx
|
|
7
|
+
* <BarPicker data={week} value={day} onValueChange={setDay} />
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* ## A column is a stack of lights
|
|
11
|
+
*
|
|
12
|
+
* Each value is drawn as a column of segments, lit from the bottom up to the
|
|
13
|
+
* value, the way a level meter shows loudness. The unlit segments stay as
|
|
14
|
+
* faint outlines of themselves, so every column is the same height and how
|
|
15
|
+
* full it is can be judged against its own top without an axis.
|
|
16
|
+
*
|
|
17
|
+
* ## The picked column is the bright one
|
|
18
|
+
*
|
|
19
|
+
* It is lit at full strength with a glow on its highest segment, and it is
|
|
20
|
+
* wider than the others. The rest are the same colour, held back. Picking
|
|
21
|
+
* another column lights it from the bottom, one segment after the next.
|
|
22
|
+
*
|
|
23
|
+
* ## A choice, not a chart
|
|
24
|
+
*
|
|
25
|
+
* It behaves like a row of radio buttons. Exactly one column is picked, and
|
|
26
|
+
* the arrow keys walk along the row. The figure for the picked column is
|
|
27
|
+
* yours to show beside it, large: a column of eight lights cannot be read to
|
|
28
|
+
* the unit, but one number can.
|
|
29
|
+
*/
|
|
30
|
+
import { useEffect, useRef, useState, } from 'react';
|
|
31
|
+
import { tv } from 'tailwind-variants';
|
|
32
|
+
const barPickerVariants = tv({
|
|
33
|
+
slots: {
|
|
34
|
+
root: 'flex w-full items-end gap-1.5',
|
|
35
|
+
column: 'group flex min-w-0 cursor-pointer flex-col items-stretch gap-2.5 rounded-lg outline-none transition-[flex-grow] duration-500 ease-[cubic-bezier(0.34,1.4,0.64,1)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-4 focus-visible:ring-offset-background motion-reduce:transition-none',
|
|
36
|
+
// Stacked from the bottom, so the first segment in the markup is the
|
|
37
|
+
// lowest light.
|
|
38
|
+
stack: 'flex h-28 flex-col-reverse gap-[3px]',
|
|
39
|
+
segment: 'min-h-0 flex-1 rounded-[5px] transition-[background-color,box-shadow,opacity] duration-300 motion-reduce:transition-none',
|
|
40
|
+
label: 'truncate text-center text-xs text-muted-foreground transition-colors',
|
|
41
|
+
},
|
|
42
|
+
variants: {
|
|
43
|
+
color: {
|
|
44
|
+
primary: { root: '[--pu-color:var(--primary)]' },
|
|
45
|
+
info: { root: '[--pu-color:var(--info)]' },
|
|
46
|
+
success: { root: '[--pu-color:var(--success)]' },
|
|
47
|
+
warning: { root: '[--pu-color:var(--warning)]' },
|
|
48
|
+
destructive: { root: '[--pu-color:var(--destructive)]' },
|
|
49
|
+
},
|
|
50
|
+
selected: {
|
|
51
|
+
true: { column: 'flex-[1.5]', label: 'font-medium text-foreground' },
|
|
52
|
+
false: { column: 'flex-1' },
|
|
53
|
+
},
|
|
54
|
+
/** What one segment is doing. */
|
|
55
|
+
light: {
|
|
56
|
+
off: { segment: 'bg-muted' },
|
|
57
|
+
// Lit in a column that is not picked: the colour, held back.
|
|
58
|
+
dim: {
|
|
59
|
+
segment: 'bg-[color-mix(in_oklab,var(--pu-color)_26%,transparent)] group-hover:bg-[color-mix(in_oklab,var(--pu-color)_48%,transparent)]',
|
|
60
|
+
},
|
|
61
|
+
on: { segment: 'bg-[var(--pu-color)]' },
|
|
62
|
+
// The highest light of the picked column.
|
|
63
|
+
peak: {
|
|
64
|
+
segment: 'bg-[var(--pu-color)] shadow-[0_0_16px_color-mix(in_oklab,var(--pu-color)_65%,transparent)]',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
/** Milliseconds between one light of a column coming on and the next. */
|
|
70
|
+
const STAGGER = 32;
|
|
71
|
+
export function BarPicker({ data, max, value, defaultValue = 0, onValueChange, segments = 8, color = 'primary', formatValue = (bar) => String(bar), label = 'Values', className, ...props }) {
|
|
72
|
+
const slots = barPickerVariants({ color });
|
|
73
|
+
const [internal, setInternal] = useState(defaultValue);
|
|
74
|
+
const picked = clampIndex(value ?? internal, data.length);
|
|
75
|
+
const refs = useRef([]);
|
|
76
|
+
// The columns start dark and light up once mounted, so they are seen
|
|
77
|
+
// arriving.
|
|
78
|
+
const [lit, setLit] = useState(false);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const frame = requestAnimationFrame(() => setLit(true));
|
|
81
|
+
return () => cancelAnimationFrame(frame);
|
|
82
|
+
}, []);
|
|
83
|
+
const count = Math.max(1, Math.round(segments));
|
|
84
|
+
const ceiling = max ?? Math.max(1, ...data.map((bar) => bar.value));
|
|
85
|
+
const pick = (index) => {
|
|
86
|
+
if (value === undefined)
|
|
87
|
+
setInternal(index);
|
|
88
|
+
onValueChange?.(index);
|
|
89
|
+
};
|
|
90
|
+
const onKeyDown = (event, index) => {
|
|
91
|
+
const rtl = getComputedStyle(event.currentTarget).direction === 'rtl';
|
|
92
|
+
const forward = rtl ? -1 : 1;
|
|
93
|
+
const moves = {
|
|
94
|
+
ArrowLeft: index - forward,
|
|
95
|
+
ArrowRight: index + forward,
|
|
96
|
+
ArrowDown: index - 1,
|
|
97
|
+
ArrowUp: index + 1,
|
|
98
|
+
Home: 0,
|
|
99
|
+
End: data.length - 1,
|
|
100
|
+
};
|
|
101
|
+
const target = moves[event.key];
|
|
102
|
+
if (target === undefined)
|
|
103
|
+
return;
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
// Round the ends, as a radio group does.
|
|
106
|
+
const next = (target + data.length) % data.length;
|
|
107
|
+
refs.current[next]?.focus();
|
|
108
|
+
pick(next);
|
|
109
|
+
};
|
|
110
|
+
return (_jsx("div", { role: "radiogroup", "aria-label": label, className: slots.root({ className }), ...props, children: data.map((bar, index) => {
|
|
111
|
+
const selected = index === picked;
|
|
112
|
+
const part = barPickerVariants({ selected });
|
|
113
|
+
const name = bar.name ?? (typeof bar.label === 'string' ? bar.label : `Bar ${index + 1}`);
|
|
114
|
+
// How many lights the value reaches. Any value above zero lights at
|
|
115
|
+
// least one, so a small value is a mark and not an empty column.
|
|
116
|
+
const share = Math.min(1, Math.max(0, bar.value / ceiling));
|
|
117
|
+
const reached = !lit || bar.value <= 0 ? 0 : Math.max(1, Math.round(share * count));
|
|
118
|
+
return (_jsxs("button", { ref: (node) => {
|
|
119
|
+
refs.current[index] = node;
|
|
120
|
+
}, type: "button", role: "radio", "aria-checked": selected, "aria-label": `${name}: ${formatValue(bar.value)}`, tabIndex: selected ? 0 : -1, onClick: () => pick(index), onKeyDown: (event) => onKeyDown(event, index), className: part.column(), children: [_jsx("span", { className: slots.stack(), children: Array.from({ length: count }, (_, level) => {
|
|
121
|
+
const on = level < reached;
|
|
122
|
+
const light = !on ? 'off' : !selected ? 'dim' : level === reached - 1 ? 'peak' : 'on';
|
|
123
|
+
return (_jsx("span", { className: barPickerVariants({ light }).segment(), style: {
|
|
124
|
+
// Bottom first, so a column is seen filling upwards.
|
|
125
|
+
transitionDelay: on ? `${level * STAGGER}ms` : undefined,
|
|
126
|
+
// The picked column brightens towards its top.
|
|
127
|
+
opacity: light === 'on'
|
|
128
|
+
? Number((0.5 + 0.5 * ((level + 1) / reached)).toFixed(3))
|
|
129
|
+
: undefined,
|
|
130
|
+
} }, level));
|
|
131
|
+
}) }), _jsx("span", { "aria-hidden": "true", className: part.label(), children: bar.label })] }, index));
|
|
132
|
+
}) }));
|
|
133
|
+
}
|
|
134
|
+
BarPicker.displayName = 'BarPicker';
|
|
135
|
+
function clampIndex(index, length) {
|
|
136
|
+
if (length === 0)
|
|
137
|
+
return 0;
|
|
138
|
+
return Math.min(length - 1, Math.max(0, index));
|
|
139
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card — a surface that holds one thing: a plan, a setting, a summary.
|
|
3
|
+
*
|
|
4
|
+
* ```tsx
|
|
5
|
+
* <Card>
|
|
6
|
+
* <Card.Header>
|
|
7
|
+
* <Card.Title>Storage</Card.Title>
|
|
8
|
+
* <Card.Description>Northwind Studio workspace</Card.Description>
|
|
9
|
+
* <Card.Action><Button variant="ghost">Manage</Button></Card.Action>
|
|
10
|
+
* </Card.Header>
|
|
11
|
+
* <Card.Content>…</Card.Content>
|
|
12
|
+
* <Card.Footer variant="panel">…</Card.Footer>
|
|
13
|
+
* </Card>
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* ## The padding is on the parts
|
|
17
|
+
*
|
|
18
|
+
* The card itself draws a surface and nothing inside it, and every part
|
|
19
|
+
* carries its own padding. That is what lets a picture reach the card's
|
|
20
|
+
* corners with nothing but `overflow-hidden` on the card, and what lets a
|
|
21
|
+
* footer be a band set into the bottom of it rather than one more row.
|
|
22
|
+
*
|
|
23
|
+
* ## The action sits beside the heading
|
|
24
|
+
*
|
|
25
|
+
* `Card.Action` is placed in the header's top trailing corner whatever order
|
|
26
|
+
* it is written in, and the title and description keep the rest of the width.
|
|
27
|
+
* A button that belongs to the whole card goes there; one that belongs to the
|
|
28
|
+
* content goes in the content.
|
|
29
|
+
*
|
|
30
|
+
* ## It renders on the server
|
|
31
|
+
*
|
|
32
|
+
* No state, no effects, no handlers of its own: everything that changes with
|
|
33
|
+
* `size` or with the parts present is done in CSS, so a page of cards costs no
|
|
34
|
+
* JavaScript.
|
|
35
|
+
*/
|
|
36
|
+
import type { ComponentProps } from 'react';
|
|
37
|
+
export interface CardProps extends ComponentProps<'div'> {
|
|
38
|
+
/** `sm` tightens the padding and the title, for a card in a dense grid. Default `default`. */
|
|
39
|
+
size?: 'default' | 'sm';
|
|
40
|
+
/**
|
|
41
|
+
* The element to render. `article` for a card that stands on its own, `li`
|
|
42
|
+
* for one in a list, `section` for one that groups a part of a page.
|
|
43
|
+
*/
|
|
44
|
+
as?: 'div' | 'article' | 'section' | 'li';
|
|
45
|
+
}
|
|
46
|
+
declare function CardRoot({ size, as: Element, className, ...props }: CardProps): import("react").JSX.Element;
|
|
47
|
+
declare namespace CardRoot {
|
|
48
|
+
var displayName: string;
|
|
49
|
+
}
|
|
50
|
+
export interface CardHeaderProps extends ComponentProps<'div'> {
|
|
51
|
+
}
|
|
52
|
+
/** The title, the line under it, and the action beside them. */
|
|
53
|
+
declare function CardHeader({ className, ...props }: CardHeaderProps): import("react").JSX.Element;
|
|
54
|
+
declare namespace CardHeader {
|
|
55
|
+
var displayName: string;
|
|
56
|
+
}
|
|
57
|
+
export interface CardTitleProps extends ComponentProps<'h3'> {
|
|
58
|
+
/** The heading level, to fit the page around the card. Default `h3`. */
|
|
59
|
+
as?: 'h2' | 'h3' | 'h4' | 'div';
|
|
60
|
+
}
|
|
61
|
+
/** What the card is about. A heading, so the page outline includes it. */
|
|
62
|
+
declare function CardTitle({ as: Element, className, ...props }: CardTitleProps): import("react").JSX.Element;
|
|
63
|
+
declare namespace CardTitle {
|
|
64
|
+
var displayName: string;
|
|
65
|
+
}
|
|
66
|
+
export interface CardDescriptionProps extends ComponentProps<'p'> {
|
|
67
|
+
}
|
|
68
|
+
/** A line under the title. */
|
|
69
|
+
declare function CardDescription({ className, ...props }: CardDescriptionProps): import("react").JSX.Element;
|
|
70
|
+
declare namespace CardDescription {
|
|
71
|
+
var displayName: string;
|
|
72
|
+
}
|
|
73
|
+
export interface CardActionProps extends ComponentProps<'div'> {
|
|
74
|
+
}
|
|
75
|
+
/** A control for the whole card, held in the header's top trailing corner. */
|
|
76
|
+
declare function CardAction({ className, ...props }: CardActionProps): import("react").JSX.Element;
|
|
77
|
+
declare namespace CardAction {
|
|
78
|
+
var displayName: string;
|
|
79
|
+
}
|
|
80
|
+
export interface CardContentProps extends ComponentProps<'div'> {
|
|
81
|
+
}
|
|
82
|
+
/** The body. */
|
|
83
|
+
declare function CardContent({ className, ...props }: CardContentProps): import("react").JSX.Element;
|
|
84
|
+
declare namespace CardContent {
|
|
85
|
+
var displayName: string;
|
|
86
|
+
}
|
|
87
|
+
export interface CardFooterProps extends ComponentProps<'div'> {
|
|
88
|
+
/**
|
|
89
|
+
* `plain` continues the card. `panel` is a darker band set into its bottom,
|
|
90
|
+
* for actions. Default `plain`.
|
|
91
|
+
*/
|
|
92
|
+
variant?: 'plain' | 'panel';
|
|
93
|
+
}
|
|
94
|
+
/** The row at the bottom, usually the card's actions. */
|
|
95
|
+
declare function CardFooter({ variant, className, ...props }: CardFooterProps): import("react").JSX.Element;
|
|
96
|
+
declare namespace CardFooter {
|
|
97
|
+
var displayName: string;
|
|
98
|
+
}
|
|
99
|
+
export declare const Card: typeof CardRoot & {
|
|
100
|
+
Header: typeof CardHeader;
|
|
101
|
+
Title: typeof CardTitle;
|
|
102
|
+
Description: typeof CardDescription;
|
|
103
|
+
Action: typeof CardAction;
|
|
104
|
+
Content: typeof CardContent;
|
|
105
|
+
Footer: typeof CardFooter;
|
|
106
|
+
};
|
|
107
|
+
export { CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { tv } from 'tailwind-variants';
|
|
3
|
+
const cardVariants = tv({
|
|
4
|
+
slots: {
|
|
5
|
+
root: 'group/card flex flex-col rounded-2xl border bg-card text-card-foreground shadow-xs',
|
|
6
|
+
header: [
|
|
7
|
+
'grid auto-rows-min grid-cols-[minmax(0,1fr)] items-start gap-x-4 gap-y-1.5 p-6',
|
|
8
|
+
'has-data-[slot=card-action]:grid-cols-[minmax(0,1fr)_auto]',
|
|
9
|
+
'group-data-[size=sm]/card:gap-y-1 group-data-[size=sm]/card:p-4',
|
|
10
|
+
],
|
|
11
|
+
title: 'text-lg font-semibold leading-tight tracking-[-0.01em] group-data-[size=sm]/card:text-base',
|
|
12
|
+
description: 'text-sm text-muted-foreground',
|
|
13
|
+
// Its own column, across both of the heading's rows, so a long title wraps
|
|
14
|
+
// beside it instead of underneath it.
|
|
15
|
+
action: 'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
|
16
|
+
// `first:` for a card with no header, which would otherwise start flush.
|
|
17
|
+
content: 'px-6 pb-6 first:pt-6 group-data-[size=sm]/card:px-4 group-data-[size=sm]/card:pb-4 group-data-[size=sm]/card:first:pt-4',
|
|
18
|
+
footer: 'flex items-center gap-2',
|
|
19
|
+
},
|
|
20
|
+
variants: {
|
|
21
|
+
footer: {
|
|
22
|
+
/** Part of the card: same surface, continuing the content's padding. */
|
|
23
|
+
plain: {
|
|
24
|
+
footer: 'px-6 pb-6 first:pt-6 group-data-[size=sm]/card:px-4 group-data-[size=sm]/card:pb-4 group-data-[size=sm]/card:first:pt-4',
|
|
25
|
+
},
|
|
26
|
+
/**
|
|
27
|
+
* A band set into the bottom of the card: a rule across its top, a step
|
|
28
|
+
* darker, and the card's own bottom corners. For a footer that is what
|
|
29
|
+
* somebody does with the card, not more of what it says.
|
|
30
|
+
*/
|
|
31
|
+
panel: {
|
|
32
|
+
footer: 'mt-auto rounded-b-[calc(var(--radius-2xl)-1px)] border-t bg-muted/60 px-6 py-4 group-data-[size=sm]/card:px-4 group-data-[size=sm]/card:py-3',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
function CardRoot({ size = 'default', as: Element = 'div', className, ...props }) {
|
|
38
|
+
const slots = cardVariants();
|
|
39
|
+
const Tag = Element;
|
|
40
|
+
return _jsx(Tag, { "data-slot": "card", "data-size": size, className: slots.root({ className }), ...props });
|
|
41
|
+
}
|
|
42
|
+
CardRoot.displayName = 'Card';
|
|
43
|
+
/** The title, the line under it, and the action beside them. */
|
|
44
|
+
function CardHeader({ className, ...props }) {
|
|
45
|
+
const slots = cardVariants();
|
|
46
|
+
return _jsx("div", { "data-slot": "card-header", className: slots.header({ className }), ...props });
|
|
47
|
+
}
|
|
48
|
+
CardHeader.displayName = 'Card.Header';
|
|
49
|
+
/** What the card is about. A heading, so the page outline includes it. */
|
|
50
|
+
function CardTitle({ as: Element = 'h3', className, ...props }) {
|
|
51
|
+
const slots = cardVariants();
|
|
52
|
+
const Tag = Element;
|
|
53
|
+
return _jsx(Tag, { "data-slot": "card-title", className: slots.title({ className }), ...props });
|
|
54
|
+
}
|
|
55
|
+
CardTitle.displayName = 'Card.Title';
|
|
56
|
+
/** A line under the title. */
|
|
57
|
+
function CardDescription({ className, ...props }) {
|
|
58
|
+
const slots = cardVariants();
|
|
59
|
+
return _jsx("p", { "data-slot": "card-description", className: slots.description({ className }), ...props });
|
|
60
|
+
}
|
|
61
|
+
CardDescription.displayName = 'Card.Description';
|
|
62
|
+
/** A control for the whole card, held in the header's top trailing corner. */
|
|
63
|
+
function CardAction({ className, ...props }) {
|
|
64
|
+
const slots = cardVariants();
|
|
65
|
+
return _jsx("div", { "data-slot": "card-action", className: slots.action({ className }), ...props });
|
|
66
|
+
}
|
|
67
|
+
CardAction.displayName = 'Card.Action';
|
|
68
|
+
/** The body. */
|
|
69
|
+
function CardContent({ className, ...props }) {
|
|
70
|
+
const slots = cardVariants();
|
|
71
|
+
return _jsx("div", { "data-slot": "card-content", className: slots.content({ className }), ...props });
|
|
72
|
+
}
|
|
73
|
+
CardContent.displayName = 'Card.Content';
|
|
74
|
+
/** The row at the bottom, usually the card's actions. */
|
|
75
|
+
function CardFooter({ variant = 'plain', className, ...props }) {
|
|
76
|
+
const slots = cardVariants({ footer: variant });
|
|
77
|
+
return _jsx("div", { "data-slot": "card-footer", className: slots.footer({ className }), ...props });
|
|
78
|
+
}
|
|
79
|
+
CardFooter.displayName = 'Card.Footer';
|
|
80
|
+
export const Card = Object.assign(CardRoot, {
|
|
81
|
+
Header: CardHeader,
|
|
82
|
+
Title: CardTitle,
|
|
83
|
+
Description: CardDescription,
|
|
84
|
+
Action: CardAction,
|
|
85
|
+
Content: CardContent,
|
|
86
|
+
Footer: CardFooter,
|
|
87
|
+
});
|
|
88
|
+
export { CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommandDock — a small button that opens into a place to ask for something,
|
|
3
|
+
* with the answer arriving under it a row at a time.
|
|
4
|
+
*
|
|
5
|
+
* ```tsx
|
|
6
|
+
* <CommandDock onSubmit={ask}>
|
|
7
|
+
* <CommandDock.Trigger>Ask</CommandDock.Trigger>
|
|
8
|
+
* <CommandDock.Panel>
|
|
9
|
+
* <CommandDock.Input placeholder="What do you want to find?" />
|
|
10
|
+
* <CommandDock.Results>
|
|
11
|
+
* {rows.map((row) => <CommandDock.Row key={row.id}>{row.text}</CommandDock.Row>)}
|
|
12
|
+
* </CommandDock.Results>
|
|
13
|
+
* </CommandDock.Panel>
|
|
14
|
+
* </CommandDock>
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* ## One surface that opens
|
|
18
|
+
*
|
|
19
|
+
* The button and the panel are the same surface. Closed, it is a pill as
|
|
20
|
+
* wide as its label. Opened, it widens, the label fades into a field in the
|
|
21
|
+
* same place, and it grows downwards as rows arrive. Closing runs the same
|
|
22
|
+
* steps backwards: the rows fold up, then it narrows. Nothing appears from
|
|
23
|
+
* elsewhere, so it is clear what was pressed and where the answer belongs.
|
|
24
|
+
*
|
|
25
|
+
* ## It runs nothing
|
|
26
|
+
*
|
|
27
|
+
* The dock does not know where answers come from. `onSubmit` is handed what
|
|
28
|
+
* was typed, and the rows are whatever you render. A row that mounts slides
|
|
29
|
+
* in after the one before it, so rows added one by one read as an answer
|
|
30
|
+
* being written, whether they come from a search, a model or a list you
|
|
31
|
+
* already had.
|
|
32
|
+
*
|
|
33
|
+
* ## Closing
|
|
34
|
+
*
|
|
35
|
+
* Escape closes it and gives focus back to where it was, and so does a press
|
|
36
|
+
* anywhere outside it.
|
|
37
|
+
*/
|
|
38
|
+
import { type ComponentProps, type ReactNode } from 'react';
|
|
39
|
+
export interface CommandDockProps extends Omit<ComponentProps<'div'>, 'onSubmit' | 'defaultValue'> {
|
|
40
|
+
/** Whether it is open, when the caller holds that. */
|
|
41
|
+
open?: boolean;
|
|
42
|
+
/** Whether it starts open, when the caller does not. */
|
|
43
|
+
defaultOpen?: boolean;
|
|
44
|
+
/** Called when it opens or closes. */
|
|
45
|
+
onOpenChange?: (open: boolean) => void;
|
|
46
|
+
/** What is typed in the field, when the caller holds it. */
|
|
47
|
+
value?: string;
|
|
48
|
+
/** Called with what is typed, each time it changes. */
|
|
49
|
+
onValueChange?: (value: string) => void;
|
|
50
|
+
/** Called with what was typed when it is sent. */
|
|
51
|
+
onSubmit?: (value: string) => void;
|
|
52
|
+
/** While true, the field cannot be sent again and `CommandDock.Status` is shown. */
|
|
53
|
+
busy?: boolean;
|
|
54
|
+
/** How wide it is while closed, as a CSS length. */
|
|
55
|
+
closedWidth?: string;
|
|
56
|
+
}
|
|
57
|
+
declare function CommandDockRoot({ open: controlledOpen, defaultOpen, onOpenChange, value: controlledValue, onValueChange, onSubmit, busy, closedWidth, className, style, children, ...props }: CommandDockProps): import("react").JSX.Element;
|
|
58
|
+
declare namespace CommandDockRoot {
|
|
59
|
+
var displayName: string;
|
|
60
|
+
}
|
|
61
|
+
export interface CommandDockTriggerProps extends ComponentProps<'button'> {
|
|
62
|
+
}
|
|
63
|
+
/** The closed form: a button that opens the dock. */
|
|
64
|
+
declare function CommandDockTrigger({ className, onClick, ...props }: CommandDockTriggerProps): import("react").JSX.Element;
|
|
65
|
+
declare namespace CommandDockTrigger {
|
|
66
|
+
var displayName: string;
|
|
67
|
+
}
|
|
68
|
+
export interface CommandDockPanelProps extends ComponentProps<'div'> {
|
|
69
|
+
}
|
|
70
|
+
/** The open form: the field, and the rows under it. */
|
|
71
|
+
declare function CommandDockPanel({ className, ...props }: CommandDockPanelProps): import("react").JSX.Element;
|
|
72
|
+
declare namespace CommandDockPanel {
|
|
73
|
+
var displayName: string;
|
|
74
|
+
}
|
|
75
|
+
export interface CommandDockInputProps extends Omit<ComponentProps<'input'>, 'value' | 'onChange' | 'type'> {
|
|
76
|
+
/** What a screen reader calls the send button. */
|
|
77
|
+
submitLabel?: string;
|
|
78
|
+
}
|
|
79
|
+
/** The field, with a button to send what is in it. Enter sends too. */
|
|
80
|
+
declare function CommandDockInput({ className, submitLabel, 'aria-label': ariaLabel, ...props }: CommandDockInputProps): import("react").JSX.Element;
|
|
81
|
+
declare namespace CommandDockInput {
|
|
82
|
+
var displayName: string;
|
|
83
|
+
}
|
|
84
|
+
export interface CommandDockResultsProps extends ComponentProps<'div'> {
|
|
85
|
+
/** What a screen reader calls the list of rows. */
|
|
86
|
+
label?: string;
|
|
87
|
+
}
|
|
88
|
+
/** Where the rows go. It takes no room until there is something in it. */
|
|
89
|
+
declare function CommandDockResults({ className, label, children, ...props }: CommandDockResultsProps): import("react").JSX.Element;
|
|
90
|
+
declare namespace CommandDockResults {
|
|
91
|
+
var displayName: string;
|
|
92
|
+
}
|
|
93
|
+
export interface CommandDockRowProps extends ComponentProps<'div'> {
|
|
94
|
+
/** Called when the row is pressed. Passing this makes the row a button. */
|
|
95
|
+
onSelect?: () => void;
|
|
96
|
+
}
|
|
97
|
+
/** One row of the answer. It slides in when it is added. */
|
|
98
|
+
declare function CommandDockRow({ className, onSelect, children, ...props }: CommandDockRowProps): import("react").JSX.Element;
|
|
99
|
+
declare namespace CommandDockRow {
|
|
100
|
+
var displayName: string;
|
|
101
|
+
}
|
|
102
|
+
export interface CommandDockStatusProps extends ComponentProps<'div'> {
|
|
103
|
+
children?: ReactNode;
|
|
104
|
+
}
|
|
105
|
+
/** Shown under the rows while `busy`: three dots, and words if you give it some. */
|
|
106
|
+
declare function CommandDockStatus({ className, children, ...props }: CommandDockStatusProps): import("react").JSX.Element;
|
|
107
|
+
declare namespace CommandDockStatus {
|
|
108
|
+
var displayName: string;
|
|
109
|
+
}
|
|
110
|
+
export declare const CommandDock: typeof CommandDockRoot & {
|
|
111
|
+
Trigger: typeof CommandDockTrigger;
|
|
112
|
+
Panel: typeof CommandDockPanel;
|
|
113
|
+
Input: typeof CommandDockInput;
|
|
114
|
+
Results: typeof CommandDockResults;
|
|
115
|
+
Row: typeof CommandDockRow;
|
|
116
|
+
Status: typeof CommandDockStatus;
|
|
117
|
+
};
|
|
118
|
+
export { CommandDockTrigger, CommandDockPanel, CommandDockInput, CommandDockResults, CommandDockRow, CommandDockStatus, };
|