@burdenoff/microfe-admin 2026.608.1 → 2026.624.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/AdminRoutes.js +14 -10
- package/dist/admin/AdminRoutes.js.map +1 -1
- package/dist/admin/components/AccessDenied.js +2 -2
- package/dist/admin/components/AccessDenied.js.map +1 -1
- package/dist/admin/components/ActivityFeed.js +4 -4
- package/dist/admin/components/ActivityFeed.js.map +1 -1
- package/dist/admin/components/Charts.js +4 -4
- package/dist/admin/components/Charts.js.map +1 -1
- package/dist/admin/components/ConfirmDialog.js +44 -25
- package/dist/admin/components/ConfirmDialog.js.map +1 -1
- package/dist/admin/components/DataTable.js +7 -7
- package/dist/admin/components/DataTable.js.map +1 -1
- package/dist/admin/components/FormSheet.js +7 -7
- package/dist/admin/components/FormSheet.js.map +1 -1
- package/dist/admin/components/MetricCard.js +4 -4
- package/dist/admin/components/MetricCard.js.map +1 -1
- package/dist/admin/components/PageLayout.js +3 -3
- package/dist/admin/components/PageLayout.js.map +1 -1
- package/dist/admin/components/PermissionGuard.js +1 -1
- package/dist/admin/components/PermissionGuard.js.map +1 -1
- package/dist/admin/components/RowActions.js +2 -2
- package/dist/admin/components/RowActions.js.map +1 -1
- package/dist/admin/components/TenantMembersPanel.js +15 -15
- package/dist/admin/components/TenantMembersPanel.js.map +1 -1
- package/dist/admin/components/TenantSsoPanel.js +14 -14
- package/dist/admin/components/TenantSsoPanel.js.map +1 -1
- package/dist/admin/components/Toast.js +9 -9
- package/dist/admin/components/Toast.js.map +1 -1
- package/dist/admin/components/forms/SubscriptionForm.js +10 -10
- package/dist/admin/components/forms/SubscriptionForm.js.map +1 -1
- package/dist/admin/components/forms/TenantForm.js +8 -8
- package/dist/admin/components/forms/TenantForm.js.map +1 -1
- package/dist/admin/components/forms/TicketForm.js +2 -2
- package/dist/admin/components/forms/TicketForm.js.map +1 -1
- package/dist/admin/components/forms/UserForm.js +8 -8
- package/dist/admin/components/forms/UserForm.js.map +1 -1
- package/dist/admin/components/ui.js +16 -16
- package/dist/admin/components/ui.js.map +1 -1
- package/dist/admin/constants/index.js +8 -8
- package/dist/admin/constants/index.js.map +1 -1
- package/dist/admin/pages/AuditLogsPage.js +8 -8
- package/dist/admin/pages/AuditLogsPage.js.map +1 -1
- package/dist/admin/pages/CommunicationsPage.js +2 -2
- package/dist/admin/pages/CommunicationsPage.js.map +1 -1
- package/dist/admin/pages/ContentPagesPage.js +661 -0
- package/dist/admin/pages/ContentPagesPage.js.map +1 -0
- package/dist/admin/pages/FeatureFlagsPage.js +4 -4
- package/dist/admin/pages/FeatureFlagsPage.js.map +1 -1
- package/dist/admin/pages/OrganizationsListPage.js +6 -6
- package/dist/admin/pages/OrganizationsListPage.js.map +1 -1
- package/dist/admin/pages/ProductsPage.js +7 -7
- package/dist/admin/pages/ProductsPage.js.map +1 -1
- package/dist/admin/pages/ReferralsPage.js +23 -23
- package/dist/admin/pages/ReferralsPage.js.map +1 -1
- package/dist/admin/pages/RolesPermissionsPage.js +8 -8
- package/dist/admin/pages/RolesPermissionsPage.js.map +1 -1
- package/dist/admin/pages/SREPage.js +8 -8
- package/dist/admin/pages/SREPage.js.map +1 -1
- package/dist/admin/pages/SandboxSettingsPage.js +20 -20
- package/dist/admin/pages/SandboxSettingsPage.js.map +1 -1
- package/dist/admin/pages/SettingsPage.js +20 -20
- package/dist/admin/pages/SettingsPage.js.map +1 -1
- package/dist/admin/pages/SyncFailuresPage.js +11 -11
- package/dist/admin/pages/SyncFailuresPage.js.map +1 -1
- package/dist/admin/pages/TenantDetailPage.js +22 -22
- package/dist/admin/pages/TenantDetailPage.js.map +1 -1
- package/dist/admin/pages/TenantFormRoutePage.js +2 -2
- package/dist/admin/pages/TenantFormRoutePage.js.map +1 -1
- package/dist/admin/pages/TenantsListPage.js +5 -5
- package/dist/admin/pages/TenantsListPage.js.map +1 -1
- package/dist/admin/pages/UsersListPage.js +6 -6
- package/dist/admin/pages/UsersListPage.js.map +1 -1
- package/dist/admin/pages/WaitlistPage.js +7 -7
- package/dist/admin/pages/WaitlistPage.js.map +1 -1
- package/dist/admin/pages/WorkflowsPage.js +3 -3
- package/dist/admin/pages/WorkflowsPage.js.map +1 -1
- package/dist/admin/pages/WorkspacesListPage.js +8 -8
- package/dist/admin/pages/WorkspacesListPage.js.map +1 -1
- package/dist/admin/pages/index.js +1 -0
- package/package.json +1 -1
|
@@ -25,19 +25,19 @@ function c({ open: r, onClose: c, title: l, description: u, children: d, width:
|
|
|
25
25
|
onClick: c,
|
|
26
26
|
"aria-hidden": "true"
|
|
27
27
|
}), /* @__PURE__ */ o("div", {
|
|
28
|
-
className: e("fixed inset-y-0 right-0 flex w-full flex-col bg-
|
|
28
|
+
className: e("fixed inset-y-0 right-0 flex w-full flex-col bg-bg-surface shadow-xl", "animate-in slide-in-from-right duration-300", s[f]),
|
|
29
29
|
role: "dialog",
|
|
30
30
|
"aria-modal": "true",
|
|
31
31
|
"aria-labelledby": "sheet-title",
|
|
32
32
|
children: [
|
|
33
33
|
/* @__PURE__ */ o("div", {
|
|
34
|
-
className: "flex items-start justify-between border-b border-border px-6 py-4",
|
|
34
|
+
className: "flex items-start justify-between border-b border-border-subtle px-6 py-4",
|
|
35
35
|
children: [/* @__PURE__ */ o("div", { children: [/* @__PURE__ */ a("h2", {
|
|
36
36
|
id: "sheet-title",
|
|
37
|
-
className: "text-lg font-semibold text-
|
|
37
|
+
className: "text-lg font-semibold text-text-primary",
|
|
38
38
|
children: l
|
|
39
39
|
}), u && /* @__PURE__ */ a("p", {
|
|
40
|
-
className: "mt-1 text-sm text-
|
|
40
|
+
className: "mt-1 text-sm text-text-secondary",
|
|
41
41
|
children: u
|
|
42
42
|
})] }), /* @__PURE__ */ a(t, {
|
|
43
43
|
variant: "ghost",
|
|
@@ -52,7 +52,7 @@ function c({ open: r, onClose: c, title: l, description: u, children: d, width:
|
|
|
52
52
|
children: d
|
|
53
53
|
}),
|
|
54
54
|
p && /* @__PURE__ */ a("div", {
|
|
55
|
-
className: "border-t border-border px-6 py-4",
|
|
55
|
+
className: "border-t border-border-subtle px-6 py-4",
|
|
56
56
|
children: p
|
|
57
57
|
})
|
|
58
58
|
]
|
|
@@ -83,10 +83,10 @@ function u({ title: t, description: n, children: r, className: i }) {
|
|
|
83
83
|
children: [(t || n) && /* @__PURE__ */ o("div", {
|
|
84
84
|
className: "space-y-1",
|
|
85
85
|
children: [t && /* @__PURE__ */ a("h3", {
|
|
86
|
-
className: "text-sm font-medium text-
|
|
86
|
+
className: "text-sm font-medium text-text-primary",
|
|
87
87
|
children: t
|
|
88
88
|
}), n && /* @__PURE__ */ a("p", {
|
|
89
|
-
className: "text-xs text-
|
|
89
|
+
className: "text-xs text-text-secondary",
|
|
90
90
|
children: n
|
|
91
91
|
})]
|
|
92
92
|
}), /* @__PURE__ */ a("div", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormSheet.js","names":[],"sources":["../../../src/admin/components/FormSheet.tsx"],"sourcesContent":["import * as React from 'react';\nimport { X, Loader2 } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { Button } from './ui';\n\n/**\n * FormSheet props\n */\nexport interface FormSheetProps {\n /** Whether the sheet is open */\n open: boolean;\n /** Callback when the sheet should close */\n onClose: () => void;\n /** Title of the sheet */\n title: string;\n /** Optional description */\n description?: string;\n /** Form content */\n children: React.ReactNode;\n /** Width of the sheet */\n width?: 'sm' | 'md' | 'lg' | 'xl';\n /** Optional footer actions */\n footer?: React.ReactNode;\n}\n\nconst widthClasses = {\n sm: 'max-w-sm',\n md: 'max-w-md',\n lg: 'max-w-lg',\n xl: 'max-w-xl',\n};\n\n/**\n * FormSheet - Slide-over panel for forms\n *\n * A side panel component for displaying forms that slides in from the right.\n * Includes backdrop overlay, header with close button, and optional footer.\n */\nexport function FormSheet({\n open,\n onClose,\n title,\n description,\n children,\n width = 'md',\n footer,\n}: FormSheetProps) {\n // Handle escape key\n React.useEffect(() => {\n const handleEscape = (e: KeyboardEvent) => {\n if (e.key === 'Escape' && open) {\n onClose();\n }\n };\n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [open, onClose]);\n\n // Prevent body scroll when open\n React.useEffect(() => {\n if (open) {\n document.body.style.overflow = 'hidden';\n } else {\n document.body.style.overflow = '';\n }\n return () => {\n document.body.style.overflow = '';\n };\n }, [open]);\n\n if (!open) return null;\n\n return (\n <div className=\"fixed inset-0 z-50\">\n {/* Backdrop */}\n <div\n className=\"fixed inset-0 bg-black/50 transition-opacity\"\n onClick={onClose}\n aria-hidden=\"true\"\n />\n\n {/* Sheet */}\n <div\n className={cn(\n 'fixed inset-y-0 right-0 flex w-full flex-col bg-
|
|
1
|
+
{"version":3,"file":"FormSheet.js","names":[],"sources":["../../../src/admin/components/FormSheet.tsx"],"sourcesContent":["import * as React from 'react';\nimport { X, Loader2 } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { Button } from './ui';\n\n/**\n * FormSheet props\n */\nexport interface FormSheetProps {\n /** Whether the sheet is open */\n open: boolean;\n /** Callback when the sheet should close */\n onClose: () => void;\n /** Title of the sheet */\n title: string;\n /** Optional description */\n description?: string;\n /** Form content */\n children: React.ReactNode;\n /** Width of the sheet */\n width?: 'sm' | 'md' | 'lg' | 'xl';\n /** Optional footer actions */\n footer?: React.ReactNode;\n}\n\nconst widthClasses = {\n sm: 'max-w-sm',\n md: 'max-w-md',\n lg: 'max-w-lg',\n xl: 'max-w-xl',\n};\n\n/**\n * FormSheet - Slide-over panel for forms\n *\n * A side panel component for displaying forms that slides in from the right.\n * Includes backdrop overlay, header with close button, and optional footer.\n */\nexport function FormSheet({\n open,\n onClose,\n title,\n description,\n children,\n width = 'md',\n footer,\n}: FormSheetProps) {\n // Handle escape key\n React.useEffect(() => {\n const handleEscape = (e: KeyboardEvent) => {\n if (e.key === 'Escape' && open) {\n onClose();\n }\n };\n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [open, onClose]);\n\n // Prevent body scroll when open\n React.useEffect(() => {\n if (open) {\n document.body.style.overflow = 'hidden';\n } else {\n document.body.style.overflow = '';\n }\n return () => {\n document.body.style.overflow = '';\n };\n }, [open]);\n\n if (!open) return null;\n\n return (\n <div className=\"fixed inset-0 z-50\">\n {/* Backdrop */}\n <div\n className=\"fixed inset-0 bg-black/50 transition-opacity\"\n onClick={onClose}\n aria-hidden=\"true\"\n />\n\n {/* Sheet */}\n <div\n className={cn(\n 'fixed inset-y-0 right-0 flex w-full flex-col bg-bg-surface shadow-xl',\n 'animate-in slide-in-from-right duration-300',\n widthClasses[width]\n )}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"sheet-title\"\n >\n {/* Header */}\n <div className=\"flex items-start justify-between border-b border-border-subtle px-6 py-4\">\n <div>\n <h2 id=\"sheet-title\" className=\"text-lg font-semibold text-text-primary\">\n {title}\n </h2>\n {description && <p className=\"mt-1 text-sm text-text-secondary\">{description}</p>}\n </div>\n <Button variant=\"ghost\" size=\"icon\" onClick={onClose} aria-label=\"Close\">\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n\n {/* Content */}\n <div className=\"flex-1 overflow-y-auto px-6 py-4\">{children}</div>\n\n {/* Footer */}\n {footer && <div className=\"border-t border-border-subtle px-6 py-4\">{footer}</div>}\n </div>\n </div>\n );\n}\n\n/**\n * FormSheetFooter props\n */\nexport interface FormSheetFooterProps {\n /** Primary action button text */\n submitLabel?: string;\n /** Cancel button text */\n cancelLabel?: string;\n /** Callback for cancel */\n onCancel: () => void;\n /** Whether form is submitting */\n isSubmitting?: boolean;\n /** Whether submit is disabled */\n disabled?: boolean;\n /** Form ID to connect submit button */\n formId?: string;\n /** Callback for submit (if not using form) */\n onSubmit?: () => void;\n /** Variant for submit button */\n submitVariant?: 'default' | 'destructive';\n}\n\n/**\n * FormSheetFooter - Standard footer for FormSheet with cancel/submit buttons\n */\nexport function FormSheetFooter({\n submitLabel = 'Save',\n cancelLabel = 'Cancel',\n onCancel,\n isSubmitting = false,\n disabled = false,\n formId,\n onSubmit,\n submitVariant = 'default',\n}: FormSheetFooterProps) {\n return (\n <div className=\"flex items-center justify-end gap-3\">\n <Button variant=\"outline\" onClick={onCancel} disabled={isSubmitting}>\n {cancelLabel}\n </Button>\n <Button\n type={formId ? 'submit' : 'button'}\n form={formId}\n onClick={onSubmit}\n disabled={disabled || isSubmitting}\n variant={submitVariant}\n >\n {isSubmitting && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\n {submitLabel}\n </Button>\n </div>\n );\n}\n\n/**\n * FormSection - Group related form fields\n */\nexport interface FormSectionProps {\n /** Section title */\n title?: string;\n /** Section description */\n description?: string;\n /** Form fields */\n children: React.ReactNode;\n /** Additional className */\n className?: string;\n}\n\nexport function FormSection({ title, description, children, className }: FormSectionProps) {\n return (\n <div className={cn('space-y-4', className)}>\n {(title || description) && (\n <div className=\"space-y-1\">\n {title && <h3 className=\"text-sm font-medium text-text-primary\">{title}</h3>}\n {description && <p className=\"text-xs text-text-secondary\">{description}</p>}\n </div>\n )}\n <div className=\"space-y-4\">{children}</div>\n </div>\n );\n}\n\n/**\n * FormRow - Horizontal row for form fields\n */\nexport interface FormRowProps {\n /** Form fields */\n children: React.ReactNode;\n /** Number of columns */\n cols?: 2 | 3 | 4;\n /** Additional className */\n className?: string;\n}\n\nexport function FormRow({ children, cols = 2, className }: FormRowProps) {\n const colsClass = {\n 2: 'grid-cols-2',\n 3: 'grid-cols-3',\n 4: 'grid-cols-4',\n };\n return <div className={cn('grid gap-4', colsClass[cols], className)}>{children}</div>;\n}\n"],"mappings":";;;;;;AAyBA,IAAM,IAAe;CACnB,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACL;AAQD,SAAgB,EAAU,EACxB,SACA,YACA,UACA,gBACA,aACA,WAAQ,MACR,aACiB;AA0BjB,QAxBA,EAAM,gBAAgB;EACpB,IAAM,KAAgB,MAAqB;AACzC,GAAI,EAAE,QAAQ,YAAY,KACxB,GAAS;;AAIb,SADA,SAAS,iBAAiB,WAAW,EAAa,QACrC,SAAS,oBAAoB,WAAW,EAAa;IACjE,CAAC,GAAM,EAAQ,CAAC,EAGnB,EAAM,iBACA,IACF,SAAS,KAAK,MAAM,WAAW,WAE/B,SAAS,KAAK,MAAM,WAAW,UAEpB;AACX,WAAS,KAAK,MAAM,WAAW;KAEhC,CAAC,EAAK,CAAC,EAEL,IAGH,kBAAC,OAAD;EAAK,WAAU;YAAf,CAEE,kBAAC,OAAD;GACE,WAAU;GACV,SAAS;GACT,eAAY;GACZ,CAAA,EAGF,kBAAC,OAAD;GACE,WAAW,EACT,wEACA,+CACA,EAAa,GACd;GACD,MAAK;GACL,cAAW;GACX,mBAAgB;aARlB;IAWE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;MAAI,IAAG;MAAc,WAAU;gBAC5B;MACE,CAAA,EACJ,KAAe,kBAAC,KAAD;MAAG,WAAU;gBAAoC;MAAgB,CAAA,CAC7E,EAAA,CAAA,EACN,kBAAC,GAAD;MAAQ,SAAQ;MAAQ,MAAK;MAAO,SAAS;MAAS,cAAW;gBAC/D,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;MAClB,CAAA,CACL;;IAGN,kBAAC,OAAD;KAAK,WAAU;KAAoC;KAAe,CAAA;IAGjE,KAAU,kBAAC,OAAD;KAAK,WAAU;eAA2C;KAAa,CAAA;IAC9E;KACF;MAzCU;;AAsEpB,SAAgB,EAAgB,EAC9B,iBAAc,QACd,iBAAc,UACd,aACA,kBAAe,IACf,cAAW,IACX,WACA,aACA,mBAAgB,aACO;AACvB,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD;GAAQ,SAAQ;GAAU,SAAS;GAAU,UAAU;aACpD;GACM,CAAA,EACT,kBAAC,GAAD;GACE,MAAM,IAAS,WAAW;GAC1B,MAAM;GACN,SAAS;GACT,UAAU,KAAY;GACtB,SAAS;aALX,CAOG,KAAgB,kBAAC,GAAD,EAAS,WAAU,6BAA8B,CAAA,EACjE,EACM;KACL;;;AAkBV,SAAgB,EAAY,EAAE,UAAO,gBAAa,aAAU,gBAA+B;AACzF,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,aAAa,EAAU;YAA1C,EACI,KAAS,MACT,kBAAC,OAAD;GAAK,WAAU;aAAf,CACG,KAAS,kBAAC,MAAD;IAAI,WAAU;cAAyC;IAAW,CAAA,EAC3E,KAAe,kBAAC,KAAD;IAAG,WAAU;cAA+B;IAAgB,CAAA,CACxE;MAER,kBAAC,OAAD;GAAK,WAAU;GAAa;GAAe,CAAA,CACvC;;;AAgBV,SAAgB,EAAQ,EAAE,aAAU,UAAO,GAAG,gBAA2B;AAMvE,QAAO,kBAAC,OAAD;EAAK,WAAW,EAAG,cALR;GAChB,GAAG;GACH,GAAG;GACH,GAAG;GACJ,CACiD,IAAO,EAAU;EAAG;EAAe,CAAA"}
|
|
@@ -15,10 +15,10 @@ function g({ title: g, value: _, change: v, changeType: y = "number", trend: b,
|
|
|
15
15
|
children: [/* @__PURE__ */ h(i, {
|
|
16
16
|
className: "flex flex-row items-center justify-between pb-2",
|
|
17
17
|
children: [/* @__PURE__ */ m(a, {
|
|
18
|
-
className: "text-sm font-medium text-
|
|
18
|
+
className: "text-sm font-medium text-text-secondary",
|
|
19
19
|
children: g
|
|
20
20
|
}), S && /* @__PURE__ */ m("div", {
|
|
21
|
-
className: "text-
|
|
21
|
+
className: "text-text-secondary",
|
|
22
22
|
children: S
|
|
23
23
|
})]
|
|
24
24
|
}), /* @__PURE__ */ m(r, { children: E ? /* @__PURE__ */ h("div", {
|
|
@@ -27,7 +27,7 @@ function g({ title: g, value: _, change: v, changeType: y = "number", trend: b,
|
|
|
27
27
|
}) : /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ h("div", {
|
|
28
28
|
className: "flex items-baseline justify-between",
|
|
29
29
|
children: [/* @__PURE__ */ m("div", {
|
|
30
|
-
className: "text-3xl font-bold text-
|
|
30
|
+
className: "text-3xl font-bold text-text-primary",
|
|
31
31
|
children: k
|
|
32
32
|
}), A !== null && /* @__PURE__ */ h(t, {
|
|
33
33
|
variant: O ? "success" : "destructive",
|
|
@@ -35,7 +35,7 @@ function g({ title: g, value: _, change: v, changeType: y = "number", trend: b,
|
|
|
35
35
|
children: [/* @__PURE__ */ m(j, { className: "h-3 w-3" }), A]
|
|
36
36
|
})]
|
|
37
37
|
}), C && /* @__PURE__ */ m("p", {
|
|
38
|
-
className: "mt-1 text-xs text-
|
|
38
|
+
className: "mt-1 text-xs text-text-secondary",
|
|
39
39
|
children: C
|
|
40
40
|
})] }) })]
|
|
41
41
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MetricCard.js","names":[],"sources":["../../../src/admin/components/MetricCard.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { Link } from 'react-router-dom';\nimport { TrendingUp, TrendingDown, Minus } from 'lucide-react';\nimport { Card, CardHeader, CardTitle, CardContent, Badge, Skeleton } from './ui';\nimport { cn } from '../utils/cn';\nimport { formatNumber, formatCurrency, formatChange } from '../utils/formatters';\n\nexport interface MetricCardProps {\n /** Card title */\n title: string;\n /** Metric value (number or formatted string) */\n value: number | string;\n /** Change from previous period */\n change?: number;\n /** Type of change display */\n changeType?: 'number' | 'percentage' | 'currency';\n /** Trend direction */\n trend?: 'up' | 'down' | 'neutral';\n /** Whether change is positive or negative for styling */\n changeIsPositive?: boolean;\n /** Icon to display */\n icon?: ReactNode;\n /** Subtitle text */\n subtitle?: string;\n /** Link to navigate to on click */\n linkTo?: string;\n /** Priority indicator */\n priority?: 'normal' | 'high' | 'critical';\n /** Loading state */\n loading?: boolean;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Metric card component for dashboard\n */\nexport function MetricCard({\n title,\n value,\n change,\n changeType = 'number',\n trend,\n changeIsPositive,\n icon,\n subtitle,\n linkTo,\n priority,\n loading,\n className,\n}: MetricCardProps) {\n // Determine if change is positive based on trend or explicit prop\n const isPositive = changeIsPositive ?? trend === 'up';\n\n // Format the value\n const formattedValue =\n typeof value === 'number'\n ? changeType === 'currency'\n ? formatCurrency(value)\n : formatNumber(value)\n : value;\n\n // Format the change\n const formattedChange =\n change !== undefined ? formatChange(change, changeType === 'percentage') : null;\n\n // Get trend icon\n const TrendIcon = trend === 'up' ? TrendingUp : trend === 'down' ? TrendingDown : Minus;\n\n // Priority border classes\n const priorityClasses = {\n normal: '',\n high: 'border-l-4 border-l-warning',\n critical: 'border-l-4 border-l-destructive',\n };\n\n const cardContent = (\n <Card\n className={cn(\n 'transition-shadow hover:shadow-md',\n priority && priorityClasses[priority],\n className\n )}\n >\n <CardHeader className=\"flex flex-row items-center justify-between pb-2\">\n <CardTitle className=\"text-sm font-medium text-
|
|
1
|
+
{"version":3,"file":"MetricCard.js","names":[],"sources":["../../../src/admin/components/MetricCard.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { Link } from 'react-router-dom';\nimport { TrendingUp, TrendingDown, Minus } from 'lucide-react';\nimport { Card, CardHeader, CardTitle, CardContent, Badge, Skeleton } from './ui';\nimport { cn } from '../utils/cn';\nimport { formatNumber, formatCurrency, formatChange } from '../utils/formatters';\n\nexport interface MetricCardProps {\n /** Card title */\n title: string;\n /** Metric value (number or formatted string) */\n value: number | string;\n /** Change from previous period */\n change?: number;\n /** Type of change display */\n changeType?: 'number' | 'percentage' | 'currency';\n /** Trend direction */\n trend?: 'up' | 'down' | 'neutral';\n /** Whether change is positive or negative for styling */\n changeIsPositive?: boolean;\n /** Icon to display */\n icon?: ReactNode;\n /** Subtitle text */\n subtitle?: string;\n /** Link to navigate to on click */\n linkTo?: string;\n /** Priority indicator */\n priority?: 'normal' | 'high' | 'critical';\n /** Loading state */\n loading?: boolean;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Metric card component for dashboard\n */\nexport function MetricCard({\n title,\n value,\n change,\n changeType = 'number',\n trend,\n changeIsPositive,\n icon,\n subtitle,\n linkTo,\n priority,\n loading,\n className,\n}: MetricCardProps) {\n // Determine if change is positive based on trend or explicit prop\n const isPositive = changeIsPositive ?? trend === 'up';\n\n // Format the value\n const formattedValue =\n typeof value === 'number'\n ? changeType === 'currency'\n ? formatCurrency(value)\n : formatNumber(value)\n : value;\n\n // Format the change\n const formattedChange =\n change !== undefined ? formatChange(change, changeType === 'percentage') : null;\n\n // Get trend icon\n const TrendIcon = trend === 'up' ? TrendingUp : trend === 'down' ? TrendingDown : Minus;\n\n // Priority border classes\n const priorityClasses = {\n normal: '',\n high: 'border-l-4 border-l-warning',\n critical: 'border-l-4 border-l-destructive',\n };\n\n const cardContent = (\n <Card\n className={cn(\n 'transition-shadow hover:shadow-md',\n priority && priorityClasses[priority],\n className\n )}\n >\n <CardHeader className=\"flex flex-row items-center justify-between pb-2\">\n <CardTitle className=\"text-sm font-medium text-text-secondary\">{title}</CardTitle>\n {icon && <div className=\"text-text-secondary\">{icon}</div>}\n </CardHeader>\n <CardContent>\n {loading ? (\n <div className=\"space-y-2\">\n <Skeleton className=\"h-8 w-24\" />\n <Skeleton className=\"h-4 w-16\" />\n </div>\n ) : (\n <div>\n <div className=\"flex items-baseline justify-between\">\n <div className=\"text-3xl font-bold text-text-primary\">{formattedValue}</div>\n {formattedChange !== null && (\n <Badge\n variant={isPositive ? 'success' : 'destructive'}\n className=\"flex items-center gap-1\"\n >\n <TrendIcon className=\"h-3 w-3\" />\n {formattedChange}\n </Badge>\n )}\n </div>\n {subtitle && <p className=\"mt-1 text-xs text-text-secondary\">{subtitle}</p>}\n </div>\n )}\n </CardContent>\n </Card>\n );\n\n if (linkTo) {\n return (\n <Link to={linkTo} className=\"block\">\n {cardContent}\n </Link>\n );\n }\n\n return cardContent;\n}\n\n/**\n * Grid container for metric cards\n */\nexport function MetricsGrid({ children, className }: { children: ReactNode; className?: string }) {\n return (\n <div className={cn('grid gap-4', 'grid-cols-1 sm:grid-cols-2 xl:grid-cols-4', className)}>\n {children}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAqCA,SAAgB,EAAW,EACzB,UACA,UACA,WACA,gBAAa,UACb,UACA,qBACA,SACA,aACA,WACA,aACA,YACA,gBACkB;CAElB,IAAM,IAAa,KAAoB,MAAU,MAG3C,IACJ,OAAO,KAAU,WACb,MAAe,aACb,EAAe,EAAM,GACrB,EAAa,EAAM,GACrB,GAGA,IACJ,MAAW,KAAA,IAAgE,OAApD,EAAa,GAAQ,MAAe,aAAa,EAGpE,IAAY,MAAU,OAAO,IAAa,MAAU,SAAS,IAAe,GAS5E,IACJ,kBAAC,GAAD;EACE,WAAW,EACT,qCACA,KAVkB;GACtB,QAAQ;GACR,MAAM;GACN,UAAU;GACX,CAMiC,IAC5B,EACD;YALH,CAOE,kBAAC,GAAD;GAAY,WAAU;aAAtB,CACE,kBAAC,GAAD;IAAW,WAAU;cAA2C;IAAkB,CAAA,EACjF,KAAQ,kBAAC,OAAD;IAAK,WAAU;cAAuB;IAAW,CAAA,CAC/C;MACb,kBAAC,GAAD,EAAA,UACG,IACC,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAU,WAAU,YAAa,CAAA,EACjC,kBAAC,GAAD,EAAU,WAAU,YAAa,CAAA,CAC7B;OAEN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAwC;IAAqB,CAAA,EAC3E,MAAoB,QACnB,kBAAC,GAAD;IACE,SAAS,IAAa,YAAY;IAClC,WAAU;cAFZ,CAIE,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAChC,EACK;MAEN;MACL,KAAY,kBAAC,KAAD;GAAG,WAAU;aAAoC;GAAa,CAAA,CACvE,EAAA,CAAA,EAEI,CAAA,CACT;;AAWT,QARI,IAEA,kBAAC,GAAD;EAAM,IAAI;EAAQ,WAAU;YACzB;EACI,CAAA,GAIJ;;AAMT,SAAgB,EAAY,EAAE,aAAU,gBAA0D;AAChG,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,cAAc,6CAA6C,EAAU;EACrF;EACG,CAAA"}
|
|
@@ -5,14 +5,14 @@ function r({ title: r, description: i, actions: a, children: o, className: s })
|
|
|
5
5
|
return /* @__PURE__ */ n("div", {
|
|
6
6
|
className: e("flex h-full flex-col", s),
|
|
7
7
|
children: [/* @__PURE__ */ n("header", {
|
|
8
|
-
className: "flex flex-col gap-4 border-b border-border bg-
|
|
8
|
+
className: "flex flex-col gap-4 border-b border-border-subtle bg-bg-surface px-6 py-4 sm:flex-row sm:items-center sm:justify-between",
|
|
9
9
|
children: [/* @__PURE__ */ n("div", {
|
|
10
10
|
className: "min-w-0 flex-1",
|
|
11
11
|
children: [/* @__PURE__ */ t("h1", {
|
|
12
|
-
className: "text-2xl font-bold tracking-tight text-
|
|
12
|
+
className: "text-2xl font-bold tracking-tight text-text-primary sm:text-3xl",
|
|
13
13
|
children: r
|
|
14
14
|
}), i && /* @__PURE__ */ t("p", {
|
|
15
|
-
className: "mt-1 text-sm text-
|
|
15
|
+
className: "mt-1 text-sm text-text-secondary sm:text-base",
|
|
16
16
|
children: i
|
|
17
17
|
})]
|
|
18
18
|
}), a && /* @__PURE__ */ t("div", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/admin/components/PageLayout.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface PageLayoutProps {\n /** Page title */\n title: string;\n /** Optional description */\n description?: string;\n /** Actions to display in the header (buttons, etc.) */\n actions?: ReactNode;\n /** Page content */\n children: ReactNode;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Standard page layout component with title, description, and content area\n */\nexport function PageLayout({ title, description, actions, children, className }: PageLayoutProps) {\n return (\n <div className={cn('flex h-full flex-col', className)}>\n {/* Page Header */}\n <header className=\"flex flex-col gap-4 border-b border-border bg-
|
|
1
|
+
{"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/admin/components/PageLayout.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface PageLayoutProps {\n /** Page title */\n title: string;\n /** Optional description */\n description?: string;\n /** Actions to display in the header (buttons, etc.) */\n actions?: ReactNode;\n /** Page content */\n children: ReactNode;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Standard page layout component with title, description, and content area\n */\nexport function PageLayout({ title, description, actions, children, className }: PageLayoutProps) {\n return (\n <div className={cn('flex h-full flex-col', className)}>\n {/* Page Header */}\n <header className=\"flex flex-col gap-4 border-b border-border-subtle bg-bg-surface px-6 py-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"min-w-0 flex-1\">\n <h1 className=\"text-2xl font-bold tracking-tight text-text-primary sm:text-3xl\">{title}</h1>\n {description && (\n <p className=\"mt-1 text-sm text-text-secondary sm:text-base\">{description}</p>\n )}\n </div>\n {actions && (\n <div className=\"flex flex-shrink-0 flex-wrap items-center gap-2\">{actions}</div>\n )}\n </header>\n\n {/* Page Content — must not use a <main> landmark here because the\n host shell (AppShellLayout) already renders one; nesting <main>\n elements is invalid HTML and an a11y violation. */}\n <div className=\"flex-1 overflow-auto p-6\">{children}</div>\n </div>\n );\n}\n\ninterface PageSectionProps {\n /** Section title */\n title?: string;\n /** Optional description */\n description?: string;\n /** Section content */\n children: ReactNode;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Section within a page\n */\nexport function PageSection({ title, description, children, className }: PageSectionProps) {\n return (\n <section className={cn('space-y-4', className)}>\n {(title || description) && (\n <div>\n {title && <h2 className=\"text-lg font-semibold text-text-primary\">{title}</h2>}\n {description && <p className=\"text-sm text-text-secondary\">{description}</p>}\n </div>\n )}\n {children}\n </section>\n );\n}\n"],"mappings":";;;AAmBA,SAAgB,EAAW,EAAE,UAAO,gBAAa,YAAS,aAAU,gBAA8B;AAChG,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,wBAAwB,EAAU;YAArD,CAEE,kBAAC,UAAD;GAAQ,WAAU;aAAlB,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eAAmE;KAAW,CAAA,EAC3F,KACC,kBAAC,KAAD;KAAG,WAAU;eAAiD;KAAgB,CAAA,CAE5E;OACL,KACC,kBAAC,OAAD;IAAK,WAAU;cAAmD;IAAc,CAAA,CAE3E;MAKT,kBAAC,OAAD;GAAK,WAAU;GAA4B;GAAe,CAAA,CACtD"}
|
|
@@ -5,7 +5,7 @@ import { Fragment as n, jsx as r } from "react/jsx-runtime";
|
|
|
5
5
|
var i = ({ allowed: i, isLoading: a, pageTitle: o, denyMessage: s, children: c }) => a ? /* @__PURE__ */ r(e, {
|
|
6
6
|
title: o,
|
|
7
7
|
children: /* @__PURE__ */ r("div", {
|
|
8
|
-
className: "text-center py-16 text-
|
|
8
|
+
className: "text-center py-16 text-text-secondary",
|
|
9
9
|
children: "Loading permissions..."
|
|
10
10
|
})
|
|
11
11
|
}) : i ? /* @__PURE__ */ r(n, { children: c }) : /* @__PURE__ */ r(e, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PermissionGuard.js","names":[],"sources":["../../../src/admin/components/PermissionGuard.tsx"],"sourcesContent":["import type { FC, ReactNode } from 'react';\nimport { PageLayout } from './PageLayout';\nimport { AccessDenied } from './AccessDenied';\n\ninterface PermissionGuardProps {\n allowed: boolean;\n isLoading: boolean;\n pageTitle: string;\n denyMessage?: string;\n children: ReactNode;\n}\n\nexport const PermissionGuard: FC<PermissionGuardProps> = ({\n allowed,\n isLoading,\n pageTitle,\n denyMessage,\n children,\n}) => {\n if (isLoading)\n return (\n <PageLayout title={pageTitle}>\n <div className=\"text-center py-16 text-
|
|
1
|
+
{"version":3,"file":"PermissionGuard.js","names":[],"sources":["../../../src/admin/components/PermissionGuard.tsx"],"sourcesContent":["import type { FC, ReactNode } from 'react';\nimport { PageLayout } from './PageLayout';\nimport { AccessDenied } from './AccessDenied';\n\ninterface PermissionGuardProps {\n allowed: boolean;\n isLoading: boolean;\n pageTitle: string;\n denyMessage?: string;\n children: ReactNode;\n}\n\nexport const PermissionGuard: FC<PermissionGuardProps> = ({\n allowed,\n isLoading,\n pageTitle,\n denyMessage,\n children,\n}) => {\n if (isLoading)\n return (\n <PageLayout title={pageTitle}>\n <div className=\"text-center py-16 text-text-secondary\">Loading permissions...</div>\n </PageLayout>\n );\n if (!allowed)\n return (\n <PageLayout title={pageTitle}>\n <AccessDenied message={denyMessage} />\n </PageLayout>\n );\n return <>{children}</>;\n};\n"],"mappings":";;;;AAYA,IAAa,KAA6C,EACxD,YACA,cACA,cACA,gBACA,kBAEI,IAEA,kBAAC,GAAD;CAAY,OAAO;WACjB,kBAAC,OAAD;EAAK,WAAU;YAAwC;EAA4B,CAAA;CACxE,CAAA,GAEZ,IAME,kBAAA,GAAA,EAAG,aAAY,CAAA,GAJlB,kBAAC,GAAD;CAAY,OAAO;WACjB,kBAAC,GAAD,EAAc,SAAS,GAAe,CAAA;CAC3B,CAAA"}
|
|
@@ -38,7 +38,7 @@ function p({ actions: a, ariaLabel: o = "Row actions", stopPropagation: s = !0 }
|
|
|
38
38
|
children: /* @__PURE__ */ d(c, { className: "h-4 w-4" })
|
|
39
39
|
}), l && /* @__PURE__ */ d("div", {
|
|
40
40
|
ref: m,
|
|
41
|
-
className: e("absolute right-0 top-full z-50 mt-1 min-w-[160px] rounded-md border border-border bg-popover p-1 shadow-lg", "animate-in fade-in-0 zoom-in-95 duration-100"),
|
|
41
|
+
className: e("absolute right-0 top-full z-50 mt-1 min-w-[160px] rounded-md border border-border-subtle bg-popover p-1 shadow-lg", "animate-in fade-in-0 zoom-in-95 duration-100"),
|
|
42
42
|
role: "menu",
|
|
43
43
|
children: a.map((t, n) => /* @__PURE__ */ f("div", { children: [t.separator && n > 0 && /* @__PURE__ */ d("div", {
|
|
44
44
|
className: "my-1 h-px bg-border",
|
|
@@ -47,7 +47,7 @@ function p({ actions: a, ariaLabel: o = "Row actions", stopPropagation: s = !0 }
|
|
|
47
47
|
role: "menuitem",
|
|
48
48
|
onClick: (e) => g(t, e),
|
|
49
49
|
disabled: t.disabled,
|
|
50
|
-
className: e("flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors", "focus:bg-accent focus:outline-none", t.disabled ? "cursor-not-allowed opacity-50" : t.destructive ? "text-
|
|
50
|
+
className: e("flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors", "focus:bg-accent focus:outline-none", t.disabled ? "cursor-not-allowed opacity-50" : t.destructive ? "text-status-error-text hover:bg-status-error-bg/10 focus:bg-status-error-bg/10" : "text-text-primary hover:bg-accent"),
|
|
51
51
|
children: [t.icon && /* @__PURE__ */ d("span", {
|
|
52
52
|
className: "h-4 w-4",
|
|
53
53
|
children: t.icon
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RowActions.js","names":[],"sources":["../../../src/admin/components/RowActions.tsx"],"sourcesContent":["import { useState, useRef, useEffect, type ReactNode } from 'react';\nimport { MoreHorizontal, Pencil, Trash2, Eye, Copy, ExternalLink } from 'lucide-react';\nimport { Button } from './ui';\nimport { cn } from '../utils/cn';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface RowAction {\n /** Unique key for the action */\n key: string;\n /** Display label */\n label: string;\n /** Icon component */\n icon?: ReactNode;\n /** Action handler */\n onClick: () => void;\n /** Whether this is a destructive action */\n destructive?: boolean;\n /** Whether the action is disabled */\n disabled?: boolean;\n /** Separator before this item */\n separator?: boolean;\n}\n\nexport interface RowActionsProps {\n /** Actions to display */\n actions: RowAction[];\n /** Accessibility label */\n ariaLabel?: string;\n /** Stop event propagation on click */\n stopPropagation?: boolean;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\n/**\n * RowActions - Dropdown menu for table row actions\n */\nexport function RowActions({\n actions,\n ariaLabel = 'Row actions',\n stopPropagation = true,\n}: RowActionsProps) {\n const [open, setOpen] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n\n // Close on click outside\n useEffect(() => {\n if (!open) return;\n\n const handleClickOutside = (e: MouseEvent) => {\n if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n setOpen(false);\n }\n };\n\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, [open]);\n\n // Close on escape\n useEffect(() => {\n if (!open) return;\n\n const handleEscape = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n setOpen(false);\n }\n };\n\n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [open]);\n\n const handleTriggerClick = (e: React.MouseEvent) => {\n if (stopPropagation) {\n e.stopPropagation();\n }\n setOpen(!open);\n };\n\n const handleActionClick = (action: RowAction, e: React.MouseEvent) => {\n if (stopPropagation) {\n e.stopPropagation();\n }\n if (!action.disabled) {\n setOpen(false);\n action.onClick();\n }\n };\n\n return (\n <div ref={containerRef} className=\"relative\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={handleTriggerClick}\n aria-label={ariaLabel}\n aria-haspopup=\"true\"\n aria-expanded={open}\n className=\"h-8 w-8\"\n >\n <MoreHorizontal className=\"h-4 w-4\" />\n </Button>\n\n {open && (\n <div\n ref={menuRef}\n className={cn(\n 'absolute right-0 top-full z-50 mt-1 min-w-[160px] rounded-md border border-border bg-popover p-1 shadow-lg',\n 'animate-in fade-in-0 zoom-in-95 duration-100'\n )}\n role=\"menu\"\n >\n {actions.map((action, index) => (\n <div key={action.key}>\n {action.separator && index > 0 && (\n <div className=\"my-1 h-px bg-border\" role=\"separator\" />\n )}\n <button\n role=\"menuitem\"\n onClick={(e) => handleActionClick(action, e)}\n disabled={action.disabled}\n className={cn(\n 'flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors',\n 'focus:bg-accent focus:outline-none',\n action.disabled\n ? 'cursor-not-allowed opacity-50'\n : action.destructive\n ? 'text-
|
|
1
|
+
{"version":3,"file":"RowActions.js","names":[],"sources":["../../../src/admin/components/RowActions.tsx"],"sourcesContent":["import { useState, useRef, useEffect, type ReactNode } from 'react';\nimport { MoreHorizontal, Pencil, Trash2, Eye, Copy, ExternalLink } from 'lucide-react';\nimport { Button } from './ui';\nimport { cn } from '../utils/cn';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface RowAction {\n /** Unique key for the action */\n key: string;\n /** Display label */\n label: string;\n /** Icon component */\n icon?: ReactNode;\n /** Action handler */\n onClick: () => void;\n /** Whether this is a destructive action */\n destructive?: boolean;\n /** Whether the action is disabled */\n disabled?: boolean;\n /** Separator before this item */\n separator?: boolean;\n}\n\nexport interface RowActionsProps {\n /** Actions to display */\n actions: RowAction[];\n /** Accessibility label */\n ariaLabel?: string;\n /** Stop event propagation on click */\n stopPropagation?: boolean;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\n/**\n * RowActions - Dropdown menu for table row actions\n */\nexport function RowActions({\n actions,\n ariaLabel = 'Row actions',\n stopPropagation = true,\n}: RowActionsProps) {\n const [open, setOpen] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n\n // Close on click outside\n useEffect(() => {\n if (!open) return;\n\n const handleClickOutside = (e: MouseEvent) => {\n if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n setOpen(false);\n }\n };\n\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, [open]);\n\n // Close on escape\n useEffect(() => {\n if (!open) return;\n\n const handleEscape = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n setOpen(false);\n }\n };\n\n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [open]);\n\n const handleTriggerClick = (e: React.MouseEvent) => {\n if (stopPropagation) {\n e.stopPropagation();\n }\n setOpen(!open);\n };\n\n const handleActionClick = (action: RowAction, e: React.MouseEvent) => {\n if (stopPropagation) {\n e.stopPropagation();\n }\n if (!action.disabled) {\n setOpen(false);\n action.onClick();\n }\n };\n\n return (\n <div ref={containerRef} className=\"relative\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={handleTriggerClick}\n aria-label={ariaLabel}\n aria-haspopup=\"true\"\n aria-expanded={open}\n className=\"h-8 w-8\"\n >\n <MoreHorizontal className=\"h-4 w-4\" />\n </Button>\n\n {open && (\n <div\n ref={menuRef}\n className={cn(\n 'absolute right-0 top-full z-50 mt-1 min-w-[160px] rounded-md border border-border-subtle bg-popover p-1 shadow-lg',\n 'animate-in fade-in-0 zoom-in-95 duration-100'\n )}\n role=\"menu\"\n >\n {actions.map((action, index) => (\n <div key={action.key}>\n {action.separator && index > 0 && (\n <div className=\"my-1 h-px bg-border\" role=\"separator\" />\n )}\n <button\n role=\"menuitem\"\n onClick={(e) => handleActionClick(action, e)}\n disabled={action.disabled}\n className={cn(\n 'flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors',\n 'focus:bg-accent focus:outline-none',\n action.disabled\n ? 'cursor-not-allowed opacity-50'\n : action.destructive\n ? 'text-status-error-text hover:bg-status-error-bg/10 focus:bg-status-error-bg/10'\n : 'text-text-primary hover:bg-accent'\n )}\n >\n {action.icon && <span className=\"h-4 w-4\">{action.icon}</span>}\n {action.label}\n </button>\n </div>\n ))}\n </div>\n )}\n </div>\n );\n}\n\n// ============================================================================\n// Pre-built Action Helpers\n// ============================================================================\n\n/**\n * Create common row actions\n */\nexport function createRowActions(config: {\n onView?: () => void;\n onEdit?: () => void;\n onDuplicate?: () => void;\n onOpenExternal?: () => void;\n onDelete?: () => void;\n}): RowAction[] {\n const actions: RowAction[] = [];\n\n if (config.onView) {\n actions.push({\n key: 'view',\n label: 'View',\n icon: <Eye className=\"h-4 w-4\" />,\n onClick: config.onView,\n });\n }\n\n if (config.onEdit) {\n actions.push({\n key: 'edit',\n label: 'Edit',\n icon: <Pencil className=\"h-4 w-4\" />,\n onClick: config.onEdit,\n });\n }\n\n if (config.onDuplicate) {\n actions.push({\n key: 'duplicate',\n label: 'Duplicate',\n icon: <Copy className=\"h-4 w-4\" />,\n onClick: config.onDuplicate,\n });\n }\n\n if (config.onOpenExternal) {\n actions.push({\n key: 'open-external',\n label: 'Open in new tab',\n icon: <ExternalLink className=\"h-4 w-4\" />,\n onClick: config.onOpenExternal,\n });\n }\n\n if (config.onDelete) {\n actions.push({\n key: 'delete',\n label: 'Delete',\n icon: <Trash2 className=\"h-4 w-4\" />,\n onClick: config.onDelete,\n destructive: true,\n separator: actions.length > 0,\n });\n }\n\n return actions;\n}\n"],"mappings":";;;;;;AA0CA,SAAgB,EAAW,EACzB,YACA,eAAY,eACZ,qBAAkB,MACA;CAClB,IAAM,CAAC,GAAM,KAAW,EAAS,GAAM,EACjC,IAAe,EAAuB,KAAK,EAC3C,IAAU,EAAuB,KAAK;AAiB5C,CAdA,QAAgB;AACd,MAAI,CAAC,EAAM;EAEX,IAAM,KAAsB,MAAkB;AAC5C,GAAI,EAAa,WAAW,CAAC,EAAa,QAAQ,SAAS,EAAE,OAAe,IAC1E,EAAQ,GAAM;;AAKlB,SADA,SAAS,iBAAiB,aAAa,EAAmB,QAC7C,SAAS,oBAAoB,aAAa,EAAmB;IACzE,CAAC,EAAK,CAAC,EAGV,QAAgB;AACd,MAAI,CAAC,EAAM;EAEX,IAAM,KAAgB,MAAqB;AACzC,GAAI,EAAE,QAAQ,YACZ,EAAQ,GAAM;;AAKlB,SADA,SAAS,iBAAiB,WAAW,EAAa,QACrC,SAAS,oBAAoB,WAAW,EAAa;IACjE,CAAC,EAAK,CAAC;CAEV,IAAM,KAAsB,MAAwB;AAIlD,EAHI,KACF,EAAE,iBAAiB,EAErB,EAAQ,CAAC,EAAK;IAGV,KAAqB,GAAmB,MAAwB;AAIpE,EAHI,KACF,EAAE,iBAAiB,EAEhB,EAAO,aACV,EAAQ,GAAM,EACd,EAAO,SAAS;;AAIpB,QACE,kBAAC,OAAD;EAAK,KAAK;EAAc,WAAU;YAAlC,CACE,kBAAC,GAAD;GACE,SAAQ;GACR,MAAK;GACL,SAAS;GACT,cAAY;GACZ,iBAAc;GACd,iBAAe;GACf,WAAU;aAEV,kBAAC,GAAD,EAAgB,WAAU,WAAY,CAAA;GAC/B,CAAA,EAER,KACC,kBAAC,OAAD;GACE,KAAK;GACL,WAAW,EACT,qHACA,+CACD;GACD,MAAK;aAEJ,EAAQ,KAAK,GAAQ,MACpB,kBAAC,OAAD,EAAA,UAAA,CACG,EAAO,aAAa,IAAQ,KAC3B,kBAAC,OAAD;IAAK,WAAU;IAAsB,MAAK;IAAc,CAAA,EAE1D,kBAAC,UAAD;IACE,MAAK;IACL,UAAU,MAAM,EAAkB,GAAQ,EAAE;IAC5C,UAAU,EAAO;IACjB,WAAW,EACT,mFACA,sCACA,EAAO,WACH,kCACA,EAAO,cACL,mFACA,oCACP;cAZH,CAcG,EAAO,QAAQ,kBAAC,QAAD;KAAM,WAAU;eAAW,EAAO;KAAY,CAAA,EAC7D,EAAO,MACD;MACL,EAAA,EArBI,EAAO,IAqBX,CACN;GACE,CAAA,CAEJ;;;AAWV,SAAgB,EAAiB,GAMjB;CACd,IAAM,IAAuB,EAAE;AAiD/B,QA/CI,EAAO,UACT,EAAQ,KAAK;EACX,KAAK;EACL,OAAO;EACP,MAAM,kBAAC,GAAD,EAAK,WAAU,WAAY,CAAA;EACjC,SAAS,EAAO;EACjB,CAAC,EAGA,EAAO,UACT,EAAQ,KAAK;EACX,KAAK;EACL,OAAO;EACP,MAAM,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;EACpC,SAAS,EAAO;EACjB,CAAC,EAGA,EAAO,eACT,EAAQ,KAAK;EACX,KAAK;EACL,OAAO;EACP,MAAM,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;EAClC,SAAS,EAAO;EACjB,CAAC,EAGA,EAAO,kBACT,EAAQ,KAAK;EACX,KAAK;EACL,OAAO;EACP,MAAM,kBAAC,GAAD,EAAc,WAAU,WAAY,CAAA;EAC1C,SAAS,EAAO;EACjB,CAAC,EAGA,EAAO,YACT,EAAQ,KAAK;EACX,KAAK;EACL,OAAO;EACP,MAAM,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;EACpC,SAAS,EAAO;EAChB,aAAa;EACb,WAAW,EAAQ,SAAS;EAC7B,CAAC,EAGG"}
|
|
@@ -28,7 +28,7 @@ function E({ tenantId: E, tenantType: D, canManage: O }) {
|
|
|
28
28
|
className: "flex items-center gap-2 text-lg",
|
|
29
29
|
children: [/* @__PURE__ */ x(y, { className: "h-4 w-4" }), A("admin.tenantMembers.title", "Tenant Members")]
|
|
30
30
|
}) }), /* @__PURE__ */ x(r, { children: /* @__PURE__ */ x("p", {
|
|
31
|
-
className: "text-sm text-
|
|
31
|
+
className: "text-sm text-text-secondary",
|
|
32
32
|
children: A("admin.tenantMembers.sharedImplicit", "This is the shared tenant. Every authenticated user has implicit access — no explicit membership rows are stored.")
|
|
33
33
|
}) })] });
|
|
34
34
|
let J = async (e) => {
|
|
@@ -71,7 +71,7 @@ function E({ tenantId: E, tenantType: D, canManage: O }) {
|
|
|
71
71
|
/* @__PURE__ */ x(y, { className: "h-4 w-4" }),
|
|
72
72
|
A("admin.tenantMembers.title", "Tenant Members"),
|
|
73
73
|
Q > 0 && /* @__PURE__ */ S("span", {
|
|
74
|
-
className: "text-sm text-
|
|
74
|
+
className: "text-sm text-text-secondary",
|
|
75
75
|
children: [
|
|
76
76
|
"(",
|
|
77
77
|
Q,
|
|
@@ -83,7 +83,7 @@ function E({ tenantId: E, tenantType: D, canManage: O }) {
|
|
|
83
83
|
className: "space-y-4",
|
|
84
84
|
children: [
|
|
85
85
|
/* @__PURE__ */ x("p", {
|
|
86
|
-
className: "text-sm text-
|
|
86
|
+
className: "text-sm text-text-secondary",
|
|
87
87
|
children: A("admin.tenantMembers.description", "Grant a user explicit access to this dedicated tenant. They will be able to create organizations under it and see it in their tenant switcher.")
|
|
88
88
|
}),
|
|
89
89
|
O && /* @__PURE__ */ S(b, { children: [/* @__PURE__ */ S("form", {
|
|
@@ -93,7 +93,7 @@ function E({ tenantId: E, tenantType: D, canManage: O }) {
|
|
|
93
93
|
/* @__PURE__ */ S("div", {
|
|
94
94
|
className: "flex-1 min-w-[240px]",
|
|
95
95
|
children: [/* @__PURE__ */ x("label", {
|
|
96
|
-
className: "mb-1 block text-xs font-medium text-
|
|
96
|
+
className: "mb-1 block text-xs font-medium text-text-secondary",
|
|
97
97
|
children: A("admin.tenantMembers.form.userIdLabel", "User ID")
|
|
98
98
|
}), /* @__PURE__ */ x(o, {
|
|
99
99
|
value: M.userId,
|
|
@@ -109,7 +109,7 @@ function E({ tenantId: E, tenantType: D, canManage: O }) {
|
|
|
109
109
|
/* @__PURE__ */ S("div", {
|
|
110
110
|
className: "min-w-[140px]",
|
|
111
111
|
children: [/* @__PURE__ */ x("label", {
|
|
112
|
-
className: "mb-1 block text-xs font-medium text-
|
|
112
|
+
className: "mb-1 block text-xs font-medium text-text-secondary",
|
|
113
113
|
children: A("admin.tenantMembers.form.roleLabel", "Role")
|
|
114
114
|
}), /* @__PURE__ */ S("select", {
|
|
115
115
|
value: M.role,
|
|
@@ -118,7 +118,7 @@ function E({ tenantId: E, tenantType: D, canManage: O }) {
|
|
|
118
118
|
role: e.target.value
|
|
119
119
|
}),
|
|
120
120
|
disabled: W,
|
|
121
|
-
className: "h-9 w-full rounded-md border border-
|
|
121
|
+
className: "h-9 w-full rounded-md border border-border-default bg-bg-surface px-3 text-sm",
|
|
122
122
|
children: [/* @__PURE__ */ x("option", {
|
|
123
123
|
value: h.Member,
|
|
124
124
|
children: A("admin.tenantMembers.role.member", "Member")
|
|
@@ -136,25 +136,25 @@ function E({ tenantId: E, tenantType: D, canManage: O }) {
|
|
|
136
136
|
]
|
|
137
137
|
}), /* @__PURE__ */ x(s, {})] }),
|
|
138
138
|
B && Z.length === 0 ? /* @__PURE__ */ S("div", {
|
|
139
|
-
className: "flex h-24 items-center justify-center text-
|
|
139
|
+
className: "flex h-24 items-center justify-center text-text-secondary",
|
|
140
140
|
children: [/* @__PURE__ */ x(_, { className: "mr-2 h-4 w-4 animate-spin" }), A("admin.tenantMembers.loading", "Loading members...")]
|
|
141
141
|
}) : V ? /* @__PURE__ */ x("div", {
|
|
142
|
-
className: "rounded-md border border-
|
|
142
|
+
className: "rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-3 text-sm text-status-error-text",
|
|
143
143
|
children: V.message
|
|
144
144
|
}) : Z.length === 0 ? /* @__PURE__ */ x("p", {
|
|
145
|
-
className: "text-sm text-
|
|
145
|
+
className: "text-sm text-text-secondary",
|
|
146
146
|
children: A("admin.tenantMembers.empty", "No explicit members yet. Grant access above to add one.")
|
|
147
147
|
}) : /* @__PURE__ */ S("div", {
|
|
148
148
|
className: "space-y-2",
|
|
149
149
|
children: [Z.map((e) => /* @__PURE__ */ S("div", {
|
|
150
|
-
className: "flex flex-wrap items-center justify-between gap-2 rounded-lg border border-border p-3",
|
|
150
|
+
className: "flex flex-wrap items-center justify-between gap-2 rounded-lg border border-border-subtle p-3",
|
|
151
151
|
children: [/* @__PURE__ */ S("div", {
|
|
152
152
|
className: "min-w-0 flex-1",
|
|
153
153
|
children: [/* @__PURE__ */ x("p", {
|
|
154
|
-
className: "truncate font-mono text-xs text-
|
|
154
|
+
className: "truncate font-mono text-xs text-text-primary",
|
|
155
155
|
children: e.userId
|
|
156
156
|
}), /* @__PURE__ */ x("p", {
|
|
157
|
-
className: "text-xs text-
|
|
157
|
+
className: "text-xs text-text-secondary",
|
|
158
158
|
children: A("admin.tenantMembers.addedAt", "Added {{when}} by {{by}}", {
|
|
159
159
|
when: c(e.createdAt),
|
|
160
160
|
by: e.addedBy
|
|
@@ -166,7 +166,7 @@ function E({ tenantId: E, tenantType: D, canManage: O }) {
|
|
|
166
166
|
value: e.role,
|
|
167
167
|
onChange: (t) => X(e.userId, t.target.value),
|
|
168
168
|
disabled: !O,
|
|
169
|
-
className: "h-8 rounded-md border border-
|
|
169
|
+
className: "h-8 rounded-md border border-border-default bg-bg-surface px-2 text-xs",
|
|
170
170
|
children: [/* @__PURE__ */ x("option", {
|
|
171
171
|
value: h.Member,
|
|
172
172
|
children: A("admin.tenantMembers.role.member", "Member")
|
|
@@ -180,13 +180,13 @@ function E({ tenantId: E, tenantType: D, canManage: O }) {
|
|
|
180
180
|
onClick: () => F({ userId: e.userId }),
|
|
181
181
|
disabled: K,
|
|
182
182
|
"aria-label": A("admin.tenantMembers.remove", "Remove"),
|
|
183
|
-
children: /* @__PURE__ */ x(v, { className: "h-4 w-4 text-
|
|
183
|
+
children: /* @__PURE__ */ x(v, { className: "h-4 w-4 text-status-error-text" })
|
|
184
184
|
})]
|
|
185
185
|
})]
|
|
186
186
|
}, e.id)), te && /* @__PURE__ */ S("div", {
|
|
187
187
|
className: "flex items-center justify-between gap-2 pt-2",
|
|
188
188
|
children: [/* @__PURE__ */ x("span", {
|
|
189
|
-
className: "text-xs text-
|
|
189
|
+
className: "text-xs text-text-secondary",
|
|
190
190
|
children: A("admin.tenantMembers.shownOfTotal", "Showing {{shown}} of {{total}}", {
|
|
191
191
|
shown: Z.length,
|
|
192
192
|
total: Q
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TenantMembersPanel.js","names":[],"sources":["../../../src/admin/components/TenantMembersPanel.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Loader2, Trash2, UserPlus, Users } from 'lucide-react';\nimport {\n useListTenantMembersQuery,\n useAddTenantMemberMutation,\n useRemoveTenantMemberMutation,\n useUpdateTenantMemberRoleMutation,\n type TenantMemberFieldsFragment,\n} from '../../generated/global-operations';\nimport { TenantMemberRole } from '../../generated/global-types';\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';\nimport { formatDate } from '../utils/formatters';\n\ninterface TenantMembersPanelProps {\n tenantId: string;\n tenantType: string;\n canManage: boolean;\n}\n\nconst EMPTY_ADD_FORM = { userId: '', role: TenantMemberRole.Member };\nconst PAGE_SIZE = 100;\n\n/**\n * Tenant Members panel (BOFF-2652) — super_admin UI to grant non-super-admin\n * users explicit access to a DEDICATED tenant. SHARED-tenant access is\n * implicit, so the panel is hidden for SHARED tenants.\n */\nexport function TenantMembersPanel({ tenantId, tenantType, canManage }: TenantMembersPanelProps) {\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 [addForm, setAddForm] = useState(EMPTY_ADD_FORM);\n const [removeTarget, setRemoveTarget] = useState<{ userId: string } | null>(null);\n // Pagination: each \"Load more\" click widens the window by PAGE_SIZE rows;\n // Apollo re-fetches the bigger page rather than appending in JS so we\n // don't need a custom merge type policy. (BOFF-2652 review)\n const [pageSize, setPageSize] = useState(PAGE_SIZE);\n\n const isShared = tenantType?.toUpperCase() === 'SHARED';\n\n const { data, loading, error, refetch } = useListTenantMembersQuery({\n variables: { tenantId, limit: pageSize, offset: 0 },\n skip: !tenantId || isShared,\n fetchPolicy: 'cache-and-network',\n });\n\n const [addMember, { loading: adding }] = useAddTenantMemberMutation();\n const [removeMember, { loading: removing }] = useRemoveTenantMemberMutation();\n const [updateRole] = useUpdateTenantMemberRoleMutation();\n\n if (isShared) {\n return (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-lg\">\n <Users className=\"h-4 w-4\" />\n {tr('admin.tenantMembers.title', 'Tenant Members')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm text-muted-foreground\">\n {tr(\n 'admin.tenantMembers.sharedImplicit',\n 'This is the shared tenant. Every authenticated user has implicit access — no explicit membership rows are stored.'\n )}\n </p>\n </CardContent>\n </Card>\n );\n }\n\n const handleAdd = async (e: React.FormEvent) => {\n e.preventDefault();\n const userId = addForm.userId.trim();\n if (!userId) return;\n try {\n await addMember({ variables: { input: { tenantId, userId, role: addForm.role } } });\n toast.success(\n tr('admin.tenantMembers.toast.addedTitle', 'Member added'),\n tr(\n 'admin.tenantMembers.toast.addedDescription',\n 'User {{userId}} can now access this tenant.',\n {\n userId,\n }\n )\n );\n setAddForm(EMPTY_ADD_FORM);\n await refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantMembers.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantMembers.toast.addFailed', 'Failed to add tenant member.')\n );\n }\n };\n\n const handleRemove = async () => {\n if (!removeTarget) return;\n const { userId } = removeTarget;\n try {\n await removeMember({ variables: { tenantId, userId } });\n toast.success(\n tr('admin.tenantMembers.toast.removedTitle', 'Member removed'),\n tr(\n 'admin.tenantMembers.toast.removedDescription',\n 'User {{userId}} no longer has access.',\n {\n userId,\n }\n )\n );\n setRemoveTarget(null);\n await refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantMembers.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantMembers.toast.removeFailed', 'Failed to remove tenant member.')\n );\n }\n };\n\n const handleRoleChange = async (userId: string, role: TenantMemberRole) => {\n try {\n await updateRole({ variables: { tenantId, userId, role } });\n await refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantMembers.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantMembers.toast.updateRoleFailed', 'Failed to update role.')\n );\n }\n };\n\n const members = data?.tenantMembers?.items ?? [];\n const total = data?.tenantMembers?.total ?? 0;\n const hasMore = data?.tenantMembers?.hasMore ?? false;\n const loadingMore = loading && members.length > 0;\n\n return (\n <>\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-lg\">\n <Users className=\"h-4 w-4\" />\n {tr('admin.tenantMembers.title', 'Tenant Members')}\n {total > 0 && <span className=\"text-sm text-muted-foreground\">({total})</span>}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <p className=\"text-sm text-muted-foreground\">\n {tr(\n 'admin.tenantMembers.description',\n 'Grant a user explicit access to this dedicated tenant. They will be able to create organizations under it and see it in their tenant switcher.'\n )}\n </p>\n\n {canManage && (\n <>\n <form onSubmit={handleAdd} className=\"flex flex-wrap items-end gap-2\">\n <div className=\"flex-1 min-w-[240px]\">\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">\n {tr('admin.tenantMembers.form.userIdLabel', 'User ID')}\n </label>\n <Input\n value={addForm.userId}\n onChange={(e) => setAddForm({ ...addForm, userId: e.target.value })}\n placeholder={tr(\n 'admin.tenantMembers.form.userIdPlaceholder',\n 'UUID of the user to grant access'\n )}\n required\n disabled={adding}\n />\n </div>\n <div className=\"min-w-[140px]\">\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">\n {tr('admin.tenantMembers.form.roleLabel', 'Role')}\n </label>\n <select\n value={addForm.role}\n onChange={(e) =>\n setAddForm({ ...addForm, role: e.target.value as TenantMemberRole })\n }\n disabled={adding}\n className=\"h-9 w-full rounded-md border border-input bg-background px-3 text-sm\"\n >\n <option value={TenantMemberRole.Member}>\n {tr('admin.tenantMembers.role.member', 'Member')}\n </option>\n <option value={TenantMemberRole.Admin}>\n {tr('admin.tenantMembers.role.admin', 'Admin')}\n </option>\n </select>\n </div>\n <Button type=\"submit\" disabled={adding || !addForm.userId.trim()}>\n {adding ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <UserPlus className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantMembers.form.submit', 'Grant access')}\n </Button>\n </form>\n <Separator />\n </>\n )}\n\n {loading && members.length === 0 ? (\n <div className=\"flex h-24 items-center justify-center text-muted-foreground\">\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n {tr('admin.tenantMembers.loading', 'Loading members...')}\n </div>\n ) : error ? (\n <div className=\"rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive\">\n {error.message}\n </div>\n ) : members.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {tr(\n 'admin.tenantMembers.empty',\n 'No explicit members yet. Grant access above to add one.'\n )}\n </p>\n ) : (\n <div className=\"space-y-2\">\n {members.map((m: TenantMemberFieldsFragment) => (\n <div\n key={m.id}\n className=\"flex flex-wrap items-center justify-between gap-2 rounded-lg border border-border p-3\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate font-mono text-xs text-foreground\">{m.userId}</p>\n <p className=\"text-xs text-muted-foreground\">\n {tr('admin.tenantMembers.addedAt', 'Added {{when}} by {{by}}', {\n when: formatDate(m.createdAt),\n by: m.addedBy,\n })}\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={m.role}\n onChange={(e) =>\n handleRoleChange(m.userId, e.target.value as TenantMemberRole)\n }\n disabled={!canManage}\n className=\"h-8 rounded-md border border-input bg-background px-2 text-xs\"\n >\n <option value={TenantMemberRole.Member}>\n {tr('admin.tenantMembers.role.member', 'Member')}\n </option>\n <option value={TenantMemberRole.Admin}>\n {tr('admin.tenantMembers.role.admin', 'Admin')}\n </option>\n </select>\n {canManage && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setRemoveTarget({ userId: m.userId })}\n disabled={removing}\n aria-label={tr('admin.tenantMembers.remove', 'Remove')}\n >\n <Trash2 className=\"h-4 w-4 text-destructive\" />\n </Button>\n )}\n </div>\n </div>\n ))}\n {hasMore && (\n <div className=\"flex items-center justify-between gap-2 pt-2\">\n <span className=\"text-xs text-muted-foreground\">\n {tr('admin.tenantMembers.shownOfTotal', 'Showing {{shown}} of {{total}}', {\n shown: members.length,\n total,\n })}\n </span>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setPageSize((prev) => prev + PAGE_SIZE)}\n disabled={loadingMore}\n >\n {loadingMore ? <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" /> : null}\n {tr('admin.tenantMembers.loadMore', 'Load more')}\n </Button>\n </div>\n )}\n </div>\n )}\n </CardContent>\n </Card>\n\n <ConfirmDialog\n open={Boolean(removeTarget)}\n title={tr('admin.tenantMembers.confirmRemove.title', 'Revoke tenant access?')}\n description={tr(\n 'admin.tenantMembers.confirmRemove.description',\n 'User {{userId}} will no longer be able to create organizations in this tenant. Existing organization and workspace memberships are not affected.',\n { userId: removeTarget?.userId ?? '' }\n )}\n confirmLabel={tr('admin.tenantMembers.confirmRemove.confirm', 'Revoke')}\n variant=\"destructive\"\n loading={removing}\n onConfirm={handleRemove}\n onClose={() => setRemoveTarget(null)}\n />\n </>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TenantMembersPanel.js","names":[],"sources":["../../../src/admin/components/TenantMembersPanel.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Loader2, Trash2, UserPlus, Users } from 'lucide-react';\nimport {\n useListTenantMembersQuery,\n useAddTenantMemberMutation,\n useRemoveTenantMemberMutation,\n useUpdateTenantMemberRoleMutation,\n type TenantMemberFieldsFragment,\n} from '../../generated/global-operations';\nimport { TenantMemberRole } from '../../generated/global-types';\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';\nimport { formatDate } from '../utils/formatters';\n\ninterface TenantMembersPanelProps {\n tenantId: string;\n tenantType: string;\n canManage: boolean;\n}\n\nconst EMPTY_ADD_FORM = { userId: '', role: TenantMemberRole.Member };\nconst PAGE_SIZE = 100;\n\n/**\n * Tenant Members panel (BOFF-2652) — super_admin UI to grant non-super-admin\n * users explicit access to a DEDICATED tenant. SHARED-tenant access is\n * implicit, so the panel is hidden for SHARED tenants.\n */\nexport function TenantMembersPanel({ tenantId, tenantType, canManage }: TenantMembersPanelProps) {\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 [addForm, setAddForm] = useState(EMPTY_ADD_FORM);\n const [removeTarget, setRemoveTarget] = useState<{ userId: string } | null>(null);\n // Pagination: each \"Load more\" click widens the window by PAGE_SIZE rows;\n // Apollo re-fetches the bigger page rather than appending in JS so we\n // don't need a custom merge type policy. (BOFF-2652 review)\n const [pageSize, setPageSize] = useState(PAGE_SIZE);\n\n const isShared = tenantType?.toUpperCase() === 'SHARED';\n\n const { data, loading, error, refetch } = useListTenantMembersQuery({\n variables: { tenantId, limit: pageSize, offset: 0 },\n skip: !tenantId || isShared,\n fetchPolicy: 'cache-and-network',\n });\n\n const [addMember, { loading: adding }] = useAddTenantMemberMutation();\n const [removeMember, { loading: removing }] = useRemoveTenantMemberMutation();\n const [updateRole] = useUpdateTenantMemberRoleMutation();\n\n if (isShared) {\n return (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-lg\">\n <Users className=\"h-4 w-4\" />\n {tr('admin.tenantMembers.title', 'Tenant Members')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.tenantMembers.sharedImplicit',\n 'This is the shared tenant. Every authenticated user has implicit access — no explicit membership rows are stored.'\n )}\n </p>\n </CardContent>\n </Card>\n );\n }\n\n const handleAdd = async (e: React.FormEvent) => {\n e.preventDefault();\n const userId = addForm.userId.trim();\n if (!userId) return;\n try {\n await addMember({ variables: { input: { tenantId, userId, role: addForm.role } } });\n toast.success(\n tr('admin.tenantMembers.toast.addedTitle', 'Member added'),\n tr(\n 'admin.tenantMembers.toast.addedDescription',\n 'User {{userId}} can now access this tenant.',\n {\n userId,\n }\n )\n );\n setAddForm(EMPTY_ADD_FORM);\n await refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantMembers.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantMembers.toast.addFailed', 'Failed to add tenant member.')\n );\n }\n };\n\n const handleRemove = async () => {\n if (!removeTarget) return;\n const { userId } = removeTarget;\n try {\n await removeMember({ variables: { tenantId, userId } });\n toast.success(\n tr('admin.tenantMembers.toast.removedTitle', 'Member removed'),\n tr(\n 'admin.tenantMembers.toast.removedDescription',\n 'User {{userId}} no longer has access.',\n {\n userId,\n }\n )\n );\n setRemoveTarget(null);\n await refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantMembers.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantMembers.toast.removeFailed', 'Failed to remove tenant member.')\n );\n }\n };\n\n const handleRoleChange = async (userId: string, role: TenantMemberRole) => {\n try {\n await updateRole({ variables: { tenantId, userId, role } });\n await refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantMembers.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantMembers.toast.updateRoleFailed', 'Failed to update role.')\n );\n }\n };\n\n const members = data?.tenantMembers?.items ?? [];\n const total = data?.tenantMembers?.total ?? 0;\n const hasMore = data?.tenantMembers?.hasMore ?? false;\n const loadingMore = loading && members.length > 0;\n\n return (\n <>\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-lg\">\n <Users className=\"h-4 w-4\" />\n {tr('admin.tenantMembers.title', 'Tenant Members')}\n {total > 0 && <span className=\"text-sm text-text-secondary\">({total})</span>}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.tenantMembers.description',\n 'Grant a user explicit access to this dedicated tenant. They will be able to create organizations under it and see it in their tenant switcher.'\n )}\n </p>\n\n {canManage && (\n <>\n <form onSubmit={handleAdd} className=\"flex flex-wrap items-end gap-2\">\n <div className=\"flex-1 min-w-[240px]\">\n <label className=\"mb-1 block text-xs font-medium text-text-secondary\">\n {tr('admin.tenantMembers.form.userIdLabel', 'User ID')}\n </label>\n <Input\n value={addForm.userId}\n onChange={(e) => setAddForm({ ...addForm, userId: e.target.value })}\n placeholder={tr(\n 'admin.tenantMembers.form.userIdPlaceholder',\n 'UUID of the user to grant access'\n )}\n required\n disabled={adding}\n />\n </div>\n <div className=\"min-w-[140px]\">\n <label className=\"mb-1 block text-xs font-medium text-text-secondary\">\n {tr('admin.tenantMembers.form.roleLabel', 'Role')}\n </label>\n <select\n value={addForm.role}\n onChange={(e) =>\n setAddForm({ ...addForm, role: e.target.value as TenantMemberRole })\n }\n disabled={adding}\n className=\"h-9 w-full rounded-md border border-border-default bg-bg-surface px-3 text-sm\"\n >\n <option value={TenantMemberRole.Member}>\n {tr('admin.tenantMembers.role.member', 'Member')}\n </option>\n <option value={TenantMemberRole.Admin}>\n {tr('admin.tenantMembers.role.admin', 'Admin')}\n </option>\n </select>\n </div>\n <Button type=\"submit\" disabled={adding || !addForm.userId.trim()}>\n {adding ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <UserPlus className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantMembers.form.submit', 'Grant access')}\n </Button>\n </form>\n <Separator />\n </>\n )}\n\n {loading && members.length === 0 ? (\n <div className=\"flex h-24 items-center justify-center text-text-secondary\">\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n {tr('admin.tenantMembers.loading', 'Loading members...')}\n </div>\n ) : error ? (\n <div className=\"rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-3 text-sm text-status-error-text\">\n {error.message}\n </div>\n ) : members.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.tenantMembers.empty',\n 'No explicit members yet. Grant access above to add one.'\n )}\n </p>\n ) : (\n <div className=\"space-y-2\">\n {members.map((m: TenantMemberFieldsFragment) => (\n <div\n key={m.id}\n className=\"flex flex-wrap items-center justify-between gap-2 rounded-lg border border-border-subtle p-3\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate font-mono text-xs text-text-primary\">{m.userId}</p>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantMembers.addedAt', 'Added {{when}} by {{by}}', {\n when: formatDate(m.createdAt),\n by: m.addedBy,\n })}\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={m.role}\n onChange={(e) =>\n handleRoleChange(m.userId, e.target.value as TenantMemberRole)\n }\n disabled={!canManage}\n className=\"h-8 rounded-md border border-border-default bg-bg-surface px-2 text-xs\"\n >\n <option value={TenantMemberRole.Member}>\n {tr('admin.tenantMembers.role.member', 'Member')}\n </option>\n <option value={TenantMemberRole.Admin}>\n {tr('admin.tenantMembers.role.admin', 'Admin')}\n </option>\n </select>\n {canManage && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setRemoveTarget({ userId: m.userId })}\n disabled={removing}\n aria-label={tr('admin.tenantMembers.remove', 'Remove')}\n >\n <Trash2 className=\"h-4 w-4 text-status-error-text\" />\n </Button>\n )}\n </div>\n </div>\n ))}\n {hasMore && (\n <div className=\"flex items-center justify-between gap-2 pt-2\">\n <span className=\"text-xs text-text-secondary\">\n {tr('admin.tenantMembers.shownOfTotal', 'Showing {{shown}} of {{total}}', {\n shown: members.length,\n total,\n })}\n </span>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setPageSize((prev) => prev + PAGE_SIZE)}\n disabled={loadingMore}\n >\n {loadingMore ? <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" /> : null}\n {tr('admin.tenantMembers.loadMore', 'Load more')}\n </Button>\n </div>\n )}\n </div>\n )}\n </CardContent>\n </Card>\n\n <ConfirmDialog\n open={Boolean(removeTarget)}\n title={tr('admin.tenantMembers.confirmRemove.title', 'Revoke tenant access?')}\n description={tr(\n 'admin.tenantMembers.confirmRemove.description',\n 'User {{userId}} will no longer be able to create organizations in this tenant. Existing organization and workspace memberships are not affected.',\n { userId: removeTarget?.userId ?? '' }\n )}\n confirmLabel={tr('admin.tenantMembers.confirmRemove.confirm', 'Revoke')}\n variant=\"destructive\"\n loading={removing}\n onConfirm={handleRemove}\n onClose={() => setRemoveTarget(null)}\n />\n </>\n );\n}\n"],"mappings":"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"}
|
|
@@ -36,7 +36,7 @@ function E({ tenantId: E, canManage: D }) {
|
|
|
36
36
|
}), /* @__PURE__ */ S("button", {
|
|
37
37
|
type: "button",
|
|
38
38
|
onClick: () => M((e) => !e),
|
|
39
|
-
className: "flex items-center gap-1 text-sm text-
|
|
39
|
+
className: "flex items-center gap-1 text-sm text-text-secondary hover:text-text-primary transition-colors",
|
|
40
40
|
"aria-expanded": j,
|
|
41
41
|
"aria-label": j ? k("admin.tenantDetail.sso.collapse", "Collapse SSO configuration") : k("admin.tenantDetail.sso.expand", "Expand SSO configuration"),
|
|
42
42
|
children: S(j ? g : h, { className: "h-4 w-4" })
|
|
@@ -45,9 +45,9 @@ function E({ tenantId: E, canManage: D }) {
|
|
|
45
45
|
className: "space-y-4",
|
|
46
46
|
children: B ? /* @__PURE__ */ S("div", {
|
|
47
47
|
className: "flex items-center justify-center py-6",
|
|
48
|
-
children: /* @__PURE__ */ S(_, { className: "h-6 w-6 animate-spin text-
|
|
48
|
+
children: /* @__PURE__ */ S(_, { className: "h-6 w-6 animate-spin text-text-secondary" })
|
|
49
49
|
}) : V ? /* @__PURE__ */ S("p", {
|
|
50
|
-
className: "text-sm text-
|
|
50
|
+
className: "text-sm text-status-error-text",
|
|
51
51
|
children: V.message
|
|
52
52
|
}) : q && !N ? /* @__PURE__ */ C("div", {
|
|
53
53
|
className: "space-y-3",
|
|
@@ -65,7 +65,7 @@ function E({ tenantId: E, canManage: D }) {
|
|
|
65
65
|
/* @__PURE__ */ C("div", {
|
|
66
66
|
className: "flex flex-col gap-0.5",
|
|
67
67
|
children: [/* @__PURE__ */ S("dt", {
|
|
68
|
-
className: "text-xs text-
|
|
68
|
+
className: "text-xs text-text-secondary",
|
|
69
69
|
children: k("admin.tenantDetail.sso.entityId", "Entity ID")
|
|
70
70
|
}), /* @__PURE__ */ S("dd", {
|
|
71
71
|
className: "font-mono text-xs break-all",
|
|
@@ -75,7 +75,7 @@ function E({ tenantId: E, canManage: D }) {
|
|
|
75
75
|
/* @__PURE__ */ C("div", {
|
|
76
76
|
className: "flex flex-col gap-0.5",
|
|
77
77
|
children: [/* @__PURE__ */ S("dt", {
|
|
78
|
-
className: "text-xs text-
|
|
78
|
+
className: "text-xs text-text-secondary",
|
|
79
79
|
children: k("admin.tenantDetail.sso.ssoUrl", "SSO URL")
|
|
80
80
|
}), /* @__PURE__ */ S("dd", {
|
|
81
81
|
className: "font-mono text-xs break-all",
|
|
@@ -85,7 +85,7 @@ function E({ tenantId: E, canManage: D }) {
|
|
|
85
85
|
q.sloUrl && /* @__PURE__ */ C("div", {
|
|
86
86
|
className: "flex flex-col gap-0.5",
|
|
87
87
|
children: [/* @__PURE__ */ S("dt", {
|
|
88
|
-
className: "text-xs text-
|
|
88
|
+
className: "text-xs text-text-secondary",
|
|
89
89
|
children: k("admin.tenantDetail.sso.sloUrl", "SLO URL")
|
|
90
90
|
}), /* @__PURE__ */ S("dd", {
|
|
91
91
|
className: "font-mono text-xs break-all",
|
|
@@ -104,7 +104,7 @@ function E({ tenantId: E, canManage: D }) {
|
|
|
104
104
|
}), /* @__PURE__ */ C(t, {
|
|
105
105
|
variant: "outline",
|
|
106
106
|
size: "sm",
|
|
107
|
-
className: "text-
|
|
107
|
+
className: "text-status-error-text hover:text-status-error-text",
|
|
108
108
|
onClick: () => R(!0),
|
|
109
109
|
children: [/* @__PURE__ */ S(b, { className: "mr-1.5 h-3.5 w-3.5" }), k("admin.tenantDetail.sso.deleteButton", "Remove")]
|
|
110
110
|
})]
|
|
@@ -134,7 +134,7 @@ function E({ tenantId: E, canManage: D }) {
|
|
|
134
134
|
className: "space-y-4",
|
|
135
135
|
children: [
|
|
136
136
|
!q && /* @__PURE__ */ S("p", {
|
|
137
|
-
className: "text-sm text-
|
|
137
|
+
className: "text-sm text-text-secondary",
|
|
138
138
|
children: k("admin.tenantDetail.sso.notConfigured", "No SSO configuration. Fill in the SAML details below to enable enterprise SSO.")
|
|
139
139
|
}),
|
|
140
140
|
/* @__PURE__ */ S(s, {}),
|
|
@@ -147,7 +147,7 @@ function E({ tenantId: E, canManage: D }) {
|
|
|
147
147
|
className: "text-sm font-medium",
|
|
148
148
|
htmlFor: `sso-entityId-${E}`,
|
|
149
149
|
children: [k("admin.tenantDetail.sso.entityId", "Entity ID"), /* @__PURE__ */ S("span", {
|
|
150
|
-
className: "ml-1 text-
|
|
150
|
+
className: "ml-1 text-status-error-text",
|
|
151
151
|
children: "*"
|
|
152
152
|
})]
|
|
153
153
|
}), /* @__PURE__ */ S(o, {
|
|
@@ -167,7 +167,7 @@ function E({ tenantId: E, canManage: D }) {
|
|
|
167
167
|
className: "text-sm font-medium",
|
|
168
168
|
htmlFor: `sso-ssoUrl-${E}`,
|
|
169
169
|
children: [k("admin.tenantDetail.sso.ssoUrl", "SSO URL"), /* @__PURE__ */ S("span", {
|
|
170
|
-
className: "ml-1 text-
|
|
170
|
+
className: "ml-1 text-status-error-text",
|
|
171
171
|
children: "*"
|
|
172
172
|
})]
|
|
173
173
|
}), /* @__PURE__ */ S(o, {
|
|
@@ -187,7 +187,7 @@ function E({ tenantId: E, canManage: D }) {
|
|
|
187
187
|
className: "text-sm font-medium",
|
|
188
188
|
htmlFor: `sso-sloUrl-${E}`,
|
|
189
189
|
children: [k("admin.tenantDetail.sso.sloUrl", "SLO URL"), /* @__PURE__ */ S("span", {
|
|
190
|
-
className: "ml-0.5 text-xs text-
|
|
190
|
+
className: "ml-0.5 text-xs text-text-secondary",
|
|
191
191
|
children: k("admin.tenantDetail.sso.optional", "(optional)")
|
|
192
192
|
})]
|
|
193
193
|
}), /* @__PURE__ */ S(o, {
|
|
@@ -206,7 +206,7 @@ function E({ tenantId: E, canManage: D }) {
|
|
|
206
206
|
className: "text-sm font-medium",
|
|
207
207
|
htmlFor: `sso-cert-${E}`,
|
|
208
208
|
children: [k("admin.tenantDetail.sso.certificate", "X.509 Certificate"), /* @__PURE__ */ S("span", {
|
|
209
|
-
className: "ml-1 text-
|
|
209
|
+
className: "ml-1 text-status-error-text",
|
|
210
210
|
children: "*"
|
|
211
211
|
})]
|
|
212
212
|
}), /* @__PURE__ */ S("textarea", {
|
|
@@ -219,7 +219,7 @@ function E({ tenantId: E, canManage: D }) {
|
|
|
219
219
|
placeholder: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
220
220
|
required: !0,
|
|
221
221
|
rows: 5,
|
|
222
|
-
className: "w-full rounded-md border border-
|
|
222
|
+
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"
|
|
223
223
|
})]
|
|
224
224
|
}),
|
|
225
225
|
/* @__PURE__ */ C("div", {
|
|
@@ -233,7 +233,7 @@ function E({ tenantId: E, canManage: D }) {
|
|
|
233
233
|
...e,
|
|
234
234
|
enabled: !e.enabled
|
|
235
235
|
})),
|
|
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-ring focus:ring-offset-2 ${F.enabled ? "bg-primary" : "bg-
|
|
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
237
|
children: /* @__PURE__ */ S("span", { className: `pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow-lg transition-transform ${F.enabled ? "translate-x-4" : "translate-x-0"}` })
|
|
238
238
|
}), /* @__PURE__ */ S("span", {
|
|
239
239
|
className: "text-sm",
|