@etherealui/react 0.0.0-stage → 0.0.3
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/LICENSE +21 -0
- package/README.md +62 -2
- package/dist/index.d.mts +583 -0
- package/dist/index.d.ts +583 -0
- package/dist/index.js +5189 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +5189 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +83 -3
- package/src/blocks/app-shell.tsx +441 -0
- package/src/blocks/billing-card.tsx +2 -0
- package/src/blocks/data-table.tsx +479 -0
- package/src/blocks/filter-bar.tsx +134 -0
- package/src/blocks/header-navbar.tsx +7 -0
- package/src/blocks/metric-grid.tsx +5 -0
- package/src/blocks/profile-form.tsx +2 -0
- package/src/blocks/security-settings.tsx +2 -0
- package/src/blocks/settings-blocks.tsx +406 -0
- package/src/blocks/sidebar.tsx +6 -0
- package/src/blocks/stat-card.tsx +7 -0
- package/src/blocks/stat-cards.tsx +212 -0
- package/src/blocks/team-members.tsx +5 -0
- package/src/index.ts +114 -0
- package/src/primitives/accordion.tsx +61 -0
- package/src/primitives/announcement.tsx +33 -0
- package/src/primitives/avatar.tsx +156 -0
- package/src/primitives/badge.tsx +108 -0
- package/src/primitives/breadcrumb.tsx +45 -0
- package/src/primitives/button-group.tsx +79 -0
- package/src/primitives/button.tsx +165 -0
- package/src/primitives/carousel.tsx +84 -0
- package/src/primitives/chart.tsx +831 -0
- package/src/primitives/checkbox.tsx +26 -0
- package/src/primitives/chip.tsx +37 -0
- package/src/primitives/close-button.tsx +29 -0
- package/src/primitives/color-picker.tsx +62 -0
- package/src/primitives/combobox.tsx +123 -0
- package/src/primitives/command-palette.tsx +216 -0
- package/src/primitives/context-menu.tsx +84 -0
- package/src/primitives/date-calendar.tsx +76 -0
- package/src/primitives/date-picker.tsx +400 -0
- package/src/primitives/dialog.tsx +279 -0
- package/src/primitives/divider.tsx +31 -0
- package/src/primitives/drawer.tsx +95 -0
- package/src/primitives/dropdown-menu.tsx +164 -0
- package/src/primitives/file-upload.tsx +123 -0
- package/src/primitives/icon-button.tsx +23 -0
- package/src/primitives/input.tsx +124 -0
- package/src/primitives/modal.tsx +26 -0
- package/src/primitives/pagination.tsx +58 -0
- package/src/primitives/popover.tsx +28 -0
- package/src/primitives/progress.tsx +145 -0
- package/src/primitives/radio-group.tsx +63 -0
- package/src/primitives/select.tsx +207 -0
- package/src/primitives/skeleton.tsx +61 -0
- package/src/primitives/slider.tsx +44 -0
- package/src/primitives/switch.tsx +146 -0
- package/src/primitives/table.tsx +47 -0
- package/src/primitives/tabs.tsx +86 -0
- package/src/primitives/textarea.tsx +40 -0
- package/src/primitives/toast.tsx +71 -0
- package/src/primitives/tooltip.tsx +58 -0
- package/src/styles/globals.css +49 -0
- package/src/templates/control-tower.tsx +41 -0
- package/src/templates/executive-analytics.tsx +272 -0
- package/src/templates/workspace-settings.tsx +179 -0
- package/src/utils/cn.ts +7 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "../utils/cn"
|
|
3
|
+
|
|
4
|
+
export const Table = React.forwardRef<
|
|
5
|
+
HTMLTableElement,
|
|
6
|
+
React.HTMLAttributes<HTMLTableElement>
|
|
7
|
+
>(({ className, ...props }, ref) => (
|
|
8
|
+
<div className="w-full overflow-x-auto rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900">
|
|
9
|
+
<table
|
|
10
|
+
ref={ref}
|
|
11
|
+
className={cn("w-full border-collapse text-left text-xs", className)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
))
|
|
16
|
+
Table.displayName = "Table"
|
|
17
|
+
|
|
18
|
+
export const TableHeader = React.forwardRef<
|
|
19
|
+
HTMLTableSectionElement,
|
|
20
|
+
React.HTMLAttributes<HTMLTableSectionElement>
|
|
21
|
+
>(({ className, ...props }, ref) => (
|
|
22
|
+
<thead
|
|
23
|
+
ref={ref}
|
|
24
|
+
className={cn(
|
|
25
|
+
"border-b border-zinc-200 bg-zinc-50/80 text-[11px] font-medium text-zinc-500 dark:border-zinc-800 dark:bg-zinc-950/50 dark:text-zinc-400",
|
|
26
|
+
className
|
|
27
|
+
)}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
))
|
|
31
|
+
TableHeader.displayName = "TableHeader"
|
|
32
|
+
|
|
33
|
+
export const TableBody = React.forwardRef<
|
|
34
|
+
HTMLTableSectionElement,
|
|
35
|
+
React.HTMLAttributes<HTMLTableSectionElement>
|
|
36
|
+
>(({ className, ...props }, ref) => (
|
|
37
|
+
<tbody
|
|
38
|
+
ref={ref}
|
|
39
|
+
className={cn(
|
|
40
|
+
"divide-y divide-zinc-200 dark:divide-zinc-800",
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
/>
|
|
45
|
+
))
|
|
46
|
+
TableBody.displayName = "TableBody"
|
|
47
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
import { cn } from "../utils/cn"
|
|
5
|
+
|
|
6
|
+
const Tabs = TabsPrimitive.Root
|
|
7
|
+
|
|
8
|
+
const tabsListVariants = cva("inline-flex items-center text-zinc-500 dark:text-zinc-400", {
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
segmented:
|
|
12
|
+
"h-9 rounded-lg border border-zinc-200 bg-zinc-100/80 p-1 gap-1 dark:border-zinc-800 dark:bg-zinc-900",
|
|
13
|
+
underline:
|
|
14
|
+
"w-full justify-start border-b border-zinc-200 bg-transparent gap-6 px-1 dark:border-zinc-800",
|
|
15
|
+
pills: "gap-1.5 bg-transparent",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: {
|
|
19
|
+
variant: "segmented",
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export interface TabsListProps
|
|
24
|
+
extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>,
|
|
25
|
+
VariantProps<typeof tabsListVariants> {}
|
|
26
|
+
|
|
27
|
+
const TabsList = React.forwardRef<
|
|
28
|
+
React.ElementRef<typeof TabsPrimitive.List>,
|
|
29
|
+
TabsListProps
|
|
30
|
+
>(({ className, variant, ...props }, ref) => (
|
|
31
|
+
<TabsPrimitive.List
|
|
32
|
+
ref={ref}
|
|
33
|
+
className={cn(tabsListVariants({ variant, className }))}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
))
|
|
37
|
+
TabsList.displayName = TabsPrimitive.List.displayName
|
|
38
|
+
|
|
39
|
+
const tabsTriggerVariants = cva(
|
|
40
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap text-xs font-medium transition-all focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 cursor-pointer select-none",
|
|
41
|
+
{
|
|
42
|
+
variants: {
|
|
43
|
+
variant: {
|
|
44
|
+
segmented:
|
|
45
|
+
"rounded-md px-3 py-1 data-[state=active]:bg-white data-[state=active]:text-zinc-900 data-[state=active]:shadow-2xs hover:text-zinc-900 dark:data-[state=active]:bg-zinc-800 dark:data-[state=active]:text-zinc-100 dark:hover:text-zinc-200",
|
|
46
|
+
underline:
|
|
47
|
+
"border-b-2 border-transparent pb-2.5 pt-1 text-zinc-500 data-[state=active]:border-blue-600 data-[state=active]:text-zinc-900 hover:text-zinc-800 dark:text-zinc-400 dark:data-[state=active]:border-blue-500 dark:data-[state=active]:text-zinc-100 dark:hover:text-zinc-200",
|
|
48
|
+
pills:
|
|
49
|
+
"rounded-lg px-3 py-1.5 border border-transparent data-[state=active]:bg-blue-50 data-[state=active]:text-blue-700 data-[state=active]:border-blue-200 hover:bg-zinc-100 dark:data-[state=active]:bg-blue-500/15 dark:data-[state=active]:text-blue-300 dark:data-[state=active]:border-blue-500/30 dark:hover:bg-zinc-800",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
defaultVariants: {
|
|
53
|
+
variant: "segmented",
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
export interface TabsTriggerProps
|
|
59
|
+
extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>,
|
|
60
|
+
VariantProps<typeof tabsTriggerVariants> {}
|
|
61
|
+
|
|
62
|
+
const TabsTrigger = React.forwardRef<
|
|
63
|
+
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
64
|
+
TabsTriggerProps
|
|
65
|
+
>(({ className, variant, ...props }, ref) => (
|
|
66
|
+
<TabsPrimitive.Trigger
|
|
67
|
+
ref={ref}
|
|
68
|
+
className={cn(tabsTriggerVariants({ variant, className }))}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
))
|
|
72
|
+
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
|
73
|
+
|
|
74
|
+
const TabsContent = React.forwardRef<
|
|
75
|
+
React.ElementRef<typeof TabsPrimitive.Content>,
|
|
76
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
77
|
+
>(({ className, ...props }, ref) => (
|
|
78
|
+
<TabsPrimitive.Content
|
|
79
|
+
ref={ref}
|
|
80
|
+
className={cn("mt-3 focus-visible:outline-none", className)}
|
|
81
|
+
{...props}
|
|
82
|
+
/>
|
|
83
|
+
))
|
|
84
|
+
TabsContent.displayName = TabsPrimitive.Content.displayName
|
|
85
|
+
|
|
86
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "../utils/cn"
|
|
3
|
+
|
|
4
|
+
export interface TextareaProps
|
|
5
|
+
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
6
|
+
autoResize?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
10
|
+
({ className, autoResize = false, onChange, ...props }, ref) => {
|
|
11
|
+
const innerRef = React.useRef<HTMLTextAreaElement | null>(null)
|
|
12
|
+
|
|
13
|
+
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
14
|
+
if (autoResize && innerRef.current) {
|
|
15
|
+
innerRef.current.style.height = "auto"
|
|
16
|
+
innerRef.current.style.height = `${innerRef.current.scrollHeight}px`
|
|
17
|
+
}
|
|
18
|
+
onChange?.(e)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<textarea
|
|
23
|
+
ref={(node) => {
|
|
24
|
+
innerRef.current = node
|
|
25
|
+
if (typeof ref === "function") ref(node)
|
|
26
|
+
else if (ref) ref.current = node
|
|
27
|
+
}}
|
|
28
|
+
onChange={handleChange}
|
|
29
|
+
className={cn(
|
|
30
|
+
"flex min-h-[84px] w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder:text-zinc-400 transition-colors focus-visible:outline-none focus-visible:border-blue-500 focus-visible:ring-2 focus-visible:ring-blue-500/20 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 dark:placeholder:text-zinc-500",
|
|
31
|
+
autoResize && "resize-none overflow-hidden",
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
Textarea.displayName = "Textarea"
|
|
40
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { CheckCircle2, AlertTriangle, XCircle, Info, X } from "lucide-react"
|
|
3
|
+
import { cn } from "../utils/cn"
|
|
4
|
+
|
|
5
|
+
export type ToastVariant = "success" | "error" | "warning" | "info"
|
|
6
|
+
|
|
7
|
+
export interface ToastItem {
|
|
8
|
+
id: string
|
|
9
|
+
title: string
|
|
10
|
+
description?: string
|
|
11
|
+
variant?: ToastVariant
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const iconMap: Record<ToastVariant, React.ReactNode> = {
|
|
15
|
+
success: <CheckCircle2 className="h-4 w-4 text-emerald-600 dark:text-emerald-400 shrink-0" />,
|
|
16
|
+
error: <XCircle className="h-4 w-4 text-rose-600 dark:text-rose-400 shrink-0" />,
|
|
17
|
+
warning: <AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400 shrink-0" />,
|
|
18
|
+
info: <Info className="h-4 w-4 text-blue-600 dark:text-blue-400 shrink-0" />,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const borderMap: Record<ToastVariant, string> = {
|
|
22
|
+
success: "border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900",
|
|
23
|
+
error: "border-rose-200 bg-rose-50/40 dark:border-rose-500/30 dark:bg-rose-950/20",
|
|
24
|
+
warning: "border-amber-200 bg-amber-50/40 dark:border-amber-500/30 dark:bg-amber-950/20",
|
|
25
|
+
info: "border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ToastCardProps extends Omit<ToastItem, "id"> {
|
|
29
|
+
onDismiss?: () => void
|
|
30
|
+
className?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const ToastCard: React.FC<ToastCardProps> = ({
|
|
34
|
+
title,
|
|
35
|
+
description,
|
|
36
|
+
variant = "info",
|
|
37
|
+
onDismiss,
|
|
38
|
+
className,
|
|
39
|
+
}) => {
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
role="status"
|
|
43
|
+
className={cn(
|
|
44
|
+
"flex w-full max-w-sm items-start gap-3 rounded-xl border p-3.5 text-zinc-900 shadow-md transition-all dark:text-zinc-100",
|
|
45
|
+
borderMap[variant],
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
<div className="mt-0.5">{iconMap[variant]}</div>
|
|
50
|
+
<div className="flex-1 space-y-1">
|
|
51
|
+
<p className="text-xs font-semibold leading-none text-zinc-900 dark:text-zinc-100">
|
|
52
|
+
{title}
|
|
53
|
+
</p>
|
|
54
|
+
{description && (
|
|
55
|
+
<p className="text-xs text-zinc-500 dark:text-zinc-400 leading-relaxed">
|
|
56
|
+
{description}
|
|
57
|
+
</p>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
{onDismiss && (
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
onClick={onDismiss}
|
|
64
|
+
className="rounded-md p-1 text-zinc-400 hover:bg-zinc-100 hover:text-zinc-700 dark:hover:bg-zinc-800 dark:hover:text-zinc-200 cursor-pointer"
|
|
65
|
+
>
|
|
66
|
+
<X className="h-3.5 w-3.5" />
|
|
67
|
+
</button>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
|
3
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
|
4
|
+
import { cn } from "../utils/cn"
|
|
5
|
+
|
|
6
|
+
const TooltipProvider = TooltipPrimitive.Provider
|
|
7
|
+
const Tooltip = TooltipPrimitive.Root
|
|
8
|
+
const TooltipTrigger = TooltipPrimitive.Trigger
|
|
9
|
+
|
|
10
|
+
const TooltipContent = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof TooltipPrimitive.Content>,
|
|
12
|
+
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
|
13
|
+
>(({ className, sideOffset = 6, ...props }, ref) => (
|
|
14
|
+
<TooltipPrimitive.Portal>
|
|
15
|
+
<TooltipPrimitive.Content
|
|
16
|
+
ref={ref}
|
|
17
|
+
sideOffset={sideOffset}
|
|
18
|
+
className={cn(
|
|
19
|
+
"z-50 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-900 px-2.5 py-1.5 text-xs text-white shadow-md dark:border-zinc-800 dark:bg-zinc-100 dark:text-zinc-900",
|
|
20
|
+
className
|
|
21
|
+
)}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
</TooltipPrimitive.Portal>
|
|
25
|
+
))
|
|
26
|
+
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
|
27
|
+
|
|
28
|
+
const Popover = PopoverPrimitive.Root
|
|
29
|
+
const PopoverTrigger = PopoverPrimitive.Trigger
|
|
30
|
+
|
|
31
|
+
const PopoverContent = React.forwardRef<
|
|
32
|
+
React.ElementRef<typeof PopoverPrimitive.Content>,
|
|
33
|
+
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
|
34
|
+
>(({ className, align = "center", sideOffset = 6, ...props }, ref) => (
|
|
35
|
+
<PopoverPrimitive.Portal>
|
|
36
|
+
<PopoverPrimitive.Content
|
|
37
|
+
ref={ref}
|
|
38
|
+
align={align}
|
|
39
|
+
sideOffset={sideOffset}
|
|
40
|
+
className={cn(
|
|
41
|
+
"z-50 w-72 rounded-xl border border-zinc-200 bg-white p-4 text-zinc-900 shadow-lg outline-none dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100",
|
|
42
|
+
className
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
</PopoverPrimitive.Portal>
|
|
47
|
+
))
|
|
48
|
+
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
Tooltip,
|
|
52
|
+
TooltipTrigger,
|
|
53
|
+
TooltipContent,
|
|
54
|
+
TooltipProvider,
|
|
55
|
+
Popover,
|
|
56
|
+
PopoverTrigger,
|
|
57
|
+
PopoverContent,
|
|
58
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@source "../**/*.{ts,tsx}";
|
|
3
|
+
|
|
4
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
5
|
+
|
|
6
|
+
@theme {
|
|
7
|
+
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
8
|
+
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
:root {
|
|
12
|
+
--bg-canvas: #ffffff;
|
|
13
|
+
--bg-subtle: #fafafa;
|
|
14
|
+
--bg-muted: #f4f4f5;
|
|
15
|
+
--bg-elevated: #ffffff;
|
|
16
|
+
--fg-primary: #18181b;
|
|
17
|
+
--fg-secondary: #52525b;
|
|
18
|
+
--fg-muted: #71717a;
|
|
19
|
+
--border-default: #e4e4e7;
|
|
20
|
+
--border-subtle: #f4f4f5;
|
|
21
|
+
--accent-primary: #2563eb;
|
|
22
|
+
--accent-soft: #dbeafe;
|
|
23
|
+
color-scheme: light;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.dark {
|
|
27
|
+
--bg-canvas: #09090b;
|
|
28
|
+
--bg-subtle: #121215;
|
|
29
|
+
--bg-muted: #18181b;
|
|
30
|
+
--bg-elevated: #18181b;
|
|
31
|
+
--fg-primary: #fafafa;
|
|
32
|
+
--fg-secondary: #a1a1aa;
|
|
33
|
+
--fg-muted: #71717a;
|
|
34
|
+
--border-default: #27272a;
|
|
35
|
+
--border-subtle: #18181b;
|
|
36
|
+
--accent-primary: #3b82f6;
|
|
37
|
+
--accent-soft: rgba(59, 130, 246, 0.15);
|
|
38
|
+
color-scheme: dark;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@layer base {
|
|
42
|
+
body {
|
|
43
|
+
background-color: var(--bg-canvas);
|
|
44
|
+
color: var(--fg-primary);
|
|
45
|
+
font-family: var(--font-sans);
|
|
46
|
+
-webkit-font-smoothing: antialiased;
|
|
47
|
+
-moz-osx-font-smoothing: grayscale;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "../utils/cn"
|
|
3
|
+
import {
|
|
4
|
+
SidebarShell,
|
|
5
|
+
HeaderNavbar,
|
|
6
|
+
PageHeaderBar,
|
|
7
|
+
} from "../blocks/app-shell"
|
|
8
|
+
import { AdvancedFilterBar } from "../blocks/filter-bar"
|
|
9
|
+
import { EnterpriseDataTable } from "../blocks/data-table"
|
|
10
|
+
|
|
11
|
+
export const ControlTowerTemplate: React.FC<{ className?: string }> = ({
|
|
12
|
+
className,
|
|
13
|
+
}) => {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
className={cn(
|
|
17
|
+
"flex min-h-[720px] w-full overflow-hidden rounded-2xl border border-zinc-200 bg-zinc-50/40 text-zinc-900 shadow-sm dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
>
|
|
21
|
+
<SidebarShell activeId="customers" />
|
|
22
|
+
|
|
23
|
+
<div className="flex flex-1 flex-col min-w-0">
|
|
24
|
+
<HeaderNavbar
|
|
25
|
+
breadcrumbs={["Workspace", "Customers", "Directory"]}
|
|
26
|
+
/>
|
|
27
|
+
|
|
28
|
+
<main className="flex-1 space-y-5 overflow-y-auto p-6">
|
|
29
|
+
<PageHeaderBar
|
|
30
|
+
title="Customer Directory"
|
|
31
|
+
subtitle="Search, filter, and inspect customer subscriptions. Click any row to open the details drawer."
|
|
32
|
+
/>
|
|
33
|
+
|
|
34
|
+
<AdvancedFilterBar />
|
|
35
|
+
|
|
36
|
+
<EnterpriseDataTable />
|
|
37
|
+
</main>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { ArrowUpRight, RefreshCw } from "lucide-react"
|
|
3
|
+
import { cn } from "../utils/cn"
|
|
4
|
+
import {
|
|
5
|
+
SidebarShell,
|
|
6
|
+
HeaderNavbar,
|
|
7
|
+
PageHeaderBar,
|
|
8
|
+
} from "../blocks/app-shell"
|
|
9
|
+
import { MultiMetricGrid, ProgressMetricCard } from "../blocks/stat-cards"
|
|
10
|
+
import { Badge } from "../primitives/badge"
|
|
11
|
+
import { Button, ToggleGroup } from "../primitives/button"
|
|
12
|
+
import { Avatar } from "../primitives/avatar"
|
|
13
|
+
import { CommandPalette } from "../primitives/command-palette"
|
|
14
|
+
|
|
15
|
+
const chartSeriesData: Record<
|
|
16
|
+
string,
|
|
17
|
+
Array<{ label: string; revenue: number; signups: number }>
|
|
18
|
+
> = {
|
|
19
|
+
"30d": [
|
|
20
|
+
{ label: "Oct 08", revenue: 62, signups: 48 },
|
|
21
|
+
{ label: "Oct 12", revenue: 68, signups: 54 },
|
|
22
|
+
{ label: "Oct 16", revenue: 64, signups: 59 },
|
|
23
|
+
{ label: "Oct 20", revenue: 75, signups: 66 },
|
|
24
|
+
{ label: "Oct 24", revenue: 81, signups: 72 },
|
|
25
|
+
{ label: "Oct 28", revenue: 79, signups: 76 },
|
|
26
|
+
{ label: "Nov 01", revenue: 89, signups: 84 },
|
|
27
|
+
{ label: "Nov 06", revenue: 96, signups: 92 },
|
|
28
|
+
],
|
|
29
|
+
"7d": [
|
|
30
|
+
{ label: "Mon", revenue: 74, signups: 68 },
|
|
31
|
+
{ label: "Tue", revenue: 79, signups: 72 },
|
|
32
|
+
{ label: "Wed", revenue: 83, signups: 77 },
|
|
33
|
+
{ label: "Thu", revenue: 80, signups: 75 },
|
|
34
|
+
{ label: "Fri", revenue: 88, signups: 85 },
|
|
35
|
+
{ label: "Sat", revenue: 92, signups: 89 },
|
|
36
|
+
{ label: "Sun", revenue: 97, signups: 94 },
|
|
37
|
+
],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const recentActivity = [
|
|
41
|
+
{
|
|
42
|
+
id: "act-1",
|
|
43
|
+
title: "Linear Orbit Inc. upgraded to Enterprise Annual",
|
|
44
|
+
meta: "+$4,200 MRR • Invoice #INV-2094 paid",
|
|
45
|
+
time: "12m ago",
|
|
46
|
+
fallback: "LO",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "act-2",
|
|
50
|
+
title: "Raycast Labs added 12 team seats",
|
|
51
|
+
meta: "Pro Plan expansion •Prorated billing applied",
|
|
52
|
+
time: "42m ago",
|
|
53
|
+
fallback: "RL",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "act-3",
|
|
57
|
+
title: "Weekly financial summary exported",
|
|
58
|
+
meta: "Generated by Elena Rostova",
|
|
59
|
+
time: "2h ago",
|
|
60
|
+
fallback: "ER",
|
|
61
|
+
},
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
const topCustomers = [
|
|
65
|
+
{
|
|
66
|
+
name: "Linear Orbit Inc.",
|
|
67
|
+
domain: "linearorbit.io",
|
|
68
|
+
fallback: "LO",
|
|
69
|
+
mrr: "$4,200",
|
|
70
|
+
growth: "+18.4%",
|
|
71
|
+
status: "Enterprise",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "Raycast Labs GmbH",
|
|
75
|
+
domain: "raycastlabs.de",
|
|
76
|
+
fallback: "RL",
|
|
77
|
+
mrr: "$3,150",
|
|
78
|
+
growth: "+12.1%",
|
|
79
|
+
status: "Enterprise",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "Northstar Analytics",
|
|
83
|
+
domain: "northstar.co",
|
|
84
|
+
fallback: "NA",
|
|
85
|
+
mrr: "$1,950",
|
|
86
|
+
growth: "+9.4%",
|
|
87
|
+
status: "Scale",
|
|
88
|
+
},
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
export const ExecutiveAnalyticsTemplate: React.FC<{ className?: string }> = ({
|
|
92
|
+
className,
|
|
93
|
+
}) => {
|
|
94
|
+
const [range, setRange] = React.useState("30d")
|
|
95
|
+
const [cmdOpen, setCmdOpen] = React.useState(false)
|
|
96
|
+
const points = chartSeriesData[range] || chartSeriesData["30d"]
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
className={cn(
|
|
101
|
+
"flex min-h-[720px] w-full overflow-hidden rounded-2xl border border-zinc-200 bg-zinc-50/40 text-zinc-900 shadow-sm dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100",
|
|
102
|
+
className
|
|
103
|
+
)}
|
|
104
|
+
>
|
|
105
|
+
<CommandPalette
|
|
106
|
+
open={cmdOpen}
|
|
107
|
+
onOpenChange={setCmdOpen}
|
|
108
|
+
enableGlobalShortcut={false}
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
<SidebarShell activeId="overview" />
|
|
112
|
+
|
|
113
|
+
<div className="flex flex-1 flex-col min-w-0">
|
|
114
|
+
<HeaderNavbar
|
|
115
|
+
breadcrumbs={["Workspace", "Analytics", "Overview"]}
|
|
116
|
+
onOpenCommandPalette={() => setCmdOpen(true)}
|
|
117
|
+
/>
|
|
118
|
+
|
|
119
|
+
<main className="flex-1 space-y-6 overflow-y-auto p-6">
|
|
120
|
+
<PageHeaderBar
|
|
121
|
+
title="Analytics Overview"
|
|
122
|
+
subtitle="Track recurring revenue, subscriber retention, and account expansion."
|
|
123
|
+
/>
|
|
124
|
+
|
|
125
|
+
<MultiMetricGrid />
|
|
126
|
+
|
|
127
|
+
<div className="grid grid-cols-1 gap-6 xl:grid-cols-3">
|
|
128
|
+
<div className="xl:col-span-2 rounded-xl border border-zinc-200 bg-white p-5 flex flex-col justify-between dark:border-zinc-800 dark:bg-zinc-900">
|
|
129
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between border-b border-zinc-200 pb-4 dark:border-zinc-800">
|
|
130
|
+
<div>
|
|
131
|
+
<div className="flex items-center gap-2">
|
|
132
|
+
<h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
|
133
|
+
Revenue & Subscriber Growth
|
|
134
|
+
</h3>
|
|
135
|
+
<Badge variant="emerald">+14.2%</Badge>
|
|
136
|
+
</div>
|
|
137
|
+
<p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
|
|
138
|
+
Net settled volume across active subscriptions
|
|
139
|
+
</p>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<ToggleGroup
|
|
143
|
+
size="sm"
|
|
144
|
+
value={range}
|
|
145
|
+
onValueChange={setRange}
|
|
146
|
+
items={[
|
|
147
|
+
{ value: "7d", label: "7d" },
|
|
148
|
+
{ value: "30d", label: "30d" },
|
|
149
|
+
]}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div className="mt-6 grid grid-cols-8 items-end gap-3 h-48 pt-4 px-2">
|
|
154
|
+
{points.map((pt) => (
|
|
155
|
+
<div
|
|
156
|
+
key={pt.label}
|
|
157
|
+
className="group flex flex-col items-center gap-2 h-full justify-end"
|
|
158
|
+
>
|
|
159
|
+
<div className="w-full flex items-end justify-center gap-1.5 h-full">
|
|
160
|
+
<div
|
|
161
|
+
style={{ height: `${pt.revenue}%` }}
|
|
162
|
+
className="w-full max-w-[18px] rounded-t-md bg-blue-600 transition-all duration-300 group-hover:bg-blue-500"
|
|
163
|
+
/>
|
|
164
|
+
<div
|
|
165
|
+
style={{ height: `${pt.signups}%` }}
|
|
166
|
+
className="w-full max-w-[14px] rounded-t-md bg-zinc-200 transition-all duration-300 dark:bg-zinc-700"
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
<span className="text-[10px] text-zinc-400">
|
|
170
|
+
{pt.label}
|
|
171
|
+
</span>
|
|
172
|
+
</div>
|
|
173
|
+
))}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div className="space-y-4">
|
|
178
|
+
<ProgressMetricCard />
|
|
179
|
+
|
|
180
|
+
<div className="rounded-xl border border-zinc-200 bg-white p-5 space-y-4 dark:border-zinc-800 dark:bg-zinc-900">
|
|
181
|
+
<div className="flex items-center justify-between">
|
|
182
|
+
<h3 className="text-xs font-semibold text-zinc-700 dark:text-zinc-300">
|
|
183
|
+
Recent Activity
|
|
184
|
+
</h3>
|
|
185
|
+
<RefreshCw className="h-3.5 w-3.5 text-zinc-400" />
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div className="space-y-3.5">
|
|
189
|
+
{recentActivity.map((item, idx) => (
|
|
190
|
+
<div
|
|
191
|
+
key={item.id}
|
|
192
|
+
className="flex items-start justify-between gap-3 text-xs"
|
|
193
|
+
>
|
|
194
|
+
<div className="flex items-start gap-2.5">
|
|
195
|
+
<Avatar
|
|
196
|
+
fallback={item.fallback}
|
|
197
|
+
tint={idx % 2 === 0 ? "blue" : "neutral"}
|
|
198
|
+
size="xs"
|
|
199
|
+
/>
|
|
200
|
+
<div>
|
|
201
|
+
<p className="font-medium text-zinc-800 dark:text-zinc-200">
|
|
202
|
+
{item.title}
|
|
203
|
+
</p>
|
|
204
|
+
<p className="text-[11px] text-zinc-400">
|
|
205
|
+
{item.meta}
|
|
206
|
+
</p>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
<span className="text-[10px] text-zinc-400 shrink-0">
|
|
210
|
+
{item.time}
|
|
211
|
+
</span>
|
|
212
|
+
</div>
|
|
213
|
+
))}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<div className="rounded-xl border border-zinc-200 bg-white overflow-hidden dark:border-zinc-800 dark:bg-zinc-900">
|
|
220
|
+
<div className="flex items-center justify-between border-b border-zinc-200 px-5 py-3.5 dark:border-zinc-800">
|
|
221
|
+
<div>
|
|
222
|
+
<h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
|
223
|
+
Top Accounts
|
|
224
|
+
</h3>
|
|
225
|
+
<p className="text-xs text-zinc-500">
|
|
226
|
+
Sorted by monthly recurring revenue
|
|
227
|
+
</p>
|
|
228
|
+
</div>
|
|
229
|
+
<Button variant="outline" size="sm">
|
|
230
|
+
View all
|
|
231
|
+
<ArrowUpRight className="h-3.5 w-3.5" />
|
|
232
|
+
</Button>
|
|
233
|
+
</div>
|
|
234
|
+
<div className="divide-y divide-zinc-200 dark:divide-zinc-800">
|
|
235
|
+
{topCustomers.map((cust, i) => (
|
|
236
|
+
<div
|
|
237
|
+
key={cust.name}
|
|
238
|
+
className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between px-5 py-3 text-xs"
|
|
239
|
+
>
|
|
240
|
+
<div className="flex items-center gap-3">
|
|
241
|
+
<Avatar
|
|
242
|
+
fallback={cust.fallback}
|
|
243
|
+
tint={i % 2 === 0 ? "blue" : "neutral"}
|
|
244
|
+
size="sm"
|
|
245
|
+
/>
|
|
246
|
+
<div>
|
|
247
|
+
<p className="font-medium text-zinc-900 dark:text-zinc-100">
|
|
248
|
+
{cust.name}
|
|
249
|
+
</p>
|
|
250
|
+
<p className="text-[11px] text-zinc-400">{cust.domain}</p>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
<div className="flex items-center gap-6">
|
|
254
|
+
<Badge variant="default">{cust.status}</Badge>
|
|
255
|
+
<div className="text-right">
|
|
256
|
+
<span className="font-semibold text-zinc-900 dark:text-zinc-100">
|
|
257
|
+
{cust.mrr}
|
|
258
|
+
</span>
|
|
259
|
+
<span className="ml-2 text-emerald-600 dark:text-emerald-400">
|
|
260
|
+
{cust.growth}
|
|
261
|
+
</span>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
))}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</main>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
)
|
|
272
|
+
}
|