@kennycorrea/ui 0.10.0 → 0.11.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/CHANGELOG.md +110 -16
- package/README.md +85 -0
- package/dist/atoms/ActionSheet/ActionSheet.d.ts +30 -0
- package/dist/atoms/ActionSheet/ActionSheet.js +42 -0
- package/dist/atoms/ActionSheet/confirm.d.ts +21 -0
- package/dist/atoms/ActionSheet/confirm.js +65 -0
- package/dist/atoms/ActionSheet/index.d.ts +2 -0
- package/dist/atoms/ActionSheet/index.js +2 -0
- package/dist/atoms/Button/Button.js +3 -3
- package/dist/atoms/Button/Button.types.d.ts +7 -1
- package/dist/atoms/Card/Card.d.ts +28 -7
- package/dist/atoms/Card/Card.js +45 -9
- package/dist/atoms/ChipGroup/ChipGroup.d.ts +31 -0
- package/dist/atoms/ChipGroup/ChipGroup.js +69 -0
- package/dist/atoms/ChipGroup/index.d.ts +1 -0
- package/dist/atoms/ChipGroup/index.js +1 -0
- package/dist/atoms/Input/Input.js +7 -1
- package/dist/atoms/List/List.d.ts +73 -0
- package/dist/atoms/List/List.js +88 -0
- package/dist/atoms/List/index.d.ts +1 -0
- package/dist/atoms/List/index.js +1 -0
- package/dist/atoms/List/swipe.d.ts +68 -0
- package/dist/atoms/List/swipe.js +207 -0
- package/dist/atoms/NavBar/BackButton.d.ts +10 -0
- package/dist/atoms/NavBar/BackButton.js +11 -0
- package/dist/atoms/NavBar/NavBar.d.ts +22 -0
- package/dist/atoms/NavBar/NavBar.js +29 -0
- package/dist/atoms/NavBar/index.d.ts +2 -0
- package/dist/atoms/NavBar/index.js +2 -0
- package/dist/atoms/PullToRefresh/PullToRefresh.d.ts +31 -0
- package/dist/atoms/PullToRefresh/PullToRefresh.js +110 -0
- package/dist/atoms/PullToRefresh/index.d.ts +1 -0
- package/dist/atoms/PullToRefresh/index.js +1 -0
- package/dist/atoms/ResponsiveDialog/ResponsiveDialog.d.ts +13 -0
- package/dist/atoms/ResponsiveDialog/ResponsiveDialog.js +27 -0
- package/dist/atoms/ResponsiveDialog/index.d.ts +1 -0
- package/dist/atoms/ResponsiveDialog/index.js +1 -0
- package/dist/atoms/ScrollStrip/ScrollStrip.d.ts +17 -0
- package/dist/atoms/ScrollStrip/ScrollStrip.js +15 -0
- package/dist/atoms/ScrollStrip/index.d.ts +1 -0
- package/dist/atoms/ScrollStrip/index.js +1 -0
- package/dist/atoms/SegmentedControl/SegmentedControl.d.ts +23 -0
- package/dist/atoms/SegmentedControl/SegmentedControl.js +41 -0
- package/dist/atoms/SegmentedControl/index.d.ts +1 -0
- package/dist/atoms/SegmentedControl/index.js +1 -0
- package/dist/atoms/Select/Select.js +1 -1
- package/dist/atoms/Sheet/Sheet.d.ts +63 -0
- package/dist/atoms/Sheet/Sheet.js +261 -0
- package/dist/atoms/Sheet/index.d.ts +1 -0
- package/dist/atoms/Sheet/index.js +1 -0
- package/dist/atoms/StatTile/StatTile.d.ts +36 -0
- package/dist/atoms/StatTile/StatTile.js +17 -0
- package/dist/atoms/StatTile/index.d.ts +1 -0
- package/dist/atoms/StatTile/index.js +1 -0
- package/dist/atoms/TabBar/TabBar.d.ts +42 -0
- package/dist/atoms/TabBar/TabBar.js +36 -0
- package/dist/atoms/TabBar/index.d.ts +1 -0
- package/dist/atoms/TabBar/index.js +1 -0
- package/dist/atoms/Textarea/Textarea.d.ts +4 -0
- package/dist/atoms/Textarea/Textarea.js +1 -1
- package/dist/atoms/Toast/Toast.d.ts +8 -2
- package/dist/atoms/Toast/Toast.js +19 -4
- package/dist/atoms/index.d.ts +11 -0
- package/dist/atoms/index.js +11 -0
- package/dist/primitives/index.d.ts +1 -1
- package/dist/primitives/index.js +1 -1
- package/dist/strings.d.ts +10 -0
- package/dist/strings.js +5 -0
- package/package.json +3 -1
- package/tokens.css +393 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useUiStrings } from '../../strings.js';
|
|
3
|
+
import { Icon } from '../Icon/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* A chevron with an optional label. The target is at least 44px square; with no `label` the
|
|
6
|
+
* accessible name comes from `UiStrings.back`.
|
|
7
|
+
*/
|
|
8
|
+
export function BackButton({ label, className, type = 'button', ...rest }) {
|
|
9
|
+
const strings = useUiStrings();
|
|
10
|
+
return (_jsxs("button", { type: type, "aria-label": label ? undefined : strings.back, className: ['ui-backbutton', className].filter(Boolean).join(' '), ...rest, children: [_jsx(Icon, { name: "collapse", className: "ui-backbutton__icon" }), label && _jsx("span", { className: "ui-backbutton__label", children: label })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface NavBarProps {
|
|
3
|
+
title: string;
|
|
4
|
+
/** Left slot: a `BackButton`, usually. */
|
|
5
|
+
leading?: ReactNode;
|
|
6
|
+
/** Right slot: icon buttons. */
|
|
7
|
+
trailing?: ReactNode;
|
|
8
|
+
/**
|
|
9
|
+
* A large left-aligned title in the content flow. It is the page's `h1`; once it has scrolled
|
|
10
|
+
* under the bar, a small centred copy fades in (that copy is `aria-hidden`).
|
|
11
|
+
* Without it the small title is the `h1`.
|
|
12
|
+
*/
|
|
13
|
+
largeTitle?: boolean;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A sticky top bar for small screens. Render it as the first child of the scrolling content:
|
|
18
|
+
* the large title follows it in the flow. The bar is transparent at the top of the page and
|
|
19
|
+
* gains `--material-bar` and a hairline once content has moved beneath it. Scroll state comes
|
|
20
|
+
* from one IntersectionObserver, with no scroll listeners.
|
|
21
|
+
*/
|
|
22
|
+
export declare function NavBar({ title, leading, trailing, largeTitle, className }: NavBarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
/** Height of the bar proper, `--safe-top` excluded. Mirrored in tokens.css. */
|
|
4
|
+
const BAR_HEIGHT = 44;
|
|
5
|
+
/**
|
|
6
|
+
* A sticky top bar for small screens. Render it as the first child of the scrolling content:
|
|
7
|
+
* the large title follows it in the flow. The bar is transparent at the top of the page and
|
|
8
|
+
* gains `--material-bar` and a hairline once content has moved beneath it. Scroll state comes
|
|
9
|
+
* from one IntersectionObserver, with no scroll listeners.
|
|
10
|
+
*/
|
|
11
|
+
export function NavBar({ title, leading, trailing, largeTitle = false, className }) {
|
|
12
|
+
const [scrolled, setScrolled] = useState(false);
|
|
13
|
+
const target = useRef(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const el = target.current;
|
|
16
|
+
if (!el || typeof IntersectionObserver === 'undefined')
|
|
17
|
+
return;
|
|
18
|
+
// With a large title, "scrolled" means it is gone beneath the bar; without one, that the
|
|
19
|
+
// page has moved at all (the sentinel is a zero-height element at the top of the flow).
|
|
20
|
+
const io = new IntersectionObserver(([entry]) => {
|
|
21
|
+
if (entry)
|
|
22
|
+
setScrolled(!entry.isIntersecting);
|
|
23
|
+
}, { rootMargin: largeTitle ? `-${BAR_HEIGHT}px 0px 0px 0px` : '0px' });
|
|
24
|
+
io.observe(el);
|
|
25
|
+
return () => io.disconnect();
|
|
26
|
+
}, [largeTitle]);
|
|
27
|
+
const root = ['ui-navbar', className].filter(Boolean).join(' ');
|
|
28
|
+
return (_jsxs(_Fragment, { children: [_jsxs("header", { className: root, "data-scrolled": scrolled ? 'true' : 'false', "data-large": largeTitle ? 'true' : 'false', children: [_jsx("div", { className: "ui-navbar__side", children: leading }), largeTitle ? (_jsx("div", { className: "ui-navbar__title", "aria-hidden": "true", children: title })) : (_jsx("h1", { className: "ui-navbar__title", children: title })), _jsx("div", { className: "ui-navbar__side ui-navbar__side--trailing", children: trailing })] }), largeTitle ? (_jsx("div", { className: "ui-navbar__large", children: _jsx("h1", { ref: target, className: "ui-navbar__large-title", children: title }) })) : (_jsx("div", { ref: target, className: "ui-navbar__sentinel", "aria-hidden": "true" }))] }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface PullToRefreshProps {
|
|
3
|
+
/** Called once the pull passes the threshold. The indicator is held until the promise settles. */
|
|
4
|
+
onRefresh: () => Promise<void>;
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
/**
|
|
7
|
+
* Which thing scrolls. `document` (default) watches the page and the wrapper is a plain block;
|
|
8
|
+
* `self` makes the wrapper the scroll container (give it a height via `className`).
|
|
9
|
+
*/
|
|
10
|
+
scroller?: 'document' | 'self';
|
|
11
|
+
/** Pixels of (damped) pull that commit a refresh. Default 64. */
|
|
12
|
+
threshold?: number;
|
|
13
|
+
/** Most the content is pushed down. Default 112. */
|
|
14
|
+
maxPull?: number;
|
|
15
|
+
/** Turns the gesture off. */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Overrides the announced label (otherwise the `refreshing` UI string). */
|
|
18
|
+
label?: string;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Pull-to-refresh for touch screens.
|
|
23
|
+
*
|
|
24
|
+
* **A shortcut, never the only way**: the parent must still offer a visible refresh action,
|
|
25
|
+
* because the gesture is invisible, touch-only and unavailable to switch and keyboard users.
|
|
26
|
+
*
|
|
27
|
+
* Active only on coarse pointers, only when the scroller is at the very top when the touch
|
|
28
|
+
* begins, and only for a downward drag; otherwise the browser's own scroll is untouched.
|
|
29
|
+
* `onRefresh` should handle its own errors — a rejection just ends the wait.
|
|
30
|
+
*/
|
|
31
|
+
export declare function PullToRefresh({ onRefresh, children, scroller, threshold, maxPull, disabled, label, className, }: PullToRefreshProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useUiStrings } from '../../strings.js';
|
|
4
|
+
import { cn } from '../../utils/cn.js';
|
|
5
|
+
import { Spinner } from '../Spinner/index.js';
|
|
6
|
+
/** A finger travels this many pixels for each pixel the content moves. */
|
|
7
|
+
const RESISTANCE = 2;
|
|
8
|
+
const query = (q) => typeof window.matchMedia === 'function' && window.matchMedia(q).matches;
|
|
9
|
+
/**
|
|
10
|
+
* Pull-to-refresh for touch screens.
|
|
11
|
+
*
|
|
12
|
+
* **A shortcut, never the only way**: the parent must still offer a visible refresh action,
|
|
13
|
+
* because the gesture is invisible, touch-only and unavailable to switch and keyboard users.
|
|
14
|
+
*
|
|
15
|
+
* Active only on coarse pointers, only when the scroller is at the very top when the touch
|
|
16
|
+
* begins, and only for a downward drag; otherwise the browser's own scroll is untouched.
|
|
17
|
+
* `onRefresh` should handle its own errors — a rejection just ends the wait.
|
|
18
|
+
*/
|
|
19
|
+
export function PullToRefresh({ onRefresh, children, scroller = 'document', threshold = 64, maxPull = 112, disabled = false, label, className, }) {
|
|
20
|
+
const strings = useUiStrings();
|
|
21
|
+
const rootRef = useRef(null);
|
|
22
|
+
const [pull, setPull] = useState(0);
|
|
23
|
+
const [dragging, setDragging] = useState(false);
|
|
24
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
25
|
+
const busy = useRef(false);
|
|
26
|
+
const refresh = useRef(onRefresh);
|
|
27
|
+
refresh.current = onRefresh;
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const root = rootRef.current;
|
|
30
|
+
if (!root || disabled)
|
|
31
|
+
return;
|
|
32
|
+
let startY = 0;
|
|
33
|
+
let tracking = false;
|
|
34
|
+
let current = 0;
|
|
35
|
+
const atTop = () => (scroller === 'self' ? root.scrollTop <= 0 : window.scrollY <= 0);
|
|
36
|
+
const set = (v) => {
|
|
37
|
+
current = v;
|
|
38
|
+
setPull(v);
|
|
39
|
+
};
|
|
40
|
+
const start = (e) => {
|
|
41
|
+
tracking = false;
|
|
42
|
+
if (busy.current || e.touches.length !== 1)
|
|
43
|
+
return;
|
|
44
|
+
if (!query('(pointer: coarse)') || !atTop())
|
|
45
|
+
return;
|
|
46
|
+
startY = e.touches[0].clientY;
|
|
47
|
+
tracking = true;
|
|
48
|
+
};
|
|
49
|
+
const move = (e) => {
|
|
50
|
+
if (!tracking)
|
|
51
|
+
return;
|
|
52
|
+
const dy = e.touches[0].clientY - startY;
|
|
53
|
+
if (dy <= 0 || !atTop()) {
|
|
54
|
+
if (current > 0)
|
|
55
|
+
set(0);
|
|
56
|
+
setDragging(false);
|
|
57
|
+
if (dy <= 0)
|
|
58
|
+
tracking = false;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (e.cancelable)
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
setDragging(true);
|
|
64
|
+
set(Math.min(maxPull, dy / RESISTANCE));
|
|
65
|
+
};
|
|
66
|
+
const end = async () => {
|
|
67
|
+
if (!tracking)
|
|
68
|
+
return;
|
|
69
|
+
tracking = false;
|
|
70
|
+
setDragging(false);
|
|
71
|
+
if (current < threshold)
|
|
72
|
+
return set(0);
|
|
73
|
+
busy.current = true;
|
|
74
|
+
setRefreshing(true);
|
|
75
|
+
set(threshold);
|
|
76
|
+
try {
|
|
77
|
+
await refresh.current();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// The caller owns error reporting; the gesture only ends the wait.
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
busy.current = false;
|
|
84
|
+
setRefreshing(false);
|
|
85
|
+
set(0);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const cancel = () => {
|
|
89
|
+
if (!tracking)
|
|
90
|
+
return;
|
|
91
|
+
tracking = false;
|
|
92
|
+
setDragging(false);
|
|
93
|
+
if (!busy.current)
|
|
94
|
+
set(0);
|
|
95
|
+
};
|
|
96
|
+
root.addEventListener('touchstart', start, { passive: true });
|
|
97
|
+
root.addEventListener('touchmove', move, { passive: false });
|
|
98
|
+
root.addEventListener('touchend', end);
|
|
99
|
+
root.addEventListener('touchcancel', cancel);
|
|
100
|
+
return () => {
|
|
101
|
+
root.removeEventListener('touchstart', start);
|
|
102
|
+
root.removeEventListener('touchmove', move);
|
|
103
|
+
root.removeEventListener('touchend', end);
|
|
104
|
+
root.removeEventListener('touchcancel', cancel);
|
|
105
|
+
};
|
|
106
|
+
}, [disabled, scroller, threshold, maxPull]);
|
|
107
|
+
const progress = Math.min(1, pull / threshold);
|
|
108
|
+
const name = label ?? strings.refreshing;
|
|
109
|
+
return (_jsxs("div", { ref: rootRef, "aria-busy": refreshing || undefined, className: cn('relative', scroller === 'self' && 'overflow-y-auto overscroll-y-contain', className), children: [_jsx("div", { className: "pointer-events-none absolute inset-x-0 top-0 flex items-center justify-center overflow-hidden", style: { height: pull, opacity: refreshing ? 1 : progress }, children: refreshing ? (_jsx(Spinner, { label: name })) : pull > 0 ? (_jsx("span", { "aria-hidden": "true", className: "size-5 rounded-full border-2 border-[var(--gray-8)] border-t-[var(--accent-9)] motion-reduce:rotate-0!", style: { transform: `rotate(${progress * 270}deg)` } })) : null }), _jsx("div", { className: cn(!dragging && 'transition-transform duration-200 ease-out motion-reduce:transition-none'), style: { transform: pull ? `translateY(${pull}px)` : undefined }, children: children })] }));
|
|
110
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './PullToRefresh.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './PullToRefresh.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type DialogProps } from '../Dialog/Dialog.js';
|
|
2
|
+
import { type SheetDetent } from '../Sheet/Sheet.js';
|
|
3
|
+
export interface ResponsiveDialogProps extends DialogProps {
|
|
4
|
+
/** The detent the sheet opens at below `md`. Default `fit`. */
|
|
5
|
+
sheetDetent?: SheetDetent;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The same props as {@link Dialog}, presented the way the screen asks: a centred dialog on a
|
|
9
|
+
* desktop, a bottom sheet on a phone. Both sit on Radix Dialog, so the role, the focus trap,
|
|
10
|
+
* Escape and the labelling are the same either way. Crossing the breakpoint while open remounts
|
|
11
|
+
* the content, which loses uncommitted state inside `children` — keep form state in the parent.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ResponsiveDialog({ sheetDetent, ...props }: ResponsiveDialogProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useSyncExternalStore } from 'react';
|
|
3
|
+
import { Dialog } from '../Dialog/Dialog.js';
|
|
4
|
+
import { Sheet } from '../Sheet/Sheet.js';
|
|
5
|
+
/** Tailwind's `md`. Below it the dialog becomes a bottom sheet. */
|
|
6
|
+
const MD = '(min-width: 768px)';
|
|
7
|
+
function subscribe(onChange) {
|
|
8
|
+
const query = window.matchMedia(MD);
|
|
9
|
+
query.addEventListener('change', onChange);
|
|
10
|
+
return () => query.removeEventListener('change', onChange);
|
|
11
|
+
}
|
|
12
|
+
/** A Dialog at `md` and up, a Sheet below. Without `matchMedia` (SSR) the Dialog. */
|
|
13
|
+
function useIsMdUp() {
|
|
14
|
+
return useSyncExternalStore(subscribe, () => window.matchMedia(MD).matches, () => true);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The same props as {@link Dialog}, presented the way the screen asks: a centred dialog on a
|
|
18
|
+
* desktop, a bottom sheet on a phone. Both sit on Radix Dialog, so the role, the focus trap,
|
|
19
|
+
* Escape and the labelling are the same either way. Crossing the breakpoint while open remounts
|
|
20
|
+
* the content, which loses uncommitted state inside `children` — keep form state in the parent.
|
|
21
|
+
*/
|
|
22
|
+
export function ResponsiveDialog({ sheetDetent = 'fit', ...props }) {
|
|
23
|
+
const mdUp = useIsMdUp();
|
|
24
|
+
if (mdUp)
|
|
25
|
+
return _jsx(Dialog, { ...props });
|
|
26
|
+
return (_jsx(Sheet, { open: props.open, onOpenChange: props.onOpenChange, title: props.title, description: props.description, footer: props.footer, className: props.className, detent: sheetDetent, showCloseButton: true, children: props.children }));
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ResponsiveDialog.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ResponsiveDialog.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'react';
|
|
2
|
+
export type ScrollStripBreakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
3
|
+
export interface ScrollStripProps extends HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
/** From this breakpoint the strip stops scrolling and lays its children out as a grid. */
|
|
5
|
+
gridFrom?: ScrollStripBreakpoint;
|
|
6
|
+
/** Grid columns from `gridFrom`. */
|
|
7
|
+
columns?: number;
|
|
8
|
+
/** Width of each item while scrolling, so the next one peeks at the edge. Any CSS length. */
|
|
9
|
+
itemWidth?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A horizontal scroll-snap row that becomes a grid at a breakpoint. Items snap to the start edge,
|
|
13
|
+
* the scroll padding equals the page gutter (`--page-gutter`, 16px unless the app sets it) so a
|
|
14
|
+
* snapped item lines up with the content above it, and the scrollbar is hidden. Items default to
|
|
15
|
+
* 78% of the strip so the next one shows. Use it for KPI rows and card carousels.
|
|
16
|
+
*/
|
|
17
|
+
export declare function ScrollStrip({ gridFrom, columns, itemWidth, className, style, ...props }: ScrollStripProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from '../../utils/cn.js';
|
|
3
|
+
/**
|
|
4
|
+
* A horizontal scroll-snap row that becomes a grid at a breakpoint. Items snap to the start edge,
|
|
5
|
+
* the scroll padding equals the page gutter (`--page-gutter`, 16px unless the app sets it) so a
|
|
6
|
+
* snapped item lines up with the content above it, and the scrollbar is hidden. Items default to
|
|
7
|
+
* 78% of the strip so the next one shows. Use it for KPI rows and card carousels.
|
|
8
|
+
*/
|
|
9
|
+
export function ScrollStrip({ gridFrom = 'md', columns = 3, itemWidth, className, style, ...props }) {
|
|
10
|
+
return (_jsx("div", { className: cn('ui-strip', className), "data-grid-from": gridFrom, style: {
|
|
11
|
+
'--strip-cols': columns,
|
|
12
|
+
...(itemWidth ? { '--strip-item': itemWidth } : null),
|
|
13
|
+
...style,
|
|
14
|
+
}, ...props }));
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ScrollStrip.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ScrollStrip.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface SegmentedControlOption {
|
|
3
|
+
value: string;
|
|
4
|
+
label: ReactNode;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface SegmentedControlProps {
|
|
8
|
+
/** 2–5 options. */
|
|
9
|
+
options: SegmentedControlOption[];
|
|
10
|
+
value: string;
|
|
11
|
+
onValueChange?: (value: string) => void;
|
|
12
|
+
size?: 'sm' | 'md';
|
|
13
|
+
/** Stretch the track to its container; segments are always equal width. */
|
|
14
|
+
fullWidth?: boolean;
|
|
15
|
+
'aria-label'?: string;
|
|
16
|
+
'aria-labelledby'?: string;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A pill track with a sliding thumb: one choice from 2–5. Radio semantics with a roving tabindex —
|
|
21
|
+
* arrow keys move and select (wrapping, skipping disabled), Home/End jump to the ends.
|
|
22
|
+
*/
|
|
23
|
+
export declare function SegmentedControl({ options, value, onValueChange, size, fullWidth, className, ...aria }: SegmentedControlProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* A pill track with a sliding thumb: one choice from 2–5. Radio semantics with a roving tabindex —
|
|
5
|
+
* arrow keys move and select (wrapping, skipping disabled), Home/End jump to the ends.
|
|
6
|
+
*/
|
|
7
|
+
export function SegmentedControl({ options, value, onValueChange, size = 'md', fullWidth = false, className, ...aria }) {
|
|
8
|
+
const refs = useRef([]);
|
|
9
|
+
const count = options.length;
|
|
10
|
+
const active = options.findIndex((o) => o.value === value);
|
|
11
|
+
const tabbable = active >= 0 && !options[active].disabled ? active : options.findIndex((o) => !o.disabled);
|
|
12
|
+
const select = (to) => {
|
|
13
|
+
refs.current[to]?.focus();
|
|
14
|
+
onValueChange?.(options[to].value);
|
|
15
|
+
};
|
|
16
|
+
const step = (from, dir) => {
|
|
17
|
+
let i = from;
|
|
18
|
+
for (let n = 0; n < count; n++) {
|
|
19
|
+
i = (i + dir + count) % count;
|
|
20
|
+
if (!options[i].disabled)
|
|
21
|
+
return select(i);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const onKeyDown = (e, i) => {
|
|
25
|
+
const enabled = options.map((o, k) => (o.disabled ? -1 : k)).filter((k) => k >= 0);
|
|
26
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown')
|
|
27
|
+
step(i, 1);
|
|
28
|
+
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp')
|
|
29
|
+
step(i, -1);
|
|
30
|
+
else if (e.key === 'Home')
|
|
31
|
+
select(enabled[0]);
|
|
32
|
+
else if (e.key === 'End')
|
|
33
|
+
select(enabled[enabled.length - 1]);
|
|
34
|
+
else
|
|
35
|
+
return;
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
};
|
|
38
|
+
return (_jsxs("div", { role: "radiogroup", className: ['ui-segmented', className].filter(Boolean).join(' '), "data-size": size, "data-full": fullWidth ? 'true' : undefined, style: { '--seg-count': count, '--seg-index': Math.max(active, 0) }, ...aria, children: [active >= 0 && _jsx("span", { className: "ui-segmented__thumb", "aria-hidden": "true" }), options.map((o, i) => (_jsx("button", { ref: (el) => {
|
|
39
|
+
refs.current[i] = el;
|
|
40
|
+
}, type: "button", role: "radio", "aria-checked": i === active, disabled: o.disabled, tabIndex: i === tabbable ? 0 : -1, className: "ui-segmented__item", onClick: () => onValueChange?.(o.value), onKeyDown: (e) => onKeyDown(e, i), children: o.label }, o.value)))] }));
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SegmentedControl.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SegmentedControl.js';
|
|
@@ -12,5 +12,5 @@ import { cn } from '../../utils/cn.js';
|
|
|
12
12
|
*/
|
|
13
13
|
export const Select = forwardRef(function Select({ value, onValueChange, options, placeholder, invalid = false, disabled, required, id, name, className, ...props }, ref) {
|
|
14
14
|
const strings = useUiStrings();
|
|
15
|
-
return (_jsxs(ThemedSelect.Root, { value: value, onValueChange: onValueChange, disabled: disabled, required: required, name: name, children: [_jsx(ThemedSelect.Trigger, { ref: ref, id: id, placeholder: placeholder ?? strings.select, color: invalid ? 'red' : undefined, "aria-invalid": invalid || undefined, className: cn('w-full', className), ...props }), _jsx(ThemedSelect.Content, { position: "popper", children: options.map((option) => (_jsx(ThemedSelect.Item, { value: option.value, children: option.label }, option.value))) })] }));
|
|
15
|
+
return (_jsxs(ThemedSelect.Root, { value: value, onValueChange: onValueChange, disabled: disabled, required: required, name: name, children: [_jsx(ThemedSelect.Trigger, { ref: ref, id: id, placeholder: placeholder ?? strings.select, color: invalid ? 'red' : undefined, "aria-invalid": invalid || undefined, className: cn('ui-select-trigger w-full', className), ...props }), _jsx(ThemedSelect.Content, { position: "popper", children: options.map((option) => (_jsx(ThemedSelect.Item, { value: option.value, children: option.label }, option.value))) })] }));
|
|
16
16
|
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Resting heights of a sheet. `medium` is half the visible viewport, `large` nearly all of it, and
|
|
4
|
+
* `fit` is the content's own height, capped at 90% of the viewport.
|
|
5
|
+
*/
|
|
6
|
+
export type SheetDetent = 'medium' | 'large' | 'fit';
|
|
7
|
+
export interface SheetProps {
|
|
8
|
+
open: boolean;
|
|
9
|
+
onOpenChange: (open: boolean) => void;
|
|
10
|
+
/** Labels the sheet (`aria-labelledby`). */
|
|
11
|
+
title: ReactNode;
|
|
12
|
+
/** Optional, wired up via `aria-describedby`. */
|
|
13
|
+
description?: ReactNode;
|
|
14
|
+
/** The body. Scrolls inside the sheet; a drag down from its top moves the sheet instead. */
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
/** Action row pinned below the body, above the bottom safe area and above the keyboard. */
|
|
17
|
+
footer?: ReactNode;
|
|
18
|
+
/** Controlled resting detent. */
|
|
19
|
+
detent?: SheetDetent;
|
|
20
|
+
/** Initial detent when uncontrolled. Default `fit`. */
|
|
21
|
+
defaultDetent?: SheetDetent;
|
|
22
|
+
/** Called when a drag or the handle moves the sheet to another detent. */
|
|
23
|
+
onDetentChange?: (detent: SheetDetent) => void;
|
|
24
|
+
/**
|
|
25
|
+
* The detents the sheet may rest at, shortest first (`fit` < `medium` < `large`). Dragging up
|
|
26
|
+
* moves to the next, down to the previous, and down from the lowest dismisses. Default: just
|
|
27
|
+
* the current detent, so the sheet dismisses or springs back and never resizes.
|
|
28
|
+
*/
|
|
29
|
+
detents?: SheetDetent[];
|
|
30
|
+
/**
|
|
31
|
+
* Scale and round the page behind while the sheet is at `large`, as iOS does. The page is
|
|
32
|
+
* everything beside the sheet inside the same Theme. Note that a transformed ancestor becomes
|
|
33
|
+
* the containing block of any `position: fixed` descendant, so fixed bars in the page scale too.
|
|
34
|
+
*/
|
|
35
|
+
scaleBackground?: boolean;
|
|
36
|
+
/** Whether a drag down, Escape, the scrim or the close button may close it. Default `true`. */
|
|
37
|
+
dismissible?: boolean;
|
|
38
|
+
/** Render a close button in the header. Default `false`. */
|
|
39
|
+
showCloseButton?: boolean;
|
|
40
|
+
/** Overrides the close button's accessible name (otherwise the `close` UI string). */
|
|
41
|
+
closeLabel?: string;
|
|
42
|
+
/** Overrides the handle's accessible name when it raises the sheet (otherwise `expand`). */
|
|
43
|
+
expandLabel?: string;
|
|
44
|
+
/** Overrides the handle's accessible name when it lowers the sheet (otherwise `collapse`). */
|
|
45
|
+
collapseLabel?: string;
|
|
46
|
+
/** Caps the width on wider screens. Default `max-w-lg`; pass `max-w-none` for full width. */
|
|
47
|
+
maxWidthClassName?: string;
|
|
48
|
+
className?: string;
|
|
49
|
+
/** Where the portal mounts. Default: the nearest `<Theme>` (`.radix-themes`), else rendered in place. */
|
|
50
|
+
container?: HTMLElement | null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A native-feeling bottom sheet on Radix Dialog.
|
|
54
|
+
*
|
|
55
|
+
* Radix owns what must not be reimplemented: the focus trap and its return on close, Escape,
|
|
56
|
+
* the scrim, `aria-modal`, and the page behind being hidden from assistive technology. This adds
|
|
57
|
+
* the sheet: a drag handle, detents, velocity-based dismissal, a body that scrolls until it is at
|
|
58
|
+
* its top and only then hands the gesture to the sheet, a footer that stays above the safe area
|
|
59
|
+
* and the keyboard, and an optional iOS-style scaled page behind the `large` detent.
|
|
60
|
+
*
|
|
61
|
+
* Like every floating atom here it portals into the nearest `<Theme>` rather than `<body>`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function Sheet({ open, onOpenChange, title, description, children, footer, detent: detentProp, defaultDetent, onDetentChange, detents, scaleBackground, dismissible, showCloseButton, closeLabel, expandLabel, collapseLabel, maxWidthClassName, className, container, }: SheetProps): import("react").JSX.Element;
|