@flusys/ng-layout 3.0.0-rc → 3.0.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/README.md CHANGED
@@ -2,343 +2,362 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- `@flusys/ng-layout` provides the application shell, menu system, theme management, and layout components for FLUSYS applications. This package creates the visual structure and navigation framework.
5
+ `@flusys/ng-layout` provides the application shell, menu system, theme management, and layout components for FLUSYS applications.
6
6
 
7
- **Key Principle:** ng-layout depends on ng-core and ng-shared, but remains independent of feature packages like ng-auth.
7
+ **Key Principle:** ng-layout depends on ng-core and ng-shared, but remains independent of feature packages like ng-auth via injection tokens.
8
8
 
9
9
  ## Package Information
10
10
 
11
- - **Package:** `@flusys/ng-layout`
12
- - **Dependencies:** ng-core, ng-shared
13
- - **Dependents:** flusysng
14
- - **Build Command:** `npm run build:ng-layout`
11
+ | Property | Value |
12
+ |----------|-------|
13
+ | Package | `@flusys/ng-layout` |
14
+ | Version | 3.0.1 |
15
+ | Dependencies | ng-core, ng-shared |
16
+ | Build | `npm run build:ng-layout` |
15
17
 
16
- ## Features
18
+ ## Architecture
17
19
 
18
- ### 1. App Layout Components
20
+ ```
21
+ ng-layout/
22
+ ├── components/
23
+ │ ├── app.layout.ts # Main layout wrapper
24
+ │ ├── app.topbar.ts # Application header
25
+ │ ├── app.sidebar.ts # Sidebar wrapper
26
+ │ ├── app.menu.ts # Menu container
27
+ │ ├── app.menuitem.ts # Recursive menu item
28
+ │ ├── app.footer.ts # Footer component
29
+ │ ├── app.configurator.ts # Theme customizer
30
+ │ ├── app.floatingconfigurator.ts
31
+ │ └── top-bar/
32
+ │ ├── app.profile.ts
33
+ │ ├── app.launcher.ts
34
+ │ └── app.company-branch-selector.ts
35
+ ├── interfaces/
36
+ │ ├── layout-auth.interface.ts # Auth integration tokens
37
+ │ └── view-transitions.ts
38
+ ├── services/
39
+ │ ├── layout.service.ts # Layout state management
40
+ │ └── layout-persistence.service.ts
41
+ ├── theme/
42
+ │ ├── green-theme.ts
43
+ │ └── navy-blue-theme.ts
44
+ └── utils/
45
+ ├── menu-filter.util.ts
46
+ └── apps-filter.util.ts
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Components
19
52
 
20
- #### AppLayout
53
+ ### AppLayout
21
54
 
22
55
  Main layout wrapper with signal-based state management.
23
56
 
24
57
  ```typescript
25
- import { AppLayout } from '@flusys/ng-layout';
58
+ import { AppLayout, LayoutService } from '@flusys/ng-layout';
26
59
 
27
60
  @Component({
28
61
  selector: 'app-root',
29
- standalone: true,
30
62
  imports: [AppLayout],
31
- template: `
32
- <app-layout>
33
- <router-outlet />
34
- </app-layout>
35
- `
63
+ template: `<app-layout><router-outlet /></app-layout>`,
64
+ host: { '[class.app-dark]': 'layoutService.isDarkTheme()' },
36
65
  })
37
- export class AppComponent {}
66
+ export class AppComponent {
67
+ readonly layoutService = inject(LayoutService);
68
+ }
38
69
  ```
39
70
 
40
- **What it includes:**
41
- - AppTopbar (header with logo, user menu)
42
- - AppSidebar (collapsible navigation menu)
43
- - Main content area with `<router-outlet>`
44
- - AppFooter
71
+ **Includes:** AppTopbar, AppSidebar, main content area, AppFooter, layout mask overlay
45
72
 
46
- **Layout modes:**
73
+ **Layout Modes:**
47
74
  - **Static** - Sidebar always visible on desktop, overlay on mobile
48
75
  - **Overlay** - Sidebar overlays content when opened
49
76
 
50
- #### AppTopbar
51
-
52
- Application header with branding and user actions. Used automatically by AppLayout.
77
+ **Dynamic CSS Classes:**
78
+ | Class | Condition |
79
+ |-------|-----------|
80
+ | `layout-static` | Static menu mode |
81
+ | `layout-overlay` | Overlay menu mode |
82
+ | `layout-static-inactive` | Static menu collapsed on desktop |
83
+ | `layout-overlay-active` | Overlay menu open |
84
+ | `layout-mobile-active` | Mobile menu open |
53
85
 
54
- **Features:**
55
- - Company name display (from `LAYOUT_AUTH_STATE` or fallback to "Flusys")
56
- - Menu toggle button (calls `layoutService.onMenuToggle()`)
57
- - Dark mode toggle button
58
- - Theme/color palette configurator (AppConfigurator via pStyleClass)
59
- - App launcher grid (if apps configured via `LayoutService.setApps()`)
60
- - Company/Branch selector (if `enableCompanyFeature` is true)
61
- - User profile dropdown (AppProfile)
62
-
63
- #### AppSidebar
64
-
65
- Collapsible navigation sidebar. Contains AppMenu.
66
-
67
- **Features:**
68
- - Multi-level menu via AppMenu
69
- - Collapsible/expandable
70
- - Active route highlighting
71
- - Icons with labels
72
- - Smooth CSS animations
73
-
74
- #### AppMenu
86
+ **Behavior:**
87
+ - Subscribes to `layoutService.overlayOpen$` for outside click handling
88
+ - Auto-hides menu on route navigation
89
+ - Blocks body scroll when mobile menu is active
75
90
 
76
- Menu container that renders `LayoutService.menu` signal (permission-filtered).
91
+ ### AppTopbar
77
92
 
78
- #### AppMenuitem
93
+ Application header with branding and user actions.
79
94
 
80
- Recursive menu item component using attribute selector `[app-menuitem]`.
95
+ ```
96
+ ┌─────────────────────────────────────────────────────────────────┐
97
+ │ [≡] CompanyName [☀/🌙] [🎨] [...] [Apps] [🏢] [👤] │
98
+ └─────────────────────────────────────────────────────────────────┘
99
+ ```
81
100
 
82
- **Features:**
83
- - Nested children with toggle animation
84
- - Router link active detection with smart path matching
85
- - Menu state synchronization via `layoutService.menuSource$`
86
- - Smooth submenu collapse/expand via CSS transitions (no `@angular/animations`)
101
+ | Element | Description |
102
+ |---------|-------------|
103
+ | `[≡]` | Menu toggle `layoutService.onMenuToggle()` |
104
+ | CompanyName | From `layoutService.companyName` → links to `/` |
105
+ | `[☀/🌙]` | Dark mode toggle |
106
+ | `[🎨]` | AppConfigurator panel |
107
+ | `[...]` | Mobile menu (hidden on desktop) |
108
+ | `[Apps]` | AppLauncher (if `layoutService.hasApps()`) |
109
+ | `[🏢]` | AppCompanyBranchSelector (if company feature enabled) |
110
+ | `[👤]` | AppProfile dropdown |
111
+
112
+ ### AppMenu & AppMenuitem
113
+
114
+ Menu container renders permission-filtered items from `layoutService.menu`.
115
+
116
+ **AppMenuitem Signal Inputs:**
117
+ | Input | Type | Description |
118
+ |-------|------|-------------|
119
+ | `item` | `IMenuItem` | Required menu item data |
120
+ | `index` | `number` | Required index in parent |
121
+ | `parentKey` | `string` | Optional parent key (default: `''`) |
122
+
123
+ **Router Link Active Options:**
124
+ - Root path (`/`) uses `'exact'` matching
125
+ - Other paths use `'subset'` matching for child route support
126
+
127
+ **CSS Transitions (no @angular/animations):**
128
+ ```css
129
+ :host ul {
130
+ transition: max-height 400ms cubic-bezier(0.86, 0, 0.07, 1);
131
+ }
132
+ :host ul.submenu-collapsed { max-height: 0; }
133
+ :host ul.submenu-expanded { max-height: 1000px; }
134
+ ```
87
135
 
88
- #### AppFooter
136
+ ### AppFooter
89
137
 
90
- Displays product branding information from `LayoutService`:
91
- - **appName** - Application name from `APP_CONFIG` or default
92
- - **authorName** - Author/company name with link
93
- - **authorUrl** - Link to author website
138
+ Displays branding from `LayoutService`: `appName`, `authorName`, `authorUrl`
94
139
 
95
- #### AppProfile
140
+ ### AppProfile
96
141
 
97
142
  User profile dropdown in topbar.
98
143
 
99
144
  **Features:**
100
- - User info display (picture, name, email) with text truncation for long values
101
- - Profile route link
102
- - Copy SignUp Link button (generates link with company slug)
103
- - Logout button with toast feedback
104
- - Responsive dropdown width (`w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[280px] max-w-[320px]`)
105
- - Uses `LAYOUT_AUTH_API` and `LAYOUT_AUTH_STATE` tokens (optional injection)
106
-
107
- #### AppCompanyBranchSelector
145
+ - User info (picture, name, email) with text truncation
146
+ - Profile link → `/profile`
147
+ - Copy SignUp Link `{origin}/auth/register?companySlug={slug}`
148
+ - Logout with toast feedback
108
149
 
109
- Company and branch switcher in topbar. Shown when company feature is enabled.
150
+ **Computed Signals:**
151
+ | Signal | Source | Fallback |
152
+ |--------|--------|----------|
153
+ | `userName` | `authState.loginUserData()?.name` | `'Guest'` |
154
+ | `userEmail` | `authState.loginUserData()?.email` | `''` |
155
+ | `profilePicture` | `layoutService.userProfilePictureUrl()` | `''` |
110
156
 
111
- **Features:**
112
- - Button shows current company name and branch name
113
- - Popover with company/branch dropdowns
114
- - Auto-selects branch if only one available
115
- - Loading states and error handling
116
- - Uses `LAYOUT_AUTH_API` for data fetching and switching
117
- - Signal-based reactivity for button enable/disable state
157
+ ### AppCompanyBranchSelector
118
158
 
119
- **User Workflow:**
120
- 1. Click company/branch button in topbar
121
- 2. Popover opens with company dropdown
122
- 3. Select company → Branches load
123
- 4. Select branch (required if branches exist)
124
- 5. Click "Switch" → Backend API call → JWT updated → Navigate to dashboard
159
+ Company/branch switcher in topbar. Shown when company feature is enabled.
125
160
 
126
- **Configuration requires:**
127
- 1. Auth service enabled in environment (`services.auth.enabled: true`)
128
- 2. Auth adapters provided in `app.config.ts`
161
+ **State Signals:**
162
+ | Signal | Description |
163
+ |--------|-------------|
164
+ | `companies` | Available companies list |
165
+ | `branches` | Branches for selected company |
166
+ | `selectedCompanyId` / `selectedBranchId` | Current selection |
167
+ | `isLoadingCompanies` / `isLoadingBranches` | Loading states |
168
+ | `isSwitching` | Switch operation in progress |
169
+ | `canSwitch` | Computed: enabled when selection differs from current |
129
170
 
130
- #### AppLauncher
171
+ **Workflow:**
172
+ 1. Click button → Panel opens, companies load
173
+ 2. Select company → Branches load (auto-select if single branch)
174
+ 3. Select branch → Click "Switch" → API call → JWT updated → Redirect
131
175
 
132
- App launcher grid in topbar for quick access to related applications.
176
+ ### AppLauncher
133
177
 
134
- **Features:**
135
- - Responsive grid: 2 columns on mobile, 3 columns on desktop (`grid-cols-2 sm:grid-cols-3`)
136
- - External links (open in new tabs)
137
- - Icon support: PrimeNG, Material, or image URLs
138
- - Permission filtering via `permissionLogic`
139
- - Only visible when `layoutService.hasApps()` is true
140
- - App names truncated to prevent overflow
178
+ App launcher grid for quick access to external applications.
141
179
 
142
- **Configuration:**
143
180
  ```typescript
144
- import { ILauncherApp } from '@flusys/ng-layout';
145
-
146
181
  const apps: ILauncherApp[] = [
147
182
  {
148
183
  id: 'docs',
149
184
  name: 'Docs',
150
- iconType: 1, // 1=primeng, 2=material, 3=image
185
+ iconType: IconTypeEnum.PrimeNg,
151
186
  icon: 'pi pi-book',
152
187
  url: 'https://docs.example.com',
153
- },
154
- {
155
- id: 'analytics',
156
- name: 'Analytics',
157
- iconType: 1,
158
- icon: 'pi pi-chart-bar',
159
- url: 'https://analytics.example.com',
160
- permissionLogic: { type: 'action', actionId: 'analytics.view' },
188
+ permissionLogic: { id: 'docs', type: 'action', actionId: 'docs.view' }, // optional
161
189
  },
162
190
  ];
163
-
164
- // In app initialization:
165
191
  layoutService.setApps(apps);
166
192
  ```
167
193
 
168
- #### AppConfigurator
194
+ **Features:**
195
+ - Responsive grid: `grid-cols-2 sm:grid-cols-3`
196
+ - External links (new tab)
197
+ - Permission filtering via `permissionLogic`
198
+ - Only visible when `layoutService.hasApps()` is true
169
199
 
170
- Theme customizer dropdown. Opened from topbar via pStyleClass.
200
+ ### AppConfigurator
171
201
 
172
- **Features:**
173
- - Primary color selector (16 colors + noir)
174
- - Surface color selector (9 colors)
175
- - Preset selector (Aura, Lara, Nora)
176
- - Menu mode selector (static/overlay)
177
- - Dynamic theme application via PrimeUI themes API
202
+ Theme customizer dropdown.
178
203
 
179
- #### AppFloatingConfigurator
204
+ **Options:**
205
+ | Setting | Values |
206
+ |---------|--------|
207
+ | Primary colors | `emerald`, `green`, `lime`, `orange`, `amber`, `yellow`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`, `noir` |
208
+ | Surface colors | `slate`, `gray`, `zinc`, `neutral`, `stone`, `soho`, `viva`, `ocean` |
209
+ | Presets | `Aura` (default), `Lara`, `Nora` |
210
+ | Menu mode | `static`, `overlay` |
180
211
 
181
- Fixed-position floating button for theme customization. Provides quick access to dark mode toggle and color palette.
212
+ Uses `$t()`, `updatePreset()`, `updateSurfacePalette()` from `@primeuix/themes`.
182
213
 
183
- ### 2. LayoutService
214
+ ### AppFloatingConfigurator
184
215
 
185
- Signal-based service for managing layout state. Singleton (`providedIn: 'root'`).
216
+ Fixed-position floating buttons for pages without AppLayout (e.g., auth pages).
186
217
 
187
218
  ```typescript
188
- import { LayoutService } from '@flusys/ng-layout';
189
-
190
- @Component({...})
191
- export class AppComponent {
192
- private readonly layoutService = inject(LayoutService);
193
- }
219
+ @Component({
220
+ imports: [AppFloatingConfigurator],
221
+ template: `<app-floating-configurator /><div class="login-form">...</div>`
222
+ })
223
+ export class LoginComponent {}
194
224
  ```
195
225
 
196
- **Signals:**
197
-
198
- | Signal | Type | Description |
199
- |--------|------|-------------|
200
- | `layoutConfig` | `Signal<LayoutConfig>` | Theme/preset/menuMode configuration |
201
- | `layoutState` | `Signal<LayoutState>` | UI state (menu active, sidebar visible) |
202
- | `transitionComplete` | `Signal<boolean>` | Dark mode transition state |
203
- | `userProfile` | `Signal<UserProfile \| null>` | Current user display data |
204
- | `companyProfile` | `Signal<CompanyProfile \| null>` | Current company display data |
205
- | `appName` | `Signal<string>` | App name for fallback display |
206
- | `authorName` | `Signal<string>` | Author/company name for footer |
207
- | `authorUrl` | `Signal<string>` | Author website URL for footer |
208
- | `menu` | `Computed<IMenuItem[]>` | Permission-filtered menu items |
209
- | `apps` | `Computed<ILauncherApp[]>` | Permission-filtered launcher apps |
226
+ Position: `top-4 md:top-8 right-2 md:right-8 z-50`
210
227
 
211
- **Computed Signals:**
228
+ ---
212
229
 
213
- | Signal | Type | Description |
214
- |--------|------|-------------|
215
- | `isDarkTheme` | `Computed<boolean>` | Whether dark theme is active |
216
- | `isSidebarActive` | `Computed<boolean>` | Whether sidebar/menu is visible |
217
- | `isOverlay` | `Computed<boolean>` | Whether menu mode is overlay |
218
- | `getPrimary` | `Computed<string>` | Current primary color |
219
- | `getSurface` | `Computed<string \| null>` | Current surface color |
220
- | `userName` | `Computed<string>` | User name or "User" fallback |
221
- | `userEmail` | `Computed<string>` | User email |
222
- | `userProfilePictureUrl` | `Computed<string \| null>` | Profile picture URL |
223
- | `companyName` | `Computed<string>` | Company name or appName fallback |
224
- | `companyLogoUrl` | `Computed<string \| null>` | Company logo URL |
225
- | `isAuthenticated` | `Computed<boolean>` | Whether user profile exists |
226
- | `hasApps` | `Computed<boolean>` | Whether filtered apps exist |
227
-
228
- **Methods:**
230
+ ## Services
229
231
 
230
- | Method | Description |
231
- |--------|-------------|
232
- | `onMenuToggle()` | Toggle menu visibility (handles static/overlay/mobile) |
233
- | `onConfigUpdate()` | Emit config change event |
234
- | `onMenuStateChange(event)` | Emit menu item state change |
235
- | `reset()` | Reset menu state |
236
- | `setUserProfile(profile)` | Set user profile for layout display |
237
- | `setCompanyProfile(profile)` | Set company profile for layout display |
238
- | `setMenu(items)` | Set raw menu items (auto-filtered by permissions) |
239
- | `clearMenu()` | Clear menu items |
240
- | `setApps(apps)` | Set launcher apps (auto-filtered by permissions) |
241
- | `clearApps()` | Clear launcher apps |
242
- | `toggleDarkMode(config?)` | Toggle dark mode CSS class |
243
- | `isDesktop()` | Check if viewport > 991px |
244
- | `isMobile()` | Check if viewport <= 991px |
245
-
246
- **RxJS Observables:**
247
-
248
- | Observable | Description |
249
- |------------|-------------|
250
- | `menuSource$` | Menu state change events |
251
- | `resetSource$` | Menu reset events |
252
- | `configUpdate$` | Config change events |
253
- | `overlayOpen$` | Menu overlay opened |
232
+ ### LayoutService
254
233
 
255
- #### Configuration Persistence
234
+ Signal-based singleton service for layout state management.
256
235
 
257
- Layout configuration is automatically persisted to localStorage and restored on app bootstrap.
236
+ #### Interfaces
258
237
 
259
- **localStorage Key:** `flusys.layout.config`
260
-
261
- **Persisted Configuration (LayoutConfig):**
262
238
  ```typescript
263
239
  interface LayoutConfig {
264
240
  preset?: string; // "Aura" | "Lara" | "Nora"
265
- primary?: string; // "emerald" | "green" | "blue" | etc.
266
- surface?: string | null; // "slate" | "gray" | "zinc" | etc. | null
241
+ primary?: string; // "emerald" | "green" | etc.
242
+ surface?: string | null; // "slate" | "gray" | etc.
267
243
  darkTheme?: boolean;
268
244
  menuMode?: 'static' | 'overlay';
269
245
  }
270
- ```
271
246
 
272
- **Behavior:**
273
- - Changes saved automatically via Angular `effect()` when config changes
274
- - Configuration restored on page refresh (merged with defaults)
275
- - SSR-safe (`isPlatformBrowser` check)
276
- - Invalid preset falls back to "Aura", invalid menuMode falls back to "static"
277
- - Corrupted JSON is cleared and defaults are used
278
- - Version mismatches trigger automatic clearing
279
-
280
- **Clearing Saved Configuration:**
281
- ```typescript
282
- import { LayoutPersistenceService } from '@flusys/ng-layout';
247
+ interface LayoutState {
248
+ staticMenuDesktopInactive?: boolean;
249
+ overlayMenuActive?: boolean;
250
+ staticMenuMobileActive?: boolean;
251
+ menuHoverActive?: boolean;
252
+ }
283
253
 
284
- inject(LayoutPersistenceService).clear();
254
+ interface UserProfile { id: string; name: string; email: string; profilePictureUrl?: string | null; }
255
+ interface CompanyProfile { id: string; name: string; slug: string; logoUrl?: string | null; }
285
256
  ```
286
257
 
287
- ### 3. Theme Management
258
+ #### Signals
288
259
 
289
- Built-in theme support with light/dark modes using PrimeUI themes.
260
+ **State Signals (readonly):**
261
+ | Signal | Type |
262
+ |--------|------|
263
+ | `layoutConfig` | `Signal<LayoutConfig>` |
264
+ | `layoutState` | `Signal<LayoutState>` |
265
+ | `transitionComplete` | `Signal<boolean>` |
266
+ | `userProfile` | `Signal<UserProfile \| null>` |
267
+ | `companyProfile` | `Signal<CompanyProfile \| null>` |
290
268
 
291
- **Dark mode toggle** uses the View Transition API (with fallback):
292
- ```typescript
293
- // AppTopbar toggles dark mode by updating layoutConfig signal:
294
- this.layoutService.layoutConfig.update((state) => ({
295
- ...state,
296
- darkTheme: !state.darkTheme,
297
- }));
298
- ```
269
+ **Static Values (from APP_CONFIG):**
270
+ | Property | Fallback |
271
+ |----------|----------|
272
+ | `appName` | `DEFAULT_APP_NAME` |
273
+ | `authorName` | `DEFAULT_AUTHOR.name` |
274
+ | `authorUrl` | `DEFAULT_AUTHOR.url` |
299
275
 
300
- **Host binding** for root component:
276
+ **Computed Signals:**
277
+ | Signal | Description |
278
+ |--------|-------------|
279
+ | `menu` | Permission-filtered menu items |
280
+ | `apps` | Permission-filtered launcher apps |
281
+ | `hasApps` | Whether apps exist |
282
+ | `isDarkTheme` | Dark theme active |
283
+ | `isSidebarActive` | Sidebar visible |
284
+ | `isOverlay` | Overlay menu mode |
285
+ | `getPrimary` / `getSurface` | Current colors |
286
+ | `userName` / `userEmail` | User info (with fallbacks) |
287
+ | `userProfilePictureUrl` / `companyLogoUrl` | Image URLs |
288
+ | `companyName` | Company name or appName fallback |
289
+ | `isAuthenticated` | User profile exists |
290
+
291
+ #### Methods
292
+
293
+ | Method | Description |
294
+ |--------|-------------|
295
+ | `onMenuToggle()` | Toggle menu (handles static/overlay/mobile) |
296
+ | `onMenuStateChange(event)` | Emit menu state change |
297
+ | `reset()` | Reset menu state |
298
+ | `isDesktop()` / `isMobile()` | Viewport checks (SSR-safe) |
299
+ | `updateLayoutConfig(config)` | Partial config update |
300
+ | `updateLayoutState(state)` | Partial state update |
301
+ | `toggleDarkMode(config?)` | Toggle `app-dark` class |
302
+ | `setUserProfile(profile)` | Set user profile |
303
+ | `setCompanyProfile(profile)` | Set company profile |
304
+ | `setMenu(items)` / `clearMenu()` | Menu management |
305
+ | `setApps(apps)` / `clearApps()` | Apps management |
306
+
307
+ #### RxJS Observables
308
+
309
+ | Observable | Purpose |
310
+ |------------|---------|
311
+ | `menuSource$` | Menu state change events |
312
+ | `resetSource$` | Menu reset events |
313
+ | `configUpdate$` | Config change events |
314
+ | `overlayOpen$` | Menu overlay opened |
315
+
316
+ #### Dark Mode Transition
317
+
318
+ Uses View Transitions API with fallback:
301
319
  ```typescript
302
- @Component({
303
- host: { '[class.app-dark]': 'layoutService.isDarkTheme()' }
304
- })
305
- export class AppComponent {
306
- readonly layoutService = inject(LayoutService);
320
+ toggleDarkMode(config?: LayoutConfig): void {
321
+ const isDark = (config ?? this._layoutConfig()).darkTheme;
322
+ this.document.documentElement.classList.toggle('app-dark', isDark);
307
323
  }
308
324
  ```
309
325
 
310
- **Pre-defined Themes:**
326
+ ### LayoutPersistenceService
311
327
 
312
- | Theme | Base | Primary Color |
313
- |-------|------|---------------|
314
- | `GreenTheme` | Material | `#01712c` |
315
- | `NavyBlueTheme` | Material | `#3535cd` |
328
+ Persists layout configuration to localStorage.
316
329
 
317
- ```typescript
318
- import { GreenTheme, NavyBlueTheme } from '@flusys/ng-layout';
319
- ```
330
+ | Method | Description |
331
+ |--------|-------------|
332
+ | `load()` | Load and validate saved config |
333
+ | `save(config)` | Save config with version |
334
+ | `clear()` | Remove saved config |
335
+
336
+ **localStorage Key:** `flusys.layout.config`
320
337
 
321
- ### 4. Auth Integration
338
+ **Validation:**
339
+ - Invalid preset → `'Aura'`
340
+ - Invalid menuMode → `'static'`
341
+ - Non-boolean darkTheme → `false`
342
+ - Version mismatch or invalid JSON → Clear and return null
322
343
 
323
- Layout components use injection tokens to access auth data without depending on ng-auth.
344
+ ---
345
+
346
+ ## Auth Integration
324
347
 
325
- #### LAYOUT_AUTH_STATE Token
348
+ Layout components use injection tokens for auth data without direct ng-auth dependency.
326
349
 
327
- Provides current user/company/branch state to layout components.
350
+ ### Tokens
328
351
 
329
352
  ```typescript
353
+ // LAYOUT_AUTH_STATE - Provides current user/company/branch state
330
354
  interface ILayoutAuthState {
331
- readonly currentCompanyInfo: Signal<ICompanyInfo | null>;
332
- readonly currentBranchInfo: Signal<IBranchInfo | null>;
333
- readonly loginUserData: Signal<IUserInfo | null>;
355
+ currentCompanyInfo: Signal<ICompanyInfo | null>;
356
+ currentBranchInfo: Signal<IBranchInfo | null>;
357
+ loginUserData: Signal<IUserInfo | null>;
334
358
  }
335
- ```
336
359
 
337
- #### LAYOUT_AUTH_API Token
338
-
339
- Provides auth actions (logout, company switching) to layout components.
340
-
341
- ```typescript
360
+ // LAYOUT_AUTH_API - Provides auth actions
342
361
  interface ILayoutAuthApi {
343
362
  logOut(): Observable<any>;
344
363
  navigateLogin(withUrl?: boolean): void;
@@ -348,7 +367,9 @@ interface ILayoutAuthApi {
348
367
  }
349
368
  ```
350
369
 
351
- #### Providing Auth Tokens
370
+ **Related:** `ICompanyInfo` (id, name, slug, imageId), `IBranchInfo` (id, name, slug, imageId?), `IUserInfo` (id, name, email, profilePicture?)
371
+
372
+ ### Configuration
352
373
 
353
374
  ```typescript
354
375
  // app.config.ts
@@ -361,53 +382,48 @@ providers: [
361
382
  ]
362
383
  ```
363
384
 
364
- Components inject tokens with `{ optional: true }` and degrade gracefully when auth is not provided.
385
+ Components inject with `{ optional: true }` for graceful degradation without auth.
365
386
 
366
- ### 5. Permission-Based Menu Filtering
387
+ ---
367
388
 
368
- Menu items and launcher apps are automatically filtered based on user permissions using `permissionLogic`.
389
+ ## Permission-Based Filtering
369
390
 
370
- **How it works:**
371
- 1. Raw menu items set via `layoutService.setMenu(items)`
372
- 2. `LayoutService.menu` is a `computed()` that filters via `filterMenuByPermissions()`
373
- 3. Permission evaluation uses `evaluateLogicNode()` from ng-shared
374
- 4. Same pattern applies to apps via `filterAppsByPermissions()`
391
+ Menu items and launcher apps are automatically filtered via `permissionLogic`.
392
+
393
+ ### How It Works
394
+
395
+ 1. Set raw items via `layoutService.setMenu(items)` or `setApps(apps)`
396
+ 2. `LayoutService.menu` / `apps` are computed signals that filter via utilities
397
+ 3. Uses `evaluateLogicNode()` from ng-shared
398
+
399
+ ### Interfaces
375
400
 
376
- **ILogicNode Types:**
377
401
  ```typescript
402
+ interface IMenuItem {
403
+ id?: string; label?: string; icon?: string; iconType?: number;
404
+ routerLink?: string[]; children?: IMenuItem[]; separator?: boolean;
405
+ permissionLogic?: ILogicNode | null;
406
+ }
407
+
378
408
  interface ILogicNode {
379
409
  id: string;
380
410
  type: 'group' | 'action' | 'role';
381
- operator?: 'AND' | 'OR'; // Required when type is 'group'
382
- actionId?: string; // Required when type is 'action'
383
- roleId?: string; // Required when type is 'role'
384
- children?: ILogicNode[]; // Required when type is 'group'
411
+ operator?: 'AND' | 'OR'; // For 'group'
412
+ actionId?: string; // For 'action'
413
+ roleId?: string; // For 'role'
414
+ children?: ILogicNode[]; // For 'group'
385
415
  }
386
416
  ```
387
417
 
388
- **Best Practice:** Use logic nodes (parent menu items without `routerLink`) with `permissionLogic` as permission gates for entire branches:
418
+ ### Examples
389
419
 
390
420
  ```typescript
391
- // Logic node gates all children
392
- {
393
- id: 'administration',
394
- label: 'Administration',
395
- icon: 'pi pi-cog',
396
- permissionLogic: { id: 'admin-check', type: 'action', actionId: 'admin.access' },
397
- children: [
398
- { id: 'users', label: 'Users', routerLink: ['/admin/users'] },
399
- { id: 'settings', label: 'Settings', routerLink: ['/admin/settings'] },
400
- ],
401
- }
402
- ```
421
+ // Single action check
422
+ permissionLogic: { id: '1', type: 'action', actionId: 'admin.access' }
403
423
 
404
- **Complex permission examples:**
405
- ```typescript
406
424
  // OR - user needs ANY permission
407
425
  permissionLogic: {
408
- id: 'reports-check',
409
- type: 'group',
410
- operator: 'OR',
426
+ id: 'check', type: 'group', operator: 'OR',
411
427
  children: [
412
428
  { id: '1', type: 'action', actionId: 'reports.view' },
413
429
  { id: '2', type: 'role', roleId: 'manager-role-id' },
@@ -416,9 +432,7 @@ permissionLogic: {
416
432
 
417
433
  // AND - user needs ALL permissions
418
434
  permissionLogic: {
419
- id: 'admin-check',
420
- type: 'group',
421
- operator: 'AND',
435
+ id: 'check', type: 'group', operator: 'AND',
422
436
  children: [
423
437
  { id: '1', type: 'action', actionId: 'admin.access' },
424
438
  { id: '2', type: 'role', roleId: 'superuser-role-id' },
@@ -426,37 +440,37 @@ permissionLogic: {
426
440
  }
427
441
  ```
428
442
 
429
- ## Installation
443
+ ### Filtering Behavior
444
+
445
+ - **Separators** pass through without permission checks
446
+ - **Parent items** with no visible children are hidden
447
+ - **Children** filtered recursively
448
+
449
+ ---
450
+
451
+ ## Pre-defined Themes
430
452
 
431
- Build ng-layout after ng-core and ng-shared:
453
+ Custom Material-based themes using `definePreset`:
432
454
 
433
- ```bash
434
- npm run build:ng-core
435
- npm run build:ng-shared
436
- npm run build:ng-layout
437
- # Or build all libraries:
438
- npm run build:libs
455
+ | Theme | Primary Color | Hover Color |
456
+ |-------|---------------|-------------|
457
+ | `GreenTheme` | `#01712c` | `#119744` |
458
+ | `NavyBlueTheme` | `#3535cd` | `#0707a9` |
459
+
460
+ ```typescript
461
+ import { GreenTheme, NavyBlueTheme } from '@flusys/ng-layout';
439
462
  ```
440
463
 
464
+ ---
465
+
441
466
  ## Usage Examples
442
467
 
443
- ### Basic Layout Setup
468
+ ### Basic Setup
444
469
 
445
470
  ```typescript
446
- // app.component.ts
447
- import { Component, inject } from '@angular/core';
448
- import { RouterOutlet } from '@angular/router';
449
- import { AppLayout, LayoutService } from '@flusys/ng-layout';
450
-
451
471
  @Component({
452
- selector: 'app-root',
453
- standalone: true,
454
- imports: [RouterOutlet, AppLayout],
455
- template: `
456
- <app-layout>
457
- <router-outlet />
458
- </app-layout>
459
- `,
472
+ imports: [AppLayout],
473
+ template: `<app-layout><router-outlet /></app-layout>`,
460
474
  host: { '[class.app-dark]': 'layoutService.isDarkTheme()' },
461
475
  })
462
476
  export class AppComponent {
@@ -467,246 +481,142 @@ export class AppComponent {
467
481
  ### Menu Configuration
468
482
 
469
483
  ```typescript
470
- import { LayoutService, IMenuItem } from '@flusys/ng-layout';
471
-
472
484
  const menuItems: IMenuItem[] = [
485
+ { id: 'dashboard', label: 'Dashboard', icon: 'pi pi-home', routerLink: ['/dashboard'] },
473
486
  {
474
- label: 'Dashboard',
475
- icon: 'pi pi-home',
476
- routerLink: ['/dashboard'],
477
- },
478
- {
479
- label: 'Management',
480
- icon: 'pi pi-cog',
487
+ id: 'admin', label: 'Admin', icon: 'pi pi-cog',
488
+ permissionLogic: { id: 'admin', type: 'action', actionId: 'admin.access' },
481
489
  children: [
482
- { label: 'Users', icon: 'pi pi-users', routerLink: ['/users'] },
483
- { label: 'Roles', icon: 'pi pi-shield', routerLink: ['/roles'] },
490
+ { id: 'users', label: 'Users', icon: 'pi pi-users', routerLink: ['/admin/users'] },
491
+ { id: 'roles', label: 'Roles', icon: 'pi pi-shield', routerLink: ['/admin/roles'] },
484
492
  ],
485
493
  },
486
494
  { separator: true },
487
- {
488
- label: 'Settings',
489
- icon: 'pi pi-cog',
490
- routerLink: ['/settings'],
491
- },
495
+ { id: 'settings', label: 'Settings', icon: 'pi pi-cog', routerLink: ['/settings'] },
492
496
  ];
493
-
494
497
  layoutService.setMenu(menuItems);
495
498
  ```
496
499
 
497
- ### Auth Integration
500
+ ### Profile Setup
498
501
 
499
502
  ```typescript
500
- // app.config.ts
501
- import { LAYOUT_AUTH_STATE, LAYOUT_AUTH_API } from '@flusys/ng-layout';
502
- import { AuthLayoutStateAdapter, AuthLayoutApiAdapter } from '@flusys/ng-auth';
503
-
504
- export const appConfig: ApplicationConfig = {
505
- providers: [
506
- { provide: LAYOUT_AUTH_STATE, useExisting: AuthLayoutStateAdapter },
507
- { provide: LAYOUT_AUTH_API, useExisting: AuthLayoutApiAdapter },
508
- ],
509
- };
503
+ // After login
504
+ layoutService.setUserProfile({
505
+ id: 'user-123',
506
+ name: 'John Doe',
507
+ email: 'john@example.com',
508
+ profilePictureUrl: 'https://example.com/avatar.jpg',
509
+ });
510
+
511
+ layoutService.setCompanyProfile({
512
+ id: 'company-456',
513
+ name: 'Acme Corp',
514
+ slug: 'acme-corp',
515
+ logoUrl: 'https://example.com/logo.png',
516
+ });
517
+
518
+ // On logout
519
+ layoutService.setUserProfile(null);
520
+ layoutService.setCompanyProfile(null);
510
521
  ```
511
522
 
512
- ### Dynamic Menu from Backend
513
-
514
- ```typescript
515
- import { LayoutService } from '@flusys/ng-layout';
516
- import { MenuApiService } from './services/menu-api.service';
517
-
518
- @Component({...})
519
- export class AppComponent implements OnInit {
520
- private readonly layoutService = inject(LayoutService);
521
- private readonly menuApi = inject(MenuApiService);
522
-
523
- async ngOnInit() {
524
- const response = await firstValueFrom(this.menuApi.getUserMenu());
525
- if (response.success) {
526
- this.layoutService.setMenu(response.data);
527
- }
528
- }
529
- }
530
- ```
523
+ ---
531
524
 
532
525
  ## Best Practices
533
526
 
534
- ### Layout Structure
535
- - Use `AppLayout` as root layout component
536
- - Set menu items via `layoutService.setMenu()` during app initialization
537
- - Use `LayoutService` for all layout state changes
538
- - Don't manipulate DOM directly for layout changes
539
-
540
- ### Menu Management
541
- - Load menus dynamically from backend (IAM module)
542
- - Group related items under parent menu items with `permissionLogic`
543
- - Use PrimeIcons for consistency
544
- - Use `children` property for nested items (not `items`)
545
-
546
- ### Auth Integration
547
- - Use injection tokens (`LAYOUT_AUTH_STATE`, `LAYOUT_AUTH_API`)
548
- - Don't import ng-auth directly in ng-layout
549
- - Provide adapters at app level in `app.config.ts`
550
- - Layout works without auth (graceful degradation)
551
-
552
- ### Theme Management
553
- - Use CSS animations, not `@angular/animations`
554
- - Apply `app-dark` class to root element via host binding
555
- - Theme preferences persist automatically to localStorage
556
- - Test both light and dark themes for contrast
557
-
558
- **Dark Mode CSS Variables:**
559
- All components use PrimeNG CSS variables for automatic dark mode support:
560
- ```css
561
- /* Backgrounds */
562
- background-color: var(--surface-overlay); /* Dropdown panels */
563
- bg-emphasis /* Hover states */
564
-
565
- /* Text */
566
- text-color /* Primary text */
567
- text-muted-color /* Secondary text */
527
+ ### Signal Patterns
568
528
 
569
- /* Borders */
570
- border-surface /* Standard borders */
529
+ Use private writable + public readonly pattern:
530
+ ```typescript
531
+ private readonly _isActive = signal(false);
532
+ readonly isActive = this._isActive.asReadonly();
571
533
 
572
- /* Primary colors */
573
- bg-primary /* Primary backgrounds */
574
- text-primary-contrast /* Text on primary */
534
+ // Update via private signal
535
+ this._isActive.set(true);
536
+ this._isActive.update(v => !v);
575
537
  ```
576
538
 
577
- ### Responsive Design
578
- - Use Tailwind for responsive utilities
579
- - Layout automatically handles responsive behavior (static → overlay on mobile)
580
- - Menu closes on navigation and outside clicks
581
-
582
- **Dropdown Panel Pattern:**
583
- ```css
584
- /* Full width on mobile, auto-width on desktop with min/max constraints */
585
- class="w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[280px] max-w-[320px]"
586
- ```
539
+ ### Responsive Widths
587
540
 
588
- **Component Responsive Widths:**
589
541
  | Component | Pattern |
590
542
  |-----------|---------|
591
- | `AppConfigurator` | `w-[calc(100vw-2rem)] sm:w-72 max-w-72` |
592
- | `AppProfile` | `w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[280px] max-w-[320px]` |
593
- | `AppCompanyBranchSelector` | `w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[300px] max-w-[360px]` |
594
- | `AppLauncher` | `w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[280px] max-w-[320px]` |
595
- | `AppFloatingConfigurator` | Position: `top-4 md:top-8 right-2 md:right-8` |
543
+ | AppConfigurator | `w-[calc(100vw-2rem)] sm:w-72 max-w-72` |
544
+ | AppProfile | `w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[280px] max-w-[320px]` |
545
+ | AppCompanyBranchSelector | `w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[300px] max-w-[360px]` |
546
+ | AppLauncher | `w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[280px] max-w-[320px]` |
547
+
548
+ ### Dark Mode CSS Variables
596
549
 
597
- **Grid Responsiveness:**
598
550
  ```css
599
- /* 2 columns mobile, 3 columns desktop */
600
- class="grid grid-cols-2 sm:grid-cols-3 gap-2"
551
+ background-color: var(--surface-overlay); /* Panels */
552
+ bg-emphasis /* Hover states */
553
+ text-color / text-muted-color /* Text */
554
+ border-surface /* Borders */
555
+ bg-primary / text-primary-contrast /* Primary */
601
556
  ```
602
557
 
603
- **Text Truncation:**
604
- ```html
605
- <!-- Prevent overflow on long names/emails -->
606
- <div class="flex flex-col min-w-0 flex-1">
607
- <span class="truncate">{{ userName() }}</span>
608
- </div>
609
- ```
558
+ ---
610
559
 
611
560
  ## Common Issues
612
561
 
613
- ### Menu Not Showing
614
-
615
- Ensure menu items are set via `LayoutService.setMenu()`:
616
- ```typescript
617
- this.layoutService.setMenu([/* items */]);
618
- ```
619
-
620
- ### Auth Info Not in Topbar
621
-
622
- Provide `LAYOUT_AUTH_STATE` in `app.config.ts`:
623
- ```typescript
624
- { provide: LAYOUT_AUTH_STATE, useExisting: AuthLayoutStateAdapter }
625
- ```
626
-
627
- ### Theme Not Applying
628
-
629
- Add host binding to root component:
630
- ```typescript
631
- @Component({
632
- host: { '[class.app-dark]': 'layoutService.isDarkTheme()' }
633
- })
634
- ```
635
-
636
- ### Sidebar Not Responsive
562
+ | Issue | Solution |
563
+ |-------|----------|
564
+ | Menu not showing | Call `layoutService.setMenu([...])` |
565
+ | Auth info missing | Provide `LAYOUT_AUTH_STATE` token |
566
+ | Theme not applying | Add `host: { '[class.app-dark]': 'layoutService.isDarkTheme()' }` |
567
+ | Menu items hidden | Check `permissionLogic` and user permissions |
568
+ | Profile picture missing | Include `profilePictureUrl` in `setUserProfile()` |
569
+ | Company feature missing | Enable `services.auth.enabled: true` and provide both auth tokens |
637
570
 
638
- Layout automatically handles responsive behavior. Ensure you're using the `AppLayout` component, not a custom layout.
571
+ ---
639
572
 
640
573
  ## API Reference
641
574
 
642
575
  ### Components
643
576
 
644
- | Component | Selector | Description |
645
- |-----------|----------|-------------|
646
- | `AppLayout` | `app-layout` | Main layout wrapper |
647
- | `AppTopbar` | `app-topbar` | Application header |
648
- | `AppSidebar` | `app-sidebar` | Navigation sidebar |
649
- | `AppMenu` | `app-menu` | Menu container |
650
- | `AppMenuitem` | `[app-menuitem]` | Recursive menu item |
651
- | `AppFooter` | `app-footer` | Footer |
652
- | `AppProfile` | `app-profile` | User profile dropdown |
653
- | `AppCompanyBranchSelector` | `app-company-branch-selector` | Company/branch switcher |
654
- | `AppLauncher` | `app-launcher` | App launcher grid |
655
- | `AppConfigurator` | `app-configurator` | Theme customizer |
656
- | `AppFloatingConfigurator` | `app-floating-configurator` | Floating theme button |
577
+ | Component | Selector |
578
+ |-----------|----------|
579
+ | `AppLayout` | `app-layout` |
580
+ | `AppTopbar` | `app-topbar` |
581
+ | `AppSidebar` | `app-sidebar` |
582
+ | `AppMenu` | `app-menu` |
583
+ | `AppMenuitem` | `[app-menuitem]` |
584
+ | `AppFooter` | `app-footer` |
585
+ | `AppProfile` | `app-profile` |
586
+ | `AppCompanyBranchSelector` | `app-company-branch-selector` |
587
+ | `AppLauncher` | `app-launcher` |
588
+ | `AppConfigurator` | `app-configurator` |
589
+ | `AppFloatingConfigurator` | `app-floating-configurator` |
657
590
 
658
591
  ### Services
659
592
 
660
593
  | Service | Description |
661
594
  |---------|-------------|
662
595
  | `LayoutService` | Signal-based layout state management |
663
- | `LayoutPersistenceService` | localStorage persistence with validation |
664
-
665
- ### Injection Tokens
596
+ | `LayoutPersistenceService` | localStorage persistence |
666
597
 
667
- | Token | Interface | Description |
668
- |-------|-----------|-------------|
669
- | `LAYOUT_AUTH_STATE` | `ILayoutAuthState` | Auth state signals |
670
- | `LAYOUT_AUTH_API` | `ILayoutAuthApi` | Auth actions |
598
+ ### Tokens
671
599
 
672
- ### Interfaces
600
+ | Token | Interface |
601
+ |-------|-----------|
602
+ | `LAYOUT_AUTH_STATE` | `ILayoutAuthState` |
603
+ | `LAYOUT_AUTH_API` | `ILayoutAuthApi` |
673
604
 
674
- | Interface | Description |
675
- |-----------|-------------|
676
- | `IMenuItem` | Menu item configuration |
677
- | `ILauncherApp` | App launcher item configuration |
678
- | `ILayoutAuthState` | Auth state for layout |
679
- | `ILayoutAuthApi` | Auth actions for layout |
680
- | `ICompanyInfo` | Company information |
681
- | `IBranchInfo` | Branch information |
682
- | `IUserInfo` | User information |
683
- | `LayoutConfig` | Theme/preset configuration |
684
- | `LayoutState` | UI state (menu active, etc.) |
685
- | `UserProfile` | User display data |
686
- | `CompanyProfile` | Company display data |
687
- | `MenuChangeEvent` | Menu state change event |
688
-
689
- ### Utility Functions
605
+ ### Utilities
690
606
 
691
607
  | Function | Description |
692
608
  |----------|-------------|
693
- | `filterMenuByPermissions(items, permissions)` | Filter menu items by permission logic |
694
- | `filterAppsByPermissions(apps, permissions)` | Filter launcher apps by permission logic |
609
+ | `filterMenuByPermissions()` | Filter menu items by permission logic |
610
+ | `filterAppsByPermissions()` | Filter launcher apps by permission logic |
695
611
 
696
- ### Pre-defined Themes
697
-
698
- | Export | Base Preset | Primary Color |
699
- |--------|-------------|---------------|
700
- | `GreenTheme` | Material | `#01712c` |
701
- | `NavyBlueTheme` | Material | `#3535cd` |
612
+ ---
702
613
 
703
614
  ## See Also
704
615
 
705
- - **[CORE-GUIDE.md](./CORE-GUIDE.md)** - Configuration, interceptors
706
- - **[SHARED-GUIDE.md](./SHARED-GUIDE.md)** - Shared components, provider interfaces
707
- - **[AUTH-GUIDE.md](./AUTH-GUIDE.md)** - Authentication integration, adapters
616
+ - [CORE-GUIDE.md](./CORE-GUIDE.md) - Configuration, interceptors
617
+ - [SHARED-GUIDE.md](./SHARED-GUIDE.md) - Shared components, provider interfaces
618
+ - [AUTH-GUIDE.md](./AUTH-GUIDE.md) - Authentication integration, adapters
708
619
 
709
620
  ---
710
621
 
711
- **Last Updated:** 2026-02-21
712
- **Angular Version:** 21
622
+ **Last Updated:** 2026-02-25 | **Version:** 3.0.1 | **Angular:** 21