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
|
@@ -32,6 +32,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
32
32
|
* a new `position` and the node goes to it, which is how nodes are driven
|
|
33
33
|
* from outside.
|
|
34
34
|
*
|
|
35
|
+
* A node drawn inside a `Flow.Group` is positioned within it, and travels
|
|
36
|
+
* with it. `confine` keeps its drag inside the group's box; `pinned` gives the
|
|
37
|
+
* drag to the group instead, so the node moves only when the group does.
|
|
38
|
+
*
|
|
35
39
|
* Edges are drawn from the boxes of the nodes they join, measured off the
|
|
36
40
|
* page. An edge therefore follows a node whatever is inside it, and redraws
|
|
37
41
|
* as either end is dragged.
|
|
@@ -94,6 +98,8 @@ const flowVariants = tv({
|
|
|
94
98
|
});
|
|
95
99
|
const FlowContext = createContext(null);
|
|
96
100
|
const NodeContext = createContext(null);
|
|
101
|
+
/** The group a node is drawn inside, if any. */
|
|
102
|
+
const GroupContext = createContext(null);
|
|
97
103
|
function useFlow(part) {
|
|
98
104
|
const flow = useContext(FlowContext);
|
|
99
105
|
if (!flow)
|
|
@@ -111,7 +117,18 @@ function FlowRoot({ defaultViewport = { x: 0, y: 0, zoom: 1 }, minZoom = 0.3, ma
|
|
|
111
117
|
const slots = flowVariants();
|
|
112
118
|
const root = useRef(null);
|
|
113
119
|
const [viewport, setViewportState] = useState(defaultViewport);
|
|
114
|
-
const [
|
|
120
|
+
const [local, setRects] = useState({});
|
|
121
|
+
const [parents, setParents] = useState({});
|
|
122
|
+
// A node in a group is placed relative to it; everything that draws or
|
|
123
|
+
// fits reads the box where it actually is on the graph.
|
|
124
|
+
const rects = useMemo(() => {
|
|
125
|
+
const out = {};
|
|
126
|
+
for (const [id, box] of Object.entries(local)) {
|
|
127
|
+
const parent = parents[id] ? local[parents[id]] : undefined;
|
|
128
|
+
out[id] = parent ? { ...box, x: parent.x + box.x, y: parent.y + box.y } : box;
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}, [local, parents]);
|
|
115
132
|
const [handles, setHandles] = useState({});
|
|
116
133
|
const [locked, setLocked] = useState(false);
|
|
117
134
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
@@ -275,7 +292,15 @@ function FlowRoot({ defaultViewport = { x: 0, y: 0, zoom: 1 }, minZoom = 0.3, ma
|
|
|
275
292
|
window.addEventListener('pointerup', up);
|
|
276
293
|
window.addEventListener('pointercancel', up);
|
|
277
294
|
}, [toGraph]);
|
|
278
|
-
const place = useCallback((id, position) => {
|
|
295
|
+
const place = useCallback((id, position, group) => {
|
|
296
|
+
if (group !== undefined) {
|
|
297
|
+
setParents((current) => {
|
|
298
|
+
if ((current[id] ?? null) === group)
|
|
299
|
+
return current;
|
|
300
|
+
const { [id]: _was, ...rest } = current;
|
|
301
|
+
return group ? { ...rest, [id]: group } : rest;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
279
304
|
setRects((current) => {
|
|
280
305
|
const before = current[id];
|
|
281
306
|
if (before && before.x === position.x && before.y === position.y)
|
|
@@ -295,6 +320,12 @@ function FlowRoot({ defaultViewport = { x: 0, y: 0, zoom: 1 }, minZoom = 0.3, ma
|
|
|
295
320
|
});
|
|
296
321
|
}, []);
|
|
297
322
|
const forget = useCallback((id) => {
|
|
323
|
+
setParents((current) => {
|
|
324
|
+
if (!(id in current))
|
|
325
|
+
return current;
|
|
326
|
+
const { [id]: _gone, ...rest } = current;
|
|
327
|
+
return rest;
|
|
328
|
+
});
|
|
298
329
|
setRects((current) => {
|
|
299
330
|
if (!(id in current))
|
|
300
331
|
return current;
|
|
@@ -311,6 +342,7 @@ function FlowRoot({ defaultViewport = { x: 0, y: 0, zoom: 1 }, minZoom = 0.3, ma
|
|
|
311
342
|
const context = useMemo(() => ({
|
|
312
343
|
viewport,
|
|
313
344
|
rects,
|
|
345
|
+
local,
|
|
314
346
|
handles,
|
|
315
347
|
locked,
|
|
316
348
|
size,
|
|
@@ -328,6 +360,7 @@ function FlowRoot({ defaultViewport = { x: 0, y: 0, zoom: 1 }, minZoom = 0.3, ma
|
|
|
328
360
|
}), [
|
|
329
361
|
viewport,
|
|
330
362
|
rects,
|
|
363
|
+
local,
|
|
331
364
|
handles,
|
|
332
365
|
locked,
|
|
333
366
|
size,
|
|
@@ -384,16 +417,28 @@ function FlowBackground({ variant = 'dots', gap = 24, size, className }) {
|
|
|
384
417
|
}
|
|
385
418
|
FlowBackground.displayName = 'Flow.Background';
|
|
386
419
|
/** What a node and a group share: a box on the graph that can be moved. */
|
|
387
|
-
function useMovable({ id, position, draggable, onPress, onDelete, moveStep, }) {
|
|
420
|
+
function useMovable({ id, position, draggable, confine, pinned, group, onPress, onDelete, moveStep, }) {
|
|
388
421
|
const flow = useFlow('Flow.Node');
|
|
389
422
|
const element = useRef(null);
|
|
390
423
|
const { place, measure, forget } = flow;
|
|
391
424
|
const step = moveStep ?? 24;
|
|
425
|
+
const movable = draggable !== false && !pinned && !flow.locked;
|
|
392
426
|
useLayoutEffect(() => {
|
|
393
|
-
place(id, position);
|
|
427
|
+
place(id, position, group);
|
|
394
428
|
// Only when the caller's position changes, not on every drawing.
|
|
395
429
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
396
|
-
}, [id, position.x, position.y, place]);
|
|
430
|
+
}, [id, position.x, position.y, group, place]);
|
|
431
|
+
/** Where a move would put it, held inside its group when it is confined. */
|
|
432
|
+
const within = (next) => {
|
|
433
|
+
const box = group && confine ? flow.local[group] : undefined;
|
|
434
|
+
if (!box || box.width === 0)
|
|
435
|
+
return next;
|
|
436
|
+
const self = flow.local[id];
|
|
437
|
+
return {
|
|
438
|
+
x: clamp(next.x, 0, Math.max(0, box.width - (self?.width ?? 0))),
|
|
439
|
+
y: clamp(next.y, 0, Math.max(0, box.height - (self?.height ?? 0))),
|
|
440
|
+
};
|
|
441
|
+
};
|
|
397
442
|
useLayoutEffect(() => {
|
|
398
443
|
const node = element.current;
|
|
399
444
|
if (!node)
|
|
@@ -405,7 +450,7 @@ function useMovable({ id, position, draggable, onPress, onDelete, moveStep, }) {
|
|
|
405
450
|
return () => observer.disconnect();
|
|
406
451
|
}, [id, measure]);
|
|
407
452
|
useEffect(() => () => forget(id), [id, forget]);
|
|
408
|
-
const at = flow.
|
|
453
|
+
const at = flow.local[id] ?? { ...position, width: 0, height: 0 };
|
|
409
454
|
const drag = useRef(null);
|
|
410
455
|
const latest = useRef(at);
|
|
411
456
|
latest.current = at;
|
|
@@ -416,6 +461,10 @@ function useMovable({ id, position, draggable, onPress, onDelete, moveStep, }) {
|
|
|
416
461
|
if (event.target.closest('[data-flow-handle], button, a, input, textarea, select, [data-no-drag]')) {
|
|
417
462
|
return;
|
|
418
463
|
}
|
|
464
|
+
// A pinned node lets the press through to the group it is in, which
|
|
465
|
+
// takes the drag.
|
|
466
|
+
if (pinned && group)
|
|
467
|
+
return;
|
|
419
468
|
// Kept from the pane, and from a group this node may be sitting in.
|
|
420
469
|
event.stopPropagation();
|
|
421
470
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
@@ -430,9 +479,9 @@ function useMovable({ id, position, draggable, onPress, onDelete, moveStep, }) {
|
|
|
430
479
|
if (!state.moved && Math.hypot(dx, dy) < 3)
|
|
431
480
|
return;
|
|
432
481
|
state.moved = true;
|
|
433
|
-
if (
|
|
482
|
+
if (!movable)
|
|
434
483
|
return;
|
|
435
|
-
place(id, { x: state.from.x + dx / flow.viewport.zoom, y: state.from.y + dy / flow.viewport.zoom });
|
|
484
|
+
place(id, within({ x: state.from.x + dx / flow.viewport.zoom, y: state.from.y + dy / flow.viewport.zoom }));
|
|
436
485
|
};
|
|
437
486
|
const onPointerUp = () => {
|
|
438
487
|
const state = drag.current;
|
|
@@ -441,7 +490,7 @@ function useMovable({ id, position, draggable, onPress, onDelete, moveStep, }) {
|
|
|
441
490
|
return;
|
|
442
491
|
if (!state.moved)
|
|
443
492
|
onPress?.();
|
|
444
|
-
else if (
|
|
493
|
+
else if (movable) {
|
|
445
494
|
flow.onNodeDragEnd?.(id, { x: latest.current.x, y: latest.current.y });
|
|
446
495
|
}
|
|
447
496
|
};
|
|
@@ -458,11 +507,11 @@ function useMovable({ id, position, draggable, onPress, onDelete, moveStep, }) {
|
|
|
458
507
|
if (by) {
|
|
459
508
|
// Kept from the canvas, which would pan on the same key.
|
|
460
509
|
event.stopPropagation();
|
|
461
|
-
if (
|
|
510
|
+
if (!movable)
|
|
462
511
|
return;
|
|
463
512
|
event.preventDefault();
|
|
464
513
|
const far = step * (event.shiftKey ? 4 : 1);
|
|
465
|
-
const next = { x: at.x + by[0] * far, y: at.y + by[1] * far };
|
|
514
|
+
const next = within({ x: at.x + by[0] * far, y: at.y + by[1] * far });
|
|
466
515
|
place(id, next);
|
|
467
516
|
flow.onNodeDragEnd?.(id, next);
|
|
468
517
|
}
|
|
@@ -492,18 +541,19 @@ function useMovable({ id, position, draggable, onPress, onDelete, moveStep, }) {
|
|
|
492
541
|
};
|
|
493
542
|
}
|
|
494
543
|
/** A box on the graph. Fill it with the node parts below, or with anything of your own. */
|
|
495
|
-
function FlowNode({ id, position, draggable = true, selected = false, onPress, onDelete, moveStep, className, style, children, ...props }) {
|
|
496
|
-
const
|
|
497
|
-
|
|
544
|
+
function FlowNode({ id, position, draggable = true, confine = false, pinned = false, selected = false, onPress, onDelete, moveStep, className, style, children, ...props }) {
|
|
545
|
+
const group = useContext(GroupContext);
|
|
546
|
+
const movable = useMovable({ id, position, draggable, confine, pinned, group, onPress, onDelete, moveStep });
|
|
547
|
+
return (_jsx(NodeContext.Provider, { value: id, children: _jsx("div", { ref: movable.element, "data-flow-node": id, "data-pinned": pinned && group ? '' : undefined, tabIndex: 0, "aria-roledescription": "Node", "aria-selected": selected || undefined, className: flowVariants({ selected }).node({ className }), style: { ...movable.style, ...style }, ...movable.handlers, ...props, children: children }) }));
|
|
498
548
|
}
|
|
499
549
|
FlowNode.displayName = 'Flow.Node';
|
|
500
550
|
/**
|
|
501
551
|
* A dashed box round a few things that belong together. It is a node like any
|
|
502
552
|
* other: it is dragged as one, and an edge can be joined to it.
|
|
503
553
|
*/
|
|
504
|
-
function FlowGroup({ id, position, draggable = true, selected: _selected, onPress, onDelete, moveStep, label, className, style, children, ...props }) {
|
|
505
|
-
const movable = useMovable({ id, position, draggable, onPress, onDelete, moveStep });
|
|
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] }) }));
|
|
554
|
+
function FlowGroup({ id, position, draggable = true, selected: _selected, onPress, onDelete, moveStep, label, size, className, style, children, ...props }) {
|
|
555
|
+
const movable = useMovable({ id, position, draggable, group: null, onPress, onDelete, moveStep });
|
|
556
|
+
return (_jsx(GroupContext.Provider, { value: id, children: _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, ...(size ? { width: size.width, height: size.height } : null), ...style }, ...movable.handlers, ...props, children: [label !== undefined ? _jsx("span", { className: flowVariants().groupLabel(), children: label }) : null, children] }) }) }));
|
|
507
557
|
}
|
|
508
558
|
FlowGroup.displayName = 'Flow.Group';
|
|
509
559
|
/** A strip across the top of a node, ruled off from the rest: a step number and a state. */
|
|
@@ -566,7 +616,18 @@ function FlowHandle({ id, position = 'right', offset = 0.5, className, style, ..
|
|
|
566
616
|
}, ...props }));
|
|
567
617
|
}
|
|
568
618
|
FlowHandle.displayName = 'Flow.Handle';
|
|
569
|
-
|
|
619
|
+
/**
|
|
620
|
+
* `"node"`, `"node.handle"` or `{ node, handle }`. A string that is a node's
|
|
621
|
+
* whole id names that node, so an id with a dot in it still works.
|
|
622
|
+
*/
|
|
623
|
+
const endpoint = (reference, rects) => {
|
|
624
|
+
if (typeof reference !== 'string')
|
|
625
|
+
return reference;
|
|
626
|
+
const dot = reference.lastIndexOf('.');
|
|
627
|
+
if (dot <= 0 || reference in rects)
|
|
628
|
+
return { node: reference };
|
|
629
|
+
return { node: reference.slice(0, dot), handle: reference.slice(dot + 1) };
|
|
630
|
+
};
|
|
570
631
|
/** A line from one node to another, which follows them as they move. */
|
|
571
632
|
function FlowEdge({ from, to, variant = 'bezier', animated = false, dashed = false, color, width = 2, arrow = false, fromSide, toSide, radius = 8, gap = 20, curvature = 0.25, }) {
|
|
572
633
|
const flow = useFlow('Flow.Edge');
|
|
@@ -579,8 +640,8 @@ function FlowEdge({ from, to, variant = 'bezier', animated = false, dashed = fal
|
|
|
579
640
|
query.addEventListener('change', read);
|
|
580
641
|
return () => query.removeEventListener('change', read);
|
|
581
642
|
}, []);
|
|
582
|
-
const source = endpoint(from);
|
|
583
|
-
const target = endpoint(to);
|
|
643
|
+
const source = endpoint(from, flow.rects);
|
|
644
|
+
const target = endpoint(to, flow.rects);
|
|
584
645
|
const a = flow.rects[source.node];
|
|
585
646
|
const b = flow.rects[target.node];
|
|
586
647
|
// Nothing to draw until both ends have been measured.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input — a single line to type into.
|
|
3
|
+
*
|
|
4
|
+
* ```tsx
|
|
5
|
+
* <Input placeholder="you@example.com" type="email" />
|
|
6
|
+
*
|
|
7
|
+
* <Input.Group>
|
|
8
|
+
* <Input.Addon><SearchIcon /></Input.Addon>
|
|
9
|
+
* <Input placeholder="Search" />
|
|
10
|
+
* <Input.Addon align="end"><kbd>⌘K</kbd></Input.Addon>
|
|
11
|
+
* </Input.Group>
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* ## The group owns the box
|
|
15
|
+
*
|
|
16
|
+
* On its own, the input draws its border, background and focus ring. Inside
|
|
17
|
+
* `Input.Group`, it drops all three and the group draws them around the input
|
|
18
|
+
* and its addons together, so an icon or a unit reads as part of the field and
|
|
19
|
+
* the ring goes round the whole of it. Pressing an addon that is not a button
|
|
20
|
+
* moves focus into the input.
|
|
21
|
+
*/
|
|
22
|
+
import type { ComponentProps } from 'react';
|
|
23
|
+
export type InputSize = 'sm' | 'md' | 'lg';
|
|
24
|
+
export interface InputProps extends Omit<ComponentProps<'input'>, 'size'> {
|
|
25
|
+
/** Its height and text size. Inside a group, set it on the group instead. Default `md`. */
|
|
26
|
+
size?: InputSize;
|
|
27
|
+
}
|
|
28
|
+
/** A single-line field. Every attribute of `<input>` passes through. */
|
|
29
|
+
declare function InputRoot({ size, className, type, ...props }: InputProps): import("react").JSX.Element;
|
|
30
|
+
declare namespace InputRoot {
|
|
31
|
+
var displayName: string;
|
|
32
|
+
}
|
|
33
|
+
export interface InputGroupProps extends ComponentProps<'div'> {
|
|
34
|
+
/** The height of the input and its addons together. Default `md`. */
|
|
35
|
+
size?: InputSize;
|
|
36
|
+
}
|
|
37
|
+
/** Draws one box round an input and its addons. */
|
|
38
|
+
declare function InputGroup({ size, className, ...props }: InputGroupProps): import("react").JSX.Element;
|
|
39
|
+
declare namespace InputGroup {
|
|
40
|
+
var displayName: string;
|
|
41
|
+
}
|
|
42
|
+
export interface InputAddonProps extends ComponentProps<'div'> {
|
|
43
|
+
/** Which side of the input it sits on, whatever its place in the markup. Default `start`. */
|
|
44
|
+
align?: 'start' | 'end';
|
|
45
|
+
}
|
|
46
|
+
/** An icon, unit, shortcut or button beside the input, inside its box. */
|
|
47
|
+
declare function InputAddon({ align, className, onMouseDown, ...props }: InputAddonProps): import("react").JSX.Element;
|
|
48
|
+
declare namespace InputAddon {
|
|
49
|
+
var displayName: string;
|
|
50
|
+
}
|
|
51
|
+
export declare const Input: typeof InputRoot & {
|
|
52
|
+
Group: typeof InputGroup;
|
|
53
|
+
Addon: typeof InputAddon;
|
|
54
|
+
};
|
|
55
|
+
export { InputGroup, InputAddon };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { tv } from 'tailwind-variants';
|
|
4
|
+
const inputVariants = tv({
|
|
5
|
+
slots: {
|
|
6
|
+
input: [
|
|
7
|
+
'w-full min-w-0 bg-transparent text-foreground outline-none',
|
|
8
|
+
'placeholder:text-muted-foreground/80 selection:bg-primary selection:text-primary-foreground',
|
|
9
|
+
'file:me-3 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground',
|
|
10
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
11
|
+
// Alone, it is the box. In a group, the group is.
|
|
12
|
+
'not-in-data-[slot=input-group]:rounded-lg not-in-data-[slot=input-group]:border not-in-data-[slot=input-group]:border-input',
|
|
13
|
+
'not-in-data-[slot=input-group]:bg-background not-in-data-[slot=input-group]:shadow-xs dark:not-in-data-[slot=input-group]:bg-input/30',
|
|
14
|
+
'not-in-data-[slot=input-group]:transition-[border-color,box-shadow]',
|
|
15
|
+
'not-in-data-[slot=input-group]:focus-visible:border-ring not-in-data-[slot=input-group]:focus-visible:ring-[3px] not-in-data-[slot=input-group]:focus-visible:ring-ring/30',
|
|
16
|
+
'aria-invalid:not-in-data-[slot=input-group]:border-destructive aria-invalid:not-in-data-[slot=input-group]:ring-destructive/20',
|
|
17
|
+
],
|
|
18
|
+
group: [
|
|
19
|
+
'group/input flex w-full min-w-0 items-center rounded-lg border border-input bg-background shadow-xs transition-[border-color,box-shadow] dark:bg-input/30',
|
|
20
|
+
'has-[input:focus-visible]:border-ring has-[input:focus-visible]:ring-[3px] has-[input:focus-visible]:ring-ring/30',
|
|
21
|
+
'has-[input[aria-invalid=true]]:border-destructive has-[input[aria-invalid=true]]:ring-destructive/20',
|
|
22
|
+
'has-[input:disabled]:opacity-50',
|
|
23
|
+
],
|
|
24
|
+
addon: [
|
|
25
|
+
'flex shrink-0 cursor-text items-center gap-1.5 text-sm text-muted-foreground select-none',
|
|
26
|
+
'[&_svg]:pointer-events-none [&_svg:not([class*=size-])]:size-4',
|
|
27
|
+
'[&_kbd]:rounded [&_kbd]:border [&_kbd]:bg-muted [&_kbd]:px-1.5 [&_kbd]:font-sans [&_kbd]:text-xs',
|
|
28
|
+
// Padded to the group's size, read from the group so it is set once.
|
|
29
|
+
'px-3 group-data-[size=sm]/input:px-2.5 group-data-[size=lg]/input:px-3.5',
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
variants: {
|
|
33
|
+
size: {
|
|
34
|
+
sm: { input: 'h-8 px-2.5 text-[0.8125rem]', group: 'h-8' },
|
|
35
|
+
md: { input: 'h-9 px-3 text-sm', group: 'h-9' },
|
|
36
|
+
lg: { input: 'h-11 px-3.5 text-[0.9375rem]', group: 'h-11 rounded-xl' },
|
|
37
|
+
},
|
|
38
|
+
align: {
|
|
39
|
+
// An addon sits closer to the text it belongs to than to the edge.
|
|
40
|
+
start: { addon: 'order-first -me-1.5' },
|
|
41
|
+
end: { addon: 'order-last -ms-1.5' },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
compoundVariants: [
|
|
45
|
+
// In a group the input's own height is the group's, minus its border.
|
|
46
|
+
// Its text and padding follow the group's size too, so the size is set once.
|
|
47
|
+
{
|
|
48
|
+
size: ['sm', 'md', 'lg'],
|
|
49
|
+
className: {
|
|
50
|
+
input: 'in-data-[slot=input-group]:h-full group-data-[size=sm]/input:px-2.5 group-data-[size=sm]/input:text-[0.8125rem] group-data-[size=lg]/input:px-3.5 group-data-[size=lg]/input:text-[0.9375rem]',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
defaultVariants: { size: 'md', align: 'start' },
|
|
55
|
+
});
|
|
56
|
+
/** A single-line field. Every attribute of `<input>` passes through. */
|
|
57
|
+
function InputRoot({ size, className, type = 'text', ...props }) {
|
|
58
|
+
return (_jsx("input", { type: type, "data-slot": "input", className: inputVariants({ size }).input({ className }), ...props }));
|
|
59
|
+
}
|
|
60
|
+
InputRoot.displayName = 'Input';
|
|
61
|
+
/** Draws one box round an input and its addons. */
|
|
62
|
+
function InputGroup({ size, className, ...props }) {
|
|
63
|
+
return (_jsx("div", { role: "group", "data-slot": "input-group", "data-size": size ?? 'md', className: inputVariants({ size }).group({ className }), ...props }));
|
|
64
|
+
}
|
|
65
|
+
InputGroup.displayName = 'Input.Group';
|
|
66
|
+
/** An icon, unit, shortcut or button beside the input, inside its box. */
|
|
67
|
+
function InputAddon({ align, className, onMouseDown, ...props }) {
|
|
68
|
+
// A press on the addon's own surface is a press on the field. A button or
|
|
69
|
+
// link inside it is left to do what it does.
|
|
70
|
+
const focusField = (event) => {
|
|
71
|
+
onMouseDown?.(event);
|
|
72
|
+
if (event.defaultPrevented)
|
|
73
|
+
return;
|
|
74
|
+
if (event.target.closest('button, a, input, select, textarea'))
|
|
75
|
+
return;
|
|
76
|
+
const input = event.currentTarget.parentElement?.querySelector('input');
|
|
77
|
+
if (!input)
|
|
78
|
+
return;
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
input.focus();
|
|
81
|
+
};
|
|
82
|
+
return (_jsx("div", { "data-slot": "input-addon", "data-align": align ?? 'start', className: inputVariants({ align }).addon({ className }), onMouseDown: focusField, ...props }));
|
|
83
|
+
}
|
|
84
|
+
InputAddon.displayName = 'Input.Addon';
|
|
85
|
+
export const Input = Object.assign(InputRoot, {
|
|
86
|
+
Group: InputGroup,
|
|
87
|
+
Addon: InputAddon,
|
|
88
|
+
});
|
|
89
|
+
export { InputGroup, InputAddon };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Label — the name of a field, and the line it sits on.
|
|
3
|
+
*
|
|
4
|
+
* ```tsx
|
|
5
|
+
* <Label htmlFor="email" required>Email</Label>
|
|
6
|
+
* <Input id="email" type="email" required />
|
|
7
|
+
*
|
|
8
|
+
* <Label htmlFor="password">
|
|
9
|
+
* <Label.Text>Password</Label.Text>
|
|
10
|
+
* <Label.Action><a href="/reset">Forgot it?</a></Label.Action>
|
|
11
|
+
* </Label>
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* ## One line, three things on it
|
|
15
|
+
*
|
|
16
|
+
* A field's label is rarely only its name. It says whether the field must be
|
|
17
|
+
* filled, it carries a hint (a limit, a format, "Optional"), and it often has
|
|
18
|
+
* something at the far end: a link to reset a password, a count of what is
|
|
19
|
+
* left. Putting those on the label's own line, rather than in lines of their
|
|
20
|
+
* own round the field, keeps a form to one row per field, and keeps the hint
|
|
21
|
+
* next to the name it qualifies.
|
|
22
|
+
*
|
|
23
|
+
* ## The control decides, where it can
|
|
24
|
+
*
|
|
25
|
+
* A label wrapped round its control, such as a checkbox, reads that control's
|
|
26
|
+
* state: it fades when the control is disabled and turns to the destructive
|
|
27
|
+
* colour when the control is invalid, with nothing passed. A label tied to its
|
|
28
|
+
* control by `htmlFor` cannot see it, so it takes `invalid` and `disabled`.
|
|
29
|
+
*
|
|
30
|
+
* ## It renders on the server
|
|
31
|
+
*
|
|
32
|
+
* The state reaches the parts through data attributes and CSS rather than
|
|
33
|
+
* through context, so a label has no client JavaScript of its own.
|
|
34
|
+
*/
|
|
35
|
+
import type { ComponentProps } from 'react';
|
|
36
|
+
export interface LabelProps extends ComponentProps<'label'> {
|
|
37
|
+
/**
|
|
38
|
+
* Marks the field as one that must be filled. The mark is for the eye; set
|
|
39
|
+
* `required` on the control as well, which is what a screen reader announces.
|
|
40
|
+
*/
|
|
41
|
+
required?: boolean;
|
|
42
|
+
/** Shows "Optional" after the text, for a form where most fields are required. */
|
|
43
|
+
optional?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Turns the label to the destructive colour. Needed when the label is tied
|
|
46
|
+
* to its control by `htmlFor`; a label wrapped round its control reads it.
|
|
47
|
+
*/
|
|
48
|
+
invalid?: boolean;
|
|
49
|
+
/** Fades the label. As with `invalid`, a wrapped control is read on its own. */
|
|
50
|
+
disabled?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The label element. A string child is the label's text; for more, compose
|
|
54
|
+
* `Label.Text`, `Label.Hint` and `Label.Action`.
|
|
55
|
+
*/
|
|
56
|
+
declare function LabelRoot({ required, optional, invalid, disabled, className, children, ...props }: LabelProps): import("react").JSX.Element;
|
|
57
|
+
declare namespace LabelRoot {
|
|
58
|
+
var displayName: string;
|
|
59
|
+
}
|
|
60
|
+
export interface LabelTextProps extends ComponentProps<'span'> {
|
|
61
|
+
}
|
|
62
|
+
/** The field's name. It truncates rather than pushing the action off the line. */
|
|
63
|
+
declare function LabelText({ className, children, ...props }: LabelTextProps): import("react").JSX.Element;
|
|
64
|
+
declare namespace LabelText {
|
|
65
|
+
var displayName: string;
|
|
66
|
+
}
|
|
67
|
+
export interface LabelHintProps extends ComponentProps<'span'> {
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A quieter word or two after the name: a limit, a format, a unit. On an
|
|
71
|
+
* invalid field it takes the destructive colour, so it can say what is wrong.
|
|
72
|
+
*/
|
|
73
|
+
declare function LabelHint({ className, ...props }: LabelHintProps): import("react").JSX.Element;
|
|
74
|
+
declare namespace LabelHint {
|
|
75
|
+
var displayName: string;
|
|
76
|
+
}
|
|
77
|
+
export interface LabelActionProps extends ComponentProps<'span'> {
|
|
78
|
+
}
|
|
79
|
+
/** Pushed to the far end of the line: a link, a count, a small button. */
|
|
80
|
+
declare function LabelAction({ className, ...props }: LabelActionProps): import("react").JSX.Element;
|
|
81
|
+
declare namespace LabelAction {
|
|
82
|
+
var displayName: string;
|
|
83
|
+
}
|
|
84
|
+
export declare const Label: typeof LabelRoot & {
|
|
85
|
+
Text: typeof LabelText;
|
|
86
|
+
Hint: typeof LabelHint;
|
|
87
|
+
Action: typeof LabelAction;
|
|
88
|
+
};
|
|
89
|
+
export { LabelText, LabelHint, LabelAction };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { tv } from 'tailwind-variants';
|
|
3
|
+
const labelVariants = tv({
|
|
4
|
+
slots: {
|
|
5
|
+
root: [
|
|
6
|
+
'group/label flex w-full min-w-0 items-center gap-2 text-sm font-medium leading-5 text-foreground select-none',
|
|
7
|
+
// A control wrapped in the label tells it how it is.
|
|
8
|
+
'has-disabled:cursor-not-allowed has-disabled:opacity-50 has-[[aria-invalid=true]]:text-destructive',
|
|
9
|
+
'data-disabled:cursor-not-allowed data-disabled:opacity-50 data-invalid:text-destructive',
|
|
10
|
+
],
|
|
11
|
+
// The required mark is drawn after the text by CSS, so it follows the name
|
|
12
|
+
// however the label is composed, and is not read out as "star".
|
|
13
|
+
text: [
|
|
14
|
+
'flex min-w-0 items-baseline',
|
|
15
|
+
"group-data-required/label:after:ms-0.5 group-data-required/label:after:text-destructive group-data-required/label:after:content-['*']",
|
|
16
|
+
],
|
|
17
|
+
textInner: 'min-w-0 truncate',
|
|
18
|
+
hint: [
|
|
19
|
+
'shrink-0 font-normal text-muted-foreground',
|
|
20
|
+
// An invalid field's hint is the reason, so it takes the colour too.
|
|
21
|
+
'group-data-invalid/label:text-destructive group-has-[[aria-invalid=true]]/label:text-destructive',
|
|
22
|
+
],
|
|
23
|
+
action: [
|
|
24
|
+
'order-last ms-auto flex shrink-0 items-center gap-1.5 font-normal text-muted-foreground',
|
|
25
|
+
'[&_a]:text-foreground [&_a]:underline-offset-4 [&_a]:outline-none [&_a:hover]:underline [&_a:focus-visible]:underline',
|
|
26
|
+
'group-data-invalid/label:text-destructive',
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* The label element. A string child is the label's text; for more, compose
|
|
32
|
+
* `Label.Text`, `Label.Hint` and `Label.Action`.
|
|
33
|
+
*/
|
|
34
|
+
function LabelRoot({ required = false, optional = false, invalid = false, disabled = false, className, children, ...props }) {
|
|
35
|
+
const slots = labelVariants();
|
|
36
|
+
const plain = typeof children === 'string' || typeof children === 'number';
|
|
37
|
+
return (_jsxs("label", { "data-slot": "label", "data-required": required ? '' : undefined, "data-invalid": invalid ? '' : undefined, "data-disabled": disabled ? '' : undefined, className: slots.root({ className }), ...props, children: [plain ? _jsx(LabelText, { children: children }) : children, optional ? _jsx(LabelHint, { children: "Optional" }) : null] }));
|
|
38
|
+
}
|
|
39
|
+
LabelRoot.displayName = 'Label';
|
|
40
|
+
/** The field's name. It truncates rather than pushing the action off the line. */
|
|
41
|
+
function LabelText({ className, children, ...props }) {
|
|
42
|
+
const slots = labelVariants();
|
|
43
|
+
return (_jsx("span", { "data-slot": "label-text", className: slots.text({ className }), ...props, children: _jsx("span", { className: slots.textInner(), children: children }) }));
|
|
44
|
+
}
|
|
45
|
+
LabelText.displayName = 'Label.Text';
|
|
46
|
+
/**
|
|
47
|
+
* A quieter word or two after the name: a limit, a format, a unit. On an
|
|
48
|
+
* invalid field it takes the destructive colour, so it can say what is wrong.
|
|
49
|
+
*/
|
|
50
|
+
function LabelHint({ className, ...props }) {
|
|
51
|
+
return _jsx("span", { "data-slot": "label-hint", className: labelVariants().hint({ className }), ...props });
|
|
52
|
+
}
|
|
53
|
+
LabelHint.displayName = 'Label.Hint';
|
|
54
|
+
/** Pushed to the far end of the line: a link, a count, a small button. */
|
|
55
|
+
function LabelAction({ className, ...props }) {
|
|
56
|
+
return _jsx("span", { "data-slot": "label-action", className: labelVariants().action({ className }), ...props });
|
|
57
|
+
}
|
|
58
|
+
LabelAction.displayName = 'Label.Action';
|
|
59
|
+
export const Label = Object.assign(LabelRoot, {
|
|
60
|
+
Text: LabelText,
|
|
61
|
+
Hint: LabelHint,
|
|
62
|
+
Action: LabelAction,
|
|
63
|
+
});
|
|
64
|
+
export { LabelText, LabelHint, LabelAction };
|