@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.
- package/package.json +179 -0
- package/src/components/atoms/README.md +11 -0
- package/src/components/atoms/aspect-ratio.tsx +32 -0
- package/src/components/atoms/avatar.tsx +98 -0
- package/src/components/atoms/badge.tsx +44 -0
- package/src/components/atoms/brand-mark.tsx +74 -0
- package/src/components/atoms/button.tsx +104 -0
- package/src/components/atoms/checkbox.tsx +63 -0
- package/src/components/atoms/flex-box.tsx +105 -0
- package/src/components/atoms/icon.tsx +34 -0
- package/src/components/atoms/input.tsx +91 -0
- package/src/components/atoms/label.tsx +41 -0
- package/src/components/atoms/logo.tsx +89 -0
- package/src/components/atoms/progress.tsx +55 -0
- package/src/components/atoms/radio-group.tsx +122 -0
- package/src/components/atoms/scroll-area.tsx +106 -0
- package/src/components/atoms/section.tsx +48 -0
- package/src/components/atoms/separator.tsx +45 -0
- package/src/components/atoms/skeleton.tsx +17 -0
- package/src/components/atoms/slider.tsx +93 -0
- package/src/components/atoms/switch.tsx +60 -0
- package/src/components/atoms/textarea.tsx +78 -0
- package/src/components/atoms/toggle.tsx +80 -0
- package/src/components/charts/activity-heatmap.tsx +96 -0
- package/src/components/charts/axes.tsx +21 -0
- package/src/components/charts/chart-container.tsx +195 -0
- package/src/components/charts/chart-legend.tsx +67 -0
- package/src/components/charts/chart-tooltip.tsx +161 -0
- package/src/components/charts/chart-types.tsx +49 -0
- package/src/components/charts/containers.tsx +11 -0
- package/src/components/charts/data.tsx +16 -0
- package/src/components/charts/details.tsx +25 -0
- package/src/components/charts/gauge.tsx +106 -0
- package/src/components/charts/grids.tsx +8 -0
- package/src/components/charts/index.ts +62 -0
- package/src/components/charts/labeled-bar-list.tsx +85 -0
- package/src/components/charts/references.tsx +8 -0
- package/src/components/charts/service-health-list.tsx +73 -0
- package/src/components/charts/sparkline.tsx +52 -0
- package/src/components/charts/stacked-bar.tsx +104 -0
- package/src/components/charts/text.tsx +10 -0
- package/src/components/charts/world-heat-map-inner.tsx +317 -0
- package/src/components/charts/world-heat-map.tsx +184 -0
- package/src/components/molecules/README.md +12 -0
- package/src/components/molecules/action-bar.tsx +73 -0
- package/src/components/molecules/activity-item.tsx +74 -0
- package/src/components/molecules/alert.tsx +80 -0
- package/src/components/molecules/animated-background.tsx +92 -0
- package/src/components/molecules/brand-lockup.tsx +48 -0
- package/src/components/molecules/breadcrumb.tsx +161 -0
- package/src/components/molecules/button-group.tsx +104 -0
- package/src/components/molecules/calendar.tsx +216 -0
- package/src/components/molecules/card.tsx +101 -0
- package/src/components/molecules/code-block.tsx +48 -0
- package/src/components/molecules/empty-state.tsx +55 -0
- package/src/components/molecules/error-state.tsx +42 -0
- package/src/components/molecules/field-display.tsx +35 -0
- package/src/components/molecules/input-otp.tsx +74 -0
- package/src/components/molecules/loading-state.tsx +36 -0
- package/src/components/molecules/notification-item.tsx +67 -0
- package/src/components/molecules/notification-list.tsx +45 -0
- package/src/components/molecules/number-badge.tsx +53 -0
- package/src/components/molecules/page-header.tsx +88 -0
- package/src/components/molecules/page-tabs.tsx +94 -0
- package/src/components/molecules/pagination.tsx +150 -0
- package/src/components/molecules/phone-input.tsx +200 -0
- package/src/components/molecules/search-bar.tsx +64 -0
- package/src/components/molecules/secret-field.tsx +158 -0
- package/src/components/molecules/section-card.tsx +91 -0
- package/src/components/molecules/stat-card.tsx +96 -0
- package/src/components/molecules/status-badge.tsx +42 -0
- package/src/components/molecules/stepper.tsx +96 -0
- package/src/components/molecules/table.tsx +157 -0
- package/src/components/molecules/toggle-group.tsx +145 -0
- package/src/components/molecules/tooltip.tsx +150 -0
- package/src/components/molecules/user-avatar-chip.tsx +71 -0
- package/src/components/organisms/README.md +14 -0
- package/src/components/organisms/accordion.tsx +149 -0
- package/src/components/organisms/alert-dialog.tsx +269 -0
- package/src/components/organisms/carousel.tsx +244 -0
- package/src/components/organisms/collapsible.tsx +69 -0
- package/src/components/organisms/command.tsx +143 -0
- package/src/components/organisms/context-menu.tsx +333 -0
- package/src/components/organisms/dashboard-grid.tsx +360 -0
- package/src/components/organisms/data-table.tsx +330 -0
- package/src/components/organisms/dialog.tsx +304 -0
- package/src/components/organisms/drawer.tsx +100 -0
- package/src/components/organisms/dropdown-menu.tsx +434 -0
- package/src/components/organisms/editors/code-editor.tsx +144 -0
- package/src/components/organisms/editors/index.ts +4 -0
- package/src/components/organisms/editors/markdown-editor.tsx +153 -0
- package/src/components/organisms/editors/markdown-renderer.ts +27 -0
- package/src/components/organisms/editors/prose-canvas-classes.ts +45 -0
- package/src/components/organisms/editors/rich-text-editor.tsx +126 -0
- package/src/components/organisms/editors/toolbar/md-toolbar.tsx +129 -0
- package/src/components/organisms/editors/toolbar/rte-toolbar.tsx +211 -0
- package/src/components/organisms/editors/toolbar/toolbar-shell.tsx +45 -0
- package/src/components/organisms/editors/use-codemirror-theme.ts +61 -0
- package/src/components/organisms/error-boundary.tsx +61 -0
- package/src/components/organisms/form.tsx +174 -0
- package/src/components/organisms/hover-card.tsx +114 -0
- package/src/components/organisms/menubar.tsx +491 -0
- package/src/components/organisms/navbar.tsx +101 -0
- package/src/components/organisms/navigation-menu.tsx +234 -0
- package/src/components/organisms/popover.tsx +144 -0
- package/src/components/organisms/resizable.tsx +39 -0
- package/src/components/organisms/schema-form.tsx +232 -0
- package/src/components/organisms/select.tsx +303 -0
- package/src/components/organisms/sheet.tsx +256 -0
- package/src/components/organisms/sidebar.tsx +1037 -0
- package/src/components/organisms/sonner.tsx +96 -0
- package/src/components/organisms/tabs.tsx +132 -0
- package/src/components/organisms/theme-provider.tsx +101 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/index.ts +547 -0
- package/src/lib/portal-container.tsx +35 -0
- package/src/lib/utils.ts +6 -0
- package/src/native.ts +23 -0
- package/src/tokens/colors.ts +91 -0
- package/src/tokens/index.ts +3 -0
- package/src/tokens/spacing.ts +55 -0
- package/src/tokens/typography.ts +27 -0
- package/styles/canvas.css +55 -0
- package/styles/dashboard-grid.css +47 -0
- package/styles/leaflet.css +13 -0
- package/styles/tokens.css +234 -0
- package/tailwind.config.ts +70 -0
- 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";
|