@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.
- package/README.md +101 -203
- package/package.json +18 -18
- package/src/index.ts +4 -1
- package/src/layouts/AppLayout/AppLayout.tsx +97 -8
- package/src/layouts/AppLayout/BaseApp.tsx +2 -0
- package/src/layouts/AppLayout/index.ts +6 -0
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +3 -1
- package/src/layouts/PrivateLayout/components/PrivateContent.tsx +15 -4
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +3 -3
- package/src/layouts/PublicLayout/PublicLayout.tsx +82 -17
- package/src/layouts/PublicLayout/components/PublicFooter/FooterProjectInfo.tsx +17 -24
- package/src/layouts/PublicLayout/components/PublicFooter/PublicFooter.tsx +79 -95
- package/src/layouts/PublicLayout/components/PublicFooter/index.ts +2 -0
- package/src/layouts/PublicLayout/components/PublicFooter/types.ts +41 -31
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +84 -40
- package/src/layouts/PublicLayout/components/PublicNavbar.tsx +22 -35
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +184 -98
- package/src/layouts/PublicLayout/components/ThemeBrandMark.tsx +83 -0
- package/src/layouts/PublicLayout/components/index.ts +2 -0
- package/src/layouts/PublicLayout/context.tsx +5 -0
- package/src/layouts/PublicLayout/hooks/index.ts +1 -1
- package/src/layouts/PublicLayout/hooks/useMobileNavPanel.ts +55 -0
- package/src/layouts/PublicLayout/index.ts +8 -0
- package/src/layouts/PublicLayout/navbarTypes.ts +20 -0
- package/src/layouts/PublicLayout/publicShellShadow.ts +12 -0
- package/src/layouts/_components/PrivateSidebarAccount.tsx +16 -3
- package/src/layouts/_components/UserMenu.tsx +133 -30
- package/src/layouts/types/index.ts +10 -1
- package/src/layouts/types/providers.types.ts +10 -0
- package/src/layouts/types/ui.types.ts +9 -0
- package/src/theme/ThemeStyleBridge.tsx +41 -0
- package/src/theme/buildThemeStyleSheet.ts +71 -0
- package/src/theme/index.ts +16 -0
- package/src/theme/themeStyle.types.ts +89 -0
- package/src/theme/themeStylePresets.ts +202 -0
- 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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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 {
|
|
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
|
+
}
|