@kznlabs/labsui 0.1.5 → 0.1.7
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 +5 -0
- package/dist/index.js +597 -157
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -231,6 +231,7 @@ export function AccentPicker({ className }: { className?: string }) {
|
|
|
231
231
|
return (
|
|
232
232
|
<fieldset className={cn("flex items-center gap-1.5", className)}>
|
|
233
233
|
<legend className="sr-only">Accent color</legend>
|
|
234
|
+
|
|
234
235
|
{accents.map((accent) => (
|
|
235
236
|
<label
|
|
236
237
|
key={accent}
|
|
@@ -245,12 +246,15 @@ export function AccentPicker({ className }: { className?: string }) {
|
|
|
245
246
|
onChange={() => setAccent(accent)}
|
|
246
247
|
className="peer sr-only"
|
|
247
248
|
/>
|
|
249
|
+
|
|
248
250
|
<span className="sr-only">{accent}</span>
|
|
251
|
+
|
|
249
252
|
<span
|
|
250
253
|
aria-hidden="true"
|
|
251
254
|
style={{ backgroundColor: swatch[accent] }}
|
|
252
255
|
className="size-4 rounded-full ring-1 ring-foreground/10 ring-inset transition-transform duration-200 peer-hover:scale-110 peer-focus-visible:ring-[3px] peer-focus-visible:ring-ring/50"
|
|
253
256
|
/>
|
|
257
|
+
|
|
254
258
|
{current === accent && (
|
|
255
259
|
<motion.span
|
|
256
260
|
aria-hidden="true"
|
|
@@ -418,6 +422,7 @@ export function AccordionTrigger({
|
|
|
418
422
|
)}
|
|
419
423
|
>
|
|
420
424
|
{children}
|
|
425
|
+
|
|
421
426
|
<motion.span
|
|
422
427
|
animate={{ rotate: open ? 180 : 0 }}
|
|
423
428
|
transition={spring}
|
|
@@ -537,7 +542,9 @@ export function Alert({
|
|
|
537
542
|
{glyph}
|
|
538
543
|
</span>
|
|
539
544
|
)}
|
|
545
|
+
|
|
540
546
|
<div className="grid min-w-0 flex-1 gap-1">{children}</div>
|
|
547
|
+
|
|
541
548
|
{action && <div className="-my-1 shrink-0 self-center">{action}</div>}
|
|
542
549
|
</div>
|
|
543
550
|
);
|
|
@@ -670,14 +677,17 @@ export function AppWindow({
|
|
|
670
677
|
);
|
|
671
678
|
})}
|
|
672
679
|
</div>
|
|
680
|
+
|
|
673
681
|
{header ??
|
|
674
682
|
(title && (
|
|
675
683
|
<p className="pointer-events-none absolute inset-x-24 truncate text-center font-medium text-muted-foreground text-xs">
|
|
676
684
|
{title}
|
|
677
685
|
</p>
|
|
678
686
|
))}
|
|
687
|
+
|
|
679
688
|
{actions && <div className="ml-auto flex items-center gap-1.5">{actions}</div>}
|
|
680
689
|
</div>
|
|
690
|
+
|
|
681
691
|
<div className={cn("flex-1", bodyClassName)}>{children}</div>
|
|
682
692
|
</div>
|
|
683
693
|
);
|
|
@@ -716,7 +726,7 @@ export function AspectRatio({ ratio = 16 / 9, className, style, ...props }: Aspe
|
|
|
716
726
|
}
|
|
717
727
|
` }] }, attachment: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "attachment", type: "registry:ui", title: "Attachment & Dropzone", description: "File chips with image thumbnails or type icons, size, a springing upload bar, error state and remove; the list springs chips in and closes the gap. FileDropzone wraps a real file input: click, keyboard and the native picker work, drop and paste are checked against accept and maxSize, and dropped files land in the input so forms and Server Actions receive them.", docs: "https://kznlabs.github.io/labsUI/components/attachment", usage: '<FileDropzone name="clips" multiple accept="video/*" maxSize={50 * 1024 * 1024} onFiles={add} onReject={warn} /><AttachmentList>{files.map((f) => <Attachment key={f.id} name={f.name} size={f.size} type={f.type} progress={f.progress} onRemove={() => remove(f.id)} />)}</AttachmentList>', examples: [{ name: "clip-upload.tsx", code: `import { FileDropzone } from "@/components/ui/attachment";
|
|
718
728
|
import { Button } from "@/components/ui/button";
|
|
719
|
-
import { uploadClips } from "./actions";
|
|
729
|
+
import { uploadClips } from "./actions";
|
|
720
730
|
|
|
721
731
|
// Picked and dropped files both land in the input, so the Server Action gets them.
|
|
722
732
|
export function ClipUpload() {
|
|
@@ -747,8 +757,14 @@ export function Attachments({ files, onRemove }: { files: File[]; onRemove: (id:
|
|
|
747
757
|
))}
|
|
748
758
|
</AttachmentList>
|
|
749
759
|
);
|
|
750
|
-
}` }
|
|
751
|
-
|
|
760
|
+
}` }, { name: "actions.ts", code: `"use server";
|
|
761
|
+
|
|
762
|
+
export async function uploadClips(formData: FormData) {
|
|
763
|
+
const clips = formData.getAll("clips") as File[];
|
|
764
|
+
|
|
765
|
+
console.log(clips.map((clip) => clip.name));
|
|
766
|
+
}` }], api: { entries: [{ kind: "function", name: "formatBytes", signature: "formatBytes(bytes: number, locale?: string) => string", description: 'Bytes as text in the given locale: 1536 is "1.5 kB".' }, { kind: "component", name: "Attachment", props: [{ name: "name", type: "string", required: true, description: "File name." }, { name: "error", type: "string", required: false, description: "Replaces the size line and turns the chip red." }, { name: "href", type: "string", required: false, description: "Makes the name a download link." }, { name: "locale", type: "string", required: false, description: "BCP 47 locale for the size." }, { name: "onRemove", type: "() => void", required: false, description: "Shows a remove button; called when it is pressed." }, { name: "progress", type: "number", required: false, description: "0 to 100 while uploading; the bar springs along the bottom edge." }, { name: "size", type: "number", required: false, description: "Bytes." }, { name: "src", type: "string", required: false, description: "Thumbnail source: a URL, or `URL.createObjectURL(file)` for a fresh upload." }, { name: "type", type: "string", required: false, description: "MIME type; picks the icon, and image types show `src` as a thumbnail." }], description: "File chip: thumbnail or type icon, name, size, upload progress or error, and remove." }, { kind: "component", name: "AttachmentList", props: [], extends: "<ul>", description: "Attachments that spring in, out and into place when one is removed. Key each child." }, { kind: "component", name: "FileDropzone", props: [{ name: "accept", type: "string", required: false, description: 'Same syntax as the input attribute: ".pdf,image/*". Enforced on drop and paste too.' }, { name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "children", type: "ReactNode", required: false, description: 'Replaces the default "Drop files or browse" content.' }, { name: "disabled", type: "boolean", required: false, description: "Blocks drops and the picker; no file is submitted." }, { name: "id", type: "string", required: false, description: "Id of the file input; Field fills it in." }, { name: "maxSize", type: "number", required: false, description: "Bytes." }, { name: "multiple", type: "boolean", required: false, description: "Accept more than one file." }, { name: "name", type: "string", required: false, description: "Form field name: picked and dropped files both submit with the form." }, { name: "onFiles", type: "(files: File[]) => void", required: false, description: "Files that passed `accept` and `maxSize`, from a drop or the picker." }, { name: "onReject", type: "(rejected: FileRejection[]) => void", required: false, description: "Files that failed `accept` or `maxSize`, each with a reason." }, { name: "required", type: "boolean", required: false, description: "Form submit needs at least one file." }], description: `Drop area around a real file input: click, Enter/Space and the native picker work as-is, and
|
|
767
|
+
dropped files are written back into the input so plain forms and Server Actions get them.` }], types: { FileRejection: '{ file: File; reason: "type" | "size" }' } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/attachment.tsx", type: "registry:ui", content: `"use client";
|
|
752
768
|
|
|
753
769
|
import {
|
|
754
770
|
CloudUpload,
|
|
@@ -904,6 +920,7 @@ export function Attachment({
|
|
|
904
920
|
<Icon className="size-5" aria-hidden="true" />
|
|
905
921
|
)}
|
|
906
922
|
</span>
|
|
923
|
+
|
|
907
924
|
<span className="grid min-w-0 flex-1">
|
|
908
925
|
{href ? (
|
|
909
926
|
<a
|
|
@@ -916,6 +933,7 @@ export function Attachment({
|
|
|
916
933
|
) : (
|
|
917
934
|
<span className="truncate font-medium text-sm">{name}</span>
|
|
918
935
|
)}
|
|
936
|
+
|
|
919
937
|
<span
|
|
920
938
|
className={cn(
|
|
921
939
|
"truncate text-xs tabular-nums",
|
|
@@ -925,6 +943,7 @@ export function Attachment({
|
|
|
925
943
|
{meta || " "}
|
|
926
944
|
</span>
|
|
927
945
|
</span>
|
|
946
|
+
|
|
928
947
|
{onRemove && (
|
|
929
948
|
<button
|
|
930
949
|
type="button"
|
|
@@ -935,6 +954,7 @@ export function Attachment({
|
|
|
935
954
|
<X className="size-3.5" aria-hidden="true" />
|
|
936
955
|
</button>
|
|
937
956
|
)}
|
|
957
|
+
|
|
938
958
|
{uploading && (
|
|
939
959
|
<motion.span
|
|
940
960
|
aria-hidden="true"
|
|
@@ -963,7 +983,7 @@ export function AttachmentList({ className, children, ...props }: ComponentProps
|
|
|
963
983
|
|
|
964
984
|
/* ---------- FileDropzone ---------- */
|
|
965
985
|
|
|
966
|
-
type
|
|
986
|
+
export type FileRejection = { file: File; reason: "type" | "size" };
|
|
967
987
|
|
|
968
988
|
type FileDropzoneProps = {
|
|
969
989
|
/** Same syntax as the input attribute: ".pdf,image/*". Enforced on drop and paste too. */
|
|
@@ -975,12 +995,12 @@ type FileDropzoneProps = {
|
|
|
975
995
|
/** Files that passed \`accept\` and \`maxSize\`, from a drop or the picker. */
|
|
976
996
|
onFiles?: (files: File[]) => void;
|
|
977
997
|
/** Files that failed \`accept\` or \`maxSize\`, each with a reason. */
|
|
978
|
-
onReject?: (rejected:
|
|
998
|
+
onReject?: (rejected: FileRejection[]) => void;
|
|
979
999
|
/** Form field name: picked and dropped files both submit with the form. */
|
|
980
1000
|
name?: string;
|
|
981
1001
|
/** Id of the file input; Field fills it in. */
|
|
982
1002
|
id?: string;
|
|
983
|
-
/** Blocks drops and the picker. */
|
|
1003
|
+
/** Blocks drops and the picker; no file is submitted. */
|
|
984
1004
|
disabled?: boolean;
|
|
985
1005
|
/** Form submit needs at least one file. */
|
|
986
1006
|
required?: boolean;
|
|
@@ -1019,7 +1039,7 @@ export function FileDropzone(allProps: FileDropzoneProps) {
|
|
|
1019
1039
|
const all = [...list];
|
|
1020
1040
|
const picked = multiple ? all : all.slice(0, 1);
|
|
1021
1041
|
const ok: File[] = [];
|
|
1022
|
-
const rejected:
|
|
1042
|
+
const rejected: FileRejection[] = [];
|
|
1023
1043
|
|
|
1024
1044
|
for (const file of picked) {
|
|
1025
1045
|
if (!accepts(file, accept)) rejected.push({ file, reason: "type" });
|
|
@@ -1075,7 +1095,7 @@ export function FileDropzone(allProps: FileDropzoneProps) {
|
|
|
1075
1095
|
}}
|
|
1076
1096
|
onDrop={onDrop}
|
|
1077
1097
|
onPaste={(event) =>
|
|
1078
|
-
event.clipboardData.files.length > 0 && take(event.clipboardData.files, false)
|
|
1098
|
+
!disabled && event.clipboardData.files.length > 0 && take(event.clipboardData.files, false)
|
|
1079
1099
|
}
|
|
1080
1100
|
className={cn(
|
|
1081
1101
|
"flex cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border border-dashed bg-card p-6 text-center transition-colors has-[:focus-visible]:ring-[3px] has-[:focus-visible]:ring-ring/50",
|
|
@@ -1095,15 +1115,18 @@ export function FileDropzone(allProps: FileDropzoneProps) {
|
|
|
1095
1115
|
className="sr-only"
|
|
1096
1116
|
{...input}
|
|
1097
1117
|
/>
|
|
1118
|
+
|
|
1098
1119
|
{children ?? (
|
|
1099
1120
|
<>
|
|
1100
1121
|
<span className="grid size-10 place-items-center rounded-full bg-muted text-muted-foreground">
|
|
1101
1122
|
<CloudUpload className="size-5" aria-hidden="true" />
|
|
1102
1123
|
</span>
|
|
1124
|
+
|
|
1103
1125
|
<span className="text-sm">
|
|
1104
1126
|
<span className="font-medium">Drop {multiple ? "files" : "a file"}</span> or{" "}
|
|
1105
1127
|
<span className="font-medium text-primary-ink">browse</span>
|
|
1106
1128
|
</span>
|
|
1129
|
+
|
|
1107
1130
|
{hint && <span className="text-muted-foreground text-xs">{hint}</span>}
|
|
1108
1131
|
</>
|
|
1109
1132
|
)}
|
|
@@ -1176,6 +1199,7 @@ export function Avatar({ src, alt, fallback, status, size = "default", className
|
|
|
1176
1199
|
className="relative flex size-full select-none items-center justify-center overflow-hidden rounded-full bg-muted font-medium text-muted-foreground ring-1 ring-border"
|
|
1177
1200
|
>
|
|
1178
1201
|
<span aria-hidden="true">{fallback ?? initials(alt)}</span>
|
|
1202
|
+
|
|
1179
1203
|
{showImg && (
|
|
1180
1204
|
// biome-ignore lint/performance/noImgElement: arbitrary remote avatars; no next/image loader config
|
|
1181
1205
|
<img
|
|
@@ -1196,6 +1220,7 @@ export function Avatar({ src, alt, fallback, status, size = "default", className
|
|
|
1196
1220
|
/>
|
|
1197
1221
|
)}
|
|
1198
1222
|
</span>
|
|
1223
|
+
|
|
1199
1224
|
{status && (
|
|
1200
1225
|
<span
|
|
1201
1226
|
className={cn(
|
|
@@ -1213,14 +1238,14 @@ export function Avatar({ src, alt, fallback, status, size = "default", className
|
|
|
1213
1238
|
` }] }, "avatar-group": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "avatar-group", type: "registry:ui", title: "AvatarGroup", description: "Overlapping avatars with a +N overflow chip. Hover gently enlarges the avatar on a spring, keeping stack order.", docs: "https://kznlabs.github.io/labsUI/components/avatar-group", usage: '<AvatarGroup users={team} max={4} size="lg" />', examples: [{ name: "team.tsx", code: `import { AvatarGroup } from "@/components/ui/avatar-group";
|
|
1214
1239
|
export function Team({ users }: { users: { alt: string; src?: string }[] }) {
|
|
1215
1240
|
return <AvatarGroup users={users} max={4} size="lg" />;
|
|
1216
|
-
}` }], api: { entries: [{ kind: "component", name: "AvatarGroup", props: [{ name: "users", type: "
|
|
1241
|
+
}` }], api: { entries: [{ kind: "component", name: "AvatarGroup", props: [{ name: "users", type: "AvatarGroupUser[]", required: true, description: "People in order; `alt` is each one's name." }, { name: "max", type: "number", required: false, default: "4", description: "How many avatars to show before the `+N` chip." }, { name: "size", type: '"sm" | "default" | "lg"', required: false, default: '"default"', description: "Size of every avatar and the chip." }], description: "Overlapping avatars with a `+N` overflow chip. Hover gently enlarges the avatar on a spring, staying in stack order." }], types: { AvatarGroupUser: "{ alt: string; src?: string; id?: string; }" } }, dependencies: ["motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/avatar.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/avatar-group.tsx", type: "registry:ui", content: `"use client";
|
|
1217
1242
|
|
|
1218
1243
|
import { motion } from "motion/react";
|
|
1219
1244
|
import { useState } from "react";
|
|
1220
1245
|
import { Avatar } from "@/components/ui/avatar";
|
|
1221
1246
|
import { cn, spring } from "@/lib/utils";
|
|
1222
1247
|
|
|
1223
|
-
type
|
|
1248
|
+
export type AvatarGroupUser = {
|
|
1224
1249
|
alt: string;
|
|
1225
1250
|
/** Image URL; initials from \`alt\` show without one. */
|
|
1226
1251
|
src?: string;
|
|
@@ -1230,7 +1255,7 @@ type GroupUser = {
|
|
|
1230
1255
|
|
|
1231
1256
|
type AvatarGroupProps = {
|
|
1232
1257
|
/** People in order; \`alt\` is each one's name. */
|
|
1233
|
-
users:
|
|
1258
|
+
users: AvatarGroupUser[];
|
|
1234
1259
|
/** How many avatars to show before the \`+N\` chip. */
|
|
1235
1260
|
max?: number;
|
|
1236
1261
|
/** Size of every avatar and the chip. */
|
|
@@ -1274,6 +1299,7 @@ export function AvatarGroup({ users, max = 4, size = "default", className }: Ava
|
|
|
1274
1299
|
<Avatar src={user.src} alt={user.alt} size={size} />
|
|
1275
1300
|
</motion.li>
|
|
1276
1301
|
))}
|
|
1302
|
+
|
|
1277
1303
|
{overflow > 0 && (
|
|
1278
1304
|
<li
|
|
1279
1305
|
className={cn(
|
|
@@ -1344,9 +1370,11 @@ export function Badge({
|
|
|
1344
1370
|
{pulse && (
|
|
1345
1371
|
<span className="absolute inset-0 animate-ping rounded-full bg-current opacity-60 [animation-duration:1.6s] motion-reduce:hidden" />
|
|
1346
1372
|
)}
|
|
1373
|
+
|
|
1347
1374
|
<span className="relative size-1.5 rounded-full bg-current" />
|
|
1348
1375
|
</span>
|
|
1349
1376
|
)}
|
|
1377
|
+
|
|
1350
1378
|
{children}
|
|
1351
1379
|
</span>
|
|
1352
1380
|
);
|
|
@@ -1363,10 +1391,10 @@ export function NavTrail({ post }: { post: { title: string } }) {
|
|
|
1363
1391
|
]}
|
|
1364
1392
|
/>
|
|
1365
1393
|
);
|
|
1366
|
-
}` }], api: { entries: [{ kind: "component", name: "Breadcrumb", props: [{ name: "items", type: "
|
|
1394
|
+
}` }], api: { entries: [{ kind: "component", name: "Breadcrumb", props: [{ name: "items", type: "BreadcrumbItem[]", required: true, description: "The trail, outermost first. The last item is the current page." }, { name: "separator", type: "ReactNode", required: false, description: "Drawn between items. Default a chevron." }], extends: "<nav>", description: "Where the page sits in the site: a list of links ending at the current page. Server-safe." }], types: { BreadcrumbItem: "{ label: ReactNode; href?: string; onClick?: MouseEventHandler<HTMLAnchorElement>; }" } }, dependencies: ["lucide-react@1.63.0"], registryDependencies: [], files: [{ path: "components/ui/breadcrumb.tsx", type: "registry:ui", content: `import { ChevronRight } from "lucide-react";
|
|
1367
1395
|
import type { ComponentProps, MouseEventHandler, ReactNode } from "react";
|
|
1368
1396
|
|
|
1369
|
-
type
|
|
1397
|
+
export type BreadcrumbItem = {
|
|
1370
1398
|
/** What the step reads as. */
|
|
1371
1399
|
label: ReactNode;
|
|
1372
1400
|
/** Leave out for a step that has no page of its own. */
|
|
@@ -1377,7 +1405,7 @@ type Crumb = {
|
|
|
1377
1405
|
|
|
1378
1406
|
type BreadcrumbProps = Omit<ComponentProps<"nav">, "children"> & {
|
|
1379
1407
|
/** The trail, outermost first. The last item is the current page. */
|
|
1380
|
-
items:
|
|
1408
|
+
items: BreadcrumbItem[];
|
|
1381
1409
|
/** Drawn between items. Default a chevron. */
|
|
1382
1410
|
separator?: ReactNode;
|
|
1383
1411
|
};
|
|
@@ -1401,6 +1429,7 @@ export function Breadcrumb({
|
|
|
1401
1429
|
{separator ?? <ChevronRight />}
|
|
1402
1430
|
</span>
|
|
1403
1431
|
)}
|
|
1432
|
+
|
|
1404
1433
|
{i === items.length - 1 ? (
|
|
1405
1434
|
<span aria-current="page" className="text-foreground">
|
|
1406
1435
|
{item.label}
|
|
@@ -1460,7 +1489,7 @@ export function DeleteAccount({
|
|
|
1460
1489
|
Delete account
|
|
1461
1490
|
</Button>
|
|
1462
1491
|
);
|
|
1463
|
-
}` }], api: { entries: [{ kind: "component", name: "Button", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, default: "3000", description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, default: "false", description: "
|
|
1492
|
+
}` }], api: { entries: [{ kind: "component", name: "Button", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, default: "3000", description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, default: "false", description: "Shows a spinner before the label and ignores clicks and Enter, so a form can't submit twice. Stays focusable." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, default: '"idle"', description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`. Both ignore clicks and Enter, so a form can't submit twice." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, default: '"left"', description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }] }], types: { ButtonStatus: '"idle" | "loading" | "success"', SwapDirection: '"left" | "right" | "up" | "down"' } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/button-variants.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/button.tsx", type: "registry:ui", content: `"use client";
|
|
1464
1493
|
|
|
1465
1494
|
import { Check, Loader2 } from "lucide-react";
|
|
1466
1495
|
import { AnimatePresence, type HTMLMotionProps, motion } from "motion/react";
|
|
@@ -1475,16 +1504,16 @@ import {
|
|
|
1475
1504
|
import { spring } from "@/lib/utils";
|
|
1476
1505
|
import { buttonVariants, type VariantProps } from "./button-variants";
|
|
1477
1506
|
|
|
1478
|
-
type ButtonStatus = "idle" | "loading" | "success";
|
|
1507
|
+
export type ButtonStatus = "idle" | "loading" | "success";
|
|
1479
1508
|
/** Where the old label slides out to; the new one comes from the opposite side. */
|
|
1480
|
-
type SwapDirection = "left" | "right" | "up" | "down";
|
|
1509
|
+
export type SwapDirection = "left" | "right" | "up" | "down";
|
|
1481
1510
|
type Phase = "idle" | "armed" | "loading" | "success";
|
|
1482
1511
|
|
|
1483
1512
|
type ButtonProps = Omit<HTMLMotionProps<"button">, "children"> &
|
|
1484
1513
|
VariantProps & {
|
|
1485
|
-
/**
|
|
1514
|
+
/** Shows a spinner before the label and ignores clicks and Enter, so a form can't submit twice. Stays focusable. */
|
|
1486
1515
|
loading?: boolean;
|
|
1487
|
-
/** In-place feedback: \`loading\` spins, \`success\` swaps the label to \`successLabel\`. */
|
|
1516
|
+
/** In-place feedback: \`loading\` spins, \`success\` swaps the label to \`successLabel\`. Both ignore clicks and Enter, so a form can't submit twice. */
|
|
1488
1517
|
status?: ButtonStatus;
|
|
1489
1518
|
/** Text shown on \`status="success"\`, with a check icon. Defaults to "Done". */
|
|
1490
1519
|
successLabel?: ReactNode;
|
|
@@ -1620,14 +1649,19 @@ export function Button({
|
|
|
1620
1649
|
{/* Hidden copies of every label reserve the widest one, so swaps never resize the button. */}
|
|
1621
1650
|
<span className="inline-grid justify-items-center">
|
|
1622
1651
|
{children !== undefined && <SizerLabel>{children}</SizerLabel>}
|
|
1652
|
+
|
|
1623
1653
|
{loadingLabel !== undefined && <SizerLabel>{loadingLabel}</SizerLabel>}
|
|
1654
|
+
|
|
1624
1655
|
{confirmLabel !== undefined && <SizerLabel>{confirmLabel}</SizerLabel>}
|
|
1656
|
+
|
|
1625
1657
|
{successLabel !== undefined && (
|
|
1626
1658
|
<SizerLabel>
|
|
1627
1659
|
<Check />
|
|
1660
|
+
|
|
1628
1661
|
{successLabel}
|
|
1629
1662
|
</SizerLabel>
|
|
1630
1663
|
)}
|
|
1664
|
+
|
|
1631
1665
|
<AnimatePresence mode="wait" initial={false}>
|
|
1632
1666
|
{phase === "success" && (
|
|
1633
1667
|
<motion.span
|
|
@@ -1645,9 +1679,11 @@ export function Button({
|
|
|
1645
1679
|
>
|
|
1646
1680
|
<Check aria-hidden="true" />
|
|
1647
1681
|
</motion.span>
|
|
1682
|
+
|
|
1648
1683
|
{successLabel ?? "Done"}
|
|
1649
1684
|
</motion.span>
|
|
1650
1685
|
)}
|
|
1686
|
+
|
|
1651
1687
|
{phase === "armed" && (
|
|
1652
1688
|
<motion.span
|
|
1653
1689
|
key="armed"
|
|
@@ -1659,6 +1695,7 @@ export function Button({
|
|
|
1659
1695
|
{confirmLabel}
|
|
1660
1696
|
</motion.span>
|
|
1661
1697
|
)}
|
|
1698
|
+
|
|
1662
1699
|
{phase === "loading" && (
|
|
1663
1700
|
<motion.span
|
|
1664
1701
|
key="loading"
|
|
@@ -1678,9 +1715,11 @@ export function Button({
|
|
|
1678
1715
|
<Loader2 className="animate-spin" aria-hidden="true" />
|
|
1679
1716
|
</motion.span>
|
|
1680
1717
|
</AnimatePresence>
|
|
1718
|
+
|
|
1681
1719
|
{loadingLabel ?? children}
|
|
1682
1720
|
</motion.span>
|
|
1683
1721
|
)}
|
|
1722
|
+
|
|
1684
1723
|
{phase === "idle" && (
|
|
1685
1724
|
<motion.span
|
|
1686
1725
|
key="idle"
|
|
@@ -2047,6 +2086,7 @@ export function Calendar(props: CalendarProps) {
|
|
|
2047
2086
|
<p id={\`\${id}-caption\`} aria-live="polite" className="font-semibold text-sm">
|
|
2048
2087
|
{month ? fmtMonth.format(month) : " "}
|
|
2049
2088
|
</p>
|
|
2089
|
+
|
|
2050
2090
|
<div className="flex gap-0.5">
|
|
2051
2091
|
<Button
|
|
2052
2092
|
variant="ghost"
|
|
@@ -2058,6 +2098,7 @@ export function Calendar(props: CalendarProps) {
|
|
|
2058
2098
|
>
|
|
2059
2099
|
<ChevronLeft />
|
|
2060
2100
|
</Button>
|
|
2101
|
+
|
|
2061
2102
|
<Button
|
|
2062
2103
|
variant="ghost"
|
|
2063
2104
|
size="icon"
|
|
@@ -2103,6 +2144,7 @@ export function Calendar(props: CalendarProps) {
|
|
|
2103
2144
|
))}
|
|
2104
2145
|
</tr>
|
|
2105
2146
|
</thead>
|
|
2147
|
+
|
|
2106
2148
|
<tbody>
|
|
2107
2149
|
{weeks.map((week, weekIndex) => (
|
|
2108
2150
|
// biome-ignore lint/suspicious/noArrayIndexKey: six fixed rows
|
|
@@ -2694,7 +2736,7 @@ export function ChangelogPage() {
|
|
|
2694
2736
|
]}
|
|
2695
2737
|
/>
|
|
2696
2738
|
);
|
|
2697
|
-
}` }], api: { entries: [{ kind: "component", name: "Changelog", props: [{ name: "releases", type: "
|
|
2739
|
+
}` }], api: { entries: [{ kind: "component", name: "Changelog", props: [{ name: "releases", type: "ChangelogRelease[]", required: true, description: "Newest first; the first one is highlighted." }], description: "Release notes on a Timeline: the newest release is the highlighted one, changes are typed with Badges, Kbd shows how to try it." }], types: { ChangelogRelease: "{ version: string; title: string; date: string; dateTime?: string; changes: Change[]; shortcut?: string[]; }" } }, dependencies: ["lucide-react@1.63.0"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/badge.json", "https://kznlabs.github.io/labsUI/r/kbd.json", "https://kznlabs.github.io/labsUI/r/timeline.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/blocks/changelog.tsx", type: "registry:block", content: `import { Rocket } from "lucide-react";
|
|
2698
2740
|
import type { ReactNode } from "react";
|
|
2699
2741
|
import { Badge } from "@/components/ui/badge";
|
|
2700
2742
|
import { KbdGroup } from "@/components/ui/kbd";
|
|
@@ -2705,7 +2747,7 @@ import { cn } from "@/lib/utils";
|
|
|
2705
2747
|
|
|
2706
2748
|
type Change = { type: "new" | "improved" | "fixed"; text: ReactNode };
|
|
2707
2749
|
|
|
2708
|
-
type
|
|
2750
|
+
export type ChangelogRelease = {
|
|
2709
2751
|
version: string;
|
|
2710
2752
|
/** Release headline, shown beside the version. */
|
|
2711
2753
|
title: string;
|
|
@@ -2730,7 +2772,7 @@ export function Changelog({
|
|
|
2730
2772
|
className,
|
|
2731
2773
|
}: {
|
|
2732
2774
|
/** Newest first; the first one is highlighted. */
|
|
2733
|
-
releases:
|
|
2775
|
+
releases: ChangelogRelease[];
|
|
2734
2776
|
className?: string;
|
|
2735
2777
|
}) {
|
|
2736
2778
|
return (
|
|
@@ -2747,6 +2789,7 @@ export function Changelog({
|
|
|
2747
2789
|
<Badge variant="secondary" className="font-mono">
|
|
2748
2790
|
{release.version}
|
|
2749
2791
|
</Badge>
|
|
2792
|
+
|
|
2750
2793
|
{release.title}
|
|
2751
2794
|
</span>
|
|
2752
2795
|
}
|
|
@@ -2758,10 +2801,12 @@ export function Changelog({
|
|
|
2758
2801
|
<Badge variant={kinds[change.type].variant} className="mt-px w-16 justify-center">
|
|
2759
2802
|
{kinds[change.type].label}
|
|
2760
2803
|
</Badge>
|
|
2804
|
+
|
|
2761
2805
|
<span>{change.text}</span>
|
|
2762
2806
|
</li>
|
|
2763
2807
|
))}
|
|
2764
2808
|
</ul>
|
|
2809
|
+
|
|
2765
2810
|
{release.shortcut && (
|
|
2766
2811
|
<p className="mt-3 flex items-center gap-2 text-muted-foreground text-xs">
|
|
2767
2812
|
Try it <KbdGroup keys={release.shortcut} />
|
|
@@ -2823,7 +2868,7 @@ import {
|
|
|
2823
2868
|
import { useDismiss } from "@/lib/use-dismiss";
|
|
2824
2869
|
import { clamp, cn, material, spring } from "@/lib/utils";
|
|
2825
2870
|
|
|
2826
|
-
type ChartSeries = {
|
|
2871
|
+
export type ChartSeries = {
|
|
2827
2872
|
key: string;
|
|
2828
2873
|
/** Legend and tooltip name of the series. */
|
|
2829
2874
|
label: string;
|
|
@@ -3168,9 +3213,11 @@ export function Chart({
|
|
|
3168
3213
|
// stop.
|
|
3169
3214
|
<Table containerClassName={view === "table" ? "max-h-[32rem]" : "overflow-x-visible"}>
|
|
3170
3215
|
<TableCaption className={view === "chart" ? "sr-only" : undefined}>{label}</TableCaption>
|
|
3216
|
+
|
|
3171
3217
|
<TableHeader>
|
|
3172
3218
|
<TableRow>
|
|
3173
3219
|
<TableHead className="capitalize">{index}</TableHead>
|
|
3220
|
+
|
|
3174
3221
|
{series.map((item) => (
|
|
3175
3222
|
<TableHead key={item.key} className="text-right">
|
|
3176
3223
|
{item.label}
|
|
@@ -3178,10 +3225,12 @@ export function Chart({
|
|
|
3178
3225
|
))}
|
|
3179
3226
|
</TableRow>
|
|
3180
3227
|
</TableHeader>
|
|
3228
|
+
|
|
3181
3229
|
<TableBody>
|
|
3182
3230
|
{data.map((row) => (
|
|
3183
3231
|
<TableRow key={String(row[index])}>
|
|
3184
3232
|
<TableCell className="font-medium">{row[index]}</TableCell>
|
|
3233
|
+
|
|
3185
3234
|
{series.map((item) => (
|
|
3186
3235
|
<TableCell key={item.key} className="text-right tabular-nums">
|
|
3187
3236
|
{fmt.format(num(row[item.key]))}
|
|
@@ -3229,6 +3278,7 @@ export function Chart({
|
|
|
3229
3278
|
}
|
|
3230
3279
|
style={{ background: color(slots.get(item.key) ?? 1) }}
|
|
3231
3280
|
/>
|
|
3281
|
+
|
|
3232
3282
|
<span className={cn(off && "line-through")}>{item.label}</span>
|
|
3233
3283
|
</button>
|
|
3234
3284
|
</li>
|
|
@@ -3238,6 +3288,7 @@ export function Chart({
|
|
|
3238
3288
|
) : (
|
|
3239
3289
|
<span />
|
|
3240
3290
|
)}
|
|
3291
|
+
|
|
3241
3292
|
<Button
|
|
3242
3293
|
size="sm"
|
|
3243
3294
|
variant="ghost"
|
|
@@ -3289,6 +3340,7 @@ export function Chart({
|
|
|
3289
3340
|
className={tick === 0 ? "stroke-foreground/25" : "stroke-border"}
|
|
3290
3341
|
shapeRendering="crispEdges"
|
|
3291
3342
|
/>
|
|
3343
|
+
|
|
3292
3344
|
<text
|
|
3293
3345
|
x={margin.left - 8}
|
|
3294
3346
|
y={y(tick)}
|
|
@@ -3300,6 +3352,7 @@ export function Chart({
|
|
|
3300
3352
|
</text>
|
|
3301
3353
|
</g>
|
|
3302
3354
|
))}
|
|
3355
|
+
|
|
3303
3356
|
{data.map((row, i) =>
|
|
3304
3357
|
i % every === 0 ? (
|
|
3305
3358
|
<text
|
|
@@ -3360,6 +3413,7 @@ export function Chart({
|
|
|
3360
3413
|
fillOpacity={0.1}
|
|
3361
3414
|
/>
|
|
3362
3415
|
)}
|
|
3416
|
+
|
|
3363
3417
|
<motion.path
|
|
3364
3418
|
initial={{ d: linePath(flat(pts)) }}
|
|
3365
3419
|
animate={{ d: linePath(pts) }}
|
|
@@ -3370,6 +3424,7 @@ export function Chart({
|
|
|
3370
3424
|
strokeLinejoin="round"
|
|
3371
3425
|
strokeLinecap="round"
|
|
3372
3426
|
/>
|
|
3427
|
+
|
|
3373
3428
|
{(active === null ? [rowCount - 1] : [active]).map((i) =>
|
|
3374
3429
|
pts[i] ? (
|
|
3375
3430
|
<motion.circle
|
|
@@ -3435,6 +3490,7 @@ export function Chart({
|
|
|
3435
3490
|
)}
|
|
3436
3491
|
>
|
|
3437
3492
|
<p className="mb-1.5 text-muted-foreground">{row[index]}</p>
|
|
3493
|
+
|
|
3438
3494
|
<ul className="grid gap-1">
|
|
3439
3495
|
{shown.map((item) => (
|
|
3440
3496
|
<li key={item.key} className="flex items-center gap-2">
|
|
@@ -3442,7 +3498,9 @@ export function Chart({
|
|
|
3442
3498
|
className="h-0.5 w-2.5 shrink-0 rounded-full"
|
|
3443
3499
|
style={{ background: color(slots.get(item.key) ?? 1) }}
|
|
3444
3500
|
/>
|
|
3501
|
+
|
|
3445
3502
|
<span className="text-muted-foreground">{item.label}</span>
|
|
3503
|
+
|
|
3446
3504
|
<span className="ml-auto pl-3 font-medium tabular-nums">
|
|
3447
3505
|
{fmt.format(num(row[item.key]))}
|
|
3448
3506
|
</span>
|
|
@@ -3454,9 +3512,11 @@ export function Chart({
|
|
|
3454
3512
|
)}
|
|
3455
3513
|
</AnimatePresence>
|
|
3456
3514
|
</div>
|
|
3515
|
+
|
|
3457
3516
|
<p className="sr-only" aria-live="polite">
|
|
3458
3517
|
{readout}
|
|
3459
3518
|
</p>
|
|
3519
|
+
|
|
3460
3520
|
{/* The same numbers for screen readers, search engines and no-JS. */}
|
|
3461
3521
|
<div className="sr-only">{table}</div>
|
|
3462
3522
|
</>
|
|
@@ -3528,6 +3588,7 @@ export function Checkbox({
|
|
|
3528
3588
|
className="peer absolute -inset-1 z-10 cursor-pointer appearance-none rounded-[5px] outline-none disabled:cursor-not-allowed"
|
|
3529
3589
|
{...props}
|
|
3530
3590
|
/>
|
|
3591
|
+
|
|
3531
3592
|
<span
|
|
3532
3593
|
aria-hidden="true"
|
|
3533
3594
|
className={cn(
|
|
@@ -3638,10 +3699,13 @@ export function ClipCard({
|
|
|
3638
3699
|
className="grid place-items-center overflow-hidden rounded-lg bg-foreground/[0.06] text-muted-foreground"
|
|
3639
3700
|
>
|
|
3640
3701
|
{thumbnail ?? <Film className="size-5" aria-hidden="true" />}
|
|
3702
|
+
|
|
3641
3703
|
<Badge variant="secondary" className="absolute right-1.5 bottom-2 tabular-nums">
|
|
3642
3704
|
<span className="sr-only">Duration </span>
|
|
3705
|
+
|
|
3643
3706
|
{duration}
|
|
3644
3707
|
</Badge>
|
|
3708
|
+
|
|
3645
3709
|
{watched !== undefined && watched > 0 && (
|
|
3646
3710
|
<Progress
|
|
3647
3711
|
value={watched}
|
|
@@ -3651,8 +3715,10 @@ export function ClipCard({
|
|
|
3651
3715
|
/>
|
|
3652
3716
|
)}
|
|
3653
3717
|
</AspectRatio>
|
|
3718
|
+
|
|
3654
3719
|
<div className="grid gap-0.5 px-0.5 text-left">
|
|
3655
3720
|
<p className="truncate font-medium text-sm">{title}</p>
|
|
3721
|
+
|
|
3656
3722
|
{meta && <p className="truncate text-muted-foreground text-xs">{meta}</p>}
|
|
3657
3723
|
</div>
|
|
3658
3724
|
</>
|
|
@@ -3672,6 +3738,7 @@ export function ClipCard({
|
|
|
3672
3738
|
>
|
|
3673
3739
|
{body}
|
|
3674
3740
|
</ContextMenuTrigger>
|
|
3741
|
+
|
|
3675
3742
|
<ContextMenuContent>{actions}</ContextMenuContent>
|
|
3676
3743
|
</ContextMenu>
|
|
3677
3744
|
);
|
|
@@ -3717,13 +3784,13 @@ import { OverflowFocus } from "@/lib/overflow-focus";
|
|
|
3717
3784
|
import { cn } from "@/lib/utils";
|
|
3718
3785
|
|
|
3719
3786
|
/** One line, or an inclusive \`[from, to]\` run. Numbers are the ones shown in the gutter. */
|
|
3720
|
-
type LineRange = number | [from: number, to: number];
|
|
3787
|
+
export type LineRange = number | [from: number, to: number];
|
|
3721
3788
|
|
|
3722
3789
|
/**
|
|
3723
3790
|
* Text to mark: every match of a string, matches on one line, or an exact \`[start, end)\`
|
|
3724
3791
|
* column range (String#slice indices) on one line.
|
|
3725
3792
|
*/
|
|
3726
|
-
type CodeMark =
|
|
3793
|
+
export type CodeMark =
|
|
3727
3794
|
| string
|
|
3728
3795
|
| { text: string; line?: number }
|
|
3729
3796
|
| { line: number; start: number; end: number };
|
|
@@ -3739,7 +3806,7 @@ type Annotations = {
|
|
|
3739
3806
|
notes?: Record<number, ReactNode>;
|
|
3740
3807
|
};
|
|
3741
3808
|
|
|
3742
|
-
type CodeFile = Annotations & {
|
|
3809
|
+
export type CodeFile = Annotations & {
|
|
3743
3810
|
name: string;
|
|
3744
3811
|
code: string;
|
|
3745
3812
|
/** Lines from \`highlightCode()\`, made ahead of time (server or build): no Shiki in the browser. */
|
|
@@ -4076,9 +4143,11 @@ function CodeLines({
|
|
|
4076
4143
|
)}
|
|
4077
4144
|
/>
|
|
4078
4145
|
)}
|
|
4146
|
+
|
|
4079
4147
|
{numbered && <span style={{ minWidth: \`\${digits}ch\` }}>{lineNumber}</span>}
|
|
4080
4148
|
</span>
|
|
4081
4149
|
)}
|
|
4150
|
+
|
|
4082
4151
|
{note ? (
|
|
4083
4152
|
<Tooltip content={note} side="top" align="start" delay={150} touch="tap">
|
|
4084
4153
|
<span
|
|
@@ -4122,8 +4191,10 @@ export function Code({
|
|
|
4122
4191
|
>
|
|
4123
4192
|
<div className="min-w-0 flex-1 overflow-x-auto whitespace-nowrap rounded-sm font-mono text-xs outline-none [scrollbar-width:none] focus-visible:ring-[3px] focus-visible:ring-ring/50 [&::-webkit-scrollbar]:hidden">
|
|
4124
4193
|
<CodeLines text={code} lines={lines} marks={annotations.marks} inline />
|
|
4194
|
+
|
|
4125
4195
|
<OverflowFocus />
|
|
4126
4196
|
</div>
|
|
4197
|
+
|
|
4127
4198
|
<CopyButton code={code} />
|
|
4128
4199
|
</div>
|
|
4129
4200
|
);
|
|
@@ -4138,10 +4209,13 @@ export function Code({
|
|
|
4138
4209
|
>
|
|
4139
4210
|
<div className="flex h-8 items-center justify-between border-b px-3">
|
|
4140
4211
|
<span className="font-mono text-muted-foreground text-xs">{language}</span>
|
|
4212
|
+
|
|
4141
4213
|
<CopyButton code={code} />
|
|
4142
4214
|
</div>
|
|
4215
|
+
|
|
4143
4216
|
<div className="overflow-x-auto font-mono text-xs leading-5 outline-none [--code-px:--spacing(3)] focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-inset">
|
|
4144
4217
|
<CodeLines text={code} lines={lines} {...annotations} />
|
|
4218
|
+
|
|
4145
4219
|
<OverflowFocus />
|
|
4146
4220
|
</div>
|
|
4147
4221
|
</div>
|
|
@@ -4196,8 +4270,10 @@ export function CodePanel({ files, format, collapse, className }: CodePanelProps
|
|
|
4196
4270
|
</TabsTrigger>
|
|
4197
4271
|
))}
|
|
4198
4272
|
</TabsList>
|
|
4273
|
+
|
|
4199
4274
|
<CopyButton code={copyText} />
|
|
4200
4275
|
</div>
|
|
4276
|
+
|
|
4201
4277
|
{files.map((tab) => (
|
|
4202
4278
|
<TabsContent
|
|
4203
4279
|
key={tab.name}
|
|
@@ -4215,6 +4291,7 @@ export function CodePanel({ files, format, collapse, className }: CodePanelProps
|
|
|
4215
4291
|
</div>
|
|
4216
4292
|
</TabsContent>
|
|
4217
4293
|
))}
|
|
4294
|
+
|
|
4218
4295
|
{foldable && (
|
|
4219
4296
|
<button
|
|
4220
4297
|
type="button"
|
|
@@ -4329,6 +4406,7 @@ export function CollapsibleTrigger({
|
|
|
4329
4406
|
{...props}
|
|
4330
4407
|
>
|
|
4331
4408
|
{children}
|
|
4409
|
+
|
|
4332
4410
|
{chevron && (
|
|
4333
4411
|
<motion.span
|
|
4334
4412
|
animate={{ rotate: open ? 180 : 0 }}
|
|
@@ -4368,7 +4446,7 @@ export function CollapsibleContent({
|
|
|
4368
4446
|
</motion.div>
|
|
4369
4447
|
);
|
|
4370
4448
|
}
|
|
4371
|
-
` }] }, combobox: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "combobox", type: "registry:ui", title: "Combobox", description: "A Select you can type into: the query filters by label and keywords (prefix matches first), arrows move, Enter picks. multiple keeps the list open and shows picks as removable tags. Submits
|
|
4449
|
+
` }] }, combobox: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "combobox", type: "registry:ui", title: "Combobox", description: "A Select you can type into: the query filters by label and keywords (prefix matches first), arrows move, Enter picks. multiple keeps the list open and shows picks as removable tags. Submits and validates with the form.", docs: "https://kznlabs.github.io/labsUI/components/combobox", usage: '<Combobox options={channels} value={channel} onValueChange={setChannel} placeholder="Pick a channel…" />', examples: [{ name: "channel-picker.tsx", code: `"use client";
|
|
4372
4450
|
|
|
4373
4451
|
import { Hash } from "lucide-react";
|
|
4374
4452
|
import { useState } from "react";
|
|
@@ -4411,7 +4489,7 @@ export function RolePicker() {
|
|
|
4411
4489
|
placeholder="Add roles…"
|
|
4412
4490
|
/>
|
|
4413
4491
|
);
|
|
4414
|
-
}` }], api: { entries: [{ kind: "component", name: "Combobox", props: [{ name: "options", type: "ComboboxOption[]", required: true, description: "Choices; filtered as the user types." }, { name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "aria-label", type: "string", required: false, description: "Accessible name when there is no visible label." }, { name: "defaultValue", type: "string | string[]", required: false, description: "Value, or values when `multiple`, at first render (uncontrolled)." }, { name: "disabled", type: "boolean", required: false, description: "Blocks typing and opening." }, { name: "emptyText", type: "ReactNode", required: false, description: "Shown in the list when nothing matches." }, { name: "id", type: "string", required: false, description: "Id of the input; Field fills it in." }, { name: "multiple", type: "boolean", required: false, description: "Picks stay open and show as removable tags; Backspace removes the last one." }, { name: "name", type: "string", required: false, description: "
|
|
4492
|
+
}` }], api: { entries: [{ kind: "component", name: "Combobox", props: [{ name: "options", type: "ComboboxOption[]", required: true, description: "Choices; filtered as the user types." }, { name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "aria-label", type: "string", required: false, description: "Accessible name when there is no visible label." }, { name: "defaultValue", type: "string | string[]", required: false, description: "Value, or values when `multiple`, at first render (uncontrolled)." }, { name: "disabled", type: "boolean", required: false, description: "Blocks typing and opening; the value isn't submitted." }, { name: "emptyText", type: "ReactNode", required: false, description: "Shown in the list when nothing matches." }, { name: "id", type: "string", required: false, description: "Id of the input; Field fills it in." }, { name: "multiple", type: "boolean", required: false, description: "Picks stay open and show as removable tags; Backspace removes the last one." }, { name: "name", type: "string", required: false, description: "Form field name: one entry per picked value." }, { name: "onValueChange", type: "((value: string) => void) | ((value: string[]) => void)", required: false, description: "Called with the picked value, or every picked value when `multiple`." }, { name: "placeholder", type: "string", required: false, description: "Field text when nothing is picked." }, { name: "required", type: "boolean", required: false, description: "Form submit needs a value." }, { name: "value", type: "string | string[]", required: false, description: "Controlled value, or values when `multiple`; pair with `onValueChange`." }], description: "Select you can type into: the input filters the list, Up and Down move, Enter picks, Esc closes.\nSingle shows the pick in the input; `multiple` shows picks as tags and keeps the list open." }], types: { ComboboxOption: "{ value: string; label: string; icon?: ReactNode; keywords?: string[]; disabled?: boolean; }" } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/form-value.json", "https://kznlabs.github.io/labsUI/r/list.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/tag-input.json", "https://kznlabs.github.io/labsUI/r/use-anchor.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/use-dismiss.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/combobox.tsx", type: "registry:ui", content: `"use client";
|
|
4415
4493
|
|
|
4416
4494
|
import { Check, ChevronsUpDown } from "lucide-react";
|
|
4417
4495
|
import { AnimatePresence, motion } from "motion/react";
|
|
@@ -4419,6 +4497,7 @@ import { type KeyboardEvent, type ReactNode, useEffect, useId, useRef, useState
|
|
|
4419
4497
|
import { createPortal } from "react-dom";
|
|
4420
4498
|
import { useFieldControl } from "@/components/ui/field";
|
|
4421
4499
|
import { Tag, tagFieldClass } from "@/components/ui/tag-input";
|
|
4500
|
+
import { FormValue } from "@/lib/form-value";
|
|
4422
4501
|
import { listKeyTarget, scrollIntoList } from "@/lib/list";
|
|
4423
4502
|
import { useMounted } from "@/lib/platform";
|
|
4424
4503
|
import { useAnchor } from "@/lib/use-anchor";
|
|
@@ -4426,7 +4505,7 @@ import { useControllableState } from "@/lib/use-controllable-state";
|
|
|
4426
4505
|
import { useDismiss } from "@/lib/use-dismiss";
|
|
4427
4506
|
import { cn, material, spring } from "@/lib/utils";
|
|
4428
4507
|
|
|
4429
|
-
type ComboboxOption = {
|
|
4508
|
+
export type ComboboxOption = {
|
|
4430
4509
|
/** Submitted and matched value. */
|
|
4431
4510
|
value: string;
|
|
4432
4511
|
/** Text shown and searched. */
|
|
@@ -4446,11 +4525,11 @@ type BaseProps = {
|
|
|
4446
4525
|
placeholder?: string;
|
|
4447
4526
|
/** Shown in the list when nothing matches. */
|
|
4448
4527
|
emptyText?: ReactNode;
|
|
4449
|
-
/**
|
|
4528
|
+
/** Form field name: one entry per picked value. */
|
|
4450
4529
|
name?: string;
|
|
4451
4530
|
/** Id of the input; Field fills it in. */
|
|
4452
4531
|
id?: string;
|
|
4453
|
-
/** Blocks typing and opening. */
|
|
4532
|
+
/** Blocks typing and opening; the value isn't submitted. */
|
|
4454
4533
|
disabled?: boolean;
|
|
4455
4534
|
/** Form submit needs a value. */
|
|
4456
4535
|
required?: boolean;
|
|
@@ -4522,6 +4601,7 @@ export function Combobox(allProps: ComboboxProps) {
|
|
|
4522
4601
|
const box = useRef<HTMLDivElement>(null);
|
|
4523
4602
|
const input = useRef<HTMLInputElement>(null);
|
|
4524
4603
|
const list = useRef<HTMLDivElement>(null);
|
|
4604
|
+
const [invalid, setInvalid] = useState(false);
|
|
4525
4605
|
// Public shape (string or string[]), "" = nothing picked.
|
|
4526
4606
|
const [raw, setRaw] = useControllableState<string | string[] | undefined>(
|
|
4527
4607
|
valueProp,
|
|
@@ -4635,11 +4715,13 @@ export function Combobox(allProps: ComboboxProps) {
|
|
|
4635
4715
|
))}
|
|
4636
4716
|
</AnimatePresence>
|
|
4637
4717
|
)}
|
|
4718
|
+
|
|
4638
4719
|
{!multiple && !open && selected[0]?.icon && (
|
|
4639
4720
|
<span className="mr-2 flex shrink-0 text-muted-foreground [&_svg]:size-4">
|
|
4640
4721
|
{selected[0].icon}
|
|
4641
4722
|
</span>
|
|
4642
4723
|
)}
|
|
4724
|
+
|
|
4643
4725
|
<input
|
|
4644
4726
|
ref={input}
|
|
4645
4727
|
id={id}
|
|
@@ -4653,6 +4735,7 @@ export function Combobox(allProps: ComboboxProps) {
|
|
|
4653
4735
|
aria-required={required}
|
|
4654
4736
|
disabled={disabled}
|
|
4655
4737
|
{...aria}
|
|
4738
|
+
aria-invalid={aria["aria-invalid"] || (invalid && !values.length) || undefined}
|
|
4656
4739
|
value={text}
|
|
4657
4740
|
placeholder={hint}
|
|
4658
4741
|
onChange={(event) => {
|
|
@@ -4665,15 +4748,26 @@ export function Combobox(allProps: ComboboxProps) {
|
|
|
4665
4748
|
multiple && "px-1",
|
|
4666
4749
|
)}
|
|
4667
4750
|
/>
|
|
4751
|
+
|
|
4668
4752
|
<ChevronsUpDown
|
|
4669
4753
|
className="ml-1 size-4 shrink-0 self-center text-muted-foreground"
|
|
4670
4754
|
aria-hidden="true"
|
|
4671
4755
|
/>
|
|
4672
4756
|
</div>
|
|
4757
|
+
|
|
4673
4758
|
{name &&
|
|
4674
4759
|
(multiple ? values : values.slice(0, 1)).map((picked) => (
|
|
4675
|
-
<input key={picked} type="hidden" name={name} value={picked} />
|
|
4760
|
+
<input key={picked} type="hidden" name={name} value={picked} disabled={disabled} />
|
|
4676
4761
|
))}
|
|
4762
|
+
|
|
4763
|
+
<FormValue
|
|
4764
|
+
value={values.join(",")}
|
|
4765
|
+
required={required}
|
|
4766
|
+
disabled={disabled}
|
|
4767
|
+
anchor={input}
|
|
4768
|
+
onInvalid={() => setInvalid(true)}
|
|
4769
|
+
/>
|
|
4770
|
+
|
|
4677
4771
|
{mounted &&
|
|
4678
4772
|
createPortal(
|
|
4679
4773
|
<AnimatePresence>
|
|
@@ -4729,7 +4823,9 @@ export function Combobox(allProps: ComboboxProps) {
|
|
|
4729
4823
|
{values.includes(option.value) && (
|
|
4730
4824
|
<Check className="!text-current absolute left-2" aria-hidden="true" />
|
|
4731
4825
|
)}
|
|
4826
|
+
|
|
4732
4827
|
{option.icon}
|
|
4828
|
+
|
|
4733
4829
|
<span className="truncate">{option.label}</span>
|
|
4734
4830
|
</div>
|
|
4735
4831
|
))
|
|
@@ -5001,6 +5097,7 @@ export function CommandGroup({
|
|
|
5001
5097
|
{heading}
|
|
5002
5098
|
</div>
|
|
5003
5099
|
)}
|
|
5100
|
+
|
|
5004
5101
|
{children}
|
|
5005
5102
|
</div>
|
|
5006
5103
|
);
|
|
@@ -5114,6 +5211,7 @@ export function CommandDialog({
|
|
|
5114
5211
|
)}
|
|
5115
5212
|
>
|
|
5116
5213
|
<DialogTitle className="sr-only">{title}</DialogTitle>
|
|
5214
|
+
|
|
5117
5215
|
<Command close={() => setOpen(false)} className="rounded-none border-0 bg-transparent">
|
|
5118
5216
|
{children}
|
|
5119
5217
|
</Command>
|
|
@@ -5176,7 +5274,7 @@ import {
|
|
|
5176
5274
|
DialogTitle,
|
|
5177
5275
|
} from "@/components/ui/dialog";
|
|
5178
5276
|
|
|
5179
|
-
type ConfirmOptions = {
|
|
5277
|
+
export type ConfirmOptions = {
|
|
5180
5278
|
/** Question in the dialog heading. */
|
|
5181
5279
|
title: ReactNode;
|
|
5182
5280
|
/** Consequences, under the title. */
|
|
@@ -5240,10 +5338,13 @@ export function Confirmer() {
|
|
|
5240
5338
|
<DialogContent alert className="max-w-md">
|
|
5241
5339
|
<DialogHeader>
|
|
5242
5340
|
<DialogTitle>{title}</DialogTitle>
|
|
5341
|
+
|
|
5243
5342
|
{description && <DialogDescription>{description}</DialogDescription>}
|
|
5244
5343
|
</DialogHeader>
|
|
5344
|
+
|
|
5245
5345
|
<DialogFooter>
|
|
5246
5346
|
<DialogClose>{cancelLabel}</DialogClose>
|
|
5347
|
+
|
|
5247
5348
|
<Button variant={destructive ? "destructive" : "default"} onClick={() => settle(true)}>
|
|
5248
5349
|
{options.confirmLabel ?? (destructive ? "Delete" : "Continue")}
|
|
5249
5350
|
</Button>
|
|
@@ -5496,9 +5597,11 @@ export function CountTo({
|
|
|
5496
5597
|
return (
|
|
5497
5598
|
<span ref={ref} className={className}>
|
|
5498
5599
|
<span className="sr-only">{target}</span>
|
|
5600
|
+
|
|
5499
5601
|
<span ref={out} aria-hidden="true" data-count-to-animated="">
|
|
5500
5602
|
{text(from)}
|
|
5501
5603
|
</span>
|
|
5604
|
+
|
|
5502
5605
|
<span aria-hidden="true" data-count-to-fallback="" className="hidden">
|
|
5503
5606
|
{target}
|
|
5504
5607
|
</span>
|
|
@@ -5607,6 +5710,7 @@ export function CustomCursor() {
|
|
|
5607
5710
|
}}
|
|
5608
5711
|
transition={spring}
|
|
5609
5712
|
/>
|
|
5713
|
+
|
|
5610
5714
|
<motion.div
|
|
5611
5715
|
className="absolute top-0 left-0 size-1.5 rounded-full bg-foreground"
|
|
5612
5716
|
style={{ x, y, translateX: "-50%", translateY: "-50%" }}
|
|
@@ -5669,7 +5773,7 @@ export function MembersDataTable({
|
|
|
5669
5773
|
)}
|
|
5670
5774
|
/>
|
|
5671
5775
|
);
|
|
5672
|
-
}` }], api: { entries: [{ kind: "component", name: "DataTable", props: [{ name: "columns", type: "DataTableColumn<T>[]", required: true, description: "Column definitions in display order." }, { name: "data", type: "T[]", required: true, description: "Rows; search, sort and paging happen on the client." }, { name: "getRowId", type: "(row: T) => string", required: true, description: "Stable id per row; selection is kept by it across pages." }, { name: "label", type: "string", required: true, description: `Names the table, the search box and each row's controls: "Members", "Select Jane".` }, { name: "rowLabel", type: "(row: T) => string", required: true, description: "Row name for screen readers, e.g. on its checkbox and ⋯ menu." }, { name: "defaultSort", type: "
|
|
5776
|
+
}` }], api: { entries: [{ kind: "component", name: "DataTable", props: [{ name: "columns", type: "DataTableColumn<T>[]", required: true, description: "Column definitions in display order." }, { name: "data", type: "T[]", required: true, description: "Rows; search, sort and paging happen on the client." }, { name: "getRowId", type: "(row: T) => string", required: true, description: "Stable id per row; selection is kept by it across pages." }, { name: "label", type: "string", required: true, description: `Names the table, the search box and each row's controls: "Members", "Select Jane".` }, { name: "rowLabel", type: "(row: T) => string", required: true, description: "Row name for screen readers, e.g. on its checkbox and ⋯ menu." }, { name: "defaultSort", type: "DataTableSort", required: false, description: "Sort at first render." }, { name: "onSelectionChange", type: "(ids: string[]) => void", required: false, description: "Called with every selected row id." }, { name: "pageSize", type: "number", required: false, default: "10", description: "Rows per page." }, { name: "rowActions", type: "(row: T) => ReactNode", required: false, description: "Items for the row's ⋯ menu (DropdownMenuItem and friends)." }, { name: "searchable", type: "boolean", required: false, default: "true", description: "Show a search field that filters every column." }, { name: "searchPlaceholder", type: "string", required: false, default: '"Search…"', description: "Search field placeholder." }, { name: "selectable", type: "boolean", required: false, default: "false", description: "Checkbox column; selection survives search and paging." }, { name: "selection", type: "string[]", required: false, description: "Controlled selected row ids; pair with `onSelectionChange`." }, { name: "striped", type: "boolean", required: false, default: "false", description: "Alternate row backgrounds." }, { name: "toolbar", type: "(selected: T[]) => ReactNode", required: false, description: "Right side of the toolbar; receives the selected rows (bulk actions)." }], description: "Table with search, sortable columns, row selection, row actions and pagination, all on the\nclient. Columns are data: `{ id, header, accessor, cell?, sortable? }`. For server-driven\ndata, compose Table + Pagination yourself." }], types: { DataTableSort: '{ id: string; dir: "ascending" | "descending" }', DataTableColumn: "{ id: string; header: ReactNode; accessor?: (row: T) => string | number; cell?: (row: T) => ReactNode; sortable?: boolean; className?: string; }" } }, dependencies: ["lucide-react@1.63.0"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/button.json", "https://kznlabs.github.io/labsUI/r/checkbox.json", "https://kznlabs.github.io/labsUI/r/dropdown-menu.json", "https://kznlabs.github.io/labsUI/r/empty.json", "https://kznlabs.github.io/labsUI/r/pagination.json", "https://kznlabs.github.io/labsUI/r/search-input.json", "https://kznlabs.github.io/labsUI/r/table.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/data-table.tsx", type: "registry:ui", content: `"use client";
|
|
5673
5777
|
|
|
5674
5778
|
import { MoreHorizontal, SearchX } from "lucide-react";
|
|
5675
5779
|
import { type ReactNode, useLayoutEffect, useRef, useState } from "react";
|
|
@@ -5694,7 +5798,7 @@ import {
|
|
|
5694
5798
|
import { useControllableState } from "@/lib/use-controllable-state";
|
|
5695
5799
|
import { cn } from "@/lib/utils";
|
|
5696
5800
|
|
|
5697
|
-
type
|
|
5801
|
+
export type DataTableSort = { id: string; dir: "ascending" | "descending" };
|
|
5698
5802
|
|
|
5699
5803
|
export type DataTableColumn<T> = {
|
|
5700
5804
|
/** Unique key; also used by \`defaultSort\`. */
|
|
@@ -5723,7 +5827,7 @@ type DataTableProps<T> = {
|
|
|
5723
5827
|
/** Rows per page. */
|
|
5724
5828
|
pageSize?: number;
|
|
5725
5829
|
/** Sort at first render. */
|
|
5726
|
-
defaultSort?:
|
|
5830
|
+
defaultSort?: DataTableSort;
|
|
5727
5831
|
/** Show a search field that filters every column. */
|
|
5728
5832
|
searchable?: boolean;
|
|
5729
5833
|
/** Search field placeholder. */
|
|
@@ -5852,6 +5956,7 @@ export function DataTable<T>({
|
|
|
5852
5956
|
className="w-full sm:max-w-64"
|
|
5853
5957
|
/>
|
|
5854
5958
|
)}
|
|
5959
|
+
|
|
5855
5960
|
{toolbar && (
|
|
5856
5961
|
<div className="ml-auto flex items-center gap-2">
|
|
5857
5962
|
{toolbar(data.filter((row) => selected.includes(getRowId(row))))}
|
|
@@ -5881,6 +5986,7 @@ export function DataTable<T>({
|
|
|
5881
5986
|
/>
|
|
5882
5987
|
</TableHead>
|
|
5883
5988
|
)}
|
|
5989
|
+
|
|
5884
5990
|
{columns.map((column) => (
|
|
5885
5991
|
<TableHead
|
|
5886
5992
|
key={column.id}
|
|
@@ -5904,6 +6010,7 @@ export function DataTable<T>({
|
|
|
5904
6010
|
{column.header}
|
|
5905
6011
|
</TableHead>
|
|
5906
6012
|
))}
|
|
6013
|
+
|
|
5907
6014
|
{rowActions && (
|
|
5908
6015
|
<TableHead className="w-px">
|
|
5909
6016
|
<span className="sr-only">Actions</span>
|
|
@@ -5911,6 +6018,7 @@ export function DataTable<T>({
|
|
|
5911
6018
|
)}
|
|
5912
6019
|
</TableRow>
|
|
5913
6020
|
</TableHeader>
|
|
6021
|
+
|
|
5914
6022
|
<TableBody>
|
|
5915
6023
|
{rows.length === 0 ? (
|
|
5916
6024
|
<TableRow className="hover:bg-transparent">
|
|
@@ -5946,11 +6054,13 @@ export function DataTable<T>({
|
|
|
5946
6054
|
/>
|
|
5947
6055
|
</TableCell>
|
|
5948
6056
|
)}
|
|
6057
|
+
|
|
5949
6058
|
{columns.map((column) => (
|
|
5950
6059
|
<TableCell key={column.id} className={column.className}>
|
|
5951
6060
|
{column.cell ? column.cell(row) : column.accessor?.(row)}
|
|
5952
6061
|
</TableCell>
|
|
5953
6062
|
))}
|
|
6063
|
+
|
|
5954
6064
|
{rowActions && (
|
|
5955
6065
|
<TableCell className="py-1 pr-2">
|
|
5956
6066
|
<DropdownMenu>
|
|
@@ -5962,6 +6072,7 @@ export function DataTable<T>({
|
|
|
5962
6072
|
>
|
|
5963
6073
|
<MoreHorizontal />
|
|
5964
6074
|
</DropdownMenuTrigger>
|
|
6075
|
+
|
|
5965
6076
|
<DropdownMenuContent align="end">{rowActions(row)}</DropdownMenuContent>
|
|
5966
6077
|
</DropdownMenu>
|
|
5967
6078
|
</TableCell>
|
|
@@ -5978,10 +6089,12 @@ export function DataTable<T>({
|
|
|
5978
6089
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
5979
6090
|
<p className="text-muted-foreground text-xs tabular-nums" aria-live="polite">
|
|
5980
6091
|
{selectable && selected.length > 0 && \`\${selected.length} selected · \`}
|
|
6092
|
+
|
|
5981
6093
|
{sorted.length === 0
|
|
5982
6094
|
? "0 results"
|
|
5983
6095
|
: \`\${from}–\${from + rows.length - 1} of \${sorted.length}\`}
|
|
5984
6096
|
</p>
|
|
6097
|
+
|
|
5985
6098
|
{pages > 1 && (
|
|
5986
6099
|
<Pagination
|
|
5987
6100
|
page={page}
|
|
@@ -5994,7 +6107,7 @@ export function DataTable<T>({
|
|
|
5994
6107
|
</div>
|
|
5995
6108
|
);
|
|
5996
6109
|
}
|
|
5997
|
-
` }] }, "date-picker": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "date-picker", type: "registry:ui", title: "Date Picker", description: "Field-styled button that opens a Calendar in a Popover, focused on the picked day. Single or range, presets such as Last 7 days, closes once the pick is complete, and submits ISO dates
|
|
6110
|
+
` }] }, "date-picker": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "date-picker", type: "registry:ui", title: "Date Picker", description: "Field-styled button that opens a Calendar in a Popover, focused on the picked day. Single or range, presets such as Last 7 days, closes once the pick is complete, and submits ISO dates with the form. Works inside Field.", docs: "https://kznlabs.github.io/labsUI/components/date-picker", usage: '<Field label="Report period"><DatePicker mode="range" name="period" presets={[{ label: "Last 7 days", value: { from, to } }]} /></Field>', examples: [{ name: "report-filters.tsx", code: `"use client";
|
|
5998
6111
|
|
|
5999
6112
|
import { DatePicker } from "@/components/ui/date-picker";
|
|
6000
6113
|
import { Field } from "@/components/ui/field";
|
|
@@ -6017,19 +6130,20 @@ export function ReportFilters() {
|
|
|
6017
6130
|
/>
|
|
6018
6131
|
</Field>
|
|
6019
6132
|
);
|
|
6020
|
-
}` }], api: { entries: [{ kind: "component", name: "DatePicker", props: [{ name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "aria-label", type: "string", required: false, description: "Accessible name when there is no visible label." }, { name: "defaultValue", type: "Date | DateRange | null", required: false, description: "Day or range at first render (uncontrolled)." }, { name: "disabled", type: "boolean", required: false, description: "Blocks opening." }, { name: "id", type: "string", required: false, description: "Pair with <Label htmlFor>, or let <Field> fill it in." }, { name: "isDisabled", type: "(date: Date) => boolean", required: false, description: "Extra rule, e.g. weekends off. Client-only (a function)." }, { name: "locale", type: "string", required: false, description: "BCP 47 locale for the button text and the calendar." }, { name: "max", type: "Date", required: false, description: "Latest selectable day." }, { name: "min", type: "Date", required: false, description: "Earliest selectable day." }, { name: "mode", type: '"single" | "range"', required: false, description: '"single": pick one day. "range": pick a start, then an end.' }, { name: "name", type: "string", required: false, description: "Submits ISO
|
|
6021
|
-
date closes on pick; a range closes once both ends are set. Optional presets, form-ready.` }], types: {
|
|
6133
|
+
}` }], api: { entries: [{ kind: "component", name: "DatePicker", props: [{ name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "aria-label", type: "string", required: false, description: "Accessible name when there is no visible label." }, { name: "defaultValue", type: "Date | DateRange | null", required: false, description: "Day or range at first render (uncontrolled)." }, { name: "disabled", type: "boolean", required: false, description: "Blocks opening; the value isn't submitted." }, { name: "id", type: "string", required: false, description: "Pair with <Label htmlFor>, or let <Field> fill it in." }, { name: "isDisabled", type: "(date: Date) => boolean", required: false, description: "Extra rule, e.g. weekends off. Client-only (a function)." }, { name: "locale", type: "string", required: false, description: "BCP 47 locale for the button text and the calendar." }, { name: "max", type: "Date", required: false, description: "Latest selectable day." }, { name: "min", type: "Date", required: false, description: "Earliest selectable day." }, { name: "mode", type: '"single" | "range"', required: false, description: '"single": pick one day. "range": pick a start, then an end.' }, { name: "name", type: "string", required: false, description: "Submits ISO: `2026-10-21`, or `2026-10-03/2026-10-09` for a range." }, { name: "onValueChange", type: "((date: Date) => void) | ((range: DateRange) => void)", required: false, description: "Called with the picked day, or with the range once both ends are set." }, { name: "placeholder", type: "string", required: false, description: "Button text when nothing is picked." }, { name: "presets", type: "DatePickerPreset<Date>[] | DatePickerPreset<DateRange>[]", required: false, description: "Shortcuts beside the grid, e.g. Today, or Last 7 days for a range. Readers reach for these first." }, { name: "required", type: "boolean", required: false, description: "Form submit needs a date." }, { name: "value", type: "Date | DateRange | null", required: false, description: "Controlled day, or `{ from, to }` in range mode; null = nothing picked." }, { name: "weekStartsOn", type: "0 | 1 | 2 | 3 | 4 | 5 | 6", required: false, description: "0 is Sunday, 6 is Saturday. Default 1 (Monday, ISO)." }], description: `Field-styled button that opens a Calendar in a Popover, focused on the picked day. A single
|
|
6134
|
+
date closes on pick; a range closes once both ends are set. Optional presets, form-ready.` }], types: { DatePickerPreset: "{ label: string; value: Value }" } }, dependencies: ["lucide-react@1.63.0"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/calendar.json", "https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/form-value.json", "https://kznlabs.github.io/labsUI/r/input.json", "https://kznlabs.github.io/labsUI/r/popover.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/date-picker.tsx", type: "registry:ui", content: `"use client";
|
|
6022
6135
|
|
|
6023
6136
|
import { CalendarDays } from "lucide-react";
|
|
6024
|
-
import { useState } from "react";
|
|
6137
|
+
import { useRef, useState } from "react";
|
|
6025
6138
|
import { Calendar, type DateRange, key } from "@/components/ui/calendar";
|
|
6026
6139
|
import { useFieldControl } from "@/components/ui/field";
|
|
6027
6140
|
import { fieldClass } from "@/components/ui/input";
|
|
6028
6141
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
6142
|
+
import { FormValue } from "@/lib/form-value";
|
|
6029
6143
|
import { useControllableState } from "@/lib/use-controllable-state";
|
|
6030
6144
|
import { cn } from "@/lib/utils";
|
|
6031
6145
|
|
|
6032
|
-
type
|
|
6146
|
+
export type DatePickerPreset<Value> = { label: string; value: Value };
|
|
6033
6147
|
|
|
6034
6148
|
type Base = {
|
|
6035
6149
|
/** Button text when nothing is picked. */
|
|
@@ -6044,11 +6158,11 @@ type Base = {
|
|
|
6044
6158
|
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
6045
6159
|
/** BCP 47 locale for the button text and the calendar. */
|
|
6046
6160
|
locale?: string;
|
|
6047
|
-
/** Submits ISO
|
|
6161
|
+
/** Submits ISO: \`2026-10-21\`, or \`2026-10-03/2026-10-09\` for a range. */
|
|
6048
6162
|
name?: string;
|
|
6049
6163
|
/** Pair with <Label htmlFor>, or let <Field> fill it in. */
|
|
6050
6164
|
id?: string;
|
|
6051
|
-
/** Blocks opening. */
|
|
6165
|
+
/** Blocks opening; the value isn't submitted. */
|
|
6052
6166
|
disabled?: boolean;
|
|
6053
6167
|
/** Form submit needs a date. */
|
|
6054
6168
|
required?: boolean;
|
|
@@ -6071,7 +6185,7 @@ type SingleProps = Base & {
|
|
|
6071
6185
|
/** Called with the picked day, or with the range once both ends are set. */
|
|
6072
6186
|
onValueChange?: (date: Date) => void;
|
|
6073
6187
|
/** Shortcuts beside the grid, e.g. Today, or Last 7 days for a range. Readers reach for these first. */
|
|
6074
|
-
presets?:
|
|
6188
|
+
presets?: DatePickerPreset<Date>[];
|
|
6075
6189
|
};
|
|
6076
6190
|
|
|
6077
6191
|
type RangeProps = Base & {
|
|
@@ -6079,7 +6193,7 @@ type RangeProps = Base & {
|
|
|
6079
6193
|
value?: DateRange | null;
|
|
6080
6194
|
defaultValue?: DateRange | null;
|
|
6081
6195
|
onValueChange?: (range: DateRange) => void;
|
|
6082
|
-
presets?:
|
|
6196
|
+
presets?: DatePickerPreset<DateRange>[];
|
|
6083
6197
|
};
|
|
6084
6198
|
|
|
6085
6199
|
type DatePickerProps = SingleProps | RangeProps;
|
|
@@ -6102,9 +6216,12 @@ export function DatePicker(allProps: DatePickerProps) {
|
|
|
6102
6216
|
weekStartsOn,
|
|
6103
6217
|
locale = "en-US",
|
|
6104
6218
|
name,
|
|
6219
|
+
required,
|
|
6105
6220
|
className,
|
|
6106
6221
|
...trigger
|
|
6107
6222
|
} = useFieldControl(allProps);
|
|
6223
|
+
const anchor = useRef<HTMLButtonElement>(null);
|
|
6224
|
+
const [invalid, setInvalid] = useState(false);
|
|
6108
6225
|
const range = mode === "range";
|
|
6109
6226
|
const [open, setOpen] = useState(false);
|
|
6110
6227
|
const [value, setValue] = useControllableState<Date | DateRange | null>(
|
|
@@ -6143,6 +6260,7 @@ export function DatePicker(allProps: DatePickerProps) {
|
|
|
6143
6260
|
<>
|
|
6144
6261
|
<Popover open={open} onOpenChange={setOpen}>
|
|
6145
6262
|
<PopoverTrigger
|
|
6263
|
+
ref={anchor}
|
|
6146
6264
|
variant="outline"
|
|
6147
6265
|
className={cn(
|
|
6148
6266
|
fieldClass,
|
|
@@ -6151,10 +6269,13 @@ export function DatePicker(allProps: DatePickerProps) {
|
|
|
6151
6269
|
className,
|
|
6152
6270
|
)}
|
|
6153
6271
|
{...trigger}
|
|
6272
|
+
aria-invalid={trigger["aria-invalid"] || (invalid && !iso) || undefined}
|
|
6154
6273
|
>
|
|
6155
6274
|
<CalendarDays />
|
|
6275
|
+
|
|
6156
6276
|
<span className="truncate">{label ?? placeholder}</span>
|
|
6157
6277
|
</PopoverTrigger>
|
|
6278
|
+
|
|
6158
6279
|
<PopoverContent align="start" className="flex w-auto flex-col gap-3 p-3 sm:flex-row">
|
|
6159
6280
|
{presets && presets.length > 0 && (
|
|
6160
6281
|
// No room beside the calendar on phones.
|
|
@@ -6178,6 +6299,7 @@ export function DatePicker(allProps: DatePickerProps) {
|
|
|
6178
6299
|
))}
|
|
6179
6300
|
</ul>
|
|
6180
6301
|
)}
|
|
6302
|
+
|
|
6181
6303
|
{range ? (
|
|
6182
6304
|
<Calendar key={shown} mode="range" {...shared} value={span} onValueChange={commit} />
|
|
6183
6305
|
) : (
|
|
@@ -6190,7 +6312,15 @@ export function DatePicker(allProps: DatePickerProps) {
|
|
|
6190
6312
|
)}
|
|
6191
6313
|
</PopoverContent>
|
|
6192
6314
|
</Popover>
|
|
6193
|
-
|
|
6315
|
+
|
|
6316
|
+
<FormValue
|
|
6317
|
+
name={name}
|
|
6318
|
+
value={iso}
|
|
6319
|
+
required={required}
|
|
6320
|
+
disabled={trigger.disabled}
|
|
6321
|
+
anchor={anchor}
|
|
6322
|
+
onInvalid={() => setInvalid(true)}
|
|
6323
|
+
/>
|
|
6194
6324
|
</>
|
|
6195
6325
|
);
|
|
6196
6326
|
}
|
|
@@ -6219,8 +6349,8 @@ export function EditProfile() {
|
|
|
6219
6349
|
</DialogContent>
|
|
6220
6350
|
</Dialog>
|
|
6221
6351
|
);
|
|
6222
|
-
}` }], api: { entries: [{ kind: "function", name: "useDialog", signature: "useDialog() =>
|
|
6223
|
-
lock page scroll, close on Esc (topmost modal only). Attach the returned ref to the panel.` }, { kind: "component", name: "DialogContent", props: [{ name: "children", type: "ReactNode", required: true, description: "Header, body and footer." }, { name: "alert", type: "boolean", required: false, default: "false", description: 'Confirm-a-destructive-action mode: role="alertdialog", backdrop click does not close.' }, { name: "showClose", type: "boolean", required: false, default: "!alert", description: "Close button in the corner. Off by default for `alert` dialogs." }], description: "Portal + blurred backdrop + spring scale-in. Esc/backdrop close, focus trap, scroll lock." }, { kind: "component", name: "DialogHeader", props: [], extends: "<div>" }, { kind: "component", name: "DialogFooter", props: [], extends: "<div>" }, { kind: "component", name: "DialogTitle", props: [], extends: "<h2>" }, { kind: "component", name: "DialogDescription", props: [], extends: "<p>" }], types: {
|
|
6352
|
+
}` }], api: { entries: [{ kind: "function", name: "useDialog", signature: "useDialog() => DialogState" }, { kind: "component", name: "Dialog", props: [{ name: "children", type: "ReactNode", required: true, description: "`DialogTrigger` and `DialogContent`." }, { name: "defaultOpen", type: "boolean", required: false, default: "false", description: "Open at first render (uncontrolled)." }, { name: "onOpenChange", type: "(open: boolean) => void", required: false, description: "Called on the trigger, Esc, backdrop click or `DialogClose`." }, { name: "open", type: "boolean", required: false, description: "Controlled state; pair with `onOpenChange`." }], description: "State root. Controlled (`open` + `onOpenChange`) or uncontrolled." }, { kind: "component", name: "DialogTrigger", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "Shows a spinner before the label and ignores clicks and Enter, so a form can't submit twice. Stays focusable." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`. Both ignore clicks and Enter, so a form can't submit twice." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }], extends: "Button" }, { kind: "component", name: "DialogClose", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "Shows a spinner before the label and ignores clicks and Enter, so a form can't submit twice. Stays focusable." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`. Both ignore clicks and Enter, so a form can't submit twice." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, default: '"outline"', description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }], extends: "Button" }, { kind: "function", name: "useModal", signature: "useModal() => RefObject<HTMLDivElement | null>", description: `Modal behavior shared by Dialog and Drawer: focus first field, trap Tab, restore focus,
|
|
6353
|
+
lock page scroll, close on Esc (topmost modal only). Attach the returned ref to the panel.` }, { kind: "component", name: "DialogContent", props: [{ name: "children", type: "ReactNode", required: true, description: "Header, body and footer." }, { name: "alert", type: "boolean", required: false, default: "false", description: 'Confirm-a-destructive-action mode: role="alertdialog", backdrop click does not close.' }, { name: "showClose", type: "boolean", required: false, default: "!alert", description: "Close button in the corner. Off by default for `alert` dialogs." }], description: "Portal + blurred backdrop + spring scale-in. Esc/backdrop close, focus trap, scroll lock." }, { kind: "component", name: "DialogHeader", props: [], extends: "<div>" }, { kind: "component", name: "DialogFooter", props: [], extends: "<div>" }, { kind: "component", name: "DialogTitle", props: [], extends: "<h2>" }, { kind: "component", name: "DialogDescription", props: [], extends: "<p>" }], types: { DialogState: "{ open: boolean; setOpen: (open: boolean) => void; titleId: string; descId: string; }" } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/button.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/dialog.tsx", type: "registry:ui", content: `"use client";
|
|
6224
6354
|
|
|
6225
6355
|
import { X } from "lucide-react";
|
|
6226
6356
|
import { AnimatePresence, motion, useIsPresent } from "motion/react";
|
|
@@ -6240,9 +6370,14 @@ import { useControllableState } from "@/lib/use-controllable-state";
|
|
|
6240
6370
|
import { cn, spring } from "@/lib/utils";
|
|
6241
6371
|
import { Button } from "./button";
|
|
6242
6372
|
|
|
6243
|
-
type
|
|
6373
|
+
export type DialogState = {
|
|
6374
|
+
open: boolean;
|
|
6375
|
+
setOpen: (open: boolean) => void;
|
|
6376
|
+
titleId: string;
|
|
6377
|
+
descId: string;
|
|
6378
|
+
};
|
|
6244
6379
|
|
|
6245
|
-
const DialogContext = createContext<
|
|
6380
|
+
const DialogContext = createContext<DialogState | null>(null);
|
|
6246
6381
|
|
|
6247
6382
|
export function useDialog() {
|
|
6248
6383
|
const ctx = use(DialogContext);
|
|
@@ -6414,6 +6549,7 @@ export function DialogContent({
|
|
|
6414
6549
|
transition={spring}
|
|
6415
6550
|
onClick={alert ? undefined : () => setOpen(false)}
|
|
6416
6551
|
/>
|
|
6552
|
+
|
|
6417
6553
|
<motion.div
|
|
6418
6554
|
ref={ref}
|
|
6419
6555
|
role={alert ? "alertdialog" : "dialog"}
|
|
@@ -6431,6 +6567,7 @@ export function DialogContent({
|
|
|
6431
6567
|
)}
|
|
6432
6568
|
>
|
|
6433
6569
|
{children}
|
|
6570
|
+
|
|
6434
6571
|
{showClose && (
|
|
6435
6572
|
<button
|
|
6436
6573
|
type="button"
|
|
@@ -6438,6 +6575,7 @@ export function DialogContent({
|
|
|
6438
6575
|
className="absolute top-4 right-4 cursor-pointer rounded-md p-1 text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
|
6439
6576
|
>
|
|
6440
6577
|
<X className="size-4" aria-hidden="true" />
|
|
6578
|
+
|
|
6441
6579
|
<span className="sr-only">Close</span>
|
|
6442
6580
|
</button>
|
|
6443
6581
|
)}
|
|
@@ -6519,8 +6657,8 @@ export function CartDrawer({ items }: { items: string[] }) {
|
|
|
6519
6657
|
</DrawerContent>
|
|
6520
6658
|
</Drawer>
|
|
6521
6659
|
);
|
|
6522
|
-
}` }], api: { entries: [{ kind: "component", name: "Drawer", props: [{ name: "children", type: "ReactNode", required: true, description: "`DialogTrigger` and `DialogContent`." }, { name: "defaultOpen", type: "boolean", required: false, default: "false", description: "Open at first render (uncontrolled)." }, { name: "onOpenChange", type: "(open: boolean) => void", required: false, description: "Called on the trigger, Esc, backdrop click or `DialogClose`." }, { name: "open", type: "boolean", required: false, description: "Controlled state; pair with `onOpenChange`." }] }, { kind: "component", name: "DrawerTrigger", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "
|
|
6523
|
-
drag from anywhere, top/bottom drawers from the handle or header so their content can scroll.` }, { kind: "component", name: "DrawerHeader", props: [], extends: "<div>", description: "Also a drag handle on top/bottom drawers." }, { kind: "component", name: "DrawerBody", props: [], extends: "<div>", description: "Scrolls when content is taller than the drawer." }, { kind: "component", name: "DrawerFooter", props: [], extends: "<div>" }], types: {
|
|
6660
|
+
}` }], api: { entries: [{ kind: "component", name: "Drawer", props: [{ name: "children", type: "ReactNode", required: true, description: "`DialogTrigger` and `DialogContent`." }, { name: "defaultOpen", type: "boolean", required: false, default: "false", description: "Open at first render (uncontrolled)." }, { name: "onOpenChange", type: "(open: boolean) => void", required: false, description: "Called on the trigger, Esc, backdrop click or `DialogClose`." }, { name: "open", type: "boolean", required: false, description: "Controlled state; pair with `onOpenChange`." }] }, { kind: "component", name: "DrawerTrigger", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "Shows a spinner before the label and ignores clicks and Enter, so a form can't submit twice. Stays focusable." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`. Both ignore clicks and Enter, so a form can't submit twice." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }], extends: "Button" }, { kind: "component", name: "DrawerClose", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "Shows a spinner before the label and ignores clicks and Enter, so a form can't submit twice. Stays focusable." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`. Both ignore clicks and Enter, so a form can't submit twice." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, default: '"outline"', description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }], extends: "Button" }, { kind: "component", name: "DrawerTitle", props: [], extends: "<h2>" }, { kind: "component", name: "DrawerDescription", props: [], extends: "<p>" }, { kind: "component", name: "DrawerContent", props: [{ name: "children", type: "ReactNode", required: true, description: "Header, body and footer." }, { name: "showClose", type: "boolean", required: false, default: 'side === "left" || side === "right"', description: "Close button in the corner. On by default for side drawers." }, { name: "side", type: "DrawerSide", required: false, default: '"right"', description: "Edge it slides in from." }], description: `Panel that slides in from an edge on a spring. Swipe toward the edge to dismiss: side drawers
|
|
6661
|
+
drag from anywhere, top/bottom drawers from the handle or header so their content can scroll.` }, { kind: "component", name: "DrawerHeader", props: [], extends: "<div>", description: "Also a drag handle on top/bottom drawers." }, { kind: "component", name: "DrawerBody", props: [], extends: "<div>", description: "Scrolls when content is taller than the drawer." }, { kind: "component", name: "DrawerFooter", props: [], extends: "<div>" }], types: { DrawerSide: '"left" | "right" | "top" | "bottom"' } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/dialog.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/drawer.tsx", type: "registry:ui", content: `"use client";
|
|
6524
6662
|
|
|
6525
6663
|
import { X } from "lucide-react";
|
|
6526
6664
|
import { AnimatePresence, motion, type PanInfo, useDragControls, useIsPresent } from "motion/react";
|
|
@@ -6546,9 +6684,9 @@ export const DrawerClose = DialogClose;
|
|
|
6546
6684
|
export const DrawerTitle = DialogTitle;
|
|
6547
6685
|
export const DrawerDescription = DialogDescription;
|
|
6548
6686
|
|
|
6549
|
-
type
|
|
6687
|
+
export type DrawerSide = "left" | "right" | "top" | "bottom";
|
|
6550
6688
|
|
|
6551
|
-
const offscreen: Record<
|
|
6689
|
+
const offscreen: Record<DrawerSide, { x?: string; y?: string }> = {
|
|
6552
6690
|
left: { x: "-100%" },
|
|
6553
6691
|
right: { x: "100%" },
|
|
6554
6692
|
top: { y: "-100%" },
|
|
@@ -6557,7 +6695,7 @@ const offscreen: Record<Side, { x?: string; y?: string }> = {
|
|
|
6557
6695
|
|
|
6558
6696
|
// The \`after:\` bleed paints past the screen edge, so overshoot or dragging inward never shows a
|
|
6559
6697
|
// gap.
|
|
6560
|
-
const placement: Record<
|
|
6698
|
+
const placement: Record<DrawerSide, string> = {
|
|
6561
6699
|
left: "inset-y-0 left-0 h-full w-[min(20rem,85vw)] border-r after:right-full after:inset-y-0 after:w-[50vw]",
|
|
6562
6700
|
right:
|
|
6563
6701
|
"inset-y-0 right-0 h-full w-[min(24rem,90vw)] border-l after:left-full after:inset-y-0 after:w-[50vw]",
|
|
@@ -6581,7 +6719,7 @@ export function DrawerContent({
|
|
|
6581
6719
|
children,
|
|
6582
6720
|
}: {
|
|
6583
6721
|
/** Edge it slides in from. */
|
|
6584
|
-
side?:
|
|
6722
|
+
side?: DrawerSide;
|
|
6585
6723
|
/** Close button in the corner. On by default for side drawers. */
|
|
6586
6724
|
showClose?: boolean;
|
|
6587
6725
|
className?: string;
|
|
@@ -6630,6 +6768,7 @@ export function DrawerContent({
|
|
|
6630
6768
|
transition={spring}
|
|
6631
6769
|
onClick={() => setOpen(false)}
|
|
6632
6770
|
/>
|
|
6771
|
+
|
|
6633
6772
|
<motion.div
|
|
6634
6773
|
ref={ref}
|
|
6635
6774
|
role="dialog"
|
|
@@ -6663,8 +6802,11 @@ export function DrawerContent({
|
|
|
6663
6802
|
)}
|
|
6664
6803
|
>
|
|
6665
6804
|
{side === "bottom" && <Handle />}
|
|
6805
|
+
|
|
6666
6806
|
{children}
|
|
6807
|
+
|
|
6667
6808
|
{side === "top" && <Handle />}
|
|
6809
|
+
|
|
6668
6810
|
{showClose && (
|
|
6669
6811
|
<button
|
|
6670
6812
|
type="button"
|
|
@@ -6673,6 +6815,7 @@ export function DrawerContent({
|
|
|
6673
6815
|
className="absolute top-4 right-4 cursor-pointer rounded-md p-1 text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
|
6674
6816
|
>
|
|
6675
6817
|
<X className="size-4" aria-hidden="true" />
|
|
6818
|
+
|
|
6676
6819
|
<span className="sr-only">Close</span>
|
|
6677
6820
|
</button>
|
|
6678
6821
|
)}
|
|
@@ -6773,7 +6916,7 @@ export function ClipActions({ onDelete }: { onDelete: () => void }) {
|
|
|
6773
6916
|
</DropdownMenuContent>
|
|
6774
6917
|
</DropdownMenu>
|
|
6775
6918
|
);
|
|
6776
|
-
}` }], api: { entries: [{ kind: "component", name: "DropdownMenu", props: [{ name: "children", type: "ReactNode", required: true, description: "Trigger and content." }, { name: "defaultOpen", type: "boolean", required: false, default: "false", description: "Open at first render (uncontrolled)." }, { name: "onOpenChange", type: "(open: boolean) => void", required: false, description: "Called on the trigger, Esc, outside click or a selection." }, { name: "open", type: "boolean", required: false, description: "Controlled state; pair with `onOpenChange`." }], description: "Action menu anchored to a button. Controlled (`open` + `onOpenChange`) or uncontrolled." }, { kind: "component", name: "DropdownMenuTrigger", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "
|
|
6919
|
+
}` }], api: { entries: [{ kind: "component", name: "DropdownMenu", props: [{ name: "children", type: "ReactNode", required: true, description: "Trigger and content." }, { name: "defaultOpen", type: "boolean", required: false, default: "false", description: "Open at first render (uncontrolled)." }, { name: "onOpenChange", type: "(open: boolean) => void", required: false, description: "Called on the trigger, Esc, outside click or a selection." }, { name: "open", type: "boolean", required: false, description: "Controlled state; pair with `onOpenChange`." }], description: "Action menu anchored to a button. Controlled (`open` + `onOpenChange`) or uncontrolled." }, { kind: "component", name: "DropdownMenuTrigger", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "Shows a spinner before the label and ignores clicks and Enter, so a form can't submit twice. Stays focusable." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`. Both ignore clicks and Enter, so a form can't submit twice." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }], extends: "Button", description: "A Button. Keyboard: Enter, Space or Down opens on the first item, Up on the last." }, { kind: "component", name: "DropdownMenuContent", props: [{ name: "children", type: "ReactNode", required: true, description: "Items, labels, groups and separators." }, { name: "align", type: '"start" | "end"', required: false, default: '"start"', description: "Edge of the trigger the menu lines up with." }] }, { kind: "component", name: "DropdownMenuItem", props: [{ name: "children", type: "ReactNode", required: true, description: "Label, optionally with a leading icon." }, { name: "disabled", type: "boolean", required: false, description: "Shown but not selectable." }, { name: "onSelect", type: "() => void", required: false, description: "Called on click or Enter; the menu then closes." }, { name: "shortcut", type: "string", required: false, description: 'Right-aligned hint, e.g. "⌘S".' }, { name: "variant", type: '"default" | "destructive"', required: false, default: '"default"', description: '"destructive": red, for irreversible actions.' }] }, { kind: "component", name: "DropdownMenuCheckboxItem", props: [{ name: "checked", type: "boolean", required: true, description: "Shows the check mark." }, { name: "children", type: "ReactNode", required: true, description: "Label, optionally with a leading icon." }, { name: "onCheckedChange", type: "(checked: boolean) => void", required: true, description: "Called with the new state." }, { name: "disabled", type: "boolean", required: false, description: "Shown but not selectable." }, { name: "shortcut", type: "string", required: false, description: 'Right-aligned hint, e.g. "⌘S".' }], description: "Toggles without closing the menu." }, { kind: "component", name: "DropdownMenuRadioGroup", props: [{ name: "children", type: "ReactNode", required: true, description: "`DropdownMenuRadioItem`s." }, { name: "onValueChange", type: "(value: string) => void", required: true, description: "Called with the picked item's value." }, { name: "value", type: "string", required: true, description: "Value of the checked item." }] }, { kind: "component", name: "DropdownMenuRadioItem", props: [{ name: "children", type: "ReactNode", required: true, description: "Label, optionally with a leading icon." }, { name: "value", type: "string", required: true, description: "Checked when it equals the group's `value`." }, { name: "disabled", type: "boolean", required: false, description: "Shown but not selectable." }], description: "One choice of a RadioGroup; picking it keeps the menu open." }, { kind: "component", name: "DropdownMenuLabel", props: [], extends: "<div>" }, { kind: "component", name: "DropdownMenuSeparator", props: [] }], types: {} }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/button.json", "https://kznlabs.github.io/labsUI/r/list.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/use-anchor.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/use-dismiss.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/dropdown-menu.tsx", type: "registry:ui", content: `"use client";
|
|
6777
6920
|
|
|
6778
6921
|
import { Check, Circle } from "lucide-react";
|
|
6779
6922
|
import { AnimatePresence, motion } from "motion/react";
|
|
@@ -7061,6 +7204,7 @@ export function DropdownMenuItem({
|
|
|
7061
7204
|
)}
|
|
7062
7205
|
>
|
|
7063
7206
|
{children}
|
|
7207
|
+
|
|
7064
7208
|
<Shortcut>{shortcut}</Shortcut>
|
|
7065
7209
|
</div>
|
|
7066
7210
|
);
|
|
@@ -7091,7 +7235,9 @@ export function DropdownMenuCheckboxItem({
|
|
|
7091
7235
|
<span className="absolute left-2 flex size-4 items-center justify-center">
|
|
7092
7236
|
{checked && <Check className="!text-current" aria-hidden="true" />}
|
|
7093
7237
|
</span>
|
|
7238
|
+
|
|
7094
7239
|
{children}
|
|
7240
|
+
|
|
7095
7241
|
<Shortcut>{shortcut}</Shortcut>
|
|
7096
7242
|
</div>
|
|
7097
7243
|
);
|
|
@@ -7148,6 +7294,7 @@ export function DropdownMenuRadioItem({
|
|
|
7148
7294
|
<span className="absolute left-2 flex size-4 items-center justify-center">
|
|
7149
7295
|
{checked && <Circle className="!size-2 !text-current fill-current" aria-hidden="true" />}
|
|
7150
7296
|
</span>
|
|
7297
|
+
|
|
7151
7298
|
{children}
|
|
7152
7299
|
</div>
|
|
7153
7300
|
);
|
|
@@ -7219,12 +7366,15 @@ export function Empty({
|
|
|
7219
7366
|
{icon}
|
|
7220
7367
|
</span>
|
|
7221
7368
|
)}
|
|
7369
|
+
|
|
7222
7370
|
<div className="grid max-w-sm gap-1.5">
|
|
7223
7371
|
<p className="font-semibold text-base tracking-tight">{title}</p>
|
|
7372
|
+
|
|
7224
7373
|
{description && (
|
|
7225
7374
|
<p className="text-muted-foreground text-sm leading-relaxed">{description}</p>
|
|
7226
7375
|
)}
|
|
7227
7376
|
</div>
|
|
7377
|
+
|
|
7228
7378
|
{children && <div className="flex flex-wrap justify-center gap-2">{children}</div>}
|
|
7229
7379
|
</div>
|
|
7230
7380
|
);
|
|
@@ -7340,6 +7490,7 @@ export function Field({
|
|
|
7340
7490
|
const labelEl = label && (
|
|
7341
7491
|
<Label htmlFor={id} className={cn(disabled && "cursor-not-allowed opacity-50")}>
|
|
7342
7492
|
{label}
|
|
7493
|
+
|
|
7343
7494
|
{required && (
|
|
7344
7495
|
<span aria-hidden="true" className="text-destructive-ink">
|
|
7345
7496
|
*
|
|
@@ -7362,14 +7513,17 @@ export function Field({
|
|
|
7362
7513
|
{horizontal ? (
|
|
7363
7514
|
<div className="flex items-center gap-2.5">
|
|
7364
7515
|
{children}
|
|
7516
|
+
|
|
7365
7517
|
{labelEl}
|
|
7366
7518
|
</div>
|
|
7367
7519
|
) : (
|
|
7368
7520
|
<>
|
|
7369
7521
|
{labelEl}
|
|
7522
|
+
|
|
7370
7523
|
{children}
|
|
7371
7524
|
</>
|
|
7372
7525
|
)}
|
|
7526
|
+
|
|
7373
7527
|
{description && (
|
|
7374
7528
|
<p
|
|
7375
7529
|
id={descId}
|
|
@@ -7378,6 +7532,7 @@ export function Field({
|
|
|
7378
7532
|
{description}
|
|
7379
7533
|
</p>
|
|
7380
7534
|
)}
|
|
7535
|
+
|
|
7381
7536
|
{/* Persistent live region, so a new error is announced. */}
|
|
7382
7537
|
{/* -mt-2 cancels the grid gap while empty; the error brings its own pt-2. */}
|
|
7383
7538
|
<div aria-live="polite" className={cn("-mt-2", horizontal && "pl-6.5")}>
|
|
@@ -7401,6 +7556,45 @@ export function Field({
|
|
|
7401
7556
|
</FieldContext>
|
|
7402
7557
|
);
|
|
7403
7558
|
}
|
|
7559
|
+
` }] }, "form-value": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "form-value", type: "registry:lib", description: "Submits a custom control's value and takes part in native validation, which a hidden input\ncan't: an empty `required` value blocks submit, shows the browser's message and focuses `anchor`.", api: { entries: [{ kind: "component", name: "FormValue", props: [{ name: "anchor", type: "RefObject<HTMLElement | null>", required: true, description: "The visible control that gets focus when the value is invalid." }, { name: "value", type: "string", required: true, description: 'The value to submit, "" when nothing is picked.' }, { name: "disabled", type: "boolean", required: false, description: "Skips validation and leaves the value out of the submit, like a disabled input." }, { name: "name", type: "string", required: false, description: "Form field name; without it the value only takes part in validation." }, { name: "onInvalid", type: "() => void", required: false, description: "Called when a submit finds the value invalid." }, { name: "required", type: "boolean", required: false, description: "An empty value blocks submit." }], description: "Submits a custom control's value and takes part in native validation, which a hidden input\ncan't: an empty `required` value blocks submit, shows the browser's message and focuses `anchor`." }], types: {} }, dependencies: [], registryDependencies: [], files: [{ path: "lib/form-value.tsx", type: "registry:lib", content: `import type { RefObject } from "react";
|
|
7560
|
+
|
|
7561
|
+
type FormValueProps = {
|
|
7562
|
+
/** Form field name; without it the value only takes part in validation. */
|
|
7563
|
+
name?: string;
|
|
7564
|
+
/** The value to submit, "" when nothing is picked. */
|
|
7565
|
+
value: string;
|
|
7566
|
+
/** An empty value blocks submit. */
|
|
7567
|
+
required?: boolean;
|
|
7568
|
+
/** Skips validation and leaves the value out of the submit, like a disabled input. */
|
|
7569
|
+
disabled?: boolean;
|
|
7570
|
+
/** The visible control that gets focus when the value is invalid. */
|
|
7571
|
+
anchor: RefObject<HTMLElement | null>;
|
|
7572
|
+
/** Called when a submit finds the value invalid. */
|
|
7573
|
+
onInvalid?: () => void;
|
|
7574
|
+
};
|
|
7575
|
+
|
|
7576
|
+
/**
|
|
7577
|
+
* Submits a custom control's value and takes part in native validation, which a hidden input
|
|
7578
|
+
* can't: an empty \`required\` value blocks submit, shows the browser's message and focuses \`anchor\`.
|
|
7579
|
+
*/
|
|
7580
|
+
export function FormValue({ name, value, required, disabled, anchor, onInvalid }: FormValueProps) {
|
|
7581
|
+
if (!name && !required) return null;
|
|
7582
|
+
|
|
7583
|
+
return (
|
|
7584
|
+
<input
|
|
7585
|
+
aria-hidden="true"
|
|
7586
|
+
tabIndex={-1}
|
|
7587
|
+
name={name}
|
|
7588
|
+
value={value}
|
|
7589
|
+
required={required}
|
|
7590
|
+
disabled={disabled}
|
|
7591
|
+
onChange={() => undefined}
|
|
7592
|
+
onInvalid={onInvalid}
|
|
7593
|
+
onFocus={() => anchor.current?.focus()}
|
|
7594
|
+
className="pointer-events-none absolute size-px opacity-0"
|
|
7595
|
+
/>
|
|
7596
|
+
);
|
|
7597
|
+
}
|
|
7404
7598
|
` }] }, highlight: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "highlight", type: "registry:lib", description: "Lines of colored tokens, with Shiki loaded on demand. Runs anywhere: Code calls it in the\nbrowser, and a Server Component or a build can call it ahead of time and pass the result as\n`tokens`, so the browser never loads Shiki. Null for unknown languages.", api: { entries: [{ kind: "function", name: "highlightCode", signature: "highlightCode(code: string, language: string) => Promise<CodeToken[][] | null>", description: "Lines of colored tokens, with Shiki loaded on demand. Runs anywhere: Code calls it in the\nbrowser, and a Server Component or a build can call it ahead of time and pass the result as\n`tokens`, so the browser never loads Shiki. Null for unknown languages." }, { kind: "function", name: "languageOf", signature: "languageOf(name: string) => string", description: "Language from a file name's extension. Falls back to tsx." }], types: { CodeToken: "{ content: string; color?: string; fontStyle?: number }" } }, dependencies: ["shiki@^4.5.0"], registryDependencies: [], files: [{ path: "lib/highlight.ts", type: "registry:lib", content: `import type { BundledLanguage } from "shiki";
|
|
7405
7599
|
|
|
7406
7600
|
/** A run of text in one color: \`light-dark()\` holds both themes, so the page's color scheme picks. */
|
|
@@ -7562,6 +7756,7 @@ export function HoverCard({
|
|
|
7562
7756
|
}}
|
|
7563
7757
|
>
|
|
7564
7758
|
{children}
|
|
7759
|
+
|
|
7565
7760
|
{mounted &&
|
|
7566
7761
|
createPortal(
|
|
7567
7762
|
<AnimatePresence>
|
|
@@ -7594,7 +7789,7 @@ export function HoverCard({
|
|
|
7594
7789
|
</span>
|
|
7595
7790
|
);
|
|
7596
7791
|
}
|
|
7597
|
-
` }] }, input: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "input", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", docs: "https://kznlabs.github.io/labsUI/components/forms", usage: '<Label htmlFor="email">Email</Label><Input id="email" type="email" aria-invalid={err} />', examples: [{ name: "sign-up.tsx", code: `import { Input } from "@/components/ui/input";
|
|
7792
|
+
` }] }, input: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "input", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", docs: "https://kznlabs.github.io/labsUI/components/forms", usage: '<Label htmlFor="email">Email</Label><Input id="email" type="email" aria-invalid={err} /><Textarea aria-label="Bio" maxLength={280} showCount /><Switch aria-label="Notifications" checked={on} onCheckedChange={setOn} />', examples: [{ name: "sign-up.tsx", code: `import { Input } from "@/components/ui/input";
|
|
7598
7793
|
import { Label } from "@/components/ui/label";
|
|
7599
7794
|
import { Switch } from "@/components/ui/switch";
|
|
7600
7795
|
import { Textarea } from "@/components/ui/textarea";
|
|
@@ -7869,6 +8064,7 @@ export function InputOTP({
|
|
|
7869
8064
|
className="h-px w-2.5 shrink-0 bg-muted-foreground/50"
|
|
7870
8065
|
/>
|
|
7871
8066
|
)}
|
|
8067
|
+
|
|
7872
8068
|
<div className="flex min-w-0 gap-1.5" aria-hidden="true">
|
|
7873
8069
|
{Array.from({ length: count }, (_, slot) => {
|
|
7874
8070
|
const i = starts[group] + slot;
|
|
@@ -7923,6 +8119,7 @@ export function InputOTP({
|
|
|
7923
8119
|
)
|
|
7924
8120
|
)}
|
|
7925
8121
|
</AnimatePresence>
|
|
8122
|
+
|
|
7926
8123
|
{isActive && !char && (
|
|
7927
8124
|
<span className="absolute h-4 w-px animate-caret-blink bg-foreground motion-reduce:animate-none" />
|
|
7928
8125
|
)}
|
|
@@ -7932,6 +8129,7 @@ export function InputOTP({
|
|
|
7932
8129
|
</div>
|
|
7933
8130
|
</Fragment>
|
|
7934
8131
|
))}
|
|
8132
|
+
|
|
7935
8133
|
<AnimatePresence>
|
|
7936
8134
|
{merge && (
|
|
7937
8135
|
// Slots fold into one bar of the same size: a finished field, not a new widget.
|
|
@@ -7950,6 +8148,7 @@ export function InputOTP({
|
|
|
7950
8148
|
transition={{ ...spring, stiffness: 120, delay: 0.15 }}
|
|
7951
8149
|
className="absolute inset-0 rounded-full border-2 border-success"
|
|
7952
8150
|
/>
|
|
8151
|
+
|
|
7953
8152
|
<motion.span
|
|
7954
8153
|
initial={{ scale: 0 }}
|
|
7955
8154
|
animate={{ scale: 1 }}
|
|
@@ -7970,6 +8169,7 @@ export function InputOTP({
|
|
|
7970
8169
|
</svg>
|
|
7971
8170
|
</motion.span>
|
|
7972
8171
|
</span>
|
|
8172
|
+
|
|
7973
8173
|
<motion.span
|
|
7974
8174
|
initial={{ opacity: 0, x: -6 }}
|
|
7975
8175
|
animate={{ opacity: 1, x: 0 }}
|
|
@@ -7981,9 +8181,11 @@ export function InputOTP({
|
|
|
7981
8181
|
</motion.div>
|
|
7982
8182
|
)}
|
|
7983
8183
|
</AnimatePresence>
|
|
8184
|
+
|
|
7984
8185
|
<span role="status" className="sr-only">
|
|
7985
8186
|
{verified ? verifiedLabel : null}
|
|
7986
8187
|
</span>
|
|
8188
|
+
|
|
7987
8189
|
<input
|
|
7988
8190
|
type="text"
|
|
7989
8191
|
autoComplete="one-time-code"
|
|
@@ -8060,7 +8262,7 @@ export function KbdGroup({
|
|
|
8060
8262
|
</kbd>
|
|
8061
8263
|
);
|
|
8062
8264
|
}
|
|
8063
|
-
` }] }, label: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "label", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", docs: "https://kznlabs.github.io/labsUI/components/forms", usage: '<Label htmlFor="email">Email</Label><Input id="email" type="email" aria-invalid={err} />', examples: [{ name: "sign-up.tsx", code: `import { Input } from "@/components/ui/input";
|
|
8265
|
+
` }] }, label: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "label", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", docs: "https://kznlabs.github.io/labsUI/components/forms", usage: '<Label htmlFor="email">Email</Label><Input id="email" type="email" aria-invalid={err} /><Textarea aria-label="Bio" maxLength={280} showCount /><Switch aria-label="Notifications" checked={on} onCheckedChange={setOn} />', examples: [{ name: "sign-up.tsx", code: `import { Input } from "@/components/ui/input";
|
|
8064
8266
|
import { Label } from "@/components/ui/label";
|
|
8065
8267
|
import { Switch } from "@/components/ui/switch";
|
|
8066
8268
|
import { Textarea } from "@/components/ui/textarea";
|
|
@@ -8092,7 +8294,7 @@ export function Label({ className, ...props }: ComponentProps<"label">) {
|
|
|
8092
8294
|
}
|
|
8093
8295
|
` }] }, list: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "list", type: "registry:lib", description: "The item a navigation key moves to in a list, from the item at `index` (-1: none active).\nUndefined when the key doesn't navigate.", api: { entries: [{ kind: "function", name: "listKeyTarget", signature: 'listKeyTarget(items: readonly T[], index: number, event: Pick<KeyboardEvent, "ctrlKey" | "key">, { orientation, loop, homeEnd }: ListKeyOptions) => T | undefined', description: "The item a navigation key moves to in a list, from the item at `index` (-1: none active).\nUndefined when the key doesn't navigate." }, { kind: "function", name: "scrollIntoList", signature: "scrollIntoList(box: HTMLElement, element: HTMLElement, pad?: number) => void", description: "Scrolls `box` just enough to show `el`, without moving the page the way scrollIntoView can." }], types: { ListKeyOptions: '{ orientation: "horizontal" | "vertical"; loop?: boolean; homeEnd?: boolean | "ctrl"; }' } }, dependencies: [], registryDependencies: ["https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "lib/list.ts", type: "registry:lib", content: `import { clamp } from "@/lib/utils";
|
|
8094
8296
|
|
|
8095
|
-
type ListKeyOptions = {
|
|
8297
|
+
export type ListKeyOptions = {
|
|
8096
8298
|
/** Which arrows move: Left/Right for "horizontal", Up/Down for "vertical". */
|
|
8097
8299
|
orientation: "horizontal" | "vertical";
|
|
8098
8300
|
/** Wrap past the last item to the first and back. Default true. */
|
|
@@ -8428,6 +8630,7 @@ export function Menubar({ className, children, ...props }: ComponentProps<"div">
|
|
|
8428
8630
|
className="-z-10 absolute inset-y-1 left-0 rounded-md bg-foreground/[0.08]"
|
|
8429
8631
|
/>
|
|
8430
8632
|
)}
|
|
8633
|
+
|
|
8431
8634
|
{children}
|
|
8432
8635
|
</div>
|
|
8433
8636
|
</BarContext>
|
|
@@ -8529,7 +8732,7 @@ export function MenubarTrigger({
|
|
|
8529
8732
|
/>
|
|
8530
8733
|
);
|
|
8531
8734
|
}
|
|
8532
|
-
` }] }, "message-bubble": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "message-bubble", type: "registry:ui", title: "Message Bubble", description: "User/assistant chat bubbles with avatar slot, streaming dots and copyable code blocks.", docs: "https://kznlabs.github.io/labsUI/components/chat", usage: '<MessageBubble from="assistant" avatar={<Avatar
|
|
8735
|
+
` }] }, "message-bubble": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "message-bubble", type: "registry:ui", title: "Message Bubble", description: "User/assistant chat bubbles with avatar slot, streaming dots and copyable code blocks.", docs: "https://kznlabs.github.io/labsUI/components/chat", usage: '<MessageBubble from="assistant" avatar={<Avatar src={bot} alt="Assistant" />} streaming>…<Code code={src} language="tsx" /></MessageBubble>', examples: [{ name: "message.tsx", code: `import { Avatar } from "@/components/ui/avatar";
|
|
8533
8736
|
import { Code } from "@/components/ui/code";
|
|
8534
8737
|
import { MessageBubble } from "@/components/ui/message-bubble";
|
|
8535
8738
|
export function Reply({ code }: { code: string }) {
|
|
@@ -8587,13 +8790,16 @@ export function MessageBubble({
|
|
|
8587
8790
|
className={cn("flex w-full gap-3", user && "flex-row-reverse", className)}
|
|
8588
8791
|
>
|
|
8589
8792
|
{avatar && <div className="shrink-0 pt-5">{avatar}</div>}
|
|
8793
|
+
|
|
8590
8794
|
<div className={cn("flex min-w-0 max-w-[85%] flex-col gap-1", user && "items-end")}>
|
|
8591
8795
|
{(name || time) && (
|
|
8592
8796
|
<header className="flex items-center gap-2 px-1 text-muted-foreground text-xs">
|
|
8593
8797
|
{name && <span className="font-medium text-foreground">{name}</span>}
|
|
8798
|
+
|
|
8594
8799
|
{time && <time>{time}</time>}
|
|
8595
8800
|
</header>
|
|
8596
8801
|
)}
|
|
8802
|
+
|
|
8597
8803
|
<div
|
|
8598
8804
|
className={cn(
|
|
8599
8805
|
"min-w-0 rounded-2xl px-4 py-2.5 text-sm leading-relaxed",
|
|
@@ -8604,6 +8810,7 @@ export function MessageBubble({
|
|
|
8604
8810
|
)}
|
|
8605
8811
|
>
|
|
8606
8812
|
{children}
|
|
8813
|
+
|
|
8607
8814
|
{streaming && <TypingDots className={children ? "mt-2" : "py-1.5"} />}
|
|
8608
8815
|
</div>
|
|
8609
8816
|
</div>
|
|
@@ -8648,14 +8855,14 @@ export function Storage() {
|
|
|
8648
8855
|
]}
|
|
8649
8856
|
/>
|
|
8650
8857
|
);
|
|
8651
|
-
}` }], api: { entries: [{ kind: "component", name: "Meter", props: [{ name: "high", type: "number", required: false, default: "max", description: "Above this the value is high." }, { name: "label", type: "ReactNode", required: false, description: "Visible name above the bar; also the accessible name." }, { name: "low", type: "number", required: false, default: "min", description: "<meter> thresholds: below `low` is low, above `high` is high." }, { name: "max", type: "number", required: false, default: "100", description: "Upper bound." }, { name: "min", type: "number", required: false, default: "0", description: "Lower bound." }, { name: "optimum", type: "number", required: false, default: "(min + max) / 2", description: "Where good is: below low, between, or above high. Decides the color, like <meter>." }, { name: "segments", type: "
|
|
8858
|
+
}` }], api: { entries: [{ kind: "component", name: "Meter", props: [{ name: "high", type: "number", required: false, default: "max", description: "Above this the value is high." }, { name: "label", type: "ReactNode", required: false, description: "Visible name above the bar; also the accessible name." }, { name: "low", type: "number", required: false, default: "min", description: "<meter> thresholds: below `low` is low, above `high` is high." }, { name: "max", type: "number", required: false, default: "100", description: "Upper bound." }, { name: "min", type: "number", required: false, default: "0", description: "Lower bound." }, { name: "optimum", type: "number", required: false, default: "(min + max) / 2", description: "Where good is: below low, between, or above high. Decides the color, like <meter>." }, { name: "segments", type: "MeterSegment[]", required: false, description: "Stacked parts with a legend (like a storage bar). Overrides the threshold color." }, { name: "value", type: "number", required: false, description: "Total used. With `segments`, defaults to their sum." }, { name: "valueText", type: "string", required: false, description: 'Text on the right and the accessible value, e.g. "48 GB of 100 GB".' }], description: `A measurement within a known range (disk, quota, server load, password strength), not a
|
|
8652
8859
|
task's progress (use Progress). Colors follow <meter>: green in the optimum region, orange one
|
|
8653
|
-
region away, red two away.` }], types: {
|
|
8860
|
+
region away, red two away.` }], types: { MeterSegment: "{ label: string; value: number; className?: string; }" } }, dependencies: [], registryDependencies: ["https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/meter.tsx", type: "registry:ui", content: `import type { ReactNode } from "react";
|
|
8654
8861
|
import { clamp, cn } from "@/lib/utils";
|
|
8655
8862
|
|
|
8656
8863
|
/* Server-safe (no "use client"). */
|
|
8657
8864
|
|
|
8658
|
-
type
|
|
8865
|
+
export type MeterSegment = {
|
|
8659
8866
|
/** Legend text for this part. */
|
|
8660
8867
|
label: string;
|
|
8661
8868
|
/** Amount this part uses. */
|
|
@@ -8678,7 +8885,7 @@ type MeterProps = {
|
|
|
8678
8885
|
/** Where good is: below low, between, or above high. Decides the color, like <meter>. */
|
|
8679
8886
|
optimum?: number;
|
|
8680
8887
|
/** Stacked parts with a legend (like a storage bar). Overrides the threshold color. */
|
|
8681
|
-
segments?:
|
|
8888
|
+
segments?: MeterSegment[];
|
|
8682
8889
|
/** Visible name above the bar; also the accessible name. */
|
|
8683
8890
|
label?: ReactNode;
|
|
8684
8891
|
/** Text on the right and the accessible value, e.g. "48 GB of 100 GB". */
|
|
@@ -8716,11 +8923,13 @@ export function Meter({
|
|
|
8716
8923
|
{(label || valueText) && (
|
|
8717
8924
|
<div className="flex items-baseline justify-between gap-3 text-sm">
|
|
8718
8925
|
{label && <span className="font-medium">{label}</span>}
|
|
8926
|
+
|
|
8719
8927
|
{valueText && (
|
|
8720
8928
|
<span className="ml-auto text-muted-foreground text-xs tabular-nums">{valueText}</span>
|
|
8721
8929
|
)}
|
|
8722
8930
|
</div>
|
|
8723
8931
|
)}
|
|
8932
|
+
|
|
8724
8933
|
{/* The real <meter> carries the semantics; the drawn bar below is decoration. */}
|
|
8725
8934
|
<meter
|
|
8726
8935
|
className="sr-only"
|
|
@@ -8734,6 +8943,7 @@ export function Meter({
|
|
|
8734
8943
|
>
|
|
8735
8944
|
{valueText ?? value}
|
|
8736
8945
|
</meter>
|
|
8946
|
+
|
|
8737
8947
|
<div
|
|
8738
8948
|
aria-hidden="true"
|
|
8739
8949
|
className="flex h-2 w-full gap-px overflow-hidden rounded-full bg-foreground/10"
|
|
@@ -8753,6 +8963,7 @@ export function Meter({
|
|
|
8753
8963
|
<span style={{ width: pct(value) }} className={cn("h-full rounded-full", tone)} />
|
|
8754
8964
|
)}
|
|
8755
8965
|
</div>
|
|
8966
|
+
|
|
8756
8967
|
{segments && (
|
|
8757
8968
|
<ul className="flex flex-wrap gap-x-4 gap-y-1 text-muted-foreground text-xs">
|
|
8758
8969
|
{segments.map((segment, i) => (
|
|
@@ -8764,6 +8975,7 @@ export function Meter({
|
|
|
8764
8975
|
segment.className ?? palette[i % palette.length],
|
|
8765
8976
|
)}
|
|
8766
8977
|
/>
|
|
8978
|
+
|
|
8767
8979
|
{segment.label}
|
|
8768
8980
|
</li>
|
|
8769
8981
|
))}
|
|
@@ -8830,6 +9042,7 @@ export function NativeSelect({ className, children, ...rest }: ComponentProps<"s
|
|
|
8830
9042
|
>
|
|
8831
9043
|
{children}
|
|
8832
9044
|
</select>
|
|
9045
|
+
|
|
8833
9046
|
<ChevronsUpDown
|
|
8834
9047
|
aria-hidden="true"
|
|
8835
9048
|
className="-translate-y-1/2 pointer-events-none absolute top-1/2 right-3 size-4 text-muted-foreground"
|
|
@@ -9189,8 +9402,10 @@ export function NavigationMenu({
|
|
|
9189
9402
|
className="-z-10 absolute inset-y-0 left-0 rounded-lg bg-foreground/[0.06]"
|
|
9190
9403
|
/>
|
|
9191
9404
|
)}
|
|
9405
|
+
|
|
9192
9406
|
{children}
|
|
9193
9407
|
</div>
|
|
9408
|
+
|
|
9194
9409
|
<motion.div
|
|
9195
9410
|
ref={setViewport}
|
|
9196
9411
|
data-floating=""
|
|
@@ -9308,6 +9523,7 @@ export function NavigationMenuTrigger({
|
|
|
9308
9523
|
{...props}
|
|
9309
9524
|
>
|
|
9310
9525
|
{children}
|
|
9526
|
+
|
|
9311
9527
|
<motion.span
|
|
9312
9528
|
aria-hidden="true"
|
|
9313
9529
|
animate={{ rotate: open ? 180 : 0 }}
|
|
@@ -9353,6 +9569,7 @@ export function NavigationMenuContent({
|
|
|
9353
9569
|
return (
|
|
9354
9570
|
<InPanel value>
|
|
9355
9571
|
{!open && <div hidden>{children}</div>}
|
|
9572
|
+
|
|
9356
9573
|
{nav.viewport &&
|
|
9357
9574
|
createPortal(
|
|
9358
9575
|
<AnimatePresence custom={nav.dir} initial={false}>
|
|
@@ -9528,8 +9745,10 @@ export function NavigationMenuLink({
|
|
|
9528
9745
|
{icon}
|
|
9529
9746
|
</span>
|
|
9530
9747
|
)}
|
|
9748
|
+
|
|
9531
9749
|
<span className="flex min-w-0 flex-col gap-0.5">
|
|
9532
9750
|
<span className="font-medium text-foreground">{title}</span>
|
|
9751
|
+
|
|
9533
9752
|
{description && (
|
|
9534
9753
|
<span className="line-clamp-2 text-muted-foreground text-xs leading-snug">
|
|
9535
9754
|
{description}
|
|
@@ -9642,13 +9861,16 @@ export function Newsletter({
|
|
|
9642
9861
|
{title}
|
|
9643
9862
|
</h3>
|
|
9644
9863
|
)}
|
|
9864
|
+
|
|
9645
9865
|
{description && <p className="text-muted-foreground text-sm">{description}</p>}
|
|
9646
9866
|
</div>
|
|
9647
9867
|
)}
|
|
9868
|
+
|
|
9648
9869
|
<InputGroup>
|
|
9649
9870
|
<InputGroupAddon>
|
|
9650
9871
|
<Mail aria-hidden="true" />
|
|
9651
9872
|
</InputGroupAddon>
|
|
9873
|
+
|
|
9652
9874
|
<Input
|
|
9653
9875
|
type="email"
|
|
9654
9876
|
name="email"
|
|
@@ -9657,6 +9879,7 @@ export function Newsletter({
|
|
|
9657
9879
|
placeholder={placeholder}
|
|
9658
9880
|
aria-label="Email address"
|
|
9659
9881
|
/>
|
|
9882
|
+
|
|
9660
9883
|
<InputGroupAddon align="end">
|
|
9661
9884
|
<Button type="submit" size="sm" status={status} successLabel="Subscribed">
|
|
9662
9885
|
{submitLabel}
|
|
@@ -9755,6 +9978,7 @@ export function NumberInput({
|
|
|
9755
9978
|
<Minus aria-hidden="true" />
|
|
9756
9979
|
</button>
|
|
9757
9980
|
</InputGroupAddon>
|
|
9981
|
+
|
|
9758
9982
|
<Input
|
|
9759
9983
|
type="number"
|
|
9760
9984
|
inputMode={Number.isInteger(step) ? "numeric" : "decimal"}
|
|
@@ -9778,6 +10002,7 @@ export function NumberInput({
|
|
|
9778
10002
|
className="px-0 text-center tabular-nums [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
|
9779
10003
|
{...props}
|
|
9780
10004
|
/>
|
|
10005
|
+
|
|
9781
10006
|
<InputGroupAddon align="end" className="pr-1">
|
|
9782
10007
|
<button
|
|
9783
10008
|
type="button"
|
|
@@ -9896,6 +10121,7 @@ function Control({
|
|
|
9896
10121
|
const content = (
|
|
9897
10122
|
<>
|
|
9898
10123
|
{active && <PaginationActivePill />}
|
|
10124
|
+
|
|
9899
10125
|
{children}
|
|
9900
10126
|
</>
|
|
9901
10127
|
);
|
|
@@ -9961,9 +10187,11 @@ export function Pagination({
|
|
|
9961
10187
|
className="gap-1 px-2.5"
|
|
9962
10188
|
>
|
|
9963
10189
|
<ChevronLeft aria-hidden="true" />
|
|
10190
|
+
|
|
9964
10191
|
<span className="hidden sm:block">{labels?.previous ?? "Previous"}</span>
|
|
9965
10192
|
</Control>
|
|
9966
10193
|
</li>
|
|
10194
|
+
|
|
9967
10195
|
{pageRange(current, total, siblings).map((page, i) => (
|
|
9968
10196
|
// biome-ignore lint/suspicious/noArrayIndexKey: slots stay mounted so numbers swipe (see pageRange)
|
|
9969
10197
|
<li key={i}>
|
|
@@ -9981,6 +10209,7 @@ export function Pagination({
|
|
|
9981
10209
|
)}
|
|
9982
10210
|
</li>
|
|
9983
10211
|
))}
|
|
10212
|
+
|
|
9984
10213
|
<li>
|
|
9985
10214
|
<Control
|
|
9986
10215
|
{...nav}
|
|
@@ -9990,6 +10219,7 @@ export function Pagination({
|
|
|
9990
10219
|
className="gap-1 px-2.5"
|
|
9991
10220
|
>
|
|
9992
10221
|
<span className="hidden sm:block">{labels?.next ?? "Next"}</span>
|
|
10222
|
+
|
|
9993
10223
|
<ChevronRight aria-hidden="true" />
|
|
9994
10224
|
</Control>
|
|
9995
10225
|
</li>
|
|
@@ -10137,7 +10367,7 @@ export function ClipFilters() {
|
|
|
10137
10367
|
</PopoverContent>
|
|
10138
10368
|
</Popover>
|
|
10139
10369
|
);
|
|
10140
|
-
}` }], api: { entries: [{ kind: "component", name: "Popover", props: [{ name: "children", type: "ReactNode", required: true, description: "`PopoverTrigger` and `PopoverContent`." }, { name: "defaultOpen", type: "boolean", required: false, default: "false", description: "Open at first render (uncontrolled)." }, { name: "onOpenChange", type: "(open: boolean) => void", required: false, description: "Called on the trigger, Esc or an outside click." }, { name: "open", type: "boolean", required: false, description: "Controlled state; pair with `onOpenChange`." }], description: "Non-modal panel anchored to a button: filters, pickers, quick edits. Controlled or uncontrolled." }, { kind: "component", name: "PopoverTrigger", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "
|
|
10370
|
+
}` }], api: { entries: [{ kind: "component", name: "Popover", props: [{ name: "children", type: "ReactNode", required: true, description: "`PopoverTrigger` and `PopoverContent`." }, { name: "defaultOpen", type: "boolean", required: false, default: "false", description: "Open at first render (uncontrolled)." }, { name: "onOpenChange", type: "(open: boolean) => void", required: false, description: "Called on the trigger, Esc or an outside click." }, { name: "open", type: "boolean", required: false, description: "Controlled state; pair with `onOpenChange`." }], description: "Non-modal panel anchored to a button: filters, pickers, quick edits. Controlled or uncontrolled." }, { kind: "component", name: "PopoverTrigger", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "Shows a spinner before the label and ignores clicks and Enter, so a form can't submit twice. Stays focusable." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`. Both ignore clicks and Enter, so a form can't submit twice." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }], extends: "Button" }, { kind: "component", name: "PopoverContent", props: [{ name: "children", type: "ReactNode", required: true, description: "Panel content." }, { name: "align", type: '"start" | "center" | "end"', required: false, default: '"center"', description: "Alignment with the trigger." }, { name: "side", type: '"top" | "bottom"', required: false, default: '"bottom"', description: "Preferred side; flips when there is no room." }], description: `Focus moves in on open. Esc or an outside click closes it; Esc returns focus to the trigger.
|
|
10141
10371
|
Not a focus trap: Tab past the end closes it and moves on, like any non-modal popup.` }], types: {} }, dependencies: ["motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/button.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/use-anchor.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/use-dismiss.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/popover.tsx", type: "registry:ui", content: `"use client";
|
|
10142
10372
|
|
|
10143
10373
|
import { AnimatePresence, motion } from "motion/react";
|
|
@@ -10292,6 +10522,19 @@ export function PopoverContent({
|
|
|
10292
10522
|
aria-labelledby={\`\${id}-trigger\`}
|
|
10293
10523
|
tabIndex={-1}
|
|
10294
10524
|
onKeyDown={(event) => {
|
|
10525
|
+
// The panel sits at the end of body: Tab past its last field goes on from the trigger.
|
|
10526
|
+
if (event.key === "Tab" && !event.shiftKey) {
|
|
10527
|
+
const last = [...(panel.current?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [])].at(
|
|
10528
|
+
-1,
|
|
10529
|
+
);
|
|
10530
|
+
|
|
10531
|
+
if (last && event.target !== last) return;
|
|
10532
|
+
setOpen(false);
|
|
10533
|
+
trigger.current?.focus();
|
|
10534
|
+
|
|
10535
|
+
return;
|
|
10536
|
+
}
|
|
10537
|
+
|
|
10295
10538
|
if (event.key !== "Escape" || event.defaultPrevented) return;
|
|
10296
10539
|
// Handled here so an enclosing Dialog stays open.
|
|
10297
10540
|
event.preventDefault();
|
|
@@ -10349,8 +10592,8 @@ export function PricingPage() {
|
|
|
10349
10592
|
]}
|
|
10350
10593
|
/>
|
|
10351
10594
|
);
|
|
10352
|
-
}` }], api: { entries: [{ kind: "component", name: "Pricing", props: [{ name: "plans", type: "
|
|
10353
|
-
rolling up when they drop and down when they rise.` }], types: {
|
|
10595
|
+
}` }], api: { entries: [{ kind: "component", name: "Pricing", props: [{ name: "plans", type: "PricingPlan[]", required: true, description: "Plans in display order." }, { name: "currency", type: "string", required: false, default: '"USD"', description: 'ISO 4217 code for the price, e.g. "EUR".' }, { name: "defaultPeriod", type: "PricingPeriod", required: false, default: '"monthly"', description: "Billing period selected at first render." }, { name: "locale", type: "string", required: false, default: '"en-US"', description: "BCP 47 locale for number formatting." }, { name: "yearlyNote", type: "string", required: false, description: 'Next to "Yearly", e.g. "−17%".' }], description: `Plan cards with a monthly / yearly switch (segmented ToggleGroup). Prices swap on a spring,
|
|
10596
|
+
rolling up when they drop and down when they rise.` }], types: { PricingPeriod: '"monthly" | "yearly"', PricingPlan: "{ name: string; description?: string; price: Record<PricingPeriod, number>; features: string[]; cta: ReactNode; featured?: boolean; badge?: string; }" } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/badge.json", "https://kznlabs.github.io/labsUI/r/card.json", "https://kznlabs.github.io/labsUI/r/toggle-group.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/blocks/pricing.tsx", type: "registry:block", content: `"use client";
|
|
10354
10597
|
|
|
10355
10598
|
import { Check } from "lucide-react";
|
|
10356
10599
|
import { AnimatePresence, motion } from "motion/react";
|
|
@@ -10367,15 +10610,15 @@ import {
|
|
|
10367
10610
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
|
10368
10611
|
import { cn, spring } from "@/lib/utils";
|
|
10369
10612
|
|
|
10370
|
-
type
|
|
10613
|
+
export type PricingPeriod = "monthly" | "yearly";
|
|
10371
10614
|
|
|
10372
|
-
type
|
|
10615
|
+
export type PricingPlan = {
|
|
10373
10616
|
/** Plan name, the card's heading. */
|
|
10374
10617
|
name: string;
|
|
10375
10618
|
/** One line under the name. */
|
|
10376
10619
|
description?: string;
|
|
10377
10620
|
/** Price per month on each billing period, e.g. \`{ monthly: 12, yearly: 10 }\`. */
|
|
10378
|
-
price: Record<
|
|
10621
|
+
price: Record<PricingPeriod, number>;
|
|
10379
10622
|
features: string[];
|
|
10380
10623
|
/** Call to action, e.g. \`<Button className="w-full">Start trial</Button>\`. */
|
|
10381
10624
|
cta: ReactNode;
|
|
@@ -10387,13 +10630,13 @@ type Plan = {
|
|
|
10387
10630
|
|
|
10388
10631
|
type PricingProps = {
|
|
10389
10632
|
/** Plans in display order. */
|
|
10390
|
-
plans:
|
|
10633
|
+
plans: PricingPlan[];
|
|
10391
10634
|
/** ISO 4217 code for the price, e.g. "EUR". */
|
|
10392
10635
|
currency?: string;
|
|
10393
10636
|
/** BCP 47 locale for number formatting. */
|
|
10394
10637
|
locale?: string;
|
|
10395
10638
|
/** Billing period selected at first render. */
|
|
10396
|
-
defaultPeriod?:
|
|
10639
|
+
defaultPeriod?: PricingPeriod;
|
|
10397
10640
|
/** Next to "Yearly", e.g. "−17%". */
|
|
10398
10641
|
yearlyNote?: string;
|
|
10399
10642
|
className?: string;
|
|
@@ -10411,7 +10654,7 @@ export function Pricing({
|
|
|
10411
10654
|
yearlyNote,
|
|
10412
10655
|
className,
|
|
10413
10656
|
}: PricingProps) {
|
|
10414
|
-
const [period, setPeriod] = useState<
|
|
10657
|
+
const [period, setPeriod] = useState<PricingPeriod>(defaultPeriod);
|
|
10415
10658
|
const money = new Intl.NumberFormat(locale, {
|
|
10416
10659
|
style: "currency",
|
|
10417
10660
|
currency,
|
|
@@ -10424,9 +10667,10 @@ export function Pricing({
|
|
|
10424
10667
|
<ToggleGroup
|
|
10425
10668
|
aria-label="Billing period"
|
|
10426
10669
|
value={period}
|
|
10427
|
-
onValueChange={(value) => value && setPeriod(value as
|
|
10670
|
+
onValueChange={(value) => value && setPeriod(value as PricingPeriod)}
|
|
10428
10671
|
>
|
|
10429
10672
|
<ToggleGroupItem value="monthly">Monthly</ToggleGroupItem>
|
|
10673
|
+
|
|
10430
10674
|
<ToggleGroupItem value="yearly">
|
|
10431
10675
|
Yearly
|
|
10432
10676
|
{yearlyNote && (
|
|
@@ -10436,6 +10680,7 @@ export function Pricing({
|
|
|
10436
10680
|
)}
|
|
10437
10681
|
</ToggleGroupItem>
|
|
10438
10682
|
</ToggleGroup>
|
|
10683
|
+
|
|
10439
10684
|
<div className="grid w-full gap-4 sm:grid-cols-[repeat(auto-fit,minmax(14rem,1fr))]">
|
|
10440
10685
|
{plans.map((plan) => (
|
|
10441
10686
|
<Card
|
|
@@ -10445,14 +10690,17 @@ export function Pricing({
|
|
|
10445
10690
|
<CardHeader>
|
|
10446
10691
|
<div className="flex min-h-6 items-center justify-between gap-2">
|
|
10447
10692
|
<CardTitle>{plan.name}</CardTitle>
|
|
10693
|
+
|
|
10448
10694
|
{plan.badge && (
|
|
10449
10695
|
<Badge variant={plan.featured ? "default" : "secondary"} shape="pill">
|
|
10450
10696
|
{plan.badge}
|
|
10451
10697
|
</Badge>
|
|
10452
10698
|
)}
|
|
10453
10699
|
</div>
|
|
10700
|
+
|
|
10454
10701
|
{plan.description && <CardDescription>{plan.description}</CardDescription>}
|
|
10455
10702
|
</CardHeader>
|
|
10703
|
+
|
|
10456
10704
|
<CardContent className="flex flex-col gap-5">
|
|
10457
10705
|
<p className="flex items-baseline gap-1">
|
|
10458
10706
|
<span className="relative inline-flex overflow-hidden font-semibold text-4xl tabular-nums tracking-tight">
|
|
@@ -10474,20 +10722,24 @@ export function Pricing({
|
|
|
10474
10722
|
</motion.span>
|
|
10475
10723
|
</AnimatePresence>
|
|
10476
10724
|
</span>
|
|
10725
|
+
|
|
10477
10726
|
<span className="text-muted-foreground text-sm">
|
|
10478
10727
|
/ month
|
|
10479
10728
|
<span className="sr-only">, billed {period}</span>
|
|
10480
10729
|
</span>
|
|
10481
10730
|
</p>
|
|
10731
|
+
|
|
10482
10732
|
<ul className="flex flex-col gap-2 text-sm">
|
|
10483
10733
|
{plan.features.map((feature) => (
|
|
10484
10734
|
<li key={feature} className="flex gap-2">
|
|
10485
10735
|
<Check className="mt-0.5 size-4 shrink-0 text-primary-ink" aria-hidden="true" />
|
|
10736
|
+
|
|
10486
10737
|
{feature}
|
|
10487
10738
|
</li>
|
|
10488
10739
|
))}
|
|
10489
10740
|
</ul>
|
|
10490
10741
|
</CardContent>
|
|
10742
|
+
|
|
10491
10743
|
<CardFooter className="mt-auto">{plan.cta}</CardFooter>
|
|
10492
10744
|
</Card>
|
|
10493
10745
|
))}
|
|
@@ -10495,7 +10747,7 @@ export function Pricing({
|
|
|
10495
10747
|
</div>
|
|
10496
10748
|
);
|
|
10497
10749
|
}
|
|
10498
|
-
` }] }, "product-card": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "product-card", type: "registry:block", title: "Product Card", description: "Shop tile: photos in an AspectRatio (a swipeable Carousel with dots when there are several), sale price, Badge and add-to-cart Button.", docs: "https://kznlabs.github.io/labsUI/components/product-card", usage: '<ProductCard name="Clip camera" price={279} compareAt={349} badge="−20%" media={[<img
|
|
10750
|
+
` }] }, "product-card": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "product-card", type: "registry:block", title: "Product Card", description: "Shop tile: photos in an AspectRatio (a swipeable Carousel with dots when there are several), sale price, Badge and add-to-cart Button.", docs: "https://kznlabs.github.io/labsUI/components/product-card", usage: '<ProductCard name="Clip camera" price={279} compareAt={349} badge="−20%" media={[<img src={front} alt="Front" />, <img src={back} alt="Back" />]} onAddToCart={add} />', examples: [{ name: "product-grid.tsx", code: `import { ProductCard } from "@/components/blocks/product-card";
|
|
10499
10751
|
|
|
10500
10752
|
type Product = {
|
|
10501
10753
|
id: string;
|
|
@@ -10601,17 +10853,20 @@ export function ProductCard({
|
|
|
10601
10853
|
<CarouselItem key={i}>{frame(slide)}</CarouselItem>
|
|
10602
10854
|
))}
|
|
10603
10855
|
</CarouselContent>
|
|
10856
|
+
|
|
10604
10857
|
<CarouselDots className="absolute inset-x-0 bottom-2 mt-0" />
|
|
10605
10858
|
</Carousel>
|
|
10606
10859
|
) : (
|
|
10607
10860
|
frame(media[0])
|
|
10608
10861
|
)}
|
|
10862
|
+
|
|
10609
10863
|
{badge && (
|
|
10610
10864
|
<Badge shape="pill" className="absolute top-2 left-2">
|
|
10611
10865
|
{badge}
|
|
10612
10866
|
</Badge>
|
|
10613
10867
|
)}
|
|
10614
10868
|
</div>
|
|
10869
|
+
|
|
10615
10870
|
<div className="flex flex-1 flex-col gap-3 p-3">
|
|
10616
10871
|
<div className="flex items-baseline justify-between gap-2">
|
|
10617
10872
|
<h3 className="min-w-0 truncate font-medium text-sm">
|
|
@@ -10623,16 +10878,20 @@ export function ProductCard({
|
|
|
10623
10878
|
name
|
|
10624
10879
|
)}
|
|
10625
10880
|
</h3>
|
|
10881
|
+
|
|
10626
10882
|
<p className="shrink-0 text-sm tabular-nums">
|
|
10627
10883
|
{compareAt !== undefined && compareAt > price && (
|
|
10628
10884
|
<>
|
|
10629
10885
|
<del className="mr-1.5 text-muted-foreground">{money.format(compareAt)}</del>
|
|
10886
|
+
|
|
10630
10887
|
<span className="sr-only">now </span>
|
|
10631
10888
|
</>
|
|
10632
10889
|
)}
|
|
10890
|
+
|
|
10633
10891
|
{money.format(price)}
|
|
10634
10892
|
</p>
|
|
10635
10893
|
</div>
|
|
10894
|
+
|
|
10636
10895
|
{onAddToCart && (
|
|
10637
10896
|
<Button size="sm" variant="outline" className="mt-auto w-full" onClick={onAddToCart}>
|
|
10638
10897
|
{addLabel}
|
|
@@ -10693,11 +10952,13 @@ export function Progress({
|
|
|
10693
10952
|
{(label || showValue) && (
|
|
10694
10953
|
<div className="flex items-center justify-between gap-3 text-xs">
|
|
10695
10954
|
{label && <span className="font-medium">{label}</span>}
|
|
10955
|
+
|
|
10696
10956
|
{showValue && determinate && (
|
|
10697
10957
|
<span className="ml-auto text-muted-foreground tabular-nums">{pct}%</span>
|
|
10698
10958
|
)}
|
|
10699
10959
|
</div>
|
|
10700
10960
|
)}
|
|
10961
|
+
|
|
10701
10962
|
<div
|
|
10702
10963
|
role="progressbar"
|
|
10703
10964
|
aria-label={label ?? ariaLabel}
|
|
@@ -10809,6 +11070,7 @@ export function Quote({
|
|
|
10809
11070
|
className={cn("flex flex-col", surface[variant], className)}
|
|
10810
11071
|
>
|
|
10811
11072
|
{large && <QuoteIcon aria-hidden="true" className="size-8 text-muted-foreground/60" />}
|
|
11073
|
+
|
|
10812
11074
|
<blockquote
|
|
10813
11075
|
lang={lang}
|
|
10814
11076
|
className={cn(
|
|
@@ -10849,11 +11111,14 @@ export function Quote({
|
|
|
10849
11111
|
)}
|
|
10850
11112
|
</div>
|
|
10851
11113
|
</blockquote>
|
|
11114
|
+
|
|
10852
11115
|
{(author || avatar) && (
|
|
10853
11116
|
<figcaption className="flex items-center gap-3">
|
|
10854
11117
|
{avatar}
|
|
11118
|
+
|
|
10855
11119
|
<span className="grid text-left">
|
|
10856
11120
|
{author && <span className="font-medium text-foreground text-sm">{author}</span>}
|
|
11121
|
+
|
|
10857
11122
|
{subtitle && <span className="text-muted-foreground text-xs">{subtitle}</span>}
|
|
10858
11123
|
</span>
|
|
10859
11124
|
</figcaption>
|
|
@@ -10870,7 +11135,7 @@ export function PlanPicker() {
|
|
|
10870
11135
|
<RadioGroupItem value="pro" description="10 servers, unlimited clips.">Pro</RadioGroupItem>
|
|
10871
11136
|
</RadioGroup>
|
|
10872
11137
|
);
|
|
10873
|
-
}` }], api: { entries: [{ kind: "component", name: "RadioGroup", props: [{ name: "aria-invalid", type: "boolean", required: false, description: "Indicates the entered value does not conform to the format expected by the application." }, { name: "defaultValue", type: "string", required: false, default: '""', description: "Value at first render (uncontrolled)." }, { name: "legend", type: "ReactNode", required: false, description: "Visible group label, rendered as the fieldset's <legend>." }, { name: "name", type: "string", required: false, description: "Form field name; generated when omitted." }, { name: "onValueChange", type: "(value: string) => void", required: false, description: "Called with the picked item's value." }, { name: "orientation", type: '"vertical" | "horizontal"', required: false, default: '"vertical"', description: "Stack items or lay them in a row." }, { name: "value", type: "string", required: false, description: "Controlled value; pair with `onValueChange`." }], extends: "<fieldset>", description: "Native radios in a fieldset: arrow keys, form submit and labels work as the browser does." }, { kind: "component", name: "RadioGroupItem", props: [{ name: "children", type: "ReactNode", required: true, description: "Label." }, { name: "value", type: "string", required: true, description: "Submitted value." }, { name: "description", type: "ReactNode", required: false, description: "Muted line under the label." }, { name: "disabled", type: "boolean", required: false, description: "Shown but not selectable." }], description: "One option. `children` is its label; `description` adds a muted line under it." }], types: {} }, dependencies: ["motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/radio-group.tsx", type: "registry:ui", content: `"use client";
|
|
11138
|
+
}` }], api: { entries: [{ kind: "component", name: "RadioGroup", props: [{ name: "aria-invalid", type: "boolean", required: false, description: "Indicates the entered value does not conform to the format expected by the application." }, { name: "defaultValue", type: "string", required: false, default: '""', description: "Value at first render (uncontrolled)." }, { name: "legend", type: "ReactNode", required: false, description: "Visible group label, rendered as the fieldset's <legend>." }, { name: "name", type: "string", required: false, description: "Form field name; generated when omitted." }, { name: "onValueChange", type: "(value: string) => void", required: false, description: "Called with the picked item's value." }, { name: "orientation", type: '"vertical" | "horizontal"', required: false, default: '"vertical"', description: "Stack items or lay them in a row." }, { name: "required", type: "boolean", required: false, description: "Submit needs a picked item." }, { name: "value", type: "string", required: false, description: "Controlled value; pair with `onValueChange`." }], extends: "<fieldset>", description: "Native radios in a fieldset: arrow keys, form submit and labels work as the browser does." }, { kind: "component", name: "RadioGroupItem", props: [{ name: "children", type: "ReactNode", required: true, description: "Label." }, { name: "value", type: "string", required: true, description: "Submitted value." }, { name: "description", type: "ReactNode", required: false, description: "Muted line under the label." }, { name: "disabled", type: "boolean", required: false, description: "Shown but not selectable." }], description: "One option. `children` is its label; `description` adds a muted line under it." }], types: {} }, dependencies: ["motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/radio-group.tsx", type: "registry:ui", content: `"use client";
|
|
10874
11139
|
|
|
10875
11140
|
import { motion } from "motion/react";
|
|
10876
11141
|
import { type ComponentProps, createContext, type ReactNode, use, useId } from "react";
|
|
@@ -10883,6 +11148,7 @@ type RadioCtx = {
|
|
|
10883
11148
|
value: string;
|
|
10884
11149
|
setValue: (value: string) => void;
|
|
10885
11150
|
disabled?: boolean;
|
|
11151
|
+
required?: boolean;
|
|
10886
11152
|
invalid?: boolean;
|
|
10887
11153
|
};
|
|
10888
11154
|
|
|
@@ -10901,6 +11167,8 @@ type RadioGroupProps = Omit<ComponentProps<"fieldset">, "defaultValue" | "onChan
|
|
|
10901
11167
|
name?: string;
|
|
10902
11168
|
/** Stack items or lay them in a row. */
|
|
10903
11169
|
orientation?: "vertical" | "horizontal";
|
|
11170
|
+
/** Submit needs a picked item. */
|
|
11171
|
+
required?: boolean;
|
|
10904
11172
|
"aria-invalid"?: boolean;
|
|
10905
11173
|
};
|
|
10906
11174
|
|
|
@@ -10916,12 +11184,8 @@ export function RadioGroup({
|
|
|
10916
11184
|
children,
|
|
10917
11185
|
...rest
|
|
10918
11186
|
}: RadioGroupProps) {
|
|
10919
|
-
// A fieldset takes no id from Field (labels can't target it)
|
|
10920
|
-
const {
|
|
10921
|
-
id: _id,
|
|
10922
|
-
required: _required,
|
|
10923
|
-
...props
|
|
10924
|
-
} = useFieldControl<typeof rest & { required?: boolean }>(rest);
|
|
11187
|
+
// A fieldset takes no id from Field (labels can't target it); required goes on the radios.
|
|
11188
|
+
const { id: _id, required, ...props } = useFieldControl(rest);
|
|
10925
11189
|
const auto = useId();
|
|
10926
11190
|
const [value, setValue] = useControllableState(valueProp, defaultValue, onValueChange);
|
|
10927
11191
|
|
|
@@ -10932,11 +11196,13 @@ export function RadioGroup({
|
|
|
10932
11196
|
value,
|
|
10933
11197
|
setValue,
|
|
10934
11198
|
disabled: props.disabled,
|
|
11199
|
+
required,
|
|
10935
11200
|
invalid: !!props["aria-invalid"],
|
|
10936
11201
|
}}
|
|
10937
11202
|
>
|
|
10938
11203
|
<fieldset className={cn("min-w-0", className)} {...props}>
|
|
10939
11204
|
{legend && <legend className="mb-3 font-medium text-sm leading-none">{legend}</legend>}
|
|
11205
|
+
|
|
10940
11206
|
<div
|
|
10941
11207
|
className={cn(
|
|
10942
11208
|
"flex gap-3",
|
|
@@ -10986,11 +11252,13 @@ export function RadioGroupItem({
|
|
|
10986
11252
|
value={value}
|
|
10987
11253
|
checked={checked}
|
|
10988
11254
|
disabled={off}
|
|
11255
|
+
required={ctx.required}
|
|
10989
11256
|
aria-invalid={ctx.invalid || undefined}
|
|
10990
11257
|
aria-describedby={description ? \`\${id}-description\` : undefined}
|
|
10991
11258
|
onChange={() => ctx.setValue(value)}
|
|
10992
11259
|
className="peer absolute inset-0 z-10 cursor-pointer appearance-none rounded-full outline-none disabled:cursor-not-allowed"
|
|
10993
11260
|
/>
|
|
11261
|
+
|
|
10994
11262
|
<span
|
|
10995
11263
|
aria-hidden="true"
|
|
10996
11264
|
className={cn(
|
|
@@ -11006,6 +11274,7 @@ export function RadioGroupItem({
|
|
|
11006
11274
|
/>
|
|
11007
11275
|
</span>
|
|
11008
11276
|
</span>
|
|
11277
|
+
|
|
11009
11278
|
<span className="grid gap-1">
|
|
11010
11279
|
<label
|
|
11011
11280
|
htmlFor={id}
|
|
@@ -11016,6 +11285,7 @@ export function RadioGroupItem({
|
|
|
11016
11285
|
>
|
|
11017
11286
|
{children}
|
|
11018
11287
|
</label>
|
|
11288
|
+
|
|
11019
11289
|
{description && (
|
|
11020
11290
|
<span id={\`\${id}-description\`} className="text-muted-foreground text-xs">
|
|
11021
11291
|
{description}
|
|
@@ -11035,11 +11305,12 @@ export function ProductRating({ average, count }: { average: number; count: numb
|
|
|
11035
11305
|
<span className="text-muted-foreground">· {count} reviews</span>
|
|
11036
11306
|
</div>
|
|
11037
11307
|
);
|
|
11038
|
-
}` }], api: { entries: [{ kind: "component", name: "Rating", props: [{ name: "
|
|
11308
|
+
}` }], api: { entries: [{ kind: "component", name: "Rating", props: [{ name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "defaultValue", type: "number", required: false, description: "Rating at first render (uncontrolled)." }, { name: "disabled", type: "boolean", required: false, description: "Stars can't be picked and the value isn't submitted." }, { name: "label", type: "string", required: false, description: "Group label (visually hidden)." }, { name: "max", type: "number", required: false, description: "Number of stars." }, { name: "name", type: "string", required: false, description: "Form field name for the chosen value." }, { name: "onValueChange", type: "(value: number) => void", required: false, description: "Called with the picked rating." }, { name: "readOnly", type: "boolean", required: false, description: "Display only: renders fractions (4.6) as a partially filled star, no inputs." }, { name: "required", type: "boolean", required: false, description: "Submit needs a rating." }, { name: "size", type: '"sm" | "default"', required: false, description: "Star size." }, { name: "value", type: "number", required: false, description: "Controlled rating; pair with `onValueChange`." }], description: "Star rating. Interactive: native radios (arrow keys, forms, screen readers) with a hover\npreview and a spring lift/press on the stars. `readOnly`: an image of the score, fractions included." }], types: {} }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/rating.tsx", type: "registry:ui", content: `"use client";
|
|
11039
11309
|
|
|
11040
11310
|
import { Star } from "lucide-react";
|
|
11041
11311
|
import { motion } from "motion/react";
|
|
11042
11312
|
import { useId, useState } from "react";
|
|
11313
|
+
import { useFieldControl } from "@/components/ui/field";
|
|
11043
11314
|
import { useControllableState } from "@/lib/use-controllable-state";
|
|
11044
11315
|
import { clamp, cn, spring } from "@/lib/utils";
|
|
11045
11316
|
|
|
@@ -11056,10 +11327,18 @@ type RatingProps = {
|
|
|
11056
11327
|
readOnly?: boolean;
|
|
11057
11328
|
/** Form field name for the chosen value. */
|
|
11058
11329
|
name?: string;
|
|
11330
|
+
/** Submit needs a rating. */
|
|
11331
|
+
required?: boolean;
|
|
11332
|
+
/** Stars can't be picked and the value isn't submitted. */
|
|
11333
|
+
disabled?: boolean;
|
|
11059
11334
|
/** Group label (visually hidden). */
|
|
11060
11335
|
label?: string;
|
|
11061
11336
|
/** Star size. */
|
|
11062
11337
|
size?: "sm" | "default";
|
|
11338
|
+
/** Ids of hint or error text; Field fills it in. */
|
|
11339
|
+
"aria-describedby"?: string;
|
|
11340
|
+
/** Error styling; Field sets it from \`error\`. */
|
|
11341
|
+
"aria-invalid"?: boolean;
|
|
11063
11342
|
className?: string;
|
|
11064
11343
|
};
|
|
11065
11344
|
|
|
@@ -11069,17 +11348,22 @@ const sizes = { sm: "size-4", default: "size-5" } as const;
|
|
|
11069
11348
|
* Star rating. Interactive: native radios (arrow keys, forms, screen readers) with a hover
|
|
11070
11349
|
* preview and a spring lift/press on the stars. \`readOnly\`: an image of the score, fractions included.
|
|
11071
11350
|
*/
|
|
11072
|
-
export function Rating({
|
|
11073
|
-
|
|
11074
|
-
|
|
11075
|
-
|
|
11076
|
-
|
|
11077
|
-
|
|
11078
|
-
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11351
|
+
export function Rating(allProps: RatingProps) {
|
|
11352
|
+
const {
|
|
11353
|
+
value: valueProp,
|
|
11354
|
+
defaultValue = 0,
|
|
11355
|
+
onValueChange,
|
|
11356
|
+
max = 5,
|
|
11357
|
+
readOnly = false,
|
|
11358
|
+
name,
|
|
11359
|
+
required,
|
|
11360
|
+
disabled,
|
|
11361
|
+
label = "Rating",
|
|
11362
|
+
size = "default",
|
|
11363
|
+
className,
|
|
11364
|
+
"aria-describedby": describedBy,
|
|
11365
|
+
"aria-invalid": invalid,
|
|
11366
|
+
} = useFieldControl(allProps);
|
|
11083
11367
|
const auto = useId();
|
|
11084
11368
|
const [value, setValue] = useControllableState(valueProp, defaultValue, onValueChange);
|
|
11085
11369
|
const [hover, setHover] = useState<number | null>(null);
|
|
@@ -11103,6 +11387,7 @@ export function Rating({
|
|
|
11103
11387
|
fill="currentColor"
|
|
11104
11388
|
strokeWidth={0}
|
|
11105
11389
|
/>
|
|
11390
|
+
|
|
11106
11391
|
<span
|
|
11107
11392
|
className="absolute inset-0 overflow-hidden"
|
|
11108
11393
|
style={{ width: \`\${fill * 100}%\` }}
|
|
@@ -11125,23 +11410,34 @@ export function Rating({
|
|
|
11125
11410
|
|
|
11126
11411
|
return (
|
|
11127
11412
|
<fieldset
|
|
11413
|
+
disabled={disabled}
|
|
11414
|
+
aria-describedby={describedBy}
|
|
11128
11415
|
onPointerLeave={() => setHover(null)}
|
|
11129
|
-
className={cn("inline-flex items-center gap-0.5", className)}
|
|
11416
|
+
className={cn("inline-flex items-center gap-0.5", disabled && "opacity-50", className)}
|
|
11130
11417
|
>
|
|
11131
11418
|
<legend className="sr-only">{label}</legend>
|
|
11419
|
+
|
|
11132
11420
|
{stars.map((star) => (
|
|
11133
|
-
<label
|
|
11421
|
+
<label
|
|
11422
|
+
key={star}
|
|
11423
|
+
className={cn("relative", disabled ? "cursor-not-allowed" : "cursor-pointer")}
|
|
11424
|
+
onPointerEnter={() => !disabled && setHover(star)}
|
|
11425
|
+
>
|
|
11134
11426
|
<input
|
|
11135
11427
|
type="radio"
|
|
11136
11428
|
name={name ?? auto}
|
|
11137
11429
|
value={star}
|
|
11138
11430
|
checked={value === star}
|
|
11431
|
+
required={required}
|
|
11432
|
+
aria-invalid={invalid || undefined}
|
|
11139
11433
|
onChange={() => setValue(star)}
|
|
11140
11434
|
className="peer sr-only"
|
|
11141
11435
|
/>
|
|
11436
|
+
|
|
11142
11437
|
<span className="sr-only">
|
|
11143
11438
|
{star} {star === 1 ? "star" : "stars"}
|
|
11144
11439
|
</span>
|
|
11440
|
+
|
|
11145
11441
|
<motion.span
|
|
11146
11442
|
aria-hidden="true"
|
|
11147
11443
|
// whileTap would make the star focusable, the radio underneath is the tab stop.
|
|
@@ -11293,11 +11589,11 @@ export function AdminLayout({ nav, log, children }: {
|
|
|
11293
11589
|
</ResizablePanel>
|
|
11294
11590
|
</ResizablePanelGroup>
|
|
11295
11591
|
);
|
|
11296
|
-
}` }], api: { entries: [{ kind: "component", name: "ResizablePanelGroup", props: [{ name: "direction", type: "
|
|
11592
|
+
}` }], api: { entries: [{ kind: "component", name: "ResizablePanelGroup", props: [{ name: "direction", type: "ResizableDirection", required: false, default: '"horizontal"', description: '"horizontal": panels side by side. "vertical": stacked.' }, { name: "keyboardStep", type: "number", required: false, default: "5", description: "% per arrow key press." }, { name: "onLayout", type: "(sizes: number[]) => void", required: false, description: "Final sizes after each drag, key or toggle, e.g. to save them in a cookie for SSR." }], extends: "<div>", description: `Split panes (admin layouts, docs, editors). Sizes are % of the group. Handles are APG window
|
|
11297
11593
|
splitters: arrows step, Home/End jump to the limits, Enter collapses a collapsible neighbor.
|
|
11298
11594
|
Drag follows the pointer; keys and toggles spring. Panels and handles find their order in the
|
|
11299
11595
|
DOM, so wrappers and Server Component parents work. Until hydration the layout is plain CSS
|
|
11300
|
-
(\`defaultSize\` as a flex basis, the others share the rest), so SSR already shows it.` }, { kind: "component", name: "ResizablePanel", props: [{ name: "collapsedSize", type: "number", required: false, default: "0", description: "Share in % when collapsed (default 0), e.g. an icon rail." }, { name: "collapsible", type: "boolean", required: false, default: "false", description: "Dragging past half the minimum snaps it shut; Enter or double-click on the handle toggles." }, { name: "defaultSize", type: "number", required: false, description: "Starting share of the group in %. Panels without one split what is left evenly." }, { name: "maxSize", type: "number", required: false, default: "100", description: "Largest size in % of the group." }, { name: "minSize", type: "number", required: false, default: "10", description: "Smallest share in % (default 10)." }], extends: "<div>" }, { kind: "component", name: "ResizableHandle", props: [{ name: "withHandle", type: "boolean", required: false, description: "Visible grip in the middle of the divider." }], extends: "<div>", description: "The divider between two panels. `withHandle` adds a visible grip." }], types: {
|
|
11596
|
+
(\`defaultSize\` as a flex basis, the others share the rest), so SSR already shows it.` }, { kind: "component", name: "ResizablePanel", props: [{ name: "collapsedSize", type: "number", required: false, default: "0", description: "Share in % when collapsed (default 0), e.g. an icon rail." }, { name: "collapsible", type: "boolean", required: false, default: "false", description: "Dragging past half the minimum snaps it shut; Enter or double-click on the handle toggles." }, { name: "defaultSize", type: "number", required: false, description: "Starting share of the group in %. Panels without one split what is left evenly." }, { name: "maxSize", type: "number", required: false, default: "100", description: "Largest size in % of the group." }, { name: "minSize", type: "number", required: false, default: "10", description: "Smallest share in % (default 10)." }], extends: "<div>" }, { kind: "component", name: "ResizableHandle", props: [{ name: "withHandle", type: "boolean", required: false, description: "Visible grip in the middle of the divider." }], extends: "<div>", description: "The divider between two panels. `withHandle` adds a visible grip." }], types: { ResizableDirection: '"horizontal" | "vertical"' } }, dependencies: ["motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/resizable.tsx", type: "registry:ui", content: `"use client";
|
|
11301
11597
|
|
|
11302
11598
|
import { type AnimationPlaybackControls, animate, useReducedMotion } from "motion/react";
|
|
11303
11599
|
import {
|
|
@@ -11313,7 +11609,7 @@ import {
|
|
|
11313
11609
|
} from "react";
|
|
11314
11610
|
import { cn, spring } from "@/lib/utils";
|
|
11315
11611
|
|
|
11316
|
-
type
|
|
11612
|
+
export type ResizableDirection = "horizontal" | "vertical";
|
|
11317
11613
|
type Limits = { min: number; max: number; collapsible: boolean; collapsed: number };
|
|
11318
11614
|
|
|
11319
11615
|
type ResizablePanelProps = ComponentProps<"div"> & {
|
|
@@ -11337,7 +11633,7 @@ type Layout = {
|
|
|
11337
11633
|
};
|
|
11338
11634
|
|
|
11339
11635
|
type GroupCtx = {
|
|
11340
|
-
direction:
|
|
11636
|
+
direction: ResizableDirection;
|
|
11341
11637
|
id: string;
|
|
11342
11638
|
layout: Layout | null;
|
|
11343
11639
|
/** Panels report their limits here from a layout effect; the group's own effect reads them. */
|
|
@@ -11439,7 +11735,7 @@ export function ResizablePanelGroup({
|
|
|
11439
11735
|
...props
|
|
11440
11736
|
}: ComponentProps<"div"> & {
|
|
11441
11737
|
/** "horizontal": panels side by side. "vertical": stacked. */
|
|
11442
|
-
direction?:
|
|
11738
|
+
direction?: ResizableDirection;
|
|
11443
11739
|
/** Final sizes after each drag, key or toggle, e.g. to save them in a cookie for SSR. */
|
|
11444
11740
|
onLayout?: (sizes: number[]) => void;
|
|
11445
11741
|
/** % per arrow key press. */
|
|
@@ -12060,7 +12356,9 @@ export function ScrollArea({
|
|
|
12060
12356
|
>
|
|
12061
12357
|
{children}
|
|
12062
12358
|
</div>
|
|
12359
|
+
|
|
12063
12360
|
{orientation !== "horizontal" && <Thumb axis="y" viewport={viewport} />}
|
|
12361
|
+
|
|
12064
12362
|
{orientation !== "vertical" && <Thumb axis="x" viewport={viewport} />}
|
|
12065
12363
|
</div>
|
|
12066
12364
|
);
|
|
@@ -12157,6 +12455,7 @@ export function SearchInput({
|
|
|
12157
12455
|
aria-hidden="true"
|
|
12158
12456
|
className="-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 size-4 text-muted-foreground"
|
|
12159
12457
|
/>
|
|
12458
|
+
|
|
12160
12459
|
<Input
|
|
12161
12460
|
ref={(element) => {
|
|
12162
12461
|
input.current = element;
|
|
@@ -12183,6 +12482,7 @@ export function SearchInput({
|
|
|
12183
12482
|
)}
|
|
12184
12483
|
{...props}
|
|
12185
12484
|
/>
|
|
12485
|
+
|
|
12186
12486
|
<span className="-translate-y-1/2 absolute top-1/2 right-2 flex">
|
|
12187
12487
|
<AnimatePresence mode="popLayout" initial={false}>
|
|
12188
12488
|
{value ? (
|
|
@@ -12289,6 +12589,7 @@ export function SegmentedTabs({ label, items, value, onChange, className }: Segm
|
|
|
12289
12589
|
transition={spring}
|
|
12290
12590
|
className="-z-10 absolute inset-y-1 left-0 rounded-md bg-raised shadow-sm ring-[0.5px] ring-raised-ring"
|
|
12291
12591
|
/>
|
|
12592
|
+
|
|
12292
12593
|
{items.map((item) => (
|
|
12293
12594
|
<button
|
|
12294
12595
|
key={item}
|
|
@@ -12307,11 +12608,12 @@ export function SegmentedTabs({ label, items, value, onChange, className }: Segm
|
|
|
12307
12608
|
{item}
|
|
12308
12609
|
</button>
|
|
12309
12610
|
))}
|
|
12611
|
+
|
|
12310
12612
|
<OverflowFocus />
|
|
12311
12613
|
</nav>
|
|
12312
12614
|
);
|
|
12313
12615
|
}
|
|
12314
|
-
` }] }, select: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "select", type: "registry:ui", title: "Select", description: "Single-choice listbox styled like Input, with groups, icons and disabled options. Native-select keyboard (arrows, typeahead even while closed), flips above the trigger near the viewport edge, submits
|
|
12616
|
+
` }] }, select: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "select", type: "registry:ui", title: "Select", description: "Single-choice listbox styled like Input, with groups, icons and disabled options. Native-select keyboard (arrows, typeahead even while closed), flips above the trigger near the viewport edge, submits and validates with the form.", docs: "https://kznlabs.github.io/labsUI/components/select", usage: '<Select id="role" name="role" options={[{ value: "admin", label: "Admin" }]} value={v} onValueChange={setV} placeholder="Pick a role" />', examples: [{ name: "role-picker.tsx", code: `"use client";
|
|
12315
12617
|
|
|
12316
12618
|
import { useState } from "react";
|
|
12317
12619
|
import { Field } from "@/components/ui/field";
|
|
@@ -12330,8 +12632,8 @@ export function RolePicker() {
|
|
|
12330
12632
|
<Select name="role" options={roles} value={role} onValueChange={setRole} />
|
|
12331
12633
|
</Field>
|
|
12332
12634
|
);
|
|
12333
|
-
}` }], api: { entries: [{ kind: "component", name: "Select", props: [{ name: "options", type: "SelectGroup[] | SelectOption[]", required: true, description: "Flat options, or labeled groups of options." }, { name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "aria-label", type: "string", required: false, description: "Accessible name when there is no visible label." }, { name: "defaultValue", type: "string", required: false, description: "Value at first render (uncontrolled)." }, { name: "disabled", type: "boolean", required: false, description: "Blocks opening." }, { name: "id", type: "string", required: false, description: "Pair with <Label htmlFor>, or let <Field> fill it in." }, { name: "name", type: "string", required: false, description: "
|
|
12334
|
-
typeahead (also while closed), Enter or Space to pick, Esc to cancel.` }], types: { SelectOption: "{ value: string; label: string; icon?: ReactNode; disabled?: boolean }", SelectGroup: "{ label: string; options: SelectOption[] }" } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/input.json", "https://kznlabs.github.io/labsUI/r/list.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/use-anchor.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/use-dismiss.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/select.tsx", type: "registry:ui", content: `"use client";
|
|
12635
|
+
}` }], api: { entries: [{ kind: "component", name: "Select", props: [{ name: "options", type: "SelectGroup[] | SelectOption[]", required: true, description: "Flat options, or labeled groups of options." }, { name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "aria-label", type: "string", required: false, description: "Accessible name when there is no visible label." }, { name: "defaultValue", type: "string", required: false, description: "Value at first render (uncontrolled)." }, { name: "disabled", type: "boolean", required: false, description: "Blocks opening; the value isn't submitted." }, { name: "id", type: "string", required: false, description: "Pair with <Label htmlFor>, or let <Field> fill it in." }, { name: "name", type: "string", required: false, description: "Form field name for the picked value." }, { name: "onValueChange", type: "(value: string) => void", required: false, description: "Called with the picked value." }, { name: "placeholder", type: "string", required: false, description: "Trigger text when nothing is picked." }, { name: "required", type: "boolean", required: false, description: "Form submit needs a value." }, { name: "value", type: "string", required: false, description: "Controlled value; pair with `onValueChange`." }], description: `Single-choice listbox styled like Input. Keyboard matches a native select: Up, Down, Home, End,
|
|
12636
|
+
typeahead (also while closed), Enter or Space to pick, Esc to cancel.` }], types: { SelectOption: "{ value: string; label: string; icon?: ReactNode; disabled?: boolean }", SelectGroup: "{ label: string; options: SelectOption[] }" } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/form-value.json", "https://kznlabs.github.io/labsUI/r/input.json", "https://kznlabs.github.io/labsUI/r/list.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/use-anchor.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/use-dismiss.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/select.tsx", type: "registry:ui", content: `"use client";
|
|
12335
12637
|
|
|
12336
12638
|
import { Check, ChevronsUpDown } from "lucide-react";
|
|
12337
12639
|
import { AnimatePresence, motion } from "motion/react";
|
|
@@ -12339,6 +12641,7 @@ import { type KeyboardEvent, type ReactNode, useEffect, useId, useRef, useState
|
|
|
12339
12641
|
import { createPortal } from "react-dom";
|
|
12340
12642
|
import { useFieldControl } from "@/components/ui/field";
|
|
12341
12643
|
import { fieldClass } from "@/components/ui/input";
|
|
12644
|
+
import { FormValue } from "@/lib/form-value";
|
|
12342
12645
|
import { listKeyTarget, scrollIntoList } from "@/lib/list";
|
|
12343
12646
|
import { useMounted } from "@/lib/platform";
|
|
12344
12647
|
import { useAnchor } from "@/lib/use-anchor";
|
|
@@ -12346,8 +12649,8 @@ import { useControllableState } from "@/lib/use-controllable-state";
|
|
|
12346
12649
|
import { useDismiss } from "@/lib/use-dismiss";
|
|
12347
12650
|
import { cn, material, spring } from "@/lib/utils";
|
|
12348
12651
|
|
|
12349
|
-
type SelectOption = { value: string; label: string; icon?: ReactNode; disabled?: boolean };
|
|
12350
|
-
type SelectGroup = { label: string; options: SelectOption[] };
|
|
12652
|
+
export type SelectOption = { value: string; label: string; icon?: ReactNode; disabled?: boolean };
|
|
12653
|
+
export type SelectGroup = { label: string; options: SelectOption[] };
|
|
12351
12654
|
|
|
12352
12655
|
type SelectProps = {
|
|
12353
12656
|
/** Flat options, or labeled groups of options. */
|
|
@@ -12360,11 +12663,11 @@ type SelectProps = {
|
|
|
12360
12663
|
onValueChange?: (value: string) => void;
|
|
12361
12664
|
/** Trigger text when nothing is picked. */
|
|
12362
12665
|
placeholder?: string;
|
|
12363
|
-
/**
|
|
12666
|
+
/** Form field name for the picked value. */
|
|
12364
12667
|
name?: string;
|
|
12365
12668
|
/** Pair with <Label htmlFor>, or let <Field> fill it in. */
|
|
12366
12669
|
id?: string;
|
|
12367
|
-
/** Blocks opening. */
|
|
12670
|
+
/** Blocks opening; the value isn't submitted. */
|
|
12368
12671
|
disabled?: boolean;
|
|
12369
12672
|
/** Form submit needs a value. */
|
|
12370
12673
|
required?: boolean;
|
|
@@ -12403,6 +12706,7 @@ export function Select(allProps: SelectProps) {
|
|
|
12403
12706
|
const list = useRef<HTMLDivElement>(null);
|
|
12404
12707
|
const [value, setValue] = useControllableState(valueProp, defaultValue, onValueChange);
|
|
12405
12708
|
const [open, setOpen] = useState(false);
|
|
12709
|
+
const [invalid, setInvalid] = useState(false);
|
|
12406
12710
|
const [active, setActive] = useState<string>();
|
|
12407
12711
|
const pos = useAnchor(open, trigger, list);
|
|
12408
12712
|
const mounted = useMounted();
|
|
@@ -12507,6 +12811,7 @@ export function Select(allProps: SelectProps) {
|
|
|
12507
12811
|
className,
|
|
12508
12812
|
)}
|
|
12509
12813
|
{...aria}
|
|
12814
|
+
aria-invalid={aria["aria-invalid"] || (invalid && !value) || undefined}
|
|
12510
12815
|
>
|
|
12511
12816
|
<span
|
|
12512
12817
|
className={cn(
|
|
@@ -12515,11 +12820,22 @@ export function Select(allProps: SelectProps) {
|
|
|
12515
12820
|
)}
|
|
12516
12821
|
>
|
|
12517
12822
|
{selected?.icon}
|
|
12823
|
+
|
|
12518
12824
|
{selected?.label ?? placeholder}
|
|
12519
12825
|
</span>
|
|
12826
|
+
|
|
12520
12827
|
<ChevronsUpDown className="text-muted-foreground" aria-hidden="true" />
|
|
12521
12828
|
</button>
|
|
12522
|
-
|
|
12829
|
+
|
|
12830
|
+
<FormValue
|
|
12831
|
+
name={name}
|
|
12832
|
+
value={value}
|
|
12833
|
+
required={required}
|
|
12834
|
+
disabled={disabled}
|
|
12835
|
+
anchor={trigger}
|
|
12836
|
+
onInvalid={() => setInvalid(true)}
|
|
12837
|
+
/>
|
|
12838
|
+
|
|
12523
12839
|
{mounted &&
|
|
12524
12840
|
createPortal(
|
|
12525
12841
|
<AnimatePresence>
|
|
@@ -12560,6 +12876,7 @@ export function Select(allProps: SelectProps) {
|
|
|
12560
12876
|
{groupIndex > 0 && (
|
|
12561
12877
|
<div aria-hidden="true" className="-mx-1 my-1 h-px bg-border" />
|
|
12562
12878
|
)}
|
|
12879
|
+
|
|
12563
12880
|
{group.label && (
|
|
12564
12881
|
<div
|
|
12565
12882
|
aria-hidden="true"
|
|
@@ -12568,6 +12885,7 @@ export function Select(allProps: SelectProps) {
|
|
|
12568
12885
|
{group.label}
|
|
12569
12886
|
</div>
|
|
12570
12887
|
)}
|
|
12888
|
+
|
|
12571
12889
|
{group.options.map((option) => (
|
|
12572
12890
|
// biome-ignore lint/a11y/useKeyWithClickEvents: keys handled on the listbox (aria-activedescendant)
|
|
12573
12891
|
<div
|
|
@@ -12591,7 +12909,9 @@ export function Select(allProps: SelectProps) {
|
|
|
12591
12909
|
)}
|
|
12592
12910
|
>
|
|
12593
12911
|
{option.icon}
|
|
12912
|
+
|
|
12594
12913
|
{option.label}
|
|
12914
|
+
|
|
12595
12915
|
{option.value === value && (
|
|
12596
12916
|
<Check className="!text-current absolute right-2" aria-hidden="true" />
|
|
12597
12917
|
)}
|
|
@@ -12778,7 +13098,7 @@ export function SidebarLayout({
|
|
|
12778
13098
|
);
|
|
12779
13099
|
}
|
|
12780
13100
|
|
|
12781
|
-
type SidebarItem = {
|
|
13101
|
+
export type SidebarItem = {
|
|
12782
13102
|
/** What \`current\` and \`onSelect\` refer to. Default: \`href\`, else \`label\`. */
|
|
12783
13103
|
id?: string;
|
|
12784
13104
|
/** Visible text; the rail shows it as a tooltip. */
|
|
@@ -12791,7 +13111,7 @@ type SidebarItem = {
|
|
|
12791
13111
|
badge?: ReactNode;
|
|
12792
13112
|
};
|
|
12793
13113
|
|
|
12794
|
-
type SidebarGroup = {
|
|
13114
|
+
export type SidebarGroup = {
|
|
12795
13115
|
/** Heading above the items; fades out in the rail. */
|
|
12796
13116
|
label?: string;
|
|
12797
13117
|
/** The group's entries. */
|
|
@@ -12855,6 +13175,7 @@ export function Sidebar({
|
|
|
12855
13175
|
const body = (
|
|
12856
13176
|
<nav aria-label={label} className="flex min-h-0 flex-1 flex-col">
|
|
12857
13177
|
{header && <div className="border-b p-2">{header}</div>}
|
|
13178
|
+
|
|
12858
13179
|
<div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto overflow-x-hidden p-2">
|
|
12859
13180
|
{groups.map((group, i) =>
|
|
12860
13181
|
group.collapsible && group.label ? (
|
|
@@ -12863,6 +13184,7 @@ export function Sidebar({
|
|
|
12863
13184
|
<CollapsibleTrigger className={cn("px-2.5 pb-1 text-muted-foreground text-xs", fade)}>
|
|
12864
13185
|
{group.label}
|
|
12865
13186
|
</CollapsibleTrigger>
|
|
13187
|
+
|
|
12866
13188
|
<CollapsibleContent>{list(group.items)}</CollapsibleContent>
|
|
12867
13189
|
</Collapsible>
|
|
12868
13190
|
) : (
|
|
@@ -12878,11 +13200,13 @@ export function Sidebar({
|
|
|
12878
13200
|
{group.label}
|
|
12879
13201
|
</p>
|
|
12880
13202
|
)}
|
|
13203
|
+
|
|
12881
13204
|
{list(group.items)}
|
|
12882
13205
|
</div>
|
|
12883
13206
|
),
|
|
12884
13207
|
)}
|
|
12885
13208
|
</div>
|
|
13209
|
+
|
|
12886
13210
|
{footer?.length ? <div className="border-t p-2">{list(footer)}</div> : null}
|
|
12887
13211
|
</nav>
|
|
12888
13212
|
);
|
|
@@ -12892,6 +13216,7 @@ export function Sidebar({
|
|
|
12892
13216
|
<Drawer open={openMobile} onOpenChange={setOpenMobile}>
|
|
12893
13217
|
<DrawerContent side="left" className={cn("bg-background pt-12 text-sm", className)}>
|
|
12894
13218
|
<DrawerTitle className="sr-only">{label}</DrawerTitle>
|
|
13219
|
+
|
|
12895
13220
|
{body}
|
|
12896
13221
|
</DrawerContent>
|
|
12897
13222
|
</Drawer>
|
|
@@ -12933,7 +13258,9 @@ function Entry({
|
|
|
12933
13258
|
const body = (
|
|
12934
13259
|
<>
|
|
12935
13260
|
{item.icon}
|
|
13261
|
+
|
|
12936
13262
|
<span className="min-w-0 flex-1 truncate">{item.label}</span>
|
|
13263
|
+
|
|
12937
13264
|
{item.badge != null && (
|
|
12938
13265
|
<span className="ml-auto text-muted-foreground text-xs tabular-nums">{item.badge}</span>
|
|
12939
13266
|
)}
|
|
@@ -13030,8 +13357,8 @@ import {
|
|
|
13030
13357
|
import { useDisclosure } from "@/lib/use-disclosure";
|
|
13031
13358
|
import { cn } from "@/lib/utils";
|
|
13032
13359
|
|
|
13033
|
-
type SiteLink = { label: string; href: string };
|
|
13034
|
-
type SiteGroup = {
|
|
13360
|
+
export type SiteLink = { label: string; href: string };
|
|
13361
|
+
export type SiteGroup = {
|
|
13035
13362
|
/** Trigger text for the panel. */
|
|
13036
13363
|
label: string;
|
|
13037
13364
|
/** Links in the panel, each with an optional description and icon. */
|
|
@@ -13096,6 +13423,7 @@ export function SiteHeader({
|
|
|
13096
13423
|
return (
|
|
13097
13424
|
<>
|
|
13098
13425
|
<span ref={sentinel} aria-hidden="true" className="block h-px" />
|
|
13426
|
+
|
|
13099
13427
|
<header
|
|
13100
13428
|
ref={header}
|
|
13101
13429
|
className={cn(
|
|
@@ -13112,6 +13440,7 @@ export function SiteHeader({
|
|
|
13112
13440
|
isGroup(link) ? (
|
|
13113
13441
|
<NavigationMenuItem key={link.label}>
|
|
13114
13442
|
<NavigationMenuTrigger>{link.label}</NavigationMenuTrigger>
|
|
13443
|
+
|
|
13115
13444
|
<NavigationMenuContent>
|
|
13116
13445
|
<ul
|
|
13117
13446
|
className={cn(
|
|
@@ -13153,6 +13482,7 @@ export function SiteHeader({
|
|
|
13153
13482
|
|
|
13154
13483
|
<div className="ml-auto flex items-center gap-2">
|
|
13155
13484
|
{actions}
|
|
13485
|
+
|
|
13156
13486
|
<Button
|
|
13157
13487
|
variant="ghost"
|
|
13158
13488
|
size="icon"
|
|
@@ -13172,6 +13502,7 @@ export function SiteHeader({
|
|
|
13172
13502
|
<DrawerHeader>
|
|
13173
13503
|
<DrawerTitle>Menu</DrawerTitle>
|
|
13174
13504
|
</DrawerHeader>
|
|
13505
|
+
|
|
13175
13506
|
<DrawerBody>
|
|
13176
13507
|
<nav aria-label={label} className="flex flex-col gap-5">
|
|
13177
13508
|
{nav.map((link) =>
|
|
@@ -13180,6 +13511,7 @@ export function SiteHeader({
|
|
|
13180
13511
|
<h2 className="px-2.5 font-medium text-muted-foreground text-xs">
|
|
13181
13512
|
{link.label}
|
|
13182
13513
|
</h2>
|
|
13514
|
+
|
|
13183
13515
|
<ul className="flex flex-col">
|
|
13184
13516
|
{link.items.map((i) => (
|
|
13185
13517
|
<li key={i.href}>
|
|
@@ -13241,6 +13573,7 @@ function MobileLink({
|
|
|
13241
13573
|
className="flex h-10 items-center gap-3 rounded-lg px-2.5 font-medium text-sm outline-none transition-colors hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-[current=page]:bg-foreground/[0.08] [&_svg]:size-4 [&_svg]:text-primary-ink"
|
|
13242
13574
|
>
|
|
13243
13575
|
{icon && <span aria-hidden="true">{icon}</span>}
|
|
13576
|
+
|
|
13244
13577
|
{children}
|
|
13245
13578
|
</a>
|
|
13246
13579
|
);
|
|
@@ -13289,6 +13622,7 @@ export function SkeletonGroup({
|
|
|
13289
13622
|
return (
|
|
13290
13623
|
<div role="status" aria-busy="true" className={className} {...props}>
|
|
13291
13624
|
<span className="sr-only">{label}</span>
|
|
13625
|
+
|
|
13292
13626
|
{children}
|
|
13293
13627
|
</div>
|
|
13294
13628
|
);
|
|
@@ -13306,7 +13640,7 @@ export function PriceFilter() {
|
|
|
13306
13640
|
<Slider value={price} onValueChange={setPrice} min={0} max={250} step={5} name="price" />
|
|
13307
13641
|
</Field>
|
|
13308
13642
|
);
|
|
13309
|
-
}` }], api: { entries: [{ kind: "component", name: "Slider", props: [{ name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "defaultValue", type: "number[]", required: false, description: "Values at first render (uncontrolled)." }, { name: "disabled", type: "boolean", required: false, description: "Blocks dragging and keys." }, { name: "id", type: "string", required: false, description: "Id of the first thumb; Field fills it in." }, { name: "labels", type: "string[]", required: false, description: 'Accessible names per thumb; defaults to "Minimum" / "Maximum" for a range.' }, { name: "max", type: "number", required: false, description: "Highest value." }, { name: "min", type: "number", required: false, description: "Lowest value." }, { name: "name", type: "string", required: false, description: "Submitted per thumb: `name` (one value) or `name` twice (range)." }, { name: "onValueChange", type: "(value: number[]) => void", required: false, description: "Called with every thumb's value while dragging." }, { name: "step", type: "number", required: false, description: "Increment for drag and arrow keys." }, { name: "value", type: "number[]", required: false, description: "One value, or two for a range (e.g. a price filter)." }], description: `Slider over native range inputs: keyboard, forms, touch and screen readers work as the
|
|
13643
|
+
}` }], api: { entries: [{ kind: "component", name: "Slider", props: [{ name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "defaultValue", type: "number[]", required: false, description: "Values at first render (uncontrolled)." }, { name: "disabled", type: "boolean", required: false, description: "Blocks dragging and keys; the values aren't submitted." }, { name: "id", type: "string", required: false, description: "Id of the first thumb; Field fills it in." }, { name: "labels", type: "string[]", required: false, description: 'Accessible names per thumb; defaults to "Minimum" / "Maximum" for a range.' }, { name: "max", type: "number", required: false, description: "Highest value." }, { name: "min", type: "number", required: false, description: "Lowest value." }, { name: "name", type: "string", required: false, description: "Submitted per thumb: `name` (one value) or `name` twice (range)." }, { name: "onValueChange", type: "(value: number[]) => void", required: false, description: "Called with every thumb's value while dragging." }, { name: "step", type: "number", required: false, description: "Increment for drag and arrow keys." }, { name: "value", type: "number[]", required: false, description: "One value, or two for a range (e.g. a price filter)." }], description: `Slider over native range inputs: keyboard, forms, touch and screen readers work as the
|
|
13310
13644
|
browser does. Thin accent track, white knob that lifts while held. Two values = range.` }], types: {} }, dependencies: ["motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/slider.tsx", type: "registry:ui", content: `"use client";
|
|
13311
13645
|
|
|
13312
13646
|
import { motion } from "motion/react";
|
|
@@ -13330,7 +13664,7 @@ type SliderProps = {
|
|
|
13330
13664
|
step?: number;
|
|
13331
13665
|
/** Submitted per thumb: \`name\` (one value) or \`name\` twice (range). */
|
|
13332
13666
|
name?: string;
|
|
13333
|
-
/** Blocks dragging and keys. */
|
|
13667
|
+
/** Blocks dragging and keys; the values aren't submitted. */
|
|
13334
13668
|
disabled?: boolean;
|
|
13335
13669
|
/** Accessible names per thumb; defaults to "Minimum" / "Maximum" for a range. */
|
|
13336
13670
|
labels?: string[];
|
|
@@ -13409,6 +13743,7 @@ export function Slider(allProps: SliderProps) {
|
|
|
13409
13743
|
}}
|
|
13410
13744
|
/>
|
|
13411
13745
|
</div>
|
|
13746
|
+
|
|
13412
13747
|
{values.map((value, i) => (
|
|
13413
13748
|
<motion.span
|
|
13414
13749
|
// biome-ignore lint/suspicious/noArrayIndexKey: thumbs are positional (min, max)
|
|
@@ -13423,6 +13758,7 @@ export function Slider(allProps: SliderProps) {
|
|
|
13423
13758
|
)}
|
|
13424
13759
|
/>
|
|
13425
13760
|
))}
|
|
13761
|
+
|
|
13426
13762
|
{values.map((thumbValue, i) => (
|
|
13427
13763
|
<input
|
|
13428
13764
|
// biome-ignore lint/suspicious/noArrayIndexKey: thumbs are positional (min, max)
|
|
@@ -13505,6 +13841,7 @@ export function Spinner({
|
|
|
13505
13841
|
{...props}
|
|
13506
13842
|
>
|
|
13507
13843
|
{label && <span className="sr-only">{label}</span>}
|
|
13844
|
+
|
|
13508
13845
|
{spokes.map((i) => (
|
|
13509
13846
|
<span
|
|
13510
13847
|
key={i}
|
|
@@ -13605,10 +13942,12 @@ export function StatTile({
|
|
|
13605
13942
|
<Card className={cn("gap-4 pb-4", className)}>
|
|
13606
13943
|
<CardHeader>
|
|
13607
13944
|
<CardDescription>{label}</CardDescription>
|
|
13945
|
+
|
|
13608
13946
|
<div className="flex flex-wrap items-center gap-2">
|
|
13609
13947
|
<CardTitle as="p" className="font-semibold text-3xl tabular-nums">
|
|
13610
13948
|
{value}
|
|
13611
13949
|
</CardTitle>
|
|
13950
|
+
|
|
13612
13951
|
{delta !== undefined && delta !== 0 && (
|
|
13613
13952
|
<Badge variant={good ? "success" : "destructive"} shape="pill">
|
|
13614
13953
|
<Icon aria-hidden="true" />
|
|
@@ -13617,12 +13956,14 @@ export function StatTile({
|
|
|
13617
13956
|
</Badge>
|
|
13618
13957
|
)}
|
|
13619
13958
|
</div>
|
|
13959
|
+
|
|
13620
13960
|
{period && (
|
|
13621
13961
|
<p aria-hidden="true" className="text-muted-foreground text-xs">
|
|
13622
13962
|
{period}
|
|
13623
13963
|
</p>
|
|
13624
13964
|
)}
|
|
13625
13965
|
</CardHeader>
|
|
13966
|
+
|
|
13626
13967
|
{data && data.length > 1 && (
|
|
13627
13968
|
<Chart
|
|
13628
13969
|
type="area"
|
|
@@ -13650,14 +13991,14 @@ export function LandingStats({ users, uptime }: { users: number; uptime: number
|
|
|
13650
13991
|
]}
|
|
13651
13992
|
/>
|
|
13652
13993
|
);
|
|
13653
|
-
}` }], api: { entries: [{ kind: "component", name: "StatsStrip", props: [{ name: "stats", type: "
|
|
13994
|
+
}` }], api: { entries: [{ kind: "component", name: "StatsStrip", props: [{ name: "stats", type: "StatsStripItem[]", required: true, description: "Numbers in display order." }], description: "A row of headline numbers: each card rises in turn, then its number springs up. Each card is a <dl>." }], types: { StatsStripItem: "{ label: string; value: number; prefix?: string; suffix?: string; decimals?: number; hint?: string; }" } }, dependencies: [], registryDependencies: ["https://kznlabs.github.io/labsUI/r/card.json", "https://kznlabs.github.io/labsUI/r/count-to.json", "https://kznlabs.github.io/labsUI/r/reveal.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/blocks/stats-strip.tsx", type: "registry:block", content: `import { Card } from "@/components/ui/card";
|
|
13654
13995
|
import { CountTo } from "@/components/ui/count-to";
|
|
13655
13996
|
import { Reveal } from "@/components/ui/reveal";
|
|
13656
13997
|
import { cn } from "@/lib/utils";
|
|
13657
13998
|
|
|
13658
13999
|
// Server-safe: CountTo and Reveal are the client parts.
|
|
13659
14000
|
|
|
13660
|
-
type
|
|
14001
|
+
export type StatsStripItem = {
|
|
13661
14002
|
/** What the number counts, e.g. "Active users". */
|
|
13662
14003
|
label: string;
|
|
13663
14004
|
/** Number to count up to. */
|
|
@@ -13678,7 +14019,7 @@ export function StatsStrip({
|
|
|
13678
14019
|
className,
|
|
13679
14020
|
}: {
|
|
13680
14021
|
/** Numbers in display order. */
|
|
13681
|
-
stats:
|
|
14022
|
+
stats: StatsStripItem[];
|
|
13682
14023
|
className?: string;
|
|
13683
14024
|
}) {
|
|
13684
14025
|
return (
|
|
@@ -13693,6 +14034,7 @@ export function StatsStrip({
|
|
|
13693
14034
|
<Card className="h-full px-5 py-4">
|
|
13694
14035
|
<dl className="flex flex-col gap-1">
|
|
13695
14036
|
<dt className="text-muted-foreground text-sm">{stat.label}</dt>
|
|
14037
|
+
|
|
13696
14038
|
<dd className="font-semibold text-3xl tabular-nums tracking-tight">
|
|
13697
14039
|
<CountTo
|
|
13698
14040
|
to={stat.value}
|
|
@@ -13701,6 +14043,7 @@ export function StatsStrip({
|
|
|
13701
14043
|
decimals={stat.decimals}
|
|
13702
14044
|
/>
|
|
13703
14045
|
</dd>
|
|
14046
|
+
|
|
13704
14047
|
{stat.hint && <dd className="text-muted-foreground text-xs">{stat.hint}</dd>}
|
|
13705
14048
|
</dl>
|
|
13706
14049
|
</Card>
|
|
@@ -13712,7 +14055,7 @@ export function StatsStrip({
|
|
|
13712
14055
|
` }] }, stepper: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "stepper", type: "registry:ui", title: "Stepper", description: "Multi-step form (questionnaire, checkout, setup wizard). Steps stay mounted so values survive Back and submit together; Continue checks only the current step's native constraints, plus an optional async onNext guard with a spinner. Progress header with springing connectors; works with onSubmit or a Server Action.", docs: "https://kznlabs.github.io/labsUI/components/stepper", usage: '<Stepper steps={[{ value: "server", title: "Server" }, { value: "bot", title: "Bot" }]} action={createBot}><StepperList /><StepperStep value="server"><Input name="serverId" required /></StepperStep><StepperStep value="bot">…</StepperStep><StepperFooter submitLabel="Create bot" /></Stepper>', examples: [{ name: "setup-wizard.tsx", code: `import { Field } from "@/components/ui/field";
|
|
13713
14056
|
import { Input } from "@/components/ui/input";
|
|
13714
14057
|
import { Stepper, StepperFooter, StepperList, StepperStep } from "@/components/ui/stepper";
|
|
13715
|
-
import { createBot } from "./actions";
|
|
14058
|
+
import { createBot } from "./actions";
|
|
13716
14059
|
|
|
13717
14060
|
const steps = [
|
|
13718
14061
|
{ value: "server", title: "Server", description: "Where the bot will live." },
|
|
@@ -13737,7 +14080,14 @@ export default function SetupPage() {
|
|
|
13737
14080
|
<StepperFooter submitLabel="Create bot" />
|
|
13738
14081
|
</Stepper>
|
|
13739
14082
|
);
|
|
13740
|
-
}` }
|
|
14083
|
+
}` }, { name: "actions.ts", code: `"use server";
|
|
14084
|
+
|
|
14085
|
+
export async function createBot(formData: FormData) {
|
|
14086
|
+
const serverId = String(formData.get("serverId"));
|
|
14087
|
+
const botName = String(formData.get("botName"));
|
|
14088
|
+
|
|
14089
|
+
console.log({ serverId, botName });
|
|
14090
|
+
}` }], api: { entries: [{ kind: "component", name: "Stepper", props: [{ name: "steps", type: "StepInfo[]", required: true, description: "Steps in order: `value`, `title` and an optional `description` for the header." }, { name: "defaultValue", type: "string", required: false, description: "Step shown at first render (uncontrolled)." }, { name: "onValueChange", type: "(value: string) => void", required: false, description: "Called with the new step's `value`." }, { name: "value", type: "string", required: false, description: "Current step's `value`. Controlled with `onValueChange`, or start at `defaultValue`." }], extends: "<form>", description: "Multi-step form. Every step stays mounted (hidden), so values survive Back and submit\ntogether; Continue checks only the current step's native constraints, and the last step\nsubmits the form (`onSubmit`, or a Server Action through `action`)." }, { kind: "component", name: "StepperList", props: [], extends: "<ol>", description: "Progress header. Finished steps are links back; the connector fills on a spring." }, { kind: "component", name: "StepperStep", props: [{ name: "children", type: "ReactNode", required: true, description: "The step's fields." }, { name: "value", type: "string", required: true, description: "Matches a step in `steps`." }, { name: "onNext", type: "StepGuard", required: false, description: "Runs on Continue after the native checks; return false to stay." }], description: "One step's fields. Hidden, not unmounted, while inactive. `onNext` runs after its native\nconstraints pass; return false (or resolve false) to stay, e.g. after a server check." }, { kind: "component", name: "StepperFooter", props: [{ name: "backLabel", type: "ReactNode", required: false, default: '"Back"', description: "Back button text." }, { name: "nextLabel", type: "ReactNode", required: false, default: '"Continue"', description: "Continue button text." }, { name: "submitLabel", type: "ReactNode", required: false, default: '"Submit"', description: "Submit button text on the last step." }], description: "Back and Continue; the last step's button submits. Continue shows a spinner while `onNext` runs." }], types: { StepInfo: "{ value: string; title: string; description?: string }", StepGuard: "() => boolean | undefined | Promise<boolean | undefined>" } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/button.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/stepper.tsx", type: "registry:ui", content: `"use client";
|
|
13741
14091
|
|
|
13742
14092
|
import { Check } from "lucide-react";
|
|
13743
14093
|
import { motion } from "motion/react";
|
|
@@ -13754,8 +14104,8 @@ import { Button } from "@/components/ui/button";
|
|
|
13754
14104
|
import { useControllableState } from "@/lib/use-controllable-state";
|
|
13755
14105
|
import { cn, spring } from "@/lib/utils";
|
|
13756
14106
|
|
|
13757
|
-
type StepInfo = { value: string; title: string; description?: string };
|
|
13758
|
-
type
|
|
14107
|
+
export type StepInfo = { value: string; title: string; description?: string };
|
|
14108
|
+
export type StepGuard = () => boolean | undefined | Promise<boolean | undefined>;
|
|
13759
14109
|
|
|
13760
14110
|
type StepperCtx = {
|
|
13761
14111
|
steps: StepInfo[];
|
|
@@ -13764,7 +14114,7 @@ type StepperCtx = {
|
|
|
13764
14114
|
next: () => Promise<void>;
|
|
13765
14115
|
back: () => void;
|
|
13766
14116
|
fieldsets: Map<string, HTMLFieldSetElement>;
|
|
13767
|
-
guards: Map<string,
|
|
14117
|
+
guards: Map<string, StepGuard>;
|
|
13768
14118
|
};
|
|
13769
14119
|
|
|
13770
14120
|
const StepperContext = createContext<StepperCtx | null>(null);
|
|
@@ -13827,7 +14177,7 @@ export function Stepper({
|
|
|
13827
14177
|
steps.findIndex((step) => step.value === value),
|
|
13828
14178
|
);
|
|
13829
14179
|
const fieldsets = useRef(new Map<string, HTMLFieldSetElement>()).current;
|
|
13830
|
-
const guards = useRef(new Map<string,
|
|
14180
|
+
const guards = useRef(new Map<string, StepGuard>()).current;
|
|
13831
14181
|
const moved = useRef(false);
|
|
13832
14182
|
|
|
13833
14183
|
const go = (i: number) => {
|
|
@@ -13940,15 +14290,19 @@ export function StepperList({ className, ...props }: ComponentProps<"ol">) {
|
|
|
13940
14290
|
className="flex cursor-pointer items-center gap-2 rounded-full pr-1 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
|
13941
14291
|
>
|
|
13942
14292
|
{mark}
|
|
14293
|
+
|
|
13943
14294
|
{label}
|
|
14295
|
+
|
|
13944
14296
|
<span className="sr-only"> (completed, go back)</span>
|
|
13945
14297
|
</button>
|
|
13946
14298
|
) : (
|
|
13947
14299
|
<span className="flex items-center gap-2 pr-1">
|
|
13948
14300
|
{mark}
|
|
14301
|
+
|
|
13949
14302
|
{label}
|
|
13950
14303
|
</span>
|
|
13951
14304
|
)}
|
|
14305
|
+
|
|
13952
14306
|
{i < steps.length - 1 && (
|
|
13953
14307
|
<span aria-hidden="true" className="relative mx-2 h-px flex-1 bg-border">
|
|
13954
14308
|
<motion.span
|
|
@@ -13963,6 +14317,7 @@ export function StepperList({ className, ...props }: ComponentProps<"ol">) {
|
|
|
13963
14317
|
);
|
|
13964
14318
|
})}
|
|
13965
14319
|
</ol>
|
|
14320
|
+
|
|
13966
14321
|
{/* Small screens hide the titles; this keeps the current one (and announces moves). */}
|
|
13967
14322
|
<p aria-live="polite" className="text-muted-foreground text-xs sm:sr-only">
|
|
13968
14323
|
Step {index + 1} of {steps.length}
|
|
@@ -13985,7 +14340,7 @@ export function StepperStep({
|
|
|
13985
14340
|
/** Matches a step in \`steps\`. */
|
|
13986
14341
|
value: string;
|
|
13987
14342
|
/** Runs on Continue after the native checks; return false to stay. */
|
|
13988
|
-
onNext?:
|
|
14343
|
+
onNext?: StepGuard;
|
|
13989
14344
|
className?: string;
|
|
13990
14345
|
/** The step's fields. */
|
|
13991
14346
|
children: ReactNode;
|
|
@@ -14021,11 +14376,13 @@ export function StepperStep({
|
|
|
14021
14376
|
{step && (
|
|
14022
14377
|
<legend className="mb-4 grid gap-1">
|
|
14023
14378
|
<span className="font-semibold text-lg leading-none">{step.title}</span>
|
|
14379
|
+
|
|
14024
14380
|
{step.description && (
|
|
14025
14381
|
<span className="text-muted-foreground text-sm">{step.description}</span>
|
|
14026
14382
|
)}
|
|
14027
14383
|
</legend>
|
|
14028
14384
|
)}
|
|
14385
|
+
|
|
14029
14386
|
{children}
|
|
14030
14387
|
</motion.fieldset>
|
|
14031
14388
|
);
|
|
@@ -14054,6 +14411,7 @@ export function StepperFooter({
|
|
|
14054
14411
|
<Button variant="ghost" onClick={back} className={cn(index === 0 && "invisible")}>
|
|
14055
14412
|
{backLabel}
|
|
14056
14413
|
</Button>
|
|
14414
|
+
|
|
14057
14415
|
{last ? (
|
|
14058
14416
|
<Button type="submit">{submitLabel}</Button>
|
|
14059
14417
|
) : (
|
|
@@ -14486,7 +14844,7 @@ export function StepperFooter({
|
|
|
14486
14844
|
display: inline !important;
|
|
14487
14845
|
}
|
|
14488
14846
|
}
|
|
14489
|
-
` }] }, switch: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "switch", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", docs: "https://kznlabs.github.io/labsUI/components/forms", usage: '<Label htmlFor="email">Email</Label><Input id="email" type="email" aria-invalid={err} />', examples: [{ name: "sign-up.tsx", code: `import { Input } from "@/components/ui/input";
|
|
14847
|
+
` }] }, switch: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "switch", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", docs: "https://kznlabs.github.io/labsUI/components/forms", usage: '<Label htmlFor="email">Email</Label><Input id="email" type="email" aria-invalid={err} /><Textarea aria-label="Bio" maxLength={280} showCount /><Switch aria-label="Notifications" checked={on} onCheckedChange={setOn} />', examples: [{ name: "sign-up.tsx", code: `import { Input } from "@/components/ui/input";
|
|
14490
14848
|
import { Label } from "@/components/ui/label";
|
|
14491
14849
|
import { Switch } from "@/components/ui/switch";
|
|
14492
14850
|
import { Textarea } from "@/components/ui/textarea";
|
|
@@ -14547,6 +14905,7 @@ export function Switch({
|
|
|
14547
14905
|
className="peer absolute inset-x-0 -inset-y-1 z-10 cursor-pointer appearance-none rounded-full outline-none disabled:cursor-not-allowed"
|
|
14548
14906
|
{...props}
|
|
14549
14907
|
/>
|
|
14908
|
+
|
|
14550
14909
|
<span
|
|
14551
14910
|
aria-hidden="true"
|
|
14552
14911
|
className={cn(
|
|
@@ -14639,6 +14998,7 @@ export function Table({ striped = false, className, containerClassName, ...props
|
|
|
14639
14998
|
)}
|
|
14640
14999
|
{...props}
|
|
14641
15000
|
/>
|
|
15001
|
+
|
|
14642
15002
|
<OverflowFocus />
|
|
14643
15003
|
</div>
|
|
14644
15004
|
);
|
|
@@ -14701,6 +15061,7 @@ export function TableHead({ sort, onSort, className, children, ...props }: Table
|
|
|
14701
15061
|
className="group -mx-1.5 inline-flex h-7 items-center gap-1 rounded-md px-1.5 outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 in-[.text-right]:flex-row-reverse"
|
|
14702
15062
|
>
|
|
14703
15063
|
{children}
|
|
15064
|
+
|
|
14704
15065
|
<ArrowUp
|
|
14705
15066
|
aria-hidden="true"
|
|
14706
15067
|
className={cn(
|
|
@@ -14867,6 +15228,7 @@ export function TabsList({ className, children, ...props }: ComponentProps<"div"
|
|
|
14867
15228
|
)}
|
|
14868
15229
|
/>
|
|
14869
15230
|
)}
|
|
15231
|
+
|
|
14870
15232
|
{children}
|
|
14871
15233
|
</div>
|
|
14872
15234
|
);
|
|
@@ -14944,7 +15306,7 @@ export function TabsContent({
|
|
|
14944
15306
|
</motion.div>
|
|
14945
15307
|
);
|
|
14946
15308
|
}
|
|
14947
|
-
` }] }, "tag-input": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "tag-input", type: "registry:ui", title: "Tag Input", description: "Free-form tags: Enter or a separator adds, Backspace removes the last, pasting a list adds them all. Duplicates, a max and a validate function keep bad tags in the input.
|
|
15309
|
+
` }] }, "tag-input": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "tag-input", type: "registry:ui", title: "Tag Input", description: "Free-form tags: Enter or a separator adds, Backspace removes the last, pasting a list adds them all. Duplicates, a max and a validate function keep bad tags in the input. Submits one entry per tag; required needs one.", docs: "https://kznlabs.github.io/labsUI/components/tag-input", usage: "<TagInput value={tags} onValueChange={setTags} max={5} validate={(t) => t.length <= 24} />", examples: [{ name: "clip-tags.tsx", code: `"use client";
|
|
14948
15310
|
|
|
14949
15311
|
import { useState } from "react";
|
|
14950
15312
|
import { Field } from "@/components/ui/field";
|
|
@@ -14964,12 +15326,13 @@ export function ClipTags() {
|
|
|
14964
15326
|
/>
|
|
14965
15327
|
</Field>
|
|
14966
15328
|
);
|
|
14967
|
-
}` }], api: { entries: [{ kind: "component", name: "Tag", props: [{ name: "children", type: "ReactNode", required: true, description: "Tag text." }, { name: "disabled", type: "boolean", required: false, description: "Hides the remove button." }, { name: "onRemove", type: "() => void", required: false, description: 'Shows a remove button; its accessible name is "Remove <children>".' }], description: 'Removable chip. Springs in and out when rendered inside AnimatePresence (`mode="popLayout"`).' }, { kind: "component", name: "TagInput", props: [{ name: "defaultValue", type: "string[]", required: false, default: "[]", description: "Tags at first render (uncontrolled)." }, { name: "max", type: "number", required: false, description: "Most tags allowed; typing stops at the limit." }, { name: "onValueChange", type: "(tags: string[]) => void", required: false, description: "Called with every tag after an add or remove." }, { name: "separators", type: "string[]", required: false, default: '[","]', description: "Keys that commit the typed text, besides Enter. Pasted text is split on these too." }, { name: "validate", type: "(tag: string) => boolean", required: false, description: "Return false to reject a tag (it stays in the input). Runs after trimming." }, { name: "value", type: "string[]", required: false, description: "Controlled tags; pair with `onValueChange`." }], extends: "<input>", description: "Free-form tags: type and press Enter or a separator; paste a list to add many; Backspace in an\nempty input removes the last tag. `name` submits one
|
|
15329
|
+
}` }], api: { entries: [{ kind: "component", name: "Tag", props: [{ name: "children", type: "ReactNode", required: true, description: "Tag text." }, { name: "disabled", type: "boolean", required: false, description: "Hides the remove button." }, { name: "onRemove", type: "() => void", required: false, description: 'Shows a remove button; its accessible name is "Remove <children>".' }], description: 'Removable chip. Springs in and out when rendered inside AnimatePresence (`mode="popLayout"`).' }, { kind: "component", name: "TagInput", props: [{ name: "defaultValue", type: "string[]", required: false, default: "[]", description: "Tags at first render (uncontrolled)." }, { name: "max", type: "number", required: false, description: "Most tags allowed; typing stops at the limit." }, { name: "onValueChange", type: "(tags: string[]) => void", required: false, description: "Called with every tag after an add or remove." }, { name: "separators", type: "string[]", required: false, default: '[","]', description: "Keys that commit the typed text, besides Enter. Pasted text is split on these too." }, { name: "validate", type: "(tag: string) => boolean", required: false, description: "Return false to reject a tag (it stays in the input). Runs after trimming." }, { name: "value", type: "string[]", required: false, description: "Controlled tags; pair with `onValueChange`." }], extends: "<input>", description: "Free-form tags: type and press Enter or a separator; paste a list to add many; Backspace in an\nempty input removes the last tag. `name` submits one value per tag; `required` needs at least one." }], types: {} }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/form-value.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/tag-input.tsx", type: "registry:ui", content: `"use client";
|
|
14968
15330
|
|
|
14969
15331
|
import { X } from "lucide-react";
|
|
14970
15332
|
import { AnimatePresence, motion } from "motion/react";
|
|
14971
15333
|
import { type ComponentProps, type ReactNode, useRef, useState } from "react";
|
|
14972
15334
|
import { useFieldControl } from "@/components/ui/field";
|
|
15335
|
+
import { FormValue } from "@/lib/form-value";
|
|
14973
15336
|
import { useControllableState } from "@/lib/use-controllable-state";
|
|
14974
15337
|
import { cn, spring } from "@/lib/utils";
|
|
14975
15338
|
|
|
@@ -15006,6 +15369,7 @@ export function Tag({
|
|
|
15006
15369
|
)}
|
|
15007
15370
|
>
|
|
15008
15371
|
<span className="truncate">{children}</span>
|
|
15372
|
+
|
|
15009
15373
|
{onRemove && (
|
|
15010
15374
|
<button
|
|
15011
15375
|
type="button"
|
|
@@ -15045,7 +15409,7 @@ type TagInputProps = Omit<
|
|
|
15045
15409
|
|
|
15046
15410
|
/**
|
|
15047
15411
|
* Free-form tags: type and press Enter or a separator; paste a list to add many; Backspace in an
|
|
15048
|
-
* empty input removes the last tag. \`name\` submits one
|
|
15412
|
+
* empty input removes the last tag. \`name\` submits one value per tag; \`required\` needs at least one.
|
|
15049
15413
|
*/
|
|
15050
15414
|
export function TagInput({
|
|
15051
15415
|
value: valueProp,
|
|
@@ -15062,7 +15426,8 @@ export function TagInput({
|
|
|
15062
15426
|
onBlur,
|
|
15063
15427
|
...rest
|
|
15064
15428
|
}: TagInputProps) {
|
|
15065
|
-
const props = useFieldControl(rest);
|
|
15429
|
+
const { required, ...props } = useFieldControl(rest);
|
|
15430
|
+
const [invalid, setInvalid] = useState(false);
|
|
15066
15431
|
const input = useRef<HTMLInputElement>(null);
|
|
15067
15432
|
const [tags, set] = useControllableState(valueProp, defaultValue, onValueChange);
|
|
15068
15433
|
const [draft, setDraft] = useState("");
|
|
@@ -15111,11 +15476,14 @@ export function TagInput({
|
|
|
15111
15476
|
</Tag>
|
|
15112
15477
|
))}
|
|
15113
15478
|
</AnimatePresence>
|
|
15479
|
+
|
|
15114
15480
|
<input
|
|
15115
15481
|
ref={input}
|
|
15116
15482
|
type="text"
|
|
15117
15483
|
data-slot="tag-input"
|
|
15118
15484
|
{...props}
|
|
15485
|
+
aria-required={required}
|
|
15486
|
+
aria-invalid={props["aria-invalid"] || (invalid && !tags.length) || undefined}
|
|
15119
15487
|
// readOnly, not disabled: Backspace still has to remove tags at the limit.
|
|
15120
15488
|
readOnly={full || props.readOnly}
|
|
15121
15489
|
placeholder={full ? undefined : placeholder}
|
|
@@ -15150,7 +15518,19 @@ export function TagInput({
|
|
|
15150
15518
|
}}
|
|
15151
15519
|
className="h-6 min-w-20 flex-1 bg-transparent px-1 outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed"
|
|
15152
15520
|
/>
|
|
15153
|
-
|
|
15521
|
+
|
|
15522
|
+
{name &&
|
|
15523
|
+
tags.map((tag) => (
|
|
15524
|
+
<input key={tag} type="hidden" name={name} value={tag} disabled={props.disabled} />
|
|
15525
|
+
))}
|
|
15526
|
+
|
|
15527
|
+
<FormValue
|
|
15528
|
+
value={tags.join(",")}
|
|
15529
|
+
required={required}
|
|
15530
|
+
disabled={props.disabled}
|
|
15531
|
+
anchor={input}
|
|
15532
|
+
onInvalid={() => setInvalid(true)}
|
|
15533
|
+
/>
|
|
15154
15534
|
</div>
|
|
15155
15535
|
);
|
|
15156
15536
|
}
|
|
@@ -15252,15 +15632,19 @@ function Line({
|
|
|
15252
15632
|
{line.time && (
|
|
15253
15633
|
<span className="mr-3 text-muted-foreground tabular-nums select-none">{line.time}</span>
|
|
15254
15634
|
)}
|
|
15635
|
+
|
|
15255
15636
|
{line.kind === "command" && (
|
|
15256
15637
|
<span className="mr-2 text-muted-foreground select-none">{prompt}</span>
|
|
15257
15638
|
)}
|
|
15639
|
+
|
|
15258
15640
|
{kind.glyph && (
|
|
15259
15641
|
<span aria-hidden="true" className={cn("mr-2", kind.glyphClass)}>
|
|
15260
15642
|
{kind.glyph}
|
|
15261
15643
|
</span>
|
|
15262
15644
|
)}
|
|
15645
|
+
|
|
15263
15646
|
{text}
|
|
15647
|
+
|
|
15264
15648
|
{cursor && <Cursor />}
|
|
15265
15649
|
</div>
|
|
15266
15650
|
);
|
|
@@ -15392,6 +15776,7 @@ export function Terminal({
|
|
|
15392
15776
|
<Line key={key(item, i)} line={item} prompt={prompt} />
|
|
15393
15777
|
))}
|
|
15394
15778
|
</div>
|
|
15779
|
+
|
|
15395
15780
|
<div aria-hidden="true" data-typewriter-animated="">
|
|
15396
15781
|
{lines
|
|
15397
15782
|
.slice(0, at.line + 1)
|
|
@@ -15408,8 +15793,10 @@ export function Terminal({
|
|
|
15408
15793
|
/>
|
|
15409
15794
|
) : null,
|
|
15410
15795
|
)}
|
|
15796
|
+
|
|
15411
15797
|
{done && <Line line={{ kind: "command", text: "" }} prompt={prompt} cursor />}
|
|
15412
15798
|
</div>
|
|
15799
|
+
|
|
15413
15800
|
<div aria-hidden="true" data-typewriter-fallback="" className="hidden">
|
|
15414
15801
|
{lines.map((item, i) => (
|
|
15415
15802
|
<Line key={key(item, i)} line={item} prompt={prompt} />
|
|
@@ -15431,6 +15818,7 @@ export function Terminal({
|
|
|
15431
15818
|
lines.map((item, i) => <Line key={key(item, i)} line={item} prompt={prompt} />)
|
|
15432
15819
|
)}
|
|
15433
15820
|
</div>
|
|
15821
|
+
|
|
15434
15822
|
<AnimatePresence>
|
|
15435
15823
|
{log && behind && (
|
|
15436
15824
|
<motion.button
|
|
@@ -15516,9 +15904,11 @@ export function Testimonial({
|
|
|
15516
15904
|
>
|
|
15517
15905
|
{children}
|
|
15518
15906
|
</Quote>
|
|
15907
|
+
|
|
15519
15908
|
{others && others.length > 0 && (
|
|
15520
15909
|
<div className="flex flex-wrap items-center gap-3 border-t pt-5">
|
|
15521
15910
|
<AvatarGroup users={others} max={5} size="sm" />
|
|
15911
|
+
|
|
15522
15912
|
{othersLabel && <p className="text-muted-foreground text-sm">{othersLabel}</p>}
|
|
15523
15913
|
</div>
|
|
15524
15914
|
)}
|
|
@@ -15600,11 +15990,13 @@ export function TextSwap({
|
|
|
15600
15990
|
aria-live={isControlled ? "polite" : undefined}
|
|
15601
15991
|
>
|
|
15602
15992
|
<span className="sr-only">{currentItem}</span>
|
|
15993
|
+
|
|
15603
15994
|
{list.map((item) => (
|
|
15604
15995
|
<span key={item} aria-hidden="true" className="invisible [grid-area:1/1]">
|
|
15605
15996
|
{item}
|
|
15606
15997
|
</span>
|
|
15607
15998
|
))}
|
|
15999
|
+
|
|
15608
16000
|
<AnimatePresence mode="popLayout" initial={false}>
|
|
15609
16001
|
<motion.span
|
|
15610
16002
|
key={\`\${activeIndex}-\${currentItem}\`}
|
|
@@ -15629,7 +16021,7 @@ export function TextSwap({
|
|
|
15629
16021
|
</span>
|
|
15630
16022
|
);
|
|
15631
16023
|
}
|
|
15632
|
-
` }] }, textarea: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "textarea", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", docs: "https://kznlabs.github.io/labsUI/components/forms", usage: '<Label htmlFor="email">Email</Label><Input id="email" type="email" aria-invalid={err} />', examples: [{ name: "sign-up.tsx", code: `import { Input } from "@/components/ui/input";
|
|
16024
|
+
` }] }, textarea: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "textarea", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", docs: "https://kznlabs.github.io/labsUI/components/forms", usage: '<Label htmlFor="email">Email</Label><Input id="email" type="email" aria-invalid={err} /><Textarea aria-label="Bio" maxLength={280} showCount /><Switch aria-label="Notifications" checked={on} onCheckedChange={setOn} />', examples: [{ name: "sign-up.tsx", code: `import { Input } from "@/components/ui/input";
|
|
15633
16025
|
import { Label } from "@/components/ui/label";
|
|
15634
16026
|
import { Switch } from "@/components/ui/switch";
|
|
15635
16027
|
import { Textarea } from "@/components/ui/textarea";
|
|
@@ -15697,6 +16089,7 @@ export function Textarea({ className, showCount, onChange, ...rest }: TextareaPr
|
|
|
15697
16089
|
return (
|
|
15698
16090
|
<span className="relative block w-full min-w-0">
|
|
15699
16091
|
{textarea}
|
|
16092
|
+
|
|
15700
16093
|
<span
|
|
15701
16094
|
id={countId}
|
|
15702
16095
|
className={cn(
|
|
@@ -15714,7 +16107,7 @@ export function Textarea({ className, showCount, onChange, ...rest }: TextareaPr
|
|
|
15714
16107
|
</span>
|
|
15715
16108
|
);
|
|
15716
16109
|
}
|
|
15717
|
-
` }] }, theme: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "theme", type: "registry:lib", description: "Retints primary, focus rings, selection and switches app-wide. Persists like the theme.", api: { entries: [{ kind: "function", name: "isDark", signature: "isDark() => boolean" }, { kind: "function", name: "setTheme", signature: "setTheme(theme: Theme) => void" }, { kind: "function", name: "subscribeTheme", signature: "subscribeTheme(callback: () => void) => () => void" }, { kind: "function", name: "getAccent", signature: 'getAccent() => "blue" | "green" | "orange" | "pink" | "purple" | "red" | "slate" | "yellow"' }, { kind: "function", name: "setAccent", signature: 'setAccent(accent: "blue" | "green" | "orange" | "pink" | "purple" | "red" | "slate" | "yellow") => void', description: "Retints primary, focus rings, selection and switches app-wide. Persists like the theme." }], types: { Theme: '"light" | "dark"' } }, dependencies: [], registryDependencies: [], files: [{ path: "lib/theme.ts", type: "registry:lib", content: `type Theme = "light" | "dark";
|
|
16110
|
+
` }] }, theme: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "theme", type: "registry:lib", description: "Retints primary, focus rings, selection and switches app-wide. Persists like the theme.", api: { entries: [{ kind: "function", name: "isDark", signature: "isDark() => boolean" }, { kind: "function", name: "setTheme", signature: "setTheme(theme: Theme) => void" }, { kind: "function", name: "subscribeTheme", signature: "subscribeTheme(callback: () => void) => () => void" }, { kind: "function", name: "getAccent", signature: 'getAccent() => "blue" | "green" | "orange" | "pink" | "purple" | "red" | "slate" | "yellow"' }, { kind: "function", name: "setAccent", signature: 'setAccent(accent: "blue" | "green" | "orange" | "pink" | "purple" | "red" | "slate" | "yellow") => void', description: "Retints primary, focus rings, selection and switches app-wide. Persists like the theme." }], types: { Theme: '"light" | "dark"' } }, dependencies: [], registryDependencies: [], files: [{ path: "lib/theme.ts", type: "registry:lib", content: `export type Theme = "light" | "dark";
|
|
15718
16111
|
|
|
15719
16112
|
const KEY = "theme";
|
|
15720
16113
|
const EVENT = "themechange";
|
|
@@ -15909,6 +16302,7 @@ export function TimelineItem({
|
|
|
15909
16302
|
aria-hidden="true"
|
|
15910
16303
|
className="absolute top-7 bottom-1 left-3 w-px -translate-x-1/2 bg-border group-last/item:hidden"
|
|
15911
16304
|
/>
|
|
16305
|
+
|
|
15912
16306
|
<span
|
|
15913
16307
|
aria-hidden="true"
|
|
15914
16308
|
className={cn(
|
|
@@ -15920,6 +16314,7 @@ export function TimelineItem({
|
|
|
15920
16314
|
>
|
|
15921
16315
|
{icon}
|
|
15922
16316
|
</span>
|
|
16317
|
+
|
|
15923
16318
|
<div className="grid min-w-0 gap-1 pt-0.5">
|
|
15924
16319
|
{time &&
|
|
15925
16320
|
(dateTime ? (
|
|
@@ -15929,7 +16324,9 @@ export function TimelineItem({
|
|
|
15929
16324
|
) : (
|
|
15930
16325
|
<span className="text-muted-foreground text-xs tabular-nums">{time}</span>
|
|
15931
16326
|
))}
|
|
16327
|
+
|
|
15932
16328
|
<p className="font-medium text-sm leading-snug">{title}</p>
|
|
16329
|
+
|
|
15933
16330
|
{children && (
|
|
15934
16331
|
<div className="text-muted-foreground text-sm leading-relaxed">{children}</div>
|
|
15935
16332
|
)}
|
|
@@ -15938,12 +16335,12 @@ export function TimelineItem({
|
|
|
15938
16335
|
);
|
|
15939
16336
|
}
|
|
15940
16337
|
` }] }, toast: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "toast", type: "registry:ui", title: "Toast", description: "Stacked notifications. Hover to expand, swipe to dismiss, timers pause on hover.", docs: "https://kznlabs.github.io/labsUI/components/toast", mount: "<Toaster />", usage: 'toast.success("Saved", { description: "All changes synced", action: { label: "Undo", onClick } })', examples: [{ name: "save.tsx", code: `import { toast } from "@/components/ui/toast";
|
|
15941
|
-
export function notifySaved() {
|
|
16338
|
+
export function notifySaved(undo: () => void) {
|
|
15942
16339
|
toast.success("Changes saved", {
|
|
15943
16340
|
description: "All changes synced",
|
|
15944
16341
|
action: { label: "Undo", onClick: undo },
|
|
15945
16342
|
});
|
|
15946
|
-
}` }], api: { entries: [{ kind: "function", name: "toast", signature: 'toast(title: ReactNode, opts?: Partial<Omit<ToastData, "id" | "title">>) => number', description: '`toast("Saved")`, `toast.success(title, { description, action, duration })`, `toast.loading(title)`, `toast.promise(p, { loading, success, error })`, `toast.dismiss(id?)`.' }, { kind: "component", name: "Toaster", props: [], description: "Mount once (root layout). Stacked toasts: hover, focus or a tap expands, swipe dismisses." }], types: { ToastData: "{ id: number; title: ReactNode; description?: ReactNode; variant:
|
|
16343
|
+
}` }], api: { entries: [{ kind: "function", name: "toast", signature: 'toast(title: ReactNode, opts?: Partial<Omit<ToastData, "id" | "title">>) => number', description: '`toast("Saved")`, `toast.success(title, { description, action, duration })`, `toast.loading(title)`, `toast.promise(p, { loading, success, error })`, `toast.dismiss(id?)`.' }, { kind: "component", name: "Toaster", props: [], description: "Mount once (root layout). Stacked toasts: hover, focus or a tap expands, swipe dismisses." }], types: { ToastData: "{ id: number; title: ReactNode; description?: ReactNode; variant: ToastVariant; duration: number; action?: { label: string; onClick: () => void }; }" } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/use-dismiss.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/toast.tsx", type: "registry:ui", content: `"use client";
|
|
15947
16344
|
|
|
15948
16345
|
import { CircleAlert, CircleCheck, Info, Loader2, X } from "lucide-react";
|
|
15949
16346
|
import { AnimatePresence, animate, motion, type PanInfo, useMotionValue } from "motion/react";
|
|
@@ -15960,12 +16357,12 @@ import { cn, material, spring } from "@/lib/utils";
|
|
|
15960
16357
|
|
|
15961
16358
|
/* ---------- store (module-level, no provider needed) ---------- */
|
|
15962
16359
|
|
|
15963
|
-
type
|
|
15964
|
-
type ToastData = {
|
|
16360
|
+
export type ToastVariant = "default" | "success" | "error" | "info" | "loading";
|
|
16361
|
+
export type ToastData = {
|
|
15965
16362
|
id: number;
|
|
15966
16363
|
title: ReactNode;
|
|
15967
16364
|
description?: ReactNode;
|
|
15968
|
-
variant:
|
|
16365
|
+
variant: ToastVariant;
|
|
15969
16366
|
/** ms; Infinity = sticky. */
|
|
15970
16367
|
duration: number;
|
|
15971
16368
|
action?: { label: string; onClick: () => void };
|
|
@@ -16235,13 +16632,16 @@ function ToastItem({ toast: data, index, y, expanded, frontHeight, total, onHeig
|
|
|
16235
16632
|
className="flex items-start gap-3 p-4"
|
|
16236
16633
|
>
|
|
16237
16634
|
{icons[data.variant] && <span className="mt-0.5">{icons[data.variant]}</span>}
|
|
16635
|
+
|
|
16238
16636
|
<div
|
|
16239
16637
|
className="flex min-w-0 flex-1 flex-col gap-0.5"
|
|
16240
16638
|
role={data.variant === "error" ? "alert" : undefined}
|
|
16241
16639
|
>
|
|
16242
16640
|
<p className="font-medium text-sm">{data.title}</p>
|
|
16641
|
+
|
|
16243
16642
|
{data.description && <p className="text-muted-foreground text-sm">{data.description}</p>}
|
|
16244
16643
|
</div>
|
|
16644
|
+
|
|
16245
16645
|
{data.action && (
|
|
16246
16646
|
<button
|
|
16247
16647
|
type="button"
|
|
@@ -16254,12 +16654,14 @@ function ToastItem({ toast: data, index, y, expanded, frontHeight, total, onHeig
|
|
|
16254
16654
|
{data.action.label}
|
|
16255
16655
|
</button>
|
|
16256
16656
|
)}
|
|
16657
|
+
|
|
16257
16658
|
<button
|
|
16258
16659
|
type="button"
|
|
16259
16660
|
onClick={() => dismiss(data.id)}
|
|
16260
16661
|
className="-mt-1 -mr-1 shrink-0 cursor-pointer rounded-md p-1 text-muted-foreground opacity-0 outline-none transition-opacity hover:text-foreground focus-visible:opacity-100 focus-visible:ring-[3px] focus-visible:ring-ring/50 group-hover:opacity-100"
|
|
16261
16662
|
>
|
|
16262
16663
|
<X className="size-3.5" aria-hidden="true" />
|
|
16664
|
+
|
|
16263
16665
|
<span className="sr-only">Dismiss</span>
|
|
16264
16666
|
</button>
|
|
16265
16667
|
</motion.div>
|
|
@@ -16405,6 +16807,7 @@ export function ToggleGroup(props: ToggleGroupProps) {
|
|
|
16405
16807
|
className="-z-10 absolute inset-y-0.5 left-0 rounded-md bg-raised shadow-sm ring-[0.5px] ring-raised-ring"
|
|
16406
16808
|
/>
|
|
16407
16809
|
)}
|
|
16810
|
+
|
|
16408
16811
|
{children}
|
|
16409
16812
|
</div>
|
|
16410
16813
|
</GroupContext>
|
|
@@ -16609,9 +17012,11 @@ export function Tooltip({
|
|
|
16609
17012
|
{...touchHandlers}
|
|
16610
17013
|
>
|
|
16611
17014
|
{children}
|
|
17015
|
+
|
|
16612
17016
|
<span id={id} hidden>
|
|
16613
17017
|
{content}
|
|
16614
17018
|
</span>
|
|
17019
|
+
|
|
16615
17020
|
{mounted &&
|
|
16616
17021
|
createPortal(
|
|
16617
17022
|
<AnimatePresence>
|
|
@@ -16784,12 +17189,15 @@ export function Typewriter({
|
|
|
16784
17189
|
return (
|
|
16785
17190
|
<span className={cn("inline-flex items-baseline", className)}>
|
|
16786
17191
|
<span className="sr-only">{fullText}</span>
|
|
17192
|
+
|
|
16787
17193
|
<span aria-hidden="true" data-typewriter-animated="">
|
|
16788
17194
|
{displayed}
|
|
16789
17195
|
</span>
|
|
17196
|
+
|
|
16790
17197
|
<span aria-hidden="true" data-typewriter-fallback="" className="hidden">
|
|
16791
17198
|
{phrases[0]}
|
|
16792
17199
|
</span>
|
|
17200
|
+
|
|
16793
17201
|
{cursor && (
|
|
16794
17202
|
<span
|
|
16795
17203
|
aria-hidden="true"
|
|
@@ -16808,24 +17216,24 @@ export function Typewriter({
|
|
|
16808
17216
|
flipped when it doesn't fit, clamped to the viewport, kept in place on scroll and resize.
|
|
16809
17217
|
Measures before paint, so nothing jumps.`, api: { entries: [{ kind: "function", name: "useAnchor", signature: `useAnchor(open: boolean, anchor: RefObject<HTMLElement | null>, floating: RefObject<HTMLElement | null>, { align, side: preferred, offset }?: {
|
|
16810
17218
|
align?: "center" | "end" | "start" | undefined;
|
|
16811
|
-
side?:
|
|
17219
|
+
side?: AnchorSide | undefined;
|
|
16812
17220
|
offset?: number | undefined;
|
|
16813
|
-
}) =>
|
|
17221
|
+
}) => AnchorPlacement`, description: `Fixed-position coordinates for a floating panel next to its anchor: on the preferred side,
|
|
16814
17222
|
flipped when it doesn't fit, clamped to the viewport, kept in place on scroll and resize.
|
|
16815
|
-
Measures before paint, so nothing jumps.` }], types: {
|
|
17223
|
+
Measures before paint, so nothing jumps.` }], types: { AnchorSide: '"bottom" | "top"', AnchorPlacement: "{ top: number; left: number; width: number; side: AnchorSide; }" } }, dependencies: [], registryDependencies: ["https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "lib/use-anchor.ts", type: "registry:hook", content: `"use client";
|
|
16816
17224
|
|
|
16817
17225
|
import { type RefObject, useLayoutEffect, useState } from "react";
|
|
16818
17226
|
import { clamp } from "@/lib/utils";
|
|
16819
17227
|
|
|
16820
|
-
type
|
|
17228
|
+
export type AnchorSide = "bottom" | "top";
|
|
16821
17229
|
|
|
16822
|
-
type
|
|
17230
|
+
export type AnchorPlacement = {
|
|
16823
17231
|
top: number;
|
|
16824
17232
|
left: number;
|
|
16825
17233
|
/** Anchor width, for \`min-width\` matching (Select). */
|
|
16826
17234
|
width: number;
|
|
16827
17235
|
/** Side actually used: the preferred one, or the other for lack of room. */
|
|
16828
|
-
side:
|
|
17236
|
+
side: AnchorSide;
|
|
16829
17237
|
};
|
|
16830
17238
|
|
|
16831
17239
|
const MARGIN = 8;
|
|
@@ -16843,9 +17251,9 @@ export function useAnchor(
|
|
|
16843
17251
|
align = "start",
|
|
16844
17252
|
side: preferred = "bottom",
|
|
16845
17253
|
offset = 6,
|
|
16846
|
-
}: { align?: "start" | "center" | "end"; side?:
|
|
17254
|
+
}: { align?: "start" | "center" | "end"; side?: AnchorSide; offset?: number } = {},
|
|
16847
17255
|
) {
|
|
16848
|
-
const [pos, setPos] = useState<
|
|
17256
|
+
const [pos, setPos] = useState<AnchorPlacement>({
|
|
16849
17257
|
top: -9999,
|
|
16850
17258
|
left: -9999,
|
|
16851
17259
|
width: 0,
|
|
@@ -16868,7 +17276,7 @@ export function useAnchor(
|
|
|
16868
17276
|
const above = anchorRect.top - offset - height;
|
|
16869
17277
|
const fitsBelow = below + height <= innerHeight - MARGIN;
|
|
16870
17278
|
const fitsAbove = above >= MARGIN;
|
|
16871
|
-
const side:
|
|
17279
|
+
const side: AnchorSide =
|
|
16872
17280
|
preferred === "bottom"
|
|
16873
17281
|
? fitsBelow || !fitsAbove
|
|
16874
17282
|
? "bottom"
|
|
@@ -16955,17 +17363,17 @@ export function useDisclosure(defaultOpen = false) {
|
|
|
16955
17363
|
props: { open, onOpenChange: setOpen },
|
|
16956
17364
|
};
|
|
16957
17365
|
}
|
|
16958
|
-
` }] }, "use-dismiss": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "use-dismiss", type: "registry:hook", description: "Inside one of `refs` (panel first, then its trigger), or inside a floating child opened from\nthe panel: a Select list or menu portals out, but it is marked `data-floating`.", api: { entries: [{ kind: "function", name: "isWithin", signature: "isWithin(refs:
|
|
17366
|
+
` }] }, "use-dismiss": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "use-dismiss", type: "registry:hook", description: "Inside one of `refs` (panel first, then its trigger), or inside a floating child opened from\nthe panel: a Select list or menu portals out, but it is marked `data-floating`.", api: { entries: [{ kind: "function", name: "isWithin", signature: "isWithin(refs: DismissRefs, target: Node | null) => boolean", description: "Inside one of `refs` (panel first, then its trigger), or inside a floating child opened from\nthe panel: a Select list or menu portals out, but it is marked `data-floating`." }, { kind: "function", name: "useDismiss", signature: "useDismiss(open: boolean, refs: DismissRefs, onDismiss: () => void) => void", description: "Closes a floating panel on a pointerdown outside it, its trigger and its floating children." }], types: { DismissRefs: "readonly RefObject<HTMLElement | null>[]" } }, dependencies: [], registryDependencies: [], files: [{ path: "lib/use-dismiss.ts", type: "registry:hook", content: `"use client";
|
|
16959
17367
|
|
|
16960
17368
|
import { type RefObject, useEffect, useEffectEvent } from "react";
|
|
16961
17369
|
|
|
16962
|
-
type
|
|
17370
|
+
export type DismissRefs = readonly RefObject<HTMLElement | null>[];
|
|
16963
17371
|
|
|
16964
17372
|
/**
|
|
16965
17373
|
* Inside one of \`refs\` (panel first, then its trigger), or inside a floating child opened from
|
|
16966
17374
|
* the panel: a Select list or menu portals out, but it is marked \`data-floating\`.
|
|
16967
17375
|
*/
|
|
16968
|
-
export function isWithin(refs:
|
|
17376
|
+
export function isWithin(refs: DismissRefs, target: Node | null) {
|
|
16969
17377
|
if (!target) return false;
|
|
16970
17378
|
if (refs.some((ref) => ref.current?.contains(target))) return true;
|
|
16971
17379
|
|
|
@@ -16975,7 +17383,7 @@ export function isWithin(refs: Refs, target: Node | null) {
|
|
|
16975
17383
|
}
|
|
16976
17384
|
|
|
16977
17385
|
/** Closes a floating panel on a pointerdown outside it, its trigger and its floating children. */
|
|
16978
|
-
export function useDismiss(open: boolean, refs:
|
|
17386
|
+
export function useDismiss(open: boolean, refs: DismissRefs, onDismiss: () => void) {
|
|
16979
17387
|
// Effect events, an inline refs array or callback can't re-subscribe every render.
|
|
16980
17388
|
const outside = useEffectEvent((event: PointerEvent) => {
|
|
16981
17389
|
if (!isWithin(refs, event.target as Node)) onDismiss();
|
|
@@ -17009,7 +17417,7 @@ export function matchesHotkey(
|
|
|
17009
17417
|
return mod ? event.metaKey || event.ctrlKey : !event.metaKey && !event.ctrlKey;
|
|
17010
17418
|
}
|
|
17011
17419
|
|
|
17012
|
-
type HotkeyOptions = {
|
|
17420
|
+
export type HotkeyOptions = {
|
|
17013
17421
|
/** Ignore the shortcut while typing in a field. Default: only for single keys. */
|
|
17014
17422
|
skipInFields?: boolean;
|
|
17015
17423
|
};
|
|
@@ -17182,9 +17590,9 @@ export const spring = { type: "spring", stiffness: 400, damping: 30 } as const;
|
|
|
17182
17590
|
export const clamp = (value: number, min: number, max: number) =>
|
|
17183
17591
|
Math.min(Math.max(value, min), max);
|
|
17184
17592
|
` }] } },
|
|
17185
|
-
index: [{ name: "accent-picker", type: "registry:ui", title: "Accent Picker", description: "Accent color row: eight swatches (red to slate) that retint primary, focus rings, selection, switches and checkboxes app-wide. Persisted, applied before paint, or set your own hue with --accent-hue.", exports: ["AccentPicker"], keywords: ["accent", "app", "color", "radio", "retint", "row", "setaccent", "swatches", "that", "the", "via", "whole"], registryDependencies: ["theme", "utils"] }, { name: "accordion", type: "registry:ui", title: "Accordion", description: "Single or multiple panels with spring height animation. Closed content stays in the DOM for SEO.", exports: ["Accordion", "AccordionItem", "AccordionTrigger", "AccordionContent"], keywords: ["accordion", "accordioncontent", "accordionitem", "accordiontrigger", "allow", "and", "called", "children", "closed", "closes", "closing", "collapsible", "content", "controlled", "crawlable", "default", "defaultvalue", "every", "first", "heading", "height", "html", "independent", "inert", "item", "items", "level", "match", "matched", "mounted", "multiple", "none", "one", "only", "onvaluechange", "open", "outline", "page", "panel", "panels", "question", "remains", "render", "single", "stays", "text", "the", "time", "type", "uncontrolled", "unique", "value", "when", "while", "with", "within", "wrapping"], registryDependencies: ["use-controllable-state", "utils"] }, { name: "alert", type: "registry:ui", title: "Alert", description: "Server-safe inline banner: tinted surface, colored icon, title, description and an optional action. Info, success, warning and destructive tones follow the accent and status tokens.", exports: ["Alert", "AlertTitle", "AlertDescription"], keywords: ["action", "add", "alert", "aligned", "and", "announced", "appears", "banner", "button", "colored", "default", "description", "did", "for", "icon", "inline", "live", "load", "none", "not", "null", "pass", "region", "replaces", "response", "right", "role", "sets", "should", "slot", "small", "something", "static", "surface", "the", "tint", "tinted", "title", "tone", "user", "variant", "when"], registryDependencies: ["utils"] }, { name: "app-window", type: "registry:ui", title: "AppWindow", description: "Window container with close / minimize / zoom controls, title, custom header slot and actions. SegmentedTabs is the compact view switcher for its title bar.", exports: ["AppWindow"], keywords: ["actions", "aligned", "area", "bar", "blurred", "bodyclassname", "button", "called", "centered", "classes", "content", "control", "custom", "dot", "for", "glass", "handler", "header", "into", "matching", "maximize", "minimize", "navbar", "onclose", "onmaximize", "onminimize", "passing", "pressed", "replaces", "right", "search", "shows", "slot", "surface", "tabs", "text", "the", "title", "titlebar", "translucent", "turns", "under", "when"], registryDependencies: ["utils"] }, { name: "aspect-ratio", type: "registry:ui", title: "Aspect Ratio", description: "Server-safe box locked to a ratio (16:9 thumbnails, 9:16 clips). A direct image or video child fills it with object-cover.", exports: ["AspectRatio"], keywords: ["box", "child", "cover", "direct", "fills", "height", "img", "locked", "object", "ratio", "video", "width"], registryDependencies: ["utils"] }, { name: "attachment", type: "registry:ui", title: "Attachment & Dropzone", description: "File chips with image thumbnails or type icons, size, a springing upload bar, error state and remove; the list springs chips in and closes the gap. FileDropzone wraps a real file input: click, keyboard and the native picker work, drop and paste are checked against accept and maxSize, and dropped files land in the input so forms and Server Actions receive them.", exports: ["formatBytes", "Attachment", "AttachmentList", "FileDropzone"], keywords: ["accept", "actions", "along", "and", "are", "area", "aria", "around", "attachments", "attribute", "back", "bar", "bcp", "blocks", "both", "bottom", "browse", "button", "bytes", "called", "child", "children", "chip", "click", "content", "createobjecturl", "default", "describedby", "disabled", "download", "drop", "dropped", "drops", "each", "edge", "enforced", "enter", "error", "failed", "field", "file", "files", "fills", "for", "form", "forms", "fresh", "from", "get", "given", "hint", "href", "icon", "ids", "image", "input", "into", "invalid", "key", "least", "line", "link", "locale", "makes", "maxsize", "mime", "more", "multiple", "name", "native", "needs", "one", "onfiles", "onreject", "onremove", "out", "passed", "paste", "pdf", "picked", "picker", "picks", "place", "plain", "pressed", "progress", "real", "reason", "red", "remove", "removed", "replaces", "required", "same", "server", "sets", "show", "shows", "size", "source", "space", "spring", "springs", "src", "styling", "submit", "syntax", "text", "than", "that", "the", "them", "thumbnail", "too", "turns", "type", "types", "upload", "uploading", "url", "when", "while", "with", "work", "written"], registryDependencies: ["field", "utils"] }, { name: "avatar", type: "registry:ui", title: "Avatar", description: "Image with initials fallback on error, three sizes and online/away/busy/offline dots.", exports: ["Avatar"], keywords: ["alt", "and", "announced", "corner", "diameter", "dot", "fails", "fallback", "for", "icon", "image", "initials", "loads", "name", "overrides", "person", "presence", "show", "size", "src", "status", "text", "the", "url", "used", "while", "with"], registryDependencies: ["utils"] }, { name: "avatar-group", type: "registry:ui", title: "AvatarGroup", description: "Overlapping avatars with a +N overflow chip. Hover gently enlarges the avatar on a spring, keeping stack order.", exports: ["AvatarGroup"], keywords: ["alt", "and", "avatar", "avatars", "before", "chip", "each", "enlarges", "every", "gently", "hover", "how", "many", "max", "name", "one", "order", "overflow", "overlapping", "people", "show", "size", "spring", "stack", "staying", "the", "users", "with"], registryDependencies: ["avatar", "utils"] }, { name: "badge", type: "registry:ui", title: "Badge & Pill", description: "Minimal status tags. Rounded badge or fully rounded pill, with optional dot. pulse adds an expanding live-status ring.", exports: ["Badge"], keywords: ["and", "color", "current", "dot", "emits", "expanding", "forever", "fully", "leading", "like", "live", "needs", "pill", "pulse", "ring", "rounded", "shape", "status", "surface", "tag", "text", "the", "variant"], registryDependencies: ["utils"] }, { name: "breadcrumb", type: "registry:ui", title: "Breadcrumb", description: "Navigation trail from a list of items: links up to the current page, which is marked for assistive tech. Server-safe, custom separator.", exports: ["Breadcrumb"], keywords: ["between", "chevron", "current", "default", "drawn", "ending", "first", "item", "items", "last", "links", "list", "outermost", "page", "safe", "separator", "server", "site", "sits", "the", "trail", "where"], registryDependencies: [] }, { name: "button", type: "registry:ui", title: "Button", description: "Five variants, four sizes, spring press feedback, an animated loading spinner and in-place success state, a two-step confirm that swaps the label on first click, and a configurable swap direction. Width stays fixed to the longest label.", exports: ["Button"], keywords: ["action", "also", "and", "aria", "armed", "before", "blur", "button", "check", "children", "click", "confirm", "confirmlabel", "confirmtimeout", "default", "defaults", "destructive", "disables", "done", "emphasis", "esc", "feedback", "fill", "first", "for", "give", "height", "icon", "irreversible", "its", "label", "left", "loading", "loadinglabel", "never", "old", "onclick", "one", "ones", "out", "padding", "per", "place", "primary", "reserved", "resets", "resize", "runs", "second", "shown", "shows", "size", "slides", "spinner", "spins", "square", "status", "step", "success", "successlabel", "swap", "swaps", "text", "the", "this", "two", "variant", "view", "where", "while", "width", "with"], registryDependencies: ["button-variants", "utils"] }, { name: "button-group", type: "registry:ui", title: "Button Group", description: "Joins buttons into one strip that shares borders, corners and active states. Horizontal or vertical.", exports: ["ButtonGroup"], keywords: ["and", "attached", "borders", "buttons", "connected", "control", "corners", "default", "fuse", "groups", "horizontal", "into", "orientation", "related", "single", "strip", "together", "toolbar", "true", "vertical"], registryDependencies: ["utils"] }, { name: "button-variants", type: "registry:ui", description: 'Button classes for non-button elements, e.g. `<a className={buttonVariants({ variant: "outline" })}>`.', exports: ["buttonVariants"], keywords: ["button", "buttonvariants", "classes", "classname", "elements", "for", "non", "outline", "variant"], registryDependencies: ["utils"] }, { name: "calendar", type: "registry:ui", title: "Calendar", description: "Month grid on the WAI-ARIA date-grid pattern: single date or range with a hover preview, min/max and custom disabled days, months that slide in from the direction of travel. Arrows, Home/End and PageUp/PageDown (+Shift for years). Hydration-safe: today is only marked in the browser.", exports: ["key", "Calendar"], keywords: ["and", "another", "any", "aria", "arrows", "autofocus", "bcp", "called", "client", "controlled", "current", "custom", "date", "datepicker", "dates", "day", "days", "default", "defaultmonth", "defaultvalue", "direction", "disabled", "displayed", "earliest", "end", "ends", "extra", "first", "focus", "for", "from", "function", "grid", "home", "hover", "into", "isdisabled", "iso", "keyboard", "latest", "like", "local", "locale", "max", "min", "mode", "monday", "month", "months", "mount", "names", "nothing", "null", "off", "one", "only", "onmonthchange", "onvaluechange", "pagedown", "pages", "pageup", "pattern", "pick", "picked", "preview", "range", "render", "rule", "saturday", "selectable", "sets", "shift", "shown", "single", "slide", "sorts", "start", "sunday", "the", "then", "this", "travel", "uncontrolled", "user", "value", "wai", "week", "weekday", "weekends", "weekstartson", "when", "with", "year", "yyyy"], registryDependencies: ["button", "use-controllable-state", "utils"] }, { name: "card", type: "registry:ui", title: "Card", description: "Composable surface: header, title, description, content and footer. Optional glass.", exports: ["Card", "CardHeader", "CardTitle", "CardDescription", "CardContent", "CardFooter"], keywords: ["backgrounds", "colored", "default", "directly", "fits", "follows", "glass", "heading", "level", "not", "outline", "over", "page", "place", "subtle", "textured", "that", "the", "translucency", "under", "when"], registryDependencies: ["utils"] }, { name: "carousel", type: "registry:ui", title: "Carousel", description: "Native scroll-snap row, so touch, trackpad and no-JS scrolling work, with spring-driven arrows, page dots, arrow keys and mouse drag that throws to the next snap point. APG carousel semantics: a labeled region of slides announced as 3 of 8.", exports: ["Carousel", "CarouselContent", "CarouselItem", "CarouselPrevious", "CarouselNext", "CarouselDots"], keywords: ["accessible", "all", "and", "announced", "any", "apg", "are", "arrow", "basis", "buttons", "carousel", "css", "default", "dot", "dots", "drag", "driven", "edge", "end", "fewer", "from", "gap", "hides", "keys", "label", "labeled", "left", "mouse", "name", "native", "not", "one", "overlaid", "page", "pages", "per", "projects", "region", "rem", "right", "row", "scroll", "scrolling", "semantics", "sized", "slide", "slides", "snap", "spring", "start", "the", "there", "total", "touch", "trackpad", "two", "variables", "view", "with", "work"], registryDependencies: ["button", "utils"] }, { name: "changelog", type: "registry:block", title: "Changelog", description: "Release notes on a Timeline: newest release highlighted, changes typed New / Improved / Fixed with Badges, and a Kbd shortcut to try the headline feature. Server-safe.", exports: ["Changelog"], keywords: ["are", "badges", "changes", "first", "highlighted", "how", "kbd", "newest", "notes", "one", "release", "releases", "shows", "the", "timeline", "try", "typed", "with"], registryDependencies: ["badge", "kbd", "timeline", "utils"] }, { name: "chart", type: "registry:ui", title: "Chart", description: "Line, area and bar (grouped or stacked) charts in SVG with a colorblind-validated eight-color palette that keeps each series' color. Crosshair tooltip listing every series, arrow-key readout, legend that hides series, direct end labels, a table view, and spring transitions between data.", exports: ["Chart"], keywords: ["add", "and", "are", "area", "areas", "arrow", "assistive", "axis", "bar", "bars", "bcp", "can", "captions", "categorical", "category", "chart", "charts", "compact", "crosshair", "currency", "data", "direct", "each", "eight", "end", "every", "filled", "fold", "for", "format", "grouping", "height", "hides", "index", "instead", "into", "key", "keys", "label", "labels", "legend", "line", "lines", "listing", "locale", "mark", "most", "names", "notation", "numbers", "object", "one", "only", "other", "pages", "palette", "past", "per", "plot", "plus", "props", "render", "row", "serializable", "series", "server", "split", "stack", "stacked", "style", "svg", "table", "tail", "tech", "that", "the", "them", "ticks", "too", "tooltip", "type", "usd", "value", "view", "with"], registryDependencies: ["button", "table", "use-dismiss", "utils"] }, { name: "checkbox", type: "registry:ui", title: "Checkbox", description: "Native checkbox with a spring-drawn check mark and an indeterminate state for select-all rows. Controlled or uncontrolled; submits with forms.", exports: ["Checkbox"], keywords: ["all", "called", "check", "checkbox", "checked", "controlled", "dash", "defaultchecked", "drawn", "field", "first", "indeterminate", "inside", "mixed", "native", "new", "oncheckedchange", "pair", "render", "rows", "select", "selected", "shows", "some", "spring", "state", "the", "uncontrolled", "with", "works"], registryDependencies: ["field", "use-controllable-state", "utils"] }, { name: "clip-card", type: "registry:block", title: "Clip Card", description: "Video tile: AspectRatio thumbnail (16:9 or 9:16), duration Badge, watched Progress along the bottom, and a ContextMenu of actions.", exports: ["ClipCard"], keywords: ["accessible", "actions", "ago", "along", "also", "and", "aspectratio", "badge", "bar", "bottom", "card", "click", "clip", "clips", "context", "contextmenuitem", "days", "duration", "f10", "film", "for", "glyph", "height", "how", "img", "items", "menu", "meta", "much", "name", "one", "opens", "print", "progress", "ratio", "right", "shift", "small", "stands", "the", "them", "thin", "thumbnail", "tile", "title", "under", "vertical", "video", "views", "was", "watched", "width", "without"], registryDependencies: ["aspect-ratio", "badge", "context-menu", "progress", "utils"] }, { name: "code", type: "registry:ui", title: "Code", description: "Shiki-highlighted code that follows light and dark through light-dark() token colors, as a block, an inline one-liner or a tabbed file panel. Line numbers (from any start line), tinted lines with an accent bar, <mark>ed words or column ranges, and per-line notes in a tooltip that keyboard users can tab to. Optional Prettier at display time; copy hands out exactly what is shown.", exports: ["CopyButton", "Code", "CodePanel"], keywords: ["accessible", "active", "add", "all", "and", "animates", "annotations", "bar", "bash", "before", "behind", "block", "body", "build", "button", "clipboard", "code", "collapse", "copied", "copy", "dark", "default", "dom", "each", "every", "excerpts", "file", "files", "first", "focus", "fold", "for", "format", "formatted", "from", "gets", "ghost", "gutter", "header", "highlight", "highlightcode", "highlighted", "highlighting", "hover", "ignored", "inline", "inside", "into", "its", "json", "keep", "label", "language", "least", "left", "light", "line", "linenumbers", "lines", "loads", "longer", "made", "many", "mark", "marks", "name", "never", "note", "notes", "number", "numbers", "one", "options", "own", "page", "paint", "panel", "per", "place", "prettier", "primary", "ranges", "refer", "registry", "remember", "renders", "run", "scrolling", "scrolls", "server", "sets", "shared", "shiki", "show", "source", "stays", "switching", "syntax", "tab", "terminal", "text", "than", "that", "the", "themes", "this", "time", "tint", "tokens", "too", "touch", "tsx", "turns", "variant", "vertically", "wheel", "with", "words", "wrapped", "written"], registryDependencies: ["button", "highlight", "overflow-focus", "tabs", "tooltip", "utils"] }, { name: "collapsible", type: "registry:ui", title: "Collapsible", description: "A single show/hide region with a spring height animation and rotating chevron. Controlled or uncontrolled; closed content stays in the DOM for SEO.", exports: ["Collapsible", "CollapsibleTrigger", "CollapsibleContent"], keywords: ["after", "and", "called", "chevron", "children", "closed", "collapsiblecontent", "collapsibletrigger", "content", "controlled", "crawlable", "default", "defaultopen", "first", "height", "hide", "html", "inert", "label", "mounted", "one", "onopenchange", "open", "pair", "region", "remains", "render", "rotating", "show", "springs", "state", "stays", "that", "the", "toggles", "trigger", "true", "uncontrolled", "when", "while", "with"], registryDependencies: ["use-controllable-state", "utils"] }, { name: "combobox", type: "registry:ui", title: "Combobox", description: "A Select you can type into: the query filters by label and keywords (prefix matches first), arrows move, Enter picks. multiple keeps the list open and shows picks as removable tags. Submits through hidden inputs.", exports: ["Combobox"], keywords: ["accessible", "and", "aria", "backspace", "blocks", "called", "can", "choices", "closes", "controlled", "defaultvalue", "describedby", "disabled", "down", "emptytext", "enter", "error", "esc", "every", "field", "fills", "filtered", "filters", "first", "form", "from", "hidden", "hint", "ids", "input", "inputs", "into", "invalid", "keeps", "label", "last", "list", "matches", "move", "multiple", "name", "needs", "nothing", "one", "onvaluechange", "open", "opening", "options", "pair", "per", "pick", "picked", "picks", "placeholder", "removable", "removes", "render", "required", "select", "sets", "show", "shown", "shows", "single", "stay", "styling", "submit", "submits", "tags", "text", "the", "there", "through", "type", "types", "typing", "uncontrolled", "user", "value", "values", "visible", "when", "with", "you"], registryDependencies: ["field", "list", "platform", "tag-input", "use-anchor", "use-controllable-state", "use-dismiss", "utils"] }, { name: "command", type: "registry:ui", title: "Command", description: "⌘K palette: filterable groups, arrow-key highlight, Enter to run, keyword matching. Inline or as CommandDialog with a global shortcut; the first Esc clears, the second closes.", exports: ["Command", "CommandInput", "CommandList", "CommandEmpty", "CommandGroup", "CommandSeparator", "CommandItem", "CommandDialog"], keywords: ["accessible", "after", "against", "and", "arrows", "but", "called", "children", "click", "close", "closes", "collapse", "command", "commanddialog", "commandempty", "commandinput", "commanditem", "commandlist", "content", "controlled", "ctrl", "defaults", "dialog", "disable", "disabled", "driven", "elsewhere", "end", "enter", "esc", "extra", "false", "filterable", "filtering", "for", "global", "group", "groups", "heading", "hidden", "hides", "home", "icon", "inline", "input", "inside", "item", "items", "its", "itself", "keepopen", "keyboard", "keywords", "label", "list", "listbox", "mac", "match", "matched", "matches", "mod", "move", "name", "none", "not", "only", "onopenchange", "onselect", "open", "option", "outside", "own", "pair", "palette", "pass", "passes", "placeholder", "preferences", "query", "results", "scrollable", "search", "selectable", "selecting", "selection", "selects", "separators", "settings", "shortcut", "shown", "shows", "stack", "state", "stay", "terms", "text", "the", "title", "toggle", "toggled", "use", "used", "value", "visible", "visually", "when", "while", "with", "would"], registryDependencies: ["dialog", "list", "search-input", "separator", "use-controllable-state", "use-hotkey", "utils"] }, { name: "confirm", type: "registry:ui", title: "Confirm", description: "Promise-based confirm dialog called like toast: await confirm({ title, description, destructive }) resolves true or false. Built on Dialog's alert mode, Cancel takes the first focus, Esc cancels, one host mounted in the layout.", exports: ["confirm", "Confirmer"], keywords: ["acting", "asks", "await", "before", "cancel", "confirm", "confirmer", "delete", "destructive", "esc", "false", "for", "host", "layout", "mount", "mounted", "needs", "newer", "next", "once", "project", "remove", "resolves", "root", "title", "toaster", "true"], registryDependencies: ["button", "dialog"] }, { name: "context-menu", type: "registry:ui", title: "Context Menu", description: "Right-click menu for an area, opening at the pointer and flipping at screen edges. Shares Dropdown Menu's items, checkbox and radio items, keyboard and typeahead; Menu key or Shift+F10 opens it from the keyboard.", exports: ["ContextMenuContent", "ContextMenuItem", "ContextMenuCheckboxItem", "ContextMenuRadioGroup", "ContextMenuRadioItem", "ContextMenuLabel", "ContextMenuSeparator", "ContextMenu", "ContextMenuTrigger"], keywords: ["actions", "align", "aligned", "and", "area", "but", "called", "center", "check", "checked", "children", "click", "clickable", "closes", "contextmenucontent", "contextmenutrigger", "destructive", "disabled", "dropdownmenuradioitem", "edge", "enter", "equals", "f10", "first", "focus", "focusable", "for", "group", "groups", "hint", "icon", "irreversible", "item", "items", "its", "key", "keyboard", "label", "labels", "leading", "lines", "mark", "menu", "new", "not", "oncheckedchange", "onopenchange", "onselect", "onvaluechange", "opens", "optionally", "picked", "pointer", "press", "red", "right", "selectable", "separators", "shift", "shortcut", "shown", "shows", "state", "the", "then", "trigger", "value", "variant", "when", "with"], registryDependencies: ["dropdown-menu", "utils"] }, { name: "count-to", type: "registry:ui", title: "CountTo", description: "Springs a number up from a start value once it scrolls into view. Prefix, suffix, decimals and locale.", exports: ["CountTo"], keywords: ["after", "and", "bcp", "before", "decimal", "decimals", "digits", "dom", "final", "for", "from", "grouping", "into", "locale", "number", "once", "point", "prefix", "scrolls", "seo", "springs", "starting", "stays", "suffix", "text", "the", "value", "view"], registryDependencies: ["utils"] }, { name: "custom-cursor", type: "registry:ui", title: "Custom Cursor", description: "Optional spring cursor that expands over interactive elements. Off on touch and reduced motion.", exports: ["CustomCursor"], keywords: ["coarse", "cursor", "dot", "elements", "expands", "interactive", "motion", "mount", "nothing", "once", "opt", "over", "pointers", "reduced", "renders", "ring", "spring", "touch", "trailing", "with"], registryDependencies: ["platform", "utils"] }, { name: "data-table", type: "registry:ui", title: "Data Table", description: "Table with columns as data: search across every column, sortable headers, checkbox selection that survives paging, a ⋯ menu per row, bulk actions in the toolbar and pagination. Built from Table, SearchInput, Checkbox, DropdownMenu, Pagination and Empty.", exports: ["DataTable"], keywords: ["accessor", "across", "actions", "all", "alternate", "and", "are", "backgrounds", "box", "bulk", "called", "cell", "checkbox", "client", "column", "columns", "compose", "controlled", "controls", "data", "defaultsort", "definitions", "display", "driven", "dropdownmenuitem", "each", "every", "field", "filters", "first", "for", "friends", "getrowid", "happen", "header", "ids", "items", "its", "jane", "kept", "label", "members", "menu", "name", "names", "onselectionchange", "order", "page", "pages", "pagesize", "pagination", "paging", "pair", "per", "placeholder", "readers", "receives", "render", "right", "row", "rowactions", "rowlabel", "rows", "screen", "search", "searchable", "searchplaceholder", "select", "selectable", "selected", "selection", "server", "show", "side", "sort", "sortable", "stable", "striped", "survives", "table", "that", "the", "toolbar", "with", "yourself"], registryDependencies: ["button", "checkbox", "dropdown-menu", "empty", "pagination", "search-input", "table", "use-controllable-state", "utils"] }, { name: "date-picker", type: "registry:ui", title: "Date Picker", description: "Field-styled button that opens a Calendar in a Popover, focused on the picked day. Single or range, presets such as Last 7 days, closes once the pick is complete, and submits ISO dates through a hidden input. Works inside Field.", exports: ["DatePicker"], keywords: ["accessible", "and", "are", "aria", "bcp", "beside", "blocks", "both", "button", "calendar", "called", "client", "closes", "controlled", "date", "day", "days", "default", "defaultvalue", "describedby", "disabled", "earliest", "end", "ends", "error", "extra", "field", "fill", "fills", "first", "focused", "for", "form", "from", "function", "grid", "hidden", "hint", "htmlfor", "ids", "input", "invalid", "isdisabled", "iso", "label", "last", "latest", "let", "locale", "max", "min", "mode", "monday", "name", "needs", "nothing", "null", "off", "once", "one", "only", "onvaluechange", "opening", "opens", "optional", "pair", "pick", "picked", "placeholder", "popover", "presets", "range", "reach", "readers", "ready", "render", "required", "rule", "saturday", "selectable", "set", "sets", "shortcuts", "single", "start", "styled", "styling", "submit", "submits", "sunday", "text", "that", "the", "then", "there", "these", "through", "today", "uncontrolled", "value", "visible", "weekends", "weekstartson", "when", "with"], registryDependencies: ["calendar", "field", "input", "popover", "use-controllable-state", "utils"] }, { name: "dialog", type: "registry:ui", title: "Dialog", description: "Portal modal with blurred backdrop, spring scale-in, focus trap, Esc and scroll lock.", exports: ["useDialog", "Dialog", "DialogTrigger", "DialogClose", "useModal", "DialogContent", "DialogHeader", "DialogFooter", "DialogTitle", "DialogDescription"], keywords: ["action", "alert", "alertdialog", "also", "and", "aria", "armed", "attach", "backdrop", "before", "behavior", "blur", "blurred", "body", "button", "called", "check", "children", "click", "close", "confirm", "confirmlabel", "confirmtimeout", "controlled", "corner", "default", "defaultopen", "defaults", "destructive", "dialog", "dialogclose", "dialogcontent", "dialogs", "dialogtrigger", "disables", "does", "done", "drawer", "emphasis", "esc", "feedback", "field", "fill", "first", "focus", "footer", "for", "give", "header", "height", "icon", "irreversible", "its", "label", "left", "loading", "loadinglabel", "lock", "modal", "mode", "never", "not", "off", "old", "onclick", "one", "ones", "only", "onopenchange", "open", "out", "padding", "page", "pair", "panel", "per", "place", "portal", "primary", "ref", "render", "reserved", "resets", "resize", "restore", "returned", "role", "root", "runs", "scale", "scroll", "second", "shared", "showclose", "shown", "shows", "size", "slides", "spinner", "spins", "spring", "square", "state", "status", "step", "success", "successlabel", "swap", "swaps", "tab", "text", "the", "this", "topmost", "trap", "trigger", "two", "uncontrolled", "variant", "view", "where", "while", "width", "with"], registryDependencies: ["button", "platform", "use-controllable-state", "utils"] }, { name: "drawer", type: "registry:ui", title: "Drawer", description: "Edge panel (left, right, top, bottom) that springs in and swipes away. Shares Dialog's focus trap, scroll lock and Esc handling. Sidebar uses it below md.", exports: ["Drawer", "DrawerTrigger", "DrawerClose", "DrawerTitle", "DrawerDescription", "DrawerContent", "DrawerHeader", "DrawerBody", "DrawerFooter"], keywords: ["action", "also", "and", "anywhere", "aria", "armed", "backdrop", "before", "blur", "body", "bottom", "button", "called", "can", "check", "children", "click", "close", "confirm", "confirmlabel", "confirmtimeout", "content", "controlled", "corner", "default", "defaultopen", "defaults", "destructive", "dialogclose", "dialogcontent", "dialogtrigger", "disables", "dismiss", "done", "drag", "drawer", "drawers", "edge", "emphasis", "esc", "feedback", "fill", "first", "footer", "for", "from", "give", "handle", "header", "height", "icon", "irreversible", "its", "label", "left", "loading", "loadinglabel", "never", "old", "onclick", "one", "ones", "onopenchange", "open", "out", "padding", "pair", "panel", "per", "place", "primary", "render", "reserved", "resets", "resize", "runs", "scroll", "scrolls", "second", "showclose", "shown", "shows", "side", "size", "slides", "spinner", "spins", "spring", "square", "state", "status", "step", "success", "successlabel", "swap", "swaps", "swipe", "taller", "text", "than", "that", "the", "their", "this", "top", "toward", "trigger", "two", "uncontrolled", "variant", "view", "when", "where", "while", "width", "with"], registryDependencies: ["dialog", "platform", "utils"] }, { name: "dropdown-menu", type: "registry:ui", title: "Dropdown Menu", description: "Action menu anchored to a Button: items with icons and shortcuts, checkbox and radio items, labels, separators, destructive variant. Arrow keys, typeahead, focus follows the pointer, Esc returns focus.", exports: ["MenuContext", "DropdownMenu", "DropdownMenuTrigger", "DropdownMenuContent", "DropdownMenuItem", "DropdownMenuCheckboxItem", "DropdownMenuRadioGroup", "DropdownMenuRadioItem", "DropdownMenuLabel", "DropdownMenuSeparator"], keywords: ["action", "actions", "align", "aligned", "also", "anchored", "and", "aria", "armed", "before", "blur", "but", "button", "called", "check", "checked", "children", "choice", "click", "closes", "closing", "confirm", "confirmlabel", "confirmtimeout", "content", "controlled", "default", "defaultopen", "defaults", "destructive", "disabled", "disables", "done", "down", "dropdownmenuradioitem", "edge", "emphasis", "enter", "equals", "esc", "feedback", "fill", "first", "for", "give", "group", "groups", "height", "hint", "icon", "irreversible", "item", "items", "its", "keeps", "keyboard", "label", "labels", "last", "leading", "left", "lines", "loading", "loadinglabel", "mark", "menu", "never", "new", "not", "old", "oncheckedchange", "onclick", "one", "ones", "onopenchange", "onselect", "onvaluechange", "open", "opens", "optionally", "out", "outside", "padding", "pair", "per", "picked", "picking", "place", "primary", "radiogroup", "red", "render", "reserved", "resets", "resize", "right", "runs", "second", "selectable", "selection", "separators", "shortcut", "shown", "shows", "size", "slides", "space", "spinner", "spins", "square", "state", "status", "step", "success", "successlabel", "swap", "swaps", "text", "the", "then", "this", "toggles", "trigger", "two", "uncontrolled", "value", "variant", "view", "when", "where", "while", "width", "with", "without"], registryDependencies: ["button", "list", "platform", "use-anchor", "use-controllable-state", "use-dismiss", "utils"] }, { name: "empty", type: "registry:ui", title: "Empty", description: "Server-safe empty state: icon tile, title, one helpful sentence and the next action. Bordered variant for empty lists and drop zones.", exports: ["Empty"], keywords: ["about", "action", "and", "app", "area", "bordered", "children", "clips", "dashed", "description", "drop", "empty", "for", "helpful", "icon", "inside", "like", "list", "next", "one", "outline", "page", "rounded", "sentence", "shown", "state", "the", "tile", "title", "what", "yet", "zone"], registryDependencies: ["utils"] }, { name: "field", type: "registry:ui", title: "Field", description: "Label, control, description and error as one unit. Controls inside (Input, Textarea, Select, Checkbox, Switch, RadioGroup) get id, aria-describedby, aria-invalid and required automatically; errors spring in and are announced.", exports: ["useFieldControl", "Field"], keywords: ["and", "aria", "automatically", "beside", "checkbox", "children", "control", "controls", "describedby", "description", "dims", "disabled", "disables", "enclosing", "error", "explicit", "field", "fills", "first", "for", "form", "from", "hint", "horizontal", "ids", "invalid", "its", "label", "makes", "marks", "match", "must", "one", "orientation", "out", "outside", "own", "picks", "props", "renders", "required", "returns", "shown", "springs", "state", "switch", "the", "unchanged", "under", "unit", "use", "when", "win", "wired", "with"], registryDependencies: ["label", "utils"] }, { name: "highlight", type: "registry:lib", description: "Lines of colored tokens, with Shiki loaded on demand. Runs anywhere: Code calls it in the\nbrowser, and a Server Component or a build can call it ahead of time and pass the result as\n`tokens`, so the browser never loads Shiki. Null for unknown languages.", exports: ["highlightCode", "languageOf"], keywords: ["ahead", "and", "anywhere", "back", "browser", "build", "call", "calls", "can", "code", "colored", "component", "demand", "extension", "falls", "file", "for", "from", "language", "languages", "lines", "loaded", "loads", "name", "never", "null", "pass", "result", "runs", "server", "shiki", "the", "time", "tokens", "tsx", "unknown", "with"], registryDependencies: [] }, { name: "hover-card", type: "registry:ui", title: "Hover Card", description: "Rich preview (profile, product, clip) that opens after a hover delay or keyboard focus. The pointer can move into the card; supplementary only.", exports: ["HoverCard"], keywords: ["acts", "align", "alignment", "avatar", "before", "bio", "can", "card", "children", "clip", "closedelay", "closes", "content", "first", "flips", "focus", "hover", "into", "keyboard", "leaves", "link", "move", "must", "once", "only", "opendelay", "opens", "pointer", "preferred", "preview", "product", "profile", "rich", "room", "second", "side", "stats", "supplementary", "tap", "the", "there", "time", "touch", "trigger", "usually", "when", "with", "without", "work"], registryDependencies: ["platform", "use-anchor", "use-dismiss", "use-hover-open", "utils"] }, { name: "input", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", exports: ["fieldClass", "Input"], keywords: ["and", "aria", "chrome", "field", "filled", "group", "handed", "inputgroup", "inside", "the", "works"], registryDependencies: ["field", "utils"] }, { name: "input-group", type: "registry:ui", title: "Input Group", description: "Server-safe wrapper that fuses an Input with icons, text (https://, USD) or buttons into one field with a shared border and focus ring. Clicking an addon focuses the input.", exports: ["InputGroup", "InputGroupAddon"], keywords: ["addons", "align", "and", "beside", "border", "button", "buttons", "clicking", "control", "data", "draws", "drops", "field", "focus", "focuses", "group", "https", "icon", "icons", "inner", "input", "its", "like", "made", "matched", "one", "own", "plain", "plus", "ring", "side", "sits", "slot", "text", "the"], registryDependencies: ["utils"] }, { name: "input-otp", type: "registry:ui", title: "Input OTP", description: "One-time code in separate slots, backed by one real input: paste and SMS autofill work, screen readers hear one field. Grouped slots, numeric or alphanumeric, characters spring in, onComplete to submit. verified turns each character into a check, then merges them into one mark.", exports: ["InputOTP"], keywords: ["accepted", "again", "allowed", "alphanumeric", "and", "announced", "are", "autocomplete", "autofill", "back", "beside", "between", "called", "change", "character", "characters", "charset", "check", "code", "controlled", "counts", "dash", "defaultvalue", "draws", "dropped", "each", "edit", "every", "false", "filled", "fires", "first", "for", "group", "groups", "input", "into", "label", "laid", "length", "mark", "merge", "merged", "number", "once", "oncomplete", "one", "only", "onvaluechange", "others", "over", "pair", "paste", "pasted", "per", "read", "readers", "real", "render", "screen", "set", "shown", "single", "sits", "slides", "slot", "slots", "sms", "submit", "the", "then", "time", "transparently", "turns", "typed", "uncontrolled", "uppercased", "value", "verified", "verifiedlabel", "via", "visual", "was", "with"], registryDependencies: ["field", "use-controllable-state", "utils"] }, { name: "kbd", type: "registry:ui", title: "Kbd", description: "Server-safe key caps for shortcut hints. KbdGroup nests keys per the HTML spec.", exports: ["Kbd", "KbdGroup"], keywords: ["cap", "html", "kbd", "kbdgroup", "key", "keys", "made", "nested", "one", "per", "several", "shortcut", "spec", "the"], registryDependencies: ["utils"] }, { name: "label", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", exports: ["Label"], keywords: [], registryDependencies: ["utils"] }, { name: "list", type: "registry:lib", description: "The item a navigation key moves to in a list, from the item at `index` (-1: none active).\nUndefined when the key doesn't navigate.", exports: ["listKeyTarget", "scrollIntoList"], keywords: ["active", "box", "can", "doesn", "enough", "from", "index", "item", "just", "key", "list", "moves", "moving", "navigate", "navigation", "none", "page", "scrollintoview", "scrolls", "show", "the", "undefined", "way", "when", "without"], registryDependencies: ["utils"] }, { name: "marker", type: "registry:ui", title: "Marker", description: "Highlighter-pen <mark> in four tints that wraps cleanly across lines and can draw itself on, left to right, when it scrolls into view. Highlight marks every match of a search in a string, for transcripts and search results.", exports: ["Marker", "Highlight"], keywords: ["across", "before", "case", "cleanly", "color", "delay", "draw", "drawing", "draws", "every", "first", "full", "fully", "highlight", "highlighter", "insensitive", "inside", "instant", "into", "left", "lines", "mark", "markers", "marks", "match", "motion", "one", "paragraph", "pen", "query", "reduced", "right", "scrolls", "search", "seconds", "several", "shown", "stagger", "sweeps", "term", "terms", "text", "the", "time", "view", "with", "without", "wraps"], registryDependencies: ["utils"] }, { name: "menubar", type: "registry:ui", title: "Menubar", description: "Desktop-style menu bar on the WAI-ARIA menubar pattern: one menu open at a time, hovering the next title while one is open switches to it, ← → walk the bar with or without a menu open, one Tab stop. A single highlight springs between titles; menus reuse every DropdownMenu item.", exports: ["MenubarContent", "MenubarItem", "MenubarCheckboxItem", "MenubarRadioGroup", "MenubarRadioItem", "MenubarLabel", "MenubarSeparator", "Menubar", "MenubarMenu", "MenubarTrigger"], keywords: ["actions", "align", "aligned", "analytics", "and", "another", "apart", "bar", "between", "but", "called", "check", "checked", "children", "click", "closes", "desktop", "destructive", "disabled", "down", "dropdownmenuradioitem", "edge", "enter", "equals", "first", "for", "generated", "give", "group", "groups", "highlight", "hint", "hovering", "icon", "irreversible", "item", "items", "its", "label", "labels", "last", "leading", "left", "lines", "mark", "menu", "menubarcontent", "menubartrigger", "menus", "move", "new", "not", "omitted", "oncheckedchange", "one", "onselect", "onvaluechange", "open", "opens", "optional", "optionally", "picked", "red", "right", "selectable", "separators", "shortcut", "shown", "shows", "space", "springs", "stable", "state", "style", "switches", "tell", "tests", "the", "then", "time", "title", "titles", "toggles", "trigger", "value", "variant", "when", "while", "with"], registryDependencies: ["dropdown-menu", "list", "use-indicator", "utils"] }, { name: "message-bubble", type: "registry:ui", title: "Message Bubble", description: "User/assistant chat bubbles with avatar slot, streaming dots and copyable code blocks.", exports: ["MessageBubble"], keywords: ["above", "accent", "after", "and", "animated", "are", "assistant", "attachments", "avatar", "bubble", "chat", "children", "code", "content", "dots", "from", "inline", "left", "lists", "message", "name", "next", "rich", "right", "sender", "shown", "shows", "side", "size", "slot", "streaming", "surface", "text", "the", "time", "typing", "user"], registryDependencies: ["utils"] }, { name: "meter", type: "registry:ui", title: "Meter", description: "Server-safe gauge for a value in a known range (storage, quota, server load, password strength). Colors by <meter> thresholds, or stacked segments with a legend, like a storage bar.", exports: ["Meter"], keywords: ["above", "accessible", "also", "and", "away", "bar", "below", "between", "bound", "color", "colors", "decides", "defaults", "disk", "follow", "good", "green", "high", "known", "label", "legend", "like", "load", "low", "lower", "max", "measurement", "meter", "min", "name", "not", "one", "optimum", "orange", "overrides", "parts", "password", "progress", "quota", "range", "red", "region", "right", "segments", "server", "stacked", "storage", "strength", "sum", "task", "text", "the", "their", "this", "threshold", "thresholds", "total", "two", "upper", "use", "used", "value", "valuetext", "visible", "where", "with", "within"], registryDependencies: ["utils"] }, { name: "motion-provider", type: "registry:component", exports: ["MotionProvider"], keywords: [], registryDependencies: ["utils"] }, { name: "native-select", type: "registry:ui", title: "Native Select", description: "A real <select> with option groups: native picker on phones, no-JS forms, native required validation. Where customizable select is supported (appearance: base-select), the open list gets the menu look.", exports: ["NativeSelect", "NativeSelectGroup", "NativeSelectOption"], keywords: ["appearance", "base", "browser", "custom", "customizable", "elsewhere", "forms", "gets", "group", "heading", "icons", "instead", "invalid", "label", "labeled", "list", "look", "menu", "native", "need", "open", "optgroup", "options", "phones", "picker", "real", "rendering", "required", "select", "stays", "supports", "the", "use", "user", "validation", "when", "where", "without", "works"], registryDependencies: ["field", "input", "utils"] }, { name: "navigation-menu", type: "registry:ui", title: "Navigation Menu", description: "Website top navigation on the APG disclosure pattern: plain links and panels of rich links. Hover opens after a beat, then sliding along the bar switches panels at once; every panel shares one viewport that springs to the next panel's size and position while the content slides in from the side you moved toward. Click, Enter, ↓, ← → and Esc work; closed panels keep their links in the HTML for crawlers.", exports: ["NavigationMenu", "NavigationMenuList", "NavigationMenuItem", "NavigationMenuTrigger", "NavigationMenuContent", "NavigationMenuLink"], keywords: ["active", "after", "all", "along", "and", "aria", "bar", "beat", "bold", "called", "children", "classname", "click", "closed", "closes", "content", "controlled", "crawlers", "current", "defaultvalue", "description", "down", "enter", "entry", "esc", "find", "first", "focuses", "following", "for", "generated", "hidden", "hover", "html", "icon", "inset", "into", "item", "items", "its", "left", "level", "line", "link", "links", "look", "matched", "menu", "mouse", "muted", "navigation", "omitted", "once", "one", "onvaluechange", "open", "opened", "opens", "optional", "own", "padding", "page", "pair", "panel", "panels", "pill", "plain", "readers", "rem", "render", "rich", "right", "row", "selected", "single", "size", "sliding", "space", "springs", "stable", "stay", "still", "switches", "the", "them", "then", "tile", "title", "toggle", "top", "trigger", "uncontrolled", "under", "value", "viewport", "walk", "website", "while", "with", "your"], registryDependencies: ["use-controllable-state", "use-dismiss", "use-indicator", "utils"] }, { name: "newsletter", type: "registry:block", title: "Newsletter", description: "Email signup: InputGroup with the submit Button inside, native email validation, a loading → success toast and an in-place success state on the button.", exports: ["Newsletter"], keywords: ["above", "address", "after", "and", "button", "check", "confirm", "confirms", "description", "email", "field", "for", "form", "heading", "inbox", "inputgroup", "inside", "into", "its", "line", "loading", "message", "native", "next", "onsubscribe", "place", "placeholder", "rejection", "resets", "resolves", "saved", "shows", "signup", "submit", "submitlabel", "success", "successmessage", "supporting", "text", "that", "the", "then", "title", "toast", "turns", "under", "validation", "when", "with", "your"], registryDependencies: ["button", "input", "input-group", "toast", "utils"] }, { name: "number-input", type: "registry:ui", title: "Number Input", description: "Native number field with −/+ buttons: arrow keys, min/max/step validation and form submit from the platform; clamps on blur, rounds to the step's precision.", exports: ["NumberInput"], keywords: ["amount", "and", "are", "arrows", "arrowup", "buttons", "called", "cart", "change", "controlled", "defaultvalue", "down", "field", "first", "form", "helpers", "highest", "keyboard", "lowest", "max", "min", "native", "new", "number", "onvaluechange", "pair", "pointer", "quantity", "render", "step", "submit", "tabindex", "the", "uncontrolled", "use", "users", "validation", "value", "with"], registryDependencies: ["input", "input-group", "use-controllable-state", "utils"] }, { name: "overflow-focus", type: "registry:lib", description: `Drop inside a scroll container: while the container overflows it becomes keyboard-focusable
|
|
17593
|
+
index: [{ name: "accent-picker", type: "registry:ui", title: "Accent Picker", description: "Accent color row: eight swatches (red to slate) that retint primary, focus rings, selection, switches and checkboxes app-wide. Persisted, applied before paint, or set your own hue with --accent-hue.", exports: ["AccentPicker"], keywords: ["accent", "app", "color", "radio", "retint", "row", "setaccent", "swatches", "that", "the", "via", "whole"], registryDependencies: ["theme", "utils"] }, { name: "accordion", type: "registry:ui", title: "Accordion", description: "Single or multiple panels with spring height animation. Closed content stays in the DOM for SEO.", exports: ["Accordion", "AccordionItem", "AccordionTrigger", "AccordionContent"], keywords: ["accordion", "accordioncontent", "accordionitem", "accordiontrigger", "allow", "and", "called", "children", "closed", "closes", "closing", "collapsible", "content", "controlled", "crawlable", "default", "defaultvalue", "every", "first", "heading", "height", "html", "independent", "inert", "item", "items", "level", "match", "matched", "mounted", "multiple", "none", "one", "only", "onvaluechange", "open", "outline", "page", "panel", "panels", "question", "remains", "render", "single", "stays", "text", "the", "time", "type", "uncontrolled", "unique", "value", "when", "while", "with", "within", "wrapping"], registryDependencies: ["use-controllable-state", "utils"] }, { name: "alert", type: "registry:ui", title: "Alert", description: "Server-safe inline banner: tinted surface, colored icon, title, description and an optional action. Info, success, warning and destructive tones follow the accent and status tokens.", exports: ["Alert", "AlertTitle", "AlertDescription"], keywords: ["action", "add", "alert", "aligned", "and", "announced", "appears", "banner", "button", "colored", "default", "description", "did", "for", "icon", "inline", "live", "load", "none", "not", "null", "pass", "region", "replaces", "response", "right", "role", "sets", "should", "slot", "small", "something", "static", "surface", "the", "tint", "tinted", "title", "tone", "user", "variant", "when"], registryDependencies: ["utils"] }, { name: "app-window", type: "registry:ui", title: "AppWindow", description: "Window container with close / minimize / zoom controls, title, custom header slot and actions. SegmentedTabs is the compact view switcher for its title bar.", exports: ["AppWindow"], keywords: ["actions", "aligned", "area", "bar", "blurred", "bodyclassname", "button", "called", "centered", "classes", "content", "control", "custom", "dot", "for", "glass", "handler", "header", "into", "matching", "maximize", "minimize", "navbar", "onclose", "onmaximize", "onminimize", "passing", "pressed", "replaces", "right", "search", "shows", "slot", "surface", "tabs", "text", "the", "title", "titlebar", "translucent", "turns", "under", "when"], registryDependencies: ["utils"] }, { name: "aspect-ratio", type: "registry:ui", title: "Aspect Ratio", description: "Server-safe box locked to a ratio (16:9 thumbnails, 9:16 clips). A direct image or video child fills it with object-cover.", exports: ["AspectRatio"], keywords: ["box", "child", "cover", "direct", "fills", "height", "img", "locked", "object", "ratio", "video", "width"], registryDependencies: ["utils"] }, { name: "attachment", type: "registry:ui", title: "Attachment & Dropzone", description: "File chips with image thumbnails or type icons, size, a springing upload bar, error state and remove; the list springs chips in and closes the gap. FileDropzone wraps a real file input: click, keyboard and the native picker work, drop and paste are checked against accept and maxSize, and dropped files land in the input so forms and Server Actions receive them.", exports: ["formatBytes", "Attachment", "AttachmentList", "FileDropzone"], keywords: ["accept", "actions", "along", "and", "are", "area", "aria", "around", "attachments", "attribute", "back", "bar", "bcp", "blocks", "both", "bottom", "browse", "button", "bytes", "called", "child", "children", "chip", "click", "content", "createobjecturl", "default", "describedby", "disabled", "download", "drop", "dropped", "drops", "each", "edge", "enforced", "enter", "error", "failed", "field", "file", "files", "fills", "for", "form", "forms", "fresh", "from", "get", "given", "hint", "href", "icon", "ids", "image", "input", "into", "invalid", "key", "least", "line", "link", "locale", "makes", "maxsize", "mime", "more", "multiple", "name", "native", "needs", "one", "onfiles", "onreject", "onremove", "out", "passed", "paste", "pdf", "picked", "picker", "picks", "place", "plain", "pressed", "progress", "real", "reason", "red", "remove", "removed", "replaces", "required", "same", "server", "sets", "show", "shows", "size", "source", "space", "spring", "springs", "src", "styling", "submit", "submitted", "syntax", "text", "than", "that", "the", "them", "thumbnail", "too", "turns", "type", "types", "upload", "uploading", "url", "when", "while", "with", "work", "written"], registryDependencies: ["field", "utils"] }, { name: "avatar", type: "registry:ui", title: "Avatar", description: "Image with initials fallback on error, three sizes and online/away/busy/offline dots.", exports: ["Avatar"], keywords: ["alt", "and", "announced", "corner", "diameter", "dot", "fails", "fallback", "for", "icon", "image", "initials", "loads", "name", "overrides", "person", "presence", "show", "size", "src", "status", "text", "the", "url", "used", "while", "with"], registryDependencies: ["utils"] }, { name: "avatar-group", type: "registry:ui", title: "AvatarGroup", description: "Overlapping avatars with a +N overflow chip. Hover gently enlarges the avatar on a spring, keeping stack order.", exports: ["AvatarGroup"], keywords: ["alt", "and", "avatar", "avatars", "before", "chip", "each", "enlarges", "every", "gently", "hover", "how", "many", "max", "name", "one", "order", "overflow", "overlapping", "people", "show", "size", "spring", "stack", "staying", "the", "users", "with"], registryDependencies: ["avatar", "utils"] }, { name: "badge", type: "registry:ui", title: "Badge & Pill", description: "Minimal status tags. Rounded badge or fully rounded pill, with optional dot. pulse adds an expanding live-status ring.", exports: ["Badge"], keywords: ["and", "color", "current", "dot", "emits", "expanding", "forever", "fully", "leading", "like", "live", "needs", "pill", "pulse", "ring", "rounded", "shape", "status", "surface", "tag", "text", "the", "variant"], registryDependencies: ["utils"] }, { name: "breadcrumb", type: "registry:ui", title: "Breadcrumb", description: "Navigation trail from a list of items: links up to the current page, which is marked for assistive tech. Server-safe, custom separator.", exports: ["Breadcrumb"], keywords: ["between", "chevron", "current", "default", "drawn", "ending", "first", "item", "items", "last", "links", "list", "outermost", "page", "safe", "separator", "server", "site", "sits", "the", "trail", "where"], registryDependencies: [] }, { name: "button", type: "registry:ui", title: "Button", description: "Five variants, four sizes, spring press feedback, an animated loading spinner and in-place success state, a two-step confirm that swaps the label on first click, and a configurable swap direction. Width stays fixed to the longest label.", exports: ["Button"], keywords: ["action", "also", "and", "aria", "armed", "before", "blur", "both", "button", "can", "check", "children", "click", "clicks", "confirm", "confirmlabel", "confirmtimeout", "default", "defaults", "destructive", "done", "emphasis", "enter", "esc", "feedback", "fill", "first", "focusable", "for", "form", "give", "height", "icon", "ignore", "ignores", "irreversible", "its", "label", "left", "loading", "loadinglabel", "never", "old", "onclick", "one", "ones", "out", "padding", "per", "place", "primary", "reserved", "resets", "resize", "runs", "second", "shown", "shows", "size", "slides", "spinner", "spins", "square", "status", "stays", "step", "submit", "success", "successlabel", "swap", "swaps", "text", "the", "this", "twice", "two", "variant", "view", "where", "while", "width", "with"], registryDependencies: ["button-variants", "utils"] }, { name: "button-group", type: "registry:ui", title: "Button Group", description: "Joins buttons into one strip that shares borders, corners and active states. Horizontal or vertical.", exports: ["ButtonGroup"], keywords: ["and", "attached", "borders", "buttons", "connected", "control", "corners", "default", "fuse", "groups", "horizontal", "into", "orientation", "related", "single", "strip", "together", "toolbar", "true", "vertical"], registryDependencies: ["utils"] }, { name: "button-variants", type: "registry:ui", description: 'Button classes for non-button elements, e.g. `<a className={buttonVariants({ variant: "outline" })}>`.', exports: ["buttonVariants"], keywords: ["button", "buttonvariants", "classes", "classname", "elements", "for", "non", "outline", "variant"], registryDependencies: ["utils"] }, { name: "calendar", type: "registry:ui", title: "Calendar", description: "Month grid on the WAI-ARIA date-grid pattern: single date or range with a hover preview, min/max and custom disabled days, months that slide in from the direction of travel. Arrows, Home/End and PageUp/PageDown (+Shift for years). Hydration-safe: today is only marked in the browser.", exports: ["key", "Calendar"], keywords: ["and", "another", "any", "aria", "arrows", "autofocus", "bcp", "called", "client", "controlled", "current", "custom", "date", "datepicker", "dates", "day", "days", "default", "defaultmonth", "defaultvalue", "direction", "disabled", "displayed", "earliest", "end", "ends", "extra", "first", "focus", "for", "from", "function", "grid", "home", "hover", "into", "isdisabled", "iso", "keyboard", "latest", "like", "local", "locale", "max", "min", "mode", "monday", "month", "months", "mount", "names", "nothing", "null", "off", "one", "only", "onmonthchange", "onvaluechange", "pagedown", "pages", "pageup", "pattern", "pick", "picked", "preview", "range", "render", "rule", "saturday", "selectable", "sets", "shift", "shown", "single", "slide", "sorts", "start", "sunday", "the", "then", "this", "travel", "uncontrolled", "user", "value", "wai", "week", "weekday", "weekends", "weekstartson", "when", "with", "year", "yyyy"], registryDependencies: ["button", "use-controllable-state", "utils"] }, { name: "card", type: "registry:ui", title: "Card", description: "Composable surface: header, title, description, content and footer. Optional glass.", exports: ["Card", "CardHeader", "CardTitle", "CardDescription", "CardContent", "CardFooter"], keywords: ["backgrounds", "colored", "default", "directly", "fits", "follows", "glass", "heading", "level", "not", "outline", "over", "page", "place", "subtle", "textured", "that", "the", "translucency", "under", "when"], registryDependencies: ["utils"] }, { name: "carousel", type: "registry:ui", title: "Carousel", description: "Native scroll-snap row, so touch, trackpad and no-JS scrolling work, with spring-driven arrows, page dots, arrow keys and mouse drag that throws to the next snap point. APG carousel semantics: a labeled region of slides announced as 3 of 8.", exports: ["Carousel", "CarouselContent", "CarouselItem", "CarouselPrevious", "CarouselNext", "CarouselDots"], keywords: ["accessible", "all", "and", "announced", "any", "apg", "are", "arrow", "basis", "buttons", "carousel", "css", "default", "dot", "dots", "drag", "driven", "edge", "end", "fewer", "from", "gap", "hides", "keys", "label", "labeled", "left", "mouse", "name", "native", "not", "one", "overlaid", "page", "pages", "per", "projects", "region", "rem", "right", "row", "scroll", "scrolling", "semantics", "sized", "slide", "slides", "snap", "spring", "start", "the", "there", "total", "touch", "trackpad", "two", "variables", "view", "with", "work"], registryDependencies: ["button", "utils"] }, { name: "changelog", type: "registry:block", title: "Changelog", description: "Release notes on a Timeline: newest release highlighted, changes typed New / Improved / Fixed with Badges, and a Kbd shortcut to try the headline feature. Server-safe.", exports: ["Changelog"], keywords: ["are", "badges", "changes", "first", "highlighted", "how", "kbd", "newest", "notes", "one", "release", "releases", "shows", "the", "timeline", "try", "typed", "with"], registryDependencies: ["badge", "kbd", "timeline", "utils"] }, { name: "chart", type: "registry:ui", title: "Chart", description: "Line, area and bar (grouped or stacked) charts in SVG with a colorblind-validated eight-color palette that keeps each series' color. Crosshair tooltip listing every series, arrow-key readout, legend that hides series, direct end labels, a table view, and spring transitions between data.", exports: ["Chart"], keywords: ["add", "and", "are", "area", "areas", "arrow", "assistive", "axis", "bar", "bars", "bcp", "can", "captions", "categorical", "category", "chart", "charts", "compact", "crosshair", "currency", "data", "direct", "each", "eight", "end", "every", "filled", "fold", "for", "format", "grouping", "height", "hides", "index", "instead", "into", "key", "keys", "label", "labels", "legend", "line", "lines", "listing", "locale", "mark", "most", "names", "notation", "numbers", "object", "one", "only", "other", "pages", "palette", "past", "per", "plot", "plus", "props", "render", "row", "serializable", "series", "server", "split", "stack", "stacked", "style", "svg", "table", "tail", "tech", "that", "the", "them", "ticks", "too", "tooltip", "type", "usd", "value", "view", "with"], registryDependencies: ["button", "table", "use-dismiss", "utils"] }, { name: "checkbox", type: "registry:ui", title: "Checkbox", description: "Native checkbox with a spring-drawn check mark and an indeterminate state for select-all rows. Controlled or uncontrolled; submits with forms.", exports: ["Checkbox"], keywords: ["all", "called", "check", "checkbox", "checked", "controlled", "dash", "defaultchecked", "drawn", "field", "first", "indeterminate", "inside", "mixed", "native", "new", "oncheckedchange", "pair", "render", "rows", "select", "selected", "shows", "some", "spring", "state", "the", "uncontrolled", "with", "works"], registryDependencies: ["field", "use-controllable-state", "utils"] }, { name: "clip-card", type: "registry:block", title: "Clip Card", description: "Video tile: AspectRatio thumbnail (16:9 or 9:16), duration Badge, watched Progress along the bottom, and a ContextMenu of actions.", exports: ["ClipCard"], keywords: ["accessible", "actions", "ago", "along", "also", "and", "aspectratio", "badge", "bar", "bottom", "card", "click", "clip", "clips", "context", "contextmenuitem", "days", "duration", "f10", "film", "for", "glyph", "height", "how", "img", "items", "menu", "meta", "much", "name", "one", "opens", "print", "progress", "ratio", "right", "shift", "small", "stands", "the", "them", "thin", "thumbnail", "tile", "title", "under", "vertical", "video", "views", "was", "watched", "width", "without"], registryDependencies: ["aspect-ratio", "badge", "context-menu", "progress", "utils"] }, { name: "code", type: "registry:ui", title: "Code", description: "Shiki-highlighted code that follows light and dark through light-dark() token colors, as a block, an inline one-liner or a tabbed file panel. Line numbers (from any start line), tinted lines with an accent bar, <mark>ed words or column ranges, and per-line notes in a tooltip that keyboard users can tab to. Optional Prettier at display time; copy hands out exactly what is shown.", exports: ["CopyButton", "Code", "CodePanel"], keywords: ["accessible", "active", "add", "all", "and", "animates", "annotations", "bar", "bash", "before", "behind", "block", "body", "build", "button", "clipboard", "code", "collapse", "copied", "copy", "dark", "default", "dom", "each", "every", "excerpts", "file", "files", "first", "focus", "fold", "for", "format", "formatted", "from", "gets", "ghost", "gutter", "header", "highlight", "highlightcode", "highlighted", "highlighting", "hover", "ignored", "inline", "inside", "into", "its", "json", "keep", "label", "language", "least", "left", "light", "line", "linenumbers", "lines", "loads", "longer", "made", "many", "mark", "marks", "name", "never", "note", "notes", "number", "numbers", "one", "options", "own", "page", "paint", "panel", "per", "place", "prettier", "primary", "ranges", "refer", "registry", "remember", "renders", "run", "scrolling", "scrolls", "server", "sets", "shared", "shiki", "show", "source", "stays", "switching", "syntax", "tab", "terminal", "text", "than", "that", "the", "themes", "this", "time", "tint", "tokens", "too", "touch", "tsx", "turns", "variant", "vertically", "wheel", "with", "words", "wrapped", "written"], registryDependencies: ["button", "highlight", "overflow-focus", "tabs", "tooltip", "utils"] }, { name: "collapsible", type: "registry:ui", title: "Collapsible", description: "A single show/hide region with a spring height animation and rotating chevron. Controlled or uncontrolled; closed content stays in the DOM for SEO.", exports: ["Collapsible", "CollapsibleTrigger", "CollapsibleContent"], keywords: ["after", "and", "called", "chevron", "children", "closed", "collapsiblecontent", "collapsibletrigger", "content", "controlled", "crawlable", "default", "defaultopen", "first", "height", "hide", "html", "inert", "label", "mounted", "one", "onopenchange", "open", "pair", "region", "remains", "render", "rotating", "show", "springs", "state", "stays", "that", "the", "toggles", "trigger", "true", "uncontrolled", "when", "while", "with"], registryDependencies: ["use-controllable-state", "utils"] }, { name: "combobox", type: "registry:ui", title: "Combobox", description: "A Select you can type into: the query filters by label and keywords (prefix matches first), arrows move, Enter picks. multiple keeps the list open and shows picks as removable tags. Submits and validates with the form.", exports: ["Combobox"], keywords: ["accessible", "and", "aria", "backspace", "blocks", "called", "can", "choices", "closes", "controlled", "defaultvalue", "describedby", "disabled", "down", "emptytext", "enter", "entry", "error", "esc", "every", "field", "fills", "filtered", "filters", "first", "form", "from", "hint", "ids", "input", "into", "invalid", "isn", "keeps", "label", "last", "list", "matches", "move", "multiple", "name", "needs", "nothing", "one", "onvaluechange", "open", "opening", "options", "pair", "per", "pick", "picked", "picks", "placeholder", "removable", "removes", "render", "required", "select", "sets", "show", "shown", "shows", "single", "stay", "styling", "submit", "submitted", "tags", "text", "the", "there", "type", "types", "typing", "uncontrolled", "user", "value", "values", "visible", "when", "with", "you"], registryDependencies: ["field", "form-value", "list", "platform", "tag-input", "use-anchor", "use-controllable-state", "use-dismiss", "utils"] }, { name: "command", type: "registry:ui", title: "Command", description: "⌘K palette: filterable groups, arrow-key highlight, Enter to run, keyword matching. Inline or as CommandDialog with a global shortcut; the first Esc clears, the second closes.", exports: ["Command", "CommandInput", "CommandList", "CommandEmpty", "CommandGroup", "CommandSeparator", "CommandItem", "CommandDialog"], keywords: ["accessible", "after", "against", "and", "arrows", "but", "called", "children", "click", "close", "closes", "collapse", "command", "commanddialog", "commandempty", "commandinput", "commanditem", "commandlist", "content", "controlled", "ctrl", "defaults", "dialog", "disable", "disabled", "driven", "elsewhere", "end", "enter", "esc", "extra", "false", "filterable", "filtering", "for", "global", "group", "groups", "heading", "hidden", "hides", "home", "icon", "inline", "input", "inside", "item", "items", "its", "itself", "keepopen", "keyboard", "keywords", "label", "list", "listbox", "mac", "match", "matched", "matches", "mod", "move", "name", "none", "not", "only", "onopenchange", "onselect", "open", "option", "outside", "own", "pair", "palette", "pass", "passes", "placeholder", "preferences", "query", "results", "scrollable", "search", "selectable", "selecting", "selection", "selects", "separators", "settings", "shortcut", "shown", "shows", "stack", "state", "stay", "terms", "text", "the", "title", "toggle", "toggled", "use", "used", "value", "visible", "visually", "when", "while", "with", "would"], registryDependencies: ["dialog", "list", "search-input", "separator", "use-controllable-state", "use-hotkey", "utils"] }, { name: "confirm", type: "registry:ui", title: "Confirm", description: "Promise-based confirm dialog called like toast: await confirm({ title, description, destructive }) resolves true or false. Built on Dialog's alert mode, Cancel takes the first focus, Esc cancels, one host mounted in the layout.", exports: ["confirm", "Confirmer"], keywords: ["acting", "asks", "await", "before", "cancel", "confirm", "confirmer", "delete", "destructive", "esc", "false", "for", "host", "layout", "mount", "mounted", "needs", "newer", "next", "once", "project", "remove", "resolves", "root", "title", "toaster", "true"], registryDependencies: ["button", "dialog"] }, { name: "context-menu", type: "registry:ui", title: "Context Menu", description: "Right-click menu for an area, opening at the pointer and flipping at screen edges. Shares Dropdown Menu's items, checkbox and radio items, keyboard and typeahead; Menu key or Shift+F10 opens it from the keyboard.", exports: ["ContextMenuContent", "ContextMenuItem", "ContextMenuCheckboxItem", "ContextMenuRadioGroup", "ContextMenuRadioItem", "ContextMenuLabel", "ContextMenuSeparator", "ContextMenu", "ContextMenuTrigger"], keywords: ["actions", "align", "aligned", "and", "area", "but", "called", "center", "check", "checked", "children", "click", "clickable", "closes", "contextmenucontent", "contextmenutrigger", "destructive", "disabled", "dropdownmenuradioitem", "edge", "enter", "equals", "f10", "first", "focus", "focusable", "for", "group", "groups", "hint", "icon", "irreversible", "item", "items", "its", "key", "keyboard", "label", "labels", "leading", "lines", "mark", "menu", "new", "not", "oncheckedchange", "onopenchange", "onselect", "onvaluechange", "opens", "optionally", "picked", "pointer", "press", "red", "right", "selectable", "separators", "shift", "shortcut", "shown", "shows", "state", "the", "then", "trigger", "value", "variant", "when", "with"], registryDependencies: ["dropdown-menu", "utils"] }, { name: "count-to", type: "registry:ui", title: "CountTo", description: "Springs a number up from a start value once it scrolls into view. Prefix, suffix, decimals and locale.", exports: ["CountTo"], keywords: ["after", "and", "bcp", "before", "decimal", "decimals", "digits", "dom", "final", "for", "from", "grouping", "into", "locale", "number", "once", "point", "prefix", "scrolls", "seo", "springs", "starting", "stays", "suffix", "text", "the", "value", "view"], registryDependencies: ["utils"] }, { name: "custom-cursor", type: "registry:ui", title: "Custom Cursor", description: "Optional spring cursor that expands over interactive elements. Off on touch and reduced motion.", exports: ["CustomCursor"], keywords: ["coarse", "cursor", "dot", "elements", "expands", "interactive", "motion", "mount", "nothing", "once", "opt", "over", "pointers", "reduced", "renders", "ring", "spring", "touch", "trailing", "with"], registryDependencies: ["platform", "utils"] }, { name: "data-table", type: "registry:ui", title: "Data Table", description: "Table with columns as data: search across every column, sortable headers, checkbox selection that survives paging, a ⋯ menu per row, bulk actions in the toolbar and pagination. Built from Table, SearchInput, Checkbox, DropdownMenu, Pagination and Empty.", exports: ["DataTable"], keywords: ["accessor", "across", "actions", "all", "alternate", "and", "are", "backgrounds", "box", "bulk", "called", "cell", "checkbox", "client", "column", "columns", "compose", "controlled", "controls", "data", "defaultsort", "definitions", "display", "driven", "dropdownmenuitem", "each", "every", "field", "filters", "first", "for", "friends", "getrowid", "happen", "header", "ids", "items", "its", "jane", "kept", "label", "members", "menu", "name", "names", "onselectionchange", "order", "page", "pages", "pagesize", "pagination", "paging", "pair", "per", "placeholder", "readers", "receives", "render", "right", "row", "rowactions", "rowlabel", "rows", "screen", "search", "searchable", "searchplaceholder", "select", "selectable", "selected", "selection", "server", "show", "side", "sort", "sortable", "stable", "striped", "survives", "table", "that", "the", "toolbar", "with", "yourself"], registryDependencies: ["button", "checkbox", "dropdown-menu", "empty", "pagination", "search-input", "table", "use-controllable-state", "utils"] }, { name: "date-picker", type: "registry:ui", title: "Date Picker", description: "Field-styled button that opens a Calendar in a Popover, focused on the picked day. Single or range, presets such as Last 7 days, closes once the pick is complete, and submits ISO dates with the form. Works inside Field.", exports: ["DatePicker"], keywords: ["accessible", "and", "are", "aria", "bcp", "beside", "blocks", "both", "button", "calendar", "called", "client", "closes", "controlled", "date", "day", "days", "default", "defaultvalue", "describedby", "disabled", "earliest", "end", "ends", "error", "extra", "field", "fill", "fills", "first", "focused", "for", "form", "from", "function", "grid", "hint", "htmlfor", "ids", "invalid", "isdisabled", "isn", "iso", "label", "last", "latest", "let", "locale", "max", "min", "mode", "monday", "name", "needs", "nothing", "null", "off", "once", "one", "only", "onvaluechange", "opening", "opens", "optional", "pair", "pick", "picked", "placeholder", "popover", "presets", "range", "reach", "readers", "ready", "render", "required", "rule", "saturday", "selectable", "set", "sets", "shortcuts", "single", "start", "styled", "styling", "submit", "submits", "submitted", "sunday", "text", "that", "the", "then", "there", "these", "today", "uncontrolled", "value", "visible", "weekends", "weekstartson", "when", "with"], registryDependencies: ["calendar", "field", "form-value", "input", "popover", "use-controllable-state", "utils"] }, { name: "dialog", type: "registry:ui", title: "Dialog", description: "Portal modal with blurred backdrop, spring scale-in, focus trap, Esc and scroll lock.", exports: ["useDialog", "Dialog", "DialogTrigger", "DialogClose", "useModal", "DialogContent", "DialogHeader", "DialogFooter", "DialogTitle", "DialogDescription"], keywords: ["action", "alert", "alertdialog", "also", "and", "aria", "armed", "attach", "backdrop", "before", "behavior", "blur", "blurred", "body", "both", "button", "called", "can", "check", "children", "click", "clicks", "close", "confirm", "confirmlabel", "confirmtimeout", "controlled", "corner", "default", "defaultopen", "defaults", "destructive", "dialog", "dialogclose", "dialogcontent", "dialogs", "dialogtrigger", "does", "done", "drawer", "emphasis", "enter", "esc", "feedback", "field", "fill", "first", "focus", "focusable", "footer", "for", "form", "give", "header", "height", "icon", "ignore", "ignores", "irreversible", "its", "label", "left", "loading", "loadinglabel", "lock", "modal", "mode", "never", "not", "off", "old", "onclick", "one", "ones", "only", "onopenchange", "open", "out", "padding", "page", "pair", "panel", "per", "place", "portal", "primary", "ref", "render", "reserved", "resets", "resize", "restore", "returned", "role", "root", "runs", "scale", "scroll", "second", "shared", "showclose", "shown", "shows", "size", "slides", "spinner", "spins", "spring", "square", "state", "status", "stays", "step", "submit", "success", "successlabel", "swap", "swaps", "tab", "text", "the", "this", "topmost", "trap", "trigger", "twice", "two", "uncontrolled", "variant", "view", "where", "while", "width", "with"], registryDependencies: ["button", "platform", "use-controllable-state", "utils"] }, { name: "drawer", type: "registry:ui", title: "Drawer", description: "Edge panel (left, right, top, bottom) that springs in and swipes away. Shares Dialog's focus trap, scroll lock and Esc handling. Sidebar uses it below md.", exports: ["Drawer", "DrawerTrigger", "DrawerClose", "DrawerTitle", "DrawerDescription", "DrawerContent", "DrawerHeader", "DrawerBody", "DrawerFooter"], keywords: ["action", "also", "and", "anywhere", "aria", "armed", "backdrop", "before", "blur", "body", "both", "bottom", "button", "called", "can", "check", "children", "click", "clicks", "close", "confirm", "confirmlabel", "confirmtimeout", "content", "controlled", "corner", "default", "defaultopen", "defaults", "destructive", "dialogclose", "dialogcontent", "dialogtrigger", "dismiss", "done", "drag", "drawer", "drawers", "edge", "emphasis", "enter", "esc", "feedback", "fill", "first", "focusable", "footer", "for", "form", "from", "give", "handle", "header", "height", "icon", "ignore", "ignores", "irreversible", "its", "label", "left", "loading", "loadinglabel", "never", "old", "onclick", "one", "ones", "onopenchange", "open", "out", "padding", "pair", "panel", "per", "place", "primary", "render", "reserved", "resets", "resize", "runs", "scroll", "scrolls", "second", "showclose", "shown", "shows", "side", "size", "slides", "spinner", "spins", "spring", "square", "state", "status", "stays", "step", "submit", "success", "successlabel", "swap", "swaps", "swipe", "taller", "text", "than", "that", "the", "their", "this", "top", "toward", "trigger", "twice", "two", "uncontrolled", "variant", "view", "when", "where", "while", "width", "with"], registryDependencies: ["dialog", "platform", "utils"] }, { name: "dropdown-menu", type: "registry:ui", title: "Dropdown Menu", description: "Action menu anchored to a Button: items with icons and shortcuts, checkbox and radio items, labels, separators, destructive variant. Arrow keys, typeahead, focus follows the pointer, Esc returns focus.", exports: ["MenuContext", "DropdownMenu", "DropdownMenuTrigger", "DropdownMenuContent", "DropdownMenuItem", "DropdownMenuCheckboxItem", "DropdownMenuRadioGroup", "DropdownMenuRadioItem", "DropdownMenuLabel", "DropdownMenuSeparator"], keywords: ["action", "actions", "align", "aligned", "also", "anchored", "and", "aria", "armed", "before", "blur", "both", "but", "button", "called", "can", "check", "checked", "children", "choice", "click", "clicks", "closes", "closing", "confirm", "confirmlabel", "confirmtimeout", "content", "controlled", "default", "defaultopen", "defaults", "destructive", "disabled", "done", "down", "dropdownmenuradioitem", "edge", "emphasis", "enter", "equals", "esc", "feedback", "fill", "first", "focusable", "for", "form", "give", "group", "groups", "height", "hint", "icon", "ignore", "ignores", "irreversible", "item", "items", "its", "keeps", "keyboard", "label", "labels", "last", "leading", "left", "lines", "loading", "loadinglabel", "mark", "menu", "never", "new", "not", "old", "oncheckedchange", "onclick", "one", "ones", "onopenchange", "onselect", "onvaluechange", "open", "opens", "optionally", "out", "outside", "padding", "pair", "per", "picked", "picking", "place", "primary", "radiogroup", "red", "render", "reserved", "resets", "resize", "right", "runs", "second", "selectable", "selection", "separators", "shortcut", "shown", "shows", "size", "slides", "space", "spinner", "spins", "square", "state", "status", "stays", "step", "submit", "success", "successlabel", "swap", "swaps", "text", "the", "then", "this", "toggles", "trigger", "twice", "two", "uncontrolled", "value", "variant", "view", "when", "where", "while", "width", "with", "without"], registryDependencies: ["button", "list", "platform", "use-anchor", "use-controllable-state", "use-dismiss", "utils"] }, { name: "empty", type: "registry:ui", title: "Empty", description: "Server-safe empty state: icon tile, title, one helpful sentence and the next action. Bordered variant for empty lists and drop zones.", exports: ["Empty"], keywords: ["about", "action", "and", "app", "area", "bordered", "children", "clips", "dashed", "description", "drop", "empty", "for", "helpful", "icon", "inside", "like", "list", "next", "one", "outline", "page", "rounded", "sentence", "shown", "state", "the", "tile", "title", "what", "yet", "zone"], registryDependencies: ["utils"] }, { name: "field", type: "registry:ui", title: "Field", description: "Label, control, description and error as one unit. Controls inside (Input, Textarea, Select, Checkbox, Switch, RadioGroup) get id, aria-describedby, aria-invalid and required automatically; errors spring in and are announced.", exports: ["useFieldControl", "Field"], keywords: ["and", "aria", "automatically", "beside", "checkbox", "children", "control", "controls", "describedby", "description", "dims", "disabled", "disables", "enclosing", "error", "explicit", "field", "fills", "first", "for", "form", "from", "hint", "horizontal", "ids", "invalid", "its", "label", "makes", "marks", "match", "must", "one", "orientation", "out", "outside", "own", "picks", "props", "renders", "required", "returns", "shown", "springs", "state", "switch", "the", "unchanged", "under", "unit", "use", "when", "win", "wired", "with"], registryDependencies: ["label", "utils"] }, { name: "form-value", type: "registry:lib", description: "Submits a custom control's value and takes part in native validation, which a hidden input\ncan't: an empty `required` value blocks submit, shows the browser's message and focuses `anchor`.", exports: ["FormValue"], keywords: ["anchor", "and", "blocks", "browser", "called", "can", "control", "custom", "disabled", "empty", "field", "finds", "focus", "focuses", "form", "gets", "hidden", "input", "invalid", "leaves", "like", "message", "name", "native", "nothing", "oninvalid", "only", "out", "part", "picked", "required", "shows", "skips", "submit", "submits", "takes", "that", "the", "validation", "value", "visible", "when", "which", "without"], registryDependencies: [] }, { name: "highlight", type: "registry:lib", description: "Lines of colored tokens, with Shiki loaded on demand. Runs anywhere: Code calls it in the\nbrowser, and a Server Component or a build can call it ahead of time and pass the result as\n`tokens`, so the browser never loads Shiki. Null for unknown languages.", exports: ["highlightCode", "languageOf"], keywords: ["ahead", "and", "anywhere", "back", "browser", "build", "call", "calls", "can", "code", "colored", "component", "demand", "extension", "falls", "file", "for", "from", "language", "languages", "lines", "loaded", "loads", "name", "never", "null", "pass", "result", "runs", "server", "shiki", "the", "time", "tokens", "tsx", "unknown", "with"], registryDependencies: [] }, { name: "hover-card", type: "registry:ui", title: "Hover Card", description: "Rich preview (profile, product, clip) that opens after a hover delay or keyboard focus. The pointer can move into the card; supplementary only.", exports: ["HoverCard"], keywords: ["acts", "align", "alignment", "avatar", "before", "bio", "can", "card", "children", "clip", "closedelay", "closes", "content", "first", "flips", "focus", "hover", "into", "keyboard", "leaves", "link", "move", "must", "once", "only", "opendelay", "opens", "pointer", "preferred", "preview", "product", "profile", "rich", "room", "second", "side", "stats", "supplementary", "tap", "the", "there", "time", "touch", "trigger", "usually", "when", "with", "without", "work"], registryDependencies: ["platform", "use-anchor", "use-dismiss", "use-hover-open", "utils"] }, { name: "input", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", exports: ["fieldClass", "Input"], keywords: ["and", "aria", "chrome", "field", "filled", "group", "handed", "inputgroup", "inside", "the", "works"], registryDependencies: ["field", "utils"] }, { name: "input-group", type: "registry:ui", title: "Input Group", description: "Server-safe wrapper that fuses an Input with icons, text (https://, USD) or buttons into one field with a shared border and focus ring. Clicking an addon focuses the input.", exports: ["InputGroup", "InputGroupAddon"], keywords: ["addons", "align", "and", "beside", "border", "button", "buttons", "clicking", "control", "data", "draws", "drops", "field", "focus", "focuses", "group", "https", "icon", "icons", "inner", "input", "its", "like", "made", "matched", "one", "own", "plain", "plus", "ring", "side", "sits", "slot", "text", "the"], registryDependencies: ["utils"] }, { name: "input-otp", type: "registry:ui", title: "Input OTP", description: "One-time code in separate slots, backed by one real input: paste and SMS autofill work, screen readers hear one field. Grouped slots, numeric or alphanumeric, characters spring in, onComplete to submit. verified turns each character into a check, then merges them into one mark.", exports: ["InputOTP"], keywords: ["accepted", "again", "allowed", "alphanumeric", "and", "announced", "are", "autocomplete", "autofill", "back", "beside", "between", "called", "change", "character", "characters", "charset", "check", "code", "controlled", "counts", "dash", "defaultvalue", "draws", "dropped", "each", "edit", "every", "false", "filled", "fires", "first", "for", "group", "groups", "input", "into", "label", "laid", "length", "mark", "merge", "merged", "number", "once", "oncomplete", "one", "only", "onvaluechange", "others", "over", "pair", "paste", "pasted", "per", "read", "readers", "real", "render", "screen", "set", "shown", "single", "sits", "slides", "slot", "slots", "sms", "submit", "the", "then", "time", "transparently", "turns", "typed", "uncontrolled", "uppercased", "value", "verified", "verifiedlabel", "via", "visual", "was", "with"], registryDependencies: ["field", "use-controllable-state", "utils"] }, { name: "kbd", type: "registry:ui", title: "Kbd", description: "Server-safe key caps for shortcut hints. KbdGroup nests keys per the HTML spec.", exports: ["Kbd", "KbdGroup"], keywords: ["cap", "html", "kbd", "kbdgroup", "key", "keys", "made", "nested", "one", "per", "several", "shortcut", "spec", "the"], registryDependencies: ["utils"] }, { name: "label", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", exports: ["Label"], keywords: [], registryDependencies: ["utils"] }, { name: "list", type: "registry:lib", description: "The item a navigation key moves to in a list, from the item at `index` (-1: none active).\nUndefined when the key doesn't navigate.", exports: ["listKeyTarget", "scrollIntoList"], keywords: ["active", "box", "can", "doesn", "enough", "from", "index", "item", "just", "key", "list", "moves", "moving", "navigate", "navigation", "none", "page", "scrollintoview", "scrolls", "show", "the", "undefined", "way", "when", "without"], registryDependencies: ["utils"] }, { name: "marker", type: "registry:ui", title: "Marker", description: "Highlighter-pen <mark> in four tints that wraps cleanly across lines and can draw itself on, left to right, when it scrolls into view. Highlight marks every match of a search in a string, for transcripts and search results.", exports: ["Marker", "Highlight"], keywords: ["across", "before", "case", "cleanly", "color", "delay", "draw", "drawing", "draws", "every", "first", "full", "fully", "highlight", "highlighter", "insensitive", "inside", "instant", "into", "left", "lines", "mark", "markers", "marks", "match", "motion", "one", "paragraph", "pen", "query", "reduced", "right", "scrolls", "search", "seconds", "several", "shown", "stagger", "sweeps", "term", "terms", "text", "the", "time", "view", "with", "without", "wraps"], registryDependencies: ["utils"] }, { name: "menubar", type: "registry:ui", title: "Menubar", description: "Desktop-style menu bar on the WAI-ARIA menubar pattern: one menu open at a time, hovering the next title while one is open switches to it, ← → walk the bar with or without a menu open, one Tab stop. A single highlight springs between titles; menus reuse every DropdownMenu item.", exports: ["MenubarContent", "MenubarItem", "MenubarCheckboxItem", "MenubarRadioGroup", "MenubarRadioItem", "MenubarLabel", "MenubarSeparator", "Menubar", "MenubarMenu", "MenubarTrigger"], keywords: ["actions", "align", "aligned", "analytics", "and", "another", "apart", "bar", "between", "but", "called", "check", "checked", "children", "click", "closes", "desktop", "destructive", "disabled", "down", "dropdownmenuradioitem", "edge", "enter", "equals", "first", "for", "generated", "give", "group", "groups", "highlight", "hint", "hovering", "icon", "irreversible", "item", "items", "its", "label", "labels", "last", "leading", "left", "lines", "mark", "menu", "menubarcontent", "menubartrigger", "menus", "move", "new", "not", "omitted", "oncheckedchange", "one", "onselect", "onvaluechange", "open", "opens", "optional", "optionally", "picked", "red", "right", "selectable", "separators", "shortcut", "shown", "shows", "space", "springs", "stable", "state", "style", "switches", "tell", "tests", "the", "then", "time", "title", "titles", "toggles", "trigger", "value", "variant", "when", "while", "with"], registryDependencies: ["dropdown-menu", "list", "use-indicator", "utils"] }, { name: "message-bubble", type: "registry:ui", title: "Message Bubble", description: "User/assistant chat bubbles with avatar slot, streaming dots and copyable code blocks.", exports: ["MessageBubble"], keywords: ["above", "accent", "after", "and", "animated", "are", "assistant", "attachments", "avatar", "bubble", "chat", "children", "code", "content", "dots", "from", "inline", "left", "lists", "message", "name", "next", "rich", "right", "sender", "shown", "shows", "side", "size", "slot", "streaming", "surface", "text", "the", "time", "typing", "user"], registryDependencies: ["utils"] }, { name: "meter", type: "registry:ui", title: "Meter", description: "Server-safe gauge for a value in a known range (storage, quota, server load, password strength). Colors by <meter> thresholds, or stacked segments with a legend, like a storage bar.", exports: ["Meter"], keywords: ["above", "accessible", "also", "and", "away", "bar", "below", "between", "bound", "color", "colors", "decides", "defaults", "disk", "follow", "good", "green", "high", "known", "label", "legend", "like", "load", "low", "lower", "max", "measurement", "meter", "min", "name", "not", "one", "optimum", "orange", "overrides", "parts", "password", "progress", "quota", "range", "red", "region", "right", "segments", "server", "stacked", "storage", "strength", "sum", "task", "text", "the", "their", "this", "threshold", "thresholds", "total", "two", "upper", "use", "used", "value", "valuetext", "visible", "where", "with", "within"], registryDependencies: ["utils"] }, { name: "motion-provider", type: "registry:component", exports: ["MotionProvider"], keywords: [], registryDependencies: ["utils"] }, { name: "native-select", type: "registry:ui", title: "Native Select", description: "A real <select> with option groups: native picker on phones, no-JS forms, native required validation. Where customizable select is supported (appearance: base-select), the open list gets the menu look.", exports: ["NativeSelect", "NativeSelectGroup", "NativeSelectOption"], keywords: ["appearance", "base", "browser", "custom", "customizable", "elsewhere", "forms", "gets", "group", "heading", "icons", "instead", "invalid", "label", "labeled", "list", "look", "menu", "native", "need", "open", "optgroup", "options", "phones", "picker", "real", "rendering", "required", "select", "stays", "supports", "the", "use", "user", "validation", "when", "where", "without", "works"], registryDependencies: ["field", "input", "utils"] }, { name: "navigation-menu", type: "registry:ui", title: "Navigation Menu", description: "Website top navigation on the APG disclosure pattern: plain links and panels of rich links. Hover opens after a beat, then sliding along the bar switches panels at once; every panel shares one viewport that springs to the next panel's size and position while the content slides in from the side you moved toward. Click, Enter, ↓, ← → and Esc work; closed panels keep their links in the HTML for crawlers.", exports: ["NavigationMenu", "NavigationMenuList", "NavigationMenuItem", "NavigationMenuTrigger", "NavigationMenuContent", "NavigationMenuLink"], keywords: ["active", "after", "all", "along", "and", "aria", "bar", "beat", "bold", "called", "children", "classname", "click", "closed", "closes", "content", "controlled", "crawlers", "current", "defaultvalue", "description", "down", "enter", "entry", "esc", "find", "first", "focuses", "following", "for", "generated", "hidden", "hover", "html", "icon", "inset", "into", "item", "items", "its", "left", "level", "line", "link", "links", "look", "matched", "menu", "mouse", "muted", "navigation", "omitted", "once", "one", "onvaluechange", "open", "opened", "opens", "optional", "own", "padding", "page", "pair", "panel", "panels", "pill", "plain", "readers", "rem", "render", "rich", "right", "row", "selected", "single", "size", "sliding", "space", "springs", "stable", "stay", "still", "switches", "the", "them", "then", "tile", "title", "toggle", "top", "trigger", "uncontrolled", "under", "value", "viewport", "walk", "website", "while", "with", "your"], registryDependencies: ["use-controllable-state", "use-dismiss", "use-indicator", "utils"] }, { name: "newsletter", type: "registry:block", title: "Newsletter", description: "Email signup: InputGroup with the submit Button inside, native email validation, a loading → success toast and an in-place success state on the button.", exports: ["Newsletter"], keywords: ["above", "address", "after", "and", "button", "check", "confirm", "confirms", "description", "email", "field", "for", "form", "heading", "inbox", "inputgroup", "inside", "into", "its", "line", "loading", "message", "native", "next", "onsubscribe", "place", "placeholder", "rejection", "resets", "resolves", "saved", "shows", "signup", "submit", "submitlabel", "success", "successmessage", "supporting", "text", "that", "the", "then", "title", "toast", "turns", "under", "validation", "when", "with", "your"], registryDependencies: ["button", "input", "input-group", "toast", "utils"] }, { name: "number-input", type: "registry:ui", title: "Number Input", description: "Native number field with −/+ buttons: arrow keys, min/max/step validation and form submit from the platform; clamps on blur, rounds to the step's precision.", exports: ["NumberInput"], keywords: ["amount", "and", "are", "arrows", "arrowup", "buttons", "called", "cart", "change", "controlled", "defaultvalue", "down", "field", "first", "form", "helpers", "highest", "keyboard", "lowest", "max", "min", "native", "new", "number", "onvaluechange", "pair", "pointer", "quantity", "render", "step", "submit", "tabindex", "the", "uncontrolled", "use", "users", "validation", "value", "with"], registryDependencies: ["input", "input-group", "use-controllable-state", "utils"] }, { name: "overflow-focus", type: "registry:lib", description: `Drop inside a scroll container: while the container overflows it becomes keyboard-focusable
|
|
17186
17594
|
(tabIndex 0) so arrow keys can scroll it, and it leaves the tab order again once it fits.
|
|
17187
|
-
Renders a hidden span, so server components (Table) can use it as a child.`, exports: ["OverflowFocus"], keywords: ["again", "and", "arrow", "becomes", "can", "child", "components", "container", "drop", "fits", "focusable", "hidden", "inside", "keyboard", "keys", "leaves", "once", "order", "overflows", "renders", "scroll", "server", "span", "tab", "tabindex", "table", "the", "use", "while"], registryDependencies: [] }, { name: "pagination", type: "registry:ui", title: "Pagination", description: "Server-safe page links from page and total: previous/next, ellipsis gaps and a sliding active pill, with numbers that swipe the way you moved. Links by default; buttons with onPageChange. The row keeps a fixed width so it never jumps.", exports: ["Pagination", "pageRange"], keywords: ["active", "always", "and", "arrows", "bar", "become", "between", "buttons", "called", "changes", "client", "controls", "current", "default", "each", "ellipsis", "first", "for", "from", "gaps", "href", "index", "instead", "items", "its", "keeps", "kept", "key", "labels", "last", "length", "links", "mounted", "moved", "never", "next", "number", "numbers", "one", "only", "onpagechange", "page", "pagerange", "pages", "parents", "phones", "picked", "pill", "previous", "remounting", "render", "returns", "row", "same", "show", "siblings", "side", "slides", "slot", "stays", "swipe", "swipes", "text", "that", "the", "total", "unused", "url", "way", "width", "with", "without", "work", "you"], registryDependencies: ["button-variants", "pagination-motion", "utils"] }, { name: "pagination-motion", type: "registry:ui", description: "Scopes the active pill's `layoutId` so several paginations on a page don't share it.", exports: ["PaginationMotion", "PaginationActivePill", "PaginationNumber"], keywords: ["active", "behind", "changes", "children", "direction", "don", "from", "items", "its", "layoutid", "link", "moved", "next", "number", "outline", "page", "pagination", "paginations", "pill", "scopes", "several", "share", "side", "slot", "springs", "swipes", "that", "the", "toward", "travel", "value", "when"], registryDependencies: ["utils"] }, { name: "platform", type: "registry:lib", description: "False on the server and during hydration, true after: gates portals and other client-only output.", exports: ["useMounted", "useIsMac", "useMediaQuery"], keywords: ["after", "and", "assumes", "client", "corrects", "ctrl", "during", "false", "gates", "hydration", "live", "match", "media", "not", "only", "other", "output", "portals", "query", "render", "server", "shortcuts", "ssr", "the", "then", "true", "use", "where"], registryDependencies: [] }, { name: "popover", type: "registry:ui", title: "Popover", description: "Non-modal panel anchored to a Button for filters, pickers and quick edits. Focus moves in, Esc returns it, outside clicks close; Selects and menus opened inside don't count as outside.", exports: ["Popover", "PopoverTrigger", "PopoverContent"], keywords: ["action", "align", "alignment", "also", "anchored", "and", "any", "aria", "armed", "before", "blur", "button", "called", "check", "children", "click", "closes", "confirm", "confirmlabel", "confirmtimeout", "content", "controlled", "default", "defaultopen", "defaults", "destructive", "disables", "done", "edits", "emphasis", "end", "esc", "feedback", "fill", "filters", "first", "flips", "focus", "for", "give", "height", "icon", "irreversible", "its", "label", "left", "like", "loading", "loadinglabel", "modal", "moves", "never", "non", "not", "old", "onclick", "one", "ones", "onopenchange", "open", "out", "outside", "padding", "pair", "panel", "past", "per", "pickers", "place", "popovercontent", "popovertrigger", "popup", "preferred", "primary", "quick", "render", "reserved", "resets", "resize", "returns", "room", "runs", "second", "shown", "shows", "side", "size", "slides", "spinner", "spins", "square", "state", "status", "step", "success", "successlabel", "swap", "swaps", "tab", "text", "the", "there", "this", "trap", "trigger", "two", "uncontrolled", "variant", "view", "when", "where", "while", "width", "with"], registryDependencies: ["button", "platform", "use-anchor", "use-controllable-state", "use-dismiss", "utils"] }, { name: "pricing", type: "registry:block", title: "Pricing", description: "Plan cards with a monthly / yearly segmented switch. Prices roll on a spring in the direction they moved; the featured plan is outlined in the accent. Card, Badge, ToggleGroup, Button.", exports: ["Pricing"], keywords: ["and", "bcp", "billing", "cards", "code", "currency", "defaultperiod", "display", "down", "drop", "eur", "first", "for", "formatting", "iso", "locale", "monthly", "next", "number", "order", "period", "plan", "plans", "price", "prices", "render", "rise", "rolling", "segmented", "selected", "spring", "swap", "switch", "the", "they", "togglegroup", "when", "with", "yearly", "yearlynote"], registryDependencies: ["badge", "card", "toggle-group", "utils"] }, { name: "product-card", type: "registry:block", title: "Product Card", description: "Shop tile: photos in an AspectRatio (a swipeable Carousel with dots when there are several), sale price, Badge and add-to-cart Button.", exports: ["ProductCard"], keywords: ["add", "addlabel", "are", "aspectratio", "badge", "bcp", "become", "button", "called", "card", "carousel", "code", "compareat", "currency", "current", "dots", "for", "formatting", "heading", "height", "href", "img", "iso", "label", "link", "locale", "major", "media", "name", "new", "number", "onaddtocart", "one", "original", "over", "page", "per", "photos", "pressed", "price", "product", "ratio", "sale", "several", "shop", "shows", "slide", "small", "square", "struck", "swipeable", "text", "the", "there", "through", "tile", "units", "when", "width", "with"], registryDependencies: ["aspect-ratio", "badge", "button", "carousel", "utils"] }, { name: "progress", type: "registry:ui", title: "Progress", description: "Accent bar that springs to each new value, with an optional label and percentage. Leave out value for an indeterminate sweep (static under reduced motion).", exports: ["Progress"], keywords: ["above", "accent", "accessible", "across", "allowed", "also", "aria", "bar", "colored", "duration", "each", "for", "frame", "indeterminate", "infinite", "its", "label", "layout", "max", "motion", "name", "new", "next", "omit", "one", "per", "percentage", "reduced", "scalex", "segment", "show", "showvalue", "size", "springs", "static", "sweeps", "that", "the", "there", "thickness", "tween", "under", "unknown", "value", "visible", "when", "work"], registryDependencies: ["utils"] }, { name: "quote", type: "registry:ui", title: "Quote", description: "Semantic figure, blockquote and figcaption. Plain, card and large variants, with author, subtitle, avatar, lang, typewriter and textSwap.", exports: ["Quote"], keywords: ["accent", "alt", "and", "array", "author", "avatar", "blockquote", "card", "children", "correct", "custom", "cycles", "delay", "display", "doe", "effect", "enables", "fades", "figcaption", "figure", "for", "into", "jane", "job", "lang", "language", "large", "line", "mark", "options", "pass", "plain", "pronunciation", "quote", "quoted", "quotes", "reader", "said", "screen", "scrolls", "secondary", "semantic", "seo", "size", "slot", "speed", "spring", "string", "strings", "subtitle", "supports", "surface", "swap", "text", "textswap", "the", "through", "title", "transitions", "typewriter", "under", "variant", "view", "when", "who", "with", "work", "wrote"], registryDependencies: ["text-swap", "typewriter", "utils"] }, { name: "radio-group", type: "registry:ui", title: "Radio Group", description: "Native radios in a fieldset with a legend: browser arrow keys and form submit, a dot that springs in, optional per-option descriptions, vertical or horizontal.", exports: ["RadioGroup", "RadioGroupItem"], keywords: ["adds", "and", "application", "aria", "arrow", "browser", "but", "called", "children", "conform", "controlled", "defaultvalue", "description", "disabled", "does", "entered", "expected", "field", "fieldset", "first", "form", "format", "generated", "group", "indicates", "invalid", "item", "items", "its", "keys", "label", "labels", "lay", "legend", "line", "muted", "name", "native", "not", "omitted", "one", "onvaluechange", "option", "orientation", "pair", "picked", "radios", "render", "rendered", "row", "selectable", "shown", "stack", "submit", "submitted", "the", "them", "uncontrolled", "under", "value", "visible", "when", "with", "work"], registryDependencies: ["field", "use-controllable-state", "utils"] }, { name: "rating", type: "registry:ui", title: "Rating", description: "Star rating on native radios (arrow keys, forms) with hover preview, or readOnly display with fractional stars for averages.", exports: ["Rating"], keywords: ["and", "arrow", "called", "chosen", "controlled", "defaultvalue", "display", "field", "filled", "first", "for", "form", "forms", "fractions", "group", "hidden", "hover", "image", "included", "inputs", "interactive", "keys", "label", "lift", "max", "name", "native", "number", "only", "onvaluechange", "pair", "partially", "picked", "press", "preview", "radios", "rating", "readers", "readonly", "render", "renders", "score", "screen", "size", "spring", "star", "stars", "the", "uncontrolled", "value", "visually", "with"], registryDependencies: ["use-controllable-state", "utils"] }, { name: "relative-time", type: "registry:ui", title: "Relative Time", description: "Self-updating “5 minutes ago” in a semantic <time> with the full date as its title. Server renders the absolute date (no hydration mismatch); one shared ticker for the whole page.", exports: ["RelativeTime"], keywords: ["absolute", "ago", "and", "both", "client", "crawlers", "current", "date", "default", "differ", "full", "get", "hydration", "its", "itself", "keeps", "locale", "minutes", "mismatch", "not", "now", "one", "page", "pass", "real", "relative", "render", "renders", "semantic", "server", "shared", "stored", "text", "that", "the", "there", "ticker", "time", "timestamp", "title", "with", "would"], registryDependencies: ["utils"] }, { name: "resizable", type: "registry:ui", title: "Resizable", description: "Split panes for admin layouts, docs and editors. Hairline dividers with a wide grab area follow the pointer; collapsible panels snap shut past half their minimum. Dividers are APG window splitters: arrows step, Home/End jump to the limits, Enter or double-click collapses with a spring. Groups nest.", exports: ["ResizablePanelGroup", "ResizablePanel", "ResizableHandle"], keywords: ["adds", "admin", "after", "already", "and", "apg", "are", "arrow", "arrows", "basis", "between", "click", "collapsed", "collapsedsize", "collapses", "collapsible", "component", "cookie", "css", "default", "defaultsize", "direction", "divider", "docs", "dom", "double", "drag", "dragging", "each", "editors", "end", "enter", "evenly", "final", "find", "flex", "follows", "for", "grip", "group", "half", "handle", "handles", "home", "horizontal", "hydration", "icon", "jump", "key", "keyboardstep", "keys", "largest", "layout", "layouts", "left", "limits", "maxsize", "middle", "minimum", "minsize", "neighbor", "one", "onlayout", "order", "others", "panels", "panes", "parents", "past", "per", "plain", "pointer", "press", "rail", "rest", "save", "server", "share", "shows", "shut", "side", "size", "sizes", "smallest", "snaps", "split", "splitters", "spring", "ssr", "stacked", "starting", "step", "the", "their", "them", "toggle", "toggles", "two", "until", "vertical", "visible", "what", "when", "window", "withhandle", "without", "work", "wrappers"], registryDependencies: ["utils"] }, { name: "reveal", type: "registry:ui", title: "Reveal", description: "Scroll reveal: content fades and rises on a spring the first time it enters the viewport.", exports: ["Reveal"], keywords: ["after", "and", "content", "delay", "entering", "fades", "first", "into", "offset", "place", "rises", "scrolls", "seconds", "spring", "start", "stays", "the", "time", "view", "visible", "wait"], registryDependencies: ["utils"] }, { name: "scroll-area", type: "registry:ui", title: "Scroll Area", description: "Overlay scrollbars: thin thumbs that fade in while scrolling or on hover, widen under the pointer and drag. Scrolling stays native; the labeled region is keyboard-focusable.", exports: ["ScrollArea"], keywords: ["also", "and", "aria", "axes", "can", "classes", "drag", "fade", "focus", "for", "get", "give", "goes", "height", "here", "horizontal", "hover", "itself", "keyboard", "label", "mouse", "names", "native", "one", "orientation", "overlay", "padding", "region", "scroll", "scrollable", "scrollbars", "scrolling", "stays", "takes", "that", "the", "thin", "thumb", "thumbs", "touch", "users", "viewport", "viewportclassname", "wheel", "while", "widen"], registryDependencies: ["utils"] }, { name: "search-input", type: "registry:ui", title: "Search Input", description: 'Search field built on Input: leading icon, spring-in clear button, Esc to clear, and an optional focus shortcut ("/" or mod+k) with a Kbd hint.', exports: ["SearchInput"], keywords: ["accessible", "already", "and", "anywhere", "aside", "border", "browser", "button", "called", "change", "clear", "cleared", "clears", "command", "controlled", "ctrl", "default", "defaultvalue", "elsewhere", "embedded", "esc", "every", "field", "find", "first", "focus", "focused", "focuses", "for", "from", "ghost", "hidden", "hint", "htmlfor", "icon", "inputs", "kbd", "key", "label", "mac", "mod", "name", "onvaluechange", "opens", "optional", "own", "pair", "palette", "pass", "query", "render", "ring", "search", "second", "shadow", "shortcut", "single", "skipped", "spring", "steps", "still", "surface", "the", "uncontrolled", "value", "variant", "when", "while", "with", "you"], registryDependencies: ["input", "kbd", "platform", "use-controllable-state", "use-hotkey", "utils"] }, { name: "segmented-tabs", type: "registry:ui", title: "AppWindow", description: "Window container with close / minimize / zoom controls, title, custom header slot and actions. SegmentedTabs is the compact view switcher for its title bar.", exports: ["SegmentedTabs"], keywords: ["accessible", "active", "also", "always", "aria", "behind", "called", "current", "for", "indicator", "item", "items", "label", "labels", "list", "marked", "name", "onchange", "one", "pressed", "segmented", "slides", "spring", "tab", "tabs", "the", "under", "used", "value", "values", "with"], registryDependencies: ["overflow-focus", "use-indicator", "utils"] }, { name: "select", type: "registry:ui", title: "Select", description: "Single-choice listbox styled like Input, with groups, icons and disabled options. Native-select keyboard (arrows, typeahead even while closed), flips above the trigger near the viewport edge, submits via a hidden input.", exports: ["Select"], keywords: ["accessible", "also", "aria", "blocks", "called", "cancel", "choice", "closed", "controlled", "defaultvalue", "describedby", "disabled", "down", "end", "enter", "error", "esc", "field", "fill", "fills", "first", "flat", "form", "from", "groups", "hidden", "hint", "home", "htmlfor", "ids", "input", "invalid", "keyboard", "label", "labeled", "let", "like", "listbox", "matches", "name", "native", "needs", "nothing", "onvaluechange", "opening", "options", "pair", "pick", "picked", "placeholder", "render", "required", "select", "sets", "single", "space", "styled", "styling", "submit", "submits", "text", "the", "there", "through", "trigger", "typeahead", "uncontrolled", "value", "visible", "when", "while", "with"], registryDependencies: ["field", "input", "list", "platform", "use-anchor", "use-controllable-state", "use-dismiss", "utils"] }, { name: "separator", type: "registry:ui", title: "Separator", description: 'Server-safe hairline, horizontal or vertical, decorative by default, with an optional centered label like "or".', exports: ["Separator"], keywords: ["assistive", "centered", "content", "decorative", "default", "direction", "false", "for", "horizontal", "label", "line", "only", "orientation", "purely", "separates", "set", "tech", "text", "the", "visual", "when"], registryDependencies: ["utils"] }, { name: "sidebar", type: "registry:ui", title: "Sidebar", description: "App navigation from data: header, groups (optionally foldable), badges and pinned footer entries. Collapses to an icon rail on a spring (⌘B / Ctrl+B) with labels kept for assistive tech; below md it is a left drawer.", exports: ["SidebarLayout", "Sidebar", "SidebarTrigger"], keywords: ["a11y", "accessible", "and", "aria", "below", "bottom", "but", "called", "client", "clip", "collapsed", "collapses", "controlled", "ctrl", "current", "data", "default", "defaultcollapsed", "drawer", "entries", "field", "footer", "for", "from", "groups", "header", "highlighted", "holds", "icon", "ignored", "item", "label", "labels", "landmark", "lays", "left", "links", "name", "navigation", "oncollapsedchange", "onselect", "page", "pair", "picked", "pinned", "rail", "routing", "settings", "shortcut", "side", "sidebar", "slot", "spring", "start", "state", "stay", "the", "toggle", "too", "top", "tree", "trigger", "true", "typing", "uncontrolled", "when", "while", "with", "workspace"], registryDependencies: ["button", "collapsible", "drawer", "platform", "use-controllable-state", "use-hotkey", "utils"] }, { name: "site-header", type: "registry:block", title: "Site Header", description: "Sticky, translucent site header from one nav array: brand, NavigationMenu panels from md up, a right Drawer with the same links below it, and your actions on the right. The hairline underneath appears only once content scrolls beneath it.", exports: ["SiteHeader"], keywords: ["accessible", "acme", "actions", "alike", "and", "appears", "aria", "below", "brand", "client", "close", "controls", "current", "demo", "desktop", "drawer", "every", "for", "from", "hairline", "hand", "header", "href", "label", "left", "level", "link", "links", "logo", "marks", "matching", "name", "nav", "navigation", "navigationmenu", "once", "onclick", "only", "onnavigate", "over", "page", "panels", "right", "routing", "same", "scrolls", "search", "shown", "sign", "site", "sticky", "stub", "the", "theme", "top", "under", "width", "with"], registryDependencies: ["button", "drawer", "navigation-menu", "use-disclosure", "utils"] }, { name: "skeleton", type: "registry:ui", title: "Skeleton", description: 'Server-safe loading placeholders that pulse (static under reduced motion). SkeletonGroup announces one "Loading" to screen readers.', exports: ["Skeleton", "SkeletonGroup"], keywords: ["announced", "block", "classes", "classname", "hear", "instead", "label", "loading", "nothing", "one", "placeholder", "readers", "screen", "shown", "size", "skeleton", "skeletons", "this", "while", "with", "wrap"], registryDependencies: ["utils"] }, { name: "slider", type: "registry:ui", title: "Slider", description: "Slider over native range inputs: thin accent track, white knob that lifts while held. One value or two for a range; keyboard, forms, touch and Field wiring come from the platform.", exports: ["Slider"], keywords: ["accent", "accessible", "and", "aria", "arrow", "blocks", "browser", "called", "defaults", "defaultvalue", "describedby", "disabled", "does", "drag", "dragging", "error", "every", "field", "fills", "filter", "first", "for", "forms", "from", "held", "highest", "hint", "ids", "increment", "inputs", "invalid", "keyboard", "keys", "knob", "labels", "lifts", "lowest", "max", "maximum", "min", "minimum", "name", "names", "native", "one", "onvaluechange", "over", "per", "price", "range", "readers", "render", "screen", "sets", "slider", "step", "styling", "submitted", "text", "that", "the", "thin", "thumb", "touch", "track", "twice", "two", "uncontrolled", "value", "values", "while", "white", "with", "work"], registryDependencies: ["field", "use-controllable-state", "utils"] }, { name: "spinner", type: "registry:ui", title: "Spinner", description: "Server-safe activity indicator: eight spokes in currentColor, three sizes, its own status label (or silent inside a parent that announces). Holds still under reduced motion.", exports: ["Spinner"], keywords: ["accessible", "activity", "already", "announces", "busy", "currentcolor", "diameter", "eight", "holds", "indicator", "label", "motion", "name", "parent", "pass", "reduced", "size", "spokes", "state", "still", "the", "under", "when"], registryDependencies: ["utils"] }, { name: "stat-tile", type: "registry:block", title: "Stat Tile", description: "Admin dashboard metric: value, a delta Badge that is green when the change is good news (invert for errors and latency), and an area Chart of the trend. Server-safe.", exports: ["StatTile"], keywords: ["admin", "and", "area", "axis", "badge", "below", "change", "chart", "dashboard", "data", "datakey", "delta", "down", "each", "errors", "for", "format", "formatted", "good", "green", "headline", "index", "intl", "invert", "key", "label", "last", "latency", "metric", "metrics", "month", "name", "news", "number", "period", "positive", "revenue", "row", "rows", "the", "tile", "tooltip", "trend", "unless", "value", "when", "where"], registryDependencies: ["badge", "card", "chart", "utils"] }, { name: "stats-strip", type: "registry:block", title: "Stats Strip", description: "Headline numbers, each card a <dl>: cards rise in turn with Reveal, then each number springs up with CountTo. Final values are in the HTML.", exports: ["StatsStrip"], keywords: ["card", "display", "each", "headline", "its", "number", "numbers", "order", "rises", "row", "springs", "stats", "then", "turn"], registryDependencies: ["card", "count-to", "reveal", "utils"] }, { name: "stepper", type: "registry:ui", title: "Stepper", description: "Multi-step form (questionnaire, checkout, setup wizard). Steps stay mounted so values survive Back and submit together; Continue checks only the current step's native constraints, plus an optional async onNext guard with a spinner. Progress header with springing connectors; works with onSubmit or a Server Action.", exports: ["Stepper", "StepperList", "StepperStep", "StepperFooter"], keywords: ["action", "after", "and", "are", "back", "backlabel", "button", "called", "check", "checks", "children", "connector", "constraints", "continue", "controlled", "current", "defaultvalue", "description", "every", "false", "fields", "fills", "finished", "first", "for", "form", "header", "hidden", "inactive", "its", "last", "links", "matches", "mounted", "multi", "native", "new", "nextlabel", "not", "one", "only", "onnext", "onsubmit", "onvaluechange", "optional", "order", "pass", "progress", "render", "resolve", "return", "runs", "server", "shown", "shows", "spinner", "spring", "start", "stay", "stays", "step", "steps", "submit", "submitlabel", "submits", "survive", "text", "the", "through", "title", "together", "uncontrolled", "unmounted", "value", "values", "while", "with"], registryDependencies: ["button", "use-controllable-state", "utils"] }, { name: "styles", type: "registry:file", exports: [], keywords: [], registryDependencies: [] }, { name: "switch", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", exports: ["Switch"], keywords: ["called", "checkbox", "checked", "controlled", "defaultchecked", "first", "knob", "native", "new", "oncheckedchange", "pair", "render", "role", "spring", "state", "switch", "the", "uncontrolled", "with"], registryDependencies: ["field", "use-controllable-state", "utils"] }, { name: "table", type: "registry:ui", title: "Table", description: "Server-safe semantic <table> in a scrolling card: header, body, footer and caption, striped rows, tinted selected rows, and sortable headers that set aria-sort and flip their arrow.", exports: ["Table", "TableHeader", "TableBody", "TableFooter", "TableRow", "TableHead", "TableCell", "TableCaption"], keywords: ["alternating", "and", "aria", "around", "arrow", "button", "called", "card", "classes", "column", "containerclassname", "data", "direction", "for", "header", "keyboard", "makes", "narrow", "onsort", "pass", "pressed", "real", "requires", "row", "screens", "scrollable", "scrolling", "scrolls", "selected", "sets", "shading", "shows", "sideways", "sort", "sortable", "state", "striped", "table", "that", "the", "then", "tint", "when"], registryDependencies: ["overflow-focus", "utils"] }, { name: "tabs", type: "registry:ui", title: "Tabs", description: "WAI-ARIA tabs with arrow, Home and End keys. One indicator springs between tabs, segmented or underline. Inactive panels stay in the DOM for SEO; the active one fades in.", exports: ["Tabs", "TabsList", "TabsTrigger", "TabsContent"], keywords: ["activated", "active", "always", "and", "aria", "arrows", "bar", "behind", "called", "children", "content", "controlled", "crawlable", "defaultvalue", "dom", "end", "first", "focus", "hidden", "home", "html", "inactive", "indicator", "labels", "matches", "move", "muted", "one", "onvaluechange", "pair", "panel", "panels", "pill", "remains", "render", "segmented", "springs", "stay", "stays", "tab", "tabs", "tabscontent", "tabstrigger", "the", "their", "track", "uncontrolled", "under", "underline", "value", "variant", "wai", "while", "with"], registryDependencies: ["list", "use-controllable-state", "use-indicator", "utils"] }, { name: "tag-input", type: "registry:ui", title: "Tag Input", description: "Free-form tags: Enter or a separator adds, Backspace removes the last, pasting a list adds them all. Duplicates, a max and a validate function keep bad tags in the input. One hidden input per tag.", exports: ["tagFieldClass", "Tag", "TagInput"], keywords: ["accessible", "add", "after", "allowed", "and", "animatepresence", "backspace", "besides", "button", "called", "children", "chip", "commit", "controlled", "defaultvalue", "disabled", "empty", "enter", "every", "false", "first", "form", "free", "hidden", "hides", "input", "inside", "its", "keys", "last", "limit", "list", "many", "max", "mode", "most", "name", "one", "onremove", "onvaluechange", "out", "pair", "paste", "pasted", "per", "poplayout", "press", "reject", "removable", "remove", "removes", "render", "rendered", "return", "runs", "separator", "separators", "shows", "split", "springs", "stays", "stops", "submits", "tag", "tags", "text", "that", "the", "these", "too", "trimming", "type", "typed", "typing", "uncontrolled", "validate", "value", "when", "with"], registryDependencies: ["field", "use-controllable-state", "utils"] }, { name: "terminal", type: "registry:ui", title: "Terminal", description: "Terminal window. Snippet mode types install commands once scrolled into view, with the full transcript kept in the DOM for SEO and no-JS; log mode streams lines into a live region that follows the bottom unless you scroll up, then offers a jump back. Status lines carry glyphs, copy grabs the commands.", exports: ["Terminal"], keywords: ["and", "append", "arrives", "back", "before", "body", "bodyclassname", "bottom", "character", "clipboard", "commands", "copy", "copytext", "defaults", "dom", "false", "follows", "for", "full", "hides", "install", "jump", "lines", "live", "log", "mode", "new", "offering", "once", "order", "output", "per", "prompt", "puts", "read", "reveals", "role", "scroll", "scrolling", "seo", "size", "snippet", "snippets", "speed", "stays", "sticks", "stream", "streams", "symbol", "terminal", "text", "the", "title", "typed", "types", "typing", "unless", "view", "what", "when", "window", "you"], registryDependencies: ["app-window", "code", "platform", "utils"] }, { name: "testimonial", type: "registry:block", title: "Testimonial", description: "A large Quote with its author, and everyone else who agrees as an overlapping AvatarGroup. Server-safe.", exports: ["Testimonial"], keywords: ["agrees", "and", "author", "avatar", "avatargroup", "behind", "beside", "carried", "children", "company", "crowd", "else", "everyone", "few", "first", "fold", "initials", "into", "its", "lang", "language", "large", "line", "one", "others", "otherslabel", "photo", "quote", "rest", "role", "said", "show", "stack", "streamers", "subtitle", "the", "trusted", "under", "url", "who", "with", "without"], registryDependencies: ["avatar", "avatar-group", "quote", "utils"] }, { name: "text-swap", type: "registry:ui", title: "TextSwap", description: "Spring-powered text morphing and ticker. Auto-cycles phrases or animates whenever children change with blur and slide transitions.", exports: ["TextSwap"], keywords: ["active", "array", "automatically", "between", "changing", "child", "children", "controlling", "cycle", "cycling", "default", "direction", "externally", "hover", "index", "interval", "items", "milliseconds", "mouse", "new", "pass", "pause", "pauseonhover", "single", "string", "swap", "swaps", "text", "the", "through", "true", "when"], registryDependencies: ["utils"] }, { name: "textarea", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", exports: ["Textarea"], keywords: ["auto", "cap", "content", "corner", "end", "field", "firefox", "grows", "max", "maxlength", "near", "needs", "red", "resizable", "show", "showcount", "sizing", "the", "then", "turns", "unsupported", "used", "warning", "where", "with"], registryDependencies: ["field", "input", "utils"] }, { name: "theme", type: "registry:lib", description: "Retints primary, focus rings, selection and switches app-wide. Persists like the theme.", exports: ["isDark", "setTheme", "subscribeTheme", "accents", "getAccent", "setAccent", "themeScript"], keywords: ["and", "app", "focus", "like", "persists", "primary", "retints", "rings", "selection", "switches", "the", "theme", "wide"], registryDependencies: [] }, { name: "theme-toggle", type: "registry:ui", title: "Theme Toggle", description: "Light/dark icon switch with spring rotate-swap; follows the OS until first use.", exports: ["ThemeToggle"], keywords: ["button", "click", "first", "follows", "icon", "moon", "rotate", "spring", "sun", "swap", "until"], registryDependencies: ["button", "theme", "utils"] }, { name: "timeline", type: "registry:ui", title: "Timeline", description: "Server-safe ordered list of events on a rail: icon or dot markers with status colors, machine-readable <time>, title and an optional body. For changelogs, experience and mod logs.", exports: ["Timeline", "TimelineItem"], keywords: ["above", "and", "body", "children", "color", "date", "datetime", "dot", "event", "events", "first", "for", "headline", "icon", "inside", "list", "machine", "marker", "newest", "oldest", "one", "optional", "order", "ordered", "pair", "rail", "readable", "render", "shown", "small", "the", "then", "time", "title", "variant", "with", "without", "yours"], registryDependencies: ["utils"] }, { name: "toast", type: "registry:ui", title: "Toast", description: "Stacked notifications. Hover to expand, swipe to dismiss, timers pause on hover.", exports: ["toast", "Toaster"], keywords: ["action", "description", "dismiss", "dismisses", "duration", "error", "expands", "focus", "hover", "layout", "loading", "mount", "once", "promise", "root", "saved", "stacked", "success", "swipe", "tap", "title", "toast", "toasts"], registryDependencies: ["use-dismiss", "utils"] }, { name: "toggle-group", type: "registry:ui", title: "Toggle Group", description: "Segmented control of toggle buttons. Single mode slides one pill to the pressed item (view mode, billing period); multiple mode toggles items independently (bold, italic). Arrow keys move between items.", exports: ["ToggleGroup", "ToggleGroupItem"], keywords: ["always", "and", "aria", "arrow", "behind", "between", "billing", "bold", "buttons", "called", "choice", "control", "controlled", "defaultvalue", "end", "every", "filters", "first", "height", "home", "icon", "independent", "italic", "item", "items", "keys", "label", "labels", "mode", "move", "multiple", "need", "one", "only", "onvaluechange", "pair", "period", "pill", "pressed", "render", "reported", "segment", "segmented", "set", "single", "size", "slides", "springs", "the", "time", "toggle", "toggles", "type", "uncontrolled", "value", "values", "view", "when", "with"], registryDependencies: ["list", "use-controllable-state", "use-indicator", "utils"] }, { name: "tooltip", type: "registry:ui", title: "Tooltip", description: "Short label on hover or keyboard focus, also wired as the trigger's aria-describedby. Warm-up: once one opens, neighbors open instantly. Hoverable, Esc dismisses, ignores touch.", exports: ["Tooltip"], keywords: ["accessible", "action", "align", "alignment", "also", "and", "aria", "becomes", "before", "button", "children", "content", "control", "default", "delay", "description", "dismisses", "element", "esc", "first", "flips", "focus", "focusable", "for", "hold", "hover", "how", "icon", "keyboard", "label", "one", "open", "opens", "own", "pointer", "preferred", "press", "reaches", "room", "short", "shows", "side", "stays", "still", "tap", "text", "the", "their", "there", "toggles", "tooltip", "touch", "trigger", "triggers", "wcag", "when", "while", "with"], registryDependencies: ["platform", "use-anchor", "use-dismiss", "use-hover-open", "utils"] }, { name: "typewriter", type: "registry:ui", title: "Typewriter", description: "Types out text character-by-character with optional phrase cycling and pulsing cursor. Full copy stays in the DOM for SEO.", exports: ["Typewriter"], keywords: ["and", "array", "before", "blinking", "called", "character", "classes", "cursor", "cursorchar", "cursorclassname", "custom", "cycle", "default", "delay", "deletespeed", "deleting", "dom", "effect", "for", "full", "fully", "headers", "hero", "loop", "milliseconds", "once", "oncomplete", "pause", "pauseduration", "per", "phrase", "phrases", "quotes", "seo", "show", "speed", "starting", "stays", "text", "the", "through", "true", "type", "typed", "typewriter", "typing", "wait", "when", "whether", "while"], registryDependencies: ["platform", "utils"] }, { name: "use-anchor", type: "registry:hook", description: `Fixed-position coordinates for a floating panel next to its anchor: on the preferred side,
|
|
17595
|
+
Renders a hidden span, so server components (Table) can use it as a child.`, exports: ["OverflowFocus"], keywords: ["again", "and", "arrow", "becomes", "can", "child", "components", "container", "drop", "fits", "focusable", "hidden", "inside", "keyboard", "keys", "leaves", "once", "order", "overflows", "renders", "scroll", "server", "span", "tab", "tabindex", "table", "the", "use", "while"], registryDependencies: [] }, { name: "pagination", type: "registry:ui", title: "Pagination", description: "Server-safe page links from page and total: previous/next, ellipsis gaps and a sliding active pill, with numbers that swipe the way you moved. Links by default; buttons with onPageChange. The row keeps a fixed width so it never jumps.", exports: ["Pagination", "pageRange"], keywords: ["active", "always", "and", "arrows", "bar", "become", "between", "buttons", "called", "changes", "client", "controls", "current", "default", "each", "ellipsis", "first", "for", "from", "gaps", "href", "index", "instead", "items", "its", "keeps", "kept", "key", "labels", "last", "length", "links", "mounted", "moved", "never", "next", "number", "numbers", "one", "only", "onpagechange", "page", "pagerange", "pages", "parents", "phones", "picked", "pill", "previous", "remounting", "render", "returns", "row", "same", "show", "siblings", "side", "slides", "slot", "stays", "swipe", "swipes", "text", "that", "the", "total", "unused", "url", "way", "width", "with", "without", "work", "you"], registryDependencies: ["button-variants", "pagination-motion", "utils"] }, { name: "pagination-motion", type: "registry:ui", description: "Scopes the active pill's `layoutId` so several paginations on a page don't share it.", exports: ["PaginationMotion", "PaginationActivePill", "PaginationNumber"], keywords: ["active", "behind", "changes", "children", "direction", "don", "from", "items", "its", "layoutid", "link", "moved", "next", "number", "outline", "page", "pagination", "paginations", "pill", "scopes", "several", "share", "side", "slot", "springs", "swipes", "that", "the", "toward", "travel", "value", "when"], registryDependencies: ["utils"] }, { name: "platform", type: "registry:lib", description: "False on the server and during hydration, true after: gates portals and other client-only output.", exports: ["useMounted", "useIsMac", "useMediaQuery"], keywords: ["after", "and", "assumes", "client", "corrects", "ctrl", "during", "false", "gates", "hydration", "live", "match", "media", "not", "only", "other", "output", "portals", "query", "render", "server", "shortcuts", "ssr", "the", "then", "true", "use", "where"], registryDependencies: [] }, { name: "popover", type: "registry:ui", title: "Popover", description: "Non-modal panel anchored to a Button for filters, pickers and quick edits. Focus moves in, Esc returns it, outside clicks close; Selects and menus opened inside don't count as outside.", exports: ["Popover", "PopoverTrigger", "PopoverContent"], keywords: ["action", "align", "alignment", "also", "anchored", "and", "any", "aria", "armed", "before", "blur", "both", "button", "called", "can", "check", "children", "click", "clicks", "closes", "confirm", "confirmlabel", "confirmtimeout", "content", "controlled", "default", "defaultopen", "defaults", "destructive", "done", "edits", "emphasis", "end", "enter", "esc", "feedback", "fill", "filters", "first", "flips", "focus", "focusable", "for", "form", "give", "height", "icon", "ignore", "ignores", "irreversible", "its", "label", "left", "like", "loading", "loadinglabel", "modal", "moves", "never", "non", "not", "old", "onclick", "one", "ones", "onopenchange", "open", "out", "outside", "padding", "pair", "panel", "past", "per", "pickers", "place", "popovercontent", "popovertrigger", "popup", "preferred", "primary", "quick", "render", "reserved", "resets", "resize", "returns", "room", "runs", "second", "shown", "shows", "side", "size", "slides", "spinner", "spins", "square", "state", "status", "stays", "step", "submit", "success", "successlabel", "swap", "swaps", "tab", "text", "the", "there", "this", "trap", "trigger", "twice", "two", "uncontrolled", "variant", "view", "when", "where", "while", "width", "with"], registryDependencies: ["button", "platform", "use-anchor", "use-controllable-state", "use-dismiss", "utils"] }, { name: "pricing", type: "registry:block", title: "Pricing", description: "Plan cards with a monthly / yearly segmented switch. Prices roll on a spring in the direction they moved; the featured plan is outlined in the accent. Card, Badge, ToggleGroup, Button.", exports: ["Pricing"], keywords: ["and", "bcp", "billing", "cards", "code", "currency", "defaultperiod", "display", "down", "drop", "eur", "first", "for", "formatting", "iso", "locale", "monthly", "next", "number", "order", "period", "plan", "plans", "price", "prices", "render", "rise", "rolling", "segmented", "selected", "spring", "swap", "switch", "the", "they", "togglegroup", "when", "with", "yearly", "yearlynote"], registryDependencies: ["badge", "card", "toggle-group", "utils"] }, { name: "product-card", type: "registry:block", title: "Product Card", description: "Shop tile: photos in an AspectRatio (a swipeable Carousel with dots when there are several), sale price, Badge and add-to-cart Button.", exports: ["ProductCard"], keywords: ["add", "addlabel", "are", "aspectratio", "badge", "bcp", "become", "button", "called", "card", "carousel", "code", "compareat", "currency", "current", "dots", "for", "formatting", "heading", "height", "href", "img", "iso", "label", "link", "locale", "major", "media", "name", "new", "number", "onaddtocart", "one", "original", "over", "page", "per", "photos", "pressed", "price", "product", "ratio", "sale", "several", "shop", "shows", "slide", "small", "square", "struck", "swipeable", "text", "the", "there", "through", "tile", "units", "when", "width", "with"], registryDependencies: ["aspect-ratio", "badge", "button", "carousel", "utils"] }, { name: "progress", type: "registry:ui", title: "Progress", description: "Accent bar that springs to each new value, with an optional label and percentage. Leave out value for an indeterminate sweep (static under reduced motion).", exports: ["Progress"], keywords: ["above", "accent", "accessible", "across", "allowed", "also", "aria", "bar", "colored", "duration", "each", "for", "frame", "indeterminate", "infinite", "its", "label", "layout", "max", "motion", "name", "new", "next", "omit", "one", "per", "percentage", "reduced", "scalex", "segment", "show", "showvalue", "size", "springs", "static", "sweeps", "that", "the", "there", "thickness", "tween", "under", "unknown", "value", "visible", "when", "work"], registryDependencies: ["utils"] }, { name: "quote", type: "registry:ui", title: "Quote", description: "Semantic figure, blockquote and figcaption. Plain, card and large variants, with author, subtitle, avatar, lang, typewriter and textSwap.", exports: ["Quote"], keywords: ["accent", "alt", "and", "array", "author", "avatar", "blockquote", "card", "children", "correct", "custom", "cycles", "delay", "display", "doe", "effect", "enables", "fades", "figcaption", "figure", "for", "into", "jane", "job", "lang", "language", "large", "line", "mark", "options", "pass", "plain", "pronunciation", "quote", "quoted", "quotes", "reader", "said", "screen", "scrolls", "secondary", "semantic", "seo", "size", "slot", "speed", "spring", "string", "strings", "subtitle", "supports", "surface", "swap", "text", "textswap", "the", "through", "title", "transitions", "typewriter", "under", "variant", "view", "when", "who", "with", "work", "wrote"], registryDependencies: ["text-swap", "typewriter", "utils"] }, { name: "radio-group", type: "registry:ui", title: "Radio Group", description: "Native radios in a fieldset with a legend: browser arrow keys and form submit, a dot that springs in, optional per-option descriptions, vertical or horizontal.", exports: ["RadioGroup", "RadioGroupItem"], keywords: ["adds", "and", "application", "aria", "arrow", "browser", "but", "called", "children", "conform", "controlled", "defaultvalue", "description", "disabled", "does", "entered", "expected", "field", "fieldset", "first", "form", "format", "generated", "group", "indicates", "invalid", "item", "items", "its", "keys", "label", "labels", "lay", "legend", "line", "muted", "name", "native", "needs", "not", "omitted", "one", "onvaluechange", "option", "orientation", "pair", "picked", "radios", "render", "rendered", "required", "row", "selectable", "shown", "stack", "submit", "submitted", "the", "them", "uncontrolled", "under", "value", "visible", "when", "with", "work"], registryDependencies: ["field", "use-controllable-state", "utils"] }, { name: "rating", type: "registry:ui", title: "Rating", description: "Star rating on native radios (arrow keys, forms) with hover preview, or readOnly display with fractional stars for averages.", exports: ["Rating"], keywords: ["and", "aria", "arrow", "called", "can", "chosen", "controlled", "defaultvalue", "describedby", "disabled", "display", "error", "field", "filled", "fills", "first", "for", "form", "forms", "fractions", "from", "group", "hidden", "hint", "hover", "ids", "image", "included", "inputs", "interactive", "invalid", "isn", "keys", "label", "lift", "max", "name", "native", "needs", "number", "only", "onvaluechange", "pair", "partially", "picked", "press", "preview", "radios", "rating", "readers", "readonly", "render", "renders", "required", "score", "screen", "sets", "size", "spring", "star", "stars", "styling", "submit", "submitted", "text", "the", "uncontrolled", "value", "visually", "with"], registryDependencies: ["field", "use-controllable-state", "utils"] }, { name: "relative-time", type: "registry:ui", title: "Relative Time", description: "Self-updating “5 minutes ago” in a semantic <time> with the full date as its title. Server renders the absolute date (no hydration mismatch); one shared ticker for the whole page.", exports: ["RelativeTime"], keywords: ["absolute", "ago", "and", "both", "client", "crawlers", "current", "date", "default", "differ", "full", "get", "hydration", "its", "itself", "keeps", "locale", "minutes", "mismatch", "not", "now", "one", "page", "pass", "real", "relative", "render", "renders", "semantic", "server", "shared", "stored", "text", "that", "the", "there", "ticker", "time", "timestamp", "title", "with", "would"], registryDependencies: ["utils"] }, { name: "resizable", type: "registry:ui", title: "Resizable", description: "Split panes for admin layouts, docs and editors. Hairline dividers with a wide grab area follow the pointer; collapsible panels snap shut past half their minimum. Dividers are APG window splitters: arrows step, Home/End jump to the limits, Enter or double-click collapses with a spring. Groups nest.", exports: ["ResizablePanelGroup", "ResizablePanel", "ResizableHandle"], keywords: ["adds", "admin", "after", "already", "and", "apg", "are", "arrow", "arrows", "basis", "between", "click", "collapsed", "collapsedsize", "collapses", "collapsible", "component", "cookie", "css", "default", "defaultsize", "direction", "divider", "docs", "dom", "double", "drag", "dragging", "each", "editors", "end", "enter", "evenly", "final", "find", "flex", "follows", "for", "grip", "group", "half", "handle", "handles", "home", "horizontal", "hydration", "icon", "jump", "key", "keyboardstep", "keys", "largest", "layout", "layouts", "left", "limits", "maxsize", "middle", "minimum", "minsize", "neighbor", "one", "onlayout", "order", "others", "panels", "panes", "parents", "past", "per", "plain", "pointer", "press", "rail", "rest", "save", "server", "share", "shows", "shut", "side", "size", "sizes", "smallest", "snaps", "split", "splitters", "spring", "ssr", "stacked", "starting", "step", "the", "their", "them", "toggle", "toggles", "two", "until", "vertical", "visible", "what", "when", "window", "withhandle", "without", "work", "wrappers"], registryDependencies: ["utils"] }, { name: "reveal", type: "registry:ui", title: "Reveal", description: "Scroll reveal: content fades and rises on a spring the first time it enters the viewport.", exports: ["Reveal"], keywords: ["after", "and", "content", "delay", "entering", "fades", "first", "into", "offset", "place", "rises", "scrolls", "seconds", "spring", "start", "stays", "the", "time", "view", "visible", "wait"], registryDependencies: ["utils"] }, { name: "scroll-area", type: "registry:ui", title: "Scroll Area", description: "Overlay scrollbars: thin thumbs that fade in while scrolling or on hover, widen under the pointer and drag. Scrolling stays native; the labeled region is keyboard-focusable.", exports: ["ScrollArea"], keywords: ["also", "and", "aria", "axes", "can", "classes", "drag", "fade", "focus", "for", "get", "give", "goes", "height", "here", "horizontal", "hover", "itself", "keyboard", "label", "mouse", "names", "native", "one", "orientation", "overlay", "padding", "region", "scroll", "scrollable", "scrollbars", "scrolling", "stays", "takes", "that", "the", "thin", "thumb", "thumbs", "touch", "users", "viewport", "viewportclassname", "wheel", "while", "widen"], registryDependencies: ["utils"] }, { name: "search-input", type: "registry:ui", title: "Search Input", description: 'Search field built on Input: leading icon, spring-in clear button, Esc to clear, and an optional focus shortcut ("/" or mod+k) with a Kbd hint.', exports: ["SearchInput"], keywords: ["accessible", "already", "and", "anywhere", "aside", "border", "browser", "button", "called", "change", "clear", "cleared", "clears", "command", "controlled", "ctrl", "default", "defaultvalue", "elsewhere", "embedded", "esc", "every", "field", "find", "first", "focus", "focused", "focuses", "for", "from", "ghost", "hidden", "hint", "htmlfor", "icon", "inputs", "kbd", "key", "label", "mac", "mod", "name", "onvaluechange", "opens", "optional", "own", "pair", "palette", "pass", "query", "render", "ring", "search", "second", "shadow", "shortcut", "single", "skipped", "spring", "steps", "still", "surface", "the", "uncontrolled", "value", "variant", "when", "while", "with", "you"], registryDependencies: ["input", "kbd", "platform", "use-controllable-state", "use-hotkey", "utils"] }, { name: "segmented-tabs", type: "registry:ui", title: "AppWindow", description: "Window container with close / minimize / zoom controls, title, custom header slot and actions. SegmentedTabs is the compact view switcher for its title bar.", exports: ["SegmentedTabs"], keywords: ["accessible", "active", "also", "always", "aria", "behind", "called", "current", "for", "indicator", "item", "items", "label", "labels", "list", "marked", "name", "onchange", "one", "pressed", "segmented", "slides", "spring", "tab", "tabs", "the", "under", "used", "value", "values", "with"], registryDependencies: ["overflow-focus", "use-indicator", "utils"] }, { name: "select", type: "registry:ui", title: "Select", description: "Single-choice listbox styled like Input, with groups, icons and disabled options. Native-select keyboard (arrows, typeahead even while closed), flips above the trigger near the viewport edge, submits and validates with the form.", exports: ["Select"], keywords: ["accessible", "also", "aria", "blocks", "called", "cancel", "choice", "closed", "controlled", "defaultvalue", "describedby", "disabled", "down", "end", "enter", "error", "esc", "field", "fill", "fills", "first", "flat", "for", "form", "from", "groups", "hint", "home", "htmlfor", "ids", "input", "invalid", "isn", "keyboard", "label", "labeled", "let", "like", "listbox", "matches", "name", "native", "needs", "nothing", "onvaluechange", "opening", "options", "pair", "pick", "picked", "placeholder", "render", "required", "select", "sets", "single", "space", "styled", "styling", "submit", "submitted", "text", "the", "there", "trigger", "typeahead", "uncontrolled", "value", "visible", "when", "while", "with"], registryDependencies: ["field", "form-value", "input", "list", "platform", "use-anchor", "use-controllable-state", "use-dismiss", "utils"] }, { name: "separator", type: "registry:ui", title: "Separator", description: 'Server-safe hairline, horizontal or vertical, decorative by default, with an optional centered label like "or".', exports: ["Separator"], keywords: ["assistive", "centered", "content", "decorative", "default", "direction", "false", "for", "horizontal", "label", "line", "only", "orientation", "purely", "separates", "set", "tech", "text", "the", "visual", "when"], registryDependencies: ["utils"] }, { name: "sidebar", type: "registry:ui", title: "Sidebar", description: "App navigation from data: header, groups (optionally foldable), badges and pinned footer entries. Collapses to an icon rail on a spring (⌘B / Ctrl+B) with labels kept for assistive tech; below md it is a left drawer.", exports: ["SidebarLayout", "Sidebar", "SidebarTrigger"], keywords: ["a11y", "accessible", "and", "aria", "below", "bottom", "but", "called", "client", "clip", "collapsed", "collapses", "controlled", "ctrl", "current", "data", "default", "defaultcollapsed", "drawer", "entries", "field", "footer", "for", "from", "groups", "header", "highlighted", "holds", "icon", "ignored", "item", "label", "labels", "landmark", "lays", "left", "links", "name", "navigation", "oncollapsedchange", "onselect", "page", "pair", "picked", "pinned", "rail", "routing", "settings", "shortcut", "side", "sidebar", "slot", "spring", "start", "state", "stay", "the", "toggle", "too", "top", "tree", "trigger", "true", "typing", "uncontrolled", "when", "while", "with", "workspace"], registryDependencies: ["button", "collapsible", "drawer", "platform", "use-controllable-state", "use-hotkey", "utils"] }, { name: "site-header", type: "registry:block", title: "Site Header", description: "Sticky, translucent site header from one nav array: brand, NavigationMenu panels from md up, a right Drawer with the same links below it, and your actions on the right. The hairline underneath appears only once content scrolls beneath it.", exports: ["SiteHeader"], keywords: ["accessible", "acme", "actions", "alike", "and", "appears", "aria", "below", "brand", "client", "close", "controls", "current", "demo", "desktop", "drawer", "every", "for", "from", "hairline", "hand", "header", "href", "label", "left", "level", "link", "links", "logo", "marks", "matching", "name", "nav", "navigation", "navigationmenu", "once", "onclick", "only", "onnavigate", "over", "page", "panels", "right", "routing", "same", "scrolls", "search", "shown", "sign", "site", "sticky", "stub", "the", "theme", "top", "under", "width", "with"], registryDependencies: ["button", "drawer", "navigation-menu", "use-disclosure", "utils"] }, { name: "skeleton", type: "registry:ui", title: "Skeleton", description: 'Server-safe loading placeholders that pulse (static under reduced motion). SkeletonGroup announces one "Loading" to screen readers.', exports: ["Skeleton", "SkeletonGroup"], keywords: ["announced", "block", "classes", "classname", "hear", "instead", "label", "loading", "nothing", "one", "placeholder", "readers", "screen", "shown", "size", "skeleton", "skeletons", "this", "while", "with", "wrap"], registryDependencies: ["utils"] }, { name: "slider", type: "registry:ui", title: "Slider", description: "Slider over native range inputs: thin accent track, white knob that lifts while held. One value or two for a range; keyboard, forms, touch and Field wiring come from the platform.", exports: ["Slider"], keywords: ["accent", "accessible", "and", "aren", "aria", "arrow", "blocks", "browser", "called", "defaults", "defaultvalue", "describedby", "disabled", "does", "drag", "dragging", "error", "every", "field", "fills", "filter", "first", "for", "forms", "from", "held", "highest", "hint", "ids", "increment", "inputs", "invalid", "keyboard", "keys", "knob", "labels", "lifts", "lowest", "max", "maximum", "min", "minimum", "name", "names", "native", "one", "onvaluechange", "over", "per", "price", "range", "readers", "render", "screen", "sets", "slider", "step", "styling", "submitted", "text", "that", "the", "thin", "thumb", "touch", "track", "twice", "two", "uncontrolled", "value", "values", "while", "white", "with", "work"], registryDependencies: ["field", "use-controllable-state", "utils"] }, { name: "spinner", type: "registry:ui", title: "Spinner", description: "Server-safe activity indicator: eight spokes in currentColor, three sizes, its own status label (or silent inside a parent that announces). Holds still under reduced motion.", exports: ["Spinner"], keywords: ["accessible", "activity", "already", "announces", "busy", "currentcolor", "diameter", "eight", "holds", "indicator", "label", "motion", "name", "parent", "pass", "reduced", "size", "spokes", "state", "still", "the", "under", "when"], registryDependencies: ["utils"] }, { name: "stat-tile", type: "registry:block", title: "Stat Tile", description: "Admin dashboard metric: value, a delta Badge that is green when the change is good news (invert for errors and latency), and an area Chart of the trend. Server-safe.", exports: ["StatTile"], keywords: ["admin", "and", "area", "axis", "badge", "below", "change", "chart", "dashboard", "data", "datakey", "delta", "down", "each", "errors", "for", "format", "formatted", "good", "green", "headline", "index", "intl", "invert", "key", "label", "last", "latency", "metric", "metrics", "month", "name", "news", "number", "period", "positive", "revenue", "row", "rows", "the", "tile", "tooltip", "trend", "unless", "value", "when", "where"], registryDependencies: ["badge", "card", "chart", "utils"] }, { name: "stats-strip", type: "registry:block", title: "Stats Strip", description: "Headline numbers, each card a <dl>: cards rise in turn with Reveal, then each number springs up with CountTo. Final values are in the HTML.", exports: ["StatsStrip"], keywords: ["card", "display", "each", "headline", "its", "number", "numbers", "order", "rises", "row", "springs", "stats", "then", "turn"], registryDependencies: ["card", "count-to", "reveal", "utils"] }, { name: "stepper", type: "registry:ui", title: "Stepper", description: "Multi-step form (questionnaire, checkout, setup wizard). Steps stay mounted so values survive Back and submit together; Continue checks only the current step's native constraints, plus an optional async onNext guard with a spinner. Progress header with springing connectors; works with onSubmit or a Server Action.", exports: ["Stepper", "StepperList", "StepperStep", "StepperFooter"], keywords: ["action", "after", "and", "are", "back", "backlabel", "button", "called", "check", "checks", "children", "connector", "constraints", "continue", "controlled", "current", "defaultvalue", "description", "every", "false", "fields", "fills", "finished", "first", "for", "form", "header", "hidden", "inactive", "its", "last", "links", "matches", "mounted", "multi", "native", "new", "nextlabel", "not", "one", "only", "onnext", "onsubmit", "onvaluechange", "optional", "order", "pass", "progress", "render", "resolve", "return", "runs", "server", "shown", "shows", "spinner", "spring", "start", "stay", "stays", "step", "steps", "submit", "submitlabel", "submits", "survive", "text", "the", "through", "title", "together", "uncontrolled", "unmounted", "value", "values", "while", "with"], registryDependencies: ["button", "use-controllable-state", "utils"] }, { name: "styles", type: "registry:file", exports: [], keywords: [], registryDependencies: [] }, { name: "switch", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", exports: ["Switch"], keywords: ["called", "checkbox", "checked", "controlled", "defaultchecked", "first", "knob", "native", "new", "oncheckedchange", "pair", "render", "role", "spring", "state", "switch", "the", "uncontrolled", "with"], registryDependencies: ["field", "use-controllable-state", "utils"] }, { name: "table", type: "registry:ui", title: "Table", description: "Server-safe semantic <table> in a scrolling card: header, body, footer and caption, striped rows, tinted selected rows, and sortable headers that set aria-sort and flip their arrow.", exports: ["Table", "TableHeader", "TableBody", "TableFooter", "TableRow", "TableHead", "TableCell", "TableCaption"], keywords: ["alternating", "and", "aria", "around", "arrow", "button", "called", "card", "classes", "column", "containerclassname", "data", "direction", "for", "header", "keyboard", "makes", "narrow", "onsort", "pass", "pressed", "real", "requires", "row", "screens", "scrollable", "scrolling", "scrolls", "selected", "sets", "shading", "shows", "sideways", "sort", "sortable", "state", "striped", "table", "that", "the", "then", "tint", "when"], registryDependencies: ["overflow-focus", "utils"] }, { name: "tabs", type: "registry:ui", title: "Tabs", description: "WAI-ARIA tabs with arrow, Home and End keys. One indicator springs between tabs, segmented or underline. Inactive panels stay in the DOM for SEO; the active one fades in.", exports: ["Tabs", "TabsList", "TabsTrigger", "TabsContent"], keywords: ["activated", "active", "always", "and", "aria", "arrows", "bar", "behind", "called", "children", "content", "controlled", "crawlable", "defaultvalue", "dom", "end", "first", "focus", "hidden", "home", "html", "inactive", "indicator", "labels", "matches", "move", "muted", "one", "onvaluechange", "pair", "panel", "panels", "pill", "remains", "render", "segmented", "springs", "stay", "stays", "tab", "tabs", "tabscontent", "tabstrigger", "the", "their", "track", "uncontrolled", "under", "underline", "value", "variant", "wai", "while", "with"], registryDependencies: ["list", "use-controllable-state", "use-indicator", "utils"] }, { name: "tag-input", type: "registry:ui", title: "Tag Input", description: "Free-form tags: Enter or a separator adds, Backspace removes the last, pasting a list adds them all. Duplicates, a max and a validate function keep bad tags in the input. Submits one entry per tag; required needs one.", exports: ["tagFieldClass", "Tag", "TagInput"], keywords: ["accessible", "add", "after", "allowed", "and", "animatepresence", "backspace", "besides", "button", "called", "children", "chip", "commit", "controlled", "defaultvalue", "disabled", "empty", "enter", "every", "false", "first", "form", "free", "hides", "input", "inside", "its", "keys", "last", "least", "limit", "list", "many", "max", "mode", "most", "name", "needs", "one", "onremove", "onvaluechange", "out", "pair", "paste", "pasted", "per", "poplayout", "press", "reject", "removable", "remove", "removes", "render", "rendered", "required", "return", "runs", "separator", "separators", "shows", "split", "springs", "stays", "stops", "submits", "tag", "tags", "text", "that", "the", "these", "too", "trimming", "type", "typed", "typing", "uncontrolled", "validate", "value", "when", "with"], registryDependencies: ["field", "form-value", "use-controllable-state", "utils"] }, { name: "terminal", type: "registry:ui", title: "Terminal", description: "Terminal window. Snippet mode types install commands once scrolled into view, with the full transcript kept in the DOM for SEO and no-JS; log mode streams lines into a live region that follows the bottom unless you scroll up, then offers a jump back. Status lines carry glyphs, copy grabs the commands.", exports: ["Terminal"], keywords: ["and", "append", "arrives", "back", "before", "body", "bodyclassname", "bottom", "character", "clipboard", "commands", "copy", "copytext", "defaults", "dom", "false", "follows", "for", "full", "hides", "install", "jump", "lines", "live", "log", "mode", "new", "offering", "once", "order", "output", "per", "prompt", "puts", "read", "reveals", "role", "scroll", "scrolling", "seo", "size", "snippet", "snippets", "speed", "stays", "sticks", "stream", "streams", "symbol", "terminal", "text", "the", "title", "typed", "types", "typing", "unless", "view", "what", "when", "window", "you"], registryDependencies: ["app-window", "code", "platform", "utils"] }, { name: "testimonial", type: "registry:block", title: "Testimonial", description: "A large Quote with its author, and everyone else who agrees as an overlapping AvatarGroup. Server-safe.", exports: ["Testimonial"], keywords: ["agrees", "and", "author", "avatar", "avatargroup", "behind", "beside", "carried", "children", "company", "crowd", "else", "everyone", "few", "first", "fold", "initials", "into", "its", "lang", "language", "large", "line", "one", "others", "otherslabel", "photo", "quote", "rest", "role", "said", "show", "stack", "streamers", "subtitle", "the", "trusted", "under", "url", "who", "with", "without"], registryDependencies: ["avatar", "avatar-group", "quote", "utils"] }, { name: "text-swap", type: "registry:ui", title: "TextSwap", description: "Spring-powered text morphing and ticker. Auto-cycles phrases or animates whenever children change with blur and slide transitions.", exports: ["TextSwap"], keywords: ["active", "array", "automatically", "between", "changing", "child", "children", "controlling", "cycle", "cycling", "default", "direction", "externally", "hover", "index", "interval", "items", "milliseconds", "mouse", "new", "pass", "pause", "pauseonhover", "single", "string", "swap", "swaps", "text", "the", "through", "true", "when"], registryDependencies: ["utils"] }, { name: "textarea", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", exports: ["Textarea"], keywords: ["auto", "cap", "content", "corner", "end", "field", "firefox", "grows", "max", "maxlength", "near", "needs", "red", "resizable", "show", "showcount", "sizing", "the", "then", "turns", "unsupported", "used", "warning", "where", "with"], registryDependencies: ["field", "input", "utils"] }, { name: "theme", type: "registry:lib", description: "Retints primary, focus rings, selection and switches app-wide. Persists like the theme.", exports: ["isDark", "setTheme", "subscribeTheme", "accents", "getAccent", "setAccent", "themeScript"], keywords: ["and", "app", "focus", "like", "persists", "primary", "retints", "rings", "selection", "switches", "the", "theme", "wide"], registryDependencies: [] }, { name: "theme-toggle", type: "registry:ui", title: "Theme Toggle", description: "Light/dark icon switch with spring rotate-swap; follows the OS until first use.", exports: ["ThemeToggle"], keywords: ["button", "click", "first", "follows", "icon", "moon", "rotate", "spring", "sun", "swap", "until"], registryDependencies: ["button", "theme", "utils"] }, { name: "timeline", type: "registry:ui", title: "Timeline", description: "Server-safe ordered list of events on a rail: icon or dot markers with status colors, machine-readable <time>, title and an optional body. For changelogs, experience and mod logs.", exports: ["Timeline", "TimelineItem"], keywords: ["above", "and", "body", "children", "color", "date", "datetime", "dot", "event", "events", "first", "for", "headline", "icon", "inside", "list", "machine", "marker", "newest", "oldest", "one", "optional", "order", "ordered", "pair", "rail", "readable", "render", "shown", "small", "the", "then", "time", "title", "variant", "with", "without", "yours"], registryDependencies: ["utils"] }, { name: "toast", type: "registry:ui", title: "Toast", description: "Stacked notifications. Hover to expand, swipe to dismiss, timers pause on hover.", exports: ["toast", "Toaster"], keywords: ["action", "description", "dismiss", "dismisses", "duration", "error", "expands", "focus", "hover", "layout", "loading", "mount", "once", "promise", "root", "saved", "stacked", "success", "swipe", "tap", "title", "toast", "toasts"], registryDependencies: ["use-dismiss", "utils"] }, { name: "toggle-group", type: "registry:ui", title: "Toggle Group", description: "Segmented control of toggle buttons. Single mode slides one pill to the pressed item (view mode, billing period); multiple mode toggles items independently (bold, italic). Arrow keys move between items.", exports: ["ToggleGroup", "ToggleGroupItem"], keywords: ["always", "and", "aria", "arrow", "behind", "between", "billing", "bold", "buttons", "called", "choice", "control", "controlled", "defaultvalue", "end", "every", "filters", "first", "height", "home", "icon", "independent", "italic", "item", "items", "keys", "label", "labels", "mode", "move", "multiple", "need", "one", "only", "onvaluechange", "pair", "period", "pill", "pressed", "render", "reported", "segment", "segmented", "set", "single", "size", "slides", "springs", "the", "time", "toggle", "toggles", "type", "uncontrolled", "value", "values", "view", "when", "with"], registryDependencies: ["list", "use-controllable-state", "use-indicator", "utils"] }, { name: "tooltip", type: "registry:ui", title: "Tooltip", description: "Short label on hover or keyboard focus, also wired as the trigger's aria-describedby. Warm-up: once one opens, neighbors open instantly. Hoverable, Esc dismisses, ignores touch.", exports: ["Tooltip"], keywords: ["accessible", "action", "align", "alignment", "also", "and", "aria", "becomes", "before", "button", "children", "content", "control", "default", "delay", "description", "dismisses", "element", "esc", "first", "flips", "focus", "focusable", "for", "hold", "hover", "how", "icon", "keyboard", "label", "one", "open", "opens", "own", "pointer", "preferred", "press", "reaches", "room", "short", "shows", "side", "stays", "still", "tap", "text", "the", "their", "there", "toggles", "tooltip", "touch", "trigger", "triggers", "wcag", "when", "while", "with"], registryDependencies: ["platform", "use-anchor", "use-dismiss", "use-hover-open", "utils"] }, { name: "typewriter", type: "registry:ui", title: "Typewriter", description: "Types out text character-by-character with optional phrase cycling and pulsing cursor. Full copy stays in the DOM for SEO.", exports: ["Typewriter"], keywords: ["and", "array", "before", "blinking", "called", "character", "classes", "cursor", "cursorchar", "cursorclassname", "custom", "cycle", "default", "delay", "deletespeed", "deleting", "dom", "effect", "for", "full", "fully", "headers", "hero", "loop", "milliseconds", "once", "oncomplete", "pause", "pauseduration", "per", "phrase", "phrases", "quotes", "seo", "show", "speed", "starting", "stays", "text", "the", "through", "true", "type", "typed", "typewriter", "typing", "wait", "when", "whether", "while"], registryDependencies: ["platform", "utils"] }, { name: "use-anchor", type: "registry:hook", description: `Fixed-position coordinates for a floating panel next to its anchor: on the preferred side,
|
|
17188
17596
|
flipped when it doesn't fit, clamped to the viewport, kept in place on scroll and resize.
|
|
17189
17597
|
Measures before paint, so nothing jumps.`, exports: ["useAnchor"], keywords: ["anchor", "and", "before", "clamped", "coordinates", "doesn", "fit", "fixed", "flipped", "floating", "for", "its", "jumps", "kept", "measures", "next", "nothing", "paint", "panel", "place", "position", "preferred", "resize", "scroll", "side", "the", "viewport", "when"], registryDependencies: ["utils"] }, { name: "use-controllable-state", type: "registry:hook", description: "State a parent may own. Controlled while `value` is defined; otherwise kept here, seeded from\n`defaultValue`. The setter always reports through `onChange`, so both modes stay observable.", exports: ["useControllableState"], keywords: ["always", "both", "controlled", "defaultvalue", "defined", "from", "here", "kept", "may", "modes", "observable", "onchange", "otherwise", "own", "parent", "reports", "seeded", "setter", "state", "stay", "the", "through", "value", "while"], registryDependencies: [] }, { name: "use-disclosure", type: "registry:hook", description: "Open state for any overlay. Spread `props` onto Dialog, Drawer, Popover or DropdownMenu and\nopen it from anywhere: `const drawer = useDisclosure(); <Drawer {...drawer.props}>`, `drawer.show()`.", exports: ["useDisclosure"], keywords: ["and", "any", "anywhere", "const", "dialog", "drawer", "dropdownmenu", "for", "from", "onto", "open", "overlay", "popover", "props", "show", "spread", "state", "usedisclosure"], registryDependencies: [] }, { name: "use-dismiss", type: "registry:hook", description: "Inside one of `refs` (panel first, then its trigger), or inside a floating child opened from\nthe panel: a Select list or menu portals out, but it is marked `data-floating`.", exports: ["isWithin", "useDismiss"], keywords: ["and", "but", "child", "children", "closes", "data", "first", "floating", "from", "inside", "its", "list", "marked", "menu", "one", "opened", "out", "outside", "panel", "pointerdown", "portals", "refs", "select", "the", "then", "trigger"], registryDependencies: [] }, { name: "use-hotkey", type: "registry:hook", description: '`"mod+k"`: ⌘K on Mac, Ctrl+K elsewhere, without Alt. `"/"`: the key alone, no modifiers.', exports: ["matchesHotkey", "useHotkey"], keywords: ["alone", "alt", "browser", "ctrl", "disable", "elsewhere", "false", "handler", "key", "keyboard", "leave", "mac", "mod", "modifiers", "pass", "preventdefault", "returns", "shortcut", "the", "undefined", "wide", "window", "without"], registryDependencies: [] }, { name: "use-hover-open", type: "registry:hook", description: "Open-on-hover state with intent delays, shared by Tooltip and HoverCard. Mouse only (touch\ntaps go to the trigger's own action); keyboard focus opens it too. Spread `trigger` on the\nanchor and `floating` on the panel so moving between them keeps it open.", exports: ["useHoverOpen"], keywords: ["action", "anchor", "and", "between", "delays", "floating", "focus", "hover", "hovercard", "intent", "keeps", "keyboard", "mouse", "moving", "only", "open", "opens", "own", "panel", "shared", "spread", "state", "taps", "the", "them", "too", "tooltip", "touch", "trigger", "with"], registryDependencies: [] }, { name: "use-indicator", type: "registry:hook", description: `Offset and width of the active item, for one indicator that springs between items (Tabs,
|
|
17190
17598
|
ToggleGroup). Re-measures when the item or its row resizes (font swap, wrap), or the pill
|
|
@@ -17284,7 +17692,7 @@ async function fetchAllItems(source) {
|
|
|
17284
17692
|
// jsr.json
|
|
17285
17693
|
var jsr_default = {
|
|
17286
17694
|
name: "@kznlabs/labsui",
|
|
17287
|
-
version: "0.1.
|
|
17695
|
+
version: "0.1.7",
|
|
17288
17696
|
license: "MIT",
|
|
17289
17697
|
exports: "./src/index.ts",
|
|
17290
17698
|
publish: {
|
|
@@ -18129,6 +18537,9 @@ function printView(views) {
|
|
|
18129
18537
|
console.log(md.replace(/^(#{2,3} .*)$/gm, (line) => bold(line)));
|
|
18130
18538
|
}
|
|
18131
18539
|
|
|
18540
|
+
// src/mcp.ts
|
|
18541
|
+
import { fileURLToPath } from "node:url";
|
|
18542
|
+
|
|
18132
18543
|
// src/rules.ts
|
|
18133
18544
|
var rules = (alias = "@/") => `# labsUI
|
|
18134
18545
|
|
|
@@ -18417,6 +18828,26 @@ function serve(ctx) {
|
|
|
18417
18828
|
`);
|
|
18418
18829
|
let queue = Promise.resolve();
|
|
18419
18830
|
let buffer = "";
|
|
18831
|
+
let usesRoots = false;
|
|
18832
|
+
let sent = 0;
|
|
18833
|
+
const waiting = new Map;
|
|
18834
|
+
const ask = (method) => new Promise((resolve) => {
|
|
18835
|
+
const id = `labsui-${++sent}`;
|
|
18836
|
+
const settle = (result) => {
|
|
18837
|
+
clearTimeout(timer);
|
|
18838
|
+
waiting.delete(id);
|
|
18839
|
+
resolve(result);
|
|
18840
|
+
};
|
|
18841
|
+
const timer = setTimeout(() => settle(undefined), 5000);
|
|
18842
|
+
waiting.set(id, settle);
|
|
18843
|
+
write({ jsonrpc: "2.0", id, method });
|
|
18844
|
+
});
|
|
18845
|
+
const readRoots = async () => {
|
|
18846
|
+
const roots = (await ask("roots/list"))?.roots;
|
|
18847
|
+
const uri = roots?.find((root) => root.uri?.startsWith("file:"))?.uri;
|
|
18848
|
+
if (uri)
|
|
18849
|
+
ctx.cwd = fileURLToPath(uri);
|
|
18850
|
+
};
|
|
18420
18851
|
const onLine = (line) => {
|
|
18421
18852
|
let msg;
|
|
18422
18853
|
try {
|
|
@@ -18424,6 +18855,15 @@ function serve(ctx) {
|
|
|
18424
18855
|
} catch {
|
|
18425
18856
|
return write({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
|
18426
18857
|
}
|
|
18858
|
+
if (!Array.isArray(msg) && msg.method === undefined)
|
|
18859
|
+
return waiting.get(String(msg.id))?.(msg.result);
|
|
18860
|
+
if (!Array.isArray(msg) && !ctx.pinned) {
|
|
18861
|
+
if (msg.method === "initialize")
|
|
18862
|
+
usesRoots = Boolean(msg.params?.capabilities?.roots);
|
|
18863
|
+
const changed = msg.method === "notifications/initialized" || msg.method === "notifications/roots/list_changed";
|
|
18864
|
+
if (usesRoots && changed)
|
|
18865
|
+
queue = queue.then(readRoots);
|
|
18866
|
+
}
|
|
18427
18867
|
queue = queue.then(async () => {
|
|
18428
18868
|
try {
|
|
18429
18869
|
if (Array.isArray(msg)) {
|
|
@@ -18564,7 +19004,7 @@ async function main() {
|
|
|
18564
19004
|
case "rules":
|
|
18565
19005
|
return console.log(rules(readConfig(cwd)?.alias));
|
|
18566
19006
|
case "mcp":
|
|
18567
|
-
return serve({ cwd, registry, version: VERSION });
|
|
19007
|
+
return serve({ cwd, pinned: values.cwd !== undefined, registry, version: VERSION });
|
|
18568
19008
|
default:
|
|
18569
19009
|
throw new CliError(`Unknown command "${command}". Run labsui --help.`);
|
|
18570
19010
|
}
|