@zitadel/components 0.1.0-alpha.16 → 0.1.0-alpha.18

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 (36) hide show
  1. package/README.md +89 -20
  2. package/dist/atoms/index.d.mts +1 -1
  3. package/dist/atoms/index.mjs +1 -1
  4. package/dist/{atoms-C-zXJjaz.mjs → atoms-5-wYIl88.mjs} +295 -190
  5. package/dist/atoms-5-wYIl88.mjs.map +1 -0
  6. package/dist/default-Bnjy2dJB.mjs +6 -0
  7. package/dist/default-Bnjy2dJB.mjs.map +1 -0
  8. package/dist/{index-BniwXaIC.d.mts → index-BRGZPZ2y.d.mts} +111 -1
  9. package/dist/index-BRGZPZ2y.d.mts.map +1 -0
  10. package/dist/{index-D1eMaWbY.d.mts → index-CsJiPZtJ.d.mts} +438 -210
  11. package/dist/index-CsJiPZtJ.d.mts.map +1 -0
  12. package/dist/{index-BwQLFs9R.d.mts → index-DIGjtG4z.d.mts} +1642 -1027
  13. package/dist/index-DIGjtG4z.d.mts.map +1 -0
  14. package/dist/index.d.mts +5 -5
  15. package/dist/index.mjs +5 -5
  16. package/dist/jsx.d.ts +63 -0
  17. package/dist/manifests.mjs +1 -1
  18. package/dist/orchestrator/index.d.mts +3 -3
  19. package/dist/orchestrator/index.mjs +3 -3
  20. package/dist/{orchestrator-BN2nT9FZ.mjs → orchestrator-CctLkpmB.mjs} +1229 -767
  21. package/dist/orchestrator-CctLkpmB.mjs.map +1 -0
  22. package/dist/standalone.mjs +2552 -1291
  23. package/dist/tokens/index.d.mts +1 -1
  24. package/dist/tokens/index.mjs +1 -1
  25. package/dist/tokens-BV-PPbQd.mjs +356 -0
  26. package/dist/tokens-BV-PPbQd.mjs.map +1 -0
  27. package/package.json +14 -10
  28. package/dist/atoms-C-zXJjaz.mjs.map +0 -1
  29. package/dist/default-BoC3JEsO.mjs +0 -6
  30. package/dist/default-BoC3JEsO.mjs.map +0 -1
  31. package/dist/index-BniwXaIC.d.mts.map +0 -1
  32. package/dist/index-BwQLFs9R.d.mts.map +0 -1
  33. package/dist/index-D1eMaWbY.d.mts.map +0 -1
  34. package/dist/orchestrator-BN2nT9FZ.mjs.map +0 -1
  35. package/dist/tokens-CbH2s1jV.mjs +0 -246
  36. package/dist/tokens-CbH2s1jV.mjs.map +0 -1
@@ -1,10 +1,11 @@
1
- import { C as baseHostStyles, S as focusVisibleStyles, T as emit, w as t, x as __decorate } from "./atoms-C-zXJjaz.mjs";
2
- import { r as tokensCss } from "./tokens-CbH2s1jV.mjs";
3
- import { t as default_default } from "./default-BoC3JEsO.mjs";
1
+ import { C as focusVisibleStyles, E as emit, S as __decorate, T as t, d as hookName, w as baseHostStyles } from "./atoms-5-wYIl88.mjs";
2
+ import { r as tokensCss } from "./tokens-BV-PPbQd.mjs";
4
3
  import { manifestRegistry } from "./manifests.mjs";
4
+ import { t as default_default } from "./default-Bnjy2dJB.mjs";
5
5
  import { LitElement, css, html, nothing } from "lit";
6
6
  import { customElement, property, state } from "lit/decorators.js";
7
7
  import { unsafeHTML } from "lit/directives/unsafe-html.js";
8
+ import { MANDATORY_GATES_TAG } from "@zitadel/config/template";
8
9
  import { Liquid } from "liquidjs";
9
10
  import DOMPurify from "dompurify";
10
11
  //#region ../api/dist/runtime/auth.mjs
@@ -204,10 +205,9 @@ var __exportAll = (all, no_symbols) => {
204
205
  * OpenAPI spec version: 0.0.1
205
206
  */
206
207
  var zitadelNextGen_exports = /* @__PURE__ */ __exportAll({
207
- activateFlowDefinition: () => activateFlowDefinition,
208
- authorizeDevice: () => authorizeDevice,
209
- authorizeGet: () => authorizeGet,
208
+ completeClaim: () => completeClaim,
210
209
  createAuthAttempt: () => createAuthAttempt,
210
+ createBranding: () => createBranding,
211
211
  createFlow: () => createFlow,
212
212
  createFlowDefinition: () => createFlowDefinition,
213
213
  createHandoff: () => createHandoff,
@@ -216,15 +216,16 @@ var zitadelNextGen_exports = /* @__PURE__ */ __exportAll({
216
216
  createSession: () => createSession,
217
217
  createTeam: () => createTeam,
218
218
  createUser: () => createUser,
219
- deactivateFlowDefinition: () => deactivateFlowDefinition,
220
219
  deleteFlowDefinition: () => deleteFlowDefinition,
221
- endSession: () => endSession,
220
+ deleteTeam: () => deleteTeam,
221
+ deleteUserByID: () => deleteUserByID,
222
222
  exchangeHandoff: () => exchangeHandoff,
223
- getActivateFlowDefinitionUrl: () => getActivateFlowDefinitionUrl,
224
223
  getAuthAttempt: () => getAuthAttempt,
225
- getAuthorizeDeviceUrl: () => getAuthorizeDeviceUrl,
226
- getAuthorizeGetUrl: () => getAuthorizeGetUrl,
224
+ getBrandingById: () => getBrandingById,
225
+ getClaimStatus: () => getClaimStatus,
226
+ getCompleteClaimUrl: () => getCompleteClaimUrl,
227
227
  getCreateAuthAttemptUrl: () => getCreateAuthAttemptUrl,
228
+ getCreateBrandingUrl: () => getCreateBrandingUrl,
228
229
  getCreateFlowDefinitionUrl: () => getCreateFlowDefinitionUrl,
229
230
  getCreateFlowUrl: () => getCreateFlowUrl,
230
231
  getCreateHandoffUrl: () => getCreateHandoffUrl,
@@ -233,230 +234,76 @@ var zitadelNextGen_exports = /* @__PURE__ */ __exportAll({
233
234
  getCreateSessionUrl: () => getCreateSessionUrl,
234
235
  getCreateTeamUrl: () => getCreateTeamUrl,
235
236
  getCreateUserUrl: () => getCreateUserUrl,
236
- getDeactivateFlowDefinitionUrl: () => getDeactivateFlowDefinitionUrl,
237
237
  getDeleteFlowDefinitionUrl: () => getDeleteFlowDefinitionUrl,
238
- getEndSessionUrl: () => getEndSessionUrl,
238
+ getDeleteTeamUrl: () => getDeleteTeamUrl,
239
+ getDeleteUserByIDUrl: () => getDeleteUserByIDUrl,
239
240
  getExchangeHandoffUrl: () => getExchangeHandoffUrl,
240
241
  getFlowDefinition: () => getFlowDefinition,
241
242
  getFlowStep: () => getFlowStep,
242
243
  getGetAuthAttemptUrl: () => getGetAuthAttemptUrl,
244
+ getGetBrandingByIdUrl: () => getGetBrandingByIdUrl,
245
+ getGetClaimStatusUrl: () => getGetClaimStatusUrl,
243
246
  getGetFlowDefinitionUrl: () => getGetFlowDefinitionUrl,
244
247
  getGetFlowStepUrl: () => getGetFlowStepUrl,
245
248
  getGetHealthUrl: () => getGetHealthUrl,
246
- getGetKeysUrl: () => getGetKeysUrl,
247
249
  getGetLiveUrl: () => getGetLiveUrl,
248
250
  getGetMySessionUrl: () => getGetMySessionUrl,
249
251
  getGetMyUserUrl: () => getGetMyUserUrl,
250
- getGetOpenIDConfigurationUrl: () => getGetOpenIDConfigurationUrl,
251
252
  getGetProjectUrl: () => getGetProjectUrl,
252
253
  getGetReadyUrl: () => getGetReadyUrl,
253
254
  getGetSchemaByIdUrl: () => getGetSchemaByIdUrl,
254
255
  getGetSessionUrl: () => getGetSessionUrl,
255
256
  getGetTeamUrl: () => getGetTeamUrl,
256
- getGetTokenUrl: () => getGetTokenUrl,
257
257
  getGetUserByIDUrl: () => getGetUserByIDUrl,
258
- getGetUserInfoUrl: () => getGetUserInfoUrl,
259
258
  getHealth: () => getHealth,
260
- getIntrospectUrl: () => getIntrospectUrl,
259
+ getInitClaimUrl: () => getInitClaimUrl,
261
260
  getIssueChallengeUrl: () => getIssueChallengeUrl,
262
- getKeys: () => getKeys,
261
+ getListBrandingUrl: () => getListBrandingUrl,
263
262
  getListFlowDefinitionsUrl: () => getListFlowDefinitionsUrl,
264
263
  getListSchemasUrl: () => getListSchemasUrl,
265
- getListSessionsUrl: () => getListSessionsUrl,
264
+ getListUserPasskeysUrl: () => getListUserPasskeysUrl,
265
+ getListUserTeamsUrl: () => getListUserTeamsUrl,
266
266
  getListUsersUrl: () => getListUsersUrl,
267
267
  getLive: () => getLive,
268
268
  getMySession: () => getMySession,
269
269
  getMyUser: () => getMyUser,
270
- getOpenIDConfiguration: () => getOpenIDConfiguration,
271
270
  getPatchProjectUrl: () => getPatchProjectUrl,
272
271
  getProject: () => getProject,
273
272
  getQueryProjectsUrl: () => getQueryProjectsUrl,
273
+ getQuerySessionsUrl: () => getQuerySessionsUrl,
274
+ getQueryTeamsUrl: () => getQueryTeamsUrl,
274
275
  getReady: () => getReady,
275
276
  getRevokeMySessionUrl: () => getRevokeMySessionUrl,
276
277
  getRevokeSessionUrl: () => getRevokeSessionUrl,
277
- getRevokeTokenUrl: () => getRevokeTokenUrl,
278
278
  getSchemaById: () => getSchemaById,
279
279
  getSession: () => getSession,
280
280
  getSetUserPasswordUrl: () => getSetUserPasswordUrl,
281
- getSubmitFlowEventUrl: () => getSubmitFlowEventUrl,
282
281
  getSubmitFlowStepUrl: () => getSubmitFlowStepUrl,
283
282
  getTeam: () => getTeam,
284
- getToken: () => getToken,
285
283
  getUpdateFlowDefinitionUrl: () => getUpdateFlowDefinitionUrl,
284
+ getUpdateTeamUrl: () => getUpdateTeamUrl,
286
285
  getUserByID: () => getUserByID,
287
- getUserInfo: () => getUserInfo,
288
286
  getVerifyChallengeProofUrl: () => getVerifyChallengeProofUrl,
289
- introspect: () => introspect,
287
+ initClaim: () => initClaim,
290
288
  issueChallenge: () => issueChallenge,
289
+ listBranding: () => listBranding,
291
290
  listFlowDefinitions: () => listFlowDefinitions,
292
291
  listSchemas: () => listSchemas,
293
- listSessions: () => listSessions,
292
+ listUserPasskeys: () => listUserPasskeys,
293
+ listUserTeams: () => listUserTeams,
294
294
  listUsers: () => listUsers,
295
295
  patchProject: () => patchProject,
296
296
  queryProjects: () => queryProjects,
297
+ querySessions: () => querySessions,
298
+ queryTeams: () => queryTeams,
297
299
  revokeMySession: () => revokeMySession,
298
300
  revokeSession: () => revokeSession,
299
- revokeToken: () => revokeToken,
300
301
  setUserPassword: () => setUserPassword,
301
- submitFlowEvent: () => submitFlowEvent,
302
302
  submitFlowStep: () => submitFlowStep,
303
303
  updateFlowDefinition: () => updateFlowDefinition,
304
+ updateTeam: () => updateTeam,
304
305
  verifyChallengeProof: () => verifyChallengeProof
305
306
  });
306
- const getGetOpenIDConfigurationUrl = () => {
307
- return `${getProxyPath()}/.well-known/openid-configuration`;
308
- };
309
- /**
310
- * Retrieve the OpenID Connect configuration
311
- * @summary Get OpenID Connect configuration
312
- */
313
- const getOpenIDConfiguration = async (options) => {
314
- return customFetch(getGetOpenIDConfigurationUrl(), {
315
- ...options,
316
- method: "GET"
317
- });
318
- };
319
- const getAuthorizeGetUrl = (params) => {
320
- const normalizedParams = new URLSearchParams();
321
- Object.entries(params || {}).forEach(([key, value]) => {
322
- if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
323
- });
324
- const stringifiedParams = normalizedParams.toString();
325
- return stringifiedParams.length > 0 ? `${getProxyPath()}/auth/authorize?${stringifiedParams}` : `${getProxyPath()}/auth/authorize`;
326
- };
327
- /**
328
- * @summary Authorize a user
329
- */
330
- const authorizeGet = async (params, options) => {
331
- return customFetch(getAuthorizeGetUrl(params), {
332
- ...options,
333
- method: "GET"
334
- });
335
- };
336
- const getAuthorizeDeviceUrl = (params) => {
337
- const normalizedParams = new URLSearchParams();
338
- Object.entries(params || {}).forEach(([key, value]) => {
339
- if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
340
- });
341
- const stringifiedParams = normalizedParams.toString();
342
- return stringifiedParams.length > 0 ? `${getProxyPath()}/auth/device-authorization?${stringifiedParams}` : `${getProxyPath()}/auth/device-authorization`;
343
- };
344
- /**
345
- * @summary Authorize a device
346
- */
347
- const authorizeDevice = async (params, options) => {
348
- return customFetch(getAuthorizeDeviceUrl(params), {
349
- ...options,
350
- method: "GET"
351
- });
352
- };
353
- const getEndSessionUrl = (params) => {
354
- const normalizedParams = new URLSearchParams();
355
- Object.entries(params || {}).forEach(([key, value]) => {
356
- if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
357
- });
358
- const stringifiedParams = normalizedParams.toString();
359
- return stringifiedParams.length > 0 ? `${getProxyPath()}/auth/end-session?${stringifiedParams}` : `${getProxyPath()}/auth/end-session`;
360
- };
361
- /**
362
- * @summary End a session
363
- */
364
- const endSession = async (params, options) => {
365
- return customFetch(getEndSessionUrl(params), {
366
- ...options,
367
- method: "GET"
368
- });
369
- };
370
- const getIntrospectUrl = () => {
371
- return `${getProxyPath()}/auth/introspect`;
372
- };
373
- /**
374
- * @summary Introspect a token
375
- */
376
- const introspect = async (introspectBody, options) => {
377
- const formUrlEncoded = new URLSearchParams();
378
- formUrlEncoded.append(`token`, introspectBody.token);
379
- if (introspectBody.token_type_hint !== void 0) formUrlEncoded.append(`token_type_hint`, introspectBody.token_type_hint);
380
- return customFetch(getIntrospectUrl(), {
381
- ...options,
382
- method: "POST",
383
- headers: {
384
- "Content-Type": "application/x-www-form-urlencoded",
385
- ...options?.headers
386
- },
387
- body: formUrlEncoded
388
- });
389
- };
390
- const getGetKeysUrl = () => {
391
- return `${getProxyPath()}/auth/keys`;
392
- };
393
- /**
394
- * @summary Get public keys
395
- */
396
- const getKeys = async (options) => {
397
- return customFetch(getGetKeysUrl(), {
398
- ...options,
399
- method: "GET"
400
- });
401
- };
402
- const getRevokeTokenUrl = () => {
403
- return `${getProxyPath()}/auth/revoke`;
404
- };
405
- /**
406
- * @summary Revoke an access token or refresh token
407
- */
408
- const revokeToken = async (revokeTokenBody, options) => {
409
- const formUrlEncoded = new URLSearchParams();
410
- if (revokeTokenBody.token !== void 0) formUrlEncoded.append(`token`, revokeTokenBody.token);
411
- if (revokeTokenBody.token_type_hint !== void 0) formUrlEncoded.append(`token_type_hint`, revokeTokenBody.token_type_hint);
412
- return customFetch(getRevokeTokenUrl(), {
413
- ...options,
414
- method: "POST",
415
- headers: {
416
- "Content-Type": "application/x-www-form-urlencoded",
417
- ...options?.headers
418
- },
419
- body: formUrlEncoded
420
- });
421
- };
422
- const getGetTokenUrl = () => {
423
- return `${getProxyPath()}/auth/token`;
424
- };
425
- /**
426
- * @summary Get access token
427
- */
428
- const getToken = async (getTokenBody, options) => {
429
- const formUrlEncoded = new URLSearchParams();
430
- if (getTokenBody.code !== void 0) formUrlEncoded.append(`code`, getTokenBody.code);
431
- if (getTokenBody.client_assertion !== void 0) formUrlEncoded.append(`client_assertion`, getTokenBody.client_assertion);
432
- if (getTokenBody.client_assertion_type !== void 0) formUrlEncoded.append(`client_assertion_type`, getTokenBody.client_assertion_type);
433
- if (getTokenBody.client_id !== void 0) formUrlEncoded.append(`client_id`, getTokenBody.client_id);
434
- if (getTokenBody.client_secret !== void 0) formUrlEncoded.append(`client_secret`, getTokenBody.client_secret);
435
- if (getTokenBody.code_verifier !== void 0) formUrlEncoded.append(`code_verifier`, getTokenBody.code_verifier);
436
- if (getTokenBody.grant_type !== void 0) formUrlEncoded.append(`grant_type`, getTokenBody.grant_type);
437
- if (getTokenBody.redirect_uri !== void 0) formUrlEncoded.append(`redirect_uri`, getTokenBody.redirect_uri);
438
- return customFetch(getGetTokenUrl(), {
439
- ...options,
440
- method: "POST",
441
- headers: {
442
- "Content-Type": "application/x-www-form-urlencoded",
443
- ...options?.headers
444
- },
445
- body: formUrlEncoded
446
- });
447
- };
448
- const getGetUserInfoUrl = () => {
449
- return `${getProxyPath()}/auth/userinfo`;
450
- };
451
- /**
452
- * @summary Get user info
453
- */
454
- const getUserInfo = async (options) => {
455
- return customFetch(getGetUserInfoUrl(), {
456
- ...options,
457
- method: "GET"
458
- });
459
- };
460
307
  const getGetHealthUrl = () => {
461
308
  return `${getProxyPath()}/healthz`;
462
309
  };
@@ -552,6 +399,40 @@ const getUserByID = async (userId, params, options) => {
552
399
  method: "GET"
553
400
  });
554
401
  };
402
+ const getDeleteUserByIDUrl = (userId, params) => {
403
+ const normalizedParams = new URLSearchParams();
404
+ Object.entries(params || {}).forEach(([key, value]) => {
405
+ if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
406
+ });
407
+ const stringifiedParams = normalizedParams.toString();
408
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/users/${userId}?${stringifiedParams}` : `${getProxyPath()}/users/${userId}`;
409
+ };
410
+ /**
411
+ * @summary Delete user by ID
412
+ */
413
+ const deleteUserByID = async (userId, params, options) => {
414
+ return customFetch(getDeleteUserByIDUrl(userId, params), {
415
+ ...options,
416
+ method: "DELETE"
417
+ });
418
+ };
419
+ const getListUserPasskeysUrl = (userId, params) => {
420
+ const normalizedParams = new URLSearchParams();
421
+ Object.entries(params || {}).forEach(([key, value]) => {
422
+ if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
423
+ });
424
+ const stringifiedParams = normalizedParams.toString();
425
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/users/${userId}/passkeys?${stringifiedParams}` : `${getProxyPath()}/users/${userId}/passkeys`;
426
+ };
427
+ /**
428
+ * @summary List user passkeys
429
+ */
430
+ const listUserPasskeys = async (userId, params, options) => {
431
+ return customFetch(getListUserPasskeysUrl(userId, params), {
432
+ ...options,
433
+ method: "GET"
434
+ });
435
+ };
555
436
  const getSetUserPasswordUrl = (userId, params) => {
556
437
  const normalizedParams = new URLSearchParams();
557
438
  Object.entries(params || {}).forEach(([key, value]) => {
@@ -574,6 +455,31 @@ const setUserPassword = async (userId, setUserPasswordBody, params, options) =>
574
455
  body: JSON.stringify(setUserPasswordBody)
575
456
  });
576
457
  };
458
+ const getListUserTeamsUrl = (userId, params) => {
459
+ const normalizedParams = new URLSearchParams();
460
+ Object.entries(params || {}).forEach(([key, value]) => {
461
+ if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
462
+ });
463
+ const stringifiedParams = normalizedParams.toString();
464
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/users/${userId}/teams?${stringifiedParams}` : `${getProxyPath()}/users/${userId}/teams`;
465
+ };
466
+ /**
467
+ * The user's team roster, ordered by team name. Each entry carries the
468
+ team's name, so a client renders a page without resolving ids one by one.
469
+
470
+ This is the N:N roster and it is not lifecycle ownership: the single team
471
+ that owns the user's lifecycle is reported as `metadata.lifecycle_owner_team_id`
472
+ on the user read endpoints. Memberships the user was removed from are not
473
+ returned.
474
+
475
+ * @summary List the teams a user belongs to
476
+ */
477
+ const listUserTeams = async (userId, params, options) => {
478
+ return customFetch(getListUserTeamsUrl(userId, params), {
479
+ ...options,
480
+ method: "GET"
481
+ });
482
+ };
577
483
  const getGetMyUserUrl = () => {
578
484
  return `${getProxyPath()}/users/me`;
579
485
  };
@@ -595,7 +501,7 @@ the first capability step. Creates a new session implicitly unless
595
501
  `session_id` is provided (for step-up / reauth on an existing session).
596
502
 
597
503
  The response contains an `id` field — the flow handle. Use it as the path
598
- parameter for all subsequent `/flow/{id}/submit` and `/flow/{id}/event` calls.
504
+ parameter for all subsequent `/flow/{id}/submit` calls.
599
505
 
600
506
  The response also sets an encrypted `HttpOnly` cookie (`_zflow`) containing
601
507
  the flow's orchestration state (current step, collected data, history).
@@ -675,26 +581,6 @@ const submitFlowStep = async (id, submitFlowStepBody, options) => {
675
581
  body: JSON.stringify(submitFlowStepBody)
676
582
  });
677
583
  };
678
- const getSubmitFlowEventUrl = (id) => {
679
- return `${getProxyPath()}/flow/${id}/event`;
680
- };
681
- /**
682
- * Submits telemetry or fingerprint data from the frontend.
683
- Does not advance the state machine. Used for risk evaluation.
684
-
685
- * @summary Submit client-side event
686
- */
687
- const submitFlowEvent = async (id, submitFlowEventBody, options) => {
688
- return customFetch(getSubmitFlowEventUrl(id), {
689
- ...options,
690
- method: "POST",
691
- headers: {
692
- "Content-Type": "application/json",
693
- ...options?.headers
694
- },
695
- body: JSON.stringify(submitFlowEventBody)
696
- });
697
- };
698
584
  const getCreateAuthAttemptUrl = () => {
699
585
  return `${getProxyPath()}/auth_attempts`;
700
586
  };
@@ -882,6 +768,68 @@ const patchProject = async (projectId, patchProjectBody, options) => {
882
768
  body: JSON.stringify(patchProjectBody)
883
769
  });
884
770
  };
771
+ const getInitClaimUrl = (projectId) => {
772
+ return `${getProxyPath()}/projects/${projectId}/claim/init`;
773
+ };
774
+ /**
775
+ * Starts a claim challenge for an unclaimed project. Authenticated by the
776
+ project secret, this mints a single-use, short-lived challenge and returns
777
+ the `claim_url` the developer opens in a browser to complete the claim,
778
+ together with the `challenge_id` the CLI polls with. The exact expiry is
779
+ carried by `expires_at` on the response.
780
+
781
+ * @summary Initialize a project claim
782
+ */
783
+ const initClaim = async (projectId, options) => {
784
+ return customFetch(getInitClaimUrl(projectId), {
785
+ ...options,
786
+ method: "POST"
787
+ });
788
+ };
789
+ const getGetClaimStatusUrl = (projectId, params) => {
790
+ const normalizedParams = new URLSearchParams();
791
+ Object.entries(params || {}).forEach(([key, value]) => {
792
+ if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
793
+ });
794
+ const stringifiedParams = normalizedParams.toString();
795
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/projects/${projectId}/claim/status?${stringifiedParams}` : `${getProxyPath()}/projects/${projectId}/claim/status`;
796
+ };
797
+ /**
798
+ * Polled by the CLI while a browser completes the claim. Authorized by the
799
+ project secret that initiated the challenge. Returns `pending`, or
800
+ `completed` with the owning team, the claim timestamp, and the dashboard
801
+ URL once the browser leg has finished.
802
+
803
+ * @summary Get claim status
804
+ */
805
+ const getClaimStatus = async (projectId, params, options) => {
806
+ return customFetch(getGetClaimStatusUrl(projectId, params), {
807
+ ...options,
808
+ method: "GET"
809
+ });
810
+ };
811
+ const getCompleteClaimUrl = (projectId) => {
812
+ return `${getProxyPath()}/projects/${projectId}/claim/complete`;
813
+ };
814
+ /**
815
+ * Called by the browser after the developer authenticates on the claim page.
816
+ Authenticated by the `__nextgen_session` cookie, it attaches the project to
817
+ the developer's personal team using the `challenge_id` from the claim URL as
818
+ its single-use, browser-safe authorization.
819
+
820
+ * @summary Complete a project claim
821
+ */
822
+ const completeClaim = async (projectId, completeClaimBody, options) => {
823
+ return customFetch(getCompleteClaimUrl(projectId), {
824
+ ...options,
825
+ method: "POST",
826
+ headers: {
827
+ "Content-Type": "application/json",
828
+ ...options?.headers
829
+ },
830
+ body: JSON.stringify(completeClaimBody)
831
+ });
832
+ };
885
833
  const getCreateSessionUrl = () => {
886
834
  return `${getProxyPath()}/sessions`;
887
835
  };
@@ -894,8 +842,16 @@ Use this explicitly when you want to:
894
842
  can be correlated with the eventual authenticated session from the start.
895
843
  - Track anonymous state (bot detection, device fingerprint) that survives until authentication.
896
844
 
897
- The returned `session_token` authorises GET and DELETE on this session.
898
- It is superseded when a handoff exchange completes — clients must replace it at that point.
845
+ Creating a session is an app-plane operation on the project credential
846
+ (`session.write`). The returned `session_token` is a session credential for the
847
+ end-user client, not a management scope: it is delivered as the
848
+ `__nextgen_session` cookie and authorises the self-service operations
849
+ `GET /sessions/me` and `DELETE /sessions/me` (`nextgenSession` scheme). The
850
+ by-id operations `GET /sessions/{session_id}` and `DELETE /sessions/{session_id}`
851
+ are operator endpoints and require `session.read` / `session.delete` instead.
852
+
853
+ The `session_token` is superseded when a handoff exchange completes — clients must
854
+ replace it at that point.
899
855
 
900
856
  Anonymous sessions expire aggressively (10-minute TTL). The TTL resets to the configured
901
857
  full session TTL when the first authentication factor is written via a completing `auth_attempt`.
@@ -913,24 +869,30 @@ const createSession = async (createSessionBody, options) => {
913
869
  body: JSON.stringify(createSessionBody)
914
870
  });
915
871
  };
916
- const getListSessionsUrl = (params) => {
872
+ const getQuerySessionsUrl = (params) => {
917
873
  const normalizedParams = new URLSearchParams();
918
874
  Object.entries(params || {}).forEach(([key, value]) => {
919
875
  if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
920
876
  });
921
877
  const stringifiedParams = normalizedParams.toString();
922
- return stringifiedParams.length > 0 ? `${getProxyPath()}/sessions?${stringifiedParams}` : `${getProxyPath()}/sessions`;
878
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/sessions/query?${stringifiedParams}` : `${getProxyPath()}/sessions/query`;
923
879
  };
924
880
  /**
925
- * Returns a paginated list of sessions for a project.
926
- Requires a project service key (OAuth2 client credentials).
881
+ * Returns the sessions of a project, paginated with a cursor.
882
+ Sessions of every lifecycle state are returned; each carries its `state`.
883
+ Requires `session.read` permission.
927
884
 
928
- * @summary List sessions
885
+ * @summary Query sessions
929
886
  */
930
- const listSessions = async (params, options) => {
931
- return customFetch(getListSessionsUrl(params), {
887
+ const querySessions = async (querySessionsBody, params, options) => {
888
+ return customFetch(getQuerySessionsUrl(params), {
932
889
  ...options,
933
- method: "GET"
890
+ method: "POST",
891
+ headers: {
892
+ "Content-Type": "application/json",
893
+ ...options?.headers
894
+ },
895
+ body: JSON.stringify(querySessionsBody)
934
896
  });
935
897
  };
936
898
  const getExchangeHandoffUrl = (params) => {
@@ -1006,10 +968,15 @@ const getRevokeSessionUrl = (sessionId, params) => {
1006
968
  return stringifiedParams.length > 0 ? `${getProxyPath()}/sessions/${sessionId}?${stringifiedParams}` : `${getProxyPath()}/sessions/${sessionId}`;
1007
969
  };
1008
970
  /**
1009
- * Revokes the session immediately (`state: revoked`). This is the logout operation.
971
+ * Permanently deletes the session, terminating it immediately.
972
+
973
+ This is the operator revoke path and requires the `session.delete` scope on a
974
+ project-bound credential. End-user logout with the `__nextgen_session` cookie is
975
+ `DELETE /sessions/me` (`nextgenSession` scheme).
1010
976
 
1011
- The session_token issued at creation (or superseded by a handoff exchange) is required.
1012
- After revocation, any tokens derived from this session are invalidated.
977
+ Idempotent: deleting a session that does not exist (or was already deleted)
978
+ still returns 204. After deletion, any tokens derived from the session are
979
+ invalidated.
1013
980
 
1014
981
  * @summary Revoke session
1015
982
  */
@@ -1042,10 +1009,12 @@ const getRevokeMySessionUrl = () => {
1042
1009
  return `${getProxyPath()}/sessions/me`;
1043
1010
  };
1044
1011
  /**
1045
- * Revokes the session immediately (`state: revoked`). This is the logout operation.
1012
+ * Logs out by permanently deleting the session.
1046
1013
 
1047
- The __nextgen_session cookie issued at creation (or superseded by a handoff exchange) is required.
1048
- After revocation, any tokens derived from this session are invalidated including the cookie itself, which is cleared in the response.
1014
+ The `__nextgen_session` cookie issued at creation (or superseded by a handoff
1015
+ exchange) is required. Idempotent: if the session is already gone this still
1016
+ returns 204. Any tokens derived from the session are invalidated, and the
1017
+ cookie itself is cleared in the response.
1049
1018
 
1050
1019
  * @summary Revoke my session
1051
1020
  */
@@ -1064,10 +1033,12 @@ const getCreateSchemaUrl = (params) => {
1064
1033
  return stringifiedParams.length > 0 ? `${getProxyPath()}/schemas?${stringifiedParams}` : `${getProxyPath()}/schemas`;
1065
1034
  };
1066
1035
  /**
1067
- * Create a new schema. The schema definition must include a unique $id field,
1068
- which will be used to identify the schema in future requests. The $id must
1069
- be a valid URI and should ideally point to the location where the schema
1070
- can be accessed.
1036
+ * Create a new schema. The optional `$id` field is the JSON Schema document
1037
+ URI used to identify the schema in future requests (GitOps-stable identity,
1038
+ not a free-form resource primary key). When `$id` is omitted, the server
1039
+ generates a `sch_*` URL. When provided, `$id` must be unique within the
1040
+ project and should ideally be a valid URI pointing at where the schema can
1041
+ be accessed.
1071
1042
 
1072
1043
  The schema can either be a concrete schema, e.g. a user schema, or a
1073
1044
  schema-url which will be resolved by the server.
@@ -1230,93 +1201,200 @@ const deleteFlowDefinition = async (id, params, options) => {
1230
1201
  method: "DELETE"
1231
1202
  });
1232
1203
  };
1233
- const getActivateFlowDefinitionUrl = (id, params) => {
1204
+ const getCreateTeamUrl = (params) => {
1205
+ const normalizedParams = new URLSearchParams();
1206
+ Object.entries(params || {}).forEach(([key, value]) => {
1207
+ if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
1208
+ });
1209
+ const stringifiedParams = normalizedParams.toString();
1210
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/teams?${stringifiedParams}` : `${getProxyPath()}/teams`;
1211
+ };
1212
+ /**
1213
+ * @summary Create team
1214
+ */
1215
+ const createTeam = async (createTeamBody, params, options) => {
1216
+ return customFetch(getCreateTeamUrl(params), {
1217
+ ...options,
1218
+ method: "POST",
1219
+ headers: {
1220
+ "Content-Type": "application/json",
1221
+ ...options?.headers
1222
+ },
1223
+ body: JSON.stringify(createTeamBody)
1224
+ });
1225
+ };
1226
+ const getQueryTeamsUrl = (params) => {
1234
1227
  const normalizedParams = new URLSearchParams();
1235
1228
  Object.entries(params || {}).forEach(([key, value]) => {
1236
1229
  if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
1237
1230
  });
1238
1231
  const stringifiedParams = normalizedParams.toString();
1239
- return stringifiedParams.length > 0 ? `${getProxyPath()}/flow_definitions/${id}/activate?${stringifiedParams}` : `${getProxyPath()}/flow_definitions/${id}/activate`;
1232
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/teams/query?${stringifiedParams}` : `${getProxyPath()}/teams/query`;
1240
1233
  };
1241
1234
  /**
1242
- * Activate a flow definition by transitioning it from a `draft` state to an `active` state.
1243
- Alternatively, the status of a flow definition can also be set via the `POST /flow_definitions` and `PUT /flow_definitions/{id}` endpoints by setting the `status` attribute in the flow definition payload.
1235
+ * Returns the teams of a project, paginated with a cursor.
1236
+ Teams of every lifecycle status are returned; each carries its `status`.
1244
1237
 
1245
- * @summary Activate a flow definition by ID.
1238
+ * @summary Query teams
1246
1239
  */
1247
- const activateFlowDefinition = async (id, params, options) => {
1248
- return customFetch(getActivateFlowDefinitionUrl(id, params), {
1240
+ const queryTeams = async (queryTeamsBody, params, options) => {
1241
+ return customFetch(getQueryTeamsUrl(params), {
1249
1242
  ...options,
1250
- method: "POST"
1243
+ method: "POST",
1244
+ headers: {
1245
+ "Content-Type": "application/json",
1246
+ ...options?.headers
1247
+ },
1248
+ body: JSON.stringify(queryTeamsBody)
1251
1249
  });
1252
1250
  };
1253
- const getDeactivateFlowDefinitionUrl = (id, params) => {
1251
+ const getGetTeamUrl = (teamId, params) => {
1254
1252
  const normalizedParams = new URLSearchParams();
1255
1253
  Object.entries(params || {}).forEach(([key, value]) => {
1256
1254
  if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
1257
1255
  });
1258
1256
  const stringifiedParams = normalizedParams.toString();
1259
- return stringifiedParams.length > 0 ? `${getProxyPath()}/flow_definitions/${id}/deactivate?${stringifiedParams}` : `${getProxyPath()}/flow_definitions/${id}/deactivate`;
1257
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/teams/${teamId}?${stringifiedParams}` : `${getProxyPath()}/teams/${teamId}`;
1260
1258
  };
1261
1259
  /**
1262
- * Deactivates a flow definition in the `active` state by transitioning it to the `draft` state.
1263
- Flow definitions in `draft` state cannot be used to start new flows. Existing flows that use the deactivated flow definition must gracefully handle this.
1264
- Alternatively, the status of a flow definition can also be set via the `POST /flow_definitions` and `PUT /flow_definitions/{id}` endpoints by setting the `status` attribute in the flow definition payload.
1260
+ * Returns a Team by its id.
1265
1261
 
1266
- * @summary Deactivate a flow definition by ID.
1262
+ * @summary Get team
1267
1263
  */
1268
- const deactivateFlowDefinition = async (id, params, options) => {
1269
- return customFetch(getDeactivateFlowDefinitionUrl(id, params), {
1264
+ const getTeam = async (teamId, params, options) => {
1265
+ return customFetch(getGetTeamUrl(teamId, params), {
1270
1266
  ...options,
1271
- method: "POST"
1267
+ method: "GET"
1272
1268
  });
1273
1269
  };
1274
- const getCreateTeamUrl = (params) => {
1270
+ const getDeleteTeamUrl = (teamId, params) => {
1275
1271
  const normalizedParams = new URLSearchParams();
1276
1272
  Object.entries(params || {}).forEach(([key, value]) => {
1277
1273
  if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
1278
1274
  });
1279
1275
  const stringifiedParams = normalizedParams.toString();
1280
- return stringifiedParams.length > 0 ? `${getProxyPath()}/teams?${stringifiedParams}` : `${getProxyPath()}/teams`;
1276
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/teams/${teamId}?${stringifiedParams}` : `${getProxyPath()}/teams/${teamId}`;
1281
1277
  };
1282
1278
  /**
1283
- * @summary Create team
1279
+ * Deactivates the team.
1280
+
1281
+ The team is tombstoned rather than erased: it stays readable through
1282
+ getTeam with status `deactivated`. Its memberships are removed and the
1283
+ users whose lifecycle it owns are deactivated with it.
1284
+
1285
+ The request is idempotent. Deleting a team that is already deactivated
1286
+ or doesn't exist succeeds without changing anything.
1287
+
1288
+ * @summary Delete team
1284
1289
  */
1285
- const createTeam = async (params, createTeamBody, options) => {
1286
- return customFetch(getCreateTeamUrl(params), {
1290
+ const deleteTeam = async (teamId, params, options) => {
1291
+ return customFetch(getDeleteTeamUrl(teamId, params), {
1287
1292
  ...options,
1288
- method: "POST",
1293
+ method: "DELETE"
1294
+ });
1295
+ };
1296
+ const getUpdateTeamUrl = (teamId, params) => {
1297
+ const normalizedParams = new URLSearchParams();
1298
+ Object.entries(params || {}).forEach(([key, value]) => {
1299
+ if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
1300
+ });
1301
+ const stringifiedParams = normalizedParams.toString();
1302
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/teams/${teamId}?${stringifiedParams}` : `${getProxyPath()}/teams/${teamId}`;
1303
+ };
1304
+ /**
1305
+ * Update team. Only active teams can be updated.
1306
+
1307
+ * @summary Update team
1308
+ */
1309
+ const updateTeam = async (teamId, updateTeamBody, params, options) => {
1310
+ return customFetch(getUpdateTeamUrl(teamId, params), {
1311
+ ...options,
1312
+ method: "PATCH",
1289
1313
  headers: {
1290
1314
  "Content-Type": "application/json",
1291
1315
  ...options?.headers
1292
1316
  },
1293
- body: JSON.stringify(createTeamBody)
1317
+ body: JSON.stringify(updateTeamBody)
1294
1318
  });
1295
1319
  };
1296
- const getGetTeamUrl = (teamId, params) => {
1320
+ const getCreateBrandingUrl = (params) => {
1297
1321
  const normalizedParams = new URLSearchParams();
1298
1322
  Object.entries(params || {}).forEach(([key, value]) => {
1299
1323
  if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
1300
1324
  });
1301
1325
  const stringifiedParams = normalizedParams.toString();
1302
- return stringifiedParams.length > 0 ? `${getProxyPath()}/teams/${teamId}?${stringifiedParams}` : `${getProxyPath()}/teams/${teamId}`;
1326
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/branding?${stringifiedParams}` : `${getProxyPath()}/branding`;
1303
1327
  };
1304
1328
  /**
1305
- * Returns the current state of a team.
1329
+ * Publishes a new immutable branding revision for the project. Branding
1330
+ revisions cannot be updated or deleted; every edit publishes a new
1331
+ revision, and flow responses resolve the latest revision per project
1332
+ (see ADR 040).
1306
1333
 
1307
- * @summary Get team
1334
+ The `liquid_template` is validated lexically on save (size, encoding,
1335
+ banned patterns such as `<script>` tags, inline event handlers, and the
1336
+ `| raw` filter). Authoritative LiquidJS validation runs at authoring
1337
+ time via `zitadel plan` / `zitadel apply`.
1338
+
1339
+ * @summary Publish a new branding revision
1308
1340
  */
1309
- const getTeam = async (teamId, params, options) => {
1310
- return customFetch(getGetTeamUrl(teamId, params), {
1341
+ const createBranding = async (createBrandingBody, params, options) => {
1342
+ return customFetch(getCreateBrandingUrl(params), {
1311
1343
  ...options,
1312
- method: "GET"
1344
+ method: "POST",
1345
+ headers: {
1346
+ "Content-Type": "application/json",
1347
+ ...options?.headers
1348
+ },
1349
+ body: JSON.stringify(createBrandingBody)
1313
1350
  });
1314
1351
  };
1315
- //#endregion
1316
- //#region ../api/dist/runtime/api-factory.mjs
1317
- /**
1318
- * Factory for the typed Zitadel client every consumer reaches for.
1319
- *
1352
+ const getListBrandingUrl = (params) => {
1353
+ const normalizedParams = new URLSearchParams();
1354
+ Object.entries(params || {}).forEach(([key, value]) => {
1355
+ if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
1356
+ });
1357
+ const stringifiedParams = normalizedParams.toString();
1358
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/branding?${stringifiedParams}` : `${getProxyPath()}/branding`;
1359
+ };
1360
+ /**
1361
+ * Lists branding revisions for the project, newest first, capped at the
1362
+ 100 most recent. The first entry is the revision flow responses
1363
+ currently resolve. Deliberately unpaginated in v1 — list endpoints
1364
+ gain a real query mechanism together (ADR 031); advertising pagination
1365
+ parameters the server ignores would be worse than none.
1366
+
1367
+ * @summary List branding revisions
1368
+ */
1369
+ const listBranding = async (params, options) => {
1370
+ return customFetch(getListBrandingUrl(params), {
1371
+ ...options,
1372
+ method: "GET"
1373
+ });
1374
+ };
1375
+ const getGetBrandingByIdUrl = (id, params) => {
1376
+ const normalizedParams = new URLSearchParams();
1377
+ Object.entries(params || {}).forEach(([key, value]) => {
1378
+ if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
1379
+ });
1380
+ const stringifiedParams = normalizedParams.toString();
1381
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/branding/${id}?${stringifiedParams}` : `${getProxyPath()}/branding/${id}`;
1382
+ };
1383
+ /**
1384
+ * Retrieves a single branding revision, including its stored configuration.
1385
+ * @summary Get a branding revision by ID
1386
+ */
1387
+ const getBrandingById = async (id, params, options) => {
1388
+ return customFetch(getGetBrandingByIdUrl(id, params), {
1389
+ ...options,
1390
+ method: "GET"
1391
+ });
1392
+ };
1393
+ //#endregion
1394
+ //#region ../api/dist/runtime/api-factory.mjs
1395
+ /**
1396
+ * Factory for the typed Zitadel client every consumer reaches for.
1397
+ *
1320
1398
  * Wraps the orval-generated endpoints in a {@link Proxy} so each
1321
1399
  * generated function sees the right base URL and (optionally) the
1322
1400
  * right bearer token at call time — without the caller having to
@@ -1338,11 +1416,12 @@ const getTeam = async (teamId, params, options) => {
1338
1416
  * generated orval function:
1339
1417
  *
1340
1418
  * const client = createZitadelClient({ baseUrl, token });
1341
- * await client.createProject({ previewOrigins: [] });
1419
+ * await client.createProject({ preview_origins: [] });
1342
1420
  * await client.createSchema(body, { project_id });
1343
1421
  */
1344
1422
  function createZitadelClient(opts) {
1345
- const baseUrl = opts.baseUrl.replace(/\/+$/, "");
1423
+ let baseUrl = opts.baseUrl;
1424
+ while (baseUrl.endsWith("/")) baseUrl = baseUrl.slice(0, -1);
1346
1425
  return new Proxy(zitadelNextGen_exports, { get(target, prop, receiver) {
1347
1426
  const value = Reflect.get(target, prop, receiver);
1348
1427
  if (typeof value !== "function") return value;
@@ -1353,15 +1432,6 @@ function createZitadelClient(opts) {
1353
1432
  };
1354
1433
  } });
1355
1434
  }
1356
- /**
1357
- * Legacy entry point: builds a client with only the base URL set, no
1358
- * token. Components and SDKs that don't carry an auth token (browser
1359
- * flows authenticated by platform cookies) still consume this name.
1360
- * Equivalent to `createZitadelClient({ baseUrl: apiBase })`.
1361
- */
1362
- function createApi(apiBase) {
1363
- return createZitadelClient({ baseUrl: apiBase });
1364
- }
1365
1435
  //#endregion
1366
1436
  //#region ../api/dist/runtime/config.mjs
1367
1437
  /**
@@ -1401,7 +1471,10 @@ const apiCache = /* @__PURE__ */ new WeakMap();
1401
1471
  function getApi(project) {
1402
1472
  let api = apiCache.get(project);
1403
1473
  if (!api) {
1404
- api = createApi(project.proxyPath);
1474
+ api = createZitadelClient({
1475
+ baseUrl: project.proxyPath,
1476
+ token: project.publishableKey
1477
+ });
1405
1478
  apiCache.set(project, api);
1406
1479
  }
1407
1480
  return api;
@@ -1489,183 +1562,6 @@ function resolveApi(project, attrs, element) {
1489
1562
  };
1490
1563
  }
1491
1564
  //#endregion
1492
- //#region src/orchestrator/branding-to-tokens.ts
1493
- /**
1494
- * Branding -> CSS token bridge.
1495
- *
1496
- * Per `docs/design/branding/README.md` the orchestrator owns theming. Templates
1497
- * never emit `<style>` blocks; they only structure HTML. This module translates
1498
- * the `Branding` JSON into a CSSStyleSheet of `:host { --zl-* }` declarations
1499
- * and applies it via `shadowRoot.adoptedStyleSheets`.
1500
- *
1501
- * Dark-mode overrides are emitted as `:host([data-theme="dark"]) { ... }`
1502
- * (matching `docs/design/branding/tokens.md`). Resolution between
1503
- * `light | dark | auto` happens in `<zitadel-login>`.
1504
- *
1505
- * Base layer: the design-tokens package ships the full `--zl-*` set as both a
1506
- * `.css` file (for host pages) and a `tokensCss` string (for shadow roots and
1507
- * SSR). We adopt the string into every orchestrator shadow root so atoms
1508
- * paint correctly even when the host page didn't `@import` tokens.css — the
1509
- * orchestrator is meant to drop into any page.
1510
- */
1511
- const RADIUS_MAP = {
1512
- none: "0",
1513
- sm: "0.25rem",
1514
- md: "0.5rem",
1515
- lg: "0.75rem",
1516
- full: "9999px"
1517
- };
1518
- const DENSITY_MAP = {
1519
- compact: {
1520
- "--zl-spacing-03": "0.75rem",
1521
- "--zl-spacing-05": "1.5rem"
1522
- },
1523
- regular: {},
1524
- comfortable: {
1525
- "--zl-spacing-03": "1.25rem",
1526
- "--zl-spacing-05": "2.25rem"
1527
- }
1528
- };
1529
- const PALETTE_MAP = {
1530
- primary: ["--zl-color-surface-default-white"],
1531
- on_primary: ["--zl-color-text-button-default"],
1532
- background: ["--zl-color-surface-default-black"],
1533
- surface: ["--zl-color-surface-default-primary-gray"],
1534
- muted: ["--zl-color-surface-default-secondary-gray"],
1535
- border: ["--zl-color-border-default-gray-200", "--zl-color-border-default-gray-100"],
1536
- text: ["--zl-color-text-primary-white"],
1537
- text_muted: ["--zl-color-text-secondary-gray"],
1538
- link: ["--zl-color-text-subtitle-pink"],
1539
- success: [
1540
- "--zl-color-text-success",
1541
- "--zl-color-border-success",
1542
- "--zl-color-icon-success"
1543
- ],
1544
- warning: ["--zl-color-text-subtitle-orange"],
1545
- error: [
1546
- "--zl-color-text-error",
1547
- "--zl-color-border-error",
1548
- "--zl-color-icon-error"
1549
- ]
1550
- };
1551
- /**
1552
- * Build a CSS string of `:host { --zl-* }` declarations from a Branding
1553
- * payload. Light values land on `:host`; dark overrides land on
1554
- * `:host([data-theme="dark"])`.
1555
- */
1556
- function buildBrandingStylesheet(branding, options = {}) {
1557
- const lightDecls = collectDeclarations(branding);
1558
- const darkPalette = branding?.theme?.dark?.palette;
1559
- const darkDecls = darkPalette ? mapPalette(darkPalette) : {};
1560
- const blocks = [];
1561
- if (Object.keys(lightDecls).length > 0) blocks.push(formatBlock(":host", lightDecls));
1562
- if (Object.keys(darkDecls).length > 0) blocks.push(formatBlock(":host([data-theme=\"dark\"])", darkDecls));
1563
- if (options.resolvedTheme === "dark" && darkPalette) {
1564
- const merged = {
1565
- ...lightDecls,
1566
- ...darkDecls
1567
- };
1568
- blocks.length = 0;
1569
- blocks.push(formatBlock(":host", merged));
1570
- }
1571
- return blocks.join("\n");
1572
- }
1573
- function collectDeclarations(branding) {
1574
- if (!branding) return {};
1575
- const decls = {};
1576
- Object.assign(decls, mapPalette(branding.palette));
1577
- Object.assign(decls, mapTypography(branding.typography));
1578
- Object.assign(decls, mapShape(branding.shape));
1579
- return decls;
1580
- }
1581
- function mapPalette(palette) {
1582
- if (!palette) return {};
1583
- const out = {};
1584
- for (const [key, varNames] of Object.entries(PALETTE_MAP)) {
1585
- const value = palette[key];
1586
- if (typeof value === "string" && value.length > 0) for (const varName of varNames) out[varName] = value;
1587
- }
1588
- return out;
1589
- }
1590
- function mapTypography(typography) {
1591
- if (!typography) return {};
1592
- const out = {};
1593
- if (typography.font_family) {
1594
- out["--zl-font-family-sans"] = typography.font_family;
1595
- out["--zl-font-family-heading"] = typography.font_family;
1596
- }
1597
- if (typography.font_family_mono) out["--zl-font-family-mono"] = typography.font_family_mono;
1598
- const scale = clamp(typography.scale ?? 1, .75, 1.25);
1599
- if (scale !== 1) out["--zl-font-scale"] = `${scale}`;
1600
- return out;
1601
- }
1602
- function mapShape(shape) {
1603
- if (!shape) return {};
1604
- const out = {};
1605
- if (shape.radius && RADIUS_MAP[shape.radius]) {
1606
- const radius = RADIUS_MAP[shape.radius];
1607
- out["--zl-radius-s"] = radius;
1608
- out["--zl-radius-m"] = radius === "0" ? "0" : `calc(${radius} * 1.5)`;
1609
- out["--zl-radius-l"] = radius === "0" ? "0" : `calc(${radius} * 2)`;
1610
- }
1611
- if (shape.density) Object.assign(out, DENSITY_MAP[shape.density]);
1612
- return out;
1613
- }
1614
- function formatBlock(selector, decls) {
1615
- return `${selector} {\n${Object.entries(decls).map(([prop, value]) => ` ${prop}: ${value};`).join("\n")}\n}`;
1616
- }
1617
- function clamp(value, min, max) {
1618
- if (Number.isNaN(value)) return min;
1619
- return Math.min(max, Math.max(min, value));
1620
- }
1621
- let baseTokenSheet;
1622
- function getBaseTokenSheet() {
1623
- if (typeof CSSStyleSheet === "undefined") return void 0;
1624
- if (!baseTokenSheet) {
1625
- baseTokenSheet = new CSSStyleSheet();
1626
- baseTokenSheet.replaceSync(tokensCss.replaceAll(":root,\n[data-theme=\"dark\"]", ":host,\n:host([data-theme=\"dark\"])").replaceAll("[data-theme=\"light\"]", ":host([data-theme=\"light\"])"));
1627
- }
1628
- return baseTokenSheet;
1629
- }
1630
- /**
1631
- * Adopt the design-system base token layer onto a `ShadowRoot`. Safe to call
1632
- * many times — the underlying constructable sheet is shared across every
1633
- * orchestrator instance and de-duplicated in the adopted list.
1634
- */
1635
- function applyBaseTokens(shadowRoot) {
1636
- const sheet = getBaseTokenSheet();
1637
- if (!sheet) return;
1638
- const existing = Array.isArray(shadowRoot.adoptedStyleSheets) ? shadowRoot.adoptedStyleSheets : [];
1639
- if (existing.includes(sheet)) return;
1640
- try {
1641
- shadowRoot.adoptedStyleSheets = [sheet, ...existing];
1642
- } catch {}
1643
- }
1644
- /**
1645
- * Apply a branding payload as a `--zl-*` overrides layer on top of the base
1646
- * token sheet. Subsequent calls replace the previous override sheet so a new
1647
- * branding payload paints cleanly without leaking older declarations.
1648
- *
1649
- * Callers should run `applyBaseTokens(shadowRoot)` first (once per shadow
1650
- * root) so the base values exist before branding patches them.
1651
- */
1652
- function applyBrandingTokens(shadowRoot, branding, resolvedTheme) {
1653
- const css = buildBrandingStylesheet(branding, { resolvedTheme });
1654
- if (typeof CSSStyleSheet === "undefined") return;
1655
- const sheet = new CSSStyleSheet();
1656
- sheet.replaceSync(css);
1657
- const previous = shadowRoot.__zlTokenSheet;
1658
- shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets.filter((s) => s !== previous), sheet];
1659
- shadowRoot.__zlTokenSheet = sheet;
1660
- }
1661
- function resolveTheme(branding) {
1662
- const mode = branding?.theme?.mode ?? "dark";
1663
- if (mode === "light") return "light";
1664
- if (mode === "dark") return "dark";
1665
- if (typeof matchMedia === "function") return matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
1666
- return "dark";
1667
- }
1668
- //#endregion
1669
1565
  //#region ../api/dist/generated/model/index.mjs
1670
1566
  const CreateFlow201BrandingLayout = {
1671
1567
  centered: "centered",
@@ -1728,83 +1624,57 @@ function sanitiseUrl(value, field, issues) {
1728
1624
  }
1729
1625
  }
1730
1626
  //#endregion
1731
- //#region src/orchestrator/font-loader.ts
1627
+ //#region src/orchestrator/exportparts.ts
1732
1628
  /**
1733
- * Document-level font stylesheet injection for `<zitadel-login>`.
1734
- *
1735
- * Two faces can be linked into the host document's `<head>`:
1736
- *
1737
- * - The **design-system default** (`applyDefaultFont`) — the brand body face
1738
- * (Arimo, named first in `--zl-font-family-sans`). The component ships this
1739
- * so the auth screens render in the brand font out of the box, even when the
1740
- * server returns no branding. This is the "default users can override" layer.
1741
- * - The **tenant override** (`applyFontUrl`) — `branding.font_url`, resolved
1742
- * server-side from the app → team → project branding hierarchy.
1629
+ * Forwards atom-internal CSS Shadow Parts through the orchestrator boundary.
1743
1630
  *
1744
- * Both must live at document level, not inside the shadow root: browsers ignore
1745
- * `@font-face` rules declared in shadow-tree stylesheets, so a shadow-scoped
1746
- * link would never register the font faces and every branded font silently
1747
- * falls back to the system stack. `font-family` references inside the shadow
1748
- * tree resolve against document-level faces.
1631
+ * Atoms stamp `part="..."` on their internals and declare the public set in
1632
+ * their manifest (`parts`). Those parts are visible one shadow boundary up
1633
+ * inside the orchestrator's shadow root but not to the embedding page,
1634
+ * because `::part()` pierces a single boundary unless each host in the chain
1635
+ * re-exports (`exportparts`). Templates never author the forwarding (the
1636
+ * sanitiser strips `exportparts`); the orchestrator stamps it after every
1637
+ * render commit so the mapping stays canonical for template-rendered and
1638
+ * gate-patched atoms alike.
1749
1639
  *
1750
- * Both injectors are idempotent: calling with the same URL is a no-op; calling
1751
- * with a new URL replaces the previous link; calling with `null`/`undefined`
1752
- * removes any previously injected link.
1753
- */
1754
- const TENANT_LINK_ID = "zl-font-link";
1755
- const DEFAULT_LINK_ID = "zl-default-font-link";
1756
- /**
1757
- * Design-system default brand font. Arimo is the Figma body face and leads the
1758
- * `--zl-font-family-sans` stack; loading it here makes the unbranded auth UI
1759
- * paint in the brand font instead of the system fallback. Tenants override it
1760
- * with `branding.font_url`. Exported so a privacy- or offline-sensitive
1761
- * deployment can self-host the same face and swap this single URL.
1640
+ * Exported names are mechanical, collision-free, and public contract
1641
+ * (override-ladder Tier 2, same stability rules as token names):
1642
+ * `<atom>-<part>` with the `zl-` prefix dropped — `zl-field`'s `input` part
1643
+ * becomes `zitadel-login::part(field-input)` on the host page. Manifests are
1644
+ * the canonical part catalogue; this module derives, never curates.
1762
1645
  */
1763
- const DEFAULT_BRAND_FONT_HREF = "https://fonts.googleapis.com/css2?family=Arimo:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap";
1646
+ const exportpartsByTag = new Map(manifestRegistry.filter((manifest) => manifest.parts.length > 0).map((manifest) => {
1647
+ const short = manifest.tag.replace(/^zl-/, "");
1648
+ const value = manifest.parts.map((part) => `${part}: ${short}-${part}`).join(", ");
1649
+ return [manifest.tag, value];
1650
+ }));
1764
1651
  /**
1765
- * Inject the tenant's `branding.font_url` stylesheet. Pass `null`/`undefined`
1766
- * to remove it (e.g. when a flow resolves with no tenant font).
1652
+ * Stamps `exportparts` onto every known atom in `scope`. Idempotent call
1653
+ * after each render commit: `unsafeHTML` re-parses replace previously
1654
+ * stamped nodes, and the mandatory-gates patcher appends atoms after
1655
+ * sanitisation.
1767
1656
  */
1768
- function applyFontUrl(shadowRoot, fontUrl) {
1769
- applyLinkedStylesheet(shadowRoot, TENANT_LINK_ID, fontUrl);
1657
+ function stampExportparts(scope) {
1658
+ for (const [tag, value] of exportpartsByTag) for (const el of scope.querySelectorAll(tag)) if (el.getAttribute("exportparts") !== value) el.setAttribute("exportparts", value);
1770
1659
  }
1660
+ //#endregion
1661
+ //#region src/internal/escape-html.ts
1771
1662
  /**
1772
- * Inject the design-system default font stylesheet. Defaults to
1773
- * {@link DEFAULT_BRAND_FONT_HREF}; pass `null` to remove it (the orchestrator
1774
- * does this when a tenant `font_url` takes over, to avoid a redundant request).
1663
+ * Escapes the five HTML-significant characters so a string is safe to
1664
+ * interpolate into either element text or a double-quoted attribute value.
1665
+ *
1666
+ * Used for the orchestrator-owned attribution markup, which is appended after
1667
+ * the Liquid output has already been sanitised — so it can't rely on the
1668
+ * DOMPurify pass and must escape its own (tenant-supplied) values.
1775
1669
  */
1776
- function applyDefaultFont(shadowRoot, href = DEFAULT_BRAND_FONT_HREF) {
1777
- applyLinkedStylesheet(shadowRoot, DEFAULT_LINK_ID, href);
1778
- }
1779
- function applyLinkedStylesheet(shadowRoot, linkId, href) {
1780
- const ownerDocument = shadowRoot.ownerDocument ?? document;
1781
- const existing = ownerDocument.head.querySelector(`link#${linkId}`);
1782
- if (!href) {
1783
- existing?.remove();
1784
- return;
1785
- }
1786
- const link = existing ?? ownerDocument.createElement("link");
1787
- link.id = linkId;
1788
- link.rel = "stylesheet";
1789
- if (link.href !== href) link.href = href;
1790
- if (!existing) ownerDocument.head.appendChild(link);
1791
- }
1792
- //#endregion
1793
- //#region src/internal/escape-html.ts
1794
- /**
1795
- * Escapes the five HTML-significant characters so a string is safe to
1796
- * interpolate into either element text or a double-quoted attribute value.
1797
- *
1798
- * Used for the orchestrator-owned attribution markup, which is appended after
1799
- * the Liquid output has already been sanitised — so it can't rely on the
1800
- * DOMPurify pass and must escape its own (tenant-supplied) values.
1801
- */
1802
- function escapeHtml(value) {
1803
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1670
+ function escapeHtml(value) {
1671
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1804
1672
  }
1805
1673
  //#endregion
1806
1674
  //#region src/orchestrator/mandatory-gates.ts
1807
1675
  const MANDATORY_GATES_MARKER = "ZL_MANDATORY_GATES";
1676
+ /** Every atom tag that participates as a named form field. */
1677
+ const FIELD_ATOM_TAGS = "zl-field, zl-select, zl-checkbox";
1808
1678
  const mandatoryGatesMarkerComment = `<!--${MANDATORY_GATES_MARKER}-->`;
1809
1679
  function patchMandatoryGates(html, step, locale) {
1810
1680
  const template = document.createElement("template");
@@ -1835,7 +1705,7 @@ function hasPrimaryButton(fragment) {
1835
1705
  return Boolean(fragment.querySelector("zl-button[hierarchy=\"primary\"]"));
1836
1706
  }
1837
1707
  function hasFieldFor(fragment, name) {
1838
- for (const field of fragment.querySelectorAll("zl-field")) if (field.getAttribute("name") === name) return true;
1708
+ for (const field of fragment.querySelectorAll(FIELD_ATOM_TAGS)) if (field.getAttribute("name") === name) return true;
1839
1709
  return false;
1840
1710
  }
1841
1711
  function findMarkerComment(fragment) {
@@ -1887,11 +1757,15 @@ function lookup(locale, key) {
1887
1757
  */
1888
1758
  const TEMPLATE_NAMES$1 = { default: "default" };
1889
1759
  /**
1890
- * Maps `error.*` text keys to a field name (Figma inline-error
1891
- * annotations). Shared by the template filters (`fieldError` routes these
1892
- * inline, `formLevelError` suppresses their banner) and by
1893
- * {@link localiseFlowErrorKeys}, which must downgrade them to a banner
1894
- * message when the step doesn't render the mapped field.
1760
+ * Maps catalog `error.*` keys with no `_<rule>` suffix (so
1761
+ * {@link localiseFlowErrorKeys} can't derive the field from the key itself)
1762
+ * to their field name the credential errors with bespoke catalog copy
1763
+ * (`error.email_exists`, `error.invalid_credentials`, …). Rule-suffixed keys
1764
+ * (`error.<field>_<rule>`) derive their field from the key. Consumed only by
1765
+ * {@link localiseFlowErrorKeys}, which stamps the resolved `field` onto the
1766
+ * error; the template filters (`fieldError` / `formLevelError`) then route by
1767
+ * that `field`, and an error whose field the step omits is downgraded to a
1768
+ * fieldless banner message.
1895
1769
  */
1896
1770
  const fieldErrorKeys = {
1897
1771
  "error.email_required": "email",
@@ -1915,9 +1789,18 @@ function createLiquidEngine(options) {
1915
1789
  outputEscape: "escape"
1916
1790
  });
1917
1791
  engine.registerFilter("raw", (value) => stringify(value));
1792
+ const warnedMissingKeys = /* @__PURE__ */ new Set();
1918
1793
  engine.registerFilter("t", function tFilter(key, ...args) {
1919
1794
  const lookupKey = stringify(key);
1920
- return interpolate(options.locale[lookupKey] ?? injectedKeyFallback(options.locale, lookupKey) ?? lookupKey, args.map(stringify));
1795
+ let template = options.locale[lookupKey] ?? injectedKeyFallback(options.locale, lookupKey) ?? fieldLabelFallback(lookupKey);
1796
+ if (template === void 0) {
1797
+ template = lookupKey;
1798
+ if (lookupKey !== "" && !warnedMissingKeys.has(lookupKey)) {
1799
+ warnedMissingKeys.add(lookupKey);
1800
+ console.warn(`[zitadel-login] missing text key "${lookupKey}" — rendering the raw key`);
1801
+ }
1802
+ }
1803
+ return interpolate(template, args.map(stringify));
1921
1804
  });
1922
1805
  /** Resolves `{text_key}.placeholder` — empty when undefined (not the raw key). */
1923
1806
  engine.registerFilter("fieldPlaceholder", (textKey) => {
@@ -1960,17 +1843,30 @@ function createLiquidEngine(options) {
1960
1843
  const name = stringify(fieldName);
1961
1844
  if (!Array.isArray(errors)) return "";
1962
1845
  for (const item of errors) {
1963
- const key = item.text_key ?? "";
1964
- if (fieldErrorKeys[key] === name) return options.locale[key] ?? key;
1846
+ const err = item;
1847
+ if (err.field !== name) continue;
1848
+ return err.text_key ? options.locale[err.text_key] ?? err.text_key : err.message ?? "";
1965
1849
  }
1966
1850
  return "";
1967
1851
  });
1968
- /** True when the error should render as `<zl-alert>`, not on a field. */
1852
+ /**
1853
+ * Automation-hook token for a field name: strips the
1854
+ * `x-auth-methods#` credential prefix so testids stay method-named
1855
+ * (`zitadel-field-password`), while the `name` attribute keeps the
1856
+ * raw wire key. See hookName for the contract.
1857
+ */
1858
+ engine.registerFilter("testid", (fieldName) => hookName(stringify(fieldName)));
1859
+ /**
1860
+ * True when the error should render as `<zl-alert>`, not on a field. A
1861
+ * field-scoped error (`field` set by {@link localiseFlowErrorKeys} when the
1862
+ * step renders that field) routes inline instead; everything else — engine
1863
+ * errors, and orphaned field errors already downgraded to a fieldless
1864
+ * message — falls to the banner.
1865
+ */
1969
1866
  engine.registerFilter("formLevelError", (err) => {
1970
- const key = err?.text_key ?? "";
1971
- return key === "" || !(key in fieldErrorKeys);
1867
+ return !err?.field;
1972
1868
  });
1973
- engine.registerTag("mandatory_gates", {
1869
+ engine.registerTag(MANDATORY_GATES_TAG, {
1974
1870
  parse() {},
1975
1871
  render() {
1976
1872
  return mandatoryGatesMarkerComment;
@@ -1993,6 +1889,22 @@ function injectedKeyFallback(locale, key) {
1993
1889
  for (const { suffix, fallback } of INJECTED_KEY_FALLBACKS) if (key.endsWith(suffix)) return locale[fallback];
1994
1890
  }
1995
1891
  /**
1892
+ * Fallback for field-label keys (`<step>.field.<name>` — see
1893
+ * `FlowField.TextKey` in `internal/domain/flow_field_resolver.go`). Both
1894
+ * halves are tenant-chosen (step names and schema property names), so no
1895
+ * catalog can enumerate the keys; a miss renders a humanised property name
1896
+ * ("dateOfBirth" → "Date of birth") instead of leaking the raw key into the
1897
+ * form. Sub-keys like `.placeholder`/`.help` are excluded — they resolve
1898
+ * through their own filters, which stay empty on a miss.
1899
+ */
1900
+ function fieldLabelFallback(key) {
1901
+ const index = key.lastIndexOf(".field.");
1902
+ if (index === -1) return void 0;
1903
+ const field = key.slice(index + 7);
1904
+ if (field === "" || field.includes(".")) return void 0;
1905
+ return capitaliseFirst(humaniseFieldName(field));
1906
+ }
1907
+ /**
1996
1908
  * Rule-suffix fallbacks for the server's field-validation keys
1997
1909
  * (`error.<field>_<rule>` — see `FlowFieldValidationError.TextKey` in
1998
1910
  * `internal/domain/flow_field_resolver.go`). Field names come from the
@@ -2031,10 +1943,12 @@ const FLOW_ERROR_RULE_FALLBACKS = [
2031
1943
  ];
2032
1944
  const FLOW_ERROR_CATCH_ALL_KEY = "error.field_invalid";
2033
1945
  /**
2034
- * Localises a `step.error` payload of field-validation text keys.
1946
+ * Localises a `step.error` payload of `error.*` catalog keys
1947
+ * field-validation violations and general engine failures (e.g.
1948
+ * `error.invalid_credentials`, `error.passkey_invalid`) alike.
2035
1949
  * Returns `null` unless EVERY `"; "`-joined segment is an `error.*` key
2036
- * — the caller keeps other payloads (outcome names, `auth_attempt.*`
2037
- * diagnostics) verbatim.
1950
+ * — the caller keeps other payloads (outcome tokens such as
1951
+ * `user_not_found`) verbatim.
2038
1952
  *
2039
1953
  * Keys the locale knows pass through as `text_key` entries: the template
2040
1954
  * localises them via `| t`, and `fieldErrorKeys` routes the known ones
@@ -2052,12 +1966,19 @@ function localiseFlowErrorKeys(raw, ctx) {
2052
1966
  function localiseFlowErrorKey(key, ctx) {
2053
1967
  const inlineField = fieldErrorKeys[key];
2054
1968
  const orphanedInline = inlineField !== void 0 && ctx.fields !== void 0 && !ctx.fields.includes(inlineField);
2055
- if (!orphanedInline && ctx.locale[key] !== void 0) return { text_key: key };
1969
+ if (!orphanedInline && ctx.locale[key] !== void 0) return inlineField !== void 0 ? {
1970
+ field: inlineField,
1971
+ text_key: key
1972
+ } : { text_key: key };
2056
1973
  for (const { suffix, generic } of FLOW_ERROR_RULE_FALLBACKS) {
2057
1974
  if (!key.endsWith(suffix)) continue;
2058
1975
  const field = key.slice(6, key.length - suffix.length);
2059
1976
  if (field === "") break;
2060
- return { message: capitaliseFirst(interpolate(ctx.locale[generic] ?? ctx.locale[FLOW_ERROR_CATCH_ALL_KEY] ?? "Please check {0}.", [fieldLabel(ctx, field)])) };
1977
+ const message = capitaliseFirst(interpolate(ctx.locale[generic] ?? ctx.locale[FLOW_ERROR_CATCH_ALL_KEY] ?? "Please check {0}.", [fieldLabel(ctx, field)]));
1978
+ return ctx.fields !== void 0 && ctx.fields.includes(field) ? {
1979
+ field,
1980
+ message
1981
+ } : { message };
2061
1982
  }
2062
1983
  if (orphanedInline && ctx.locale[key] !== void 0) return { message: ctx.locale[key] };
2063
1984
  return { text_key: key };
@@ -2136,8 +2057,8 @@ const TEMPLATE_NAMES = {
2136
2057
  const en = {
2137
2058
  "identifier.title": "Sign in",
2138
2059
  "identifier.description": "Enter your email to continue",
2139
- "identifier.field.email": "Work email",
2140
- "identifier.field.email.placeholder": "you@company.com",
2060
+ "identifier.field.email": "Email",
2061
+ "identifier.field.email.placeholder": "you@example.com",
2141
2062
  "identifier.field.password": "Password",
2142
2063
  "identifier.action.submit": "Sign in",
2143
2064
  "identifier.action.continue": "Sign in",
@@ -2154,8 +2075,8 @@ const en = {
2154
2075
  "choose-register.action.register-with-passkey": "Sign up with passkey",
2155
2076
  "collect-credentials.title": "Create your account",
2156
2077
  "collect-credentials.description": "Set up your email and password",
2157
- "collect-credentials.field.email": "Work email",
2158
- "collect-credentials.field.email.placeholder": "you@company.com",
2078
+ "collect-credentials.field.email": "Email",
2079
+ "collect-credentials.field.email.placeholder": "you@example.com",
2159
2080
  "collect-credentials.field.password": "Password",
2160
2081
  "collect-credentials.action.submit": "Sign up",
2161
2082
  "register-password.title": "Create your password",
@@ -2172,8 +2093,8 @@ const en = {
2172
2093
  "passkey-upsell.action.setup": "Set up passkey",
2173
2094
  "collect-passkey-email.title": "Create your account",
2174
2095
  "collect-passkey-email.description": "Enter your email to set up a passkey",
2175
- "collect-passkey-email.field.email": "Work email",
2176
- "collect-passkey-email.field.email.placeholder": "you@company.com",
2096
+ "collect-passkey-email.field.email": "Email",
2097
+ "collect-passkey-email.field.email.placeholder": "you@example.com",
2177
2098
  "collect-passkey-email.action.submit": "Continue",
2178
2099
  "passkey-enroll.title": "Set up your passkey",
2179
2100
  "passkey-enroll.description": "Use Face ID, Touch ID, or your device PIN to create a passkey.",
@@ -2190,11 +2111,12 @@ const en = {
2190
2111
  "password.action.register.link": "Sign up",
2191
2112
  "register.title": "Create your account",
2192
2113
  "register.description": "",
2193
- "register.field.email": "Work email",
2194
- "register.field.email.placeholder": "you@company.com",
2114
+ "register.field.email": "Email",
2115
+ "register.field.email.placeholder": "you@example.com",
2195
2116
  "register.field.password": "Password",
2196
2117
  "register.field.givenName": "Given name",
2197
2118
  "register.field.familyName": "Family name",
2119
+ "register.field.companyName": "Company name",
2198
2120
  "register.field.dateOfBirth": "Date of birth",
2199
2121
  "register.field.country": "Country",
2200
2122
  "register.field.country.placeholder": "Select your country",
@@ -2229,6 +2151,8 @@ const en = {
2229
2151
  "error.passkey_setup_failed": "Passkey registration did not complete. Please try again.",
2230
2152
  "error.passkey_unsupported": "This device does not support passkeys",
2231
2153
  "error.passkey_failed": "Something went wrong. Please try again.",
2154
+ "error.passkey_invalid": "This passkey could not be verified. Please try again.",
2155
+ "error.passkey_registration_invalid": "The new passkey could not be verified. Please try registering it again.",
2232
2156
  "error.email_required": "Please enter an email address",
2233
2157
  "error.email_invalid": "Please enter a valid email",
2234
2158
  "error.password_required": "Please enter a password",
@@ -2251,7 +2175,7 @@ const de = {
2251
2175
  "identifier.title": "Anmelden",
2252
2176
  "identifier.description": "Gib deine E-Mail-Adresse ein, um fortzufahren",
2253
2177
  "identifier.field.email": "E-Mail",
2254
- "identifier.field.email.placeholder": "du@unternehmen.com",
2178
+ "identifier.field.email.placeholder": "du@beispiel.de",
2255
2179
  "identifier.field.password": "Passwort",
2256
2180
  "identifier.action.submit": "Anmelden",
2257
2181
  "identifier.action.continue": "Anmelden",
@@ -2269,7 +2193,7 @@ const de = {
2269
2193
  "collect-credentials.title": "Konto erstellen",
2270
2194
  "collect-credentials.description": "Richte E-Mail und Passwort ein",
2271
2195
  "collect-credentials.field.email": "E-Mail",
2272
- "collect-credentials.field.email.placeholder": "du@unternehmen.com",
2196
+ "collect-credentials.field.email.placeholder": "du@beispiel.de",
2273
2197
  "collect-credentials.field.password": "Passwort",
2274
2198
  "collect-credentials.action.submit": "Registrieren",
2275
2199
  "register-password.title": "Passwort erstellen",
@@ -2287,7 +2211,7 @@ const de = {
2287
2211
  "collect-passkey-email.title": "Konto erstellen",
2288
2212
  "collect-passkey-email.description": "Gib deine E-Mail-Adresse ein, um einen Passkey einzurichten",
2289
2213
  "collect-passkey-email.field.email": "E-Mail",
2290
- "collect-passkey-email.field.email.placeholder": "du@unternehmen.com",
2214
+ "collect-passkey-email.field.email.placeholder": "du@beispiel.de",
2291
2215
  "collect-passkey-email.action.submit": "Weiter",
2292
2216
  "passkey-enroll.title": "Passkey einrichten",
2293
2217
  "passkey-enroll.description": "Verwende Face ID, Touch ID oder die Geräte-PIN, um einen Passkey zu erstellen.",
@@ -2305,10 +2229,11 @@ const de = {
2305
2229
  "register.title": "Konto erstellen",
2306
2230
  "register.description": "",
2307
2231
  "register.field.email": "E-Mail",
2308
- "register.field.email.placeholder": "du@unternehmen.com",
2232
+ "register.field.email.placeholder": "du@beispiel.de",
2309
2233
  "register.field.password": "Passwort",
2310
2234
  "register.field.givenName": "Vorname",
2311
2235
  "register.field.familyName": "Nachname",
2236
+ "register.field.companyName": "Firmenname",
2312
2237
  "register.field.dateOfBirth": "Geburtsdatum",
2313
2238
  "register.field.country": "Land",
2314
2239
  "register.field.country.placeholder": "Land auswählen",
@@ -2343,6 +2268,8 @@ const de = {
2343
2268
  "error.passkey_setup_failed": "Passkey-Registrierung wurde nicht abgeschlossen. Bitte versuche es erneut.",
2344
2269
  "error.passkey_unsupported": "Dieses Gerät unterstützt keine Passkeys",
2345
2270
  "error.passkey_failed": "Etwas ist schiefgelaufen. Bitte versuche es erneut.",
2271
+ "error.passkey_invalid": "Dieser Passkey konnte nicht bestätigt werden. Bitte versuche es erneut.",
2272
+ "error.passkey_registration_invalid": "Der neue Passkey konnte nicht bestätigt werden. Bitte registriere ihn erneut.",
2346
2273
  "error.email_required": "Bitte gib eine E-Mail-Adresse ein",
2347
2274
  "error.email_invalid": "Bitte gib eine gültige E-Mail-Adresse ein",
2348
2275
  "error.password_required": "Bitte gib ein Passwort ein",
@@ -2363,8 +2290,8 @@ const de = {
2363
2290
  const it = {
2364
2291
  "identifier.title": "Accedi",
2365
2292
  "identifier.description": "Inserisci la tua e-mail per continuare",
2366
- "identifier.field.email": "E-mail aziendale",
2367
- "identifier.field.email.placeholder": "tu@azienda.com",
2293
+ "identifier.field.email": "E-mail",
2294
+ "identifier.field.email.placeholder": "tu@esempio.com",
2368
2295
  "identifier.field.password": "Password",
2369
2296
  "identifier.action.submit": "Accedi",
2370
2297
  "identifier.action.continue": "Accedi",
@@ -2381,8 +2308,8 @@ const it = {
2381
2308
  "choose-register.action.register-with-passkey": "Registrati con passkey",
2382
2309
  "collect-credentials.title": "Crea il tuo account",
2383
2310
  "collect-credentials.description": "Configura e-mail e password",
2384
- "collect-credentials.field.email": "E-mail aziendale",
2385
- "collect-credentials.field.email.placeholder": "tu@azienda.com",
2311
+ "collect-credentials.field.email": "E-mail",
2312
+ "collect-credentials.field.email.placeholder": "tu@esempio.com",
2386
2313
  "collect-credentials.field.password": "Password",
2387
2314
  "collect-credentials.action.submit": "Registrati",
2388
2315
  "register-password.title": "Crea la tua password",
@@ -2399,8 +2326,8 @@ const it = {
2399
2326
  "passkey-upsell.action.setup": "Configura passkey",
2400
2327
  "collect-passkey-email.title": "Crea il tuo account",
2401
2328
  "collect-passkey-email.description": "Inserisci la tua e-mail per configurare una passkey",
2402
- "collect-passkey-email.field.email": "E-mail aziendale",
2403
- "collect-passkey-email.field.email.placeholder": "tu@azienda.com",
2329
+ "collect-passkey-email.field.email": "E-mail",
2330
+ "collect-passkey-email.field.email.placeholder": "tu@esempio.com",
2404
2331
  "collect-passkey-email.action.submit": "Continua",
2405
2332
  "passkey-enroll.title": "Configura la tua passkey",
2406
2333
  "passkey-enroll.description": "Usa Face ID, Touch ID o il PIN del dispositivo per creare una passkey.",
@@ -2417,11 +2344,12 @@ const it = {
2417
2344
  "password.action.register.link": "Registrati",
2418
2345
  "register.title": "Crea il tuo account",
2419
2346
  "register.description": "",
2420
- "register.field.email": "E-mail aziendale",
2421
- "register.field.email.placeholder": "tu@azienda.com",
2347
+ "register.field.email": "E-mail",
2348
+ "register.field.email.placeholder": "tu@esempio.com",
2422
2349
  "register.field.password": "Password",
2423
2350
  "register.field.givenName": "Nome",
2424
2351
  "register.field.familyName": "Cognome",
2352
+ "register.field.companyName": "Nome azienda",
2425
2353
  "register.field.dateOfBirth": "Data di nascita",
2426
2354
  "register.field.country": "Paese",
2427
2355
  "register.field.country.placeholder": "Seleziona il tuo paese",
@@ -2456,6 +2384,8 @@ const it = {
2456
2384
  "error.passkey_setup_failed": "La registrazione della passkey non è stata completata. Riprova.",
2457
2385
  "error.passkey_unsupported": "Questo dispositivo non supporta le passkey",
2458
2386
  "error.passkey_failed": "Qualcosa è andato storto. Riprova.",
2387
+ "error.passkey_invalid": "Non è stato possibile verificare questa passkey. Riprova.",
2388
+ "error.passkey_registration_invalid": "Non è stato possibile verificare la nuova passkey. Riprova a registrarla.",
2459
2389
  "error.email_required": "Inserisci un indirizzo e-mail",
2460
2390
  "error.email_invalid": "Inserisci un indirizzo e-mail valido",
2461
2391
  "error.password_required": "Inserisci una password",
@@ -2472,6 +2402,40 @@ const it = {
2472
2402
  "error.sign_in_server.body": "Riprova tra qualche minuto"
2473
2403
  };
2474
2404
  //#endregion
2405
+ //#region src/orchestrator/locales/business.ts
2406
+ const businessLocales = {
2407
+ en: {
2408
+ "identifier.field.email": "Work email",
2409
+ "identifier.field.email.placeholder": "you@company.com",
2410
+ "collect-credentials.field.email": "Work email",
2411
+ "collect-credentials.field.email.placeholder": "you@company.com",
2412
+ "collect-passkey-email.field.email": "Work email",
2413
+ "collect-passkey-email.field.email.placeholder": "you@company.com",
2414
+ "register.field.email": "Work email",
2415
+ "register.field.email.placeholder": "you@company.com"
2416
+ },
2417
+ de: {
2418
+ "identifier.field.email": "Geschäftliche E-Mail",
2419
+ "identifier.field.email.placeholder": "du@unternehmen.com",
2420
+ "collect-credentials.field.email": "Geschäftliche E-Mail",
2421
+ "collect-credentials.field.email.placeholder": "du@unternehmen.com",
2422
+ "collect-passkey-email.field.email": "Geschäftliche E-Mail",
2423
+ "collect-passkey-email.field.email.placeholder": "du@unternehmen.com",
2424
+ "register.field.email": "Geschäftliche E-Mail",
2425
+ "register.field.email.placeholder": "du@unternehmen.com"
2426
+ },
2427
+ it: {
2428
+ "identifier.field.email": "E-mail aziendale",
2429
+ "identifier.field.email.placeholder": "tu@azienda.com",
2430
+ "collect-credentials.field.email": "E-mail aziendale",
2431
+ "collect-credentials.field.email.placeholder": "tu@azienda.com",
2432
+ "collect-passkey-email.field.email": "E-mail aziendale",
2433
+ "collect-passkey-email.field.email.placeholder": "tu@azienda.com",
2434
+ "register.field.email": "E-mail aziendale",
2435
+ "register.field.email.placeholder": "tu@azienda.com"
2436
+ }
2437
+ };
2438
+ //#endregion
2475
2439
  //#region src/orchestrator/locales/index.ts
2476
2440
  const builtinLocales = {
2477
2441
  en,
@@ -2510,128 +2474,411 @@ function zitadelAttributionPillInnerHtml() {
2510
2474
  * `<zl-*>` atoms. Anything outside this list and the manifest registry is
2511
2475
  * stripped at sanitise-time.
2512
2476
  */
2513
- const STRUCTURAL_TAGS = [
2514
- "div",
2515
- "span",
2516
- "p",
2517
- "small",
2518
- "strong",
2519
- "em",
2520
- "br",
2521
- "hr",
2522
- "h1",
2523
- "h2",
2524
- "h3",
2525
- "h4",
2526
- "h5",
2527
- "h6",
2528
- "ul",
2529
- "ol",
2530
- "li",
2531
- "a",
2532
- "img",
2533
- "section",
2534
- "header",
2535
- "footer",
2536
- "main",
2537
- "nav",
2538
- "aside",
2539
- "article",
2540
- "figure",
2541
- "figcaption",
2542
- "label",
2543
- "fieldset",
2544
- "legend"
2545
- ];
2477
+ const STRUCTURAL_TAGS = [
2478
+ "div",
2479
+ "span",
2480
+ "p",
2481
+ "small",
2482
+ "strong",
2483
+ "em",
2484
+ "br",
2485
+ "hr",
2486
+ "h1",
2487
+ "h2",
2488
+ "h3",
2489
+ "h4",
2490
+ "h5",
2491
+ "h6",
2492
+ "ul",
2493
+ "ol",
2494
+ "li",
2495
+ "a",
2496
+ "img",
2497
+ "section",
2498
+ "header",
2499
+ "footer",
2500
+ "main",
2501
+ "nav",
2502
+ "aside",
2503
+ "article",
2504
+ "figure",
2505
+ "figcaption",
2506
+ "label",
2507
+ "fieldset",
2508
+ "legend"
2509
+ ];
2510
+ /**
2511
+ * Attributes templates may carry on structural tags. Per-atom attributes are
2512
+ * derived from the manifest registry below — manifests are the single source
2513
+ * of truth for "what attributes does `<zl-field>` accept?".
2514
+ */
2515
+ const COMMON_ATTRS = [
2516
+ "id",
2517
+ "class",
2518
+ "role",
2519
+ "slot",
2520
+ "tabindex",
2521
+ "lang",
2522
+ "dir",
2523
+ "alt",
2524
+ "title",
2525
+ "src",
2526
+ "srcset",
2527
+ "sizes",
2528
+ "href",
2529
+ "target",
2530
+ "rel",
2531
+ "style",
2532
+ "name",
2533
+ "for",
2534
+ "data-testid",
2535
+ "data-theme",
2536
+ "hidden"
2537
+ ];
2538
+ const CUSTOM_TAG_PATTERN = /^zl-[a-z0-9-]+$/;
2539
+ function buildAllowedTagList() {
2540
+ const set = new Set(STRUCTURAL_TAGS);
2541
+ for (const manifest of manifestRegistry) set.add(manifest.tag);
2542
+ return [...set];
2543
+ }
2544
+ function buildAllowedAttrList() {
2545
+ const set = new Set(COMMON_ATTRS);
2546
+ for (const manifest of manifestRegistry) for (const attr of manifest.attrs) set.add(attr);
2547
+ return [...set];
2548
+ }
2549
+ /**
2550
+ * Returns a sanitiser bound to the current manifest registry. Callers should
2551
+ * cache the result; rebuilding is cheap but allocates a fresh DOMPurify config.
2552
+ */
2553
+ function createSanitiser() {
2554
+ const config = {
2555
+ USE_PROFILES: { html: true },
2556
+ ADD_TAGS: buildAllowedTagList(),
2557
+ ADD_ATTR: buildAllowedAttrList(),
2558
+ FORBID_TAGS: [
2559
+ "script",
2560
+ "style",
2561
+ "iframe",
2562
+ "object",
2563
+ "embed",
2564
+ "form",
2565
+ "input",
2566
+ "button"
2567
+ ],
2568
+ FORBID_ATTR: [
2569
+ "onerror",
2570
+ "onclick",
2571
+ "onload",
2572
+ "onfocus",
2573
+ "onblur"
2574
+ ],
2575
+ CUSTOM_ELEMENT_HANDLING: {
2576
+ tagNameCheck: CUSTOM_TAG_PATTERN,
2577
+ attributeNameCheck: /^(?:[a-z][a-z0-9-]*)$/,
2578
+ allowCustomizedBuiltInElements: false
2579
+ },
2580
+ KEEP_CONTENT: true,
2581
+ ALLOW_DATA_ATTR: true,
2582
+ ALLOW_ARIA_ATTR: true,
2583
+ RETURN_TRUSTED_TYPE: false
2584
+ };
2585
+ return function sanitise(html) {
2586
+ const result = DOMPurify.sanitize(html, config);
2587
+ return typeof result === "string" ? result : String(result);
2588
+ };
2589
+ }
2590
+ //#endregion
2591
+ //#region src/orchestrator/branding-to-tokens.ts
2592
+ /**
2593
+ * Branding -> CSS token bridge.
2594
+ *
2595
+ * Per `docs/design/branding/README.md` the orchestrator owns theming. Templates
2596
+ * never emit `<style>` blocks; they only structure HTML. This module translates
2597
+ * the `Branding` JSON into a CSSStyleSheet of `:host { --zl-* }` declarations
2598
+ * and applies it via `shadowRoot.adoptedStyleSheets`.
2599
+ *
2600
+ * Dark-mode overrides are emitted as `:host([data-theme="dark"]) { ... }`
2601
+ * (matching `docs/design/branding/tokens.md`). Resolution between
2602
+ * `light | dark | auto` happens in `<zitadel-login>`.
2603
+ *
2604
+ * Base layer: the design-tokens package ships the full `--zl-*` set as both a
2605
+ * `.css` file (for host pages) and a `tokensCss` string (for shadow roots and
2606
+ * SSR). We adopt the string into every orchestrator shadow root so atoms
2607
+ * paint correctly even when the host page didn't `@import` tokens.css — the
2608
+ * orchestrator is meant to drop into any page.
2609
+ */
2610
+ const RADIUS_MAP = {
2611
+ none: "0",
2612
+ sm: "0.25rem",
2613
+ md: "0.5rem",
2614
+ lg: "0.75rem",
2615
+ full: "9999px"
2616
+ };
2617
+ const DENSITY_MAP = {
2618
+ compact: {
2619
+ "--zl-spacing-03": "0.75rem",
2620
+ "--zl-spacing-05": "1.5rem"
2621
+ },
2622
+ regular: {},
2623
+ comfortable: {
2624
+ "--zl-spacing-03": "1.25rem",
2625
+ "--zl-spacing-05": "2.25rem"
2626
+ }
2627
+ };
2628
+ const PALETTE_MAP = {
2629
+ primary: ["--zl-color-surface-default-white"],
2630
+ on_primary: ["--zl-color-text-button-default"],
2631
+ background: ["--zl-color-surface-default-black"],
2632
+ surface: ["--zl-color-surface-default-primary-gray"],
2633
+ muted: ["--zl-color-surface-default-secondary-gray"],
2634
+ border: ["--zl-color-border-default-gray-200", "--zl-color-border-default-gray-100"],
2635
+ text: ["--zl-color-text-primary-white"],
2636
+ text_muted: ["--zl-color-text-secondary-gray"],
2637
+ link: ["--zl-color-text-subtitle-pink"],
2638
+ success: [
2639
+ "--zl-color-text-success",
2640
+ "--zl-color-border-success",
2641
+ "--zl-color-icon-success"
2642
+ ],
2643
+ warning: ["--zl-color-text-subtitle-orange"],
2644
+ error: [
2645
+ "--zl-color-text-error",
2646
+ "--zl-color-border-error",
2647
+ "--zl-color-icon-error"
2648
+ ]
2649
+ };
2650
+ /**
2651
+ * Build a CSS string of `:host { --zl-* }` declarations from a Branding
2652
+ * payload. Light values land on `:host`; dark overrides land on
2653
+ * `:host([data-theme="dark"])`.
2654
+ */
2655
+ function buildBrandingStylesheet(branding, options = {}) {
2656
+ const lightDecls = collectDeclarations(branding);
2657
+ const darkPalette = branding?.theme?.dark?.palette;
2658
+ const darkDecls = darkPalette ? mapPalette(darkPalette) : {};
2659
+ const blocks = [];
2660
+ if (Object.keys(lightDecls).length > 0) blocks.push(formatBlock(":host", lightDecls));
2661
+ if (Object.keys(darkDecls).length > 0) blocks.push(formatBlock(":host([data-theme=\"dark\"])", darkDecls));
2662
+ if (options.resolvedTheme === "dark" && darkPalette) {
2663
+ const merged = {
2664
+ ...lightDecls,
2665
+ ...darkDecls
2666
+ };
2667
+ blocks.length = 0;
2668
+ blocks.push(formatBlock(":host", merged));
2669
+ }
2670
+ return blocks.join("\n");
2671
+ }
2672
+ function collectDeclarations(branding) {
2673
+ if (!branding) return {};
2674
+ const decls = {};
2675
+ Object.assign(decls, mapPalette(branding.palette));
2676
+ Object.assign(decls, mapTypography(branding.typography));
2677
+ Object.assign(decls, mapShape(branding.shape));
2678
+ return decls;
2679
+ }
2680
+ function mapPalette(palette) {
2681
+ if (!palette) return {};
2682
+ const out = {};
2683
+ for (const [key, varNames] of Object.entries(PALETTE_MAP)) {
2684
+ const value = palette[key];
2685
+ if (typeof value === "string" && value.length > 0) for (const varName of varNames) out[varName] = value;
2686
+ }
2687
+ return out;
2688
+ }
2689
+ function mapTypography(typography) {
2690
+ if (!typography) return {};
2691
+ const out = {};
2692
+ if (typography.font_family) {
2693
+ out["--zl-font-family-sans"] = typography.font_family;
2694
+ out["--zl-font-family-heading"] = typography.font_family;
2695
+ }
2696
+ if (typography.font_family_mono) out["--zl-font-family-mono"] = typography.font_family_mono;
2697
+ const scale = clamp(typography.scale ?? 1, .75, 1.25);
2698
+ if (scale !== 1) out["--zl-font-scale"] = `${scale}`;
2699
+ return out;
2700
+ }
2701
+ function mapShape(shape) {
2702
+ if (!shape) return {};
2703
+ const out = {};
2704
+ if (shape.radius && RADIUS_MAP[shape.radius]) {
2705
+ const radius = RADIUS_MAP[shape.radius];
2706
+ out["--zl-radius-s"] = radius;
2707
+ out["--zl-radius-m"] = radius === "0" ? "0" : `calc(${radius} * 1.5)`;
2708
+ out["--zl-radius-l"] = radius === "0" ? "0" : `calc(${radius} * 2)`;
2709
+ }
2710
+ if (shape.density) Object.assign(out, DENSITY_MAP[shape.density]);
2711
+ return out;
2712
+ }
2713
+ function formatBlock(selector, decls) {
2714
+ return `${selector} {\n${Object.entries(decls).map(([prop, value]) => ` ${prop}: ${value};`).join("\n")}\n}`;
2715
+ }
2716
+ function clamp(value, min, max) {
2717
+ if (Number.isNaN(value)) return min;
2718
+ return Math.min(max, Math.max(min, value));
2719
+ }
2720
+ let baseTokenSheet;
2721
+ function getBaseTokenSheet() {
2722
+ if (typeof CSSStyleSheet === "undefined") return void 0;
2723
+ if (!baseTokenSheet) {
2724
+ baseTokenSheet = new CSSStyleSheet();
2725
+ baseTokenSheet.replaceSync(tokensCss.replaceAll(":root,\n[data-theme=\"dark\"]", ":host,\n:host([data-theme=\"dark\"])").replaceAll("[data-theme=\"light\"]", ":host([data-theme=\"light\"])"));
2726
+ }
2727
+ return baseTokenSheet;
2728
+ }
2729
+ /**
2730
+ * Adopt the design-system base token layer onto a `ShadowRoot`. Safe to call
2731
+ * many times — the underlying constructable sheet is shared across every
2732
+ * orchestrator instance and de-duplicated in the adopted list.
2733
+ */
2734
+ function applyBaseTokens(shadowRoot) {
2735
+ const sheet = getBaseTokenSheet();
2736
+ if (!sheet) return;
2737
+ const existing = Array.isArray(shadowRoot.adoptedStyleSheets) ? shadowRoot.adoptedStyleSheets : [];
2738
+ if (existing.includes(sheet)) return;
2739
+ try {
2740
+ shadowRoot.adoptedStyleSheets = [sheet, ...existing];
2741
+ } catch {}
2742
+ }
2743
+ /**
2744
+ * Apply a branding payload as a `--zl-*` overrides layer on top of the base
2745
+ * token sheet. Subsequent calls replace the previous override sheet so a new
2746
+ * branding payload paints cleanly without leaking older declarations.
2747
+ *
2748
+ * Callers should run `applyBaseTokens(shadowRoot)` first (once per shadow
2749
+ * root) so the base values exist before branding patches them.
2750
+ */
2751
+ function applyBrandingTokens(shadowRoot, branding, resolvedTheme) {
2752
+ const css = buildBrandingStylesheet(branding, { resolvedTheme });
2753
+ if (typeof CSSStyleSheet === "undefined") return;
2754
+ const sheet = new CSSStyleSheet();
2755
+ sheet.replaceSync(css);
2756
+ const previous = shadowRoot.__zlTokenSheet;
2757
+ shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets.filter((s) => s !== previous), sheet];
2758
+ shadowRoot.__zlTokenSheet = sheet;
2759
+ }
2760
+ /**
2761
+ * Standalone branding→theme resolution for callers outside the orchestrator
2762
+ * (SSR, tests, embedders composing atoms by hand). `<zitadel-login>` itself
2763
+ * uses {@link ThemeController}, which layers the element's `theme` property
2764
+ * and a variant-derived fallback on top of the same branding input.
2765
+ *
2766
+ * Defaults to dark: the design system's primary surface, and the mode a
2767
+ * hosted login page renders when a tenant states no preference.
2768
+ */
2769
+ function resolveTheme(branding) {
2770
+ const mode = branding?.theme?.mode ?? "dark";
2771
+ if (mode === "light") return "light";
2772
+ if (mode === "dark") return "dark";
2773
+ if (typeof matchMedia === "function") return matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
2774
+ return "dark";
2775
+ }
2776
+ //#endregion
2777
+ //#region src/orchestrator/font-loader.ts
2778
+ /**
2779
+ * Document-level font stylesheet injection for the orchestrator surfaces.
2780
+ *
2781
+ * Two faces can be linked into the host document's `<head>`:
2782
+ *
2783
+ * - The **design-system default** (`applyDefaultFont`) — the brand body face
2784
+ * (Arimo, named first in `--zl-font-family-sans`). The component ships this
2785
+ * so the auth screens render in the brand font out of the box, even when the
2786
+ * server returns no branding. This is the "default users can override" layer.
2787
+ * - The **tenant override** (`applyFontUrl`) — `branding.font_url`, resolved
2788
+ * server-side from the app → team → project branding hierarchy.
2789
+ *
2790
+ * Both must live at document level, not inside the shadow root: browsers ignore
2791
+ * `@font-face` rules declared in shadow-tree stylesheets, so a shadow-scoped
2792
+ * link would never register the font faces and every branded font silently
2793
+ * falls back to the system stack. `font-family` references inside the shadow
2794
+ * tree resolve against document-level faces.
2795
+ *
2796
+ * Each `<link>` is shared per document but **owned collectively**: every
2797
+ * calling surface (identified by its shadow root) registers the href it wants,
2798
+ * and passing `null`/`undefined` withdraws only that surface's registration —
2799
+ * never the link another surface still needs. Multiple surfaces now call these
2800
+ * helpers on every update (`<zitadel-login>` and `<zitadel-session>` via the
2801
+ * shared surface base), so a widget-mode session mounting next to a page-mode
2802
+ * login must not strip the login's fonts. The rendered href is the most
2803
+ * recently registered one (matching the old single-owner behavior when only
2804
+ * one surface is present); the link is removed once no surface wants it.
2805
+ * Registrations from since-disconnected surfaces are pruned lazily on the
2806
+ * next call, so a removed element neither pins a link forever nor leaks its
2807
+ * shadow root — while the link itself stays in place until another surface
2808
+ * actually updates, exactly like the pre-ownership behavior.
2809
+ */
2810
+ const TENANT_LINK_ID = "zl-font-link";
2811
+ const DEFAULT_LINK_ID = "zl-default-font-link";
2812
+ /**
2813
+ * Design-system default brand font. Arimo is the Figma body face and leads the
2814
+ * `--zl-font-family-sans` stack; loading it here makes the unbranded auth UI
2815
+ * paint in the brand font instead of the system fallback. Tenants override it
2816
+ * with `branding.font_url`. Exported so a privacy- or offline-sensitive
2817
+ * deployment can self-host the same face and swap this single URL.
2818
+ */
2819
+ const DEFAULT_BRAND_FONT_HREF = "https://fonts.googleapis.com/css2?family=Arimo:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap";
2820
+ /** Per-document, per-link-id map of each owner's desired href. */
2821
+ const registries = /* @__PURE__ */ new WeakMap();
2546
2822
  /**
2547
- * Attributes templates may carry on structural tags. Per-atom attributes are
2548
- * derived from the manifest registry below manifests are the single source
2549
- * of truth for "what attributes does `<zl-field>` accept?".
2823
+ * Inject the tenant's `branding.font_url` stylesheet. Pass `null`/`undefined`
2824
+ * to withdraw this surface's registration (e.g. when a flow resolves with no
2825
+ * tenant font); the link disappears once no surface registers a URL.
2550
2826
  */
2551
- const COMMON_ATTRS = [
2552
- "id",
2553
- "class",
2554
- "role",
2555
- "slot",
2556
- "tabindex",
2557
- "lang",
2558
- "dir",
2559
- "alt",
2560
- "title",
2561
- "src",
2562
- "srcset",
2563
- "sizes",
2564
- "href",
2565
- "target",
2566
- "rel",
2567
- "style",
2568
- "name",
2569
- "for",
2570
- "data-testid",
2571
- "data-theme",
2572
- "hidden"
2573
- ];
2574
- const CUSTOM_TAG_PATTERN = /^zl-[a-z0-9-]+$/;
2575
- function buildAllowedTagList() {
2576
- const set = new Set(STRUCTURAL_TAGS);
2577
- for (const manifest of manifestRegistry) set.add(manifest.tag);
2578
- return [...set];
2579
- }
2580
- function buildAllowedAttrList() {
2581
- const set = new Set(COMMON_ATTRS);
2582
- for (const manifest of manifestRegistry) for (const attr of manifest.attrs) set.add(attr);
2583
- return [...set];
2827
+ function applyFontUrl(shadowRoot, fontUrl) {
2828
+ applyLinkedStylesheet(shadowRoot, TENANT_LINK_ID, fontUrl);
2584
2829
  }
2585
2830
  /**
2586
- * Returns a sanitiser bound to the current manifest registry. Callers should
2587
- * cache the result; rebuilding is cheap but allocates a fresh DOMPurify config.
2831
+ * Inject the design-system default font stylesheet. Defaults to
2832
+ * {@link DEFAULT_BRAND_FONT_HREF}; pass `null` to withdraw this surface's
2833
+ * registration (the orchestrator does this when a tenant `font_url` takes
2834
+ * over, to avoid a redundant request).
2588
2835
  */
2589
- function createSanitiser() {
2590
- const config = {
2591
- USE_PROFILES: { html: true },
2592
- ADD_TAGS: buildAllowedTagList(),
2593
- ADD_ATTR: buildAllowedAttrList(),
2594
- FORBID_TAGS: [
2595
- "script",
2596
- "style",
2597
- "iframe",
2598
- "object",
2599
- "embed",
2600
- "form",
2601
- "input",
2602
- "button"
2603
- ],
2604
- FORBID_ATTR: [
2605
- "onerror",
2606
- "onclick",
2607
- "onload",
2608
- "onfocus",
2609
- "onblur"
2610
- ],
2611
- CUSTOM_ELEMENT_HANDLING: {
2612
- tagNameCheck: CUSTOM_TAG_PATTERN,
2613
- attributeNameCheck: /^(?:[a-z][a-z0-9-]*)$/,
2614
- allowCustomizedBuiltInElements: false
2615
- },
2616
- KEEP_CONTENT: true,
2617
- ALLOW_DATA_ATTR: true,
2618
- ALLOW_ARIA_ATTR: true,
2619
- RETURN_TRUSTED_TYPE: false
2620
- };
2621
- return function sanitise(html) {
2622
- const result = DOMPurify.sanitize(html, config);
2623
- return typeof result === "string" ? result : String(result);
2624
- };
2836
+ function applyDefaultFont(shadowRoot, href = DEFAULT_BRAND_FONT_HREF) {
2837
+ applyLinkedStylesheet(shadowRoot, DEFAULT_LINK_ID, href);
2838
+ }
2839
+ function applyLinkedStylesheet(shadowRoot, linkId, href) {
2840
+ const ownerDocument = shadowRoot.ownerDocument ?? document;
2841
+ let registry = registries.get(ownerDocument);
2842
+ if (!registry) {
2843
+ registry = /* @__PURE__ */ new Map();
2844
+ registries.set(ownerDocument, registry);
2845
+ }
2846
+ let desires = registry.get(linkId);
2847
+ if (!desires) {
2848
+ desires = /* @__PURE__ */ new Map();
2849
+ registry.set(linkId, desires);
2850
+ }
2851
+ for (const owner of desires.keys()) if (!owner.host.isConnected) desires.delete(owner);
2852
+ if (href) {
2853
+ desires.delete(shadowRoot);
2854
+ desires.set(shadowRoot, href);
2855
+ } else desires.delete(shadowRoot);
2856
+ const effective = [...desires.values()].at(-1) ?? null;
2857
+ const existing = ownerDocument.head.querySelector(`link#${linkId}`);
2858
+ if (!effective) {
2859
+ existing?.remove();
2860
+ return;
2861
+ }
2862
+ if (!href) {
2863
+ if (existing && existing.href !== effective) existing.href = effective;
2864
+ return;
2865
+ }
2866
+ const link = existing ?? ownerDocument.createElement("link");
2867
+ link.id = linkId;
2868
+ link.rel = "stylesheet";
2869
+ if (link.href !== effective) link.href = effective;
2870
+ if (!existing) ownerDocument.head.appendChild(link);
2625
2871
  }
2626
- //#endregion
2627
- //#region src/orchestrator/templates/layout-chrome.css?inline
2628
- var layout_chrome_default = ":host {\n background: var(--zl-color-surface-default-black, #0f0f11);\n min-height: 100%;\n color: var(--zl-color-text-primary-white, #f4f4f6);\n font-family: var(--zl-font-family-sans, system-ui, sans-serif);\n display: block;\n}\n\n.zl-mount {\n background: inherit;\n min-height: 100vh;\n color: inherit;\n flex-direction: column;\n display: flex;\n}\n\n.zl-mount > zl-page-shell, .zl-mount > [data-zl-template-root] {\n flex: auto;\n}\n\n.zl-attribution {\n background: none;\n justify-content: center;\n align-items: center;\n display: flex;\n}\n\n.zl-attribution[hidden] {\n display: none;\n}\n\n.zl-card-title {\n font-family: var(--zl-font-family-heading, var(--zl-font-family-sans, system-ui, sans-serif));\n letter-spacing: -.02em;\n color: var(--zl-color-text-primary-white, #f4f4f6);\n text-align: left;\n margin: 0;\n font-size: 2rem;\n font-weight: 700;\n line-height: 2.5rem;\n}\n\n.zl-card-subtitle {\n font-family: var(--zl-font-family-sans, system-ui, sans-serif);\n color: var(--zl-color-text-primary-white, #f4f4f6);\n margin: 0;\n font-size: .875rem;\n font-weight: 400;\n line-height: 1.25rem;\n}\n\n.zl-card-nav {\n font-family: var(--zl-font-family-sans, system-ui, sans-serif);\n color: var(--zl-color-text-primary-white, #f4f4f6);\n text-align: left;\n margin: 0;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5rem;\n}\n\n.zl-card-nav__link {\n font: inherit;\n line-height: inherit;\n color: var(--zl-color-icon-default-purple, #bba5e4);\n cursor: pointer;\n background: none;\n border: none;\n margin: 0;\n padding: 0;\n text-decoration: none;\n display: inline;\n}\n\n.zl-card-nav__link:hover {\n text-decoration: underline;\n}\n\n.zl-card-nav__link:focus-visible {\n outline: var(--zl-focus-width, 2px) solid var(--zl-focus-color, #f4f4f6);\n outline-offset: var(--zl-focus-offset, 2px);\n}\n\n.zl-card-forgot {\n text-align: left;\n margin: 0;\n}\n\n.zl-card-forgot__link {\n font-family: var(--zl-font-family-sans, system-ui, sans-serif);\n color: var(--zl-color-icon-default-purple, #bba5e4);\n cursor: pointer;\n background: none;\n border: none;\n margin: 0;\n padding: 0;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5rem;\n text-decoration: none;\n display: inline;\n}\n\n.zl-card-forgot__link:hover {\n text-decoration: underline;\n}\n\n.zl-card-forgot__link:focus-visible {\n outline: var(--zl-focus-width, 2px) solid var(--zl-focus-color, #f4f4f6);\n outline-offset: var(--zl-focus-offset, 2px);\n}\n";
2629
2872
  //#endregion
2630
2873
  //#region src/orchestrator/theme-controller.ts
2631
2874
  const DARK_QUERY = "(prefers-color-scheme: dark)";
2632
2875
  var ThemeController = class {
2633
2876
  host;
2634
2877
  branding;
2878
+ /** Element-level `theme` property — the embedding developer's explicit call. */
2879
+ explicitMode;
2880
+ /** Used when neither the element nor the branding payload names a mode. */
2881
+ fallbackMode = "dark";
2635
2882
  mediaQuery = null;
2636
2883
  _theme = "dark";
2637
2884
  constructor(host) {
@@ -2645,6 +2892,19 @@ var ThemeController = class {
2645
2892
  this.branding = branding;
2646
2893
  this.refresh();
2647
2894
  }
2895
+ /**
2896
+ * Sets the element-level preference and the fallback used when neither the
2897
+ * element nor the branding payload names a mode. Precedence, strongest
2898
+ * first: element `theme` property → `branding.theme.mode` → fallback. The
2899
+ * element wins because the page embedding the widget knows its own surface
2900
+ * better than the tenant's stored branding does.
2901
+ */
2902
+ setModePreference(explicit, fallback) {
2903
+ if (this.explicitMode === explicit && this.fallbackMode === fallback) return;
2904
+ this.explicitMode = explicit;
2905
+ this.fallbackMode = fallback;
2906
+ this.refresh();
2907
+ }
2648
2908
  hostConnected() {
2649
2909
  this.refresh();
2650
2910
  }
@@ -2652,7 +2912,7 @@ var ThemeController = class {
2652
2912
  this.detach();
2653
2913
  }
2654
2914
  refresh() {
2655
- const mode = this.branding?.theme?.mode ?? "dark";
2915
+ const mode = this.explicitMode ?? this.branding?.theme?.mode ?? this.fallbackMode;
2656
2916
  if (mode === "auto") {
2657
2917
  this.attach();
2658
2918
  this.update(this.mediaQuery?.matches === false ? "light" : "dark");
@@ -2681,8 +2941,72 @@ var ThemeController = class {
2681
2941
  }
2682
2942
  };
2683
2943
  //#endregion
2944
+ //#region src/orchestrator/surface.ts
2945
+ /**
2946
+ * Shared base for the orchestrator host elements (`<zitadel-login>`,
2947
+ * `<zitadel-session>`) that render a themable page/widget surface.
2948
+ *
2949
+ * Owns the `variant` / `theme` public properties, the {@link ThemeController},
2950
+ * and the per-update surface application (base tokens, branding overrides,
2951
+ * font links, and the `data-theme` host stamp). Subclasses call
2952
+ * {@link applySurfaceTheme} from their `willUpdate` so the resolved theme is
2953
+ * committed in the same update cycle that reads it.
2954
+ *
2955
+ * Not a registered custom element — purely an implementation-sharing base.
2956
+ */
2957
+ var ZitadelSurface = class extends LitElement {
2958
+ #_variant_accessor_storage = "widget";
2959
+ get variant() {
2960
+ return this.#_variant_accessor_storage;
2961
+ }
2962
+ set variant(value) {
2963
+ this.#_variant_accessor_storage = value;
2964
+ }
2965
+ #_theme_accessor_storage = "";
2966
+ get theme() {
2967
+ return this.#_theme_accessor_storage;
2968
+ }
2969
+ set theme(value) {
2970
+ this.#_theme_accessor_storage = value;
2971
+ }
2972
+ themeController = new ThemeController(this);
2973
+ /**
2974
+ * Resolve the active theme and commit it to this element's surface: adopt
2975
+ * the base token sheet, layer the branding overrides, manage the
2976
+ * document-level font links, and stamp `data-theme` / `data-theme-dark` on
2977
+ * the host so token lookups (and host-page selectors) flip with the mode.
2978
+ * `branding` is `undefined` for surfaces without a tenant payload — the
2979
+ * theme then resolves through the element preference and variant fallback.
2980
+ */
2981
+ applySurfaceTheme(branding) {
2982
+ this.themeController.setModePreference(this.theme === "" ? void 0 : this.theme, this.variant === "page" ? "dark" : "auto");
2983
+ const root = this.shadowRoot;
2984
+ if (root) {
2985
+ applyBaseTokens(root);
2986
+ applyBrandingTokens(root, branding, this.themeController.theme);
2987
+ const tenantFontUrl = branding?.font_url ?? null;
2988
+ applyDefaultFont(root, this.variant === "page" && !tenantFontUrl ? void 0 : null);
2989
+ applyFontUrl(root, tenantFontUrl);
2990
+ }
2991
+ this.dataset.theme = this.themeController.theme;
2992
+ this.toggleAttribute("data-theme-dark", this.themeController.theme === "dark");
2993
+ }
2994
+ };
2995
+ __decorate([property({
2996
+ type: String,
2997
+ reflect: true
2998
+ })], ZitadelSurface.prototype, "variant", null);
2999
+ __decorate([property({ type: String })], ZitadelSurface.prototype, "theme", null);
3000
+ //#endregion
3001
+ //#region src/orchestrator/templates/layout-chrome.css?inline
3002
+ var layout_chrome_default = ":host {\n color: var(--zl-color-text-primary-white, #f4f4f6);\n font-family: var(--zl-font-family-sans, system-ui, sans-serif);\n background: none;\n display: block;\n}\n\n:host([variant=\"page\"]) {\n background: var(--zl-color-surface-default-black, #0f0f11);\n min-height: 100%;\n}\n\n.zl-mount {\n min-height: var(--zl-page-min-height, auto);\n background: inherit;\n color: inherit;\n flex-direction: column;\n display: flex;\n container-type: inline-size;\n}\n\n:host([variant=\"page\"]) .zl-mount {\n min-height: var(--zl-page-min-height, 100vh);\n}\n\n.zl-mount > zl-page-shell, .zl-mount > [data-zl-template-root] {\n flex: auto;\n}\n\n.zl-attribution {\n background: none;\n justify-content: center;\n align-items: center;\n display: flex;\n}\n\n.zl-attribution[hidden] {\n display: none;\n}\n\n.zl-card-title {\n font-family: var(--zl-font-family-heading, var(--zl-font-family-sans, system-ui, sans-serif));\n letter-spacing: -.02em;\n color: var(--zl-color-text-primary-white, #f4f4f6);\n text-align: left;\n margin: 0;\n font-size: 2rem;\n font-weight: 700;\n line-height: 2.5rem;\n}\n\n.zl-card-subtitle {\n font-family: var(--zl-font-family-sans, system-ui, sans-serif);\n color: var(--zl-color-text-primary-white, #f4f4f6);\n margin: 0;\n font-size: .875rem;\n font-weight: 400;\n line-height: 1.25rem;\n}\n\n.zl-card-nav {\n font-family: var(--zl-font-family-sans, system-ui, sans-serif);\n color: var(--zl-color-text-primary-white, #f4f4f6);\n text-align: left;\n margin: 0;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5rem;\n}\n\n.zl-card-nav__link {\n font: inherit;\n line-height: inherit;\n color: var(--zl-color-icon-default-purple, #bba5e4);\n cursor: pointer;\n background: none;\n border: none;\n margin: 0;\n padding: 0;\n text-decoration: none;\n display: inline;\n}\n\n.zl-card-nav__link:hover {\n text-decoration: underline;\n}\n\n.zl-card-nav__link:focus-visible {\n outline: var(--zl-focus-width, 2px) solid var(--zl-focus-color, #f4f4f6);\n outline-offset: var(--zl-focus-offset, 2px);\n}\n\n.zl-card-forgot {\n text-align: left;\n margin: 0;\n}\n\n.zl-card-forgot__link {\n font-family: var(--zl-font-family-sans, system-ui, sans-serif);\n color: var(--zl-color-icon-default-purple, #bba5e4);\n cursor: pointer;\n background: none;\n border: none;\n margin: 0;\n padding: 0;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5rem;\n text-decoration: none;\n display: inline;\n}\n\n.zl-card-forgot__link:hover {\n text-decoration: underline;\n}\n\n.zl-card-forgot__link:focus-visible {\n outline: var(--zl-focus-width, 2px) solid var(--zl-focus-color, #f4f4f6);\n outline-offset: var(--zl-focus-offset, 2px);\n}\n\n.zl-split {\n grid-template-columns: var(--zl-split-columns, minmax(0, 1fr) minmax(0, 1fr));\n align-items: var(--zl-split-align, center);\n gap: var(--zl-spacing-05, 2rem);\n min-height: 100%;\n display: grid;\n}\n\n.zl-split__brand {\n justify-content: center;\n align-items: center;\n gap: var(--zl-spacing-04, 1.5rem);\n flex-direction: column;\n padding: 52px 16px;\n display: flex;\n}\n\n.zl-split__logo {\n max-width: min(16rem, 60%);\n}\n\n.zl-split__hero {\n border-radius: var(--zl-radius-m, .75rem);\n object-fit: cover;\n max-width: 100%;\n}\n\n.zl-split__form {\n justify-content: center;\n align-items: center;\n padding: 16px;\n display: flex;\n}\n\n.zl-split__compact {\n max-width: min(12rem, 60%);\n display: none;\n}\n\nimg.zl-split__compact {\n max-height: 2.5rem;\n}\n\nimg.zl-split__compact--hero {\n border-radius: var(--zl-radius-m, .75rem);\n object-fit: cover;\n width: 100%;\n max-width: 100%;\n max-height: 6rem;\n}\n\n.zl-split--right .zl-split__brand {\n order: 2;\n}\n\n.zl-split--right .zl-split__form {\n order: 1;\n}\n\n@container (width <= 48rem) {\n .zl-split {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .zl-split__brand {\n display: var(--zl-split-brand-mobile, none);\n }\n\n .zl-split__form {\n gap: var(--zl-spacing-04, 1.5rem);\n flex-direction: column;\n }\n\n .zl-split__compact {\n display: block;\n }\n}\n\n.zl-hero {\n text-align: left;\n width: 100%;\n max-width: 34rem;\n color: var(--zl-color-text-primary-white, #f4f4f6);\n font-family: var(--zl-font-family-sans, system-ui, sans-serif);\n flex-direction: column;\n display: flex;\n}\n\n.zl-hero__nav {\n margin-bottom: var(--zl-spacing-05, 2rem);\n justify-content: space-between;\n align-items: center;\n display: flex;\n}\n\n.zl-hero__brand {\n align-items: center;\n gap: var(--zl-spacing-02, .5rem);\n letter-spacing: -.01em;\n font-size: 1.125rem;\n font-weight: 700;\n display: inline-flex;\n}\n\n.zl-hero__brand-logo {\n max-height: 1.75rem;\n}\n\n.zl-hero__brand-mark {\n border-radius: var(--zl-radius-s, .5rem);\n background: linear-gradient(135deg,\n var(--zl-color-icon-default-purple, #bba5e4),\n var(--zl-color-icon-default-pink, #f48aa0));\n width: 1.75rem;\n height: 1.75rem;\n display: inline-flex;\n}\n\n.zl-hero__compact-brand {\n overflow-wrap: anywhere;\n letter-spacing: -.01em;\n max-width: 100%;\n color: var(--zl-color-text-primary-white, #f4f4f6);\n margin: 0;\n font-size: 1.125rem;\n font-weight: 700;\n}\n\n.zl-hero__nav-links {\n gap: var(--zl-spacing-04, 1.5rem);\n color: var(--zl-color-text-secondary-gray, #bfbfcf);\n font-size: .9375rem;\n display: flex;\n}\n\n.zl-hero__nav-links a {\n color: inherit;\n text-decoration: none;\n}\n\n.zl-hero__nav-links a:hover {\n text-decoration: underline;\n}\n\n.zl-hero__eyebrow {\n margin: 0 0 var(--zl-spacing-03, 1rem);\n letter-spacing: .08em;\n text-transform: uppercase;\n color: var(--zl-color-text-subtitle-purple, #bba5e4);\n font-size: .8125rem;\n font-weight: 600;\n}\n\n.zl-hero__headline {\n margin: 0 0 var(--zl-spacing-03, 1rem);\n font-family: var(--zl-font-family-heading, var(--zl-font-family-sans, system-ui, sans-serif));\n letter-spacing: -.02em;\n font-size: 2.75rem;\n font-weight: 700;\n line-height: 1.1;\n}\n\n.zl-hero__sub {\n margin: 0 0 var(--zl-spacing-05, 2rem);\n max-width: 30rem;\n color: var(--zl-color-text-secondary-gray, #bfbfcf);\n font-size: 1.0625rem;\n line-height: 1.6;\n}\n\n.zl-hero__bullets {\n gap: var(--zl-spacing-02, .5rem);\n margin: 0 0 var(--zl-spacing-05, 2rem);\n flex-direction: column;\n padding: 0;\n list-style: none;\n display: flex;\n}\n\n.zl-hero__bullets li {\n align-items: center;\n gap: var(--zl-spacing-02, .5rem);\n font-size: 1rem;\n display: flex;\n}\n\n.zl-hero__bullets li:before {\n content: \"✓\";\n color: var(--zl-color-icon-default-purple, #bba5e4);\n font-weight: 700;\n}\n\n.zl-hero__footnote {\n padding-top: var(--zl-spacing-04, 1.5rem);\n border-top: 1px solid var(--zl-color-border-default-black, #252528);\n color: var(--zl-color-text-disabled, #686883);\n margin: 0;\n font-size: .8125rem;\n}\n\n.zl-hero__footnote strong {\n color: var(--zl-color-text-secondary-gray, #bfbfcf);\n font-weight: 600;\n}\n\n.zl-minimal {\n gap: var(--zl-spacing-04, 1.5rem);\n flex-direction: column;\n width: min(24rem, 100%);\n margin-inline: auto;\n display: flex;\n}\n\n:host([variant=\"page\"]) .zl-minimal {\n padding: 52px 16px;\n}\n";
3003
+ //#endregion
2684
3004
  //#region src/orchestrator/zitadel-login.ts
2685
- let ZitadelLogin = class ZitadelLogin extends LitElement {
3005
+ /** Narrow a rendered named element to the `formValue` field-atom contract. */
3006
+ function isFieldAtom(el) {
3007
+ return typeof el.formValue === "string";
3008
+ }
3009
+ let ZitadelLogin = class ZitadelLogin extends ZitadelSurface {
2686
3010
  static shadowRootOptions = {
2687
3011
  ...LitElement.shadowRootOptions,
2688
3012
  delegatesFocus: true
@@ -2691,7 +3015,6 @@ let ZitadelLogin = class ZitadelLogin extends LitElement {
2691
3015
  :host {
2692
3016
  display: block;
2693
3017
  width: 100%;
2694
- min-height: 100vh;
2695
3018
  }
2696
3019
  `;
2697
3020
  #_purpose_accessor_storage = "login";
@@ -2799,7 +3122,6 @@ let ZitadelLogin = class ZitadelLogin extends LitElement {
2799
3122
  set formValues(value) {
2800
3123
  this.#_formValues_accessor_storage = value;
2801
3124
  }
2802
- themeController = new ThemeController(this);
2803
3125
  engine = null;
2804
3126
  sanitise = createSanitiser();
2805
3127
  /**
@@ -2807,6 +3129,25 @@ let ZitadelLogin = class ZitadelLogin extends LitElement {
2807
3129
  * every `formValues` change otherwise re-parses the same template.
2808
3130
  */
2809
3131
  tenantTemplateCache = null;
3132
+ /**
3133
+ * Whether the widget currently owns a same-document history entry (the
3134
+ * "sentinel"). The sentinel exists so the browser's back gesture fires
3135
+ * `popstate` instead of leaving the page. Exactly one sentinel is on
3136
+ * the stack at a time: it is pushed when a step with a `kind: "back"`
3137
+ * action renders, re-armed by `onPopState` after the browser consumes
3138
+ * it, and retired by `applyResponse` when a step without a back action
3139
+ * renders. The entry reuses the current URL, so the host page's
3140
+ * location (including any hash-router fragment) is never modified.
3141
+ */
3142
+ armed = false;
3143
+ /**
3144
+ * Set immediately before a self-initiated `history.back()` so the
3145
+ * resulting `popstate` is ignored instead of being interpreted as a
3146
+ * user back gesture.
3147
+ */
3148
+ ignoreNextPop = false;
3149
+ /** Bound `popstate` handler stored for cleanup in `disconnectedCallback`. */
3150
+ handlePopState = this.onPopState.bind(this);
2810
3151
  createRenderRoot() {
2811
3152
  const root = super.createRenderRoot();
2812
3153
  if (root instanceof ShadowRoot) {
@@ -2825,6 +3166,14 @@ let ZitadelLogin = class ZitadelLogin extends LitElement {
2825
3166
  }
2826
3167
  return root;
2827
3168
  }
3169
+ connectedCallback() {
3170
+ super.connectedCallback();
3171
+ if (typeof window !== "undefined") window.addEventListener("popstate", this.handlePopState);
3172
+ }
3173
+ disconnectedCallback() {
3174
+ super.disconnectedCallback();
3175
+ if (typeof window !== "undefined") window.removeEventListener("popstate", this.handlePopState);
3176
+ }
2828
3177
  /**
2829
3178
  * Start the flow after the first render rather than in `connectedCallback`.
2830
3179
  * Frameworks that wrap web components (e.g. `@lit/react` in the console)
@@ -2851,28 +3200,25 @@ let ZitadelLogin = class ZitadelLogin extends LitElement {
2851
3200
  const primary = ((this.lang || (typeof navigator !== "undefined" ? navigator.language : "") || (typeof document !== "undefined" ? document.documentElement.lang : "")).split("-")[0] ?? "").toLowerCase();
2852
3201
  const builtin = builtinLocales[primary] ?? en;
2853
3202
  const custom = this.locales?.[primary];
2854
- return custom ? {
2855
- ...builtin,
2856
- ...custom
2857
- } : builtin;
3203
+ if (!custom) return builtin;
3204
+ const merged = { ...builtin };
3205
+ for (const [key, value] of Object.entries(custom)) if (value !== void 0) merged[key] = value;
3206
+ return merged;
2858
3207
  }
2859
3208
  willUpdate(changed) {
2860
3209
  if (!this.engine || changed.has("locales") || changed.has("lang")) this.engine = createLiquidEngine({ locale: this.resolveLocale() });
2861
- const root = this.shadowRoot;
2862
- if (root) {
2863
- applyBaseTokens(root);
2864
- applyBrandingTokens(root, this.branding, this.themeController.theme);
2865
- const tenantFontUrl = this.branding?.font_url ?? null;
2866
- applyDefaultFont(root, tenantFontUrl ? null : void 0);
2867
- applyFontUrl(root, tenantFontUrl);
2868
- }
2869
- this.dataset.theme = this.themeController.theme;
2870
- this.toggleAttribute("data-theme-dark", this.themeController.theme === "dark");
3210
+ this.applySurfaceTheme(this.branding);
2871
3211
  this.setAttribute("aria-busy", this.loading ? "true" : "false");
2872
3212
  }
2873
3213
  updated(changed) {
2874
- if (!changed.has("response")) return;
2875
- this.hydrateStepAfterRender();
3214
+ if (this.shadowRoot) {
3215
+ stampExportparts(this.shadowRoot);
3216
+ const widget = this.variant !== "page";
3217
+ for (const shell of this.shadowRoot.querySelectorAll("zl-page-shell")) shell.toggleAttribute("data-widget", widget);
3218
+ }
3219
+ const props = changed;
3220
+ if (!props.has("response")) return;
3221
+ this.hydrateStepAfterRender(props.get("response") == null);
2876
3222
  }
2877
3223
  /**
2878
3224
  * Apply captured values and move focus once the new step has fully
@@ -2880,13 +3226,18 @@ let ZitadelLogin = class ZitadelLogin extends LitElement {
2880
3226
  * but those render their own shadow DOM on a later microtask — so await
2881
3227
  * this element's update *and* the child atoms' first render before touching
2882
3228
  * them, rather than guessing a frame with `requestAnimationFrame`.
3229
+ *
3230
+ * Focus on the *initial* response is page-mode-only: a dedicated login
3231
+ * route should focus its first field, but a widget embedded further down
3232
+ * an arbitrary page must not steal focus and scroll-jump on load. Step
3233
+ * swaps are user-initiated, so focus moves in both modes.
2883
3234
  */
2884
- async hydrateStepAfterRender() {
3235
+ async hydrateStepAfterRender(initial = false) {
2885
3236
  await this.updateComplete;
2886
3237
  const atoms = this.shadowRoot?.querySelectorAll("zl-field, zl-button");
2887
3238
  if (atoms) await Promise.all(Array.from(atoms).map((atom) => atom.updateComplete));
2888
3239
  this.applyValuesToFields();
2889
- this.moveFocusToFirstField();
3240
+ if (!initial || this.variant === "page") this.moveFocusToFirstField();
2890
3241
  }
2891
3242
  render() {
2892
3243
  if (this.startupError) return html`<form class="zl-mount" novalidate>
@@ -2960,6 +3311,7 @@ let ZitadelLogin = class ZitadelLogin extends LitElement {
2960
3311
  });
2961
3312
  }
2962
3313
  this.applyResponse(wire);
3314
+ emit(this, "zitadel-flow-step", { step: wire.step });
2963
3315
  } catch (error) {
2964
3316
  this.handleTransportError(this.describeFlowSelectionError(error));
2965
3317
  } finally {
@@ -2990,6 +3342,22 @@ let ZitadelLogin = class ZitadelLogin extends LitElement {
2990
3342
  ...collectInitialValues(wire.step),
2991
3343
  ...this.formValues
2992
3344
  };
3345
+ if (typeof window !== "undefined") {
3346
+ const hasBack = Boolean(wire.step.actions?.some((a) => a.kind === "back"));
3347
+ if (hasBack && !this.armed) {
3348
+ history.pushState({
3349
+ ...history.state,
3350
+ zl: true
3351
+ }, "");
3352
+ this.armed = true;
3353
+ } else if (!hasBack && this.armed) {
3354
+ this.armed = false;
3355
+ if (history.state?.zl === true) {
3356
+ this.ignoreNextPop = true;
3357
+ history.back();
3358
+ }
3359
+ }
3360
+ }
2993
3361
  this.maybeCompleteFlow(wire);
2994
3362
  }
2995
3363
  /**
@@ -3096,42 +3464,89 @@ let ZitadelLogin = class ZitadelLogin extends LitElement {
3096
3464
  ...display ? { display_name: display } : email ? { display_name: email } : {}
3097
3465
  };
3098
3466
  }
3099
- applyValuesToFields() {
3467
+ /** All rendered input atoms exposing the `formValue` contract. */
3468
+ fieldAtoms() {
3100
3469
  const root = this.shadowRoot;
3101
- if (!root) return;
3102
- const fields = root.querySelectorAll("zl-field");
3103
- if (fields.length === 0) return;
3104
- for (const field of fields) {
3105
- const name = field.getAttribute("name");
3106
- if (!name || !(name in this.formValues)) continue;
3470
+ if (!root) return [];
3471
+ return Array.from(root.querySelectorAll("[name]")).filter(isFieldAtom);
3472
+ }
3473
+ applyValuesToFields() {
3474
+ for (const atom of this.fieldAtoms()) {
3475
+ const name = atom.getAttribute("name");
3476
+ if (!name) continue;
3107
3477
  const next = this.formValues[name];
3108
- if (field.value !== next) field.value = next;
3478
+ if (next !== void 0 && atom.formValue !== next) atom.formValue = next;
3109
3479
  }
3110
3480
  }
3111
- captureValuesFromFields() {
3112
- const root = this.shadowRoot;
3113
- if (!root) return this.formValues;
3114
- const fields = root.querySelectorAll("zl-field");
3115
- if (fields.length === 0) return this.formValues;
3116
- const next = { ...this.formValues };
3117
- let changed = false;
3118
- for (const field of fields) {
3119
- const name = field.getAttribute("name");
3120
- const value = this.currentFieldValue(field);
3121
- if (!name || value === void 0) continue;
3122
- if (field.value !== value) field.value = value;
3123
- if (next[name] !== value) {
3124
- next[name] = value;
3125
- changed = true;
3126
- }
3481
+ /**
3482
+ * Names of the current step's `required` fields whose captured value is
3483
+ * empty. Reads each atom's live `formValue` (the getter reflects the native
3484
+ * control, so autofill that skipped `input` events is still seen), so this
3485
+ * is browser-independent and does not rely on native constraint validation.
3486
+ */
3487
+ missingRequiredFields() {
3488
+ const values = /* @__PURE__ */ new Map();
3489
+ for (const atom of this.fieldAtoms()) {
3490
+ const name = atom.getAttribute("name");
3491
+ if (name) values.set(name, atom.formValue);
3127
3492
  }
3128
- if (changed) this.formValues = next;
3129
- return next;
3493
+ const missing = [];
3494
+ for (const field of this.response?.step.fields ?? []) {
3495
+ if (field.type === "checkbox") continue;
3496
+ if (field.required && (values.get(field.name) ?? "") === "") missing.push(field.name);
3497
+ }
3498
+ return missing;
3499
+ }
3500
+ /**
3501
+ * Surface a client-side required-field error using the server's own
3502
+ * validation dialect (`error.<field>_required`, "; "-joined), so it flows
3503
+ * through the same localisation and inline/banner routing as a real
3504
+ * backend rejection — no native browser bubble. Idempotent: re-running with
3505
+ * the same keys (both submit entry points fire on one click) is a no-op.
3506
+ */
3507
+ reportRequiredErrors(fields) {
3508
+ if (!this.response) return;
3509
+ const errorKey = fields.map((name) => `error.${name}_required`).join("; ");
3510
+ if (this.response.step.error === errorKey) return;
3511
+ this.stepErrorDismissed = false;
3512
+ this.response = {
3513
+ ...this.response,
3514
+ step: {
3515
+ ...this.response.step,
3516
+ error: errorKey
3517
+ }
3518
+ };
3130
3519
  }
3131
- currentFieldValue(field) {
3132
- const native = field.shadowRoot?.querySelector("input");
3133
- if (native && native.value !== field.value) return native.value;
3134
- return typeof field.value === "string" ? field.value : void 0;
3520
+ /**
3521
+ * Snapshot the current step's field values straight from the rendered input
3522
+ * atoms through their uniform `formValue` contract. Tag-agnostic: every
3523
+ * form-participating atom is read the same way, so a new field type needs no
3524
+ * change here. Declared fields default to "" so the backend still runs its
3525
+ * required-checks and challenge dispatch instead of silently advancing on a
3526
+ * field-less payload. Captured values are folded into `formValues` for
3527
+ * cross-step identity (the signed-in greeting) and post-error restoration.
3528
+ */
3529
+ collectSubmitFields() {
3530
+ const current = /* @__PURE__ */ new Map();
3531
+ for (const atom of this.fieldAtoms()) {
3532
+ const name = atom.getAttribute("name");
3533
+ if (name) current.set(name, atom.formValue);
3534
+ }
3535
+ if (current.size > 0) this.formValues = {
3536
+ ...this.formValues,
3537
+ ...Object.fromEntries(current)
3538
+ };
3539
+ const fields = {};
3540
+ for (const f of this.response?.step.fields ?? []) {
3541
+ const value = current.get(f.name) ?? "";
3542
+ if (f.type === "checkbox") {
3543
+ fields[f.name] = value !== "";
3544
+ continue;
3545
+ }
3546
+ if (f.type === "select" && !isAllowedSelectValue(f, value)) continue;
3547
+ fields[f.name] = value;
3548
+ }
3549
+ return fields;
3135
3550
  }
3136
3551
  /**
3137
3552
  * True once the user retired the current step error by editing a field or
@@ -3159,10 +3574,25 @@ let ZitadelLogin = class ZitadelLogin extends LitElement {
3159
3574
  value
3160
3575
  });
3161
3576
  };
3162
- /** `zl-change` from <zl-checkbox>/<zl-select>: only error clearing. */
3577
+ /**
3578
+ * `zl-change` from <zl-checkbox>/<zl-select>. Persist the atom's value into
3579
+ * `formValues` — mirroring `handleAtomInput` for text fields — so a later
3580
+ * step re-render (a validation error re-parses the template via `unsafeHTML`
3581
+ * and rebuilds the atoms) restores the selection/checked state through
3582
+ * `applyValuesToFields` instead of dropping back to the template default.
3583
+ * Reads the live `formValue` off the atom rather than the event's `value`
3584
+ * token, because an unchecked checkbox reports "" there but keeps its token
3585
+ * in the detail. Also clears stale errors on the edited field.
3586
+ */
3163
3587
  handleAtomEdited = (event) => {
3164
3588
  const name = event.detail?.name;
3165
- if (name) this.clearStaleErrors(name);
3589
+ if (!name) return;
3590
+ const atom = event.target;
3591
+ if (atom instanceof HTMLElement && isFieldAtom(atom)) this.formValues = {
3592
+ ...this.formValues,
3593
+ [name]: atom.formValue
3594
+ };
3595
+ this.clearStaleErrors(name);
3166
3596
  };
3167
3597
  /** Explicit dismiss of the step-error alert (it removes itself). */
3168
3598
  handleAlertDismiss = (event) => {
@@ -3188,16 +3618,9 @@ let ZitadelLogin = class ZitadelLogin extends LitElement {
3188
3618
  field.error = "";
3189
3619
  }
3190
3620
  }
3621
+ /** Mirror a value onto the matching rendered atom (used after atom events). */
3191
3622
  syncFieldElementValue(name, value) {
3192
- const root = this.shadowRoot;
3193
- if (!root) return;
3194
- const fields = root.querySelectorAll("zl-field");
3195
- for (const field of fields) {
3196
- if (field.getAttribute("name") !== name) continue;
3197
- if (field.value !== value) field.value = value;
3198
- const native = field.shadowRoot?.querySelector("input");
3199
- if (native && native.value !== value) native.value = value;
3200
- }
3623
+ for (const atom of this.fieldAtoms()) if (atom.getAttribute("name") === name && atom.formValue !== value) atom.formValue = value;
3201
3624
  }
3202
3625
  handleAtomSubmit = (event) => {
3203
3626
  if (this.loading) return;
@@ -3215,6 +3638,11 @@ let ZitadelLogin = class ZitadelLogin extends LitElement {
3215
3638
  handleFormSubmit = (event) => {
3216
3639
  event.preventDefault();
3217
3640
  if (this.loading) return;
3641
+ const missing = this.missingRequiredFields();
3642
+ if (missing.length > 0) {
3643
+ this.reportRequiredErrors(missing);
3644
+ return;
3645
+ }
3218
3646
  const action = event.submitter?.getAttribute?.("action") ?? null ?? this.findPrimaryAction();
3219
3647
  this.submit(action);
3220
3648
  };
@@ -3277,13 +3705,7 @@ let ZitadelLogin = class ZitadelLogin extends LitElement {
3277
3705
  const { id, session_token } = this.response;
3278
3706
  this.loading = true;
3279
3707
  try {
3280
- const formValues = this.captureValuesFromFields();
3281
- const stepFields = this.response.step.fields ?? [];
3282
- const fields = {};
3283
- for (const f of stepFields) {
3284
- const value = formValues[f.name];
3285
- if (value !== void 0) fields[f.name] = value;
3286
- }
3708
+ const fields = this.collectSubmitFields();
3287
3709
  const body = {
3288
3710
  session_token,
3289
3711
  action: action ?? "submit",
@@ -3300,6 +3722,47 @@ let ZitadelLogin = class ZitadelLogin extends LitElement {
3300
3722
  this.loading = false;
3301
3723
  }
3302
3724
  }
3725
+ /**
3726
+ * Handle the browser's back/forward gesture (ADR 022). When `popstate`
3727
+ * fires:
3728
+ *
3729
+ * - **Self-initiated** (`ignoreNextPop`) → `applyResponse` is retiring
3730
+ * the sentinel; ignore.
3731
+ * - **Back press while armed** → the browser consumed the sentinel.
3732
+ * Re-arm it immediately — so the stack shape is identical on every
3733
+ * step and repeated presses behave the same at any flow depth — then
3734
+ * submit the step's `kind: "back"` action.
3735
+ * - **Landing on the sentinel while armed** → the host page pushed an
3736
+ * entry above the sentinel (e.g. an in-page `#anchor` click) and the
3737
+ * user backed out of it. They are back where the widget expects them
3738
+ * — not asking the flow to go back; do nothing.
3739
+ * - **Forward press onto a retired sentinel** (it survives as a forward
3740
+ * entry after `history.back()`) → bounce back: flow state is
3741
+ * server-authoritative, the browser cannot skip ahead
3742
+ * (ADR 022 §Edge cases).
3743
+ * - Anything else is host-page traversal — leave the browser alone.
3744
+ */
3745
+ onPopState(event) {
3746
+ if (this.ignoreNextPop) {
3747
+ this.ignoreNextPop = false;
3748
+ return;
3749
+ }
3750
+ if (this.armed) {
3751
+ if (event.state?.zl === true) return;
3752
+ this.armed = false;
3753
+ const backAction = this.response?.step?.actions?.find((a) => a.kind === "back");
3754
+ if (backAction) {
3755
+ history.pushState({ zl: true }, "");
3756
+ this.armed = true;
3757
+ this.submit(backAction.name);
3758
+ }
3759
+ return;
3760
+ }
3761
+ if (event.state?.zl === true) {
3762
+ this.ignoreNextPop = true;
3763
+ history.back();
3764
+ }
3765
+ }
3303
3766
  handleTransportError(error) {
3304
3767
  const message = error instanceof ApiError ? apiErrorMessage(error) : error instanceof Error ? error.message : "Unexpected error contacting the Flow API.";
3305
3768
  this.startupError = message;
@@ -3338,6 +3801,16 @@ __decorate([state()], ZitadelLogin.prototype, "loading", null);
3338
3801
  __decorate([state()], ZitadelLogin.prototype, "startupError", null);
3339
3802
  __decorate([state()], ZitadelLogin.prototype, "formValues", null);
3340
3803
  ZitadelLogin = __decorate([customElement("zitadel-login")], ZitadelLogin);
3804
+ /**
3805
+ * Whether `value` is a member of a select field's closed `enum`. A select
3806
+ * with no explicit enum has no submittable value, so this returns `false`
3807
+ * and the caller omits the field. Because the enum never contains "" unless
3808
+ * the schema deliberately lists it, an untouched placeholder ("") is omitted
3809
+ * rather than sent and rejected by the server's enum validation.
3810
+ */
3811
+ function isAllowedSelectValue(field, value) {
3812
+ return field.validation?.enum?.includes(value) ?? false;
3813
+ }
3341
3814
  function collectInitialValues(step) {
3342
3815
  const values = {};
3343
3816
  if (!step.fields) return values;
@@ -3519,12 +3992,12 @@ let ZitadelLogout = class ZitadelLogout extends LitElement {
3519
3992
  set postSignOutUrl(value) {
3520
3993
  this.#_postSignOutUrl_accessor_storage = value;
3521
3994
  }
3522
- #_clientId_accessor_storage = "";
3523
- get clientId() {
3524
- return this.#_clientId_accessor_storage;
3995
+ #_theme_accessor_storage = "";
3996
+ get theme() {
3997
+ return this.#_theme_accessor_storage;
3525
3998
  }
3526
- set clientId(value) {
3527
- this.#_clientId_accessor_storage = value;
3999
+ set theme(value) {
4000
+ this.#_theme_accessor_storage = value;
3528
4001
  }
3529
4002
  #_displayName_accessor_storage = "";
3530
4003
  get displayName() {
@@ -3572,9 +4045,9 @@ let ZitadelLogout = class ZitadelLogout extends LitElement {
3572
4045
  pendingTemplate = null;
3573
4046
  projectedContainer = null;
3574
4047
  identityRequested = false;
4048
+ themeController = new ThemeController(this);
3575
4049
  connectedCallback() {
3576
4050
  super.connectedCallback();
3577
- this.dataset.theme = "dark";
3578
4051
  const tmpl = this.querySelector("template");
3579
4052
  if (tmpl instanceof HTMLTemplateElement) {
3580
4053
  this.templateMode = true;
@@ -3590,9 +4063,17 @@ let ZitadelLogout = class ZitadelLogout extends LitElement {
3590
4063
  document.removeEventListener("click", this.handleDocumentClick);
3591
4064
  document.removeEventListener("keydown", this.handleDocumentKeydown);
3592
4065
  }
3593
- updated() {
4066
+ willUpdate() {
4067
+ this.themeController.setModePreference(this.theme === "" ? void 0 : this.theme, "auto");
3594
4068
  const root = this.shadowRoot;
3595
- if (root && !this.templateMode) applyBaseTokens(root);
4069
+ if (root && !this.templateMode) {
4070
+ applyBaseTokens(root);
4071
+ applyBrandingTokens(root, void 0, this.themeController.theme);
4072
+ }
4073
+ this.dataset.theme = this.themeController.theme;
4074
+ this.toggleAttribute("data-theme-dark", this.themeController.theme === "dark");
4075
+ }
4076
+ updated() {
3596
4077
  this.maybeLoadIdentity();
3597
4078
  }
3598
4079
  /** Declarative config read from this element's attributes. */
@@ -3703,20 +4184,6 @@ let ZitadelLogout = class ZitadelLogout extends LitElement {
3703
4184
  });
3704
4185
  if (this.postSignOutUrl && typeof window !== "undefined") window.location.href = this.postSignOutUrl;
3705
4186
  }
3706
- /**
3707
- * Returns the absolute URL the end-session request will hit. Useful for
3708
- * test assertions and for consumers that prefer to navigate the browser
3709
- * directly (instead of fetching) so the OIDC session-end redirect is
3710
- * driven by the user agent.
3711
- */
3712
- getEndSessionUrl() {
3713
- const params = {
3714
- ...this.clientId ? { client_id: this.clientId } : {},
3715
- ...this.postSignOutUrl ? { post_logout_redirect_uri: this.postSignOutUrl } : {}
3716
- };
3717
- const { api } = resolveApi(this.project, this.projectAttrs, "<zitadel-logout>");
3718
- return api.getEndSessionUrl(params);
3719
- }
3720
4187
  handleSignOutClick(event) {
3721
4188
  event.preventDefault();
3722
4189
  this.doLogout();
@@ -3795,10 +4262,7 @@ __decorate([property({
3795
4262
  type: String,
3796
4263
  attribute: "post-sign-out-url"
3797
4264
  })], ZitadelLogout.prototype, "postSignOutUrl", null);
3798
- __decorate([property({
3799
- type: String,
3800
- attribute: "client-id"
3801
- })], ZitadelLogout.prototype, "clientId", null);
4265
+ __decorate([property({ type: String })], ZitadelLogout.prototype, "theme", null);
3802
4266
  __decorate([state()], ZitadelLogout.prototype, "displayName", null);
3803
4267
  __decorate([state()], ZitadelLogout.prototype, "displayEmail", null);
3804
4268
  __decorate([state()], ZitadelLogout.prototype, "displayUserId", null);
@@ -3821,13 +4285,11 @@ function fillTemplateTokens(fragment, name, email, initial) {
3821
4285
  }
3822
4286
  //#endregion
3823
4287
  //#region src/orchestrator/zitadel-session.ts
3824
- let ZitadelSession = class ZitadelSession extends LitElement {
4288
+ let ZitadelSession = class ZitadelSession extends ZitadelSurface {
3825
4289
  static styles = [baseHostStyles, css`
3826
4290
  :host {
3827
4291
  display: block;
3828
- min-height: 100vh;
3829
- background: ${t.color.surface.defaultBlack};
3830
- color: ${t.color.text.primaryWhite};
4292
+ width: 100%;
3831
4293
  }
3832
4294
  .title {
3833
4295
  margin: 0;
@@ -3943,16 +4405,16 @@ let ZitadelSession = class ZitadelSession extends LitElement {
3943
4405
  identityRequested = false;
3944
4406
  connectedCallback() {
3945
4407
  super.connectedCallback();
3946
- this.dataset.theme = "dark";
3947
4408
  this.maybeLoadIdentity();
3948
4409
  }
3949
4410
  /** Single identity line: human-readable name, then email, then user_id. */
3950
4411
  get identityLabel() {
3951
4412
  return this.displayName || this.displayEmail || this.displayUserId;
3952
4413
  }
4414
+ willUpdate() {
4415
+ this.applySurfaceTheme(void 0);
4416
+ }
3953
4417
  updated() {
3954
- const root = this.shadowRoot;
3955
- if (root) applyBaseTokens(root);
3956
4418
  this.maybeLoadIdentity();
3957
4419
  }
3958
4420
  /** Declarative config read from this element's attributes. */
@@ -4017,7 +4479,7 @@ let ZitadelSession = class ZitadelSession extends LitElement {
4017
4479
  }
4018
4480
  render() {
4019
4481
  return html`
4020
- <zl-page-shell>
4482
+ <zl-page-shell ?data-widget=${this.variant !== "page"}>
4021
4483
  <zl-card>
4022
4484
  <h1 slot="header" class="title">${this.heading}</h1>
4023
4485
  ${this.identityLabel ? html`<p slot="header" class="identity">${this.identityLabel}</p>` : nothing}
@@ -4064,6 +4526,6 @@ __decorate([state()], ZitadelSession.prototype, "loading", null);
4064
4526
  __decorate([state()], ZitadelSession.prototype, "errorMessage", null);
4065
4527
  ZitadelSession = __decorate([customElement("zitadel-session")], ZitadelSession);
4066
4528
  //#endregion
4067
- export { submitStep as S, applyBrandingTokens as _, layout_chrome_default as a, getCurrentStep as b, it as c, TEMPLATE_NAMES as d, MANDATORY_GATES_MARKER as f, validateBranding as g, applyFontUrl as h, ThemeController as i, de as l, patchMandatoryGates as m, ZitadelLogout as n, createSanitiser as o, mandatoryGatesMarkerComment as p, ZitadelLogin as r, builtinLocales as s, ZitadelSession as t, en as u, buildBrandingStylesheet as v, startFlow as x, resolveTheme as y };
4529
+ export { submitStep as C, startFlow as S, MANDATORY_GATES_MARKER as _, ThemeController as a, validateBranding as b, buildBrandingStylesheet as c, builtinLocales as d, businessLocales as f, TEMPLATE_NAMES as g, en as h, layout_chrome_default as i, resolveTheme as l, de as m, ZitadelLogout as n, applyFontUrl as o, it as p, ZitadelLogin as r, applyBrandingTokens as s, ZitadelSession as t, createSanitiser as u, mandatoryGatesMarkerComment as v, getCurrentStep as x, patchMandatoryGates as y };
4068
4530
 
4069
- //# sourceMappingURL=orchestrator-BN2nT9FZ.mjs.map
4531
+ //# sourceMappingURL=orchestrator-CctLkpmB.mjs.map