@multiplatform.one/keycloak-theme 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +94 -0
- package/package.json +65 -0
- package/src/SpringboardStyles.tsx +84 -0
- package/src/css.ts +655 -0
- package/src/glyph.ts +109 -0
- package/src/index.ts +47 -0
- package/src/themeName.ts +69 -0
- package/src/tokens.ts +260 -0
- package/types/SpringboardStyles.d.ts +49 -0
- package/types/SpringboardStyles.d.ts.map +1 -0
- package/types/css.d.ts +72 -0
- package/types/css.d.ts.map +1 -0
- package/types/glyph.d.ts +73 -0
- package/types/glyph.d.ts.map +1 -0
- package/types/index.d.ts +22 -0
- package/types/index.d.ts.map +1 -0
- package/types/themeName.d.ts +54 -0
- package/types/themeName.d.ts.map +1 -0
- package/types/tokens.d.ts +245 -0
- package/types/tokens.d.ts.map +1 -0
package/src/css.ts
ADDED
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE SPRINGBOARD SKIN FOR KEYCLOAK'S LOGIN PAGES, AS ONE STYLESHEET.
|
|
3
|
+
*
|
|
4
|
+
* ★ A STYLESHEET AND NOT A COMPONENT TREE, ON PURPOSE. Keycloak's login
|
|
5
|
+
* flow is ~38 FreeMarker pages. Keycloakify renders every one of them from
|
|
6
|
+
* its own React components, and the ones nobody ever looks at — WebAuthn
|
|
7
|
+
* errors, recovery-code confirmation, SAML post-forms, X.509 info — are
|
|
8
|
+
* exactly the ones that must not break, because a user only ever reaches
|
|
9
|
+
* them on the worst day of their week. Restyling a page means forking it,
|
|
10
|
+
* a fork means owning its form markup forever, and a form's markup is its
|
|
11
|
+
* flow: the `action`, the `name` on every input, the hidden
|
|
12
|
+
* `credentialId`, the `tabIndex` order. Every page this sheet themes is a
|
|
13
|
+
* page whose flow it provably cannot touch, because it never renders one.
|
|
14
|
+
*
|
|
15
|
+
* ★ WHAT IT TARGETS. keycloakify's pages emit PatternFly class names
|
|
16
|
+
* (`.pf-c-form-control`, `.pf-c-button`, `.card-pf`) and a stable set of
|
|
17
|
+
* ids (`#kc-form-login`, `#kc-page-title`, `#kc-social-providers`). Those
|
|
18
|
+
* are the theme's API surface, and they are what this sheet hooks. The
|
|
19
|
+
* pages are left entirely alone.
|
|
20
|
+
*
|
|
21
|
+
* ★ HOW IT COMPOSES WITH THE HOUSE SHEET. `apps/keycloak` already ships
|
|
22
|
+
* `KcHouseStyles`, which resolves structure — radius, control height, the
|
|
23
|
+
* 44px press-target floor, the focus ring — from the SAME tamagui knobs
|
|
24
|
+
* the catalog components use, and paints colour from the theme ramps. This
|
|
25
|
+
* sheet keeps all of that structure and replaces only the SURFACE. It does
|
|
26
|
+
* so twice over so it works either way:
|
|
27
|
+
*
|
|
28
|
+
* 1. it redefines the `--kc-*` custom properties `KcHouseStyles` invents,
|
|
29
|
+
* so every rule that sheet already wrote follows the springboard
|
|
30
|
+
* without either file knowing about the other, and
|
|
31
|
+
* 2. it states the springboard surface directly against the same
|
|
32
|
+
* PatternFly selectors, so the skin is complete on its own when the
|
|
33
|
+
* house sheet is absent.
|
|
34
|
+
*
|
|
35
|
+
* Its selectors carry one extra `html.login-pf` over the house sheet's,
|
|
36
|
+
* which puts every rule here at a higher specificity than its counterpart
|
|
37
|
+
* there. That is deliberate: it makes the layering independent of which
|
|
38
|
+
* <style> element React happens to mount first.
|
|
39
|
+
*
|
|
40
|
+
* ★ ONE SURFACE IN BOTH COLOUR SCHEMES. Every var block below is emitted
|
|
41
|
+
* for `html.login-pf` AND `html.login-pf.t_dark`, and `prefers-color-scheme`
|
|
42
|
+
* is never consulted. shc's `wallpaper.ts` carries the ruling and the
|
|
43
|
+
* defect that produced it — see ./tokens.ts `wallpaperColors`.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import type { GlyphMark } from "./glyph";
|
|
47
|
+
import {
|
|
48
|
+
accent,
|
|
49
|
+
caption,
|
|
50
|
+
field,
|
|
51
|
+
fontFamily as defaultFontFamily,
|
|
52
|
+
icon,
|
|
53
|
+
ink,
|
|
54
|
+
plate,
|
|
55
|
+
status,
|
|
56
|
+
trigger,
|
|
57
|
+
wallpaperColors,
|
|
58
|
+
wallpaperLayers,
|
|
59
|
+
} from "./tokens";
|
|
60
|
+
|
|
61
|
+
export interface SpringboardCssOptions {
|
|
62
|
+
/**
|
|
63
|
+
* The glyph plate to draw above the page title. Omit it and no plate is
|
|
64
|
+
* emitted at all — the rule is absent rather than empty, so a caller
|
|
65
|
+
* without a realm name gets stock spacing instead of a blank square.
|
|
66
|
+
*/
|
|
67
|
+
mark?: GlyphMark;
|
|
68
|
+
/** Body face. Defaults to the no-webfont stack in ./tokens.ts. */
|
|
69
|
+
fontFamily?: string;
|
|
70
|
+
/**
|
|
71
|
+
* The keyboard focus ring. Defaults match `FOCUS_VISIBLE_RING` in
|
|
72
|
+
* `@multiplatform.one/theme`; they are options rather than an import so
|
|
73
|
+
* this package stays dependency-free and testable without tamagui.
|
|
74
|
+
*/
|
|
75
|
+
focusRingWidthPx?: number;
|
|
76
|
+
focusRingOffsetPx?: number;
|
|
77
|
+
/**
|
|
78
|
+
* The press-target floor (`MIN_PRESS_TARGET` in `@multiplatform.one/theme`).
|
|
79
|
+
* DG-A11Y-01; restated here so the skin cannot shrink a control below it.
|
|
80
|
+
*/
|
|
81
|
+
minPressTargetPx?: number;
|
|
82
|
+
/** Card width. The springboard has no card, so this is a login-page choice. */
|
|
83
|
+
cardMaxWidthPx?: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** CSS string literal for `content:` — escapes the quote and the backslash. */
|
|
87
|
+
function cssString(value: string): string {
|
|
88
|
+
return JSON.stringify(value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The `--kc-*` contract `KcHouseStyles` publishes, restated in springboard
|
|
93
|
+
* values. Redefining these is what carries the skin into every rule that
|
|
94
|
+
* sheet already wrote (inputs, buttons, checkbox, locale menu, alerts,
|
|
95
|
+
* info band) without this file having to repeat them.
|
|
96
|
+
*/
|
|
97
|
+
function customProperties(): string {
|
|
98
|
+
return [
|
|
99
|
+
// Surfaces.
|
|
100
|
+
`--kc-page-bg: ${wallpaperColors.base};`,
|
|
101
|
+
`--kc-card-bg: ${plate.fill};`,
|
|
102
|
+
`--kc-input-bg: ${field.background};`,
|
|
103
|
+
// Ink.
|
|
104
|
+
`--kc-text: ${ink.primary};`,
|
|
105
|
+
`--kc-muted: ${ink.muted};`,
|
|
106
|
+
`--kc-placeholder: ${ink.faint};`,
|
|
107
|
+
// Hairlines.
|
|
108
|
+
`--kc-border: ${field.border};`,
|
|
109
|
+
`--kc-border-hover: ${field.borderHover};`,
|
|
110
|
+
`--kc-border-focus: ${field.borderFocus};`,
|
|
111
|
+
`--kc-border-strong: ${field.borderHover};`,
|
|
112
|
+
`--kc-divider: ${field.divider};`,
|
|
113
|
+
// Interaction.
|
|
114
|
+
`--kc-hover-bg: ${field.ghost};`,
|
|
115
|
+
`--kc-press-bg: ${field.ghostHover};`,
|
|
116
|
+
`--kc-ring: ${accent.sky};`,
|
|
117
|
+
`--kc-link: ${accent.sky};`,
|
|
118
|
+
// Call to action.
|
|
119
|
+
`--kc-cta-bg: ${accent.indigo};`,
|
|
120
|
+
`--kc-cta-fg: ${ink.primary};`,
|
|
121
|
+
`--kc-cta-hover: ${accent.indigoHover};`,
|
|
122
|
+
`--kc-cta-press: ${accent.indigoPress};`,
|
|
123
|
+
// Disabled — washed chrome, lowest legible ink.
|
|
124
|
+
`--kc-disabled-bg: ${field.ghost};`,
|
|
125
|
+
`--kc-disabled-border: ${field.border};`,
|
|
126
|
+
`--kc-disabled-text: ${ink.faint};`,
|
|
127
|
+
// Feedback.
|
|
128
|
+
`--kc-error-text: ${status.error};`,
|
|
129
|
+
`--kc-error-border: ${status.error};`,
|
|
130
|
+
`--kc-warning-border: ${status.warning};`,
|
|
131
|
+
`--kc-success-border: ${status.success};`,
|
|
132
|
+
`--kc-info-border: ${status.info};`,
|
|
133
|
+
`--kc-menu-shadow: rgba(0,0,0,0.45);`,
|
|
134
|
+
// Springboard's own, consumed only by this sheet.
|
|
135
|
+
`--sb-plate-fill: ${plate.fill};`,
|
|
136
|
+
`--sb-plate-highlight: ${plate.highlight};`,
|
|
137
|
+
`--sb-plate-radius: ${plate.radius}px;`,
|
|
138
|
+
`--sb-icon-size: ${icon.size}px;`,
|
|
139
|
+
`--sb-icon-radius: ${icon.radius}px;`,
|
|
140
|
+
`--sb-caption-size: ${caption.fontSize}px;`,
|
|
141
|
+
`--sb-caption-line: ${caption.lineHeight}px;`,
|
|
142
|
+
].join("\n ");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The white-on-wallpaper halo every label outside the plate wears. */
|
|
146
|
+
const onWallpaperShadow = `0 ${caption.shadowOffsetY}px ${caption.shadowBlur}px ${wallpaperColors.textShadow}`;
|
|
147
|
+
|
|
148
|
+
/** The plate's elevation — `trigger`'s, not `icon`'s. See ./tokens.ts. */
|
|
149
|
+
const plateShadow = `0 ${trigger.shadowOffsetY}px ${trigger.shadowBlur}px rgba(0,0,0,0.45)`;
|
|
150
|
+
|
|
151
|
+
/** The squircle's own elevation — `icon`'s. */
|
|
152
|
+
const iconShadow = `0 ${icon.shadowOffsetY}px ${icon.shadowBlur}px ${icon.shadowColor}`;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The glyph plate above the page title, drawn as a `::before` on the title
|
|
156
|
+
* itself.
|
|
157
|
+
*
|
|
158
|
+
* ★ A PSEUDO-ELEMENT RATHER THAN MARKUP, and this is the decision that
|
|
159
|
+
* keeps the whole skin out of the pages. A React mark would have to be
|
|
160
|
+
* placed inside the card above the title, which means owning `Template` —
|
|
161
|
+
* and `Template` is where the locale switcher, the alert rail, the
|
|
162
|
+
* `socialProvidersNode` slot and the try-another-way form live. Forking it
|
|
163
|
+
* to insert one square is the same trade as forking a page: a permanent
|
|
164
|
+
* maintenance debt against upstream, taken on for decoration. A generated
|
|
165
|
+
* `content:` costs one rule, is implicitly `aria-hidden` (pseudo-elements
|
|
166
|
+
* are not in the accessibility tree), and the realm's name is already read
|
|
167
|
+
* out loud by the `<h1>` it hangs from.
|
|
168
|
+
*
|
|
169
|
+
* ★ IT ALSO COVERS `#kc-username`. keycloakify's Template swaps the `<h1>`
|
|
170
|
+
* for a `#kc-username` block whenever `auth.showUsername` is set and the
|
|
171
|
+
* flow is not a credential reset — the second step of a split
|
|
172
|
+
* username/password flow, and a page users very much do see. Both get the
|
|
173
|
+
* plate so the mark does not vanish between step one and step two.
|
|
174
|
+
*/
|
|
175
|
+
function markRules(scope: string, mark: GlyphMark): string {
|
|
176
|
+
const monogramSize = Math.round(icon.size * 0.34);
|
|
177
|
+
const monogramLine = Math.round(icon.size * 0.4);
|
|
178
|
+
return `
|
|
179
|
+
/* ── the estate's glyph: the springboard's own squircle ────── */
|
|
180
|
+
${scope} .login-pf-page #kc-page-title,
|
|
181
|
+
${scope} .login-pf-page #kc-username {
|
|
182
|
+
display: flex;
|
|
183
|
+
flex-direction: column;
|
|
184
|
+
align-items: center;
|
|
185
|
+
gap: ${caption.marginTop}px;
|
|
186
|
+
}
|
|
187
|
+
${scope} .login-pf-page #kc-page-title::before,
|
|
188
|
+
${scope} .login-pf-page #kc-username::before {
|
|
189
|
+
content: ${cssString(mark.monogram)};
|
|
190
|
+
box-sizing: border-box;
|
|
191
|
+
width: var(--sb-icon-size);
|
|
192
|
+
height: var(--sb-icon-size);
|
|
193
|
+
border-radius: var(--sb-icon-radius);
|
|
194
|
+
background: ${mark.background};
|
|
195
|
+
color: ${mark.foreground};
|
|
196
|
+
box-shadow: ${iconShadow};
|
|
197
|
+
display: flex;
|
|
198
|
+
align-items: center;
|
|
199
|
+
justify-content: center;
|
|
200
|
+
font-size: ${monogramSize}px;
|
|
201
|
+
line-height: ${monogramLine}px;
|
|
202
|
+
font-weight: 800;
|
|
203
|
+
letter-spacing: 0.5px;
|
|
204
|
+
user-select: none;
|
|
205
|
+
}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Build the springboard stylesheet. Pure — same options, same string. */
|
|
209
|
+
export function buildSpringboardCss(options: SpringboardCssOptions = {}): string {
|
|
210
|
+
const {
|
|
211
|
+
mark,
|
|
212
|
+
fontFamily = defaultFontFamily,
|
|
213
|
+
focusRingWidthPx = 2,
|
|
214
|
+
focusRingOffsetPx = 2,
|
|
215
|
+
minPressTargetPx = 44,
|
|
216
|
+
cardMaxWidthPx = 420,
|
|
217
|
+
} = options;
|
|
218
|
+
|
|
219
|
+
const scope = "html.login-pf";
|
|
220
|
+
const vars = customProperties();
|
|
221
|
+
// The card's own padding, named so the #kc-info band below can bleed back
|
|
222
|
+
// out to the card edge by exactly the amount the card came in by. The
|
|
223
|
+
// stock sheet hardcodes 40/30 there and gets a band that stops short of
|
|
224
|
+
// the corners under any other padding.
|
|
225
|
+
const cardPadY = plate.paddingVertical + 12;
|
|
226
|
+
const cardPadX = plate.paddingHorizontal + 8;
|
|
227
|
+
|
|
228
|
+
return `
|
|
229
|
+
/* ── springboard tokens, both colour schemes ───────────────── */
|
|
230
|
+
/* One dark surface in light and dark alike — see ./tokens.ts. */
|
|
231
|
+
${scope},
|
|
232
|
+
${scope}.t_dark {
|
|
233
|
+
${vars}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* ── the wallpaper ─────────────────────────────────────────── */
|
|
237
|
+
${scope} {
|
|
238
|
+
background-color: ${wallpaperColors.base};
|
|
239
|
+
background-image: ${wallpaperLayers};
|
|
240
|
+
/* Fixed so the glows stay anchored to the viewport instead of tiling
|
|
241
|
+
down a page that grew a scrollbar (a long consent screen, a locale
|
|
242
|
+
list). The springboard's wallpaper is a layer behind everything, not
|
|
243
|
+
a background that scrolls with content. */
|
|
244
|
+
background-attachment: fixed;
|
|
245
|
+
background-repeat: no-repeat;
|
|
246
|
+
background-size: cover;
|
|
247
|
+
min-height: 100%;
|
|
248
|
+
}
|
|
249
|
+
${scope} body {
|
|
250
|
+
background: transparent none;
|
|
251
|
+
color: ${ink.primary};
|
|
252
|
+
font-family: ${fontFamily};
|
|
253
|
+
-webkit-font-smoothing: antialiased;
|
|
254
|
+
}
|
|
255
|
+
${scope} .login-pf-page {
|
|
256
|
+
background: transparent;
|
|
257
|
+
padding-bottom: 40px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* ── the realm title, sitting straight on the wallpaper ────── */
|
|
261
|
+
/* Caption anatomy from the launcher: white with a halo, because this
|
|
262
|
+
text has no plate under it and may land on either glow. */
|
|
263
|
+
${scope} .login-pf-page #kc-header-wrapper {
|
|
264
|
+
color: ${wallpaperColors.text};
|
|
265
|
+
text-shadow: ${onWallpaperShadow};
|
|
266
|
+
font-weight: 700;
|
|
267
|
+
font-size: 22px;
|
|
268
|
+
line-height: 1.3;
|
|
269
|
+
letter-spacing: normal;
|
|
270
|
+
text-transform: none;
|
|
271
|
+
padding: 44px 16px 18px;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* ── the card: the dock's glass shelf, grown to hold a form ── */
|
|
275
|
+
${scope} .login-pf-page .card-pf {
|
|
276
|
+
max-width: ${cardMaxWidthPx}px;
|
|
277
|
+
margin: 0 auto;
|
|
278
|
+
background: var(--sb-plate-fill);
|
|
279
|
+
border: 1px solid var(--sb-plate-highlight);
|
|
280
|
+
border-radius: var(--sb-plate-radius);
|
|
281
|
+
/* The shelf's top-edge inner highlight ('inset 0 1px 0 #fff4' in the
|
|
282
|
+
launcher payload) plus the trigger's elevation under the slab. */
|
|
283
|
+
box-shadow:
|
|
284
|
+
inset 0 1px 0 ${plate.highlight},
|
|
285
|
+
${plateShadow};
|
|
286
|
+
/* "blur-ish" is the launcher's own word for the dock material. Where
|
|
287
|
+
backdrop-filter is unsupported the fill alone still reads as a slab,
|
|
288
|
+
so this degrades rather than breaks. */
|
|
289
|
+
backdrop-filter: blur(22px) saturate(140%);
|
|
290
|
+
-webkit-backdrop-filter: blur(22px) saturate(140%);
|
|
291
|
+
padding: ${cardPadY}px ${cardPadX}px;
|
|
292
|
+
overflow: hidden;
|
|
293
|
+
}
|
|
294
|
+
${scope} .login-pf-page .login-pf-header h1,
|
|
295
|
+
${scope} .login-pf-page #kc-page-title {
|
|
296
|
+
color: ${ink.primary};
|
|
297
|
+
font-weight: 700;
|
|
298
|
+
font-size: 20px;
|
|
299
|
+
line-height: 1.3;
|
|
300
|
+
margin: 0 0 ${plate.gap}px;
|
|
301
|
+
}
|
|
302
|
+
${mark ? markRules(scope, mark) : ""}
|
|
303
|
+
|
|
304
|
+
/* ── labels and prose ──────────────────────────────────────── */
|
|
305
|
+
${scope} .login-pf-page label,
|
|
306
|
+
${scope} .login-pf-page .pf-c-form__label,
|
|
307
|
+
${scope} .login-pf-page .pf-c-form__label-text {
|
|
308
|
+
color: ${ink.secondary};
|
|
309
|
+
font-weight: 500;
|
|
310
|
+
font-size: 14px;
|
|
311
|
+
}
|
|
312
|
+
${scope} .login-pf-page .instruction,
|
|
313
|
+
${scope} .login-pf-page .subtitle,
|
|
314
|
+
${scope} .login-pf-page .pf-c-form__helper-text,
|
|
315
|
+
${scope} .login-pf-page #kc-info,
|
|
316
|
+
${scope} .login-pf-page .login-pf-signup {
|
|
317
|
+
color: ${ink.muted};
|
|
318
|
+
}
|
|
319
|
+
${scope} .login-pf-page #kc-attempted-username {
|
|
320
|
+
color: ${ink.primary};
|
|
321
|
+
font-weight: 600;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/* ── fields: inset into the slab, not laid on top of it ────── */
|
|
325
|
+
${scope} .login-pf-page .pf-c-form-control,
|
|
326
|
+
${scope} .login-pf-page input[type="text"],
|
|
327
|
+
${scope} .login-pf-page input[type="password"],
|
|
328
|
+
${scope} .login-pf-page input[type="email"],
|
|
329
|
+
${scope} .login-pf-page input[type="tel"],
|
|
330
|
+
${scope} .login-pf-page input[type="number"],
|
|
331
|
+
${scope} .login-pf-page select,
|
|
332
|
+
${scope} .login-pf-page textarea {
|
|
333
|
+
min-height: ${minPressTargetPx}px;
|
|
334
|
+
background-color: ${field.background};
|
|
335
|
+
color: ${ink.primary};
|
|
336
|
+
border: 1px solid ${field.border};
|
|
337
|
+
box-shadow: none;
|
|
338
|
+
}
|
|
339
|
+
${scope} .login-pf-page .pf-c-form-control:hover,
|
|
340
|
+
${scope} .login-pf-page select:hover,
|
|
341
|
+
${scope} .login-pf-page textarea:hover {
|
|
342
|
+
border-color: ${field.borderHover};
|
|
343
|
+
}
|
|
344
|
+
${scope} .login-pf-page .pf-c-form-control:focus,
|
|
345
|
+
${scope} .login-pf-page select:focus,
|
|
346
|
+
${scope} .login-pf-page textarea:focus {
|
|
347
|
+
border-color: ${field.borderFocus};
|
|
348
|
+
background-color: ${field.background};
|
|
349
|
+
}
|
|
350
|
+
${scope} .login-pf-page .pf-c-form-control::placeholder,
|
|
351
|
+
${scope} .login-pf-page input::placeholder {
|
|
352
|
+
color: ${ink.faint};
|
|
353
|
+
}
|
|
354
|
+
${scope} .login-pf-page .pf-c-form-control[aria-invalid="true"] {
|
|
355
|
+
border-color: ${status.error};
|
|
356
|
+
}
|
|
357
|
+
/* ★ THE INPUT GROUP IS OPAQUE WHITE IN PATTERNFLY, and that one fact
|
|
358
|
+
defeats the whole field treatment above. '.pf-c-input-group' wraps the
|
|
359
|
+
password field and its visibility toggle; both of those are painted in
|
|
360
|
+
TRANSLUCENT springboard colours, so over a white parent the field
|
|
361
|
+
composites to mid-grey and the toggle to near-white — which is exactly
|
|
362
|
+
how it rendered before this rule existed. Clearing the parent is what
|
|
363
|
+
makes the two translucent children land on the slab instead. */
|
|
364
|
+
${scope} .login-pf-page .pf-c-input-group {
|
|
365
|
+
background: transparent;
|
|
366
|
+
display: flex;
|
|
367
|
+
align-items: stretch;
|
|
368
|
+
gap: 8px;
|
|
369
|
+
}
|
|
370
|
+
${scope} .login-pf-page .pf-c-input-group .pf-c-form-control {
|
|
371
|
+
flex: 1 1 auto;
|
|
372
|
+
}
|
|
373
|
+
${scope} .login-pf-page .pf-c-input-group .pf-c-button.pf-m-control {
|
|
374
|
+
min-width: ${minPressTargetPx}px;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* Chrome paints autofilled inputs a fixed pale yellow that no colour
|
|
378
|
+
property overrides — the inset shadow is the documented way to repaint
|
|
379
|
+
the field, and without it a remembered username lands as a white bar in
|
|
380
|
+
the middle of the slab. */
|
|
381
|
+
${scope} .login-pf-page input:-webkit-autofill,
|
|
382
|
+
${scope} .login-pf-page input:-webkit-autofill:hover,
|
|
383
|
+
${scope} .login-pf-page input:-webkit-autofill:focus {
|
|
384
|
+
-webkit-box-shadow: 0 0 0 1000px ${field.background} inset;
|
|
385
|
+
-webkit-text-fill-color: ${ink.primary};
|
|
386
|
+
caret-color: ${ink.primary};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* ── buttons ───────────────────────────────────────────────── */
|
|
390
|
+
${scope} .login-pf-page .pf-c-button {
|
|
391
|
+
min-height: ${minPressTargetPx}px;
|
|
392
|
+
font-weight: 600;
|
|
393
|
+
}
|
|
394
|
+
${scope} .login-pf-page .pf-c-button.pf-m-primary,
|
|
395
|
+
${scope} .login-pf-page input.pf-m-primary[type="submit"],
|
|
396
|
+
${scope} .login-pf-page input.pf-m-primary[type="button"] {
|
|
397
|
+
background-color: ${accent.indigo};
|
|
398
|
+
border-color: ${accent.indigo};
|
|
399
|
+
color: ${ink.primary};
|
|
400
|
+
box-shadow: ${iconShadow};
|
|
401
|
+
}
|
|
402
|
+
${scope} .login-pf-page .pf-c-button.pf-m-primary:hover,
|
|
403
|
+
${scope} .login-pf-page input.pf-m-primary[type="submit"]:hover {
|
|
404
|
+
background-color: ${accent.indigoHover};
|
|
405
|
+
border-color: ${accent.indigoHover};
|
|
406
|
+
}
|
|
407
|
+
${scope} .login-pf-page .pf-c-button.pf-m-primary:active,
|
|
408
|
+
${scope} .login-pf-page input.pf-m-primary[type="submit"]:active {
|
|
409
|
+
background-color: ${accent.indigoPress};
|
|
410
|
+
border-color: ${accent.indigoPress};
|
|
411
|
+
}
|
|
412
|
+
${scope} .login-pf-page .pf-c-button.pf-m-primary:disabled,
|
|
413
|
+
${scope} .login-pf-page input.pf-m-primary[type="submit"]:disabled {
|
|
414
|
+
background-color: ${field.ghost};
|
|
415
|
+
border-color: ${field.border};
|
|
416
|
+
color: ${ink.faint};
|
|
417
|
+
box-shadow: none;
|
|
418
|
+
}
|
|
419
|
+
${scope} .login-pf-page .pf-c-button.pf-m-control,
|
|
420
|
+
${scope} .login-pf-page .pf-c-button.pf-m-secondary,
|
|
421
|
+
${scope} .login-pf-page .pf-c-button.btn-default:not(.pf-m-primary) {
|
|
422
|
+
background-color: ${field.ghost};
|
|
423
|
+
border-color: ${field.border};
|
|
424
|
+
color: ${ink.primary};
|
|
425
|
+
}
|
|
426
|
+
${scope} .login-pf-page .pf-c-button.pf-m-control:hover,
|
|
427
|
+
${scope} .login-pf-page .pf-c-button.pf-m-secondary:hover,
|
|
428
|
+
${scope} .login-pf-page .pf-c-button.btn-default:not(.pf-m-primary):hover {
|
|
429
|
+
background-color: ${field.ghostHover};
|
|
430
|
+
border-color: ${field.borderHover};
|
|
431
|
+
color: ${ink.primary};
|
|
432
|
+
}
|
|
433
|
+
/* PatternFly draws control-button borders on an ::after overlay and
|
|
434
|
+
accents its bottom edge on interaction; on the slab that reads as a
|
|
435
|
+
stray underline beneath the password-visibility toggle. */
|
|
436
|
+
${scope} .login-pf-page .pf-c-button::after,
|
|
437
|
+
${scope} .login-pf-page .pf-c-button:hover::after,
|
|
438
|
+
${scope} .login-pf-page .pf-c-button:focus::after,
|
|
439
|
+
${scope} .login-pf-page .pf-c-button:active::after {
|
|
440
|
+
content: none;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/* ── the remember-me checkbox ──────────────────────────────── */
|
|
444
|
+
/* ★ THE RESET IS PART OF THE SKIN, not borrowed from the house sheet.
|
|
445
|
+
A native checkbox is a 13px white box drawn by the platform: no colour
|
|
446
|
+
property reaches it, so on the slab it reads as a bright chip with no
|
|
447
|
+
relationship to anything around it. 'appearance: none' is what makes it
|
|
448
|
+
paintable at all. The visual square stays 22px inside a transparent
|
|
449
|
+
border that pads the hit box out to the press-target floor, so the
|
|
450
|
+
control is small and the target is not (DG-A11Y-01).
|
|
451
|
+
|
|
452
|
+
The house sheet bakes its tick glyph as a data-URI at build time from
|
|
453
|
+
the ramp's CTA foreground, so the springboard's tick is restated here
|
|
454
|
+
rather than inherited through a custom property. */
|
|
455
|
+
${scope} .login-pf-page input[type="checkbox"] {
|
|
456
|
+
appearance: none;
|
|
457
|
+
-webkit-appearance: none;
|
|
458
|
+
position: static;
|
|
459
|
+
box-sizing: content-box;
|
|
460
|
+
width: 22px;
|
|
461
|
+
height: 22px;
|
|
462
|
+
border: ${Math.round((minPressTargetPx - 22) / 2)}px solid transparent;
|
|
463
|
+
border-radius: ${Math.round((minPressTargetPx - 22) / 2) + 8}px;
|
|
464
|
+
background-clip: padding-box;
|
|
465
|
+
background-repeat: no-repeat;
|
|
466
|
+
background-position: center;
|
|
467
|
+
background-size: 15px 15px;
|
|
468
|
+
margin: 0 4px 0 0;
|
|
469
|
+
flex: 0 0 auto;
|
|
470
|
+
cursor: pointer;
|
|
471
|
+
background-color: ${field.background};
|
|
472
|
+
box-shadow: inset 0 0 0 1.5px ${field.borderHover};
|
|
473
|
+
}
|
|
474
|
+
${scope} .login-pf-page .checkbox label,
|
|
475
|
+
${scope} .login-pf-page .pf-c-check {
|
|
476
|
+
display: inline-flex;
|
|
477
|
+
align-items: center;
|
|
478
|
+
min-height: ${minPressTargetPx}px;
|
|
479
|
+
padding-left: 0;
|
|
480
|
+
margin: 0;
|
|
481
|
+
cursor: pointer;
|
|
482
|
+
}
|
|
483
|
+
${scope} .login-pf-page input[type="checkbox"]:checked,
|
|
484
|
+
${scope}.t_dark .login-pf-page input[type="checkbox"]:checked {
|
|
485
|
+
background-color: ${accent.indigo};
|
|
486
|
+
box-shadow: inset 0 0 0 1.5px ${accent.indigo};
|
|
487
|
+
background-image: url("data:image/svg+xml,${encodeURIComponent(
|
|
488
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${ink.primary}" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>`,
|
|
489
|
+
)}");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/* ── links ─────────────────────────────────────────────────── */
|
|
493
|
+
${scope} .login-pf-page a {
|
|
494
|
+
color: ${accent.sky};
|
|
495
|
+
}
|
|
496
|
+
${scope} .login-pf-page a:hover {
|
|
497
|
+
color: ${accent.sky};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/* ── alerts ────────────────────────────────────────────────── */
|
|
501
|
+
${scope} .login-pf-page .pf-c-alert.pf-m-inline {
|
|
502
|
+
background: ${field.background};
|
|
503
|
+
border: 1px solid ${field.border};
|
|
504
|
+
border-left-width: 3px;
|
|
505
|
+
border-left-color: ${status.info};
|
|
506
|
+
}
|
|
507
|
+
${scope} .login-pf-page .pf-c-alert.pf-m-inline.pf-m-danger {
|
|
508
|
+
border-left-color: ${status.error};
|
|
509
|
+
}
|
|
510
|
+
${scope} .login-pf-page .pf-c-alert.pf-m-inline.pf-m-warning {
|
|
511
|
+
border-left-color: ${status.warning};
|
|
512
|
+
}
|
|
513
|
+
${scope} .login-pf-page .pf-c-alert.pf-m-inline.pf-m-success {
|
|
514
|
+
border-left-color: ${status.success};
|
|
515
|
+
}
|
|
516
|
+
${scope} .login-pf-page .pf-c-alert__title {
|
|
517
|
+
color: ${ink.primary};
|
|
518
|
+
}
|
|
519
|
+
${scope} .login-pf-page .pf-c-alert__icon {
|
|
520
|
+
color: ${ink.muted};
|
|
521
|
+
}
|
|
522
|
+
${scope} .login-pf-page span#input-error,
|
|
523
|
+
${scope} .login-pf-page [id^="input-error"],
|
|
524
|
+
${scope} .login-pf-page .pf-m-error,
|
|
525
|
+
${scope} .login-pf-page .required {
|
|
526
|
+
color: ${status.error};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/* ── identity providers: the SSO buttons ───────────────────── */
|
|
530
|
+
/* ★ CHROME ONLY. Every provider row is an <a href={p.loginUrl}> that
|
|
531
|
+
keycloakify renders from social.providers; nothing here changes what
|
|
532
|
+
it points at, what it is called, or whether it appears. */
|
|
533
|
+
${scope} .login-pf-page #kc-social-providers h2 {
|
|
534
|
+
color: ${ink.muted};
|
|
535
|
+
font-size: 13px;
|
|
536
|
+
font-weight: 500;
|
|
537
|
+
}
|
|
538
|
+
${scope} .login-pf-page hr,
|
|
539
|
+
${scope} .login-pf-page #kc-social-providers hr {
|
|
540
|
+
border-top: 1px solid ${field.divider};
|
|
541
|
+
opacity: 1;
|
|
542
|
+
}
|
|
543
|
+
${scope} .login-pf-page .kc-social-item,
|
|
544
|
+
${scope} .login-pf-page .kc-social-links a,
|
|
545
|
+
${scope} .login-pf-page #kc-social-providers li a {
|
|
546
|
+
min-height: ${minPressTargetPx}px;
|
|
547
|
+
background: ${field.ghost};
|
|
548
|
+
border: 1px solid ${field.border};
|
|
549
|
+
border-radius: var(--sb-icon-radius);
|
|
550
|
+
color: ${ink.primary};
|
|
551
|
+
text-decoration: none;
|
|
552
|
+
}
|
|
553
|
+
${scope} .login-pf-page .kc-social-item:hover,
|
|
554
|
+
${scope} .login-pf-page .kc-social-links a:hover,
|
|
555
|
+
${scope} .login-pf-page #kc-social-providers li a:hover {
|
|
556
|
+
background: ${field.ghostHover};
|
|
557
|
+
border-color: ${field.borderHover};
|
|
558
|
+
color: ${ink.primary};
|
|
559
|
+
}
|
|
560
|
+
${scope} .login-pf-page .kc-social-provider-name,
|
|
561
|
+
${scope} .login-pf-page .kc-social-icon-text {
|
|
562
|
+
color: ${ink.primary};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/* ── the OTP credential tiles (login-otp.ftl) ──────────────── */
|
|
566
|
+
${scope} .login-pf-page .pf-c-tile {
|
|
567
|
+
background: ${field.background};
|
|
568
|
+
border: 1px solid ${field.border};
|
|
569
|
+
border-radius: var(--sb-icon-radius);
|
|
570
|
+
color: ${ink.primary};
|
|
571
|
+
}
|
|
572
|
+
${scope} .login-pf-page .pf-c-tile__input:checked + .pf-c-tile,
|
|
573
|
+
${scope} .login-pf-page .pf-c-tile__input:checked + label {
|
|
574
|
+
border-color: ${accent.indigo};
|
|
575
|
+
background: ${field.ghostHover};
|
|
576
|
+
}
|
|
577
|
+
${scope} .login-pf-page .pf-c-tile__title,
|
|
578
|
+
${scope} .login-pf-page .pf-c-tile__icon {
|
|
579
|
+
color: ${ink.primary};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/* ── the registration / back-to-app footer band ────────────── */
|
|
583
|
+
/* The stock sheet bleeds #kc-info to the card edge as a #f0f0f0 band.
|
|
584
|
+
On the slab it becomes the dock's own darker step. */
|
|
585
|
+
${scope} .login-pf-page #kc-info {
|
|
586
|
+
margin: ${plate.gap}px -${cardPadX}px -${cardPadY}px;
|
|
587
|
+
padding: 0;
|
|
588
|
+
color: ${ink.muted};
|
|
589
|
+
}
|
|
590
|
+
${scope} .login-pf-page #kc-info-wrapper {
|
|
591
|
+
background: rgba(8, 11, 20, 0.42);
|
|
592
|
+
border-top: 1px solid ${field.divider};
|
|
593
|
+
border-radius: 0 0 ${plate.radius - 1}px ${plate.radius - 1}px;
|
|
594
|
+
padding: 14px ${cardPadX}px;
|
|
595
|
+
color: ${ink.muted};
|
|
596
|
+
text-align: center;
|
|
597
|
+
}
|
|
598
|
+
${scope} .login-pf-page #kc-info-wrapper a {
|
|
599
|
+
color: ${accent.sky};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/* ── the locale menu ───────────────────────────────────────── */
|
|
603
|
+
${scope} .login-pf-page #kc-current-locale-link {
|
|
604
|
+
color: ${ink.muted};
|
|
605
|
+
}
|
|
606
|
+
${scope} .login-pf-page #kc-current-locale-link:hover {
|
|
607
|
+
background: ${field.ghost};
|
|
608
|
+
color: ${ink.primary};
|
|
609
|
+
}
|
|
610
|
+
${scope} .login-pf-page .pf-c-dropdown__menu {
|
|
611
|
+
background: ${wallpaperColors.base};
|
|
612
|
+
border: 1px solid ${plate.highlight};
|
|
613
|
+
box-shadow: ${plateShadow};
|
|
614
|
+
}
|
|
615
|
+
${scope} .login-pf-page .pf-c-dropdown__menu-item {
|
|
616
|
+
color: ${ink.primary};
|
|
617
|
+
}
|
|
618
|
+
${scope} .login-pf-page .pf-c-dropdown__menu-item:hover {
|
|
619
|
+
background: ${field.ghost};
|
|
620
|
+
color: ${ink.primary};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/* ── focus ring (DG-A11Y-01: non-removable) ────────────────── */
|
|
624
|
+
/* !important outranks both PatternFly's focus styles and the house
|
|
625
|
+
ThemeProvider's global outline reset. The keyboard ring is the one
|
|
626
|
+
affordance a skin must never take away. */
|
|
627
|
+
${scope} a:focus-visible,
|
|
628
|
+
${scope} button:focus-visible,
|
|
629
|
+
${scope} input:focus-visible,
|
|
630
|
+
${scope} select:focus-visible,
|
|
631
|
+
${scope} textarea:focus-visible,
|
|
632
|
+
${scope} [tabindex]:focus-visible {
|
|
633
|
+
outline: ${focusRingWidthPx}px solid ${accent.sky} !important;
|
|
634
|
+
outline-offset: ${focusRingOffsetPx}px !important;
|
|
635
|
+
box-shadow: none !important;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/* ── small screens keep the slab ───────────────────────────── */
|
|
639
|
+
@media (max-width: 767px) {
|
|
640
|
+
${scope} .login-pf-page .card-pf {
|
|
641
|
+
margin: 0 12px;
|
|
642
|
+
background: var(--sb-plate-fill);
|
|
643
|
+
border: 1px solid var(--sb-plate-highlight);
|
|
644
|
+
border-radius: var(--sb-plate-radius);
|
|
645
|
+
box-shadow:
|
|
646
|
+
inset 0 1px 0 ${plate.highlight},
|
|
647
|
+
${plateShadow};
|
|
648
|
+
}
|
|
649
|
+
${scope} .login-pf-page #kc-header-wrapper {
|
|
650
|
+
padding: 28px 16px 14px;
|
|
651
|
+
font-size: 19px;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
`;
|
|
655
|
+
}
|