@djangocfg/layouts 2.1.256 → 2.1.259

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.
Files changed (36) hide show
  1. package/README.md +101 -203
  2. package/package.json +18 -18
  3. package/src/index.ts +4 -1
  4. package/src/layouts/AppLayout/AppLayout.tsx +97 -8
  5. package/src/layouts/AppLayout/BaseApp.tsx +2 -0
  6. package/src/layouts/AppLayout/index.ts +6 -0
  7. package/src/layouts/PrivateLayout/PrivateLayout.tsx +3 -1
  8. package/src/layouts/PrivateLayout/components/PrivateContent.tsx +15 -4
  9. package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +3 -3
  10. package/src/layouts/PublicLayout/PublicLayout.tsx +82 -17
  11. package/src/layouts/PublicLayout/components/PublicFooter/FooterProjectInfo.tsx +17 -24
  12. package/src/layouts/PublicLayout/components/PublicFooter/PublicFooter.tsx +79 -95
  13. package/src/layouts/PublicLayout/components/PublicFooter/index.ts +2 -0
  14. package/src/layouts/PublicLayout/components/PublicFooter/types.ts +41 -31
  15. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +84 -40
  16. package/src/layouts/PublicLayout/components/PublicNavbar.tsx +22 -35
  17. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +184 -98
  18. package/src/layouts/PublicLayout/components/ThemeBrandMark.tsx +83 -0
  19. package/src/layouts/PublicLayout/components/index.ts +2 -0
  20. package/src/layouts/PublicLayout/context.tsx +5 -0
  21. package/src/layouts/PublicLayout/hooks/index.ts +1 -1
  22. package/src/layouts/PublicLayout/hooks/useMobileNavPanel.ts +55 -0
  23. package/src/layouts/PublicLayout/index.ts +8 -0
  24. package/src/layouts/PublicLayout/navbarTypes.ts +20 -0
  25. package/src/layouts/PublicLayout/publicShellShadow.ts +12 -0
  26. package/src/layouts/_components/PrivateSidebarAccount.tsx +16 -3
  27. package/src/layouts/_components/UserMenu.tsx +133 -30
  28. package/src/layouts/types/index.ts +10 -1
  29. package/src/layouts/types/providers.types.ts +10 -0
  30. package/src/layouts/types/ui.types.ts +9 -0
  31. package/src/theme/ThemeStyleBridge.tsx +41 -0
  32. package/src/theme/buildThemeStyleSheet.ts +71 -0
  33. package/src/theme/index.ts +16 -0
  34. package/src/theme/themeStyle.types.ts +89 -0
  35. package/src/theme/themeStylePresets.ts +202 -0
  36. package/src/layouts/PublicLayout/hooks/useFloatingPanel.ts +0 -61
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Shared “floating glass” chrome for navbar + mobile drawer.
3
+ *
4
+ * Light: barely-there lift. Dark: one soft outer shadow (border already defines the edge).
5
+ */
6
+ export const publicFloatingChromeClassName =
7
+ [
8
+ '!border-0 ring-0 outline-none',
9
+ 'shadow-[0_1px_6px_rgba(0,0,0,0.028)]',
10
+ 'dark:!border dark:!border-border/75',
11
+ 'dark:shadow-[0_3px_14px_rgba(0,0,0,0.07)]',
12
+ ].join(' ');
@@ -30,6 +30,17 @@ import { LocaleSwitcher } from './LocaleSwitcher';
30
30
  import type { I18nLayoutConfig } from '../AppLayout/AppLayout';
31
31
  import type { HeaderConfig } from '../PrivateLayout/PrivateLayout';
32
32
 
33
+ /** Radix portals (dropdown, select, popover) render outside the account node — ignore those clicks for “outside”. */
34
+ function isPointerFromRadixOverlay(target: EventTarget | null): boolean {
35
+ if (!target || !(target instanceof Element)) return false;
36
+ return Boolean(
37
+ target.closest('[data-radix-popper-content-wrapper]') ||
38
+ target.closest('[data-radix-dropdown-menu-content]') ||
39
+ target.closest('[data-radix-select-content]') ||
40
+ target.closest('[data-radix-popover-content]'),
41
+ );
42
+ }
43
+
33
44
  interface PrivateSidebarAccountProps {
34
45
  header?: HeaderConfig;
35
46
  i18n?: I18nLayoutConfig;
@@ -53,9 +64,11 @@ export function PrivateSidebarAccount({ header, i18n }: PrivateSidebarAccountPro
53
64
 
54
65
  const handlePointerDown = (event: PointerEvent) => {
55
66
  const root = accountRootRef.current;
56
- if (root && !root.contains(event.target as Node)) {
57
- setAccountOpen(false);
58
- }
67
+ const target = event.target;
68
+ if (!(target instanceof Node)) return;
69
+ if (root?.contains(target)) return;
70
+ if (isPointerFromRadixOverlay(target)) return;
71
+ setAccountOpen(false);
59
72
  };
60
73
 
61
74
  document.addEventListener('pointerdown', handlePointerDown);
@@ -30,7 +30,7 @@
30
30
 
31
31
  'use client';
32
32
 
33
- import { ArrowRight, LogOut } from 'lucide-react';
33
+ import { ArrowRight, Globe, LogOut } from 'lucide-react';
34
34
  import Link from 'next/link';
35
35
  import React, { useMemo } from 'react';
36
36
 
@@ -39,12 +39,26 @@ import { useAppT } from '@djangocfg/i18n';
39
39
 
40
40
  import { useLogout } from '../../hooks';
41
41
  import {
42
- Avatar, AvatarFallback, AvatarImage, Button, DropdownMenu, DropdownMenuContent,
43
- DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator,
44
- DropdownMenuTrigger
42
+ Avatar,
43
+ AvatarFallback,
44
+ AvatarImage,
45
+ Button,
46
+ DropdownMenu,
47
+ DropdownMenuContent,
48
+ DropdownMenuGroup,
49
+ DropdownMenuItem,
50
+ DropdownMenuLabel,
51
+ DropdownMenuSeparator,
52
+ DropdownMenuSub,
53
+ DropdownMenuSubContent,
54
+ DropdownMenuSubTrigger,
55
+ DropdownMenuTrigger,
56
+ getLanguageFlag,
45
57
  } from '@djangocfg/ui-core/components';
46
58
 
47
- import type { UserMenuGroup } from '../types';
59
+ import { LOCALE_LABELS } from './LocaleSwitcher';
60
+
61
+ import type { UserMenuGroup, UserMenuLocaleConfig } from '../types';
48
62
 
49
63
  export interface UserMenuProps {
50
64
  /** Display variant */
@@ -53,12 +67,18 @@ export interface UserMenuProps {
53
67
  groups?: UserMenuGroup[];
54
68
  /** Auth page path (for sign in button) */
55
69
  authPath?: string;
70
+ /**
71
+ * Language switching inside this menu (desktop: submenu; mobile: button row).
72
+ * Prefer this over nesting `<LocaleSwitcher />` in the same dropdown — nested menus close each other.
73
+ */
74
+ i18n?: UserMenuLocaleConfig;
56
75
  }
57
76
 
58
77
  export function UserMenu({
59
78
  variant = 'desktop',
60
79
  groups,
61
80
  authPath = '/auth',
81
+ i18n,
62
82
  }: UserMenuProps) {
63
83
  const { user, isAuthenticated } = useAuth();
64
84
  const handleLogout = useLogout();
@@ -69,36 +89,43 @@ export function UserMenu({
69
89
  signIn: t('layouts.profile.login'),
70
90
  signOut: t('layouts.profile.signOut'),
71
91
  userMenu: t('layouts.profile.userMenu'),
92
+ language: t('layouts.profile.language'),
72
93
  }), [t]);
73
94
 
74
95
  React.useEffect(() => {
75
96
  setMounted(true);
76
97
  }, []);
77
98
 
78
- // Prepare menu groups
79
- // Must be before early return to maintain hook order
80
- const menuGroups: UserMenuGroup[] = React.useMemo(() => {
81
- const allGroups: UserMenuGroup[] = [];
99
+ /** Profile links only; sign out rendered separately so we can insert locale UI between. */
100
+ const profileGroups: UserMenuGroup[] = React.useMemo(() => {
101
+ if (groups && groups.length > 0) return [...groups];
102
+ return [];
103
+ }, [groups]);
82
104
 
83
- // Add custom groups if provided
84
- if (groups && groups.length > 0) {
85
- allGroups.push(...groups);
86
- }
105
+ const signOutItem = React.useMemo(
106
+ () => ({
107
+ label: labels.signOut,
108
+ onClick: handleLogout,
109
+ icon: LogOut,
110
+ variant: 'destructive' as const,
111
+ }),
112
+ [handleLogout, labels.signOut],
113
+ );
87
114
 
88
- // Always add Sign Out at the end
89
- allGroups.push({
90
- items: [
91
- {
92
- label: labels.signOut,
93
- onClick: handleLogout,
94
- icon: LogOut,
95
- variant: 'destructive',
96
- },
97
- ],
98
- });
115
+ /** Prepared locale UI data avoid `i18n && i18n.locales.length` chains in JSX. */
116
+ const localeMenu = React.useMemo(() => {
117
+ const codes = i18n?.locales;
118
+ if (!i18n || !codes?.length) return null;
119
+ return {
120
+ codes,
121
+ current: i18n.locale,
122
+ onChange: i18n.onLocaleChange,
123
+ };
124
+ }, [i18n]);
99
125
 
100
- return allGroups;
101
- }, [groups, handleLogout, labels.signOut]);
126
+ const hasProfileGroups = profileGroups.length > 0;
127
+
128
+ const localeLabel = (code: string) => LOCALE_LABELS[code] || code.toUpperCase();
102
129
 
103
130
  if (!mounted) {
104
131
  return null;
@@ -111,7 +138,7 @@ export function UserMenu({
111
138
  <div className="pt-4 border-t border-border/50">
112
139
  <Link
113
140
  href={authPath}
114
- className="group flex items-center justify-between rounded-lg px-2 py-2.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground hover:bg-accent/40"
141
+ className="group flex items-center justify-between rounded-full px-4 py-2.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground hover:bg-accent/40"
115
142
  >
116
143
  <span>{labels.signIn}</span>
117
144
  <ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
@@ -121,7 +148,7 @@ export function UserMenu({
121
148
  }
122
149
  return (
123
150
  <Link href={authPath}>
124
- <Button variant="default" size="sm">
151
+ <Button variant="default" size="sm" className="rounded-full px-5">
125
152
  {labels.signIn}
126
153
  </Button>
127
154
  </Link>
@@ -151,7 +178,7 @@ export function UserMenu({
151
178
  </div>
152
179
  </div>
153
180
  <div className="space-y-1">
154
- {menuGroups.map((group, groupIndex) => (
181
+ {profileGroups.map((group, groupIndex) => (
155
182
  <div key={groupIndex}>
156
183
  {group.title && (
157
184
  <div className="px-4 py-2">
@@ -195,6 +222,44 @@ export function UserMenu({
195
222
  })}
196
223
  </div>
197
224
  ))}
225
+ {localeMenu && (
226
+ <div className="border-t border-border/50 px-4 pt-3">
227
+ <p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
228
+ {labels.language}
229
+ </p>
230
+ <div className="mt-2 flex flex-wrap gap-2">
231
+ {localeMenu.codes.map((code) => {
232
+ const flag = getLanguageFlag(code);
233
+ const active = code === localeMenu.current;
234
+ return (
235
+ <button
236
+ key={code}
237
+ type="button"
238
+ onClick={() => localeMenu.onChange(code)}
239
+ className={`rounded-md border px-2.5 py-1.5 text-sm font-medium transition-colors ${
240
+ active
241
+ ? 'border-primary bg-accent text-foreground'
242
+ : 'border-border/60 text-muted-foreground hover:bg-accent hover:text-foreground'
243
+ }`}
244
+ >
245
+ {flag ? <span className="mr-1.5">{flag}</span> : null}
246
+ {localeLabel(code)}
247
+ </button>
248
+ );
249
+ })}
250
+ </div>
251
+ </div>
252
+ )}
253
+ <div>
254
+ <button
255
+ type="button"
256
+ onClick={signOutItem.onClick}
257
+ className="flex w-full items-center gap-3 rounded-sm px-4 py-3 text-left text-sm font-medium text-destructive transition-colors hover:bg-destructive/10"
258
+ >
259
+ <LogOut className="h-4 w-4" />
260
+ {signOutItem.label}
261
+ </button>
262
+ </div>
198
263
  </div>
199
264
  </div>
200
265
  );
@@ -222,7 +287,7 @@ export function UserMenu({
222
287
  </div>
223
288
  </DropdownMenuLabel>
224
289
  <DropdownMenuSeparator />
225
- {menuGroups.map((group, groupIndex) => (
290
+ {profileGroups.map((group, groupIndex) => (
226
291
  <React.Fragment key={groupIndex}>
227
292
  {groupIndex > 0 && <DropdownMenuSeparator />}
228
293
  <DropdownMenuGroup>
@@ -267,6 +332,44 @@ export function UserMenu({
267
332
  </DropdownMenuGroup>
268
333
  </React.Fragment>
269
334
  ))}
335
+ {localeMenu && (
336
+ <>
337
+ {hasProfileGroups && <DropdownMenuSeparator />}
338
+ <DropdownMenuSub>
339
+ <DropdownMenuSubTrigger className="cursor-default">
340
+ <Globe className="mr-2 h-4 w-4" />
341
+ <span>{labels.language}</span>
342
+ </DropdownMenuSubTrigger>
343
+ <DropdownMenuSubContent>
344
+ {localeMenu.codes.map((code) => {
345
+ const flag = getLanguageFlag(code);
346
+ return (
347
+ <DropdownMenuItem
348
+ key={code}
349
+ onSelect={() => {
350
+ localeMenu.onChange(code);
351
+ }}
352
+ className={code === localeMenu.current ? 'bg-accent' : ''}
353
+ >
354
+ {flag ? <span className="mr-2">{flag}</span> : null}
355
+ {localeLabel(code)}
356
+ </DropdownMenuItem>
357
+ );
358
+ })}
359
+ </DropdownMenuSubContent>
360
+ </DropdownMenuSub>
361
+ </>
362
+ )}
363
+ <DropdownMenuSeparator />
364
+ <DropdownMenuGroup>
365
+ <DropdownMenuItem
366
+ onClick={signOutItem.onClick}
367
+ className="text-destructive focus:text-destructive"
368
+ >
369
+ <LogOut className="mr-2 h-4 w-4" />
370
+ <span>{signOutItem.label}</span>
371
+ </DropdownMenuItem>
372
+ </DropdownMenuGroup>
270
373
  </DropdownMenuContent>
271
374
  </DropdownMenu>
272
375
  );
@@ -10,7 +10,15 @@
10
10
  // ============================================================================
11
11
 
12
12
  // Local provider configs
13
- export type { ThemeConfig, SWRConfigOptions, CentrifugoConfig } from './providers.types';
13
+ export type {
14
+ ThemeConfig,
15
+ ThemeStyleConfig,
16
+ ThemeCssVarKey,
17
+ ThemeCssVarMap,
18
+ ThemeStylePresetId,
19
+ SWRConfigOptions,
20
+ CentrifugoConfig,
21
+ } from './providers.types';
14
22
 
15
23
  // External provider configs (re-export for convenience)
16
24
  export type { AnalyticsConfig } from '../../snippets/Analytics/types';
@@ -36,6 +44,7 @@ export type {
36
44
  FooterConfig,
37
45
  UserMenuItem,
38
46
  UserMenuGroup,
47
+ UserMenuLocaleConfig,
39
48
  UserMenuConfig,
40
49
  } from './ui.types';
41
50
 
@@ -5,6 +5,11 @@
5
5
  * Note: Analytics, PWA, Push, and Error types are defined in their respective modules
6
6
  */
7
7
 
8
+ import type { ThemeStyleConfig } from '../../theme/themeStyle.types';
9
+
10
+ // Re-export for consumers that only import from `layouts/types`
11
+ export type { ThemeStyleConfig, ThemeCssVarKey, ThemeCssVarMap, ThemeStylePresetId } from '../../theme/themeStyle.types';
12
+
8
13
  // ============================================================================
9
14
  // Theme Configuration
10
15
  // ============================================================================
@@ -12,6 +17,11 @@
12
17
  export interface ThemeConfig {
13
18
  defaultTheme?: 'light' | 'dark' | 'system';
14
19
  storageKey?: string;
20
+ /**
21
+ * Typed CSS variable presets and per-mode overrides (see `ThemeStyleBridge` in BaseApp).
22
+ * For heavy visual editing, use the playground Theme Configurator and export CSS instead.
23
+ */
24
+ style?: ThemeStyleConfig;
15
25
  }
16
26
 
17
27
  // ============================================================================
@@ -97,9 +97,18 @@ export interface UserMenuGroup {
97
97
  items: UserMenuItem[];
98
98
  }
99
99
 
100
+ /** Optional locale switching inside the user menu (submenu on desktop; avoids nested `DropdownMenu` + portal issues). */
101
+ export interface UserMenuLocaleConfig {
102
+ locale: string;
103
+ locales: string[];
104
+ onLocaleChange: (locale: string) => void;
105
+ }
106
+
100
107
  export interface UserMenuConfig {
101
108
  /** Menu groups for authenticated users */
102
109
  groups?: UserMenuGroup[];
103
110
  /** Auth page path (for sign in button) */
104
111
  authPath?: string;
112
+ /** When set, language is rendered inside the user menu (not as a separate nested dropdown). */
113
+ i18n?: UserMenuLocaleConfig;
105
114
  }
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useMemo } from 'react';
4
+
5
+ import { buildThemeStyleSheet } from './buildThemeStyleSheet';
6
+
7
+ import type { ThemeStyleConfig } from './themeStyle.types';
8
+
9
+ const STYLE_ELEMENT_ID = 'djangocfg-baseapp-theme-style';
10
+
11
+ export interface ThemeStyleBridgeProps {
12
+ style?: ThemeStyleConfig;
13
+ }
14
+
15
+ /**
16
+ * Injects merged theme CSS variables after globals (preset + typed overrides).
17
+ * Renders nothing; safe to mount once under ThemeProvider.
18
+ */
19
+ export function ThemeStyleBridge({ style }: ThemeStyleBridgeProps) {
20
+ const css = useMemo(() => buildThemeStyleSheet(style), [style]);
21
+
22
+ useEffect(() => {
23
+ if (typeof document === 'undefined') return;
24
+
25
+ let el = document.getElementById(STYLE_ELEMENT_ID) as HTMLStyleElement | null;
26
+ if (!css) {
27
+ el?.remove();
28
+ return;
29
+ }
30
+
31
+ if (!el) {
32
+ el = document.createElement('style');
33
+ el.id = STYLE_ELEMENT_ID;
34
+ document.head.appendChild(el);
35
+ }
36
+
37
+ el.textContent = css;
38
+ }, [css]);
39
+
40
+ return null;
41
+ }
@@ -0,0 +1,71 @@
1
+ import type { ThemeCssVarMap, ThemeStyleConfig, ThemeStylePresetId } from './themeStyle.types';
2
+ import { THEME_STYLE_PRESETS } from './themeStylePresets';
3
+
4
+ function mergeLayer(
5
+ base: ThemeCssVarMap | undefined,
6
+ over: ThemeCssVarMap | undefined
7
+ ): ThemeCssVarMap {
8
+ return { ...base, ...over };
9
+ }
10
+
11
+ /**
12
+ * Tailwind v4 `rounded-*` utilities are backed by `--radius-*` scale variables (xs/sm/md/…),
13
+ * not by our semantic `--radius`.
14
+ *
15
+ * When semantic `radius` is present, emit the derived scale too so the injected stylesheet
16
+ * controls both semantic and Tailwind scale rounding across the UI.
17
+ */
18
+ function withTailwindRadiusScale(vars: ThemeCssVarMap): Array<[string, string]> {
19
+ const entries = Object.entries(vars);
20
+ const radius = vars.radius;
21
+ if (!radius) return entries;
22
+
23
+ const r = String(radius).trim();
24
+ if (!r) return entries;
25
+
26
+ const scale: Array<[string, string]> = [
27
+ ['radius-xs', `calc(${r} - 6px)`],
28
+ ['radius-sm', `calc(${r} - 4px)`],
29
+ ['radius-md', `calc(${r} - 2px)`],
30
+ ['radius-lg', r],
31
+ ['radius-xl', `calc(${r} + 4px)`],
32
+ ['radius-2xl', `calc(${r} + 8px)`],
33
+ ['radius-3xl', `calc(${r} + 12px)`],
34
+ ['radius-4xl', `calc(${r} + 16px)`],
35
+ ];
36
+
37
+ // Put derived values after semantic `radius` so they always win in the injected block.
38
+ return [...entries, ...scale];
39
+ }
40
+
41
+ /**
42
+ * Build a small stylesheet fragment for injection after globals.
43
+ * Order: preset (if not default) → `vars.light` / `vars.dark`.
44
+ */
45
+ export function buildThemeStyleSheet(style?: ThemeStyleConfig): string {
46
+ if (!style) return '';
47
+
48
+ const presetId: ThemeStylePresetId = style.preset ?? 'default';
49
+ const preset = THEME_STYLE_PRESETS[presetId] ?? THEME_STYLE_PRESETS.default;
50
+
51
+ const light = mergeLayer(preset.light, style.vars?.light);
52
+ const dark = mergeLayer(preset.dark, style.vars?.dark);
53
+
54
+ const blocks: string[] = [];
55
+
56
+ if (Object.keys(light).length > 0) {
57
+ const body = withTailwindRadiusScale(light)
58
+ .map(([k, v]) => ` --${k}: ${v};`)
59
+ .join('\n');
60
+ blocks.push(`:root {\n${body}\n}`);
61
+ }
62
+
63
+ if (Object.keys(dark).length > 0) {
64
+ const body = withTailwindRadiusScale(dark)
65
+ .map(([k, v]) => ` --${k}: ${v};`)
66
+ .join('\n');
67
+ blocks.push(`.dark {\n${body}\n}`);
68
+ }
69
+
70
+ return blocks.join('\n\n');
71
+ }
@@ -0,0 +1,16 @@
1
+ export type {
2
+ ThemeCssVarChartKey,
3
+ ThemeCssVarChromeKey,
4
+ ThemeCssVarColorKey,
5
+ ThemeCssVarKey,
6
+ ThemeCssVarMap,
7
+ ThemeCssVarSidebarKey,
8
+ ThemeStyleConfig,
9
+ ThemeStylePresetId,
10
+ } from './themeStyle.types';
11
+
12
+ export { THEME_STYLE_PRESETS, THEME_STYLE_PRESET_ORDER } from './themeStylePresets';
13
+
14
+ export { buildThemeStyleSheet } from './buildThemeStyleSheet';
15
+
16
+ export { ThemeStyleBridge, type ThemeStyleBridgeProps } from './ThemeStyleBridge';
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Typed theme token overrides for BaseApp (`theme.style`).
3
+ *
4
+ * Values are raw HSL components as in ui-core CSS, e.g. `192 90% 35%` (no `hsl()` wrapper).
5
+ * **`radius`** accepts any valid CSS length (`0.75rem`, `1rem`, …).
6
+ *
7
+ * ### Parity with the Theme Configurator playground
8
+ *
9
+ * The playground (`apps/playground`) uses **`ThemeData`**: nested objects (`colors.primary`, `radius`, …).
10
+ * This package maps the **same semantics** onto **global CSS variables** (`--primary`, `--radius`, …) that
11
+ * `buildThemeStyleSheet` injects. Playground-only buckets (**`shadows`**, **`typography`**, **`spacing`**, …)
12
+ * are **not** represented here — export full CSS from the configurator when you need those.
13
+ *
14
+ * Rough mapping: `colors.*` → kebab key without `Foreground` → `*-foreground`; `radius` → `radius`;
15
+ * `sidebar.*` → `sidebar-*`.
16
+ */
17
+
18
+ /** Core semantic colors (HSL triplets) */
19
+ export type ThemeCssVarColorKey =
20
+ | 'background'
21
+ | 'foreground'
22
+ | 'card'
23
+ | 'card-foreground'
24
+ | 'popover'
25
+ | 'popover-foreground'
26
+ | 'primary'
27
+ | 'primary-foreground'
28
+ | 'secondary'
29
+ | 'secondary-foreground'
30
+ | 'muted'
31
+ | 'muted-foreground'
32
+ | 'accent'
33
+ | 'accent-foreground'
34
+ | 'destructive'
35
+ | 'destructive-foreground';
36
+
37
+ /** Layout / focus tokens — `radius` is usually a length, rest are HSL or shared with colors */
38
+ export type ThemeCssVarChromeKey = 'border' | 'input' | 'ring' | 'radius';
39
+
40
+ export type ThemeCssVarSidebarKey =
41
+ | 'sidebar-background'
42
+ | 'sidebar-foreground'
43
+ | 'sidebar-primary'
44
+ | 'sidebar-primary-foreground'
45
+ | 'sidebar-accent'
46
+ | 'sidebar-accent-foreground'
47
+ | 'sidebar-border'
48
+ | 'sidebar-ring';
49
+
50
+ export type ThemeCssVarChartKey = 'chart-1' | 'chart-2' | 'chart-3' | 'chart-4' | 'chart-5';
51
+
52
+ /**
53
+ * Keys that match `--${key}` in `ui-core` theme files (`light.css` / `dark.css`).
54
+ * Use `ThemeCssVarMap` for partial overrides on top of a preset.
55
+ */
56
+ export type ThemeCssVarKey =
57
+ | ThemeCssVarColorKey
58
+ | ThemeCssVarChromeKey
59
+ | ThemeCssVarSidebarKey
60
+ | ThemeCssVarChartKey;
61
+
62
+ /** Partial map of semantic variables for one color mode */
63
+ export type ThemeCssVarMap = Partial<Record<ThemeCssVarKey, string>>;
64
+
65
+ /** Built-in bundles in `THEME_STYLE_PRESETS` — see README preset table. */
66
+ export type ThemeStylePresetId =
67
+ | 'default'
68
+ | 'django-cfg'
69
+ | 'ios'
70
+ | 'soft'
71
+ | 'dense'
72
+ | 'high-contrast';
73
+
74
+ /**
75
+ * Optional style layer: named preset + per-mode overrides.
76
+ * Merged as: globals (CSS imports) → **preset** → **`vars.light` / `vars.dark`** (later wins).
77
+ */
78
+ export interface ThemeStyleConfig {
79
+ /**
80
+ * Built-in token bundle. Use `'default'` or omit to rely only on imported CSS + `vars`.
81
+ * @default 'default'
82
+ */
83
+ preset?: ThemeStylePresetId;
84
+ /** Fine-grained overrides on top of globals and preset */
85
+ vars?: {
86
+ light?: ThemeCssVarMap;
87
+ dark?: ThemeCssVarMap;
88
+ };
89
+ }