@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.
Files changed (136) hide show
  1. package/README.md +35 -9
  2. package/dist/alert.d.ts.map +1 -1
  3. package/dist/alert.js +1 -0
  4. package/dist/alert.js.map +1 -1
  5. package/dist/badge.d.ts.map +1 -1
  6. package/dist/badge.js.map +1 -1
  7. package/dist/button.d.ts.map +1 -1
  8. package/dist/button.js +1 -0
  9. package/dist/button.js.map +1 -1
  10. package/dist/card-skeleton.d.ts +10 -0
  11. package/dist/card-skeleton.d.ts.map +1 -0
  12. package/dist/card-skeleton.js +11 -0
  13. package/dist/card-skeleton.js.map +1 -0
  14. package/dist/card.d.ts.map +1 -1
  15. package/dist/card.js +1 -0
  16. package/dist/card.js.map +1 -1
  17. package/dist/checkbox.d.ts.map +1 -1
  18. package/dist/checkbox.js +1 -0
  19. package/dist/checkbox.js.map +1 -1
  20. package/dist/checkmark.d.ts +11 -0
  21. package/dist/checkmark.d.ts.map +1 -0
  22. package/dist/checkmark.js +12 -0
  23. package/dist/checkmark.js.map +1 -0
  24. package/dist/data-table-skeleton.d.ts +14 -0
  25. package/dist/data-table-skeleton.d.ts.map +1 -0
  26. package/dist/data-table-skeleton.js +13 -0
  27. package/dist/data-table-skeleton.js.map +1 -0
  28. package/dist/detail-panel-skeleton.d.ts +11 -0
  29. package/dist/detail-panel-skeleton.d.ts.map +1 -0
  30. package/dist/detail-panel-skeleton.js +11 -0
  31. package/dist/detail-panel-skeleton.js.map +1 -0
  32. package/dist/dialog.d.ts.map +1 -1
  33. package/dist/dialog.js +3 -2
  34. package/dist/dialog.js.map +1 -1
  35. package/dist/drawer.d.ts +30 -0
  36. package/dist/drawer.d.ts.map +1 -0
  37. package/dist/drawer.js +35 -0
  38. package/dist/drawer.js.map +1 -0
  39. package/dist/field.d.ts.map +1 -1
  40. package/dist/field.js +1 -0
  41. package/dist/field.js.map +1 -1
  42. package/dist/form.d.ts.map +1 -1
  43. package/dist/form.js +1 -0
  44. package/dist/form.js.map +1 -1
  45. package/dist/index.d.ts +10 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +10 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/input.d.ts.map +1 -1
  50. package/dist/input.js +1 -0
  51. package/dist/input.js.map +1 -1
  52. package/dist/label.d.ts.map +1 -1
  53. package/dist/label.js +1 -0
  54. package/dist/label.js.map +1 -1
  55. package/dist/live-pulse.d.ts.map +1 -1
  56. package/dist/live-pulse.js +2 -1
  57. package/dist/live-pulse.js.map +1 -1
  58. package/dist/page-header-skeleton.d.ts +12 -0
  59. package/dist/page-header-skeleton.d.ts.map +1 -0
  60. package/dist/page-header-skeleton.js +11 -0
  61. package/dist/page-header-skeleton.js.map +1 -0
  62. package/dist/progress-bar.d.ts +16 -0
  63. package/dist/progress-bar.d.ts.map +1 -0
  64. package/dist/progress-bar.js +16 -0
  65. package/dist/progress-bar.js.map +1 -0
  66. package/dist/select.d.ts.map +1 -1
  67. package/dist/select.js +1 -0
  68. package/dist/select.js.map +1 -1
  69. package/dist/separator.d.ts.map +1 -1
  70. package/dist/separator.js +1 -0
  71. package/dist/separator.js.map +1 -1
  72. package/dist/skeleton.d.ts.map +1 -1
  73. package/dist/skeleton.js +2 -1
  74. package/dist/skeleton.js.map +1 -1
  75. package/dist/sonner.d.ts.map +1 -1
  76. package/dist/sonner.js +1 -0
  77. package/dist/sonner.js.map +1 -1
  78. package/dist/status-dot.d.ts +13 -0
  79. package/dist/status-dot.d.ts.map +1 -0
  80. package/dist/status-dot.js +20 -0
  81. package/dist/status-dot.js.map +1 -0
  82. package/dist/status-label.d.ts +13 -0
  83. package/dist/status-label.d.ts.map +1 -0
  84. package/dist/status-label.js +30 -0
  85. package/dist/status-label.js.map +1 -0
  86. package/dist/switch.d.ts.map +1 -1
  87. package/dist/switch.js +1 -0
  88. package/dist/switch.js.map +1 -1
  89. package/dist/table.d.ts.map +1 -1
  90. package/dist/table.js.map +1 -1
  91. package/dist/tabs.d.ts.map +1 -1
  92. package/dist/tabs.js +1 -0
  93. package/dist/tabs.js.map +1 -1
  94. package/dist/textarea.d.ts.map +1 -1
  95. package/dist/textarea.js +1 -0
  96. package/dist/textarea.js.map +1 -1
  97. package/dist/updated-ago.d.ts +13 -0
  98. package/dist/updated-ago.d.ts.map +1 -0
  99. package/dist/updated-ago.js +29 -0
  100. package/dist/updated-ago.js.map +1 -0
  101. package/dist/utils.d.ts.map +1 -1
  102. package/dist/utils.js +1 -0
  103. package/dist/utils.js.map +1 -1
  104. package/package.json +23 -3
  105. package/src/alert.tsx +1 -0
  106. package/src/badge.tsx +1 -0
  107. package/src/button.tsx +1 -0
  108. package/src/card-skeleton.tsx +31 -0
  109. package/src/card.tsx +1 -0
  110. package/src/checkbox.tsx +1 -0
  111. package/src/checkmark.tsx +34 -0
  112. package/src/data-table-skeleton.tsx +53 -0
  113. package/src/detail-panel-skeleton.tsx +29 -0
  114. package/src/dialog.tsx +9 -1
  115. package/src/drawer.tsx +140 -0
  116. package/src/field.tsx +1 -0
  117. package/src/form.tsx +1 -0
  118. package/src/index.ts +22 -0
  119. package/src/input.tsx +1 -0
  120. package/src/label.tsx +1 -0
  121. package/src/live-pulse.tsx +2 -8
  122. package/src/page-header-skeleton.tsx +39 -0
  123. package/src/progress-bar.tsx +45 -0
  124. package/src/select.tsx +1 -0
  125. package/src/separator.tsx +1 -0
  126. package/src/skeleton.tsx +2 -1
  127. package/src/sonner.tsx +1 -0
  128. package/src/status-dot.tsx +42 -0
  129. package/src/status-label.tsx +53 -0
  130. package/src/styles.css +76 -0
  131. package/src/switch.tsx +1 -0
  132. package/src/table.tsx +1 -0
  133. package/src/tabs.tsx +1 -0
  134. package/src/textarea.tsx +1 -0
  135. package/src/updated-ago.tsx +38 -0
  136. 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
@@ -1,3 +1,4 @@
1
+ // Derived from shadcn/ui (card) — see Provenance in packages/ui/README.md. Tolerated exception: migrated to custom code only when customization needs it.
1
2
  import * as React from "react";
2
3
 
3
4
  import { cn } from "./utils.js";
package/src/checkbox.tsx CHANGED
@@ -1,3 +1,4 @@
1
+ // Derived from shadcn/ui (checkbox) — 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";
@@ -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("fixed inset-0 z-50 bg-overlay/80", className)}
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
@@ -1,3 +1,4 @@
1
+ // Derived from shadcn/ui (field) — 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 type * as React from "react";
package/src/form.tsx CHANGED
@@ -1,3 +1,4 @@
1
+ // Derived from shadcn/ui (form) — 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";
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
@@ -1,3 +1,4 @@
1
+ // Derived from shadcn/ui (input) — see Provenance in packages/ui/README.md. Tolerated exception: migrated to custom code only when customization needs it.
1
2
  import * as React from "react";
2
3
  import { cn } from "./utils.js";
3
4
 
package/src/label.tsx CHANGED
@@ -1,3 +1,4 @@
1
+ // Derived from shadcn/ui (label) — 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";
@@ -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
- <span className="relative flex size-2">
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
@@ -1,3 +1,4 @@
1
+ // Derived from shadcn/ui (select) — 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";
package/src/separator.tsx CHANGED
@@ -1,3 +1,4 @@
1
+ // Derived from shadcn/ui (separator) — 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";
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("animate-pulse rounded-md bg-muted", className)} {...props} />;
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
@@ -1,3 +1,4 @@
1
+ // Derived from shadcn/ui (sonner) — 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 { Toaster as Sonner, type ToasterProps } from "sonner";
@@ -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
+ }