panelui-web 0.3.0 → 0.5.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.
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Select — a button that opens a list of options, one of which is chosen.
3
+ *
4
+ * ```tsx
5
+ * <Select items={plans} defaultValue="team">
6
+ * <Select.Trigger>
7
+ * <Select.Value placeholder="Pick a plan" />
8
+ * </Select.Trigger>
9
+ * <Select.Content>
10
+ * {plans.map((plan) => (
11
+ * <Select.Item key={plan.value} value={plan.value} description={plan.price}>
12
+ * {plan.label}
13
+ * </Select.Item>
14
+ * ))}
15
+ * </Select.Content>
16
+ * </Select>
17
+ * ```
18
+ *
19
+ * ## The list opens over the trigger
20
+ *
21
+ * Opened with a pointer, the list is placed so the chosen option sits exactly
22
+ * over the trigger, and the eye does not have to go looking for it. Where
23
+ * there is no room for that, or `alignItemWithTrigger` is false, it opens
24
+ * below. Opened from the keyboard, the arrow keys move through the options,
25
+ * typing jumps to one by its first letters, and Enter chooses.
26
+ *
27
+ * ## Labels
28
+ *
29
+ * Pass `items` to the root, as `{ value, label }` pairs, and `Select.Value`
30
+ * shows the chosen option's label rather than its value. Without `items`,
31
+ * give `Select.Value` a function that turns a value into what to show.
32
+ *
33
+ * ## Three triggers
34
+ *
35
+ * `default` is a field, for a form. `ghost` has no box until it is hovered,
36
+ * for a choice set into a toolbar or a line of text. `pill` is a rounded chip
37
+ * with an edge, for a choice that sits among other chips, such as the model
38
+ * in a prompt.
39
+ */
40
+ import { Select as BaseSelect } from '@base-ui/react/select';
41
+ import type { ComponentProps, ReactNode } from 'react';
42
+ export type SelectProps<Value = string, Multiple extends boolean | undefined = false> = BaseSelect.Root.Props<Value, Multiple>;
43
+ /** Holds the chosen value. Takes `value`/`onValueChange`, `defaultValue`, `items`, `name`, `disabled` and `multiple`. */
44
+ declare function SelectRoot<Value = string, Multiple extends boolean | undefined = false>(props: SelectProps<Value, Multiple>): import("react").JSX.Element;
45
+ declare namespace SelectRoot {
46
+ var displayName: string;
47
+ }
48
+ export interface SelectTriggerProps extends BaseSelect.Trigger.Props {
49
+ /** `default` is a field, `ghost` has no box until hovered, `pill` is a rounded chip. Default `default`. */
50
+ variant?: 'default' | 'ghost' | 'pill';
51
+ /** Its height and text size. Default `md`. */
52
+ size?: 'sm' | 'md' | 'lg';
53
+ /** Replaces the chevron at the end. Pass `null` for none. */
54
+ icon?: ReactNode;
55
+ className?: string;
56
+ }
57
+ /** The button that opens the list. Put a `Select.Value` in it. */
58
+ declare function SelectTrigger({ variant, size, icon, className, children, ...props }: SelectTriggerProps): import("react").JSX.Element;
59
+ declare namespace SelectTrigger {
60
+ var displayName: string;
61
+ }
62
+ export interface SelectValueProps extends BaseSelect.Value.Props {
63
+ className?: string;
64
+ }
65
+ /** The chosen option's label, or `placeholder` while there is none. */
66
+ declare function SelectValue({ className, ...props }: SelectValueProps): import("react").JSX.Element;
67
+ declare namespace SelectValue {
68
+ var displayName: string;
69
+ }
70
+ export interface SelectContentProps extends BaseSelect.Popup.Props {
71
+ /** Which side of the trigger it opens on, when it is not placed over it. Default `bottom`. */
72
+ side?: BaseSelect.Positioner.Props['side'];
73
+ /** How it lines up with the trigger along that side. Default `start`. */
74
+ align?: BaseSelect.Positioner.Props['align'];
75
+ /** The gap between the trigger and the list, in pixels. Default `6`. */
76
+ sideOffset?: number;
77
+ /** Whether a pointer opens it with the chosen option over the trigger. Default `true`. */
78
+ alignItemWithTrigger?: boolean;
79
+ /** Where in the document the list is put. Default `document.body`. */
80
+ container?: BaseSelect.Portal.Props['container'];
81
+ className?: string;
82
+ }
83
+ /** The list of options, opened in a layer over the page. */
84
+ declare function SelectContent({ side, align, sideOffset, alignItemWithTrigger, container, className, children, ...props }: SelectContentProps): import("react").JSX.Element;
85
+ declare namespace SelectContent {
86
+ var displayName: string;
87
+ }
88
+ export interface SelectItemProps extends Omit<BaseSelect.Item.Props, 'children'> {
89
+ /** The option's label: what the trigger shows once it is chosen. */
90
+ children?: ReactNode;
91
+ /** A second, quieter line under the label. */
92
+ description?: ReactNode;
93
+ /** An icon before the label. */
94
+ icon?: ReactNode;
95
+ className?: string;
96
+ }
97
+ /** One option. A check marks the chosen one. */
98
+ declare function SelectItem({ description, icon, className, children, ...props }: SelectItemProps): import("react").JSX.Element;
99
+ declare namespace SelectItem {
100
+ var displayName: string;
101
+ }
102
+ export interface SelectGroupProps extends BaseSelect.Group.Props {
103
+ className?: string;
104
+ }
105
+ /** Options that belong together, under a `Select.GroupLabel`. */
106
+ declare function SelectGroup({ className, ...props }: SelectGroupProps): import("react").JSX.Element;
107
+ declare namespace SelectGroup {
108
+ var displayName: string;
109
+ }
110
+ export interface SelectGroupLabelProps extends BaseSelect.GroupLabel.Props {
111
+ className?: string;
112
+ }
113
+ /** The heading of a group. Screen readers announce the group by it. */
114
+ declare function SelectGroupLabel({ className, ...props }: SelectGroupLabelProps): import("react").JSX.Element;
115
+ declare namespace SelectGroupLabel {
116
+ var displayName: string;
117
+ }
118
+ export interface SelectSeparatorProps extends ComponentProps<'div'> {
119
+ }
120
+ /** A rule between groups of options. */
121
+ declare function SelectSeparator({ className, ...props }: SelectSeparatorProps): import("react").JSX.Element;
122
+ declare namespace SelectSeparator {
123
+ var displayName: string;
124
+ }
125
+ export declare const Select: typeof SelectRoot & {
126
+ Trigger: typeof SelectTrigger;
127
+ Value: typeof SelectValue;
128
+ Content: typeof SelectContent;
129
+ Item: typeof SelectItem;
130
+ Group: typeof SelectGroup;
131
+ GroupLabel: typeof SelectGroupLabel;
132
+ Separator: typeof SelectSeparator;
133
+ };
134
+ export { SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectGroupLabel, SelectSeparator };
@@ -0,0 +1,164 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Select — a button that opens a list of options, one of which is chosen.
4
+ *
5
+ * ```tsx
6
+ * <Select items={plans} defaultValue="team">
7
+ * <Select.Trigger>
8
+ * <Select.Value placeholder="Pick a plan" />
9
+ * </Select.Trigger>
10
+ * <Select.Content>
11
+ * {plans.map((plan) => (
12
+ * <Select.Item key={plan.value} value={plan.value} description={plan.price}>
13
+ * {plan.label}
14
+ * </Select.Item>
15
+ * ))}
16
+ * </Select.Content>
17
+ * </Select>
18
+ * ```
19
+ *
20
+ * ## The list opens over the trigger
21
+ *
22
+ * Opened with a pointer, the list is placed so the chosen option sits exactly
23
+ * over the trigger, and the eye does not have to go looking for it. Where
24
+ * there is no room for that, or `alignItemWithTrigger` is false, it opens
25
+ * below. Opened from the keyboard, the arrow keys move through the options,
26
+ * typing jumps to one by its first letters, and Enter chooses.
27
+ *
28
+ * ## Labels
29
+ *
30
+ * Pass `items` to the root, as `{ value, label }` pairs, and `Select.Value`
31
+ * shows the chosen option's label rather than its value. Without `items`,
32
+ * give `Select.Value` a function that turns a value into what to show.
33
+ *
34
+ * ## Three triggers
35
+ *
36
+ * `default` is a field, for a form. `ghost` has no box until it is hovered,
37
+ * for a choice set into a toolbar or a line of text. `pill` is a rounded chip
38
+ * with an edge, for a choice that sits among other chips, such as the model
39
+ * in a prompt.
40
+ */
41
+ import { Select as BaseSelect } from '@base-ui/react/select';
42
+ import { tv } from 'tailwind-variants';
43
+ const selectVariants = tv({
44
+ slots: {
45
+ trigger: [
46
+ 'group/select-trigger inline-flex min-w-0 cursor-pointer select-none items-center justify-between gap-2 whitespace-nowrap text-foreground outline-none',
47
+ 'transition-[background-color,border-color,box-shadow,color] duration-150',
48
+ 'focus-visible:ring-[3px] focus-visible:ring-ring/30',
49
+ 'data-disabled:cursor-not-allowed data-disabled:opacity-50',
50
+ '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',
51
+ ],
52
+ icon: 'flex text-muted-foreground transition-transform duration-200 group-data-popup-open/select-trigger:rotate-180 motion-reduce:transition-none',
53
+ value: 'flex min-w-0 flex-1 items-center gap-2 truncate text-start data-placeholder:text-muted-foreground/80',
54
+ positioner: 'z-50 outline-none',
55
+ popup: [
56
+ 'min-w-(--anchor-width) origin-(--transform-origin) overflow-hidden rounded-xl border bg-popover text-popover-foreground shadow-lg outline-none',
57
+ 'transition-[opacity,scale] duration-150 ease-out',
58
+ 'data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:scale-95 data-ending-style:opacity-0',
59
+ // Placed over the trigger, it only fades: growing from a point inside
60
+ // the list would look like it came from nowhere.
61
+ 'data-[side=none]:data-starting-style:scale-100 data-[side=none]:data-ending-style:scale-100',
62
+ 'motion-reduce:transition-none',
63
+ ],
64
+ list: 'max-h-(--available-height) scroll-py-1 overflow-y-auto p-1 [scrollbar-color:var(--border)_transparent] [scrollbar-width:thin]',
65
+ item: [
66
+ 'flex cursor-default items-center gap-2.5 rounded-lg px-2 py-1.5 text-sm outline-none select-none',
67
+ 'data-highlighted:bg-accent data-highlighted:text-accent-foreground',
68
+ 'data-disabled:pointer-events-none data-disabled:opacity-50',
69
+ 'in-data-[side=none]:min-w-[calc(var(--anchor-width)+0.5rem)]',
70
+ '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',
71
+ ],
72
+ itemIcon: 'flex size-4 shrink-0 items-center justify-center text-muted-foreground',
73
+ itemBody: 'flex min-w-0 flex-1 flex-col',
74
+ itemText: 'truncate',
75
+ itemDescription: 'truncate text-xs text-muted-foreground',
76
+ // The box is always there, so an option does not change width as it is chosen.
77
+ indicator: 'flex size-4 shrink-0 items-center justify-center text-foreground',
78
+ group: 'flex flex-col',
79
+ groupLabel: 'px-2 pb-1 pt-2 text-xs font-medium text-muted-foreground',
80
+ separator: 'mx-2 my-1 h-px bg-border',
81
+ },
82
+ variants: {
83
+ variant: {
84
+ default: {
85
+ trigger: 'rounded-lg border border-input bg-background shadow-xs hover:bg-accent/40 data-popup-open:border-ring dark:bg-input/30',
86
+ },
87
+ ghost: {
88
+ trigger: 'rounded-lg text-muted-foreground hover:bg-accent hover:text-foreground data-popup-open:bg-accent data-popup-open:text-foreground',
89
+ },
90
+ pill: {
91
+ trigger: 'rounded-full border bg-background shadow-xs hover:bg-accent data-popup-open:bg-accent dark:bg-input/30',
92
+ },
93
+ },
94
+ size: {
95
+ sm: { trigger: 'h-8 px-2.5 text-[0.8125rem]' },
96
+ md: { trigger: 'h-9 px-3 text-sm' },
97
+ lg: { trigger: 'h-11 px-3.5 text-[0.9375rem]' },
98
+ },
99
+ },
100
+ compoundVariants: [
101
+ // A field is as wide as its column; a ghost or a pill is as wide as its label.
102
+ { variant: 'default', className: { trigger: 'w-full' } },
103
+ { variant: 'ghost', size: 'sm', className: { trigger: 'px-2' } },
104
+ ],
105
+ defaultVariants: { variant: 'default', size: 'md' },
106
+ });
107
+ /** Holds the chosen value. Takes `value`/`onValueChange`, `defaultValue`, `items`, `name`, `disabled` and `multiple`. */
108
+ function SelectRoot(props) {
109
+ return _jsx(BaseSelect.Root, { ...props });
110
+ }
111
+ SelectRoot.displayName = 'Select';
112
+ function Chevron({ variant }) {
113
+ // A field shows both directions, since its list can open either way over it.
114
+ // A chip shows one, and turns over while its list is open.
115
+ return (_jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: variant === 'default' || variant === undefined ? _jsx("path", { d: "m7 15 5 5 5-5M7 9l5-5 5 5" }) : _jsx("path", { d: "m6 9 6 6 6-6" }) }));
116
+ }
117
+ /** The button that opens the list. Put a `Select.Value` in it. */
118
+ function SelectTrigger({ variant, size, icon, className, children, ...props }) {
119
+ const slots = selectVariants({ variant, size });
120
+ return (_jsxs(BaseSelect.Trigger, { "data-slot": "select-trigger", className: slots.trigger({ className }), ...props, children: [children, icon === null ? null : (_jsx(BaseSelect.Icon, { "data-slot": "select-icon", className: slots.icon({ className: variant === 'default' || variant === undefined ? 'rotate-0!' : undefined }), children: icon ?? _jsx(Chevron, { variant: variant }) }))] }));
121
+ }
122
+ SelectTrigger.displayName = 'Select.Trigger';
123
+ /** The chosen option's label, or `placeholder` while there is none. */
124
+ function SelectValue({ className, ...props }) {
125
+ return _jsx(BaseSelect.Value, { "data-slot": "select-value", className: selectVariants().value({ className }), ...props });
126
+ }
127
+ SelectValue.displayName = 'Select.Value';
128
+ /** The list of options, opened in a layer over the page. */
129
+ function SelectContent({ side = 'bottom', align = 'start', sideOffset = 6, alignItemWithTrigger = true, container, className, children, ...props }) {
130
+ const slots = selectVariants();
131
+ return (_jsx(BaseSelect.Portal, { container: container, children: _jsx(BaseSelect.Positioner, { side: side, align: align, sideOffset: sideOffset, alignItemWithTrigger: alignItemWithTrigger, className: slots.positioner(), children: _jsx(BaseSelect.Popup, { "data-slot": "select-content", className: slots.popup({ className }), ...props, children: _jsx(BaseSelect.List, { className: slots.list(), children: children }) }) }) }));
132
+ }
133
+ SelectContent.displayName = 'Select.Content';
134
+ /** One option. A check marks the chosen one. */
135
+ function SelectItem({ description, icon, className, children, ...props }) {
136
+ const slots = selectVariants();
137
+ return (_jsxs(BaseSelect.Item, { "data-slot": "select-item", className: slots.item({ className }), ...props, children: [icon ? (_jsx("span", { "data-slot": "select-item-icon", "aria-hidden": "true", className: slots.itemIcon(), children: icon })) : null, _jsxs("span", { className: slots.itemBody(), children: [_jsx(BaseSelect.ItemText, { className: slots.itemText(), children: children }), description ? _jsx("span", { className: slots.itemDescription(), children: description }) : null] }), _jsx("span", { className: slots.indicator(), children: _jsx(BaseSelect.ItemIndicator, { className: "flex", children: _jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.25", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M20 6 9 17l-5-5" }) }) }) })] }));
138
+ }
139
+ SelectItem.displayName = 'Select.Item';
140
+ /** Options that belong together, under a `Select.GroupLabel`. */
141
+ function SelectGroup({ className, ...props }) {
142
+ return _jsx(BaseSelect.Group, { "data-slot": "select-group", className: selectVariants().group({ className }), ...props });
143
+ }
144
+ SelectGroup.displayName = 'Select.Group';
145
+ /** The heading of a group. Screen readers announce the group by it. */
146
+ function SelectGroupLabel({ className, ...props }) {
147
+ return (_jsx(BaseSelect.GroupLabel, { "data-slot": "select-group-label", className: selectVariants().groupLabel({ className }), ...props }));
148
+ }
149
+ SelectGroupLabel.displayName = 'Select.GroupLabel';
150
+ /** A rule between groups of options. */
151
+ function SelectSeparator({ className, ...props }) {
152
+ return _jsx("div", { role: "separator", "data-slot": "select-separator", className: selectVariants().separator({ className }), ...props });
153
+ }
154
+ SelectSeparator.displayName = 'Select.Separator';
155
+ export const Select = Object.assign(SelectRoot, {
156
+ Trigger: SelectTrigger,
157
+ Value: SelectValue,
158
+ Content: SelectContent,
159
+ Item: SelectItem,
160
+ Group: SelectGroup,
161
+ GroupLabel: SelectGroupLabel,
162
+ Separator: SelectSeparator,
163
+ });
164
+ export { SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectGroupLabel, SelectSeparator };
@@ -41,7 +41,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
41
41
  */
42
42
  import { Drawer } from '@base-ui/react/drawer';
43
43
  import { ScrollArea } from '@base-ui/react/scroll-area';
44
- import { createContext, useContext, useMemo, } from 'react';
44
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
45
45
  import { tv } from 'tailwind-variants';
46
46
  /*
47
47
  * One curve for every movement: the sheet arriving, the one behind it stepping
@@ -216,8 +216,44 @@ function useSheet(part) {
216
216
  function SheetRoot({ children, open, onOpenChange, defaultOpen = false, side = 'bottom', dismissible = true, }) {
217
217
  // A sheet inside another sheet's content finds that sheet's context.
218
218
  const parent = useContext(SheetContext);
219
- const context = useMemo(() => ({ side, nested: parent !== null && !parent.inline, inline: false, showClose: true }), [side, parent]);
220
- return (_jsx(SheetContext.Provider, { value: context, children: _jsx(Drawer.Root, { open: open, defaultOpen: defaultOpen, swipeDirection: SWIPE[side], disablePointerDismissal: !dismissible, onOpenChange: (next, details) => {
219
+ const nested = parent !== null && !parent.inline;
220
+ // Held here as well as by the primitive, so a sheet opened from this one
221
+ // can close it.
222
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
223
+ const isOpen = open ?? internalOpen;
224
+ const setOpen = useCallback((next) => {
225
+ if (open === undefined)
226
+ setInternalOpen(next);
227
+ onOpenChange?.(next);
228
+ }, [open, onOpenChange]);
229
+ // The sheets opened from this one, so a press on the page behind them all
230
+ // can close the whole stack. The primitive gives that press to no one
231
+ // while a second sheet is open.
232
+ const opened = useRef(new Set());
233
+ const live = useRef({ isOpen, setOpen });
234
+ live.current = { isOpen, setOpen };
235
+ const handle = useMemo(() => ({
236
+ isOpen: () => live.current.isOpen,
237
+ closeTree: () => {
238
+ opened.current.forEach((child) => child.closeTree());
239
+ if (live.current.isOpen)
240
+ live.current.setOpen(false);
241
+ },
242
+ }), []);
243
+ useEffect(() => (nested && parent ? parent.register(handle) : undefined), [nested, parent, handle]);
244
+ const context = useMemo(() => ({
245
+ side,
246
+ nested,
247
+ inline: false,
248
+ showClose: true,
249
+ register: (child) => {
250
+ opened.current.add(child);
251
+ return () => opened.current.delete(child);
252
+ },
253
+ closeTree: handle.closeTree,
254
+ hasOpenChild: () => [...opened.current].some((child) => child.isOpen()),
255
+ }), [side, nested, handle]);
256
+ return (_jsx(SheetContext.Provider, { value: context, children: _jsx(Drawer.Root, { open: isOpen, swipeDirection: SWIPE[side], disablePointerDismissal: !dismissible, onOpenChange: (next, details) => {
221
257
  // The primitive has a switch for a press outside and none for
222
258
  // Escape or a swipe, so those are refused here.
223
259
  if (!next &&
@@ -226,7 +262,7 @@ function SheetRoot({ children, open, onOpenChange, defaultOpen = false, side = '
226
262
  details.reason !== 'imperative-action') {
227
263
  return;
228
264
  }
229
- onOpenChange?.(next);
265
+ setOpen(next);
230
266
  }, children: children }) }));
231
267
  }
232
268
  SheetRoot.displayName = 'Sheet';
@@ -250,7 +286,12 @@ function SheetContent({ children, detached = true, size = 'auto', showGrabber, s
250
286
  if (inline) {
251
287
  return (_jsx(SheetContext.Provider, { value: context, children: _jsx("div", { className: slots.popup({ className: ['[transform:none]', className] }), ...props, children: inside }) }));
252
288
  }
253
- return (_jsxs(Drawer.Portal, { children: [sheet.nested ? null : _jsx(Drawer.Backdrop, { className: slots.backdrop() }), _jsx(Drawer.Viewport, { className: slots.viewport(), children: _jsx(Drawer.Popup, { className: slots.popup({ className }), ...props, children: _jsx(SheetContext.Provider, { value: context, children: inside }) }) })] }));
289
+ return (_jsxs(Drawer.Portal, { children: [sheet.nested ? null : (_jsx(Drawer.Backdrop, { className: slots.backdrop(),
290
+ // With a second sheet open, a press here is outside both, and closes both.
291
+ onClick: () => {
292
+ if (sheet.hasOpenChild())
293
+ sheet.closeTree();
294
+ } })), _jsx(Drawer.Viewport, { className: slots.viewport(), children: _jsx(Drawer.Popup, { className: slots.popup({ className }), ...props, children: _jsx(SheetContext.Provider, { value: context, children: inside }) }) })] }));
254
295
  }
255
296
  SheetContent.displayName = 'Sheet.Content';
256
297
  /**
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Textarea — a field for more than one line, that grows as it is written in.
3
+ *
4
+ * ```tsx
5
+ * <Textarea placeholder="What happened?" />
6
+ * <Textarea minRows={1} maxRows={6} variant="ghost" />
7
+ * ```
8
+ *
9
+ * ## Growing
10
+ *
11
+ * It starts `minRows` lines tall and grows a line at a time as text is
12
+ * written, up to `maxRows`. Past that it scrolls, with a thin scrollbar of its
13
+ * own rather than the browser's. Pass `autoResize={false}` for a field that
14
+ * keeps the size `rows` gives it and can be resized by hand.
15
+ *
16
+ * The height is measured from the text, so it is right on the first render
17
+ * for a value that was already there, and for a value set from outside.
18
+ *
19
+ * ## Ghost
20
+ *
21
+ * `variant="ghost"` drops the border, background and ring, for a field set
22
+ * inside something that already draws them, such as a card or a composer.
23
+ */
24
+ import { type ComponentProps } from 'react';
25
+ export interface TextareaProps extends Omit<ComponentProps<'textarea'>, 'size'> {
26
+ /** `ghost` draws no box, for a field inside something that draws one. Default `default`. */
27
+ variant?: 'default' | 'ghost';
28
+ /** Its text size and line height. Default `md`. */
29
+ size?: 'sm' | 'md' | 'lg';
30
+ /** Whether it grows with what is written in it. Default `true`. */
31
+ autoResize?: boolean;
32
+ /** How many lines tall it starts, while growing. Default `2`. */
33
+ minRows?: number;
34
+ /** How many lines tall it can grow before it scrolls. Default `8`. */
35
+ maxRows?: number;
36
+ }
37
+ /** A multi-line field. Every attribute of `<textarea>` passes through. */
38
+ declare function TextareaRoot({ variant, size, autoResize, minRows, maxRows, rows, value, onInput, className, style, ref, ...props }: TextareaProps): import("react").JSX.Element;
39
+ declare namespace TextareaRoot {
40
+ var displayName: string;
41
+ }
42
+ export declare const Textarea: typeof TextareaRoot;
43
+ export {};
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ /**
4
+ * Textarea — a field for more than one line, that grows as it is written in.
5
+ *
6
+ * ```tsx
7
+ * <Textarea placeholder="What happened?" />
8
+ * <Textarea minRows={1} maxRows={6} variant="ghost" />
9
+ * ```
10
+ *
11
+ * ## Growing
12
+ *
13
+ * It starts `minRows` lines tall and grows a line at a time as text is
14
+ * written, up to `maxRows`. Past that it scrolls, with a thin scrollbar of its
15
+ * own rather than the browser's. Pass `autoResize={false}` for a field that
16
+ * keeps the size `rows` gives it and can be resized by hand.
17
+ *
18
+ * The height is measured from the text, so it is right on the first render
19
+ * for a value that was already there, and for a value set from outside.
20
+ *
21
+ * ## Ghost
22
+ *
23
+ * `variant="ghost"` drops the border, background and ring, for a field set
24
+ * inside something that already draws them, such as a card or a composer.
25
+ */
26
+ import { useCallback, useLayoutEffect, useRef } from 'react';
27
+ import { tv } from 'tailwind-variants';
28
+ const textareaVariants = tv({
29
+ base: [
30
+ 'block w-full min-w-0 bg-transparent text-foreground outline-none',
31
+ 'placeholder:text-muted-foreground/80 selection:bg-primary selection:text-primary-foreground',
32
+ 'disabled:cursor-not-allowed disabled:opacity-50',
33
+ // A thin bar in the theme's border colour, never the platform's wide one.
34
+ '[scrollbar-color:var(--border)_transparent] [scrollbar-width:thin]',
35
+ ],
36
+ variants: {
37
+ variant: {
38
+ default: [
39
+ 'rounded-lg border border-input bg-background shadow-xs transition-[border-color,box-shadow] dark:bg-input/30',
40
+ 'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/30',
41
+ 'aria-invalid:border-destructive aria-invalid:ring-destructive/20',
42
+ ],
43
+ ghost: 'rounded-none border-0 p-0 shadow-none',
44
+ },
45
+ size: {
46
+ sm: 'text-[0.8125rem] leading-5',
47
+ md: 'text-sm leading-6',
48
+ lg: 'text-base leading-7',
49
+ },
50
+ autoResize: {
51
+ true: 'resize-none',
52
+ false: 'resize-y',
53
+ },
54
+ },
55
+ compoundVariants: [
56
+ { variant: 'default', size: 'sm', className: 'px-2.5 py-1.5' },
57
+ { variant: 'default', size: 'md', className: 'px-3 py-2' },
58
+ { variant: 'default', size: 'lg', className: 'px-3.5 py-2.5' },
59
+ ],
60
+ defaultVariants: { variant: 'default', size: 'md', autoResize: true },
61
+ });
62
+ function setRef(ref, value) {
63
+ if (typeof ref === 'function')
64
+ ref(value);
65
+ else if (ref)
66
+ ref.current = value;
67
+ }
68
+ /** A multi-line field. Every attribute of `<textarea>` passes through. */
69
+ function TextareaRoot({ variant, size, autoResize = true, minRows = 2, maxRows = 8, rows, value, onInput, className, style, ref, ...props }) {
70
+ const field = useRef(null);
71
+ // Set to one row, read how tall the text wants to be, and clamp that to the
72
+ // rows allowed. The padding and border are part of both ends of the clamp.
73
+ const fit = useCallback(() => {
74
+ const node = field.current;
75
+ if (!node || !autoResize)
76
+ return;
77
+ const computed = getComputedStyle(node);
78
+ const line = parseFloat(computed.lineHeight) || 20;
79
+ const frame = parseFloat(computed.paddingTop) +
80
+ parseFloat(computed.paddingBottom) +
81
+ parseFloat(computed.borderTopWidth) +
82
+ parseFloat(computed.borderBottomWidth);
83
+ node.style.height = 'auto';
84
+ const wanted = node.scrollHeight + parseFloat(computed.borderTopWidth) + parseFloat(computed.borderBottomWidth);
85
+ const min = line * minRows + frame;
86
+ const max = line * Math.max(maxRows, minRows) + frame;
87
+ node.style.height = `${Math.min(Math.max(wanted, min), max)}px`;
88
+ node.style.overflowY = wanted > max ? 'auto' : 'hidden';
89
+ }, [autoResize, minRows, maxRows]);
90
+ // Before paint, so a value that was already there never shows at the wrong height.
91
+ useLayoutEffect(fit, [fit, value]);
92
+ return (_jsx("textarea", { ref: (node) => {
93
+ field.current = node;
94
+ setRef(ref, node);
95
+ }, "data-slot": "textarea", rows: autoResize ? minRows : rows, value: value, onInput: (event) => {
96
+ onInput?.(event);
97
+ fit();
98
+ }, className: textareaVariants({ variant, size, autoResize, className }), style: style, ...props }));
99
+ }
100
+ TextareaRoot.displayName = 'Textarea';
101
+ export const Textarea = TextareaRoot;
package/dist/index.d.ts CHANGED
@@ -14,3 +14,10 @@ export { Flow, FlowBackground, FlowNode, FlowHandle, FlowEdge, FlowGroup, FlowGr
14
14
  export { TiltCard, TiltCardLayer, TiltCardTitle, TiltCardDescription, type TiltCardProps, type TiltCardLayerProps, type TiltCardTitleProps, type TiltCardDescriptionProps, type TiltCardColor, } from './components/tilt-card/index.js';
15
15
  export { Stat, StatContent, StatLabel, StatDescription, StatValue, StatUnit, StatBadge, type StatProps, type StatContentProps, type StatLabelProps, type StatDescriptionProps, type StatValueProps, type StatUnitProps, type StatBadgeProps, } from './components/stat/index.js';
16
16
  export { CommandDock, CommandDockTrigger, CommandDockPanel, CommandDockInput, CommandDockResults, CommandDockRow, CommandDockRowTitle, CommandDockRowMeta, CommandDockStatus, type CommandDockProps, type CommandDockTriggerProps, type CommandDockPanelProps, type CommandDockInputProps, type CommandDockResultsProps, type CommandDockRowProps, type CommandDockRowTitleProps, type CommandDockRowMetaProps, type CommandDockStatusProps, } from './components/command-dock/index.js';
17
+ export { NotFound, NotFoundCode, NotFoundTitle, NotFoundDescription, NotFoundActions, type NotFoundProps, type NotFoundCodeProps, type NotFoundTitleProps, type NotFoundDescriptionProps, type NotFoundActionsProps, type NotFoundEffect, type NotFoundColor, } from './components/not-found/index.js';
18
+ export { Button, buttonVariants, type ButtonProps, type ButtonVariant, type ButtonSize } from './components/button/index.js';
19
+ export { Input, InputGroup, InputAddon, type InputProps, type InputGroupProps, type InputAddonProps, type InputSize } from './components/input/index.js';
20
+ export { Textarea, type TextareaProps } from './components/textarea/index.js';
21
+ export { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectGroupLabel, SelectSeparator, type SelectProps, type SelectTriggerProps, type SelectValueProps, type SelectContentProps, type SelectItemProps, type SelectGroupProps, type SelectGroupLabelProps, type SelectSeparatorProps, } from './components/select/index.js';
22
+ export { Prompt, PromptBanner, PromptBannerAction, PromptField, PromptAttachments, PromptTextarea, PromptFooter, PromptTools, PromptTool, PromptAttach, PromptMenu, PromptAttachItem, PromptModel, PromptSubmit, PromptStatus, PromptTasks, PromptTask, PromptCommands, PromptRecorder, PromptContext, PromptContextItem, PromptHint, type PromptProps, type PromptState, type PromptMessage, type PromptAttachment, type PromptRejectReason, type PromptBannerProps, type PromptBannerActionProps, type PromptFieldProps, type PromptAttachmentsProps, type PromptTextareaProps, type PromptFooterProps, type PromptToolsProps, type PromptToolProps, type PromptAttachProps, type PromptMenuProps, type PromptAttachItemProps, type PromptModelProps, type PromptModelItem, type PromptSubmitProps, type PromptStatusProps, type PromptTasksProps, type PromptTaskProps, type PromptTaskStatus, type PromptCommandsProps, type PromptCommandItem, type PromptRecorderProps, type PromptContextProps, type PromptContextItemProps, type PromptHintProps, } from './components/prompt/index.js';
23
+ export { Menu, MenuTrigger, MenuContent, MenuItem, MenuCheckboxItem, MenuRadioGroup, MenuRadioItem, MenuSub, MenuSubTrigger, MenuSubContent, MenuGroup, MenuGroupLabel, MenuSeparator, MenuDescription, MenuShortcut, MenuBadge, type MenuProps, type MenuTriggerProps, type MenuContentProps, type MenuItemProps, type MenuCheckboxItemProps, type MenuRadioGroupProps, type MenuRadioItemProps, type MenuSubProps, type MenuSubTriggerProps, type MenuSubContentProps, type MenuGroupProps, type MenuGroupLabelProps, type MenuSeparatorProps, type MenuDescriptionProps, type MenuShortcutProps, type MenuBadgeProps, } from './components/menu/index.js';
package/dist/index.js CHANGED
@@ -14,3 +14,10 @@ export { Flow, FlowBackground, FlowNode, FlowHandle, FlowEdge, FlowGroup, FlowGr
14
14
  export { TiltCard, TiltCardLayer, TiltCardTitle, TiltCardDescription, } from './components/tilt-card/index.js';
15
15
  export { Stat, StatContent, StatLabel, StatDescription, StatValue, StatUnit, StatBadge, } from './components/stat/index.js';
16
16
  export { CommandDock, CommandDockTrigger, CommandDockPanel, CommandDockInput, CommandDockResults, CommandDockRow, CommandDockRowTitle, CommandDockRowMeta, CommandDockStatus, } from './components/command-dock/index.js';
17
+ export { NotFound, NotFoundCode, NotFoundTitle, NotFoundDescription, NotFoundActions, } from './components/not-found/index.js';
18
+ export { Button, buttonVariants } from './components/button/index.js';
19
+ export { Input, InputGroup, InputAddon } from './components/input/index.js';
20
+ export { Textarea } from './components/textarea/index.js';
21
+ export { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectGroupLabel, SelectSeparator, } from './components/select/index.js';
22
+ export { Prompt, PromptBanner, PromptBannerAction, PromptField, PromptAttachments, PromptTextarea, PromptFooter, PromptTools, PromptTool, PromptAttach, PromptMenu, PromptAttachItem, PromptModel, PromptSubmit, PromptStatus, PromptTasks, PromptTask, PromptCommands, PromptRecorder, PromptContext, PromptContextItem, PromptHint, } from './components/prompt/index.js';
23
+ export { Menu, MenuTrigger, MenuContent, MenuItem, MenuCheckboxItem, MenuRadioGroup, MenuRadioItem, MenuSub, MenuSubTrigger, MenuSubContent, MenuGroup, MenuGroupLabel, MenuSeparator, MenuDescription, MenuShortcut, MenuBadge, } from './components/menu/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panelui-web",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "PanelUI for the web: animated, data-rich React components styled with Tailwind CSS on shadcn tokens.",
5
5
  "type": "module",
6
6
  "sideEffects": false,