@memberjunction/ng-explorer-settings 5.42.0 → 5.44.0
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/dist/__tests__/role-management-agent-context.test.d.ts +2 -0
- package/dist/__tests__/role-management-agent-context.test.d.ts.map +1 -0
- package/dist/__tests__/role-management-agent-context.test.js +107 -0
- package/dist/__tests__/role-management-agent-context.test.js.map +1 -0
- package/dist/__tests__/user-management-agent-context.test.d.ts +2 -0
- package/dist/__tests__/user-management-agent-context.test.d.ts.map +1 -0
- package/dist/__tests__/user-management-agent-context.test.js +134 -0
- package/dist/__tests__/user-management-agent-context.test.js.map +1 -0
- package/dist/lib/application-management/application-dialog/application-dialog.component.js +176 -200
- package/dist/lib/application-management/application-dialog/application-dialog.component.js.map +1 -1
- package/dist/lib/application-management/application-management.component.d.ts +7 -0
- package/dist/lib/application-management/application-management.component.d.ts.map +1 -1
- package/dist/lib/application-management/application-management.component.js +49 -39
- package/dist/lib/application-management/application-management.component.js.map +1 -1
- package/dist/lib/application-settings/application-settings.component.js +58 -70
- package/dist/lib/application-settings/application-settings.component.js.map +1 -1
- package/dist/lib/entity-permissions/entity-permissions.component.d.ts +10 -0
- package/dist/lib/entity-permissions/entity-permissions.component.d.ts.map +1 -1
- package/dist/lib/entity-permissions/entity-permissions.component.js +32 -16
- package/dist/lib/entity-permissions/entity-permissions.component.js.map +1 -1
- package/dist/lib/entity-permissions/permission-dialog/permission-dialog.component.js +32 -33
- package/dist/lib/entity-permissions/permission-dialog/permission-dialog.component.js.map +1 -1
- package/dist/lib/module.d.ts +1 -1
- package/dist/lib/module.d.ts.map +1 -1
- package/dist/lib/module.js +13 -4
- package/dist/lib/module.js.map +1 -1
- package/dist/lib/notification-preferences/notification-preferences.component.js +30 -35
- package/dist/lib/notification-preferences/notification-preferences.component.js.map +1 -1
- package/dist/lib/role-management/role-dialog/role-dialog.component.js +26 -46
- package/dist/lib/role-management/role-dialog/role-dialog.component.js.map +1 -1
- package/dist/lib/role-management/role-management-agent-context.d.ts +96 -0
- package/dist/lib/role-management/role-management-agent-context.d.ts.map +1 -0
- package/dist/lib/role-management/role-management-agent-context.js +85 -0
- package/dist/lib/role-management/role-management-agent-context.js.map +1 -0
- package/dist/lib/role-management/role-management.component.d.ts +32 -0
- package/dist/lib/role-management/role-management.component.d.ts.map +1 -1
- package/dist/lib/role-management/role-management.component.js +236 -41
- package/dist/lib/role-management/role-management.component.js.map +1 -1
- package/dist/lib/sql-logging/sql-logging.component.js +151 -184
- package/dist/lib/sql-logging/sql-logging.component.js.map +1 -1
- package/dist/lib/user-app-config/user-app-config.component.js +80 -93
- package/dist/lib/user-app-config/user-app-config.component.js.map +1 -1
- package/dist/lib/user-management/user-dialog/user-dialog.component.js +20 -21
- package/dist/lib/user-management/user-dialog/user-dialog.component.js.map +1 -1
- package/dist/lib/user-management/user-management-agent-context.d.ts +107 -0
- package/dist/lib/user-management/user-management-agent-context.d.ts.map +1 -0
- package/dist/lib/user-management/user-management-agent-context.js +132 -0
- package/dist/lib/user-management/user-management-agent-context.js.map +1 -0
- package/dist/lib/user-management/user-management.component.d.ts +35 -0
- package/dist/lib/user-management/user-management.component.d.ts.map +1 -1
- package/dist/lib/user-management/user-management.component.js +311 -71
- package/dist/lib/user-management/user-management.component.js.map +1 -1
- package/dist/lib/user-profile-settings/user-profile-settings.component.js +52 -70
- package/dist/lib/user-profile-settings/user-profile-settings.component.js.map +1 -1
- package/package.json +19 -19
|
@@ -7,9 +7,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { Component } from '@angular/core';
|
|
8
8
|
import { Subject, BehaviorSubject } from 'rxjs';
|
|
9
9
|
import { takeUntil, debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
|
10
|
-
import { RunView } from '@memberjunction/core';
|
|
10
|
+
import { RunView, CompositeKey } from '@memberjunction/core';
|
|
11
11
|
import { BaseDashboard } from '@memberjunction/ng-shared';
|
|
12
|
-
import { RegisterClass } from '@memberjunction/global';
|
|
12
|
+
import { RegisterClass, UUIDsEqual } from '@memberjunction/global';
|
|
13
|
+
import { buildRoleManagementAgentContext, isValidRoleTypeFilter, resolveRoleByIDOrName, } from './role-management-agent-context';
|
|
13
14
|
import * as i0 from "@angular/core";
|
|
14
15
|
import * as i1 from "@memberjunction/ng-shared-generic";
|
|
15
16
|
import * as i2 from "@memberjunction/ng-ui-components";
|
|
@@ -166,19 +167,18 @@ function RoleManagementComponent_Conditional_16_For_2_Template(rf, ctx) { if (rf
|
|
|
166
167
|
i0.ɵɵconditional(ctx_r1.isRoleExpanded(role_r4.ID) ? 20 : -1);
|
|
167
168
|
} }
|
|
168
169
|
function RoleManagementComponent_Conditional_16_Conditional_3_Template(rf, ctx) { if (rf & 1) {
|
|
169
|
-
i0.ɵɵ
|
|
170
|
-
i0.ɵɵ
|
|
171
|
-
i0.ɵɵ
|
|
172
|
-
i0.ɵɵtext(3, "No roles found");
|
|
170
|
+
const _r6 = i0.ɵɵgetCurrentView();
|
|
171
|
+
i0.ɵɵelementStart(0, "mj-empty-state", 56);
|
|
172
|
+
i0.ɵɵlistener("Action", function RoleManagementComponent_Conditional_16_Conditional_3_Template_mj_empty_state_Action_0_listener() { i0.ɵɵrestoreView(_r6); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.resetAllFiltersAndSearch()); });
|
|
173
173
|
i0.ɵɵelementEnd();
|
|
174
|
-
|
|
175
|
-
i0.ɵɵ
|
|
176
|
-
i0.ɵɵ
|
|
174
|
+
} if (rf & 2) {
|
|
175
|
+
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
176
|
+
i0.ɵɵproperty("Variant", ctx_r1.IsListNarrowed ? "no-results" : "empty")("ActionText", ctx_r1.IsListNarrowed ? "Reset filters" : "");
|
|
177
177
|
} }
|
|
178
178
|
function RoleManagementComponent_Conditional_16_Template(rf, ctx) { if (rf & 1) {
|
|
179
179
|
i0.ɵɵelementStart(0, "div", 14);
|
|
180
180
|
i0.ɵɵrepeaterCreate(1, RoleManagementComponent_Conditional_16_For_2_Template, 21, 16, "div", 23, _forTrack0);
|
|
181
|
-
i0.ɵɵconditionalCreate(3, RoleManagementComponent_Conditional_16_Conditional_3_Template,
|
|
181
|
+
i0.ɵɵconditionalCreate(3, RoleManagementComponent_Conditional_16_Conditional_3_Template, 1, 2, "mj-empty-state", 24);
|
|
182
182
|
i0.ɵɵelementEnd();
|
|
183
183
|
} if (rf & 2) {
|
|
184
184
|
const ctx_r1 = i0.ɵɵnextContext();
|
|
@@ -187,55 +187,53 @@ function RoleManagementComponent_Conditional_16_Template(rf, ctx) { if (rf & 1)
|
|
|
187
187
|
i0.ɵɵadvance(2);
|
|
188
188
|
i0.ɵɵconditional(ctx_r1.filteredRoles.length === 0 ? 3 : -1);
|
|
189
189
|
} }
|
|
190
|
-
function
|
|
191
|
-
i0.ɵɵelement(0, "i",
|
|
190
|
+
function RoleManagementComponent_Conditional_18_Conditional_17_Template(rf, ctx) { if (rf & 1) {
|
|
191
|
+
i0.ɵɵelement(0, "i", 69);
|
|
192
192
|
i0.ɵɵtext(1, " Deleting... ");
|
|
193
193
|
} }
|
|
194
|
-
function
|
|
194
|
+
function RoleManagementComponent_Conditional_18_Conditional_18_Template(rf, ctx) { if (rf & 1) {
|
|
195
195
|
i0.ɵɵelement(0, "i", 38);
|
|
196
196
|
i0.ɵɵtext(1, " Delete Role ");
|
|
197
197
|
} }
|
|
198
198
|
function RoleManagementComponent_Conditional_18_Template(rf, ctx) { if (rf & 1) {
|
|
199
|
-
const
|
|
200
|
-
i0.ɵɵelementStart(0, "div",
|
|
201
|
-
i0.ɵɵlistener("click", function RoleManagementComponent_Conditional_18_Template_div_click_0_listener() { i0.ɵɵrestoreView(
|
|
202
|
-
i0.ɵɵelementStart(1, "div",
|
|
203
|
-
i0.ɵɵlistener("click", function RoleManagementComponent_Conditional_18_Template_div_click_1_listener($event) { i0.ɵɵrestoreView(
|
|
204
|
-
i0.ɵɵelementStart(2, "div",
|
|
205
|
-
i0.ɵɵelement(4, "i",
|
|
199
|
+
const _r7 = i0.ɵɵgetCurrentView();
|
|
200
|
+
i0.ɵɵelementStart(0, "div", 57);
|
|
201
|
+
i0.ɵɵlistener("click", function RoleManagementComponent_Conditional_18_Template_div_click_0_listener() { i0.ɵɵrestoreView(_r7); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.showDeleteConfirm = false); });
|
|
202
|
+
i0.ɵɵelementStart(1, "div", 58);
|
|
203
|
+
i0.ɵɵlistener("click", function RoleManagementComponent_Conditional_18_Template_div_click_1_listener($event) { i0.ɵɵrestoreView(_r7); return i0.ɵɵresetView($event.stopPropagation()); });
|
|
204
|
+
i0.ɵɵelementStart(2, "div", 59)(3, "h3", 60);
|
|
205
|
+
i0.ɵɵelement(4, "i", 61);
|
|
206
206
|
i0.ɵɵtext(5, " Confirm Delete ");
|
|
207
207
|
i0.ɵɵelementEnd();
|
|
208
|
-
i0.ɵɵelementStart(6, "button",
|
|
209
|
-
i0.ɵɵlistener("click", function RoleManagementComponent_Conditional_18_Template_button_click_6_listener() { i0.ɵɵrestoreView(
|
|
210
|
-
i0.ɵɵelement(7, "i",
|
|
208
|
+
i0.ɵɵelementStart(6, "button", 62);
|
|
209
|
+
i0.ɵɵlistener("click", function RoleManagementComponent_Conditional_18_Template_button_click_6_listener() { i0.ɵɵrestoreView(_r7); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.showDeleteConfirm = false); });
|
|
210
|
+
i0.ɵɵelement(7, "i", 63);
|
|
211
211
|
i0.ɵɵelementEnd()();
|
|
212
|
-
i0.ɵɵelementStart(8, "div",
|
|
212
|
+
i0.ɵɵelementStart(8, "div", 64)(9, "p");
|
|
213
213
|
i0.ɵɵtext(10, "Are you sure you want to delete the role ");
|
|
214
214
|
i0.ɵɵelementStart(11, "strong");
|
|
215
215
|
i0.ɵɵtext(12);
|
|
216
216
|
i0.ɵɵelementEnd();
|
|
217
217
|
i0.ɵɵtext(13, "?");
|
|
218
218
|
i0.ɵɵelementEnd();
|
|
219
|
-
i0.ɵɵ
|
|
220
|
-
i0.ɵɵelement(15, "i", 68);
|
|
221
|
-
i0.ɵɵtext(16, " This will affect all users assigned to this role. ");
|
|
222
|
-
i0.ɵɵelementEnd()();
|
|
223
|
-
i0.ɵɵelementStart(17, "div", 69)(18, "button", 70);
|
|
224
|
-
i0.ɵɵlistener("click", function RoleManagementComponent_Conditional_18_Template_button_click_18_listener() { i0.ɵɵrestoreView(_r6); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.deleteRole()); });
|
|
225
|
-
i0.ɵɵconditionalCreate(19, RoleManagementComponent_Conditional_18_Conditional_19_Template, 2, 0)(20, RoleManagementComponent_Conditional_18_Conditional_20_Template, 2, 0);
|
|
219
|
+
i0.ɵɵelement(14, "mj-alert", 65);
|
|
226
220
|
i0.ɵɵelementEnd();
|
|
227
|
-
i0.ɵɵelementStart(
|
|
228
|
-
i0.ɵɵlistener("click", function
|
|
229
|
-
i0.ɵɵ
|
|
221
|
+
i0.ɵɵelementStart(15, "div", 66)(16, "button", 67);
|
|
222
|
+
i0.ɵɵlistener("click", function RoleManagementComponent_Conditional_18_Template_button_click_16_listener() { i0.ɵɵrestoreView(_r7); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.deleteRole()); });
|
|
223
|
+
i0.ɵɵconditionalCreate(17, RoleManagementComponent_Conditional_18_Conditional_17_Template, 2, 0)(18, RoleManagementComponent_Conditional_18_Conditional_18_Template, 2, 0);
|
|
224
|
+
i0.ɵɵelementEnd();
|
|
225
|
+
i0.ɵɵelementStart(19, "button", 68);
|
|
226
|
+
i0.ɵɵlistener("click", function RoleManagementComponent_Conditional_18_Template_button_click_19_listener() { i0.ɵɵrestoreView(_r7); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.showDeleteConfirm = false); });
|
|
227
|
+
i0.ɵɵtext(20, " Cancel ");
|
|
230
228
|
i0.ɵɵelementEnd()()()();
|
|
231
229
|
} if (rf & 2) {
|
|
232
230
|
const ctx_r1 = i0.ɵɵnextContext();
|
|
233
231
|
i0.ɵɵadvance(12);
|
|
234
232
|
i0.ɵɵtextInterpolate(ctx_r1.selectedRole.Name);
|
|
235
|
-
i0.ɵɵadvance(
|
|
233
|
+
i0.ɵɵadvance(4);
|
|
236
234
|
i0.ɵɵproperty("disabled", ctx_r1.isLoading);
|
|
237
235
|
i0.ɵɵadvance();
|
|
238
|
-
i0.ɵɵconditional(ctx_r1.isLoading ?
|
|
236
|
+
i0.ɵɵconditional(ctx_r1.isLoading ? 17 : 18);
|
|
239
237
|
} }
|
|
240
238
|
let RoleManagementComponent = class RoleManagementComponent extends BaseDashboard {
|
|
241
239
|
cdr;
|
|
@@ -268,6 +266,10 @@ let RoleManagementComponent = class RoleManagementComponent extends BaseDashboar
|
|
|
268
266
|
expandedRoleId = null;
|
|
269
267
|
// Role permissions (simplified view)
|
|
270
268
|
rolePermissions = new Map();
|
|
269
|
+
// Read-only entity-permission summary per role id (lazily loaded on selection),
|
|
270
|
+
// used only to surface a non-sensitive count summary to the agent context.
|
|
271
|
+
permissionSummaryCache = new Map();
|
|
272
|
+
selectedRolePermissionSummary = null;
|
|
271
273
|
destroy$ = new Subject();
|
|
272
274
|
get metadata() { return this.ProviderToUse; }
|
|
273
275
|
constructor(cdr, ngZone) {
|
|
@@ -280,10 +282,176 @@ let RoleManagementComponent = class RoleManagementComponent extends BaseDashboar
|
|
|
280
282
|
}
|
|
281
283
|
initDashboard() {
|
|
282
284
|
this.setupFilterSubscription();
|
|
285
|
+
this.registerAgentClientTools();
|
|
283
286
|
}
|
|
284
287
|
loadData() {
|
|
285
288
|
this.loadInitialData();
|
|
286
289
|
}
|
|
290
|
+
// ================================================================
|
|
291
|
+
// AI Agent Context & Client Tools
|
|
292
|
+
//
|
|
293
|
+
// 🚨 SAFETY BOUNDARY — READ-ONLY / GOVERNANCE SURFACE 🚨
|
|
294
|
+
// Role Management is a security-sensitive admin surface. The agent context
|
|
295
|
+
// and client tools registered here are strictly READ-ONLY / navigational:
|
|
296
|
+
// type filter, free-text search, SELECTING a role (view, with a read-only
|
|
297
|
+
// permission-count summary), clearing filters, refreshing, and navigating to
|
|
298
|
+
// a role's record for viewing. The mutating operations on this component —
|
|
299
|
+
// create/edit/delete roles — AND any permission-grant/revoke surface are
|
|
300
|
+
// DELIBERATELY NOT exposed to the agent; they remain human-initiated. The
|
|
301
|
+
// permission summary published here is COUNTS ONLY (how many entities the role
|
|
302
|
+
// can read/create/update/delete) — never a grant control. Context exposes only
|
|
303
|
+
// counts, active filter selection, and on-screen display names.
|
|
304
|
+
// ================================================================
|
|
305
|
+
/**
|
|
306
|
+
* Publish the current role-management state to the AI agent. Re-invoked on
|
|
307
|
+
* every meaningful state change (data load, filter, selection). Shaped by the
|
|
308
|
+
* pure {@link buildRoleManagementAgentContext} helper (unit-tested in isolation).
|
|
309
|
+
*/
|
|
310
|
+
publishAgentContext() {
|
|
311
|
+
const selected = this.expandedRoleId
|
|
312
|
+
? this.roles.find(r => UUIDsEqual(r.ID, this.expandedRoleId)) ?? null
|
|
313
|
+
: null;
|
|
314
|
+
const input = {
|
|
315
|
+
TotalRoleCount: this.roles.length,
|
|
316
|
+
FilteredRoleCount: this.filteredRoles.length,
|
|
317
|
+
SystemRoleCount: this.stats.systemRoles,
|
|
318
|
+
CustomRoleCount: this.stats.customRoles,
|
|
319
|
+
TypeFilter: this.filters$.value.type,
|
|
320
|
+
SearchText: this.filters$.value.search,
|
|
321
|
+
SelectedRoleId: selected?.ID ?? null,
|
|
322
|
+
SelectedRoleName: selected?.Name ?? null,
|
|
323
|
+
VisibleRoleNames: this.filteredRoles.map(r => r.Name ?? '').filter(n => n !== ''),
|
|
324
|
+
SelectedRolePermissions: selected ? this.selectedRolePermissionSummary : null,
|
|
325
|
+
};
|
|
326
|
+
this.navigationService.SetAgentContext(this, buildRoleManagementAgentContext(input));
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Register the read-only / navigational client tools the agent may invoke.
|
|
330
|
+
* Every Handler is tolerant: validates input and returns
|
|
331
|
+
* `{ Success: false, ErrorMessage }` rather than throwing.
|
|
332
|
+
*/
|
|
333
|
+
registerAgentClientTools() {
|
|
334
|
+
this.navigationService.SetAgentClientTools(this, [
|
|
335
|
+
{
|
|
336
|
+
Name: 'FilterRolesByType',
|
|
337
|
+
Description: 'Filter the role list by type. Valid values: all, system, custom.',
|
|
338
|
+
ParameterSchema: { type: 'object', properties: { type: { type: 'string', enum: ['all', 'system', 'custom'] } }, required: ['type'] },
|
|
339
|
+
Handler: async (params) => this.handleFilterByTypeTool(params),
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
Name: 'SearchRoles',
|
|
343
|
+
Description: 'Free-text search across role name and description.',
|
|
344
|
+
ParameterSchema: { type: 'object', properties: { searchText: { type: 'string' } }, required: ['searchText'] },
|
|
345
|
+
Handler: async (params) => this.handleSearchTool(params),
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
Name: 'SelectRole',
|
|
349
|
+
Description: 'Select a role for VIEWING by ID or name (exact ID → exact name → partial match). Read-only — does not edit the role. Loads a read-only permission-count summary into SelectedRolePermissions.',
|
|
350
|
+
ParameterSchema: { type: 'object', properties: { role: { type: 'string' } }, required: ['role'] },
|
|
351
|
+
Handler: async (params) => this.handleSelectRoleTool(params),
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
Name: 'NavigateToRoleRecord',
|
|
355
|
+
Description: 'Open the record for a role (by ID or name) in a tab for VIEWING. Read-only navigation — does not edit the role.',
|
|
356
|
+
ParameterSchema: { type: 'object', properties: { role: { type: 'string' } }, required: ['role'] },
|
|
357
|
+
Handler: async (params) => this.handleNavigateToRoleRecordTool(params),
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
Name: 'ClearRoleFilters',
|
|
361
|
+
Description: 'Clear all role filters (type and search) and show the full list.',
|
|
362
|
+
ParameterSchema: { type: 'object', properties: {} },
|
|
363
|
+
Handler: async () => this.handleClearFiltersTool(),
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
Name: 'RefreshRoles',
|
|
367
|
+
Description: 'Reload the role list from the server. Read-only — does not mutate any data.',
|
|
368
|
+
ParameterSchema: { type: 'object', properties: {} },
|
|
369
|
+
Handler: async () => this.handleRefreshTool(),
|
|
370
|
+
},
|
|
371
|
+
]);
|
|
372
|
+
}
|
|
373
|
+
handleFilterByTypeTool(params) {
|
|
374
|
+
const type = params?.['type'];
|
|
375
|
+
if (!isValidRoleTypeFilter(type)) {
|
|
376
|
+
return { Success: false, ErrorMessage: `Invalid type "${String(type)}". Expected one of: all, system, custom.` };
|
|
377
|
+
}
|
|
378
|
+
this.onTypeFilterChange(type);
|
|
379
|
+
this.publishAgentContext();
|
|
380
|
+
return { Success: true };
|
|
381
|
+
}
|
|
382
|
+
handleSearchTool(params) {
|
|
383
|
+
const searchText = params?.['searchText'];
|
|
384
|
+
if (typeof searchText !== 'string') {
|
|
385
|
+
return { Success: false, ErrorMessage: 'searchText must be a string.' };
|
|
386
|
+
}
|
|
387
|
+
this.updateFilter({ search: searchText });
|
|
388
|
+
this.publishAgentContext();
|
|
389
|
+
return { Success: true };
|
|
390
|
+
}
|
|
391
|
+
async handleSelectRoleTool(params) {
|
|
392
|
+
const raw = String(params?.['role'] ?? '');
|
|
393
|
+
const resolved = resolveRoleByIDOrName(raw, this.roles.map(r => ({ ID: r.ID, Name: r.Name ?? '' })));
|
|
394
|
+
if (!resolved.ok) {
|
|
395
|
+
return { Success: false, ErrorMessage: resolved.error };
|
|
396
|
+
}
|
|
397
|
+
this.expandedRoleId = resolved.match.ID;
|
|
398
|
+
this.selectedRolePermissionSummary = await this.loadPermissionSummary(resolved.match.ID);
|
|
399
|
+
this.cdr.markForCheck();
|
|
400
|
+
this.publishAgentContext();
|
|
401
|
+
return { Success: true };
|
|
402
|
+
}
|
|
403
|
+
handleNavigateToRoleRecordTool(params) {
|
|
404
|
+
const raw = String(params?.['role'] ?? '');
|
|
405
|
+
const resolved = resolveRoleByIDOrName(raw, this.roles.map(r => ({ ID: r.ID, Name: r.Name ?? '' })));
|
|
406
|
+
if (!resolved.ok) {
|
|
407
|
+
return { Success: false, ErrorMessage: resolved.error };
|
|
408
|
+
}
|
|
409
|
+
this.navigationService.OpenEntityRecord('MJ: Roles', CompositeKey.FromID(resolved.match.ID));
|
|
410
|
+
return { Success: true };
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Lazily load and cache the READ-ONLY entity-permission count summary for a role.
|
|
414
|
+
* Surfaces only how broadly the role is granted access — never a grant control.
|
|
415
|
+
*/
|
|
416
|
+
async loadPermissionSummary(roleId) {
|
|
417
|
+
const cached = this.permissionSummaryCache.get(roleId);
|
|
418
|
+
if (cached) {
|
|
419
|
+
return cached;
|
|
420
|
+
}
|
|
421
|
+
const rv = RunView.FromMetadataProvider(this.ProviderToUse);
|
|
422
|
+
const result = await rv.RunView({
|
|
423
|
+
EntityName: 'MJ: Entity Permissions',
|
|
424
|
+
ExtraFilter: `RoleID='${roleId.replace(/'/g, "''")}'`,
|
|
425
|
+
ResultType: 'entity_object'
|
|
426
|
+
});
|
|
427
|
+
const rows = result.Success ? result.Results : [];
|
|
428
|
+
const summary = {
|
|
429
|
+
EntityCount: rows.length,
|
|
430
|
+
ReadCount: rows.filter(p => p.CanRead).length,
|
|
431
|
+
CreateCount: rows.filter(p => p.CanCreate).length,
|
|
432
|
+
UpdateCount: rows.filter(p => p.CanUpdate).length,
|
|
433
|
+
DeleteCount: rows.filter(p => p.CanDelete).length,
|
|
434
|
+
};
|
|
435
|
+
this.permissionSummaryCache.set(roleId, summary);
|
|
436
|
+
return summary;
|
|
437
|
+
}
|
|
438
|
+
handleClearFiltersTool() {
|
|
439
|
+
this.resetAllFiltersAndSearch();
|
|
440
|
+
this.publishAgentContext();
|
|
441
|
+
return { Success: true };
|
|
442
|
+
}
|
|
443
|
+
async handleRefreshTool() {
|
|
444
|
+
try {
|
|
445
|
+
this.permissionSummaryCache.clear();
|
|
446
|
+
this.selectedRolePermissionSummary = null;
|
|
447
|
+
await this.loadInitialData();
|
|
448
|
+
this.publishAgentContext();
|
|
449
|
+
return { Success: true };
|
|
450
|
+
}
|
|
451
|
+
catch (e) {
|
|
452
|
+
return { Success: false, ErrorMessage: e instanceof Error ? e.message : 'Refresh failed.' };
|
|
453
|
+
}
|
|
454
|
+
}
|
|
287
455
|
ngOnDestroy() {
|
|
288
456
|
this.destroy$.next();
|
|
289
457
|
this.destroy$.complete();
|
|
@@ -307,6 +475,7 @@ let RoleManagementComponent = class RoleManagementComponent extends BaseDashboar
|
|
|
307
475
|
this.ngZone.run(() => {
|
|
308
476
|
this.isLoading = false;
|
|
309
477
|
this.cdr.markForCheck();
|
|
478
|
+
this.publishAgentContext();
|
|
310
479
|
});
|
|
311
480
|
}
|
|
312
481
|
}
|
|
@@ -324,6 +493,7 @@ let RoleManagementComponent = class RoleManagementComponent extends BaseDashboar
|
|
|
324
493
|
.pipe(debounceTime(300), distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), takeUntil(this.destroy$))
|
|
325
494
|
.subscribe(() => {
|
|
326
495
|
this.applyFilters();
|
|
496
|
+
this.publishAgentContext();
|
|
327
497
|
});
|
|
328
498
|
}
|
|
329
499
|
applyFilters() {
|
|
@@ -405,8 +575,33 @@ let RoleManagementComponent = class RoleManagementComponent extends BaseDashboar
|
|
|
405
575
|
clearAllAppliedFilters() {
|
|
406
576
|
this.updateFilter({ type: 'all' });
|
|
407
577
|
}
|
|
578
|
+
/** True when search and/or panel filters are narrowing the list — gates the
|
|
579
|
+
* no-results empty-state "Reset filters" CTA. */
|
|
580
|
+
get IsListNarrowed() {
|
|
581
|
+
return this.filters$.value.search !== '' || this.TotalActiveFilterCount > 0;
|
|
582
|
+
}
|
|
583
|
+
/** Reset everything narrowing the list (search + Type) and refresh
|
|
584
|
+
* immediately. Wired to the no-results empty-state CTA. Unlike
|
|
585
|
+
* clearAllAppliedFilters(), this also clears the search box. */
|
|
586
|
+
resetAllFiltersAndSearch() {
|
|
587
|
+
this.filters$.next({ type: 'all', search: '' });
|
|
588
|
+
this.applyFilters();
|
|
589
|
+
this.cdr.markForCheck();
|
|
590
|
+
}
|
|
408
591
|
toggleRoleExpansion(roleId) {
|
|
409
592
|
this.expandedRoleId = this.expandedRoleId === roleId ? null : roleId;
|
|
593
|
+
if (this.expandedRoleId) {
|
|
594
|
+
// Lazy-load the read-only permission summary so the agent context reflects
|
|
595
|
+
// the user's current selection; fire-and-forget (re-publishes on completion).
|
|
596
|
+
void this.loadPermissionSummary(this.expandedRoleId).then(summary => {
|
|
597
|
+
this.selectedRolePermissionSummary = summary;
|
|
598
|
+
this.publishAgentContext();
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
this.selectedRolePermissionSummary = null;
|
|
603
|
+
}
|
|
604
|
+
this.publishAgentContext();
|
|
410
605
|
}
|
|
411
606
|
isRoleExpanded(roleId) {
|
|
412
607
|
return this.expandedRoleId === roleId;
|
|
@@ -482,7 +677,7 @@ let RoleManagementComponent = class RoleManagementComponent extends BaseDashboar
|
|
|
482
677
|
}
|
|
483
678
|
}
|
|
484
679
|
static ɵfac = function RoleManagementComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || RoleManagementComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i0.NgZone)); };
|
|
485
|
-
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: RoleManagementComponent, selectors: [["mj-role-management"]], standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 19, vars: 14, consts: [["AriaLabel", "Filter roles", "Title", "Roles", "Subtitle", "Define system and custom roles for permissions"], ["meta", ""], ["Label", "roles", 3, "Count", "Total"], ["toolbar", ""], ["Placeholder", "Search roles by name or description...", 3, "ValueChange", "Value"], ["Label", "Filters", "Icon", "fa-solid fa-filter", 3, "ClearAllRequested", "ActiveCount", "ShowClearAll"], [3, "ValuesChange", "Reset", "Fields", "Values"], ["actions", ""], [3, "Clicked", "Loading"], ["mjButton", "", "variant", "primary", "size", "sm", "aria-label", "Add new role", 3, "click"], [1, "fa-solid", "fa-plus"], [1, "mj-action-label"], ["role", "status", "aria-live", "polite", "aria-busy", "true", 1, "loading-container"], ["role", "alert", "aria-live", "assertive", 1, "error-container"], ["role", "list", "aria-label", "Roles list", 1, "roles-list"], [3, "result", "data", "visible"], ["role", "presentation", 1, "modal-backdrop"], ["text", "Loading roles...", "size", "medium"], [1, "error-content"], ["aria-hidden", "true", 1, "fa-solid", "fa-exclamation-triangle", "error-icon"], [1, "error-message"], ["mjButton", "", "variant", "primary", "aria-label", "Retry loading roles", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-refresh"], ["role", "listitem", 1, "role-card", 3, "expanded"], ["
|
|
680
|
+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: RoleManagementComponent, selectors: [["mj-role-management"]], standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 19, vars: 14, consts: [["AriaLabel", "Filter roles", "Title", "Roles", "Subtitle", "Define system and custom roles for permissions"], ["meta", ""], ["Label", "roles", 3, "Count", "Total"], ["toolbar", ""], ["Placeholder", "Search roles by name or description...", 3, "ValueChange", "Value"], ["Label", "Filters", "Icon", "fa-solid fa-filter", 3, "ClearAllRequested", "ActiveCount", "ShowClearAll"], [3, "ValuesChange", "Reset", "Fields", "Values"], ["actions", ""], [3, "Clicked", "Loading"], ["mjButton", "", "variant", "primary", "size", "sm", "aria-label", "Add new role", 3, "click"], [1, "fa-solid", "fa-plus"], [1, "mj-action-label"], ["role", "status", "aria-live", "polite", "aria-busy", "true", 1, "loading-container"], ["role", "alert", "aria-live", "assertive", 1, "error-container"], ["role", "list", "aria-label", "Roles list", 1, "roles-list"], [3, "result", "data", "visible"], ["role", "presentation", 1, "modal-backdrop"], ["text", "Loading roles...", "size", "medium"], [1, "error-content"], ["aria-hidden", "true", 1, "fa-solid", "fa-exclamation-triangle", "error-icon"], [1, "error-message"], ["mjButton", "", "variant", "primary", "aria-label", "Retry loading roles", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-refresh"], ["role", "listitem", 1, "role-card", 3, "expanded"], ["Icon", "fa-solid fa-user-tag", "Title", "No roles found", "Message", "Try adjusting your filters or create a new role", "ActionIcon", "fa-solid fa-rotate-left", 3, "Variant", "ActionText"], ["role", "listitem", 1, "role-card"], ["tabindex", "0", "role", "button", 1, "role-header", 3, "click", "keydown.enter", "keydown.space"], [1, "role-info"], ["aria-hidden", "true", 1, "role-icon-wrapper"], [1, "role-details"], [1, "role-name"], [1, "role-description"], [1, "role-meta"], [1, "role-type-badge"], [1, "role-actions", 3, "click"], ["mjButton", "", "variant", "flat", "size", "sm", "title", "Edit", 3, "click", "disabled"], ["aria-hidden", "true", 1, "fa-solid", "fa-edit"], ["mjButton", "", "variant", "danger", "size", "sm", "title", "Delete", 3, "click", "disabled"], ["aria-hidden", "true", 1, "fa-solid", "fa-trash"], ["aria-label", "Toggle details", 1, "expand-btn"], ["aria-hidden", "true", 1, "fa-solid", "fa-chevron-down"], [1, "role-content"], [1, "mobile-actions-bar", 3, "click"], [1, "mobile-action-buttons"], [1, "btn-label"], [1, "role-stats"], [1, "stat-item"], ["aria-hidden", "true", 1, "fa-solid", "fa-users"], [1, "stat-label"], [1, "stat-value"], ["aria-hidden", "true", 1, "fa-solid", "fa-calendar"], ["aria-hidden", "true", 1, "fa-solid", "fa-clock"], [1, "permissions-preview"], [1, "section-title"], ["aria-hidden", "true", 1, "fa-solid", "fa-key"], [1, "permissions-note"], ["Icon", "fa-solid fa-user-tag", "Title", "No roles found", "Message", "Try adjusting your filters or create a new role", "ActionIcon", "fa-solid fa-rotate-left", 3, "Action", "Variant", "ActionText"], ["role", "presentation", 1, "modal-backdrop", 3, "click"], ["role", "dialog", "aria-modal", "true", "aria-labelledby", "delete-dialog-title", "aria-describedby", "delete-dialog-desc", 1, "modal-dialog", 3, "click"], [1, "modal-header"], ["id", "delete-dialog-title", 1, "modal-title"], ["aria-hidden", "true", 1, "fa-solid", "fa-exclamation-triangle", "text-danger"], ["aria-label", "Close dialog", 1, "modal-close", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-times"], ["id", "delete-dialog-desc", 1, "modal-body"], ["Variant", "warning", "Message", "This will affect all users assigned to this role."], [1, "modal-footer"], ["mjButton", "", "variant", "danger", 3, "click", "disabled"], ["mjButton", "", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-spinner", "fa-spin"]], template: function RoleManagementComponent_Template(rf, ctx) { if (rf & 1) {
|
|
486
681
|
i0.ɵɵelementStart(0, "mj-page-header-interior", 0)(1, "div", 1);
|
|
487
682
|
i0.ɵɵelement(2, "mj-stat-badge", 2);
|
|
488
683
|
i0.ɵɵelementEnd();
|
|
@@ -511,7 +706,7 @@ let RoleManagementComponent = class RoleManagementComponent extends BaseDashboar
|
|
|
511
706
|
i0.ɵɵelementStart(17, "mj-role-dialog", 15);
|
|
512
707
|
i0.ɵɵlistener("result", function RoleManagementComponent_Template_mj_role_dialog_result_17_listener($event) { return ctx.onRoleDialogResult($event); });
|
|
513
708
|
i0.ɵɵelementEnd();
|
|
514
|
-
i0.ɵɵconditionalCreate(18, RoleManagementComponent_Conditional_18_Template,
|
|
709
|
+
i0.ɵɵconditionalCreate(18, RoleManagementComponent_Conditional_18_Template, 21, 3, "div", 16);
|
|
515
710
|
} if (rf & 2) {
|
|
516
711
|
i0.ɵɵadvance(2);
|
|
517
712
|
i0.ɵɵproperty("Count", ctx.filteredRoles.length)("Total", ctx.roles.length);
|
|
@@ -533,7 +728,7 @@ let RoleManagementComponent = class RoleManagementComponent extends BaseDashboar
|
|
|
533
728
|
i0.ɵɵproperty("data", ctx.roleDialogData)("visible", ctx.showRoleDialog);
|
|
534
729
|
i0.ɵɵadvance();
|
|
535
730
|
i0.ɵɵconditional(ctx.showDeleteConfirm && ctx.selectedRole ? 18 : -1);
|
|
536
|
-
} }, dependencies: [i1.LoadingComponent, i2.MJButtonDirective, i2.MJPageHeaderInteriorComponent, i2.MJPageBodyInteriorComponent, i2.MJPageSearchComponent, i2.MJFilterPopoverComponent, i2.MJFilterPanelComponent, i2.MJRefreshButtonComponent, i2.MJStatBadgeComponent, i3.RoleDialogComponent, i4.DatePipe], styles: ["\n\n\n\n\n\n\n\n\n\n\n[_nghost-%COMP%] {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n overflow: hidden;\n}\n\n\n\n\n\n.sticky-header[_ngcontent-%COMP%] {\n flex-shrink: 0;\n background: var(--mj-bg-page);\n border-bottom: 1px solid var(--mj-border-default);\n box-shadow: var(--mj-shadow-sm);\n z-index: 10;\n}\n\n\n\n\n\n.scrollable-content[_ngcontent-%COMP%] {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n -webkit-overflow-scrolling: touch;\n padding: 1rem;\n background: var(--mj-bg-surface-sunken);\n}\n\n@media (min-width: 768px) {\n .scrollable-content[_ngcontent-%COMP%] {\n padding: 1.5rem 2rem;\n }\n}\n\n@media (min-width: 1024px) {\n .scrollable-content[_ngcontent-%COMP%] {\n padding: 2rem;\n }\n}\n\n@media (min-width: 1440px) {\n .scrollable-content[_ngcontent-%COMP%] {\n padding: 2rem 2.5rem;\n }\n}\n\n\n\n\n\n.action-buttons[_ngcontent-%COMP%] {\n flex-shrink: 0;\n display: flex;\n gap: 0.75rem;\n justify-content: flex-end;\n padding: 0.75rem 1rem;\n background: var(--mj-bg-page);\n}\n\n@media (min-width: 768px) {\n .action-buttons[_ngcontent-%COMP%] {\n padding: 1rem 1.5rem;\n }\n}\n\n@media (max-width: 639px) {\n .action-buttons[_ngcontent-%COMP%] {\n justify-content: center;\n flex-wrap: wrap;\n }\n}\n\n\n\n\n\n.mj-grid-4[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 0.5rem;\n padding: 0 1rem 0.75rem 1rem;\n background: var(--mj-bg-page);\n}\n\n@media (min-width: 768px) {\n .mj-grid-4[_ngcontent-%COMP%] {\n grid-template-columns: repeat(4, 1fr);\n padding: 0 1.5rem 1rem 1.5rem;\n gap: 0.75rem;\n }\n}\n\n@media (min-width: 1024px) {\n .mj-grid-4[_ngcontent-%COMP%] {\n gap: 1rem;\n }\n}\n\n\n\n.mj-card[_ngcontent-%COMP%] {\n background: var(--mj-bg-surface-card);\n border-radius: var(--mj-radius-lg);\n padding: 1rem;\n box-shadow: var(--mj-shadow-sm);\n display: flex;\n align-items: center;\n gap: 1rem;\n border: 1px solid var(--mj-border-default);\n cursor: default;\n pointer-events: none;\n}\n\n@media (min-width: 768px) {\n .mj-card[_ngcontent-%COMP%] {\n padding: 1rem 1.25rem;\n }\n}\n\n\n\n\n\n.stat-icon[_ngcontent-%COMP%] {\n width: 48px;\n height: 48px;\n border-radius: var(--mj-radius-lg);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 1.25rem;\n flex-shrink: 0;\n}\n\n@media (min-width: 768px) {\n .stat-icon[_ngcontent-%COMP%] {\n width: 52px;\n height: 52px;\n font-size: 1.375rem;\n }\n}\n\n.stat-icon-total[_ngcontent-%COMP%] {\n background: var(--mj-color-accent-300);\n color: var(--mj-brand-primary);\n}\n\n.stat-icon-active[_ngcontent-%COMP%] {\n background: var(--mj-color-success-100);\n color: var(--mj-status-success);\n}\n\n\n\n\n\n.stat-content[_ngcontent-%COMP%] {\n flex: 1;\n min-width: 0;\n}\n\n.stat-content[_ngcontent-%COMP%] .stat-value[_ngcontent-%COMP%] {\n font-size: 1.75rem;\n font-weight: 700;\n color: var(--mj-text-primary);\n line-height: 1;\n letter-spacing: -0.02em;\n}\n\n@media (min-width: 768px) {\n .stat-content[_ngcontent-%COMP%] .stat-value[_ngcontent-%COMP%] {\n font-size: 2rem;\n }\n}\n\n.stat-content[_ngcontent-%COMP%] .stat-label[_ngcontent-%COMP%] {\n font-size: 0.75rem;\n font-weight: 500;\n color: var(--mj-text-secondary);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin-top: 0.25rem;\n}\n\n@media (min-width: 768px) {\n .stat-content[_ngcontent-%COMP%] .stat-label[_ngcontent-%COMP%] {\n font-size: 0.8125rem;\n }\n}\n\n\n\n\n\n.filters-section[_ngcontent-%COMP%] {\n flex-shrink: 0;\n background: var(--mj-bg-surface-card);\n margin: 0 1rem 0.75rem 1rem;\n padding: 0.75rem;\n border-radius: var(--mj-radius-lg);\n border: 1px solid var(--mj-border-default);\n}\n\n@media (min-width: 768px) {\n .filters-section[_ngcontent-%COMP%] {\n margin: 0 1.5rem 1rem 1.5rem;\n padding: 1rem 1.25rem;\n }\n}\n\n.filters-row[_ngcontent-%COMP%] {\n display: flex;\n gap: 1rem;\n align-items: flex-end;\n flex-wrap: wrap;\n}\n\n@media (min-width: 768px) {\n .filters-row[_ngcontent-%COMP%] {\n gap: 1.5rem;\n }\n}\n\n.mj-filter-group[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.mj-filter-label[_ngcontent-%COMP%] {\n font-size: 0.75rem;\n font-weight: 500;\n color: var(--mj-text-primary);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n}\n\n.mj-filter-buttons[_ngcontent-%COMP%] {\n display: flex;\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-full);\n padding: 4px;\n gap: 2px;\n}\n\n\n\n\n\n.mj-search[_ngcontent-%COMP%] {\n position: relative;\n flex: 1;\n min-width: 200px;\n}\n\n@media (min-width: 640px) {\n .mj-search[_ngcontent-%COMP%] {\n min-width: 280px;\n }\n}\n\n@media (min-width: 1024px) {\n .mj-search[_ngcontent-%COMP%] {\n min-width: 400px;\n max-width: 600px;\n }\n}\n\n@media (min-width: 1440px) {\n .mj-search[_ngcontent-%COMP%] {\n min-width: 500px;\n max-width: 800px;\n }\n}\n\n.mj-search[_ngcontent-%COMP%] .mj-search-icon[_ngcontent-%COMP%] {\n position: absolute;\n left: 1rem;\n top: 50%;\n transform: translateY(-50%);\n color: var(--mj-text-secondary);\n font-size: 1rem;\n pointer-events: none;\n transition: color 0.2s ease;\n}\n\n@media (min-width: 1024px) {\n .mj-search[_ngcontent-%COMP%] .mj-search-icon[_ngcontent-%COMP%] {\n left: 1.25rem;\n font-size: 1.125rem;\n }\n}\n\n.mj-search[_ngcontent-%COMP%] .mj-search-input[_ngcontent-%COMP%] {\n width: 100%;\n padding: 0.875rem 1rem 0.875rem 2.75rem;\n border: 2px solid var(--mj-border-default);\n border-radius: var(--mj-radius-full);\n font-size: 1rem;\n background: var(--mj-bg-page);\n color: var(--mj-text-primary);\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n min-height: 44px;\n}\n\n@media (min-width: 1024px) {\n .mj-search[_ngcontent-%COMP%] .mj-search-input[_ngcontent-%COMP%] {\n padding: 1rem 1.25rem 1rem 3.25rem;\n font-size: 1.0625rem;\n }\n}\n\n@media (min-width: 1440px) {\n .mj-search[_ngcontent-%COMP%] .mj-search-input[_ngcontent-%COMP%] {\n padding: 1.125rem 1.5rem 1.125rem 3.5rem;\n }\n}\n\n.mj-search[_ngcontent-%COMP%] .mj-search-input[_ngcontent-%COMP%]::placeholder {\n color: var(--mj-text-secondary);\n}\n\n.mj-search[_ngcontent-%COMP%] .mj-search-input[_ngcontent-%COMP%]:hover {\n border-color: var(--mj-brand-primary);\n background: var(--mj-bg-surface);\n}\n\n.mj-search[_ngcontent-%COMP%] .mj-search-input[_ngcontent-%COMP%]:focus {\n outline: none;\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px rgba(0, 118, 182, 0.2);\n background: var(--mj-bg-page);\n}\n\n.mj-search[_ngcontent-%COMP%]:focus-within .mj-search-icon[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n}\n\n\n\n\n\n.content-area[_ngcontent-%COMP%] {\n flex: 1 1 auto;\n overflow: visible;\n position: relative;\n background: var(--mj-bg-page);\n border-radius: var(--mj-radius-xl);\n box-shadow: var(--mj-shadow-sm);\n padding: 1rem;\n border: 1px solid var(--mj-border-default);\n}\n\n@media (min-width: 768px) {\n .content-area[_ngcontent-%COMP%] {\n padding: 1.5rem;\n }\n}\n\n\n\n\n\n.expand-btn[_ngcontent-%COMP%] {\n padding: 0.625rem;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 1rem;\n cursor: pointer;\n transition: all 0.2s ease;\n border-radius: var(--mj-radius-full);\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.expand-btn[_ngcontent-%COMP%]:hover {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n}\n\n.expand-btn[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n transition: transform 0.3s cubic-bezier(0, 0, 0.2, 1);\n}\n\n\n\n\n\n.status-badge[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.375rem 0.875rem;\n border-radius: var(--mj-radius-full);\n font-size: 0.75rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.03em;\n}\n\n.status-badge.status-active[_ngcontent-%COMP%] {\n background: var(--mj-color-success-100);\n color: var(--mj-color-success-800);\n border: 1px solid var(--mj-status-success);\n}\n\n.status-badge.status-inactive[_ngcontent-%COMP%] {\n background: var(--mj-color-error-100);\n color: var(--mj-status-error);\n border: 1px solid var(--mj-status-error);\n}\n\n.status-badge[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 0.625rem;\n}\n\n\n\n\n\n.mobile-actions-bar[_ngcontent-%COMP%] {\n display: none;\n}\n\n.mobile-action-buttons[_ngcontent-%COMP%] {\n display: flex;\n gap: 0.5rem;\n}\n\n\n\n\n\n.empty-state[_ngcontent-%COMP%] {\n text-align: center;\n padding: 4rem 2rem;\n}\n\n.empty-state[_ngcontent-%COMP%] .empty-icon[_ngcontent-%COMP%] {\n font-size: 4rem;\n color: var(--mj-border-default);\n margin-bottom: 1.5rem;\n}\n\n.empty-state[_ngcontent-%COMP%] .empty-text[_ngcontent-%COMP%] {\n font-size: 1.25rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0 0 0.5rem 0;\n}\n\n.empty-state[_ngcontent-%COMP%] .empty-subtext[_ngcontent-%COMP%] {\n font-size: 1rem;\n color: var(--mj-text-secondary);\n margin: 0;\n}\n\n\n\n\n\n.loading-container[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 4rem 2rem;\n}\n\n\n\n\n\n.error-container[_ngcontent-%COMP%] {\n text-align: center;\n padding: 4rem 2rem;\n}\n\n.error-container[_ngcontent-%COMP%] .error-content[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 1rem;\n}\n\n.error-container[_ngcontent-%COMP%] .error-icon[_ngcontent-%COMP%] {\n font-size: 3.5rem;\n color: var(--mj-status-error);\n margin-bottom: 1rem;\n}\n\n.error-container[_ngcontent-%COMP%] .error-message[_ngcontent-%COMP%] {\n font-size: 1.0625rem;\n color: var(--mj-text-primary);\n margin: 0 0 1.5rem 0;\n}\n\n\n\n\n\n.modal-backdrop[_ngcontent-%COMP%] {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.4);\n backdrop-filter: blur(4px);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1000;\n padding: 1rem;\n animation: _ngcontent-%COMP%_fadeIn 0.2s cubic-bezier(0, 0, 0.2, 1);\n}\n\n.modal-dialog[_ngcontent-%COMP%] {\n background: var(--mj-bg-page);\n border-radius: var(--mj-radius-2xl);\n box-shadow: var(--mj-shadow-2xl);\n max-width: 500px;\n width: 100%;\n max-height: 90vh;\n overflow: hidden;\n animation: _ngcontent-%COMP%_slideUp 0.3s cubic-bezier(0, 0, 0.2, 1);\n}\n\n.modal-header[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1.5rem;\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.modal-header[_ngcontent-%COMP%] .modal-title[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n font-size: 1.375rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0;\n}\n\n.modal-header[_ngcontent-%COMP%] .modal-close[_ngcontent-%COMP%] {\n padding: 0.625rem;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 1.25rem;\n cursor: pointer;\n border-radius: var(--mj-radius-full);\n transition: all 0.2s ease;\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.modal-header[_ngcontent-%COMP%] .modal-close[_ngcontent-%COMP%]:hover {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n}\n\n.modal-body[_ngcontent-%COMP%] {\n padding: 1.5rem;\n}\n\n.modal-body[_ngcontent-%COMP%] p[_ngcontent-%COMP%] {\n font-size: 1rem;\n color: var(--mj-text-primary);\n margin: 0 0 1rem 0;\n line-height: 1.5;\n}\n\n.modal-body[_ngcontent-%COMP%] p[_ngcontent-%COMP%]:last-child {\n margin-bottom: 0;\n}\n\n.modal-footer[_ngcontent-%COMP%] {\n display: flex;\n justify-content: flex-start;\n gap: 0.75rem;\n padding: 1.5rem;\n border-top: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface-sunken);\n}\n\n@media (max-width: 639px) {\n .modal-dialog[_ngcontent-%COMP%] {\n width: 95%;\n max-height: 85vh;\n }\n\n .modal-header[_ngcontent-%COMP%] {\n padding: 1rem;\n }\n\n .modal-header[_ngcontent-%COMP%] .modal-title[_ngcontent-%COMP%] {\n font-size: 1.125rem;\n }\n\n .modal-body[_ngcontent-%COMP%] {\n padding: 1rem;\n }\n\n .modal-footer[_ngcontent-%COMP%] {\n padding: 1rem;\n flex-direction: column;\n }\n\n}\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n.filter-button[_ngcontent-%COMP%] {\n display: none;\n align-items: center;\n justify-content: center;\n gap: 0.5rem;\n padding: 0.75rem 1.25rem;\n border: 2px solid var(--mj-border-default);\n background: var(--mj-bg-page);\n color: var(--mj-text-primary);\n border-radius: var(--mj-radius-full);\n font-size: 0.875rem;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n min-height: 44px;\n min-width: 44px;\n}\n\n.filter-button[_ngcontent-%COMP%]:hover {\n border-color: var(--mj-brand-primary);\n background: var(--mj-bg-surface);\n}\n\n.filter-button[_ngcontent-%COMP%]:active {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n border-color: var(--mj-brand-primary);\n}\n\n.filter-button[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 1rem;\n}\n\n\n\n\n\n.filter-modal-backdrop[_ngcontent-%COMP%] {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.4);\n backdrop-filter: blur(4px);\n display: flex;\n align-items: flex-end;\n justify-content: center;\n z-index: 1000;\n animation: _ngcontent-%COMP%_fadeIn 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n padding: 0;\n}\n\n.filter-modal[_ngcontent-%COMP%] {\n background: var(--mj-bg-page);\n border-radius: var(--mj-radius-2xl) var(--mj-radius-2xl) 0 0;\n box-shadow: var(--mj-shadow-2xl);\n width: 100%;\n max-height: 80vh;\n overflow: hidden;\n animation: _ngcontent-%COMP%_slideUpFromBottom 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n display: flex;\n flex-direction: column;\n}\n\n.filter-modal-header[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1.25rem 1.5rem;\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.filter-modal-title[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n font-size: 1.25rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0;\n}\n\n.filter-modal-title[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n font-size: 1.375rem;\n}\n\n.filter-modal-close[_ngcontent-%COMP%] {\n padding: 0.625rem;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 1.25rem;\n cursor: pointer;\n border-radius: var(--mj-radius-full);\n transition: all 0.2s ease;\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.filter-modal-close[_ngcontent-%COMP%]:hover {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n}\n\n.filter-modal-body[_ngcontent-%COMP%] {\n padding: 1.5rem;\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n flex: 1;\n}\n\n.filter-modal-footer[_ngcontent-%COMP%] {\n display: flex;\n gap: 0.75rem;\n padding: 1.25rem 1.5rem;\n border-top: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface-sunken);\n flex-shrink: 0;\n}\n\n.filter-modal-footer[_ngcontent-%COMP%] button[_ngcontent-%COMP%] {\n flex: 1;\n margin: 0;\n}\n\n\n\n.filter-options-container[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n}\n\n.filter-group[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.filter-group-label[_ngcontent-%COMP%] {\n font-size: 0.875rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin: 0;\n}\n\n.filter-group-options[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.filter-option[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 0.875rem 1rem;\n background: var(--mj-bg-surface-sunken);\n border: 2px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg);\n cursor: pointer;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n min-height: 56px;\n}\n\n.filter-option[_ngcontent-%COMP%]:hover {\n border-color: var(--mj-brand-primary);\n background: var(--mj-bg-surface-sunken);\n}\n\n.filter-option.selected[_ngcontent-%COMP%] {\n background: var(--mj-color-accent-300);\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n}\n\n.filter-option[_ngcontent-%COMP%] input[type=\"checkbox\"][_ngcontent-%COMP%], \n.filter-option[_ngcontent-%COMP%] input[type=\"radio\"][_ngcontent-%COMP%] {\n width: 20px;\n height: 20px;\n cursor: pointer;\n accent-color: var(--mj-brand-primary);\n flex-shrink: 0;\n}\n\n.filter-option-label[_ngcontent-%COMP%] {\n flex: 1;\n font-size: 1rem;\n font-weight: 500;\n color: var(--mj-text-primary);\n}\n\n.filter-option.selected[_ngcontent-%COMP%] .filter-option-label[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n font-weight: 600;\n}\n\n\n\n\n\n.text-danger[_ngcontent-%COMP%] {\n color: var(--mj-status-error);\n}\n\n.text-warning[_ngcontent-%COMP%] {\n color: var(--mj-color-warning-500);\n}\n\n.text-success[_ngcontent-%COMP%] {\n color: var(--mj-status-success);\n}\n\n\n\n\n\n@keyframes _ngcontent-%COMP%_fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n@keyframes _ngcontent-%COMP%_slideUp {\n from {\n transform: translateY(20px);\n opacity: 0;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n}\n\n@keyframes _ngcontent-%COMP%_slideDown {\n from {\n opacity: 0;\n transform: translateY(-10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes _ngcontent-%COMP%_slideUpFromBottom {\n from {\n transform: translateY(100%);\n opacity: 0;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n}\n\n\n\n\n\n.mj-search-input[_ngcontent-%COMP%]:focus-visible, \nbutton[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid var(--mj-brand-primary);\n outline-offset: 2px;\n}\n\n\n\n\n\n@media (prefers-reduced-motion: reduce) {\n *[_ngcontent-%COMP%], \n *[_ngcontent-%COMP%]::before, \n *[_ngcontent-%COMP%]::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n }\n}", "\n\n\n\n\n\n\n\n\n\n\n\n\n.status-chips[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: var(--mj-space-2);\n flex-shrink: 0;\n}\n\n\n\n\n\n.mj-grid-4[_ngcontent-%COMP%] {\n gap: 0.75rem;\n padding: 0 1rem 1rem 1rem;\n}\n\n@media (min-width: 768px) {\n .mj-grid-4[_ngcontent-%COMP%] {\n padding: 0 1.5rem 1.25rem 1.5rem;\n gap: 1rem;\n }\n}\n\n@media (min-width: 1024px) {\n .mj-grid-4[_ngcontent-%COMP%] {\n gap: 1.25rem;\n }\n}\n\n\n\n.mj-card[_ngcontent-%COMP%] {\n box-shadow: none;\n}\n\n@media (min-width: 768px) {\n .mj-card[_ngcontent-%COMP%] {\n padding: 1.125rem 1.25rem;\n }\n}\n\n\n\n.stat-icon-system[_ngcontent-%COMP%] {\n background: var(--mj-color-warning-50);\n color: var(--mj-color-warning-700);\n}\n\n.stat-icon-custom[_ngcontent-%COMP%] {\n background: var(--mj-color-success-100);\n color: var(--mj-status-success);\n}\n\n.stat-icon-active[_ngcontent-%COMP%] {\n background: var(--mj-color-accent-300);\n color: var(--mj-brand-primary);\n}\n\n\n\n\n\n.roles-list[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n\n\n\n\n.role-card[_ngcontent-%COMP%] {\n background: var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-xl);\n overflow: hidden;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.role-card[_ngcontent-%COMP%]:hover {\n box-shadow: var(--mj-shadow-md);\n border-color: var(--mj-brand-primary);\n}\n\n.role-card.expanded[_ngcontent-%COMP%] {\n box-shadow: var(--mj-shadow-lg);\n border-color: var(--mj-brand-primary);\n}\n\n.role-card.expanded[_ngcontent-%COMP%] .expand-btn[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n transform: rotate(180deg);\n}\n\n.role-header[_ngcontent-%COMP%] {\n padding: 1.25rem 1.5rem;\n display: flex;\n justify-content: space-between;\n align-items: center;\n cursor: pointer;\n background: var(--mj-bg-surface);\n transition: background-color 0.2s ease;\n}\n\n.role-header[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-sunken);\n}\n\n.role-info[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 1rem;\n flex: 1;\n min-width: 0;\n}\n\n.role-icon-wrapper[_ngcontent-%COMP%] {\n width: 52px;\n height: 52px;\n border-radius: var(--mj-radius-lg);\n background: var(--mj-color-accent-300);\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--mj-brand-primary);\n font-size: 1.375rem;\n flex-shrink: 0;\n}\n\n.role-details[_ngcontent-%COMP%] {\n flex: 1;\n min-width: 0;\n}\n\n.role-details[_ngcontent-%COMP%] .role-name[_ngcontent-%COMP%] {\n font-size: 1.125rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0 0 0.25rem 0;\n word-break: break-word;\n overflow-wrap: break-word;\n}\n\n.role-details[_ngcontent-%COMP%] .role-description[_ngcontent-%COMP%] {\n font-size: 0.875rem;\n color: var(--mj-text-secondary);\n margin: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n@media (max-width: 639px) {\n .role-details[_ngcontent-%COMP%] .role-description[_ngcontent-%COMP%] {\n white-space: normal;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n }\n}\n\n.role-meta[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 1rem;\n flex-shrink: 0;\n}\n\n\n\n\n\n.role-type-badge[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.375rem 0.875rem;\n border-radius: var(--mj-radius-full);\n font-size: 0.75rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.03em;\n}\n\n.role-type-badge.badge-system[_ngcontent-%COMP%] {\n background: var(--mj-color-warning-50);\n color: var(--mj-color-warning-800);\n border: 1px solid var(--mj-color-warning-500);\n}\n\n.role-type-badge.badge-custom[_ngcontent-%COMP%] {\n background: var(--mj-color-success-100);\n color: var(--mj-color-success-800);\n border: 1px solid var(--mj-status-success);\n}\n\n\n\n\n\n.role-actions[_ngcontent-%COMP%] {\n display: flex;\n gap: 0.375rem;\n}\n\n\n\n.expand-btn[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n\n\n\n\n.role-content[_ngcontent-%COMP%] {\n padding: 1.5rem;\n background: var(--mj-bg-surface);\n border-top: 1px solid var(--mj-border-default);\n animation: slideDown 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.role-stats[_ngcontent-%COMP%] {\n display: flex;\n gap: 2rem;\n margin-bottom: 1.5rem;\n flex-wrap: wrap;\n}\n\n.role-stats[_ngcontent-%COMP%] .stat-item[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n font-size: 0.9375rem;\n padding: 0.5rem 1rem;\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-md);\n}\n\n.role-stats[_ngcontent-%COMP%] .stat-item[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n font-size: 1rem;\n}\n\n.role-stats[_ngcontent-%COMP%] .stat-item[_ngcontent-%COMP%] .stat-label[_ngcontent-%COMP%] {\n color: var(--mj-text-secondary);\n}\n\n.role-stats[_ngcontent-%COMP%] .stat-item[_ngcontent-%COMP%] .stat-value[_ngcontent-%COMP%] {\n color: var(--mj-text-primary);\n font-weight: 600;\n}\n\n\n\n\n\n.permissions-preview[_ngcontent-%COMP%] {\n padding: 1.25rem;\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-lg);\n border: 1px solid var(--mj-border-default);\n}\n\n.permissions-preview[_ngcontent-%COMP%] .section-title[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 0.625rem;\n font-size: 1rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0 0 0.5rem 0;\n}\n\n.permissions-preview[_ngcontent-%COMP%] .section-title[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n font-size: 1.125rem;\n}\n\n.permissions-preview[_ngcontent-%COMP%] .permissions-note[_ngcontent-%COMP%] {\n font-size: 0.9375rem;\n color: var(--mj-text-secondary);\n margin: 0;\n}\n\n\n\n.modal-body[_ngcontent-%COMP%] p[_ngcontent-%COMP%] {\n line-height: 1.6;\n}\n\n.modal-body[_ngcontent-%COMP%] .text-warning[_ngcontent-%COMP%] {\n color: var(--mj-color-warning-500);\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.75rem 1rem;\n background: var(--mj-color-warning-50);\n border-radius: var(--mj-radius-md);\n font-weight: 500;\n}\n\n\n\n.modal-backdrop[_ngcontent-%COMP%] {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n}\n\n\n\n\n\n@media (min-width: 1440px) {\n .role-management-container[_ngcontent-%COMP%] {\n margin: 0 auto;\n }\n\n .action-buttons[_ngcontent-%COMP%] {\n padding: 1rem 2rem;\n }\n\n .mj-grid-4[_ngcontent-%COMP%] {\n padding: 0 2rem 1.25rem 2rem;\n gap: 1.5rem;\n }\n\n}\n\n\n\n\n\n@media (max-width: 639px) {\n .mj-grid-4[_ngcontent-%COMP%] {\n display: none !important;\n }\n\n .scrollable-content[_ngcontent-%COMP%] {\n padding: 0.75rem;\n }\n\n .modal-backdrop[_ngcontent-%COMP%] {\n padding: 0.5rem;\n }\n\n .modal-dialog[_ngcontent-%COMP%] {\n border-radius: var(--mj-radius-xl);\n max-height: 95vh;\n }\n\n .roles-list[_ngcontent-%COMP%] {\n gap: 0.75rem;\n }\n\n .role-card[_ngcontent-%COMP%] {\n border-radius: var(--mj-radius-lg);\n overflow: hidden;\n }\n\n .role-header[_ngcontent-%COMP%] {\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n padding: 1rem;\n }\n\n .role-info[_ngcontent-%COMP%] {\n flex: 1;\n min-width: 0;\n flex-direction: row;\n align-items: flex-start;\n gap: 0.875rem;\n }\n\n .role-icon-wrapper[_ngcontent-%COMP%] {\n width: 40px;\n height: 40px;\n font-size: 1rem;\n flex-shrink: 0;\n }\n\n .role-details[_ngcontent-%COMP%] {\n flex: 1;\n min-width: 0;\n }\n\n .role-details[_ngcontent-%COMP%] .role-name[_ngcontent-%COMP%] {\n font-size: 1rem;\n line-height: 1.3;\n margin-bottom: 0.125rem;\n }\n\n .role-details[_ngcontent-%COMP%] .role-description[_ngcontent-%COMP%] {\n font-size: 0.8125rem;\n -webkit-line-clamp: 1;\n line-clamp: 1;\n }\n\n .role-meta[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: flex-end;\n padding-top: 0;\n border-top: none;\n width: auto;\n }\n\n .role-meta[_ngcontent-%COMP%] .role-type-badge[_ngcontent-%COMP%] {\n display: none;\n }\n\n .role-meta[_ngcontent-%COMP%] .role-actions[_ngcontent-%COMP%] {\n display: none;\n }\n\n .expand-btn[_ngcontent-%COMP%] {\n flex-shrink: 0;\n min-width: 40px;\n min-height: 40px;\n padding: 0.5rem;\n }\n\n .expand-btn[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 1rem;\n }\n\n .mobile-actions-bar[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n padding: 0.875rem;\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-lg);\n margin-bottom: 1rem;\n }\n\n .mobile-actions-bar[_ngcontent-%COMP%] .role-type-badge[_ngcontent-%COMP%] {\n padding: 0.375rem 0.75rem;\n font-size: 0.6875rem;\n }\n\n .mobile-action-buttons[_ngcontent-%COMP%] {\n display: flex;\n gap: 0.5rem;\n }\n\n .mobile-action-buttons[_ngcontent-%COMP%] .mj-btn-sm[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.5rem 0.875rem;\n min-height: 36px;\n }\n\n .mobile-action-buttons[_ngcontent-%COMP%] .mj-btn-sm[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 0.875rem;\n }\n\n .mobile-action-buttons[_ngcontent-%COMP%] .btn-label[_ngcontent-%COMP%] {\n font-size: 0.8125rem;\n font-weight: 500;\n }\n\n .role-stats[_ngcontent-%COMP%] {\n flex-direction: column;\n gap: 0.5rem;\n margin-bottom: 1rem;\n }\n\n .role-content[_ngcontent-%COMP%] {\n padding: 1rem;\n }\n\n .permissions-preview[_ngcontent-%COMP%] {\n padding: 1rem;\n }\n\n .empty-state[_ngcontent-%COMP%] {\n padding: 3rem 1.5rem;\n }\n\n .empty-state[_ngcontent-%COMP%] .empty-icon[_ngcontent-%COMP%] {\n font-size: 3rem;\n }\n\n .loading-container[_ngcontent-%COMP%] {\n padding: 3rem 1.5rem;\n }\n}\n\n\n\n@media (max-width: 374px) {\n .action-buttons[_ngcontent-%COMP%] {\n padding: 0.375rem 0.5rem;\n }\n\n .scrollable-content[_ngcontent-%COMP%] {\n padding: 0.5rem;\n }\n\n .role-header[_ngcontent-%COMP%] {\n padding: 0.75rem;\n }\n}"] });
|
|
731
|
+
} }, dependencies: [i1.LoadingComponent, i2.MJButtonDirective, i2.MJPageHeaderInteriorComponent, i2.MJPageBodyInteriorComponent, i2.MJPageSearchComponent, i2.MJFilterPopoverComponent, i2.MJFilterPanelComponent, i2.MJRefreshButtonComponent, i2.MJStatBadgeComponent, i2.MJEmptyStateComponent, i2.MJAlertComponent, i3.RoleDialogComponent, i4.DatePipe], styles: ["\n\n\n\n\n\n\n\n\n\n\n[_nghost-%COMP%] {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n overflow: hidden;\n}\n\n\n\n\n\n.sticky-header[_ngcontent-%COMP%] {\n flex-shrink: 0;\n background: var(--mj-bg-page);\n border-bottom: 1px solid var(--mj-border-default);\n box-shadow: var(--mj-shadow-sm);\n z-index: 10;\n}\n\n\n\n\n\n.scrollable-content[_ngcontent-%COMP%] {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n -webkit-overflow-scrolling: touch;\n padding: 1rem;\n background: var(--mj-bg-surface-sunken);\n}\n\n@media (min-width: 768px) {\n .scrollable-content[_ngcontent-%COMP%] {\n padding: 1.5rem 2rem;\n }\n}\n\n@media (min-width: 1024px) {\n .scrollable-content[_ngcontent-%COMP%] {\n padding: 2rem;\n }\n}\n\n@media (min-width: 1440px) {\n .scrollable-content[_ngcontent-%COMP%] {\n padding: 2rem 2.5rem;\n }\n}\n\n\n\n\n\n.action-buttons[_ngcontent-%COMP%] {\n flex-shrink: 0;\n display: flex;\n gap: 0.75rem;\n justify-content: flex-end;\n padding: 0.75rem 1rem;\n background: var(--mj-bg-page);\n}\n\n@media (min-width: 768px) {\n .action-buttons[_ngcontent-%COMP%] {\n padding: 1rem 1.5rem;\n }\n}\n\n@media (max-width: 639px) {\n .action-buttons[_ngcontent-%COMP%] {\n justify-content: center;\n flex-wrap: wrap;\n }\n}\n\n\n\n\n\n.mj-grid-4[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 0.5rem;\n padding: 0 1rem 0.75rem 1rem;\n background: var(--mj-bg-page);\n}\n\n@media (min-width: 768px) {\n .mj-grid-4[_ngcontent-%COMP%] {\n grid-template-columns: repeat(4, 1fr);\n padding: 0 1.5rem 1rem 1.5rem;\n gap: 0.75rem;\n }\n}\n\n@media (min-width: 1024px) {\n .mj-grid-4[_ngcontent-%COMP%] {\n gap: 1rem;\n }\n}\n\n\n\n.mj-card[_ngcontent-%COMP%] {\n background: var(--mj-bg-surface-card);\n border-radius: var(--mj-radius-lg);\n padding: 1rem;\n box-shadow: var(--mj-shadow-sm);\n display: flex;\n align-items: center;\n gap: 1rem;\n border: 1px solid var(--mj-border-default);\n cursor: default;\n pointer-events: none;\n}\n\n@media (min-width: 768px) {\n .mj-card[_ngcontent-%COMP%] {\n padding: 1rem 1.25rem;\n }\n}\n\n\n\n\n\n.stat-icon[_ngcontent-%COMP%] {\n width: 48px;\n height: 48px;\n border-radius: var(--mj-radius-lg);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 1.25rem;\n flex-shrink: 0;\n}\n\n@media (min-width: 768px) {\n .stat-icon[_ngcontent-%COMP%] {\n width: 52px;\n height: 52px;\n font-size: 1.375rem;\n }\n}\n\n.stat-icon-total[_ngcontent-%COMP%] {\n background: var(--mj-color-accent-300);\n color: var(--mj-brand-primary);\n}\n\n.stat-icon-active[_ngcontent-%COMP%] {\n background: var(--mj-color-success-100);\n color: var(--mj-status-success);\n}\n\n\n\n\n\n.stat-content[_ngcontent-%COMP%] {\n flex: 1;\n min-width: 0;\n}\n\n.stat-content[_ngcontent-%COMP%] .stat-value[_ngcontent-%COMP%] {\n font-size: 1.75rem;\n font-weight: 700;\n color: var(--mj-text-primary);\n line-height: 1;\n letter-spacing: -0.02em;\n}\n\n@media (min-width: 768px) {\n .stat-content[_ngcontent-%COMP%] .stat-value[_ngcontent-%COMP%] {\n font-size: 2rem;\n }\n}\n\n.stat-content[_ngcontent-%COMP%] .stat-label[_ngcontent-%COMP%] {\n font-size: 0.75rem;\n font-weight: 500;\n color: var(--mj-text-secondary);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin-top: 0.25rem;\n}\n\n@media (min-width: 768px) {\n .stat-content[_ngcontent-%COMP%] .stat-label[_ngcontent-%COMP%] {\n font-size: 0.8125rem;\n }\n}\n\n\n\n\n\n.filters-section[_ngcontent-%COMP%] {\n flex-shrink: 0;\n background: var(--mj-bg-surface-card);\n margin: 0 1rem 0.75rem 1rem;\n padding: 0.75rem;\n border-radius: var(--mj-radius-lg);\n border: 1px solid var(--mj-border-default);\n}\n\n@media (min-width: 768px) {\n .filters-section[_ngcontent-%COMP%] {\n margin: 0 1.5rem 1rem 1.5rem;\n padding: 1rem 1.25rem;\n }\n}\n\n.filters-row[_ngcontent-%COMP%] {\n display: flex;\n gap: 1rem;\n align-items: flex-end;\n flex-wrap: wrap;\n}\n\n@media (min-width: 768px) {\n .filters-row[_ngcontent-%COMP%] {\n gap: 1.5rem;\n }\n}\n\n.mj-filter-group[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.mj-filter-label[_ngcontent-%COMP%] {\n font-size: 0.75rem;\n font-weight: 500;\n color: var(--mj-text-primary);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n}\n\n.mj-filter-buttons[_ngcontent-%COMP%] {\n display: flex;\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-full);\n padding: 4px;\n gap: 2px;\n}\n\n\n\n\n\n.mj-search[_ngcontent-%COMP%] {\n position: relative;\n flex: 1;\n min-width: 200px;\n}\n\n@media (min-width: 640px) {\n .mj-search[_ngcontent-%COMP%] {\n min-width: 280px;\n }\n}\n\n@media (min-width: 1024px) {\n .mj-search[_ngcontent-%COMP%] {\n min-width: 400px;\n max-width: 600px;\n }\n}\n\n@media (min-width: 1440px) {\n .mj-search[_ngcontent-%COMP%] {\n min-width: 500px;\n max-width: 800px;\n }\n}\n\n.mj-search[_ngcontent-%COMP%] .mj-search-icon[_ngcontent-%COMP%] {\n position: absolute;\n left: 1rem;\n top: 50%;\n transform: translateY(-50%);\n color: var(--mj-text-secondary);\n font-size: 1rem;\n pointer-events: none;\n transition: color 0.2s ease;\n}\n\n@media (min-width: 1024px) {\n .mj-search[_ngcontent-%COMP%] .mj-search-icon[_ngcontent-%COMP%] {\n left: 1.25rem;\n font-size: 1.125rem;\n }\n}\n\n.mj-search[_ngcontent-%COMP%] .mj-search-input[_ngcontent-%COMP%] {\n width: 100%;\n padding: 0.875rem 1rem 0.875rem 2.75rem;\n border: 2px solid var(--mj-border-default);\n border-radius: var(--mj-radius-full);\n font-size: 1rem;\n background: var(--mj-bg-page);\n color: var(--mj-text-primary);\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n min-height: 44px;\n}\n\n@media (min-width: 1024px) {\n .mj-search[_ngcontent-%COMP%] .mj-search-input[_ngcontent-%COMP%] {\n padding: 1rem 1.25rem 1rem 3.25rem;\n font-size: 1.0625rem;\n }\n}\n\n@media (min-width: 1440px) {\n .mj-search[_ngcontent-%COMP%] .mj-search-input[_ngcontent-%COMP%] {\n padding: 1.125rem 1.5rem 1.125rem 3.5rem;\n }\n}\n\n.mj-search[_ngcontent-%COMP%] .mj-search-input[_ngcontent-%COMP%]::placeholder {\n color: var(--mj-text-secondary);\n}\n\n.mj-search[_ngcontent-%COMP%] .mj-search-input[_ngcontent-%COMP%]:hover {\n border-color: var(--mj-brand-primary);\n background: var(--mj-bg-surface);\n}\n\n.mj-search[_ngcontent-%COMP%] .mj-search-input[_ngcontent-%COMP%]:focus {\n outline: none;\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px rgba(0, 118, 182, 0.2);\n background: var(--mj-bg-page);\n}\n\n.mj-search[_ngcontent-%COMP%]:focus-within .mj-search-icon[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n}\n\n\n\n\n\n.content-area[_ngcontent-%COMP%] {\n flex: 1 1 auto;\n overflow: visible;\n position: relative;\n background: var(--mj-bg-page);\n border-radius: var(--mj-radius-xl);\n box-shadow: var(--mj-shadow-sm);\n padding: 1rem;\n border: 1px solid var(--mj-border-default);\n}\n\n@media (min-width: 768px) {\n .content-area[_ngcontent-%COMP%] {\n padding: 1.5rem;\n }\n}\n\n\n\n\n\n.expand-btn[_ngcontent-%COMP%] {\n padding: 0.625rem;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 1rem;\n cursor: pointer;\n transition: all 0.2s ease;\n border-radius: var(--mj-radius-full);\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.expand-btn[_ngcontent-%COMP%]:hover {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n}\n\n.expand-btn[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n transition: transform 0.3s cubic-bezier(0, 0, 0.2, 1);\n}\n\n\n\n\n\n.status-badge[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.375rem 0.875rem;\n border-radius: var(--mj-radius-full);\n font-size: 0.75rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.03em;\n}\n\n.status-badge.status-active[_ngcontent-%COMP%] {\n background: var(--mj-color-success-100);\n color: var(--mj-color-success-800);\n border: 1px solid var(--mj-status-success);\n}\n\n.status-badge.status-inactive[_ngcontent-%COMP%] {\n background: var(--mj-color-error-100);\n color: var(--mj-status-error);\n border: 1px solid var(--mj-status-error);\n}\n\n.status-badge[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 0.625rem;\n}\n\n\n\n\n\n.mobile-actions-bar[_ngcontent-%COMP%] {\n display: none;\n}\n\n.mobile-action-buttons[_ngcontent-%COMP%] {\n display: flex;\n gap: 0.5rem;\n}\n\n\n\n\n\n.empty-state[_ngcontent-%COMP%] {\n text-align: center;\n padding: 4rem 2rem;\n}\n\n.empty-state[_ngcontent-%COMP%] .empty-icon[_ngcontent-%COMP%] {\n font-size: 4rem;\n color: var(--mj-border-default);\n margin-bottom: 1.5rem;\n}\n\n.empty-state[_ngcontent-%COMP%] .empty-text[_ngcontent-%COMP%] {\n font-size: 1.25rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0 0 0.5rem 0;\n}\n\n.empty-state[_ngcontent-%COMP%] .empty-subtext[_ngcontent-%COMP%] {\n font-size: 1rem;\n color: var(--mj-text-secondary);\n margin: 0;\n}\n\n\n\n\n\n.loading-container[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 4rem 2rem;\n}\n\n\n\n\n\n.error-container[_ngcontent-%COMP%] {\n text-align: center;\n padding: 4rem 2rem;\n}\n\n.error-container[_ngcontent-%COMP%] .error-content[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 1rem;\n}\n\n.error-container[_ngcontent-%COMP%] .error-icon[_ngcontent-%COMP%] {\n font-size: 3.5rem;\n color: var(--mj-status-error);\n margin-bottom: 1rem;\n}\n\n.error-container[_ngcontent-%COMP%] .error-message[_ngcontent-%COMP%] {\n font-size: 1.0625rem;\n color: var(--mj-text-primary);\n margin: 0 0 1.5rem 0;\n}\n\n\n\n\n\n.modal-backdrop[_ngcontent-%COMP%] {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.4);\n backdrop-filter: blur(4px);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1000;\n padding: 1rem;\n animation: _ngcontent-%COMP%_fadeIn 0.2s cubic-bezier(0, 0, 0.2, 1);\n}\n\n.modal-dialog[_ngcontent-%COMP%] {\n background: var(--mj-bg-page);\n border-radius: var(--mj-radius-2xl);\n box-shadow: var(--mj-shadow-2xl);\n max-width: 500px;\n width: 100%;\n max-height: 90vh;\n overflow: hidden;\n animation: _ngcontent-%COMP%_slideUp 0.3s cubic-bezier(0, 0, 0.2, 1);\n}\n\n.modal-header[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1.5rem;\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.modal-header[_ngcontent-%COMP%] .modal-title[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n font-size: 1.375rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0;\n}\n\n.modal-header[_ngcontent-%COMP%] .modal-close[_ngcontent-%COMP%] {\n padding: 0.625rem;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 1.25rem;\n cursor: pointer;\n border-radius: var(--mj-radius-full);\n transition: all 0.2s ease;\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.modal-header[_ngcontent-%COMP%] .modal-close[_ngcontent-%COMP%]:hover {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n}\n\n.modal-body[_ngcontent-%COMP%] {\n padding: 1.5rem;\n}\n\n.modal-body[_ngcontent-%COMP%] p[_ngcontent-%COMP%] {\n font-size: 1rem;\n color: var(--mj-text-primary);\n margin: 0 0 1rem 0;\n line-height: 1.5;\n}\n\n.modal-body[_ngcontent-%COMP%] p[_ngcontent-%COMP%]:last-child {\n margin-bottom: 0;\n}\n\n.modal-footer[_ngcontent-%COMP%] {\n display: flex;\n justify-content: flex-start;\n gap: 0.75rem;\n padding: 1.5rem;\n border-top: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface-sunken);\n}\n\n@media (max-width: 639px) {\n .modal-dialog[_ngcontent-%COMP%] {\n width: 95%;\n max-height: 85vh;\n }\n\n .modal-header[_ngcontent-%COMP%] {\n padding: 1rem;\n }\n\n .modal-header[_ngcontent-%COMP%] .modal-title[_ngcontent-%COMP%] {\n font-size: 1.125rem;\n }\n\n .modal-body[_ngcontent-%COMP%] {\n padding: 1rem;\n }\n\n .modal-footer[_ngcontent-%COMP%] {\n padding: 1rem;\n flex-direction: column;\n }\n\n}\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n.filter-button[_ngcontent-%COMP%] {\n display: none;\n align-items: center;\n justify-content: center;\n gap: 0.5rem;\n padding: 0.75rem 1.25rem;\n border: 2px solid var(--mj-border-default);\n background: var(--mj-bg-page);\n color: var(--mj-text-primary);\n border-radius: var(--mj-radius-full);\n font-size: 0.875rem;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n min-height: 44px;\n min-width: 44px;\n}\n\n.filter-button[_ngcontent-%COMP%]:hover {\n border-color: var(--mj-brand-primary);\n background: var(--mj-bg-surface);\n}\n\n.filter-button[_ngcontent-%COMP%]:active {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n border-color: var(--mj-brand-primary);\n}\n\n.filter-button[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 1rem;\n}\n\n\n\n\n\n.filter-modal-backdrop[_ngcontent-%COMP%] {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.4);\n backdrop-filter: blur(4px);\n display: flex;\n align-items: flex-end;\n justify-content: center;\n z-index: 1000;\n animation: _ngcontent-%COMP%_fadeIn 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n padding: 0;\n}\n\n.filter-modal[_ngcontent-%COMP%] {\n background: var(--mj-bg-page);\n border-radius: var(--mj-radius-2xl) var(--mj-radius-2xl) 0 0;\n box-shadow: var(--mj-shadow-2xl);\n width: 100%;\n max-height: 80vh;\n overflow: hidden;\n animation: _ngcontent-%COMP%_slideUpFromBottom 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n display: flex;\n flex-direction: column;\n}\n\n.filter-modal-header[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1.25rem 1.5rem;\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.filter-modal-title[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n font-size: 1.25rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0;\n}\n\n.filter-modal-title[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n font-size: 1.375rem;\n}\n\n.filter-modal-close[_ngcontent-%COMP%] {\n padding: 0.625rem;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 1.25rem;\n cursor: pointer;\n border-radius: var(--mj-radius-full);\n transition: all 0.2s ease;\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.filter-modal-close[_ngcontent-%COMP%]:hover {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n}\n\n.filter-modal-body[_ngcontent-%COMP%] {\n padding: 1.5rem;\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n flex: 1;\n}\n\n.filter-modal-footer[_ngcontent-%COMP%] {\n display: flex;\n gap: 0.75rem;\n padding: 1.25rem 1.5rem;\n border-top: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface-sunken);\n flex-shrink: 0;\n}\n\n.filter-modal-footer[_ngcontent-%COMP%] button[_ngcontent-%COMP%] {\n flex: 1;\n margin: 0;\n}\n\n\n\n.filter-options-container[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n}\n\n.filter-group[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.filter-group-label[_ngcontent-%COMP%] {\n font-size: 0.875rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin: 0;\n}\n\n.filter-group-options[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.filter-option[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 0.875rem 1rem;\n background: var(--mj-bg-surface-sunken);\n border: 2px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg);\n cursor: pointer;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n min-height: 56px;\n}\n\n.filter-option[_ngcontent-%COMP%]:hover {\n border-color: var(--mj-brand-primary);\n background: var(--mj-bg-surface-sunken);\n}\n\n.filter-option.selected[_ngcontent-%COMP%] {\n background: var(--mj-color-accent-300);\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n}\n\n.filter-option[_ngcontent-%COMP%] input[type=\"checkbox\"][_ngcontent-%COMP%], \n.filter-option[_ngcontent-%COMP%] input[type=\"radio\"][_ngcontent-%COMP%] {\n width: 20px;\n height: 20px;\n cursor: pointer;\n accent-color: var(--mj-brand-primary);\n flex-shrink: 0;\n}\n\n.filter-option-label[_ngcontent-%COMP%] {\n flex: 1;\n font-size: 1rem;\n font-weight: 500;\n color: var(--mj-text-primary);\n}\n\n.filter-option.selected[_ngcontent-%COMP%] .filter-option-label[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n font-weight: 600;\n}\n\n\n\n\n\n.text-danger[_ngcontent-%COMP%] {\n color: var(--mj-status-error);\n}\n\n.text-warning[_ngcontent-%COMP%] {\n color: var(--mj-color-warning-500);\n}\n\n.text-success[_ngcontent-%COMP%] {\n color: var(--mj-status-success);\n}\n\n\n\n\n\n@keyframes _ngcontent-%COMP%_fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n@keyframes _ngcontent-%COMP%_slideUp {\n from {\n transform: translateY(20px);\n opacity: 0;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n}\n\n@keyframes _ngcontent-%COMP%_slideDown {\n from {\n opacity: 0;\n transform: translateY(-10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes _ngcontent-%COMP%_slideUpFromBottom {\n from {\n transform: translateY(100%);\n opacity: 0;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n}\n\n\n\n\n\n.mj-search-input[_ngcontent-%COMP%]:focus-visible, \nbutton[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid var(--mj-brand-primary);\n outline-offset: 2px;\n}\n\n\n\n\n\n@media (prefers-reduced-motion: reduce) {\n *[_ngcontent-%COMP%], \n *[_ngcontent-%COMP%]::before, \n *[_ngcontent-%COMP%]::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n }\n}", "\n\n\n\n\n\n\n\n\n\n\n\n\n.status-chips[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: var(--mj-space-2);\n flex-shrink: 0;\n}\n\n\n\n\n\n.mj-grid-4[_ngcontent-%COMP%] {\n gap: 0.75rem;\n padding: 0 1rem 1rem 1rem;\n}\n\n@media (min-width: 768px) {\n .mj-grid-4[_ngcontent-%COMP%] {\n padding: 0 1.5rem 1.25rem 1.5rem;\n gap: 1rem;\n }\n}\n\n@media (min-width: 1024px) {\n .mj-grid-4[_ngcontent-%COMP%] {\n gap: 1.25rem;\n }\n}\n\n\n\n.mj-card[_ngcontent-%COMP%] {\n box-shadow: none;\n}\n\n@media (min-width: 768px) {\n .mj-card[_ngcontent-%COMP%] {\n padding: 1.125rem 1.25rem;\n }\n}\n\n\n\n.stat-icon-system[_ngcontent-%COMP%] {\n background: var(--mj-color-warning-50);\n color: var(--mj-color-warning-700);\n}\n\n.stat-icon-custom[_ngcontent-%COMP%] {\n background: var(--mj-color-success-100);\n color: var(--mj-status-success);\n}\n\n.stat-icon-active[_ngcontent-%COMP%] {\n background: var(--mj-color-accent-300);\n color: var(--mj-brand-primary);\n}\n\n\n\n\n\n.roles-list[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n\n\n\n\n.role-card[_ngcontent-%COMP%] {\n background: var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-xl);\n overflow: hidden;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.role-card[_ngcontent-%COMP%]:hover {\n box-shadow: var(--mj-shadow-md);\n border-color: var(--mj-brand-primary);\n}\n\n.role-card.expanded[_ngcontent-%COMP%] {\n box-shadow: var(--mj-shadow-lg);\n border-color: var(--mj-brand-primary);\n}\n\n.role-card.expanded[_ngcontent-%COMP%] .expand-btn[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n transform: rotate(180deg);\n}\n\n.role-header[_ngcontent-%COMP%] {\n padding: 1.25rem 1.5rem;\n display: flex;\n justify-content: space-between;\n align-items: center;\n cursor: pointer;\n background: var(--mj-bg-surface);\n transition: background-color 0.2s ease;\n}\n\n.role-header[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-sunken);\n}\n\n.role-info[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 1rem;\n flex: 1;\n min-width: 0;\n}\n\n.role-icon-wrapper[_ngcontent-%COMP%] {\n width: 52px;\n height: 52px;\n border-radius: var(--mj-radius-lg);\n background: var(--mj-color-accent-300);\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--mj-brand-primary);\n font-size: 1.375rem;\n flex-shrink: 0;\n}\n\n.role-details[_ngcontent-%COMP%] {\n flex: 1;\n min-width: 0;\n}\n\n.role-details[_ngcontent-%COMP%] .role-name[_ngcontent-%COMP%] {\n font-size: 1.125rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0 0 0.25rem 0;\n word-break: break-word;\n overflow-wrap: break-word;\n}\n\n.role-details[_ngcontent-%COMP%] .role-description[_ngcontent-%COMP%] {\n font-size: 0.875rem;\n color: var(--mj-text-secondary);\n margin: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n@media (max-width: 639px) {\n .role-details[_ngcontent-%COMP%] .role-description[_ngcontent-%COMP%] {\n white-space: normal;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n }\n}\n\n.role-meta[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 1rem;\n flex-shrink: 0;\n}\n\n\n\n\n\n.role-type-badge[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.375rem 0.875rem;\n border-radius: var(--mj-radius-full);\n font-size: 0.75rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.03em;\n}\n\n.role-type-badge.badge-system[_ngcontent-%COMP%] {\n background: var(--mj-color-warning-50);\n color: var(--mj-color-warning-800);\n border: 1px solid var(--mj-color-warning-500);\n}\n\n.role-type-badge.badge-custom[_ngcontent-%COMP%] {\n background: var(--mj-color-success-100);\n color: var(--mj-color-success-800);\n border: 1px solid var(--mj-status-success);\n}\n\n\n\n\n\n.role-actions[_ngcontent-%COMP%] {\n display: flex;\n gap: 0.375rem;\n}\n\n\n\n.expand-btn[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n\n\n\n\n.role-content[_ngcontent-%COMP%] {\n padding: 1.5rem;\n background: var(--mj-bg-surface);\n border-top: 1px solid var(--mj-border-default);\n animation: slideDown 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.role-stats[_ngcontent-%COMP%] {\n display: flex;\n gap: 2rem;\n margin-bottom: 1.5rem;\n flex-wrap: wrap;\n}\n\n.role-stats[_ngcontent-%COMP%] .stat-item[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n font-size: 0.9375rem;\n padding: 0.5rem 1rem;\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-md);\n}\n\n.role-stats[_ngcontent-%COMP%] .stat-item[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n font-size: 1rem;\n}\n\n.role-stats[_ngcontent-%COMP%] .stat-item[_ngcontent-%COMP%] .stat-label[_ngcontent-%COMP%] {\n color: var(--mj-text-secondary);\n}\n\n.role-stats[_ngcontent-%COMP%] .stat-item[_ngcontent-%COMP%] .stat-value[_ngcontent-%COMP%] {\n color: var(--mj-text-primary);\n font-weight: 600;\n}\n\n\n\n\n\n.permissions-preview[_ngcontent-%COMP%] {\n padding: 1.25rem;\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-lg);\n border: 1px solid var(--mj-border-default);\n}\n\n.permissions-preview[_ngcontent-%COMP%] .section-title[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 0.625rem;\n font-size: 1rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0 0 0.5rem 0;\n}\n\n.permissions-preview[_ngcontent-%COMP%] .section-title[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n font-size: 1.125rem;\n}\n\n.permissions-preview[_ngcontent-%COMP%] .permissions-note[_ngcontent-%COMP%] {\n font-size: 0.9375rem;\n color: var(--mj-text-secondary);\n margin: 0;\n}\n\n\n\n.modal-body[_ngcontent-%COMP%] p[_ngcontent-%COMP%] {\n line-height: 1.6;\n}\n\n\n\n.modal-backdrop[_ngcontent-%COMP%] {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n}\n\n\n\n\n\n@media (min-width: 1440px) {\n .role-management-container[_ngcontent-%COMP%] {\n margin: 0 auto;\n }\n\n .action-buttons[_ngcontent-%COMP%] {\n padding: 1rem 2rem;\n }\n\n .mj-grid-4[_ngcontent-%COMP%] {\n padding: 0 2rem 1.25rem 2rem;\n gap: 1.5rem;\n }\n\n}\n\n\n\n\n\n@media (max-width: 639px) {\n .mj-grid-4[_ngcontent-%COMP%] {\n display: none !important;\n }\n\n .scrollable-content[_ngcontent-%COMP%] {\n padding: 0.75rem;\n }\n\n .modal-backdrop[_ngcontent-%COMP%] {\n padding: 0.5rem;\n }\n\n .modal-dialog[_ngcontent-%COMP%] {\n border-radius: var(--mj-radius-xl);\n max-height: 95vh;\n }\n\n .roles-list[_ngcontent-%COMP%] {\n gap: 0.75rem;\n }\n\n .role-card[_ngcontent-%COMP%] {\n border-radius: var(--mj-radius-lg);\n overflow: hidden;\n }\n\n .role-header[_ngcontent-%COMP%] {\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n padding: 1rem;\n }\n\n .role-info[_ngcontent-%COMP%] {\n flex: 1;\n min-width: 0;\n flex-direction: row;\n align-items: flex-start;\n gap: 0.875rem;\n }\n\n .role-icon-wrapper[_ngcontent-%COMP%] {\n width: 40px;\n height: 40px;\n font-size: 1rem;\n flex-shrink: 0;\n }\n\n .role-details[_ngcontent-%COMP%] {\n flex: 1;\n min-width: 0;\n }\n\n .role-details[_ngcontent-%COMP%] .role-name[_ngcontent-%COMP%] {\n font-size: 1rem;\n line-height: 1.3;\n margin-bottom: 0.125rem;\n }\n\n .role-details[_ngcontent-%COMP%] .role-description[_ngcontent-%COMP%] {\n font-size: 0.8125rem;\n -webkit-line-clamp: 1;\n line-clamp: 1;\n }\n\n .role-meta[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: flex-end;\n padding-top: 0;\n border-top: none;\n width: auto;\n }\n\n .role-meta[_ngcontent-%COMP%] .role-type-badge[_ngcontent-%COMP%] {\n display: none;\n }\n\n .role-meta[_ngcontent-%COMP%] .role-actions[_ngcontent-%COMP%] {\n display: none;\n }\n\n .expand-btn[_ngcontent-%COMP%] {\n flex-shrink: 0;\n min-width: 40px;\n min-height: 40px;\n padding: 0.5rem;\n }\n\n .expand-btn[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 1rem;\n }\n\n .mobile-actions-bar[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n padding: 0.875rem;\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-lg);\n margin-bottom: 1rem;\n }\n\n .mobile-actions-bar[_ngcontent-%COMP%] .role-type-badge[_ngcontent-%COMP%] {\n padding: 0.375rem 0.75rem;\n font-size: 0.6875rem;\n }\n\n .mobile-action-buttons[_ngcontent-%COMP%] {\n display: flex;\n gap: 0.5rem;\n }\n\n .mobile-action-buttons[_ngcontent-%COMP%] .mj-btn-sm[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.5rem 0.875rem;\n min-height: 36px;\n }\n\n .mobile-action-buttons[_ngcontent-%COMP%] .mj-btn-sm[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 0.875rem;\n }\n\n .mobile-action-buttons[_ngcontent-%COMP%] .btn-label[_ngcontent-%COMP%] {\n font-size: 0.8125rem;\n font-weight: 500;\n }\n\n .role-stats[_ngcontent-%COMP%] {\n flex-direction: column;\n gap: 0.5rem;\n margin-bottom: 1rem;\n }\n\n .role-content[_ngcontent-%COMP%] {\n padding: 1rem;\n }\n\n .permissions-preview[_ngcontent-%COMP%] {\n padding: 1rem;\n }\n\n .loading-container[_ngcontent-%COMP%] {\n padding: 3rem 1.5rem;\n }\n}\n\n\n\n@media (max-width: 374px) {\n .action-buttons[_ngcontent-%COMP%] {\n padding: 0.375rem 0.5rem;\n }\n\n .scrollable-content[_ngcontent-%COMP%] {\n padding: 0.5rem;\n }\n\n .role-header[_ngcontent-%COMP%] {\n padding: 0.75rem;\n }\n}"] });
|
|
537
732
|
};
|
|
538
733
|
RoleManagementComponent = __decorate([
|
|
539
734
|
RegisterClass(BaseDashboard, 'RoleManagement')
|
|
@@ -541,7 +736,7 @@ RoleManagementComponent = __decorate([
|
|
|
541
736
|
export { RoleManagementComponent };
|
|
542
737
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(RoleManagementComponent, [{
|
|
543
738
|
type: Component,
|
|
544
|
-
args: [{ standalone: false, selector: 'mj-role-management', template: "<!--\n Interior chrome \u2014 <mj-page-header-interior> is the body-level sibling of\n <mj-page-header>. Same slot conventions ([toolbar] / [meta] / [actions]);\n different visual shape (single-row card). See Section 10 of\n explorer-chrome-conventions.\n-->\n<mj-page-header-interior\n AriaLabel=\"Filter roles\"\n Title=\"Roles\"\n Subtitle=\"Define system and custom roles for permissions\">\n <!-- X-of-Y filtered count earns its meta spot per chrome conventions \u00A72.\n Partition badges (system/custom) stay dropped \u2014 they mirror the\n System/Custom filter chips in [toolbar]. -->\n <div meta>\n <mj-stat-badge\n [Count]=\"filteredRoles.length\"\n [Total]=\"roles.length\"\n Label=\"roles\">\n </mj-stat-badge>\n </div>\n <div toolbar>\n <!-- Concise-chrome control bar: search \u00B7 Filter. Type lives behind the one\n Filter button; applied filters show as removable chips below the card.\n On mobile the popover docks as a bottom sheet. -->\n <mj-page-search\n Placeholder=\"Search roles by name or description...\"\n [Value]=\"filters$.value.search\"\n (ValueChange)=\"updateFilter({ search: $event })\">\n </mj-page-search>\n <mj-filter-popover\n Label=\"Filters\"\n Icon=\"fa-solid fa-filter\"\n [ActiveCount]=\"TotalActiveFilterCount\"\n [ShowClearAll]=\"TotalActiveFilterCount > 0\"\n (ClearAllRequested)=\"clearAllAppliedFilters()\">\n <mj-filter-panel\n [Fields]=\"filterFields\"\n [Values]=\"filterValues\"\n (ValuesChange)=\"onFilterPanelChange($event)\"\n (Reset)=\"clearAllAppliedFilters()\">\n </mj-filter-panel>\n </mj-filter-popover>\n </div>\n <div actions>\n <mj-refresh-button [Loading]=\"isLoading\" (Clicked)=\"refreshData()\"></mj-refresh-button>\n <button mjButton variant=\"primary\" size=\"sm\" (click)=\"createNewRole()\" aria-label=\"Add new role\">\n <i class=\"fa-solid fa-plus\"></i> <span class=\"mj-action-label\">Add Role</span>\n </button>\n </div>\n</mj-page-header-interior>\n\n<!-- Scrollable Content Section \u2014 owned by <mj-page-body-interior>. -->\n<mj-page-body-interior>\n <!-- Loading State -->\n @if (isLoading) {\n <div class=\"loading-container\" role=\"status\" aria-live=\"polite\" aria-busy=\"true\">\n <mj-loading text=\"Loading roles...\" size=\"medium\"></mj-loading>\n </div>\n }\n\n <!-- Error State -->\n @if (error && !isLoading) {\n <div class=\"error-container\" role=\"alert\" aria-live=\"assertive\">\n <div class=\"error-content\">\n <i class=\"fa-solid fa-exclamation-triangle error-icon\" aria-hidden=\"true\"></i>\n <p class=\"error-message\">{{ error }}</p>\n <button mjButton variant=\"primary\" (click)=\"loadInitialData()\" aria-label=\"Retry loading roles\">\n <i class=\"fa-solid fa-refresh\" aria-hidden=\"true\"></i>\n Try Again\n </button>\n </div>\n </div>\n }\n\n <!-- Roles List -->\n @if (!isLoading && !error) {\n <div class=\"roles-list\" role=\"list\" aria-label=\"Roles list\">\n @for (role of filteredRoles; track role.ID) {\n <div\n class=\"role-card\"\n [class.expanded]=\"isRoleExpanded(role.ID)\"\n role=\"listitem\"\n [attr.aria-expanded]=\"isRoleExpanded(role.ID)\"\n >\n <div\n class=\"role-header\"\n (click)=\"toggleRoleExpansion(role.ID)\"\n tabindex=\"0\"\n (keydown.enter)=\"toggleRoleExpansion(role.ID)\"\n (keydown.space)=\"toggleRoleExpansion(role.ID); $event.preventDefault()\"\n role=\"button\"\n [attr.aria-label]=\"'Toggle details for ' + role.Name\"\n >\n <div class=\"role-info\">\n <div class=\"role-icon-wrapper\" aria-hidden=\"true\">\n <i [class]=\"'fa-solid ' + getRoleIcon(role)\"></i>\n </div>\n <div class=\"role-details\">\n <h3 class=\"role-name\">{{ role.Name }}</h3>\n <p class=\"role-description\">{{ role.Description || 'No description available' }}</p>\n </div>\n </div>\n\n <div class=\"role-meta\">\n <span class=\"role-type-badge\" [class]=\"getRoleTypeClass(role)\">\n {{ getRoleTypeLabel(role) }}\n </span>\n <div class=\"role-actions\" (click)=\"$event.stopPropagation()\">\n <button\n mjButton variant=\"flat\" size=\"sm\"\n (click)=\"editRole(role)\"\n title=\"Edit\"\n [attr.aria-label]=\"'Edit ' + role.Name\"\n [disabled]=\"isSystemRole(role)\"\n >\n <i class=\"fa-solid fa-edit\" aria-hidden=\"true\"></i>\n </button>\n <button\n mjButton variant=\"danger\" size=\"sm\"\n (click)=\"confirmDeleteRole(role)\"\n title=\"Delete\"\n [attr.aria-label]=\"'Delete ' + role.Name\"\n [disabled]=\"isSystemRole(role)\"\n >\n <i class=\"fa-solid fa-trash\" aria-hidden=\"true\"></i>\n </button>\n </div>\n <button class=\"expand-btn\" aria-label=\"Toggle details\">\n <i class=\"fa-solid fa-chevron-down\" aria-hidden=\"true\"></i>\n </button>\n </div>\n </div>\n\n @if (isRoleExpanded(role.ID)) {\n <div class=\"role-content\">\n <!-- Mobile-only actions bar -->\n <div class=\"mobile-actions-bar\" (click)=\"$event.stopPropagation()\">\n <span class=\"role-type-badge\" [class]=\"getRoleTypeClass(role)\">\n {{ getRoleTypeLabel(role) }}\n </span>\n <div class=\"mobile-action-buttons\">\n <button\n mjButton variant=\"flat\" size=\"sm\"\n (click)=\"editRole(role)\"\n title=\"Edit\"\n [attr.aria-label]=\"'Edit ' + role.Name\"\n [disabled]=\"isSystemRole(role)\"\n >\n <i class=\"fa-solid fa-edit\" aria-hidden=\"true\"></i>\n <span class=\"btn-label\">Edit</span>\n </button>\n <button\n mjButton variant=\"danger\" size=\"sm\"\n (click)=\"confirmDeleteRole(role)\"\n title=\"Delete\"\n [attr.aria-label]=\"'Delete ' + role.Name\"\n [disabled]=\"isSystemRole(role)\"\n >\n <i class=\"fa-solid fa-trash\" aria-hidden=\"true\"></i>\n <span class=\"btn-label\">Delete</span>\n </button>\n </div>\n </div>\n\n <div class=\"role-stats\">\n <div class=\"stat-item\">\n <i class=\"fa-solid fa-users\" aria-hidden=\"true\"></i>\n <span class=\"stat-label\">Users:</span>\n <span class=\"stat-value\">0</span>\n </div>\n <div class=\"stat-item\">\n <i class=\"fa-solid fa-calendar\" aria-hidden=\"true\"></i>\n <span class=\"stat-label\">Created:</span>\n <span class=\"stat-value\">{{ role.__mj_CreatedAt | date:'short' }}</span>\n </div>\n <div class=\"stat-item\">\n <i class=\"fa-solid fa-clock\" aria-hidden=\"true\"></i>\n <span class=\"stat-label\">Updated:</span>\n <span class=\"stat-value\">{{ role.__mj_UpdatedAt | date:'short' }}</span>\n </div>\n </div>\n\n <div class=\"permissions-preview\">\n <h4 class=\"section-title\">\n <i class=\"fa-solid fa-key\" aria-hidden=\"true\"></i>\n Permissions Preview\n </h4>\n <p class=\"permissions-note\">Full permission management available in the Permissions tab</p>\n </div>\n </div>\n }\n </div>\n }\n\n @if (filteredRoles.length === 0) {\n <div class=\"empty-state\" role=\"status\">\n <i class=\"fa-solid fa-user-tag empty-icon\" aria-hidden=\"true\"></i>\n <p class=\"empty-text\">No roles found</p>\n <p class=\"empty-subtext\">Try adjusting your filters or create a new role</p>\n </div>\n }\n </div>\n }\n </mj-page-body-interior>\n\n <!-- Role Create/Edit Dialog -->\n <mj-role-dialog\n [data]=\"roleDialogData\"\n [visible]=\"showRoleDialog\"\n (result)=\"onRoleDialogResult($event)\">\n </mj-role-dialog>\n\n <!-- Delete Confirmation Dialog -->\n @if (showDeleteConfirm && selectedRole) {\n <div\n class=\"modal-backdrop\"\n (click)=\"showDeleteConfirm = false\"\n role=\"presentation\"\n >\n <div\n class=\"modal-dialog\"\n (click)=\"$event.stopPropagation()\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"delete-dialog-title\"\n aria-describedby=\"delete-dialog-desc\"\n >\n <div class=\"modal-header\">\n <h3 class=\"modal-title\" id=\"delete-dialog-title\">\n <i class=\"fa-solid fa-exclamation-triangle text-danger\" aria-hidden=\"true\"></i>\n Confirm Delete\n </h3>\n <button\n class=\"modal-close\"\n (click)=\"showDeleteConfirm = false\"\n aria-label=\"Close dialog\"\n >\n <i class=\"fa-solid fa-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n <div class=\"modal-body\" id=\"delete-dialog-desc\">\n <p>Are you sure you want to delete the role <strong>{{ selectedRole.Name }}</strong>?</p>\n <p class=\"text-warning\">\n <i class=\"fa-solid fa-warning\" aria-hidden=\"true\"></i>\n This will affect all users assigned to this role.\n </p>\n </div>\n <div class=\"modal-footer\">\n <!-- Primary action (Delete) on LEFT per MD3 design guide -->\n <button mjButton variant=\"danger\" (click)=\"deleteRole()\" [disabled]=\"isLoading\">\n @if (isLoading) {\n <i class=\"fa-solid fa-spinner fa-spin\" aria-hidden=\"true\"></i>\n Deleting...\n } @else {\n <i class=\"fa-solid fa-trash\" aria-hidden=\"true\"></i>\n Delete Role\n }\n </button>\n <button mjButton (click)=\"showDeleteConfirm = false\">\n Cancel\n </button>\n </div>\n </div>\n </div>\n }\n\n", styles: ["/* =============================================================================\n Shared Admin Patterns - MJ Design System\n Common styles shared across all admin settings components.\n Component-specific CSS files override these values where needed.\n ============================================================================= */\n\n/* -----------------------------------------------------------------------------\n Host & Container\n ----------------------------------------------------------------------------- */\n:host {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n overflow: hidden;\n}\n\n/* -----------------------------------------------------------------------------\n Sticky Header\n ----------------------------------------------------------------------------- */\n.sticky-header {\n flex-shrink: 0;\n background: var(--mj-bg-page);\n border-bottom: 1px solid var(--mj-border-default);\n box-shadow: var(--mj-shadow-sm);\n z-index: 10;\n}\n\n/* -----------------------------------------------------------------------------\n Scrollable Content\n ----------------------------------------------------------------------------- */\n.scrollable-content {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n -webkit-overflow-scrolling: touch;\n padding: 1rem;\n background: var(--mj-bg-surface-sunken);\n}\n\n@media (min-width: 768px) {\n .scrollable-content {\n padding: 1.5rem 2rem;\n }\n}\n\n@media (min-width: 1024px) {\n .scrollable-content {\n padding: 2rem;\n }\n}\n\n@media (min-width: 1440px) {\n .scrollable-content {\n padding: 2rem 2.5rem;\n }\n}\n\n/* -----------------------------------------------------------------------------\n Action Buttons (In Sticky Header)\n ----------------------------------------------------------------------------- */\n.action-buttons {\n flex-shrink: 0;\n display: flex;\n gap: 0.75rem;\n justify-content: flex-end;\n padding: 0.75rem 1rem;\n background: var(--mj-bg-page);\n}\n\n@media (min-width: 768px) {\n .action-buttons {\n padding: 1rem 1.5rem;\n }\n}\n\n@media (max-width: 639px) {\n .action-buttons {\n justify-content: center;\n flex-wrap: wrap;\n }\n}\n\n/* -----------------------------------------------------------------------------\n Statistics Cards Grid - Static Display (Non-interactive)\n ----------------------------------------------------------------------------- */\n.mj-grid-4 {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 0.5rem;\n padding: 0 1rem 0.75rem 1rem;\n background: var(--mj-bg-page);\n}\n\n@media (min-width: 768px) {\n .mj-grid-4 {\n grid-template-columns: repeat(4, 1fr);\n padding: 0 1.5rem 1rem 1.5rem;\n gap: 0.75rem;\n }\n}\n\n@media (min-width: 1024px) {\n .mj-grid-4 {\n gap: 1rem;\n }\n}\n\n/* Static Card - No hover effects */\n.mj-card {\n background: var(--mj-bg-surface-card);\n border-radius: var(--mj-radius-lg);\n padding: 1rem;\n box-shadow: var(--mj-shadow-sm);\n display: flex;\n align-items: center;\n gap: 1rem;\n border: 1px solid var(--mj-border-default);\n cursor: default;\n pointer-events: none;\n}\n\n@media (min-width: 768px) {\n .mj-card {\n padding: 1rem 1.25rem;\n }\n}\n\n/* -----------------------------------------------------------------------------\n Stat Icon Containers\n ----------------------------------------------------------------------------- */\n.stat-icon {\n width: 48px;\n height: 48px;\n border-radius: var(--mj-radius-lg);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 1.25rem;\n flex-shrink: 0;\n}\n\n@media (min-width: 768px) {\n .stat-icon {\n width: 52px;\n height: 52px;\n font-size: 1.375rem;\n }\n}\n\n.stat-icon-total {\n background: var(--mj-color-accent-300);\n color: var(--mj-brand-primary);\n}\n\n.stat-icon-active {\n background: var(--mj-color-success-100);\n color: var(--mj-status-success);\n}\n\n/* -----------------------------------------------------------------------------\n Stat Content Typography\n ----------------------------------------------------------------------------- */\n.stat-content {\n flex: 1;\n min-width: 0;\n}\n\n.stat-content .stat-value {\n font-size: 1.75rem;\n font-weight: 700;\n color: var(--mj-text-primary);\n line-height: 1;\n letter-spacing: -0.02em;\n}\n\n@media (min-width: 768px) {\n .stat-content .stat-value {\n font-size: 2rem;\n }\n}\n\n.stat-content .stat-label {\n font-size: 0.75rem;\n font-weight: 500;\n color: var(--mj-text-secondary);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin-top: 0.25rem;\n}\n\n@media (min-width: 768px) {\n .stat-content .stat-label {\n font-size: 0.8125rem;\n }\n}\n\n/* -----------------------------------------------------------------------------\n Filters Section\n ----------------------------------------------------------------------------- */\n.filters-section {\n flex-shrink: 0;\n background: var(--mj-bg-surface-card);\n margin: 0 1rem 0.75rem 1rem;\n padding: 0.75rem;\n border-radius: var(--mj-radius-lg);\n border: 1px solid var(--mj-border-default);\n}\n\n@media (min-width: 768px) {\n .filters-section {\n margin: 0 1.5rem 1rem 1.5rem;\n padding: 1rem 1.25rem;\n }\n}\n\n.filters-row {\n display: flex;\n gap: 1rem;\n align-items: flex-end;\n flex-wrap: wrap;\n}\n\n@media (min-width: 768px) {\n .filters-row {\n gap: 1.5rem;\n }\n}\n\n.mj-filter-group {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.mj-filter-label {\n font-size: 0.75rem;\n font-weight: 500;\n color: var(--mj-text-primary);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n}\n\n.mj-filter-buttons {\n display: flex;\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-full);\n padding: 4px;\n gap: 2px;\n}\n\n/* -----------------------------------------------------------------------------\n Search Input\n ----------------------------------------------------------------------------- */\n.mj-search {\n position: relative;\n flex: 1;\n min-width: 200px;\n}\n\n@media (min-width: 640px) {\n .mj-search {\n min-width: 280px;\n }\n}\n\n@media (min-width: 1024px) {\n .mj-search {\n min-width: 400px;\n max-width: 600px;\n }\n}\n\n@media (min-width: 1440px) {\n .mj-search {\n min-width: 500px;\n max-width: 800px;\n }\n}\n\n.mj-search .mj-search-icon {\n position: absolute;\n left: 1rem;\n top: 50%;\n transform: translateY(-50%);\n color: var(--mj-text-secondary);\n font-size: 1rem;\n pointer-events: none;\n transition: color 0.2s ease;\n}\n\n@media (min-width: 1024px) {\n .mj-search .mj-search-icon {\n left: 1.25rem;\n font-size: 1.125rem;\n }\n}\n\n.mj-search .mj-search-input {\n width: 100%;\n padding: 0.875rem 1rem 0.875rem 2.75rem;\n border: 2px solid var(--mj-border-default);\n border-radius: var(--mj-radius-full);\n font-size: 1rem;\n background: var(--mj-bg-page);\n color: var(--mj-text-primary);\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n min-height: 44px;\n}\n\n@media (min-width: 1024px) {\n .mj-search .mj-search-input {\n padding: 1rem 1.25rem 1rem 3.25rem;\n font-size: 1.0625rem;\n }\n}\n\n@media (min-width: 1440px) {\n .mj-search .mj-search-input {\n padding: 1.125rem 1.5rem 1.125rem 3.5rem;\n }\n}\n\n.mj-search .mj-search-input::placeholder {\n color: var(--mj-text-secondary);\n}\n\n.mj-search .mj-search-input:hover {\n border-color: var(--mj-brand-primary);\n background: var(--mj-bg-surface);\n}\n\n.mj-search .mj-search-input:focus {\n outline: none;\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px rgba(0, 118, 182, 0.2);\n background: var(--mj-bg-page);\n}\n\n.mj-search:focus-within .mj-search-icon {\n color: var(--mj-brand-primary);\n}\n\n/* -----------------------------------------------------------------------------\n Content Area\n ----------------------------------------------------------------------------- */\n.content-area {\n flex: 1 1 auto;\n overflow: visible;\n position: relative;\n background: var(--mj-bg-page);\n border-radius: var(--mj-radius-xl);\n box-shadow: var(--mj-shadow-sm);\n padding: 1rem;\n border: 1px solid var(--mj-border-default);\n}\n\n@media (min-width: 768px) {\n .content-area {\n padding: 1.5rem;\n }\n}\n\n/* -----------------------------------------------------------------------------\n Expand Button\n ----------------------------------------------------------------------------- */\n.expand-btn {\n padding: 0.625rem;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 1rem;\n cursor: pointer;\n transition: all 0.2s ease;\n border-radius: var(--mj-radius-full);\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.expand-btn:hover {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n}\n\n.expand-btn i {\n transition: transform 0.3s cubic-bezier(0, 0, 0.2, 1);\n}\n\n/* -----------------------------------------------------------------------------\n Status Badge\n ----------------------------------------------------------------------------- */\n.status-badge {\n display: inline-flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.375rem 0.875rem;\n border-radius: var(--mj-radius-full);\n font-size: 0.75rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.03em;\n}\n\n.status-badge.status-active {\n background: var(--mj-color-success-100);\n color: var(--mj-color-success-800);\n border: 1px solid var(--mj-status-success);\n}\n\n.status-badge.status-inactive {\n background: var(--mj-color-error-100);\n color: var(--mj-status-error);\n border: 1px solid var(--mj-status-error);\n}\n\n.status-badge i {\n font-size: 0.625rem;\n}\n\n/* -----------------------------------------------------------------------------\n Mobile Actions Bar\n ----------------------------------------------------------------------------- */\n.mobile-actions-bar {\n display: none;\n}\n\n.mobile-action-buttons {\n display: flex;\n gap: 0.5rem;\n}\n\n/* -----------------------------------------------------------------------------\n Empty State\n ----------------------------------------------------------------------------- */\n.empty-state {\n text-align: center;\n padding: 4rem 2rem;\n}\n\n.empty-state .empty-icon {\n font-size: 4rem;\n color: var(--mj-border-default);\n margin-bottom: 1.5rem;\n}\n\n.empty-state .empty-text {\n font-size: 1.25rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0 0 0.5rem 0;\n}\n\n.empty-state .empty-subtext {\n font-size: 1rem;\n color: var(--mj-text-secondary);\n margin: 0;\n}\n\n/* -----------------------------------------------------------------------------\n Loading State\n ----------------------------------------------------------------------------- */\n.loading-container {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 4rem 2rem;\n}\n\n/* -----------------------------------------------------------------------------\n Error State\n ----------------------------------------------------------------------------- */\n.error-container {\n text-align: center;\n padding: 4rem 2rem;\n}\n\n.error-container .error-content {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 1rem;\n}\n\n.error-container .error-icon {\n font-size: 3.5rem;\n color: var(--mj-status-error);\n margin-bottom: 1rem;\n}\n\n.error-container .error-message {\n font-size: 1.0625rem;\n color: var(--mj-text-primary);\n margin: 0 0 1.5rem 0;\n}\n\n/* -----------------------------------------------------------------------------\n Modal Dialog\n ----------------------------------------------------------------------------- */\n.modal-backdrop {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.4);\n backdrop-filter: blur(4px);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1000;\n padding: 1rem;\n animation: fadeIn 0.2s cubic-bezier(0, 0, 0.2, 1);\n}\n\n.modal-dialog {\n background: var(--mj-bg-page);\n border-radius: var(--mj-radius-2xl);\n box-shadow: var(--mj-shadow-2xl);\n max-width: 500px;\n width: 100%;\n max-height: 90vh;\n overflow: hidden;\n animation: slideUp 0.3s cubic-bezier(0, 0, 0.2, 1);\n}\n\n.modal-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1.5rem;\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.modal-header .modal-title {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n font-size: 1.375rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0;\n}\n\n.modal-header .modal-close {\n padding: 0.625rem;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 1.25rem;\n cursor: pointer;\n border-radius: var(--mj-radius-full);\n transition: all 0.2s ease;\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.modal-header .modal-close:hover {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n}\n\n.modal-body {\n padding: 1.5rem;\n}\n\n.modal-body p {\n font-size: 1rem;\n color: var(--mj-text-primary);\n margin: 0 0 1rem 0;\n line-height: 1.5;\n}\n\n.modal-body p:last-child {\n margin-bottom: 0;\n}\n\n.modal-footer {\n display: flex;\n justify-content: flex-start;\n gap: 0.75rem;\n padding: 1.5rem;\n border-top: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface-sunken);\n}\n\n@media (max-width: 639px) {\n .modal-dialog {\n width: 95%;\n max-height: 85vh;\n }\n\n .modal-header {\n padding: 1rem;\n }\n\n .modal-header .modal-title {\n font-size: 1.125rem;\n }\n\n .modal-body {\n padding: 1rem;\n }\n\n .modal-footer {\n padding: 1rem;\n flex-direction: column;\n }\n\n}\n\n/* The legacy \"MJ Button System\" rule block (.mj-btn / .mj-btn-primary /\n .mj-btn-secondary / .mj-btn-ghost / .mj-btn-danger / .mj-btn-sm /\n .mj-btn-icon-only / .mj-btn-icon-mobile) was removed 2026-05-19. These\n rules pre-dated the mjButton directive standardization (the directive\n uses BEM modifiers like .mj-btn--primary, single-dash names like\n .mj-btn-primary don't match it anyway). The base .mj-btn rule WAS active\n though \u2014 its component-scoped specificity outranked button.scss's global\n directive styles, so admin sub-page buttons rendered pill-shaped + 44px\n regardless of the variant/size requested via mjButton inputs. Removing the\n rule lets every sub-page fall back to the directive's intended look. */\n\n/* -----------------------------------------------------------------------------\n Filter Button (Mobile Only)\n ----------------------------------------------------------------------------- */\n.filter-button {\n display: none;\n align-items: center;\n justify-content: center;\n gap: 0.5rem;\n padding: 0.75rem 1.25rem;\n border: 2px solid var(--mj-border-default);\n background: var(--mj-bg-page);\n color: var(--mj-text-primary);\n border-radius: var(--mj-radius-full);\n font-size: 0.875rem;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n min-height: 44px;\n min-width: 44px;\n}\n\n.filter-button:hover {\n border-color: var(--mj-brand-primary);\n background: var(--mj-bg-surface);\n}\n\n.filter-button:active {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n border-color: var(--mj-brand-primary);\n}\n\n.filter-button i {\n font-size: 1rem;\n}\n\n/* -----------------------------------------------------------------------------\n Filter Modal (Mobile Bottom Sheet)\n ----------------------------------------------------------------------------- */\n.filter-modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.4);\n backdrop-filter: blur(4px);\n display: flex;\n align-items: flex-end;\n justify-content: center;\n z-index: 1000;\n animation: fadeIn 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n padding: 0;\n}\n\n.filter-modal {\n background: var(--mj-bg-page);\n border-radius: var(--mj-radius-2xl) var(--mj-radius-2xl) 0 0;\n box-shadow: var(--mj-shadow-2xl);\n width: 100%;\n max-height: 80vh;\n overflow: hidden;\n animation: slideUpFromBottom 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n display: flex;\n flex-direction: column;\n}\n\n.filter-modal-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1.25rem 1.5rem;\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.filter-modal-title {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n font-size: 1.25rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0;\n}\n\n.filter-modal-title i {\n color: var(--mj-brand-primary);\n font-size: 1.375rem;\n}\n\n.filter-modal-close {\n padding: 0.625rem;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 1.25rem;\n cursor: pointer;\n border-radius: var(--mj-radius-full);\n transition: all 0.2s ease;\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.filter-modal-close:hover {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n}\n\n.filter-modal-body {\n padding: 1.5rem;\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n flex: 1;\n}\n\n.filter-modal-footer {\n display: flex;\n gap: 0.75rem;\n padding: 1.25rem 1.5rem;\n border-top: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface-sunken);\n flex-shrink: 0;\n}\n\n.filter-modal-footer button {\n flex: 1;\n margin: 0;\n}\n\n/* Filter Options Container (Inside Modal) */\n.filter-options-container {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n}\n\n.filter-group {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.filter-group-label {\n font-size: 0.875rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin: 0;\n}\n\n.filter-group-options {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.filter-option {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 0.875rem 1rem;\n background: var(--mj-bg-surface-sunken);\n border: 2px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg);\n cursor: pointer;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n min-height: 56px;\n}\n\n.filter-option:hover {\n border-color: var(--mj-brand-primary);\n background: var(--mj-bg-surface-sunken);\n}\n\n.filter-option.selected {\n background: var(--mj-color-accent-300);\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n}\n\n.filter-option input[type=\"checkbox\"],\n.filter-option input[type=\"radio\"] {\n width: 20px;\n height: 20px;\n cursor: pointer;\n accent-color: var(--mj-brand-primary);\n flex-shrink: 0;\n}\n\n.filter-option-label {\n flex: 1;\n font-size: 1rem;\n font-weight: 500;\n color: var(--mj-text-primary);\n}\n\n.filter-option.selected .filter-option-label {\n color: var(--mj-brand-primary);\n font-weight: 600;\n}\n\n/* -----------------------------------------------------------------------------\n Utility Classes\n ----------------------------------------------------------------------------- */\n.text-danger {\n color: var(--mj-status-error);\n}\n\n.text-warning {\n color: var(--mj-color-warning-500);\n}\n\n.text-success {\n color: var(--mj-status-success);\n}\n\n/* -----------------------------------------------------------------------------\n Animations\n ----------------------------------------------------------------------------- */\n@keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n@keyframes slideUp {\n from {\n transform: translateY(20px);\n opacity: 0;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n}\n\n@keyframes slideDown {\n from {\n opacity: 0;\n transform: translateY(-10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes slideUpFromBottom {\n from {\n transform: translateY(100%);\n opacity: 0;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n}\n\n/* -----------------------------------------------------------------------------\n Accessibility: Focus Indicators\n ----------------------------------------------------------------------------- */\n.mj-search-input:focus-visible,\nbutton:focus-visible {\n outline: 2px solid var(--mj-brand-primary);\n outline-offset: 2px;\n}\n\n/* -----------------------------------------------------------------------------\n Accessibility: Reduced Motion\n ----------------------------------------------------------------------------- */\n@media (prefers-reduced-motion: reduce) {\n *,\n *::before,\n *::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n }\n}\n", "/* =============================================================================\n Role Management Component - Component-Specific Styles\n Shared patterns are in ../shared/styles/_admin-patterns.css\n ============================================================================= */\n\n/* :host already provides flex column / 100% height / overflow hidden via\n _admin-patterns.css. No bespoke container wrapper \u2014 see Section 10 of\n explorer-chrome-conventions for the standardized sub-page layout contract. */\n\n/* Status / type chip group inside <mj-page-header-interior>'s [toolbar] slot. */\n.status-chips {\n display: inline-flex;\n align-items: center;\n gap: var(--mj-space-2);\n flex-shrink: 0;\n}\n\n/* -----------------------------------------------------------------------------\n Stats Grid Override - Different gap/padding values\n ----------------------------------------------------------------------------- */\n.mj-grid-4 {\n gap: 0.75rem;\n padding: 0 1rem 1rem 1rem;\n}\n\n@media (min-width: 768px) {\n .mj-grid-4 {\n padding: 0 1.5rem 1.25rem 1.5rem;\n gap: 1rem;\n }\n}\n\n@media (min-width: 1024px) {\n .mj-grid-4 {\n gap: 1.25rem;\n }\n}\n\n/* Stat Card Override - no box-shadow, different 768px padding */\n.mj-card {\n box-shadow: none;\n}\n\n@media (min-width: 768px) {\n .mj-card {\n padding: 1.125rem 1.25rem;\n }\n}\n\n/* Stat Icon Override - Component-specific colors */\n.stat-icon-system {\n background: var(--mj-color-warning-50);\n color: var(--mj-color-warning-700);\n}\n\n.stat-icon-custom {\n background: var(--mj-color-success-100);\n color: var(--mj-status-success);\n}\n\n.stat-icon-active {\n background: var(--mj-color-accent-300);\n color: var(--mj-brand-primary);\n}\n\n/* -----------------------------------------------------------------------------\n Roles List\n ----------------------------------------------------------------------------- */\n.roles-list {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n/* -----------------------------------------------------------------------------\n Role Card\n ----------------------------------------------------------------------------- */\n.role-card {\n background: var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-xl);\n overflow: hidden;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.role-card:hover {\n box-shadow: var(--mj-shadow-md);\n border-color: var(--mj-brand-primary);\n}\n\n.role-card.expanded {\n box-shadow: var(--mj-shadow-lg);\n border-color: var(--mj-brand-primary);\n}\n\n.role-card.expanded .expand-btn i {\n transform: rotate(180deg);\n}\n\n.role-header {\n padding: 1.25rem 1.5rem;\n display: flex;\n justify-content: space-between;\n align-items: center;\n cursor: pointer;\n background: var(--mj-bg-surface);\n transition: background-color 0.2s ease;\n}\n\n.role-header:hover {\n background: var(--mj-bg-surface-sunken);\n}\n\n.role-info {\n display: flex;\n align-items: center;\n gap: 1rem;\n flex: 1;\n min-width: 0;\n}\n\n.role-icon-wrapper {\n width: 52px;\n height: 52px;\n border-radius: var(--mj-radius-lg);\n background: var(--mj-color-accent-300);\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--mj-brand-primary);\n font-size: 1.375rem;\n flex-shrink: 0;\n}\n\n.role-details {\n flex: 1;\n min-width: 0;\n}\n\n.role-details .role-name {\n font-size: 1.125rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0 0 0.25rem 0;\n word-break: break-word;\n overflow-wrap: break-word;\n}\n\n.role-details .role-description {\n font-size: 0.875rem;\n color: var(--mj-text-secondary);\n margin: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n@media (max-width: 639px) {\n .role-details .role-description {\n white-space: normal;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n }\n}\n\n.role-meta {\n display: flex;\n align-items: center;\n gap: 1rem;\n flex-shrink: 0;\n}\n\n/* -----------------------------------------------------------------------------\n Role Type Badge\n ----------------------------------------------------------------------------- */\n.role-type-badge {\n display: inline-flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.375rem 0.875rem;\n border-radius: var(--mj-radius-full);\n font-size: 0.75rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.03em;\n}\n\n.role-type-badge.badge-system {\n background: var(--mj-color-warning-50);\n color: var(--mj-color-warning-800);\n border: 1px solid var(--mj-color-warning-500);\n}\n\n.role-type-badge.badge-custom {\n background: var(--mj-color-success-100);\n color: var(--mj-color-success-800);\n border: 1px solid var(--mj-status-success);\n}\n\n/* -----------------------------------------------------------------------------\n Role Actions\n ----------------------------------------------------------------------------- */\n.role-actions {\n display: flex;\n gap: 0.375rem;\n}\n\n/* Expand Button Override - different transition easing */\n.expand-btn i {\n transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n/* -----------------------------------------------------------------------------\n Role Content (Expanded)\n ----------------------------------------------------------------------------- */\n.role-content {\n padding: 1.5rem;\n background: var(--mj-bg-surface);\n border-top: 1px solid var(--mj-border-default);\n animation: slideDown 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.role-stats {\n display: flex;\n gap: 2rem;\n margin-bottom: 1.5rem;\n flex-wrap: wrap;\n}\n\n.role-stats .stat-item {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n font-size: 0.9375rem;\n padding: 0.5rem 1rem;\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-md);\n}\n\n.role-stats .stat-item i {\n color: var(--mj-brand-primary);\n font-size: 1rem;\n}\n\n.role-stats .stat-item .stat-label {\n color: var(--mj-text-secondary);\n}\n\n.role-stats .stat-item .stat-value {\n color: var(--mj-text-primary);\n font-weight: 600;\n}\n\n/* -----------------------------------------------------------------------------\n Permissions Preview\n ----------------------------------------------------------------------------- */\n.permissions-preview {\n padding: 1.25rem;\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-lg);\n border: 1px solid var(--mj-border-default);\n}\n\n.permissions-preview .section-title {\n display: flex;\n align-items: center;\n gap: 0.625rem;\n font-size: 1rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0 0 0.5rem 0;\n}\n\n.permissions-preview .section-title i {\n color: var(--mj-brand-primary);\n font-size: 1.125rem;\n}\n\n.permissions-preview .permissions-note {\n font-size: 0.9375rem;\n color: var(--mj-text-secondary);\n margin: 0;\n}\n\n/* Modal Body Override - line-height and warning callout */\n.modal-body p {\n line-height: 1.6;\n}\n\n.modal-body .text-warning {\n color: var(--mj-color-warning-500);\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.75rem 1rem;\n background: var(--mj-color-warning-50);\n border-radius: var(--mj-radius-md);\n font-weight: 500;\n}\n\n/* Modal Backdrop Override - uses top/left/right/bottom instead of inset */\n.modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n}\n\n/* -----------------------------------------------------------------------------\n Large Desktop (1440px+)\n ----------------------------------------------------------------------------- */\n@media (min-width: 1440px) {\n .role-management-container {\n margin: 0 auto;\n }\n\n .action-buttons {\n padding: 1rem 2rem;\n }\n\n .mj-grid-4 {\n padding: 0 2rem 1.25rem 2rem;\n gap: 1.5rem;\n }\n\n}\n\n/* -----------------------------------------------------------------------------\n Mobile (< 640px)\n ----------------------------------------------------------------------------- */\n@media (max-width: 639px) {\n .mj-grid-4 {\n display: none !important;\n }\n\n .scrollable-content {\n padding: 0.75rem;\n }\n\n .modal-backdrop {\n padding: 0.5rem;\n }\n\n .modal-dialog {\n border-radius: var(--mj-radius-xl);\n max-height: 95vh;\n }\n\n .roles-list {\n gap: 0.75rem;\n }\n\n .role-card {\n border-radius: var(--mj-radius-lg);\n overflow: hidden;\n }\n\n .role-header {\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n padding: 1rem;\n }\n\n .role-info {\n flex: 1;\n min-width: 0;\n flex-direction: row;\n align-items: flex-start;\n gap: 0.875rem;\n }\n\n .role-icon-wrapper {\n width: 40px;\n height: 40px;\n font-size: 1rem;\n flex-shrink: 0;\n }\n\n .role-details {\n flex: 1;\n min-width: 0;\n }\n\n .role-details .role-name {\n font-size: 1rem;\n line-height: 1.3;\n margin-bottom: 0.125rem;\n }\n\n .role-details .role-description {\n font-size: 0.8125rem;\n -webkit-line-clamp: 1;\n line-clamp: 1;\n }\n\n .role-meta {\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: flex-end;\n padding-top: 0;\n border-top: none;\n width: auto;\n }\n\n .role-meta .role-type-badge {\n display: none;\n }\n\n .role-meta .role-actions {\n display: none;\n }\n\n .expand-btn {\n flex-shrink: 0;\n min-width: 40px;\n min-height: 40px;\n padding: 0.5rem;\n }\n\n .expand-btn i {\n font-size: 1rem;\n }\n\n .mobile-actions-bar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n padding: 0.875rem;\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-lg);\n margin-bottom: 1rem;\n }\n\n .mobile-actions-bar .role-type-badge {\n padding: 0.375rem 0.75rem;\n font-size: 0.6875rem;\n }\n\n .mobile-action-buttons {\n display: flex;\n gap: 0.5rem;\n }\n\n .mobile-action-buttons .mj-btn-sm {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.5rem 0.875rem;\n min-height: 36px;\n }\n\n .mobile-action-buttons .mj-btn-sm i {\n font-size: 0.875rem;\n }\n\n .mobile-action-buttons .btn-label {\n font-size: 0.8125rem;\n font-weight: 500;\n }\n\n .role-stats {\n flex-direction: column;\n gap: 0.5rem;\n margin-bottom: 1rem;\n }\n\n .role-content {\n padding: 1rem;\n }\n\n .permissions-preview {\n padding: 1rem;\n }\n\n .empty-state {\n padding: 3rem 1.5rem;\n }\n\n .empty-state .empty-icon {\n font-size: 3rem;\n }\n\n .loading-container {\n padding: 3rem 1.5rem;\n }\n}\n\n/* Small Mobile (< 375px) */\n@media (max-width: 374px) {\n .action-buttons {\n padding: 0.375rem 0.5rem;\n }\n\n .scrollable-content {\n padding: 0.5rem;\n }\n\n .role-header {\n padding: 0.75rem;\n }\n}\n"] }]
|
|
739
|
+
args: [{ standalone: false, selector: 'mj-role-management', template: "<!--\n Interior chrome \u2014 <mj-page-header-interior> is the body-level sibling of\n <mj-page-header>. Same slot conventions ([toolbar] / [meta] / [actions]);\n different visual shape (single-row card). See Section 10 of\n explorer-chrome-conventions.\n-->\n<mj-page-header-interior\n AriaLabel=\"Filter roles\"\n Title=\"Roles\"\n Subtitle=\"Define system and custom roles for permissions\">\n <!-- X-of-Y filtered count earns its meta spot per chrome conventions \u00A72.\n Partition badges (system/custom) stay dropped \u2014 they mirror the\n System/Custom filter chips in [toolbar]. -->\n <div meta>\n <mj-stat-badge\n [Count]=\"filteredRoles.length\"\n [Total]=\"roles.length\"\n Label=\"roles\">\n </mj-stat-badge>\n </div>\n <div toolbar>\n <!-- Concise-chrome control bar: search \u00B7 Filter. Type lives behind the one\n Filter button; applied filters show as removable chips below the card.\n On mobile the popover docks as a bottom sheet. -->\n <mj-page-search\n Placeholder=\"Search roles by name or description...\"\n [Value]=\"filters$.value.search\"\n (ValueChange)=\"updateFilter({ search: $event })\">\n </mj-page-search>\n <mj-filter-popover\n Label=\"Filters\"\n Icon=\"fa-solid fa-filter\"\n [ActiveCount]=\"TotalActiveFilterCount\"\n [ShowClearAll]=\"TotalActiveFilterCount > 0\"\n (ClearAllRequested)=\"clearAllAppliedFilters()\">\n <mj-filter-panel\n [Fields]=\"filterFields\"\n [Values]=\"filterValues\"\n (ValuesChange)=\"onFilterPanelChange($event)\"\n (Reset)=\"clearAllAppliedFilters()\">\n </mj-filter-panel>\n </mj-filter-popover>\n </div>\n <div actions>\n <mj-refresh-button [Loading]=\"isLoading\" (Clicked)=\"refreshData()\"></mj-refresh-button>\n <button mjButton variant=\"primary\" size=\"sm\" (click)=\"createNewRole()\" aria-label=\"Add new role\">\n <i class=\"fa-solid fa-plus\"></i> <span class=\"mj-action-label\">Add Role</span>\n </button>\n </div>\n</mj-page-header-interior>\n\n<!-- Scrollable Content Section \u2014 owned by <mj-page-body-interior>. -->\n<mj-page-body-interior>\n <!-- Loading State -->\n @if (isLoading) {\n <div class=\"loading-container\" role=\"status\" aria-live=\"polite\" aria-busy=\"true\">\n <mj-loading text=\"Loading roles...\" size=\"medium\"></mj-loading>\n </div>\n }\n\n <!-- Error State -->\n @if (error && !isLoading) {\n <div class=\"error-container\" role=\"alert\" aria-live=\"assertive\">\n <div class=\"error-content\">\n <i class=\"fa-solid fa-exclamation-triangle error-icon\" aria-hidden=\"true\"></i>\n <p class=\"error-message\">{{ error }}</p>\n <button mjButton variant=\"primary\" (click)=\"loadInitialData()\" aria-label=\"Retry loading roles\">\n <i class=\"fa-solid fa-refresh\" aria-hidden=\"true\"></i>\n Try Again\n </button>\n </div>\n </div>\n }\n\n <!-- Roles List -->\n @if (!isLoading && !error) {\n <div class=\"roles-list\" role=\"list\" aria-label=\"Roles list\">\n @for (role of filteredRoles; track role.ID) {\n <div\n class=\"role-card\"\n [class.expanded]=\"isRoleExpanded(role.ID)\"\n role=\"listitem\"\n [attr.aria-expanded]=\"isRoleExpanded(role.ID)\"\n >\n <div\n class=\"role-header\"\n (click)=\"toggleRoleExpansion(role.ID)\"\n tabindex=\"0\"\n (keydown.enter)=\"toggleRoleExpansion(role.ID)\"\n (keydown.space)=\"toggleRoleExpansion(role.ID); $event.preventDefault()\"\n role=\"button\"\n [attr.aria-label]=\"'Toggle details for ' + role.Name\"\n >\n <div class=\"role-info\">\n <div class=\"role-icon-wrapper\" aria-hidden=\"true\">\n <i [class]=\"'fa-solid ' + getRoleIcon(role)\"></i>\n </div>\n <div class=\"role-details\">\n <h3 class=\"role-name\">{{ role.Name }}</h3>\n <p class=\"role-description\">{{ role.Description || 'No description available' }}</p>\n </div>\n </div>\n\n <div class=\"role-meta\">\n <span class=\"role-type-badge\" [class]=\"getRoleTypeClass(role)\">\n {{ getRoleTypeLabel(role) }}\n </span>\n <div class=\"role-actions\" (click)=\"$event.stopPropagation()\">\n <button\n mjButton variant=\"flat\" size=\"sm\"\n (click)=\"editRole(role)\"\n title=\"Edit\"\n [attr.aria-label]=\"'Edit ' + role.Name\"\n [disabled]=\"isSystemRole(role)\"\n >\n <i class=\"fa-solid fa-edit\" aria-hidden=\"true\"></i>\n </button>\n <button\n mjButton variant=\"danger\" size=\"sm\"\n (click)=\"confirmDeleteRole(role)\"\n title=\"Delete\"\n [attr.aria-label]=\"'Delete ' + role.Name\"\n [disabled]=\"isSystemRole(role)\"\n >\n <i class=\"fa-solid fa-trash\" aria-hidden=\"true\"></i>\n </button>\n </div>\n <button class=\"expand-btn\" aria-label=\"Toggle details\">\n <i class=\"fa-solid fa-chevron-down\" aria-hidden=\"true\"></i>\n </button>\n </div>\n </div>\n\n @if (isRoleExpanded(role.ID)) {\n <div class=\"role-content\">\n <!-- Mobile-only actions bar -->\n <div class=\"mobile-actions-bar\" (click)=\"$event.stopPropagation()\">\n <span class=\"role-type-badge\" [class]=\"getRoleTypeClass(role)\">\n {{ getRoleTypeLabel(role) }}\n </span>\n <div class=\"mobile-action-buttons\">\n <button\n mjButton variant=\"flat\" size=\"sm\"\n (click)=\"editRole(role)\"\n title=\"Edit\"\n [attr.aria-label]=\"'Edit ' + role.Name\"\n [disabled]=\"isSystemRole(role)\"\n >\n <i class=\"fa-solid fa-edit\" aria-hidden=\"true\"></i>\n <span class=\"btn-label\">Edit</span>\n </button>\n <button\n mjButton variant=\"danger\" size=\"sm\"\n (click)=\"confirmDeleteRole(role)\"\n title=\"Delete\"\n [attr.aria-label]=\"'Delete ' + role.Name\"\n [disabled]=\"isSystemRole(role)\"\n >\n <i class=\"fa-solid fa-trash\" aria-hidden=\"true\"></i>\n <span class=\"btn-label\">Delete</span>\n </button>\n </div>\n </div>\n\n <div class=\"role-stats\">\n <div class=\"stat-item\">\n <i class=\"fa-solid fa-users\" aria-hidden=\"true\"></i>\n <span class=\"stat-label\">Users:</span>\n <span class=\"stat-value\">0</span>\n </div>\n <div class=\"stat-item\">\n <i class=\"fa-solid fa-calendar\" aria-hidden=\"true\"></i>\n <span class=\"stat-label\">Created:</span>\n <span class=\"stat-value\">{{ role.__mj_CreatedAt | date:'short' }}</span>\n </div>\n <div class=\"stat-item\">\n <i class=\"fa-solid fa-clock\" aria-hidden=\"true\"></i>\n <span class=\"stat-label\">Updated:</span>\n <span class=\"stat-value\">{{ role.__mj_UpdatedAt | date:'short' }}</span>\n </div>\n </div>\n\n <div class=\"permissions-preview\">\n <h4 class=\"section-title\">\n <i class=\"fa-solid fa-key\" aria-hidden=\"true\"></i>\n Permissions Preview\n </h4>\n <p class=\"permissions-note\">Full permission management available in the Permissions tab</p>\n </div>\n </div>\n }\n </div>\n }\n\n @if (filteredRoles.length === 0) {\n <mj-empty-state\n [Variant]=\"IsListNarrowed ? 'no-results' : 'empty'\"\n Icon=\"fa-solid fa-user-tag\"\n Title=\"No roles found\"\n Message=\"Try adjusting your filters or create a new role\"\n [ActionText]=\"IsListNarrowed ? 'Reset filters' : ''\"\n ActionIcon=\"fa-solid fa-rotate-left\"\n (Action)=\"resetAllFiltersAndSearch()\" />\n }\n </div>\n }\n </mj-page-body-interior>\n\n <!-- Role Create/Edit Dialog -->\n <mj-role-dialog\n [data]=\"roleDialogData\"\n [visible]=\"showRoleDialog\"\n (result)=\"onRoleDialogResult($event)\">\n </mj-role-dialog>\n\n <!-- Delete Confirmation Dialog -->\n @if (showDeleteConfirm && selectedRole) {\n <div\n class=\"modal-backdrop\"\n (click)=\"showDeleteConfirm = false\"\n role=\"presentation\"\n >\n <div\n class=\"modal-dialog\"\n (click)=\"$event.stopPropagation()\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"delete-dialog-title\"\n aria-describedby=\"delete-dialog-desc\"\n >\n <div class=\"modal-header\">\n <h3 class=\"modal-title\" id=\"delete-dialog-title\">\n <i class=\"fa-solid fa-exclamation-triangle text-danger\" aria-hidden=\"true\"></i>\n Confirm Delete\n </h3>\n <button\n class=\"modal-close\"\n (click)=\"showDeleteConfirm = false\"\n aria-label=\"Close dialog\"\n >\n <i class=\"fa-solid fa-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n <div class=\"modal-body\" id=\"delete-dialog-desc\">\n <p>Are you sure you want to delete the role <strong>{{ selectedRole.Name }}</strong>?</p>\n <mj-alert Variant=\"warning\" Message=\"This will affect all users assigned to this role.\" />\n </div>\n <div class=\"modal-footer\">\n <!-- Primary action (Delete) on LEFT per MD3 design guide -->\n <button mjButton variant=\"danger\" (click)=\"deleteRole()\" [disabled]=\"isLoading\">\n @if (isLoading) {\n <i class=\"fa-solid fa-spinner fa-spin\" aria-hidden=\"true\"></i>\n Deleting...\n } @else {\n <i class=\"fa-solid fa-trash\" aria-hidden=\"true\"></i>\n Delete Role\n }\n </button>\n <button mjButton (click)=\"showDeleteConfirm = false\">\n Cancel\n </button>\n </div>\n </div>\n </div>\n }\n\n", styles: ["/* =============================================================================\n Shared Admin Patterns - MJ Design System\n Common styles shared across all admin settings components.\n Component-specific CSS files override these values where needed.\n ============================================================================= */\n\n/* -----------------------------------------------------------------------------\n Host & Container\n ----------------------------------------------------------------------------- */\n:host {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n overflow: hidden;\n}\n\n/* -----------------------------------------------------------------------------\n Sticky Header\n ----------------------------------------------------------------------------- */\n.sticky-header {\n flex-shrink: 0;\n background: var(--mj-bg-page);\n border-bottom: 1px solid var(--mj-border-default);\n box-shadow: var(--mj-shadow-sm);\n z-index: 10;\n}\n\n/* -----------------------------------------------------------------------------\n Scrollable Content\n ----------------------------------------------------------------------------- */\n.scrollable-content {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n -webkit-overflow-scrolling: touch;\n padding: 1rem;\n background: var(--mj-bg-surface-sunken);\n}\n\n@media (min-width: 768px) {\n .scrollable-content {\n padding: 1.5rem 2rem;\n }\n}\n\n@media (min-width: 1024px) {\n .scrollable-content {\n padding: 2rem;\n }\n}\n\n@media (min-width: 1440px) {\n .scrollable-content {\n padding: 2rem 2.5rem;\n }\n}\n\n/* -----------------------------------------------------------------------------\n Action Buttons (In Sticky Header)\n ----------------------------------------------------------------------------- */\n.action-buttons {\n flex-shrink: 0;\n display: flex;\n gap: 0.75rem;\n justify-content: flex-end;\n padding: 0.75rem 1rem;\n background: var(--mj-bg-page);\n}\n\n@media (min-width: 768px) {\n .action-buttons {\n padding: 1rem 1.5rem;\n }\n}\n\n@media (max-width: 639px) {\n .action-buttons {\n justify-content: center;\n flex-wrap: wrap;\n }\n}\n\n/* -----------------------------------------------------------------------------\n Statistics Cards Grid - Static Display (Non-interactive)\n ----------------------------------------------------------------------------- */\n.mj-grid-4 {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 0.5rem;\n padding: 0 1rem 0.75rem 1rem;\n background: var(--mj-bg-page);\n}\n\n@media (min-width: 768px) {\n .mj-grid-4 {\n grid-template-columns: repeat(4, 1fr);\n padding: 0 1.5rem 1rem 1.5rem;\n gap: 0.75rem;\n }\n}\n\n@media (min-width: 1024px) {\n .mj-grid-4 {\n gap: 1rem;\n }\n}\n\n/* Static Card - No hover effects */\n.mj-card {\n background: var(--mj-bg-surface-card);\n border-radius: var(--mj-radius-lg);\n padding: 1rem;\n box-shadow: var(--mj-shadow-sm);\n display: flex;\n align-items: center;\n gap: 1rem;\n border: 1px solid var(--mj-border-default);\n cursor: default;\n pointer-events: none;\n}\n\n@media (min-width: 768px) {\n .mj-card {\n padding: 1rem 1.25rem;\n }\n}\n\n/* -----------------------------------------------------------------------------\n Stat Icon Containers\n ----------------------------------------------------------------------------- */\n.stat-icon {\n width: 48px;\n height: 48px;\n border-radius: var(--mj-radius-lg);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 1.25rem;\n flex-shrink: 0;\n}\n\n@media (min-width: 768px) {\n .stat-icon {\n width: 52px;\n height: 52px;\n font-size: 1.375rem;\n }\n}\n\n.stat-icon-total {\n background: var(--mj-color-accent-300);\n color: var(--mj-brand-primary);\n}\n\n.stat-icon-active {\n background: var(--mj-color-success-100);\n color: var(--mj-status-success);\n}\n\n/* -----------------------------------------------------------------------------\n Stat Content Typography\n ----------------------------------------------------------------------------- */\n.stat-content {\n flex: 1;\n min-width: 0;\n}\n\n.stat-content .stat-value {\n font-size: 1.75rem;\n font-weight: 700;\n color: var(--mj-text-primary);\n line-height: 1;\n letter-spacing: -0.02em;\n}\n\n@media (min-width: 768px) {\n .stat-content .stat-value {\n font-size: 2rem;\n }\n}\n\n.stat-content .stat-label {\n font-size: 0.75rem;\n font-weight: 500;\n color: var(--mj-text-secondary);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin-top: 0.25rem;\n}\n\n@media (min-width: 768px) {\n .stat-content .stat-label {\n font-size: 0.8125rem;\n }\n}\n\n/* -----------------------------------------------------------------------------\n Filters Section\n ----------------------------------------------------------------------------- */\n.filters-section {\n flex-shrink: 0;\n background: var(--mj-bg-surface-card);\n margin: 0 1rem 0.75rem 1rem;\n padding: 0.75rem;\n border-radius: var(--mj-radius-lg);\n border: 1px solid var(--mj-border-default);\n}\n\n@media (min-width: 768px) {\n .filters-section {\n margin: 0 1.5rem 1rem 1.5rem;\n padding: 1rem 1.25rem;\n }\n}\n\n.filters-row {\n display: flex;\n gap: 1rem;\n align-items: flex-end;\n flex-wrap: wrap;\n}\n\n@media (min-width: 768px) {\n .filters-row {\n gap: 1.5rem;\n }\n}\n\n.mj-filter-group {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.mj-filter-label {\n font-size: 0.75rem;\n font-weight: 500;\n color: var(--mj-text-primary);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n}\n\n.mj-filter-buttons {\n display: flex;\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-full);\n padding: 4px;\n gap: 2px;\n}\n\n/* -----------------------------------------------------------------------------\n Search Input\n ----------------------------------------------------------------------------- */\n.mj-search {\n position: relative;\n flex: 1;\n min-width: 200px;\n}\n\n@media (min-width: 640px) {\n .mj-search {\n min-width: 280px;\n }\n}\n\n@media (min-width: 1024px) {\n .mj-search {\n min-width: 400px;\n max-width: 600px;\n }\n}\n\n@media (min-width: 1440px) {\n .mj-search {\n min-width: 500px;\n max-width: 800px;\n }\n}\n\n.mj-search .mj-search-icon {\n position: absolute;\n left: 1rem;\n top: 50%;\n transform: translateY(-50%);\n color: var(--mj-text-secondary);\n font-size: 1rem;\n pointer-events: none;\n transition: color 0.2s ease;\n}\n\n@media (min-width: 1024px) {\n .mj-search .mj-search-icon {\n left: 1.25rem;\n font-size: 1.125rem;\n }\n}\n\n.mj-search .mj-search-input {\n width: 100%;\n padding: 0.875rem 1rem 0.875rem 2.75rem;\n border: 2px solid var(--mj-border-default);\n border-radius: var(--mj-radius-full);\n font-size: 1rem;\n background: var(--mj-bg-page);\n color: var(--mj-text-primary);\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n min-height: 44px;\n}\n\n@media (min-width: 1024px) {\n .mj-search .mj-search-input {\n padding: 1rem 1.25rem 1rem 3.25rem;\n font-size: 1.0625rem;\n }\n}\n\n@media (min-width: 1440px) {\n .mj-search .mj-search-input {\n padding: 1.125rem 1.5rem 1.125rem 3.5rem;\n }\n}\n\n.mj-search .mj-search-input::placeholder {\n color: var(--mj-text-secondary);\n}\n\n.mj-search .mj-search-input:hover {\n border-color: var(--mj-brand-primary);\n background: var(--mj-bg-surface);\n}\n\n.mj-search .mj-search-input:focus {\n outline: none;\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px rgba(0, 118, 182, 0.2);\n background: var(--mj-bg-page);\n}\n\n.mj-search:focus-within .mj-search-icon {\n color: var(--mj-brand-primary);\n}\n\n/* -----------------------------------------------------------------------------\n Content Area\n ----------------------------------------------------------------------------- */\n.content-area {\n flex: 1 1 auto;\n overflow: visible;\n position: relative;\n background: var(--mj-bg-page);\n border-radius: var(--mj-radius-xl);\n box-shadow: var(--mj-shadow-sm);\n padding: 1rem;\n border: 1px solid var(--mj-border-default);\n}\n\n@media (min-width: 768px) {\n .content-area {\n padding: 1.5rem;\n }\n}\n\n/* -----------------------------------------------------------------------------\n Expand Button\n ----------------------------------------------------------------------------- */\n.expand-btn {\n padding: 0.625rem;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 1rem;\n cursor: pointer;\n transition: all 0.2s ease;\n border-radius: var(--mj-radius-full);\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.expand-btn:hover {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n}\n\n.expand-btn i {\n transition: transform 0.3s cubic-bezier(0, 0, 0.2, 1);\n}\n\n/* -----------------------------------------------------------------------------\n Status Badge\n ----------------------------------------------------------------------------- */\n.status-badge {\n display: inline-flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.375rem 0.875rem;\n border-radius: var(--mj-radius-full);\n font-size: 0.75rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.03em;\n}\n\n.status-badge.status-active {\n background: var(--mj-color-success-100);\n color: var(--mj-color-success-800);\n border: 1px solid var(--mj-status-success);\n}\n\n.status-badge.status-inactive {\n background: var(--mj-color-error-100);\n color: var(--mj-status-error);\n border: 1px solid var(--mj-status-error);\n}\n\n.status-badge i {\n font-size: 0.625rem;\n}\n\n/* -----------------------------------------------------------------------------\n Mobile Actions Bar\n ----------------------------------------------------------------------------- */\n.mobile-actions-bar {\n display: none;\n}\n\n.mobile-action-buttons {\n display: flex;\n gap: 0.5rem;\n}\n\n/* -----------------------------------------------------------------------------\n Empty State\n ----------------------------------------------------------------------------- */\n.empty-state {\n text-align: center;\n padding: 4rem 2rem;\n}\n\n.empty-state .empty-icon {\n font-size: 4rem;\n color: var(--mj-border-default);\n margin-bottom: 1.5rem;\n}\n\n.empty-state .empty-text {\n font-size: 1.25rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0 0 0.5rem 0;\n}\n\n.empty-state .empty-subtext {\n font-size: 1rem;\n color: var(--mj-text-secondary);\n margin: 0;\n}\n\n/* -----------------------------------------------------------------------------\n Loading State\n ----------------------------------------------------------------------------- */\n.loading-container {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 4rem 2rem;\n}\n\n/* -----------------------------------------------------------------------------\n Error State\n ----------------------------------------------------------------------------- */\n.error-container {\n text-align: center;\n padding: 4rem 2rem;\n}\n\n.error-container .error-content {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 1rem;\n}\n\n.error-container .error-icon {\n font-size: 3.5rem;\n color: var(--mj-status-error);\n margin-bottom: 1rem;\n}\n\n.error-container .error-message {\n font-size: 1.0625rem;\n color: var(--mj-text-primary);\n margin: 0 0 1.5rem 0;\n}\n\n/* -----------------------------------------------------------------------------\n Modal Dialog\n ----------------------------------------------------------------------------- */\n.modal-backdrop {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.4);\n backdrop-filter: blur(4px);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1000;\n padding: 1rem;\n animation: fadeIn 0.2s cubic-bezier(0, 0, 0.2, 1);\n}\n\n.modal-dialog {\n background: var(--mj-bg-page);\n border-radius: var(--mj-radius-2xl);\n box-shadow: var(--mj-shadow-2xl);\n max-width: 500px;\n width: 100%;\n max-height: 90vh;\n overflow: hidden;\n animation: slideUp 0.3s cubic-bezier(0, 0, 0.2, 1);\n}\n\n.modal-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1.5rem;\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.modal-header .modal-title {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n font-size: 1.375rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0;\n}\n\n.modal-header .modal-close {\n padding: 0.625rem;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 1.25rem;\n cursor: pointer;\n border-radius: var(--mj-radius-full);\n transition: all 0.2s ease;\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.modal-header .modal-close:hover {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n}\n\n.modal-body {\n padding: 1.5rem;\n}\n\n.modal-body p {\n font-size: 1rem;\n color: var(--mj-text-primary);\n margin: 0 0 1rem 0;\n line-height: 1.5;\n}\n\n.modal-body p:last-child {\n margin-bottom: 0;\n}\n\n.modal-footer {\n display: flex;\n justify-content: flex-start;\n gap: 0.75rem;\n padding: 1.5rem;\n border-top: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface-sunken);\n}\n\n@media (max-width: 639px) {\n .modal-dialog {\n width: 95%;\n max-height: 85vh;\n }\n\n .modal-header {\n padding: 1rem;\n }\n\n .modal-header .modal-title {\n font-size: 1.125rem;\n }\n\n .modal-body {\n padding: 1rem;\n }\n\n .modal-footer {\n padding: 1rem;\n flex-direction: column;\n }\n\n}\n\n/* The legacy \"MJ Button System\" rule block (.mj-btn / .mj-btn-primary /\n .mj-btn-secondary / .mj-btn-ghost / .mj-btn-danger / .mj-btn-sm /\n .mj-btn-icon-only / .mj-btn-icon-mobile) was removed 2026-05-19. These\n rules pre-dated the mjButton directive standardization (the directive\n uses BEM modifiers like .mj-btn--primary, single-dash names like\n .mj-btn-primary don't match it anyway). The base .mj-btn rule WAS active\n though \u2014 its component-scoped specificity outranked button.scss's global\n directive styles, so admin sub-page buttons rendered pill-shaped + 44px\n regardless of the variant/size requested via mjButton inputs. Removing the\n rule lets every sub-page fall back to the directive's intended look. */\n\n/* -----------------------------------------------------------------------------\n Filter Button (Mobile Only)\n ----------------------------------------------------------------------------- */\n.filter-button {\n display: none;\n align-items: center;\n justify-content: center;\n gap: 0.5rem;\n padding: 0.75rem 1.25rem;\n border: 2px solid var(--mj-border-default);\n background: var(--mj-bg-page);\n color: var(--mj-text-primary);\n border-radius: var(--mj-radius-full);\n font-size: 0.875rem;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n min-height: 44px;\n min-width: 44px;\n}\n\n.filter-button:hover {\n border-color: var(--mj-brand-primary);\n background: var(--mj-bg-surface);\n}\n\n.filter-button:active {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n border-color: var(--mj-brand-primary);\n}\n\n.filter-button i {\n font-size: 1rem;\n}\n\n/* -----------------------------------------------------------------------------\n Filter Modal (Mobile Bottom Sheet)\n ----------------------------------------------------------------------------- */\n.filter-modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.4);\n backdrop-filter: blur(4px);\n display: flex;\n align-items: flex-end;\n justify-content: center;\n z-index: 1000;\n animation: fadeIn 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n padding: 0;\n}\n\n.filter-modal {\n background: var(--mj-bg-page);\n border-radius: var(--mj-radius-2xl) var(--mj-radius-2xl) 0 0;\n box-shadow: var(--mj-shadow-2xl);\n width: 100%;\n max-height: 80vh;\n overflow: hidden;\n animation: slideUpFromBottom 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n display: flex;\n flex-direction: column;\n}\n\n.filter-modal-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1.25rem 1.5rem;\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.filter-modal-title {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n font-size: 1.25rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0;\n}\n\n.filter-modal-title i {\n color: var(--mj-brand-primary);\n font-size: 1.375rem;\n}\n\n.filter-modal-close {\n padding: 0.625rem;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 1.25rem;\n cursor: pointer;\n border-radius: var(--mj-radius-full);\n transition: all 0.2s ease;\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.filter-modal-close:hover {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n}\n\n.filter-modal-body {\n padding: 1.5rem;\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n flex: 1;\n}\n\n.filter-modal-footer {\n display: flex;\n gap: 0.75rem;\n padding: 1.25rem 1.5rem;\n border-top: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface-sunken);\n flex-shrink: 0;\n}\n\n.filter-modal-footer button {\n flex: 1;\n margin: 0;\n}\n\n/* Filter Options Container (Inside Modal) */\n.filter-options-container {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n}\n\n.filter-group {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.filter-group-label {\n font-size: 0.875rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin: 0;\n}\n\n.filter-group-options {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.filter-option {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 0.875rem 1rem;\n background: var(--mj-bg-surface-sunken);\n border: 2px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg);\n cursor: pointer;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n min-height: 56px;\n}\n\n.filter-option:hover {\n border-color: var(--mj-brand-primary);\n background: var(--mj-bg-surface-sunken);\n}\n\n.filter-option.selected {\n background: var(--mj-color-accent-300);\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n}\n\n.filter-option input[type=\"checkbox\"],\n.filter-option input[type=\"radio\"] {\n width: 20px;\n height: 20px;\n cursor: pointer;\n accent-color: var(--mj-brand-primary);\n flex-shrink: 0;\n}\n\n.filter-option-label {\n flex: 1;\n font-size: 1rem;\n font-weight: 500;\n color: var(--mj-text-primary);\n}\n\n.filter-option.selected .filter-option-label {\n color: var(--mj-brand-primary);\n font-weight: 600;\n}\n\n/* -----------------------------------------------------------------------------\n Utility Classes\n ----------------------------------------------------------------------------- */\n.text-danger {\n color: var(--mj-status-error);\n}\n\n.text-warning {\n color: var(--mj-color-warning-500);\n}\n\n.text-success {\n color: var(--mj-status-success);\n}\n\n/* -----------------------------------------------------------------------------\n Animations\n ----------------------------------------------------------------------------- */\n@keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n@keyframes slideUp {\n from {\n transform: translateY(20px);\n opacity: 0;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n}\n\n@keyframes slideDown {\n from {\n opacity: 0;\n transform: translateY(-10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes slideUpFromBottom {\n from {\n transform: translateY(100%);\n opacity: 0;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n}\n\n/* -----------------------------------------------------------------------------\n Accessibility: Focus Indicators\n ----------------------------------------------------------------------------- */\n.mj-search-input:focus-visible,\nbutton:focus-visible {\n outline: 2px solid var(--mj-brand-primary);\n outline-offset: 2px;\n}\n\n/* -----------------------------------------------------------------------------\n Accessibility: Reduced Motion\n ----------------------------------------------------------------------------- */\n@media (prefers-reduced-motion: reduce) {\n *,\n *::before,\n *::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n }\n}\n", "/* =============================================================================\n Role Management Component - Component-Specific Styles\n Shared patterns are in ../shared/styles/_admin-patterns.css\n ============================================================================= */\n\n/* :host already provides flex column / 100% height / overflow hidden via\n _admin-patterns.css. No bespoke container wrapper \u2014 see Section 10 of\n explorer-chrome-conventions for the standardized sub-page layout contract. */\n\n/* Status / type chip group inside <mj-page-header-interior>'s [toolbar] slot. */\n.status-chips {\n display: inline-flex;\n align-items: center;\n gap: var(--mj-space-2);\n flex-shrink: 0;\n}\n\n/* -----------------------------------------------------------------------------\n Stats Grid Override - Different gap/padding values\n ----------------------------------------------------------------------------- */\n.mj-grid-4 {\n gap: 0.75rem;\n padding: 0 1rem 1rem 1rem;\n}\n\n@media (min-width: 768px) {\n .mj-grid-4 {\n padding: 0 1.5rem 1.25rem 1.5rem;\n gap: 1rem;\n }\n}\n\n@media (min-width: 1024px) {\n .mj-grid-4 {\n gap: 1.25rem;\n }\n}\n\n/* Stat Card Override - no box-shadow, different 768px padding */\n.mj-card {\n box-shadow: none;\n}\n\n@media (min-width: 768px) {\n .mj-card {\n padding: 1.125rem 1.25rem;\n }\n}\n\n/* Stat Icon Override - Component-specific colors */\n.stat-icon-system {\n background: var(--mj-color-warning-50);\n color: var(--mj-color-warning-700);\n}\n\n.stat-icon-custom {\n background: var(--mj-color-success-100);\n color: var(--mj-status-success);\n}\n\n.stat-icon-active {\n background: var(--mj-color-accent-300);\n color: var(--mj-brand-primary);\n}\n\n/* -----------------------------------------------------------------------------\n Roles List\n ----------------------------------------------------------------------------- */\n.roles-list {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n/* -----------------------------------------------------------------------------\n Role Card\n ----------------------------------------------------------------------------- */\n.role-card {\n background: var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-xl);\n overflow: hidden;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.role-card:hover {\n box-shadow: var(--mj-shadow-md);\n border-color: var(--mj-brand-primary);\n}\n\n.role-card.expanded {\n box-shadow: var(--mj-shadow-lg);\n border-color: var(--mj-brand-primary);\n}\n\n.role-card.expanded .expand-btn i {\n transform: rotate(180deg);\n}\n\n.role-header {\n padding: 1.25rem 1.5rem;\n display: flex;\n justify-content: space-between;\n align-items: center;\n cursor: pointer;\n background: var(--mj-bg-surface);\n transition: background-color 0.2s ease;\n}\n\n.role-header:hover {\n background: var(--mj-bg-surface-sunken);\n}\n\n.role-info {\n display: flex;\n align-items: center;\n gap: 1rem;\n flex: 1;\n min-width: 0;\n}\n\n.role-icon-wrapper {\n width: 52px;\n height: 52px;\n border-radius: var(--mj-radius-lg);\n background: var(--mj-color-accent-300);\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--mj-brand-primary);\n font-size: 1.375rem;\n flex-shrink: 0;\n}\n\n.role-details {\n flex: 1;\n min-width: 0;\n}\n\n.role-details .role-name {\n font-size: 1.125rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0 0 0.25rem 0;\n word-break: break-word;\n overflow-wrap: break-word;\n}\n\n.role-details .role-description {\n font-size: 0.875rem;\n color: var(--mj-text-secondary);\n margin: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n@media (max-width: 639px) {\n .role-details .role-description {\n white-space: normal;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n }\n}\n\n.role-meta {\n display: flex;\n align-items: center;\n gap: 1rem;\n flex-shrink: 0;\n}\n\n/* -----------------------------------------------------------------------------\n Role Type Badge\n ----------------------------------------------------------------------------- */\n.role-type-badge {\n display: inline-flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.375rem 0.875rem;\n border-radius: var(--mj-radius-full);\n font-size: 0.75rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.03em;\n}\n\n.role-type-badge.badge-system {\n background: var(--mj-color-warning-50);\n color: var(--mj-color-warning-800);\n border: 1px solid var(--mj-color-warning-500);\n}\n\n.role-type-badge.badge-custom {\n background: var(--mj-color-success-100);\n color: var(--mj-color-success-800);\n border: 1px solid var(--mj-status-success);\n}\n\n/* -----------------------------------------------------------------------------\n Role Actions\n ----------------------------------------------------------------------------- */\n.role-actions {\n display: flex;\n gap: 0.375rem;\n}\n\n/* Expand Button Override - different transition easing */\n.expand-btn i {\n transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n/* -----------------------------------------------------------------------------\n Role Content (Expanded)\n ----------------------------------------------------------------------------- */\n.role-content {\n padding: 1.5rem;\n background: var(--mj-bg-surface);\n border-top: 1px solid var(--mj-border-default);\n animation: slideDown 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.role-stats {\n display: flex;\n gap: 2rem;\n margin-bottom: 1.5rem;\n flex-wrap: wrap;\n}\n\n.role-stats .stat-item {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n font-size: 0.9375rem;\n padding: 0.5rem 1rem;\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-md);\n}\n\n.role-stats .stat-item i {\n color: var(--mj-brand-primary);\n font-size: 1rem;\n}\n\n.role-stats .stat-item .stat-label {\n color: var(--mj-text-secondary);\n}\n\n.role-stats .stat-item .stat-value {\n color: var(--mj-text-primary);\n font-weight: 600;\n}\n\n/* -----------------------------------------------------------------------------\n Permissions Preview\n ----------------------------------------------------------------------------- */\n.permissions-preview {\n padding: 1.25rem;\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-lg);\n border: 1px solid var(--mj-border-default);\n}\n\n.permissions-preview .section-title {\n display: flex;\n align-items: center;\n gap: 0.625rem;\n font-size: 1rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0 0 0.5rem 0;\n}\n\n.permissions-preview .section-title i {\n color: var(--mj-brand-primary);\n font-size: 1.125rem;\n}\n\n.permissions-preview .permissions-note {\n font-size: 0.9375rem;\n color: var(--mj-text-secondary);\n margin: 0;\n}\n\n/* Modal Body Override - line-height and warning callout */\n.modal-body p {\n line-height: 1.6;\n}\n\n/* Modal Backdrop Override - uses top/left/right/bottom instead of inset */\n.modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n}\n\n/* -----------------------------------------------------------------------------\n Large Desktop (1440px+)\n ----------------------------------------------------------------------------- */\n@media (min-width: 1440px) {\n .role-management-container {\n margin: 0 auto;\n }\n\n .action-buttons {\n padding: 1rem 2rem;\n }\n\n .mj-grid-4 {\n padding: 0 2rem 1.25rem 2rem;\n gap: 1.5rem;\n }\n\n}\n\n/* -----------------------------------------------------------------------------\n Mobile (< 640px)\n ----------------------------------------------------------------------------- */\n@media (max-width: 639px) {\n .mj-grid-4 {\n display: none !important;\n }\n\n .scrollable-content {\n padding: 0.75rem;\n }\n\n .modal-backdrop {\n padding: 0.5rem;\n }\n\n .modal-dialog {\n border-radius: var(--mj-radius-xl);\n max-height: 95vh;\n }\n\n .roles-list {\n gap: 0.75rem;\n }\n\n .role-card {\n border-radius: var(--mj-radius-lg);\n overflow: hidden;\n }\n\n .role-header {\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n padding: 1rem;\n }\n\n .role-info {\n flex: 1;\n min-width: 0;\n flex-direction: row;\n align-items: flex-start;\n gap: 0.875rem;\n }\n\n .role-icon-wrapper {\n width: 40px;\n height: 40px;\n font-size: 1rem;\n flex-shrink: 0;\n }\n\n .role-details {\n flex: 1;\n min-width: 0;\n }\n\n .role-details .role-name {\n font-size: 1rem;\n line-height: 1.3;\n margin-bottom: 0.125rem;\n }\n\n .role-details .role-description {\n font-size: 0.8125rem;\n -webkit-line-clamp: 1;\n line-clamp: 1;\n }\n\n .role-meta {\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: flex-end;\n padding-top: 0;\n border-top: none;\n width: auto;\n }\n\n .role-meta .role-type-badge {\n display: none;\n }\n\n .role-meta .role-actions {\n display: none;\n }\n\n .expand-btn {\n flex-shrink: 0;\n min-width: 40px;\n min-height: 40px;\n padding: 0.5rem;\n }\n\n .expand-btn i {\n font-size: 1rem;\n }\n\n .mobile-actions-bar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n padding: 0.875rem;\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-lg);\n margin-bottom: 1rem;\n }\n\n .mobile-actions-bar .role-type-badge {\n padding: 0.375rem 0.75rem;\n font-size: 0.6875rem;\n }\n\n .mobile-action-buttons {\n display: flex;\n gap: 0.5rem;\n }\n\n .mobile-action-buttons .mj-btn-sm {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.5rem 0.875rem;\n min-height: 36px;\n }\n\n .mobile-action-buttons .mj-btn-sm i {\n font-size: 0.875rem;\n }\n\n .mobile-action-buttons .btn-label {\n font-size: 0.8125rem;\n font-weight: 500;\n }\n\n .role-stats {\n flex-direction: column;\n gap: 0.5rem;\n margin-bottom: 1rem;\n }\n\n .role-content {\n padding: 1rem;\n }\n\n .permissions-preview {\n padding: 1rem;\n }\n\n .loading-container {\n padding: 3rem 1.5rem;\n }\n}\n\n/* Small Mobile (< 375px) */\n@media (max-width: 374px) {\n .action-buttons {\n padding: 0.375rem 0.5rem;\n }\n\n .scrollable-content {\n padding: 0.5rem;\n }\n\n .role-header {\n padding: 0.75rem;\n }\n}\n"] }]
|
|
545
740
|
}], () => [{ type: i0.ChangeDetectorRef }, { type: i0.NgZone }], null); })();
|
|
546
|
-
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(RoleManagementComponent, { className: "RoleManagementComponent", filePath: "src/lib/role-management/role-management.component.ts", lineNumber:
|
|
741
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(RoleManagementComponent, { className: "RoleManagementComponent", filePath: "src/lib/role-management/role-management.component.ts", lineNumber: 37 }); })();
|
|
547
742
|
//# sourceMappingURL=role-management.component.js.map
|