@burdenoff/microfe-admin 2026.610.1 → 2026.625.1
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/dist/admin/components/AccessDenied.js +2 -2
- package/dist/admin/components/AccessDenied.js.map +1 -1
- package/dist/admin/components/ActivityFeed.js +4 -4
- package/dist/admin/components/ActivityFeed.js.map +1 -1
- package/dist/admin/components/Charts.js +4 -4
- package/dist/admin/components/Charts.js.map +1 -1
- package/dist/admin/components/ConfirmDialog.js +6 -6
- package/dist/admin/components/ConfirmDialog.js.map +1 -1
- package/dist/admin/components/DataTable.js +8 -8
- package/dist/admin/components/DataTable.js.map +1 -1
- package/dist/admin/components/FormSheet.js +7 -7
- package/dist/admin/components/FormSheet.js.map +1 -1
- package/dist/admin/components/MetricCard.js +43 -40
- package/dist/admin/components/MetricCard.js.map +1 -1
- package/dist/admin/components/PageLayout.js +28 -22
- package/dist/admin/components/PageLayout.js.map +1 -1
- package/dist/admin/components/PermissionGuard.js +1 -1
- package/dist/admin/components/PermissionGuard.js.map +1 -1
- package/dist/admin/components/RowActions.js +2 -2
- package/dist/admin/components/RowActions.js.map +1 -1
- package/dist/admin/components/TenantMembersPanel.js +15 -15
- package/dist/admin/components/TenantMembersPanel.js.map +1 -1
- package/dist/admin/components/TenantSsoPanel.js +14 -14
- package/dist/admin/components/TenantSsoPanel.js.map +1 -1
- package/dist/admin/components/Toast.js +9 -9
- package/dist/admin/components/Toast.js.map +1 -1
- package/dist/admin/components/forms/SubscriptionForm.js +10 -10
- package/dist/admin/components/forms/SubscriptionForm.js.map +1 -1
- package/dist/admin/components/forms/TenantForm.js +8 -8
- package/dist/admin/components/forms/TenantForm.js.map +1 -1
- package/dist/admin/components/forms/TicketForm.js +2 -2
- package/dist/admin/components/forms/TicketForm.js.map +1 -1
- package/dist/admin/components/forms/UserForm.js +8 -8
- package/dist/admin/components/forms/UserForm.js.map +1 -1
- package/dist/admin/components/ui.js +58 -51
- package/dist/admin/components/ui.js.map +1 -1
- package/dist/admin/constants/index.js +8 -8
- package/dist/admin/constants/index.js.map +1 -1
- package/dist/admin/pages/AuditLogsPage.js +10 -8
- package/dist/admin/pages/AuditLogsPage.js.map +1 -1
- package/dist/admin/pages/BillingPage.js +51 -43
- package/dist/admin/pages/BillingPage.js.map +1 -1
- package/dist/admin/pages/CommunicationsPage.js +5 -2
- package/dist/admin/pages/CommunicationsPage.js.map +1 -1
- package/dist/admin/pages/ContentPagesPage.js +2 -0
- package/dist/admin/pages/ContentPagesPage.js.map +1 -1
- package/dist/admin/pages/DashboardPage.js +7 -4
- package/dist/admin/pages/DashboardPage.js.map +1 -1
- package/dist/admin/pages/FeatureFlagsPage.js +6 -4
- package/dist/admin/pages/FeatureFlagsPage.js.map +1 -1
- package/dist/admin/pages/OrganizationsListPage.js +8 -6
- package/dist/admin/pages/OrganizationsListPage.js.map +1 -1
- package/dist/admin/pages/ProductsPage.js +10 -7
- package/dist/admin/pages/ProductsPage.js.map +1 -1
- package/dist/admin/pages/ReferralsPage.js +27 -23
- package/dist/admin/pages/ReferralsPage.js.map +1 -1
- package/dist/admin/pages/RolesPermissionsPage.js +11 -8
- package/dist/admin/pages/RolesPermissionsPage.js.map +1 -1
- package/dist/admin/pages/SREPage.js +12 -8
- package/dist/admin/pages/SREPage.js.map +1 -1
- package/dist/admin/pages/SandboxSettingsPage.js +21 -20
- package/dist/admin/pages/SandboxSettingsPage.js.map +1 -1
- package/dist/admin/pages/SettingsPage.js +154 -142
- package/dist/admin/pages/SettingsPage.js.map +1 -1
- package/dist/admin/pages/SyncFailuresPage.js +14 -11
- package/dist/admin/pages/SyncFailuresPage.js.map +1 -1
- package/dist/admin/pages/TenantDetailPage.js +22 -22
- package/dist/admin/pages/TenantDetailPage.js.map +1 -1
- package/dist/admin/pages/TenantFormRoutePage.js +2 -2
- package/dist/admin/pages/TenantFormRoutePage.js.map +1 -1
- package/dist/admin/pages/TenantsListPage.js +7 -5
- package/dist/admin/pages/TenantsListPage.js.map +1 -1
- package/dist/admin/pages/UsersListPage.js +8 -6
- package/dist/admin/pages/UsersListPage.js.map +1 -1
- package/dist/admin/pages/WaitlistPage.js +9 -7
- package/dist/admin/pages/WaitlistPage.js.map +1 -1
- package/dist/admin/pages/WorkflowsPage.js +6 -3
- package/dist/admin/pages/WorkflowsPage.js.map +1 -1
- package/dist/admin/pages/WorkspacesListPage.js +9 -8
- package/dist/admin/pages/WorkspacesListPage.js.map +1 -1
- package/package.json +2 -2
|
@@ -12,11 +12,11 @@ var r = ({ message: r = "You don't have permission to access this page." }) => /
|
|
|
12
12
|
children: /* @__PURE__ */ t(e, { className: "w-7 h-7 text-red-500" })
|
|
13
13
|
}),
|
|
14
14
|
/* @__PURE__ */ t("h2", {
|
|
15
|
-
className: "text-lg font-semibold text-
|
|
15
|
+
className: "text-lg font-semibold text-text-primary",
|
|
16
16
|
children: "Access Denied"
|
|
17
17
|
}),
|
|
18
18
|
/* @__PURE__ */ t("p", {
|
|
19
|
-
className: "text-sm text-
|
|
19
|
+
className: "text-sm text-text-secondary max-w-sm",
|
|
20
20
|
children: r
|
|
21
21
|
})
|
|
22
22
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AccessDenied.js","names":[],"sources":["../../../src/admin/components/AccessDenied.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { ShieldOff } from 'lucide-react';\n\ninterface AccessDeniedProps {\n message?: string;\n}\n\nexport const AccessDenied: FC<AccessDeniedProps> = ({\n message = \"You don't have permission to access this page.\",\n}) => {\n return (\n <div role=\"alert\" className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-3\">\n <div className=\"w-14 h-14 mx-auto rounded-full bg-red-50 dark:bg-red-950/30 flex items-center justify-center\">\n <ShieldOff className=\"w-7 h-7 text-red-500\" />\n </div>\n <h2 className=\"text-lg font-semibold text-
|
|
1
|
+
{"version":3,"file":"AccessDenied.js","names":[],"sources":["../../../src/admin/components/AccessDenied.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { ShieldOff } from 'lucide-react';\n\ninterface AccessDeniedProps {\n message?: string;\n}\n\nexport const AccessDenied: FC<AccessDeniedProps> = ({\n message = \"You don't have permission to access this page.\",\n}) => {\n return (\n <div role=\"alert\" className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-3\">\n <div className=\"w-14 h-14 mx-auto rounded-full bg-red-50 dark:bg-red-950/30 flex items-center justify-center\">\n <ShieldOff className=\"w-7 h-7 text-red-500\" />\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Access Denied</h2>\n <p className=\"text-sm text-text-secondary max-w-sm\">{message}</p>\n </div>\n </div>\n );\n};\n"],"mappings":";;;AAOA,IAAa,KAAuC,EAClD,aAAU,uDAGR,kBAAC,OAAD;CAAK,MAAK;CAAQ,WAAU;WAC1B,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD,EAAW,WAAU,wBAAyB,CAAA;IAC1C,CAAA;GACN,kBAAC,MAAD;IAAI,WAAU;cAA0C;IAAkB,CAAA;GAC1E,kBAAC,KAAD;IAAG,WAAU;cAAwC;IAAY,CAAA;GAC7D;;CACF,CAAA"}
|
|
@@ -23,7 +23,7 @@ function S({ activities: e, maxItems: a = 10, loading: o, viewAllLink: s = "/act
|
|
|
23
23
|
className: "space-y-4",
|
|
24
24
|
children: Array.from({ length: 5 }).map((e, t) => /* @__PURE__ */ b(w, {}, t))
|
|
25
25
|
}) : l.length === 0 ? /* @__PURE__ */ b("p", {
|
|
26
|
-
className: "py-8 text-center text-sm text-
|
|
26
|
+
className: "py-8 text-center text-sm text-text-secondary",
|
|
27
27
|
children: "No recent activity"
|
|
28
28
|
}) : /* @__PURE__ */ b("div", {
|
|
29
29
|
className: "space-y-4",
|
|
@@ -39,17 +39,17 @@ function C({ activity: t }) {
|
|
|
39
39
|
className: e("rounded-full p-2", {
|
|
40
40
|
info: "bg-info/10 text-info",
|
|
41
41
|
warning: "bg-warning/10 text-warning",
|
|
42
|
-
error: "bg-
|
|
42
|
+
error: "bg-status-error-bg/10 text-status-error-text",
|
|
43
43
|
success: "bg-success/10 text-success"
|
|
44
44
|
}[t.severity || "info"]),
|
|
45
45
|
children: n
|
|
46
46
|
}), /* @__PURE__ */ x("div", {
|
|
47
47
|
className: "min-w-0 flex-1",
|
|
48
48
|
children: [/* @__PURE__ */ b("p", {
|
|
49
|
-
className: "text-sm font-medium text-
|
|
49
|
+
className: "text-sm font-medium text-text-primary",
|
|
50
50
|
children: t.description
|
|
51
51
|
}), /* @__PURE__ */ x("div", {
|
|
52
|
-
className: "mt-1 flex items-center gap-2 text-xs text-
|
|
52
|
+
className: "mt-1 flex items-center gap-2 text-xs text-text-secondary",
|
|
53
53
|
children: [
|
|
54
54
|
/* @__PURE__ */ b("span", { children: t.actor }),
|
|
55
55
|
/* @__PURE__ */ b("span", { children: "-" }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActivityFeed.js","names":[],"sources":["../../../src/admin/components/ActivityFeed.tsx"],"sourcesContent":["import { Link } from 'react-router-dom';\nimport {\n Plus,\n Ban,\n AlertTriangle,\n CheckCircle,\n UserPlus,\n Rocket,\n XCircle,\n Activity,\n CreditCard,\n Mail,\n Settings,\n Shield,\n} from 'lucide-react';\nimport { Card, CardHeader, CardTitle, CardContent, Skeleton } from './ui';\nimport { cn } from '../utils/cn';\nimport { formatRelativeTime } from '../utils/formatters';\nimport type { Activity as ActivityType } from '../types';\n\ninterface ActivityFeedProps {\n /** List of activities */\n activities: ActivityType[];\n /** Maximum items to display */\n maxItems?: number;\n /** Loading state */\n loading?: boolean;\n /** Link to view all activities */\n viewAllLink?: string;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Activity feed component showing recent platform activities\n */\nexport function ActivityFeed({\n activities,\n maxItems = 10,\n loading,\n viewAllLink = '/activity',\n className,\n}: ActivityFeedProps) {\n const displayActivities = activities.slice(0, maxItems);\n\n return (\n <Card className={className}>\n <CardHeader className=\"flex flex-row items-center justify-between\">\n <CardTitle className=\"text-lg\">Recent Activity</CardTitle>\n <Link to={viewAllLink} className=\"text-sm text-primary hover:underline\">\n View All\n </Link>\n </CardHeader>\n <CardContent>\n {loading ? (\n <div className=\"space-y-4\">\n {Array.from({ length: 5 }).map((_, i) => (\n <ActivityItemSkeleton key={i} />\n ))}\n </div>\n ) : displayActivities.length === 0 ? (\n <p className=\"py-8 text-center text-sm text-
|
|
1
|
+
{"version":3,"file":"ActivityFeed.js","names":[],"sources":["../../../src/admin/components/ActivityFeed.tsx"],"sourcesContent":["import { Link } from 'react-router-dom';\nimport {\n Plus,\n Ban,\n AlertTriangle,\n CheckCircle,\n UserPlus,\n Rocket,\n XCircle,\n Activity,\n CreditCard,\n Mail,\n Settings,\n Shield,\n} from 'lucide-react';\nimport { Card, CardHeader, CardTitle, CardContent, Skeleton } from './ui';\nimport { cn } from '../utils/cn';\nimport { formatRelativeTime } from '../utils/formatters';\nimport type { Activity as ActivityType } from '../types';\n\ninterface ActivityFeedProps {\n /** List of activities */\n activities: ActivityType[];\n /** Maximum items to display */\n maxItems?: number;\n /** Loading state */\n loading?: boolean;\n /** Link to view all activities */\n viewAllLink?: string;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Activity feed component showing recent platform activities\n */\nexport function ActivityFeed({\n activities,\n maxItems = 10,\n loading,\n viewAllLink = '/activity',\n className,\n}: ActivityFeedProps) {\n const displayActivities = activities.slice(0, maxItems);\n\n return (\n <Card className={className}>\n <CardHeader className=\"flex flex-row items-center justify-between\">\n <CardTitle className=\"text-lg\">Recent Activity</CardTitle>\n <Link to={viewAllLink} className=\"text-sm text-primary hover:underline\">\n View All\n </Link>\n </CardHeader>\n <CardContent>\n {loading ? (\n <div className=\"space-y-4\">\n {Array.from({ length: 5 }).map((_, i) => (\n <ActivityItemSkeleton key={i} />\n ))}\n </div>\n ) : displayActivities.length === 0 ? (\n <p className=\"py-8 text-center text-sm text-text-secondary\">No recent activity</p>\n ) : (\n <div className=\"space-y-4\">\n {displayActivities.map((activity) => (\n <ActivityItem key={activity.id} activity={activity} />\n ))}\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n\ninterface ActivityItemProps {\n activity: ActivityType;\n}\n\nfunction ActivityItem({ activity }: ActivityItemProps) {\n const icon = getActivityIcon(activity.type);\n const severityColors = {\n info: 'bg-info/10 text-info',\n warning: 'bg-warning/10 text-warning',\n error: 'bg-status-error-bg/10 text-status-error-text',\n success: 'bg-success/10 text-success',\n };\n const severity = activity.severity || 'info';\n\n return (\n <div className=\"flex items-start gap-3\">\n <div className={cn('rounded-full p-2', severityColors[severity])}>{icon}</div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">{activity.description}</p>\n <div className=\"mt-1 flex items-center gap-2 text-xs text-text-secondary\">\n <span>{activity.actor}</span>\n <span>-</span>\n <span>{formatRelativeTime(activity.timestamp)}</span>\n </div>\n </div>\n </div>\n );\n}\n\nfunction ActivityItemSkeleton() {\n return (\n <div className=\"flex items-start gap-3\">\n <Skeleton className=\"h-9 w-9 rounded-full\" />\n <div className=\"flex-1 space-y-2\">\n <Skeleton className=\"h-4 w-3/4\" />\n <Skeleton className=\"h-3 w-1/2\" />\n </div>\n </div>\n );\n}\n\nfunction getActivityIcon(type: string) {\n const iconMap: Record<string, React.ReactNode> = {\n 'tenant.created': <Plus className=\"h-4 w-4\" />,\n 'tenant.suspended': <Ban className=\"h-4 w-4\" />,\n 'tenant.activated': <CheckCircle className=\"h-4 w-4\" />,\n 'payment.failed': <AlertTriangle className=\"h-4 w-4\" />,\n 'payment.succeeded': <CreditCard className=\"h-4 w-4\" />,\n 'user.created': <UserPlus className=\"h-4 w-4\" />,\n 'user.blocked': <Ban className=\"h-4 w-4\" />,\n 'deployment.started': <Rocket className=\"h-4 w-4\" />,\n 'deployment.completed': <CheckCircle className=\"h-4 w-4\" />,\n 'deployment.failed': <XCircle className=\"h-4 w-4\" />,\n 'newsletter.sent': <Mail className=\"h-4 w-4\" />,\n 'settings.changed': <Settings className=\"h-4 w-4\" />,\n 'security.alert': <Shield className=\"h-4 w-4\" />,\n };\n\n return iconMap[type] || <Activity className=\"h-4 w-4\" />;\n}\n"],"mappings":";;;;;;;AAoCA,SAAgB,EAAa,EAC3B,eACA,cAAW,IACX,YACA,iBAAc,aACd,gBACoB;CACpB,IAAM,IAAoB,EAAW,MAAM,GAAG,EAAS;AAEvD,QACE,kBAAC,GAAD;EAAiB;YAAjB,CACE,kBAAC,GAAD;GAAY,WAAU;aAAtB,CACE,kBAAC,GAAD;IAAW,WAAU;cAAU;IAA2B,CAAA,EAC1D,kBAAC,GAAD;IAAM,IAAI;IAAa,WAAU;cAAuC;IAEjE,CAAA,CACI;MACb,kBAAC,GAAD,EAAA,UACG,IACC,kBAAC,OAAD;GAAK,WAAU;aACZ,MAAM,KAAK,EAAE,QAAQ,GAAG,CAAC,CAAC,KAAK,GAAG,MACjC,kBAAC,GAAD,EAAgC,EAAL,EAAK,CAChC;GACE,CAAA,GACJ,EAAkB,WAAW,IAC/B,kBAAC,KAAD;GAAG,WAAU;aAA+C;GAAsB,CAAA,GAElF,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAkB,KAAK,MACtB,kBAAC,GAAD,EAA0C,aAAY,EAAnC,EAAS,GAA0B,CACtD;GACE,CAAA,EAEI,CAAA,CACT;;;AAQX,SAAS,EAAa,EAAE,eAA+B;CACrD,IAAM,IAAO,EAAgB,EAAS,KAAK;AAS3C,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAW,EAAG,oBAVA;IACrB,MAAM;IACN,SAAS;IACT,OAAO;IACP,SAAS;IACV,CACgB,EAAS,YAAY,QAI8B;aAAG;GAAW,CAAA,EAC9E,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,KAAD;IAAG,WAAU;cAAyC,EAAS;IAAgB,CAAA,EAC/E,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,QAAD,EAAA,UAAO,EAAS,OAAa,CAAA;KAC7B,kBAAC,QAAD,EAAA,UAAM,KAAQ,CAAA;KACd,kBAAC,QAAD,EAAA,UAAO,EAAmB,EAAS,UAAU,EAAQ,CAAA;KACjD;MACF;KACF;;;AAIV,SAAS,IAAuB;AAC9B,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD,EAAU,WAAU,wBAAyB,CAAA,EAC7C,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAU,WAAU,aAAc,CAAA,EAClC,kBAAC,GAAD,EAAU,WAAU,aAAc,CAAA,CAC9B;KACF;;;AAIV,SAAS,EAAgB,GAAc;AAiBrC,QAhBiD;EAC/C,kBAAkB,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;EAC9C,oBAAoB,kBAAC,GAAD,EAAK,WAAU,WAAY,CAAA;EAC/C,oBAAoB,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA;EACvD,kBAAkB,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;EACvD,qBAAqB,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA;EACvD,gBAAgB,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;EAChD,gBAAgB,kBAAC,GAAD,EAAK,WAAU,WAAY,CAAA;EAC3C,sBAAsB,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;EACpD,wBAAwB,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA;EAC3D,qBAAqB,kBAAC,GAAD,EAAS,WAAU,WAAY,CAAA;EACpD,mBAAmB,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;EAC/C,oBAAoB,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;EACpD,kBAAkB,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;EACjD,CAEc,MAAS,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA"}
|
|
@@ -13,17 +13,17 @@ function s({ links: s, className: c }) {
|
|
|
13
13
|
className: "grid grid-cols-1 gap-3 sm:grid-cols-2",
|
|
14
14
|
children: s.map((t) => /* @__PURE__ */ o("a", {
|
|
15
15
|
href: t.to,
|
|
16
|
-
className: e("flex items-center gap-3 rounded-lg border border-border p-3", "transition-colors hover:bg-accent"),
|
|
16
|
+
className: e("flex items-center gap-3 rounded-lg border border-border-subtle p-3", "transition-colors hover:bg-accent"),
|
|
17
17
|
children: [/* @__PURE__ */ a("div", {
|
|
18
|
-
className: "rounded-md bg-primary/10 p-2 text-primary",
|
|
18
|
+
className: "rounded-md bg-action-primary-bg/10 p-2 text-primary",
|
|
19
19
|
children: t.icon
|
|
20
20
|
}), /* @__PURE__ */ o("div", {
|
|
21
21
|
className: "min-w-0 flex-1",
|
|
22
22
|
children: [/* @__PURE__ */ a("p", {
|
|
23
|
-
className: "font-medium text-
|
|
23
|
+
className: "font-medium text-text-primary",
|
|
24
24
|
children: t.label
|
|
25
25
|
}), t.description && /* @__PURE__ */ a("p", {
|
|
26
|
-
className: "truncate text-xs text-
|
|
26
|
+
className: "truncate text-xs text-text-secondary",
|
|
27
27
|
children: t.description
|
|
28
28
|
})]
|
|
29
29
|
})]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Charts.js","names":[],"sources":["../../../src/admin/components/Charts.tsx"],"sourcesContent":["import {\n LineChart,\n Line,\n AreaChart,\n Area,\n PieChart,\n Pie,\n Cell,\n XAxis,\n YAxis,\n CartesianGrid,\n Tooltip,\n Legend,\n ResponsiveContainer,\n} from 'recharts';\nimport { Card, CardHeader, CardTitle, CardContent, Skeleton } from './ui';\nimport { cn } from '../utils/cn';\nimport { formatCurrency, formatNumber } from '../utils/formatters';\nimport type { RevenueTrendPoint, TenantGrowthPoint, ProductUsagePoint } from '../types';\n\n// Chart colors using CSS variables for theme support\nconst CHART_COLORS = [\n 'hsl(var(--chart-1, 221.2 83.2% 53.3%))',\n 'hsl(var(--chart-2, 142.1 76.2% 36.3%))',\n 'hsl(var(--chart-3, 47.9 95.8% 53.1%))',\n 'hsl(var(--chart-4, 0 84.2% 60.2%))',\n 'hsl(var(--chart-5, 262.1 83.3% 57.8%))',\n];\n\n// ============================================================================\n// Revenue Trend Chart (Line Chart)\n// ============================================================================\n\ninterface RevenueTrendChartProps {\n data: RevenueTrendPoint[];\n loading?: boolean;\n className?: string;\n}\n\nexport function RevenueTrendChart({ data, loading, className }: RevenueTrendChartProps) {\n return (\n <Card className={className}>\n <CardHeader>\n <CardTitle className=\"text-lg\">Revenue Trend</CardTitle>\n </CardHeader>\n <CardContent>\n {loading ? (\n <Skeleton className=\"h-[300px] w-full\" />\n ) : (\n <ResponsiveContainer width=\"100%\" height={300}>\n <LineChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>\n <CartesianGrid strokeDasharray=\"3 3\" className=\"stroke-border\" />\n <XAxis\n dataKey=\"date\"\n className=\"text-xs fill-muted-foreground\"\n tick={{ fill: 'currentColor' }}\n />\n <YAxis\n className=\"text-xs fill-muted-foreground\"\n tick={{ fill: 'currentColor' }}\n tickFormatter={(value) => formatCurrency(value)}\n />\n <Tooltip\n contentStyle={{\n backgroundColor: 'hsl(var(--card))',\n border: '1px solid hsl(var(--border))',\n borderRadius: '6px',\n }}\n labelStyle={{ color: 'hsl(var(--foreground))' }}\n formatter={(value: number) => [formatCurrency(value), '']}\n />\n <Legend />\n <Line\n type=\"monotone\"\n dataKey=\"mrr\"\n name=\"MRR\"\n stroke={CHART_COLORS[0]}\n strokeWidth={2}\n dot={false}\n activeDot={{ r: 6 }}\n />\n <Line\n type=\"monotone\"\n dataKey=\"arr\"\n name=\"ARR\"\n stroke={CHART_COLORS[1]}\n strokeWidth={2}\n dot={false}\n activeDot={{ r: 6 }}\n />\n </LineChart>\n </ResponsiveContainer>\n )}\n </CardContent>\n </Card>\n );\n}\n\n// ============================================================================\n// Tenant Growth Chart (Area Chart)\n// ============================================================================\n\ninterface TenantGrowthChartProps {\n data: TenantGrowthPoint[];\n loading?: boolean;\n className?: string;\n}\n\nexport function TenantGrowthChart({ data, loading, className }: TenantGrowthChartProps) {\n return (\n <Card className={className}>\n <CardHeader>\n <CardTitle className=\"text-lg\">Tenant Growth</CardTitle>\n </CardHeader>\n <CardContent>\n {loading ? (\n <Skeleton className=\"h-[250px] w-full\" />\n ) : (\n <ResponsiveContainer width=\"100%\" height={250}>\n <AreaChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>\n <CartesianGrid strokeDasharray=\"3 3\" className=\"stroke-border\" />\n <XAxis\n dataKey=\"date\"\n className=\"text-xs fill-muted-foreground\"\n tick={{ fill: 'currentColor' }}\n />\n <YAxis className=\"text-xs fill-muted-foreground\" tick={{ fill: 'currentColor' }} />\n <Tooltip\n contentStyle={{\n backgroundColor: 'hsl(var(--card))',\n border: '1px solid hsl(var(--border))',\n borderRadius: '6px',\n }}\n labelStyle={{ color: 'hsl(var(--foreground))' }}\n />\n <Legend />\n <Area\n type=\"monotone\"\n dataKey=\"new\"\n name=\"New\"\n stackId=\"1\"\n stroke={CHART_COLORS[0]}\n fill={CHART_COLORS[0]}\n fillOpacity={0.6}\n />\n <Area\n type=\"monotone\"\n dataKey=\"active\"\n name=\"Active\"\n stackId=\"2\"\n stroke={CHART_COLORS[1]}\n fill={CHART_COLORS[1]}\n fillOpacity={0.6}\n />\n <Area\n type=\"monotone\"\n dataKey=\"churned\"\n name=\"Churned\"\n stackId=\"3\"\n stroke={CHART_COLORS[3]}\n fill={CHART_COLORS[3]}\n fillOpacity={0.6}\n />\n </AreaChart>\n </ResponsiveContainer>\n )}\n </CardContent>\n </Card>\n );\n}\n\n// ============================================================================\n// Product Usage Chart (Pie Chart)\n// ============================================================================\n\ninterface ProductUsageChartProps {\n data: ProductUsagePoint[];\n loading?: boolean;\n className?: string;\n}\n\nexport function ProductUsageChart({ data, loading, className }: ProductUsageChartProps) {\n return (\n <Card className={className}>\n <CardHeader>\n <CardTitle className=\"text-lg\">Product Usage</CardTitle>\n </CardHeader>\n <CardContent>\n {loading ? (\n <Skeleton className=\"h-[250px] w-full\" />\n ) : (\n <ResponsiveContainer width=\"100%\" height={250}>\n <PieChart>\n <Pie\n data={data}\n dataKey=\"tenants\"\n nameKey=\"product\"\n cx=\"50%\"\n cy=\"50%\"\n outerRadius={80}\n label={({ name, percent }: { name: string; percent: number }) =>\n `${name} (${(percent * 100).toFixed(0)}%)`\n }\n labelLine={false}\n >\n {data.map((entry, index) => (\n <Cell\n key={`cell-${index}`}\n fill={entry.color || CHART_COLORS[index % CHART_COLORS.length]}\n />\n ))}\n </Pie>\n <Tooltip\n contentStyle={{\n backgroundColor: 'hsl(var(--card))',\n border: '1px solid hsl(var(--border))',\n borderRadius: '6px',\n }}\n formatter={(value: number) => [formatNumber(value) + ' tenants', '']}\n />\n <Legend />\n </PieChart>\n </ResponsiveContainer>\n )}\n </CardContent>\n </Card>\n );\n}\n\n// ============================================================================\n// Quick Links Card\n// ============================================================================\n\ninterface QuickLink {\n to: string;\n icon: React.ReactNode;\n label: string;\n description?: string;\n}\n\ninterface QuickLinksProps {\n links: QuickLink[];\n className?: string;\n}\n\nexport function QuickLinks({ links, className }: QuickLinksProps) {\n return (\n <Card className={className}>\n <CardHeader>\n <CardTitle className=\"text-lg\">Quick Actions</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {links.map((link) => (\n <a\n key={link.to}\n href={link.to}\n className={cn(\n 'flex items-center gap-3 rounded-lg border border-border p-3',\n 'transition-colors hover:bg-accent'\n )}\n >\n <div className=\"rounded-md bg-primary/10 p-2 text-primary\">{link.icon}</div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"font-medium text-
|
|
1
|
+
{"version":3,"file":"Charts.js","names":[],"sources":["../../../src/admin/components/Charts.tsx"],"sourcesContent":["import {\n LineChart,\n Line,\n AreaChart,\n Area,\n PieChart,\n Pie,\n Cell,\n XAxis,\n YAxis,\n CartesianGrid,\n Tooltip,\n Legend,\n ResponsiveContainer,\n} from 'recharts';\nimport { Card, CardHeader, CardTitle, CardContent, Skeleton } from './ui';\nimport { cn } from '../utils/cn';\nimport { formatCurrency, formatNumber } from '../utils/formatters';\nimport type { RevenueTrendPoint, TenantGrowthPoint, ProductUsagePoint } from '../types';\n\n// Chart colors using CSS variables for theme support\nconst CHART_COLORS = [\n 'hsl(var(--chart-1, 221.2 83.2% 53.3%))',\n 'hsl(var(--chart-2, 142.1 76.2% 36.3%))',\n 'hsl(var(--chart-3, 47.9 95.8% 53.1%))',\n 'hsl(var(--chart-4, 0 84.2% 60.2%))',\n 'hsl(var(--chart-5, 262.1 83.3% 57.8%))',\n];\n\n// ============================================================================\n// Revenue Trend Chart (Line Chart)\n// ============================================================================\n\ninterface RevenueTrendChartProps {\n data: RevenueTrendPoint[];\n loading?: boolean;\n className?: string;\n}\n\nexport function RevenueTrendChart({ data, loading, className }: RevenueTrendChartProps) {\n return (\n <Card className={className}>\n <CardHeader>\n <CardTitle className=\"text-lg\">Revenue Trend</CardTitle>\n </CardHeader>\n <CardContent>\n {loading ? (\n <Skeleton className=\"h-[300px] w-full\" />\n ) : (\n <ResponsiveContainer width=\"100%\" height={300}>\n <LineChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>\n <CartesianGrid strokeDasharray=\"3 3\" className=\"stroke-border\" />\n <XAxis\n dataKey=\"date\"\n className=\"text-xs fill-muted-foreground\"\n tick={{ fill: 'currentColor' }}\n />\n <YAxis\n className=\"text-xs fill-muted-foreground\"\n tick={{ fill: 'currentColor' }}\n tickFormatter={(value) => formatCurrency(value)}\n />\n <Tooltip\n contentStyle={{\n backgroundColor: 'hsl(var(--card))',\n border: '1px solid hsl(var(--border))',\n borderRadius: '6px',\n }}\n labelStyle={{ color: 'hsl(var(--foreground))' }}\n formatter={(value: number) => [formatCurrency(value), '']}\n />\n <Legend />\n <Line\n type=\"monotone\"\n dataKey=\"mrr\"\n name=\"MRR\"\n stroke={CHART_COLORS[0]}\n strokeWidth={2}\n dot={false}\n activeDot={{ r: 6 }}\n />\n <Line\n type=\"monotone\"\n dataKey=\"arr\"\n name=\"ARR\"\n stroke={CHART_COLORS[1]}\n strokeWidth={2}\n dot={false}\n activeDot={{ r: 6 }}\n />\n </LineChart>\n </ResponsiveContainer>\n )}\n </CardContent>\n </Card>\n );\n}\n\n// ============================================================================\n// Tenant Growth Chart (Area Chart)\n// ============================================================================\n\ninterface TenantGrowthChartProps {\n data: TenantGrowthPoint[];\n loading?: boolean;\n className?: string;\n}\n\nexport function TenantGrowthChart({ data, loading, className }: TenantGrowthChartProps) {\n return (\n <Card className={className}>\n <CardHeader>\n <CardTitle className=\"text-lg\">Tenant Growth</CardTitle>\n </CardHeader>\n <CardContent>\n {loading ? (\n <Skeleton className=\"h-[250px] w-full\" />\n ) : (\n <ResponsiveContainer width=\"100%\" height={250}>\n <AreaChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>\n <CartesianGrid strokeDasharray=\"3 3\" className=\"stroke-border\" />\n <XAxis\n dataKey=\"date\"\n className=\"text-xs fill-muted-foreground\"\n tick={{ fill: 'currentColor' }}\n />\n <YAxis className=\"text-xs fill-muted-foreground\" tick={{ fill: 'currentColor' }} />\n <Tooltip\n contentStyle={{\n backgroundColor: 'hsl(var(--card))',\n border: '1px solid hsl(var(--border))',\n borderRadius: '6px',\n }}\n labelStyle={{ color: 'hsl(var(--foreground))' }}\n />\n <Legend />\n <Area\n type=\"monotone\"\n dataKey=\"new\"\n name=\"New\"\n stackId=\"1\"\n stroke={CHART_COLORS[0]}\n fill={CHART_COLORS[0]}\n fillOpacity={0.6}\n />\n <Area\n type=\"monotone\"\n dataKey=\"active\"\n name=\"Active\"\n stackId=\"2\"\n stroke={CHART_COLORS[1]}\n fill={CHART_COLORS[1]}\n fillOpacity={0.6}\n />\n <Area\n type=\"monotone\"\n dataKey=\"churned\"\n name=\"Churned\"\n stackId=\"3\"\n stroke={CHART_COLORS[3]}\n fill={CHART_COLORS[3]}\n fillOpacity={0.6}\n />\n </AreaChart>\n </ResponsiveContainer>\n )}\n </CardContent>\n </Card>\n );\n}\n\n// ============================================================================\n// Product Usage Chart (Pie Chart)\n// ============================================================================\n\ninterface ProductUsageChartProps {\n data: ProductUsagePoint[];\n loading?: boolean;\n className?: string;\n}\n\nexport function ProductUsageChart({ data, loading, className }: ProductUsageChartProps) {\n return (\n <Card className={className}>\n <CardHeader>\n <CardTitle className=\"text-lg\">Product Usage</CardTitle>\n </CardHeader>\n <CardContent>\n {loading ? (\n <Skeleton className=\"h-[250px] w-full\" />\n ) : (\n <ResponsiveContainer width=\"100%\" height={250}>\n <PieChart>\n <Pie\n data={data}\n dataKey=\"tenants\"\n nameKey=\"product\"\n cx=\"50%\"\n cy=\"50%\"\n outerRadius={80}\n label={({ name, percent }: { name: string; percent: number }) =>\n `${name} (${(percent * 100).toFixed(0)}%)`\n }\n labelLine={false}\n >\n {data.map((entry, index) => (\n <Cell\n key={`cell-${index}`}\n fill={entry.color || CHART_COLORS[index % CHART_COLORS.length]}\n />\n ))}\n </Pie>\n <Tooltip\n contentStyle={{\n backgroundColor: 'hsl(var(--card))',\n border: '1px solid hsl(var(--border))',\n borderRadius: '6px',\n }}\n formatter={(value: number) => [formatNumber(value) + ' tenants', '']}\n />\n <Legend />\n </PieChart>\n </ResponsiveContainer>\n )}\n </CardContent>\n </Card>\n );\n}\n\n// ============================================================================\n// Quick Links Card\n// ============================================================================\n\ninterface QuickLink {\n to: string;\n icon: React.ReactNode;\n label: string;\n description?: string;\n}\n\ninterface QuickLinksProps {\n links: QuickLink[];\n className?: string;\n}\n\nexport function QuickLinks({ links, className }: QuickLinksProps) {\n return (\n <Card className={className}>\n <CardHeader>\n <CardTitle className=\"text-lg\">Quick Actions</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {links.map((link) => (\n <a\n key={link.to}\n href={link.to}\n className={cn(\n 'flex items-center gap-3 rounded-lg border border-border-subtle p-3',\n 'transition-colors hover:bg-accent'\n )}\n >\n <div className=\"rounded-md bg-action-primary-bg/10 p-2 text-primary\">{link.icon}</div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"font-medium text-text-primary\">{link.label}</p>\n {link.description && (\n <p className=\"truncate text-xs text-text-secondary\">{link.description}</p>\n )}\n </div>\n </a>\n ))}\n </div>\n </CardContent>\n </Card>\n );\n}\n"],"mappings":";;;;;AAqPA,SAAgB,EAAW,EAAE,UAAO,gBAA8B;AAChE,QACE,kBAAC,GAAD;EAAiB;YAAjB,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;GAAW,WAAU;aAAU;GAAyB,CAAA,EAC7C,CAAA,EACb,kBAAC,GAAD,EAAA,UACE,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAM,KAAK,MACV,kBAAC,KAAD;IAEE,MAAM,EAAK;IACX,WAAW,EACT,sEACA,oCACD;cANH,CAQE,kBAAC,OAAD;KAAK,WAAU;eAAuD,EAAK;KAAW,CAAA,EACtF,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAiC,EAAK;MAAU,CAAA,EAC5D,EAAK,eACJ,kBAAC,KAAD;MAAG,WAAU;gBAAwC,EAAK;MAAgB,CAAA,CAExE;OACJ;MAdG,EAAK,GAcR,CACJ;GACE,CAAA,EACM,CAAA,CACT"}
|
|
@@ -27,7 +27,7 @@ function l({ open: o, onClose: l, onConfirm: u, title: d, description: f, confir
|
|
|
27
27
|
}), [o]), !o) return null;
|
|
28
28
|
let y = async () => {
|
|
29
29
|
await u();
|
|
30
|
-
}, b = h === "destructive", x = b ? /* @__PURE__ */ s(i, { className: "h-6 w-6 text-
|
|
30
|
+
}, b = h === "destructive", x = b ? /* @__PURE__ */ s(i, { className: "h-6 w-6 text-status-error-text" }) : null;
|
|
31
31
|
return /* @__PURE__ */ c("div", {
|
|
32
32
|
className: "fixed inset-0 z-50 flex items-center justify-center",
|
|
33
33
|
children: [/* @__PURE__ */ s("div", {
|
|
@@ -35,7 +35,7 @@ function l({ open: o, onClose: l, onConfirm: u, title: d, description: f, confir
|
|
|
35
35
|
onClick: g ? void 0 : l,
|
|
36
36
|
"aria-hidden": "true"
|
|
37
37
|
}), /* @__PURE__ */ c("div", {
|
|
38
|
-
className: e("relative z-10 w-full max-w-md rounded-lg bg-
|
|
38
|
+
className: e("relative z-10 w-full max-w-md rounded-lg bg-bg-surface p-6 shadow-xl", "animate-in zoom-in-95 fade-in duration-200"),
|
|
39
39
|
role: "alertdialog",
|
|
40
40
|
"aria-modal": "true",
|
|
41
41
|
"aria-labelledby": "confirm-dialog-title",
|
|
@@ -43,17 +43,17 @@ function l({ open: o, onClose: l, onConfirm: u, title: d, description: f, confir
|
|
|
43
43
|
children: [/* @__PURE__ */ c("div", {
|
|
44
44
|
className: "flex gap-4",
|
|
45
45
|
children: [(_ || x) && /* @__PURE__ */ s("div", {
|
|
46
|
-
className: e("flex h-12 w-12 shrink-0 items-center justify-center rounded-full", b ? "bg-
|
|
46
|
+
className: e("flex h-12 w-12 shrink-0 items-center justify-center rounded-full", b ? "bg-status-error-bg/10" : "bg-action-primary-bg/10"),
|
|
47
47
|
children: _ || x
|
|
48
48
|
}), /* @__PURE__ */ c("div", {
|
|
49
49
|
className: "flex-1",
|
|
50
50
|
children: [/* @__PURE__ */ s("h3", {
|
|
51
51
|
id: "confirm-dialog-title",
|
|
52
|
-
className: "text-lg font-semibold text-
|
|
52
|
+
className: "text-lg font-semibold text-text-primary",
|
|
53
53
|
children: d
|
|
54
54
|
}), /* @__PURE__ */ s("div", {
|
|
55
55
|
id: "confirm-dialog-description",
|
|
56
|
-
className: "mt-2 text-sm text-
|
|
56
|
+
className: "mt-2 text-sm text-text-secondary",
|
|
57
57
|
children: f
|
|
58
58
|
})]
|
|
59
59
|
})]
|
|
@@ -84,7 +84,7 @@ function u({ open: e, onClose: t, onConfirm: n, itemName: r, itemType: i, loadin
|
|
|
84
84
|
description: /* @__PURE__ */ c(o, { children: [
|
|
85
85
|
"Are you sure you want to delete ",
|
|
86
86
|
/* @__PURE__ */ s("strong", {
|
|
87
|
-
className: "text-
|
|
87
|
+
className: "text-text-primary",
|
|
88
88
|
children: r
|
|
89
89
|
}),
|
|
90
90
|
"? This action cannot be undone."
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConfirmDialog.js","names":[],"sources":["../../../src/admin/components/ConfirmDialog.tsx"],"sourcesContent":["import { useEffect, useRef, type ReactNode } from 'react';\nimport { AlertTriangle, Loader2 } from 'lucide-react';\nimport { Button } from './ui';\nimport { cn } from '../utils/cn';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface ConfirmDialogProps {\n /** Whether the dialog is open */\n open: boolean;\n /** Callback when the dialog should close */\n onClose: () => void;\n /** Callback when confirmed */\n onConfirm: () => void | Promise<void>;\n /** Dialog title */\n title: string;\n /** Dialog description */\n description: ReactNode;\n /** Confirm button text */\n confirmLabel?: string;\n /** Cancel button text */\n cancelLabel?: string;\n /** Variant for styling */\n variant?: 'default' | 'destructive';\n /** Loading state */\n loading?: boolean;\n /** Icon to display */\n icon?: ReactNode;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\n/**\n * ConfirmDialog - Modal dialog for confirming actions\n *\n * Use for destructive actions like deleting, or important state changes.\n */\nexport function ConfirmDialog({\n open,\n onClose,\n onConfirm,\n title,\n description,\n confirmLabel = 'Confirm',\n cancelLabel = 'Cancel',\n variant = 'default',\n loading = false,\n icon,\n}: ConfirmDialogProps) {\n const confirmRef = useRef<HTMLButtonElement>(null);\n\n // Handle escape key\n useEffect(() => {\n const handleEscape = (e: KeyboardEvent) => {\n if (e.key === 'Escape' && open && !loading) {\n onClose();\n }\n };\n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [open, onClose, loading]);\n\n // Focus confirm button when opened\n useEffect(() => {\n if (open) {\n // Small delay to ensure dialog is rendered\n const timer = setTimeout(() => {\n confirmRef.current?.focus();\n }, 50);\n return () => clearTimeout(timer);\n }\n }, [open]);\n\n // Prevent body scroll when open\n useEffect(() => {\n if (open) {\n document.body.style.overflow = 'hidden';\n } else {\n document.body.style.overflow = '';\n }\n return () => {\n document.body.style.overflow = '';\n };\n }, [open]);\n\n if (!open) return null;\n\n const handleConfirm = async () => {\n await onConfirm();\n };\n\n const isDestructive = variant === 'destructive';\n const defaultIcon = isDestructive ? <AlertTriangle className=\"h-6 w-6 text-
|
|
1
|
+
{"version":3,"file":"ConfirmDialog.js","names":[],"sources":["../../../src/admin/components/ConfirmDialog.tsx"],"sourcesContent":["import { useEffect, useRef, type ReactNode } from 'react';\nimport { AlertTriangle, Loader2 } from 'lucide-react';\nimport { Button } from './ui';\nimport { cn } from '../utils/cn';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface ConfirmDialogProps {\n /** Whether the dialog is open */\n open: boolean;\n /** Callback when the dialog should close */\n onClose: () => void;\n /** Callback when confirmed */\n onConfirm: () => void | Promise<void>;\n /** Dialog title */\n title: string;\n /** Dialog description */\n description: ReactNode;\n /** Confirm button text */\n confirmLabel?: string;\n /** Cancel button text */\n cancelLabel?: string;\n /** Variant for styling */\n variant?: 'default' | 'destructive';\n /** Loading state */\n loading?: boolean;\n /** Icon to display */\n icon?: ReactNode;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\n/**\n * ConfirmDialog - Modal dialog for confirming actions\n *\n * Use for destructive actions like deleting, or important state changes.\n */\nexport function ConfirmDialog({\n open,\n onClose,\n onConfirm,\n title,\n description,\n confirmLabel = 'Confirm',\n cancelLabel = 'Cancel',\n variant = 'default',\n loading = false,\n icon,\n}: ConfirmDialogProps) {\n const confirmRef = useRef<HTMLButtonElement>(null);\n\n // Handle escape key\n useEffect(() => {\n const handleEscape = (e: KeyboardEvent) => {\n if (e.key === 'Escape' && open && !loading) {\n onClose();\n }\n };\n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [open, onClose, loading]);\n\n // Focus confirm button when opened\n useEffect(() => {\n if (open) {\n // Small delay to ensure dialog is rendered\n const timer = setTimeout(() => {\n confirmRef.current?.focus();\n }, 50);\n return () => clearTimeout(timer);\n }\n }, [open]);\n\n // Prevent body scroll when open\n useEffect(() => {\n if (open) {\n document.body.style.overflow = 'hidden';\n } else {\n document.body.style.overflow = '';\n }\n return () => {\n document.body.style.overflow = '';\n };\n }, [open]);\n\n if (!open) return null;\n\n const handleConfirm = async () => {\n await onConfirm();\n };\n\n const isDestructive = variant === 'destructive';\n const defaultIcon = isDestructive ? <AlertTriangle className=\"h-6 w-6 text-status-error-text\" /> : null;\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center\">\n {/* Backdrop */}\n <div\n className=\"fixed inset-0 bg-black/50 transition-opacity\"\n onClick={loading ? undefined : onClose}\n aria-hidden=\"true\"\n />\n\n {/* Dialog */}\n <div\n className={cn(\n 'relative z-10 w-full max-w-md rounded-lg bg-bg-surface p-6 shadow-xl',\n 'animate-in zoom-in-95 fade-in duration-200'\n )}\n role=\"alertdialog\"\n aria-modal=\"true\"\n aria-labelledby=\"confirm-dialog-title\"\n aria-describedby=\"confirm-dialog-description\"\n >\n {/* Icon & Content */}\n <div className=\"flex gap-4\">\n {(icon || defaultIcon) && (\n <div\n className={cn(\n 'flex h-12 w-12 shrink-0 items-center justify-center rounded-full',\n isDestructive ? 'bg-status-error-bg/10' : 'bg-action-primary-bg/10'\n )}\n >\n {icon || defaultIcon}\n </div>\n )}\n <div className=\"flex-1\">\n <h3 id=\"confirm-dialog-title\" className=\"text-lg font-semibold text-text-primary\">\n {title}\n </h3>\n <div id=\"confirm-dialog-description\" className=\"mt-2 text-sm text-text-secondary\">\n {description}\n </div>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"mt-6 flex justify-end gap-3\">\n <Button variant=\"outline\" onClick={onClose} disabled={loading}>\n {cancelLabel}\n </Button>\n <Button\n ref={confirmRef}\n variant={isDestructive ? 'destructive' : 'default'}\n onClick={handleConfirm}\n disabled={loading}\n >\n {loading && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\n {confirmLabel}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n\n// ============================================================================\n// Delete Confirmation Helper\n// ============================================================================\n\nexport interface DeleteConfirmDialogProps {\n open: boolean;\n onClose: () => void;\n onConfirm: () => void | Promise<void>;\n /** Name of item being deleted */\n itemName: string;\n /** Type of item (e.g., \"tenant\", \"user\") */\n itemType: string;\n loading?: boolean;\n}\n\n/**\n * DeleteConfirmDialog - Pre-configured dialog for delete confirmations\n */\nexport function DeleteConfirmDialog({\n open,\n onClose,\n onConfirm,\n itemName,\n itemType,\n loading,\n}: DeleteConfirmDialogProps) {\n return (\n <ConfirmDialog\n open={open}\n onClose={onClose}\n onConfirm={onConfirm}\n title={`Delete ${itemType}?`}\n description={\n <>\n Are you sure you want to delete <strong className=\"text-text-primary\">{itemName}</strong>?\n This action cannot be undone.\n </>\n }\n confirmLabel=\"Delete\"\n variant=\"destructive\"\n loading={loading}\n />\n );\n}\n"],"mappings":";;;;;;AAyCA,SAAgB,EAAc,EAC5B,SACA,YACA,cACA,UACA,gBACA,kBAAe,WACf,iBAAc,UACd,aAAU,WACV,aAAU,IACV,WACqB;CACrB,IAAM,IAAa,EAA0B,KAAK;AAoClD,KAjCA,QAAgB;EACd,IAAM,KAAgB,MAAqB;AACzC,GAAI,EAAE,QAAQ,YAAY,KAAQ,CAAC,KACjC,GAAS;;AAIb,SADA,SAAS,iBAAiB,WAAW,EAAa,QACrC,SAAS,oBAAoB,WAAW,EAAa;IACjE;EAAC;EAAM;EAAS;EAAQ,CAAC,EAG5B,QAAgB;AACd,MAAI,GAAM;GAER,IAAM,IAAQ,iBAAiB;AAC7B,MAAW,SAAS,OAAO;MAC1B,GAAG;AACN,gBAAa,aAAa,EAAM;;IAEjC,CAAC,EAAK,CAAC,EAGV,SACM,IACF,SAAS,KAAK,MAAM,WAAW,WAE/B,SAAS,KAAK,MAAM,WAAW,UAEpB;AACX,WAAS,KAAK,MAAM,WAAW;KAEhC,CAAC,EAAK,CAAC,EAEN,CAAC,EAAM,QAAO;CAElB,IAAM,IAAgB,YAAY;AAChC,QAAM,GAAW;IAGb,IAAgB,MAAY,eAC5B,IAAc,IAAgB,kBAAC,GAAD,EAAe,WAAU,kCAAmC,CAAA,GAAG;AAEnG,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CAEE,kBAAC,OAAD;GACE,WAAU;GACV,SAAS,IAAU,KAAA,IAAY;GAC/B,eAAY;GACZ,CAAA,EAGF,kBAAC,OAAD;GACE,WAAW,EACT,wEACA,6CACD;GACD,MAAK;GACL,cAAW;GACX,mBAAgB;GAChB,oBAAiB;aARnB,CAWE,kBAAC,OAAD;IAAK,WAAU;cAAf,EACI,KAAQ,MACR,kBAAC,OAAD;KACE,WAAW,EACT,oEACA,IAAgB,0BAA0B,0BAC3C;eAEA,KAAQ;KACL,CAAA,EAER,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,MAAD;MAAI,IAAG;MAAuB,WAAU;gBACrC;MACE,CAAA,EACL,kBAAC,OAAD;MAAK,IAAG;MAA6B,WAAU;gBAC5C;MACG,CAAA,CACF;OACF;OAGN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAQ,SAAQ;KAAU,SAAS;KAAS,UAAU;eACnD;KACM,CAAA,EACT,kBAAC,GAAD;KACE,KAAK;KACL,SAAS,IAAgB,gBAAgB;KACzC,SAAS;KACT,UAAU;eAJZ,CAMG,KAAW,kBAAC,GAAD,EAAS,WAAU,6BAA8B,CAAA,EAC5D,EACM;OACL;MACF;KACF;;;AAsBV,SAAgB,EAAoB,EAClC,SACA,YACA,cACA,aACA,aACA,cAC2B;AAC3B,QACE,kBAAC,GAAD;EACQ;EACG;EACE;EACX,OAAO,UAAU,EAAS;EAC1B,aACE,kBAAA,GAAA,EAAA,UAAA;GAAE;GACgC,kBAAC,UAAD;IAAQ,WAAU;cAAqB;IAAkB,CAAA;;GAExF,EAAA,CAAA;EAEL,cAAa;EACb,SAAQ;EACC;EACT,CAAA"}
|
|
@@ -14,15 +14,15 @@ function l({ columns: t, data: r, keyExtractor: i, loading: a, skeletonRows: o =
|
|
|
14
14
|
right: "text-right"
|
|
15
15
|
};
|
|
16
16
|
return /* @__PURE__ */ s("div", {
|
|
17
|
-
className: e("overflow-hidden rounded-
|
|
17
|
+
className: e("overflow-hidden rounded-card border border-border-seam", m),
|
|
18
18
|
children: /* @__PURE__ */ s("div", {
|
|
19
19
|
className: "overflow-x-auto",
|
|
20
20
|
children: /* @__PURE__ */ c("table", {
|
|
21
21
|
className: "w-full border-collapse",
|
|
22
22
|
children: [/* @__PURE__ */ s("thead", { children: /* @__PURE__ */ s("tr", {
|
|
23
|
-
className: "border-b border-border bg-
|
|
23
|
+
className: "border-b border-border-seam bg-bg-sunken/40",
|
|
24
24
|
children: t.map((t) => /* @__PURE__ */ s("th", {
|
|
25
|
-
className: e("px-4 py-3 text-sm font-medium text-
|
|
25
|
+
className: e("px-4 py-3 text-sm font-medium text-text-secondary", g[t.align || "left"], t.sortable && "cursor-pointer select-none hover:text-text-primary", t.width),
|
|
26
26
|
style: t.width ? { width: t.width } : void 0,
|
|
27
27
|
onClick: () => h(t),
|
|
28
28
|
children: /* @__PURE__ */ c("div", {
|
|
@@ -34,20 +34,20 @@ function l({ columns: t, data: r, keyExtractor: i, loading: a, skeletonRows: o =
|
|
|
34
34
|
})
|
|
35
35
|
}, t.key))
|
|
36
36
|
}) }), /* @__PURE__ */ s("tbody", { children: a ? Array.from({ length: o }).map((e, r) => /* @__PURE__ */ s("tr", {
|
|
37
|
-
className: "border-b border-border last:border-0",
|
|
37
|
+
className: "border-b border-border-seam last:border-0",
|
|
38
38
|
children: t.map((e) => /* @__PURE__ */ s("td", {
|
|
39
39
|
className: "px-4 py-3",
|
|
40
|
-
children: /* @__PURE__ */ s(n, { className: "h-5 w-full" })
|
|
40
|
+
children: /* @__PURE__ */ s(n, { className: "h-5 w-full motion-safe:animate-vc-shimmer" })
|
|
41
41
|
}, e.key))
|
|
42
42
|
}, r)) : r.length === 0 ? /* @__PURE__ */ s("tr", { children: /* @__PURE__ */ s("td", {
|
|
43
43
|
colSpan: t.length,
|
|
44
44
|
className: "px-4 py-8",
|
|
45
45
|
children: l || /* @__PURE__ */ s("div", {
|
|
46
|
-
className: "text-center text-sm text-
|
|
46
|
+
className: "text-center text-sm text-text-secondary",
|
|
47
47
|
children: "No data available"
|
|
48
48
|
})
|
|
49
49
|
}) }) : r.map((n, r) => /* @__PURE__ */ s("tr", {
|
|
50
|
-
className: e("border-b border-border last:border-0 transition-colors", u && "cursor-pointer hover:bg-
|
|
50
|
+
className: e("border-b border-border-seam last:border-0 transition-colors", u && "cursor-pointer hover:bg-bg-sunken/40"),
|
|
51
51
|
onClick: () => u?.(n),
|
|
52
52
|
children: t.map((t) => /* @__PURE__ */ s("td", {
|
|
53
53
|
className: e("px-4 py-3 text-sm", g[t.align || "left"]),
|
|
@@ -63,7 +63,7 @@ function u({ page: n, totalPages: l, totalItems: u, pageSize: d, onPageChange: f
|
|
|
63
63
|
return /* @__PURE__ */ c("div", {
|
|
64
64
|
className: e("flex items-center justify-between px-4 py-3", p),
|
|
65
65
|
children: [/* @__PURE__ */ c("div", {
|
|
66
|
-
className: "text-sm text-
|
|
66
|
+
className: "text-sm text-text-secondary",
|
|
67
67
|
children: [
|
|
68
68
|
"Showing ",
|
|
69
69
|
m,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DataTable.js","names":[],"sources":["../../../src/admin/components/DataTable.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';\nimport { Button, Skeleton } from './ui';\nimport { cn } from '../utils/cn';\n\ninterface Column<T> {\n /** Column key (used for accessing data) */\n key: string;\n /** Column header */\n header: string;\n /** Custom render function */\n render?: (item: T, index: number) => ReactNode;\n /** Column width */\n width?: string;\n /** Align content */\n align?: 'left' | 'center' | 'right';\n /** Sortable */\n sortable?: boolean;\n}\n\ninterface DataTableProps<T> {\n /** Column definitions */\n columns: Column<T>[];\n /** Data to display */\n data: T[];\n /** Key extractor for rows */\n keyExtractor: (item: T) => string;\n /** Loading state */\n loading?: boolean;\n /** Number of skeleton rows when loading */\n skeletonRows?: number;\n /** Empty state content */\n emptyState?: ReactNode;\n /** On row click handler */\n onRowClick?: (item: T) => void;\n /** Current sort column */\n sortBy?: string;\n /** Sort direction */\n sortOrder?: 'asc' | 'desc';\n /** On sort change */\n onSort?: (column: string, order: 'asc' | 'desc') => void;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Reusable data table component\n */\nexport function DataTable<T>({\n columns,\n data,\n keyExtractor,\n loading,\n skeletonRows = 5,\n emptyState,\n onRowClick,\n sortBy,\n sortOrder,\n onSort,\n className,\n}: DataTableProps<T>) {\n const handleSort = (column: Column<T>) => {\n if (!column.sortable || !onSort) return;\n const newOrder = sortBy === column.key && sortOrder === 'asc' ? 'desc' : 'asc';\n onSort(column.key, newOrder);\n };\n\n const alignClasses = {\n left: 'text-left',\n center: 'text-center',\n right: 'text-right',\n };\n\n return (\n <div className={cn('overflow-hidden rounded-
|
|
1
|
+
{"version":3,"file":"DataTable.js","names":[],"sources":["../../../src/admin/components/DataTable.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';\nimport { Button, Skeleton } from './ui';\nimport { cn } from '../utils/cn';\n\ninterface Column<T> {\n /** Column key (used for accessing data) */\n key: string;\n /** Column header */\n header: string;\n /** Custom render function */\n render?: (item: T, index: number) => ReactNode;\n /** Column width */\n width?: string;\n /** Align content */\n align?: 'left' | 'center' | 'right';\n /** Sortable */\n sortable?: boolean;\n}\n\ninterface DataTableProps<T> {\n /** Column definitions */\n columns: Column<T>[];\n /** Data to display */\n data: T[];\n /** Key extractor for rows */\n keyExtractor: (item: T) => string;\n /** Loading state */\n loading?: boolean;\n /** Number of skeleton rows when loading */\n skeletonRows?: number;\n /** Empty state content */\n emptyState?: ReactNode;\n /** On row click handler */\n onRowClick?: (item: T) => void;\n /** Current sort column */\n sortBy?: string;\n /** Sort direction */\n sortOrder?: 'asc' | 'desc';\n /** On sort change */\n onSort?: (column: string, order: 'asc' | 'desc') => void;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Reusable data table component\n */\nexport function DataTable<T>({\n columns,\n data,\n keyExtractor,\n loading,\n skeletonRows = 5,\n emptyState,\n onRowClick,\n sortBy,\n sortOrder,\n onSort,\n className,\n}: DataTableProps<T>) {\n const handleSort = (column: Column<T>) => {\n if (!column.sortable || !onSort) return;\n const newOrder = sortBy === column.key && sortOrder === 'asc' ? 'desc' : 'asc';\n onSort(column.key, newOrder);\n };\n\n const alignClasses = {\n left: 'text-left',\n center: 'text-center',\n right: 'text-right',\n };\n\n return (\n <div className={cn('overflow-hidden rounded-card border border-border-seam', className)}>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full border-collapse\">\n <thead>\n <tr className=\"border-b border-border-seam bg-bg-sunken/40\">\n {columns.map((column) => (\n <th\n key={column.key}\n className={cn(\n 'px-4 py-3 text-sm font-medium text-text-secondary',\n alignClasses[column.align || 'left'],\n column.sortable && 'cursor-pointer select-none hover:text-text-primary',\n column.width\n )}\n style={column.width ? { width: column.width } : undefined}\n onClick={() => handleSort(column)}\n >\n <div className=\"flex items-center gap-2\">\n {column.header}\n {column.sortable && sortBy === column.key && (\n <span className=\"text-xs\">{sortOrder === 'asc' ? '↑' : '↓'}</span>\n )}\n </div>\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {loading ? (\n Array.from({ length: skeletonRows }).map((_, rowIndex) => (\n <tr key={rowIndex} className=\"border-b border-border-seam last:border-0\">\n {columns.map((column) => (\n <td key={column.key} className=\"px-4 py-3\">\n <Skeleton className=\"h-5 w-full motion-safe:animate-vc-shimmer\" />\n </td>\n ))}\n </tr>\n ))\n ) : data.length === 0 ? (\n <tr>\n <td colSpan={columns.length} className=\"px-4 py-8\">\n {emptyState || (\n <div className=\"text-center text-sm text-text-secondary\">\n No data available\n </div>\n )}\n </td>\n </tr>\n ) : (\n data.map((item, index) => (\n <tr\n key={keyExtractor(item)}\n className={cn(\n 'border-b border-border-seam last:border-0 transition-colors',\n onRowClick && 'cursor-pointer hover:bg-bg-sunken/40'\n )}\n onClick={() => onRowClick?.(item)}\n >\n {columns.map((column) => (\n <td\n key={column.key}\n className={cn('px-4 py-3 text-sm', alignClasses[column.align || 'left'])}\n >\n {column.render\n ? column.render(item, index)\n : String((item as Record<string, unknown>)[column.key] ?? '')}\n </td>\n ))}\n </tr>\n ))\n )}\n </tbody>\n </table>\n </div>\n </div>\n );\n}\n\ninterface PaginationProps {\n /** Current page (1-indexed) */\n page: number;\n /** Total pages */\n totalPages: number;\n /** Total items */\n totalItems: number;\n /** Items per page */\n pageSize: number;\n /** On page change */\n onPageChange: (page: number) => void;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Pagination controls\n */\nexport function Pagination({\n page,\n totalPages,\n totalItems,\n pageSize,\n onPageChange,\n className,\n}: PaginationProps) {\n const startItem = (page - 1) * pageSize + 1;\n const endItem = Math.min(page * pageSize, totalItems);\n\n return (\n <div className={cn('flex items-center justify-between px-4 py-3', className)}>\n <div className=\"text-sm text-text-secondary\">\n Showing {startItem} to {endItem} of {totalItems} results\n </div>\n <div className=\"flex items-center gap-1\">\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={() => onPageChange(1)}\n disabled={page === 1}\n aria-label=\"First page\"\n >\n <ChevronsLeft className=\"h-4 w-4\" />\n </Button>\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={() => onPageChange(page - 1)}\n disabled={page === 1}\n aria-label=\"Previous page\"\n >\n <ChevronLeft className=\"h-4 w-4\" />\n </Button>\n <span className=\"px-3 text-sm\">\n Page {page} of {totalPages}\n </span>\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={() => onPageChange(page + 1)}\n disabled={page === totalPages}\n aria-label=\"Next page\"\n >\n <ChevronRight className=\"h-4 w-4\" />\n </Button>\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={() => onPageChange(totalPages)}\n disabled={page === totalPages}\n aria-label=\"Last page\"\n >\n <ChevronsRight className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;AAgDA,SAAgB,EAAa,EAC3B,YACA,SACA,iBACA,YACA,kBAAe,GACf,eACA,eACA,WACA,cACA,WACA,gBACoB;CACpB,IAAM,KAAc,MAAsB;AACxC,MAAI,CAAC,EAAO,YAAY,CAAC,EAAQ;EACjC,IAAM,IAAW,MAAW,EAAO,OAAO,MAAc,QAAQ,SAAS;AACzE,IAAO,EAAO,KAAK,EAAS;IAGxB,IAAe;EACnB,MAAM;EACN,QAAQ;EACR,OAAO;EACR;AAED,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,0DAA0D,EAAU;YACrF,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,SAAD;IAAO,WAAU;cAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAQ,KAAK,MACZ,kBAAC,MAAD;MAEE,WAAW,EACT,qDACA,EAAa,EAAO,SAAS,SAC7B,EAAO,YAAY,sDACnB,EAAO,MACR;MACD,OAAO,EAAO,QAAQ,EAAE,OAAO,EAAO,OAAO,GAAG,KAAA;MAChD,eAAe,EAAW,EAAO;gBAEjC,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACG,EAAO,QACP,EAAO,YAAY,MAAW,EAAO,OACpC,kBAAC,QAAD;QAAM,WAAU;kBAAW,MAAc,QAAQ,MAAM;QAAW,CAAA,CAEhE;;MACH,EAhBE,EAAO,IAgBT,CACL;KACC,CAAA,EACC,CAAA,EACR,kBAAC,SAAD,EAAA,UACG,IACC,MAAM,KAAK,EAAE,QAAQ,GAAc,CAAC,CAAC,KAAK,GAAG,MAC3C,kBAAC,MAAD;KAAmB,WAAU;eAC1B,EAAQ,KAAK,MACZ,kBAAC,MAAD;MAAqB,WAAU;gBAC7B,kBAAC,GAAD,EAAU,WAAU,6CAA8C,CAAA;MAC/D,EAFI,EAAO,IAEX,CACL;KACC,EANI,EAMJ,CACL,GACA,EAAK,WAAW,IAClB,kBAAC,MAAD,EAAA,UACE,kBAAC,MAAD;KAAI,SAAS,EAAQ;KAAQ,WAAU;eACpC,KACC,kBAAC,OAAD;MAAK,WAAU;gBAA0C;MAEnD,CAAA;KAEL,CAAA,EACF,CAAA,GAEL,EAAK,KAAK,GAAM,MACd,kBAAC,MAAD;KAEE,WAAW,EACT,+DACA,KAAc,uCACf;KACD,eAAe,IAAa,EAAK;eAEhC,EAAQ,KAAK,MACZ,kBAAC,MAAD;MAEE,WAAW,EAAG,qBAAqB,EAAa,EAAO,SAAS,QAAQ;gBAEvE,EAAO,SACJ,EAAO,OAAO,GAAM,EAAM,GAC1B,OAAQ,EAAiC,EAAO,QAAQ,GAAG;MAC5D,EANE,EAAO,IAMT,CACL;KACC,EAjBE,EAAa,EAAK,CAiBpB,CACL,EAEE,CAAA,CACF;;GACJ,CAAA;EACF,CAAA;;AAsBV,SAAgB,EAAW,EACzB,SACA,eACA,eACA,aACA,iBACA,gBACkB;CAClB,IAAM,KAAa,IAAO,KAAK,IAAW,GACpC,IAAU,KAAK,IAAI,IAAO,GAAU,EAAW;AAErD,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,+CAA+C,EAAU;YAA5E,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf;IAA6C;IAClC;IAAU;IAAK;IAAQ;IAAK;IAAW;IAC5C;MACN,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD;KACE,SAAQ;KACR,MAAK;KACL,eAAe,EAAa,EAAE;KAC9B,UAAU,MAAS;KACnB,cAAW;eAEX,kBAAC,GAAD,EAAc,WAAU,WAAY,CAAA;KAC7B,CAAA;IACT,kBAAC,GAAD;KACE,SAAQ;KACR,MAAK;KACL,eAAe,EAAa,IAAO,EAAE;KACrC,UAAU,MAAS;KACnB,cAAW;eAEX,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA;KAC5B,CAAA;IACT,kBAAC,QAAD;KAAM,WAAU;eAAhB;MAA+B;MACvB;MAAK;MAAK;MACX;;IACP,kBAAC,GAAD;KACE,SAAQ;KACR,MAAK;KACL,eAAe,EAAa,IAAO,EAAE;KACrC,UAAU,MAAS;KACnB,cAAW;eAEX,kBAAC,GAAD,EAAc,WAAU,WAAY,CAAA;KAC7B,CAAA;IACT,kBAAC,GAAD;KACE,SAAQ;KACR,MAAK;KACL,eAAe,EAAa,EAAW;KACvC,UAAU,MAAS;KACnB,cAAW;eAEX,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;KAC9B,CAAA;IACL;KACF"}
|
|
@@ -25,19 +25,19 @@ function c({ open: r, onClose: c, title: l, description: u, children: d, width:
|
|
|
25
25
|
onClick: c,
|
|
26
26
|
"aria-hidden": "true"
|
|
27
27
|
}), /* @__PURE__ */ o("div", {
|
|
28
|
-
className: e("fixed inset-y-0 right-0 flex w-full flex-col bg-
|
|
28
|
+
className: e("fixed inset-y-0 right-0 flex w-full flex-col bg-bg-surface shadow-xl", "animate-in slide-in-from-right duration-300", s[f]),
|
|
29
29
|
role: "dialog",
|
|
30
30
|
"aria-modal": "true",
|
|
31
31
|
"aria-labelledby": "sheet-title",
|
|
32
32
|
children: [
|
|
33
33
|
/* @__PURE__ */ o("div", {
|
|
34
|
-
className: "flex items-start justify-between border-b border-border px-6 py-4",
|
|
34
|
+
className: "flex items-start justify-between border-b border-border-subtle px-6 py-4",
|
|
35
35
|
children: [/* @__PURE__ */ o("div", { children: [/* @__PURE__ */ a("h2", {
|
|
36
36
|
id: "sheet-title",
|
|
37
|
-
className: "text-lg font-semibold text-
|
|
37
|
+
className: "text-lg font-semibold text-text-primary",
|
|
38
38
|
children: l
|
|
39
39
|
}), u && /* @__PURE__ */ a("p", {
|
|
40
|
-
className: "mt-1 text-sm text-
|
|
40
|
+
className: "mt-1 text-sm text-text-secondary",
|
|
41
41
|
children: u
|
|
42
42
|
})] }), /* @__PURE__ */ a(t, {
|
|
43
43
|
variant: "ghost",
|
|
@@ -52,7 +52,7 @@ function c({ open: r, onClose: c, title: l, description: u, children: d, width:
|
|
|
52
52
|
children: d
|
|
53
53
|
}),
|
|
54
54
|
p && /* @__PURE__ */ a("div", {
|
|
55
|
-
className: "border-t border-border px-6 py-4",
|
|
55
|
+
className: "border-t border-border-subtle px-6 py-4",
|
|
56
56
|
children: p
|
|
57
57
|
})
|
|
58
58
|
]
|
|
@@ -83,10 +83,10 @@ function u({ title: t, description: n, children: r, className: i }) {
|
|
|
83
83
|
children: [(t || n) && /* @__PURE__ */ o("div", {
|
|
84
84
|
className: "space-y-1",
|
|
85
85
|
children: [t && /* @__PURE__ */ a("h3", {
|
|
86
|
-
className: "text-sm font-medium text-
|
|
86
|
+
className: "text-sm font-medium text-text-primary",
|
|
87
87
|
children: t
|
|
88
88
|
}), n && /* @__PURE__ */ a("p", {
|
|
89
|
-
className: "text-xs text-
|
|
89
|
+
className: "text-xs text-text-secondary",
|
|
90
90
|
children: n
|
|
91
91
|
})]
|
|
92
92
|
}), /* @__PURE__ */ a("div", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormSheet.js","names":[],"sources":["../../../src/admin/components/FormSheet.tsx"],"sourcesContent":["import * as React from 'react';\nimport { X, Loader2 } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { Button } from './ui';\n\n/**\n * FormSheet props\n */\nexport interface FormSheetProps {\n /** Whether the sheet is open */\n open: boolean;\n /** Callback when the sheet should close */\n onClose: () => void;\n /** Title of the sheet */\n title: string;\n /** Optional description */\n description?: string;\n /** Form content */\n children: React.ReactNode;\n /** Width of the sheet */\n width?: 'sm' | 'md' | 'lg' | 'xl';\n /** Optional footer actions */\n footer?: React.ReactNode;\n}\n\nconst widthClasses = {\n sm: 'max-w-sm',\n md: 'max-w-md',\n lg: 'max-w-lg',\n xl: 'max-w-xl',\n};\n\n/**\n * FormSheet - Slide-over panel for forms\n *\n * A side panel component for displaying forms that slides in from the right.\n * Includes backdrop overlay, header with close button, and optional footer.\n */\nexport function FormSheet({\n open,\n onClose,\n title,\n description,\n children,\n width = 'md',\n footer,\n}: FormSheetProps) {\n // Handle escape key\n React.useEffect(() => {\n const handleEscape = (e: KeyboardEvent) => {\n if (e.key === 'Escape' && open) {\n onClose();\n }\n };\n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [open, onClose]);\n\n // Prevent body scroll when open\n React.useEffect(() => {\n if (open) {\n document.body.style.overflow = 'hidden';\n } else {\n document.body.style.overflow = '';\n }\n return () => {\n document.body.style.overflow = '';\n };\n }, [open]);\n\n if (!open) return null;\n\n return (\n <div className=\"fixed inset-0 z-50\">\n {/* Backdrop */}\n <div\n className=\"fixed inset-0 bg-black/50 transition-opacity\"\n onClick={onClose}\n aria-hidden=\"true\"\n />\n\n {/* Sheet */}\n <div\n className={cn(\n 'fixed inset-y-0 right-0 flex w-full flex-col bg-
|
|
1
|
+
{"version":3,"file":"FormSheet.js","names":[],"sources":["../../../src/admin/components/FormSheet.tsx"],"sourcesContent":["import * as React from 'react';\nimport { X, Loader2 } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { Button } from './ui';\n\n/**\n * FormSheet props\n */\nexport interface FormSheetProps {\n /** Whether the sheet is open */\n open: boolean;\n /** Callback when the sheet should close */\n onClose: () => void;\n /** Title of the sheet */\n title: string;\n /** Optional description */\n description?: string;\n /** Form content */\n children: React.ReactNode;\n /** Width of the sheet */\n width?: 'sm' | 'md' | 'lg' | 'xl';\n /** Optional footer actions */\n footer?: React.ReactNode;\n}\n\nconst widthClasses = {\n sm: 'max-w-sm',\n md: 'max-w-md',\n lg: 'max-w-lg',\n xl: 'max-w-xl',\n};\n\n/**\n * FormSheet - Slide-over panel for forms\n *\n * A side panel component for displaying forms that slides in from the right.\n * Includes backdrop overlay, header with close button, and optional footer.\n */\nexport function FormSheet({\n open,\n onClose,\n title,\n description,\n children,\n width = 'md',\n footer,\n}: FormSheetProps) {\n // Handle escape key\n React.useEffect(() => {\n const handleEscape = (e: KeyboardEvent) => {\n if (e.key === 'Escape' && open) {\n onClose();\n }\n };\n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [open, onClose]);\n\n // Prevent body scroll when open\n React.useEffect(() => {\n if (open) {\n document.body.style.overflow = 'hidden';\n } else {\n document.body.style.overflow = '';\n }\n return () => {\n document.body.style.overflow = '';\n };\n }, [open]);\n\n if (!open) return null;\n\n return (\n <div className=\"fixed inset-0 z-50\">\n {/* Backdrop */}\n <div\n className=\"fixed inset-0 bg-black/50 transition-opacity\"\n onClick={onClose}\n aria-hidden=\"true\"\n />\n\n {/* Sheet */}\n <div\n className={cn(\n 'fixed inset-y-0 right-0 flex w-full flex-col bg-bg-surface shadow-xl',\n 'animate-in slide-in-from-right duration-300',\n widthClasses[width]\n )}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"sheet-title\"\n >\n {/* Header */}\n <div className=\"flex items-start justify-between border-b border-border-subtle px-6 py-4\">\n <div>\n <h2 id=\"sheet-title\" className=\"text-lg font-semibold text-text-primary\">\n {title}\n </h2>\n {description && <p className=\"mt-1 text-sm text-text-secondary\">{description}</p>}\n </div>\n <Button variant=\"ghost\" size=\"icon\" onClick={onClose} aria-label=\"Close\">\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n\n {/* Content */}\n <div className=\"flex-1 overflow-y-auto px-6 py-4\">{children}</div>\n\n {/* Footer */}\n {footer && <div className=\"border-t border-border-subtle px-6 py-4\">{footer}</div>}\n </div>\n </div>\n );\n}\n\n/**\n * FormSheetFooter props\n */\nexport interface FormSheetFooterProps {\n /** Primary action button text */\n submitLabel?: string;\n /** Cancel button text */\n cancelLabel?: string;\n /** Callback for cancel */\n onCancel: () => void;\n /** Whether form is submitting */\n isSubmitting?: boolean;\n /** Whether submit is disabled */\n disabled?: boolean;\n /** Form ID to connect submit button */\n formId?: string;\n /** Callback for submit (if not using form) */\n onSubmit?: () => void;\n /** Variant for submit button */\n submitVariant?: 'default' | 'destructive';\n}\n\n/**\n * FormSheetFooter - Standard footer for FormSheet with cancel/submit buttons\n */\nexport function FormSheetFooter({\n submitLabel = 'Save',\n cancelLabel = 'Cancel',\n onCancel,\n isSubmitting = false,\n disabled = false,\n formId,\n onSubmit,\n submitVariant = 'default',\n}: FormSheetFooterProps) {\n return (\n <div className=\"flex items-center justify-end gap-3\">\n <Button variant=\"outline\" onClick={onCancel} disabled={isSubmitting}>\n {cancelLabel}\n </Button>\n <Button\n type={formId ? 'submit' : 'button'}\n form={formId}\n onClick={onSubmit}\n disabled={disabled || isSubmitting}\n variant={submitVariant}\n >\n {isSubmitting && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\n {submitLabel}\n </Button>\n </div>\n );\n}\n\n/**\n * FormSection - Group related form fields\n */\nexport interface FormSectionProps {\n /** Section title */\n title?: string;\n /** Section description */\n description?: string;\n /** Form fields */\n children: React.ReactNode;\n /** Additional className */\n className?: string;\n}\n\nexport function FormSection({ title, description, children, className }: FormSectionProps) {\n return (\n <div className={cn('space-y-4', className)}>\n {(title || description) && (\n <div className=\"space-y-1\">\n {title && <h3 className=\"text-sm font-medium text-text-primary\">{title}</h3>}\n {description && <p className=\"text-xs text-text-secondary\">{description}</p>}\n </div>\n )}\n <div className=\"space-y-4\">{children}</div>\n </div>\n );\n}\n\n/**\n * FormRow - Horizontal row for form fields\n */\nexport interface FormRowProps {\n /** Form fields */\n children: React.ReactNode;\n /** Number of columns */\n cols?: 2 | 3 | 4;\n /** Additional className */\n className?: string;\n}\n\nexport function FormRow({ children, cols = 2, className }: FormRowProps) {\n const colsClass = {\n 2: 'grid-cols-2',\n 3: 'grid-cols-3',\n 4: 'grid-cols-4',\n };\n return <div className={cn('grid gap-4', colsClass[cols], className)}>{children}</div>;\n}\n"],"mappings":";;;;;;AAyBA,IAAM,IAAe;CACnB,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACL;AAQD,SAAgB,EAAU,EACxB,SACA,YACA,UACA,gBACA,aACA,WAAQ,MACR,aACiB;AA0BjB,QAxBA,EAAM,gBAAgB;EACpB,IAAM,KAAgB,MAAqB;AACzC,GAAI,EAAE,QAAQ,YAAY,KACxB,GAAS;;AAIb,SADA,SAAS,iBAAiB,WAAW,EAAa,QACrC,SAAS,oBAAoB,WAAW,EAAa;IACjE,CAAC,GAAM,EAAQ,CAAC,EAGnB,EAAM,iBACA,IACF,SAAS,KAAK,MAAM,WAAW,WAE/B,SAAS,KAAK,MAAM,WAAW,UAEpB;AACX,WAAS,KAAK,MAAM,WAAW;KAEhC,CAAC,EAAK,CAAC,EAEL,IAGH,kBAAC,OAAD;EAAK,WAAU;YAAf,CAEE,kBAAC,OAAD;GACE,WAAU;GACV,SAAS;GACT,eAAY;GACZ,CAAA,EAGF,kBAAC,OAAD;GACE,WAAW,EACT,wEACA,+CACA,EAAa,GACd;GACD,MAAK;GACL,cAAW;GACX,mBAAgB;aARlB;IAWE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;MAAI,IAAG;MAAc,WAAU;gBAC5B;MACE,CAAA,EACJ,KAAe,kBAAC,KAAD;MAAG,WAAU;gBAAoC;MAAgB,CAAA,CAC7E,EAAA,CAAA,EACN,kBAAC,GAAD;MAAQ,SAAQ;MAAQ,MAAK;MAAO,SAAS;MAAS,cAAW;gBAC/D,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;MAClB,CAAA,CACL;;IAGN,kBAAC,OAAD;KAAK,WAAU;KAAoC;KAAe,CAAA;IAGjE,KAAU,kBAAC,OAAD;KAAK,WAAU;eAA2C;KAAa,CAAA;IAC9E;KACF;MAzCU;;AAsEpB,SAAgB,EAAgB,EAC9B,iBAAc,QACd,iBAAc,UACd,aACA,kBAAe,IACf,cAAW,IACX,WACA,aACA,mBAAgB,aACO;AACvB,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD;GAAQ,SAAQ;GAAU,SAAS;GAAU,UAAU;aACpD;GACM,CAAA,EACT,kBAAC,GAAD;GACE,MAAM,IAAS,WAAW;GAC1B,MAAM;GACN,SAAS;GACT,UAAU,KAAY;GACtB,SAAS;aALX,CAOG,KAAgB,kBAAC,GAAD,EAAS,WAAU,6BAA8B,CAAA,EACjE,EACM;KACL;;;AAkBV,SAAgB,EAAY,EAAE,UAAO,gBAAa,aAAU,gBAA+B;AACzF,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,aAAa,EAAU;YAA1C,EACI,KAAS,MACT,kBAAC,OAAD;GAAK,WAAU;aAAf,CACG,KAAS,kBAAC,MAAD;IAAI,WAAU;cAAyC;IAAW,CAAA,EAC3E,KAAe,kBAAC,KAAD;IAAG,WAAU;cAA+B;IAAgB,CAAA,CACxE;MAER,kBAAC,OAAD;GAAK,WAAU;GAAa;GAAe,CAAA,CACvC;;;AAgBV,SAAgB,EAAQ,EAAE,aAAU,UAAO,GAAG,gBAA2B;AAMvE,QAAO,kBAAC,OAAD;EAAK,WAAW,EAAG,cALR;GAChB,GAAG;GACH,GAAG;GACH,GAAG;GACJ,CACiD,IAAO,EAAU;EAAG;EAAe,CAAA"}
|
|
@@ -1,57 +1,60 @@
|
|
|
1
1
|
import { cn as e } from "../utils/cn.js";
|
|
2
|
-
import { Badge as t,
|
|
3
|
-
import { formatChange as
|
|
4
|
-
import { Minus as
|
|
5
|
-
import { Link as
|
|
6
|
-
import { jsx as
|
|
2
|
+
import { Badge as t, Skeleton as n } from "./ui.js";
|
|
3
|
+
import { formatChange as r, formatCurrency as i, formatNumber as a } from "../utils/formatters.js";
|
|
4
|
+
import { Minus as o, TrendingDown as s, TrendingUp as c } from "lucide-react";
|
|
5
|
+
import { Link as l } from "react-router-dom";
|
|
6
|
+
import { jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
7
|
+
import { GlassCard as f } from "@burdenoff/fe-libs/ui";
|
|
7
8
|
//#region src/admin/components/MetricCard.tsx
|
|
8
|
-
function
|
|
9
|
-
let
|
|
10
|
-
|
|
9
|
+
function p({ title: p, value: m, change: h, changeType: g = "number", trend: _, changeIsPositive: v, icon: y, subtitle: b, linkTo: x, priority: S, loading: C, hero: w = !1, className: T }) {
|
|
10
|
+
let E = v ?? _ === "up", D = typeof m == "number" ? g === "currency" ? i(m) : a(m) : m, O = h === void 0 ? null : r(h, g === "percentage"), k = _ === "up" ? c : _ === "down" ? s : o, A = /* @__PURE__ */ d(f, {
|
|
11
|
+
treatment: w ? "glass" : "metric",
|
|
12
|
+
glow: w,
|
|
13
|
+
className: e("h-full p-6", S && {
|
|
11
14
|
normal: "",
|
|
12
|
-
high: "border-l-4 border-l-warning",
|
|
13
|
-
critical: "border-l-4 border-l-
|
|
14
|
-
}[
|
|
15
|
-
children: [/* @__PURE__ */
|
|
15
|
+
high: "border-l-4 border-l-status-warning-border",
|
|
16
|
+
critical: "border-l-4 border-l-status-error-border"
|
|
17
|
+
}[S], T),
|
|
18
|
+
children: [/* @__PURE__ */ d("div", {
|
|
16
19
|
className: "flex flex-row items-center justify-between pb-2",
|
|
17
|
-
children: [/* @__PURE__ */
|
|
18
|
-
className: "text-sm font-medium text-
|
|
19
|
-
children:
|
|
20
|
-
}),
|
|
21
|
-
className: "text-
|
|
22
|
-
children:
|
|
20
|
+
children: [/* @__PURE__ */ u("span", {
|
|
21
|
+
className: "text-sm font-medium text-text-secondary",
|
|
22
|
+
children: p
|
|
23
|
+
}), y && /* @__PURE__ */ u("div", {
|
|
24
|
+
className: "text-text-secondary",
|
|
25
|
+
children: y
|
|
23
26
|
})]
|
|
24
|
-
}),
|
|
27
|
+
}), C ? /* @__PURE__ */ d("div", {
|
|
25
28
|
className: "space-y-2",
|
|
26
|
-
children: [/* @__PURE__ */
|
|
27
|
-
}) : /* @__PURE__ */
|
|
29
|
+
children: [/* @__PURE__ */ u(n, { className: "h-8 w-24" }), /* @__PURE__ */ u(n, { className: "h-4 w-16" })]
|
|
30
|
+
}) : /* @__PURE__ */ d("div", { children: [/* @__PURE__ */ d("div", {
|
|
28
31
|
className: "flex items-baseline justify-between",
|
|
29
|
-
children: [/* @__PURE__ */
|
|
30
|
-
className: "text-3xl font-bold text-
|
|
31
|
-
children:
|
|
32
|
-
}),
|
|
33
|
-
variant:
|
|
34
|
-
className: "flex items-center gap-1",
|
|
35
|
-
children: [/* @__PURE__ */
|
|
32
|
+
children: [/* @__PURE__ */ u("div", {
|
|
33
|
+
className: "text-3xl font-bold tabular-nums text-text-primary",
|
|
34
|
+
children: D
|
|
35
|
+
}), O !== null && /* @__PURE__ */ d(t, {
|
|
36
|
+
variant: E ? "success" : "destructive",
|
|
37
|
+
className: "flex items-center gap-1 tabular-nums",
|
|
38
|
+
children: [/* @__PURE__ */ u(k, { className: "h-3 w-3" }), O]
|
|
36
39
|
})]
|
|
37
|
-
}),
|
|
38
|
-
className: "mt-1 text-xs text-
|
|
39
|
-
children:
|
|
40
|
-
})] })
|
|
40
|
+
}), b && /* @__PURE__ */ u("p", {
|
|
41
|
+
className: "mt-1 text-xs text-text-secondary",
|
|
42
|
+
children: b
|
|
43
|
+
})] })]
|
|
41
44
|
});
|
|
42
|
-
return
|
|
43
|
-
to:
|
|
44
|
-
className: "block",
|
|
45
|
-
children:
|
|
46
|
-
}) :
|
|
45
|
+
return x ? /* @__PURE__ */ u(l, {
|
|
46
|
+
to: x,
|
|
47
|
+
className: "block rounded-card focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] focus-visible:ring-offset-2",
|
|
48
|
+
children: A
|
|
49
|
+
}) : A;
|
|
47
50
|
}
|
|
48
|
-
function
|
|
49
|
-
return /* @__PURE__ */
|
|
51
|
+
function m({ children: t, className: n }) {
|
|
52
|
+
return /* @__PURE__ */ u("div", {
|
|
50
53
|
className: e("grid gap-4", "grid-cols-1 sm:grid-cols-2 xl:grid-cols-4", n),
|
|
51
54
|
children: t
|
|
52
55
|
});
|
|
53
56
|
}
|
|
54
57
|
//#endregion
|
|
55
|
-
export {
|
|
58
|
+
export { p as MetricCard, m as MetricsGrid };
|
|
56
59
|
|
|
57
60
|
//# sourceMappingURL=MetricCard.js.map
|