panelui-web 0.1.0 → 0.3.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.
@@ -6,7 +6,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
6
6
  * ```tsx
7
7
  * <Flow>
8
8
  * <Flow.Background variant="dots" />
9
- * <Flow.Node id="source" position={{ x: 24, y: 40 }}>…</Flow.Node>
9
+ * <Flow.Node id="source" position={{ x: 24, y: 40 }}>
10
+ * <Flow.NodeContent>
11
+ * <Flow.NodeTitle>Source</Flow.NodeTitle>
12
+ * <Flow.NodeDescription>main</Flow.NodeDescription>
13
+ * </Flow.NodeContent>
14
+ * </Flow.Node>
10
15
  * <Flow.Node id="build" position={{ x: 24, y: 200 }}>…</Flow.Node>
11
16
  * <Flow.Edge from="source" to="build" dashed animated arrow />
12
17
  * <Flow.Controls />
@@ -50,6 +55,15 @@ const flowVariants = tv({
50
55
  node: 'absolute left-0 top-0 z-10 cursor-grab rounded-2xl border bg-card text-card-foreground shadow-sm outline-none transition-shadow focus-visible:ring-2 focus-visible:ring-ring active:cursor-grabbing',
51
56
  group: 'absolute left-0 top-0 cursor-grab rounded-3xl border border-dashed bg-muted/40 p-3 pt-8 outline-none focus-visible:ring-2 focus-visible:ring-ring active:cursor-grabbing',
52
57
  groupLabel: 'absolute start-4 top-2.5 text-xs font-medium uppercase tracking-wider text-muted-foreground',
58
+ // What goes inside a node. The padding is on the parts, so a header can
59
+ // draw its rule from edge to edge.
60
+ nodeHeader: 'flex items-center justify-between gap-3 border-b px-4 py-2.5',
61
+ // Inside a group item the item already has its padding.
62
+ nodeContent: 'px-4 py-3 in-data-[slot=flow-group-item]:p-0',
63
+ nodeTitle: 'text-sm font-medium',
64
+ nodeDescription: 'text-sm text-muted-foreground in-data-[slot=flow-group-item]:text-xs',
65
+ nodeBadge: 'rounded-full bg-[color-mix(in_oklab,var(--pu-color)_14%,transparent)] px-2 py-0.5 text-xs font-medium text-[var(--pu-color)]',
66
+ groupItem: 'flex items-center gap-3 rounded-xl border bg-card px-3 py-2.5 [&_svg]:size-4 [&_svg]:text-muted-foreground',
53
67
  handle: 'absolute z-20 size-3 -translate-x-1/2 -translate-y-1/2 cursor-crosshair rounded-full border-2 border-background bg-muted-foreground outline-none transition-[scale,background-color] hover:scale-150 hover:bg-foreground focus-visible:ring-2 focus-visible:ring-ring',
54
68
  edge: 'pointer-events-none absolute left-0 top-0 size-px overflow-visible text-[var(--pu-color)]',
55
69
  controls: 'absolute flex flex-col divide-y divide-border overflow-hidden rounded-xl border bg-card text-card-foreground shadow-sm',
@@ -58,13 +72,16 @@ const flowVariants = tv({
58
72
  },
59
73
  variants: {
60
74
  color: {
61
- primary: { edge: '[--pu-color:var(--primary)]' },
62
- info: { edge: '[--pu-color:var(--info)]' },
63
- success: { edge: '[--pu-color:var(--success)]' },
64
- warning: { edge: '[--pu-color:var(--warning)]' },
65
- destructive: { edge: '[--pu-color:var(--destructive)]' },
75
+ primary: { edge: '[--pu-color:var(--primary)]', nodeBadge: '[--pu-color:var(--primary)]' },
76
+ info: { edge: '[--pu-color:var(--info)]', nodeBadge: '[--pu-color:var(--info)]' },
77
+ success: { edge: '[--pu-color:var(--success)]', nodeBadge: '[--pu-color:var(--success)]' },
78
+ warning: { edge: '[--pu-color:var(--warning)]', nodeBadge: '[--pu-color:var(--warning)]' },
79
+ destructive: { edge: '[--pu-color:var(--destructive)]', nodeBadge: '[--pu-color:var(--destructive)]' },
66
80
  // The colour of a hairline that is meant to be seen but not looked at.
67
- muted: { edge: '[--pu-color:var(--muted-foreground)]' },
81
+ muted: {
82
+ edge: '[--pu-color:var(--muted-foreground)]',
83
+ nodeBadge: 'bg-muted text-muted-foreground',
84
+ },
68
85
  },
69
86
  selected: { true: { node: 'z-20 ring-2 ring-ring' } },
70
87
  position: {
@@ -474,7 +491,7 @@ function useMovable({ id, position, draggable, onPress, onDelete, moveStep, }) {
474
491
  },
475
492
  };
476
493
  }
477
- /** A box on the graph. Whatever is inside it is yours. */
494
+ /** A box on the graph. Fill it with the node parts below, or with anything of your own. */
478
495
  function FlowNode({ id, position, draggable = true, selected = false, onPress, onDelete, moveStep, className, style, children, ...props }) {
479
496
  const movable = useMovable({ id, position, draggable, onPress, onDelete, moveStep });
480
497
  return (_jsx(NodeContext.Provider, { value: id, children: _jsx("div", { ref: movable.element, "data-flow-node": id, tabIndex: 0, "aria-roledescription": "Node", "aria-selected": selected || undefined, className: flowVariants({ selected }).node({ className }), style: { ...movable.style, ...style }, ...movable.handlers, ...props, children: children }) }));
@@ -489,6 +506,39 @@ function FlowGroup({ id, position, draggable = true, selected: _selected, onPres
489
506
  return (_jsx(NodeContext.Provider, { value: id, children: _jsxs("div", { ref: movable.element, "data-flow-node": id, tabIndex: 0, "aria-roledescription": "Group", className: flowVariants().group({ className }), style: { ...movable.style, ...style }, ...movable.handlers, ...props, children: [label !== undefined ? _jsx("span", { className: flowVariants().groupLabel(), children: label }) : null, children] }) }));
490
507
  }
491
508
  FlowGroup.displayName = 'Flow.Group';
509
+ /** A strip across the top of a node, ruled off from the rest: a step number and a state. */
510
+ function FlowNodeHeader({ className, ...props }) {
511
+ return _jsx("div", { "data-slot": "flow-node-header", className: flowVariants().nodeHeader({ className }), ...props });
512
+ }
513
+ FlowNodeHeader.displayName = 'Flow.NodeHeader';
514
+ /** The body of a node, with the node's padding: a title over a description. */
515
+ function FlowNodeContent({ className, ...props }) {
516
+ return _jsx("div", { "data-slot": "flow-node-content", className: flowVariants().nodeContent({ className }), ...props });
517
+ }
518
+ FlowNodeContent.displayName = 'Flow.NodeContent';
519
+ /** What the node is. */
520
+ function FlowNodeTitle({ className, ...props }) {
521
+ return _jsx("p", { "data-slot": "flow-node-title", className: flowVariants().nodeTitle({ className }), ...props });
522
+ }
523
+ FlowNodeTitle.displayName = 'Flow.NodeTitle';
524
+ /** A quieter line about the node. Smaller inside a `Flow.GroupItem`. */
525
+ function FlowNodeDescription({ className, ...props }) {
526
+ return (_jsx("p", { "data-slot": "flow-node-description", className: flowVariants().nodeDescription({ className }), ...props }));
527
+ }
528
+ FlowNodeDescription.displayName = 'Flow.NodeDescription';
529
+ /** A word in a pill, for a node's state: done, now, waiting. */
530
+ function FlowNodeBadge({ color = 'muted', className, ...props }) {
531
+ return (_jsx("span", { "data-slot": "flow-node-badge", className: flowVariants({ color }).nodeBadge({ className }), ...props }));
532
+ }
533
+ FlowNodeBadge.displayName = 'Flow.NodeBadge';
534
+ /**
535
+ * One thing inside a `Flow.Group` that is not a node of its own: an icon
536
+ * beside a `Flow.NodeContent`. It moves with the group and edges cannot join it.
537
+ */
538
+ function FlowGroupItem({ className, ...props }) {
539
+ return _jsx("div", { "data-slot": "flow-group-item", className: flowVariants().groupItem({ className }), ...props });
540
+ }
541
+ FlowGroupItem.displayName = 'Flow.GroupItem';
492
542
  const SIDE_STYLE = {
493
543
  top: (offset) => ({ left: `${offset * 100}%`, top: '0%' }),
494
544
  bottom: (offset) => ({ left: `${offset * 100}%`, top: '100%' }),
@@ -589,9 +639,15 @@ export const Flow = Object.assign(FlowRoot, {
589
639
  Handle: FlowHandle,
590
640
  Edge: FlowEdge,
591
641
  Group: FlowGroup,
642
+ GroupItem: FlowGroupItem,
643
+ NodeHeader: FlowNodeHeader,
644
+ NodeContent: FlowNodeContent,
645
+ NodeTitle: FlowNodeTitle,
646
+ NodeDescription: FlowNodeDescription,
647
+ NodeBadge: FlowNodeBadge,
592
648
  Controls: FlowControls,
593
649
  MiniMap: FlowMiniMap,
594
650
  });
595
651
  /* By name as well, for a Server Component, which cannot dot into a client
596
652
  module. */
597
- export { FlowBackground, FlowNode, FlowHandle, FlowEdge, FlowGroup, FlowControls, FlowMiniMap };
653
+ export { FlowBackground, FlowNode, FlowHandle, FlowEdge, FlowGroup, FlowGroupItem, FlowNodeHeader, FlowNodeContent, FlowNodeTitle, FlowNodeDescription, FlowNodeBadge, FlowControls, FlowMiniMap, };
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Frame — a widget shell: a panel of rows sitting in a tray, with the tray's
3
+ * one exposed strip along the top carrying the title.
4
+ *
5
+ * ```tsx
6
+ * <Frame>
7
+ * <Frame.Header>
8
+ * <Frame.Title>Deploys</Frame.Title>
9
+ * <Frame.Action>Last 24 hours</Frame.Action>
10
+ * </Frame.Header>
11
+ * <Frame.Panel>
12
+ * <Frame.Row>
13
+ * <Frame.Media><RocketIcon /></Frame.Media>
14
+ * <Frame.Content>
15
+ * <Frame.Title>main · a41c2e9</Frame.Title>
16
+ * <Frame.Description>Production · 2 minutes ago</Frame.Description>
17
+ * </Frame.Content>
18
+ * <Frame.Actions><Badge>Ready</Badge></Frame.Actions>
19
+ * </Frame.Row>
20
+ * </Frame.Panel>
21
+ * </Frame>
22
+ * ```
23
+ *
24
+ * ## Two surfaces, one exposed edge
25
+ *
26
+ * The panel sits flush against the shell's left, right and bottom, so the
27
+ * shell reads as something the panel is set into rather than as a border
28
+ * round it — and the strip left at the top is the header, which is why the
29
+ * header needs no rule under it and no background of its own. The panel's
30
+ * bottom corners are not drawn at all: the shell clips them to its own radius.
31
+ *
32
+ * ## Four ways to nest them
33
+ *
34
+ * `default` is the tray. `band` holds a bordered card in a thin muted band,
35
+ * with the band's top strip naming what is inside — the frame for a widget
36
+ * with one thing to show, a figure or a chart, rather than a list of rows.
37
+ * `inset` floats the panel clear of the shell on all four sides, at the radius
38
+ * that keeps the two curves concentric, and the band left round it is a recess
39
+ * — darker than the panel in a light theme and a dark one alike — that
40
+ * `Frame.Footer` puts its actions in. `plain` drops the shell, for a frame
41
+ * inside something that already draws an edge, where a second one just inside
42
+ * it reads as a double line.
43
+ *
44
+ * ## Rows divide themselves
45
+ *
46
+ * The panel draws the hairline between its children, so no row has to know
47
+ * whether it is first. Under `band` it draws none: a figure and its chart are
48
+ * one thing, not two rows. A row with `href` is a link and one with `onClick`
49
+ * is a button, each with the hover and focus a pressable row should have, and
50
+ * `chevron` marks one that leads somewhere.
51
+ *
52
+ * ## It renders on the server
53
+ *
54
+ * The variant reaches the parts through a data attribute and CSS rather than
55
+ * through context, so a frame has no client JavaScript of its own.
56
+ */
57
+ import type { ComponentProps, MouseEventHandler, ReactNode } from 'react';
58
+ export type FrameVariant = 'default' | 'band' | 'inset' | 'plain';
59
+ export interface FrameProps extends ComponentProps<'div'> {
60
+ /**
61
+ * `default` sets the panel in a tray. `band` holds a bordered card in a
62
+ * thin muted band, for a widget showing one figure or chart, and its panel
63
+ * draws no rule between its children. `inset` floats
64
+ * the panel in a recessed band and gives `Frame.Footer` somewhere to sit.
65
+ * `plain` drops the shell, for a frame inside a container that already draws
66
+ * its own edge.
67
+ */
68
+ variant?: FrameVariant;
69
+ }
70
+ declare function FrameRoot({ variant, className, ...props }: FrameProps): import("react").JSX.Element;
71
+ declare namespace FrameRoot {
72
+ var displayName: string;
73
+ }
74
+ export interface FrameHeaderProps extends ComponentProps<'div'> {
75
+ }
76
+ /**
77
+ * The strip of shell above the panel: a title, an optional description under
78
+ * it, and `Frame.Action` on the trailing side.
79
+ */
80
+ declare function FrameHeader({ className, ...props }: FrameHeaderProps): import("react").JSX.Element;
81
+ declare namespace FrameHeader {
82
+ var displayName: string;
83
+ }
84
+ export interface FrameTitleProps extends ComponentProps<'h3'> {
85
+ /** The heading level in a header. Default `h3`; a row title is always a `p`. */
86
+ as?: 'h2' | 'h3' | 'h4' | 'p';
87
+ }
88
+ /** A caption in the header; the subject line inside `Frame.Content`. */
89
+ declare function FrameTitle({ as: Tag, className, ...props }: FrameTitleProps): import("react").JSX.Element;
90
+ declare namespace FrameTitle {
91
+ var displayName: string;
92
+ }
93
+ export interface FrameDescriptionProps extends ComponentProps<'p'> {
94
+ }
95
+ /** A line under a title, in the header or in a row. */
96
+ declare function FrameDescription({ className, ...props }: FrameDescriptionProps): import("react").JSX.Element;
97
+ declare namespace FrameDescription {
98
+ var displayName: string;
99
+ }
100
+ export interface FrameActionProps extends ComponentProps<'div'> {
101
+ }
102
+ /** The header's trailing side: a short status, a link, a small button. */
103
+ declare function FrameAction({ className, ...props }: FrameActionProps): import("react").JSX.Element;
104
+ declare namespace FrameAction {
105
+ var displayName: string;
106
+ }
107
+ export interface FramePanelProps extends ComponentProps<'div'> {
108
+ }
109
+ /** The card the rows sit on. It draws the hairline between its children, except under `band`. */
110
+ declare function FramePanel({ className, ...props }: FramePanelProps): import("react").JSX.Element;
111
+ declare namespace FramePanel {
112
+ var displayName: string;
113
+ }
114
+ export interface FrameRowProps extends Omit<ComponentProps<'div'>, 'onClick'> {
115
+ /** Make the row a link to this address. */
116
+ href?: string;
117
+ /** Where a link row opens, as on an `<a>`. */
118
+ target?: string;
119
+ /** The relationship of a link row's target, as on an `<a>`. */
120
+ rel?: string;
121
+ /** Make the row a button that calls this. Ignored when `href` is set. */
122
+ onClick?: MouseEventHandler<HTMLElement>;
123
+ /** A trailing chevron, for a row that leads somewhere. */
124
+ chevron?: boolean;
125
+ }
126
+ /**
127
+ * One line of the panel: `Frame.Media`, `Frame.Content` and `Frame.Actions`
128
+ * side by side. With `href` it is a link and with `onClick` a button.
129
+ */
130
+ declare function FrameRow({ className, children, chevron, href, target, rel, ...props }: FrameRowProps): import("react").JSX.Element;
131
+ declare namespace FrameRow {
132
+ var displayName: string;
133
+ }
134
+ export interface FrameMediaProps extends ComponentProps<'div'> {
135
+ }
136
+ /** A row's leading icon, avatar or picture. Holds its size. */
137
+ declare function FrameMedia({ className, ...props }: FrameMediaProps): import("react").JSX.Element;
138
+ declare namespace FrameMedia {
139
+ var displayName: string;
140
+ }
141
+ export interface FrameContentProps extends ComponentProps<'div'> {
142
+ }
143
+ /**
144
+ * The flexible middle of a row, usually a title over a description. It takes
145
+ * what the media and actions leave and may shrink past its content, which is
146
+ * what keeps a long line from pushing the rest of the row off the edge.
147
+ */
148
+ declare function FrameContent({ className, ...props }: FrameContentProps): import("react").JSX.Element;
149
+ declare namespace FrameContent {
150
+ var displayName: string;
151
+ }
152
+ export interface FrameActionsProps extends ComponentProps<'div'> {
153
+ }
154
+ /** A row's trailing side: a badge, a value, a switch, a small button. Holds its size. */
155
+ declare function FrameActions({ className, ...props }: FrameActionsProps): import("react").JSX.Element;
156
+ declare namespace FrameActions {
157
+ var displayName: string;
158
+ }
159
+ export interface FrameSectionProps extends Omit<ComponentProps<'section'>, 'title'> {
160
+ /** The heading over the section's rows. */
161
+ title?: ReactNode;
162
+ }
163
+ /** A labelled group of rows, for a panel holding more than one. */
164
+ declare function FrameSection({ title, className, children, ...props }: FrameSectionProps): import("react").JSX.Element;
165
+ declare namespace FrameSection {
166
+ var displayName: string;
167
+ }
168
+ export interface FrameFooterProps extends ComponentProps<'div'> {
169
+ }
170
+ /**
171
+ * The strip below the panel. Under `inset` its children become equal pills in
172
+ * the recessed band, the shape a row of decisions takes.
173
+ */
174
+ declare function FrameFooter({ className, ...props }: FrameFooterProps): import("react").JSX.Element;
175
+ declare namespace FrameFooter {
176
+ var displayName: string;
177
+ }
178
+ export declare const Frame: typeof FrameRoot & {
179
+ Header: typeof FrameHeader;
180
+ Title: typeof FrameTitle;
181
+ Description: typeof FrameDescription;
182
+ Action: typeof FrameAction;
183
+ Panel: typeof FramePanel;
184
+ Row: typeof FrameRow;
185
+ Media: typeof FrameMedia;
186
+ Content: typeof FrameContent;
187
+ Actions: typeof FrameActions;
188
+ Section: typeof FrameSection;
189
+ Footer: typeof FrameFooter;
190
+ };
191
+ export { FrameHeader, FrameTitle, FrameDescription, FrameAction, FramePanel, FrameRow, FrameMedia, FrameContent, FrameActions, FrameSection, FrameFooter, };
@@ -0,0 +1,186 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { tv } from 'tailwind-variants';
3
+ /*
4
+ * `inset` geometry. The panel's radius is the shell's less the shell's padding,
5
+ * so the two curves are concentric — a panel radius that ignored the band
6
+ * would leave a crescent of shell thicker at the corners than along the sides.
7
+ */
8
+ const frameVariants = tv({
9
+ slots: {
10
+ root: 'group/frame flex flex-col text-foreground',
11
+ header: [
12
+ 'grid grid-cols-[minmax(0,1fr)] items-center gap-x-3 px-4 pt-2.5 pb-3',
13
+ 'has-data-[slot=frame-action]:grid-cols-[minmax(0,1fr)_auto]',
14
+ // The action spans two rows, so with no description the second is empty
15
+ // and a gap under the title would only add height.
16
+ 'has-data-[slot=frame-description]:gap-y-0.5',
17
+ 'group-data-[variant=inset]/frame:px-3 group-data-[variant=inset]/frame:pt-1 group-data-[variant=inset]/frame:pb-2.5',
18
+ 'group-data-[variant=plain]/frame:px-1',
19
+ 'group-data-[variant=band]/frame:px-3.5 group-data-[variant=band]/frame:py-2.5',
20
+ ],
21
+ /*
22
+ * One part in two places. In the header it labels the tray, so it stays
23
+ * quiet; inside a row's content it is the row's subject, so it takes the
24
+ * foreground and the weight, and truncates rather than pushing the row's
25
+ * trailing slot out of view.
26
+ */
27
+ title: [
28
+ 'text-sm font-medium text-muted-foreground',
29
+ 'in-data-[slot=frame-content]:truncate in-data-[slot=frame-content]:text-foreground',
30
+ // A band names one thing rather than captioning a list, so its title reads first.
31
+ 'group-data-[variant=band]/frame:text-foreground',
32
+ ],
33
+ description: 'text-sm text-muted-foreground in-data-[slot=frame-content]:truncate',
34
+ action: 'col-start-2 row-span-2 row-start-1 text-sm text-muted-foreground',
35
+ // The panel's shape follows the root's variant, read from its data attribute.
36
+ panel: [
37
+ 'flex flex-col divide-y overflow-hidden bg-card text-card-foreground',
38
+ // A band holds one figure, not a list of rows, so nothing in it is ruled off.
39
+ 'group-data-[variant=band]/frame:divide-y-0',
40
+ 'group-data-[variant=default]/frame:rounded-t-2xl group-data-[variant=default]/frame:border-t',
41
+ 'group-data-[variant=plain]/frame:rounded-3xl group-data-[variant=plain]/frame:border group-data-[variant=plain]/frame:shadow-xs',
42
+ 'group-data-[variant=inset]/frame:rounded-[30px] group-data-[variant=inset]/frame:bg-popover group-data-[variant=inset]/frame:text-popover-foreground group-data-[variant=inset]/frame:shadow-xs',
43
+ // 18px is the band's 24 less its 6px of padding: one corner, two curves.
44
+ 'group-data-[variant=band]/frame:rounded-[1.125rem] group-data-[variant=band]/frame:border group-data-[variant=band]/frame:shadow-xs',
45
+ ],
46
+ row: 'flex w-full items-center gap-3 px-4 py-3 text-start',
47
+ rowPressable: [
48
+ 'cursor-pointer outline-none transition-colors hover:bg-muted/60',
49
+ 'focus-visible:bg-muted/60 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset',
50
+ ],
51
+ chevron: 'size-4 shrink-0 text-muted-foreground/70 rtl:-scale-x-100',
52
+ media: 'flex shrink-0 items-center justify-center',
53
+ content: 'flex min-w-0 flex-1 flex-col gap-0.5',
54
+ actions: 'flex shrink-0 items-center gap-1.5',
55
+ section: 'flex flex-col divide-y',
56
+ sectionTitle: 'bg-muted/40 px-4 pt-2.5 pb-1.5 text-xs font-medium uppercase tracking-wider text-muted-foreground',
57
+ footer: [
58
+ 'flex items-center gap-2 px-4 pt-3 pb-3.5',
59
+ // Under `inset` the band is a row of equal decisions: equal widths, full pills.
60
+ 'group-data-[variant=inset]/frame:gap-3.5 group-data-[variant=inset]/frame:px-[18px] group-data-[variant=inset]/frame:pt-4 group-data-[variant=inset]/frame:pb-2.5',
61
+ 'group-data-[variant=inset]/frame:*:h-11 group-data-[variant=inset]/frame:*:flex-1 group-data-[variant=inset]/frame:*:rounded-full',
62
+ 'group-data-[variant=plain]/frame:px-1',
63
+ 'group-data-[variant=band]/frame:px-3.5 group-data-[variant=band]/frame:pt-2.5 group-data-[variant=band]/frame:pb-1',
64
+ ],
65
+ },
66
+ variants: {
67
+ variant: {
68
+ default: {
69
+ // `overflow-hidden` is what gives the panel the shell's bottom corners.
70
+ root: 'overflow-hidden rounded-3xl border bg-muted',
71
+ },
72
+ plain: {},
73
+ band: {
74
+ root: 'rounded-3xl bg-muted p-1.5',
75
+ },
76
+ inset: {
77
+ // The band is the popover surface with black mixed in, so it is the
78
+ // darker of the two in either theme, which a step on a grey scale
79
+ // cannot promise: those run darker one way and lighter the other.
80
+ root: [
81
+ 'rounded-[38px] p-2 ring-1 ring-foreground/[0.06]',
82
+ 'bg-[color-mix(in_oklab,var(--popover),black_5%)] dark:bg-[color-mix(in_oklab,var(--popover),black_14%)]',
83
+ ],
84
+ },
85
+ },
86
+ },
87
+ defaultVariants: { variant: 'default' },
88
+ });
89
+ function FrameRoot({ variant = 'default', className, ...props }) {
90
+ const slots = frameVariants({ variant });
91
+ return _jsx("div", { "data-slot": "frame", "data-variant": variant, className: slots.root({ className }), ...props });
92
+ }
93
+ FrameRoot.displayName = 'Frame';
94
+ /**
95
+ * The strip of shell above the panel: a title, an optional description under
96
+ * it, and `Frame.Action` on the trailing side.
97
+ */
98
+ function FrameHeader({ className, ...props }) {
99
+ return _jsx("div", { "data-slot": "frame-header", className: frameVariants().header({ className }), ...props });
100
+ }
101
+ FrameHeader.displayName = 'Frame.Header';
102
+ /** A caption in the header; the subject line inside `Frame.Content`. */
103
+ function FrameTitle({ as: Tag = 'h3', className, ...props }) {
104
+ return _jsx(Tag, { "data-slot": "frame-title", className: frameVariants().title({ className }), ...props });
105
+ }
106
+ FrameTitle.displayName = 'Frame.Title';
107
+ /** A line under a title, in the header or in a row. */
108
+ function FrameDescription({ className, ...props }) {
109
+ return (_jsx("p", { "data-slot": "frame-description", className: frameVariants().description({ className }), ...props }));
110
+ }
111
+ FrameDescription.displayName = 'Frame.Description';
112
+ /** The header's trailing side: a short status, a link, a small button. */
113
+ function FrameAction({ className, ...props }) {
114
+ return _jsx("div", { "data-slot": "frame-action", className: frameVariants().action({ className }), ...props });
115
+ }
116
+ FrameAction.displayName = 'Frame.Action';
117
+ /** The card the rows sit on. It draws the hairline between its children, except under `band`. */
118
+ function FramePanel({ className, ...props }) {
119
+ return _jsx("div", { "data-slot": "frame-panel", className: frameVariants().panel({ className }), ...props });
120
+ }
121
+ FramePanel.displayName = 'Frame.Panel';
122
+ const Chevron = ({ className }) => (_jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": true, className: className, fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m9 6 6 6-6 6" }) }));
123
+ /**
124
+ * One line of the panel: `Frame.Media`, `Frame.Content` and `Frame.Actions`
125
+ * side by side. With `href` it is a link and with `onClick` a button.
126
+ */
127
+ function FrameRow({ className, children, chevron = false, href, target, rel, ...props }) {
128
+ const slots = frameVariants();
129
+ const inside = (_jsxs(_Fragment, { children: [children, chevron ? _jsx(Chevron, { className: slots.chevron() }) : null] }));
130
+ if (href !== undefined) {
131
+ return (_jsx("a", { "data-slot": "frame-row", href: href, target: target, rel: rel, className: slots.row({ className: [slots.rowPressable(), className] }), ...props, children: inside }));
132
+ }
133
+ // `onClick` stays in `props`, so it reaches whichever element is rendered.
134
+ if (props.onClick !== undefined) {
135
+ return (_jsx("button", { type: "button", "data-slot": "frame-row", className: slots.row({ className: [slots.rowPressable(), className] }), ...props, children: inside }));
136
+ }
137
+ return (_jsx("div", { "data-slot": "frame-row", className: slots.row({ className }), ...props, children: inside }));
138
+ }
139
+ FrameRow.displayName = 'Frame.Row';
140
+ /** A row's leading icon, avatar or picture. Holds its size. */
141
+ function FrameMedia({ className, ...props }) {
142
+ return _jsx("div", { "data-slot": "frame-media", className: frameVariants().media({ className }), ...props });
143
+ }
144
+ FrameMedia.displayName = 'Frame.Media';
145
+ /**
146
+ * The flexible middle of a row, usually a title over a description. It takes
147
+ * what the media and actions leave and may shrink past its content, which is
148
+ * what keeps a long line from pushing the rest of the row off the edge.
149
+ */
150
+ function FrameContent({ className, ...props }) {
151
+ return _jsx("div", { "data-slot": "frame-content", className: frameVariants().content({ className }), ...props });
152
+ }
153
+ FrameContent.displayName = 'Frame.Content';
154
+ /** A row's trailing side: a badge, a value, a switch, a small button. Holds its size. */
155
+ function FrameActions({ className, ...props }) {
156
+ return _jsx("div", { "data-slot": "frame-actions", className: frameVariants().actions({ className }), ...props });
157
+ }
158
+ FrameActions.displayName = 'Frame.Actions';
159
+ /** A labelled group of rows, for a panel holding more than one. */
160
+ function FrameSection({ title, className, children, ...props }) {
161
+ const slots = frameVariants();
162
+ return (_jsxs("section", { "data-slot": "frame-section", className: slots.section({ className }), ...props, children: [title !== undefined ? _jsx("h4", { className: slots.sectionTitle(), children: title }) : null, children] }));
163
+ }
164
+ FrameSection.displayName = 'Frame.Section';
165
+ /**
166
+ * The strip below the panel. Under `inset` its children become equal pills in
167
+ * the recessed band, the shape a row of decisions takes.
168
+ */
169
+ function FrameFooter({ className, ...props }) {
170
+ return _jsx("div", { "data-slot": "frame-footer", className: frameVariants().footer({ className }), ...props });
171
+ }
172
+ FrameFooter.displayName = 'Frame.Footer';
173
+ export const Frame = Object.assign(FrameRoot, {
174
+ Header: FrameHeader,
175
+ Title: FrameTitle,
176
+ Description: FrameDescription,
177
+ Action: FrameAction,
178
+ Panel: FramePanel,
179
+ Row: FrameRow,
180
+ Media: FrameMedia,
181
+ Content: FrameContent,
182
+ Actions: FrameActions,
183
+ Section: FrameSection,
184
+ Footer: FrameFooter,
185
+ });
186
+ export { FrameHeader, FrameTitle, FrameDescription, FrameAction, FramePanel, FrameRow, FrameMedia, FrameContent, FrameActions, FrameSection, FrameFooter, };
@@ -187,9 +187,11 @@ declare namespace KpiSeparator {
187
187
  export interface KpiGroupProps extends ComponentProps<'div'> {
188
188
  /** Side by side with upright rules, or stacked with level ones. */
189
189
  orientation?: 'horizontal' | 'vertical';
190
+ /** Draws the group as a card of its own, for a group not already inside one. */
191
+ surface?: boolean;
190
192
  }
191
193
  /** 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;
194
+ declare function KpiGroup({ orientation, surface, className, ...props }: KpiGroupProps): import("react").JSX.Element;
193
195
  declare namespace KpiGroup {
194
196
  var displayName: string;
195
197
  }
@@ -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, };
@@ -124,9 +124,32 @@ declare function PlanetControls({ step, className, children, ...props }: PlanetC
124
124
  declare namespace PlanetControls {
125
125
  var displayName: string;
126
126
  }
127
+ export interface PlanetListProps extends ComponentProps<'ul'> {
128
+ }
129
+ /**
130
+ * The places on the globe as a list, in two columns, for picking one by name.
131
+ * Put it beside or under the globe and turn the globe to whatever is picked.
132
+ */
133
+ declare function PlanetList({ className, ...props }: PlanetListProps): import("react").JSX.Element;
134
+ declare namespace PlanetList {
135
+ var displayName: string;
136
+ }
137
+ export interface PlanetListItemProps extends Omit<ComponentProps<'button'>, 'color'> {
138
+ /** The colour of the dot before the name. Match it to the place's marker. */
139
+ color?: PlanetMarkerColor;
140
+ /** Draws the row as the one that has been picked. */
141
+ selected?: boolean;
142
+ }
143
+ /** One place in `Planet.List`: a dot in its marker's colour, and its name. A button. */
144
+ declare function PlanetListItem({ color, selected, className, children, ...props }: PlanetListItemProps): import("react").JSX.Element;
145
+ declare namespace PlanetListItem {
146
+ var displayName: string;
147
+ }
127
148
  export declare const Planet: import("react").ForwardRefExoticComponent<Omit<PlanetProps, "ref"> & import("react").RefAttributes<PlanetHandle>> & {
128
149
  Globe: typeof PlanetGlobe;
129
150
  Marker: typeof PlanetMarker;
130
151
  Controls: typeof PlanetControls;
152
+ List: typeof PlanetList;
153
+ ListItem: typeof PlanetListItem;
131
154
  };
132
- export { PlanetGlobe, PlanetMarker, PlanetControls };
155
+ export { PlanetGlobe, PlanetMarker, PlanetControls, PlanetList, PlanetListItem };