@olympusoss/canvas 2.6.19

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 (128) hide show
  1. package/package.json +179 -0
  2. package/src/components/atoms/README.md +11 -0
  3. package/src/components/atoms/aspect-ratio.tsx +32 -0
  4. package/src/components/atoms/avatar.tsx +98 -0
  5. package/src/components/atoms/badge.tsx +44 -0
  6. package/src/components/atoms/brand-mark.tsx +74 -0
  7. package/src/components/atoms/button.tsx +104 -0
  8. package/src/components/atoms/checkbox.tsx +63 -0
  9. package/src/components/atoms/flex-box.tsx +105 -0
  10. package/src/components/atoms/icon.tsx +34 -0
  11. package/src/components/atoms/input.tsx +91 -0
  12. package/src/components/atoms/label.tsx +41 -0
  13. package/src/components/atoms/logo.tsx +89 -0
  14. package/src/components/atoms/progress.tsx +55 -0
  15. package/src/components/atoms/radio-group.tsx +122 -0
  16. package/src/components/atoms/scroll-area.tsx +106 -0
  17. package/src/components/atoms/section.tsx +48 -0
  18. package/src/components/atoms/separator.tsx +45 -0
  19. package/src/components/atoms/skeleton.tsx +17 -0
  20. package/src/components/atoms/slider.tsx +93 -0
  21. package/src/components/atoms/switch.tsx +60 -0
  22. package/src/components/atoms/textarea.tsx +78 -0
  23. package/src/components/atoms/toggle.tsx +80 -0
  24. package/src/components/charts/activity-heatmap.tsx +96 -0
  25. package/src/components/charts/axes.tsx +21 -0
  26. package/src/components/charts/chart-container.tsx +195 -0
  27. package/src/components/charts/chart-legend.tsx +67 -0
  28. package/src/components/charts/chart-tooltip.tsx +161 -0
  29. package/src/components/charts/chart-types.tsx +49 -0
  30. package/src/components/charts/containers.tsx +11 -0
  31. package/src/components/charts/data.tsx +16 -0
  32. package/src/components/charts/details.tsx +25 -0
  33. package/src/components/charts/gauge.tsx +106 -0
  34. package/src/components/charts/grids.tsx +8 -0
  35. package/src/components/charts/index.ts +62 -0
  36. package/src/components/charts/labeled-bar-list.tsx +85 -0
  37. package/src/components/charts/references.tsx +8 -0
  38. package/src/components/charts/service-health-list.tsx +73 -0
  39. package/src/components/charts/sparkline.tsx +52 -0
  40. package/src/components/charts/stacked-bar.tsx +104 -0
  41. package/src/components/charts/text.tsx +10 -0
  42. package/src/components/charts/world-heat-map-inner.tsx +317 -0
  43. package/src/components/charts/world-heat-map.tsx +184 -0
  44. package/src/components/molecules/README.md +12 -0
  45. package/src/components/molecules/action-bar.tsx +73 -0
  46. package/src/components/molecules/activity-item.tsx +74 -0
  47. package/src/components/molecules/alert.tsx +80 -0
  48. package/src/components/molecules/animated-background.tsx +92 -0
  49. package/src/components/molecules/brand-lockup.tsx +48 -0
  50. package/src/components/molecules/breadcrumb.tsx +161 -0
  51. package/src/components/molecules/button-group.tsx +104 -0
  52. package/src/components/molecules/calendar.tsx +216 -0
  53. package/src/components/molecules/card.tsx +101 -0
  54. package/src/components/molecules/code-block.tsx +48 -0
  55. package/src/components/molecules/empty-state.tsx +55 -0
  56. package/src/components/molecules/error-state.tsx +42 -0
  57. package/src/components/molecules/field-display.tsx +35 -0
  58. package/src/components/molecules/input-otp.tsx +74 -0
  59. package/src/components/molecules/loading-state.tsx +36 -0
  60. package/src/components/molecules/notification-item.tsx +67 -0
  61. package/src/components/molecules/notification-list.tsx +45 -0
  62. package/src/components/molecules/number-badge.tsx +53 -0
  63. package/src/components/molecules/page-header.tsx +88 -0
  64. package/src/components/molecules/page-tabs.tsx +94 -0
  65. package/src/components/molecules/pagination.tsx +150 -0
  66. package/src/components/molecules/phone-input.tsx +200 -0
  67. package/src/components/molecules/search-bar.tsx +64 -0
  68. package/src/components/molecules/secret-field.tsx +158 -0
  69. package/src/components/molecules/section-card.tsx +91 -0
  70. package/src/components/molecules/stat-card.tsx +96 -0
  71. package/src/components/molecules/status-badge.tsx +42 -0
  72. package/src/components/molecules/stepper.tsx +96 -0
  73. package/src/components/molecules/table.tsx +157 -0
  74. package/src/components/molecules/toggle-group.tsx +145 -0
  75. package/src/components/molecules/tooltip.tsx +150 -0
  76. package/src/components/molecules/user-avatar-chip.tsx +71 -0
  77. package/src/components/organisms/README.md +14 -0
  78. package/src/components/organisms/accordion.tsx +149 -0
  79. package/src/components/organisms/alert-dialog.tsx +269 -0
  80. package/src/components/organisms/carousel.tsx +244 -0
  81. package/src/components/organisms/collapsible.tsx +69 -0
  82. package/src/components/organisms/command.tsx +143 -0
  83. package/src/components/organisms/context-menu.tsx +333 -0
  84. package/src/components/organisms/dashboard-grid.tsx +360 -0
  85. package/src/components/organisms/data-table.tsx +330 -0
  86. package/src/components/organisms/dialog.tsx +304 -0
  87. package/src/components/organisms/drawer.tsx +100 -0
  88. package/src/components/organisms/dropdown-menu.tsx +434 -0
  89. package/src/components/organisms/editors/code-editor.tsx +144 -0
  90. package/src/components/organisms/editors/index.ts +4 -0
  91. package/src/components/organisms/editors/markdown-editor.tsx +153 -0
  92. package/src/components/organisms/editors/markdown-renderer.ts +27 -0
  93. package/src/components/organisms/editors/prose-canvas-classes.ts +45 -0
  94. package/src/components/organisms/editors/rich-text-editor.tsx +126 -0
  95. package/src/components/organisms/editors/toolbar/md-toolbar.tsx +129 -0
  96. package/src/components/organisms/editors/toolbar/rte-toolbar.tsx +211 -0
  97. package/src/components/organisms/editors/toolbar/toolbar-shell.tsx +45 -0
  98. package/src/components/organisms/editors/use-codemirror-theme.ts +61 -0
  99. package/src/components/organisms/error-boundary.tsx +61 -0
  100. package/src/components/organisms/form.tsx +174 -0
  101. package/src/components/organisms/hover-card.tsx +114 -0
  102. package/src/components/organisms/menubar.tsx +491 -0
  103. package/src/components/organisms/navbar.tsx +101 -0
  104. package/src/components/organisms/navigation-menu.tsx +234 -0
  105. package/src/components/organisms/popover.tsx +144 -0
  106. package/src/components/organisms/resizable.tsx +39 -0
  107. package/src/components/organisms/schema-form.tsx +232 -0
  108. package/src/components/organisms/select.tsx +303 -0
  109. package/src/components/organisms/sheet.tsx +256 -0
  110. package/src/components/organisms/sidebar.tsx +1037 -0
  111. package/src/components/organisms/sonner.tsx +96 -0
  112. package/src/components/organisms/tabs.tsx +132 -0
  113. package/src/components/organisms/theme-provider.tsx +101 -0
  114. package/src/hooks/use-mobile.tsx +19 -0
  115. package/src/index.ts +547 -0
  116. package/src/lib/portal-container.tsx +35 -0
  117. package/src/lib/utils.ts +6 -0
  118. package/src/native.ts +23 -0
  119. package/src/tokens/colors.ts +91 -0
  120. package/src/tokens/index.ts +3 -0
  121. package/src/tokens/spacing.ts +55 -0
  122. package/src/tokens/typography.ts +27 -0
  123. package/styles/canvas.css +55 -0
  124. package/styles/dashboard-grid.css +47 -0
  125. package/styles/leaflet.css +13 -0
  126. package/styles/tokens.css +234 -0
  127. package/tailwind.config.ts +70 -0
  128. package/tsconfig.json +23 -0
@@ -0,0 +1,216 @@
1
+ "use client";
2
+
3
+ import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { type DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
6
+
7
+ import { cn } from "../../lib/utils";
8
+ import { Button, buttonVariants } from "../atoms/button";
9
+
10
+ export type CalendarProps = React.ComponentProps<typeof DayPicker> & {
11
+ /**
12
+ * Variant applied to the prev/next month nav buttons. Maps to the
13
+ * `Button` atom's variants.
14
+ * @default "ghost"
15
+ */
16
+ buttonVariant?: React.ComponentProps<typeof Button>["variant"];
17
+ /**
18
+ * Show days from the previous/next month as faded entries to fill the
19
+ * grid.
20
+ * @default true
21
+ */
22
+ showOutsideDays?: boolean;
23
+ /**
24
+ * How the month/year heading renders. `label` is plain text, the
25
+ * dropdown variants render selectable controls.
26
+ * @default "label"
27
+ */
28
+ captionLayout?: "label" | "dropdown" | "dropdown-months" | "dropdown-years";
29
+ className?: string;
30
+ };
31
+
32
+ function Calendar({
33
+ className,
34
+ classNames,
35
+ showOutsideDays = true,
36
+ captionLayout = "label",
37
+ buttonVariant = "ghost",
38
+ formatters,
39
+ components,
40
+ ...props
41
+ }: CalendarProps) {
42
+ const defaultClassNames = getDefaultClassNames();
43
+
44
+ return (
45
+ <DayPicker
46
+ showOutsideDays={showOutsideDays}
47
+ className={cn(
48
+ "bg-background group/calendar p-4 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
49
+ String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
50
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
51
+ className,
52
+ )}
53
+ captionLayout={captionLayout}
54
+ formatters={{
55
+ formatMonthDropdown: (date) => date.toLocaleString("default", { month: "short" }),
56
+ ...formatters,
57
+ }}
58
+ classNames={{
59
+ root: cn("w-fit", defaultClassNames.root),
60
+ months: cn("relative flex flex-col gap-4 md:flex-row", defaultClassNames.months),
61
+ month: cn("flex w-full flex-col gap-3", defaultClassNames.month),
62
+ nav: cn(
63
+ "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
64
+ defaultClassNames.nav,
65
+ ),
66
+ button_previous: cn(
67
+ buttonVariants({ variant: buttonVariant }),
68
+ "h-[var(--cell-size)] w-[var(--cell-size)] select-none p-0 aria-disabled:opacity-50",
69
+ defaultClassNames.button_previous,
70
+ ),
71
+ button_next: cn(
72
+ buttonVariants({ variant: buttonVariant }),
73
+ "h-[var(--cell-size)] w-[var(--cell-size)] select-none p-0 aria-disabled:opacity-50",
74
+ defaultClassNames.button_next,
75
+ ),
76
+ month_caption: cn(
77
+ "flex h-[var(--cell-size)] w-full items-center justify-center px-[var(--cell-size)]",
78
+ defaultClassNames.month_caption,
79
+ ),
80
+ dropdowns: cn(
81
+ "flex h-[var(--cell-size)] w-full items-center justify-center gap-1.5 text-sm font-medium",
82
+ defaultClassNames.dropdowns,
83
+ ),
84
+ dropdown_root: cn(
85
+ "has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
86
+ defaultClassNames.dropdown_root,
87
+ ),
88
+ dropdown: cn("bg-popover absolute inset-0 opacity-0", defaultClassNames.dropdown),
89
+ caption_label: cn(
90
+ "select-none font-semibold tracking-tight text-foreground",
91
+ captionLayout === "label"
92
+ ? "text-sm"
93
+ : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
94
+ defaultClassNames.caption_label,
95
+ ),
96
+ table: "w-full border-collapse",
97
+ weekdays: cn("flex border-b border-border pb-1.5", defaultClassNames.weekdays),
98
+ weekday: cn(
99
+ "text-muted-foreground flex-1 select-none rounded-md text-[0.7rem] font-medium uppercase tracking-wider",
100
+ defaultClassNames.weekday,
101
+ ),
102
+ week: cn("mt-1.5 flex w-full", defaultClassNames.week),
103
+ week_number_header: cn(
104
+ "w-[var(--cell-size)] select-none",
105
+ defaultClassNames.week_number_header,
106
+ ),
107
+ week_number: cn(
108
+ "text-muted-foreground select-none text-[0.8rem]",
109
+ defaultClassNames.week_number,
110
+ ),
111
+ day: cn(
112
+ "group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
113
+ defaultClassNames.day,
114
+ ),
115
+ range_start: cn("bg-brand/10 rounded-l-md", defaultClassNames.range_start),
116
+ range_middle: cn("bg-brand/10 rounded-none", defaultClassNames.range_middle),
117
+ range_end: cn("bg-brand/10 rounded-r-md", defaultClassNames.range_end),
118
+ today: cn(
119
+ "text-brand font-semibold data-[selected=true]:rounded-none",
120
+ defaultClassNames.today,
121
+ ),
122
+ outside: cn(
123
+ "text-muted-foreground/50 aria-selected:text-muted-foreground",
124
+ defaultClassNames.outside,
125
+ ),
126
+ disabled: cn("text-muted-foreground opacity-40", defaultClassNames.disabled),
127
+ hidden: cn("invisible", defaultClassNames.hidden),
128
+ ...classNames,
129
+ }}
130
+ components={{
131
+ Root: ({ className, rootRef, ...props }) => {
132
+ return <div data-slot="calendar" ref={rootRef} className={cn(className)} {...props} />;
133
+ },
134
+ Chevron: ({ className, orientation, ...props }) => {
135
+ if (orientation === "left") {
136
+ return <ChevronLeftIcon className={cn("size-4", className)} {...props} />;
137
+ }
138
+
139
+ if (orientation === "right") {
140
+ return <ChevronRightIcon className={cn("size-4", className)} {...props} />;
141
+ }
142
+
143
+ return <ChevronDownIcon className={cn("size-4", className)} {...props} />;
144
+ },
145
+ DayButton: CalendarDayButton,
146
+ WeekNumber: ({ children, ...props }) => {
147
+ return (
148
+ <td {...props}>
149
+ <div className="flex size-[var(--cell-size)] items-center justify-center text-center">
150
+ {children}
151
+ </div>
152
+ </td>
153
+ );
154
+ },
155
+ ...components,
156
+ }}
157
+ {...props}
158
+ />
159
+ );
160
+ }
161
+
162
+ function CalendarDayButton({
163
+ className,
164
+ day,
165
+ modifiers,
166
+ ...props
167
+ }: React.ComponentProps<typeof DayButton>) {
168
+ const defaultClassNames = getDefaultClassNames();
169
+
170
+ const ref = React.useRef<HTMLButtonElement>(null);
171
+ React.useEffect(() => {
172
+ if (modifiers.focused) ref.current?.focus();
173
+ }, [modifiers.focused]);
174
+
175
+ return (
176
+ <Button
177
+ ref={ref}
178
+ variant="ghost"
179
+ size="icon"
180
+ data-day={day.date.toLocaleDateString()}
181
+ data-selected-single={
182
+ modifiers.selected &&
183
+ !modifiers.range_start &&
184
+ !modifiers.range_end &&
185
+ !modifiers.range_middle
186
+ }
187
+ data-range-start={modifiers.range_start}
188
+ data-range-end={modifiers.range_end}
189
+ data-range-middle={modifiers.range_middle}
190
+ className={cn(
191
+ // base size + typography (small inner inset so adjacent dates breathe)
192
+ "m-0.5 flex aspect-square h-auto w-[calc(100%-0.25rem)] min-w-[calc(var(--cell-size)-0.25rem)] items-center justify-center rounded-md text-sm font-normal leading-none transition-colors",
193
+ // hover (when not selected)
194
+ "hover:bg-accent hover:text-accent-foreground",
195
+ // today: keep brand text colour from row classNames; add subtle ring
196
+ "group-[[data-today=true]]/day:ring-1 group-[[data-today=true]]/day:ring-brand/40",
197
+ // single selection: brand fill, white text, full rounding
198
+ "data-[selected-single=true]:bg-brand data-[selected-single=true]:text-brand-foreground data-[selected-single=true]:hover:bg-brand/90 data-[selected-single=true]:hover:text-brand-foreground",
199
+ // range endpoints: brand fill, only outer corners rounded
200
+ "data-[range-start=true]:bg-brand data-[range-start=true]:text-brand-foreground data-[range-start=true]:hover:bg-brand/90 data-[range-start=true]:hover:text-brand-foreground data-[range-start=true]:rounded-r-none",
201
+ "data-[range-end=true]:bg-brand data-[range-end=true]:text-brand-foreground data-[range-end=true]:hover:bg-brand/90 data-[range-end=true]:hover:text-brand-foreground data-[range-end=true]:rounded-l-none",
202
+ // range middle: foreground stays default, no rounding
203
+ "data-[range-middle=true]:bg-transparent data-[range-middle=true]:text-foreground data-[range-middle=true]:rounded-none",
204
+ // focus ring (for keyboard nav)
205
+ "group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-brand/40 group-data-[focused=true]/day:border-brand",
206
+ // modifier label (e.g. holidays) sub-line
207
+ "[&>span]:text-xs [&>span]:opacity-70",
208
+ defaultClassNames.day,
209
+ className,
210
+ )}
211
+ {...props}
212
+ />
213
+ );
214
+ }
215
+
216
+ export { Calendar, CalendarDayButton };
@@ -0,0 +1,101 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+
5
+ export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ /** `<CardHeader>`, `<CardContent>`, `<CardFooter>` (or any custom layout). */
7
+ children?: React.ReactNode;
8
+ /**
9
+ * Tailwind / CSS classes merged onto the card via `cn()`. Default
10
+ * styling: `rounded-xl` (12px), `border`, `bg-card`, `shadow`.
11
+ */
12
+ className?: string;
13
+ }
14
+
15
+ const Card = React.forwardRef<HTMLDivElement, CardProps>(({ className, ...props }, ref) => (
16
+ <div
17
+ ref={ref}
18
+ className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
19
+ {...props}
20
+ />
21
+ ));
22
+ Card.displayName = "Card";
23
+
24
+ export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
25
+ /** Typically `<CardTitle>` + `<CardDescription>`. */
26
+ children?: React.ReactNode;
27
+ /** Tailwind / CSS classes merged onto the header via `cn()`. Default: `p-6 space-y-1.5`. */
28
+ className?: string;
29
+ }
30
+
31
+ const CardHeader = React.forwardRef<HTMLDivElement, CardHeaderProps>(
32
+ ({ className, ...props }, ref) => (
33
+ <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
34
+ ),
35
+ );
36
+ CardHeader.displayName = "CardHeader";
37
+
38
+ export interface CardTitleProps extends React.HTMLAttributes<HTMLDivElement> {
39
+ /** The card heading. */
40
+ children?: React.ReactNode;
41
+ /** Tailwind / CSS classes merged onto the title via `cn()`. */
42
+ className?: string;
43
+ }
44
+
45
+ const CardTitle = React.forwardRef<HTMLDivElement, CardTitleProps>(
46
+ ({ className, ...props }, ref) => (
47
+ <div
48
+ ref={ref}
49
+ className={cn("font-semibold leading-none tracking-tight", className)}
50
+ {...props}
51
+ />
52
+ ),
53
+ );
54
+ CardTitle.displayName = "CardTitle";
55
+
56
+ export interface CardDescriptionProps extends React.HTMLAttributes<HTMLDivElement> {
57
+ /** Subtitle / supporting copy below the title. */
58
+ children?: React.ReactNode;
59
+ /** Tailwind / CSS classes merged onto the description via `cn()`. */
60
+ className?: string;
61
+ }
62
+
63
+ const CardDescription = React.forwardRef<HTMLDivElement, CardDescriptionProps>(
64
+ ({ className, ...props }, ref) => (
65
+ <div ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
66
+ ),
67
+ );
68
+ CardDescription.displayName = "CardDescription";
69
+
70
+ export interface CardContentProps extends React.HTMLAttributes<HTMLDivElement> {
71
+ /** Card body. */
72
+ children?: React.ReactNode;
73
+ /** Tailwind / CSS classes merged onto the content via `cn()`. Default: `p-6 pt-0`. */
74
+ className?: string;
75
+ }
76
+
77
+ const CardContent = React.forwardRef<HTMLDivElement, CardContentProps>(
78
+ ({ className, ...props }, ref) => (
79
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
80
+ ),
81
+ );
82
+ CardContent.displayName = "CardContent";
83
+
84
+ export interface CardFooterProps extends React.HTMLAttributes<HTMLDivElement> {
85
+ /** Card actions / metadata row. */
86
+ children?: React.ReactNode;
87
+ /**
88
+ * Tailwind / CSS classes merged onto the footer via `cn()`. Default:
89
+ * `flex items-center p-6 pt-0`.
90
+ */
91
+ className?: string;
92
+ }
93
+
94
+ const CardFooter = React.forwardRef<HTMLDivElement, CardFooterProps>(
95
+ ({ className, ...props }, ref) => (
96
+ <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
97
+ ),
98
+ );
99
+ CardFooter.displayName = "CardFooter";
100
+
101
+ export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
@@ -0,0 +1,48 @@
1
+ "use client";
2
+
3
+ import { Check, Copy } from "lucide-react";
4
+ import * as React from "react";
5
+
6
+ import { cn } from "../../lib/utils";
7
+ import { Button } from "../atoms/button";
8
+
9
+ export interface CodeBlockProps extends React.HTMLAttributes<HTMLDivElement> {
10
+ code: string;
11
+ language?: string;
12
+ showCopy?: boolean;
13
+ }
14
+
15
+ const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
16
+ ({ code, language, showCopy = true, className, ...props }, ref) => {
17
+ const [copied, setCopied] = React.useState(false);
18
+
19
+ const handleCopy = React.useCallback(async () => {
20
+ await navigator.clipboard.writeText(code);
21
+ setCopied(true);
22
+ setTimeout(() => setCopied(false), 2000);
23
+ }, [code]);
24
+
25
+ return (
26
+ <div ref={ref} className={cn("relative rounded-md bg-muted", className)} {...props}>
27
+ {(language || showCopy) && (
28
+ <div className="flex items-center justify-between border-b px-4 py-2">
29
+ {language && (
30
+ <span className="text-xs font-medium text-muted-foreground">{language}</span>
31
+ )}
32
+ {showCopy && (
33
+ <Button variant="ghost" size="icon" className="h-6 w-6" onClick={handleCopy}>
34
+ {copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
35
+ </Button>
36
+ )}
37
+ </div>
38
+ )}
39
+ <pre className="overflow-x-auto p-4">
40
+ <code className="text-sm">{code}</code>
41
+ </pre>
42
+ </div>
43
+ );
44
+ },
45
+ );
46
+ CodeBlock.displayName = "CodeBlock";
47
+
48
+ export { CodeBlock };
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import { Inbox } from "lucide-react";
4
+ import * as React from "react";
5
+
6
+ import { cn } from "../../lib/utils";
7
+ import { Button } from "../atoms/button";
8
+
9
+ export interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
10
+ icon?: React.ReactNode;
11
+ /** Primary message. Renders below the icon pill, 15px font-medium. */
12
+ title?: string;
13
+ /**
14
+ * @deprecated Use `title`. Retained as an alias so existing call sites
15
+ * (e.g. DataTable's empty fallback) keep working.
16
+ */
17
+ message?: string;
18
+ description?: string;
19
+ action?: {
20
+ label: string;
21
+ onClick: () => void;
22
+ };
23
+ }
24
+
25
+ const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
26
+ ({ icon, title, message, description, action, className, ...props }, ref) => {
27
+ const heading = title ?? message ?? "No items found";
28
+ return (
29
+ <div
30
+ ref={ref}
31
+ className={cn(
32
+ "flex flex-col items-center justify-center gap-3 py-12 text-muted-foreground",
33
+ className,
34
+ )}
35
+ {...props}
36
+ >
37
+ <div className="mb-1 inline-flex rounded-full bg-muted p-3 text-muted-foreground">
38
+ {icon ?? <Inbox className="h-5 w-5" />}
39
+ </div>
40
+ <div className="text-center">
41
+ <p className="text-[15px] font-medium text-foreground">{heading}</p>
42
+ {description && <p className="mt-1 text-xs text-muted-foreground">{description}</p>}
43
+ </div>
44
+ {action && (
45
+ <Button variant="outline" size="sm" onClick={action.onClick}>
46
+ {action.label}
47
+ </Button>
48
+ )}
49
+ </div>
50
+ );
51
+ },
52
+ );
53
+ EmptyState.displayName = "EmptyState";
54
+
55
+ export { EmptyState };
@@ -0,0 +1,42 @@
1
+ "use client";
2
+
3
+ import { AlertCircle } from "lucide-react";
4
+ import * as React from "react";
5
+
6
+ import { cn } from "../../lib/utils";
7
+ import { Button } from "../atoms/button";
8
+
9
+ export interface ErrorStateProps extends React.HTMLAttributes<HTMLDivElement> {
10
+ message?: string;
11
+ onRetry?: () => void;
12
+ retryLabel?: string;
13
+ }
14
+
15
+ const ErrorState = React.forwardRef<HTMLDivElement, ErrorStateProps>(
16
+ (
17
+ { message = "Something went wrong.", onRetry, retryLabel = "Try again", className, ...props },
18
+ ref,
19
+ ) => {
20
+ return (
21
+ <div
22
+ ref={ref}
23
+ className={cn(
24
+ "flex flex-col items-center justify-center gap-3 py-8 text-muted-foreground",
25
+ className,
26
+ )}
27
+ {...props}
28
+ >
29
+ <AlertCircle className="h-8 w-8 text-destructive" />
30
+ <p className="text-sm">{message}</p>
31
+ {onRetry && (
32
+ <Button variant="outline" size="sm" onClick={onRetry}>
33
+ {retryLabel}
34
+ </Button>
35
+ )}
36
+ </div>
37
+ );
38
+ },
39
+ );
40
+ ErrorState.displayName = "ErrorState";
41
+
42
+ export { ErrorState };
@@ -0,0 +1,35 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+
5
+ export interface FieldDisplayProps extends React.HTMLAttributes<HTMLDListElement> {
6
+ label: string;
7
+ value?: React.ReactNode;
8
+ mono?: boolean;
9
+ }
10
+
11
+ const FieldDisplay = React.forwardRef<HTMLDListElement, FieldDisplayProps>(
12
+ ({ label, value, mono = false, className, ...props }, ref) => {
13
+ return (
14
+ <dl
15
+ ref={ref}
16
+ className={cn("grid grid-cols-[180px_1fr] items-baseline gap-4 py-2", className)}
17
+ {...props}
18
+ >
19
+ <dt className="text-[13px] font-medium text-muted-foreground">{label}</dt>
20
+ <dd
21
+ className={cn(
22
+ "break-words text-[13px]",
23
+ mono && "font-mono text-[12.5px]",
24
+ value ? "text-foreground" : "text-muted-foreground",
25
+ )}
26
+ >
27
+ {value ?? "—"}
28
+ </dd>
29
+ </dl>
30
+ );
31
+ },
32
+ );
33
+ FieldDisplay.displayName = "FieldDisplay";
34
+
35
+ export { FieldDisplay };
@@ -0,0 +1,74 @@
1
+ "use client";
2
+
3
+ import { OTPInput, OTPInputContext } from "input-otp";
4
+ import { Minus } from "lucide-react";
5
+ import * as React from "react";
6
+
7
+ import { cn } from "../../lib/utils";
8
+
9
+ const InputOTP = React.forwardRef<
10
+ React.ElementRef<typeof OTPInput>,
11
+ React.ComponentPropsWithoutRef<typeof OTPInput>
12
+ >(({ className, containerClassName, ...props }, ref) => (
13
+ <OTPInput
14
+ ref={ref}
15
+ containerClassName={cn(
16
+ "flex items-center gap-2 has-[:disabled]:opacity-50",
17
+ containerClassName,
18
+ )}
19
+ className={cn("disabled:cursor-not-allowed", className)}
20
+ {...props}
21
+ />
22
+ ));
23
+ InputOTP.displayName = "InputOTP";
24
+
25
+ const InputOTPGroup = React.forwardRef<
26
+ React.ElementRef<"div">,
27
+ React.ComponentPropsWithoutRef<"div">
28
+ >(({ className, ...props }, ref) => (
29
+ <div ref={ref} className={cn("flex items-center", className)} {...props} />
30
+ ));
31
+ InputOTPGroup.displayName = "InputOTPGroup";
32
+
33
+ const InputOTPSlot = React.forwardRef<
34
+ React.ElementRef<"div">,
35
+ React.ComponentPropsWithoutRef<"div"> & { index: number }
36
+ >(({ index, className, ...props }, ref) => {
37
+ const inputOTPContext = React.useContext(OTPInputContext);
38
+ const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
39
+
40
+ /* c8 ignore next -- hasFakeCaret is only set by the input-otp library for a live selection range, which jsdom's fireEvent doesn't produce */
41
+ const fakeCaret = hasFakeCaret ? (
42
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
43
+ <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
44
+ </div>
45
+ ) : null;
46
+
47
+ return (
48
+ <div
49
+ ref={ref}
50
+ className={cn(
51
+ "relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
52
+ isActive && "z-10 ring-1 ring-ring",
53
+ className,
54
+ )}
55
+ {...props}
56
+ >
57
+ {char}
58
+ {fakeCaret}
59
+ </div>
60
+ );
61
+ });
62
+ InputOTPSlot.displayName = "InputOTPSlot";
63
+
64
+ const InputOTPSeparator = React.forwardRef<
65
+ React.ElementRef<"div">,
66
+ React.ComponentPropsWithoutRef<"div">
67
+ >(({ ...props }, ref) => (
68
+ <div ref={ref} role="separator" {...props}>
69
+ <Minus />
70
+ </div>
71
+ ));
72
+ InputOTPSeparator.displayName = "InputOTPSeparator";
73
+
74
+ export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot };
@@ -0,0 +1,36 @@
1
+ import { Loader2 } from "lucide-react";
2
+ import * as React from "react";
3
+
4
+ import { cn } from "../../lib/utils";
5
+
6
+ export interface LoadingStateProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ message?: string;
8
+ size?: "sm" | "default" | "lg";
9
+ }
10
+
11
+ const sizeMap = {
12
+ sm: "h-4 w-4",
13
+ default: "h-6 w-6",
14
+ lg: "h-8 w-8",
15
+ };
16
+
17
+ const LoadingState = React.forwardRef<HTMLDivElement, LoadingStateProps>(
18
+ ({ message = "Loading...", size = "default", className, ...props }, ref) => {
19
+ return (
20
+ <div
21
+ ref={ref}
22
+ className={cn(
23
+ "flex flex-col items-center justify-center gap-3 py-8 text-muted-foreground",
24
+ className,
25
+ )}
26
+ {...props}
27
+ >
28
+ <Loader2 className={cn("animate-spin", sizeMap[size])} />
29
+ {message && <p className="text-sm">{message}</p>}
30
+ </div>
31
+ );
32
+ },
33
+ );
34
+ LoadingState.displayName = "LoadingState";
35
+
36
+ export { LoadingState };
@@ -0,0 +1,67 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+
5
+ export interface NotificationItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
6
+ /** Leading icon — typically a Canvas `<Icon name="…" />`. */
7
+ icon?: React.ReactNode;
8
+ /** Tint variant for the icon's circular background. */
9
+ iconTone?: "neutral" | "destructive" | "info" | "success" | "warning";
10
+ /** Bold title line. */
11
+ title: React.ReactNode;
12
+ /** Optional secondary line (description / metadata). */
13
+ description?: React.ReactNode;
14
+ /** Right-aligned timestamp (relative or absolute). */
15
+ timestamp?: React.ReactNode;
16
+ /** When provided, the entire row becomes clickable. */
17
+ onClick?: () => void;
18
+ }
19
+
20
+ const TONE: Record<NonNullable<NotificationItemProps["iconTone"]>, string> = {
21
+ neutral: "bg-muted text-muted-foreground",
22
+ destructive: "bg-[hsl(var(--stat-destructive)/0.1)] text-[hsl(var(--stat-destructive))]",
23
+ info: "bg-[hsl(var(--stat-blue)/0.1)] text-[hsl(var(--stat-blue))]",
24
+ success: "bg-[hsl(var(--stat-success)/0.1)] text-[hsl(var(--stat-success))]",
25
+ warning: "bg-[hsl(var(--stat-amber)/0.1)] text-[hsl(var(--stat-amber))]",
26
+ };
27
+
28
+ export const NotificationItem = React.forwardRef<HTMLDivElement, NotificationItemProps>(
29
+ (
30
+ { icon, iconTone = "neutral", title, description, timestamp, onClick, className, ...props },
31
+ ref,
32
+ ) => {
33
+ const Comp = onClick ? "button" : "div";
34
+ return (
35
+ <Comp
36
+ ref={ref as never}
37
+ type={onClick ? "button" : undefined}
38
+ onClick={onClick}
39
+ className={cn(
40
+ "flex w-full items-start gap-3 px-3 py-3 text-left transition-colors",
41
+ onClick && "cursor-pointer hover:bg-accent",
42
+ className,
43
+ )}
44
+ {...(props as Record<string, unknown>)}
45
+ >
46
+ {icon && (
47
+ <div
48
+ className={cn(
49
+ "flex h-8 w-8 shrink-0 items-center justify-center rounded-full",
50
+ TONE[iconTone],
51
+ )}
52
+ >
53
+ {icon}
54
+ </div>
55
+ )}
56
+ <div className="min-w-0 flex-1 space-y-0.5">
57
+ <div className="text-[13px] font-semibold text-foreground">{title}</div>
58
+ {description && <div className="text-[12.5px] text-muted-foreground">{description}</div>}
59
+ {timestamp && (
60
+ <div className="font-mono text-[11px] text-muted-foreground">{timestamp}</div>
61
+ )}
62
+ </div>
63
+ </Comp>
64
+ );
65
+ },
66
+ );
67
+ NotificationItem.displayName = "NotificationItem";