@djangocfg/layouts 2.1.358 → 2.1.360

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.
@@ -41,8 +41,6 @@ export interface PrivateLayoutContextValue {
41
41
  isExpanded: boolean;
42
42
  /** Whether the sidebar is collapsed to icon rail */
43
43
  isCollapsed: boolean;
44
- /** Whether the sidebar is temporarily hover-expanded overlay */
45
- isHoverExpanded: boolean;
46
44
  /** Nav density based on total item count */
47
45
  density: NavDensity;
48
46
  /** Density tokens for the current state */
@@ -94,7 +92,6 @@ interface PrivateLayoutProviderProps {
94
92
  pathname: string;
95
93
  isMobile: boolean;
96
94
  state: 'expanded' | 'collapsed';
97
- isHoverExpanded?: boolean;
98
95
  }
99
96
 
100
97
  // ============================================================================
@@ -108,7 +105,6 @@ export function PrivateLayoutProvider({
108
105
  pathname,
109
106
  isMobile,
110
107
  state,
111
- isHoverExpanded = false,
112
108
  }: PrivateLayoutProviderProps) {
113
109
  const homeHref = sidebar?.homeHref || '/';
114
110
  const brandTitle = header?.title?.trim() || 'Dashboard';
@@ -127,8 +123,7 @@ export function PrivateLayoutProvider({
127
123
  const isExpanded = state === 'expanded';
128
124
  const isCollapsed = state === 'collapsed';
129
125
 
130
- /** Expanded or hover-expanded: follow item-count tier; collapsed rail: fixed `default` sizing so icons stay uniform. */
131
- const menuNav = isCollapsed && !isHoverExpanded ? RAIL_NAV : DENSITY[density];
126
+ const menuNav = isCollapsed ? RAIL_NAV : DENSITY[density];
132
127
 
133
128
  const isActive = React.useCallback(
134
129
  (href: string) => {
@@ -144,7 +139,7 @@ export function PrivateLayoutProvider({
144
139
  [pathname, allItems],
145
140
  );
146
141
 
147
- const collapsedRail = !isMobile && isCollapsed && !isHoverExpanded;
142
+ const collapsedRail = !isMobile && isCollapsed;
148
143
 
149
144
  const hasMenuStart = sidebar?.menuStart != null && sidebar.menuStart !== false;
150
145
  const hasMenuEnd = sidebar?.menuEnd != null && sidebar.menuEnd !== false;
@@ -166,7 +161,6 @@ export function PrivateLayoutProvider({
166
161
  isMobile,
167
162
  isExpanded,
168
163
  isCollapsed,
169
- isHoverExpanded,
170
164
  density,
171
165
  menuNav,
172
166
  activeIndicator,
@@ -187,7 +181,6 @@ export function PrivateLayoutProvider({
187
181
  isMobile,
188
182
  isExpanded,
189
183
  isCollapsed,
190
- isHoverExpanded,
191
184
  density,
192
185
  menuNav,
193
186
  activeIndicator,
@@ -4,11 +4,6 @@
4
4
 
5
5
  export { useAuthGuard } from './useAuthGuard';
6
6
  export { useLayoutVisual } from './useLayoutVisual';
7
- export {
8
- useHoverExpand,
9
- blockSidebarCollapse,
10
- allowSidebarCollapse,
11
- } from './useHoverExpand';
12
7
  export { useShellVisualState } from './useShellVisualState';
13
8
  export { useSidebarKeyboard } from './useSidebarKeyboard';
14
9
  export { useSidebarDefaultOpen } from './useSidebarDefaultOpen';
@@ -2,7 +2,7 @@
2
2
  * Shell Visual State Hook
3
3
  *
4
4
  * Centralizes ALL visual decisions for the sidebar + content pair:
5
- * - sidebar state: expanded | hover-overlay | collapsed-rail
5
+ * - sidebar state: expanded | collapsed-rail
6
6
  * - layout variant: boxed (inset) | full-bleed
7
7
  * - what to show/hide in each state
8
8
  * - CSS modifiers for sidebar root, inner shell, and content inset
@@ -28,9 +28,7 @@ export interface ShellVisualFlags {
28
28
  isExpanded: boolean;
29
29
  /** Collapsed to icon rail */
30
30
  isCollapsed: boolean;
31
- /** Temporary hover-expand overlay on desktop rail */
32
- isHoverOverlay: boolean;
33
- /** True only in desktop collapsed rail without hover */
31
+ /** True only in desktop collapsed rail */
34
32
  isRail: boolean;
35
33
  }
36
34
 
@@ -58,12 +56,8 @@ export interface ShellContentFlags {
58
56
  // ============================================================================
59
57
 
60
58
  export interface ShellChromeFlags {
61
- /** Sidebar casts a shadow (overlay mode) */
62
- showShadow: boolean;
63
59
  /** Right border/separator on sidebar */
64
60
  showBorder: boolean;
65
- /** Internal right padding inside sidebar (overlay breathing room) */
66
- needsInternalPadding: boolean;
67
61
  /** Content inset should have margin to make room for sidebar */
68
62
  contentHasSidebarGap: boolean;
69
63
  /** Sidebar width is fixed to icon-rail */
@@ -101,107 +95,36 @@ export interface ShellVisualState {
101
95
  export function useShellVisualState(
102
96
  layoutVariant?: LayoutVisualConfig['variant'],
103
97
  ): ShellVisualState {
104
- const { isMobile, isExpanded, isCollapsed, isHoverExpanded } =
105
- usePrivateLayoutContext();
98
+ const { isMobile, isExpanded, isCollapsed } = usePrivateLayoutContext();
106
99
 
107
- const isRail = !isMobile && isCollapsed && !isHoverExpanded;
108
- const isHoverOverlay = !isMobile && isCollapsed && isHoverExpanded;
100
+ const isRail = !isMobile && isCollapsed;
109
101
  const variant = layoutVariant ?? 'boxed';
110
102
 
111
103
  return useMemo(() => {
112
- // ------------------------------------------------------------------------
113
- // Content visibility
114
- // ------------------------------------------------------------------------
115
- const showLabels = isExpanded || isHoverOverlay;
104
+ const showLabels = isExpanded || isMobile;
116
105
  const showGroupLabels = showLabels;
117
106
  const showBadgeText = showLabels;
118
107
  const showTooltips = isRail;
119
108
  const isAccountCompact = isRail;
120
109
  const hideSlots = isRail;
121
110
 
122
- // ------------------------------------------------------------------------
123
- // Chrome
124
- // ------------------------------------------------------------------------
125
- // Only hover-overlay is an overlay — it needs shadow + border + padding.
126
- // Persistent expanded is part of the layout flow — no shadow.
127
- const showShadow = isHoverOverlay;
128
- const showBorder = isHoverOverlay;
129
- const needsInternalPadding = isHoverOverlay;
130
-
131
- // Content gap: only persistent expanded pushes the inset.
132
- // Hover-overlay is temporary — content must NOT shift.
133
- // Collapsed rail also leaves a gap (the rail itself is narrow but present).
134
111
  const contentHasSidebarGap = isExpanded || isRail;
135
112
  const isRailWidth = isRail;
113
+ const showBorder = false;
136
114
 
137
- // ------------------------------------------------------------------------
138
- // Modifiers
139
- // ------------------------------------------------------------------------
140
115
  const sidebarRoot: string[] = [];
141
116
  const sidebarInner: string[] = [];
142
117
  const sidebarContent: string[] = [];
143
118
 
144
- if (isHoverOverlay) {
145
- sidebarRoot.push('z-50', '!w-[var(--sidebar-width)]', 'min-w-[var(--sidebar-width)]');
146
- // Allow scroll inside hover-expanded overlay — shadcn hardcodes
147
- // overflow-hidden on div[data-sidebar=sidebar]; override it here.
148
- sidebarInner.push('!overflow-auto');
149
- // Extra right padding on content so text doesn't hug the right edge
150
- sidebarContent.push('!overflow-auto', 'pr-4');
151
-
152
- if (showShadow) {
153
- sidebarInner.push(
154
- 'shadow-[4px_0_24px_-4px_rgba(0,0,0,0.08)]',
155
- 'dark:shadow-[4px_0_24px_-4px_rgba(0,0,0,0.25)]',
156
- );
157
- }
158
- if (showBorder) {
159
- sidebarInner.push('border-r', 'border-sidebar-border/30');
160
- }
161
- // Hide the shadcn gradient border line on hover overlay
162
- sidebarInner.push('[&>div[aria-hidden]]:hidden');
163
- }
164
-
165
- // Boxed variant: inner sidebar gets rounded corners when persistent expanded
166
119
  if (variant === 'boxed' && isExpanded) {
167
120
  sidebarInner.push('rounded-sm');
168
121
  }
169
122
 
170
123
  return {
171
- flags: {
172
- isMobile,
173
- isExpanded,
174
- isCollapsed,
175
- isHoverOverlay,
176
- isRail,
177
- },
178
- content: {
179
- showLabels,
180
- showGroupLabels,
181
- showBadgeText,
182
- showTooltips,
183
- isAccountCompact,
184
- hideSlots,
185
- },
186
- chrome: {
187
- showShadow,
188
- showBorder,
189
- needsInternalPadding,
190
- contentHasSidebarGap,
191
- isRailWidth,
192
- },
193
- modifiers: {
194
- sidebarRoot,
195
- sidebarInner,
196
- sidebarContent,
197
- },
124
+ flags: { isMobile, isExpanded, isCollapsed, isRail },
125
+ content: { showLabels, showGroupLabels, showBadgeText, showTooltips, isAccountCompact, hideSlots },
126
+ chrome: { showBorder, contentHasSidebarGap, isRailWidth },
127
+ modifiers: { sidebarRoot, sidebarInner, sidebarContent },
198
128
  };
199
- }, [
200
- isMobile,
201
- isExpanded,
202
- isCollapsed,
203
- isHoverOverlay,
204
- isRail,
205
- variant,
206
- ]);
129
+ }, [isMobile, isExpanded, isCollapsed, isRail, variant]);
207
130
  }
@@ -1,11 +1,16 @@
1
1
  /**
2
- * Reads the sidebar open/closed state from the cookie written by shadcn-sidebar.
3
- * Falls back to `true` (expanded) when no cookie exists.
2
+ * Resolves the default sidebar open/closed state.
3
+ *
4
+ * Priority:
5
+ * 1. Cookie written by shadcn-sidebar (user's last explicit choice)
6
+ * 2. Viewport width — collapse by default on tablet and below (< 1024px lg)
7
+ * 3. Expanded (large desktop, no cookie)
4
8
  *
5
9
  * Must run on the client — returns `true` during SSR.
6
10
  */
7
11
 
8
12
  const SIDEBAR_COOKIE_NAME = 'sidebar_state';
13
+ const COLLAPSE_BELOW_PX = 1024; // Tailwind lg
9
14
 
10
15
  export function useSidebarDefaultOpen(): boolean {
11
16
  if (typeof document === 'undefined') return true;
@@ -14,8 +19,14 @@ export function useSidebarDefaultOpen(): boolean {
14
19
  .split('; ')
15
20
  .find((row) => row.startsWith(`${SIDEBAR_COOKIE_NAME}=`));
16
21
 
17
- if (!match) return true;
22
+ if (match) {
23
+ return match.split('=')[1] === 'true';
24
+ }
25
+
26
+ // No cookie — first visit. Collapse on smaller screens.
27
+ if (typeof window !== 'undefined') {
28
+ return window.innerWidth >= COLLAPSE_BELOW_PX;
29
+ }
18
30
 
19
- const value = match.split('=')[1];
20
- return value === 'true';
31
+ return true;
21
32
  }
@@ -4,6 +4,7 @@
4
4
 
5
5
  export { PrivateLayout } from './PrivateLayout';
6
6
  export { PrivateLayoutProvider, usePrivateLayoutContext } from './context';
7
+ export { SidebarBrandSwitcher } from './components';
7
8
  export type {
8
9
  PrivateLayoutProps,
9
10
  SidebarItem,
@@ -13,4 +14,6 @@ export type {
13
14
  SidebarActiveIndicator,
14
15
  SidebarGroupLabelStyle,
15
16
  SidebarFeaturedConfig,
17
+ SidebarBrandSwitcherConfig,
18
+ SidebarBrandSwitcherItem,
16
19
  } from './types';
@@ -111,11 +111,46 @@ export interface SidebarConfig {
111
111
  density?: 'comfortable' | 'default' | 'compact';
112
112
  }
113
113
 
114
+ // ============================================================================
115
+ // Brand Switcher Types
116
+ // ============================================================================
117
+
118
+ export interface SidebarBrandSwitcherItem {
119
+ /** Display name */
120
+ label: string;
121
+ /** Avatar image URL or initials fallback */
122
+ avatar?: string;
123
+ /** Single letter shown when `avatar` is absent */
124
+ monogram?: string;
125
+ /** Navigation target on select */
126
+ href?: string;
127
+ /** Callback on select (alternative to href) */
128
+ onSelect?: () => void;
129
+ /** Mark as currently active */
130
+ active?: boolean;
131
+ /** Small secondary line under label (e.g. plan name, role) */
132
+ description?: string;
133
+ }
134
+
135
+ export interface SidebarBrandSwitcherConfig {
136
+ items: SidebarBrandSwitcherItem[];
137
+ /** Label for "add new" action at the bottom of the dropdown. Omit to hide. */
138
+ addLabel?: string;
139
+ /** Called when "add new" is clicked */
140
+ onAdd?: () => void;
141
+ }
142
+
114
143
  // ============================================================================
115
144
  // Header Config
116
145
  // ============================================================================
117
146
 
118
147
  export interface HeaderConfig {
148
+ /**
149
+ * Brand switcher config. When provided, replaces the static brand header
150
+ * with a dropdown for switching workspaces/accounts/projects.
151
+ * Takes priority over `brand`, `title`, `brandIcon`, `brandLetter`.
152
+ */
153
+ switcher?: SidebarBrandSwitcherConfig;
119
154
  /** Custom header brand node (same idea as PublicNavbar `brand`). */
120
155
  brand?: ReactNode;
121
156
  /** Shown next to the logo when the sidebar is expanded */
@@ -5,10 +5,10 @@
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';
8
+ import type { ThemeStyleConfig } from '@djangocfg/ui-core/styles/presets';
9
9
 
10
10
  // Re-export for consumers that only import from `layouts/types`
11
- export type { ThemeStyleConfig, ThemeCssVarKey, ThemeCssVarMap, ThemeStylePresetId } from '../../theme/themeStyle.types';
11
+ export type { ThemeStyleConfig, ThemeCssVarKey, ThemeCssVarMap, ThemeStylePresetId } from '@djangocfg/ui-core/styles/presets';
12
12
 
13
13
  // ============================================================================
14
14
  // Theme Configuration
@@ -2,9 +2,7 @@
2
2
 
3
3
  import { useEffect, useMemo } from 'react';
4
4
 
5
- import { buildThemeStyleSheet } from './buildThemeStyleSheet';
6
-
7
- import type { ThemeStyleConfig } from './themeStyle.types';
5
+ import { buildThemeStyleSheet, type ThemeStyleConfig } from '@djangocfg/ui-core/styles/presets';
8
6
 
9
7
  const STYLE_ELEMENT_ID = 'djangocfg-baseapp-theme-style';
10
8
 
@@ -7,10 +7,8 @@ export type {
7
7
  ThemeCssVarSidebarKey,
8
8
  ThemeStyleConfig,
9
9
  ThemeStylePresetId,
10
- } from './themeStyle.types';
10
+ } from '@djangocfg/ui-core/styles/presets';
11
11
 
12
- export { THEME_STYLE_PRESETS, THEME_STYLE_PRESET_ORDER } from './themeStylePresets';
13
-
14
- export { buildThemeStyleSheet } from './buildThemeStyleSheet';
12
+ export { THEME_STYLE_PRESETS, THEME_STYLE_PRESET_ORDER, buildThemeStyleSheet } from '@djangocfg/ui-core/styles/presets';
15
13
 
16
14
  export { ThemeStyleBridge, type ThemeStyleBridgeProps } from './ThemeStyleBridge';
@@ -1,71 +0,0 @@
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
- }
@@ -1,89 +0,0 @@
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
- }