@vvedantb/ui 0.17.1 → 0.18.1
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 +251 -3
- package/package.json +13 -5
- package/shell/src/SidebarResize.tsx +1 -4
- package/src/ai-elements/__snapshots__/queue.test.tsx.snap +11 -0
- package/src/ai-elements/activity-question-utils.ts +26 -0
- package/src/ai-elements/activity-question.tsx +159 -0
- package/src/ai-elements/activity-shared.tsx +459 -0
- package/src/ai-elements/activity-step-detail.tsx +115 -0
- package/src/ai-elements/activity-step-label.ts +981 -0
- package/src/ai-elements/activity-tasks-utils.ts +199 -0
- package/src/ai-elements/activity-tasks.tsx +770 -0
- package/src/ai-elements/artifact.tsx +162 -52
- package/src/ai-elements/attachments.tsx +62 -32
- package/src/ai-elements/code-block.tsx +76 -22
- package/src/ai-elements/context.tsx +370 -0
- package/src/ai-elements/conversation.tsx +186 -62
- package/src/ai-elements/file-chip.tsx +53 -0
- package/src/ai-elements/message.tsx +161 -50
- package/src/ai-elements/model-picker-content.tsx +505 -0
- package/src/ai-elements/model-picker-types.ts +56 -0
- package/src/ai-elements/model-picker.tsx +301 -0
- package/src/ai-elements/model-selector.tsx +102 -21
- package/src/ai-elements/plan.tsx +130 -0
- package/src/ai-elements/prompt-input-settings.tsx +144 -0
- package/src/ai-elements/prompt-input-speech.tsx +189 -0
- package/src/ai-elements/prompt-input-traits.tsx +281 -0
- package/src/ai-elements/prompt-input.tsx +320 -134
- package/src/ai-elements/provider-icon.tsx +133 -0
- package/src/ai-elements/queue.tsx +293 -9
- package/src/ai-elements/shimmer.tsx +72 -1
- package/src/ai-elements/simple-model-ladder.tsx +108 -0
- package/src/ai-elements/speech-input.tsx +5 -3
- package/src/ai-elements/test-results.tsx +510 -0
- package/src/ai-elements/trace.tsx +243 -0
- package/src/ai-elements/use-streaming-disclosure.ts +50 -0
- package/src/ai-elements/web-preview.tsx +349 -0
- package/src/composer/input-area/ComposerSlashMenu.tsx +1 -1
- package/src/index.ts +66 -12
- package/src/kibo/avatar-stack.tsx +40 -0
- package/src/kibo/contribution-graph.tsx +539 -0
- package/src/kibo/cursor.tsx +72 -0
- package/src/kibo/gantt-features.tsx +439 -0
- package/src/kibo/gantt-header.tsx +181 -0
- package/src/kibo/gantt-provider.tsx +657 -0
- package/src/kibo/gantt-sidebar.tsx +157 -0
- package/src/kibo/gantt-timeline.tsx +154 -0
- package/src/kibo/index.ts +151 -0
- package/src/kibo/kanban.tsx +338 -0
- package/src/kibo/kanbanDropTarget.ts +25 -0
- package/src/kibo/list.tsx +162 -0
- package/src/kibo/theme-switcher.tsx +109 -0
- package/src/kibo/video-player.tsx +237 -0
- package/src/markdown/Markdown.tsx +112 -0
- package/src/markdown/MarkdownBlockquote.tsx +45 -0
- package/src/markdown/MarkdownCodeBlock.tsx +197 -0
- package/src/markdown/MarkdownInlineCode.tsx +40 -0
- package/src/markdown/MarkdownLink.tsx +92 -0
- package/src/markdown/codeFence.ts +57 -0
- package/src/markdown/codeHighlight.ts +138 -0
- package/src/markdown/incrementalBlocks.ts +182 -0
- package/src/markdown/index.ts +3 -0
- package/src/markdown/markdownClipboard.ts +178 -0
- package/src/markdown/mermaid.ts +155 -0
- package/src/markdown/rehypeGithubAlerts.ts +80 -0
- package/src/motion/presets.ts +24 -0
- package/src/ui/__snapshots__/alt-wrappers.test.tsx.snap +19 -0
- package/src/ui/__snapshots__/sonner-dom.test.tsx.snap +5 -0
- package/src/ui/_menu-parts.tsx +29 -0
- package/src/ui/accordion.alt.tsx +46 -53
- package/src/ui/accordion.tsx +59 -10
- package/src/ui/avatar.tsx +56 -0
- package/src/ui/badge.tsx +45 -21
- package/src/ui/border-beam.tsx +97 -0
- package/src/ui/button-group.tsx +50 -21
- package/src/ui/button.tsx +67 -45
- package/src/ui/calendar.tsx +48 -5
- package/src/ui/card.tsx +77 -8
- package/src/ui/carousel.tsx +60 -0
- package/src/ui/centered-spinner.tsx +30 -0
- package/src/ui/checkbox.tsx +34 -16
- package/src/ui/clear-input.alt.tsx +33 -75
- package/src/ui/clear-input.tsx +140 -15
- package/src/ui/collapsible.tsx +40 -2
- package/src/ui/command.tsx +143 -70
- package/src/ui/context-menu.tsx +159 -93
- package/src/ui/crossfade-icon.tsx +123 -0
- package/src/ui/dialog.tsx +154 -70
- package/src/ui/dropdown-menu.tsx +177 -117
- package/src/ui/hover-card.tsx +59 -17
- package/src/ui/input-group.alt.tsx +83 -83
- package/src/ui/input-group.tsx +129 -58
- package/src/ui/input.tsx +13 -4
- package/src/ui/label.tsx +11 -8
- package/src/ui/list-row.tsx +232 -0
- package/src/ui/loading-state.tsx +215 -0
- package/src/ui/popover.tsx +56 -23
- package/src/ui/progress.tsx +28 -16
- package/src/ui/refresh-spin-icon.tsx +41 -0
- package/src/ui/search-input.tsx +66 -15
- package/src/ui/select.tsx +171 -100
- package/src/ui/separator.tsx +23 -14
- package/src/ui/slider.tsx +21 -7
- package/src/ui/sonner.tsx +68 -30
- package/src/ui/spinner.tsx +229 -11
- package/src/ui/status-dot.tsx +84 -0
- package/src/ui/surface.tsx +49 -0
- package/src/ui/switch.tsx +29 -15
- package/src/ui/table.tsx +38 -16
- package/src/ui/tabs.tsx +123 -35
- package/src/ui/tabsSliding.ts +33 -1
- package/src/ui/textarea.tsx +13 -4
- package/src/ui/tooltip.tsx +50 -14
- package/src/ui/ui-config.tsx +149 -0
- package/src/utils/cat-color.ts +123 -0
- package/src/utils/dayjs.ts +6 -0
- package/src/utils/gesture.ts +59 -0
- package/src/utils/runtimeVisibility.ts +167 -0
- package/src/utils/sharedClock.ts +96 -0
- package/src/utils/surface-radius.ts +16 -0
- package/src/utils/useDragSensors.ts +58 -0
- package/tokens/examples/tokens-moved/Fixture.tsx +283 -0
- package/tokens/examples/tokens-moved/README.md +20 -0
- package/tokens/examples/tokens-moved/index.html +12 -0
- package/tokens/examples/tokens-moved/main.tsx +6 -0
- package/tokens/examples/tokens-moved/package.json +14 -0
- package/tokens/examples/tokens-moved/styles.css +12 -0
- package/tokens/examples/tokens-moved/tsconfig.json +14 -0
- package/tokens/examples/tokens-moved/vite.config.ts +11 -0
- package/tokens/src/css/border-beam.css +116 -0
- package/tokens/src/css/markdown.css +318 -0
package/src/ui/sonner.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
-
import { Toaster, type ToasterProps } from "sonner";
|
|
2
|
+
import { Toaster as SonnerPrimitive, type ToasterProps } from "sonner";
|
|
3
3
|
import { filledToastIcons } from "./sonner-icons";
|
|
4
|
+
import { useUiConfig } from "./ui-config";
|
|
4
5
|
import "./sonner.css";
|
|
5
6
|
|
|
6
7
|
// Sonner toaster aligned with flat tonal surfaces. `unstyled: true` strips
|
|
@@ -55,8 +56,24 @@ type SonnerToasterOptions = {
|
|
|
55
56
|
* while the stack is collapsed. Default false. `sonner.css` has the same rule.
|
|
56
57
|
*/
|
|
57
58
|
hideStackedContent?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Replaces built-in classes. Toast parts (`toast`, `title`, `loader`, …)
|
|
61
|
+
* replace the package `classNames` per key (`""` removes one); caller
|
|
62
|
+
* `toastOptions.classNames` still merge after. `toaster` replaces
|
|
63
|
+
* `toaster group !font-sans` on the toaster list (`""` removes it).
|
|
64
|
+
*/
|
|
65
|
+
classes?: ToastClassNames & { toaster?: string };
|
|
58
66
|
};
|
|
59
67
|
|
|
68
|
+
/**
|
|
69
|
+
* App defaults for `SonnerToaster` (`UiConfig.SonnerToaster`): every key is a
|
|
70
|
+
* default for the prop of the same name. An explicit prop wins; a prop passed
|
|
71
|
+
* as `undefined` keeps the config value.
|
|
72
|
+
*/
|
|
73
|
+
type SonnerToasterConfig = Omit<ToasterProps, "className" | "toastOptions"> & SonnerToasterOptions;
|
|
74
|
+
|
|
75
|
+
const defaultToasterClassName = "toaster group !font-sans";
|
|
76
|
+
|
|
60
77
|
const hideStackedContentClassName =
|
|
61
78
|
"[&[data-expanded=false][data-front=false]>*]:opacity-0";
|
|
62
79
|
|
|
@@ -161,23 +178,37 @@ function mergeToastOptions(
|
|
|
161
178
|
};
|
|
162
179
|
}
|
|
163
180
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
/** `{ ...defaults, ...props }`, except that a prop set to `undefined` keeps its default. */
|
|
182
|
+
function withConfigDefaults<T extends object>(defaults: Partial<T> | undefined, props: T): T {
|
|
183
|
+
const merged = { ...defaults, ...props };
|
|
184
|
+
for (const key in defaults) {
|
|
185
|
+
const value = defaults[key];
|
|
186
|
+
if (props[key] === undefined && value !== undefined) merged[key] = value;
|
|
187
|
+
}
|
|
188
|
+
return merged;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Defaults come from `UiConfig.SonnerToaster` when the app sets it; explicit props win. */
|
|
192
|
+
function SonnerToaster(props: ToasterProps & SonnerToasterOptions) {
|
|
193
|
+
const {
|
|
194
|
+
classes,
|
|
195
|
+
toastRadius,
|
|
196
|
+
density,
|
|
197
|
+
toastIcons = "default",
|
|
198
|
+
closeButtonStyle = "default",
|
|
199
|
+
sonnerVars = false,
|
|
200
|
+
descriptionMuted,
|
|
201
|
+
titleLeading,
|
|
202
|
+
loadingIcon,
|
|
203
|
+
hideStackedContent,
|
|
204
|
+
toastOptions,
|
|
205
|
+
icons,
|
|
206
|
+
style,
|
|
207
|
+
className,
|
|
208
|
+
closeButton,
|
|
209
|
+
...rest
|
|
210
|
+
} = withConfigDefaults(useUiConfig("SonnerToaster"), props);
|
|
211
|
+
const { toaster: toasterClassName = defaultToasterClassName, ...toastClasses } = classes ?? {};
|
|
181
212
|
const loadingOverride =
|
|
182
213
|
loadingIcon != null ? { loading: loadingIcon } : undefined;
|
|
183
214
|
const mergedIcons =
|
|
@@ -187,26 +218,33 @@ function SonnerToaster({
|
|
|
187
218
|
? { ...loadingOverride, ...icons }
|
|
188
219
|
: icons;
|
|
189
220
|
return (
|
|
190
|
-
<
|
|
221
|
+
<SonnerPrimitive
|
|
191
222
|
expand={false}
|
|
192
223
|
visibleToasts={4}
|
|
193
224
|
gap={12}
|
|
194
|
-
{...
|
|
225
|
+
{...rest}
|
|
195
226
|
closeButton={closeButton ?? (closeButtonStyle === "hoverChip" ? true : undefined)}
|
|
196
|
-
className={
|
|
227
|
+
className={
|
|
228
|
+
toasterClassName && className
|
|
229
|
+
? `${toasterClassName} ${className}`
|
|
230
|
+
: toasterClassName || className || undefined
|
|
231
|
+
}
|
|
197
232
|
icons={mergedIcons}
|
|
198
233
|
style={mergeToasterStyle(sonnerVars ? sonnerVarsStyle : undefined, style)}
|
|
199
234
|
toastOptions={mergeToastOptions(
|
|
200
235
|
{
|
|
201
236
|
unstyled: true,
|
|
202
|
-
classNames:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
237
|
+
classNames: {
|
|
238
|
+
...createToasterClassNames({
|
|
239
|
+
toastRadius,
|
|
240
|
+
density,
|
|
241
|
+
closeButtonStyle,
|
|
242
|
+
descriptionMuted,
|
|
243
|
+
titleLeading,
|
|
244
|
+
hideStackedContent,
|
|
245
|
+
}),
|
|
246
|
+
...toastClasses,
|
|
247
|
+
},
|
|
210
248
|
},
|
|
211
249
|
toastOptions,
|
|
212
250
|
)}
|
|
@@ -224,4 +262,4 @@ export {
|
|
|
224
262
|
sonnerVarsStyle,
|
|
225
263
|
};
|
|
226
264
|
export { filledToastIcons } from "./sonner-icons";
|
|
227
|
-
export type { SonnerToasterOptions };
|
|
265
|
+
export type { SonnerToasterConfig, SonnerToasterOptions };
|
package/src/ui/spinner.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { IconLoader2 } from "@tabler/icons-react";
|
|
2
|
-
import {
|
|
2
|
+
import { bindRuntimeAnimation } from "../utils/runtimeVisibility";
|
|
3
|
+
import { useCn, useUiConfig } from "./ui-config";
|
|
3
4
|
|
|
4
5
|
const sizeClasses = {
|
|
5
6
|
sm: "size-4",
|
|
@@ -7,35 +8,252 @@ const sizeClasses = {
|
|
|
7
8
|
lg: "size-8",
|
|
8
9
|
};
|
|
9
10
|
|
|
11
|
+
/** One outline of a {@link SpinnerMark}: a `<polygon>` (`points`) or a `<path>` (`d`). */
|
|
12
|
+
export type SpinnerMarkShape =
|
|
13
|
+
| { points: string; stroke: string }
|
|
14
|
+
| { d: string; stroke: string };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A logo (or any outline) drawn as strokes, with a dash that runs around each
|
|
18
|
+
* shape forever. Each shape gets `pathLength=1`, so `dashArray` is a fraction
|
|
19
|
+
* of its perimeter (`"0.28 0.72"`: a dash over 28% of the outline).
|
|
20
|
+
*/
|
|
21
|
+
export interface SpinnerMark {
|
|
22
|
+
viewBox: string;
|
|
23
|
+
shapes: readonly SpinnerMarkShape[];
|
|
24
|
+
strokeWidth: number;
|
|
25
|
+
dashArray: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Stroke colours of the `circle` variant. Each defaults to `currentColor`. */
|
|
29
|
+
export interface SpinnerCircleColors {
|
|
30
|
+
/** The faint full ring (drawn at 20% opacity). */
|
|
31
|
+
track?: string;
|
|
32
|
+
/** The longer arc. */
|
|
33
|
+
arc?: string;
|
|
34
|
+
/** The shorter arc drawn over the leading half of `arc`. */
|
|
35
|
+
lead?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SpinnerConfig {
|
|
39
|
+
/** Default for the `mark` prop: every `<Spinner />` without a `variant` draws this mark. */
|
|
40
|
+
mark?: SpinnerMark;
|
|
41
|
+
/** Default for the `colors` prop of the `circle` variant. */
|
|
42
|
+
circle?: SpinnerCircleColors;
|
|
43
|
+
}
|
|
44
|
+
|
|
10
45
|
export type SpinnerProps = Omit<React.ComponentPropsWithoutRef<"svg">, "size" | "stroke"> & {
|
|
11
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* What to draw. Without it: the `mark` (prop, else `UiConfig.Spinner.mark`),
|
|
48
|
+
* else the loader icon. An explicit `variant` wins over the configured mark.
|
|
49
|
+
* - `"icon"`: the rotating loader icon.
|
|
50
|
+
* - `"circle"`: a faint ring with a two-tone arc sweeping around it, for
|
|
51
|
+
* small slots (inline buttons, menu rows) where a mark is too small to read.
|
|
52
|
+
* - `"ring"`: the `Button` loading ring, a fixed 16px (`h-4 w-4`) ring and arc
|
|
53
|
+
* in `currentColor`. Unlabelled (the button's `disabled` and `data-loading`
|
|
54
|
+
* carry the state); `size`, `tone`, `stroke` and `decorative` do not apply.
|
|
55
|
+
*/
|
|
56
|
+
variant?: "icon" | "circle" | "ring";
|
|
57
|
+
/** Named size (a `size-*` class) or a pixel size (`width`/`height`, no size class). Default `"md"`. Not used with `"ring"`. */
|
|
12
58
|
size?: "sm" | "md" | "lg" | number;
|
|
13
|
-
/** Icon stroke width. Default the icon's own (2). */
|
|
59
|
+
/** Icon stroke width. Default the icon's own (2). Only for `"icon"`. */
|
|
14
60
|
stroke?: number | string;
|
|
15
|
-
/** `"muted"` adds `text-muted-foreground`; `"inherit"` adds no colour class, so the icon uses `currentColor`. Default `"muted"`. */
|
|
61
|
+
/** `"muted"` adds `text-muted-foreground`; `"inherit"` adds no colour class, so the icon uses `currentColor`. Default `"muted"`. Only for `"icon"`. */
|
|
16
62
|
tone?: "muted" | "inherit";
|
|
17
|
-
/** Hide from assistive tech (`aria-hidden`, no `role="status"` or label), e.g. inside a labelled button. Default `false`. */
|
|
63
|
+
/** Hide from assistive tech (`aria-hidden`, no `role="status"` or label), e.g. inside a labelled button. Default `false`. Not used with `"ring"`. */
|
|
18
64
|
decorative?: boolean;
|
|
65
|
+
/** Draw this outline with a running dash instead of the loader icon. Default `UiConfig.Spinner.mark`, else none. Ignored with a `variant`. */
|
|
66
|
+
mark?: SpinnerMark;
|
|
67
|
+
/** Stroke colours of the `"circle"` variant. Default `UiConfig.Spinner.circle`, else `currentColor`. */
|
|
68
|
+
colors?: SpinnerCircleColors;
|
|
19
69
|
};
|
|
20
70
|
|
|
21
|
-
|
|
71
|
+
/** Props every variant renderer takes: the svg props plus the shared ones. */
|
|
72
|
+
type VariantProps = Omit<SpinnerProps, "variant" | "mark" | "stroke" | "tone" | "colors">;
|
|
73
|
+
|
|
74
|
+
function a11yProps(decorative: boolean) {
|
|
75
|
+
return decorative ? { "aria-hidden": true } : { role: "status", "aria-label": "Loading" };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Runs the dash around one shape: `stroke-dashoffset` 0 → -1 over 2s, forever,
|
|
80
|
+
* against `pathLength=1`. Web Animations rather than SMIL `<animate>`: SMIL
|
|
81
|
+
* ticks each shape's animated attribute on its own (a style recalc per shape
|
|
82
|
+
* per frame); WAAPI folds into the frame's single recalc and needs no app
|
|
83
|
+
* keyframes. {@link bindRuntimeAnimation} pauses it while the tab is hidden or
|
|
84
|
+
* the shape is off screen. Module-level so the ref identity is stable across
|
|
85
|
+
* renders; React 19 runs the returned cleanup on detach.
|
|
86
|
+
*/
|
|
87
|
+
function traceDash(shape: SVGGeometryElement | null): (() => void) | undefined {
|
|
88
|
+
if (!shape) return undefined;
|
|
89
|
+
const trace = shape.animate(
|
|
90
|
+
[{ strokeDashoffset: 0 }, { strokeDashoffset: -1 }],
|
|
91
|
+
{ duration: 2000, iterations: Infinity },
|
|
92
|
+
);
|
|
93
|
+
return bindRuntimeAnimation(shape, trace);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function MarkSpinner({
|
|
97
|
+
mark,
|
|
98
|
+
size = "md",
|
|
99
|
+
decorative = false,
|
|
100
|
+
className,
|
|
101
|
+
...props
|
|
102
|
+
}: VariantProps & { mark: SpinnerMark }) {
|
|
103
|
+
const pixels = typeof size === "number" ? size : undefined;
|
|
104
|
+
const merge = useCn();
|
|
105
|
+
return (
|
|
106
|
+
<svg
|
|
107
|
+
viewBox={mark.viewBox}
|
|
108
|
+
{...a11yProps(decorative)}
|
|
109
|
+
width={pixels}
|
|
110
|
+
height={pixels}
|
|
111
|
+
className={merge(typeof size === "number" ? undefined : sizeClasses[size], className)}
|
|
112
|
+
{...props}
|
|
113
|
+
>
|
|
114
|
+
{mark.shapes.map((shape, i) =>
|
|
115
|
+
"points" in shape ? (
|
|
116
|
+
<polygon
|
|
117
|
+
key={i}
|
|
118
|
+
ref={traceDash}
|
|
119
|
+
points={shape.points}
|
|
120
|
+
fill="none"
|
|
121
|
+
stroke={shape.stroke}
|
|
122
|
+
strokeWidth={mark.strokeWidth}
|
|
123
|
+
strokeLinejoin="round"
|
|
124
|
+
strokeLinecap="round"
|
|
125
|
+
pathLength={1}
|
|
126
|
+
strokeDasharray={mark.dashArray}
|
|
127
|
+
/>
|
|
128
|
+
) : (
|
|
129
|
+
<path
|
|
130
|
+
key={i}
|
|
131
|
+
ref={traceDash}
|
|
132
|
+
d={shape.d}
|
|
133
|
+
fill="none"
|
|
134
|
+
stroke={shape.stroke}
|
|
135
|
+
strokeWidth={mark.strokeWidth}
|
|
136
|
+
strokeLinejoin="round"
|
|
137
|
+
strokeLinecap="round"
|
|
138
|
+
pathLength={1}
|
|
139
|
+
strokeDasharray={mark.dashArray}
|
|
140
|
+
/>
|
|
141
|
+
),
|
|
142
|
+
)}
|
|
143
|
+
</svg>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Rotates the arc around the viewBox centre, forever. Web Animations rather
|
|
149
|
+
* than a CSS class: WAAPI gives {@link bindRuntimeAnimation} a handle to pause
|
|
150
|
+
* the loop while the tab is hidden or the spinner is off screen, which a CSS
|
|
151
|
+
* `animate-spin` cannot offer. Module-level so the ref identity is stable.
|
|
152
|
+
*/
|
|
153
|
+
function spinArc(arc: SVGGElement): () => void {
|
|
154
|
+
const spin = arc.animate(
|
|
155
|
+
[{ transform: "rotate(0deg)" }, { transform: "rotate(360deg)" }],
|
|
156
|
+
{ duration: 900, iterations: Infinity, easing: "linear" },
|
|
157
|
+
);
|
|
158
|
+
return bindRuntimeAnimation(arc, spin);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The `circle` variant: a faint full ring with a two-tone arc sweeping around it.
|
|
163
|
+
*
|
|
164
|
+
* The arc is two circles rather than one gradient stroke: a gradient only
|
|
165
|
+
* samples the slice of its box the arc crosses, so at 12–16px one end washes
|
|
166
|
+
* out. Drawing `lead` over the leading half of a longer `arc` dash keeps both
|
|
167
|
+
* colours legible at every size, and needs no per-instance gradient id.
|
|
168
|
+
*/
|
|
169
|
+
function CircleSpinner({
|
|
170
|
+
colors,
|
|
22
171
|
size = "md",
|
|
23
|
-
tone = "muted",
|
|
24
172
|
decorative = false,
|
|
25
173
|
className,
|
|
26
174
|
...props
|
|
27
|
-
}:
|
|
175
|
+
}: VariantProps & { colors: SpinnerCircleColors }) {
|
|
176
|
+
const merge = useCn();
|
|
177
|
+
const pixels = typeof size === "number" ? size : undefined;
|
|
178
|
+
const { track = "currentColor", arc = "currentColor", lead = "currentColor" } = colors;
|
|
179
|
+
return (
|
|
180
|
+
<svg
|
|
181
|
+
viewBox="0 0 24 24"
|
|
182
|
+
{...a11yProps(decorative)}
|
|
183
|
+
width={pixels}
|
|
184
|
+
height={pixels}
|
|
185
|
+
className={merge(typeof size === "number" ? undefined : sizeClasses[size], className)}
|
|
186
|
+
{...props}
|
|
187
|
+
>
|
|
188
|
+
<circle cx="12" cy="12" r="9" fill="none" stroke={track} strokeWidth={3} opacity={0.2} />
|
|
189
|
+
<g ref={spinArc} style={{ transformBox: "view-box", transformOrigin: "12px 12px" }}>
|
|
190
|
+
<circle
|
|
191
|
+
cx="12"
|
|
192
|
+
cy="12"
|
|
193
|
+
r="9"
|
|
194
|
+
fill="none"
|
|
195
|
+
stroke={arc}
|
|
196
|
+
strokeWidth={3}
|
|
197
|
+
strokeLinecap="round"
|
|
198
|
+
pathLength={1}
|
|
199
|
+
strokeDasharray="0.36 0.64"
|
|
200
|
+
/>
|
|
201
|
+
<circle
|
|
202
|
+
cx="12"
|
|
203
|
+
cy="12"
|
|
204
|
+
r="9"
|
|
205
|
+
fill="none"
|
|
206
|
+
stroke={lead}
|
|
207
|
+
strokeWidth={3}
|
|
208
|
+
strokeLinecap="round"
|
|
209
|
+
pathLength={1}
|
|
210
|
+
strokeDasharray="0.18 0.82"
|
|
211
|
+
/>
|
|
212
|
+
</g>
|
|
213
|
+
</svg>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The `ring` variant: the `Button` loading ring, a fixed markup with a CSS `animate-spin`. */
|
|
218
|
+
function RingSpinner({
|
|
219
|
+
className,
|
|
220
|
+
size: _size,
|
|
221
|
+
decorative: _decorative,
|
|
222
|
+
...props
|
|
223
|
+
}: VariantProps) {
|
|
224
|
+
const merge = useCn();
|
|
225
|
+
return (
|
|
226
|
+
<svg className={merge("h-4 w-4 animate-spin", className)} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" {...props}>
|
|
227
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
228
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
229
|
+
</svg>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The one loading spinner: the loader icon, an app mark with a running dash,
|
|
235
|
+
* the two-tone `circle`, or the `Button` `ring`. See {@link SpinnerProps.variant}.
|
|
236
|
+
*/
|
|
237
|
+
function Spinner({ variant, mark, colors, stroke, tone = "muted", ...props }: SpinnerProps) {
|
|
238
|
+
const config = useUiConfig("Spinner");
|
|
239
|
+
const merge = useCn();
|
|
240
|
+
if (variant === "ring") return <RingSpinner {...props} />;
|
|
241
|
+
if (variant === "circle") return <CircleSpinner colors={colors ?? config?.circle ?? {}} {...props} />;
|
|
242
|
+
const resolvedMark = variant ? undefined : (mark ?? config?.mark);
|
|
243
|
+
if (resolvedMark) return <MarkSpinner mark={resolvedMark} {...props} />;
|
|
244
|
+
const { size = "md", decorative = false, className, ...iconProps } = props;
|
|
28
245
|
return (
|
|
29
246
|
<IconLoader2
|
|
30
|
-
{...(decorative
|
|
247
|
+
{...a11yProps(decorative)}
|
|
31
248
|
size={typeof size === "number" ? size : undefined}
|
|
32
|
-
|
|
249
|
+
stroke={stroke}
|
|
250
|
+
className={merge(
|
|
33
251
|
"animate-spin",
|
|
34
252
|
tone === "muted" && "text-muted-foreground",
|
|
35
253
|
typeof size === "number" ? undefined : sizeClasses[size],
|
|
36
254
|
className,
|
|
37
255
|
)}
|
|
38
|
-
{...
|
|
256
|
+
{...iconProps}
|
|
39
257
|
/>
|
|
40
258
|
);
|
|
41
259
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { HTMLAttributes, Ref } from "react";
|
|
2
|
+
|
|
3
|
+
import { useCn, useUiConfig } from "./ui-config";
|
|
4
|
+
|
|
5
|
+
export type StatusTone =
|
|
6
|
+
| "progress"
|
|
7
|
+
| "business-review"
|
|
8
|
+
| "code-review"
|
|
9
|
+
| "done"
|
|
10
|
+
| "cancelled"
|
|
11
|
+
/**
|
|
12
|
+
* Live/online/enabled. Distinct from `done`: this is a *state* something is
|
|
13
|
+
* currently in, not a workflow stage it has reached.
|
|
14
|
+
*/
|
|
15
|
+
| "active"
|
|
16
|
+
/** Something is wrong and needs attention: a critical finding, a hard error. */
|
|
17
|
+
| "critical"
|
|
18
|
+
| "neutral";
|
|
19
|
+
|
|
20
|
+
export interface StatusDotConfig {
|
|
21
|
+
/** Replaces the built-in classes of a part. */
|
|
22
|
+
classes?: {
|
|
23
|
+
/** Fill class per tone. A tone left out keeps its built-in class. */
|
|
24
|
+
tone?: Partial<Record<StatusTone, string>>;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Plain Record so Tailwind's static-class scanner can see every literal
|
|
29
|
+
// class here — a computed `bg-${tone}` string would not survive the purge.
|
|
30
|
+
const TONE_CLASS: Record<StatusTone, string> = {
|
|
31
|
+
progress: "bg-warning",
|
|
32
|
+
"business-review": "bg-primary",
|
|
33
|
+
"code-review": "bg-primary/70",
|
|
34
|
+
done: "bg-success",
|
|
35
|
+
cancelled: "bg-muted-foreground/40",
|
|
36
|
+
active: "bg-success",
|
|
37
|
+
critical: "bg-destructive",
|
|
38
|
+
neutral: "bg-muted-foreground/60",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const SIZE_CLASS: Record<"sm" | "md", string> = {
|
|
42
|
+
sm: "size-1.5",
|
|
43
|
+
md: "size-2",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export interface StatusDotProps extends HTMLAttributes<HTMLSpanElement> {
|
|
47
|
+
tone: StatusTone;
|
|
48
|
+
/** Slightly larger dot for detail views. Default "sm". */
|
|
49
|
+
size?: "sm" | "md";
|
|
50
|
+
ref?: Ref<HTMLSpanElement>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Linear-style status glyph: a small coloured dot that carries the status,
|
|
55
|
+
* so the label next to it can stay neutral text instead of a coloured pill.
|
|
56
|
+
*
|
|
57
|
+
* `ref` is load-bearing: call sites make the dot the tooltip anchor via
|
|
58
|
+
* `<TooltipTrigger asChild>`, and Radix composes its ref onto that child to
|
|
59
|
+
* position the popper.
|
|
60
|
+
*/
|
|
61
|
+
export function StatusDot({
|
|
62
|
+
tone,
|
|
63
|
+
size = "sm",
|
|
64
|
+
className,
|
|
65
|
+
ref,
|
|
66
|
+
"aria-hidden": ariaHidden = true,
|
|
67
|
+
...props
|
|
68
|
+
}: StatusDotProps) {
|
|
69
|
+
const config = useUiConfig("StatusDot");
|
|
70
|
+
const merge = useCn();
|
|
71
|
+
return (
|
|
72
|
+
<span
|
|
73
|
+
ref={ref}
|
|
74
|
+
aria-hidden={ariaHidden}
|
|
75
|
+
className={merge(
|
|
76
|
+
"rounded-full shrink-0",
|
|
77
|
+
SIZE_CLASS[size],
|
|
78
|
+
config?.classes?.tone?.[tone] ?? TONE_CLASS[tone],
|
|
79
|
+
className,
|
|
80
|
+
)}
|
|
81
|
+
{...props}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
|
|
4
|
+
import { useCn } from "./ui-config";
|
|
5
|
+
import { SURFACE_RADIUS_CLASS } from "../utils/surface-radius";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A panel with the card fill and one of four paddings.
|
|
9
|
+
*
|
|
10
|
+
* `Card` splits padding across Header / Content / Footer, which is right for a
|
|
11
|
+
* titled card and wrong for a box that just needs one padding. Density is a
|
|
12
|
+
* prop rather than a `className` so the paddings stay a closed set;
|
|
13
|
+
* `className` still overrides when a layout needs it.
|
|
14
|
+
*/
|
|
15
|
+
const surfaceVariants = cva(
|
|
16
|
+
`${SURFACE_RADIUS_CLASS} bg-card text-card-foreground`,
|
|
17
|
+
{
|
|
18
|
+
variants: {
|
|
19
|
+
density: {
|
|
20
|
+
// Dense list items and inline panels.
|
|
21
|
+
tight: "p-3",
|
|
22
|
+
// The default box: settings sections, stat cards, banners.
|
|
23
|
+
default: "p-4",
|
|
24
|
+
// Standalone cards that carry a heading of their own.
|
|
25
|
+
comfortable: "p-6",
|
|
26
|
+
// The caller owns padding entirely — for surfaces with their own
|
|
27
|
+
// internal regions, such as a table that must reach the edge.
|
|
28
|
+
none: "",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: {
|
|
32
|
+
density: "default",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export interface SurfaceProps
|
|
38
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
39
|
+
VariantProps<typeof surfaceVariants> {}
|
|
40
|
+
|
|
41
|
+
const Surface = React.forwardRef<HTMLDivElement, SurfaceProps>(
|
|
42
|
+
({ className, density, ...props }, ref) => {
|
|
43
|
+
const merge = useCn();
|
|
44
|
+
return <div ref={ref} className={merge(surfaceVariants({ density }), className)} {...props} />;
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
Surface.displayName = "Surface";
|
|
48
|
+
|
|
49
|
+
export { Surface, surfaceVariants };
|
package/src/ui/switch.tsx
CHANGED
|
@@ -1,26 +1,40 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
|
3
3
|
import { cn } from "../utils/cn";
|
|
4
|
+
import { useCn, useUiConfig } from "./ui-config";
|
|
5
|
+
|
|
6
|
+
/** `UiConfig.Switch`: `classes` replace the built-in part classes. */
|
|
7
|
+
export interface SwitchConfig {
|
|
8
|
+
classes?: { root?: string; thumb?: string };
|
|
9
|
+
}
|
|
4
10
|
|
|
5
11
|
const Switch = React.forwardRef<
|
|
6
12
|
React.ComponentRef<typeof SwitchPrimitive.Root>,
|
|
7
13
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>
|
|
8
|
-
>(({ className, ...props }, ref) =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<SwitchPrimitive.Thumb
|
|
18
|
-
className={cn(
|
|
19
|
-
"pointer-events-none block size-4 rounded-full bg-background ring-0 transition-[transform,background-color] duration-200 ease-smooth data-[state=checked]:translate-x-4 data-[state=checked]:bg-primary-foreground data-[state=unchecked]:translate-x-0 dark:data-[state=unchecked]:bg-foreground",
|
|
14
|
+
>(({ className, ...props }, ref) => {
|
|
15
|
+
const classes = useUiConfig("Switch")?.classes;
|
|
16
|
+
const merge = useCn();
|
|
17
|
+
return (
|
|
18
|
+
<SwitchPrimitive.Root
|
|
19
|
+
className={merge(
|
|
20
|
+
classes?.root ??
|
|
21
|
+
"peer relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-border bg-default transition-[background-color,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=unchecked]:bg-default dark:data-[state=unchecked]:border-muted/60 dark:data-[state=unchecked]:bg-muted/40 before:absolute before:inset-[-8px] before:content-['']", // check-tokens-allow: muted
|
|
22
|
+
className,
|
|
20
23
|
)}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
{...props}
|
|
25
|
+
ref={ref}
|
|
26
|
+
>
|
|
27
|
+
<SwitchPrimitive.Thumb
|
|
28
|
+
className={
|
|
29
|
+
classes?.thumb ??
|
|
30
|
+
cn(
|
|
31
|
+
"pointer-events-none block size-4 rounded-full bg-background ring-0 transition-[transform,background-color] duration-200 ease-smooth data-[state=checked]:translate-x-4 data-[state=checked]:bg-primary-foreground data-[state=unchecked]:translate-x-0 dark:data-[state=unchecked]:bg-foreground",
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
/>
|
|
35
|
+
</SwitchPrimitive.Root>
|
|
36
|
+
);
|
|
37
|
+
});
|
|
24
38
|
Switch.displayName = SwitchPrimitive.Root.displayName;
|
|
25
39
|
|
|
26
40
|
export { Switch };
|