@zitadel/config 0.1.0-alpha.17 → 0.1.0-alpha.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/defaults/README-branding.md +96 -0
- package/defaults/README-flows.md +21 -4
- package/defaults/README-schemas.md +2 -2
- package/defaults/branding/centered/login.liquid +164 -0
- package/defaults/branding/hero/login.liquid +224 -0
- package/defaults/branding/minimal/login.liquid +154 -0
- package/defaults/branding/split/login.liquid +183 -0
- package/defaults/branding/split-right/login.liquid +183 -0
- package/defaults/default-human-user.json +2 -19
- package/defaults/default-login.json +21 -4
- package/defaults/presets/passkey-first/human-user.json +2 -19
- package/defaults/presets/passkey-first/login.json +31 -4
- package/dist/branding-url.d.mts +14 -0
- package/dist/branding-url.d.mts.map +1 -0
- package/dist/branding-url.mjs +23 -0
- package/dist/branding-url.mjs.map +1 -0
- package/dist/defaults-Bh4_xVbh.d.mts +84 -0
- package/dist/defaults-Bh4_xVbh.d.mts.map +1 -0
- package/dist/defaults-eiGHyI5Y.mjs +569 -0
- package/dist/defaults-eiGHyI5Y.mjs.map +1 -0
- package/dist/defaults.d.mts +2 -2
- package/dist/defaults.mjs +2 -2
- package/dist/index.d.mts +6 -4
- package/dist/index.mjs +6 -4
- package/dist/{meta-schemas-8nlyVL7g.mjs → meta-schemas-CSc18JqO.mjs} +92 -31
- package/dist/meta-schemas-CSc18JqO.mjs.map +1 -0
- package/dist/meta-schemas.d.mts +6 -1
- package/dist/meta-schemas.d.mts.map +1 -1
- package/dist/meta-schemas.mjs +2 -2
- package/dist/normalize.d.mts +11 -9
- package/dist/normalize.d.mts.map +1 -1
- package/dist/normalize.mjs +24 -17
- package/dist/normalize.mjs.map +1 -1
- package/dist/schemas.d.mts +14 -1
- package/dist/schemas.d.mts.map +1 -1
- package/dist/schemas.mjs +62 -2
- package/dist/schemas.mjs.map +1 -1
- package/dist/template.d.mts +76 -0
- package/dist/template.d.mts.map +1 -0
- package/dist/template.mjs +145 -0
- package/dist/template.mjs.map +1 -0
- package/dist/validate.mjs +70 -11
- package/dist/validate.mjs.map +1 -1
- package/meta-schemas/auth-method.json +2 -7
- package/meta-schemas/branding.json +44 -0
- package/meta-schemas/flow-definition.json +20 -1
- package/meta-schemas/property-name.json +7 -0
- package/meta-schemas/user-property.json +8 -24
- package/meta-schemas/user-schema.json +4 -1
- package/package.json +17 -4
- package/dist/defaults-BQej3KIY.mjs +0 -388
- package/dist/defaults-BQej3KIY.mjs.map +0 -1
- package/dist/defaults-CSjBjBkQ.d.mts +0 -41
- package/dist/defaults-CSjBjBkQ.d.mts.map +0 -1
- package/dist/meta-schemas-8nlyVL7g.mjs.map +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @zitadel/config
|
|
2
|
+
|
|
3
|
+
Versioned Zitadel local config schemas and defaults — the contract behind the
|
|
4
|
+
`.zitadel/` directory that `zitadel setup` scaffolds into your project.
|
|
5
|
+
|
|
6
|
+
## What's in here
|
|
7
|
+
|
|
8
|
+
| Import | What it carries |
|
|
9
|
+
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
10
|
+
| `@zitadel/config/schemas` | Zod schemas for the local config files (`schemaConfigSchema`, `flowConfigSchema`, `brandingConfigSchema`) |
|
|
11
|
+
| `@zitadel/config/branding-url` | Canonical loopback HTTP URL predicate shared by config and browser render gates |
|
|
12
|
+
| `@zitadel/config/validate` | Validation entry points the CLI runs on `plan`/`apply` |
|
|
13
|
+
| `@zitadel/config/normalize` | Normalization used to compare local files against server state |
|
|
14
|
+
| `@zitadel/config/defaults` | The versioned default user schema, login flow, and branding designs |
|
|
15
|
+
| `@zitadel/config/meta-schemas` | The JSON meta-schemas the defaults pin |
|
|
16
|
+
| `@zitadel/config/template` | Liquid template helpers |
|
|
17
|
+
| `@zitadel/config/defaults/default-login.json` | The default login flow — the authority for flow step shape |
|
|
18
|
+
| `@zitadel/config/defaults/default-human-user.json` | The default user schema |
|
|
19
|
+
|
|
20
|
+
## The customer-facing part
|
|
21
|
+
|
|
22
|
+
`zitadel setup` copies the defaults into your repo as
|
|
23
|
+
`.zitadel/{schemas,flows,branding}/`, each with a README explaining how to
|
|
24
|
+
edit it (`defaults/README-schemas.md`, `README-flows.md`,
|
|
25
|
+
`README-branding.md` in this package become those files). Those files are
|
|
26
|
+
yours to edit; `zitadel plan` / `zitadel apply` reconcile them with the
|
|
27
|
+
server, and edits publish new revisions rather than mutating state in place.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Branding
|
|
2
|
+
|
|
3
|
+
This directory owns how your login UI looks: `branding.json` (layout preset
|
|
4
|
+
and asset URLs) plus `login.liquid`, the LiquidJS template the
|
|
5
|
+
`<zitadel-login>` component renders for every step.
|
|
6
|
+
|
|
7
|
+
## Workflow
|
|
8
|
+
|
|
9
|
+
1. Edit `login.liquid` (and `branding.json` for logo/hero URLs).
|
|
10
|
+
2. `zitadel plan` — validates the template (LiquidJS parse, banned patterns,
|
|
11
|
+
the required `{% mandatory_gates %}` tag) and shows the pending revision.
|
|
12
|
+
3. `zitadel apply` — publishes an immutable branding revision. The login UI
|
|
13
|
+
picks up the newest revision on the next step response.
|
|
14
|
+
|
|
15
|
+
Every edit publishes a new revision; there is no in-place update. Roll back
|
|
16
|
+
by re-applying an earlier template.
|
|
17
|
+
|
|
18
|
+
## Rules of the template
|
|
19
|
+
|
|
20
|
+
- Compose `<zl-*>` atoms and plain structural HTML. No `<script>`, no
|
|
21
|
+
`<style>`, no inline event handlers — theming happens through design
|
|
22
|
+
tokens, not template CSS.
|
|
23
|
+
- User-facing copy goes through translation keys: `{{ "key" | t }}`.
|
|
24
|
+
- Keep the trailing `{% mandatory_gates %}` tag: it appends any required
|
|
25
|
+
field, gate, or submit action your template forgot, so a broken template
|
|
26
|
+
still yields a submittable step.
|
|
27
|
+
|
|
28
|
+
## Make it yours
|
|
29
|
+
|
|
30
|
+
- Brand asset URLs go in `branding.json` and must use `https://`. Each template
|
|
31
|
+
decides which fields it renders: `centered` and `hero` use `logo_url`; `split`
|
|
32
|
+
and `split-right` use both `logo_url` and `hero_url`; `minimal` uses neither
|
|
33
|
+
until you add them to `login.liquid`. The `hero` design's brand pane is
|
|
34
|
+
editable markup rather than a `hero_url` background. Custom fonts are not
|
|
35
|
+
configurable here yet; load them from the embedding page.
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"$schema": "../meta/branding.json",
|
|
40
|
+
"layout": "split",
|
|
41
|
+
"liquid_template_file": "./login.liquid",
|
|
42
|
+
"logo_url": "https://example.com/logo.svg",
|
|
43
|
+
"hero_url": "https://example.com/hero.jpg"
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
A well-formed URL that serves nothing is the one branding mistake nothing
|
|
48
|
+
else catches — it passes validation, publishes a revision, and then renders
|
|
49
|
+
as an invisible image. So `zitadel plan` probes each asset URL and warns
|
|
50
|
+
(never fails) when it is unreachable, bodyless, or not an image, and the
|
|
51
|
+
login UI hides an asset that fails to load, restoring the shipped design's
|
|
52
|
+
no-asset content rather than leaving a gap. If the host is only
|
|
53
|
+
reachable from where the login page renders — or you are offline — set
|
|
54
|
+
`ZITADEL_SKIP_ASSET_PROBE=1` to skip the check. The probe contacts public
|
|
55
|
+
HTTPS destinations only and re-checks redirects; loopback/private/internal
|
|
56
|
+
targets are left to the browser-side fallback instead of being requested by
|
|
57
|
+
the machine running `plan`.
|
|
58
|
+
|
|
59
|
+
`layout` is the degrade preset (`centered` or `split`), **not** the complete
|
|
60
|
+
design catalog. All named designs (`centered`, `split`, `split-right`, `hero`,
|
|
61
|
+
`minimal`) are delivered as templates and map onto those two values. Switch
|
|
62
|
+
designs with `zitadel branding eject --design <name>`, don't edit `layout`.
|
|
63
|
+
|
|
64
|
+
- In the split-family designs (`split`, `split-right`, `hero`) the
|
|
65
|
+
`.zl-split__brand` pane is yours: structural HTML plus inline `style=""`
|
|
66
|
+
attributes are allowed; `button`, `input`, and `form` tags are stripped —
|
|
67
|
+
use `<a>` for landing CTAs. The `hero` design ships a full landing-page
|
|
68
|
+
starting point on token-styled `zl-hero__*` classes.
|
|
69
|
+
- On narrow viewports the brand pane collapses and the `.zl-split__compact`
|
|
70
|
+
node inside the form pane takes over (logo or brand line) — keep one so
|
|
71
|
+
your identity survives on phones.
|
|
72
|
+
- The split chrome is tunable from your template root's `style` attribute
|
|
73
|
+
(`--zl-split-columns`, `--zl-split-align`, `--zl-split-brand-mobile`), and
|
|
74
|
+
`zl-split--right` on the wrapper mirrors the panes. Knob reference:
|
|
75
|
+
[`docs/design/branding/templates.md`](https://github.com/zitadel/nextgen/blob/main/docs/design/branding/templates.md).
|
|
76
|
+
- The "Secured with Zitadel" attribution is licence-gated and on by default.
|
|
77
|
+
- Back-navigation: the engine injects a `kind: "back"` action on steps
|
|
78
|
+
that can return to their predecessor, and the browser's back gesture
|
|
79
|
+
submits it automatically — the shipped designs deliberately render **no
|
|
80
|
+
visible back control**. Your template may render one if you want it;
|
|
81
|
+
select the action by kind (never by name) and submit it like any other
|
|
82
|
+
action:
|
|
83
|
+
|
|
84
|
+
```liquid
|
|
85
|
+
{% assign back = actions | where: "kind", "back" | first %}
|
|
86
|
+
{% if back %}
|
|
87
|
+
<a href="#" class="zl-card-nav__link" data-action="{{ back.name }}">{{ back.text_key | t }}</a>
|
|
88
|
+
{% endif %}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
If you list actions generically, exclude `a.kind == 'back'` from button
|
|
92
|
+
loops so the injected action doesn't render as a stray secondary button.
|
|
93
|
+
|
|
94
|
+
Start over anytime with `zitadel branding eject --design <name>` (designs:
|
|
95
|
+
`centered`, `split`, `split-right`, `hero`, `minimal`; add `--force` to
|
|
96
|
+
overwrite).
|
package/defaults/README-flows.md
CHANGED
|
@@ -37,8 +37,25 @@ Typical edits:
|
|
|
37
37
|
tokens).
|
|
38
38
|
- **Add or remove a step** — extend `steps[]`.
|
|
39
39
|
- **Rewire transitions** — edit `steps[].transitions` to point at a
|
|
40
|
-
different next step
|
|
41
|
-
|
|
40
|
+
different next step. Cross-flow jumps (`action: switch` / `pivot`)
|
|
41
|
+
validate in a definition but are **not yet executed by the runtime** —
|
|
42
|
+
submitting through one fails with a 400 (`code: "flow.unsupported"`);
|
|
43
|
+
keep transitions within one flow, using a purpose switch (next bullet)
|
|
44
|
+
for login ↔ register navigation.
|
|
45
|
+
- **Offer a purpose switch in the card** — pair a `kind: navigate`
|
|
46
|
+
action with a transition that declares a local `purpose`. The default
|
|
47
|
+
login flow ships this in both directions:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
"register": { "target": "register", "purpose": "register" }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Navigation skips field validation (an empty email box can't block the
|
|
54
|
+
link), and the declared purpose is what makes the landing leg run with
|
|
55
|
+
the right semantics — register creates the user instead of trying to
|
|
56
|
+
verify one. Rules: the purpose must be one this flow's `purposes`
|
|
57
|
+
serves, the target must be that purpose's entry step, and `purpose`
|
|
58
|
+
never combines with `action` (which targets another flow instead).
|
|
42
59
|
- **Add another flow** — drop a new JSON file with its own `purposes`
|
|
43
60
|
and a distinct `name` (e.g. a per-team login). See
|
|
44
61
|
[Multiple flows](#multiple-flows) for how it gets selected at runtime.
|
|
@@ -70,8 +87,8 @@ experiment can't silently take over `/login` — scope it or pin
|
|
|
70
87
|
|
|
71
88
|
## Presets
|
|
72
89
|
|
|
73
|
-
`zitadel setup` scaffolds this folder from a preset (
|
|
74
|
-
|
|
90
|
+
`zitadel setup` scaffolds this folder from a preset: `password-first` (the
|
|
91
|
+
default root files) or `--preset passkey-first`. The passkey-first flow
|
|
75
92
|
enters login on a fields-less passkey step with an email → password
|
|
76
93
|
fallback path. The preset only decides the starting point — edit
|
|
77
94
|
anything here afterwards.
|
|
@@ -28,8 +28,8 @@ Here's a simplified example:
|
|
|
28
28
|
"firstName"
|
|
29
29
|
],
|
|
30
30
|
"x-auth-methods": {
|
|
31
|
-
"password": { "enabled": true
|
|
32
|
-
"passkey": { "enabled": true
|
|
31
|
+
"password": { "enabled": true },
|
|
32
|
+
"passkey": { "enabled": true }
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
```
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<zl-page-shell data-zl-template-root>
|
|
2
|
+
{% if branding.logo_url %}
|
|
3
|
+
<img slot="header" class="zl-card-logo" src="{{ branding.logo_url }}" alt="" />
|
|
4
|
+
{% endif %}
|
|
5
|
+
<zl-card>
|
|
6
|
+
<h1 slot="header" class="zl-card-title">{{ step.texts.title_key | t: identity.display_name }}</h1>
|
|
7
|
+
{% if step.texts.description_key %}
|
|
8
|
+
{% assign card_subtitle = step.texts.description_key | t: identity.display_name %}
|
|
9
|
+
{% if card_subtitle != "" %}
|
|
10
|
+
<p slot="header" class="zl-card-subtitle">{{ card_subtitle }}</p>
|
|
11
|
+
{% endif %}
|
|
12
|
+
{% endif %}
|
|
13
|
+
|
|
14
|
+
{% if errors and errors.size > 0 %}
|
|
15
|
+
{% for err in errors %}
|
|
16
|
+
{% if err | formLevelError %}
|
|
17
|
+
{% assign alert_heading = err.text_key | alertHeading %}
|
|
18
|
+
{% assign alert_body = err.text_key | alertBody %}
|
|
19
|
+
<zl-alert severity="error" dismissible data-zl-step-error{% if alert_heading != "" %} heading="{{ alert_heading | t }}"{% endif %}>
|
|
20
|
+
{% if alert_body != "" %}{{ alert_body | t }}{% elsif err.text_key %}{{ err.text_key | t }}{% else %}{{ err.message }}{% endif %}
|
|
21
|
+
</zl-alert>
|
|
22
|
+
{% endif %}
|
|
23
|
+
{% endfor %}
|
|
24
|
+
{% endif %}
|
|
25
|
+
|
|
26
|
+
{% for f in fields %}
|
|
27
|
+
{% assign field_placeholder = f.text_key | fieldPlaceholder %}
|
|
28
|
+
{% assign field_help = f.text_key | fieldHelp %}
|
|
29
|
+
{% assign field_err = f.name | fieldError: errors %}
|
|
30
|
+
{% case f.type %}
|
|
31
|
+
{% when 'checkbox' %}
|
|
32
|
+
<zl-checkbox
|
|
33
|
+
name="{{ f.name }}"
|
|
34
|
+
data-testid="zitadel-field-{{ f.name | testid }}"
|
|
35
|
+
label="{{ f.text_key | t }}"
|
|
36
|
+
value="true"
|
|
37
|
+
{% if f.value %}checked{% endif %}
|
|
38
|
+
{% if field_err != "" %}invalid error="{{ field_err }}"{% endif %}
|
|
39
|
+
></zl-checkbox>
|
|
40
|
+
{% when 'select' %}
|
|
41
|
+
<zl-select
|
|
42
|
+
name="{{ f.name }}"
|
|
43
|
+
data-testid="zitadel-field-{{ f.name | testid }}"
|
|
44
|
+
label="{{ f.text_key | t }}"
|
|
45
|
+
value="{{ f.value }}"
|
|
46
|
+
options='{{ f.validation.enum | selectOptions | json }}'
|
|
47
|
+
{% if f.required %}required{% endif %}
|
|
48
|
+
{% if field_placeholder != "" %}placeholder="{{ field_placeholder }}"{% endif %}
|
|
49
|
+
{% if field_err != "" %}invalid error="{{ field_err }}"{% endif %}
|
|
50
|
+
></zl-select>
|
|
51
|
+
{% else %}
|
|
52
|
+
{% assign autocomplete_val = '' %}
|
|
53
|
+
{% if f.name == 'email' %}
|
|
54
|
+
{% assign autocomplete_val = 'email' %}
|
|
55
|
+
{% elsif f.type == 'password' and step.name contains 'register' %}
|
|
56
|
+
{% assign autocomplete_val = 'new-password' %}
|
|
57
|
+
{% elsif f.type == 'password' %}
|
|
58
|
+
{% assign autocomplete_val = 'current-password' %}
|
|
59
|
+
{% endif %}
|
|
60
|
+
<zl-field
|
|
61
|
+
name="{{ f.name }}"
|
|
62
|
+
data-testid="zitadel-field-{{ f.name | testid }}"
|
|
63
|
+
label="{{ f.text_key | t }}"
|
|
64
|
+
type="{{ f.type }}"
|
|
65
|
+
value="{{ f.value }}"
|
|
66
|
+
{% if autocomplete_val != '' %}autocomplete="{{ autocomplete_val }}"{% endif %}
|
|
67
|
+
{% if f.required %}required{% endif %}
|
|
68
|
+
{% if field_placeholder != "" %}placeholder="{{ field_placeholder }}"{% endif %}
|
|
69
|
+
{% if field_err != "" %}invalid error="{{ field_err }}"{% endif %}
|
|
70
|
+
>
|
|
71
|
+
{% if field_help != "" %}
|
|
72
|
+
<span slot="help">{{ field_help }}</span>
|
|
73
|
+
{% endif %}
|
|
74
|
+
</zl-field>
|
|
75
|
+
{% endcase %}
|
|
76
|
+
{% endfor %}
|
|
77
|
+
|
|
78
|
+
{% assign recover = actions | where: "name", "recover" | first %}
|
|
79
|
+
{% if recover %}
|
|
80
|
+
<p class="zl-card-forgot">
|
|
81
|
+
<a href="#" class="zl-card-forgot__link" data-action="recover">{{ recover.text_key | t }}</a>
|
|
82
|
+
</p>
|
|
83
|
+
{% endif %}
|
|
84
|
+
|
|
85
|
+
{% for a in actions %}
|
|
86
|
+
{% if a.primary %}
|
|
87
|
+
<zl-button
|
|
88
|
+
hierarchy="primary"
|
|
89
|
+
size="medium"
|
|
90
|
+
type="submit"
|
|
91
|
+
block
|
|
92
|
+
action="{{ a.name }}"
|
|
93
|
+
data-testid="zitadel-action-{{ a.name }}"
|
|
94
|
+
label="{{ a.text_key | t }}"
|
|
95
|
+
{% if loading %}loading{% endif %}
|
|
96
|
+
></zl-button>
|
|
97
|
+
{% endif %}
|
|
98
|
+
{% endfor %}
|
|
99
|
+
|
|
100
|
+
{% for a in actions %}
|
|
101
|
+
{% unless a.primary or a.kind == 'back' or a.name == 'passkey' or a.name == 'register' or a.name == 'sign_in' or a.name == 'recover' %}
|
|
102
|
+
<zl-button
|
|
103
|
+
hierarchy="secondary"
|
|
104
|
+
size="medium"
|
|
105
|
+
type="button"
|
|
106
|
+
block
|
|
107
|
+
action="{{ a.name }}"
|
|
108
|
+
data-testid="zitadel-action-{{ a.name }}"
|
|
109
|
+
label="{{ a.text_key | t }}"
|
|
110
|
+
></zl-button>
|
|
111
|
+
{% endunless %}
|
|
112
|
+
{% endfor %}
|
|
113
|
+
|
|
114
|
+
{% assign passkey = actions | where: "name", "passkey" | first %}
|
|
115
|
+
{% if passkey %}
|
|
116
|
+
{% unless passkey.primary %}
|
|
117
|
+
<zl-button
|
|
118
|
+
hierarchy="secondary"
|
|
119
|
+
size="medium"
|
|
120
|
+
block
|
|
121
|
+
action="passkey"
|
|
122
|
+
data-testid="zitadel-action-passkey"
|
|
123
|
+
label="{{ passkey.text_key | t }}"
|
|
124
|
+
></zl-button>
|
|
125
|
+
{% endunless %}
|
|
126
|
+
{% endif %}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
{% assign register = actions | where: "name", "register" | first %}
|
|
130
|
+
{% if register %}
|
|
131
|
+
<p class="zl-card-nav">
|
|
132
|
+
{{ 'identifier.action.register.lead' | t }}<a href="#" class="zl-card-nav__link" data-action="register" data-testid="zitadel-action-register-link">{{ 'identifier.action.register.link' | t }}</a>
|
|
133
|
+
</p>
|
|
134
|
+
{% endif %}
|
|
135
|
+
|
|
136
|
+
{% assign sign_in = actions | where: "name", "sign_in" | first %}
|
|
137
|
+
{% if sign_in %}
|
|
138
|
+
<p class="zl-card-nav">
|
|
139
|
+
{{ 'register.action.sign_in.lead' | t }}<a href="#" class="zl-card-nav__link" data-action="sign_in" data-testid="zitadel-action-sign_in-link">{{ 'register.action.sign_in.link' | t }}</a>
|
|
140
|
+
</p>
|
|
141
|
+
{% endif %}
|
|
142
|
+
|
|
143
|
+
{% if challenge %}
|
|
144
|
+
{% if challenge.method == "passkey" or challenge.method == "passkey_register" %}
|
|
145
|
+
{% assign ceremony = "authenticate" %}
|
|
146
|
+
{% assign submit_method = challenge.method %}
|
|
147
|
+
{% if challenge.method == "passkey_register" or challenge.options.user %}
|
|
148
|
+
{% assign ceremony = "register" %}
|
|
149
|
+
{% assign submit_method = "passkey_register" %}
|
|
150
|
+
{% endif %}
|
|
151
|
+
<zl-passkey
|
|
152
|
+
ceremony="{{ ceremony }}"
|
|
153
|
+
method="{{ submit_method }}"
|
|
154
|
+
challenge-id="{{ challenge.challenge_id }}"
|
|
155
|
+
options='{{ challenge.options | json }}'
|
|
156
|
+
pending-label="{{ 'passkey.pending.status' | t }}"
|
|
157
|
+
cancel-label="{{ 'action.cancel' | t }}"
|
|
158
|
+
></zl-passkey>
|
|
159
|
+
{% endif %}
|
|
160
|
+
{% endif %}
|
|
161
|
+
|
|
162
|
+
{% mandatory_gates %}
|
|
163
|
+
</zl-card>
|
|
164
|
+
</zl-page-shell>
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
<zl-page-shell data-zl-template-root>
|
|
2
|
+
<div class="zl-split">
|
|
3
|
+
<aside class="zl-split__brand">
|
|
4
|
+
{% comment %} ──────────────────────────────────────────────────────────
|
|
5
|
+
YOUR LANDING CONTENT — everything inside this aside is yours.
|
|
6
|
+
Edit the copy and structure freely: structural HTML (div, section,
|
|
7
|
+
nav, h1–h6, ul, li, a, img, …) plus inline style="" attributes are
|
|
8
|
+
allowed; button, input, form, script, and style tags are stripped —
|
|
9
|
+
use plain anchors for landing CTAs. The zl-hero__* classes keep
|
|
10
|
+
this pane on design tokens so palettes and theme mode apply.
|
|
11
|
+
|
|
12
|
+
The login card in the section below is fully wired to the flow
|
|
13
|
+
engine; leave its zl-* atoms intact. Layout knobs (set on the root
|
|
14
|
+
element's style attribute): --zl-split-columns, --zl-split-align,
|
|
15
|
+
--zl-split-brand-mobile. Add zl-split--right to the wrapper div to
|
|
16
|
+
mirror the panes. More in .zitadel/branding/README.md.
|
|
17
|
+
────────────────────────────────────────────────────────────── {% endcomment %}
|
|
18
|
+
<div class="zl-hero">
|
|
19
|
+
<nav class="zl-hero__nav">
|
|
20
|
+
<span class="zl-hero__brand">
|
|
21
|
+
{% if branding.logo_url %}
|
|
22
|
+
<img class="zl-hero__brand-logo" src="{{ branding.logo_url }}" alt="" data-zl-asset-fallback />
|
|
23
|
+
<span class="zl-hero__brand-mark" aria-hidden="true" hidden data-zl-asset-fallback-content></span>
|
|
24
|
+
{% else %}
|
|
25
|
+
<span class="zl-hero__brand-mark" aria-hidden="true"></span>
|
|
26
|
+
{% endif %}
|
|
27
|
+
Your brand
|
|
28
|
+
</span>
|
|
29
|
+
<span class="zl-hero__nav-links">
|
|
30
|
+
<a href="#">Product</a>
|
|
31
|
+
<a href="#">Pricing</a>
|
|
32
|
+
<a href="#">Docs</a>
|
|
33
|
+
</span>
|
|
34
|
+
</nav>
|
|
35
|
+
|
|
36
|
+
<p class="zl-hero__eyebrow">Your product category</p>
|
|
37
|
+
<h2 class="zl-hero__headline">A headline about what you ship.</h2>
|
|
38
|
+
<p class="zl-hero__sub">
|
|
39
|
+
Use this pane to tell visitors why they sign up: what your product
|
|
40
|
+
does, who it is for, and what happens after they sign in. Replace
|
|
41
|
+
every line here before you publish.
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
<ul class="zl-hero__bullets">
|
|
45
|
+
<li>Your first selling point</li>
|
|
46
|
+
<li>Your second selling point</li>
|
|
47
|
+
<li>Your third selling point</li>
|
|
48
|
+
</ul>
|
|
49
|
+
|
|
50
|
+
<p class="zl-hero__footnote">
|
|
51
|
+
Replace this line with real social proof, such as customer logos or
|
|
52
|
+
a certification you actually hold.
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
</aside>
|
|
56
|
+
<section class="zl-split__form">
|
|
57
|
+
{% if branding.logo_url %}
|
|
58
|
+
<img class="zl-split__compact" src="{{ branding.logo_url }}" alt="" data-zl-asset-fallback />
|
|
59
|
+
<p class="zl-split__compact zl-hero__compact-brand" hidden data-zl-asset-fallback-content>Your brand</p>
|
|
60
|
+
{% else %}
|
|
61
|
+
<p class="zl-split__compact zl-hero__compact-brand">Your brand</p>
|
|
62
|
+
{% endif %}
|
|
63
|
+
<zl-card>
|
|
64
|
+
<h1 slot="header" class="zl-card-title">{{ step.texts.title_key | t: identity.display_name }}</h1>
|
|
65
|
+
{% if step.texts.description_key %}
|
|
66
|
+
{% assign card_subtitle = step.texts.description_key | t: identity.display_name %}
|
|
67
|
+
{% if card_subtitle != "" %}
|
|
68
|
+
<p slot="header" class="zl-card-subtitle">{{ card_subtitle }}</p>
|
|
69
|
+
{% endif %}
|
|
70
|
+
{% endif %}
|
|
71
|
+
|
|
72
|
+
{% if errors and errors.size > 0 %}
|
|
73
|
+
{% for err in errors %}
|
|
74
|
+
{% if err | formLevelError %}
|
|
75
|
+
{% assign alert_heading = err.text_key | alertHeading %}
|
|
76
|
+
{% assign alert_body = err.text_key | alertBody %}
|
|
77
|
+
<zl-alert severity="error" dismissible data-zl-step-error{% if alert_heading != "" %} heading="{{ alert_heading | t }}"{% endif %}>
|
|
78
|
+
{% if alert_body != "" %}{{ alert_body | t }}{% elsif err.text_key %}{{ err.text_key | t }}{% else %}{{ err.message }}{% endif %}
|
|
79
|
+
</zl-alert>
|
|
80
|
+
{% endif %}
|
|
81
|
+
{% endfor %}
|
|
82
|
+
{% endif %}
|
|
83
|
+
|
|
84
|
+
{% for f in fields %}
|
|
85
|
+
{% assign field_placeholder = f.text_key | fieldPlaceholder %}
|
|
86
|
+
{% assign field_help = f.text_key | fieldHelp %}
|
|
87
|
+
{% assign field_err = f.name | fieldError: errors %}
|
|
88
|
+
{% case f.type %}
|
|
89
|
+
{% when 'checkbox' %}
|
|
90
|
+
<zl-checkbox
|
|
91
|
+
name="{{ f.name }}"
|
|
92
|
+
data-testid="zitadel-field-{{ f.name | testid }}"
|
|
93
|
+
label="{{ f.text_key | t }}"
|
|
94
|
+
value="true"
|
|
95
|
+
{% if f.value %}checked{% endif %}
|
|
96
|
+
{% if field_err != "" %}invalid error="{{ field_err }}"{% endif %}
|
|
97
|
+
></zl-checkbox>
|
|
98
|
+
{% when 'select' %}
|
|
99
|
+
<zl-select
|
|
100
|
+
name="{{ f.name }}"
|
|
101
|
+
data-testid="zitadel-field-{{ f.name | testid }}"
|
|
102
|
+
label="{{ f.text_key | t }}"
|
|
103
|
+
value="{{ f.value }}"
|
|
104
|
+
options='{{ f.validation.enum | selectOptions | json }}'
|
|
105
|
+
{% if f.required %}required{% endif %}
|
|
106
|
+
{% if field_placeholder != "" %}placeholder="{{ field_placeholder }}"{% endif %}
|
|
107
|
+
{% if field_err != "" %}invalid error="{{ field_err }}"{% endif %}
|
|
108
|
+
></zl-select>
|
|
109
|
+
{% else %}
|
|
110
|
+
{% assign autocomplete_val = '' %}
|
|
111
|
+
{% if f.name == 'email' %}
|
|
112
|
+
{% assign autocomplete_val = 'email' %}
|
|
113
|
+
{% elsif f.type == 'password' and step.name contains 'register' %}
|
|
114
|
+
{% assign autocomplete_val = 'new-password' %}
|
|
115
|
+
{% elsif f.type == 'password' %}
|
|
116
|
+
{% assign autocomplete_val = 'current-password' %}
|
|
117
|
+
{% endif %}
|
|
118
|
+
<zl-field
|
|
119
|
+
name="{{ f.name }}"
|
|
120
|
+
data-testid="zitadel-field-{{ f.name | testid }}"
|
|
121
|
+
label="{{ f.text_key | t }}"
|
|
122
|
+
type="{{ f.type }}"
|
|
123
|
+
value="{{ f.value }}"
|
|
124
|
+
{% if autocomplete_val != '' %}autocomplete="{{ autocomplete_val }}"{% endif %}
|
|
125
|
+
{% if f.required %}required{% endif %}
|
|
126
|
+
{% if field_placeholder != "" %}placeholder="{{ field_placeholder }}"{% endif %}
|
|
127
|
+
{% if field_err != "" %}invalid error="{{ field_err }}"{% endif %}
|
|
128
|
+
>
|
|
129
|
+
{% if field_help != "" %}
|
|
130
|
+
<span slot="help">{{ field_help }}</span>
|
|
131
|
+
{% endif %}
|
|
132
|
+
</zl-field>
|
|
133
|
+
{% endcase %}
|
|
134
|
+
{% endfor %}
|
|
135
|
+
|
|
136
|
+
{% assign recover = actions | where: "name", "recover" | first %}
|
|
137
|
+
{% if recover %}
|
|
138
|
+
<p class="zl-card-forgot">
|
|
139
|
+
<a href="#" class="zl-card-forgot__link" data-action="recover">{{ recover.text_key | t }}</a>
|
|
140
|
+
</p>
|
|
141
|
+
{% endif %}
|
|
142
|
+
|
|
143
|
+
{% for a in actions %}
|
|
144
|
+
{% if a.primary %}
|
|
145
|
+
<zl-button
|
|
146
|
+
hierarchy="primary"
|
|
147
|
+
size="medium"
|
|
148
|
+
type="submit"
|
|
149
|
+
block
|
|
150
|
+
action="{{ a.name }}"
|
|
151
|
+
data-testid="zitadel-action-{{ a.name }}"
|
|
152
|
+
label="{{ a.text_key | t }}"
|
|
153
|
+
{% if loading %}loading{% endif %}
|
|
154
|
+
></zl-button>
|
|
155
|
+
{% endif %}
|
|
156
|
+
{% endfor %}
|
|
157
|
+
|
|
158
|
+
{% for a in actions %}
|
|
159
|
+
{% unless a.primary or a.kind == 'back' or a.name == 'passkey' or a.name == 'register' or a.name == 'sign_in' or a.name == 'recover' %}
|
|
160
|
+
<zl-button
|
|
161
|
+
hierarchy="secondary"
|
|
162
|
+
size="medium"
|
|
163
|
+
type="button"
|
|
164
|
+
block
|
|
165
|
+
action="{{ a.name }}"
|
|
166
|
+
data-testid="zitadel-action-{{ a.name }}"
|
|
167
|
+
label="{{ a.text_key | t }}"
|
|
168
|
+
></zl-button>
|
|
169
|
+
{% endunless %}
|
|
170
|
+
{% endfor %}
|
|
171
|
+
|
|
172
|
+
{% assign passkey = actions | where: "name", "passkey" | first %}
|
|
173
|
+
{% if passkey %}
|
|
174
|
+
{% unless passkey.primary %}
|
|
175
|
+
<zl-button
|
|
176
|
+
hierarchy="secondary"
|
|
177
|
+
size="medium"
|
|
178
|
+
block
|
|
179
|
+
action="passkey"
|
|
180
|
+
data-testid="zitadel-action-passkey"
|
|
181
|
+
label="{{ passkey.text_key | t }}"
|
|
182
|
+
></zl-button>
|
|
183
|
+
{% endunless %}
|
|
184
|
+
{% endif %}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
{% assign register = actions | where: "name", "register" | first %}
|
|
188
|
+
{% if register %}
|
|
189
|
+
<p class="zl-card-nav">
|
|
190
|
+
{{ 'identifier.action.register.lead' | t }}<a href="#" class="zl-card-nav__link" data-action="register" data-testid="zitadel-action-register-link">{{ 'identifier.action.register.link' | t }}</a>
|
|
191
|
+
</p>
|
|
192
|
+
{% endif %}
|
|
193
|
+
|
|
194
|
+
{% assign sign_in = actions | where: "name", "sign_in" | first %}
|
|
195
|
+
{% if sign_in %}
|
|
196
|
+
<p class="zl-card-nav">
|
|
197
|
+
{{ 'register.action.sign_in.lead' | t }}<a href="#" class="zl-card-nav__link" data-action="sign_in" data-testid="zitadel-action-sign_in-link">{{ 'register.action.sign_in.link' | t }}</a>
|
|
198
|
+
</p>
|
|
199
|
+
{% endif %}
|
|
200
|
+
|
|
201
|
+
{% if challenge %}
|
|
202
|
+
{% if challenge.method == "passkey" or challenge.method == "passkey_register" %}
|
|
203
|
+
{% assign ceremony = "authenticate" %}
|
|
204
|
+
{% assign submit_method = challenge.method %}
|
|
205
|
+
{% if challenge.method == "passkey_register" or challenge.options.user %}
|
|
206
|
+
{% assign ceremony = "register" %}
|
|
207
|
+
{% assign submit_method = "passkey_register" %}
|
|
208
|
+
{% endif %}
|
|
209
|
+
<zl-passkey
|
|
210
|
+
ceremony="{{ ceremony }}"
|
|
211
|
+
method="{{ submit_method }}"
|
|
212
|
+
challenge-id="{{ challenge.challenge_id }}"
|
|
213
|
+
options='{{ challenge.options | json }}'
|
|
214
|
+
pending-label="{{ 'passkey.pending.status' | t }}"
|
|
215
|
+
cancel-label="{{ 'action.cancel' | t }}"
|
|
216
|
+
></zl-passkey>
|
|
217
|
+
{% endif %}
|
|
218
|
+
{% endif %}
|
|
219
|
+
|
|
220
|
+
{% mandatory_gates %}
|
|
221
|
+
</zl-card>
|
|
222
|
+
</section>
|
|
223
|
+
</div>
|
|
224
|
+
</zl-page-shell>
|