convex-cms 0.0.9-alpha.8 → 0.0.9-alpha.9

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/README.md CHANGED
@@ -120,6 +120,33 @@ Leverage included features or extend and customize within your own convex functi
120
120
 
121
121
  Both modes call the same functions from your `convex/admin.ts`.
122
122
 
123
+ ### Embedding with Theme Modes
124
+
125
+ When embedding CmsAdmin in your React app, you can control how it handles CSS variables:
126
+
127
+ ```tsx
128
+ // Isolated mode (default) - admin uses its own theme
129
+ <CmsAdmin api={api.admin} auth={authConfig} themeMode="isolated" />
130
+
131
+ // Inherit mode - admin inherits your app's CSS variables (for shadcn apps)
132
+ <CmsAdmin api={api.admin} auth={authConfig} themeMode="inherit" />
133
+ ```
134
+
135
+ | Mode | Behavior |
136
+ |------|----------|
137
+ | `isolated` | Admin defines all CSS variables, ignoring parent app styles |
138
+ | `inherit` | Admin inherits parent's shadcn variables, only defines sidebar fallbacks |
139
+
140
+ **Critical for Tailwind 4 apps:** If Tailwind utility classes aren't applying to the embedded admin, add a `@source` directive to your app's CSS:
141
+
142
+ ```css
143
+ /* your-app/src/index.css */
144
+ @import "tailwindcss";
145
+ @source "../node_modules/convex-cms/admin/dist/**/*.js";
146
+ ```
147
+
148
+ This tells Tailwind to scan the admin's compiled JavaScript for utility classes.
149
+
123
150
  ## Documentation
124
151
 
125
152
  | Guide | Description |
@@ -0,0 +1,74 @@
1
+ import * as React from 'react'
2
+ import { Search, X } from 'lucide-react'
3
+ import { CmsInput } from './CmsInput'
4
+ import { CmsSelect, type CmsSelectOption } from './CmsSelect'
5
+ import { CmsButton } from './CmsButton'
6
+ import { cn } from '~/lib/cn'
7
+
8
+ export interface CmsFilterBarFilter {
9
+ key: string
10
+ value: string
11
+ onChange: (value: string) => void
12
+ options: CmsSelectOption[]
13
+ placeholder?: string
14
+ className?: string
15
+ }
16
+
17
+ export interface CmsFilterBarProps {
18
+ search?: {
19
+ value: string
20
+ onChange: (value: string) => void
21
+ placeholder?: string
22
+ className?: string
23
+ }
24
+ filters?: CmsFilterBarFilter[]
25
+ actions?: React.ReactNode
26
+ onClearFilters?: () => void
27
+ hasActiveFilters?: boolean
28
+ className?: string
29
+ }
30
+
31
+ export function CmsFilterBar({
32
+ search,
33
+ filters,
34
+ actions,
35
+ onClearFilters,
36
+ hasActiveFilters,
37
+ className,
38
+ }: CmsFilterBarProps) {
39
+ return (
40
+ <div className={cn("flex flex-wrap items-center gap-3 pb-4", className)}>
41
+ <div className="flex flex-1 flex-wrap items-center gap-2">
42
+ {search && (
43
+ <div className="relative w-full max-w-xs">
44
+ <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
45
+ <CmsInput
46
+ type="search"
47
+ placeholder={search.placeholder ?? "Search..."}
48
+ value={search.value}
49
+ onChange={(e) => search.onChange(e.target.value)}
50
+ className={cn("pl-9", search.className)}
51
+ />
52
+ </div>
53
+ )}
54
+ {filters?.map((filter) => (
55
+ <CmsSelect
56
+ key={filter.key}
57
+ value={filter.value}
58
+ onValueChange={filter.onChange}
59
+ options={filter.options}
60
+ placeholder={filter.placeholder}
61
+ className={cn("w-[150px]", filter.className)}
62
+ />
63
+ ))}
64
+ {hasActiveFilters && onClearFilters && (
65
+ <CmsButton variant="ghost" size="sm" onClick={onClearFilters}>
66
+ <X className="mr-1 size-4" />
67
+ Clear
68
+ </CmsButton>
69
+ )}
70
+ </div>
71
+ {actions && <div className="flex items-center gap-2">{actions}</div>}
72
+ </div>
73
+ )
74
+ }
@@ -0,0 +1,24 @@
1
+ import * as React from 'react'
2
+ import { Input } from '~/components/ui/input'
3
+ import { cn } from '~/lib/cn'
4
+
5
+ export interface CmsInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
6
+ error?: boolean
7
+ }
8
+
9
+ export const CmsInput = React.forwardRef<HTMLInputElement, CmsInputProps>(
10
+ ({ className, error, ...props }, ref) => {
11
+ return (
12
+ <Input
13
+ ref={ref}
14
+ className={cn(
15
+ error && "border-destructive focus-visible:ring-destructive",
16
+ className
17
+ )}
18
+ aria-invalid={error}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+ )
24
+ CmsInput.displayName = 'CmsInput'
@@ -0,0 +1,79 @@
1
+ import {
2
+ ChevronLeft,
3
+ ChevronRight,
4
+ ChevronsLeft,
5
+ ChevronsRight,
6
+ } from "lucide-react";
7
+ import { CmsButton } from "./CmsButton";
8
+ import { cn } from "~/lib/cn";
9
+
10
+ export interface CmsPaginationProps {
11
+ currentPage: number;
12
+ totalPages: number;
13
+ onPageChange: (page: number) => void;
14
+ showFirstLast?: boolean;
15
+ className?: string;
16
+ }
17
+
18
+ export function CmsPagination({
19
+ currentPage,
20
+ totalPages,
21
+ onPageChange,
22
+ showFirstLast = true,
23
+ className,
24
+ }: CmsPaginationProps) {
25
+ const canGoPrev = currentPage > 1;
26
+ const canGoNext = currentPage < totalPages;
27
+
28
+ if (totalPages <= 1) {
29
+ return null;
30
+ }
31
+
32
+ return (
33
+ <div className={cn("flex items-center justify-center gap-1", className)}>
34
+ {showFirstLast && (
35
+ <CmsButton
36
+ variant="ghost"
37
+ size="sm"
38
+ onClick={() => onPageChange(1)}
39
+ disabled={!canGoPrev}
40
+ aria-label="First page"
41
+ >
42
+ <ChevronsLeft className="size-4" />
43
+ </CmsButton>
44
+ )}
45
+ <CmsButton
46
+ variant="ghost"
47
+ size="sm"
48
+ onClick={() => onPageChange(currentPage - 1)}
49
+ disabled={!canGoPrev}
50
+ aria-label="Previous page"
51
+ >
52
+ <ChevronLeft className="size-4" />
53
+ </CmsButton>
54
+ <span className="px-3 text-sm text-muted-foreground">
55
+ Page {currentPage} of {totalPages}
56
+ </span>
57
+ <CmsButton
58
+ variant="ghost"
59
+ size="sm"
60
+ onClick={() => onPageChange(currentPage + 1)}
61
+ disabled={!canGoNext}
62
+ aria-label="Next page"
63
+ >
64
+ <ChevronRight className="size-4" />
65
+ </CmsButton>
66
+ {showFirstLast && (
67
+ <CmsButton
68
+ variant="ghost"
69
+ size="sm"
70
+ onClick={() => onPageChange(totalPages)}
71
+ disabled={!canGoNext}
72
+ aria-label="Last page"
73
+ >
74
+ <ChevronsRight className="size-4" />
75
+ </CmsButton>
76
+ )}
77
+ </div>
78
+ );
79
+ }
@@ -0,0 +1,59 @@
1
+ import {
2
+ Select,
3
+ SelectContent,
4
+ SelectItem,
5
+ SelectTrigger,
6
+ SelectValue,
7
+ } from "~/components/ui/select";
8
+ import { cn } from "~/lib/cn";
9
+
10
+ export interface CmsSelectOption {
11
+ value: string;
12
+ label: string;
13
+ disabled?: boolean;
14
+ }
15
+
16
+ export interface CmsSelectProps {
17
+ value?: string;
18
+ onValueChange?: (value: string) => void;
19
+ options: CmsSelectOption[];
20
+ placeholder?: string;
21
+ disabled?: boolean;
22
+ error?: boolean;
23
+ className?: string;
24
+ }
25
+
26
+ export function CmsSelect({
27
+ value,
28
+ onValueChange,
29
+ options,
30
+ placeholder = "Select...",
31
+ disabled,
32
+ error,
33
+ className,
34
+ }: CmsSelectProps) {
35
+ return (
36
+ <Select value={value} onValueChange={onValueChange} disabled={disabled}>
37
+ <SelectTrigger
38
+ className={cn(
39
+ error && "border-destructive focus:ring-destructive",
40
+ className,
41
+ )}
42
+ aria-invalid={error}
43
+ >
44
+ <SelectValue placeholder={placeholder} />
45
+ </SelectTrigger>
46
+ <SelectContent>
47
+ {options.map((option) => (
48
+ <SelectItem
49
+ key={option.value}
50
+ value={option.value}
51
+ disabled={option.disabled}
52
+ >
53
+ {option.label}
54
+ </SelectItem>
55
+ ))}
56
+ </SelectContent>
57
+ </Select>
58
+ );
59
+ }
@@ -0,0 +1,79 @@
1
+ import * as React from 'react'
2
+ import { CmsSurface } from './CmsSurface'
3
+ import { cn } from '~/lib/cn'
4
+
5
+ export interface CmsStatCardProps {
6
+ title: string
7
+ value: string | number
8
+ description?: string
9
+ icon?: React.ReactNode
10
+ trend?: { value: number; label: string }
11
+ onClick?: () => void
12
+ isLoading?: boolean
13
+ className?: string
14
+ }
15
+
16
+ export function CmsStatCard({
17
+ title,
18
+ value,
19
+ description,
20
+ icon,
21
+ trend,
22
+ onClick,
23
+ isLoading,
24
+ className,
25
+ }: CmsStatCardProps) {
26
+ const content = (
27
+ <>
28
+ <div className="flex items-start justify-between">
29
+ <div className="space-y-1">
30
+ <p className="text-sm font-medium text-muted-foreground">{title}</p>
31
+ {isLoading ? (
32
+ <div className="h-8 w-16 animate-pulse rounded bg-muted" />
33
+ ) : (
34
+ <p className="text-2xl font-semibold text-foreground">{value}</p>
35
+ )}
36
+ {description && (
37
+ <p className="text-xs text-muted-foreground">{description}</p>
38
+ )}
39
+ </div>
40
+ {icon && (
41
+ <div className="flex size-10 items-center justify-center rounded-lg bg-primary/10 text-primary">
42
+ {icon}
43
+ </div>
44
+ )}
45
+ </div>
46
+ {trend && !isLoading && (
47
+ <div className={cn(
48
+ "mt-2 text-xs font-medium",
49
+ trend.value >= 0 ? "text-diff-added-foreground" : "text-diff-removed-foreground"
50
+ )}>
51
+ {trend.value >= 0 ? "+" : ""}{trend.value}% {trend.label}
52
+ </div>
53
+ )}
54
+ </>
55
+ )
56
+
57
+ if (onClick) {
58
+ return (
59
+ <button
60
+ type="button"
61
+ onClick={onClick}
62
+ className={cn(
63
+ "text-left w-full cursor-pointer transition-colors hover:bg-accent/50",
64
+ className
65
+ )}
66
+ >
67
+ <CmsSurface elevation="base" padding="md" className="h-full">
68
+ {content}
69
+ </CmsSurface>
70
+ </button>
71
+ )
72
+ }
73
+
74
+ return (
75
+ <CmsSurface elevation="base" padding="md" className={cn("h-full", className)}>
76
+ {content}
77
+ </CmsSurface>
78
+ )
79
+ }
@@ -96,7 +96,7 @@ const colorToClassName: Record<WorkflowStateColor, string> = {
96
96
  blue: "bg-info-bg text-info-foreground",
97
97
  green: "bg-diff-added-bg text-diff-added-foreground",
98
98
  red: "bg-diff-removed-bg text-diff-removed-foreground",
99
- purple: "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400",
99
+ purple: "bg-purple-bg text-purple-foreground",
100
100
  orange: "bg-diff-modified-bg text-diff-modified-foreground",
101
101
  };
102
102
 
@@ -8,3 +8,8 @@ export { CmsToolbar, type CmsToolbarProps } from './CmsToolbar'
8
8
  export { CmsDropdown, type CmsDropdownProps, type CmsDropdownAction } from './CmsDropdown'
9
9
  export { CmsTable, type CmsTableProps, type CmsTableColumn } from './CmsTable'
10
10
  export { CmsDialog, CmsConfirmDialog, type CmsDialogProps, type CmsConfirmDialogProps } from './CmsDialog'
11
+ export { CmsInput, type CmsInputProps } from './CmsInput'
12
+ export { CmsSelect, type CmsSelectProps, type CmsSelectOption } from './CmsSelect'
13
+ export { CmsFilterBar, type CmsFilterBarProps, type CmsFilterBarFilter } from './CmsFilterBar'
14
+ export { CmsStatCard, type CmsStatCardProps } from './CmsStatCard'
15
+ export { CmsPagination, type CmsPaginationProps } from './CmsPagination'
@@ -13,6 +13,7 @@ interface ThemeContextValue {
13
13
  theme: Theme
14
14
  resolvedTheme: 'light' | 'dark'
15
15
  setTheme: (theme: Theme) => void
16
+ canToggleDarkMode: boolean
16
17
  }
17
18
 
18
19
  const ThemeContext = createContext<ThemeContextValue | null>(null)
@@ -33,9 +34,36 @@ function getStoredTheme(): Theme {
33
34
  return 'system'
34
35
  }
35
36
 
36
- export function ThemeProvider({ children }: { children: ReactNode }) {
37
- const [theme, setThemeState] = useState<Theme>(() => getStoredTheme())
37
+ function getParentDarkMode(): boolean {
38
+ if (typeof window === 'undefined') return false
39
+ return document.documentElement.classList.contains('dark')
40
+ }
41
+
42
+ export interface ThemeProviderProps {
43
+ children: ReactNode
44
+ themeMode?: 'isolated' | 'inherit'
45
+ darkModeControl?: 'independent' | 'follow-parent'
46
+ }
47
+
48
+ export function ThemeProvider({
49
+ children,
50
+ themeMode = 'isolated',
51
+ darkModeControl = 'independent',
52
+ }: ThemeProviderProps) {
53
+ const canToggleDarkMode = themeMode === 'isolated' || darkModeControl === 'independent'
54
+ const shouldFollowParent = themeMode === 'inherit' && darkModeControl === 'follow-parent'
55
+
56
+ const [theme, setThemeState] = useState<Theme>(() => {
57
+ if (shouldFollowParent) {
58
+ return getParentDarkMode() ? 'dark' : 'light'
59
+ }
60
+ return getStoredTheme()
61
+ })
62
+
38
63
  const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>(() => {
64
+ if (shouldFollowParent) {
65
+ return getParentDarkMode() ? 'dark' : 'light'
66
+ }
39
67
  const stored = getStoredTheme()
40
68
  return stored === 'system' ? getSystemTheme() : stored
41
69
  })
@@ -44,39 +72,79 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
44
72
  const resolved = newTheme === 'system' ? getSystemTheme() : newTheme
45
73
  setResolvedTheme(resolved)
46
74
 
47
- const root = document.documentElement
48
- root.classList.remove('light', 'dark')
49
- root.classList.add(resolved)
50
- }, [])
75
+ if (canToggleDarkMode) {
76
+ const root = document.documentElement
77
+ root.classList.remove('light', 'dark')
78
+ root.classList.add(resolved)
79
+ }
80
+ }, [canToggleDarkMode])
51
81
 
52
82
  const setTheme = useCallback(
53
83
  (newTheme: Theme) => {
84
+ if (!canToggleDarkMode) return
85
+
54
86
  setThemeState(newTheme)
55
87
  localStorage.setItem(STORAGE_KEY, newTheme)
56
88
  applyTheme(newTheme)
57
89
  },
58
- [applyTheme]
90
+ [applyTheme, canToggleDarkMode]
59
91
  )
60
92
 
61
93
  useEffect(() => {
62
- applyTheme(theme)
63
- }, [theme, applyTheme])
94
+ if (!shouldFollowParent) {
95
+ applyTheme(theme)
96
+ }
97
+ }, [theme, applyTheme, shouldFollowParent])
64
98
 
65
99
  useEffect(() => {
66
- const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
100
+ if (!shouldFollowParent) {
101
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
67
102
 
68
- const handleChange = () => {
69
- if (theme === 'system') {
70
- applyTheme('system')
103
+ const handleChange = () => {
104
+ if (theme === 'system') {
105
+ applyTheme('system')
106
+ }
71
107
  }
108
+
109
+ mediaQuery.addEventListener('change', handleChange)
110
+ return () => mediaQuery.removeEventListener('change', handleChange)
111
+ }
112
+ }, [theme, applyTheme, shouldFollowParent])
113
+
114
+ useEffect(() => {
115
+ if (!shouldFollowParent) return
116
+
117
+ const handleParentThemeChange = () => {
118
+ const isDark = getParentDarkMode()
119
+ const newTheme = isDark ? 'dark' : 'light'
120
+ setThemeState(newTheme)
121
+ setResolvedTheme(newTheme)
72
122
  }
73
123
 
74
- mediaQuery.addEventListener('change', handleChange)
75
- return () => mediaQuery.removeEventListener('change', handleChange)
76
- }, [theme, applyTheme])
124
+ handleParentThemeChange()
125
+
126
+ const observer = new MutationObserver((mutations) => {
127
+ for (const mutation of mutations) {
128
+ if (
129
+ mutation.type === 'attributes' &&
130
+ mutation.attributeName === 'class'
131
+ ) {
132
+ handleParentThemeChange()
133
+ break
134
+ }
135
+ }
136
+ })
137
+
138
+ observer.observe(document.documentElement, {
139
+ attributes: true,
140
+ attributeFilter: ['class'],
141
+ })
142
+
143
+ return () => observer.disconnect()
144
+ }, [shouldFollowParent])
77
145
 
78
146
  return (
79
- <ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
147
+ <ThemeContext.Provider value={{ theme, resolvedTheme, setTheme, canToggleDarkMode }}>
80
148
  {children}
81
149
  </ThemeContext.Provider>
82
150
  )
@@ -11,7 +11,7 @@ import { useEmbedNavigation } from "../navigation";
11
11
  export function EmbedHeader() {
12
12
  const { goBack, canGoBack, currentRoute } = useEmbedNavigation();
13
13
  const { branding: _branding } = useAdminConfig();
14
- const { theme, setTheme } = useTheme();
14
+ const { theme, setTheme, canToggleDarkMode } = useTheme();
15
15
  const { user, logout, isAuthenticated } = useAuth();
16
16
 
17
17
  const routeTitles: Record<string, string> = {
@@ -44,14 +44,16 @@ export function EmbedHeader() {
44
44
  </div>
45
45
 
46
46
  <div className="flex items-center gap-2">
47
- <button
48
- type="button"
49
- onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
50
- className="flex size-9 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
51
- title={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
52
- >
53
- {theme === "dark" ? <Sun className="size-5" /> : <Moon className="size-5" />}
54
- </button>
47
+ {canToggleDarkMode && (
48
+ <button
49
+ type="button"
50
+ onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
51
+ className="flex size-9 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
52
+ title={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
53
+ >
54
+ {theme === "dark" ? <Sun className="size-5" /> : <Moon className="size-5" />}
55
+ </button>
56
+ )}
55
57
 
56
58
  <button
57
59
  type="button"
@@ -3,7 +3,6 @@
3
3
  *
4
4
  * A router-agnostic layout for the embedded admin that uses
5
5
  * EmbedSidebar instead of the router-dependent Sidebar.
6
- * Uses CSS Grid for proper container-relative positioning.
7
6
  */
8
7
 
9
8
  import type { ReactNode } from "react";
@@ -19,12 +18,9 @@ export function EmbedLayout({ children }: EmbedLayoutProps) {
19
18
  const { layout } = useAdminConfig();
20
19
 
21
20
  return (
22
- <div
23
- className="grid h-full bg-background"
24
- style={{ gridTemplateColumns: `${layout.sidebarWidth}px 1fr` }}
25
- >
21
+ <div className="flex min-h-screen bg-background">
26
22
  <EmbedSidebar />
27
- <div className="flex flex-col overflow-hidden">
23
+ <div className="flex flex-1 flex-col" style={{ marginLeft: layout.sidebarWidth }}>
28
24
  <EmbedHeader />
29
25
  <main className="flex-1 overflow-auto p-6">{children}</main>
30
26
  </div>
@@ -36,9 +36,7 @@ function pathToRoute(path: string): EmbedRoute {
36
36
  export function EmbedSidebar() {
37
37
  const { currentPath, navigate, navigateToContentType } = useEmbedNavigation();
38
38
  const config = useAdminConfig();
39
- const { navItems, branding,
40
- // layout
41
- } = config;
39
+ const { navItems, branding, layout } = config;
42
40
  const api = useApi();
43
41
 
44
42
  const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
@@ -154,9 +152,14 @@ export function EmbedSidebar() {
154
152
  </Collapsible>
155
153
  );
156
154
 
155
+ const sidebarWidth = layout.sidebarWidth;
156
+
157
157
  return (
158
158
  <>
159
- <aside className="sticky top-0 z-40 flex h-full flex-col overflow-hidden border-r border-sidebar-border bg-sidebar">
159
+ <aside
160
+ className="fixed inset-y-0 left-0 z-50 flex flex-col border-r border-sidebar-border bg-sidebar"
161
+ style={{ width: sidebarWidth }}
162
+ >
160
163
  <div className="flex h-14 items-center gap-2 border-b border-sidebar-border px-4">
161
164
  <button
162
165
  type="button"
@@ -36,6 +36,7 @@
36
36
 
37
37
  import { useConvex } from "convex/react";
38
38
  import { useMemo } from "react";
39
+ import { cn } from "../lib/cn";
39
40
  import { SettingsConfigProvider } from "../contexts/SettingsConfigContext";
40
41
  import {
41
42
  AuthProvider,
@@ -46,7 +47,6 @@ import {
46
47
  import { ThemeProvider } from "../contexts/ThemeContext";
47
48
  import { RouteGuard } from "../components/RouteGuard";
48
49
  import { resolveAdminConfig } from "../lib/admin-config";
49
- import { cn } from "../lib/cn";
50
50
  import type { CmsAdminProps, CmsAdminAuthConfig } from "./types";
51
51
  import { ApiProvider } from "./contexts/ApiContext";
52
52
  import {
@@ -129,6 +129,7 @@ export function CmsAdmin({
129
129
  basePath = "/admin",
130
130
  className,
131
131
  themeMode = "isolated",
132
+ darkModeControl = "independent",
132
133
  initialRoute = "dashboard",
133
134
  onNavigate,
134
135
  }: CmsAdminProps & {
@@ -165,7 +166,7 @@ export function CmsAdmin({
165
166
  return (
166
167
  <div className={cn("h-full", className)} data-cms-admin={themeMode}>
167
168
  <ApiProvider api={api}>
168
- <ThemeProvider>
169
+ <ThemeProvider themeMode={themeMode} darkModeControl={darkModeControl}>
169
170
  <SettingsConfigProvider baseConfig={adminConfig} api={settingsApi}>
170
171
  <AuthProvider
171
172
  getUser={authConfig.getUser}
@@ -26,4 +26,10 @@ export interface CmsAdminProps {
26
26
  * - 'inherit': Admin inherits parent app's CSS variables (for shadcn apps)
27
27
  */
28
28
  themeMode?: "isolated" | "inherit";
29
+ /**
30
+ * Dark mode control behavior:
31
+ * - 'independent' (default): Admin has its own dark mode toggle
32
+ * - 'follow-parent': Admin follows parent app's dark mode (hides toggle in inherit mode)
33
+ */
34
+ darkModeControl?: "independent" | "follow-parent";
29
35
  }