@adecore/ui 0.20.0 → 0.21.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/Pill.d.ts +2 -1
- package/dist/Pill.js +9 -4
- package/dist/Slider.d.ts +15 -0
- package/dist/Slider.js +16 -0
- package/dist/Stepper.js +1 -1
- package/dist/Toasts.js +1 -1
- package/dist/Tooltip.d.ts +7 -3
- package/dist/Tooltip.js +2 -2
- package/dist/file-tree/use-slots.js +3 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/step-places.d.ts +1 -0
- package/dist/step-places.js +2 -0
- package/package.json +3 -3
- package/src/Pill.tsx +11 -4
- package/src/Slider.tsx +68 -0
- package/src/Stepper.tsx +1 -2
- package/src/Toasts.tsx +1 -1
- package/src/Tooltip.tsx +11 -7
- package/src/file-tree/use-slots.ts +3 -1
- package/src/index.ts +1 -0
- package/src/step-places.ts +2 -0
package/dist/Pill.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface PillProps {
|
|
|
16
16
|
children: ReactNode;
|
|
17
17
|
tone?: keyof typeof TONES;
|
|
18
18
|
shape?: keyof typeof SHAPES;
|
|
19
|
+
size?: 'md' | 'sm';
|
|
19
20
|
mono?: boolean;
|
|
20
21
|
onClick?: () => void;
|
|
21
22
|
pressed?: boolean;
|
|
@@ -23,5 +24,5 @@ export interface PillProps {
|
|
|
23
24
|
className?: string;
|
|
24
25
|
ref?: Ref<HTMLElement>;
|
|
25
26
|
}
|
|
26
|
-
export declare function Pill({ icon, children, tone, shape, mono, onClick, pressed, disabled, className, ref }: PillProps): import("react").JSX.Element;
|
|
27
|
+
export declare function Pill({ icon, children, tone, shape, size, mono, onClick, pressed, disabled, className, ref }: PillProps): import("react").JSX.Element;
|
|
27
28
|
export {};
|
package/dist/Pill.js
CHANGED
|
@@ -16,13 +16,18 @@ const TONES = {
|
|
|
16
16
|
};
|
|
17
17
|
/* The corners of a button, so a pill reads as a small label of the same family and not as a badge. A tag, at the end of a line of prose, is tighter and heavier. */
|
|
18
18
|
const SHAPES = {
|
|
19
|
-
pill: 'rounded-md
|
|
20
|
-
tag: 'rounded-md
|
|
19
|
+
pill: 'rounded-md',
|
|
20
|
+
tag: 'rounded-md font-medium'
|
|
21
|
+
};
|
|
22
|
+
/* The text stays at the theme's smallest size, so `sm` gives up padding: 20px tall instead of 24. */
|
|
23
|
+
const PADDING = {
|
|
24
|
+
pill: { md: 'px-2 py-0.5', sm: 'px-1.5' },
|
|
25
|
+
tag: { md: 'px-1.5 py-0.5', sm: 'px-1' }
|
|
21
26
|
};
|
|
22
27
|
/* The small rounded label in a header, a sidebar row or a settings line: a count, a branch, a status. */
|
|
23
|
-
export function Pill({ icon, children, tone = 'muted', shape = 'pill', mono = false, onClick, pressed, disabled, className, ref }) {
|
|
28
|
+
export function Pill({ icon, children, tone = 'muted', shape = 'pill', size = 'md', mono = false, onClick, pressed, disabled, className, ref }) {
|
|
24
29
|
// Never broken inside itself: in a row that wraps, a pill that does not fit moves to the next line whole.
|
|
25
|
-
const shared = clsx('inline-flex shrink-0 items-center gap-1 whitespace-nowrap text-2xs', SHAPES[shape], TONES[tone], mono && 'font-mono', className);
|
|
30
|
+
const shared = clsx('inline-flex shrink-0 items-center gap-1 whitespace-nowrap text-2xs', SHAPES[shape], PADDING[shape][size], TONES[tone], mono && 'font-mono', className);
|
|
26
31
|
if (!onClick) {
|
|
27
32
|
return (_jsxs("span", { ref: ref, className: shared, children: [icon, children] }));
|
|
28
33
|
}
|
package/dist/Slider.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Ref } from 'react';
|
|
2
|
+
export interface SliderProps {
|
|
3
|
+
value: number;
|
|
4
|
+
onValueChange(value: number): void;
|
|
5
|
+
min: number;
|
|
6
|
+
max: number;
|
|
7
|
+
step?: number;
|
|
8
|
+
decimals?: number;
|
|
9
|
+
unit?: string;
|
|
10
|
+
label: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
className?: string;
|
|
13
|
+
ref?: Ref<HTMLDivElement>;
|
|
14
|
+
}
|
|
15
|
+
export declare function Slider({ value, onValueChange, min, max, step, decimals, unit, label, disabled, className, ref }: SliderProps): import("react").JSX.Element;
|
package/dist/Slider.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { Slider as BaseSlider } from '@base-ui-components/react/slider';
|
|
5
|
+
import { formatFixed } from './format/number.js';
|
|
6
|
+
import { placesOf } from './step-places.js';
|
|
7
|
+
/* A number picked along a range: the label and the value on one line, the track under them. */
|
|
8
|
+
export function Slider({ value, onValueChange, min, max, step = 1, decimals = placesOf(step), unit, label, disabled, className, ref }) {
|
|
9
|
+
const labelId = useId();
|
|
10
|
+
const reading = formatFixed(value, decimals);
|
|
11
|
+
// Base UI adds steps in floating point, so 0.2 plus 0.1 would arrive as 0.30000000000000004.
|
|
12
|
+
const places = Math.max(placesOf(step), placesOf(min));
|
|
13
|
+
// Base UI's Page Up jumps 10 whatever the range; a tenth of the range suits 0 to 1 as well as 0 to 1000.
|
|
14
|
+
const largeStep = Math.max(step, Math.round((max - min) / 10 / step) * step);
|
|
15
|
+
return (_jsxs(BaseSlider.Root, { ref: ref, value: value, onValueChange: (next) => onValueChange(Number(next.toFixed(places))), min: min, max: max, step: step, largeStep: largeStep, disabled: disabled, "aria-labelledby": labelId, className: clsx('flex flex-col gap-1 data-disabled:opacity-50', className), children: [_jsxs("div", { className: "flex items-baseline justify-between gap-3 text-xs", children: [_jsx("span", { id: labelId, className: "min-w-0 truncate text-text", children: label }), _jsxs("span", { "aria-hidden": true, className: "shrink-0 text-text-muted tabular-nums", children: [reading, unit] })] }), _jsx(BaseSlider.Control, { className: "flex h-5 touch-none items-center px-2 select-none", children: _jsxs(BaseSlider.Track, { className: "relative h-1 w-full rounded-full bg-border-strong", children: [_jsx(BaseSlider.Indicator, { className: "rounded-full bg-accent" }), _jsx(BaseSlider.Thumb, { getAriaValueText: () => `${reading}${unit ?? ''}`, className: "block size-4 rounded-full bg-surface-raised shadow-raised outline-offset-2 outline-accent has-focus-visible:outline-2" })] }) })] }));
|
|
16
|
+
}
|
package/dist/Stepper.js
CHANGED
|
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
5
5
|
import { ButtonGroup } from './ButtonGroup.js';
|
|
6
6
|
import { formatFixed } from './format/number.js';
|
|
7
7
|
import { IconButton } from './IconButton.js';
|
|
8
|
-
|
|
8
|
+
import { placesOf } from './step-places.js';
|
|
9
9
|
/* A number between minus and plus, in one sunken group so the three read as a single control. */
|
|
10
10
|
export function Stepper({ value, onValueChange, min, max, step, decimals = placesOf(step), unit, label, className, ref }) {
|
|
11
11
|
const { t } = useTranslation('ui');
|
package/dist/Toasts.js
CHANGED
|
@@ -42,7 +42,7 @@ function ToastCard({ toast, footer, onDismiss }) {
|
|
|
42
42
|
const actions = [...(toast.action === undefined ? [] : [toast.action]), ...(toast.actions ?? [])];
|
|
43
43
|
// An offer that runs out shows how long it has left instead.
|
|
44
44
|
const countdown = actions.length > 0 ? toast.deadline : undefined;
|
|
45
|
-
return (_jsxs(Surface, { className: "group flex items-start gap-3 rounded-[10px] py-2.5 pr-2 pl-3", children: [_jsx("span", { className: clsx(TITLE_LINE, 'w-4 justify-center'), children: toast.kind === 'progress' ? (_jsx(Spinner, { size: 16, className: TONE.progress })) : (_jsx(Icon, { icon: ICON[toast.kind], size: 16, className: TONE[toast.kind] })) }), _jsxs("div", { className: "flex min-w-0 grow flex-col gap-1", children: [_jsx("span", { className: "text-sm text-text", children: toast.title }), toast.description !== undefined && toast.description !== '' && (_jsx("span", { className: "text-xs break-words text-pretty text-text-muted", children: toast.description })), actions.length > 1 && (_jsx("span", { className: "mt-1 flex flex-wrap items-center gap-x-4 gap-y-1", children: actions.map((action) => (_jsx(ActionButton, { action: action }, action.label))) })), footer?.(toast)] }), actions.length === 1 && (_jsx("span", { className: TITLE_LINE, children: _jsx(ActionButton, { action: actions[0] }) })), countdown !== undefined ? (_jsx(TimerRing, { deadline: countdown, onDismiss: onDismiss }, countdown.start)) : (_jsx(IconButton, { icon: X, size: "xs", label: t('action.dismiss'), className: clsx('-my-px', leaves && 'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100'), onClick: onDismiss }))] }));
|
|
45
|
+
return (_jsxs(Surface, { className: "group flex items-start gap-3 rounded-[10px] py-2.5 pr-2 pl-3", children: [_jsx("span", { className: clsx(TITLE_LINE, 'w-4 justify-center'), children: toast.kind === 'progress' ? (_jsx(Spinner, { size: 16, className: TONE.progress })) : (_jsx(Icon, { icon: ICON[toast.kind], size: 16, className: TONE[toast.kind] })) }), _jsxs("div", { className: "flex min-w-0 grow flex-col gap-1", children: [_jsx("span", { className: "text-sm break-words text-text", children: toast.title }), toast.description !== undefined && toast.description !== '' && (_jsx("span", { className: "text-xs break-words text-pretty text-text-muted", children: toast.description })), actions.length > 1 && (_jsx("span", { className: "mt-1 flex flex-wrap items-center gap-x-4 gap-y-1", children: actions.map((action) => (_jsx(ActionButton, { action: action }, action.label))) })), footer?.(toast)] }), actions.length === 1 && (_jsx("span", { className: TITLE_LINE, children: _jsx(ActionButton, { action: actions[0] }) })), countdown !== undefined ? (_jsx(TimerRing, { deadline: countdown, onDismiss: onDismiss }, countdown.start)) : (_jsx(IconButton, { icon: X, size: "xs", label: t('action.dismiss'), className: clsx('-my-px', leaves && 'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100'), onClick: onDismiss }))] }));
|
|
46
46
|
}
|
|
47
47
|
/*
|
|
48
48
|
* Bottom right, over everything: what an action is doing while it runs and how it went when it is
|
package/dist/Tooltip.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type ReactElement, type ReactNode } from 'react';
|
|
1
|
+
import { type ComponentProps, type ReactElement, type ReactNode } from 'react';
|
|
2
|
+
import { Tooltip as BaseTooltip } from '@base-ui-components/react/tooltip';
|
|
2
3
|
import type { Shortcut } from './shortcut.ts';
|
|
3
4
|
export type TooltipSide = 'top' | 'bottom' | 'left' | 'right';
|
|
4
5
|
export declare function TooltipProvider({ children }: {
|
|
@@ -6,11 +7,14 @@ export declare function TooltipProvider({ children }: {
|
|
|
6
7
|
}): import("react").JSX.Element;
|
|
7
8
|
export interface TooltipProps {
|
|
8
9
|
label: ReactNode;
|
|
10
|
+
open?: boolean;
|
|
11
|
+
onOpenChange?(open: boolean): void;
|
|
12
|
+
anchor?: ComponentProps<typeof BaseTooltip.Positioner>['anchor'];
|
|
9
13
|
kbd?: Shortcut | string;
|
|
10
14
|
side?: TooltipSide;
|
|
11
15
|
sideOffset?: number;
|
|
12
16
|
name?: boolean;
|
|
13
17
|
disabled?: boolean;
|
|
14
|
-
children
|
|
18
|
+
children?: ReactElement<Record<string, unknown>>;
|
|
15
19
|
}
|
|
16
|
-
export declare function Tooltip({ label, kbd, side, sideOffset, name, disabled, children }: TooltipProps): import("react").JSX.Element;
|
|
20
|
+
export declare function Tooltip({ label, open, onOpenChange, anchor, kbd, side, sideOffset, name, disabled, children }: TooltipProps): import("react").JSX.Element;
|
package/dist/Tooltip.js
CHANGED
|
@@ -8,7 +8,7 @@ export function TooltipProvider({ children }) {
|
|
|
8
8
|
return (_jsx(BaseTooltip.Provider, { delay: 150, closeDelay: 0, children: children }));
|
|
9
9
|
}
|
|
10
10
|
/* A hint on hover and on keyboard focus. Every hint in an app is one of these, never a `title`. */
|
|
11
|
-
export function Tooltip({ label, kbd, side = 'top', sideOffset = 6, name = false, disabled = false, children }) {
|
|
11
|
+
export function Tooltip({ label, open, onOpenChange, anchor, kbd, side = 'top', sideOffset = 6, name = false, disabled = false, children }) {
|
|
12
12
|
// Held Cmd (Ctrl off macOS) prints this shortcut under the trigger (`ShortcutHints`).
|
|
13
13
|
const hintRef = useCallback((element) => {
|
|
14
14
|
if (element === null || kbd === undefined || typeof kbd === 'string') {
|
|
@@ -16,6 +16,6 @@ export function Tooltip({ label, kbd, side = 'top', sideOffset = 6, name = false
|
|
|
16
16
|
}
|
|
17
17
|
return registerShortcutHint(element, kbd);
|
|
18
18
|
}, [kbd]);
|
|
19
|
-
return (_jsxs(BaseTooltip.Root, { disabled: disabled, children: [_jsx(BaseTooltip.Trigger, { ref: hintRef, render: children, "aria-label": name && typeof label === 'string' ? label : undefined }), _jsx(BaseTooltip.Portal, { children: _jsx(BaseTooltip.Positioner, { side: side, sideOffset: sideOffset, className: "tooltip-positioner", children: _jsx(BaseTooltip.Popup, { className: "tooltip-popup", children: _jsxs(BaseTooltip.Viewport, { className: "max-w-72 px-[9px] py-[5px] text-pretty", children: [_jsx("span", { className: "wrap-anywhere", children: label }), kbd !== undefined &&
|
|
19
|
+
return (_jsxs(BaseTooltip.Root, { disabled: disabled, open: open, onOpenChange: onOpenChange, disableHoverablePopup: children === undefined, children: [children && _jsx(BaseTooltip.Trigger, { ref: hintRef, render: children, "aria-label": name && typeof label === 'string' ? label : undefined }), _jsx(BaseTooltip.Portal, { children: _jsx(BaseTooltip.Positioner, { anchor: anchor, side: side, sideOffset: sideOffset, className: "tooltip-positioner", children: _jsx(BaseTooltip.Popup, { role: "tooltip", className: "tooltip-popup", children: _jsxs(BaseTooltip.Viewport, { className: "max-w-72 px-[9px] py-[5px] text-pretty", children: [_jsx("span", { className: "wrap-anywhere", children: label }), kbd !== undefined &&
|
|
20
20
|
(typeof kbd === 'string' ? (_jsx(Kbd, { variant: "inline", className: "ml-2", children: kbd })) : (_jsx(Kbd, { shortcut: kbd, variant: "inline", className: "ml-2" })))] }) }) }) })] }));
|
|
21
21
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useId, useState } from 'react';
|
|
2
|
+
import { flushSync } from 'react-dom';
|
|
2
3
|
import { pathOfRow, visibleRows } from './helpers.js';
|
|
3
4
|
import { FILE_TREE_CSS } from './style.js';
|
|
4
5
|
// beta.6 owns button rows. Sibling slots keep real controls outside those buttons and in the app's stylesheet.
|
|
@@ -134,7 +135,8 @@ export function useFileTreeSlots(model, frame, label) {
|
|
|
134
135
|
.join('|');
|
|
135
136
|
if (snapshot !== key) {
|
|
136
137
|
snapshot = key;
|
|
137
|
-
|
|
138
|
+
// Recycled rows must paint with their new portal contents in the same frame.
|
|
139
|
+
flushSync(() => setSlots(next));
|
|
138
140
|
}
|
|
139
141
|
position();
|
|
140
142
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ export { Select, type SelectGroup, type SelectItem, type SelectProps } from './S
|
|
|
46
46
|
export { Separator, type SeparatorProps } from './Separator.tsx';
|
|
47
47
|
export { ShortcutHints } from './ShortcutHints.tsx';
|
|
48
48
|
export { Skeleton, type SkeletonProps } from './Skeleton.tsx';
|
|
49
|
+
export { Slider, type SliderProps } from './Slider.tsx';
|
|
49
50
|
export { SlidingColumn, type SlidingColumnProps } from './SlidingColumn.tsx';
|
|
50
51
|
export { Spinner, type SpinnerProps } from './Spinner.tsx';
|
|
51
52
|
export { Stepper, type StepperProps } from './Stepper.tsx';
|
package/dist/index.js
CHANGED
|
@@ -45,6 +45,7 @@ export { Select } from './Select.js';
|
|
|
45
45
|
export { Separator } from './Separator.js';
|
|
46
46
|
export { ShortcutHints } from './ShortcutHints.js';
|
|
47
47
|
export { Skeleton } from './Skeleton.js';
|
|
48
|
+
export { Slider } from './Slider.js';
|
|
48
49
|
export { SlidingColumn } from './SlidingColumn.js';
|
|
49
50
|
export { Spinner } from './Spinner.js';
|
|
50
51
|
export { Stepper } from './Stepper.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const placesOf: (step: number) => number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "React components, a theme, formatters and a settings dialog for desktop-grade apps on React 19, Base UI and Tailwind 4.",
|
|
5
5
|
"license": "FSL-1.1-MIT",
|
|
6
6
|
"author": "Bas Milius",
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
"react-i18next": "^17.0.15"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
|
+
"@happy-dom/global-registrator": "^20.14.5",
|
|
81
82
|
"@types/bun": "^1.4.2",
|
|
82
83
|
"@types/opentype.js": "^1.3.10",
|
|
83
84
|
"@types/react": "^19.3.0",
|
|
@@ -88,7 +89,6 @@
|
|
|
88
89
|
"react": "^19.3.0",
|
|
89
90
|
"react-dom": "^19.3.0",
|
|
90
91
|
"react-i18next": "^17.0.15",
|
|
91
|
-
"typescript": "^7.0.2"
|
|
92
|
-
"@happy-dom/global-registrator": "^20.14.5"
|
|
92
|
+
"typescript": "^7.0.2"
|
|
93
93
|
}
|
|
94
94
|
}
|
package/src/Pill.tsx
CHANGED
|
@@ -18,8 +18,14 @@ const TONES = {
|
|
|
18
18
|
|
|
19
19
|
/* The corners of a button, so a pill reads as a small label of the same family and not as a badge. A tag, at the end of a line of prose, is tighter and heavier. */
|
|
20
20
|
const SHAPES = {
|
|
21
|
-
pill: 'rounded-md
|
|
22
|
-
tag: 'rounded-md
|
|
21
|
+
pill: 'rounded-md',
|
|
22
|
+
tag: 'rounded-md font-medium'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/* The text stays at the theme's smallest size, so `sm` gives up padding: 20px tall instead of 24. */
|
|
26
|
+
const PADDING = {
|
|
27
|
+
pill: { md: 'px-2 py-0.5', sm: 'px-1.5' },
|
|
28
|
+
tag: { md: 'px-1.5 py-0.5', sm: 'px-1' }
|
|
23
29
|
};
|
|
24
30
|
|
|
25
31
|
export interface PillProps {
|
|
@@ -27,6 +33,7 @@ export interface PillProps {
|
|
|
27
33
|
children: ReactNode;
|
|
28
34
|
tone?: keyof typeof TONES;
|
|
29
35
|
shape?: keyof typeof SHAPES;
|
|
36
|
+
size?: 'md' | 'sm';
|
|
30
37
|
/* A branch name or a count reads better in the monospace face. */
|
|
31
38
|
mono?: boolean;
|
|
32
39
|
/* With a handler the pill is a button; without one it is a label. */
|
|
@@ -39,9 +46,9 @@ export interface PillProps {
|
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
/* The small rounded label in a header, a sidebar row or a settings line: a count, a branch, a status. */
|
|
42
|
-
export function Pill({ icon, children, tone = 'muted', shape = 'pill', mono = false, onClick, pressed, disabled, className, ref }: PillProps) {
|
|
49
|
+
export function Pill({ icon, children, tone = 'muted', shape = 'pill', size = 'md', mono = false, onClick, pressed, disabled, className, ref }: PillProps) {
|
|
43
50
|
// Never broken inside itself: in a row that wraps, a pill that does not fit moves to the next line whole.
|
|
44
|
-
const shared = clsx('inline-flex shrink-0 items-center gap-1 whitespace-nowrap text-2xs', SHAPES[shape], TONES[tone], mono && 'font-mono', className);
|
|
51
|
+
const shared = clsx('inline-flex shrink-0 items-center gap-1 whitespace-nowrap text-2xs', SHAPES[shape], PADDING[shape][size], TONES[tone], mono && 'font-mono', className);
|
|
45
52
|
if (!onClick) {
|
|
46
53
|
return (
|
|
47
54
|
<span ref={ref} className={shared}>
|
package/src/Slider.tsx
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useId, type Ref } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { Slider as BaseSlider } from '@base-ui-components/react/slider';
|
|
4
|
+
import { formatFixed } from './format/number.ts';
|
|
5
|
+
import { placesOf } from './step-places.ts';
|
|
6
|
+
|
|
7
|
+
export interface SliderProps {
|
|
8
|
+
value: number;
|
|
9
|
+
onValueChange(value: number): void;
|
|
10
|
+
min: number;
|
|
11
|
+
max: number;
|
|
12
|
+
step?: number;
|
|
13
|
+
/* Places after the separator, always shown. Defaults to the places `step` is written with. */
|
|
14
|
+
decimals?: number;
|
|
15
|
+
/* Printed right after the number, such as `px` or `%`. */
|
|
16
|
+
unit?: string;
|
|
17
|
+
/* Shown above the track and read as the slider's name. */
|
|
18
|
+
label: string;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
className?: string;
|
|
21
|
+
ref?: Ref<HTMLDivElement>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* A number picked along a range: the label and the value on one line, the track under them. */
|
|
25
|
+
export function Slider({ value, onValueChange, min, max, step = 1, decimals = placesOf(step), unit, label, disabled, className, ref }: SliderProps) {
|
|
26
|
+
const labelId = useId();
|
|
27
|
+
const reading = formatFixed(value, decimals);
|
|
28
|
+
// Base UI adds steps in floating point, so 0.2 plus 0.1 would arrive as 0.30000000000000004.
|
|
29
|
+
const places = Math.max(placesOf(step), placesOf(min));
|
|
30
|
+
// Base UI's Page Up jumps 10 whatever the range; a tenth of the range suits 0 to 1 as well as 0 to 1000.
|
|
31
|
+
const largeStep = Math.max(step, Math.round((max - min) / 10 / step) * step);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<BaseSlider.Root
|
|
35
|
+
ref={ref}
|
|
36
|
+
value={value}
|
|
37
|
+
onValueChange={(next) => onValueChange(Number(next.toFixed(places)))}
|
|
38
|
+
min={min}
|
|
39
|
+
max={max}
|
|
40
|
+
step={step}
|
|
41
|
+
largeStep={largeStep}
|
|
42
|
+
disabled={disabled}
|
|
43
|
+
aria-labelledby={labelId}
|
|
44
|
+
className={clsx('flex flex-col gap-1 data-disabled:opacity-50', className)}
|
|
45
|
+
>
|
|
46
|
+
<div className="flex items-baseline justify-between gap-3 text-xs">
|
|
47
|
+
<span id={labelId} className="min-w-0 truncate text-text">
|
|
48
|
+
{label}
|
|
49
|
+
</span>
|
|
50
|
+
{/* The thumb already says the value through `aria-valuetext`. */}
|
|
51
|
+
<span aria-hidden className="shrink-0 text-text-muted tabular-nums">
|
|
52
|
+
{reading}
|
|
53
|
+
{unit}
|
|
54
|
+
</span>
|
|
55
|
+
</div>
|
|
56
|
+
{/* The padding gives the thumb room at either end; Base UI leaves it out of the pointer's position. */}
|
|
57
|
+
<BaseSlider.Control className="flex h-5 touch-none items-center px-2 select-none">
|
|
58
|
+
<BaseSlider.Track className="relative h-1 w-full rounded-full bg-border-strong">
|
|
59
|
+
<BaseSlider.Indicator className="rounded-full bg-accent" />
|
|
60
|
+
<BaseSlider.Thumb
|
|
61
|
+
getAriaValueText={() => `${reading}${unit ?? ''}`}
|
|
62
|
+
className="block size-4 rounded-full bg-surface-raised shadow-raised outline-offset-2 outline-accent has-focus-visible:outline-2"
|
|
63
|
+
/>
|
|
64
|
+
</BaseSlider.Track>
|
|
65
|
+
</BaseSlider.Control>
|
|
66
|
+
</BaseSlider.Root>
|
|
67
|
+
);
|
|
68
|
+
}
|
package/src/Stepper.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
5
5
|
import { ButtonGroup } from './ButtonGroup.tsx';
|
|
6
6
|
import { formatFixed } from './format/number.ts';
|
|
7
7
|
import { IconButton } from './IconButton.tsx';
|
|
8
|
+
import { placesOf } from './step-places.ts';
|
|
8
9
|
|
|
9
10
|
export interface StepperProps {
|
|
10
11
|
value: number;
|
|
@@ -21,8 +22,6 @@ export interface StepperProps {
|
|
|
21
22
|
ref?: Ref<HTMLDivElement>;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
const placesOf = (step: number): number => String(step).split('.')[1]?.length ?? 0;
|
|
25
|
-
|
|
26
25
|
/* A number between minus and plus, in one sunken group so the three read as a single control. */
|
|
27
26
|
export function Stepper({ value, onValueChange, min, max, step, decimals = placesOf(step), unit, label, className, ref }: StepperProps) {
|
|
28
27
|
const { t } = useTranslation('ui');
|
package/src/Toasts.tsx
CHANGED
|
@@ -82,7 +82,7 @@ function ToastCard<T extends Toast>({ toast, footer, onDismiss }: { toast: T; fo
|
|
|
82
82
|
)}
|
|
83
83
|
</span>
|
|
84
84
|
<div className="flex min-w-0 grow flex-col gap-1">
|
|
85
|
-
<span className="text-sm text-text">{toast.title}</span>
|
|
85
|
+
<span className="text-sm break-words text-text">{toast.title}</span>
|
|
86
86
|
{/* Wraps rather than clips: the line under the title is the one that says what went
|
|
87
87
|
wrong, and a reason cut off at the card's edge is no reason at all. */}
|
|
88
88
|
{toast.description !== undefined && toast.description !== '' && (
|
package/src/Tooltip.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, type ReactElement, type ReactNode } from 'react';
|
|
1
|
+
import { useCallback, type ComponentProps, type ReactElement, type ReactNode } from 'react';
|
|
2
2
|
import { Tooltip as BaseTooltip } from '@base-ui-components/react/tooltip';
|
|
3
3
|
import { Kbd } from './Kbd.tsx';
|
|
4
4
|
import type { Shortcut } from './shortcut.ts';
|
|
@@ -17,6 +17,10 @@ export function TooltipProvider({ children }: { children: ReactNode }) {
|
|
|
17
17
|
|
|
18
18
|
export interface TooltipProps {
|
|
19
19
|
label: ReactNode;
|
|
20
|
+
open?: boolean;
|
|
21
|
+
onOpenChange?(open: boolean): void;
|
|
22
|
+
/* A virtual anchor positions a controlled hint over canvas content without adding an input target. */
|
|
23
|
+
anchor?: ComponentProps<typeof BaseTooltip.Positioner>['anchor'];
|
|
20
24
|
/* A shortcut, or a short phrase about a key that is not one ("Shift skips the cache"). */
|
|
21
25
|
kbd?: Shortcut | string;
|
|
22
26
|
side?: TooltipSide;
|
|
@@ -27,11 +31,11 @@ export interface TooltipProps {
|
|
|
27
31
|
/* Keeps the hint closed, for a trigger that only needs one some of the time. */
|
|
28
32
|
disabled?: boolean;
|
|
29
33
|
/* The trigger element. Its own children and handlers are kept; Base UI merges the tooltip props in. */
|
|
30
|
-
children
|
|
34
|
+
children?: ReactElement<Record<string, unknown>>;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
/* A hint on hover and on keyboard focus. Every hint in an app is one of these, never a `title`. */
|
|
34
|
-
export function Tooltip({ label, kbd, side = 'top', sideOffset = 6, name = false, disabled = false, children }: TooltipProps) {
|
|
38
|
+
export function Tooltip({ label, open, onOpenChange, anchor, kbd, side = 'top', sideOffset = 6, name = false, disabled = false, children }: TooltipProps) {
|
|
35
39
|
// Held Cmd (Ctrl off macOS) prints this shortcut under the trigger (`ShortcutHints`).
|
|
36
40
|
const hintRef = useCallback(
|
|
37
41
|
(element: HTMLElement | null) => {
|
|
@@ -44,11 +48,11 @@ export function Tooltip({ label, kbd, side = 'top', sideOffset = 6, name = false
|
|
|
44
48
|
);
|
|
45
49
|
|
|
46
50
|
return (
|
|
47
|
-
<BaseTooltip.Root disabled={disabled}>
|
|
48
|
-
<BaseTooltip.Trigger ref={hintRef} render={children} aria-label={name && typeof label === 'string' ? label : undefined} />
|
|
51
|
+
<BaseTooltip.Root disabled={disabled} open={open} onOpenChange={onOpenChange} disableHoverablePopup={children === undefined}>
|
|
52
|
+
{children && <BaseTooltip.Trigger ref={hintRef} render={children} aria-label={name && typeof label === 'string' ? label : undefined} />}
|
|
49
53
|
<BaseTooltip.Portal>
|
|
50
|
-
<BaseTooltip.Positioner side={side} sideOffset={sideOffset} className="tooltip-positioner">
|
|
51
|
-
<BaseTooltip.Popup className="tooltip-popup">
|
|
54
|
+
<BaseTooltip.Positioner anchor={anchor} side={side} sideOffset={sideOffset} className="tooltip-positioner">
|
|
55
|
+
<BaseTooltip.Popup role="tooltip" className="tooltip-popup">
|
|
52
56
|
<BaseTooltip.Viewport className="max-w-72 px-[9px] py-[5px] text-pretty">
|
|
53
57
|
<span className="wrap-anywhere">{label}</span>
|
|
54
58
|
{/* The viewport wraps its children in a div of its own, so the 8px
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useId, useState } from 'react';
|
|
2
|
+
import { flushSync } from 'react-dom';
|
|
2
3
|
import type { FileTree, FileTreeVisibleRow } from '@pierre/trees';
|
|
3
4
|
import { pathOfRow, visibleRows } from './helpers.ts';
|
|
4
5
|
import { FILE_TREE_CSS } from './style.ts';
|
|
@@ -153,7 +154,8 @@ export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, lab
|
|
|
153
154
|
.join('|');
|
|
154
155
|
if (snapshot !== key) {
|
|
155
156
|
snapshot = key;
|
|
156
|
-
|
|
157
|
+
// Recycled rows must paint with their new portal contents in the same frame.
|
|
158
|
+
flushSync(() => setSlots(next));
|
|
157
159
|
}
|
|
158
160
|
position();
|
|
159
161
|
};
|
package/src/index.ts
CHANGED
|
@@ -61,6 +61,7 @@ export { Select, type SelectGroup, type SelectItem, type SelectProps } from './S
|
|
|
61
61
|
export { Separator, type SeparatorProps } from './Separator.tsx';
|
|
62
62
|
export { ShortcutHints } from './ShortcutHints.tsx';
|
|
63
63
|
export { Skeleton, type SkeletonProps } from './Skeleton.tsx';
|
|
64
|
+
export { Slider, type SliderProps } from './Slider.tsx';
|
|
64
65
|
export { SlidingColumn, type SlidingColumnProps } from './SlidingColumn.tsx';
|
|
65
66
|
export { Spinner, type SpinnerProps } from './Spinner.tsx';
|
|
66
67
|
export { Stepper, type StepperProps } from './Stepper.tsx';
|