@idp.global/catalog 2.0.0 → 2.1.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.
Files changed (70) hide show
  1. package/changelog.md +15 -0
  2. package/dist_bundle/bundle.js +4428 -1496
  3. package/dist_ts_web/00_commitinfo_data.js +1 -1
  4. package/dist_ts_web/elements/idp-admin-overview.d.ts +28 -0
  5. package/dist_ts_web/elements/idp-admin-overview.js +460 -0
  6. package/dist_ts_web/elements/idp-admin-shared.d.ts +18 -0
  7. package/dist_ts_web/elements/idp-admin-shared.js +404 -0
  8. package/dist_ts_web/elements/idp-admin-shell.d.ts +96 -11
  9. package/dist_ts_web/elements/idp-admin-shell.js +442 -816
  10. package/dist_ts_web/elements/idp-admin-sidebar.d.ts +42 -0
  11. package/dist_ts_web/elements/idp-admin-sidebar.js +700 -0
  12. package/dist_ts_web/elements/idp-app-pair.d.ts +19 -0
  13. package/dist_ts_web/elements/idp-app-pair.js +187 -0
  14. package/dist_ts_web/elements/idp-auth-card.d.ts +17 -0
  15. package/dist_ts_web/elements/idp-auth-card.js +180 -0
  16. package/dist_ts_web/elements/idp-avatar.d.ts +17 -0
  17. package/dist_ts_web/elements/idp-avatar.js +151 -0
  18. package/dist_ts_web/elements/idp-badge.d.ts +2 -0
  19. package/dist_ts_web/elements/idp-badge.js +12 -3
  20. package/dist_ts_web/elements/idp-brand-mark.d.ts +16 -0
  21. package/dist_ts_web/elements/idp-brand-mark.js +139 -0
  22. package/dist_ts_web/elements/idp-button.d.ts +3 -0
  23. package/dist_ts_web/elements/idp-button.js +47 -3
  24. package/dist_ts_web/elements/idp-consent-card.d.ts +34 -0
  25. package/dist_ts_web/elements/idp-consent-card.js +217 -0
  26. package/dist_ts_web/elements/idp-delegation-section.d.ts +59 -0
  27. package/dist_ts_web/elements/idp-delegation-section.js +609 -0
  28. package/dist_ts_web/elements/idp-form-submit.d.ts +2 -1
  29. package/dist_ts_web/elements/idp-form-submit.js +11 -2
  30. package/dist_ts_web/elements/idp-form.js +2 -2
  31. package/dist_ts_web/elements/idp-input.d.ts +4 -0
  32. package/dist_ts_web/elements/idp-input.js +68 -15
  33. package/dist_ts_web/elements/idp-option-row.d.ts +25 -0
  34. package/dist_ts_web/elements/idp-option-row.js +267 -0
  35. package/dist_ts_web/elements/idp-otp-input.d.ts +32 -0
  36. package/dist_ts_web/elements/idp-otp-input.js +274 -0
  37. package/dist_ts_web/elements/idp-progress-bar.d.ts +14 -0
  38. package/dist_ts_web/elements/idp-progress-bar.js +127 -0
  39. package/dist_ts_web/elements/idp-signed-in-pill.d.ts +15 -0
  40. package/dist_ts_web/elements/idp-signed-in-pill.js +113 -0
  41. package/dist_ts_web/elements/idp-spinner.d.ts +13 -0
  42. package/dist_ts_web/elements/idp-spinner.js +109 -0
  43. package/dist_ts_web/elements/idp-status-icon.d.ts +18 -0
  44. package/dist_ts_web/elements/idp-status-icon.js +136 -0
  45. package/dist_ts_web/elements/index.d.ts +15 -0
  46. package/dist_ts_web/elements/index.js +16 -1
  47. package/package.json +1 -1
  48. package/ts_web/00_commitinfo_data.ts +1 -1
  49. package/ts_web/elements/idp-admin-overview.ts +388 -0
  50. package/ts_web/elements/idp-admin-shared.ts +419 -0
  51. package/ts_web/elements/idp-admin-shell.ts +514 -871
  52. package/ts_web/elements/idp-admin-sidebar.ts +646 -0
  53. package/ts_web/elements/idp-app-pair.ts +120 -0
  54. package/ts_web/elements/idp-auth-card.ts +113 -0
  55. package/ts_web/elements/idp-avatar.ts +84 -0
  56. package/ts_web/elements/idp-badge.ts +5 -1
  57. package/ts_web/elements/idp-brand-mark.ts +76 -0
  58. package/ts_web/elements/idp-button.ts +36 -1
  59. package/ts_web/elements/idp-consent-card.ts +146 -0
  60. package/ts_web/elements/idp-delegation-section.ts +576 -0
  61. package/ts_web/elements/idp-form-submit.ts +5 -1
  62. package/ts_web/elements/idp-form.ts +1 -1
  63. package/ts_web/elements/idp-input.ts +58 -13
  64. package/ts_web/elements/idp-option-row.ts +184 -0
  65. package/ts_web/elements/idp-otp-input.ts +204 -0
  66. package/ts_web/elements/idp-progress-bar.ts +69 -0
  67. package/ts_web/elements/idp-signed-in-pill.ts +55 -0
  68. package/ts_web/elements/idp-spinner.ts +56 -0
  69. package/ts_web/elements/idp-status-icon.ts +76 -0
  70. package/ts_web/elements/index.ts +15 -0
@@ -0,0 +1,646 @@
1
+ import { DeesElement, html, property, state, customElement, css, type TemplateResult } from '@design.estate/dees-element';
2
+ import { idpElementStyles } from './tokens.js';
3
+ import { idpAdminSharedStyles } from './idp-admin-shared.js';
4
+ import './idp-icon.js';
5
+ import './idp-badge.js';
6
+ import './idp-avatar.js';
7
+ import type {
8
+ TIdpAdminPage,
9
+ IIdpAdminUser,
10
+ IIdpAdminOrg,
11
+ IIdpAdminNavigateEventDetail,
12
+ IIdpAdminOrgSelectEventDetail,
13
+ IIdpAdminAccountSwitchEventDetail,
14
+ } from './idp-admin-shell.js';
15
+
16
+ declare global {
17
+ interface HTMLElementTagNameMap {
18
+ 'idp-admin-sidebar': IdpAdminSidebar;
19
+ }
20
+ }
21
+
22
+ type TNavItem = {
23
+ id: TIdpAdminPage;
24
+ label: string;
25
+ icon: string;
26
+ badge?: string;
27
+ };
28
+
29
+ @customElement('idp-admin-sidebar')
30
+ export class IdpAdminSidebar extends DeesElement {
31
+ public static demo = () => html`
32
+ <div style="height: 520px; width: 220px; border: 1px solid var(--sh-border, #e8e8ed); overflow: hidden;">
33
+ <idp-admin-sidebar
34
+ global-admin
35
+ page="overview"
36
+ selected-org-id="org_foss"
37
+ session-count="3"
38
+ .user=${{ name: 'Alex Mercer', email: 'alex@lossless.com' }}
39
+ .accounts=${[{ name: 'Alex Mercer', email: 'alex.mercer@gmail.com' }]}
40
+ .orgs=${[
41
+ { id: 'org_foss', name: 'Lossless GmbH', slug: 'lossless', myRole: 'owner' },
42
+ { id: 'org_task', name: 'Task VC', slug: 'task', myRole: 'admin' },
43
+ ]}
44
+ ></idp-admin-sidebar>
45
+ </div>
46
+ `;
47
+ public static demoGroups = ['idp.global v5 composed surfaces'];
48
+
49
+ public static styles = [
50
+ ...idpElementStyles,
51
+ idpAdminSharedStyles,
52
+ css`
53
+ :host {
54
+ display: block;
55
+ height: 100%;
56
+ min-height: 0;
57
+ }
58
+ .sidebar {
59
+ height: 100%;
60
+ min-height: 0;
61
+ display: flex;
62
+ flex-direction: column;
63
+ overflow: hidden;
64
+ border-right: 1px solid var(--idp-border);
65
+ background: var(--idp-bg);
66
+ }
67
+ .logo-block {
68
+ padding: 14px 16px 10px;
69
+ border-bottom: 1px solid var(--idp-border);
70
+ }
71
+ .logo {
72
+ display: flex;
73
+ align-items: center;
74
+ gap: 8px;
75
+ }
76
+ .logo-icon {
77
+ width: 26px;
78
+ height: 26px;
79
+ display: flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ flex-shrink: 0;
83
+ border-radius: 7px;
84
+ background: var(--idp-primary);
85
+ color: var(--idp-primary-fg);
86
+ }
87
+ .logo-text {
88
+ color: var(--idp-fg);
89
+ font-size: 13px;
90
+ font-weight: 700;
91
+ letter-spacing: -0.3px;
92
+ }
93
+ .nav-wrap {
94
+ flex: 1;
95
+ min-height: 0;
96
+ overflow: auto;
97
+ padding: 10px 8px;
98
+ }
99
+ .nav-section {
100
+ margin-bottom: 20px;
101
+ }
102
+ .nav-title {
103
+ padding: 0 8px;
104
+ margin-bottom: 4px;
105
+ color: var(--idp-muted-fg);
106
+ font-size: 10px;
107
+ font-weight: 600;
108
+ letter-spacing: 0.8px;
109
+ text-transform: uppercase;
110
+ }
111
+ .nav-list {
112
+ display: grid;
113
+ }
114
+ .nav-item,
115
+ .org-switch {
116
+ border: 0;
117
+ font-family: inherit;
118
+ }
119
+ .nav-item {
120
+ width: 100%;
121
+ display: flex;
122
+ align-items: center;
123
+ gap: 8px;
124
+ padding: 6px 8px;
125
+ border-radius: 7px;
126
+ background: transparent;
127
+ color: var(--idp-muted-fg);
128
+ font-size: 13px;
129
+ font-weight: 400;
130
+ text-align: left;
131
+ }
132
+ .nav-item.active {
133
+ background: var(--idp-muted);
134
+ color: var(--idp-fg);
135
+ font-weight: 500;
136
+ }
137
+ .nav-item idp-icon {
138
+ color: currentColor;
139
+ }
140
+ .nav-item.active idp-icon {
141
+ color: var(--idp-accent);
142
+ }
143
+ .org-switch-wrap {
144
+ position: relative;
145
+ margin-bottom: 2px;
146
+ }
147
+ .org-switch {
148
+ width: 100%;
149
+ display: flex;
150
+ align-items: center;
151
+ gap: 8px;
152
+ padding: 6px 8px;
153
+ border-radius: 7px;
154
+ background: transparent;
155
+ color: var(--idp-muted-fg);
156
+ cursor: pointer;
157
+ }
158
+ .org-switch:hover {
159
+ background: var(--idp-muted);
160
+ }
161
+ .org-switch.open {
162
+ background: var(--idp-muted);
163
+ }
164
+ .org-switch.open .org-name {
165
+ color: var(--idp-accent);
166
+ }
167
+ .org-switch.open idp-icon {
168
+ transform: rotate(180deg);
169
+ }
170
+ .org-menu {
171
+ position: absolute;
172
+ top: calc(100% + 4px);
173
+ left: 0;
174
+ right: 0;
175
+ z-index: 40;
176
+ overflow: hidden;
177
+ border: 1px solid var(--idp-border);
178
+ border-radius: 10px;
179
+ background: var(--idp-card);
180
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
181
+ }
182
+ .org-menu-title {
183
+ padding: 6px 8px 3px;
184
+ color: var(--idp-muted-fg);
185
+ font-size: 10px;
186
+ font-weight: 600;
187
+ letter-spacing: 0.7px;
188
+ text-transform: uppercase;
189
+ }
190
+ .org-menu-item,
191
+ .org-create {
192
+ width: 100%;
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 8px;
196
+ border: 0;
197
+ background: transparent;
198
+ font-family: inherit;
199
+ text-align: left;
200
+ }
201
+ .org-menu-item {
202
+ padding: 6px 8px;
203
+ cursor: pointer;
204
+ }
205
+ .org-menu-item.selected {
206
+ background: var(--idp-accent-soft);
207
+ }
208
+ .org-menu-item .org-avatar {
209
+ width: 20px;
210
+ height: 20px;
211
+ font-size: 7px;
212
+ }
213
+ .org-menu-name {
214
+ overflow: hidden;
215
+ color: var(--idp-fg);
216
+ font-size: 12px;
217
+ font-weight: 400;
218
+ text-overflow: ellipsis;
219
+ white-space: nowrap;
220
+ }
221
+ .org-menu-item.selected .org-menu-name {
222
+ color: var(--idp-accent);
223
+ }
224
+ .org-menu-role {
225
+ color: var(--idp-muted-fg);
226
+ font-size: 10px;
227
+ }
228
+ .org-menu-divider {
229
+ height: 1px;
230
+ margin: 3px 0;
231
+ background: var(--idp-border);
232
+ }
233
+ .org-create {
234
+ padding: 6px 8px 8px;
235
+ color: var(--idp-muted-fg);
236
+ cursor: pointer;
237
+ font-size: 12px;
238
+ }
239
+ .org-create-icon {
240
+ width: 20px;
241
+ height: 20px;
242
+ display: flex;
243
+ align-items: center;
244
+ justify-content: center;
245
+ flex-shrink: 0;
246
+ border: 1.5px dashed var(--idp-border);
247
+ border-radius: 5px;
248
+ }
249
+ .org-name {
250
+ flex: 1;
251
+ min-width: 0;
252
+ overflow: hidden;
253
+ color: var(--idp-muted-fg);
254
+ font-size: 13px;
255
+ font-weight: 400;
256
+ text-align: left;
257
+ text-overflow: ellipsis;
258
+ white-space: nowrap;
259
+ }
260
+ .user-footer {
261
+ position: relative;
262
+ padding: 8px;
263
+ border-top: 1px solid var(--idp-border);
264
+ }
265
+ .user-trigger {
266
+ display: flex;
267
+ align-items: center;
268
+ gap: 9px;
269
+ width: 100%;
270
+ padding: 6px 8px;
271
+ border: none;
272
+ border-radius: 8px;
273
+ background: transparent;
274
+ color: var(--idp-muted-fg);
275
+ font-family: var(--idp-font);
276
+ text-align: left;
277
+ cursor: pointer;
278
+ transition: background 120ms ease;
279
+ }
280
+ .user-trigger:hover,
281
+ .user-trigger.open {
282
+ background: var(--idp-muted);
283
+ }
284
+ .user-menu {
285
+ position: absolute;
286
+ bottom: calc(100% + 2px);
287
+ left: 8px;
288
+ right: 8px;
289
+ z-index: 40;
290
+ padding: 4px;
291
+ background: var(--idp-card);
292
+ border: 1px solid var(--idp-border);
293
+ border-radius: var(--idp-radius);
294
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
295
+ overflow: hidden;
296
+ }
297
+ .user-menu-identity {
298
+ display: flex;
299
+ align-items: center;
300
+ gap: 9px;
301
+ padding: 8px 10px 9px;
302
+ margin-bottom: 4px;
303
+ border-bottom: 1px solid var(--idp-border);
304
+ }
305
+ .user-menu-item {
306
+ display: flex;
307
+ align-items: center;
308
+ gap: 9px;
309
+ width: 100%;
310
+ padding: 7px 10px;
311
+ border: none;
312
+ border-radius: 7px;
313
+ background: transparent;
314
+ color: var(--idp-fg);
315
+ font-family: var(--idp-font);
316
+ font-size: 12.5px;
317
+ text-align: left;
318
+ cursor: pointer;
319
+ transition: background 100ms ease;
320
+ }
321
+ .user-menu-item idp-icon {
322
+ color: var(--idp-muted-fg);
323
+ flex-shrink: 0;
324
+ }
325
+ .user-menu-item span {
326
+ flex: 1;
327
+ }
328
+ .user-menu-item:hover {
329
+ background: var(--idp-muted);
330
+ }
331
+ .user-menu-item.destructive {
332
+ color: var(--idp-destructive);
333
+ }
334
+ .user-menu-item.destructive idp-icon {
335
+ color: var(--idp-destructive);
336
+ }
337
+ .user-menu-item.destructive:hover {
338
+ background: var(--idp-error-bg);
339
+ }
340
+ .user-menu-item.account .user-meta {
341
+ flex: 1;
342
+ }
343
+ .user-menu-hint {
344
+ flex: 0 0 auto !important;
345
+ font-size: 10.5px;
346
+ color: var(--idp-muted-fg);
347
+ }
348
+ .user-menu-divider {
349
+ height: 1px;
350
+ margin: 4px 0;
351
+ background: var(--idp-border);
352
+ }
353
+ .user-menu-title {
354
+ padding: 4px 10px 2px;
355
+ font-size: 10px;
356
+ font-weight: 600;
357
+ letter-spacing: 0.7px;
358
+ text-transform: uppercase;
359
+ }
360
+ .user-menu-title,
361
+ .user-email {
362
+ color: var(--idp-muted-fg);
363
+ }
364
+ .user-avatar {
365
+ display: flex;
366
+ align-items: center;
367
+ justify-content: center;
368
+ flex-shrink: 0;
369
+ width: 26px;
370
+ height: 26px;
371
+ border-radius: 50%;
372
+ background: oklch(0.85 0.08 240);
373
+ color: oklch(0.3 0.08 240);
374
+ font-family: var(--idp-mono);
375
+ font-size: 9px;
376
+ font-weight: 700;
377
+ }
378
+ .user-meta {
379
+ flex: 1;
380
+ min-width: 0;
381
+ }
382
+ .user-name,
383
+ .user-email {
384
+ overflow: hidden;
385
+ text-overflow: ellipsis;
386
+ white-space: nowrap;
387
+ }
388
+ .user-name {
389
+ color: var(--idp-fg);
390
+ font-size: 12px;
391
+ font-weight: 500;
392
+ }
393
+ .user-email {
394
+ font-size: 10px;
395
+ }
396
+ `,
397
+ ];
398
+
399
+ @property({ type: String })
400
+ public accessor page: TIdpAdminPage = 'overview';
401
+
402
+ @property({ type: Object })
403
+ public accessor user: IIdpAdminUser = {
404
+ name: '',
405
+ email: '',
406
+ };
407
+
408
+ @property({ type: Array })
409
+ public accessor accounts: IIdpAdminUser[] = [];
410
+
411
+ @property({ type: Array })
412
+ public accessor orgs: IIdpAdminOrg[] = [];
413
+
414
+ @property({ type: String, attribute: 'selected-org-id' })
415
+ public accessor selectedOrgId = '';
416
+
417
+ @property({ type: Boolean, attribute: 'global-admin', reflect: true })
418
+ public accessor globalAdmin = false;
419
+
420
+ @property({ type: Number, attribute: 'session-count' })
421
+ public accessor sessionCount = 0;
422
+
423
+ @state()
424
+ private accessor orgMenuOpen = false;
425
+
426
+ @state()
427
+ private accessor userMenuOpen = false;
428
+
429
+ private workspaceNav: TNavItem[] = [
430
+ { id: 'overview', label: 'Overview', icon: 'grid' },
431
+ ];
432
+
433
+ private accountNav: TNavItem[] = [
434
+ { id: 'profile', label: 'Profile', icon: 'user' },
435
+ { id: 'security', label: 'Security', icon: 'shield' },
436
+ { id: 'sessions', label: 'Sessions & Devices', icon: 'monitor' },
437
+ { id: 'apps', label: 'Connected Apps', icon: 'grid' },
438
+ ];
439
+
440
+ private orgNav: TNavItem[] = [
441
+ { id: 'org-general', label: 'General', icon: 'building' },
442
+ { id: 'org-settings', label: 'Settings', icon: 'settings' },
443
+ { id: 'org-members', label: 'Members', icon: 'users' },
444
+ { id: 'org-apps', label: 'OAuth Apps', icon: 'box' },
445
+ ];
446
+
447
+ private supportNav: TNavItem[] = [
448
+ { id: 'support', label: 'Support', icon: 'shield' },
449
+ ];
450
+
451
+ private adminNav: TNavItem[] = [
452
+ { id: 'ga-users', label: 'All Users', icon: 'users' },
453
+ { id: 'ga-orgs', label: 'All Organisations', icon: 'building' },
454
+ { id: 'ga-apps', label: 'Platform Apps', icon: 'globe' },
455
+ ];
456
+
457
+ private get safeOrgs(): IIdpAdminOrg[] {
458
+ return Array.isArray(this.orgs) ? this.orgs.filter(Boolean) : [];
459
+ }
460
+
461
+ private get currentOrg(): IIdpAdminOrg {
462
+ const orgs = this.safeOrgs;
463
+ return orgs.find((orgArg) => orgArg.id === this.selectedOrgId) || orgs[0] || {
464
+ id: '',
465
+ name: 'No organisation',
466
+ slug: '',
467
+ myRole: '',
468
+ };
469
+ }
470
+
471
+ private get userInitials(): string {
472
+ if (this.user?.initials) {
473
+ return this.user.initials;
474
+ }
475
+
476
+ const source = this.user?.name || this.user?.email || '?';
477
+ return source
478
+ .split(/\s+|@/)
479
+ .filter(Boolean)
480
+ .map((partArg) => partArg[0])
481
+ .slice(0, 2)
482
+ .join('')
483
+ .toUpperCase();
484
+ }
485
+
486
+ private dispatchSidebarEvent<TDetail>(eventNameArg: string, detailArg: TDetail) {
487
+ this.dispatchEvent(new CustomEvent<TDetail>(eventNameArg, {
488
+ detail: detailArg,
489
+ bubbles: true,
490
+ composed: true,
491
+ }));
492
+ }
493
+
494
+ private setPage(pageArg: TIdpAdminPage) {
495
+ this.page = pageArg;
496
+ this.orgMenuOpen = false;
497
+ this.userMenuOpen = false;
498
+ this.dispatchSidebarEvent<IIdpAdminNavigateEventDetail>('idp-admin-navigate', { page: pageArg });
499
+ }
500
+
501
+ private selectOrg(orgIdArg: string) {
502
+ this.selectedOrgId = orgIdArg;
503
+ this.orgMenuOpen = false;
504
+ this.page = 'org-general';
505
+ this.dispatchSidebarEvent<IIdpAdminOrgSelectEventDetail>('idp-admin-org-select', {
506
+ orgId: orgIdArg,
507
+ org: this.safeOrgs.find((orgArg) => orgArg.id === orgIdArg) || null,
508
+ });
509
+ }
510
+
511
+ private requestOrgCreate() {
512
+ this.orgMenuOpen = false;
513
+ this.dispatchSidebarEvent('idp-admin-org-create-request', {});
514
+ }
515
+
516
+ private requestLogout() {
517
+ this.userMenuOpen = false;
518
+ this.dispatchSidebarEvent('idp-admin-logout-request', {});
519
+ }
520
+
521
+ private switchAccount(accountArg: IIdpAdminUser) {
522
+ this.userMenuOpen = false;
523
+ this.dispatchSidebarEvent<IIdpAdminAccountSwitchEventDetail>('idp-admin-account-switch', { account: accountArg });
524
+ }
525
+
526
+ private requestAddAccount() {
527
+ this.userMenuOpen = false;
528
+ this.dispatchEvent(new CustomEvent('idp-admin-account-add', { bubbles: true, composed: true }));
529
+ }
530
+
531
+ private renderNavGroup(items: TNavItem[], active = ''): TemplateResult {
532
+ return html`
533
+ <div class="nav-list">
534
+ ${items.map((item) => html`
535
+ <button class="nav-item ${item.id === active ? 'active' : ''}" @click=${() => this.setPage(item.id)}>
536
+ <idp-icon name=${item.icon as any} size="14"></idp-icon>
537
+ ${item.label}
538
+ ${item.badge ? html`<idp-badge variant="accent" style="margin-left:auto">${item.badge}</idp-badge>` : html``}
539
+ </button>
540
+ `)}
541
+ </div>
542
+ `;
543
+ }
544
+
545
+ private renderUserMenu(): TemplateResult {
546
+ const otherAccounts = (this.accounts || []).filter((accountArg) => accountArg.email !== this.user?.email);
547
+ return html`
548
+ <div class="user-menu" role="menu">
549
+ <div class="user-menu-identity">
550
+ <idp-avatar name=${this.user?.name || this.user?.email || '?'} size="32"></idp-avatar>
551
+ <div class="user-meta"><div class="user-name">${this.user?.name || 'Unknown User'}</div><div class="user-email">${this.user?.email || 'No email'}</div></div>
552
+ ${this.globalAdmin ? html`<idp-badge variant="accent">Admin</idp-badge>` : html``}
553
+ </div>
554
+ <button class="user-menu-item" @click=${() => this.setPage('profile')}>
555
+ <idp-icon name="user" size="14"></idp-icon><span>Profile</span>
556
+ </button>
557
+ <button class="user-menu-item" @click=${() => this.setPage('security')}>
558
+ <idp-icon name="shield" size="14"></idp-icon><span>Security</span>
559
+ </button>
560
+ <button class="user-menu-item" @click=${() => this.setPage('sessions')}>
561
+ <idp-icon name="monitor" size="14"></idp-icon><span>Sessions &amp; Devices</span>
562
+ ${this.sessionCount ? html`<span class="user-menu-hint">${this.sessionCount} active</span>` : html``}
563
+ </button>
564
+ ${otherAccounts.length
565
+ ? html`
566
+ <div class="user-menu-divider"></div>
567
+ <div class="user-menu-title">Switch account</div>
568
+ ${otherAccounts.map(
569
+ (accountArg) => html`
570
+ <button class="user-menu-item account" @click=${() => this.switchAccount(accountArg)}>
571
+ <idp-avatar name=${accountArg.name || accountArg.email} size="22"></idp-avatar>
572
+ <div class="user-meta"><div class="user-name">${accountArg.name}</div><div class="user-email">${accountArg.email}</div></div>
573
+ <idp-icon name="lucide:ArrowLeftRight" size="12"></idp-icon>
574
+ </button>
575
+ `
576
+ )}
577
+ <button class="user-menu-item" @click=${() => this.requestAddAccount()}>
578
+ <idp-icon name="plus" size="14"></idp-icon><span>Add account</span>
579
+ </button>
580
+ `
581
+ : html``}
582
+ <div class="user-menu-divider"></div>
583
+ <button class="user-menu-item destructive" @click=${() => this.requestLogout()}>
584
+ <idp-icon name="logout" size="14"></idp-icon><span>Sign out</span>
585
+ </button>
586
+ </div>
587
+ `;
588
+ }
589
+
590
+ public render(): TemplateResult {
591
+ const currentOrg = this.currentOrg;
592
+ return html`
593
+ <aside class="sidebar">
594
+ <div class="logo-block">
595
+ <div class="logo">
596
+ <div class="logo-icon" aria-hidden="true">
597
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
598
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
599
+ </svg>
600
+ </div>
601
+ <span class="logo-text">idp.global</span>
602
+ </div>
603
+ </div>
604
+ <div class="nav-wrap">
605
+ <div class="nav-section"><div class="nav-title">Workspace</div>${this.renderNavGroup(this.workspaceNav, this.page)}</div>
606
+ <div class="nav-section"><div class="nav-title">My Account</div>${this.renderNavGroup(this.accountNav, this.page)}</div>
607
+ <div class="nav-section">
608
+ <div class="nav-title">Organisation</div>
609
+ <div class="org-switch-wrap">
610
+ <button class="org-switch ${this.orgMenuOpen ? 'open' : ''}" @click=${() => { this.orgMenuOpen = !this.orgMenuOpen; }}>
611
+ <span class="org-avatar">${currentOrg.name.slice(0, 2).toUpperCase()}</span>
612
+ <span class="org-name">${currentOrg.name}</span>
613
+ <idp-icon name="chevron-down" size="10"></idp-icon>
614
+ </button>
615
+ ${this.orgMenuOpen ? html`
616
+ <div class="org-menu">
617
+ <div class="org-menu-title">Switch organisation</div>
618
+ ${this.safeOrgs.map((orgArg) => html`
619
+ <button class="org-menu-item ${orgArg.id === this.selectedOrgId ? 'selected' : ''}" @click=${() => this.selectOrg(orgArg.id)}>
620
+ <span class="org-avatar">${orgArg.name.slice(0, 2).toUpperCase()}</span>
621
+ <span style="flex:1;min-width:0"><span class="org-menu-name">${orgArg.name}</span><span class="org-menu-role">${orgArg.myRole || 'member'}</span></span>
622
+ ${orgArg.id === this.selectedOrgId ? html`<idp-icon name="check" size="11"></idp-icon>` : html``}
623
+ </button>
624
+ `)}
625
+ <div class="org-menu-divider"></div>
626
+ <button class="org-create" @click=${() => this.requestOrgCreate()}><span class="org-create-icon"><idp-icon name="plus" size="9"></idp-icon></span>Create organisation</button>
627
+ </div>
628
+ ` : html``}
629
+ </div>
630
+ ${this.renderNavGroup(this.orgNav, this.page)}
631
+ </div>
632
+ <div class="nav-section"><div class="nav-title">Support</div>${this.renderNavGroup(this.supportNav, this.page)}</div>
633
+ ${this.globalAdmin ? html`<div class="nav-section"><div class="nav-title">Global Admin</div>${this.renderNavGroup(this.adminNav, this.page)}</div>` : html``}
634
+ </div>
635
+ <div class="user-footer">
636
+ <button class="user-trigger ${this.userMenuOpen ? 'open' : ''}" aria-haspopup="menu" aria-expanded=${this.userMenuOpen} @click=${() => { this.userMenuOpen = !this.userMenuOpen; }}>
637
+ <span class="user-avatar">${this.userInitials}</span>
638
+ <div class="user-meta"><div class="user-name">${this.user?.name || 'Unknown User'}</div><div class="user-email">${this.user?.email || 'No email'}</div></div>
639
+ <idp-icon name="lucide:ChevronsUpDown" size="13"></idp-icon>
640
+ </button>
641
+ ${this.userMenuOpen ? this.renderUserMenu() : html``}
642
+ </div>
643
+ </aside>
644
+ `;
645
+ }
646
+ }