@zedtech/zedkit-ui 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -9
- package/dist/alert.d.ts.map +1 -1
- package/dist/alert.js +1 -0
- package/dist/alert.js.map +1 -1
- package/dist/badge.d.ts.map +1 -1
- package/dist/badge.js.map +1 -1
- package/dist/button.d.ts.map +1 -1
- package/dist/button.js +1 -0
- package/dist/button.js.map +1 -1
- package/dist/card-skeleton.d.ts +10 -0
- package/dist/card-skeleton.d.ts.map +1 -0
- package/dist/card-skeleton.js +11 -0
- package/dist/card-skeleton.js.map +1 -0
- package/dist/card.d.ts.map +1 -1
- package/dist/card.js +1 -0
- package/dist/card.js.map +1 -1
- package/dist/checkbox.d.ts.map +1 -1
- package/dist/checkbox.js +1 -0
- package/dist/checkbox.js.map +1 -1
- package/dist/checkmark.d.ts +11 -0
- package/dist/checkmark.d.ts.map +1 -0
- package/dist/checkmark.js +12 -0
- package/dist/checkmark.js.map +1 -0
- package/dist/data-table-skeleton.d.ts +14 -0
- package/dist/data-table-skeleton.d.ts.map +1 -0
- package/dist/data-table-skeleton.js +13 -0
- package/dist/data-table-skeleton.js.map +1 -0
- package/dist/detail-panel-skeleton.d.ts +11 -0
- package/dist/detail-panel-skeleton.d.ts.map +1 -0
- package/dist/detail-panel-skeleton.js +11 -0
- package/dist/detail-panel-skeleton.js.map +1 -0
- package/dist/dialog.d.ts.map +1 -1
- package/dist/dialog.js +3 -2
- package/dist/dialog.js.map +1 -1
- package/dist/drawer.d.ts +30 -0
- package/dist/drawer.d.ts.map +1 -0
- package/dist/drawer.js +35 -0
- package/dist/drawer.js.map +1 -0
- package/dist/field.d.ts.map +1 -1
- package/dist/field.js +1 -0
- package/dist/field.js.map +1 -1
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +1 -0
- package/dist/form.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/input.d.ts.map +1 -1
- package/dist/input.js +1 -0
- package/dist/input.js.map +1 -1
- package/dist/label.d.ts.map +1 -1
- package/dist/label.js +1 -0
- package/dist/label.js.map +1 -1
- package/dist/live-pulse.d.ts.map +1 -1
- package/dist/live-pulse.js +2 -1
- package/dist/live-pulse.js.map +1 -1
- package/dist/page-header-skeleton.d.ts +12 -0
- package/dist/page-header-skeleton.d.ts.map +1 -0
- package/dist/page-header-skeleton.js +11 -0
- package/dist/page-header-skeleton.js.map +1 -0
- package/dist/progress-bar.d.ts +16 -0
- package/dist/progress-bar.d.ts.map +1 -0
- package/dist/progress-bar.js +16 -0
- package/dist/progress-bar.js.map +1 -0
- package/dist/select.d.ts.map +1 -1
- package/dist/select.js +1 -0
- package/dist/select.js.map +1 -1
- package/dist/separator.d.ts.map +1 -1
- package/dist/separator.js +1 -0
- package/dist/separator.js.map +1 -1
- package/dist/skeleton.d.ts.map +1 -1
- package/dist/skeleton.js +2 -1
- package/dist/skeleton.js.map +1 -1
- package/dist/sonner.d.ts.map +1 -1
- package/dist/sonner.js +1 -0
- package/dist/sonner.js.map +1 -1
- package/dist/status-dot.d.ts +13 -0
- package/dist/status-dot.d.ts.map +1 -0
- package/dist/status-dot.js +20 -0
- package/dist/status-dot.js.map +1 -0
- package/dist/status-label.d.ts +13 -0
- package/dist/status-label.d.ts.map +1 -0
- package/dist/status-label.js +30 -0
- package/dist/status-label.js.map +1 -0
- package/dist/switch.d.ts.map +1 -1
- package/dist/switch.js +1 -0
- package/dist/switch.js.map +1 -1
- package/dist/table.d.ts.map +1 -1
- package/dist/table.js.map +1 -1
- package/dist/tabs.d.ts.map +1 -1
- package/dist/tabs.js +1 -0
- package/dist/tabs.js.map +1 -1
- package/dist/textarea.d.ts.map +1 -1
- package/dist/textarea.js +1 -0
- package/dist/textarea.js.map +1 -1
- package/dist/updated-ago.d.ts +13 -0
- package/dist/updated-ago.d.ts.map +1 -0
- package/dist/updated-ago.js +29 -0
- package/dist/updated-ago.js.map +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +1 -0
- package/dist/utils.js.map +1 -1
- package/package.json +23 -3
- package/src/alert.tsx +1 -0
- package/src/badge.tsx +1 -0
- package/src/button.tsx +1 -0
- package/src/card-skeleton.tsx +31 -0
- package/src/card.tsx +1 -0
- package/src/checkbox.tsx +1 -0
- package/src/checkmark.tsx +34 -0
- package/src/data-table-skeleton.tsx +53 -0
- package/src/detail-panel-skeleton.tsx +29 -0
- package/src/dialog.tsx +9 -1
- package/src/drawer.tsx +140 -0
- package/src/field.tsx +1 -0
- package/src/form.tsx +1 -0
- package/src/index.ts +22 -0
- package/src/input.tsx +1 -0
- package/src/label.tsx +1 -0
- package/src/live-pulse.tsx +2 -8
- package/src/page-header-skeleton.tsx +39 -0
- package/src/progress-bar.tsx +45 -0
- package/src/select.tsx +1 -0
- package/src/separator.tsx +1 -0
- package/src/skeleton.tsx +2 -1
- package/src/sonner.tsx +1 -0
- package/src/status-dot.tsx +42 -0
- package/src/status-label.tsx +53 -0
- package/src/styles.css +76 -0
- package/src/switch.tsx +1 -0
- package/src/table.tsx +1 -0
- package/src/tabs.tsx +1 -0
- package/src/textarea.tsx +1 -0
- package/src/updated-ago.tsx +38 -0
- package/src/utils.ts +1 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Skeleton } from "./skeleton.js";
|
|
2
|
+
import { cn } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
export interface CardSkeletonProps {
|
|
5
|
+
label?: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Faithful loading shape of Card: header block plus three text lines
|
|
11
|
+
* in the same bordered box, so content lands without shifting.
|
|
12
|
+
*/
|
|
13
|
+
export function CardSkeleton({ label = "Loading", className }: CardSkeletonProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
role="status"
|
|
17
|
+
aria-label={label}
|
|
18
|
+
className={cn("rounded-xl border bg-card p-6 shadow", className)}
|
|
19
|
+
>
|
|
20
|
+
<div className="space-y-1.5">
|
|
21
|
+
<Skeleton className="h-5 w-1/3" />
|
|
22
|
+
<Skeleton className="h-4 w-2/3" />
|
|
23
|
+
</div>
|
|
24
|
+
<div className="mt-4 space-y-2">
|
|
25
|
+
<Skeleton className="h-4 w-full" />
|
|
26
|
+
<Skeleton className="h-4 w-full" />
|
|
27
|
+
<Skeleton className="h-4 w-4/5" />
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
package/src/card.tsx
CHANGED
package/src/checkbox.tsx
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { cn } from "./utils.js";
|
|
2
|
+
|
|
3
|
+
export interface CheckmarkProps {
|
|
4
|
+
/** Accessible name; omitted means decorative (pair with visible text). */
|
|
5
|
+
label?: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Success check that draws itself once (static full check under
|
|
11
|
+
* reduced motion — see `zk-check-draw` in styles.css).
|
|
12
|
+
*/
|
|
13
|
+
export function Checkmark({ label, className }: CheckmarkProps) {
|
|
14
|
+
return (
|
|
15
|
+
<svg
|
|
16
|
+
viewBox="0 0 16 16"
|
|
17
|
+
fill="none"
|
|
18
|
+
{...(label === undefined
|
|
19
|
+
? { "aria-hidden": true as const }
|
|
20
|
+
: { role: "img" as const, "aria-label": label })}
|
|
21
|
+
className={cn("size-4 text-success", className)}
|
|
22
|
+
>
|
|
23
|
+
<circle cx="8" cy="8" r="7" stroke="currentColor" strokeWidth="1.5" />
|
|
24
|
+
<path
|
|
25
|
+
d="M5.5 8.2 7.2 9.9 10.6 6.3"
|
|
26
|
+
stroke="currentColor"
|
|
27
|
+
strokeWidth="1.8"
|
|
28
|
+
strokeLinecap="round"
|
|
29
|
+
strokeLinejoin="round"
|
|
30
|
+
className="[stroke-dasharray:8] motion-safe:animate-[zk-check-draw_0.45s_ease-out_forwards]"
|
|
31
|
+
/>
|
|
32
|
+
</svg>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Skeleton } from "./skeleton.js";
|
|
2
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./table.js";
|
|
3
|
+
import { cn } from "./utils.js";
|
|
4
|
+
|
|
5
|
+
export interface DataTableSkeletonProps {
|
|
6
|
+
label?: string;
|
|
7
|
+
columns?: number;
|
|
8
|
+
rows?: number;
|
|
9
|
+
filter?: boolean;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Standalone loading shape of DataTable for Suspense fallbacks: filter
|
|
15
|
+
* slot plus a headed table of skeleton rows. (DataTable's own inline
|
|
16
|
+
* loading state stays untouched inside the table body.)
|
|
17
|
+
*/
|
|
18
|
+
export function DataTableSkeleton({
|
|
19
|
+
label = "Loading",
|
|
20
|
+
columns = 4,
|
|
21
|
+
rows = 3,
|
|
22
|
+
filter = true,
|
|
23
|
+
className,
|
|
24
|
+
}: DataTableSkeletonProps) {
|
|
25
|
+
return (
|
|
26
|
+
<div role="status" aria-label={label} className={cn("space-y-4", className)}>
|
|
27
|
+
{filter ? <Skeleton className="h-9 max-w-sm" /> : null}
|
|
28
|
+
{/* Decorative shape only — the zone label above carries the meaning. */}
|
|
29
|
+
<Table aria-hidden>
|
|
30
|
+
<TableHeader>
|
|
31
|
+
<TableRow>
|
|
32
|
+
{Array.from({ length: columns }, (_, index) => (
|
|
33
|
+
<TableHead key={index}>
|
|
34
|
+
<Skeleton className="h-4 w-2/3" />
|
|
35
|
+
</TableHead>
|
|
36
|
+
))}
|
|
37
|
+
</TableRow>
|
|
38
|
+
</TableHeader>
|
|
39
|
+
<TableBody>
|
|
40
|
+
{Array.from({ length: rows }, (_, row) => (
|
|
41
|
+
<TableRow key={row}>
|
|
42
|
+
{Array.from({ length: columns }, (_, column) => (
|
|
43
|
+
<TableCell key={column}>
|
|
44
|
+
<Skeleton className="h-4 w-full" />
|
|
45
|
+
</TableCell>
|
|
46
|
+
))}
|
|
47
|
+
</TableRow>
|
|
48
|
+
))}
|
|
49
|
+
</TableBody>
|
|
50
|
+
</Table>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Skeleton } from "./skeleton.js";
|
|
2
|
+
import { cn } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
export interface DetailPanelSkeletonProps {
|
|
5
|
+
label?: string;
|
|
6
|
+
rows?: number;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Faithful loading shape of DetailPanel: label/value skeleton pairs
|
|
12
|
+
* in the same divided box, so rows land without shifting.
|
|
13
|
+
*/
|
|
14
|
+
export function DetailPanelSkeleton({ label = "Loading", rows = 4, className }: DetailPanelSkeletonProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
role="status"
|
|
18
|
+
aria-label={label}
|
|
19
|
+
className={cn("divide-y divide-border rounded-lg border", className)}
|
|
20
|
+
>
|
|
21
|
+
{Array.from({ length: rows }, (_, index) => (
|
|
22
|
+
<div key={index} className="grid grid-cols-3 gap-4 px-4 py-3">
|
|
23
|
+
<Skeleton className="h-4 w-20" />
|
|
24
|
+
<Skeleton className="col-span-2 h-4 w-2/3" />
|
|
25
|
+
</div>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
package/src/dialog.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Derived from shadcn/ui (dialog) — see Provenance in packages/ui/README.md. Tolerated exception: migrated to custom code only when customization needs it.
|
|
1
2
|
"use client";
|
|
2
3
|
|
|
3
4
|
import * as React from "react";
|
|
@@ -16,7 +17,12 @@ const DialogOverlay = React.forwardRef<
|
|
|
16
17
|
>(({ className, ...props }, ref) => (
|
|
17
18
|
<DialogPrimitive.Overlay
|
|
18
19
|
ref={ref}
|
|
19
|
-
className={cn(
|
|
20
|
+
className={cn(
|
|
21
|
+
"fixed inset-0 z-50 bg-overlay/80",
|
|
22
|
+
"motion-safe:data-[state=open]:animate-[zk-fade-in_var(--motion-base)_var(--ease-enter)]",
|
|
23
|
+
"motion-safe:data-[state=closed]:animate-[zk-fade-out_var(--motion-base)_var(--ease-exit)]",
|
|
24
|
+
className,
|
|
25
|
+
)}
|
|
20
26
|
{...props}
|
|
21
27
|
/>
|
|
22
28
|
));
|
|
@@ -32,6 +38,8 @@ const DialogContent = React.forwardRef<
|
|
|
32
38
|
ref={ref}
|
|
33
39
|
className={cn(
|
|
34
40
|
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg sm:rounded-lg",
|
|
41
|
+
"motion-safe:data-[state=open]:animate-[zk-zoom-in_var(--motion-base)_var(--ease-enter)]",
|
|
42
|
+
"motion-safe:data-[state=closed]:animate-[zk-zoom-out_var(--motion-base)_var(--ease-exit)]",
|
|
35
43
|
className,
|
|
36
44
|
)}
|
|
37
45
|
{...props}
|
package/src/drawer.tsx
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Derived from shadcn/ui (drawer) — see Provenance in packages/ui/README.md. Adapted: direction-aware
|
|
2
|
+
// positioning via data variants (vaul sets data-vaul-drawer-direction on the content), drag handle and
|
|
3
|
+
// close left to the consumer to compose, ref-as-prop (React 19). Vaul injects its own base CSS
|
|
4
|
+
// (drag transitions, open/close keyframes) at import; the kit adds no drawer CSS.
|
|
5
|
+
"use client";
|
|
6
|
+
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
import { Drawer as DrawerPrimitive } from "vaul";
|
|
9
|
+
import { cn } from "./utils.js";
|
|
10
|
+
|
|
11
|
+
const Drawer = DrawerPrimitive.Root;
|
|
12
|
+
const DrawerTrigger = DrawerPrimitive.Trigger;
|
|
13
|
+
const DrawerClose = DrawerPrimitive.Close;
|
|
14
|
+
const DrawerPortal = DrawerPrimitive.Portal;
|
|
15
|
+
|
|
16
|
+
function DrawerOverlay({
|
|
17
|
+
className,
|
|
18
|
+
ref,
|
|
19
|
+
...props
|
|
20
|
+
}: React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay> & {
|
|
21
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
22
|
+
}) {
|
|
23
|
+
return (
|
|
24
|
+
<DrawerPrimitive.Overlay
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn("fixed inset-0 z-50 bg-overlay/80", className)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function DrawerContent({
|
|
33
|
+
className,
|
|
34
|
+
children,
|
|
35
|
+
ref,
|
|
36
|
+
modal = true,
|
|
37
|
+
...props
|
|
38
|
+
}: React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> & {
|
|
39
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
40
|
+
/**
|
|
41
|
+
* Mirror of the `Drawer` root `modal` prop: `false` renders no
|
|
42
|
+
* overlay so the background stays interactive. Defaults to `true`.
|
|
43
|
+
*/
|
|
44
|
+
modal?: boolean;
|
|
45
|
+
}) {
|
|
46
|
+
return (
|
|
47
|
+
<DrawerPortal>
|
|
48
|
+
{modal ? <DrawerOverlay /> : null}
|
|
49
|
+
<DrawerPrimitive.Content
|
|
50
|
+
ref={ref}
|
|
51
|
+
className={cn(
|
|
52
|
+
"fixed z-50 flex flex-col border bg-background",
|
|
53
|
+
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:max-h-[96dvh] data-[vaul-drawer-direction=bottom]:rounded-t-xl",
|
|
54
|
+
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:max-h-[96dvh] data-[vaul-drawer-direction=top]:rounded-b-xl",
|
|
55
|
+
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-full data-[vaul-drawer-direction=right]:sm:max-w-md",
|
|
56
|
+
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-full data-[vaul-drawer-direction=left]:sm:max-w-md",
|
|
57
|
+
className,
|
|
58
|
+
)}
|
|
59
|
+
{...props}
|
|
60
|
+
>
|
|
61
|
+
{children}
|
|
62
|
+
</DrawerPrimitive.Content>
|
|
63
|
+
</DrawerPortal>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function DrawerHandle({
|
|
68
|
+
className,
|
|
69
|
+
ref,
|
|
70
|
+
...props
|
|
71
|
+
}: React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Handle> & {
|
|
72
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
73
|
+
}) {
|
|
74
|
+
return (
|
|
75
|
+
<DrawerPrimitive.Handle
|
|
76
|
+
ref={ref}
|
|
77
|
+
className={cn("mx-auto mt-4 h-1.5 w-12 shrink-0 rounded-full bg-muted", className)}
|
|
78
|
+
{...props}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function DrawerHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
className={cn("flex flex-col gap-1.5 p-4 text-center sm:text-left", className)}
|
|
87
|
+
{...props}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function DrawerFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
93
|
+
return <div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function DrawerTitle({
|
|
97
|
+
className,
|
|
98
|
+
ref,
|
|
99
|
+
...props
|
|
100
|
+
}: React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title> & {
|
|
101
|
+
ref?: React.Ref<HTMLHeadingElement>;
|
|
102
|
+
}) {
|
|
103
|
+
return (
|
|
104
|
+
<DrawerPrimitive.Title
|
|
105
|
+
ref={ref}
|
|
106
|
+
className={cn("text-lg leading-none font-semibold tracking-tight", className)}
|
|
107
|
+
{...props}
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function DrawerDescription({
|
|
113
|
+
className,
|
|
114
|
+
ref,
|
|
115
|
+
...props
|
|
116
|
+
}: React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description> & {
|
|
117
|
+
ref?: React.Ref<HTMLParagraphElement>;
|
|
118
|
+
}) {
|
|
119
|
+
return (
|
|
120
|
+
<DrawerPrimitive.Description
|
|
121
|
+
ref={ref}
|
|
122
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
123
|
+
{...props}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export {
|
|
129
|
+
Drawer,
|
|
130
|
+
DrawerClose,
|
|
131
|
+
DrawerContent,
|
|
132
|
+
DrawerDescription,
|
|
133
|
+
DrawerFooter,
|
|
134
|
+
DrawerHandle,
|
|
135
|
+
DrawerHeader,
|
|
136
|
+
DrawerOverlay,
|
|
137
|
+
DrawerPortal,
|
|
138
|
+
DrawerTitle,
|
|
139
|
+
DrawerTrigger,
|
|
140
|
+
};
|
package/src/field.tsx
CHANGED
package/src/form.tsx
CHANGED
package/src/index.ts
CHANGED
|
@@ -24,6 +24,19 @@ export {
|
|
|
24
24
|
DialogTrigger,
|
|
25
25
|
} from "./dialog.js";
|
|
26
26
|
export { DetailPanel, type DetailItem, type DetailPanelProps } from "./detail-panel.js";
|
|
27
|
+
export {
|
|
28
|
+
Drawer,
|
|
29
|
+
DrawerClose,
|
|
30
|
+
DrawerContent,
|
|
31
|
+
DrawerDescription,
|
|
32
|
+
DrawerFooter,
|
|
33
|
+
DrawerHandle,
|
|
34
|
+
DrawerHeader,
|
|
35
|
+
DrawerOverlay,
|
|
36
|
+
DrawerPortal,
|
|
37
|
+
DrawerTitle,
|
|
38
|
+
DrawerTrigger,
|
|
39
|
+
} from "./drawer.js";
|
|
27
40
|
export {
|
|
28
41
|
Field,
|
|
29
42
|
FieldContent,
|
|
@@ -52,6 +65,11 @@ export { Input } from "./input.js";
|
|
|
52
65
|
export { Label } from "./label.js";
|
|
53
66
|
export { Section, Stack, stackVariants, type SectionProps, type StackProps } from "./layout.js";
|
|
54
67
|
export { LivePulse, type LivePulseProps } from "./live-pulse.js";
|
|
68
|
+
export { StatusDot, type StatusDotProps, type StatusDotTone } from "./status-dot.js";
|
|
69
|
+
export { ProgressBar, type ProgressBarProps } from "./progress-bar.js";
|
|
70
|
+
export { Checkmark, type CheckmarkProps } from "./checkmark.js";
|
|
71
|
+
export { StatusLabel, type StatusLabelProps, type StatusLabelTone } from "./status-label.js";
|
|
72
|
+
export { UpdatedAgo, type UpdatedAgoProps } from "./updated-ago.js";
|
|
55
73
|
export { OptionCards, type OptionCard, type OptionCardsProps } from "./option-cards.js";
|
|
56
74
|
export { PageHeader, type PageHeaderProps } from "./page-header.js";
|
|
57
75
|
export { SecretField, type SecretFieldProps } from "./secret-field.js";
|
|
@@ -93,3 +111,7 @@ export {
|
|
|
93
111
|
type TextProps,
|
|
94
112
|
} from "./typography.js";
|
|
95
113
|
export { cn } from "./utils.js";
|
|
114
|
+
export { CardSkeleton, type CardSkeletonProps } from "./card-skeleton.js";
|
|
115
|
+
export { DataTableSkeleton, type DataTableSkeletonProps } from "./data-table-skeleton.js";
|
|
116
|
+
export { DetailPanelSkeleton, type DetailPanelSkeletonProps } from "./detail-panel-skeleton.js";
|
|
117
|
+
export { PageHeaderSkeleton, type PageHeaderSkeletonProps } from "./page-header-skeleton.js";
|
package/src/input.tsx
CHANGED
package/src/label.tsx
CHANGED
package/src/live-pulse.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { StatusDot } from "./status-dot.js";
|
|
1
2
|
import { cn } from "./utils.js";
|
|
2
3
|
|
|
3
4
|
export interface LivePulseProps {
|
|
@@ -13,14 +14,7 @@ export function LivePulse({ active = true, label = "Live", className }: LivePuls
|
|
|
13
14
|
aria-label={active ? label : "Idle"}
|
|
14
15
|
className={cn("inline-flex items-center gap-1.5 text-xs text-muted-foreground", className)}
|
|
15
16
|
>
|
|
16
|
-
<
|
|
17
|
-
{active ? (
|
|
18
|
-
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-success opacity-60" />
|
|
19
|
-
) : null}
|
|
20
|
-
<span
|
|
21
|
-
className={cn("relative inline-flex size-2 rounded-full", active ? "bg-success" : "bg-muted-foreground/40")}
|
|
22
|
-
/>
|
|
23
|
-
</span>
|
|
17
|
+
<StatusDot tone={active ? "success" : "idle"} pulse={active} />
|
|
24
18
|
{active ? label : "Idle"}
|
|
25
19
|
</span>
|
|
26
20
|
);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Skeleton } from "./skeleton.js";
|
|
2
|
+
import { cn } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
export interface PageHeaderSkeletonProps {
|
|
5
|
+
label?: string;
|
|
6
|
+
description?: boolean;
|
|
7
|
+
actions?: boolean;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Faithful loading shape of PageHeader: title, optional description
|
|
13
|
+
* and action slots. Same box, no layout shift when content lands.
|
|
14
|
+
*/
|
|
15
|
+
export function PageHeaderSkeleton({
|
|
16
|
+
label = "Loading",
|
|
17
|
+
description = true,
|
|
18
|
+
actions = true,
|
|
19
|
+
className,
|
|
20
|
+
}: PageHeaderSkeletonProps) {
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
role="status"
|
|
24
|
+
aria-label={label}
|
|
25
|
+
className={cn("mb-6 flex flex-wrap items-start justify-between gap-4", className)}
|
|
26
|
+
>
|
|
27
|
+
<div className="space-y-1">
|
|
28
|
+
<Skeleton className="h-7 w-48" />
|
|
29
|
+
{description ? <Skeleton className="h-4 w-64" /> : null}
|
|
30
|
+
</div>
|
|
31
|
+
{actions ? (
|
|
32
|
+
<div className="flex items-center gap-2">
|
|
33
|
+
<Skeleton className="h-9 w-24" />
|
|
34
|
+
<Skeleton className="h-9 w-24" />
|
|
35
|
+
</div>
|
|
36
|
+
) : null}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { cn } from "./utils.js";
|
|
2
|
+
|
|
3
|
+
export interface ProgressBarProps {
|
|
4
|
+
/** Current value, clamped to 0..max (`NaN` reads as empty). */
|
|
5
|
+
value: number;
|
|
6
|
+
max?: number;
|
|
7
|
+
/** Accessible name (kept screen-only — pair with visible text). */
|
|
8
|
+
label: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const SEGMENTS = 10;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Determinate progress as filled segments (exact value stays in
|
|
16
|
+
* `aria-valuenow`). No animation, so nothing to disable for reduced
|
|
17
|
+
* motion. Announce the operation with visible text (e.g. StatusLabel)
|
|
18
|
+
* next to it — the label here stays screen-only.
|
|
19
|
+
*/
|
|
20
|
+
export function ProgressBar({ value, max = 100, label, className }: ProgressBarProps) {
|
|
21
|
+
const safeMax = Number.isFinite(max) && max > 0 ? max : 100;
|
|
22
|
+
const safeValue = Number.isFinite(value) ? Math.min(Math.max(value, 0), safeMax) : 0;
|
|
23
|
+
const filled = Math.round((safeValue / safeMax) * SEGMENTS);
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
role="progressbar"
|
|
27
|
+
aria-label={label}
|
|
28
|
+
aria-valuemin={0}
|
|
29
|
+
aria-valuemax={safeMax}
|
|
30
|
+
aria-valuenow={Math.round(safeValue)}
|
|
31
|
+
className={cn("flex w-full gap-0.5", className)}
|
|
32
|
+
>
|
|
33
|
+
{Array.from({ length: SEGMENTS }, (_, index) => (
|
|
34
|
+
<span
|
|
35
|
+
key={index}
|
|
36
|
+
aria-hidden
|
|
37
|
+
className={cn(
|
|
38
|
+
"h-1.5 flex-1 rounded-full motion-safe:transition-colors",
|
|
39
|
+
index < filled ? "bg-primary" : "bg-muted",
|
|
40
|
+
)}
|
|
41
|
+
/>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
package/src/select.tsx
CHANGED
package/src/separator.tsx
CHANGED
package/src/skeleton.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
// Derived from shadcn/ui (skeleton) — see Provenance in packages/ui/README.md. Tolerated exception: migrated to custom code only when customization needs it.
|
|
1
2
|
import { cn } from "./utils.js";
|
|
2
3
|
|
|
3
4
|
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
4
|
-
return <div className={cn("
|
|
5
|
+
return <div className={cn("rounded-md bg-muted motion-safe:animate-pulse", className)} {...props} />;
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
export { Skeleton };
|
package/src/sonner.tsx
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { cn } from "./utils.js";
|
|
2
|
+
|
|
3
|
+
export type StatusDotTone = "active" | "idle" | "success" | "warning" | "danger";
|
|
4
|
+
|
|
5
|
+
export interface StatusDotProps {
|
|
6
|
+
tone?: StatusDotTone;
|
|
7
|
+
/** Override the default ping (ongoing tones ping, others stay static). */
|
|
8
|
+
pulse?: boolean;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const TONE_COLOR: Record<StatusDotTone, string> = {
|
|
13
|
+
active: "bg-primary",
|
|
14
|
+
idle: "bg-muted-foreground/40",
|
|
15
|
+
success: "bg-success",
|
|
16
|
+
warning: "bg-warning",
|
|
17
|
+
danger: "bg-destructive",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const PING_TONES: ReadonlySet<StatusDotTone> = new Set(["active", "warning", "danger"]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Decorative status dot: pings for ongoing tones (motion-safe), static
|
|
24
|
+
* otherwise. Pair with visible text (e.g. StatusLabel) — never alone.
|
|
25
|
+
*/
|
|
26
|
+
export function StatusDot({ tone = "active", pulse, className }: StatusDotProps) {
|
|
27
|
+
const color = TONE_COLOR[tone];
|
|
28
|
+
const ping = pulse ?? PING_TONES.has(tone);
|
|
29
|
+
return (
|
|
30
|
+
<span aria-hidden className={cn("relative flex size-2", className)}>
|
|
31
|
+
{ping ? (
|
|
32
|
+
<span
|
|
33
|
+
className={cn(
|
|
34
|
+
"absolute inline-flex h-full w-full rounded-full opacity-60 motion-safe:animate-ping",
|
|
35
|
+
color,
|
|
36
|
+
)}
|
|
37
|
+
/>
|
|
38
|
+
) : null}
|
|
39
|
+
<span className={cn("relative inline-flex size-2 rounded-full", color)} />
|
|
40
|
+
</span>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { WifiOff } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { Checkmark } from "./checkmark.js";
|
|
4
|
+
import { StatusDot } from "./status-dot.js";
|
|
5
|
+
import { cn } from "./utils.js";
|
|
6
|
+
|
|
7
|
+
export type StatusLabelTone = "loading" | "updating" | "syncing" | "saved" | "retry" | "error" | "offline";
|
|
8
|
+
|
|
9
|
+
export interface StatusLabelProps {
|
|
10
|
+
tone: StatusLabelTone;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ToneIcon({ tone }: { tone: StatusLabelTone }) {
|
|
16
|
+
switch (tone) {
|
|
17
|
+
case "saved":
|
|
18
|
+
return <Checkmark />;
|
|
19
|
+
case "retry":
|
|
20
|
+
return <StatusDot tone="warning" />;
|
|
21
|
+
case "error":
|
|
22
|
+
return <StatusDot tone="danger" />;
|
|
23
|
+
case "offline":
|
|
24
|
+
return <WifiOff aria-hidden className="size-3.5 text-muted-foreground" />;
|
|
25
|
+
case "loading":
|
|
26
|
+
case "updating":
|
|
27
|
+
case "syncing":
|
|
28
|
+
return <StatusDot tone="active" />;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Named inline status: icon + text, announced politely (`role=status`).
|
|
34
|
+
* Name the operation ("Saving report…", not "Loading…") so a worried
|
|
35
|
+
* user always knows what is happening and where.
|
|
36
|
+
*/
|
|
37
|
+
export function StatusLabel({ tone, children, className }: StatusLabelProps) {
|
|
38
|
+
return (
|
|
39
|
+
<span
|
|
40
|
+
role="status"
|
|
41
|
+
className={cn(
|
|
42
|
+
"inline-flex items-center gap-1.5 text-xs",
|
|
43
|
+
tone === "error" ? "text-destructive" : "text-muted-foreground",
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
<span className="flex size-4 shrink-0 items-center justify-center">
|
|
48
|
+
<ToneIcon tone={tone} />
|
|
49
|
+
</span>
|
|
50
|
+
{children}
|
|
51
|
+
</span>
|
|
52
|
+
);
|
|
53
|
+
}
|