@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/src/glyph.ts ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * THE SPRINGBOARD'S APP GLYPH, TRANSCRIBED FOR KEYCLOAK.
3
+ *
4
+ * ★ PROVENANCE: selfhosted-cloud/shc `packages/springboard-ui/src/glyph.ts`,
5
+ * function for function. See ./tokens.ts for why the values are copied
6
+ * across repositories rather than imported.
7
+ *
8
+ * ★ WHY A LOGIN PAGE WANTS THIS AT ALL. The springboard draws every stack
9
+ * as a rounded square carrying a two-letter monogram over a hue hashed
10
+ * from the app's name, so a user recognises an app by its colour before
11
+ * they read its label. The login page is the surface they land on BEFORE
12
+ * the springboard, and it is per-realm — so seeding the same hash with the
13
+ * realm name puts the estate's own colour on its front door, and the tile
14
+ * a user then sees in the launcher is the same shape they just signed in
15
+ * under. Nothing here is decorative-only: it is the one piece of continuity
16
+ * between the two surfaces that costs no configuration.
17
+ *
18
+ * ★ THE HASH MUST STAY BIT-IDENTICAL TO shc's. Two renderers agreeing on
19
+ * a colour is the entire point, and they agree only because both run
20
+ * FNV-1a 32-bit over the same seed with the same 62%/42% HSL pair.
21
+ * `./glyph.spec.ts` pins concrete seed→hue outputs rather than just
22
+ * asserting determinism, so a "harmless" rewrite of the mixing step fails
23
+ * instead of quietly recolouring one half of the product.
24
+ */
25
+
26
+ /**
27
+ * FNV-1a 32-bit over the seed string. Stable across platforms (pure
28
+ * integer math), well-distributed enough for hue spacing.
29
+ */
30
+ export function stableHash(seed: string): number {
31
+ let hash = 0x811c9dc5;
32
+ for (let i = 0; i < seed.length; i++) {
33
+ hash ^= seed.charCodeAt(i);
34
+ // 32-bit FNV prime multiply, kept in uint32 range.
35
+ hash = Math.imul(hash, 0x01000193) >>> 0;
36
+ }
37
+ return hash >>> 0;
38
+ }
39
+
40
+ /** Deterministic hue (0–359) for a glyph seed. */
41
+ export function glyphHue(seed: string): number {
42
+ return stableHash(seed) % 360;
43
+ }
44
+
45
+ /**
46
+ * Two-letter monogram: first letters of the first two words when the name
47
+ * has separators (dashes/underscores/spaces/dots), else the first two
48
+ * characters. Uppercased; empty name renders "?".
49
+ */
50
+ export function glyphMonogram(name: string): string {
51
+ const trimmed = name.trim();
52
+ if (!trimmed) return "?";
53
+ const words = trimmed.split(/[\s\-_.]+/).filter(Boolean);
54
+ if (words.length >= 2) {
55
+ return `${words[0]![0] ?? ""}${words[1]![0] ?? ""}`.toUpperCase();
56
+ }
57
+ return trimmed.slice(0, 2).toUpperCase();
58
+ }
59
+
60
+ export interface GlyphColors {
61
+ /** Tile fill. */
62
+ background: string;
63
+ /** Monogram color. */
64
+ foreground: string;
65
+ }
66
+
67
+ /**
68
+ * HSL pair for the glyph: a saturated mid-dark fill with a near-white
69
+ * monogram — legible in both color schemes without consulting the theme.
70
+ */
71
+ export function glyphColors(seed: string): GlyphColors {
72
+ const hue = glyphHue(seed);
73
+ return {
74
+ background: `hsl(${hue}, 62%, 42%)`,
75
+ foreground: "hsl(0, 0%, 98%)",
76
+ };
77
+ }
78
+
79
+ /** Everything the stylesheet needs to draw one glyph plate. */
80
+ export interface GlyphMark extends GlyphColors {
81
+ /** The two-letter monogram, already uppercased. */
82
+ monogram: string;
83
+ /** The hue the fill was generated from — exposed for tests and tooling. */
84
+ hue: number;
85
+ }
86
+
87
+ /**
88
+ * Resolve a realm (or client, or estate) name into the mark its login page
89
+ * should wear.
90
+ *
91
+ * ★ SEED THE HASH WITH THE NAME, LABEL WITH THE DISPLAY NAME. Keycloak
92
+ * realms carry both a `name` (the stable slug in every URL) and a
93
+ * `displayName` (free text an admin retypes at will). The springboard
94
+ * hashes app IDENTITY, not app labels — `glyphSeed()` in shc prefers the
95
+ * deployment's `app_name` over the stack's display name for exactly this
96
+ * reason — so the hue is seeded from the slug and stays put when somebody
97
+ * fixes a capital letter in the display name. The monogram is drawn from
98
+ * whichever string a human would read.
99
+ */
100
+ export function glyphMark(params: { name: string; displayName?: string }): GlyphMark {
101
+ const { name, displayName } = params;
102
+ const seed = name.trim() || displayName?.trim() || "";
103
+ const label = displayName?.trim() || name;
104
+ return {
105
+ ...glyphColors(seed),
106
+ monogram: glyphMonogram(label),
107
+ hue: glyphHue(seed),
108
+ };
109
+ }
package/src/index.ts ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @multiplatform.one/keycloak-theme — the SHC springboard as a Keycloak
3
+ * login skin.
4
+ *
5
+ * The design values are transcribed from the SHC console's springboard
6
+ * package; ./tokens.ts carries the provenance and the reason the copy is
7
+ * deliberate. Nothing here renders a Keycloak page: the skin is one
8
+ * stylesheet, hooked onto the class names and ids keycloakify's own pages
9
+ * already emit, so no flow, form or field is owned by this package.
10
+ *
11
+ * Typical wiring, inside a keycloakify `KcPage`:
12
+ *
13
+ * import { SpringboardStyles, isSpringboardTheme } from "@multiplatform.one/keycloak-theme";
14
+ *
15
+ * {isSpringboardTheme(kcContext) && <SpringboardStyles realm={kcContext.realm} />}
16
+ */
17
+
18
+ export { buildSpringboardCss, type SpringboardCssOptions } from "./css";
19
+ export {
20
+ glyphColors,
21
+ glyphHue,
22
+ glyphMark,
23
+ glyphMonogram,
24
+ stableHash,
25
+ type GlyphColors,
26
+ type GlyphMark,
27
+ } from "./glyph";
28
+ export {
29
+ resolveMark,
30
+ SpringboardStyles,
31
+ type SpringboardRealm,
32
+ type SpringboardStylesProps,
33
+ } from "./SpringboardStyles";
34
+ export { houseThemeName, isSpringboardTheme, springboardThemeName } from "./themeName";
35
+ export {
36
+ accent,
37
+ caption,
38
+ field,
39
+ fontFamily,
40
+ icon,
41
+ ink,
42
+ plate,
43
+ status,
44
+ trigger,
45
+ wallpaperColors,
46
+ wallpaperLayers,
47
+ } from "./tokens";
@@ -0,0 +1,69 @@
1
+ /**
2
+ * WHICH THEME IS THIS PAGE WEARING.
3
+ *
4
+ * ★ ONE BUILD, TWO THEME NAMES. `keycloakify build` accepts an array for
5
+ * `themeName` and writes one `theme/<name>/` tree per entry into the same
6
+ * JAR, from the same bundle. That is what lets the springboard skin ship
7
+ * beside the existing house look instead of replacing it:
8
+ *
9
+ * platform-keycloak the house look — tamagui ramps, light and dark,
10
+ * what every existing realm already points at.
11
+ * shc-springboard this skin — the launcher's wallpaper and plate,
12
+ * one dark surface in both schemes.
13
+ *
14
+ * A realm chooses between them with one field (`loginTheme`), which is a
15
+ * value an operator can change and change back without a redeploy. The
16
+ * alternative — restyling `platform-keycloak` in place — would have made
17
+ * the springboard the front door of every estate and every product realm
18
+ * that already points at it, in the same commit, with no way back short of
19
+ * a revert and an image rebuild.
20
+ *
21
+ * ★ HOW A PAGE KNOWS WHICH ONE IT IS. Keycloak serves a theme's own assets
22
+ * from `/resources/<resourceVersion>/<themeType>/<themeName>` and hands the
23
+ * page that prefix as `url.resourcesPath`. The last segment is therefore
24
+ * the name of the theme Keycloak actually loaded — not a build-time guess,
25
+ * and not something the two JAR entries could disagree about, because it
26
+ * is the path the browser fetched this very bundle from.
27
+ *
28
+ * ★ WHY NOT A BUILD-TIME FLAG. keycloakify's `environmentVariables` and
29
+ * `extraThemeProperties` are properties of the BUILD, not of a theme name,
30
+ * so both trees would receive the same value and neither could tell itself
31
+ * apart. There is no per-name build hook. The served path is the only
32
+ * signal that differs, which makes it the right one rather than merely an
33
+ * available one.
34
+ */
35
+
36
+ /** The theme name this skin is published under. */
37
+ export const springboardThemeName = "shc-springboard";
38
+
39
+ /** The theme name the house look keeps. */
40
+ export const houseThemeName = "platform-keycloak";
41
+
42
+ /** The last path segment of a resources path, ignoring any trailing slash. */
43
+ function lastSegment(resourcesPath: string): string {
44
+ const trimmed = resourcesPath.replace(/[?#].*$/, "").replace(/\/+$/, "");
45
+ const slash = trimmed.lastIndexOf("/");
46
+ return slash === -1 ? trimmed : trimmed.slice(slash + 1);
47
+ }
48
+
49
+ /**
50
+ * True when Keycloak served this page from the springboard theme.
51
+ *
52
+ * Accepts the whole context or just the url block — callers in a page have
53
+ * the first, callers in a story have the second. An absent or unparseable
54
+ * path answers `false`: the skin is the opt-in surface, so the house look
55
+ * is what an unknown answer degrades to.
56
+ */
57
+ export function isSpringboardTheme(
58
+ kcContext: { url?: { resourcesPath?: string } } | { resourcesPath?: string } | undefined,
59
+ ): boolean {
60
+ if (!kcContext) return false;
61
+ const resourcesPath =
62
+ "url" in kcContext && kcContext.url
63
+ ? kcContext.url.resourcesPath
64
+ : "resourcesPath" in kcContext
65
+ ? kcContext.resourcesPath
66
+ : undefined;
67
+ if (typeof resourcesPath !== "string" || resourcesPath === "") return false;
68
+ return lastSegment(resourcesPath) === springboardThemeName;
69
+ }
package/src/tokens.ts ADDED
@@ -0,0 +1,260 @@
1
+ /**
2
+ * THE SPRINGBOARD'S DESIGN VALUES, TRANSCRIBED FOR KEYCLOAK.
3
+ *
4
+ * ★ PROVENANCE. Every constant below is copied, by value, from the SHC
5
+ * console's springboard package:
6
+ *
7
+ * selfhosted-cloud/shc packages/springboard-ui/src/wallpaper.ts
8
+ * selfhosted-cloud/shc packages/springboard-ui/src/launcherGeometry.ts
9
+ * selfhosted-cloud/shc packages/springboard-ui/src/glyph.ts
10
+ *
11
+ * which are themselves a second copy of `dress()` in that repo's
12
+ * `traefik/plugin/menu_homescreen.go` and `traefik/plugin/menu.go`. So
13
+ * this file is the THIRD writing of the same numbers, and the duplication
14
+ * is deliberate at every hop for the same reason the shc header gives:
15
+ * the launcher is hand-written ES5 interpreted by yaegi inside Traefik and
16
+ * can never import a TypeScript module. This hop adds one more constraint
17
+ * on top of that — a Keycloak login page is served by Quarkus from a JAR
18
+ * baked into an image, in a different repository, at a different release
19
+ * cadence. A cross-repo import would couple the SHC console's release
20
+ * train to Keycloak's image build for the sake of eight colours.
21
+ *
22
+ * ★ WHAT KEEPS THIS HONEST. Nothing automatic, and that is stated rather
23
+ * than hidden. shc's `launcherGeometry.spec.ts` reads the Go payload and
24
+ * fails when a dimension drifts; there is no equivalent tripwire across
25
+ * repositories. `./tokens.spec.ts` therefore pins every value below as a
26
+ * literal, so a change here is a deliberate edit to a failing test and
27
+ * never a silent drift. When the springboard moves, the sequence is:
28
+ * change shc, change this file, change the spec.
29
+ *
30
+ * ★ THE UNITS ARE CSS PIXELS AT K=1 — the launcher's own. `P(n)` in the
31
+ * payload is `Math.round(n * K)` where K undoes a host page's
32
+ * down-scaling; a Keycloak login page is a document of its own and is
33
+ * never injected into a foreign one, so it has no K to apply, exactly as
34
+ * the console does not.
35
+ */
36
+
37
+ /**
38
+ * The wallpaper: two coloured glows over a navy→indigo diagonal.
39
+ *
40
+ * Transcribed from shc `packages/springboard-ui/src/wallpaper.ts`
41
+ * (`WALLPAPER_LAYERS`). Ready to drop straight into `background-image`.
42
+ */
43
+ export const wallpaperLayers = [
44
+ "radial-gradient(ellipse at 18% 110%, rgba(56,189,248,.24), transparent 56%)",
45
+ "radial-gradient(ellipse at 90% 0, rgba(99,102,241,.26), transparent 50%)",
46
+ "linear-gradient(170deg, #071018, #152544 46%, #1a1544 76%, #080e18)",
47
+ ].join(", ");
48
+
49
+ /**
50
+ * The springboard's flat surface colours.
51
+ *
52
+ * ★ THERE IS NO LIGHT VARIANT, AND THAT IS THE RULING RATHER THAN AN
53
+ * OVERSIGHT. shc's wallpaper.ts spends a paragraph on it: the first cut
54
+ * painted the wallpaper from the theme ramp so it would flip with dark
55
+ * mode, and in the light theme that resolved to a near-white page with no
56
+ * wallpaper and no depth. "The springboard is not a themed page with icons
57
+ * on it; it is a HOME SCREEN, and a home screen has one dark wallpaper in
58
+ * both modes, exactly as iPadOS and the launcher do."
59
+ *
60
+ * A Keycloak login page is the same kind of surface — it is the estate's
61
+ * front door, not a page inside an app — so the sheet built from these
62
+ * values overrides BOTH colour schemes and never consults
63
+ * `prefers-color-scheme`.
64
+ */
65
+ export const wallpaperColors = {
66
+ /** Flat base beneath the gradient stack. `WALLPAPER_BASE`. */
67
+ base: "#0b1220",
68
+ /** Indigo glow, top-right. `WALLPAPER_GLOW_A`. */
69
+ glowA: "rgba(99,102,241,0.26)",
70
+ /** Sky glow, bottom-left. `WALLPAPER_GLOW_B`. */
71
+ glowB: "rgba(56,189,248,0.24)",
72
+ /** Text drawn directly on the wallpaper. `ON_WALLPAPER_TEXT`. */
73
+ text: "#ffffff",
74
+ /** The halo that keeps that text legible over either glow. `ON_WALLPAPER_SHADOW`. */
75
+ textShadow: "#000000",
76
+ } as const;
77
+
78
+ /**
79
+ * The dock's glass shelf — the springboard's PLATE material, and the one
80
+ * the login card is built from.
81
+ *
82
+ * ★ WHY THE DOCK AND NOT THE SPOTLIGHT PANEL. The console has two
83
+ * surfaces that float above the wallpaper: the dock shelf, whose colours
84
+ * are these constants, and the Spotlight dialog, which is fully themed
85
+ * from the tamagui ramp (shc wallpaper.ts: "Chrome that sits on PANELS
86
+ * above the wallpaper … stays fully themed"). A login page cannot take
87
+ * the second one — it has no ThemeProvider ramp of its own that is
88
+ * guaranteed to resolve dark, and a light Spotlight panel on a dark
89
+ * wallpaper is the exact defect that ruling was written against. The dock
90
+ * shelf is the springboard's one plate with fixed, wallpaper-aware
91
+ * colours, so it is the one a login card can be cut from.
92
+ */
93
+ export const plate = {
94
+ /** `DOCK_SHELF_FILL` — the translucent slab. */
95
+ fill: "rgba(16,20,32,0.48)",
96
+ /** `DOCK_SHELF_HIGHLIGHT` — the 1px inner highlight along the top edge. */
97
+ highlight: "rgba(255,255,255,0.27)",
98
+ /** `launcherDock.radius`. */
99
+ radius: 26,
100
+ /** `launcherDock.paddingVertical` / `.paddingHorizontal`. */
101
+ paddingVertical: 12,
102
+ paddingHorizontal: 16,
103
+ /** `launcherDock.gap`. */
104
+ gap: 18,
105
+ } as const;
106
+
107
+ /**
108
+ * One app icon — the launcher's `face()` squircle.
109
+ *
110
+ * ★ ONE SIZE, EVERY VIEWPORT. Owner ruling 2026-08-19, quoted in shc's
111
+ * launcherGeometry.ts: "the scale and sizing on floating menu seems more
112
+ * correct". The console had a responsive 64/76/84 ladder and deleted it;
113
+ * a home-screen icon is the same size on a phone and a desktop. The login
114
+ * page draws exactly one of these, so the ruling reaches it for free.
115
+ */
116
+ export const icon = {
117
+ /** `launcherIcon.size` — applied to the squircle AND the art inside it. */
118
+ size: 60,
119
+ /** `launcherIcon.radius` — 0.2333 of the box, not tamagui's 0.22. */
120
+ radius: 14,
121
+ /** `launcherIcon.shadowOffsetY` / `.shadowBlur` — `0 3px 8px #0006`. */
122
+ shadowOffsetY: 3,
123
+ shadowBlur: 8,
124
+ shadowColor: "#0006",
125
+ } as const;
126
+
127
+ /**
128
+ * The caption under an icon.
129
+ *
130
+ * ★ THE LINE BOX IS LOAD-BEARING, NOT TIDY. shc's launcherGeometry.ts
131
+ * carries the reading: the owner saw "hulv" and "openreplav" on his phone
132
+ * because a caption box with no line-height of its own inherited one from
133
+ * the host document and clipped the descenders off `y`. An 11px face in a
134
+ * 15px line box carries the descender of every Latin glyph. A Keycloak
135
+ * page has no host document to inherit from either, but it renders the
136
+ * same names at the same size and gets the same box for the same reason.
137
+ */
138
+ export const caption = {
139
+ /** `launcherCaption.fontSize`. */
140
+ fontSize: 11,
141
+ /** `launcherCaption.lineHeight`. */
142
+ lineHeight: 15,
143
+ /** `launcherCaption.marginTop` — squircle to label. */
144
+ marginTop: 6,
145
+ /** StackTile.tsx: `textShadowOffset={{ width: 0, height: 1 }}`, radius 3. */
146
+ shadowOffsetY: 1,
147
+ shadowBlur: 3,
148
+ /** StackTile.tsx: `fontWeight="600"`. */
149
+ fontWeight: 600,
150
+ } as const;
151
+
152
+ /**
153
+ * The floating trigger — `trig()` in shc's `traefik/plugin/menu.go`, and
154
+ * the launcher's LARGER elevation.
155
+ *
156
+ * ★ WHAT THIS IS FOR HERE. The login card is plate-sized, not icon-sized,
157
+ * and `icon.shadow*` (`0 3px 8px`) under a 400px card reads as no shadow
158
+ * at all. The trigger is the only other elevation the launcher declares,
159
+ * it is the one the owner has quoted a number for out loud ("a 52px
160
+ * control becomes a 20px one"), and borrowing its shadow keeps the card
161
+ * inside the springboard's two-step elevation scale instead of inventing
162
+ * a third.
163
+ */
164
+ export const trigger = {
165
+ size: 52,
166
+ radius: 16,
167
+ inset: 16,
168
+ shadowOffsetY: 6,
169
+ shadowBlur: 20,
170
+ } as const;
171
+
172
+ /**
173
+ * The accent pair, taken from the wallpaper's own two glows.
174
+ *
175
+ * ★ THESE ARE DERIVED, NOT TRANSCRIBED, and the derivation is one step:
176
+ * the opaque form of `wallpaperColors.glowA` / `.glowB`, i.e. the same
177
+ * `rgb()` triples with the alpha dropped. The springboard itself never
178
+ * needs an opaque accent — its only saturated fills are per-app generated
179
+ * hues (see ./glyph.ts) and fixed system-app hues from the tamagui ramp,
180
+ * and a login page has neither an app to hash nor a ramp to resolve. It
181
+ * does need one call-to-action colour, and taking it from the wallpaper
182
+ * is the only choice that cannot drift away from the wallpaper.
183
+ *
184
+ * rgba(99,102,241,.26) -> #6366f1 (indigo — the primary CTA)
185
+ * rgba(56,189,248,.24) -> #38bdf8 (sky — links and focus)
186
+ */
187
+ export const accent = {
188
+ /** Opaque `wallpaperColors.glowA`. Primary buttons. */
189
+ indigo: "#6366f1",
190
+ /** One step down for :hover — the same hue, 8% darker. */
191
+ indigoHover: "#5457dd",
192
+ /** One step further for :active. */
193
+ indigoPress: "#484bc4",
194
+ /** Opaque `wallpaperColors.glowB`. Links and the focus ring. */
195
+ sky: "#38bdf8",
196
+ } as const;
197
+
198
+ /**
199
+ * Ink levels for text ON the plate.
200
+ *
201
+ * ★ NOT TRANSCRIBED — the springboard has no equivalent, because it has
202
+ * no dense text on a plate. Its captions sit straight on the wallpaper
203
+ * (`wallpaperColors.text` plus a halo) and its panels are themed. A login
204
+ * form is mostly labels, helper text and error text, which the springboard
205
+ * simply never renders. These are the wallpaper's white stepped down by
206
+ * opacity so that every level stays the SAME colour as the caption and can
207
+ * only differ in weight — which is the property the springboard does have
208
+ * and the one worth preserving.
209
+ */
210
+ export const ink = {
211
+ /** Headings and input values. `wallpaperColors.text`. */
212
+ primary: "#ffffff",
213
+ /** Labels. */
214
+ secondary: "rgba(255,255,255,0.82)",
215
+ /** Helper text, captions, the muted half of a row. */
216
+ muted: "rgba(255,255,255,0.62)",
217
+ /** Placeholders and disabled labels. */
218
+ faint: "rgba(255,255,255,0.42)",
219
+ } as const;
220
+
221
+ /**
222
+ * Field chrome on the plate — hairlines cut from the plate's own
223
+ * highlight so a field reads as an inset in the slab rather than a
224
+ * borrowed control.
225
+ */
226
+ export const field = {
227
+ /** Input fill: the plate darkened, so a field sits INTO the slab. */
228
+ background: "rgba(8,11,20,0.55)",
229
+ /** Resting hairline — `plate.highlight` at half strength. */
230
+ border: "rgba(255,255,255,0.14)",
231
+ borderHover: "rgba(255,255,255,0.24)",
232
+ borderFocus: accent.sky,
233
+ /** Divider rules (the social-provider separator). */
234
+ divider: "rgba(255,255,255,0.12)",
235
+ /** Secondary/ghost button fill. */
236
+ ghost: "rgba(255,255,255,0.08)",
237
+ ghostHover: "rgba(255,255,255,0.14)",
238
+ } as const;
239
+
240
+ /** Feedback hues for the alert rail. Tailwind-family, matched to the glows. */
241
+ export const status = {
242
+ error: "#f87171",
243
+ warning: "#fbbf24",
244
+ success: "#34d399",
245
+ info: accent.sky,
246
+ } as const;
247
+
248
+ /**
249
+ * The default body face.
250
+ *
251
+ * ★ NO WEBFONT, DELIBERATELY. The springboard renders in Inter because
252
+ * tamagui's `@tamagui/font-inter` is already in the console bundle. A
253
+ * Keycloak login page is the front door of an estate that may have no
254
+ * egress at all, and a `@font-face` pointing at a CDN is a render-blocking
255
+ * request to a host an air-gapped install cannot reach. Inter first for
256
+ * the machines that have it, the platform UI stack behind it for the ones
257
+ * that do not.
258
+ */
259
+ export const fontFamily =
260
+ 'Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The springboard skin, delivered as one <style> element inside the React
3
+ * tree.
4
+ *
5
+ * ★ WHY NOT A .css FILE. Two reasons, and only the second is about taste.
6
+ * The first is that the sheet is PARAMETRIC — the glyph plate's fill is a
7
+ * hue hashed from the realm name at render time (see ./glyph.ts), so it
8
+ * cannot exist as a static asset without one stylesheet per realm. The
9
+ * second is that `apps/keycloak` already delivers `KcHouseStyles` this way
10
+ * and the repo's apps carry a no-.css-files rule; a skin that composes
11
+ * with that sheet should arrive by the same door.
12
+ *
13
+ * ★ MOUNT IT ONCE, HIGH. It belongs beside `KcHouseStyles` — inside the
14
+ * theme provider, above the page switch — so it is present on every
15
+ * pageId including the ones nobody remembers exist. It renders no visible
16
+ * markup, so where in <body> it lands does not matter; see ./css.ts on why
17
+ * the layering does not depend on document order either.
18
+ */
19
+ import type { SpringboardCssOptions } from "./css";
20
+ import type { GlyphMark } from "./glyph";
21
+ /**
22
+ * The shape this component reads off a Keycloak context.
23
+ *
24
+ * ★ STRUCTURAL, NOT `import type { KcContext }`. The realm block is two
25
+ * strings and it has been those two strings since Keycloak 1.x. Typing
26
+ * against the real context would pull `keycloakify` into this package's
27
+ * type graph — and with it a `kc.gen.ts` that only exists inside a built
28
+ * theme — to describe a pair of optional strings.
29
+ */
30
+ export interface SpringboardRealm {
31
+ name?: string;
32
+ displayName?: string;
33
+ }
34
+ export interface SpringboardStylesProps extends Omit<SpringboardCssOptions, "mark"> {
35
+ /**
36
+ * The realm the page belongs to — `kcContext.realm`. Seeds the glyph
37
+ * plate. Omit it (or pass a realm with no name) and no plate is drawn.
38
+ */
39
+ realm?: SpringboardRealm;
40
+ /**
41
+ * An explicit mark, when the caller would rather not be hashed. Wins
42
+ * over `realm`.
43
+ */
44
+ mark?: GlyphMark;
45
+ }
46
+ /** Resolve the plate for a realm; `undefined` when there is nothing to seed with. */
47
+ export declare function resolveMark(realm: SpringboardRealm | undefined): GlyphMark | undefined;
48
+ export declare function SpringboardStyles(props: SpringboardStylesProps): import("react/jsx-runtime").JSX.Element;
49
+ //# sourceMappingURL=SpringboardStyles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SpringboardStyles.d.ts","sourceRoot":"","sources":["../src/SpringboardStyles.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,OAAO,CAAC;AAEnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAGzC;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,sBAAuB,SAAQ,IAAI,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACjF;;;OAGG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB;AAED,qFAAqF;AACrF,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAKtF;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,2CAuB9D"}
package/types/css.d.ts ADDED
@@ -0,0 +1,72 @@
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
+ import type { GlyphMark } from "./glyph";
46
+ export interface SpringboardCssOptions {
47
+ /**
48
+ * The glyph plate to draw above the page title. Omit it and no plate is
49
+ * emitted at all — the rule is absent rather than empty, so a caller
50
+ * without a realm name gets stock spacing instead of a blank square.
51
+ */
52
+ mark?: GlyphMark;
53
+ /** Body face. Defaults to the no-webfont stack in ./tokens.ts. */
54
+ fontFamily?: string;
55
+ /**
56
+ * The keyboard focus ring. Defaults match `FOCUS_VISIBLE_RING` in
57
+ * `@multiplatform.one/theme`; they are options rather than an import so
58
+ * this package stays dependency-free and testable without tamagui.
59
+ */
60
+ focusRingWidthPx?: number;
61
+ focusRingOffsetPx?: number;
62
+ /**
63
+ * The press-target floor (`MIN_PRESS_TARGET` in `@multiplatform.one/theme`).
64
+ * DG-A11Y-01; restated here so the skin cannot shrink a control below it.
65
+ */
66
+ minPressTargetPx?: number;
67
+ /** Card width. The springboard has no card, so this is a login-page choice. */
68
+ cardMaxWidthPx?: number;
69
+ }
70
+ /** Build the springboard stylesheet. Pure — same options, same string. */
71
+ export declare function buildSpringboardCss(options?: SpringboardCssOptions): string;
72
+ //# sourceMappingURL=css.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"css.d.ts","sourceRoot":"","sources":["../src/css.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAezC,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AA4HD,0EAA0E;AAC1E,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,qBAA0B,GAAG,MAAM,CA8b/E"}