panelui-web 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/avatar/index.d.ts +66 -0
- package/dist/components/avatar/index.js +251 -0
- package/dist/components/button/index.d.ts +111 -0
- package/dist/components/button/index.js +45 -0
- package/dist/components/flow/index.d.ts +26 -4
- package/dist/components/flow/index.js +81 -20
- package/dist/components/input/index.d.ts +55 -0
- package/dist/components/input/index.js +89 -0
- package/dist/components/label/index.d.ts +89 -0
- package/dist/components/label/index.js +64 -0
- package/dist/components/menu/index.d.ts +202 -0
- package/dist/components/menu/index.js +188 -0
- package/dist/components/message/index.d.ts +161 -0
- package/dist/components/message/index.js +295 -0
- package/dist/components/planet/index.js +4 -1
- package/dist/components/prompt/index.d.ts +414 -0
- package/dist/components/prompt/index.js +779 -0
- package/dist/components/select/index.d.ts +134 -0
- package/dist/components/select/index.js +164 -0
- package/dist/components/sheet/index.d.ts +43 -4
- package/dist/components/sheet/index.js +121 -12
- package/dist/components/switch/index.d.ts +51 -0
- package/dist/components/switch/index.js +88 -0
- package/dist/components/tabs/index.d.ts +94 -0
- package/dist/components/tabs/index.js +196 -0
- package/dist/components/textarea/index.d.ts +43 -0
- package/dist/components/textarea/index.js +101 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.js +12 -1
- package/package.json +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode } from 'react';
|
|
2
|
+
export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
3
|
+
export type AvatarShape = 'circle' | 'square';
|
|
4
|
+
export type AvatarColor = 'chart-1' | 'chart-2' | 'chart-3' | 'chart-4' | 'chart-5' | 'muted';
|
|
5
|
+
export type AvatarStatus = 'online' | 'away' | 'busy' | 'offline';
|
|
6
|
+
export interface AvatarProps extends Omit<ComponentProps<'span'>, 'color'> {
|
|
7
|
+
/** The picture. Without it, or until it has loaded, the initials show. */
|
|
8
|
+
src?: string;
|
|
9
|
+
/** Who it is. Gives the initials, the colour and what a screen reader says. */
|
|
10
|
+
name?: string;
|
|
11
|
+
/** Read out in place of `name`, when that is not the right thing to say. */
|
|
12
|
+
alt?: string;
|
|
13
|
+
/** Default `md`. In a group, the group's size. */
|
|
14
|
+
size?: AvatarSize;
|
|
15
|
+
/** `circle` for people, `square` for teams, projects and things. Default `circle`. */
|
|
16
|
+
shape?: AvatarShape;
|
|
17
|
+
/** The tint behind the initials. Picked from `name` when left out. */
|
|
18
|
+
color?: AvatarColor;
|
|
19
|
+
/** In place of the initials, such as an icon for a team or a bot. */
|
|
20
|
+
fallback?: ReactNode;
|
|
21
|
+
/** An `Avatar.Badge`. */
|
|
22
|
+
children?: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
/** A picture of someone, or their initials in a colour of their own. */
|
|
25
|
+
declare function AvatarRoot({ src, name, alt, size, shape, color, fallback, className, style, children, ...props }: AvatarProps): import("react").JSX.Element;
|
|
26
|
+
declare namespace AvatarRoot {
|
|
27
|
+
var displayName: string;
|
|
28
|
+
}
|
|
29
|
+
export interface AvatarBadgeProps extends Omit<ComponentProps<'span'>, 'color'> {
|
|
30
|
+
/** A presence dot in the colour that says it. Leave out for a badge of your own. */
|
|
31
|
+
status?: AvatarStatus;
|
|
32
|
+
/** For a badge with an icon in it: the fill behind the icon. */
|
|
33
|
+
color?: AvatarColor;
|
|
34
|
+
/** What a screen reader says after the name. Defaults to the status. */
|
|
35
|
+
label?: string;
|
|
36
|
+
/** An icon, such as a check for a verified account. */
|
|
37
|
+
children?: ReactNode;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A dot or a small icon on the avatar's lower trailing edge. The avatar is
|
|
41
|
+
* cut away round it, so it reads on any background.
|
|
42
|
+
*/
|
|
43
|
+
declare function AvatarBadge({ status, color, label: _label, className, ...props }: AvatarBadgeProps): import("react").JSX.Element;
|
|
44
|
+
declare namespace AvatarBadge {
|
|
45
|
+
var displayName: string;
|
|
46
|
+
}
|
|
47
|
+
export interface AvatarGroupProps extends ComponentProps<'div'> {
|
|
48
|
+
/** How many to show. The rest are counted in a last circle that lists them. */
|
|
49
|
+
max?: number;
|
|
50
|
+
/** The size of every avatar in it. Default `md`. */
|
|
51
|
+
size?: AvatarSize;
|
|
52
|
+
/** The shape of every avatar in it. Default `circle`. */
|
|
53
|
+
shape?: AvatarShape;
|
|
54
|
+
/** What a screen reader calls the group, such as "Members". */
|
|
55
|
+
label?: string;
|
|
56
|
+
}
|
|
57
|
+
/** A row of overlapping avatars, that opens up when it is hovered or focused. */
|
|
58
|
+
declare function AvatarGroup({ max, size, shape, label, className, style, children, ...props }: AvatarGroupProps): import("react").JSX.Element;
|
|
59
|
+
declare namespace AvatarGroup {
|
|
60
|
+
var displayName: string;
|
|
61
|
+
}
|
|
62
|
+
export declare const Avatar: typeof AvatarRoot & {
|
|
63
|
+
Badge: typeof AvatarBadge;
|
|
64
|
+
Group: typeof AvatarGroup;
|
|
65
|
+
};
|
|
66
|
+
export { AvatarBadge, AvatarGroup };
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Avatar — a person or a team as a picture, with their initials when there
|
|
5
|
+
* is no picture to show.
|
|
6
|
+
*
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <Avatar src="/noor.jpg" name="Noor Haddad" />
|
|
9
|
+
*
|
|
10
|
+
* <Avatar name="Tomás Ferreira" size="lg">
|
|
11
|
+
* <Avatar.Badge status="online" />
|
|
12
|
+
* </Avatar>
|
|
13
|
+
*
|
|
14
|
+
* <Avatar.Group max={4}>
|
|
15
|
+
* {people.map((person) => <Avatar key={person.id} src={person.photo} name={person.name} />)}
|
|
16
|
+
* </Avatar.Group>
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* ## Initials in a colour of their own
|
|
20
|
+
*
|
|
21
|
+
* Without a picture, or while one loads, the avatar shows the initials of
|
|
22
|
+
* `name` on a tint of one of the theme's chart colours. The colour is picked
|
|
23
|
+
* from the name, so the same person has the same colour on every screen, and
|
|
24
|
+
* a row of people without pictures can still be told apart at a glance.
|
|
25
|
+
*
|
|
26
|
+
* ## Cut out, not ringed
|
|
27
|
+
*
|
|
28
|
+
* A badge, and the avatar overlapping the next one in a group, are not drawn
|
|
29
|
+
* with a ring in the page's background colour. The avatar underneath is cut
|
|
30
|
+
* away with a mask instead, so the gap round them is a real gap: it shows
|
|
31
|
+
* whatever the avatar sits on, a card, a tinted row, a photograph, and needs
|
|
32
|
+
* no colour to match it.
|
|
33
|
+
*
|
|
34
|
+
* ## A group opens up
|
|
35
|
+
*
|
|
36
|
+
* Avatars in a group overlap. Hovered or focused, they move apart, and the cut
|
|
37
|
+
* in each moves with the one beside it. Past `max`, the rest are counted in a
|
|
38
|
+
* last circle that opens a list of who they are.
|
|
39
|
+
*/
|
|
40
|
+
import { Avatar as BaseAvatar } from '@base-ui/react/avatar';
|
|
41
|
+
import { Popover } from '@base-ui/react/popover';
|
|
42
|
+
import { Children, createContext, isValidElement, useContext, useMemo, } from 'react';
|
|
43
|
+
import { tv } from 'tailwind-variants';
|
|
44
|
+
const SIZE = {
|
|
45
|
+
xs: '1.5rem',
|
|
46
|
+
sm: '2rem',
|
|
47
|
+
md: '2.5rem',
|
|
48
|
+
lg: '3.5rem',
|
|
49
|
+
xl: '4.5rem',
|
|
50
|
+
};
|
|
51
|
+
const COLORS = ['chart-1', 'chart-2', 'chart-3', 'chart-4', 'chart-5'];
|
|
52
|
+
const avatarVariants = tv({
|
|
53
|
+
slots: {
|
|
54
|
+
root: 'relative inline-flex size-(--avatar-size) shrink-0 align-middle',
|
|
55
|
+
/*
|
|
56
|
+
* The face is what gets cut. Its hairline is inside it, so the cut takes
|
|
57
|
+
* the hairline with it and no stray arc of it is left in the gap.
|
|
58
|
+
*/
|
|
59
|
+
face: [
|
|
60
|
+
'relative flex size-full overflow-hidden bg-muted select-none',
|
|
61
|
+
"after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:ring-1 after:ring-inset after:ring-foreground/10 after:content-['']",
|
|
62
|
+
'[mask-repeat:no-repeat] [mask-composite:intersect]',
|
|
63
|
+
],
|
|
64
|
+
image: 'size-full object-cover',
|
|
65
|
+
fallback: [
|
|
66
|
+
'flex size-full items-center justify-center font-medium leading-none tracking-tight',
|
|
67
|
+
'text-[calc(var(--avatar-size)*0.38)]',
|
|
68
|
+
'bg-[color-mix(in_oklab,var(--avatar-color)_22%,var(--card))] text-[color-mix(in_oklab,var(--avatar-color)_70%,var(--foreground))]',
|
|
69
|
+
'[&_svg]:size-[55%] [&_svg]:opacity-70',
|
|
70
|
+
],
|
|
71
|
+
badge: [
|
|
72
|
+
'absolute flex size-(--badge-size) items-center justify-center rounded-full',
|
|
73
|
+
'left-[calc(var(--badge-center)-var(--badge-size)/2)] top-[calc(var(--badge-center)-var(--badge-size)/2)]',
|
|
74
|
+
'text-[calc(var(--badge-size)*0.6)] [&_svg]:size-[70%]',
|
|
75
|
+
],
|
|
76
|
+
group: [
|
|
77
|
+
'group/avatars inline-flex items-center [--avatar-overlap:0.22]',
|
|
78
|
+
// Hovered or focused, the group opens up so each face can be seen whole.
|
|
79
|
+
'hover:[--avatar-overlap:0.08] has-focus-visible:[--avatar-overlap:0.08] motion-reduce:hover:[--avatar-overlap:0.22]',
|
|
80
|
+
],
|
|
81
|
+
item: [
|
|
82
|
+
'not-first:ms-[calc(var(--avatar-size)*var(--avatar-overlap)*-1)]',
|
|
83
|
+
'transition-[margin] duration-300 ease-[cubic-bezier(0.2,0,0,1)] motion-reduce:transition-none',
|
|
84
|
+
// The face's cut follows the margin, in step with it.
|
|
85
|
+
'*:data-[slot=avatar-face]:transition-[mask-position] *:data-[slot=avatar-face]:duration-300 *:data-[slot=avatar-face]:ease-[cubic-bezier(0.2,0,0,1)]',
|
|
86
|
+
],
|
|
87
|
+
count: [
|
|
88
|
+
'flex size-full cursor-pointer items-center justify-center bg-muted font-medium tabular-nums text-muted-foreground outline-none',
|
|
89
|
+
'text-[calc(var(--avatar-size)*0.34)] transition-colors hover:bg-accent hover:text-foreground',
|
|
90
|
+
'focus-visible:ring-2 focus-visible:ring-ring data-popup-open:bg-accent data-popup-open:text-foreground',
|
|
91
|
+
],
|
|
92
|
+
popup: [
|
|
93
|
+
'z-50 min-w-52 origin-(--transform-origin) rounded-xl border bg-popover p-1 text-popover-foreground shadow-lg outline-none',
|
|
94
|
+
'max-h-(--available-height) overflow-y-auto',
|
|
95
|
+
'transition-[opacity,scale] duration-150 ease-out data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:scale-95 data-ending-style:opacity-0',
|
|
96
|
+
'motion-reduce:transition-none',
|
|
97
|
+
],
|
|
98
|
+
row: 'flex items-center gap-2.5 rounded-lg px-2 py-1.5 text-sm',
|
|
99
|
+
},
|
|
100
|
+
variants: {
|
|
101
|
+
shape: {
|
|
102
|
+
circle: { face: 'rounded-full', root: '[--badge-center:85.4%]' },
|
|
103
|
+
// The badge sits further in on a square, whose corner is further out.
|
|
104
|
+
square: { face: 'rounded-[28%]', root: '[--badge-center:calc(100%-var(--badge-size)*0.3)]' },
|
|
105
|
+
},
|
|
106
|
+
status: {
|
|
107
|
+
online: { badge: 'bg-success' },
|
|
108
|
+
away: { badge: 'bg-warning' },
|
|
109
|
+
busy: { badge: 'bg-destructive' },
|
|
110
|
+
// Hollow: there, but not reachable.
|
|
111
|
+
offline: { badge: 'border-[max(1.5px,calc(var(--badge-size)*0.2))] border-muted-foreground/70' },
|
|
112
|
+
},
|
|
113
|
+
color: {
|
|
114
|
+
'chart-1': { root: '[--avatar-color:var(--chart-1)]', badge: 'bg-chart-1 text-background' },
|
|
115
|
+
'chart-2': { root: '[--avatar-color:var(--chart-2)]', badge: 'bg-chart-2 text-background' },
|
|
116
|
+
'chart-3': { root: '[--avatar-color:var(--chart-3)]', badge: 'bg-chart-3 text-background' },
|
|
117
|
+
'chart-4': { root: '[--avatar-color:var(--chart-4)]', badge: 'bg-chart-4 text-background' },
|
|
118
|
+
'chart-5': { root: '[--avatar-color:var(--chart-5)]', badge: 'bg-chart-5 text-background' },
|
|
119
|
+
muted: { root: '[--avatar-color:var(--muted-foreground)]', badge: 'bg-foreground text-background' },
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
defaultVariants: { shape: 'circle' },
|
|
123
|
+
});
|
|
124
|
+
/** The same name always lands on the same colour. */
|
|
125
|
+
function colorOf(name) {
|
|
126
|
+
if (!name)
|
|
127
|
+
return 'muted';
|
|
128
|
+
let hash = 0;
|
|
129
|
+
for (const char of name)
|
|
130
|
+
hash = (hash * 31 + char.codePointAt(0)) | 0;
|
|
131
|
+
return COLORS[Math.abs(hash) % COLORS.length];
|
|
132
|
+
}
|
|
133
|
+
/** First letter of the first and last words: "Noor al-Haddad" is "NA". */
|
|
134
|
+
function initialsOf(name) {
|
|
135
|
+
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
136
|
+
if (words.length === 0)
|
|
137
|
+
return '';
|
|
138
|
+
const first = [...words[0]][0] ?? '';
|
|
139
|
+
const last = words.length > 1 ? ([...words[words.length - 1]][0] ?? '') : '';
|
|
140
|
+
return (first + last).toUpperCase();
|
|
141
|
+
}
|
|
142
|
+
const STATUS_LABEL = {
|
|
143
|
+
online: 'online',
|
|
144
|
+
away: 'away',
|
|
145
|
+
busy: 'busy',
|
|
146
|
+
offline: 'offline',
|
|
147
|
+
};
|
|
148
|
+
/* -------------------------------------------------------------------------- */
|
|
149
|
+
/* Masks */
|
|
150
|
+
/* -------------------------------------------------------------------------- */
|
|
151
|
+
/*
|
|
152
|
+
* Every face has two mask layers, intersected: one for the badge, one for the
|
|
153
|
+
* avatar overlapping it in a group. A layer with nothing to cut is solid.
|
|
154
|
+
*
|
|
155
|
+
* Each cut is a gradient drawn in a box bigger than the face and placed with
|
|
156
|
+
* `mask-position`, rather than a gradient placed by its own coordinates. A
|
|
157
|
+
* position animates; a gradient's coordinates do not. That is what lets the
|
|
158
|
+
* cut slide along with the next avatar as the group opens.
|
|
159
|
+
*/
|
|
160
|
+
const SOLID = 'linear-gradient(#000,#000)';
|
|
161
|
+
const GAP = 'max(1.5px, calc(var(--avatar-size) * 0.05))';
|
|
162
|
+
/** A hole round the badge. */
|
|
163
|
+
const BADGE_CUT = `radial-gradient(circle at var(--badge-center) var(--badge-center), transparent calc(var(--badge-size) / 2 + ${GAP}), #000 calc(var(--badge-size) / 2 + ${GAP} + 0.5px))`;
|
|
164
|
+
/**
|
|
165
|
+
* A hole where the next avatar lies over this one. The box is three faces
|
|
166
|
+
* wide with the hole at its centre, then shifted so the centre lands on the
|
|
167
|
+
* next avatar's centre: `size × (1 − overlap)` along.
|
|
168
|
+
*/
|
|
169
|
+
const CIRCLE_CUT = `radial-gradient(circle, transparent calc(var(--avatar-size) / 2 + ${GAP}), #000 calc(var(--avatar-size) / 2 + ${GAP} + 0.5px))`;
|
|
170
|
+
const CIRCLE_CUT_SIZE = 'calc(var(--avatar-size) * 3) calc(var(--avatar-size) * 3)';
|
|
171
|
+
const CIRCLE_CUT_AT = 'calc(var(--avatar-size) * var(--avatar-overlap) * -1) calc(var(--avatar-size) * -1)';
|
|
172
|
+
/**
|
|
173
|
+
* A square's hole is the next square's own shape, a rounded square a little
|
|
174
|
+
* larger than it, drawn in the same three-face box so it moves the same way.
|
|
175
|
+
* A gradient cannot draw a rounded square, so this one is an SVG: 100 units
|
|
176
|
+
* to a face, with the corner at the face's 28% and the gap at 5%.
|
|
177
|
+
*/
|
|
178
|
+
const SQUARE_CUT = `url("data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300" preserveAspectRatio="none"><path fill-rule="evenodd" d="M0 0H300V300H0Z M128 95H172A33 33 0 0 1 205 128V172A33 33 0 0 1 172 205H128A33 33 0 0 1 95 172V128A33 33 0 0 1 128 95Z"/></svg>')}")`;
|
|
179
|
+
function maskStyle(shape, badge, overlapped) {
|
|
180
|
+
if (!badge && !overlapped)
|
|
181
|
+
return undefined;
|
|
182
|
+
const layers = [
|
|
183
|
+
badge ? [BADGE_CUT, '100% 100%', '0 0'] : [SOLID, '100% 100%', '0 0'],
|
|
184
|
+
overlapped
|
|
185
|
+
? shape === 'circle'
|
|
186
|
+
? [CIRCLE_CUT, CIRCLE_CUT_SIZE, CIRCLE_CUT_AT]
|
|
187
|
+
: [SQUARE_CUT, CIRCLE_CUT_SIZE, CIRCLE_CUT_AT]
|
|
188
|
+
: [SOLID, '100% 100%', '0 0'],
|
|
189
|
+
];
|
|
190
|
+
return {
|
|
191
|
+
maskImage: layers.map((layer) => layer[0]).join(', '),
|
|
192
|
+
maskSize: layers.map((layer) => layer[1]).join(', '),
|
|
193
|
+
maskPosition: layers.map((layer) => layer[2]).join(', '),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const GroupContext = createContext(null);
|
|
197
|
+
/** Set on every avatar in a group but the last, which nothing overlaps. */
|
|
198
|
+
const OverlappedContext = createContext(false);
|
|
199
|
+
/** A picture of someone, or their initials in a colour of their own. */
|
|
200
|
+
function AvatarRoot({ src, name, alt, size, shape, color, fallback, className, style, children, ...props }) {
|
|
201
|
+
const group = useContext(GroupContext);
|
|
202
|
+
const overlapped = useContext(OverlappedContext);
|
|
203
|
+
const resolvedSize = size ?? group?.size ?? 'md';
|
|
204
|
+
const resolvedShape = shape ?? group?.shape ?? 'circle';
|
|
205
|
+
const slots = avatarVariants({ shape: resolvedShape, color: color ?? colorOf(name) });
|
|
206
|
+
// A badge's status is part of who it is, so it is said with the name.
|
|
207
|
+
const badge = Children.toArray(children).find((child) => isValidElement(child) && child.type === AvatarBadge);
|
|
208
|
+
const status = badge?.props.label ?? (badge?.props.status ? STATUS_LABEL[badge.props.status] : undefined);
|
|
209
|
+
const label = [alt ?? name, status].filter(Boolean).join(', ') || undefined;
|
|
210
|
+
return (_jsxs("span", { "data-slot": "avatar", "data-size": resolvedSize, "data-shape": resolvedShape, role: label ? 'img' : undefined, "aria-label": label, className: slots.root({ className }), style: {
|
|
211
|
+
['--avatar-size']: SIZE[resolvedSize],
|
|
212
|
+
// Here and not on the badge, so the face's cut and the badge's place can both read it.
|
|
213
|
+
['--badge-size']: 'max(0.5rem, calc(var(--avatar-size) * 0.28))',
|
|
214
|
+
...style,
|
|
215
|
+
}, ...props, children: [_jsxs(BaseAvatar.Root, { "data-slot": "avatar-face", "aria-hidden": "true", className: slots.face(), style: maskStyle(resolvedShape, badge !== undefined, overlapped), children: [src ? _jsx(BaseAvatar.Image, { src: src, alt: "", className: slots.image() }) : null, _jsx(BaseAvatar.Fallback, { className: slots.fallback(), children: fallback ?? (name ? initialsOf(name) : _jsx(PersonGlyph, {})) })] }), children] }));
|
|
216
|
+
}
|
|
217
|
+
AvatarRoot.displayName = 'Avatar';
|
|
218
|
+
function PersonGlyph() {
|
|
219
|
+
return (_jsxs("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: [_jsx("circle", { cx: "12", cy: "8.5", r: "4" }), _jsx("path", { d: "M4 20.5c0-4 3.6-6.5 8-6.5s8 2.5 8 6.5z" })] }));
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* A dot or a small icon on the avatar's lower trailing edge. The avatar is
|
|
223
|
+
* cut away round it, so it reads on any background.
|
|
224
|
+
*/
|
|
225
|
+
function AvatarBadge({ status, color, label: _label, className, ...props }) {
|
|
226
|
+
const slots = avatarVariants({ status, color: status ? undefined : (color ?? 'muted') });
|
|
227
|
+
return (_jsx("span", { "aria-hidden": "true", "data-slot": "avatar-badge", "data-status": status, className: slots.badge({ className }), ...props }));
|
|
228
|
+
}
|
|
229
|
+
AvatarBadge.displayName = 'Avatar.Badge';
|
|
230
|
+
/** A row of overlapping avatars, that opens up when it is hovered or focused. */
|
|
231
|
+
function AvatarGroup({ max, size = 'md', shape = 'circle', label, className, style, children, ...props }) {
|
|
232
|
+
const slots = avatarVariants({ shape });
|
|
233
|
+
const context = useMemo(() => ({ size, shape }), [size, shape]);
|
|
234
|
+
const all = Children.toArray(children).filter(isValidElement);
|
|
235
|
+
const shown = max !== undefined && all.length > max ? all.slice(0, Math.max(0, max)) : all;
|
|
236
|
+
const rest = all.slice(shown.length);
|
|
237
|
+
const items = shown.map((child, index) => (_jsx(OverlappedContext.Provider, { value: index < shown.length - 1 || rest.length > 0, children: _jsx("span", { "data-slot": "avatar-group-item", className: slots.item(), children: child }) }, child.key ?? index)));
|
|
238
|
+
return (_jsx(GroupContext.Provider, { value: context, children: _jsxs("div", { role: "group", "aria-label": label, "data-slot": "avatar-group", className: slots.group({ className }), style: { ['--avatar-size']: SIZE[size], ...style }, ...props, children: [items, rest.length > 0 ? (_jsx("span", { "data-slot": "avatar-group-item", className: slots.item(), children: _jsx(AvatarGroupCount, { people: rest, shape: shape }) })) : null] }) }));
|
|
239
|
+
}
|
|
240
|
+
AvatarGroup.displayName = 'Avatar.Group';
|
|
241
|
+
/** "+3", which opens a list of the three. */
|
|
242
|
+
function AvatarGroupCount({ people, shape }) {
|
|
243
|
+
const slots = avatarVariants({ shape });
|
|
244
|
+
const names = people.map((person) => person.props.alt ?? person.props.name).filter(Boolean);
|
|
245
|
+
return (_jsx("span", { "data-slot": "avatar", className: slots.root(), children: _jsxs(Popover.Root, { children: [_jsxs(Popover.Trigger, { "data-slot": "avatar-group-count", "aria-label": `${people.length} more${names.length ? `: ${names.join(', ')}` : ''}`, className: avatarVariants({ shape }).face({ className: slots.count() }), children: ["+", people.length] }), _jsx(Popover.Portal, { children: _jsx(Popover.Positioner, { side: "bottom", align: "start", sideOffset: 8, className: "z-50", children: _jsx(Popover.Popup, { className: slots.popup(), children: _jsx(GroupContext.Provider, { value: null, children: people.map((person, index) => (_jsxs("div", { className: slots.row(), children: [_jsx(AvatarRoot, { ...person.props, size: "xs", shape: shape, children: null }), _jsx("span", { className: "min-w-0 truncate", children: person.props.name ?? person.props.alt })] }, person.key ?? index))) }) }) }) })] }) }));
|
|
246
|
+
}
|
|
247
|
+
export const Avatar = Object.assign(AvatarRoot, {
|
|
248
|
+
Badge: AvatarBadge,
|
|
249
|
+
Group: AvatarGroup,
|
|
250
|
+
});
|
|
251
|
+
export { AvatarBadge, AvatarGroup };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Button — something to press, in the weight its action deserves.
|
|
3
|
+
*
|
|
4
|
+
* ```tsx
|
|
5
|
+
* <Button>Save</Button>
|
|
6
|
+
* <Button variant="outline" size="sm">Cancel</Button>
|
|
7
|
+
* <Button variant="ghost" size="icon" aria-label="Close"><XIcon /></Button>
|
|
8
|
+
* <Button loading>Saving</Button>
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* ## Weight
|
|
12
|
+
*
|
|
13
|
+
* `default` is the one action a view is for, and there should be one of it.
|
|
14
|
+
* `secondary` and `outline` are the actions beside it, `ghost` the ones that
|
|
15
|
+
* should not draw the eye until they are looked for, and `destructive` the
|
|
16
|
+
* one that cannot be taken back.
|
|
17
|
+
*
|
|
18
|
+
* ## Loading
|
|
19
|
+
*
|
|
20
|
+
* `loading` sets a spinner over the label and stops presses. The label stays
|
|
21
|
+
* in place, hidden, so the width does not change: a button that resizes as it
|
|
22
|
+
* is pressed moves whatever sits beside it.
|
|
23
|
+
*/
|
|
24
|
+
import type { ComponentProps } from 'react';
|
|
25
|
+
import { type VariantProps } from 'tailwind-variants';
|
|
26
|
+
export declare const buttonVariants: import("tailwind-variants").TVReturnType<{
|
|
27
|
+
variant: {
|
|
28
|
+
default: string;
|
|
29
|
+
secondary: string;
|
|
30
|
+
outline: string;
|
|
31
|
+
ghost: string;
|
|
32
|
+
destructive: string;
|
|
33
|
+
link: string;
|
|
34
|
+
};
|
|
35
|
+
size: {
|
|
36
|
+
sm: string;
|
|
37
|
+
md: string;
|
|
38
|
+
lg: string;
|
|
39
|
+
'icon-sm': string;
|
|
40
|
+
icon: string;
|
|
41
|
+
'icon-lg': string;
|
|
42
|
+
};
|
|
43
|
+
/** `pill` rounds the ends fully, whatever the size. */
|
|
44
|
+
shape: {
|
|
45
|
+
default: string;
|
|
46
|
+
pill: string;
|
|
47
|
+
};
|
|
48
|
+
}, undefined, string[], {
|
|
49
|
+
variant: {
|
|
50
|
+
default: string;
|
|
51
|
+
secondary: string;
|
|
52
|
+
outline: string;
|
|
53
|
+
ghost: string;
|
|
54
|
+
destructive: string;
|
|
55
|
+
link: string;
|
|
56
|
+
};
|
|
57
|
+
size: {
|
|
58
|
+
sm: string;
|
|
59
|
+
md: string;
|
|
60
|
+
lg: string;
|
|
61
|
+
'icon-sm': string;
|
|
62
|
+
icon: string;
|
|
63
|
+
'icon-lg': string;
|
|
64
|
+
};
|
|
65
|
+
/** `pill` rounds the ends fully, whatever the size. */
|
|
66
|
+
shape: {
|
|
67
|
+
default: string;
|
|
68
|
+
pill: string;
|
|
69
|
+
};
|
|
70
|
+
}, undefined, import("tailwind-variants").TVReturnType<{
|
|
71
|
+
variant: {
|
|
72
|
+
default: string;
|
|
73
|
+
secondary: string;
|
|
74
|
+
outline: string;
|
|
75
|
+
ghost: string;
|
|
76
|
+
destructive: string;
|
|
77
|
+
link: string;
|
|
78
|
+
};
|
|
79
|
+
size: {
|
|
80
|
+
sm: string;
|
|
81
|
+
md: string;
|
|
82
|
+
lg: string;
|
|
83
|
+
'icon-sm': string;
|
|
84
|
+
icon: string;
|
|
85
|
+
'icon-lg': string;
|
|
86
|
+
};
|
|
87
|
+
/** `pill` rounds the ends fully, whatever the size. */
|
|
88
|
+
shape: {
|
|
89
|
+
default: string;
|
|
90
|
+
pill: string;
|
|
91
|
+
};
|
|
92
|
+
}, undefined, string[], unknown, unknown, undefined>>;
|
|
93
|
+
export type ButtonVariant = NonNullable<VariantProps<typeof buttonVariants>['variant']>;
|
|
94
|
+
export type ButtonSize = NonNullable<VariantProps<typeof buttonVariants>['size']>;
|
|
95
|
+
export interface ButtonProps extends ComponentProps<'button'> {
|
|
96
|
+
/** How much the button asks to be pressed. Default `default`. */
|
|
97
|
+
variant?: ButtonVariant;
|
|
98
|
+
/** Its height and padding. The `icon` sizes are square, for a button that holds only an icon. Default `md`. */
|
|
99
|
+
size?: ButtonSize;
|
|
100
|
+
/** `pill` rounds the ends fully. Default `default`. */
|
|
101
|
+
shape?: 'default' | 'pill';
|
|
102
|
+
/** Shows a spinner over the label, sets `aria-busy` and stops presses, keeping the width. */
|
|
103
|
+
loading?: boolean;
|
|
104
|
+
}
|
|
105
|
+
/** A button. Every attribute of `<button>` passes through. */
|
|
106
|
+
declare function ButtonRoot({ variant, size, shape, loading, disabled, type, className, children, ...props }: ButtonProps): import("react").JSX.Element;
|
|
107
|
+
declare namespace ButtonRoot {
|
|
108
|
+
var displayName: string;
|
|
109
|
+
}
|
|
110
|
+
export declare const Button: typeof ButtonRoot;
|
|
111
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { tv } from 'tailwind-variants';
|
|
3
|
+
export const buttonVariants = tv({
|
|
4
|
+
base: [
|
|
5
|
+
'relative inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-2 whitespace-nowrap font-medium outline-none',
|
|
6
|
+
'transition-[background-color,color,border-color,box-shadow,scale] duration-150',
|
|
7
|
+
'focus-visible:ring-[3px] focus-visible:ring-ring/40',
|
|
8
|
+
'active:not-disabled:scale-[0.97] motion-reduce:active:scale-100',
|
|
9
|
+
'disabled:cursor-default disabled:opacity-50 aria-busy:cursor-progress',
|
|
10
|
+
'[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',
|
|
11
|
+
],
|
|
12
|
+
variants: {
|
|
13
|
+
variant: {
|
|
14
|
+
default: 'bg-primary text-primary-foreground shadow-xs inset-shadow-[0_1px_0_rgb(255_255_255/0.12)] hover:not-disabled:bg-primary/90',
|
|
15
|
+
secondary: 'bg-secondary text-secondary-foreground hover:not-disabled:bg-secondary/80',
|
|
16
|
+
outline: 'border bg-background text-foreground shadow-xs hover:not-disabled:bg-accent hover:not-disabled:text-accent-foreground dark:bg-input/30',
|
|
17
|
+
ghost: 'text-foreground hover:not-disabled:bg-accent hover:not-disabled:text-accent-foreground',
|
|
18
|
+
destructive: 'bg-destructive text-white shadow-xs hover:not-disabled:bg-destructive/90 focus-visible:ring-destructive/40',
|
|
19
|
+
link: 'text-foreground underline-offset-4 hover:underline',
|
|
20
|
+
},
|
|
21
|
+
size: {
|
|
22
|
+
sm: 'h-8 rounded-md px-3 text-[0.8125rem]',
|
|
23
|
+
md: 'h-9 rounded-lg px-4 text-sm',
|
|
24
|
+
lg: 'h-11 rounded-xl px-5 text-[0.9375rem]',
|
|
25
|
+
'icon-sm': 'size-8 rounded-md',
|
|
26
|
+
icon: 'size-9 rounded-lg',
|
|
27
|
+
'icon-lg': 'size-11 rounded-xl [&_svg:not([class*=size-])]:size-5',
|
|
28
|
+
},
|
|
29
|
+
/** `pill` rounds the ends fully, whatever the size. */
|
|
30
|
+
shape: {
|
|
31
|
+
default: '',
|
|
32
|
+
pill: 'rounded-full',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
defaultVariants: { variant: 'default', size: 'md', shape: 'default' },
|
|
36
|
+
});
|
|
37
|
+
function Spinner() {
|
|
38
|
+
return (_jsx("svg", { "aria-hidden": "true", "data-slot": "button-spinner", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", className: "absolute inset-0 m-auto animate-spin motion-reduce:animate-none", children: _jsx("path", { d: "M21 12a9 9 0 1 1-6.22-8.56" }) }));
|
|
39
|
+
}
|
|
40
|
+
/** A button. Every attribute of `<button>` passes through. */
|
|
41
|
+
function ButtonRoot({ variant, size, shape, loading = false, disabled, type = 'button', className, children, ...props }) {
|
|
42
|
+
return (_jsxs("button", { type: type, "data-slot": "button", "data-variant": variant ?? 'default', "aria-busy": loading || undefined, disabled: disabled || loading, className: buttonVariants({ variant, size, shape, className }), ...props, children: [loading ? _jsx(Spinner, {}) : null, _jsx("span", { className: loading ? 'invisible contents' : 'contents', children: children })] }));
|
|
43
|
+
}
|
|
44
|
+
ButtonRoot.displayName = 'Button';
|
|
45
|
+
export const Button = ButtonRoot;
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
* a new `position` and the node goes to it, which is how nodes are driven
|
|
31
31
|
* from outside.
|
|
32
32
|
*
|
|
33
|
+
* A node drawn inside a `Flow.Group` is positioned within it, and travels
|
|
34
|
+
* with it. `confine` keeps its drag inside the group's box; `pinned` gives the
|
|
35
|
+
* drag to the group instead, so the node moves only when the group does.
|
|
36
|
+
*
|
|
33
37
|
* Edges are drawn from the boxes of the nodes they join, measured off the
|
|
34
38
|
* page. An edge therefore follows a node whatever is inside it, and redraws
|
|
35
39
|
* as either end is dragged.
|
|
@@ -124,6 +128,16 @@ export interface FlowNodeProps extends Omit<ComponentProps<'div'>, 'id'> {
|
|
|
124
128
|
position: FlowNodePosition;
|
|
125
129
|
/** Whether it can be dragged and moved with the arrow keys. */
|
|
126
130
|
draggable?: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Inside a `Flow.Group`, stop a drag at the group's edge instead of letting
|
|
133
|
+
* the node leave it. Does nothing outside a group.
|
|
134
|
+
*/
|
|
135
|
+
confine?: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Inside a `Flow.Group`, take no drag of its own: a drag that starts on it
|
|
138
|
+
* moves the group, and the node goes with it.
|
|
139
|
+
*/
|
|
140
|
+
pinned?: boolean;
|
|
127
141
|
/** Draws it as the one that has been picked. */
|
|
128
142
|
selected?: boolean;
|
|
129
143
|
/** Called when it is pressed without being dragged, or with Enter. */
|
|
@@ -134,19 +148,27 @@ export interface FlowNodeProps extends Omit<ComponentProps<'div'>, 'id'> {
|
|
|
134
148
|
moveStep?: number;
|
|
135
149
|
}
|
|
136
150
|
/** A box on the graph. Fill it with the node parts below, or with anything of your own. */
|
|
137
|
-
declare function FlowNode({ id, position, draggable, selected, onPress, onDelete, moveStep, className, style, children, ...props }: FlowNodeProps): import("react").JSX.Element;
|
|
151
|
+
declare function FlowNode({ id, position, draggable, confine, pinned, selected, onPress, onDelete, moveStep, className, style, children, ...props }: FlowNodeProps): import("react").JSX.Element;
|
|
138
152
|
declare namespace FlowNode {
|
|
139
153
|
var displayName: string;
|
|
140
154
|
}
|
|
141
|
-
export interface FlowGroupProps extends FlowNodeProps {
|
|
155
|
+
export interface FlowGroupProps extends Omit<FlowNodeProps, 'confine' | 'pinned'> {
|
|
142
156
|
/** Written in the corner of the group. */
|
|
143
157
|
label?: ReactNode;
|
|
158
|
+
/**
|
|
159
|
+
* A fixed size for the group. Give one when it holds `Flow.Node`s, which are
|
|
160
|
+
* placed inside it rather than laid out by it, so they do not size it.
|
|
161
|
+
*/
|
|
162
|
+
size?: {
|
|
163
|
+
width: number;
|
|
164
|
+
height: number;
|
|
165
|
+
};
|
|
144
166
|
}
|
|
145
167
|
/**
|
|
146
168
|
* A dashed box round a few things that belong together. It is a node like any
|
|
147
169
|
* other: it is dragged as one, and an edge can be joined to it.
|
|
148
170
|
*/
|
|
149
|
-
declare function FlowGroup({ id, position, draggable, selected: _selected, onPress, onDelete, moveStep, label, className, style, children, ...props }: FlowGroupProps): import("react").JSX.Element;
|
|
171
|
+
declare function FlowGroup({ id, position, draggable, selected: _selected, onPress, onDelete, moveStep, label, size, className, style, children, ...props }: FlowGroupProps): import("react").JSX.Element;
|
|
150
172
|
declare namespace FlowGroup {
|
|
151
173
|
var displayName: string;
|
|
152
174
|
}
|
|
@@ -211,7 +233,7 @@ declare namespace FlowHandle {
|
|
|
211
233
|
var displayName: string;
|
|
212
234
|
}
|
|
213
235
|
export interface FlowEdgeProps {
|
|
214
|
-
/** Where it leaves from: a node's `id`, or `{ node, handle }`. */
|
|
236
|
+
/** Where it leaves from: a node's `id`, `"node.handle"` for one of its handles, or `{ node, handle }`. */
|
|
215
237
|
from: FlowEndpointReference;
|
|
216
238
|
/** Where it arrives. */
|
|
217
239
|
to: FlowEndpointReference;
|