@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
@@ -3,6 +3,13 @@ import { idpElementStyles } from './tokens.js';
3
3
  import './idp-badge.js';
4
4
  import './idp-icon.js';
5
5
  import './idp-data-table.js';
6
+ import './idp-avatar.js';
7
+ import './idp-toggle.js';
8
+ import './idp-delegation-section.js';
9
+ import './idp-admin-sidebar.js';
10
+ import './idp-admin-overview.js';
11
+ import { idpAdminSharedStyles, renderAdminKpi, renderAdminStateCard, formatAdminTimeAgo, type TIdpAdminKpi } from './idp-admin-shared.js';
12
+ import type { IIdpDelegation, IIdpDelegationApp } from './idp-delegation-section.js';
6
13
 
7
14
  declare global {
8
15
  interface HTMLElementTagNameMap {
@@ -10,25 +17,6 @@ declare global {
10
17
  }
11
18
  }
12
19
 
13
- type TNavItem = {
14
- id: TIdpAdminPage;
15
- label: string;
16
- icon: string;
17
- badge?: string;
18
- };
19
-
20
- type TKpi = {
21
- label: string;
22
- value: string;
23
- unit?: string;
24
- delta: string;
25
- deltaKind: 'up' | 'live';
26
- sub: string;
27
- accent: string;
28
- spark: number[];
29
- sparkColor: string;
30
- };
31
-
32
20
  export type TIdpAdminPage =
33
21
  | 'overview'
34
22
  | 'profile'
@@ -58,6 +46,10 @@ export interface IIdpAdminOrg {
58
46
  name: string;
59
47
  slug: string;
60
48
  myRole?: string;
49
+ plan?: string;
50
+ status?: string;
51
+ memberCount?: number;
52
+ createdAt?: number;
61
53
  }
62
54
 
63
55
  export interface IIdpAdminNavigateEventDetail {
@@ -69,6 +61,79 @@ export interface IIdpAdminOrgSelectEventDetail {
69
61
  org: IIdpAdminOrg | null;
70
62
  }
71
63
 
64
+ export interface IIdpAdminLogoutEventDetail {
65
+ everywhere: boolean;
66
+ }
67
+
68
+ export interface IIdpAdminAccountSwitchEventDetail {
69
+ account: IIdpAdminUser;
70
+ }
71
+
72
+ export interface IIdpAdminOrgCreateEventDetail {
73
+ name: string;
74
+ slug: string;
75
+ }
76
+
77
+ export interface IIdpAdminGaUser {
78
+ id: string;
79
+ name: string;
80
+ email: string;
81
+ status: string;
82
+ isGlobalAdmin: boolean;
83
+ orgCount: number;
84
+ createdAt: number;
85
+ }
86
+
87
+ export interface IIdpAdminGaUserActionEventDetail {
88
+ userId: string;
89
+ action: 'suspend' | 'unsuspend';
90
+ }
91
+
92
+ export interface IIdpAdminGaOrgActionEventDetail {
93
+ orgId: string;
94
+ action: 'suspend' | 'unsuspend';
95
+ }
96
+
97
+ export interface IIdpAdminGaAppActionEventDetail {
98
+ appId: string;
99
+ action: 'approve' | 'reject' | 'suspend' | 'reinstate';
100
+ }
101
+
102
+ export interface IIdpAdminOverviewSeriesPoint {
103
+ /** epoch-ms timestamp or Date-parseable string; plain clock times like '14:00' are not chartable */
104
+ x: string | number;
105
+ y: number;
106
+ }
107
+
108
+ export interface IIdpAdminOverviewThreat {
109
+ severity: 'error' | 'warning' | 'info';
110
+ title: string;
111
+ meta: string;
112
+ action?: string;
113
+ }
114
+
115
+ export interface IIdpAdminOverviewRegion {
116
+ name: string;
117
+ code: string;
118
+ count: number;
119
+ share: number;
120
+ kind?: 'home' | 'risk';
121
+ }
122
+
123
+ export interface IIdpAdminOverviewFeedItem {
124
+ tone: 'ok' | 'info' | 'warn' | 'error';
125
+ text: string;
126
+ meta: string;
127
+ }
128
+
129
+ export interface IIdpAdminOverviewData {
130
+ chartLabel?: string;
131
+ chartSeries?: { name: string; data: IIdpAdminOverviewSeriesPoint[] }[];
132
+ threats?: IIdpAdminOverviewThreat[];
133
+ regions?: IIdpAdminOverviewRegion[];
134
+ feed?: IIdpAdminOverviewFeedItem[];
135
+ }
136
+
72
137
  export interface IIdpAdminSession {
73
138
  id: string;
74
139
  deviceName: string;
@@ -274,11 +339,95 @@ export interface IIdpAdminPasskeyEventDetail {
274
339
 
275
340
  @customElement('idp-admin-shell')
276
341
  export class IdpAdminShell extends DeesElement {
277
- public static demo = () => html`<idp-admin-shell global-admin></idp-admin-shell>`;
278
- public static demoGroups = ['idp.global v3 full pages'];
342
+ public static demo = () => {
343
+ const now = Date.now();
344
+ const hours = Array.from({ length: 24 }, (unused, indexArg) => indexArg);
345
+ return html`
346
+ <idp-admin-shell
347
+ global-admin
348
+ .user=${{ name: 'Alex Mercer', email: 'alex@lossless.com' }}
349
+ .accounts=${[
350
+ { name: 'Alex Mercer', email: 'alex.mercer@gmail.com' },
351
+ ]}
352
+ .orgs=${[
353
+ { id: 'org_foss', name: 'Lossless GmbH', slug: 'lossless', myRole: 'owner', plan: 'Pro', status: 'active', memberCount: 5, createdAt: now - 86400000 * 200 },
354
+ { id: 'org_task', name: 'Task VC', slug: 'task', myRole: 'admin', plan: 'Pro', status: 'active', memberCount: 3, createdAt: now - 86400000 * 140 },
355
+ { id: 'org_sus', name: 'Suspended Co.', slug: 'suspended', myRole: 'viewer', plan: 'Pro', status: 'suspended', memberCount: 2, createdAt: now - 86400000 * 80 },
356
+ ]}
357
+ .sessions=${[
358
+ { id: 's1', deviceName: 'MacBook Pro', browser: 'Safari 18', os: 'macOS', ip: '84.132.10.4', lastActive: now - 120000, createdAt: now - 86400000 * 3, isCurrent: true },
359
+ { id: 's2', deviceName: 'iPhone 15 Pro', browser: 'Safari', os: 'iOS 18', ip: '84.132.10.4', lastActive: now - 3600000, createdAt: now - 86400000 * 10, isCurrent: false },
360
+ { id: 's3', deviceName: 'Work laptop', browser: 'Chrome 132', os: 'Windows 11', ip: '13.48.2.1', lastActive: now - 86400000, createdAt: now - 86400000 * 30, isCurrent: false },
361
+ ]}
362
+ .gaUsers=${[
363
+ { id: 'u01', name: 'Alex Mercer', email: 'alex@lossless.com', status: 'active', isGlobalAdmin: true, orgCount: 2, createdAt: now - 86400000 * 200 },
364
+ { id: 'u02', name: 'Jordan Kim', email: 'jordan@lossless.com', status: 'active', isGlobalAdmin: false, orgCount: 1, createdAt: now - 86400000 * 100 },
365
+ { id: 'u05', name: 'Dana Walsh', email: 'dana@suspended.de', status: 'suspended', isGlobalAdmin: false, orgCount: 0, createdAt: now - 86400000 * 60 },
366
+ { id: 'u06', name: 'Morgan Lee', email: 'morgan@newuser.com', status: 'new', isGlobalAdmin: false, orgCount: 0, createdAt: now - 86400000 },
367
+ ]}
368
+ .adminApps=${[
369
+ { id: 'app_foss', name: 'foss.global', type: 'global', category: 'Productivity', status: 'active' },
370
+ { id: 'app_p1', name: 'Acme HR Portal', type: 'partner', category: 'HR', connectionCount: 412, status: 'approved' },
371
+ { id: 'app_p2', name: 'DevOps Suite', type: 'partner', category: 'DevOps', connectionCount: 87, status: 'pending_review' },
372
+ ]}
373
+ .overviewData=${{
374
+ chartLabel: 'Approval activity',
375
+ chartSeries: [
376
+ { name: 'Approvals', data: hours.map((hourArg) => ({ x: now - (23 - hourArg) * 3600000, y: [22, 18, 14, 10, 8, 7, 9, 14, 22, 38, 52, 64, 71, 74, 70, 65, 58, 52, 45, 38, 32, 28, 25, 23][hourArg] })) },
377
+ { name: 'OAuth grants', data: hours.map((hourArg) => ({ x: now - (23 - hourArg) * 3600000, y: [16, 12, 9, 7, 6, 5, 7, 11, 18, 30, 42, 50, 56, 58, 56, 52, 47, 41, 36, 30, 26, 23, 20, 18][hourArg] })) },
378
+ ],
379
+ threats: [
380
+ { severity: 'error', title: 'Repeated denied logins', meta: '5 attempts · Lagos, NG · last 1h', action: 'Block IP' },
381
+ { severity: 'warning', title: 'Stale device key', meta: 'sam@ · Apple Watch · 9 mo old', action: 'Rotate' },
382
+ { severity: 'info', title: 'Pending key rotations', meta: '3 identities · Cardano awaiting', action: 'Run' },
383
+ ],
384
+ regions: [
385
+ { name: 'Berlin, DE', code: 'DE', count: 1284, share: 1, kind: 'home' },
386
+ { name: 'Amsterdam, NL', code: 'NL', count: 642, share: 0.5 },
387
+ { name: 'San Francisco, US', code: 'US', count: 521, share: 0.41 },
388
+ { name: 'Lagos, NG', code: 'NG', count: 12, share: 0.01, kind: 'risk' },
389
+ ],
390
+ feed: [
391
+ { tone: 'info', text: 'Identity created · did:idp:0x9b12…f034', meta: 'block #9 841 222' },
392
+ { tone: 'ok', text: 'Anchor confirmed · 12 blocks deep', meta: '2m' },
393
+ { tone: 'warn', text: 'Device registered · MacBook Pro · pending', meta: '7m' },
394
+ ],
395
+ }}
396
+ .delegationApps=${[
397
+ {
398
+ id: 'foss',
399
+ name: 'foss.global',
400
+ perms: [
401
+ { id: 'projects:read', label: 'View projects', hint: 'Read-only access to your projects' },
402
+ { id: 'projects:write', label: 'Manage projects', hint: 'Create, edit and archive projects' },
403
+ ],
404
+ },
405
+ {
406
+ id: 'task',
407
+ name: 'task.vc',
408
+ perms: [
409
+ { id: 'tasks:read', label: 'View tasks', hint: 'See your tasks and boards' },
410
+ { id: 'tasks:write', label: 'Act on tasks', hint: 'Create, assign and complete tasks as you' },
411
+ ],
412
+ },
413
+ ]}
414
+ .delegations=${[
415
+ {
416
+ id: 'd1',
417
+ account: { name: 'Jordan Kim', email: 'jordan@lossless.com' },
418
+ grants: { task: ['tasks:read', 'tasks:write'] },
419
+ expiresAt: now + 86400000 * 24,
420
+ createdAt: now - 86400000 * 6,
421
+ },
422
+ ]}
423
+ ></idp-admin-shell>
424
+ `;
425
+ };
426
+ public static demoGroups = ['idp.global v5 full pages'];
279
427
 
280
428
  public static styles = [
281
429
  ...idpElementStyles,
430
+ idpAdminSharedStyles,
282
431
  css`
283
432
  :host {
284
433
  display: block;
@@ -297,78 +446,9 @@ export class IdpAdminShell extends DeesElement {
297
446
  border: 0;
298
447
  background: var(--idp-bg);
299
448
  }
300
- .sidebar {
449
+ idp-admin-sidebar {
301
450
  height: 100%;
302
451
  min-height: 0;
303
- display: flex;
304
- flex-direction: column;
305
- overflow: hidden;
306
- border-right: 1px solid var(--idp-border);
307
- background: var(--idp-bg);
308
- }
309
- .logo-block {
310
- padding: 14px 16px 10px;
311
- border-bottom: 1px solid var(--idp-border);
312
- }
313
- .logo {
314
- display: flex;
315
- align-items: center;
316
- gap: 8px;
317
- }
318
- .logo-icon {
319
- width: 26px;
320
- height: 26px;
321
- display: flex;
322
- align-items: center;
323
- justify-content: center;
324
- flex-shrink: 0;
325
- border-radius: 7px;
326
- background: var(--idp-primary);
327
- color: var(--idp-primary-fg);
328
- }
329
- .logo-text {
330
- color: var(--idp-fg);
331
- font-size: 13px;
332
- font-weight: 700;
333
- letter-spacing: -0.3px;
334
- }
335
- .nav-wrap {
336
- flex: 1;
337
- min-height: 0;
338
- overflow: auto;
339
- padding: 10px 8px;
340
- }
341
- .state-card {
342
- padding: 42px 24px;
343
- border: 1px solid var(--idp-border);
344
- border-radius: var(--idp-radius);
345
- background: var(--idp-card);
346
- color: var(--idp-muted-fg);
347
- text-align: center;
348
- }
349
- .state-icon {
350
- width: 38px;
351
- height: 38px;
352
- display: inline-flex;
353
- align-items: center;
354
- justify-content: center;
355
- margin-bottom: 12px;
356
- border: 1px solid var(--idp-border);
357
- border-radius: 10px;
358
- background: var(--idp-muted);
359
- color: var(--idp-muted-fg);
360
- }
361
- .state-title {
362
- margin-bottom: 4px;
363
- color: var(--idp-fg);
364
- font-size: 14px;
365
- font-weight: 600;
366
- }
367
- .state-description {
368
- max-width: 420px;
369
- margin: 0 auto;
370
- font-size: 12px;
371
- line-height: 1.5;
372
452
  }
373
453
  .notice-card {
374
454
  display: flex;
@@ -383,219 +463,48 @@ export class IdpAdminShell extends DeesElement {
383
463
  .notice-card .muted {
384
464
  color: var(--idp-muted-fg);
385
465
  }
386
- .nav-section {
387
- margin-bottom: 20px;
388
- }
389
- .nav-title {
390
- padding: 0 8px;
391
- margin-bottom: 4px;
392
- color: var(--idp-muted-fg);
393
- font-size: 10px;
394
- font-weight: 600;
395
- letter-spacing: 0.8px;
396
- text-transform: uppercase;
397
- }
398
- .nav-list {
399
- display: grid;
400
- }
401
- .nav-item,
402
- .org-switch,
403
- .plain-button {
404
- border: 0;
405
- font-family: inherit;
406
- }
407
- .nav-item {
408
- width: 100%;
409
- display: flex;
410
- align-items: center;
411
- gap: 8px;
412
- padding: 6px 8px;
413
- border-radius: 7px;
414
- background: transparent;
415
- color: var(--idp-muted-fg);
416
- font-size: 13px;
417
- font-weight: 400;
418
- text-align: left;
419
- }
420
- .nav-item.active {
421
- background: var(--idp-muted);
422
- color: var(--idp-fg);
423
- font-weight: 500;
424
- }
425
- .nav-item idp-icon {
426
- color: currentColor;
427
- }
428
- .nav-item.active idp-icon {
429
- color: var(--idp-accent);
430
- }
431
- .org-switch-wrap {
432
- position: relative;
433
- margin-bottom: 2px;
434
- }
435
- .org-switch {
436
- width: 100%;
466
+ .logout-everywhere-row {
437
467
  display: flex;
438
468
  align-items: center;
439
- gap: 8px;
440
- padding: 6px 8px;
441
- border-radius: 7px;
442
- background: transparent;
443
- color: var(--idp-muted-fg);
444
- cursor: pointer;
445
- }
446
- .org-switch:hover {
447
- background: var(--idp-muted);
448
- }
449
- .org-switch.open {
450
- background: var(--idp-muted);
451
- }
452
- .org-switch.open .org-name {
453
- color: var(--idp-accent);
454
- }
455
- .org-switch.open idp-icon {
456
- transform: rotate(180deg);
457
- }
458
- .org-menu {
459
- position: absolute;
460
- top: calc(100% + 4px);
461
- left: 0;
462
- right: 0;
463
- z-index: 40;
464
- overflow: hidden;
469
+ gap: 10px;
470
+ margin-top: 14px;
471
+ padding: 10px 12px;
465
472
  border: 1px solid var(--idp-border);
466
- border-radius: 10px;
467
- background: var(--idp-card);
468
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
469
- }
470
- .org-menu-title {
471
- padding: 6px 8px 3px;
472
- color: var(--idp-muted-fg);
473
- font-size: 10px;
474
- font-weight: 600;
475
- letter-spacing: 0.7px;
476
- text-transform: uppercase;
477
- }
478
- .org-menu-item,
479
- .org-create {
480
- width: 100%;
481
- display: flex;
482
- align-items: center;
483
- gap: 8px;
484
- border: 0;
485
- background: transparent;
486
- font-family: inherit;
487
- text-align: left;
488
- }
489
- .org-menu-item {
490
- padding: 6px 8px;
491
- cursor: pointer;
492
- }
493
- .org-menu-item.selected {
494
- background: var(--idp-accent-soft);
495
- }
496
- .org-menu-item .org-avatar {
497
- width: 20px;
498
- height: 20px;
499
- font-size: 7px;
500
- }
501
- .org-menu-name {
502
- overflow: hidden;
473
+ border-radius: 9px;
474
+ font-size: 12.5px;
503
475
  color: var(--idp-fg);
504
- font-size: 12px;
505
- font-weight: 400;
506
- text-overflow: ellipsis;
507
- white-space: nowrap;
508
- }
509
- .org-menu-item.selected .org-menu-name {
510
- color: var(--idp-accent);
511
- }
512
- .org-menu-role {
513
- color: var(--idp-muted-fg);
514
- font-size: 10px;
515
- }
516
- .org-menu-divider {
517
- height: 1px;
518
- margin: 3px 0;
519
- background: var(--idp-border);
520
- }
521
- .org-create {
522
- padding: 6px 8px 8px;
523
- color: var(--idp-muted-fg);
524
476
  cursor: pointer;
525
- font-size: 12px;
526
477
  }
527
- .org-create-icon {
528
- width: 20px;
529
- height: 20px;
478
+ .toast {
479
+ position: fixed;
480
+ right: 20px;
481
+ bottom: 20px;
482
+ z-index: 60;
530
483
  display: flex;
531
484
  align-items: center;
532
- justify-content: center;
533
- flex-shrink: 0;
534
- border: 1.5px dashed var(--idp-border);
535
- border-radius: 5px;
536
- }
537
- .org-name {
538
- flex: 1;
539
- min-width: 0;
540
- overflow: hidden;
541
- color: var(--idp-muted-fg);
485
+ gap: 10px;
486
+ max-width: 320px;
487
+ padding: 10px 14px;
488
+ border-radius: 9px;
489
+ border: 1px solid;
490
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.07);
542
491
  font-size: 13px;
543
- font-weight: 400;
544
- text-align: left;
545
- text-overflow: ellipsis;
546
- white-space: nowrap;
547
- }
548
- .org-avatar,
549
- .user-avatar {
550
- display: flex;
551
- align-items: center;
552
- justify-content: center;
553
- flex-shrink: 0;
554
- font-family: var(--idp-mono);
555
- font-weight: 700;
556
- }
557
- .org-avatar {
558
- width: 14px;
559
- height: 14px;
560
- border: 1px solid var(--idp-border);
561
- border-radius: 6px;
562
- background: var(--idp-accent-soft);
563
- color: var(--idp-accent);
564
- font-size: 5px;
565
- }
566
- .user-footer {
567
- display: flex;
568
- align-items: center;
569
- gap: 9px;
570
- padding: 10px 12px;
571
- border-top: 1px solid var(--idp-border);
572
- }
573
- .user-avatar {
574
- width: 26px;
575
- height: 26px;
576
- border-radius: 50%;
577
- background: oklch(0.85 0.08 240);
578
- color: oklch(0.3 0.08 240);
579
- font-size: 9px;
580
- }
581
- .user-meta {
582
- flex: 1;
583
- min-width: 0;
492
+ font-weight: 500;
584
493
  }
585
- .user-name,
586
- .user-email {
587
- overflow: hidden;
588
- text-overflow: ellipsis;
589
- white-space: nowrap;
494
+ .toast.ok {
495
+ background: var(--idp-ok-bg);
496
+ border-color: var(--idp-ok-border);
497
+ color: var(--idp-ok);
590
498
  }
591
- .user-name {
592
- color: var(--idp-fg);
593
- font-size: 12px;
594
- font-weight: 500;
499
+ .toast.warn {
500
+ background: var(--idp-warn-bg);
501
+ border-color: var(--idp-warn-border);
502
+ color: var(--idp-warn);
595
503
  }
596
- .user-email {
597
- color: var(--idp-muted-fg);
598
- font-size: 10px;
504
+ .toast.error {
505
+ background: var(--idp-error-bg);
506
+ border-color: var(--idp-error-border);
507
+ color: var(--idp-error);
599
508
  }
600
509
  main {
601
510
  height: 100%;
@@ -604,146 +513,6 @@ export class IdpAdminShell extends DeesElement {
604
513
  overflow: auto;
605
514
  background: var(--idp-bg);
606
515
  }
607
- .page-head {
608
- display: flex;
609
- align-items: flex-end;
610
- justify-content: space-between;
611
- gap: 16px;
612
- flex-wrap: wrap;
613
- padding: 24px 28px 18px;
614
- border-bottom: 1px solid var(--idp-border);
615
- background: var(--idp-bg);
616
- }
617
- .eyebrow-row {
618
- display: flex;
619
- align-items: center;
620
- gap: 8px;
621
- margin-bottom: 4px;
622
- }
623
- .eyebrow {
624
- color: var(--idp-muted-fg);
625
- font-family: var(--idp-mono);
626
- font-size: 10px;
627
- font-weight: 600;
628
- letter-spacing: 0.08em;
629
- text-transform: uppercase;
630
- }
631
- .live-pill {
632
- display: inline-flex;
633
- align-items: center;
634
- gap: 4px;
635
- padding: 1px 7px;
636
- border: 1px solid var(--idp-ok-border);
637
- border-radius: 999px;
638
- background: var(--idp-ok-bg);
639
- color: var(--idp-ok);
640
- font-family: var(--idp-mono);
641
- font-size: 10px;
642
- font-weight: 500;
643
- }
644
- .live-dot,
645
- .feed-dot {
646
- width: 6px;
647
- height: 6px;
648
- border-radius: 50%;
649
- background: var(--idp-ok);
650
- box-shadow: 0 0 6px var(--idp-ok);
651
- animation: idp-blink 1.6s ease-in-out infinite;
652
- }
653
- @keyframes idp-blink {
654
- 0%, 100% { opacity: 1; }
655
- 50% { opacity: 0.35; }
656
- }
657
- h1,
658
- h2 {
659
- margin: 0;
660
- color: var(--idp-fg);
661
- font-family: var(--idp-display);
662
- letter-spacing: -0.025em;
663
- }
664
- h1 {
665
- font-size: 26px;
666
- font-weight: 700;
667
- line-height: 1.1;
668
- }
669
- .lead {
670
- margin-top: 4px;
671
- color: var(--idp-muted-fg);
672
- font-size: 13px;
673
- }
674
- .lead code,
675
- .mono {
676
- color: var(--idp-info);
677
- font-family: var(--idp-mono);
678
- }
679
- .page-actions {
680
- display: flex;
681
- align-items: center;
682
- gap: 12px;
683
- }
684
- .tabs {
685
- display: inline-flex;
686
- gap: 1px;
687
- padding: 2px;
688
- border: 1px solid var(--idp-border);
689
- border-radius: 6px;
690
- background: var(--idp-bg-2);
691
- }
692
- .tab {
693
- padding: 4px 10px;
694
- border-radius: 4px;
695
- color: var(--idp-muted-fg);
696
- font-family: var(--idp-mono);
697
- font-size: 11px;
698
- font-weight: 500;
699
- }
700
- .tab.active {
701
- background: var(--idp-muted);
702
- color: var(--idp-fg);
703
- }
704
- .plain-button {
705
- display: inline-flex;
706
- align-items: center;
707
- gap: 6px;
708
- height: 28px;
709
- padding: 5px 10px;
710
- border-radius: 8px;
711
- background: transparent;
712
- color: var(--idp-fg);
713
- font-size: 12px;
714
- font-weight: 500;
715
- white-space: nowrap;
716
- }
717
- .plain-button.outline {
718
- border: 1px solid var(--idp-border);
719
- }
720
- .plain-button.primary {
721
- background: var(--idp-accent);
722
- color: var(--idp-accent-fg);
723
- }
724
- .plain-button.destructive {
725
- background: var(--idp-destructive);
726
- color: var(--idp-accent-fg);
727
- }
728
- .plain-button.ghost {
729
- color: var(--idp-fg);
730
- }
731
- .plain-button:disabled {
732
- opacity: 0.45;
733
- cursor: not-allowed;
734
- }
735
- .body {
736
- display: flex;
737
- flex-direction: column;
738
- gap: 16px;
739
- padding: 20px 28px 32px;
740
- }
741
- .narrow-body {
742
- max-width: 700px;
743
- }
744
- .wide-body {
745
- max-width: 860px;
746
- }
747
516
  .section-card {
748
517
  margin-bottom: 16px;
749
518
  padding: 20px;
@@ -913,183 +682,54 @@ export class IdpAdminShell extends DeesElement {
913
682
  }
914
683
  .split-row {
915
684
  display: flex;
916
- align-items: center;
917
- justify-content: space-between;
918
- gap: 16px;
919
- }
920
- .chip-row {
921
- display: flex;
922
- flex-wrap: wrap;
923
- gap: 4px;
924
- }
925
- .switch {
926
- width: 36px;
927
- height: 20px;
928
- position: relative;
929
- flex-shrink: 0;
930
- border-radius: 10px;
931
- background: var(--idp-accent);
932
- }
933
- .switch::after {
934
- content: '';
935
- position: absolute;
936
- top: 2px;
937
- left: 18px;
938
- width: 16px;
939
- height: 16px;
940
- border-radius: 8px;
941
- background: #fff;
942
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
943
- }
944
- .sub-tabs {
945
- width: fit-content;
946
- display: flex;
947
- gap: 2px;
948
- margin-bottom: 16px;
949
- padding: 3px;
950
- border-radius: 8px;
951
- background: var(--idp-muted);
952
- }
953
- .sub-tab {
954
- padding: 5px 12px;
955
- border-radius: 6px;
956
- color: var(--idp-muted-fg);
957
- font-size: 12px;
958
- }
959
- .sub-tab.active {
960
- background: var(--idp-bg);
961
- color: var(--idp-fg);
962
- font-weight: 500;
963
- box-shadow: 0 1px 3px rgba(0,0,0,0.06);
964
- }
965
- .kpis {
966
- display: grid;
967
- grid-template-columns: repeat(4, 1fr);
968
- gap: 12px;
969
- }
970
- .kpi,
971
- .card {
972
- border: 1px solid var(--idp-border);
973
- background: var(--idp-card);
974
- }
975
- .kpi {
976
- position: relative;
977
- min-height: 132px;
978
- display: flex;
979
- flex-direction: column;
980
- gap: 4px;
981
- overflow: hidden;
982
- padding: 18px 20px 14px;
983
- border-radius: 10px;
984
- }
985
- .kpi::before {
986
- content: '';
987
- position: absolute;
988
- top: 0;
989
- left: 0;
990
- right: 0;
991
- height: 2px;
992
- background: var(--kpi-accent);
993
- }
994
- .kpi-label {
995
- color: var(--idp-muted-fg);
996
- font-size: 11.5px;
997
- font-weight: 500;
998
- letter-spacing: 0.02em;
999
- }
1000
- .kpi-value {
1001
- color: var(--idp-fg);
1002
- font-family: var(--idp-display);
1003
- font-size: 30px;
1004
- font-weight: 700;
1005
- font-variant-numeric: tabular-nums;
1006
- letter-spacing: -0.025em;
1007
- line-height: 1.1;
1008
- }
1009
- .kpi-value span {
1010
- margin-left: 2px;
1011
- color: var(--idp-muted-fg);
1012
- font-size: 14px;
1013
- font-weight: 500;
1014
- }
1015
- .kpi-sub {
1016
- color: var(--idp-fg-3);
1017
- font-family: var(--idp-mono);
1018
- font-size: 11px;
1019
- }
1020
- .kpi-foot {
1021
- display: flex;
1022
- align-items: flex-end;
685
+ align-items: center;
1023
686
  justify-content: space-between;
1024
- gap: 8px;
1025
- margin-top: auto;
687
+ gap: 16px;
1026
688
  }
1027
- .delta {
1028
- display: inline-flex;
1029
- align-items: center;
689
+ .chip-row {
690
+ display: flex;
691
+ flex-wrap: wrap;
1030
692
  gap: 4px;
1031
- color: var(--idp-ok);
1032
- font-family: var(--idp-mono);
1033
- font-size: 11px;
1034
- font-weight: 600;
1035
693
  }
1036
- .sparkline {
1037
- width: 84px;
1038
- opacity: 0.85;
1039
- }
1040
- .primary-grid,
1041
- .secondary-grid {
1042
- display: grid;
1043
- grid-template-columns: minmax(0, 1.6fr) minmax(300px, 1fr);
1044
- gap: 16px;
694
+ .switch {
695
+ width: 36px;
696
+ height: 20px;
697
+ position: relative;
698
+ flex-shrink: 0;
699
+ border-radius: 10px;
700
+ background: var(--idp-accent);
1045
701
  }
1046
- .card {
1047
- overflow: hidden;
702
+ .switch::after {
703
+ content: '';
704
+ position: absolute;
705
+ top: 2px;
706
+ left: 18px;
707
+ width: 16px;
708
+ height: 16px;
1048
709
  border-radius: 8px;
710
+ background: #fff;
711
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
1049
712
  }
1050
- .card-head {
713
+ .sub-tabs {
714
+ width: fit-content;
1051
715
  display: flex;
1052
- align-items: center;
1053
- gap: 10px;
1054
- padding: 12px 16px;
1055
- border-bottom: 1px solid var(--idp-border-soft);
1056
- }
1057
- .card-title {
1058
- color: var(--idp-fg);
1059
- font-size: 13px;
1060
- font-weight: 600;
1061
- }
1062
- .feed-dot {
1063
- display: inline-block;
1064
- width: 8px;
1065
- height: 8px;
1066
- border-radius: 4px;
1067
- background: var(--dot-color);
1068
- box-shadow: var(--dot-glow, none);
716
+ gap: 2px;
717
+ margin-bottom: 16px;
718
+ padding: 3px;
719
+ border-radius: 8px;
720
+ background: var(--idp-muted);
1069
721
  }
1070
- .feed-meta {
722
+ .sub-tab {
723
+ padding: 5px 12px;
724
+ border-radius: 6px;
1071
725
  color: var(--idp-muted-fg);
1072
- font-family: var(--idp-mono);
1073
- font-size: 11.5px;
1074
- }
1075
- .feed-item {
1076
- display: grid;
1077
- grid-template-columns: 14px 1fr auto;
1078
- gap: 12px;
1079
- align-items: center;
1080
- padding: 11px 16px;
1081
- border-bottom: 1px solid var(--idp-border-soft);
1082
- }
1083
- .feed-item:last-child {
1084
- border-bottom: 0;
1085
- }
1086
- .feed-text {
1087
- color: var(--idp-fg-2);
1088
- font-size: 12.5px;
726
+ font-size: 12px;
1089
727
  }
1090
- .feed-text strong {
728
+ .sub-tab.active {
729
+ background: var(--idp-bg);
1091
730
  color: var(--idp-fg);
1092
- font-weight: 600;
731
+ font-weight: 500;
732
+ box-shadow: 0 1px 3px rgba(0,0,0,0.06);
1093
733
  }
1094
734
  .dialog-backdrop {
1095
735
  position: fixed;
@@ -1190,29 +830,11 @@ export class IdpAdminShell extends DeesElement {
1190
830
  .shell {
1191
831
  grid-template-columns: 1fr;
1192
832
  }
1193
- .sidebar {
833
+ idp-admin-sidebar {
1194
834
  display: none;
1195
835
  }
1196
- .kpis {
1197
- grid-template-columns: repeat(2, 1fr);
1198
- }
1199
- .primary-grid,
1200
- .secondary-grid {
1201
- grid-template-columns: 1fr;
1202
- }
1203
836
  }
1204
837
  @media (max-width: 720px) {
1205
- .page-head,
1206
- .body {
1207
- padding-left: 20px;
1208
- padding-right: 20px;
1209
- }
1210
- .page-actions {
1211
- flex-wrap: wrap;
1212
- }
1213
- .kpis {
1214
- grid-template-columns: 1fr;
1215
- }
1216
838
  .mapping-row {
1217
839
  grid-template-columns: 1fr;
1218
840
  }
@@ -1289,8 +911,37 @@ export class IdpAdminShell extends DeesElement {
1289
911
  @property({ type: String, attribute: 'credential-message' })
1290
912
  public accessor credentialMessage = '';
1291
913
 
914
+ @property({ type: Array })
915
+ public accessor accounts: IIdpAdminUser[] = [];
916
+
917
+ @property({ type: Array })
918
+ public accessor gaUsers: IIdpAdminGaUser[] = [];
919
+
920
+ @property({ type: Object })
921
+ public accessor overviewData: IIdpAdminOverviewData | null = null;
922
+
923
+ @property({ type: Array })
924
+ public accessor delegationApps: IIdpDelegationApp[] = [];
925
+
926
+ @property({ type: Array })
927
+ public accessor delegations: IIdpDelegation[] = [];
928
+
929
+ @state()
930
+ private accessor logoutEverywhere = false;
931
+
932
+ @state()
933
+ private accessor createOrgName = '';
934
+
935
+ @state()
936
+ private accessor createOrgSlug = '';
937
+
938
+ @state()
939
+ private accessor gaUserSearch = '';
940
+
1292
941
  @state()
1293
- private accessor orgMenuOpen = false;
942
+ private accessor toast: { message: string; tone: 'ok' | 'warn' | 'error' } | null = null;
943
+
944
+ private toastTimer: ReturnType<typeof setTimeout> | null = null;
1294
945
 
1295
946
  @state()
1296
947
  private accessor currentPassword = '';
@@ -1329,7 +980,7 @@ export class IdpAdminShell extends DeesElement {
1329
980
  private accessor orgSettingsError = '';
1330
981
 
1331
982
  @state()
1332
- private accessor dialogMode: 'none' | 'role-upsert' | 'role-delete' | 'member-roles' | 'app-role-mappings' = 'none';
983
+ private accessor dialogMode: 'none' | 'role-upsert' | 'role-delete' | 'member-roles' | 'app-role-mappings' | 'create-org' | 'logout-confirm' = 'none';
1333
984
 
1334
985
  @state()
1335
986
  private accessor dialogError = '';
@@ -1358,34 +1009,6 @@ export class IdpAdminShell extends DeesElement {
1358
1009
  @state()
1359
1010
  private accessor dialogAppMappings: IIdpAdminAppRoleMapping[] = [];
1360
1011
 
1361
- private workspaceNav: TNavItem[] = [
1362
- { id: 'overview', label: 'Overview', icon: 'grid' },
1363
- ];
1364
-
1365
- private accountNav: TNavItem[] = [
1366
- { id: 'profile', label: 'Profile', icon: 'user' },
1367
- { id: 'security', label: 'Security', icon: 'shield' },
1368
- { id: 'sessions', label: 'Sessions & Devices', icon: 'monitor' },
1369
- { id: 'apps', label: 'Connected Apps', icon: 'grid' },
1370
- ];
1371
-
1372
- private orgNav: TNavItem[] = [
1373
- { id: 'org-general', label: 'General', icon: 'building' },
1374
- { id: 'org-settings', label: 'Settings', icon: 'settings' },
1375
- { id: 'org-members', label: 'Members', icon: 'users' },
1376
- { id: 'org-apps', label: 'OAuth Apps', icon: 'box' },
1377
- ];
1378
-
1379
- private supportNav: TNavItem[] = [
1380
- { id: 'support', label: 'Support', icon: 'shield' },
1381
- ];
1382
-
1383
- private adminNav: TNavItem[] = [
1384
- { id: 'ga-users', label: 'All Users', icon: 'users' },
1385
- { id: 'ga-orgs', label: 'All Organisations', icon: 'building' },
1386
- { id: 'ga-apps', label: 'Platform Apps', icon: 'globe' },
1387
- ];
1388
-
1389
1012
  private get safeOrgs(): IIdpAdminOrg[] {
1390
1013
  return Array.isArray(this.orgs) ? this.orgs.filter(Boolean) : [];
1391
1014
  }
@@ -1617,7 +1240,6 @@ export class IdpAdminShell extends DeesElement {
1617
1240
 
1618
1241
  private setPage(pageArg: TIdpAdminPage) {
1619
1242
  this.page = pageArg;
1620
- this.orgMenuOpen = false;
1621
1243
  this.dispatchEvent(new CustomEvent<IIdpAdminNavigateEventDetail>('idp-admin-navigate', {
1622
1244
  detail: { page: pageArg },
1623
1245
  bubbles: true,
@@ -1625,26 +1247,76 @@ export class IdpAdminShell extends DeesElement {
1625
1247
  }));
1626
1248
  }
1627
1249
 
1628
- private selectOrg(orgIdArg: string) {
1629
- this.selectedOrgId = orgIdArg;
1630
- this.orgMenuOpen = false;
1250
+ private handleSidebarNavigate(eventArg: CustomEvent<IIdpAdminNavigateEventDetail>) {
1251
+ this.page = eventArg.detail.page;
1252
+ }
1253
+
1254
+ private handleSidebarOrgSelect(eventArg: CustomEvent<IIdpAdminOrgSelectEventDetail>) {
1255
+ this.selectedOrgId = eventArg.detail.orgId;
1631
1256
  this.page = 'org-general';
1632
- this.dispatchEvent(new CustomEvent<IIdpAdminOrgSelectEventDetail>('idp-admin-org-select', {
1633
- detail: {
1634
- orgId: orgIdArg,
1635
- org: this.orgs.find((orgArg) => orgArg.id === orgIdArg) || null,
1636
- },
1637
- bubbles: true,
1638
- composed: true,
1639
- }));
1640
1257
  }
1641
1258
 
1642
1259
  private requestOrgCreate() {
1643
- this.orgMenuOpen = false;
1644
- this.dispatchEvent(new CustomEvent('idp-admin-org-create', {
1645
- bubbles: true,
1646
- composed: true,
1647
- }));
1260
+ this.createOrgName = '';
1261
+ this.createOrgSlug = '';
1262
+ this.dialogError = '';
1263
+ this.dialogMode = 'create-org';
1264
+ }
1265
+
1266
+ private slugify(valueArg: string): string {
1267
+ return valueArg
1268
+ .toLowerCase()
1269
+ .replace(/[^a-z0-9]/g, '-')
1270
+ .replace(/-+/g, '-')
1271
+ .replace(/^-|-$/g, '');
1272
+ }
1273
+
1274
+ private setCreateOrgName(eventArg: Event) {
1275
+ const value = (eventArg.target as HTMLInputElement).value;
1276
+ this.createOrgName = value;
1277
+ this.createOrgSlug = this.slugify(value);
1278
+ }
1279
+
1280
+ private submitCreateOrg() {
1281
+ if (!this.createOrgName.trim()) {
1282
+ this.dialogError = 'Enter an organisation name.';
1283
+ return;
1284
+ }
1285
+ this.dispatchShellEvent<IIdpAdminOrgCreateEventDetail>('idp-admin-org-create', {
1286
+ name: this.createOrgName.trim(),
1287
+ slug: this.createOrgSlug || this.slugify(this.createOrgName),
1288
+ });
1289
+ this.closeDialog();
1290
+ }
1291
+
1292
+ private requestLogout() {
1293
+ this.logoutEverywhere = false;
1294
+ this.dialogMode = 'logout-confirm';
1295
+ }
1296
+
1297
+ private confirmLogout() {
1298
+ const everywhere = this.logoutEverywhere;
1299
+ this.closeDialog();
1300
+ this.dispatchShellEvent<IIdpAdminLogoutEventDetail>('idp-admin-logout', { everywhere });
1301
+ }
1302
+
1303
+ public showToast(messageArg: string, toneArg: 'ok' | 'warn' | 'error' = 'ok') {
1304
+ this.toast = { message: messageArg, tone: toneArg };
1305
+ if (this.toastTimer) {
1306
+ clearTimeout(this.toastTimer);
1307
+ }
1308
+ this.toastTimer = setTimeout(() => {
1309
+ this.toast = null;
1310
+ this.toastTimer = null;
1311
+ }, 3000);
1312
+ }
1313
+
1314
+ public override async disconnectedCallback() {
1315
+ if (this.toastTimer) {
1316
+ clearTimeout(this.toastTimer);
1317
+ this.toastTimer = null;
1318
+ }
1319
+ await super.disconnectedCallback();
1648
1320
  }
1649
1321
 
1650
1322
  private dispatchShellEvent<TDetail>(eventNameArg: string, detailArg: TDetail) {
@@ -1656,30 +1328,11 @@ export class IdpAdminShell extends DeesElement {
1656
1328
  }
1657
1329
 
1658
1330
  private formatTimeAgo(timestampArg?: number): string {
1659
- if (!timestampArg) {
1660
- return 'unknown';
1661
- }
1662
-
1663
- const diff = Date.now() - timestampArg;
1664
- const minutes = Math.floor(diff / 60000);
1665
- const hours = Math.floor(diff / 3600000);
1666
- const days = Math.floor(diff / 86400000);
1667
-
1668
- if (minutes < 1) return 'just now';
1669
- if (minutes < 60) return `${minutes}m ago`;
1670
- if (hours < 24) return `${hours}h ago`;
1671
- if (days < 7) return `${days}d ago`;
1672
- return new Date(timestampArg).toLocaleDateString();
1331
+ return formatAdminTimeAgo(timestampArg);
1673
1332
  }
1674
1333
 
1675
1334
  private renderStateCard(titleArg: string, descriptionArg: string, iconArg = 'box'): TemplateResult {
1676
- return html`
1677
- <div class="state-card">
1678
- <span class="state-icon"><idp-icon name=${iconArg as any} size="17"></idp-icon></span>
1679
- <div class="state-title">${titleArg}</div>
1680
- <div class="state-description">${descriptionArg}</div>
1681
- </div>
1682
- `;
1335
+ return renderAdminStateCard(titleArg, descriptionArg, iconArg);
1683
1336
  }
1684
1337
 
1685
1338
  private renderDataState(emptyTitleArg: string, emptyDescriptionArg: string, iconArg = 'box'): TemplateResult | null {
@@ -1995,121 +1648,66 @@ export class IdpAdminShell extends DeesElement {
1995
1648
  `;
1996
1649
  }
1997
1650
 
1998
- private renderDialog(): TemplateResult {
1999
- if (this.dialogMode === 'none') {
2000
- return html``;
2001
- }
2002
-
2003
- const dialog = this.dialogMode === 'role-upsert'
2004
- ? this.renderRoleUpsertDialog()
2005
- : this.dialogMode === 'role-delete'
2006
- ? this.renderRoleDeleteDialog()
2007
- : this.dialogMode === 'member-roles'
2008
- ? this.renderMemberRolesDialog()
2009
- : this.renderAppRoleMappingsDialog();
2010
- return html`<div class="dialog-backdrop" @click=${() => this.closeDialog()}>${dialog}</div>`;
2011
- }
2012
-
2013
- private renderNavGroup(items: TNavItem[], active = ''): TemplateResult {
1651
+ private renderCreateOrgDialog(): TemplateResult {
2014
1652
  return html`
2015
- <div class="nav-list">
2016
- ${items.map((item) => html`
2017
- <button class="nav-item ${item.id === active ? 'active' : ''}" @click=${() => this.setPage(item.id)}>
2018
- <idp-icon name=${item.icon as any} size="14"></idp-icon>
2019
- ${item.label}
2020
- ${item.badge ? html`<idp-badge variant="accent" style="margin-left:auto">${item.badge}</idp-badge>` : html``}
2021
- </button>
2022
- `)}
1653
+ <div class="dialog-card" @click=${(eventArg: Event) => eventArg.stopPropagation()}>
1654
+ <div class="dialog-head">
1655
+ <div><div class="dialog-title">Create organisation</div><div class="muted">Organisations group members, roles, and OAuth apps.</div></div>
1656
+ <button class="plain-button ghost" @click=${() => this.closeDialog()}>Close</button>
1657
+ </div>
1658
+ <div class="dialog-body">
1659
+ ${this.renderDialogError()}
1660
+ ${this.renderFormRow('Organisation name', 'Shown to members and in app consent screens.', html`<input class="input" placeholder="Acme Corp" .value=${this.createOrgName} @input=${(eventArg: Event) => this.setCreateOrgName(eventArg)} />`, true)}
1661
+ ${this.renderFormRow('URL slug', 'Public identifier under idp.global/', html`<input class="input" placeholder="acme-corp" .value=${this.createOrgSlug} @input=${(eventArg: Event) => { this.createOrgSlug = this.slugify((eventArg.target as HTMLInputElement).value); }} />`)}
1662
+ </div>
1663
+ <div class="dialog-actions">
1664
+ <button class="plain-button outline" @click=${() => this.closeDialog()}>Cancel</button>
1665
+ <button class="plain-button primary" ?disabled=${!this.createOrgName.trim()} @click=${() => this.submitCreateOrg()}>Create</button>
1666
+ </div>
2023
1667
  </div>
2024
1668
  `;
2025
1669
  }
2026
1670
 
2027
- private renderSidebar(): TemplateResult {
2028
- const currentOrg = this.currentOrg;
1671
+ private renderLogoutConfirmDialog(): TemplateResult {
1672
+ const otherSessionCount = this.sessions.filter((sessionArg) => !sessionArg.isCurrent).length;
2029
1673
  return html`
2030
- <aside class="sidebar">
2031
- <div class="logo-block">
2032
- <div class="logo">
2033
- <div class="logo-icon" aria-hidden="true">
2034
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2035
- <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
2036
- </svg>
2037
- </div>
2038
- <span class="logo-text">idp.global</span>
2039
- </div>
1674
+ <div class="dialog-card" @click=${(eventArg: Event) => eventArg.stopPropagation()}>
1675
+ <div class="dialog-head">
1676
+ <div><div class="dialog-title">Sign out?</div></div>
1677
+ <button class="plain-button ghost" @click=${() => this.closeDialog()}>Close</button>
2040
1678
  </div>
2041
- <div class="nav-wrap">
2042
- <div class="nav-section"><div class="nav-title">Workspace</div>${this.renderNavGroup(this.workspaceNav, this.page)}</div>
2043
- <div class="nav-section"><div class="nav-title">My Account</div>${this.renderNavGroup(this.accountNav, this.page)}</div>
2044
- <div class="nav-section">
2045
- <div class="nav-title">Organisation</div>
2046
- <div class="org-switch-wrap">
2047
- <button class="org-switch ${this.orgMenuOpen ? 'open' : ''}" @click=${() => { this.orgMenuOpen = !this.orgMenuOpen; }}>
2048
- <span class="org-avatar">${currentOrg.name.slice(0, 2).toUpperCase()}</span>
2049
- <span class="org-name">${currentOrg.name}</span>
2050
- <idp-icon name="chevron-down" size="10"></idp-icon>
2051
- </button>
2052
- ${this.orgMenuOpen ? html`
2053
- <div class="org-menu">
2054
- <div class="org-menu-title">Switch organisation</div>
2055
- ${this.safeOrgs.map((orgArg) => html`
2056
- <button class="org-menu-item ${orgArg.id === this.selectedOrgId ? 'selected' : ''}" @click=${() => this.selectOrg(orgArg.id)}>
2057
- <span class="org-avatar">${orgArg.name.slice(0, 2).toUpperCase()}</span>
2058
- <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>
2059
- ${orgArg.id === this.selectedOrgId ? html`<idp-icon name="check" size="11"></idp-icon>` : html``}
2060
- </button>
2061
- `)}
2062
- <div class="org-menu-divider"></div>
2063
- <button class="org-create" @click=${() => this.requestOrgCreate()}><span class="org-create-icon"><idp-icon name="plus" size="9"></idp-icon></span>Create organisation</button>
2064
- </div>
2065
- ` : html``}
2066
- </div>
2067
- ${this.renderNavGroup(this.orgNav, this.page)}
2068
- </div>
2069
- <div class="nav-section"><div class="nav-title">Support</div>${this.renderNavGroup(this.supportNav, this.page)}</div>
2070
- ${this.globalAdmin ? html`<div class="nav-section"><div class="nav-title">Global Admin</div>${this.renderNavGroup(this.adminNav, this.page)}</div>` : html``}
1679
+ <div class="dialog-body">
1680
+ <div class="muted">You'll be signed out of idp.global on this device. Apps you've signed into with idp.global keep their own sessions.</div>
1681
+ <label class="logout-everywhere-row">
1682
+ <idp-toggle .checked=${this.logoutEverywhere} @idp-toggle-change=${(eventArg: CustomEvent) => { this.logoutEverywhere = !!eventArg.detail?.checked; }}></idp-toggle>
1683
+ <span>Also sign out of all other devices${otherSessionCount ? html` <span class="muted">(${otherSessionCount} other ${otherSessionCount === 1 ? 'session' : 'sessions'})</span>` : html``}</span>
1684
+ </label>
2071
1685
  </div>
2072
- <div class="user-footer">
2073
- <span class="user-avatar">${this.userInitials}</span>
2074
- <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>
1686
+ <div class="dialog-actions">
1687
+ <button class="plain-button outline" @click=${() => this.closeDialog()}>Cancel</button>
1688
+ <button class="plain-button destructive" @click=${() => this.confirmLogout()}>${this.logoutEverywhere ? 'Sign out everywhere' : 'Sign out'}</button>
2075
1689
  </div>
2076
- </aside>
1690
+ </div>
2077
1691
  `;
2078
1692
  }
2079
1693
 
2080
- private renderSparkline(data: number[], color: string): TemplateResult {
2081
- const max = Math.max(...data);
2082
- const min = Math.min(...data);
2083
- const range = max - min || 1;
2084
- const width = 100;
2085
- const height = 22;
2086
- const points = data.map((valueArg, indexArg) => {
2087
- const x = (indexArg / (data.length - 1)) * width;
2088
- const y = height - ((valueArg - min) / range) * (height - 4) - 2;
2089
- return `${x},${y}`;
2090
- }).join(' ');
2091
- const area = `0,${height} ${points} ${width},${height}`;
2092
-
2093
- return html`
2094
- <svg viewBox="0 0 ${width} ${height}" preserveAspectRatio="none">
2095
- <polygon points=${area} fill=${color} opacity="0.12"></polygon>
2096
- <polyline points=${points} fill="none" stroke=${color} stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></polyline>
2097
- </svg>
2098
- `;
2099
- }
1694
+ private renderDialog(): TemplateResult {
1695
+ if (this.dialogMode === 'none') {
1696
+ return html``;
1697
+ }
2100
1698
 
2101
- private renderKpi(kpi: TKpi): TemplateResult {
2102
- return html`
2103
- <div class="kpi" style="--kpi-accent:${kpi.accent}">
2104
- <div class="kpi-label">${kpi.label}</div>
2105
- <div class="kpi-value">${kpi.value}${kpi.unit ? html`<span>${kpi.unit}</span>` : html``}</div>
2106
- <div class="kpi-sub">${kpi.sub}</div>
2107
- <div class="kpi-foot">
2108
- <span class="delta">${kpi.deltaKind === 'live' ? html`<span class="live-dot"></span>` : html``}${kpi.delta}</span>
2109
- <div class="sparkline">${this.renderSparkline(kpi.spark, kpi.sparkColor)}</div>
2110
- </div>
2111
- </div>
2112
- `;
1699
+ const dialog = this.dialogMode === 'role-upsert'
1700
+ ? this.renderRoleUpsertDialog()
1701
+ : this.dialogMode === 'role-delete'
1702
+ ? this.renderRoleDeleteDialog()
1703
+ : this.dialogMode === 'member-roles'
1704
+ ? this.renderMemberRolesDialog()
1705
+ : this.dialogMode === 'create-org'
1706
+ ? this.renderCreateOrgDialog()
1707
+ : this.dialogMode === 'logout-confirm'
1708
+ ? this.renderLogoutConfirmDialog()
1709
+ : this.renderAppRoleMappingsDialog();
1710
+ return html`<div class="dialog-backdrop" @click=${() => this.closeDialog()}>${dialog}</div>`;
2113
1711
  }
2114
1712
 
2115
1713
  private renderPageHeader(titleArg: string, descriptionArg: string, actionArg?: TemplateResult): TemplateResult {
@@ -2149,78 +1747,17 @@ export class IdpAdminShell extends DeesElement {
2149
1747
  }
2150
1748
 
2151
1749
  private renderOverview(): TemplateResult {
2152
- const firstName = this.user?.name?.split(' ')[0] || 'there';
2153
- const activeSessionCount = this.sessions.filter((sessionArg) => sessionArg.isCurrent).length || this.sessions.length;
2154
- const activePassportCount = this.passportDevices.filter((deviceArg) => deviceArg.status === 'active').length;
2155
- const kpis: TKpi[] = [
2156
- {
2157
- label: 'Organisations',
2158
- value: String(this.orgs.length),
2159
- delta: 'live',
2160
- deltaKind: 'live',
2161
- spark: [1, 1, 1, 1, 1, 1, 1, Math.max(this.orgs.length, 1)],
2162
- sparkColor: 'var(--idp-spark-info)',
2163
- accent: 'var(--idp-chart-1)',
2164
- sub: 'Memberships visible to this account',
2165
- },
2166
- {
2167
- label: 'Sessions',
2168
- value: String(this.sessions.length),
2169
- delta: `${activeSessionCount} active`,
2170
- deltaKind: 'up',
2171
- spark: [1, 2, 1, 2, 3, 2, Math.max(this.sessions.length, 1)],
2172
- sparkColor: 'var(--idp-spark-up)',
2173
- accent: 'var(--idp-chart-2)',
2174
- sub: this.sessions.length ? `Latest ${this.formatTimeAgo(Math.max(...this.sessions.map((sessionArg) => sessionArg.lastActive)))}` : 'No active sessions loaded',
2175
- },
2176
- {
2177
- label: 'Passport devices',
2178
- value: String(this.passportDevices.length),
2179
- delta: `${activePassportCount} active`,
2180
- deltaKind: 'live',
2181
- spark: [1, 1, 1, Math.max(this.passportDevices.length, 1)],
2182
- sparkColor: 'var(--idp-spark-info)',
2183
- accent: 'var(--idp-chart-5)',
2184
- sub: 'Cryptographic credentials',
2185
- },
2186
- {
2187
- label: 'Activity',
2188
- value: String(this.activities.length),
2189
- delta: 'live',
2190
- deltaKind: 'live',
2191
- spark: [1, 2, 2, 3, Math.max(this.activities.length, 1)],
2192
- sparkColor: 'var(--idp-spark-info)',
2193
- accent: 'var(--idp-info)',
2194
- sub: 'Recent account events',
2195
- },
2196
- ];
2197
1750
  return html`
2198
- <header class="page-head">
2199
- <div>
2200
- <div class="eyebrow-row"><span class="eyebrow">Account - Overview</span><span class="live-pill"><span class="live-dot"></span>live</span></div>
2201
- <h1>Good morning, ${firstName}.</h1>
2202
- <div class="lead">Account-wide operational snapshot using live identity data.</div>
2203
- </div>
2204
- <div class="page-actions"><button class="plain-button outline" @click=${() => this.setPage('sessions')}>Review sessions</button><button class="plain-button primary" @click=${() => this.setPage('security')}>Manage security</button></div>
2205
- </header>
2206
- <div class="body">
2207
- <div class="kpis">${kpis.map((kpiArg) => this.renderKpi(kpiArg))}</div>
2208
- ${this.dataLoading || this.dataError ? this.renderDataState('No overview data', 'The console has not received live account data yet.', 'activity') : html``}
2209
- <div class="primary-grid">
2210
- <div class="card">
2211
- <div class="card-head"><span class="card-title">Recent activity</span><idp-badge>${this.activities.length} events</idp-badge></div>
2212
- ${this.activities.length ? this.activities.slice(0, 8).map((activityArg) => html`
2213
- <div class="feed-item"><span class="feed-dot" style="--dot-color:var(--idp-info)"></span><div class="feed-text"><strong>${activityArg.action.replace(/_/g, ' ')}</strong> - ${activityArg.description}</div><span class="feed-meta">${this.formatTimeAgo(activityArg.timestamp)}</span></div>
2214
- `) : this.renderStateCard('No activity yet', 'Activity events will appear here after logins, app changes, and organisation updates.', 'activity')}
2215
- </div>
2216
- <div class="card">
2217
- <div class="card-head"><span class="card-title">Current sessions</span><idp-badge>${this.sessions.length} total</idp-badge></div>
2218
- ${this.sessions.length ? this.sessions.slice(0, 5).map((sessionArg) => html`
2219
- <div class="feed-item"><span class="feed-dot" style="--dot-color:${sessionArg.isCurrent ? 'var(--idp-ok)' : 'var(--idp-muted-fg)'}"></span><div class="feed-text"><strong>${sessionArg.deviceName || 'Unknown device'}</strong> - <span class="mono">${sessionArg.browser} ${sessionArg.os}</span></div><span class="feed-meta">${this.formatTimeAgo(sessionArg.lastActive)}</span></div>
2220
- `) : this.renderStateCard('No sessions loaded', 'Active session telemetry is unavailable or there are no sessions.', 'monitor')}
2221
- </div>
2222
- </div>
2223
- </div>
1751
+ <idp-admin-overview
1752
+ .user=${this.user}
1753
+ .orgs=${this.orgs}
1754
+ .sessions=${this.sessions}
1755
+ .activities=${this.activities}
1756
+ .passportDevices=${this.passportDevices}
1757
+ .dataLoading=${this.dataLoading}
1758
+ .dataError=${this.dataError}
1759
+ .overviewData=${this.overviewData}
1760
+ ></idp-admin-overview>
2224
1761
  `;
2225
1762
  }
2226
1763
 
@@ -2238,6 +1775,9 @@ export class IdpAdminShell extends DeesElement {
2238
1775
  ${this.renderFormRow('Mobile number', 'Used for SMS verification', html`<input class="input" .value=${this.user?.mobileNumber || ''} disabled />`)}
2239
1776
  `)}
2240
1777
  ${this.renderSectionCard('Account status', '', html`<div class="split-row"><div><div class="section-title">Status</div><div class="muted">Your account is currently ${status}.</div></div><idp-badge variant=${status === 'active' ? 'ok' : status === 'suspended' ? 'error' : 'warn'}>${status}</idp-badge></div>${this.globalAdmin ? html`<div class="divider"></div><div class="split-row"><div><div class="section-title">Global admin</div><div class="muted">You have platform-wide administrative access.</div></div><idp-badge variant="accent">Admin</idp-badge></div>` : html``}`)}
1778
+ ${this.delegationApps.length
1779
+ ? html`<idp-delegation-section .apps=${this.delegationApps} .delegations=${this.delegations}></idp-delegation-section>`
1780
+ : html``}
2241
1781
  </div>
2242
1782
  `;
2243
1783
  }
@@ -2411,7 +1951,7 @@ export class IdpAdminShell extends DeesElement {
2411
1951
  || searchableText.includes('organisation');
2412
1952
  })
2413
1953
  .slice(0, 5);
2414
- const kpis: TKpi[] = [
1954
+ const kpis: TIdpAdminKpi[] = [
2415
1955
  {
2416
1956
  label: 'Members',
2417
1957
  value: String(this.orgMembers.length),
@@ -2464,7 +2004,7 @@ export class IdpAdminShell extends DeesElement {
2464
2004
  <div class="page-actions"><button class="plain-button outline" @click=${() => this.setPage('org-members')}>Manage members</button><button class="plain-button primary" @click=${() => this.setPage('org-apps')}>Manage apps</button></div>
2465
2005
  </header>
2466
2006
  <div class="body">
2467
- <div class="kpis">${kpis.map((kpiArg) => this.renderKpi(kpiArg))}</div>
2007
+ <div class="kpis">${kpis.map((kpiArg) => renderAdminKpi(kpiArg))}</div>
2468
2008
  ${this.dataLoading || this.dataError ? this.renderDataState('No organisation data', 'The console has not received live organisation data yet.', 'building') : html``}
2469
2009
  <div class="primary-grid">
2470
2010
  <div class="card">
@@ -2695,10 +2235,64 @@ export class IdpAdminShell extends DeesElement {
2695
2235
  }
2696
2236
 
2697
2237
  private renderGAUsers(): TemplateResult {
2698
- return html`${this.renderPageHeader('All Users', 'Platform-wide user administration.')}<div class="body wide-body">${this.renderDataState('User directory not connected', 'The shell is ready for global user data, but no platform user-list endpoint is wired into this app yet. No demo users are shown in production mode.', 'users')}</div>`;
2238
+ if (!this.gaUsers.length) {
2239
+ return html`${this.renderPageHeader('All Users', 'Platform-wide user administration.')}<div class="body wide-body">${this.renderDataState('User directory not connected', 'The shell is ready for global user data, but no platform user-list endpoint is wired into this app yet. No demo users are shown in production mode.', 'users')}</div>`;
2240
+ }
2241
+
2242
+ const query = this.gaUserSearch.trim().toLowerCase();
2243
+ const filtered = query
2244
+ ? this.gaUsers.filter((userArg) => userArg.name.toLowerCase().includes(query) || userArg.email.toLowerCase().includes(query))
2245
+ : this.gaUsers;
2246
+ const statusVariant = (statusArg: string): 'ok' | 'error' | 'warn' | 'default' =>
2247
+ statusArg === 'active' ? 'ok' : statusArg === 'suspended' ? 'error' : statusArg === 'new' ? 'warn' : 'default';
2248
+ const userRows = filtered.map((userArg) => ({
2249
+ cells: [
2250
+ html`
2251
+ <div class="identity-cell">
2252
+ <idp-avatar name=${userArg.name || userArg.email} size="28"></idp-avatar>
2253
+ <div>
2254
+ <div class="identity-primary">${userArg.name}</div>
2255
+ <div class="identity-secondary">${userArg.email}</div>
2256
+ </div>
2257
+ </div>
2258
+ `,
2259
+ String(userArg.orgCount),
2260
+ html`<idp-badge variant=${statusVariant(userArg.status)}>${userArg.status}</idp-badge>`,
2261
+ userArg.isGlobalAdmin ? html`<idp-badge variant="accent">Admin</idp-badge>` : '-',
2262
+ this.formatTimeAgo(userArg.createdAt),
2263
+ html`<button class="plain-button ghost" style="font-size:11px" @click=${() => this.dispatchShellEvent<IIdpAdminGaUserActionEventDetail>('idp-admin-ga-user-action', { userId: userArg.id, action: userArg.status === 'suspended' ? 'unsuspend' : 'suspend' })}>${userArg.status === 'suspended' ? 'Unsuspend' : 'Suspend'}</button>`,
2264
+ ],
2265
+ }));
2266
+
2267
+ return html`
2268
+ ${this.renderPageHeader('All Users', `${this.gaUsers.length} users on the platform`)}
2269
+ <div class="body wide-body">
2270
+ <div style="margin-bottom:12px;max-width:360px">
2271
+ <input class="input" placeholder="Search users…" .value=${this.gaUserSearch} @input=${(eventArg: Event) => { this.gaUserSearch = (eventArg.target as HTMLInputElement).value; }} />
2272
+ </div>
2273
+ <idp-data-table
2274
+ title="Users"
2275
+ badge=${`${filtered.length} shown`}
2276
+ empty-title="No matching users"
2277
+ empty-description="No users match the current search."
2278
+ .columns=${[
2279
+ { label: 'User' },
2280
+ { label: 'Orgs', mono: true, hideBelow: 'mobile' },
2281
+ { label: 'Status' },
2282
+ { label: 'Admin', hideBelow: 'mobile' },
2283
+ { label: 'Joined', hideBelow: 'tablet' },
2284
+ { label: '' },
2285
+ ]}
2286
+ .rows=${userRows}
2287
+ ></idp-data-table>
2288
+ </div>
2289
+ `;
2699
2290
  }
2700
2291
 
2701
2292
  private renderGAOrgs(): TemplateResult {
2293
+ const planVariant = (planArg: string): 'accent' | 'ok' | 'warn' | 'default' =>
2294
+ planArg === 'Pro' ? 'accent' : planArg === 'Enterprise' ? 'ok' : planArg === 'Internal' ? 'warn' : 'default';
2295
+ const hasAdminFields = this.orgs.some((orgArg) => orgArg.status || orgArg.plan);
2702
2296
  const orgRows = this.orgs.map((orgArg) => ({
2703
2297
  cells: [
2704
2298
  html`
@@ -2711,8 +2305,17 @@ export class IdpAdminShell extends DeesElement {
2711
2305
  </div>
2712
2306
  `,
2713
2307
  orgArg.slug || '-',
2714
- html`<idp-badge variant=${this.roleVariant(orgArg.myRole || 'member')}>${orgArg.myRole || 'member'}</idp-badge>`,
2715
- orgArg.id,
2308
+ ...(hasAdminFields
2309
+ ? [
2310
+ orgArg.memberCount !== undefined ? String(orgArg.memberCount) : '-',
2311
+ orgArg.plan ? html`<idp-badge variant=${planVariant(orgArg.plan)}>${orgArg.plan}</idp-badge>` : '-',
2312
+ html`<idp-badge variant=${orgArg.status === 'suspended' ? 'error' : 'ok'}>${orgArg.status || 'active'}</idp-badge>`,
2313
+ html`<button class="plain-button ghost" style="font-size:11px" @click=${() => this.dispatchShellEvent<IIdpAdminGaOrgActionEventDetail>('idp-admin-ga-org-action', { orgId: orgArg.id, action: orgArg.status === 'suspended' ? 'unsuspend' : 'suspend' })}>${orgArg.status === 'suspended' ? 'Unsuspend' : 'Suspend'}</button>`,
2314
+ ]
2315
+ : [
2316
+ html`<idp-badge variant=${this.roleVariant(orgArg.myRole || 'member')}>${orgArg.myRole || 'member'}</idp-badge>`,
2317
+ orgArg.id,
2318
+ ]),
2716
2319
  ],
2717
2320
  }));
2718
2321
 
@@ -2724,12 +2327,21 @@ export class IdpAdminShell extends DeesElement {
2724
2327
  badge=${`${this.orgs.length} total`}
2725
2328
  empty-title="No organisations"
2726
2329
  empty-description="No organisations are visible to this admin session."
2727
- .columns=${[
2728
- { label: 'Organisation' },
2729
- { label: 'Slug', mono: true, hideBelow: 'mobile' },
2730
- { label: 'Role' },
2731
- { label: 'Identifier', mono: true, hideBelow: 'tablet' },
2732
- ]}
2330
+ .columns=${hasAdminFields
2331
+ ? [
2332
+ { label: 'Organisation' },
2333
+ { label: 'Slug', mono: true, hideBelow: 'mobile' },
2334
+ { label: 'Members', mono: true, hideBelow: 'mobile' },
2335
+ { label: 'Plan' },
2336
+ { label: 'Status' },
2337
+ { label: '' },
2338
+ ]
2339
+ : [
2340
+ { label: 'Organisation' },
2341
+ { label: 'Slug', mono: true, hideBelow: 'mobile' },
2342
+ { label: 'Role' },
2343
+ { label: 'Identifier', mono: true, hideBelow: 'tablet' },
2344
+ ]}
2733
2345
  .rows=${orgRows}
2734
2346
  ></idp-data-table>
2735
2347
  </div>
@@ -2752,7 +2364,15 @@ export class IdpAdminShell extends DeesElement {
2752
2364
  html`<idp-badge variant=${appArg.type === 'global' ? 'accent' : 'outline'}>${appArg.type || 'global'}</idp-badge>`,
2753
2365
  appArg.category || '-',
2754
2366
  appArg.connectionCount ?? '-',
2755
- html`<idp-badge variant=${appArg.status === 'suspended' ? 'error' : appArg.status === 'pending_review' ? 'warn' : 'ok'}>${appArg.status || 'active'}</idp-badge>`,
2367
+ html`<idp-badge variant=${appArg.status === 'suspended' || appArg.status === 'rejected' ? 'error' : appArg.status === 'pending_review' ? 'warn' : 'ok'}>${(appArg.status || 'active').replace(/_/g, ' ')}</idp-badge>`,
2368
+ appArg.status === 'pending_review'
2369
+ ? html`
2370
+ <div style="display:flex;gap:4px">
2371
+ <button class="plain-button outline" style="font-size:11px;color:var(--idp-ok);border-color:var(--idp-ok-border)" @click=${() => this.dispatchShellEvent<IIdpAdminGaAppActionEventDetail>('idp-admin-ga-app-action', { appId: appArg.id, action: 'approve' })}>Approve</button>
2372
+ <button class="plain-button ghost" style="font-size:11px;color:var(--idp-destructive)" @click=${() => this.dispatchShellEvent<IIdpAdminGaAppActionEventDetail>('idp-admin-ga-app-action', { appId: appArg.id, action: 'reject' })}>Reject</button>
2373
+ </div>
2374
+ `
2375
+ : html`<button class="plain-button ghost" style="font-size:11px" @click=${() => this.dispatchShellEvent<IIdpAdminGaAppActionEventDetail>('idp-admin-ga-app-action', { appId: appArg.id, action: appArg.status === 'suspended' ? 'reinstate' : 'suspend' })}>${appArg.status === 'suspended' ? 'Reinstate' : 'Suspend'}</button>`,
2756
2376
  ],
2757
2377
  }));
2758
2378
 
@@ -2770,6 +2390,7 @@ export class IdpAdminShell extends DeesElement {
2770
2390
  { label: 'Category', hideBelow: 'mobile' },
2771
2391
  { label: 'Connections', align: 'right', mono: true, hideBelow: 'mobile' },
2772
2392
  { label: 'Status' },
2393
+ { label: '' },
2773
2394
  ]}
2774
2395
  .rows=${appRows}
2775
2396
  ></idp-data-table>
@@ -2798,11 +2419,33 @@ export class IdpAdminShell extends DeesElement {
2798
2419
 
2799
2420
  public render(): TemplateResult {
2800
2421
  return html`
2801
- <div class="shell">
2802
- ${this.renderSidebar()}
2422
+ <div
2423
+ class="shell"
2424
+ @idp-admin-navigate=${this.handleSidebarNavigate}
2425
+ @idp-admin-org-select=${this.handleSidebarOrgSelect}
2426
+ @idp-admin-org-create-request=${this.requestOrgCreate}
2427
+ @idp-admin-logout-request=${this.requestLogout}
2428
+ >
2429
+ <idp-admin-sidebar
2430
+ .page=${this.page}
2431
+ .user=${this.user}
2432
+ .accounts=${this.accounts}
2433
+ .orgs=${this.orgs}
2434
+ .selectedOrgId=${this.selectedOrgId}
2435
+ .globalAdmin=${this.globalAdmin}
2436
+ .sessionCount=${this.sessions.length}
2437
+ ></idp-admin-sidebar>
2803
2438
  <main>${this.renderMainContent()}</main>
2804
2439
  </div>
2805
2440
  ${this.renderDialog()}
2441
+ ${this.toast
2442
+ ? html`
2443
+ <div class="toast ${this.toast.tone}">
2444
+ <idp-icon name=${this.toast.tone === 'ok' ? 'check' : 'alert'} size="14"></idp-icon>
2445
+ <span style="flex:1">${this.toast.message}</span>
2446
+ </div>
2447
+ `
2448
+ : html``}
2806
2449
  `;
2807
2450
  }
2808
2451
  }