@idp.global/catalog 2.2.0 → 3.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.
@@ -22,6 +22,7 @@ export * from './idp-signed-in-pill.js';
22
22
  export * from './idp-app-pair.js';
23
23
  export * from './idp-auth-card.js';
24
24
  export * from './idp-consent-card.js';
25
+ export * from './idp-invite-card.js';
25
26
  export * from './idp-approval-card.js';
26
27
  export * from './idp-mobile-frame.js';
27
28
  export * from './idp-inbox-preview.js';
@@ -34,3 +35,5 @@ export * from './idp-admin-shell.js';
34
35
  export * from './idp-landing-hero.js';
35
36
  export * from './idp-landing-page.js';
36
37
  export * from './idp-mobile-showcase.js';
38
+ export * from './idp-sso-connection-form.js';
39
+ export * from './idp-scim-token-panel.js';
@@ -22,6 +22,7 @@ export * from './idp-signed-in-pill.js';
22
22
  export * from './idp-app-pair.js';
23
23
  export * from './idp-auth-card.js';
24
24
  export * from './idp-consent-card.js';
25
+ export * from './idp-invite-card.js';
25
26
  export * from './idp-approval-card.js';
26
27
  export * from './idp-mobile-frame.js';
27
28
  export * from './idp-inbox-preview.js';
@@ -34,4 +35,6 @@ export * from './idp-admin-shell.js';
34
35
  export * from './idp-landing-hero.js';
35
36
  export * from './idp-landing-page.js';
36
37
  export * from './idp-mobile-showcase.js';
37
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90c193ZWIvZWxlbWVudHMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxlQUFlLENBQUM7QUFDOUIsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLGdCQUFnQixDQUFDO0FBQy9CLGNBQWMsZUFBZSxDQUFDO0FBQzlCLGNBQWMsaUNBQWlDLENBQUM7QUFDaEQsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMsa0JBQWtCLENBQUM7QUFDakMsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLHNCQUFzQixDQUFDO0FBQ3JDLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxtQkFBbUIsQ0FBQztBQUNsQyxjQUFjLGVBQWUsQ0FBQztBQUM5QixjQUFjLHNCQUFzQixDQUFDO0FBQ3JDLGNBQWMsaUJBQWlCLENBQUM7QUFDaEMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMsbUJBQW1CLENBQUM7QUFDbEMsY0FBYyxvQkFBb0IsQ0FBQztBQUNuQyxjQUFjLHVCQUF1QixDQUFDO0FBQ3RDLGNBQWMsd0JBQXdCLENBQUM7QUFDdkMsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLHdCQUF3QixDQUFDO0FBQ3ZDLGNBQWMsMkJBQTJCLENBQUM7QUFDMUMsY0FBYyw2QkFBNkIsQ0FBQztBQUM1QyxjQUFjLHVCQUF1QixDQUFDO0FBQ3RDLGNBQWMsd0JBQXdCLENBQUM7QUFDdkMsY0FBYyx5QkFBeUIsQ0FBQztBQUN4QyxjQUFjLHNCQUFzQixDQUFDO0FBQ3JDLGNBQWMsdUJBQXVCLENBQUM7QUFDdEMsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLDBCQUEwQixDQUFDIn0=
38
+ export * from './idp-sso-connection-form.js';
39
+ export * from './idp-scim-token-panel.js';
40
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90c193ZWIvZWxlbWVudHMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxlQUFlLENBQUM7QUFDOUIsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLGdCQUFnQixDQUFDO0FBQy9CLGNBQWMsZUFBZSxDQUFDO0FBQzlCLGNBQWMsaUNBQWlDLENBQUM7QUFDaEQsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMsa0JBQWtCLENBQUM7QUFDakMsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLHNCQUFzQixDQUFDO0FBQ3JDLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxtQkFBbUIsQ0FBQztBQUNsQyxjQUFjLGVBQWUsQ0FBQztBQUM5QixjQUFjLHNCQUFzQixDQUFDO0FBQ3JDLGNBQWMsaUJBQWlCLENBQUM7QUFDaEMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMsbUJBQW1CLENBQUM7QUFDbEMsY0FBYyxvQkFBb0IsQ0FBQztBQUNuQyxjQUFjLHVCQUF1QixDQUFDO0FBQ3RDLGNBQWMsc0JBQXNCLENBQUM7QUFDckMsY0FBYyx3QkFBd0IsQ0FBQztBQUN2QyxjQUFjLHVCQUF1QixDQUFDO0FBQ3RDLGNBQWMsd0JBQXdCLENBQUM7QUFDdkMsY0FBYywyQkFBMkIsQ0FBQztBQUMxQyxjQUFjLDZCQUE2QixDQUFDO0FBQzVDLGNBQWMsdUJBQXVCLENBQUM7QUFDdEMsY0FBYyx3QkFBd0IsQ0FBQztBQUN2QyxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMsc0JBQXNCLENBQUM7QUFDckMsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLHVCQUF1QixDQUFDO0FBQ3RDLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyw4QkFBOEIsQ0FBQztBQUM3QyxjQUFjLDJCQUEyQixDQUFDIn0=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idp.global/catalog",
3
- "version": "2.2.0",
3
+ "version": "3.1.0",
4
4
  "private": false,
5
5
  "description": "Web component catalog for idp.global, based on the v5 Apple-system design language.",
6
6
  "exports": {
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@idp.global/catalog',
6
- version: '2.2.0',
6
+ version: '3.1.0',
7
7
  description: 'Web component catalog for idp.global, based on the v5 Apple-system design language.'
8
8
  }
@@ -8,8 +8,12 @@ import './idp-toggle.js';
8
8
  import './idp-delegation-section.js';
9
9
  import './idp-admin-sidebar.js';
10
10
  import './idp-admin-overview.js';
11
+ import './idp-sso-connection-form.js';
12
+ import './idp-scim-token-panel.js';
11
13
  import { idpAdminSharedStyles, renderAdminKpi, renderAdminStateCard, formatAdminTimeAgo, type TIdpAdminKpi } from './idp-admin-shared.js';
12
14
  import type { IIdpDelegation, IIdpDelegationApp } from './idp-delegation-section.js';
15
+ import type { IIdpSsoConnectionItem, IIdpSsoSpDetails } from './idp-sso-connection-form.js';
16
+ import type { IIdpScimTokenItem } from './idp-scim-token-panel.js';
13
17
 
14
18
  declare global {
15
19
  interface HTMLElementTagNameMap {
@@ -27,6 +31,7 @@ export type TIdpAdminPage =
27
31
  | 'org-settings'
28
32
  | 'org-members'
29
33
  | 'org-apps'
34
+ | 'org-sso'
30
35
  | 'support'
31
36
  | 'ga-users'
32
37
  | 'ga-orgs'
@@ -89,6 +94,12 @@ export interface IIdpAdminGaUserActionEventDetail {
89
94
  action: 'suspend' | 'unsuspend';
90
95
  }
91
96
 
97
+ export interface IIdpAdminProfileUpdateEventDetail {
98
+ name: string;
99
+ username: string;
100
+ mobileNumber: string;
101
+ }
102
+
92
103
  export interface IIdpAdminGaOrgActionEventDetail {
93
104
  orgId: string;
94
105
  action: 'suspend' | 'unsuspend';
@@ -826,13 +837,88 @@ export class IdpAdminShell extends DeesElement {
826
837
  .mapping-row .form-label {
827
838
  margin-bottom: 4px;
828
839
  }
840
+ .mobile-topbar {
841
+ display: none;
842
+ align-items: center;
843
+ gap: 10px;
844
+ padding: 10px 14px;
845
+ border-bottom: 1px solid var(--sh-border);
846
+ background: var(--sh-card);
847
+ position: sticky;
848
+ top: 0;
849
+ z-index: 40;
850
+ }
851
+ .mobile-topbar-title {
852
+ font-size: 13px;
853
+ font-weight: 650;
854
+ letter-spacing: -0.01em;
855
+ }
856
+ .mobile-nav-toggle,
857
+ .mobile-drawer-close {
858
+ display: inline-flex;
859
+ align-items: center;
860
+ justify-content: center;
861
+ width: 34px;
862
+ height: 34px;
863
+ border: 1px solid var(--sh-border);
864
+ border-radius: 9px;
865
+ background: var(--sh-card);
866
+ color: var(--sh-fg);
867
+ cursor: pointer;
868
+ }
869
+ .mobile-nav-toggle:focus-visible,
870
+ .mobile-drawer-close:focus-visible {
871
+ outline: 2px solid var(--sh-accent);
872
+ outline-offset: 2px;
873
+ }
874
+ .mobile-drawer-backdrop {
875
+ position: fixed;
876
+ inset: 0;
877
+ background: rgba(0, 0, 0, 0.42);
878
+ z-index: 60;
879
+ }
880
+ .mobile-drawer {
881
+ position: fixed;
882
+ top: 0;
883
+ left: 0;
884
+ bottom: 0;
885
+ width: min(300px, 86vw);
886
+ background: var(--sh-card);
887
+ border-right: 1px solid var(--sh-border);
888
+ z-index: 61;
889
+ overflow-y: auto;
890
+ animation: drawer-in 0.18s ease-out;
891
+ }
892
+ .mobile-drawer idp-admin-sidebar {
893
+ display: block;
894
+ }
895
+ .mobile-drawer-close {
896
+ position: absolute;
897
+ top: 10px;
898
+ right: 10px;
899
+ z-index: 62;
900
+ }
901
+ @keyframes drawer-in {
902
+ from {
903
+ transform: translateX(-24px);
904
+ opacity: 0;
905
+ }
906
+ to {
907
+ transform: translateX(0);
908
+ opacity: 1;
909
+ }
910
+ }
829
911
  @media (max-width: 1120px) {
830
912
  .shell {
831
913
  grid-template-columns: 1fr;
914
+ grid-template-rows: auto 1fr;
832
915
  }
833
- idp-admin-sidebar {
916
+ .shell > idp-admin-sidebar {
834
917
  display: none;
835
918
  }
919
+ .mobile-topbar {
920
+ display: flex;
921
+ }
836
922
  }
837
923
  @media (max-width: 720px) {
838
924
  .mapping-row {
@@ -917,6 +1003,18 @@ export class IdpAdminShell extends DeesElement {
917
1003
  @property({ type: Array })
918
1004
  public accessor gaUsers: IIdpAdminGaUser[] = [];
919
1005
 
1006
+ @property({ type: Array })
1007
+ public accessor ssoConnections: IIdpSsoConnectionItem[] = [];
1008
+
1009
+ @property({ type: Object })
1010
+ public accessor ssoSpDetails: IIdpSsoSpDetails | null = null;
1011
+
1012
+ @property({ type: Array })
1013
+ public accessor scimTokens: IIdpScimTokenItem[] = [];
1014
+
1015
+ @property({ type: Object })
1016
+ public accessor scimCreatedToken: { label: string; plaintextToken: string } | null = null;
1017
+
920
1018
  @property({ type: Object })
921
1019
  public accessor overviewData: IIdpAdminOverviewData | null = null;
922
1020
 
@@ -926,6 +1024,20 @@ export class IdpAdminShell extends DeesElement {
926
1024
  @property({ type: Array })
927
1025
  public accessor delegations: IIdpDelegation[] = [];
928
1026
 
1027
+ /** enables editing of name/username/mobile on the profile page */
1028
+ @property({ type: Boolean })
1029
+ public accessor profileEditable = false;
1030
+
1031
+ /** host sets this while a profile update request is in flight */
1032
+ @property({ type: Boolean })
1033
+ public accessor profileSaving = false;
1034
+
1035
+ @state()
1036
+ private accessor profileDraft: { name: string; username: string; mobileNumber: string } | null = null;
1037
+
1038
+ @state()
1039
+ private accessor mobileNavOpen = false;
1040
+
929
1041
  @state()
930
1042
  private accessor logoutEverywhere = false;
931
1043
 
@@ -1319,6 +1431,22 @@ export class IdpAdminShell extends DeesElement {
1319
1431
  await super.disconnectedCallback();
1320
1432
  }
1321
1433
 
1434
+ protected override willUpdate(changedProperties: Map<PropertyKey, unknown>) {
1435
+ super.willUpdate(changedProperties);
1436
+ // a refreshed user means the host persisted (or reloaded) the profile
1437
+ if (changedProperties.has('user')) {
1438
+ this.profileDraft = null;
1439
+ }
1440
+ }
1441
+
1442
+ protected override updated(changedProperties: Map<PropertyKey, unknown>) {
1443
+ super.updated(changedProperties);
1444
+ if (changedProperties.has('mobileNavOpen') && this.mobileNavOpen) {
1445
+ const closeButton = this.shadowRoot?.querySelector('.mobile-drawer-close') as HTMLElement | null;
1446
+ closeButton?.focus();
1447
+ }
1448
+ }
1449
+
1322
1450
  private dispatchShellEvent<TDetail>(eventNameArg: string, detailArg: TDetail) {
1323
1451
  this.dispatchEvent(new CustomEvent<TDetail>(eventNameArg, {
1324
1452
  detail: detailArg,
@@ -1761,18 +1889,54 @@ export class IdpAdminShell extends DeesElement {
1761
1889
  `;
1762
1890
  }
1763
1891
 
1892
+ private getProfileDraft() {
1893
+ return (
1894
+ this.profileDraft || {
1895
+ name: this.user?.name || '',
1896
+ username: this.user?.username || this.user?.email?.split('@')[0] || '',
1897
+ mobileNumber: this.user?.mobileNumber || '',
1898
+ }
1899
+ );
1900
+ }
1901
+
1902
+ private setProfileDraftField(fieldArg: 'name' | 'username' | 'mobileNumber', eventArg: Event) {
1903
+ const value = (eventArg.target as HTMLInputElement).value;
1904
+ this.profileDraft = { ...this.getProfileDraft(), [fieldArg]: value };
1905
+ }
1906
+
1907
+ private handleProfileSave() {
1908
+ const draft = this.getProfileDraft();
1909
+ this.dispatchShellEvent<IIdpAdminProfileUpdateEventDetail>('idp-admin-profile-update', {
1910
+ name: draft.name.trim(),
1911
+ username: draft.username.trim(),
1912
+ mobileNumber: draft.mobileNumber.trim(),
1913
+ });
1914
+ }
1915
+
1764
1916
  private renderProfile(): TemplateResult {
1765
- const username = this.user?.username || this.user?.email?.split('@')[0] || '';
1766
1917
  const status = this.user?.status || 'active';
1918
+ const draft = this.getProfileDraft();
1919
+ const editable = this.profileEditable;
1920
+ const dirty =
1921
+ this.profileDraft !== null &&
1922
+ (draft.name !== (this.user?.name || '') ||
1923
+ draft.username !== (this.user?.username || this.user?.email?.split('@')[0] || '') ||
1924
+ draft.mobileNumber !== (this.user?.mobileNumber || ''));
1767
1925
  return html`
1768
1926
  ${this.renderPageHeader('Profile', 'Your personal identity details visible to connected apps.')}
1769
1927
  <div class="body narrow-body">
1770
- ${this.renderSectionCard('Avatar', 'Shown to apps that request your profile.', html`<div class="split-row" style="justify-content:flex-start"><span class="avatar" style="width:56px;height:56px;font-size:20px">${this.userInitials}</span><div><div class="section-title">${this.user?.name || 'Unknown User'}</div><div class="muted" style="margin-top:5px">Profile update endpoints are not exposed in this console yet.</div></div></div>`)}
1928
+ ${this.renderSectionCard('Avatar', 'Shown to apps that request your profile.', html`<div class="split-row" style="justify-content:flex-start"><span class="avatar" style="width:56px;height:56px;font-size:20px">${this.userInitials}</span><div><div class="section-title">${this.user?.name || 'Unknown User'}</div><div class="muted" style="margin-top:5px">${this.user?.email || ''}</div></div></div>`)}
1771
1929
  ${this.renderSectionCard('Personal information', '', html`
1772
- ${this.renderFormRow('Full name', '', html`<input class="input" .value=${this.user?.name || ''} disabled />`, true)}
1773
- ${this.renderFormRow('Username', 'Used in your public profile URL', html`<div class="input-group"><span class="input-prefix">idp.global/user/</span><input class="input" .value=${username} disabled /></div>`)}
1774
- ${this.renderFormRow('Email', 'Primary address for login and notifications', html`<input class="input" .value=${this.user?.email || ''} disabled />`)}
1775
- ${this.renderFormRow('Mobile number', 'Used for SMS verification', html`<input class="input" .value=${this.user?.mobileNumber || ''} disabled />`)}
1930
+ ${this.renderFormRow('Full name', '', html`<input class="input" .value=${draft.name} ?disabled=${!editable || this.profileSaving} @input=${(eventArg: Event) => this.setProfileDraftField('name', eventArg)} />`, true)}
1931
+ ${this.renderFormRow('Username', 'Used in your public profile URL', html`<div class="input-group"><span class="input-prefix">idp.global/user/</span><input class="input" .value=${draft.username} ?disabled=${!editable || this.profileSaving} @input=${(eventArg: Event) => this.setProfileDraftField('username', eventArg)} /></div>`)}
1932
+ ${this.renderFormRow('Email', 'Primary address for login and notifications - contact support to change it', html`<input class="input" .value=${this.user?.email || ''} disabled />`)}
1933
+ ${this.renderFormRow('Mobile number', 'Optional contact number shown to connected apps', html`<input class="input" .value=${draft.mobileNumber} ?disabled=${!editable || this.profileSaving} @input=${(eventArg: Event) => this.setProfileDraftField('mobileNumber', eventArg)} />`)}
1934
+ ${editable
1935
+ ? html`<div class="split-row" style="justify-content:flex-end;gap:10px;margin-top:14px">
1936
+ <idp-button variant="outline" shape="rect" ?disabled=${!dirty || this.profileSaving} @click=${() => { this.profileDraft = null; }}>Reset</idp-button>
1937
+ <idp-button variant="accent" shape="rect" ?loading=${this.profileSaving} ?disabled=${!dirty} @click=${this.handleProfileSave}>Save changes</idp-button>
1938
+ </div>`
1939
+ : html``}
1776
1940
  `)}
1777
1941
  ${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
1942
  ${this.delegationApps.length
@@ -2220,6 +2384,25 @@ export class IdpAdminShell extends DeesElement {
2220
2384
  `;
2221
2385
  }
2222
2386
 
2387
+ private renderOrgSso(): TemplateResult {
2388
+ return html`
2389
+ ${this.renderPageHeader('SSO & Provisioning', 'Enterprise single sign-on and automated user provisioning for this organisation.')}
2390
+ <div class="body narrow-body">
2391
+ <div class="list-stack">
2392
+ <idp-sso-connection-form
2393
+ .connections=${this.ssoConnections}
2394
+ .spDetails=${this.ssoSpDetails}
2395
+ ></idp-sso-connection-form>
2396
+ <idp-scim-token-panel
2397
+ .tokens=${this.scimTokens}
2398
+ .createdToken=${this.scimCreatedToken}
2399
+ scimBaseUrl=${this.ssoSpDetails?.scimBaseUrl || ''}
2400
+ ></idp-scim-token-panel>
2401
+ </div>
2402
+ </div>
2403
+ `;
2404
+ }
2405
+
2223
2406
  private renderSupport(): TemplateResult {
2224
2407
  const services = [
2225
2408
  ['Account Recovery', 'Lost access to your account or locked out of your organisation? Our team will verify your identity and restore access securely.', 'EUR149', 'per incident', 'key', '1-2 business days'],
@@ -2293,6 +2476,7 @@ export class IdpAdminShell extends DeesElement {
2293
2476
  const planVariant = (planArg: string): 'accent' | 'ok' | 'warn' | 'default' =>
2294
2477
  planArg === 'Pro' ? 'accent' : planArg === 'Enterprise' ? 'ok' : planArg === 'Internal' ? 'warn' : 'default';
2295
2478
  const hasAdminFields = this.orgs.some((orgArg) => orgArg.status || orgArg.plan);
2479
+ const hasMemberCounts = this.orgs.some((orgArg) => orgArg.memberCount !== undefined);
2296
2480
  const orgRows = this.orgs.map((orgArg) => ({
2297
2481
  cells: [
2298
2482
  html`
@@ -2305,9 +2489,9 @@ export class IdpAdminShell extends DeesElement {
2305
2489
  </div>
2306
2490
  `,
2307
2491
  orgArg.slug || '-',
2492
+ ...(hasMemberCounts ? [orgArg.memberCount !== undefined ? String(orgArg.memberCount) : '-'] : []),
2308
2493
  ...(hasAdminFields
2309
2494
  ? [
2310
- orgArg.memberCount !== undefined ? String(orgArg.memberCount) : '-',
2311
2495
  orgArg.plan ? html`<idp-badge variant=${planVariant(orgArg.plan)}>${orgArg.plan}</idp-badge>` : '-',
2312
2496
  html`<idp-badge variant=${orgArg.status === 'suspended' ? 'error' : 'ok'}>${orgArg.status || 'active'}</idp-badge>`,
2313
2497
  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>`,
@@ -2327,21 +2511,17 @@ export class IdpAdminShell extends DeesElement {
2327
2511
  badge=${`${this.orgs.length} total`}
2328
2512
  empty-title="No organisations"
2329
2513
  empty-description="No organisations are visible to this admin session."
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
- ]}
2514
+ .columns=${[
2515
+ { label: 'Organisation' },
2516
+ { label: 'Slug', mono: true, hideBelow: 'mobile' },
2517
+ ...(hasMemberCounts ? [{ label: 'Members', mono: true, hideBelow: 'mobile' }] : []),
2518
+ ...(hasAdminFields
2519
+ ? [{ label: 'Plan' }, { label: 'Status' }, { label: '' }]
2520
+ : [
2521
+ { label: 'Role' },
2522
+ { label: 'Identifier', mono: true, hideBelow: 'tablet' },
2523
+ ]),
2524
+ ]}
2345
2525
  .rows=${orgRows}
2346
2526
  ></idp-data-table>
2347
2527
  </div>
@@ -2409,6 +2589,7 @@ export class IdpAdminShell extends DeesElement {
2409
2589
  'org-settings': () => this.renderOrgSettings(),
2410
2590
  'org-members': () => this.renderOrgMembers(),
2411
2591
  'org-apps': () => this.renderOrgApps(),
2592
+ 'org-sso': () => this.renderOrgSso(),
2412
2593
  support: () => this.renderSupport(),
2413
2594
  'ga-users': () => this.renderGAUsers(),
2414
2595
  'ga-orgs': () => this.renderGAOrgs(),
@@ -2417,15 +2598,27 @@ export class IdpAdminShell extends DeesElement {
2417
2598
  return (renderers[this.page] || renderers.overview)();
2418
2599
  }
2419
2600
 
2601
+ private handleMobileNavKeydown(eventArg: KeyboardEvent) {
2602
+ if (eventArg.key === 'Escape') {
2603
+ this.mobileNavOpen = false;
2604
+ }
2605
+ }
2606
+
2420
2607
  public render(): TemplateResult {
2421
2608
  return html`
2422
2609
  <div
2423
2610
  class="shell"
2424
- @idp-admin-navigate=${this.handleSidebarNavigate}
2611
+ @idp-admin-navigate=${(eventArg: CustomEvent) => { this.mobileNavOpen = false; this.handleSidebarNavigate(eventArg as CustomEvent<IIdpAdminNavigateEventDetail>); }}
2425
2612
  @idp-admin-org-select=${this.handleSidebarOrgSelect}
2426
2613
  @idp-admin-org-create-request=${this.requestOrgCreate}
2427
2614
  @idp-admin-logout-request=${this.requestLogout}
2428
2615
  >
2616
+ <div class="mobile-topbar">
2617
+ <button class="mobile-nav-toggle" aria-label="Open navigation" aria-haspopup="dialog" aria-expanded=${this.mobileNavOpen} @click=${() => { this.mobileNavOpen = true; }}>
2618
+ <idp-icon name="lucide:Menu" size="17"></idp-icon>
2619
+ </button>
2620
+ <span class="mobile-topbar-title">idp.global</span>
2621
+ </div>
2429
2622
  <idp-admin-sidebar
2430
2623
  .page=${this.page}
2431
2624
  .user=${this.user}
@@ -2435,12 +2628,31 @@ export class IdpAdminShell extends DeesElement {
2435
2628
  .globalAdmin=${this.globalAdmin}
2436
2629
  .sessionCount=${this.sessions.length}
2437
2630
  ></idp-admin-sidebar>
2631
+ ${this.mobileNavOpen
2632
+ ? html`
2633
+ <div class="mobile-drawer-backdrop" @click=${() => { this.mobileNavOpen = false; }}></div>
2634
+ <div class="mobile-drawer" role="dialog" aria-label="Navigation" @keydown=${this.handleMobileNavKeydown}>
2635
+ <button class="mobile-drawer-close" aria-label="Close navigation" @click=${() => { this.mobileNavOpen = false; }}>
2636
+ <idp-icon name="x" size="16"></idp-icon>
2637
+ </button>
2638
+ <idp-admin-sidebar
2639
+ .page=${this.page}
2640
+ .user=${this.user}
2641
+ .accounts=${this.accounts}
2642
+ .orgs=${this.orgs}
2643
+ .selectedOrgId=${this.selectedOrgId}
2644
+ .globalAdmin=${this.globalAdmin}
2645
+ .sessionCount=${this.sessions.length}
2646
+ ></idp-admin-sidebar>
2647
+ </div>
2648
+ `
2649
+ : html``}
2438
2650
  <main>${this.renderMainContent()}</main>
2439
2651
  </div>
2440
2652
  ${this.renderDialog()}
2441
2653
  ${this.toast
2442
2654
  ? html`
2443
- <div class="toast ${this.toast.tone}">
2655
+ <div class="toast ${this.toast.tone}" role="status" aria-live="polite">
2444
2656
  <idp-icon name=${this.toast.tone === 'ok' ? 'check' : 'alert'} size="14"></idp-icon>
2445
2657
  <span style="flex:1">${this.toast.message}</span>
2446
2658
  </div>
@@ -444,6 +444,7 @@ export class IdpAdminSidebar extends DeesElement {
444
444
  { id: 'org-settings', label: 'Settings', icon: 'settings' },
445
445
  { id: 'org-members', label: 'Members', icon: 'users' },
446
446
  { id: 'org-apps', label: 'OAuth Apps', icon: 'box' },
447
+ { id: 'org-sso', label: 'SSO & Provisioning', icon: 'key' },
447
448
  ];
448
449
 
449
450
  private supportNav: TNavItem[] = [
@@ -534,7 +535,7 @@ export class IdpAdminSidebar extends DeesElement {
534
535
  return html`
535
536
  <div class="nav-list">
536
537
  ${items.map((item) => html`
537
- <button class="nav-item ${item.id === active ? 'active' : ''}" @click=${() => this.setPage(item.id)}>
538
+ <button class="nav-item ${item.id === active ? 'active' : ''}" aria-current=${item.id === active ? 'page' : 'false'} @click=${() => this.setPage(item.id)}>
538
539
  <idp-icon name=${item.icon as any} size="14"></idp-icon>
539
540
  ${item.label}
540
541
  ${item.badge ? html`<idp-badge variant="accent" style="margin-left:auto">${item.badge}</idp-badge>` : html``}
@@ -603,13 +604,13 @@ export class IdpAdminSidebar extends DeesElement {
603
604
  <span class="logo-text">idp.global</span>
604
605
  </div>
605
606
  </div>
606
- <div class="nav-wrap">
607
+ <nav class="nav-wrap" aria-label="Console navigation">
607
608
  <div class="nav-section"><div class="nav-title">Workspace</div>${this.renderNavGroup(this.workspaceNav, this.page)}</div>
608
609
  <div class="nav-section"><div class="nav-title">My Account</div>${this.renderNavGroup(this.accountNav, this.page)}</div>
609
610
  <div class="nav-section">
610
611
  <div class="nav-title">Organisation</div>
611
612
  <div class="org-switch-wrap">
612
- <button class="org-switch ${this.orgMenuOpen ? 'open' : ''}" @click=${() => { this.orgMenuOpen = !this.orgMenuOpen; }}>
613
+ <button class="org-switch ${this.orgMenuOpen ? 'open' : ''}" aria-haspopup="menu" aria-expanded=${this.orgMenuOpen} @click=${() => { this.orgMenuOpen = !this.orgMenuOpen; }}>
613
614
  <span class="org-avatar">${currentOrg.name.slice(0, 2).toUpperCase()}</span>
614
615
  <span class="org-name">${currentOrg.name}</span>
615
616
  <idp-icon name="chevron-down" size="10"></idp-icon>
@@ -633,7 +634,7 @@ export class IdpAdminSidebar extends DeesElement {
633
634
  </div>
634
635
  <div class="nav-section"><div class="nav-title">Support</div>${this.renderNavGroup(this.supportNav, this.page)}</div>
635
636
  ${this.globalAdmin ? html`<div class="nav-section"><div class="nav-title">Global Admin</div>${this.renderNavGroup(this.adminNav, this.page)}</div>` : html``}
636
- </div>
637
+ </nav>
637
638
  <div class="user-footer">
638
639
  <button class="user-trigger ${this.userMenuOpen ? 'open' : ''}" aria-haspopup="menu" aria-expanded=${this.userMenuOpen} @click=${() => { this.userMenuOpen = !this.userMenuOpen; }}>
639
640
  <span class="user-avatar">${this.userInitials}</span>