@burdenoff/microfe-admin 2026.625.1 → 2026.626.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.map +1 -1
- package/dist/admin/components/ConfirmDialog.js +1 -1
- package/dist/admin/components/ConfirmDialog.js.map +1 -1
- package/dist/admin/components/FormSheet.js +1 -1
- package/dist/admin/components/FormSheet.js.map +1 -1
- package/dist/admin/components/TenantSsoPanel.js +1 -1
- package/dist/admin/components/TenantSsoPanel.js.map +1 -1
- package/dist/admin/components/Toast.js +11 -11
- package/dist/admin/components/Toast.js.map +1 -1
- package/dist/admin/components/forms/SubscriptionForm.js +2 -2
- package/dist/admin/components/forms/SubscriptionForm.js.map +1 -1
- package/dist/admin/components/forms/TenantForm.js +1 -1
- package/dist/admin/components/forms/UserForm.js +1 -1
- package/dist/admin/components/ui.js +3 -3
- package/dist/admin/components/ui.js.map +1 -1
- package/dist/admin/constants/index.js +33 -24
- package/dist/admin/constants/index.js.map +1 -1
- package/dist/admin/pages/AuditLogsPage.js +92 -77
- package/dist/admin/pages/AuditLogsPage.js.map +1 -1
- package/dist/admin/pages/BillingPage.js +95 -51
- package/dist/admin/pages/BillingPage.js.map +1 -1
- package/dist/admin/pages/CommunicationsPage.js +97 -92
- package/dist/admin/pages/CommunicationsPage.js.map +1 -1
- package/dist/admin/pages/ContentPagesPage.js +274 -259
- package/dist/admin/pages/ContentPagesPage.js.map +1 -1
- package/dist/admin/pages/DashboardPage.js +113 -84
- package/dist/admin/pages/DashboardPage.js.map +1 -1
- package/dist/admin/pages/FeatureFlagsPage.js +93 -75
- package/dist/admin/pages/FeatureFlagsPage.js.map +1 -1
- package/dist/admin/pages/OrganizationsListPage.js +105 -87
- package/dist/admin/pages/OrganizationsListPage.js.map +1 -1
- package/dist/admin/pages/ProductsPage.js +138 -130
- package/dist/admin/pages/ProductsPage.js.map +1 -1
- package/dist/admin/pages/ReferralsPage.js +70 -52
- package/dist/admin/pages/ReferralsPage.js.map +1 -1
- package/dist/admin/pages/RolesPermissionsPage.js +136 -118
- package/dist/admin/pages/RolesPermissionsPage.js.map +1 -1
- package/dist/admin/pages/SREPage.js +34 -32
- package/dist/admin/pages/SREPage.js.map +1 -1
- package/dist/admin/pages/SandboxSettingsPage.js +360 -322
- package/dist/admin/pages/SandboxSettingsPage.js.map +1 -1
- package/dist/admin/pages/SettingsPage.js +63 -60
- package/dist/admin/pages/SettingsPage.js.map +1 -1
- package/dist/admin/pages/SyncFailuresPage.js +120 -118
- package/dist/admin/pages/SyncFailuresPage.js.map +1 -1
- package/dist/admin/pages/TenantDetailPage.js +261 -218
- package/dist/admin/pages/TenantDetailPage.js.map +1 -1
- package/dist/admin/pages/TenantFormRoutePage.js +65 -61
- package/dist/admin/pages/TenantFormRoutePage.js.map +1 -1
- package/dist/admin/pages/TenantsListPage.js +86 -71
- package/dist/admin/pages/TenantsListPage.js.map +1 -1
- package/dist/admin/pages/UsersListPage.js +128 -113
- package/dist/admin/pages/UsersListPage.js.map +1 -1
- package/dist/admin/pages/WaitlistPage.js +78 -63
- package/dist/admin/pages/WaitlistPage.js.map +1 -1
- package/dist/admin/pages/WorkflowsPage.js +107 -102
- package/dist/admin/pages/WorkflowsPage.js.map +1 -1
- package/dist/admin/pages/WorkspacesListPage.js +61 -49
- package/dist/admin/pages/WorkspacesListPage.js.map +1 -1
- package/dist/node_modules/@hookform/resolvers/dist/resolvers.js +2 -2
- package/dist/node_modules/@hookform/resolvers/dist/resolvers.js.map +1 -1
- package/dist/node_modules/@hookform/resolvers/zod/dist/zod.js +35 -26
- package/dist/node_modules/@hookform/resolvers/zod/dist/zod.js.map +1 -1
- package/dist/node_modules/react-hook-form/dist/index.esm.js +609 -516
- package/dist/node_modules/react-hook-form/dist/index.esm.js.map +1 -1
- package/package.json +2 -2
|
@@ -8,8 +8,8 @@ var r = ({ message: r = "You don't have permission to access this page." }) => /
|
|
|
8
8
|
className: "text-center space-y-3",
|
|
9
9
|
children: [
|
|
10
10
|
/* @__PURE__ */ t("div", {
|
|
11
|
-
className: "w-14 h-14 mx-auto rounded-full bg-
|
|
12
|
-
children: /* @__PURE__ */ t(e, { className: "w-7 h-7 text-
|
|
11
|
+
className: "w-14 h-14 mx-auto rounded-full bg-status-error-bg border border-status-error-border flex items-center justify-center",
|
|
12
|
+
children: /* @__PURE__ */ t(e, { className: "w-7 h-7 text-status-error-text" })
|
|
13
13
|
}),
|
|
14
14
|
/* @__PURE__ */ t("h2", {
|
|
15
15
|
className: "text-lg font-semibold text-text-primary",
|
|
@@ -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-
|
|
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-status-error-bg border border-status-error-border flex items-center justify-center\">\n <ShieldOff className=\"w-7 h-7 text-status-error-text\" />\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,kCAAmC,CAAA;IACpD,CAAA;GACN,kBAAC,MAAD;IAAI,WAAU;cAA0C;IAAkB,CAAA;GAC1E,kBAAC,KAAD;IAAG,WAAU;cAAwC;IAAY,CAAA;GAC7D;;CACF,CAAA"}
|
|
@@ -37,10 +37,10 @@ function C({ activity: t }) {
|
|
|
37
37
|
className: "flex items-start gap-3",
|
|
38
38
|
children: [/* @__PURE__ */ b("div", {
|
|
39
39
|
className: e("rounded-full p-2", {
|
|
40
|
-
info: "bg-info
|
|
41
|
-
warning: "bg-warning
|
|
42
|
-
error: "bg-status-error-bg
|
|
43
|
-
success: "bg-success
|
|
40
|
+
info: "bg-status-info-bg text-status-info-text",
|
|
41
|
+
warning: "bg-status-warning-bg text-status-warning-text",
|
|
42
|
+
error: "bg-status-error-bg text-status-error-text",
|
|
43
|
+
success: "bg-status-success-bg text-status-success-text"
|
|
44
44
|
}[t.severity || "info"]),
|
|
45
45
|
children: n
|
|
46
46
|
}), /* @__PURE__ */ x("div", {
|
|
@@ -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-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
|
|
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-status-info-bg text-status-info-text',\n warning: 'bg-status-warning-bg text-status-warning-text',\n error: 'bg-status-error-bg text-status-error-text',\n success: 'bg-status-success-bg text-status-success-text',\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"}
|
|
@@ -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
|
|
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 semantic chart tokens (theme-aware; resolve correctly in\n// every theme + dark mode — no hardcoded hsl() fallbacks).\nconst CHART_COLORS = [\n 'var(--color-chart-1)',\n 'var(--color-chart-2)',\n 'var(--color-chart-3)',\n 'var(--color-chart-4)',\n 'var(--color-chart-5)',\n];\n\n// Shared tooltip styling resolved from semantic surface tokens so popovers stay\n// legible (strong contrast) in dark mode rather than rendering invalid colors.\nconst CHART_TOOLTIP_CONTENT_STYLE = {\n backgroundColor: 'var(--color-bg-elevated)',\n border: '1px solid var(--color-border-seam)',\n borderRadius: '6px',\n} as const;\n\nconst CHART_TOOLTIP_LABEL_STYLE = { color: 'var(--color-text-primary)' } as const;\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={CHART_TOOLTIP_CONTENT_STYLE}\n labelStyle={CHART_TOOLTIP_LABEL_STYLE}\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={CHART_TOOLTIP_CONTENT_STYLE}\n labelStyle={CHART_TOOLTIP_LABEL_STYLE}\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={CHART_TOOLTIP_CONTENT_STYLE}\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":";;;;;AAoPA,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"}
|
|
@@ -31,7 +31,7 @@ function l({ open: o, onClose: l, onConfirm: u, title: d, description: f, confir
|
|
|
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", {
|
|
34
|
-
className: "fixed inset-0 bg-
|
|
34
|
+
className: "fixed inset-0 bg-overlay transition-opacity",
|
|
35
35
|
onClick: g ? void 0 : l,
|
|
36
36
|
"aria-hidden": "true"
|
|
37
37
|
}), /* @__PURE__ */ c("div", {
|
|
@@ -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-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-
|
|
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-overlay 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"}
|
|
@@ -21,7 +21,7 @@ function c({ open: r, onClose: c, title: l, description: u, children: d, width:
|
|
|
21
21
|
}), [r]), r ? /* @__PURE__ */ o("div", {
|
|
22
22
|
className: "fixed inset-0 z-50",
|
|
23
23
|
children: [/* @__PURE__ */ a("div", {
|
|
24
|
-
className: "fixed inset-0 bg-
|
|
24
|
+
className: "fixed inset-0 bg-overlay transition-opacity",
|
|
25
25
|
onClick: c,
|
|
26
26
|
"aria-hidden": "true"
|
|
27
27
|
}), /* @__PURE__ */ o("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-
|
|
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-overlay 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"}
|
|
@@ -234,7 +234,7 @@ function E({ tenantId: E, canManage: D }) {
|
|
|
234
234
|
enabled: !e.enabled
|
|
235
235
|
})),
|
|
236
236
|
className: `relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2 ${F.enabled ? "bg-action-primary-bg" : "bg-bg-sunken"}`,
|
|
237
|
-
children: /* @__PURE__ */ S("span", { className: `pointer-events-none inline-block h-4 w-4 transform rounded-full bg-
|
|
237
|
+
children: /* @__PURE__ */ S("span", { className: `pointer-events-none inline-block h-4 w-4 transform rounded-full bg-bg-surface shadow-lg transition-transform ${F.enabled ? "translate-x-4" : "translate-x-0"}` })
|
|
238
238
|
}), /* @__PURE__ */ S("span", {
|
|
239
239
|
className: "text-sm",
|
|
240
240
|
children: k("admin.tenantDetail.sso.enabledLabel", "Enable SSO for this tenant")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TenantSsoPanel.js","names":[],"sources":["../../../src/admin/components/TenantSsoPanel.tsx"],"sourcesContent":["import { useState, useEffect } from 'react';\nimport { ShieldCheck, Loader2, Trash2, Save, ChevronDown, ChevronUp } from 'lucide-react';\nimport {\n useAdminGetSsoConfigQuery,\n useAdminSaveSsoConfigMutation,\n useAdminDeleteSsoConfigMutation,\n} from '../../generated/global-operations';\nimport { Card, CardHeader, CardTitle, CardContent, Button, Input, Separator } from './ui';\nimport { useToast } from './Toast';\nimport { ConfirmDialog } from './ConfirmDialog';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../utils/i18n';\n\ninterface TenantSsoPanelProps {\n tenantId: string;\n canManage: boolean;\n}\n\ninterface SsoFormData {\n entityId: string;\n ssoUrl: string;\n sloUrl: string;\n certificate: string;\n enabled: boolean;\n}\n\nconst EMPTY_FORM: SsoFormData = {\n entityId: '',\n ssoUrl: '',\n sloUrl: '',\n certificate: '',\n enabled: true,\n};\n\nexport function TenantSsoPanel({ tenantId, canManage }: TenantSsoPanelProps) {\n const { t } = useI18n();\n const tr = (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params);\n const toast = useToast();\n\n const [expanded, setExpanded] = useState(false);\n const [editing, setEditing] = useState(false);\n const [form, setForm] = useState<SsoFormData>(EMPTY_FORM);\n const [confirmDelete, setConfirmDelete] = useState(false);\n\n const { data, loading, error, refetch } = useAdminGetSsoConfigQuery({\n variables: { tenantId },\n fetchPolicy: 'cache-and-network',\n });\n\n const [saveSso, { loading: saving }] = useAdminSaveSsoConfigMutation();\n const [deleteSso, { loading: deleting }] = useAdminDeleteSsoConfigMutation();\n\n const config = data?.getSSOConfig;\n\n useEffect(() => {\n if (config) {\n setForm({\n entityId: config.entityId,\n ssoUrl: config.ssoUrl,\n sloUrl: config.sloUrl ?? '',\n certificate: config.certificate,\n enabled: config.enabled,\n });\n } else {\n setForm(EMPTY_FORM);\n }\n }, [config]);\n\n const handleSave = async (e: React.FormEvent) => {\n e.preventDefault();\n try {\n await saveSso({\n variables: {\n tenantId,\n input: {\n entityId: form.entityId,\n ssoUrl: form.ssoUrl,\n sloUrl: form.sloUrl || undefined,\n certificate: form.certificate,\n enabled: form.enabled,\n },\n },\n context: { headers: { 'x-workspace-id': '' } },\n });\n toast.success(\n tr('admin.tenantDetail.sso.savedTitle', 'SSO configuration saved'),\n tr(\n 'admin.tenantDetail.sso.savedDescription',\n 'SAML SSO has been configured for this tenant.'\n )\n );\n setEditing(false);\n void refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.sso.errorTitle', 'Failed to save SSO config'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.sso.unknownError', 'Unknown error')\n );\n }\n };\n\n const handleDelete = async () => {\n try {\n await deleteSso({\n variables: { tenantId },\n context: { headers: { 'x-workspace-id': '' } },\n });\n toast.success(\n tr('admin.tenantDetail.sso.deletedTitle', 'SSO configuration removed'),\n tr('admin.tenantDetail.sso.deletedDescription', 'SSO has been disabled for this tenant.')\n );\n setConfirmDelete(false);\n setEditing(false);\n void refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.sso.errorTitle', 'Failed to delete SSO config'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.sso.unknownError', 'Unknown error')\n );\n }\n };\n\n return (\n <>\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <CardTitle className=\"flex items-center gap-2 text-lg\">\n <ShieldCheck className=\"h-5 w-5\" />\n {tr('admin.tenantDetail.sso.title', 'SSO Configuration')}\n </CardTitle>\n <button\n type=\"button\"\n onClick={() => setExpanded((v) => !v)}\n className=\"flex items-center gap-1 text-sm text-text-secondary hover:text-text-primary transition-colors\"\n aria-expanded={expanded}\n aria-label={\n expanded\n ? tr('admin.tenantDetail.sso.collapse', 'Collapse SSO configuration')\n : tr('admin.tenantDetail.sso.expand', 'Expand SSO configuration')\n }\n >\n {expanded ? <ChevronUp className=\"h-4 w-4\" /> : <ChevronDown className=\"h-4 w-4\" />}\n </button>\n </div>\n </CardHeader>\n\n {expanded && (\n <CardContent className=\"space-y-4\">\n {loading ? (\n <div className=\"flex items-center justify-center py-6\">\n <Loader2 className=\"h-6 w-6 animate-spin text-text-secondary\" />\n </div>\n ) : error ? (\n <p className=\"text-sm text-status-error-text\">{error.message}</p>\n ) : config && !editing ? (\n /* ── Configured state — read-only view ── */\n <div className=\"space-y-3\">\n <div className=\"flex items-center gap-2\">\n <span\n className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${\n config.enabled\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-bg-sunken text-text-secondary'\n }`}\n >\n {config.enabled\n ? tr('admin.tenantDetail.sso.enabled', 'Enabled')\n : tr('admin.tenantDetail.sso.disabled', 'Disabled')}\n </span>\n </div>\n\n <dl className=\"grid gap-2 text-sm\">\n <div className=\"flex flex-col gap-0.5\">\n <dt className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.sso.entityId', 'Entity ID')}\n </dt>\n <dd className=\"font-mono text-xs break-all\">{config.entityId}</dd>\n </div>\n <div className=\"flex flex-col gap-0.5\">\n <dt className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.sso.ssoUrl', 'SSO URL')}\n </dt>\n <dd className=\"font-mono text-xs break-all\">{config.ssoUrl}</dd>\n </div>\n {config.sloUrl && (\n <div className=\"flex flex-col gap-0.5\">\n <dt className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.sso.sloUrl', 'SLO URL')}\n </dt>\n <dd className=\"font-mono text-xs break-all\">{config.sloUrl}</dd>\n </div>\n )}\n </dl>\n\n {canManage && (\n <div className=\"flex gap-2 pt-2\">\n <Button variant=\"outline\" size=\"sm\" onClick={() => setEditing(true)}>\n {tr('admin.tenantDetail.sso.editButton', 'Edit')}\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"text-status-error-text hover:text-status-error-text\"\n onClick={() => setConfirmDelete(true)}\n >\n <Trash2 className=\"mr-1.5 h-3.5 w-3.5\" />\n {tr('admin.tenantDetail.sso.deleteButton', 'Remove')}\n </Button>\n </div>\n )}\n </div>\n ) : (\n /* ── Not configured or editing — show form ── */\n <form onSubmit={handleSave} className=\"space-y-4\">\n {!config && (\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.tenantDetail.sso.notConfigured',\n 'No SSO configuration. Fill in the SAML details below to enable enterprise SSO.'\n )}\n </p>\n )}\n\n <Separator />\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"sm:col-span-2 space-y-1.5\">\n <label className=\"text-sm font-medium\" htmlFor={`sso-entityId-${tenantId}`}>\n {tr('admin.tenantDetail.sso.entityId', 'Entity ID')}\n <span className=\"ml-1 text-status-error-text\">*</span>\n </label>\n <Input\n id={`sso-entityId-${tenantId}`}\n value={form.entityId}\n onChange={(e) => setForm((f) => ({ ...f, entityId: e.target.value }))}\n placeholder=\"https://idp.example.com/entity\"\n required\n />\n </div>\n\n <div className=\"sm:col-span-2 space-y-1.5\">\n <label className=\"text-sm font-medium\" htmlFor={`sso-ssoUrl-${tenantId}`}>\n {tr('admin.tenantDetail.sso.ssoUrl', 'SSO URL')}\n <span className=\"ml-1 text-status-error-text\">*</span>\n </label>\n <Input\n id={`sso-ssoUrl-${tenantId}`}\n value={form.ssoUrl}\n onChange={(e) => setForm((f) => ({ ...f, ssoUrl: e.target.value }))}\n placeholder=\"https://idp.example.com/sso\"\n required\n />\n </div>\n\n <div className=\"sm:col-span-2 space-y-1.5\">\n <label className=\"text-sm font-medium\" htmlFor={`sso-sloUrl-${tenantId}`}>\n {tr('admin.tenantDetail.sso.sloUrl', 'SLO URL')}\n <span className=\"ml-0.5 text-xs text-text-secondary\">\n {tr('admin.tenantDetail.sso.optional', '(optional)')}\n </span>\n </label>\n <Input\n id={`sso-sloUrl-${tenantId}`}\n value={form.sloUrl}\n onChange={(e) => setForm((f) => ({ ...f, sloUrl: e.target.value }))}\n placeholder=\"https://idp.example.com/slo\"\n />\n </div>\n\n <div className=\"sm:col-span-2 space-y-1.5\">\n <label className=\"text-sm font-medium\" htmlFor={`sso-cert-${tenantId}`}>\n {tr('admin.tenantDetail.sso.certificate', 'X.509 Certificate')}\n <span className=\"ml-1 text-status-error-text\">*</span>\n </label>\n <textarea\n id={`sso-cert-${tenantId}`}\n value={form.certificate}\n onChange={(e) => setForm((f) => ({ ...f, certificate: e.target.value }))}\n placeholder=\"-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----\"\n required\n rows={5}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 font-mono text-xs text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-none\"\n />\n </div>\n\n <div className=\"sm:col-span-2 flex items-center gap-3\">\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={form.enabled}\n aria-label={tr(\n 'admin.tenantDetail.sso.enabledLabel',\n 'Enable SSO for this tenant'\n )}\n onClick={() => setForm((f) => ({ ...f, enabled: !f.enabled }))}\n className={`relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2 ${\n form.enabled ? 'bg-action-primary-bg' : 'bg-bg-sunken'\n }`}\n >\n <span\n className={`pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow-lg transition-transform ${\n form.enabled ? 'translate-x-4' : 'translate-x-0'\n }`}\n />\n </button>\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.sso.enabledLabel', 'Enable SSO for this tenant')}\n </span>\n </div>\n </div>\n\n <Separator />\n\n <div className=\"flex gap-2\">\n <Button type=\"submit\" size=\"sm\" disabled={saving}>\n {saving ? (\n <Loader2 className=\"mr-1.5 h-3.5 w-3.5 animate-spin\" />\n ) : (\n <Save className=\"mr-1.5 h-3.5 w-3.5\" />\n )}\n {tr('admin.tenantDetail.sso.saveButton', 'Save SSO Config')}\n </Button>\n {editing && (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setEditing(false)}\n >\n {tr('admin.tenantDetail.sso.cancelButton', 'Cancel')}\n </Button>\n )}\n </div>\n </form>\n )}\n </CardContent>\n )}\n </Card>\n\n <ConfirmDialog\n open={confirmDelete}\n onClose={() => setConfirmDelete(false)}\n title={tr('admin.tenantDetail.sso.deleteTitle', 'Remove SSO configuration?')}\n description={tr(\n 'admin.tenantDetail.sso.deleteDescription',\n 'This will remove the SAML SSO configuration. Users will no longer be able to sign in via SSO.'\n )}\n confirmLabel={tr('admin.tenantDetail.sso.deleteConfirm', 'Remove SSO')}\n variant=\"destructive\"\n onConfirm={handleDelete}\n loading={deleting}\n />\n </>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TenantSsoPanel.js","names":[],"sources":["../../../src/admin/components/TenantSsoPanel.tsx"],"sourcesContent":["import { useState, useEffect } from 'react';\nimport { ShieldCheck, Loader2, Trash2, Save, ChevronDown, ChevronUp } from 'lucide-react';\nimport {\n useAdminGetSsoConfigQuery,\n useAdminSaveSsoConfigMutation,\n useAdminDeleteSsoConfigMutation,\n} from '../../generated/global-operations';\nimport { Card, CardHeader, CardTitle, CardContent, Button, Input, Separator } from './ui';\nimport { useToast } from './Toast';\nimport { ConfirmDialog } from './ConfirmDialog';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../utils/i18n';\n\ninterface TenantSsoPanelProps {\n tenantId: string;\n canManage: boolean;\n}\n\ninterface SsoFormData {\n entityId: string;\n ssoUrl: string;\n sloUrl: string;\n certificate: string;\n enabled: boolean;\n}\n\nconst EMPTY_FORM: SsoFormData = {\n entityId: '',\n ssoUrl: '',\n sloUrl: '',\n certificate: '',\n enabled: true,\n};\n\nexport function TenantSsoPanel({ tenantId, canManage }: TenantSsoPanelProps) {\n const { t } = useI18n();\n const tr = (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params);\n const toast = useToast();\n\n const [expanded, setExpanded] = useState(false);\n const [editing, setEditing] = useState(false);\n const [form, setForm] = useState<SsoFormData>(EMPTY_FORM);\n const [confirmDelete, setConfirmDelete] = useState(false);\n\n const { data, loading, error, refetch } = useAdminGetSsoConfigQuery({\n variables: { tenantId },\n fetchPolicy: 'cache-and-network',\n });\n\n const [saveSso, { loading: saving }] = useAdminSaveSsoConfigMutation();\n const [deleteSso, { loading: deleting }] = useAdminDeleteSsoConfigMutation();\n\n const config = data?.getSSOConfig;\n\n useEffect(() => {\n if (config) {\n setForm({\n entityId: config.entityId,\n ssoUrl: config.ssoUrl,\n sloUrl: config.sloUrl ?? '',\n certificate: config.certificate,\n enabled: config.enabled,\n });\n } else {\n setForm(EMPTY_FORM);\n }\n }, [config]);\n\n const handleSave = async (e: React.FormEvent) => {\n e.preventDefault();\n try {\n await saveSso({\n variables: {\n tenantId,\n input: {\n entityId: form.entityId,\n ssoUrl: form.ssoUrl,\n sloUrl: form.sloUrl || undefined,\n certificate: form.certificate,\n enabled: form.enabled,\n },\n },\n context: { headers: { 'x-workspace-id': '' } },\n });\n toast.success(\n tr('admin.tenantDetail.sso.savedTitle', 'SSO configuration saved'),\n tr(\n 'admin.tenantDetail.sso.savedDescription',\n 'SAML SSO has been configured for this tenant.'\n )\n );\n setEditing(false);\n void refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.sso.errorTitle', 'Failed to save SSO config'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.sso.unknownError', 'Unknown error')\n );\n }\n };\n\n const handleDelete = async () => {\n try {\n await deleteSso({\n variables: { tenantId },\n context: { headers: { 'x-workspace-id': '' } },\n });\n toast.success(\n tr('admin.tenantDetail.sso.deletedTitle', 'SSO configuration removed'),\n tr('admin.tenantDetail.sso.deletedDescription', 'SSO has been disabled for this tenant.')\n );\n setConfirmDelete(false);\n setEditing(false);\n void refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantDetail.sso.errorTitle', 'Failed to delete SSO config'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantDetail.sso.unknownError', 'Unknown error')\n );\n }\n };\n\n return (\n <>\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <CardTitle className=\"flex items-center gap-2 text-lg\">\n <ShieldCheck className=\"h-5 w-5\" />\n {tr('admin.tenantDetail.sso.title', 'SSO Configuration')}\n </CardTitle>\n <button\n type=\"button\"\n onClick={() => setExpanded((v) => !v)}\n className=\"flex items-center gap-1 text-sm text-text-secondary hover:text-text-primary transition-colors\"\n aria-expanded={expanded}\n aria-label={\n expanded\n ? tr('admin.tenantDetail.sso.collapse', 'Collapse SSO configuration')\n : tr('admin.tenantDetail.sso.expand', 'Expand SSO configuration')\n }\n >\n {expanded ? <ChevronUp className=\"h-4 w-4\" /> : <ChevronDown className=\"h-4 w-4\" />}\n </button>\n </div>\n </CardHeader>\n\n {expanded && (\n <CardContent className=\"space-y-4\">\n {loading ? (\n <div className=\"flex items-center justify-center py-6\">\n <Loader2 className=\"h-6 w-6 animate-spin text-text-secondary\" />\n </div>\n ) : error ? (\n <p className=\"text-sm text-status-error-text\">{error.message}</p>\n ) : config && !editing ? (\n /* ── Configured state — read-only view ── */\n <div className=\"space-y-3\">\n <div className=\"flex items-center gap-2\">\n <span\n className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${\n config.enabled\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-bg-sunken text-text-secondary'\n }`}\n >\n {config.enabled\n ? tr('admin.tenantDetail.sso.enabled', 'Enabled')\n : tr('admin.tenantDetail.sso.disabled', 'Disabled')}\n </span>\n </div>\n\n <dl className=\"grid gap-2 text-sm\">\n <div className=\"flex flex-col gap-0.5\">\n <dt className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.sso.entityId', 'Entity ID')}\n </dt>\n <dd className=\"font-mono text-xs break-all\">{config.entityId}</dd>\n </div>\n <div className=\"flex flex-col gap-0.5\">\n <dt className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.sso.ssoUrl', 'SSO URL')}\n </dt>\n <dd className=\"font-mono text-xs break-all\">{config.ssoUrl}</dd>\n </div>\n {config.sloUrl && (\n <div className=\"flex flex-col gap-0.5\">\n <dt className=\"text-xs text-text-secondary\">\n {tr('admin.tenantDetail.sso.sloUrl', 'SLO URL')}\n </dt>\n <dd className=\"font-mono text-xs break-all\">{config.sloUrl}</dd>\n </div>\n )}\n </dl>\n\n {canManage && (\n <div className=\"flex gap-2 pt-2\">\n <Button variant=\"outline\" size=\"sm\" onClick={() => setEditing(true)}>\n {tr('admin.tenantDetail.sso.editButton', 'Edit')}\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"text-status-error-text hover:text-status-error-text\"\n onClick={() => setConfirmDelete(true)}\n >\n <Trash2 className=\"mr-1.5 h-3.5 w-3.5\" />\n {tr('admin.tenantDetail.sso.deleteButton', 'Remove')}\n </Button>\n </div>\n )}\n </div>\n ) : (\n /* ── Not configured or editing — show form ── */\n <form onSubmit={handleSave} className=\"space-y-4\">\n {!config && (\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.tenantDetail.sso.notConfigured',\n 'No SSO configuration. Fill in the SAML details below to enable enterprise SSO.'\n )}\n </p>\n )}\n\n <Separator />\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"sm:col-span-2 space-y-1.5\">\n <label className=\"text-sm font-medium\" htmlFor={`sso-entityId-${tenantId}`}>\n {tr('admin.tenantDetail.sso.entityId', 'Entity ID')}\n <span className=\"ml-1 text-status-error-text\">*</span>\n </label>\n <Input\n id={`sso-entityId-${tenantId}`}\n value={form.entityId}\n onChange={(e) => setForm((f) => ({ ...f, entityId: e.target.value }))}\n placeholder=\"https://idp.example.com/entity\"\n required\n />\n </div>\n\n <div className=\"sm:col-span-2 space-y-1.5\">\n <label className=\"text-sm font-medium\" htmlFor={`sso-ssoUrl-${tenantId}`}>\n {tr('admin.tenantDetail.sso.ssoUrl', 'SSO URL')}\n <span className=\"ml-1 text-status-error-text\">*</span>\n </label>\n <Input\n id={`sso-ssoUrl-${tenantId}`}\n value={form.ssoUrl}\n onChange={(e) => setForm((f) => ({ ...f, ssoUrl: e.target.value }))}\n placeholder=\"https://idp.example.com/sso\"\n required\n />\n </div>\n\n <div className=\"sm:col-span-2 space-y-1.5\">\n <label className=\"text-sm font-medium\" htmlFor={`sso-sloUrl-${tenantId}`}>\n {tr('admin.tenantDetail.sso.sloUrl', 'SLO URL')}\n <span className=\"ml-0.5 text-xs text-text-secondary\">\n {tr('admin.tenantDetail.sso.optional', '(optional)')}\n </span>\n </label>\n <Input\n id={`sso-sloUrl-${tenantId}`}\n value={form.sloUrl}\n onChange={(e) => setForm((f) => ({ ...f, sloUrl: e.target.value }))}\n placeholder=\"https://idp.example.com/slo\"\n />\n </div>\n\n <div className=\"sm:col-span-2 space-y-1.5\">\n <label className=\"text-sm font-medium\" htmlFor={`sso-cert-${tenantId}`}>\n {tr('admin.tenantDetail.sso.certificate', 'X.509 Certificate')}\n <span className=\"ml-1 text-status-error-text\">*</span>\n </label>\n <textarea\n id={`sso-cert-${tenantId}`}\n value={form.certificate}\n onChange={(e) => setForm((f) => ({ ...f, certificate: e.target.value }))}\n placeholder=\"-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----\"\n required\n rows={5}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 font-mono text-xs text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-none\"\n />\n </div>\n\n <div className=\"sm:col-span-2 flex items-center gap-3\">\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={form.enabled}\n aria-label={tr(\n 'admin.tenantDetail.sso.enabledLabel',\n 'Enable SSO for this tenant'\n )}\n onClick={() => setForm((f) => ({ ...f, enabled: !f.enabled }))}\n className={`relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:ring-offset-2 ${\n form.enabled ? 'bg-action-primary-bg' : 'bg-bg-sunken'\n }`}\n >\n <span\n className={`pointer-events-none inline-block h-4 w-4 transform rounded-full bg-bg-surface shadow-lg transition-transform ${\n form.enabled ? 'translate-x-4' : 'translate-x-0'\n }`}\n />\n </button>\n <span className=\"text-sm\">\n {tr('admin.tenantDetail.sso.enabledLabel', 'Enable SSO for this tenant')}\n </span>\n </div>\n </div>\n\n <Separator />\n\n <div className=\"flex gap-2\">\n <Button type=\"submit\" size=\"sm\" disabled={saving}>\n {saving ? (\n <Loader2 className=\"mr-1.5 h-3.5 w-3.5 animate-spin\" />\n ) : (\n <Save className=\"mr-1.5 h-3.5 w-3.5\" />\n )}\n {tr('admin.tenantDetail.sso.saveButton', 'Save SSO Config')}\n </Button>\n {editing && (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setEditing(false)}\n >\n {tr('admin.tenantDetail.sso.cancelButton', 'Cancel')}\n </Button>\n )}\n </div>\n </form>\n )}\n </CardContent>\n )}\n </Card>\n\n <ConfirmDialog\n open={confirmDelete}\n onClose={() => setConfirmDelete(false)}\n title={tr('admin.tenantDetail.sso.deleteTitle', 'Remove SSO configuration?')}\n description={tr(\n 'admin.tenantDetail.sso.deleteDescription',\n 'This will remove the SAML SSO configuration. Users will no longer be able to sign in via SSO.'\n )}\n confirmLabel={tr('admin.tenantDetail.sso.deleteConfirm', 'Remove SSO')}\n variant=\"destructive\"\n onConfirm={handleDelete}\n loading={deleting}\n />\n </>\n );\n}\n"],"mappings":"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"}
|
|
@@ -75,27 +75,27 @@ function g({ toasts: e, removeToast: t }) {
|
|
|
75
75
|
var _ = {
|
|
76
76
|
success: {
|
|
77
77
|
icon: c,
|
|
78
|
-
bg: "bg-success
|
|
79
|
-
border: "border-success
|
|
80
|
-
iconColor: "text-success"
|
|
78
|
+
bg: "bg-status-success-bg",
|
|
79
|
+
border: "border-status-success-border",
|
|
80
|
+
iconColor: "text-status-success-text"
|
|
81
81
|
},
|
|
82
82
|
error: {
|
|
83
83
|
icon: o,
|
|
84
|
-
bg: "bg-status-error-bg
|
|
85
|
-
border: "border-status-error-border
|
|
84
|
+
bg: "bg-status-error-bg",
|
|
85
|
+
border: "border-status-error-border",
|
|
86
86
|
iconColor: "text-status-error-text"
|
|
87
87
|
},
|
|
88
88
|
warning: {
|
|
89
89
|
icon: s,
|
|
90
|
-
bg: "bg-warning
|
|
91
|
-
border: "border-warning
|
|
92
|
-
iconColor: "text-warning"
|
|
90
|
+
bg: "bg-status-warning-bg",
|
|
91
|
+
border: "border-status-warning-border",
|
|
92
|
+
iconColor: "text-status-warning-text"
|
|
93
93
|
},
|
|
94
94
|
info: {
|
|
95
95
|
icon: l,
|
|
96
|
-
bg: "bg-
|
|
97
|
-
border: "border-
|
|
98
|
-
iconColor: "text-
|
|
96
|
+
bg: "bg-status-info-bg",
|
|
97
|
+
border: "border-status-info-border",
|
|
98
|
+
iconColor: "text-status-info-text"
|
|
99
99
|
}
|
|
100
100
|
};
|
|
101
101
|
function v({ toast: t, onDismiss: n }) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Toast.js","names":[],"sources":["../../../src/admin/components/Toast.tsx"],"sourcesContent":["import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from 'react';\nimport { X, CheckCircle2, AlertCircle, AlertTriangle, Info } from 'lucide-react';\nimport { cn } from '../utils/cn';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type ToastType = 'success' | 'error' | 'warning' | 'info';\n\nexport interface Toast {\n id: string;\n type: ToastType;\n title: string;\n description?: string;\n duration?: number;\n}\n\ninterface ToastContextValue {\n toasts: Toast[];\n addToast: (toast: Omit<Toast, 'id'>) => void;\n removeToast: (id: string) => void;\n success: (title: string, description?: string) => void;\n error: (title: string, description?: string) => void;\n warning: (title: string, description?: string) => void;\n info: (title: string, description?: string) => void;\n}\n\n// ============================================================================\n// Context\n// ============================================================================\n\nconst ToastContext = createContext<ToastContextValue | null>(null);\n\nexport function useToast() {\n const context = useContext(ToastContext);\n if (!context) {\n throw new Error('useToast must be used within a ToastProvider');\n }\n return context;\n}\n\n// ============================================================================\n// Provider\n// ============================================================================\n\ninterface ToastProviderProps {\n children: ReactNode;\n}\n\nexport function ToastProvider({ children }: ToastProviderProps) {\n const [toasts, setToasts] = useState<Toast[]>([]);\n\n const addToast = useCallback((toast: Omit<Toast, 'id'>) => {\n const id = `toast-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n setToasts((prev) => [...prev, { ...toast, id }]);\n }, []);\n\n const removeToast = useCallback((id: string) => {\n setToasts((prev) => prev.filter((t) => t.id !== id));\n }, []);\n\n const success = useCallback(\n (title: string, description?: string) => {\n addToast({ type: 'success', title, description, duration: 4000 });\n },\n [addToast]\n );\n\n const error = useCallback(\n (title: string, description?: string) => {\n addToast({ type: 'error', title, description, duration: 6000 });\n },\n [addToast]\n );\n\n const warning = useCallback(\n (title: string, description?: string) => {\n addToast({ type: 'warning', title, description, duration: 5000 });\n },\n [addToast]\n );\n\n const info = useCallback(\n (title: string, description?: string) => {\n addToast({ type: 'info', title, description, duration: 4000 });\n },\n [addToast]\n );\n\n return (\n <ToastContext.Provider value={{ toasts, addToast, removeToast, success, error, warning, info }}>\n {children}\n <ToastContainer toasts={toasts} removeToast={removeToast} />\n </ToastContext.Provider>\n );\n}\n\n// ============================================================================\n// Toast Container\n// ============================================================================\n\ninterface ToastContainerProps {\n toasts: Toast[];\n removeToast: (id: string) => void;\n}\n\nfunction ToastContainer({ toasts, removeToast }: ToastContainerProps) {\n if (toasts.length === 0) return null;\n\n return (\n <div className=\"fixed bottom-4 right-4 z-[100] flex flex-col gap-2 max-w-sm\">\n {toasts.map((toast) => (\n <ToastItem key={toast.id} toast={toast} onDismiss={() => removeToast(toast.id)} />\n ))}\n </div>\n );\n}\n\n// ============================================================================\n// Toast Item\n// ============================================================================\n\ninterface ToastItemProps {\n toast: Toast;\n onDismiss: () => void;\n}\n\nconst toastStyles: Record<\n ToastType,\n { icon: typeof CheckCircle2; bg: string; border: string; iconColor: string }\n> = {\n success: {\n icon: CheckCircle2,\n bg: 'bg-success
|
|
1
|
+
{"version":3,"file":"Toast.js","names":[],"sources":["../../../src/admin/components/Toast.tsx"],"sourcesContent":["import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from 'react';\nimport { X, CheckCircle2, AlertCircle, AlertTriangle, Info } from 'lucide-react';\nimport { cn } from '../utils/cn';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type ToastType = 'success' | 'error' | 'warning' | 'info';\n\nexport interface Toast {\n id: string;\n type: ToastType;\n title: string;\n description?: string;\n duration?: number;\n}\n\ninterface ToastContextValue {\n toasts: Toast[];\n addToast: (toast: Omit<Toast, 'id'>) => void;\n removeToast: (id: string) => void;\n success: (title: string, description?: string) => void;\n error: (title: string, description?: string) => void;\n warning: (title: string, description?: string) => void;\n info: (title: string, description?: string) => void;\n}\n\n// ============================================================================\n// Context\n// ============================================================================\n\nconst ToastContext = createContext<ToastContextValue | null>(null);\n\nexport function useToast() {\n const context = useContext(ToastContext);\n if (!context) {\n throw new Error('useToast must be used within a ToastProvider');\n }\n return context;\n}\n\n// ============================================================================\n// Provider\n// ============================================================================\n\ninterface ToastProviderProps {\n children: ReactNode;\n}\n\nexport function ToastProvider({ children }: ToastProviderProps) {\n const [toasts, setToasts] = useState<Toast[]>([]);\n\n const addToast = useCallback((toast: Omit<Toast, 'id'>) => {\n const id = `toast-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n setToasts((prev) => [...prev, { ...toast, id }]);\n }, []);\n\n const removeToast = useCallback((id: string) => {\n setToasts((prev) => prev.filter((t) => t.id !== id));\n }, []);\n\n const success = useCallback(\n (title: string, description?: string) => {\n addToast({ type: 'success', title, description, duration: 4000 });\n },\n [addToast]\n );\n\n const error = useCallback(\n (title: string, description?: string) => {\n addToast({ type: 'error', title, description, duration: 6000 });\n },\n [addToast]\n );\n\n const warning = useCallback(\n (title: string, description?: string) => {\n addToast({ type: 'warning', title, description, duration: 5000 });\n },\n [addToast]\n );\n\n const info = useCallback(\n (title: string, description?: string) => {\n addToast({ type: 'info', title, description, duration: 4000 });\n },\n [addToast]\n );\n\n return (\n <ToastContext.Provider value={{ toasts, addToast, removeToast, success, error, warning, info }}>\n {children}\n <ToastContainer toasts={toasts} removeToast={removeToast} />\n </ToastContext.Provider>\n );\n}\n\n// ============================================================================\n// Toast Container\n// ============================================================================\n\ninterface ToastContainerProps {\n toasts: Toast[];\n removeToast: (id: string) => void;\n}\n\nfunction ToastContainer({ toasts, removeToast }: ToastContainerProps) {\n if (toasts.length === 0) return null;\n\n return (\n <div className=\"fixed bottom-4 right-4 z-[100] flex flex-col gap-2 max-w-sm\">\n {toasts.map((toast) => (\n <ToastItem key={toast.id} toast={toast} onDismiss={() => removeToast(toast.id)} />\n ))}\n </div>\n );\n}\n\n// ============================================================================\n// Toast Item\n// ============================================================================\n\ninterface ToastItemProps {\n toast: Toast;\n onDismiss: () => void;\n}\n\nconst toastStyles: Record<\n ToastType,\n { icon: typeof CheckCircle2; bg: string; border: string; iconColor: string }\n> = {\n success: {\n icon: CheckCircle2,\n bg: 'bg-status-success-bg',\n border: 'border-status-success-border',\n iconColor: 'text-status-success-text',\n },\n error: {\n icon: AlertCircle,\n bg: 'bg-status-error-bg',\n border: 'border-status-error-border',\n iconColor: 'text-status-error-text',\n },\n warning: {\n icon: AlertTriangle,\n bg: 'bg-status-warning-bg',\n border: 'border-status-warning-border',\n iconColor: 'text-status-warning-text',\n },\n info: {\n icon: Info,\n bg: 'bg-status-info-bg',\n border: 'border-status-info-border',\n iconColor: 'text-status-info-text',\n },\n};\n\nfunction ToastItem({ toast, onDismiss }: ToastItemProps) {\n const { icon: Icon, bg, border, iconColor } = toastStyles[toast.type];\n\n // Auto-dismiss\n useEffect(() => {\n const duration = toast.duration ?? 4000;\n const timer = setTimeout(onDismiss, duration);\n return () => clearTimeout(timer);\n }, [toast.duration, onDismiss]);\n\n return (\n <div\n className={cn(\n 'flex items-start gap-3 rounded-lg border p-4 shadow-lg backdrop-blur-sm',\n 'animate-in slide-in-from-right-full duration-300',\n 'bg-bg-surface/95',\n bg,\n border\n )}\n role=\"alert\"\n >\n <Icon className={cn('h-5 w-5 shrink-0 mt-0.5', iconColor)} />\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium text-text-primary\">{toast.title}</p>\n {toast.description && (\n <p className=\"mt-1 text-xs text-text-secondary\">{toast.description}</p>\n )}\n </div>\n <button\n onClick={onDismiss}\n className=\"shrink-0 rounded-md p-1 hover:bg-bg-sunken transition-colors\"\n aria-label=\"Dismiss\"\n >\n <X className=\"h-4 w-4 text-text-secondary\" />\n </button>\n </div>\n );\n}\n"],"mappings":";;;;;AAgCA,IAAM,IAAe,EAAwC,KAAK;AAElE,SAAgB,IAAW;CACzB,IAAM,IAAU,EAAW,EAAa;AACxC,KAAI,CAAC,EACH,OAAU,MAAM,+CAA+C;AAEjE,QAAO;;AAWT,SAAgB,EAAc,EAAE,eAAgC;CAC9D,IAAM,CAAC,GAAQ,KAAa,EAAkB,EAAE,CAAC,EAE3C,IAAW,GAAa,MAA6B;EACzD,IAAM,IAAK,SAAS,KAAK,KAAK,CAAC,GAAG,KAAK,QAAQ,CAAC,SAAS,GAAG,CAAC,MAAM,GAAG,EAAE;AACxE,KAAW,MAAS,CAAC,GAAG,GAAM;GAAE,GAAG;GAAO;GAAI,CAAC,CAAC;IAC/C,EAAE,CAAC,EAEA,IAAc,GAAa,MAAe;AAC9C,KAAW,MAAS,EAAK,QAAQ,MAAM,EAAE,OAAO,EAAG,CAAC;IACnD,EAAE,CAAC,EAEA,IAAU,GACb,GAAe,MAAyB;AACvC,IAAS;GAAE,MAAM;GAAW;GAAO;GAAa,UAAU;GAAM,CAAC;IAEnE,CAAC,EAAS,CACX,EAEK,IAAQ,GACX,GAAe,MAAyB;AACvC,IAAS;GAAE,MAAM;GAAS;GAAO;GAAa,UAAU;GAAM,CAAC;IAEjE,CAAC,EAAS,CACX,EAEK,IAAU,GACb,GAAe,MAAyB;AACvC,IAAS;GAAE,MAAM;GAAW;GAAO;GAAa,UAAU;GAAM,CAAC;IAEnE,CAAC,EAAS,CACX,EAEK,IAAO,GACV,GAAe,MAAyB;AACvC,IAAS;GAAE,MAAM;GAAQ;GAAO;GAAa,UAAU;GAAM,CAAC;IAEhE,CAAC,EAAS,CACX;AAED,QACE,kBAAC,EAAa,UAAd;EAAuB,OAAO;GAAE;GAAQ;GAAU;GAAa;GAAS;GAAO;GAAS;GAAM;YAA9F,CACG,GACD,kBAAC,GAAD;GAAwB;GAAqB;GAAe,CAAA,CACtC;;;AAa5B,SAAS,EAAe,EAAE,WAAQ,kBAAoC;AAGpE,QAFI,EAAO,WAAW,IAAU,OAG9B,kBAAC,OAAD;EAAK,WAAU;YACZ,EAAO,KAAK,MACX,kBAAC,GAAD;GAAiC;GAAO,iBAAiB,EAAY,EAAM,GAAG;GAAI,EAAlE,EAAM,GAA4D,CAClF;EACE,CAAA;;AAaV,IAAM,IAGF;CACF,SAAS;EACP,MAAM;EACN,IAAI;EACJ,QAAQ;EACR,WAAW;EACZ;CACD,OAAO;EACL,MAAM;EACN,IAAI;EACJ,QAAQ;EACR,WAAW;EACZ;CACD,SAAS;EACP,MAAM;EACN,IAAI;EACJ,QAAQ;EACR,WAAW;EACZ;CACD,MAAM;EACJ,MAAM;EACN,IAAI;EACJ,QAAQ;EACR,WAAW;EACZ;CACF;AAED,SAAS,EAAU,EAAE,UAAO,gBAA6B;CACvD,IAAM,EAAE,MAAM,GAAM,OAAI,WAAQ,iBAAc,EAAY,EAAM;AAShE,QANA,QAAgB;EACd,IAAM,IAAW,EAAM,YAAY,KAC7B,IAAQ,WAAW,GAAW,EAAS;AAC7C,eAAa,aAAa,EAAM;IAC/B,CAAC,EAAM,UAAU,EAAU,CAAC,EAG7B,kBAAC,OAAD;EACE,WAAW,EACT,2EACA,oDACA,oBACA,GACA,EACD;EACD,MAAK;YARP;GAUE,kBAAC,GAAD,EAAM,WAAW,EAAG,2BAA2B,EAAU,EAAI,CAAA;GAC7D,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eAAyC,EAAM;KAAU,CAAA,EACrE,EAAM,eACL,kBAAC,KAAD;KAAG,WAAU;eAAoC,EAAM;KAAgB,CAAA,CAErE;;GACN,kBAAC,UAAD;IACE,SAAS;IACT,WAAU;IACV,cAAW;cAEX,kBAAC,GAAD,EAAG,WAAU,+BAAgC,CAAA;IACtC,CAAA;GACL"}
|
|
@@ -2,7 +2,7 @@ import { cn as e } from "../../utils/cn.js";
|
|
|
2
2
|
import { Input as t } from "../ui.js";
|
|
3
3
|
import { FormRow as n, FormSection as r, FormSheet as i, FormSheetFooter as a } from "../FormSheet.js";
|
|
4
4
|
import { useForm as o } from "../../../node_modules/react-hook-form/dist/index.esm.js";
|
|
5
|
-
import {
|
|
5
|
+
import { u as s } from "../../../node_modules/@hookform/resolvers/zod/dist/zod.js";
|
|
6
6
|
import { coerce as c, enumType as l, objectType as u, stringType as d } from "../../../node_modules/zod/v3/types.js";
|
|
7
7
|
import * as f from "react";
|
|
8
8
|
import { jsx as p, jsxs as m } from "react/jsx-runtime";
|
|
@@ -265,7 +265,7 @@ function x({ open: e, onClose: c, subscription: l, tenantId: u, tenants: d = [],
|
|
|
265
265
|
}), /* @__PURE__ */ m("span", { children: ["× ", P] })]
|
|
266
266
|
}),
|
|
267
267
|
N === "yearly" && /* @__PURE__ */ m("div", {
|
|
268
|
-
className: "flex justify-between text-sm text-success",
|
|
268
|
+
className: "flex justify-between text-sm text-status-success-text",
|
|
269
269
|
children: [/* @__PURE__ */ p("span", { children: "Annual Discount" }), /* @__PURE__ */ p("span", { children: "-20%" })]
|
|
270
270
|
}),
|
|
271
271
|
/* @__PURE__ */ p("div", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SubscriptionForm.js","names":[],"sources":["../../../../src/admin/components/forms/SubscriptionForm.tsx"],"sourcesContent":["import * as React from 'react';\nimport { useForm } from 'react-hook-form';\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport { z } from 'zod';\nimport { FormSheet, FormSheetFooter, FormSection, FormRow } from '../FormSheet';\nimport { Input } from '../ui';\nimport { cn } from '../../utils/cn';\nimport type { Subscription } from '../../types';\n\n/**\n * Form field wrapper with label and error\n */\ninterface FormFieldProps {\n label: string;\n error?: string;\n required?: boolean;\n children: React.ReactNode;\n description?: string;\n}\n\nfunction FormField({ label, error, required, children, description }: FormFieldProps) {\n return (\n <div className=\"space-y-1.5\">\n <label className=\"text-sm font-medium text-text-primary\">\n {label}\n {required && <span className=\"text-status-error-text ml-0.5\">*</span>}\n </label>\n {children}\n {description && !error && <p className=\"text-xs text-text-secondary\">{description}</p>}\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n </div>\n );\n}\n\n/**\n * Select wrapper styled like Input\n */\ninterface SelectFieldProps extends React.SelectHTMLAttributes<HTMLSelectElement> {\n options: { value: string; label: string }[];\n placeholder?: string;\n}\n\nconst SelectField = React.forwardRef<HTMLSelectElement, SelectFieldProps>(\n ({ className, options, placeholder, ...props }, ref) => (\n <select\n ref={ref}\n className={cn(\n 'flex h-9 w-full rounded-md border border-border-default bg-transparent px-3 py-1 text-sm shadow-sm transition-colors',\n 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)]',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n className\n )}\n {...props}\n >\n {placeholder && (\n <option value=\"\" disabled>\n {placeholder}\n </option>\n )}\n {options.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n )\n);\nSelectField.displayName = 'SelectField';\n\n// Unified form schema (handles both create and edit)\nconst subscriptionFormSchema = z.object({\n tenantId: z.string().min(1, 'Tenant is required'),\n plan: z.enum(['Free', 'Starter', 'Professional', 'Enterprise', 'Custom']),\n billingCycle: z.enum(['monthly', 'yearly']),\n seats: z.coerce.number().int().min(1, 'At least 1 seat is required'),\n status: z.enum(['active', 'cancelled', 'past_due', 'trialing', 'paused']).optional(),\n startDate: z.string().optional(),\n trialEndDate: z.string().optional(),\n});\n\ntype SubscriptionFormData = z.infer<typeof subscriptionFormSchema>;\n\n/**\n * SubscriptionForm props\n */\nexport interface SubscriptionFormProps {\n /** Whether the form is open */\n open: boolean;\n /** Callback when the form should close */\n onClose: () => void;\n /** Existing subscription data for editing (undefined for create) */\n subscription?: Subscription;\n /** Pre-selected tenant ID */\n tenantId?: string;\n /** List of available tenants for selection */\n tenants?: { id: string; name: string }[];\n /** Callback on successful save */\n onSave: (data: SubscriptionFormData & { id?: string }) => Promise<void>;\n}\n\nconst planOptions = [\n { value: 'Free', label: 'Free - $0/mo' },\n { value: 'Starter', label: 'Starter - $29/mo' },\n { value: 'Professional', label: 'Professional - $99/mo' },\n { value: 'Enterprise', label: 'Enterprise - $299/mo' },\n { value: 'Custom', label: 'Custom Pricing' },\n];\n\nconst billingCycleOptions = [\n { value: 'monthly', label: 'Monthly' },\n { value: 'yearly', label: 'Yearly (Save 20%)' },\n];\n\nconst statusOptions = [\n { value: 'active', label: 'Active' },\n { value: 'cancelled', label: 'Cancelled' },\n { value: 'past_due', label: 'Past Due' },\n { value: 'trialing', label: 'Trialing' },\n { value: 'paused', label: 'Paused' },\n];\n\n/**\n * SubscriptionForm - Create/Edit subscription form in a slide-over panel\n */\nexport function SubscriptionForm({\n open,\n onClose,\n subscription,\n tenantId,\n tenants = [],\n onSave,\n}: SubscriptionFormProps) {\n const isEditing = !!subscription;\n const formId = 'subscription-form';\n\n const defaultValues: SubscriptionFormData = {\n tenantId: subscription?.tenantId ?? tenantId ?? '',\n plan: (subscription?.plan ?? 'Starter') as SubscriptionFormData['plan'],\n billingCycle: (subscription?.billingCycle ?? 'monthly') as SubscriptionFormData['billingCycle'],\n seats: subscription?.seats ?? 1,\n status: subscription?.status as SubscriptionFormData['status'],\n startDate: subscription?.startDate\n ? new Date(subscription.startDate).toISOString().split('T')[0]\n : new Date().toISOString().split('T')[0],\n trialEndDate: subscription?.trialEndDate\n ? new Date(subscription.trialEndDate).toISOString().split('T')[0]\n : undefined,\n };\n\n const {\n register,\n handleSubmit,\n formState: { errors, isSubmitting },\n reset,\n watch,\n } = useForm<SubscriptionFormData>({\n resolver: zodResolver(subscriptionFormSchema),\n defaultValues,\n });\n\n // Reset form when subscription or tenantId changes\n React.useEffect(() => {\n if (open) {\n reset(defaultValues);\n }\n }, [open, subscription?.id, tenantId]);\n\n const onSubmit = async (data: SubscriptionFormData) => {\n try {\n await onSave({ ...data, id: subscription?.id });\n onClose();\n } catch (error) {\n console.error('Failed to save subscription:', error);\n }\n };\n\n // Calculate estimated cost\n const plan = watch('plan');\n const billingCycle = watch('billingCycle');\n const seats = watch('seats') || 1;\n\n const planPrices: Record<string, number> = {\n Free: 0,\n Starter: 29,\n Professional: 99,\n Enterprise: 299,\n Custom: 0,\n };\n\n const monthlyPrice = planPrices[plan] || 0;\n const yearlyMultiplier = billingCycle === 'yearly' ? 0.8 : 1; // 20% discount for yearly\n const estimatedCost = monthlyPrice * seats * yearlyMultiplier;\n const billingPeriod = billingCycle === 'yearly' ? 12 : 1;\n const totalPerPeriod = estimatedCost * billingPeriod;\n\n return (\n <FormSheet\n open={open}\n onClose={onClose}\n title={isEditing ? 'Edit Subscription' : 'Create Subscription'}\n description={\n isEditing ? 'Modify subscription details' : 'Set up a new subscription for a tenant'\n }\n width=\"md\"\n footer={\n <FormSheetFooter\n formId={formId}\n submitLabel={isEditing ? 'Save Changes' : 'Create Subscription'}\n onCancel={onClose}\n isSubmitting={isSubmitting}\n />\n }\n >\n <form id={formId} onSubmit={handleSubmit(onSubmit)} className=\"space-y-6\">\n {/* Tenant Selection */}\n {tenants.length > 0 && !tenantId && (\n <FormSection>\n <FormField\n label=\"Tenant\"\n required\n error={errors.tenantId?.message}\n description=\"Select the tenant for this subscription\"\n >\n <SelectField\n {...register('tenantId')}\n options={tenants.map((t) => ({ value: t.id, label: t.name }))}\n placeholder=\"Select a tenant...\"\n />\n </FormField>\n </FormSection>\n )}\n\n {/* Plan Selection */}\n <FormSection title=\"Plan Details\">\n <FormRow>\n <FormField label=\"Plan\" required>\n <SelectField {...register('plan')} options={planOptions} />\n </FormField>\n\n <FormField label=\"Billing Cycle\" required>\n <SelectField {...register('billingCycle')} options={billingCycleOptions} />\n </FormField>\n </FormRow>\n\n <FormField\n label=\"Seats\"\n required\n error={errors.seats?.message}\n description=\"Number of user seats included\"\n >\n <Input type=\"number\" {...register('seats')} placeholder=\"1\" min={1} max={10000} />\n </FormField>\n\n {isEditing && (\n <FormField label=\"Status\">\n <SelectField {...register('status')} options={statusOptions} />\n </FormField>\n )}\n </FormSection>\n\n {/* Dates */}\n <FormSection title=\"Dates\">\n <FormRow>\n <FormField label=\"Start Date\" error={errors.startDate?.message}>\n <Input type=\"date\" {...register('startDate')} />\n </FormField>\n\n <FormField\n label=\"Trial End Date\"\n error={errors.trialEndDate?.message}\n description=\"Leave empty for no trial\"\n >\n <Input type=\"date\" {...register('trialEndDate')} />\n </FormField>\n </FormRow>\n </FormSection>\n\n {/* Cost Summary */}\n {plan !== 'Custom' && monthlyPrice > 0 && (\n <FormSection title=\"Cost Summary\">\n <div className=\"rounded-lg border border-border-subtle bg-bg-sunken/30 p-4\">\n <div className=\"space-y-2\">\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-text-secondary\">Base Price</span>\n <span>${monthlyPrice}/user/month</span>\n </div>\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-text-secondary\">Seats</span>\n <span>× {seats}</span>\n </div>\n {billingCycle === 'yearly' && (\n <div className=\"flex justify-between text-sm text-success\">\n <span>Annual Discount</span>\n <span>-20%</span>\n </div>\n )}\n <div className=\"border-t border-border-subtle pt-2 mt-2\">\n <div className=\"flex justify-between font-medium\">\n <span>{billingCycle === 'yearly' ? 'Annual Total' : 'Monthly Total'}</span>\n <span className=\"text-lg\">\n ${totalPerPeriod.toFixed(2)}\n <span className=\"text-sm font-normal text-text-secondary\">\n /{billingCycle === 'yearly' ? 'year' : 'month'}\n </span>\n </span>\n </div>\n </div>\n </div>\n </div>\n </FormSection>\n )}\n </form>\n </FormSheet>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SubscriptionForm.js","names":[],"sources":["../../../../src/admin/components/forms/SubscriptionForm.tsx"],"sourcesContent":["import * as React from 'react';\nimport { useForm } from 'react-hook-form';\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport { z } from 'zod';\nimport { FormSheet, FormSheetFooter, FormSection, FormRow } from '../FormSheet';\nimport { Input } from '../ui';\nimport { cn } from '../../utils/cn';\nimport type { Subscription } from '../../types';\n\n/**\n * Form field wrapper with label and error\n */\ninterface FormFieldProps {\n label: string;\n error?: string;\n required?: boolean;\n children: React.ReactNode;\n description?: string;\n}\n\nfunction FormField({ label, error, required, children, description }: FormFieldProps) {\n return (\n <div className=\"space-y-1.5\">\n <label className=\"text-sm font-medium text-text-primary\">\n {label}\n {required && <span className=\"text-status-error-text ml-0.5\">*</span>}\n </label>\n {children}\n {description && !error && <p className=\"text-xs text-text-secondary\">{description}</p>}\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n </div>\n );\n}\n\n/**\n * Select wrapper styled like Input\n */\ninterface SelectFieldProps extends React.SelectHTMLAttributes<HTMLSelectElement> {\n options: { value: string; label: string }[];\n placeholder?: string;\n}\n\nconst SelectField = React.forwardRef<HTMLSelectElement, SelectFieldProps>(\n ({ className, options, placeholder, ...props }, ref) => (\n <select\n ref={ref}\n className={cn(\n 'flex h-9 w-full rounded-md border border-border-default bg-transparent px-3 py-1 text-sm shadow-sm transition-colors',\n 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)]',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n className\n )}\n {...props}\n >\n {placeholder && (\n <option value=\"\" disabled>\n {placeholder}\n </option>\n )}\n {options.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n )\n);\nSelectField.displayName = 'SelectField';\n\n// Unified form schema (handles both create and edit)\nconst subscriptionFormSchema = z.object({\n tenantId: z.string().min(1, 'Tenant is required'),\n plan: z.enum(['Free', 'Starter', 'Professional', 'Enterprise', 'Custom']),\n billingCycle: z.enum(['monthly', 'yearly']),\n seats: z.coerce.number().int().min(1, 'At least 1 seat is required'),\n status: z.enum(['active', 'cancelled', 'past_due', 'trialing', 'paused']).optional(),\n startDate: z.string().optional(),\n trialEndDate: z.string().optional(),\n});\n\ntype SubscriptionFormData = z.infer<typeof subscriptionFormSchema>;\n\n/**\n * SubscriptionForm props\n */\nexport interface SubscriptionFormProps {\n /** Whether the form is open */\n open: boolean;\n /** Callback when the form should close */\n onClose: () => void;\n /** Existing subscription data for editing (undefined for create) */\n subscription?: Subscription;\n /** Pre-selected tenant ID */\n tenantId?: string;\n /** List of available tenants for selection */\n tenants?: { id: string; name: string }[];\n /** Callback on successful save */\n onSave: (data: SubscriptionFormData & { id?: string }) => Promise<void>;\n}\n\nconst planOptions = [\n { value: 'Free', label: 'Free - $0/mo' },\n { value: 'Starter', label: 'Starter - $29/mo' },\n { value: 'Professional', label: 'Professional - $99/mo' },\n { value: 'Enterprise', label: 'Enterprise - $299/mo' },\n { value: 'Custom', label: 'Custom Pricing' },\n];\n\nconst billingCycleOptions = [\n { value: 'monthly', label: 'Monthly' },\n { value: 'yearly', label: 'Yearly (Save 20%)' },\n];\n\nconst statusOptions = [\n { value: 'active', label: 'Active' },\n { value: 'cancelled', label: 'Cancelled' },\n { value: 'past_due', label: 'Past Due' },\n { value: 'trialing', label: 'Trialing' },\n { value: 'paused', label: 'Paused' },\n];\n\n/**\n * SubscriptionForm - Create/Edit subscription form in a slide-over panel\n */\nexport function SubscriptionForm({\n open,\n onClose,\n subscription,\n tenantId,\n tenants = [],\n onSave,\n}: SubscriptionFormProps) {\n const isEditing = !!subscription;\n const formId = 'subscription-form';\n\n const defaultValues: SubscriptionFormData = {\n tenantId: subscription?.tenantId ?? tenantId ?? '',\n plan: (subscription?.plan ?? 'Starter') as SubscriptionFormData['plan'],\n billingCycle: (subscription?.billingCycle ?? 'monthly') as SubscriptionFormData['billingCycle'],\n seats: subscription?.seats ?? 1,\n status: subscription?.status as SubscriptionFormData['status'],\n startDate: subscription?.startDate\n ? new Date(subscription.startDate).toISOString().split('T')[0]\n : new Date().toISOString().split('T')[0],\n trialEndDate: subscription?.trialEndDate\n ? new Date(subscription.trialEndDate).toISOString().split('T')[0]\n : undefined,\n };\n\n const {\n register,\n handleSubmit,\n formState: { errors, isSubmitting },\n reset,\n watch,\n } = useForm<SubscriptionFormData>({\n resolver: zodResolver(subscriptionFormSchema),\n defaultValues,\n });\n\n // Reset form when subscription or tenantId changes\n React.useEffect(() => {\n if (open) {\n reset(defaultValues);\n }\n }, [open, subscription?.id, tenantId]);\n\n const onSubmit = async (data: SubscriptionFormData) => {\n try {\n await onSave({ ...data, id: subscription?.id });\n onClose();\n } catch (error) {\n console.error('Failed to save subscription:', error);\n }\n };\n\n // Calculate estimated cost\n const plan = watch('plan');\n const billingCycle = watch('billingCycle');\n const seats = watch('seats') || 1;\n\n const planPrices: Record<string, number> = {\n Free: 0,\n Starter: 29,\n Professional: 99,\n Enterprise: 299,\n Custom: 0,\n };\n\n const monthlyPrice = planPrices[plan] || 0;\n const yearlyMultiplier = billingCycle === 'yearly' ? 0.8 : 1; // 20% discount for yearly\n const estimatedCost = monthlyPrice * seats * yearlyMultiplier;\n const billingPeriod = billingCycle === 'yearly' ? 12 : 1;\n const totalPerPeriod = estimatedCost * billingPeriod;\n\n return (\n <FormSheet\n open={open}\n onClose={onClose}\n title={isEditing ? 'Edit Subscription' : 'Create Subscription'}\n description={\n isEditing ? 'Modify subscription details' : 'Set up a new subscription for a tenant'\n }\n width=\"md\"\n footer={\n <FormSheetFooter\n formId={formId}\n submitLabel={isEditing ? 'Save Changes' : 'Create Subscription'}\n onCancel={onClose}\n isSubmitting={isSubmitting}\n />\n }\n >\n <form id={formId} onSubmit={handleSubmit(onSubmit)} className=\"space-y-6\">\n {/* Tenant Selection */}\n {tenants.length > 0 && !tenantId && (\n <FormSection>\n <FormField\n label=\"Tenant\"\n required\n error={errors.tenantId?.message}\n description=\"Select the tenant for this subscription\"\n >\n <SelectField\n {...register('tenantId')}\n options={tenants.map((t) => ({ value: t.id, label: t.name }))}\n placeholder=\"Select a tenant...\"\n />\n </FormField>\n </FormSection>\n )}\n\n {/* Plan Selection */}\n <FormSection title=\"Plan Details\">\n <FormRow>\n <FormField label=\"Plan\" required>\n <SelectField {...register('plan')} options={planOptions} />\n </FormField>\n\n <FormField label=\"Billing Cycle\" required>\n <SelectField {...register('billingCycle')} options={billingCycleOptions} />\n </FormField>\n </FormRow>\n\n <FormField\n label=\"Seats\"\n required\n error={errors.seats?.message}\n description=\"Number of user seats included\"\n >\n <Input type=\"number\" {...register('seats')} placeholder=\"1\" min={1} max={10000} />\n </FormField>\n\n {isEditing && (\n <FormField label=\"Status\">\n <SelectField {...register('status')} options={statusOptions} />\n </FormField>\n )}\n </FormSection>\n\n {/* Dates */}\n <FormSection title=\"Dates\">\n <FormRow>\n <FormField label=\"Start Date\" error={errors.startDate?.message}>\n <Input type=\"date\" {...register('startDate')} />\n </FormField>\n\n <FormField\n label=\"Trial End Date\"\n error={errors.trialEndDate?.message}\n description=\"Leave empty for no trial\"\n >\n <Input type=\"date\" {...register('trialEndDate')} />\n </FormField>\n </FormRow>\n </FormSection>\n\n {/* Cost Summary */}\n {plan !== 'Custom' && monthlyPrice > 0 && (\n <FormSection title=\"Cost Summary\">\n <div className=\"rounded-lg border border-border-subtle bg-bg-sunken/30 p-4\">\n <div className=\"space-y-2\">\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-text-secondary\">Base Price</span>\n <span>${monthlyPrice}/user/month</span>\n </div>\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-text-secondary\">Seats</span>\n <span>× {seats}</span>\n </div>\n {billingCycle === 'yearly' && (\n <div className=\"flex justify-between text-sm text-status-success-text\">\n <span>Annual Discount</span>\n <span>-20%</span>\n </div>\n )}\n <div className=\"border-t border-border-subtle pt-2 mt-2\">\n <div className=\"flex justify-between font-medium\">\n <span>{billingCycle === 'yearly' ? 'Annual Total' : 'Monthly Total'}</span>\n <span className=\"text-lg\">\n ${totalPerPeriod.toFixed(2)}\n <span className=\"text-sm font-normal text-text-secondary\">\n /{billingCycle === 'yearly' ? 'year' : 'month'}\n </span>\n </span>\n </div>\n </div>\n </div>\n </div>\n </FormSection>\n )}\n </form>\n </FormSheet>\n );\n}\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { cn as e } from "../../utils/cn.js";
|
|
|
2
2
|
import { Input as t } from "../ui.js";
|
|
3
3
|
import { FormRow as n, FormSection as r, FormSheet as i, FormSheetFooter as a } from "../FormSheet.js";
|
|
4
4
|
import { Controller as o, useForm as s } from "../../../node_modules/react-hook-form/dist/index.esm.js";
|
|
5
|
-
import {
|
|
5
|
+
import { u as c } from "../../../node_modules/@hookform/resolvers/zod/dist/zod.js";
|
|
6
6
|
import { booleanType as l, coerce as u, literalType as d, objectType as f, stringType as p } from "../../../node_modules/zod/v3/types.js";
|
|
7
7
|
import { tWithFallback as m } from "../../utils/i18n.js";
|
|
8
8
|
import * as h from "react";
|
|
@@ -2,7 +2,7 @@ import { cn as e } from "../../utils/cn.js";
|
|
|
2
2
|
import { Input as t } from "../ui.js";
|
|
3
3
|
import { FormRow as n, FormSection as r, FormSheet as i, FormSheetFooter as a } from "../FormSheet.js";
|
|
4
4
|
import { Controller as o, useForm as s } from "../../../node_modules/react-hook-form/dist/index.esm.js";
|
|
5
|
-
import {
|
|
5
|
+
import { u as c } from "../../../node_modules/@hookform/resolvers/zod/dist/zod.js";
|
|
6
6
|
import { booleanType as l, enumType as u, objectType as d, stringType as f } from "../../../node_modules/zod/v3/types.js";
|
|
7
7
|
import * as p from "react";
|
|
8
8
|
import { jsx as m, jsxs as h } from "react/jsx-runtime";
|