panelui-web 0.9.0 → 0.10.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.
@@ -46,6 +46,18 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
46
46
  * Every sheet follows a drag and leaves on a flick toward its own edge. The
47
47
  * grabber on a top or bottom sheet says so before anybody tries.
48
48
  *
49
+ * With a mouse, the text in a sheet is for reading: a press on the title,
50
+ * the description, the body or the footer selects text the way it does on the
51
+ * page, and the sheet is dragged by its grabber and the padding around the
52
+ * header. A finger still swipes the sheet from anywhere.
53
+ *
54
+ * ## The close button
55
+ *
56
+ * Every sheet has a round close button in its top trailing corner. Write a
57
+ * bare `<Sheet.Close />` somewhere in the sheet to put it there instead, for
58
+ * example first in `Sheet.Header` to stand before the title, and the one in
59
+ * the corner steps aside. `showClose={false}` leaves the sheet with none.
60
+ *
49
61
  * ## Slides
50
62
  *
51
63
  * `Sheet.Slides` takes the place of the body with a column of rounded
@@ -58,7 +70,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
58
70
  */
59
71
  import { Drawer } from '@base-ui/react/drawer';
60
72
  import { ScrollArea } from '@base-ui/react/scroll-area';
61
- import { Children, createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
73
+ import { Children, createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
62
74
  import { tv } from 'tailwind-variants';
63
75
  /*
64
76
  * One curve for every movement: the sheet arriving, the one behind it stepping
@@ -116,12 +128,24 @@ const sheetVariants = tv({
116
128
  'before:h-[5px] before:w-9 before:rounded-full before:bg-muted-foreground/35',
117
129
  ],
118
130
  close: [
119
- 'absolute end-4 top-4 z-10 inline-flex size-8 cursor-pointer items-center justify-center rounded-full',
131
+ 'relative inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full',
120
132
  'bg-muted text-muted-foreground outline-none transition-[background-color,color,scale] duration-150',
121
133
  'after:absolute after:-inset-1.5 hover:bg-foreground/10 hover:text-foreground active:scale-95',
122
134
  'focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none',
123
135
  ],
124
- header: 'flex shrink-0 flex-col gap-1 px-6 pt-6 pb-3',
136
+ closeCorner: 'absolute end-4 top-4 z-10',
137
+ /*
138
+ * A close button written into the header shares the title's line: the
139
+ * header becomes three columns, the button takes the first or the last
140
+ * depending on where it was written, and the text keeps the middle.
141
+ */
142
+ header: [
143
+ 'flex shrink-0 flex-col gap-1 px-6 pt-6 pb-3',
144
+ 'has-[>[data-slot=sheet-close]]:grid has-[>[data-slot=sheet-close]]:grid-cols-[auto_minmax(0,1fr)_auto] has-[>[data-slot=sheet-close]]:gap-x-3',
145
+ '[&>*]:col-start-2',
146
+ '[&>[data-slot=sheet-close]]:row-span-2 [&>[data-slot=sheet-close]]:row-start-1 [&>[data-slot=sheet-close]]:-my-1 [&>[data-slot=sheet-close]]:self-start',
147
+ '[&>[data-slot=sheet-close]:first-child]:col-start-1 [&>[data-slot=sheet-close]:not(:first-child)]:col-start-3',
148
+ ],
125
149
  title: 'text-[19px] font-semibold leading-6 tracking-[-0.01em]',
126
150
  description: 'text-[15px] leading-5 text-muted-foreground',
127
151
  body: 'relative flex min-h-0 flex-1 flex-col',
@@ -319,14 +343,16 @@ function SheetFrame({ children, open, onOpenChange, defaultOpen = false, side =
319
343
  stack,
320
344
  nested,
321
345
  inline: false,
346
+ dismissible,
322
347
  showClose: true,
348
+ claimClose: () => () => { },
323
349
  register: (child) => {
324
350
  opened.current.add(child);
325
351
  return () => opened.current.delete(child);
326
352
  },
327
353
  closeTree: handle.closeTree,
328
354
  hasOpenChild: () => [...opened.current].some((child) => child.isOpen()),
329
- }), [side, stack, nested, handle]);
355
+ }), [side, stack, nested, dismissible, handle]);
330
356
  return (_jsx(SheetContext.Provider, { value: context, children: _jsx(Drawer.Root, { open: isOpen, handle: drawerHandle, swipeDirection: SWIPE[side], disablePointerDismissal: !dismissible, onOpenChange: (next, details) => {
331
357
  // The primitive has a switch for a press outside and none for
332
358
  // Escape or a swipe, so those are refused here.
@@ -384,8 +410,16 @@ function SheetContent({ children, detached = true, size = 'auto', showGrabber, s
384
410
  attached: !detached || (vertical && size === 'full'),
385
411
  blur,
386
412
  });
387
- const context = useMemo(() => ({ ...sheet, inline, showClose }), [sheet, inline, showClose]);
388
- const inside = (_jsxs(_Fragment, { children: [(showGrabber ?? vertical) ? _jsx("div", { "aria-hidden": true, className: slots.grabber() }) : null, showClose ? _jsx(SheetClose, { "aria-label": "Close", className: slots.close(), icon: true }) : null, children] }));
413
+ // Bare close buttons written into the sheet. While there is one, the
414
+ // corner button is not drawn.
415
+ const [placed, setPlaced] = useState(0);
416
+ const claimClose = useCallback(() => {
417
+ setPlaced((count) => count + 1);
418
+ return () => setPlaced((count) => count - 1);
419
+ }, []);
420
+ const corner = showClose && placed === 0;
421
+ const context = useMemo(() => ({ ...sheet, inline, showClose: corner, claimClose }), [sheet, inline, corner, claimClose]);
422
+ const inside = (_jsxs(_Fragment, { children: [(showGrabber ?? (vertical && sheet.dismissible)) ? _jsx("div", { "aria-hidden": true, className: slots.grabber() }) : null, corner ? _jsx(CloseButton, { className: slots.closeCorner() }) : null, children] }));
389
423
  if (inline) {
390
424
  return (_jsx(SheetContext.Provider, { value: context, children: _jsx("div", { className: slots.popup({ className: ['[transform:none]', className] }), ...props, children: inside }) }));
391
425
  }
@@ -413,7 +447,7 @@ function SheetTitle({ className, ...props }) {
413
447
  const slots = sheetVariants();
414
448
  if (inline)
415
449
  return _jsx("h2", { className: slots.title({ className }), ...props });
416
- return _jsx(Drawer.Title, { className: slots.title({ className }), ...props });
450
+ return _jsx(Drawer.Content, { render: _jsx(Drawer.Title, { className: slots.title({ className }), ...props }) });
417
451
  }
418
452
  SheetTitle.displayName = 'Sheet.Title';
419
453
  /** A sentence under the title, read out after it. */
@@ -422,7 +456,7 @@ function SheetDescription({ className, ...props }) {
422
456
  const slots = sheetVariants();
423
457
  if (inline)
424
458
  return _jsx("p", { className: slots.description({ className }), ...props });
425
- return _jsx(Drawer.Description, { className: slots.description({ className }), ...props });
459
+ return (_jsx(Drawer.Content, { render: _jsx(Drawer.Description, { className: slots.description({ className }), ...props }) }));
426
460
  }
427
461
  SheetDescription.displayName = 'Sheet.Description';
428
462
  /**
@@ -430,26 +464,49 @@ SheetDescription.displayName = 'Sheet.Description';
430
464
  * the scrollbar is a thin drawn thumb that shows while the content is moving.
431
465
  */
432
466
  function SheetBody({ className, children, ...props }) {
467
+ const { inline } = useSheet('Sheet.Body');
433
468
  const slots = sheetVariants();
434
- return (_jsxs(ScrollArea.Root, { className: slots.body(), children: [_jsx(ScrollArea.Viewport, { className: slots.bodyViewport(), children: _jsx(ScrollArea.Content, { className: slots.bodyContent({ className }), ...props, children: children }) }), _jsx(ScrollArea.Scrollbar, { orientation: "vertical", className: slots.scrollbar(), children: _jsx(ScrollArea.Thumb, { className: slots.thumb() }) })] }));
469
+ const content = (_jsx(ScrollArea.Content, { className: slots.bodyContent({ className }), ...props, children: children }));
470
+ return (_jsxs(ScrollArea.Root, { className: slots.body(), children: [_jsx(ScrollArea.Viewport, { className: slots.bodyViewport(), children: inline ? content : _jsx(Drawer.Content, { render: content }) }), _jsx(ScrollArea.Scrollbar, { orientation: "vertical", className: slots.scrollbar(), children: _jsx(ScrollArea.Thumb, { className: slots.thumb() }) })] }));
435
471
  }
436
472
  SheetBody.displayName = 'Sheet.Body';
437
473
  /** The actions, stacked full width at the bottom of the sheet. */
438
474
  function SheetFooter({ className, ...props }) {
475
+ const { inline } = useSheet('Sheet.Footer');
439
476
  const slots = sheetVariants();
440
- return _jsx("div", { className: slots.footer({ className }), ...props });
477
+ const footer = _jsx("div", { className: slots.footer({ className }), ...props });
478
+ return inline ? footer : _jsx(Drawer.Content, { render: footer });
441
479
  }
442
480
  SheetFooter.displayName = 'Sheet.Footer';
443
- /** Closes the sheet. Works even when `dismissible` is off. */
481
+ const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
482
+ /**
483
+ * Closes the sheet. Works even when `dismissible` is off. Written bare, it is
484
+ * the round close button, and the sheet stops drawing its own in the corner.
485
+ */
444
486
  function SheetClose({ render, icon = false, children, ...props }) {
487
+ const sheet = useSheet('Sheet.Close');
488
+ const bare = icon || (render === undefined && children === undefined);
489
+ const { claimClose } = sheet;
490
+ useIsomorphicLayoutEffect(() => (bare ? claimClose() : undefined), [bare, claimClose]);
491
+ if (bare)
492
+ return _jsx(CloseButton, { ...props });
493
+ if (sheet.inline) {
494
+ return (_jsx("button", { type: "button", "data-slot": "sheet-close", ...props, children: children }));
495
+ }
496
+ return (_jsx(Drawer.Close, { "data-slot": "sheet-close", render: render, ...props, children: children }));
497
+ }
498
+ SheetClose.displayName = 'Sheet.Close';
499
+ /** The round button with a cross in it. */
500
+ function CloseButton({ className, 'aria-label': label = 'Close', ...props }) {
445
501
  const { inline } = useSheet('Sheet.Close');
446
- const content = icon ? (_jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": true, className: "size-4", fill: "none", stroke: "currentColor", strokeWidth: 2.4, strokeLinecap: "round", children: _jsx("path", { d: "M6 6l12 12M18 6L6 18" }) })) : (children);
502
+ const slots = sheetVariants();
503
+ const glyph = (_jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": true, className: "size-4", fill: "none", stroke: "currentColor", strokeWidth: 2.4, strokeLinecap: "round", children: _jsx("path", { d: "M6 6l12 12M18 6L6 18" }) }));
504
+ const shared = { 'data-slot': 'sheet-close', 'aria-label': label, className: slots.close({ className }), ...props };
447
505
  if (inline) {
448
- return (_jsx("button", { type: "button", ...props, children: content }));
506
+ return (_jsx("button", { type: "button", ...shared, children: glyph }));
449
507
  }
450
- return (_jsx(Drawer.Close, { render: render, ...props, children: content }));
508
+ return _jsx(Drawer.Close, { ...shared, children: glyph });
451
509
  }
452
- SheetClose.displayName = 'Sheet.Close';
453
510
  const SlidesContext = createContext(null);
454
511
  /** The rounded frame of a slide: the slide itself in a deck, inside its row in a column. */
455
512
  const frameOf = (slide) => slide.querySelector('[data-slide-frame]') ?? slide;
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Slider — a number chosen by dragging along a track, with its name and its
3
+ * value written inside the track.
4
+ *
5
+ * ```tsx
6
+ * <Slider label="Opacity" defaultValue={64} format={{ style: 'unit', unit: 'percent' }} />
7
+ *
8
+ * <Slider variant="line" label="Zoom" min={10} max={400} defaultValue={100}>
9
+ * <Slider.Step by={-10} aria-label="Zoom out"><MinusIcon /></Slider.Step>
10
+ * <Slider.Step by={10} aria-label="Zoom in"><PlusIcon /></Slider.Step>
11
+ * </Slider>
12
+ * ```
13
+ *
14
+ * ## The track is the label
15
+ *
16
+ * In a panel of settings, a column of sliders each with a label above and a
17
+ * number beside is three things to line up per row. The `fill` slider puts
18
+ * the name at the start of the track and the value at its end, so a row is
19
+ * one bar the height of a button, and a stack of them reads as a table.
20
+ *
21
+ * ## It moves the way the hand does
22
+ *
23
+ * A press away from the handle glides the fill there on a curve with a
24
+ * little overshoot; a drag follows the pointer exactly, with nothing
25
+ * easing behind it. Pulled past either end, the track stretches a few
26
+ * pixels towards the pointer and springs back on release, so the end of
27
+ * the range is felt rather than hit.
28
+ *
29
+ * ## Two weights
30
+ *
31
+ * `fill` is the slider for a settings panel. `line` is a thin track with a
32
+ * round knob, for a toolbar, where a zoom or a scrubber sits between two
33
+ * buttons: `Slider.Step` is that button, and it moves the value by `by`.
34
+ *
35
+ * `origin="center"` fills from the middle out, for a value that can go
36
+ * either way from nothing: exposure, a balance, a pitch shift.
37
+ */
38
+ import { Slider as BaseSlider } from '@base-ui/react/slider';
39
+ import { type ComponentProps, type CSSProperties, type ReactNode } from 'react';
40
+ export type SliderVariant = 'fill' | 'line';
41
+ export type SliderSize = 'sm' | 'md' | 'lg';
42
+ export type SliderColor = 'neutral' | 'primary' | 'info' | 'success' | 'warning' | 'destructive';
43
+ declare const sliderVariants: import("tailwind-variants").TVReturnType<{
44
+ variant: {
45
+ fill: {
46
+ control: string;
47
+ track: string;
48
+ fill: string;
49
+ tick: string[];
50
+ handle: string[];
51
+ text: string;
52
+ };
53
+ line: {
54
+ control: string;
55
+ track: string;
56
+ fill: string;
57
+ tick: string;
58
+ handle: string[];
59
+ };
60
+ };
61
+ color: {
62
+ neutral: {
63
+ root: string;
64
+ };
65
+ primary: {
66
+ root: string;
67
+ };
68
+ info: {
69
+ root: string;
70
+ };
71
+ success: {
72
+ root: string;
73
+ };
74
+ warning: {
75
+ root: string;
76
+ };
77
+ destructive: {
78
+ root: string;
79
+ };
80
+ };
81
+ size: {
82
+ sm: {
83
+ root: string;
84
+ };
85
+ md: {
86
+ root: string;
87
+ };
88
+ lg: {
89
+ root: string;
90
+ };
91
+ };
92
+ }, {
93
+ root: string;
94
+ control: string[];
95
+ track: string;
96
+ fill: string[];
97
+ tick: string;
98
+ center: string;
99
+ handle: string[];
100
+ thumb: string;
101
+ text: string;
102
+ label: string;
103
+ value: string;
104
+ step: string[];
105
+ }, undefined, {
106
+ variant: {
107
+ fill: {
108
+ control: string;
109
+ track: string;
110
+ fill: string;
111
+ tick: string[];
112
+ handle: string[];
113
+ text: string;
114
+ };
115
+ line: {
116
+ control: string;
117
+ track: string;
118
+ fill: string;
119
+ tick: string;
120
+ handle: string[];
121
+ };
122
+ };
123
+ color: {
124
+ neutral: {
125
+ root: string;
126
+ };
127
+ primary: {
128
+ root: string;
129
+ };
130
+ info: {
131
+ root: string;
132
+ };
133
+ success: {
134
+ root: string;
135
+ };
136
+ warning: {
137
+ root: string;
138
+ };
139
+ destructive: {
140
+ root: string;
141
+ };
142
+ };
143
+ size: {
144
+ sm: {
145
+ root: string;
146
+ };
147
+ md: {
148
+ root: string;
149
+ };
150
+ lg: {
151
+ root: string;
152
+ };
153
+ };
154
+ }, {
155
+ root: string;
156
+ control: string[];
157
+ track: string;
158
+ fill: string[];
159
+ tick: string;
160
+ center: string;
161
+ handle: string[];
162
+ thumb: string;
163
+ text: string;
164
+ label: string;
165
+ value: string;
166
+ step: string[];
167
+ }, import("tailwind-variants").TVReturnType<{
168
+ variant: {
169
+ fill: {
170
+ control: string;
171
+ track: string;
172
+ fill: string;
173
+ tick: string[];
174
+ handle: string[];
175
+ text: string;
176
+ };
177
+ line: {
178
+ control: string;
179
+ track: string;
180
+ fill: string;
181
+ tick: string;
182
+ handle: string[];
183
+ };
184
+ };
185
+ color: {
186
+ neutral: {
187
+ root: string;
188
+ };
189
+ primary: {
190
+ root: string;
191
+ };
192
+ info: {
193
+ root: string;
194
+ };
195
+ success: {
196
+ root: string;
197
+ };
198
+ warning: {
199
+ root: string;
200
+ };
201
+ destructive: {
202
+ root: string;
203
+ };
204
+ };
205
+ size: {
206
+ sm: {
207
+ root: string;
208
+ };
209
+ md: {
210
+ root: string;
211
+ };
212
+ lg: {
213
+ root: string;
214
+ };
215
+ };
216
+ }, {
217
+ root: string;
218
+ control: string[];
219
+ track: string;
220
+ fill: string[];
221
+ tick: string;
222
+ center: string;
223
+ handle: string[];
224
+ thumb: string;
225
+ text: string;
226
+ label: string;
227
+ value: string;
228
+ step: string[];
229
+ }, undefined, unknown, unknown, undefined>>;
230
+ type SliderValue = number | number[];
231
+ export interface SliderProps<Value extends SliderValue = number> extends Omit<BaseSlider.Root.Props<Value>, 'className' | 'style' | 'format' | 'orientation' | 'children' | 'render' | 'onValueChange' | 'value' | 'defaultValue'> {
232
+ /** The value, when the caller holds it. Two numbers make a range with two handles. */
233
+ value?: Value;
234
+ /** The value to start at, when the caller does not hold it. Default `min`. */
235
+ defaultValue?: Value;
236
+ /** Called with the new value while it changes, by a drag, a press, a key or a `Slider.Step`. */
237
+ onValueChange?: (value: Value) => void;
238
+ /** `fill` writes the name and value inside a bar; `line` is a thin track with a knob. Default `fill`. */
239
+ variant?: SliderVariant;
240
+ /**
241
+ * What it sets. Written inside a `fill` slider, and what a screen reader
242
+ * calls either kind.
243
+ */
244
+ label?: string;
245
+ /**
246
+ * How the value is written: number-format options such as
247
+ * `{ style: 'unit', unit: 'percent' }`, or a function from the number to
248
+ * a string for anything else, such as a multiplier written `1.40×`. Also
249
+ * what a screen reader reads.
250
+ */
251
+ format?: Intl.NumberFormatOptions | ((value: number) => string);
252
+ /** Where the fill grows from. `center` fills outwards from the middle of the range. Default `start`. */
253
+ origin?: 'start' | 'center';
254
+ /** The colour of the fill and the handle. `neutral` by default. */
255
+ color?: SliderColor;
256
+ /** Height of a `fill` slider. Default `md`. */
257
+ size?: SliderSize;
258
+ /** Show the value at the end of a `fill` slider. Default true. */
259
+ showValue?: boolean;
260
+ /** Mark every step with a notch. Default: on when there are 12 steps or fewer. */
261
+ ticks?: boolean;
262
+ /** `Slider.Step` buttons. One with a negative `by` stands before the track, a positive one after it. */
263
+ children?: ReactNode;
264
+ className?: string;
265
+ style?: CSSProperties;
266
+ }
267
+ /**
268
+ * A slider. `defaultValue`, or `value` with `onValueChange`; `min`, `max`
269
+ * and `step` as for any range.
270
+ */
271
+ declare function SliderRoot<Value extends SliderValue = number>({ value: valueProp, defaultValue, onValueChange, variant, label, format, origin, color, size, showValue, ticks, min, max, step, disabled, locale, children, className, style, ...props }: SliderProps<Value>): import("react").JSX.Element;
272
+ declare namespace SliderRoot {
273
+ var displayName: string;
274
+ }
275
+ export interface SliderStepProps extends Omit<ComponentProps<'button'>, 'children'> {
276
+ /** How much one press moves the value. Negative steps down and sits before the track. */
277
+ by: number;
278
+ /** Usually an icon. Give the button an `aria-label` then. */
279
+ children?: ReactNode;
280
+ }
281
+ /**
282
+ * A button beside the track that moves the value by `by`: zoom out and zoom
283
+ * in on either side of a zoom slider. It is disabled at the end it moves
284
+ * towards.
285
+ */
286
+ declare function SliderStep({ by, className, disabled, onClick, ...props }: SliderStepProps): import("react").JSX.Element;
287
+ declare namespace SliderStep {
288
+ var displayName: string;
289
+ }
290
+ export declare const Slider: typeof SliderRoot & {
291
+ Step: typeof SliderStep;
292
+ };
293
+ export { SliderStep, sliderVariants };