@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/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";
|
package/src/themeName.ts
ADDED
|
@@ -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"}
|