@vvedantb/ui 0.17.0 → 0.18.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 +253 -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/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/compat-shadcn-rgb.css +6 -0
- package/tokens/src/css/markdown.css +318 -0
package/src/ui/carousel.tsx
CHANGED
|
@@ -6,6 +6,12 @@ import useEmblaCarousel, {
|
|
|
6
6
|
import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
|
|
7
7
|
import { cn } from "../utils/cn";
|
|
8
8
|
import { Button } from "./button";
|
|
9
|
+
import { useCn, useUiConfig } from "./ui-config";
|
|
10
|
+
|
|
11
|
+
/** `UiConfig.Carousel`: `classes` replace the built-in `CarouselDots` part classes. */
|
|
12
|
+
export interface CarouselConfig {
|
|
13
|
+
classes?: { dots?: string; dot?: string; dotActive?: string; dotInactive?: string };
|
|
14
|
+
}
|
|
9
15
|
type CarouselApi = UseEmblaCarouselType[1];
|
|
10
16
|
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
|
11
17
|
type CarouselOptions = UseCarouselParameters[0];
|
|
@@ -23,6 +29,11 @@ type CarouselContextProps = {
|
|
|
23
29
|
scrollNext: () => void;
|
|
24
30
|
canScrollPrev: boolean;
|
|
25
31
|
canScrollNext: boolean;
|
|
32
|
+
/** Index of the selected snap. */
|
|
33
|
+
selectedIndex: number;
|
|
34
|
+
/** Embla's snap positions, one per page. */
|
|
35
|
+
scrollSnaps: number[];
|
|
36
|
+
scrollTo: (index: number) => void;
|
|
26
37
|
} & CarouselProps;
|
|
27
38
|
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
|
28
39
|
function useCarousel() {
|
|
@@ -50,10 +61,18 @@ function Carousel({
|
|
|
50
61
|
);
|
|
51
62
|
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
|
52
63
|
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
|
64
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
65
|
+
const [scrollSnaps, setScrollSnaps] = React.useState<number[]>([]);
|
|
53
66
|
const onSelect = (api: CarouselApi) => {
|
|
54
67
|
if (!api) return;
|
|
55
68
|
setCanScrollPrev(api.canScrollPrev());
|
|
56
69
|
setCanScrollNext(api.canScrollNext());
|
|
70
|
+
setSelectedIndex(api.selectedScrollSnap());
|
|
71
|
+
// keep the old array when nothing moved, so this can run on every render
|
|
72
|
+
const next = api.scrollSnapList();
|
|
73
|
+
setScrollSnaps((prev) =>
|
|
74
|
+
prev.length === next.length && prev.every((snap, i) => snap === next[i]) ? prev : next,
|
|
75
|
+
);
|
|
57
76
|
};
|
|
58
77
|
const scrollPrev = () => {
|
|
59
78
|
api?.scrollPrev();
|
|
@@ -61,6 +80,9 @@ function Carousel({
|
|
|
61
80
|
const scrollNext = () => {
|
|
62
81
|
api?.scrollNext();
|
|
63
82
|
};
|
|
83
|
+
const scrollTo = (index: number) => {
|
|
84
|
+
api?.scrollTo(index);
|
|
85
|
+
};
|
|
64
86
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
65
87
|
if (event.key === "ArrowLeft") {
|
|
66
88
|
event.preventDefault();
|
|
@@ -95,6 +117,9 @@ function Carousel({
|
|
|
95
117
|
scrollNext,
|
|
96
118
|
canScrollPrev,
|
|
97
119
|
canScrollNext,
|
|
120
|
+
selectedIndex,
|
|
121
|
+
scrollSnaps,
|
|
122
|
+
scrollTo,
|
|
98
123
|
}}
|
|
99
124
|
>
|
|
100
125
|
<div
|
|
@@ -201,6 +226,39 @@ function CarouselNext({
|
|
|
201
226
|
</Button>
|
|
202
227
|
);
|
|
203
228
|
}
|
|
229
|
+
/** One dot per snap; the selected one is wider. Renders nothing for a single page. */
|
|
230
|
+
function CarouselDots({ className }: { className?: string }) {
|
|
231
|
+
const { scrollSnaps, selectedIndex, scrollTo } = useCarousel();
|
|
232
|
+
const classes = useUiConfig("Carousel")?.classes;
|
|
233
|
+
const merge = useCn();
|
|
234
|
+
|
|
235
|
+
if (scrollSnaps.length <= 1) return null;
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div
|
|
239
|
+
data-slot="carousel-dots"
|
|
240
|
+
className={merge(classes?.dots ?? "flex justify-center gap-1.5 pt-2", className)}
|
|
241
|
+
>
|
|
242
|
+
{scrollSnaps.map((_, index) => (
|
|
243
|
+
<button
|
|
244
|
+
key={index}
|
|
245
|
+
type="button"
|
|
246
|
+
className={merge(
|
|
247
|
+
classes?.dot ??
|
|
248
|
+
"h-1.5 cursor-pointer rounded-full transition-[background-color,transform] duration-150 ease-out active:scale-90",
|
|
249
|
+
index === selectedIndex
|
|
250
|
+
? (classes?.dotActive ?? "w-4 bg-primary")
|
|
251
|
+
: (classes?.dotInactive ??
|
|
252
|
+
"w-1.5 bg-muted-foreground/30 hover:bg-muted-foreground/50"),
|
|
253
|
+
)}
|
|
254
|
+
onClick={() => scrollTo(index)}
|
|
255
|
+
aria-label={`Go to slide ${index + 1}`}
|
|
256
|
+
/>
|
|
257
|
+
))}
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
204
262
|
export {
|
|
205
263
|
type CarouselApi,
|
|
206
264
|
Carousel,
|
|
@@ -208,4 +266,6 @@ export {
|
|
|
208
266
|
CarouselItem,
|
|
209
267
|
CarouselPrevious,
|
|
210
268
|
CarouselNext,
|
|
269
|
+
CarouselDots,
|
|
270
|
+
useCarousel,
|
|
211
271
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Spinner } from "./spinner";
|
|
2
|
+
import { useCn } from "./ui-config";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A loading state for a pane, list or page: {@link Spinner} (with the app's
|
|
6
|
+
* `UiConfig.Spinner` mark, if any), centred in whatever box it is given.
|
|
7
|
+
* `label` names the spinner for assistive tech.
|
|
8
|
+
*
|
|
9
|
+
* A spinner says "wait" as well as a skeleton does, and never has to be
|
|
10
|
+
* re-shaped when the content changes.
|
|
11
|
+
*/
|
|
12
|
+
export function CenteredSpinner({
|
|
13
|
+
label,
|
|
14
|
+
className,
|
|
15
|
+
...props
|
|
16
|
+
}: React.ComponentProps<"div"> & { label: string }) {
|
|
17
|
+
const merge = useCn();
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
aria-busy="true"
|
|
21
|
+
className={merge(
|
|
22
|
+
"flex min-h-0 w-full flex-1 items-center justify-center p-6",
|
|
23
|
+
className,
|
|
24
|
+
)}
|
|
25
|
+
{...props}
|
|
26
|
+
>
|
|
27
|
+
<Spinner aria-label={label} />
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
package/src/ui/checkbox.tsx
CHANGED
|
@@ -2,26 +2,44 @@ import * as React from "react";
|
|
|
2
2
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
|
3
3
|
import { IconCheck } from "@tabler/icons-react";
|
|
4
4
|
import { cn } from "../utils/cn";
|
|
5
|
+
import { useCn, useUiConfig } from "./ui-config";
|
|
6
|
+
|
|
7
|
+
/** `UiConfig.Checkbox`: `classes` replace built-in part classes; `icon` is the default for the prop. */
|
|
8
|
+
export interface CheckboxConfig {
|
|
9
|
+
classes?: { root?: string; indicator?: string };
|
|
10
|
+
icon?: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type CheckboxProps = React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> & {
|
|
14
|
+
/** Shown in the indicator while checked. Default a 14px check icon. */
|
|
15
|
+
icon?: React.ReactNode;
|
|
16
|
+
};
|
|
5
17
|
|
|
6
18
|
const Checkbox = React.forwardRef<
|
|
7
19
|
React.ComponentRef<typeof CheckboxPrimitive.Root>,
|
|
8
|
-
|
|
9
|
-
>(({ className, ...props }, ref) =>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
CheckboxProps
|
|
21
|
+
>(({ className, icon, ...props }, ref) => {
|
|
22
|
+
const config = useUiConfig("Checkbox");
|
|
23
|
+
const merge = useCn();
|
|
24
|
+
return (
|
|
25
|
+
<CheckboxPrimitive.Root
|
|
26
|
+
ref={ref}
|
|
27
|
+
className={merge(
|
|
28
|
+
config?.classes?.root ??
|
|
29
|
+
"peer relative h-4 w-4 shrink-0 rounded-[4px] border border-border bg-field-background 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=checked]:text-primary-foreground before:absolute before:inset-[-12px] before:content-['']",
|
|
30
|
+
className,
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
20
33
|
>
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
<CheckboxPrimitive.Indicator
|
|
35
|
+
className={config?.classes?.indicator ?? cn("flex items-center justify-center text-current")}
|
|
36
|
+
>
|
|
37
|
+
{icon ?? config?.icon ?? <IconCheck className="h-3.5 w-3.5" />}
|
|
38
|
+
</CheckboxPrimitive.Indicator>
|
|
39
|
+
</CheckboxPrimitive.Root>
|
|
40
|
+
);
|
|
41
|
+
});
|
|
25
42
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
|
26
43
|
|
|
27
44
|
export { Checkbox };
|
|
45
|
+
export type { CheckboxProps };
|
|
@@ -1,78 +1,36 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import { IconX } from "@tabler/icons-react";
|
|
4
|
-
import { cn } from "../utils/cn";
|
|
5
3
|
import { fieldFocusClass } from "./_field-classes";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<div className={cn("relative", wrapperClassName)}>
|
|
41
|
-
{leading}
|
|
42
|
-
<input
|
|
43
|
-
ref={mergedRef}
|
|
44
|
-
type="text"
|
|
45
|
-
value={value}
|
|
46
|
-
onChange={(event) => {
|
|
47
|
-
onChange(event.target.value);
|
|
48
|
-
}}
|
|
49
|
-
placeholder={placeholder}
|
|
50
|
-
className={cn(
|
|
51
|
-
"flex h-10 w-full rounded-field border border-border bg-field-background px-3.5 py-2 text-sm transition-[border-color,background-color] duration-200 ease-smooth placeholder:text-field-placeholder disabled:cursor-not-allowed disabled:opacity-50",
|
|
52
|
-
fieldFocusClass,
|
|
53
|
-
"pr-9",
|
|
54
|
-
className,
|
|
55
|
-
)}
|
|
56
|
-
{...props}
|
|
57
|
-
/>
|
|
58
|
-
<button
|
|
59
|
-
type="button"
|
|
60
|
-
tabIndex={hasValue ? 0 : -1}
|
|
61
|
-
className={cn(
|
|
62
|
-
"absolute right-2 top-1/2 inline-flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-[opacity,background-color,color,transform] duration-150 ease-out active:duration-100 active:ease-out active:scale-[0.97] hover:bg-surface-tertiary hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring before:absolute before:inset-[-8px] before:content-['']",
|
|
63
|
-
hasValue ? "opacity-100" : "pointer-events-none opacity-0",
|
|
64
|
-
)}
|
|
65
|
-
aria-hidden={!hasValue}
|
|
66
|
-
aria-label={clearLabel}
|
|
67
|
-
onClick={() => {
|
|
68
|
-
onChange("");
|
|
69
|
-
inputRef.current?.focus({ preventScroll: true });
|
|
70
|
-
}}
|
|
71
|
-
>
|
|
72
|
-
<IconX size={14} stroke={1.75} />
|
|
73
|
-
</button>
|
|
74
|
-
</div>
|
|
75
|
-
);
|
|
76
|
-
},
|
|
77
|
-
);
|
|
78
|
-
export { ClearInput };
|
|
4
|
+
import { ClearInput, type ClearInputProps } from "./clear-input";
|
|
5
|
+
|
|
6
|
+
const altClasses = {
|
|
7
|
+
wrapper: "relative",
|
|
8
|
+
input: `flex h-10 w-full rounded-field border border-border bg-field-background px-3.5 py-2 text-sm transition-[border-color,background-color] duration-200 ease-smooth placeholder:text-field-placeholder disabled:cursor-not-allowed disabled:opacity-50 ${fieldFocusClass}`,
|
|
9
|
+
button:
|
|
10
|
+
"absolute right-2 top-1/2 inline-flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-[opacity,background-color,color,transform] duration-150 ease-out active:duration-100 active:ease-out active:scale-[0.97] hover:bg-surface-tertiary hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring before:absolute before:inset-[-8px] before:content-['']",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The `Alt` look on `ClearInput`'s `animation="none"` path: the clear button
|
|
15
|
+
* stays mounted (`clearButton="always"`), per-instance `classes` replace the
|
|
16
|
+
* main classes (and any `UiConfig.ClearInput` entry), and an explicit
|
|
17
|
+
* `placeholder={undefined}` keeps the input free of the main `placeholder=""`.
|
|
18
|
+
*
|
|
19
|
+
* @deprecated Use `ClearInput`. Kept for existing consumers; removed in a later minor after they move.
|
|
20
|
+
*/
|
|
21
|
+
const ClearInputAlt = React.forwardRef<
|
|
22
|
+
HTMLInputElement,
|
|
23
|
+
Omit<ClearInputProps, "animation" | "clearButton" | "classes">
|
|
24
|
+
>(({ placeholder, ...props }, ref) => (
|
|
25
|
+
<ClearInput
|
|
26
|
+
ref={ref}
|
|
27
|
+
animation="none"
|
|
28
|
+
clearButton="always"
|
|
29
|
+
classes={altClasses}
|
|
30
|
+
placeholder={placeholder}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
));
|
|
34
|
+
ClearInputAlt.displayName = "ClearInputAlt";
|
|
35
|
+
|
|
36
|
+
export { ClearInputAlt };
|
package/src/ui/clear-input.tsx
CHANGED
|
@@ -2,9 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { IconX } from "@tabler/icons-react";
|
|
5
|
-
import {
|
|
5
|
+
import { assignRef } from "../utils/ref";
|
|
6
6
|
import { fieldFocusClass } from "./_field-classes";
|
|
7
7
|
import { runClearInputDissolve } from "./clearInputDissolve";
|
|
8
|
+
import { useCn, useUiConfig } from "./ui-config";
|
|
9
|
+
|
|
10
|
+
type ClearInputAnimation = "dissolve" | "none";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `UiConfig.ClearInput`: `animation` is the default for the prop. `classes`
|
|
14
|
+
* replace the built-in input and button classes, and the wrapper's `relative`
|
|
15
|
+
* in the `none` layout (the `dissolve` wrapper keeps its `t-clear` hooks and
|
|
16
|
+
* adds `classes.wrapper`). The `classes` prop wins over it key by key.
|
|
17
|
+
*/
|
|
18
|
+
export interface ClearInputConfig {
|
|
19
|
+
animation?: ClearInputAnimation;
|
|
20
|
+
classes?: { wrapper?: string; input?: string; button?: string };
|
|
21
|
+
}
|
|
8
22
|
|
|
9
23
|
export interface ClearInputProps extends Omit<
|
|
10
24
|
React.ComponentProps<"input">,
|
|
@@ -16,9 +30,123 @@ export interface ClearInputProps extends Omit<
|
|
|
16
30
|
/** Icon or adornment positioned in the field's left gutter. */
|
|
17
31
|
leading?: React.ReactNode;
|
|
18
32
|
clearLabel?: string;
|
|
33
|
+
/**
|
|
34
|
+
* `dissolve` (default): the text dissolves before the value clears (needs
|
|
35
|
+
* `clear-input.css`). `none`: clear at once, focus the input, and render
|
|
36
|
+
* only the wrapper, input and clear button.
|
|
37
|
+
*/
|
|
38
|
+
animation?: ClearInputAnimation;
|
|
39
|
+
/**
|
|
40
|
+
* With `animation="none"` only. `"filled"` (default): the clear button
|
|
41
|
+
* mounts while there is a value. `"always"`: it stays mounted so the field
|
|
42
|
+
* never reflows; while empty it is hidden (`pointer-events-none opacity-0`,
|
|
43
|
+
* `tabIndex=-1`, `aria-hidden`), while filled it shows (`opacity-100`,
|
|
44
|
+
* `tabIndex=0`), and the input always reserves `pr-9`.
|
|
45
|
+
*/
|
|
46
|
+
clearButton?: "filled" | "always";
|
|
47
|
+
/** Replaces the wrapper, input and button classes for this instance (each key wins over `UiConfig.ClearInput.classes`). */
|
|
48
|
+
classes?: ClearInputConfig["classes"];
|
|
19
49
|
}
|
|
20
50
|
|
|
51
|
+
const inputClasses =
|
|
52
|
+
"flex h-10 w-full rounded-field border border-border bg-field-background px-3.5 py-2 text-base sm:text-sm transition-[border-color,box-shadow,background-color] duration-200 ease-smooth placeholder:text-field-placeholder disabled:cursor-not-allowed disabled:opacity-50";
|
|
53
|
+
|
|
54
|
+
const buttonClasses =
|
|
55
|
+
"absolute right-2 top-1/2 z-4 inline-flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-[background-color,color,transform] duration-100 ease-out active:scale-[0.92] hover:bg-surface-tertiary/50 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring";
|
|
56
|
+
|
|
21
57
|
const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
|
|
58
|
+
({ animation, clearButton, classes, ...props }, ref) => {
|
|
59
|
+
const config = useUiConfig("ClearInput");
|
|
60
|
+
const merged = { ...config?.classes, ...classes };
|
|
61
|
+
if ((animation ?? config?.animation) === "none") {
|
|
62
|
+
return <PlainClearInput ref={ref} classes={merged} clearButton={clearButton} {...props} />;
|
|
63
|
+
}
|
|
64
|
+
return <DissolveClearInput ref={ref} classes={merged} {...props} />;
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
ClearInput.displayName = "ClearInput";
|
|
68
|
+
|
|
69
|
+
type ClearInputPartsProps = Omit<ClearInputProps, "animation" | "clearButton">;
|
|
70
|
+
|
|
71
|
+
const PlainClearInput = React.forwardRef<
|
|
72
|
+
HTMLInputElement,
|
|
73
|
+
ClearInputPartsProps & Pick<ClearInputProps, "clearButton">
|
|
74
|
+
>(
|
|
75
|
+
(
|
|
76
|
+
{
|
|
77
|
+
value,
|
|
78
|
+
onChange,
|
|
79
|
+
wrapperClassName,
|
|
80
|
+
leading,
|
|
81
|
+
clearLabel = "Clear",
|
|
82
|
+
className,
|
|
83
|
+
classes,
|
|
84
|
+
clearButton = "filled",
|
|
85
|
+
...props
|
|
86
|
+
},
|
|
87
|
+
ref,
|
|
88
|
+
) => {
|
|
89
|
+
const merge = useCn();
|
|
90
|
+
const filled = value.length > 0;
|
|
91
|
+
const always = clearButton === "always";
|
|
92
|
+
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
|
93
|
+
|
|
94
|
+
const preventFocusSteal = (event: React.SyntheticEvent) => {
|
|
95
|
+
if (document.activeElement === inputRef.current) {
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div className={merge(classes?.wrapper ?? "relative", wrapperClassName)}>
|
|
102
|
+
{leading}
|
|
103
|
+
<input
|
|
104
|
+
ref={(node) => {
|
|
105
|
+
inputRef.current = node;
|
|
106
|
+
assignRef(ref, node);
|
|
107
|
+
}}
|
|
108
|
+
type="text"
|
|
109
|
+
value={value}
|
|
110
|
+
onChange={(event) => {
|
|
111
|
+
onChange(event.target.value);
|
|
112
|
+
}}
|
|
113
|
+
// A caller `placeholder` in `props` replaces this default in place;
|
|
114
|
+
// an explicit `undefined` drops the attribute.
|
|
115
|
+
placeholder=""
|
|
116
|
+
className={merge(
|
|
117
|
+
classes?.input ?? [inputClasses, fieldFocusClass],
|
|
118
|
+
(always || filled) && "pr-9",
|
|
119
|
+
className,
|
|
120
|
+
)}
|
|
121
|
+
{...props}
|
|
122
|
+
/>
|
|
123
|
+
{always || filled ? (
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
{...(always ? { tabIndex: filled ? 0 : -1 } : null)}
|
|
127
|
+
className={
|
|
128
|
+
always
|
|
129
|
+
? merge(classes?.button ?? buttonClasses, filled ? "opacity-100" : "pointer-events-none opacity-0")
|
|
130
|
+
: (classes?.button ?? buttonClasses)
|
|
131
|
+
}
|
|
132
|
+
{...(always ? { "aria-hidden": !filled } : null)}
|
|
133
|
+
aria-label={clearLabel}
|
|
134
|
+
onPointerDown={preventFocusSteal}
|
|
135
|
+
onMouseDown={preventFocusSteal}
|
|
136
|
+
onClick={() => {
|
|
137
|
+
onChange("");
|
|
138
|
+
inputRef.current?.focus();
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
<IconX size={14} stroke={1.75} />
|
|
142
|
+
</button>
|
|
143
|
+
) : null}
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const DissolveClearInput = React.forwardRef<HTMLInputElement, ClearInputPartsProps>(
|
|
22
150
|
(
|
|
23
151
|
{
|
|
24
152
|
value,
|
|
@@ -28,10 +156,12 @@ const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
|
|
|
28
156
|
clearLabel = "Clear",
|
|
29
157
|
className,
|
|
30
158
|
placeholder = "",
|
|
159
|
+
classes,
|
|
31
160
|
...props
|
|
32
161
|
},
|
|
33
162
|
ref,
|
|
34
163
|
) => {
|
|
164
|
+
const merge = useCn();
|
|
35
165
|
const wrapRef = React.useRef<HTMLDivElement>(null);
|
|
36
166
|
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
|
37
167
|
const mirrorRef = React.useRef<HTMLDivElement>(null);
|
|
@@ -42,11 +172,7 @@ const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
|
|
|
42
172
|
|
|
43
173
|
function mergedRef(node: HTMLInputElement | null) {
|
|
44
174
|
inputRef.current = node;
|
|
45
|
-
|
|
46
|
-
ref(node);
|
|
47
|
-
} else if (ref) {
|
|
48
|
-
ref.current = node;
|
|
49
|
-
}
|
|
175
|
+
assignRef(ref, node);
|
|
50
176
|
}
|
|
51
177
|
|
|
52
178
|
const hasValue = value.length > 0 || clearing;
|
|
@@ -104,15 +230,16 @@ const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
|
|
|
104
230
|
);
|
|
105
231
|
};
|
|
106
232
|
|
|
107
|
-
const fieldClassName =
|
|
233
|
+
const fieldClassName = merge(hasValue && "pr-9", className);
|
|
108
234
|
|
|
109
235
|
return (
|
|
110
236
|
<div
|
|
111
237
|
ref={wrapRef}
|
|
112
|
-
className={
|
|
238
|
+
className={merge(
|
|
113
239
|
"t-clear",
|
|
114
240
|
hasValue && "has-value",
|
|
115
241
|
clearing && "is-clearing",
|
|
242
|
+
classes?.wrapper,
|
|
116
243
|
wrapperClassName,
|
|
117
244
|
)}
|
|
118
245
|
>
|
|
@@ -125,9 +252,8 @@ const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
|
|
|
125
252
|
onChange(event.target.value);
|
|
126
253
|
}}
|
|
127
254
|
placeholder={placeholder}
|
|
128
|
-
className={
|
|
129
|
-
|
|
130
|
-
fieldFocusClass,
|
|
255
|
+
className={merge(
|
|
256
|
+
classes?.input ?? [inputClasses, fieldFocusClass],
|
|
131
257
|
fieldClassName,
|
|
132
258
|
)}
|
|
133
259
|
{...props}
|
|
@@ -135,14 +261,14 @@ const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
|
|
|
135
261
|
<div className="t-clear-clip" aria-hidden="true">
|
|
136
262
|
<div
|
|
137
263
|
ref={mirrorRef}
|
|
138
|
-
className={
|
|
264
|
+
className={merge(
|
|
139
265
|
"t-clear-mirror border-0 bg-transparent text-foreground shadow-none",
|
|
140
266
|
fieldClassName,
|
|
141
267
|
)}
|
|
142
268
|
/>
|
|
143
269
|
<div
|
|
144
270
|
ref={placeholderRef}
|
|
145
|
-
className={
|
|
271
|
+
className={merge(
|
|
146
272
|
"t-clear-placeholder border-0 bg-transparent text-field-placeholder shadow-none",
|
|
147
273
|
fieldClassName,
|
|
148
274
|
)}
|
|
@@ -154,7 +280,7 @@ const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
|
|
|
154
280
|
{value.length > 0 && !clearing ? (
|
|
155
281
|
<button
|
|
156
282
|
type="button"
|
|
157
|
-
className=
|
|
283
|
+
className={classes?.button ? `t-clear-btn ${classes.button}` : `t-clear-btn ${buttonClasses}`}
|
|
158
284
|
aria-label={clearLabel}
|
|
159
285
|
onPointerDown={preventFocusSteal}
|
|
160
286
|
onMouseDown={preventFocusSteal}
|
|
@@ -167,6 +293,5 @@ const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
|
|
|
167
293
|
);
|
|
168
294
|
},
|
|
169
295
|
);
|
|
170
|
-
ClearInput.displayName = "ClearInput";
|
|
171
296
|
|
|
172
297
|
export { ClearInput };
|
package/src/ui/collapsible.tsx
CHANGED
|
@@ -1,7 +1,45 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
1
2
|
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
|
3
|
+
import { useCn, useUiConfig } from "./ui-config";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `UiConfig.Collapsible`: classes for the trigger and the content (for
|
|
7
|
+
* example an app's open/close animation class). The parts have no built-in
|
|
8
|
+
* classes, so without an entry `className` passes through untouched.
|
|
9
|
+
*/
|
|
10
|
+
export interface CollapsibleConfig {
|
|
11
|
+
classes?: { trigger?: string; content?: string };
|
|
12
|
+
}
|
|
2
13
|
|
|
3
14
|
const Collapsible = CollapsiblePrimitive.Root;
|
|
4
|
-
|
|
5
|
-
|
|
15
|
+
|
|
16
|
+
function CollapsibleTrigger({
|
|
17
|
+
className,
|
|
18
|
+
...props
|
|
19
|
+
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
|
20
|
+
const trigger = useUiConfig("Collapsible")?.classes?.trigger;
|
|
21
|
+
const cn = useCn();
|
|
22
|
+
return (
|
|
23
|
+
<CollapsiblePrimitive.CollapsibleTrigger
|
|
24
|
+
className={trigger === undefined ? className : cn(trigger, className)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type CollapsibleContentProps = React.ComponentProps<
|
|
31
|
+
typeof CollapsiblePrimitive.CollapsibleContent
|
|
32
|
+
>;
|
|
33
|
+
|
|
34
|
+
function CollapsibleContent({ className, ...props }: CollapsibleContentProps) {
|
|
35
|
+
const content = useUiConfig("Collapsible")?.classes?.content;
|
|
36
|
+
const cn = useCn();
|
|
37
|
+
return (
|
|
38
|
+
<CollapsiblePrimitive.CollapsibleContent
|
|
39
|
+
className={content === undefined ? className : cn(content, className)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
6
44
|
|
|
7
45
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|