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.
@@ -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 [rects, setRects] = useState({});
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.rects[id] ?? { ...position, width: 0, height: 0 };
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 (draggable === false || flow.locked)
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 (draggable !== false && !flow.locked) {
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 (draggable === false || flow.locked)
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 movable = useMovable({ id, position, draggable, onPress, onDelete, moveStep });
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 }) }));
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
- const endpoint = (reference) => typeof reference === 'string' ? { node: reference } : reference;
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 };