cortena-ui 1.2.0 → 1.3.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/README.md +34 -0
- package/dist/index.d.ts +249 -54
- package/dist/index.js +347 -64
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/alert.tsx +9 -2
- package/src/components/badge.tsx +9 -4
- package/src/components/breadcrumb.tsx +171 -0
- package/src/components/button-link.tsx +3 -10
- package/src/components/card.tsx +27 -9
- package/src/components/chart/chart.tsx +13 -2
- package/src/components/chart/container.tsx +33 -2
- package/src/components/chart/data.ts +25 -1
- package/src/components/chart/index.tsx +1 -0
- package/src/components/chart/nivo-charts.tsx +4 -3
- package/src/components/chart/types.ts +19 -2
- package/src/components/data-table/data-table.tsx +23 -1
- package/src/components/dropzone.tsx +35 -1
- package/src/components/section-card.tsx +4 -0
- package/src/components/select.tsx +30 -1
- package/src/components/sheet.tsx +40 -4
- package/src/components/sortable-list.tsx +77 -9
- package/src/components/spinner.tsx +21 -4
- package/src/components/status-dot.tsx +14 -1
- package/src/index.ts +5 -3
- package/src/lib/render.tsx +39 -0
|
@@ -21,6 +21,30 @@ import { cn } from "@/lib/cn";
|
|
|
21
21
|
* `onFiles` receives only the accepted files; rejections go to `onReject`.
|
|
22
22
|
* `FileList` and `formatFileSize` are separate parts for listing what was
|
|
23
23
|
* picked, so a form can keep the zone and the list in different places.
|
|
24
|
+
*
|
|
25
|
+
* ## Wrapping a whole page as a drop target
|
|
26
|
+
*
|
|
27
|
+
* `noClick` and `noKeyboard` turn off the two activators that only make sense
|
|
28
|
+
* on a dashed box: clicking anywhere inside opens the file dialog, and Enter /
|
|
29
|
+
* Space on the focused root does the same. A page-sized zone wraps content
|
|
30
|
+
* that has its own buttons and inputs, so both have to go — otherwise every
|
|
31
|
+
* click in the page opens a file picker.
|
|
32
|
+
*
|
|
33
|
+
* ```tsx
|
|
34
|
+
* <Dropzone noClick noKeyboard onFiles={upload} className="min-h-dvh border-0 bg-transparent p-0">
|
|
35
|
+
* {({ isDragActive, open }) => (
|
|
36
|
+
* <>
|
|
37
|
+
* <PageContent />
|
|
38
|
+
* <Button onClick={open}>Browse…</Button>
|
|
39
|
+
* {isDragActive ? <DropHint /> : null}
|
|
40
|
+
* </>
|
|
41
|
+
* )}
|
|
42
|
+
* </Dropzone>
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* The render-prop form gets react-dropzone's `open()`, which is the way back
|
|
46
|
+
* to the file dialog from a deliberate control. With `noClick` the root also
|
|
47
|
+
* drops its `cursor-pointer`, since it is no longer clickable.
|
|
24
48
|
*/
|
|
25
49
|
|
|
26
50
|
const UNITS = ["B", "KB", "MB", "GB", "TB"] as const;
|
|
@@ -71,6 +95,10 @@ export interface DropzoneProps extends Omit<
|
|
|
71
95
|
/** Most files accepted per drop; `0` means unlimited. */
|
|
72
96
|
maxFiles?: number;
|
|
73
97
|
disabled?: boolean;
|
|
98
|
+
/** Do not open the file dialog on click; for a zone that wraps clickable content. @default false */
|
|
99
|
+
noClick?: boolean;
|
|
100
|
+
/** Do not open the file dialog on Enter or Space; pair with `noClick`. @default false */
|
|
101
|
+
noKeyboard?: boolean;
|
|
74
102
|
/** Primary line of the default body. */
|
|
75
103
|
label?: React.ReactNode;
|
|
76
104
|
/** Muted second line of the default body; derived from `accept` and `maxSize` when omitted. */
|
|
@@ -87,6 +115,8 @@ function Dropzone({
|
|
|
87
115
|
maxSize,
|
|
88
116
|
maxFiles,
|
|
89
117
|
disabled = false,
|
|
118
|
+
noClick = false,
|
|
119
|
+
noKeyboard = false,
|
|
90
120
|
label,
|
|
91
121
|
hint,
|
|
92
122
|
children,
|
|
@@ -100,6 +130,8 @@ function Dropzone({
|
|
|
100
130
|
maxSize,
|
|
101
131
|
maxFiles,
|
|
102
132
|
disabled,
|
|
133
|
+
noClick,
|
|
134
|
+
noKeyboard,
|
|
103
135
|
onDropAccepted: (files) => onFiles(files),
|
|
104
136
|
onDropRejected: (rejections) => onReject?.(rejections),
|
|
105
137
|
});
|
|
@@ -168,7 +200,7 @@ function Dropzone({
|
|
|
168
200
|
className: cn(
|
|
169
201
|
"group/dropzone flex flex-col items-center justify-center gap-2 p-6 text-center",
|
|
170
202
|
"rounded-[var(--ds-radius-lg)] border border-dashed border-[var(--ds-border)] bg-[var(--ds-card)]",
|
|
171
|
-
"cursor-pointer select-none outline-none",
|
|
203
|
+
noClick ? "select-none outline-none" : "cursor-pointer select-none outline-none",
|
|
172
204
|
"transition-[border-color,background-color] duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]",
|
|
173
205
|
"hover:border-[var(--ds-border-strong)] hover:bg-[var(--ds-hover)]",
|
|
174
206
|
"focus-visible:border-[var(--ds-ring)] focus-visible:ring-[3px] focus-visible:ring-[var(--ds-ring)]/40",
|
|
@@ -184,6 +216,8 @@ function Dropzone({
|
|
|
184
216
|
data-reject={isDragReject || undefined}
|
|
185
217
|
data-focused={isFocused || undefined}
|
|
186
218
|
data-disabled={disabled || undefined}
|
|
219
|
+
data-no-click={noClick || undefined}
|
|
220
|
+
data-no-keyboard={noKeyboard || undefined}
|
|
187
221
|
>
|
|
188
222
|
<input data-slot="dropzone-input" {...getInputProps()} />
|
|
189
223
|
{body}
|
|
@@ -29,6 +29,10 @@ export interface SectionCardProps extends Omit<CardProps, "title"> {
|
|
|
29
29
|
* SectionCard — a Card with a header row (title, description, actions), a
|
|
30
30
|
* body and an optional footer. It composes the Card parts rather than
|
|
31
31
|
* restyling them, so a SectionCard and a hand-built Card look identical.
|
|
32
|
+
*
|
|
33
|
+
* `render` is Card's and reaches the outer element, so a section card can be
|
|
34
|
+
* the landmark it is named after: `render={<section aria-labelledby={id} />}`
|
|
35
|
+
* with the same `id` on the title, or `render={<aside />}` for a side panel.
|
|
32
36
|
*/
|
|
33
37
|
function SectionCard({
|
|
34
38
|
title,
|
|
@@ -18,9 +18,38 @@ import { cn } from "@/lib/cn";
|
|
|
18
18
|
* - `onValueChange` receives `(value, eventDetails)`.
|
|
19
19
|
* - `SelectContent` defaults to the dropdown layout; pass
|
|
20
20
|
* `alignItemWithTrigger` for Base UI's native-like overlay of the selected item.
|
|
21
|
+
*
|
|
22
|
+
* ## Typing the value
|
|
23
|
+
*
|
|
24
|
+
* `null` is the cleared / placeholder state, so `onValueChange` always hands
|
|
25
|
+
* back `Value | null`, never bare `Value`. Name the value type on the element
|
|
26
|
+
* to keep that union at `string | null` instead of letting inference widen it:
|
|
27
|
+
*
|
|
28
|
+
* ```tsx
|
|
29
|
+
* const [status, setStatus] = useState("");
|
|
30
|
+
*
|
|
31
|
+
* <Select<string>
|
|
32
|
+
* // "" is not a value any item carries; `|| null` is the placeholder state,
|
|
33
|
+
* // which is what makes SelectValue fall back to its `placeholder`.
|
|
34
|
+
* value={status || null}
|
|
35
|
+
* onValueChange={(next) => setStatus(next ?? "")} // next: string | null
|
|
36
|
+
* >
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* Without the explicit `<string>` the type argument is inferred from `value`,
|
|
40
|
+
* so a `string | null` value widens `Value` and the handler's parameter turns
|
|
41
|
+
* into `string | null | null`. Multi-select narrows too:
|
|
42
|
+
* `<Select<string, true> multiple>` hands back `string[]`, never `null`.
|
|
43
|
+
*
|
|
44
|
+
* `SelectProps` is the same props type, for a wrapper that forwards them.
|
|
21
45
|
*/
|
|
46
|
+
export type SelectProps<
|
|
47
|
+
Value = string,
|
|
48
|
+
Multiple extends boolean | undefined = false,
|
|
49
|
+
> = SelectPrimitive.Root.Props<Value, Multiple>;
|
|
50
|
+
|
|
22
51
|
function Select<Value, Multiple extends boolean | undefined = false>(
|
|
23
|
-
props:
|
|
52
|
+
props: SelectProps<Value, Multiple>,
|
|
24
53
|
) {
|
|
25
54
|
return <SelectPrimitive.Root {...props} />;
|
|
26
55
|
}
|
package/src/components/sheet.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
|
|
4
4
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
-
import
|
|
5
|
+
import * as React from "react";
|
|
6
6
|
import { cn } from "@/lib/cn";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -12,9 +12,43 @@ import { cn } from "@/lib/cn";
|
|
|
12
12
|
* Description, Close); only the Popup's placement differs, chosen by `side`.
|
|
13
13
|
* `SheetContent` composes Portal + Backdrop + Popup so consumers keep writing
|
|
14
14
|
* `<SheetContent side="right">…</SheetContent>`.
|
|
15
|
+
*
|
|
16
|
+
* ## Non-modal side panels
|
|
17
|
+
*
|
|
18
|
+
* `modal={false}` is the help-panel / inspector case: a sheet read *alongside*
|
|
19
|
+
* the app rather than instead of it. No backdrop is rendered, focus is not
|
|
20
|
+
* trapped, page scroll is not locked, and the rest of the page stays clickable,
|
|
21
|
+
* so a user can keep working with the panel open. `modal="trap-focus"` is the
|
|
22
|
+
* middle setting Base UI offers: focus stays inside, the page still scrolls.
|
|
23
|
+
*
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <Sheet modal={false} open={helpOpen} onOpenChange={setHelpOpen}>
|
|
26
|
+
* <SheetContent side="right" disablePointerDismissal>…</SheetContent>
|
|
27
|
+
* </Sheet>
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* A non-modal sheet closes when focus or a press leaves it; pass
|
|
31
|
+
* `disablePointerDismissal` (a Base UI Root prop, forwarded) to keep it open
|
|
32
|
+
* until the user closes it. `modal` is on the Root because that is where Base
|
|
33
|
+
* UI reads it; `SheetContent` picks the backdrop up from there through context.
|
|
15
34
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
35
|
+
const SheetModalContext = React.createContext<boolean | "trap-focus">(true);
|
|
36
|
+
|
|
37
|
+
export interface SheetProps extends SheetPrimitive.Root.Props {
|
|
38
|
+
/**
|
|
39
|
+
* `true` (default) traps focus, locks page scroll and draws a backdrop.
|
|
40
|
+
* `false` draws no backdrop and traps nothing, for a panel read alongside
|
|
41
|
+
* the app. `"trap-focus"` keeps focus in but leaves the page scrollable.
|
|
42
|
+
*/
|
|
43
|
+
modal?: boolean | "trap-focus";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function Sheet({ modal = true, ...props }: SheetProps) {
|
|
47
|
+
return (
|
|
48
|
+
<SheetModalContext.Provider value={modal}>
|
|
49
|
+
<SheetPrimitive.Root modal={modal} {...props} />
|
|
50
|
+
</SheetModalContext.Provider>
|
|
51
|
+
);
|
|
18
52
|
}
|
|
19
53
|
|
|
20
54
|
function SheetTrigger(props: SheetPrimitive.Trigger.Props) {
|
|
@@ -85,12 +119,14 @@ function SheetContent({
|
|
|
85
119
|
container,
|
|
86
120
|
...props
|
|
87
121
|
}: SheetContentProps) {
|
|
122
|
+
const modal = React.useContext(SheetModalContext);
|
|
88
123
|
return (
|
|
89
124
|
<SheetPortal keepMounted={keepMounted} container={container}>
|
|
90
|
-
<SheetOverlay />
|
|
125
|
+
{modal === false ? null : <SheetOverlay />}
|
|
91
126
|
<SheetPrimitive.Popup
|
|
92
127
|
data-slot="sheet-content"
|
|
93
128
|
data-side={side}
|
|
129
|
+
data-modal={modal === false ? undefined : modal === true ? "" : modal}
|
|
94
130
|
className={cn(sheetVariants({ side }), className)}
|
|
95
131
|
{...props}
|
|
96
132
|
>
|
|
@@ -4,7 +4,9 @@ import {
|
|
|
4
4
|
DndContext,
|
|
5
5
|
DragOverlay,
|
|
6
6
|
KeyboardSensor,
|
|
7
|
+
MouseSensor,
|
|
7
8
|
PointerSensor,
|
|
9
|
+
TouchSensor,
|
|
8
10
|
closestCenter,
|
|
9
11
|
useSensor,
|
|
10
12
|
useSensors,
|
|
@@ -40,8 +42,29 @@ import { cn } from "@/lib/cn";
|
|
|
40
42
|
* By default the whole row is the drag activator. Pass `handle` to restrict
|
|
41
43
|
* dragging to a `<SortableHandle>` (or any element given `handleProps`) so
|
|
42
44
|
* buttons and inputs inside a row stay clickable.
|
|
45
|
+
*
|
|
46
|
+
* ## Touch
|
|
47
|
+
*
|
|
48
|
+
* A pointer drag and a touch scroll start the same way, so something has to
|
|
49
|
+
* say which one a finger means. In handle mode only the small handle is
|
|
50
|
+
* `touch-none`, so the rest of the row scrolls: nothing to decide. In
|
|
51
|
+
* whole-row mode the whole row is `touch-none`, which is why a touch list
|
|
52
|
+
* whose rows are the activators cannot be scrolled by dragging over them.
|
|
53
|
+
*
|
|
54
|
+
* `touchScroll` is the way out, and it defaults to whichever of those is
|
|
55
|
+
* already true — `true` in handle mode, `false` in whole-row mode, so existing
|
|
56
|
+
* lists behave exactly as before. Setting it `true` on a whole-row list drops
|
|
57
|
+
* `touch-none` and swaps the PointerSensor for a MouseSensor plus a
|
|
58
|
+
* TouchSensor with delay activation: a quick swipe scrolls, a press held for
|
|
59
|
+
* {@link TOUCH_DELAY_MS} picks the row up. `false` in handle mode is the
|
|
60
|
+
* opposite trade — the row never scrolls under a finger.
|
|
43
61
|
*/
|
|
44
62
|
|
|
63
|
+
/** How long a finger must rest on a row before it becomes a drag. */
|
|
64
|
+
const TOUCH_DELAY_MS = 250;
|
|
65
|
+
/** How far it may slip in that time and still count as a press, not a scroll. */
|
|
66
|
+
const TOUCH_TOLERANCE_PX = 8;
|
|
67
|
+
|
|
45
68
|
/** Spread onto the element that should start a drag. */
|
|
46
69
|
export interface SortableHandleProps extends Partial<DraggableAttributes> {
|
|
47
70
|
ref: (element: HTMLElement | null) => void;
|
|
@@ -63,17 +86,40 @@ export interface SortableRenderState {
|
|
|
63
86
|
isDragging: boolean;
|
|
64
87
|
}
|
|
65
88
|
|
|
89
|
+
/** What moved where, passed to `onReorder` alongside the reordered array. */
|
|
90
|
+
export interface SortableReorderDetails {
|
|
91
|
+
/** Index the item came from, in the array as it was passed in. */
|
|
92
|
+
from: number;
|
|
93
|
+
/** Index it landed on. */
|
|
94
|
+
to: number;
|
|
95
|
+
/** Id of the item that moved (`getId` of the dragged item). */
|
|
96
|
+
activeId: UniqueIdentifier;
|
|
97
|
+
/** Id of the item it was dropped onto. */
|
|
98
|
+
overId: UniqueIdentifier;
|
|
99
|
+
}
|
|
100
|
+
|
|
66
101
|
export interface SortableListProps<T> extends Omit<React.ComponentProps<"ul">, "children"> {
|
|
67
102
|
items: readonly T[];
|
|
68
103
|
/** Stable id per item; must be unique within the list. */
|
|
69
104
|
getId: (item: T) => UniqueIdentifier;
|
|
70
|
-
/**
|
|
71
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Called with the reordered array after a successful drop, and with the move
|
|
107
|
+
* itself — `{ from, to, activeId, overId }` — for a server that persists a
|
|
108
|
+
* position rather than the whole list.
|
|
109
|
+
*/
|
|
110
|
+
onReorder: (next: T[], details: SortableReorderDetails) => void;
|
|
72
111
|
renderItem: (item: T, state: SortableRenderState) => React.ReactNode;
|
|
73
112
|
/** Layout axis; also selects the sorting strategy. */
|
|
74
113
|
orientation?: "vertical" | "horizontal";
|
|
75
114
|
/** Only a `<SortableHandle>` (or an element given `handleProps`) starts a drag. */
|
|
76
115
|
handle?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Let a finger scroll the page over the rows. Defaults to `handle`: `true`
|
|
118
|
+
* in handle mode, `false` in whole-row mode, which is what each already did.
|
|
119
|
+
* `true` on a whole-row list drops `touch-none` and uses a TouchSensor with
|
|
120
|
+
* delay activation, so a swipe scrolls and a held press drags.
|
|
121
|
+
*/
|
|
122
|
+
touchScroll?: boolean;
|
|
77
123
|
/** Disables every sensor and hides the handles. */
|
|
78
124
|
disabled?: boolean;
|
|
79
125
|
/** Class name for each item wrapper. */
|
|
@@ -106,11 +152,12 @@ interface SortableItemProps<T> {
|
|
|
106
152
|
item: T;
|
|
107
153
|
handle: boolean;
|
|
108
154
|
disabled: boolean;
|
|
155
|
+
touchScroll: boolean;
|
|
109
156
|
renderItem: SortableListProps<T>["renderItem"];
|
|
110
157
|
className: string | undefined;
|
|
111
158
|
}
|
|
112
159
|
|
|
113
|
-
function SortableItem<T>({ id, item, handle, disabled, renderItem, className }: SortableItemProps<T>) {
|
|
160
|
+
function SortableItem<T>({ id, item, handle, disabled, touchScroll, renderItem, className }: SortableItemProps<T>) {
|
|
114
161
|
const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging } =
|
|
115
162
|
useSortable({ id, disabled });
|
|
116
163
|
|
|
@@ -134,7 +181,9 @@ function SortableItem<T>({ id, item, handle, disabled, renderItem, className }:
|
|
|
134
181
|
style={{ transform: CSS.Translate.toString(transform), transition }}
|
|
135
182
|
className={cn(
|
|
136
183
|
itemClassName,
|
|
137
|
-
!handle && !disabled && "cursor-grab
|
|
184
|
+
!handle && !disabled && "cursor-grab data-[dragging]:cursor-grabbing",
|
|
185
|
+
// touch-action is what decides whether a finger scrolls or drags.
|
|
186
|
+
!touchScroll && !disabled && "touch-none",
|
|
138
187
|
className,
|
|
139
188
|
)}
|
|
140
189
|
{...rowActivator}
|
|
@@ -184,15 +233,27 @@ function SortableList<T>({
|
|
|
184
233
|
orientation = "vertical",
|
|
185
234
|
handle = false,
|
|
186
235
|
disabled = false,
|
|
236
|
+
touchScroll = handle,
|
|
187
237
|
itemClassName: itemClass,
|
|
188
238
|
className,
|
|
189
239
|
...props
|
|
190
240
|
}: SortableListProps<T>) {
|
|
191
241
|
const [activeId, setActiveId] = React.useState<UniqueIdentifier | null>(null);
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
242
|
+
// Only a whole-row list needs the split sensors: in handle mode the row is
|
|
243
|
+
// already scrollable because only the handle is touch-none, and swapping
|
|
244
|
+
// sensors there would put a 250 ms delay on the handle for no gain.
|
|
245
|
+
const delayTouch = touchScroll && !handle;
|
|
246
|
+
// Every sensor is created on every render (they are hooks); which ones are
|
|
247
|
+
// handed to DndContext is what changes.
|
|
248
|
+
const pointerSensor = useSensor(PointerSensor, { activationConstraint: { distance: 4 } });
|
|
249
|
+
const mouseSensor = useSensor(MouseSensor, { activationConstraint: { distance: 4 } });
|
|
250
|
+
const touchSensor = useSensor(TouchSensor, {
|
|
251
|
+
activationConstraint: { delay: TOUCH_DELAY_MS, tolerance: TOUCH_TOLERANCE_PX },
|
|
252
|
+
});
|
|
253
|
+
const keyboardSensor = useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates });
|
|
254
|
+
const pointerSensors = useSensors(pointerSensor, keyboardSensor);
|
|
255
|
+
const splitSensors = useSensors(mouseSensor, touchSensor, keyboardSensor);
|
|
256
|
+
const sensors = delayTouch ? splitSensors : pointerSensors;
|
|
196
257
|
const ids = React.useMemo(() => items.map(getId), [items, getId]);
|
|
197
258
|
const activeItem = activeId === null ? undefined : items[ids.indexOf(activeId)];
|
|
198
259
|
|
|
@@ -207,7 +268,12 @@ function SortableList<T>({
|
|
|
207
268
|
const from = ids.indexOf(active.id);
|
|
208
269
|
const to = ids.indexOf(over.id);
|
|
209
270
|
if (from < 0 || to < 0) return;
|
|
210
|
-
onReorder(arrayMove([...items], from, to)
|
|
271
|
+
onReorder(arrayMove([...items], from, to), {
|
|
272
|
+
from,
|
|
273
|
+
to,
|
|
274
|
+
activeId: active.id,
|
|
275
|
+
overId: over.id,
|
|
276
|
+
});
|
|
211
277
|
},
|
|
212
278
|
[ids, items, onReorder],
|
|
213
279
|
);
|
|
@@ -230,6 +296,7 @@ function SortableList<T>({
|
|
|
230
296
|
data-slot="sortable-list"
|
|
231
297
|
data-orientation={orientation}
|
|
232
298
|
data-disabled={disabled || undefined}
|
|
299
|
+
data-touch-scroll={touchScroll ? "" : undefined}
|
|
233
300
|
className={cn(
|
|
234
301
|
"flex list-none gap-2 p-0",
|
|
235
302
|
orientation === "horizontal" ? "flex-row flex-wrap" : "flex-col",
|
|
@@ -244,6 +311,7 @@ function SortableList<T>({
|
|
|
244
311
|
item={item}
|
|
245
312
|
handle={handle}
|
|
246
313
|
disabled={disabled}
|
|
314
|
+
touchScroll={touchScroll}
|
|
247
315
|
renderItem={renderItem}
|
|
248
316
|
className={itemClass}
|
|
249
317
|
/>
|
|
@@ -6,21 +6,38 @@ import { cn } from "@/lib/cn";
|
|
|
6
6
|
export interface SpinnerProps extends React.ComponentProps<"span"> {
|
|
7
7
|
/** Diameter in px. Defaults to 16. */
|
|
8
8
|
size?: number;
|
|
9
|
+
/**
|
|
10
|
+
* `inverse` for a spinner on a filled surface — inside a primary or
|
|
11
|
+
* destructive Button, on a toast, on any `-foreground` pairing. The default
|
|
12
|
+
* ring is `--ds-border` with a `--ds-primary` leading edge, which all but
|
|
13
|
+
* disappears on a primary fill; `inverse` draws both from
|
|
14
|
+
* `--ds-primary-foreground` instead.
|
|
15
|
+
*/
|
|
16
|
+
tone?: "default" | "inverse";
|
|
9
17
|
}
|
|
10
18
|
|
|
11
19
|
/**
|
|
12
|
-
* Spinner — a ring with a
|
|
20
|
+
* Spinner — a ring with a coloured leading edge. Announces itself as
|
|
13
21
|
* `role="status"` with a "Loading" label; pass `aria-label` to override.
|
|
22
|
+
*
|
|
23
|
+
* ```tsx
|
|
24
|
+
* <Button disabled>
|
|
25
|
+
* <Spinner tone="inverse" /> Saving…
|
|
26
|
+
* </Button>
|
|
27
|
+
* ```
|
|
14
28
|
*/
|
|
15
|
-
function Spinner({ className, size = 16, style, ...props }: SpinnerProps) {
|
|
29
|
+
function Spinner({ className, size = 16, tone = "default", style, ...props }: SpinnerProps) {
|
|
16
30
|
return (
|
|
17
31
|
<span
|
|
18
32
|
data-slot="spinner"
|
|
33
|
+
data-tone={tone}
|
|
19
34
|
role="status"
|
|
20
35
|
aria-label="Loading"
|
|
21
36
|
className={cn(
|
|
22
|
-
"inline-block shrink-0 animate-spin rounded-[var(--ds-radius-full)]",
|
|
23
|
-
|
|
37
|
+
"inline-block shrink-0 animate-spin rounded-[var(--ds-radius-full)] border-2",
|
|
38
|
+
tone === "inverse"
|
|
39
|
+
? "border-[var(--ds-primary-foreground)]/35 border-t-[var(--ds-primary-foreground)]"
|
|
40
|
+
: "border-[var(--ds-border)] border-t-[var(--ds-primary)]",
|
|
24
41
|
className,
|
|
25
42
|
)}
|
|
26
43
|
style={{ width: size, height: size, ...style }}
|
|
@@ -3,12 +3,25 @@
|
|
|
3
3
|
import type * as React from "react";
|
|
4
4
|
import { cn } from "@/lib/cn";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The error tone is `destructive`, the same spelling Button, Alert and Badge
|
|
8
|
+
* use. `danger` is a deprecated alias of it and paints the same.
|
|
9
|
+
*/
|
|
10
|
+
type StatusDotTone =
|
|
11
|
+
| "accent"
|
|
12
|
+
| "success"
|
|
13
|
+
| "warning"
|
|
14
|
+
| "destructive"
|
|
15
|
+
/** @deprecated Use `destructive`; this alias paints the same. */
|
|
16
|
+
| "danger"
|
|
17
|
+
| "info"
|
|
18
|
+
| "neutral";
|
|
7
19
|
|
|
8
20
|
const TONE_CLASS: Record<StatusDotTone, string> = {
|
|
9
21
|
accent: "bg-[var(--ds-primary)]",
|
|
10
22
|
success: "bg-[var(--ds-success)]",
|
|
11
23
|
warning: "bg-[var(--ds-warning)]",
|
|
24
|
+
destructive: "bg-[var(--ds-destructive)]",
|
|
12
25
|
danger: "bg-[var(--ds-destructive)]",
|
|
13
26
|
info: "bg-[var(--ds-info)]",
|
|
14
27
|
neutral: "bg-[var(--ds-text-tertiary)]",
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type { AlertProps } from "@/components/alert";
|
|
|
11
11
|
export { Avatar, AvatarFallback, AvatarImage } from "@/components/avatar";
|
|
12
12
|
export { Badge, badgeVariants } from "@/components/badge";
|
|
13
13
|
export type { BadgeProps } from "@/components/badge";
|
|
14
|
+
export { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/breadcrumb";
|
|
15
|
+
export type { BreadcrumbLinkProps } from "@/components/breadcrumb";
|
|
14
16
|
export { ButtonLink } from "@/components/button-link";
|
|
15
17
|
export type { ButtonLinkProps } from "@/components/button-link";
|
|
16
18
|
export { Button, buttonVariants } from "@/components/button";
|
|
@@ -69,14 +71,14 @@ export type { SectionCardProps } from "@/components/section-card";
|
|
|
69
71
|
export { Segmented } from "@/components/segmented";
|
|
70
72
|
export type { SegmentedOption, SegmentedProps } from "@/components/segmented";
|
|
71
73
|
export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue } from "@/components/select";
|
|
72
|
-
export type { SelectContentProps, SelectTriggerProps } from "@/components/select";
|
|
74
|
+
export type { SelectContentProps, SelectProps, SelectTriggerProps } from "@/components/select";
|
|
73
75
|
export { Separator } from "@/components/separator";
|
|
74
76
|
export type { SeparatorProps } from "@/components/separator";
|
|
75
77
|
export { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, sheetVariants } from "@/components/sheet";
|
|
76
|
-
export type { SheetContentProps } from "@/components/sheet";
|
|
78
|
+
export type { SheetContentProps, SheetProps } from "@/components/sheet";
|
|
77
79
|
export { Skeleton } from "@/components/skeleton";
|
|
78
80
|
export { SortableHandle, SortableList, arrayMove } from "@/components/sortable-list";
|
|
79
|
-
export type { SortableHandleProps, SortableListProps, SortableRenderState } from "@/components/sortable-list";
|
|
81
|
+
export type { SortableHandleProps, SortableListProps, SortableRenderState, SortableReorderDetails } from "@/components/sortable-list";
|
|
80
82
|
export { Spinner } from "@/components/spinner";
|
|
81
83
|
export type { SpinnerProps } from "@/components/spinner";
|
|
82
84
|
export { StatusDot } from "@/components/status-dot";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `render` prop for the components that are plain markup rather than Base
|
|
5
|
+
* UI primitives (Card, SectionCard, ButtonLink, BreadcrumbLink).
|
|
6
|
+
*
|
|
7
|
+
* Base UI parts take their own `render` and need nothing from here. The rest
|
|
8
|
+
* are a `<div>` or an `<a>` this package writes itself, and a consumer
|
|
9
|
+
* sometimes needs a different element under the same styling: a Card that is
|
|
10
|
+
* the page's `<aside>` landmark, or a link that is the router's `<Link>`.
|
|
11
|
+
* Passing an element rather than a component keeps the call site readable and
|
|
12
|
+
* lets the caller set props on it (`render={<Link to="/x" />}`).
|
|
13
|
+
*
|
|
14
|
+
* `props` wins over the passed element's own props, so the `className` and
|
|
15
|
+
* handlers a component computed are not silently dropped, while the element's
|
|
16
|
+
* own props (`href`, `to`, `aria-labelledby`) come through untouched. What a
|
|
17
|
+
* component lets a *caller* override is decided by the order it builds `props`
|
|
18
|
+
* in, not here. Children are `children`, falling back to the element's.
|
|
19
|
+
*/
|
|
20
|
+
export type RenderProp = React.ReactElement<Record<string, unknown>>;
|
|
21
|
+
|
|
22
|
+
export function renderWith(
|
|
23
|
+
render: RenderProp | undefined,
|
|
24
|
+
props: Record<string, unknown>,
|
|
25
|
+
children: React.ReactNode,
|
|
26
|
+
Fallback: React.ElementType,
|
|
27
|
+
): React.ReactElement {
|
|
28
|
+
if (!render) {
|
|
29
|
+
const Element = Fallback;
|
|
30
|
+
return <Element {...props}>{children}</Element>;
|
|
31
|
+
}
|
|
32
|
+
const Element = render.type as React.ElementType;
|
|
33
|
+
const given = render.props as { children?: React.ReactNode };
|
|
34
|
+
return (
|
|
35
|
+
<Element {...(render.props as Record<string, unknown>)} {...props}>
|
|
36
|
+
{children ?? given.children}
|
|
37
|
+
</Element>
|
|
38
|
+
);
|
|
39
|
+
}
|