@omniharmonic/openspace-theme 0.0.0-stage → 0.1.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 +26 -0
- package/README.md +10 -2
- package/dist/index.d.ts +587 -0
- package/dist/index.js +2342 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The OpenSpace contributors
|
|
4
|
+
|
|
5
|
+
This license covers everything in this repository except where a directory
|
|
6
|
+
states otherwise: apps/studio and apps/hub are licensed under AGPL-3.0-only
|
|
7
|
+
(see LICENSE-AGPL), and documentation and template designs are licensed under
|
|
8
|
+
CC-BY-4.0.
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @omniharmonic/openspace-theme
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Design tokens: OKLCH palettes, the contrast solver, type pairings and the Site Styles compiler.
|
|
4
|
+
|
|
5
|
+
Part of [OpenSpace](https://github.com/omniharmonic/openspace), an open-source website builder. Most sites only depend on `@omniharmonic/openspace-site-kit` (and `@omniharmonic/openspace-cli` as a dev dependency); the other packages come along as its dependencies.
|
|
6
|
+
|
|
7
|
+
Versions of the OpenSpace packages are released in lockstep.
|
|
8
|
+
|
|
9
|
+
## License
|
|
10
|
+
|
|
11
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import { COLOR_SCALE_STEPS, COLOR_ROLES, ColorTheme, StyleChoices, Styles, CustomFont, BUTTON_STYLES, DENSITIES, ImageTreatment, MOTION_LEVELS, SHAPES, DtcgToken, DtcgValue, DtcgGroup } from '@omniharmonic/openspace-schema';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Color math: OKLCH ⇄ OKLab ⇄ linear sRGB ⇄ sRGB (hex), CSS color parsing, and gamut mapping
|
|
5
|
+
* by chroma reduction. Matrices from Björn Ottosson's OKLab reference and CSS Color 4.
|
|
6
|
+
*/
|
|
7
|
+
/** OKLCH: lightness 0–1, chroma ≥ 0 (≈0–0.37 in sRGB), hue in degrees 0–360. */
|
|
8
|
+
interface Oklch {
|
|
9
|
+
l: number;
|
|
10
|
+
c: number;
|
|
11
|
+
h: number;
|
|
12
|
+
}
|
|
13
|
+
/** OKLab: lightness 0–1, a/b opponent axes. */
|
|
14
|
+
interface Oklab {
|
|
15
|
+
l: number;
|
|
16
|
+
a: number;
|
|
17
|
+
b: number;
|
|
18
|
+
}
|
|
19
|
+
/** RGB triple. Gamma-encoded sRGB or linear-light sRGB depending on context, channels 0–1. */
|
|
20
|
+
interface Rgb {
|
|
21
|
+
r: number;
|
|
22
|
+
g: number;
|
|
23
|
+
b: number;
|
|
24
|
+
}
|
|
25
|
+
/** Below this chroma a color is treated as achromatic (hue is meaningless). */
|
|
26
|
+
declare const ACHROMATIC_CHROMA = 0.0001;
|
|
27
|
+
declare function srgbChannelToLinear(v: number): number;
|
|
28
|
+
declare function linearChannelToSrgb(v: number): number;
|
|
29
|
+
declare function srgbToLinear({ r, g, b }: Rgb): Rgb;
|
|
30
|
+
declare function linearToSrgb({ r, g, b }: Rgb): Rgb;
|
|
31
|
+
declare function linearSrgbToOklab({ r, g, b }: Rgb): Oklab;
|
|
32
|
+
declare function oklabToLinearSrgb({ l: L, a, b }: Oklab): Rgb;
|
|
33
|
+
declare function normalizeHue(h: number): number;
|
|
34
|
+
declare function oklabToOklch({ l, a, b }: Oklab): Oklch;
|
|
35
|
+
declare function oklchToOklab({ l, c, h }: Oklch): Oklab;
|
|
36
|
+
declare function srgbToOklch(rgb: Rgb): Oklch;
|
|
37
|
+
/** Unclamped sRGB (may fall outside 0–1 when the color is out of gamut). */
|
|
38
|
+
declare function oklchToSrgb(color: Oklch): Rgb;
|
|
39
|
+
declare function inSrgbGamut(color: Oklch): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Map an OKLCH color into sRGB by reducing chroma (hue and lightness preserved) — binary search
|
|
42
|
+
* for the largest in-gamut chroma. Lightness is clamped to 0–1 first.
|
|
43
|
+
*/
|
|
44
|
+
declare function gamutMap(color: Oklch): Oklch;
|
|
45
|
+
/** Gamut-mapped, clamped sRGB (0–1 channels). */
|
|
46
|
+
declare function oklchToSrgbInGamut(color: Oklch): Rgb;
|
|
47
|
+
/** sRGB (0–1) → lowercase `#rrggbb`. */
|
|
48
|
+
declare function rgbToHex({ r, g, b }: Rgb): string;
|
|
49
|
+
/** `#rgb`, `#rgba`, `#rrggbb` or `#rrggbbaa` → sRGB (alpha ignored). */
|
|
50
|
+
declare function hexToRgb(hex: string): Rgb;
|
|
51
|
+
/** OKLCH → gamut-mapped `#rrggbb`. */
|
|
52
|
+
declare function oklchToHex(color: Oklch): string;
|
|
53
|
+
declare function hexToOklch(hex: string): Oklch;
|
|
54
|
+
/**
|
|
55
|
+
* Parse a CSS color (hex, `oklch()`, `oklab()`, `rgb()/rgba()`, `hsl()/hsla()`, `lab()`, `lch()`)
|
|
56
|
+
* into OKLCH. Out-of-gamut inputs are kept as-is; map with {@link gamutMap} before display.
|
|
57
|
+
*/
|
|
58
|
+
declare function parseColor(input: string): Oklch;
|
|
59
|
+
/** True when {@link parseColor} accepts the input. */
|
|
60
|
+
declare function isColor(input: string): boolean;
|
|
61
|
+
/** Format as a CSS `oklch()` string, e.g. `oklch(0.62 0.09 145)`. */
|
|
62
|
+
declare function formatOklch({ l, c, h }: Oklch): string;
|
|
63
|
+
/** Any CSS color → gamut-mapped `#rrggbb`. */
|
|
64
|
+
declare function toHex(input: string | Oklch): string;
|
|
65
|
+
/** Shortest-path interpolation between two hues (t = 0 → a, 1 → b). */
|
|
66
|
+
declare function mixHue(a: number, b: number, t: number): number;
|
|
67
|
+
/** Euclidean distance in OKLab (ΔE_OK). */
|
|
68
|
+
declare function deltaEOK(x: Oklch, y: Oklch): number;
|
|
69
|
+
|
|
70
|
+
/** A semantic color role (doc 04 §3.1), e.g. `text-muted`. */
|
|
71
|
+
type ColorRole = (typeof COLOR_ROLES)[number];
|
|
72
|
+
/** A color scale step, 50–950. */
|
|
73
|
+
type ScaleStep = (typeof COLOR_SCALE_STEPS)[number];
|
|
74
|
+
/** An 11-step color scale. */
|
|
75
|
+
type ColorScale<T = string> = Record<ScaleStep, T>;
|
|
76
|
+
/** Interaction states the solver derives from `accent` (not schema roles; theme-internal). */
|
|
77
|
+
declare const STATE_ROLES: readonly ["accent-hover", "accent-pressed"];
|
|
78
|
+
type StateRole = (typeof STATE_ROLES)[number];
|
|
79
|
+
/** Every color a section theme defines: the 14 semantic roles plus the accent states. */
|
|
80
|
+
type ThemeRole = ColorRole | StateRole;
|
|
81
|
+
|
|
82
|
+
/** WCAG 2.x relative luminance of an sRGB color (0 = black, 1 = white). */
|
|
83
|
+
declare function relativeLuminance(color: string | Oklch): number;
|
|
84
|
+
/** WCAG 2.x contrast ratio, 1–21. Colors are gamut-mapped and quantized to 8-bit sRGB first. */
|
|
85
|
+
declare function contrastRatio(a: string | Oklch, b: string | Oklch): number;
|
|
86
|
+
/** Contrast levels: `body` text, or `large` text and UI components (WCAG 1.4.3 / 1.4.11). */
|
|
87
|
+
type ContrastLevel = "body" | "large";
|
|
88
|
+
type ContrastMode = "standard" | "high";
|
|
89
|
+
/** Minimum ratios. `standard` = WCAG 2.2 AA; `high` = AAA (7:1 body, 4.5:1 large/UI). */
|
|
90
|
+
declare const CONTRAST_MINIMUMS: Record<ContrastMode, Record<ContrastLevel, number>>;
|
|
91
|
+
interface ContrastPair {
|
|
92
|
+
fg: ThemeRole;
|
|
93
|
+
bg: ThemeRole;
|
|
94
|
+
level: ContrastLevel;
|
|
95
|
+
/** Which side the solver moves (default `fg`). State colors are always the side that moves. */
|
|
96
|
+
adjust?: "fg" | "bg";
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Every role pair that must meet contrast in every section theme. Order matters to the solver:
|
|
100
|
+
* `accent` is settled against the backgrounds before `accent-text` is settled against `accent`.
|
|
101
|
+
* `border` is decorative (inputs use `text-muted`-strength outlines) and is not checked.
|
|
102
|
+
* The last four pairs cover the button hover/pressed fills ({@link STATE_PAIRS}).
|
|
103
|
+
*/
|
|
104
|
+
declare const CORE_CONTRAST_PAIRS: readonly ContrastPair[];
|
|
105
|
+
/** Button hover/pressed fills: labels stay legible and the fill stays visible as UI. */
|
|
106
|
+
declare const STATE_PAIRS: readonly ContrastPair[];
|
|
107
|
+
declare const CONTRAST_PAIRS: readonly ContrastPair[];
|
|
108
|
+
type ThemeColors = Record<ThemeRole, string>;
|
|
109
|
+
type ThemeColorSet = Record<ColorTheme, ThemeColors>;
|
|
110
|
+
interface ContrastFailure {
|
|
111
|
+
theme: ColorTheme;
|
|
112
|
+
fg: ThemeRole;
|
|
113
|
+
bg: ThemeRole;
|
|
114
|
+
fgColor: string;
|
|
115
|
+
bgColor: string;
|
|
116
|
+
ratio: number;
|
|
117
|
+
required: number;
|
|
118
|
+
level: ContrastLevel;
|
|
119
|
+
/** Nearest accessible replacement (lightness-only) for the side the solver adjusts: `fg`, or `bg` for state fills. */
|
|
120
|
+
suggestion: string;
|
|
121
|
+
}
|
|
122
|
+
/** List every failing text/background pair across the given themes. */
|
|
123
|
+
declare function auditContrast(themes: Partial<ThemeColorSet>, options?: {
|
|
124
|
+
contrast?: ContrastMode;
|
|
125
|
+
}): ContrastFailure[];
|
|
126
|
+
/**
|
|
127
|
+
* Lightness-only search: the color closest in lightness to `color` (same hue, chroma reduced only
|
|
128
|
+
* as the gamut requires) that reaches `min` contrast against `against`. Returns `null` when even
|
|
129
|
+
* the darkest/lightest end of the line can't reach `min`. `direction` restricts the search to
|
|
130
|
+
* moving darker or lighter.
|
|
131
|
+
*/
|
|
132
|
+
type Direction = "darker" | "lighter" | "either";
|
|
133
|
+
declare function solveLightness(color: Oklch, against: Oklch | string, min: number, direction?: Direction): Oklch | null;
|
|
134
|
+
/**
|
|
135
|
+
* Nearest accessible version of `color` against `against`: same hue, lightness moved the least
|
|
136
|
+
* distance needed to reach `min` (default 4.5:1). Falls back to the higher-contrast extreme.
|
|
137
|
+
* Returns `#rrggbb`.
|
|
138
|
+
*/
|
|
139
|
+
declare function nearestAccessible(color: string, against: string, min?: number): string;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Curated type pairings (doc 04 §4.1). Every family is SIL Open Font License, available from
|
|
143
|
+
* Google Fonts and Fontsource (self-hosted at build time — no third-party font requests).
|
|
144
|
+
*/
|
|
145
|
+
type FontCategory = "serif" | "sans-serif" | "monospace";
|
|
146
|
+
interface FontFamilySpec {
|
|
147
|
+
/** CSS / Google Fonts family name. */
|
|
148
|
+
family: string;
|
|
149
|
+
category: FontCategory;
|
|
150
|
+
/** Weights to load (ascending). */
|
|
151
|
+
weights: number[];
|
|
152
|
+
/** Load italics for these weights too. */
|
|
153
|
+
italic?: boolean;
|
|
154
|
+
/** Fontsource package id (`@fontsource/<id>`); derived from the family name when omitted. */
|
|
155
|
+
fontsourceId?: string;
|
|
156
|
+
/** The family has an optical-size axis (`font-optical-sizing: auto` is meaningful). */
|
|
157
|
+
opticalSize?: boolean;
|
|
158
|
+
}
|
|
159
|
+
interface TypePairing {
|
|
160
|
+
id: string;
|
|
161
|
+
name: string;
|
|
162
|
+
heading: FontFamilySpec;
|
|
163
|
+
body: FontFamilySpec;
|
|
164
|
+
/** Weight for display/h1–h4. */
|
|
165
|
+
headingWeight: number;
|
|
166
|
+
/** Weight for running text. */
|
|
167
|
+
bodyWeight: number;
|
|
168
|
+
/** Weight for `<strong>`, buttons and nav. */
|
|
169
|
+
strongWeight: number;
|
|
170
|
+
/** Tracking for large headings (display, h1, h2); smaller headings get half. */
|
|
171
|
+
headingTracking: string;
|
|
172
|
+
/** Uppercase eyebrow tracking. */
|
|
173
|
+
eyebrowTracking: string;
|
|
174
|
+
/** Mood keywords for Studio's pairing picker. */
|
|
175
|
+
mood: string[];
|
|
176
|
+
/** Launch template that uses this pairing (doc 04 §7). */
|
|
177
|
+
template?: string;
|
|
178
|
+
}
|
|
179
|
+
/** System fallback stacks by category (appended after the web font). */
|
|
180
|
+
declare const FALLBACK_STACKS: Record<FontCategory, string[]>;
|
|
181
|
+
declare const TYPE_PAIRINGS: readonly TypePairing[];
|
|
182
|
+
/** Pairing used when `choices.pairing` names an unknown id. */
|
|
183
|
+
declare const DEFAULT_PAIRING_ID = "fraunces-hanken";
|
|
184
|
+
declare function getPairing(id: string): TypePairing | undefined;
|
|
185
|
+
/** `"Hanken Grotesk"` → `hanken-grotesk` */
|
|
186
|
+
declare function fontsourceId(spec: FontFamilySpec): string;
|
|
187
|
+
/** CSS `font-family` stack for one family: the web font, then the category's system stack. */
|
|
188
|
+
declare function fontStack(spec: FontFamilySpec): string;
|
|
189
|
+
/** Families of a pairing with merged weights (heading and body may share a family). */
|
|
190
|
+
declare function pairingFamilies(p: TypePairing): FontFamilySpec[];
|
|
191
|
+
/**
|
|
192
|
+
* Google Fonts CSS2 API URL for a pairing — for previews and the Studio font picker only;
|
|
193
|
+
* published sites self-host via Fontsource.
|
|
194
|
+
*/
|
|
195
|
+
declare function googleFontsUrl(p: TypePairing, display?: string): string;
|
|
196
|
+
/** Fontsource npm packages to install for a pairing, e.g. `@fontsource/fraunces`. */
|
|
197
|
+
declare function fontsourcePackages(p: TypePairing): string[];
|
|
198
|
+
/**
|
|
199
|
+
* Fontsource CSS entry points per weight/style: module specifiers (`@fontsource/fraunces/400.css`)
|
|
200
|
+
* by default, or jsDelivr URLs with `{ cdn: true }`.
|
|
201
|
+
*/
|
|
202
|
+
declare function fontsourceCss(p: TypePairing, options?: {
|
|
203
|
+
cdn?: boolean;
|
|
204
|
+
}): string[];
|
|
205
|
+
|
|
206
|
+
type NeutralTint = "warm" | "cool" | "pure";
|
|
207
|
+
/** Accent ladder lightness per step: perceptually even in OKLCH. */
|
|
208
|
+
declare const ACCENT_LIGHTNESS: ColorScale<number>;
|
|
209
|
+
declare const NEUTRAL_LIGHTNESS: ColorScale<number>;
|
|
210
|
+
/**
|
|
211
|
+
* 11-step accent scale (50–950) from a brand color: fixed perceptual lightness ladder, hue held,
|
|
212
|
+
* chroma following the brand's and tapering toward the ends, each step gamut-mapped to sRGB.
|
|
213
|
+
*/
|
|
214
|
+
declare function accentScale(brand: string | Oklch): ColorScale<Oklch>;
|
|
215
|
+
/**
|
|
216
|
+
* 11-step neutral scale tinted toward the brand. `warm` and `cool` lean the hue toward amber or
|
|
217
|
+
* blue (pulled 35% toward the brand hue); `pure` is achromatic gray.
|
|
218
|
+
*/
|
|
219
|
+
declare function neutralScale(brand: string | Oklch, tint: NeutralTint): ColorScale<Oklch>;
|
|
220
|
+
/** The scale step whose lightness is closest to the color's (where the brand "sits"). */
|
|
221
|
+
declare function nearestStep(color: string | Oklch): ScaleStep;
|
|
222
|
+
|
|
223
|
+
interface PaletteInput {
|
|
224
|
+
brand: string;
|
|
225
|
+
neutral: NeutralTint;
|
|
226
|
+
contrast?: ContrastMode;
|
|
227
|
+
}
|
|
228
|
+
interface Palette {
|
|
229
|
+
brand: Oklch;
|
|
230
|
+
accent: ColorScale<Oklch>;
|
|
231
|
+
neutral: ColorScale<Oklch>;
|
|
232
|
+
}
|
|
233
|
+
declare function buildPalette(brand: string | Oklch, neutral: NeutralTint): Palette;
|
|
234
|
+
type RoleSet = Record<ColorRole, Oklch>;
|
|
235
|
+
/** Pick the candidate with the highest contrast against `bg`. */
|
|
236
|
+
declare function bestOn(bg: Oklch, candidates: Oklch[]): Oklch;
|
|
237
|
+
/** Label color for text sitting on `bg`: white or the darkest neutral, whichever contrasts more. */
|
|
238
|
+
declare function onColor(bg: Oklch, palette: Palette): Oklch;
|
|
239
|
+
/** The un-solved role sets for each section theme (doc 04 §3.2). */
|
|
240
|
+
declare function themeRoles(palette: Palette): Record<ColorTheme, RoleSet>;
|
|
241
|
+
interface ContrastAdjustment {
|
|
242
|
+
theme: ColorTheme;
|
|
243
|
+
role: ThemeRole;
|
|
244
|
+
from: string;
|
|
245
|
+
to: string;
|
|
246
|
+
/** The role it was adjusted against. */
|
|
247
|
+
against: ThemeRole;
|
|
248
|
+
required: number;
|
|
249
|
+
}
|
|
250
|
+
interface SolvedThemes {
|
|
251
|
+
themes: ThemeColorSet;
|
|
252
|
+
adjustments: ContrastAdjustment[];
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Unsolved hover/pressed fills: the accent stepped in lightness away from its label (toward it
|
|
256
|
+
* only when the accent is already at black/white).
|
|
257
|
+
*/
|
|
258
|
+
declare function accentStates(accent: Oklch, label: Oklch): Record<StateRole, Oklch>;
|
|
259
|
+
/**
|
|
260
|
+
* The contrast solver (doc 04 §3.3). Lightness only, hue never changes.
|
|
261
|
+
*
|
|
262
|
+
* 1. Polarity: each theme has light-on-dark or dark-on-light text. Every surface is moved (if
|
|
263
|
+
* needed) so the pure extreme of that polarity clears the body minimum with headroom — this
|
|
264
|
+
* makes all text/surface pairs jointly solvable.
|
|
265
|
+
* 2. For every pair in {@link CONTRAST_PAIRS}, the foreground moves the minimum distance (on
|
|
266
|
+
* surfaces: only away from them, in the theme's polarity).
|
|
267
|
+
* 3. If a foreground can't get there (e.g. `accent-text` on a mid-tone `accent` in `high` mode),
|
|
268
|
+
* the background moves instead — choosing the direction that keeps the background's own pairs
|
|
269
|
+
* passing, flipping the foreground's polarity when that is what it takes.
|
|
270
|
+
*
|
|
271
|
+
* 4. Button states: `accent-hover` / `accent-pressed` step the accent's lightness away from its
|
|
272
|
+
* label, then are solved against `accent-text` (body) and `background` (UI).
|
|
273
|
+
*
|
|
274
|
+
* `text-muted` is kept no stronger than `text`. Iterates until every pair passes.
|
|
275
|
+
*/
|
|
276
|
+
declare function solveThemes(roles: Record<ColorTheme, RoleSet>, mode?: ContrastMode): SolvedThemes;
|
|
277
|
+
|
|
278
|
+
interface CompileWarning {
|
|
279
|
+
code: "invalid-brand" | "unknown-pairing" | "invalid-override" | "custom-token" | "contrast-adjusted" | "contrast-failure";
|
|
280
|
+
path?: string;
|
|
281
|
+
message: string;
|
|
282
|
+
}
|
|
283
|
+
interface CompileOptions {
|
|
284
|
+
/**
|
|
285
|
+
* Run the contrast solver (default `true`). With `false`, colors ship exactly as chosen and
|
|
286
|
+
* overridden, and `contrast.failures` lists what fails — what Studio shows before the
|
|
287
|
+
* one-click "nearest accessible" fix.
|
|
288
|
+
*/
|
|
289
|
+
solve?: boolean;
|
|
290
|
+
}
|
|
291
|
+
interface ThemeTokens {
|
|
292
|
+
choices: StyleChoices;
|
|
293
|
+
pairing: TypePairing;
|
|
294
|
+
palette: {
|
|
295
|
+
brand: string;
|
|
296
|
+
brandOklch: string;
|
|
297
|
+
accent: ColorScale<string>;
|
|
298
|
+
neutral: ColorScale<string>;
|
|
299
|
+
};
|
|
300
|
+
/** Final role colors (`#rrggbb`) per section theme. */
|
|
301
|
+
themes: ThemeColorSet;
|
|
302
|
+
fonts: {
|
|
303
|
+
googleFontsUrl: string;
|
|
304
|
+
fontsource: string[];
|
|
305
|
+
};
|
|
306
|
+
/** Every emitted custom property, by where it is declared. */
|
|
307
|
+
vars: {
|
|
308
|
+
root: Record<string, string>;
|
|
309
|
+
themes: Record<ColorTheme, Record<string, string>>;
|
|
310
|
+
/** Declared on `:root` and every theme class so `var()` references resolve per section. */
|
|
311
|
+
themed: Record<string, string>;
|
|
312
|
+
imageTreatments: Record<string, Record<string, string>>;
|
|
313
|
+
reducedMotion: Record<string, string>;
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
interface CompiledStyles {
|
|
317
|
+
tokens: ThemeTokens;
|
|
318
|
+
css: string;
|
|
319
|
+
contrast: {
|
|
320
|
+
mode: ContrastMode;
|
|
321
|
+
adjustments: ContrastAdjustment[];
|
|
322
|
+
failures: ContrastFailure[];
|
|
323
|
+
};
|
|
324
|
+
warnings: CompileWarning[];
|
|
325
|
+
}
|
|
326
|
+
/** Override scope that targets every section theme: `color.all.<role>`. */
|
|
327
|
+
declare const ALL_THEMES = "all";
|
|
328
|
+
/**
|
|
329
|
+
* Compile `content/styles.json` into the full token set and the site's token stylesheet.
|
|
330
|
+
*
|
|
331
|
+
* Override paths (DTCG):
|
|
332
|
+
* - `color.all.<role>`: the role in **every** section theme;
|
|
333
|
+
* - `color.<role>`: the role in the default (light) theme only;
|
|
334
|
+
* - `color.<theme>.<role>`: the role in one section theme.
|
|
335
|
+
* Precedence is theme > light > all. All three apply before the contrast solver runs; an
|
|
336
|
+
* overridden `accent` gets a freshly picked `accent-text` unless that is overridden too, and
|
|
337
|
+
* `accent-hover`/`accent-pressed` are always derived from the final accent.
|
|
338
|
+
* - `color.accent-500` / `color.neutral-200` / `color.brand` replace scale variables.
|
|
339
|
+
* - Every other path maps to `--os-<path>` (see `vars.ts`), e.g. `type.display.letterSpacing`.
|
|
340
|
+
*/
|
|
341
|
+
declare function compileStyles(styles: Styles, options?: CompileOptions): CompiledStyles;
|
|
342
|
+
/** Deterministic token stylesheet. */
|
|
343
|
+
declare function emitCss(tokens: ThemeTokens): string;
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Uploaded fonts (PRD §5.1 "Font uploads"): `@font-face` rules for a site's own font files and
|
|
347
|
+
* the font tokens that use them. The files publish with the site (`media/fonts/`), so the URLs
|
|
348
|
+
* are resolved by the caller (renderer: base path; Studio canvas: preview URLs).
|
|
349
|
+
*/
|
|
350
|
+
|
|
351
|
+
/** `"Family", <fallback stack>` for an uploaded family. */
|
|
352
|
+
declare function customFontStack(font: CustomFont): string;
|
|
353
|
+
/** The uploaded font a choice (`headingFont` / `bodyFont`) points at, if any. */
|
|
354
|
+
declare function chosenCustomFont(styles: Pick<Styles, "fonts">, family: string | undefined): CustomFont | undefined;
|
|
355
|
+
/** The available weight closest to `wanted` (ties go to the heavier one). */
|
|
356
|
+
declare function nearestWeight(font: CustomFont, wanted: number): number;
|
|
357
|
+
/**
|
|
358
|
+
* `@font-face` rules for every uploaded font in use (heading/body choices), or for all of them
|
|
359
|
+
* with `all: true` (Studio previews). `font-display: swap` keeps text visible while they load.
|
|
360
|
+
*/
|
|
361
|
+
declare function customFontFaces(styles: Pick<Styles, "fonts" | "choices">, url: (src: string) => string, opts?: {
|
|
362
|
+
all?: boolean;
|
|
363
|
+
}): string;
|
|
364
|
+
/** WOFF2 files worth preloading: the upright file nearest each chosen weight (heading, body). */
|
|
365
|
+
declare function customFontPreloads(styles: Pick<Styles, "fonts" | "choices">, weights: {
|
|
366
|
+
heading: number;
|
|
367
|
+
body: number;
|
|
368
|
+
}): string[];
|
|
369
|
+
|
|
370
|
+
interface PaletteCandidate {
|
|
371
|
+
hex: string;
|
|
372
|
+
oklch: string;
|
|
373
|
+
l: number;
|
|
374
|
+
c: number;
|
|
375
|
+
h: number;
|
|
376
|
+
/** Share of (opaque, sampled) pixels in this cluster, 0–1. */
|
|
377
|
+
population: number;
|
|
378
|
+
/** Ranking score: population weighted by chroma, penalizing near-white/black. */
|
|
379
|
+
score: number;
|
|
380
|
+
}
|
|
381
|
+
interface ExtractOptions {
|
|
382
|
+
/** Number of clusters (default 6). */
|
|
383
|
+
k?: number;
|
|
384
|
+
/** Max candidates returned (default 5). */
|
|
385
|
+
count?: number;
|
|
386
|
+
/** Max pixels sampled (default 20 000; sampling is a fixed stride, so deterministic). */
|
|
387
|
+
maxSamples?: number;
|
|
388
|
+
/** Channels per pixel in `pixels` (default 4, RGBA). */
|
|
389
|
+
channels?: 3 | 4;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Brand color candidates from image pixels (logo or hero photo): weighted k-means in OKLab
|
|
393
|
+
* (samples weighted toward chromatic pixels), deterministic farthest-point seeding, clusters
|
|
394
|
+
* ranked by population × chroma. Near-white, near-black and gray clusters rank last; if the image
|
|
395
|
+
* is entirely achromatic the grays are still returned.
|
|
396
|
+
*/
|
|
397
|
+
declare function extractPalette(pixels: Uint8ClampedArray | Uint8Array | number[], options?: ExtractOptions): PaletteCandidate[];
|
|
398
|
+
|
|
399
|
+
type Density = (typeof DENSITIES)[number];
|
|
400
|
+
type Shape = (typeof SHAPES)[number];
|
|
401
|
+
type ButtonStyle = (typeof BUTTON_STYLES)[number];
|
|
402
|
+
type MotionLevel = (typeof MOTION_LEVELS)[number];
|
|
403
|
+
/** Space steps (`3xs`…`3xl`) on a 4px base, in px, per density. */
|
|
404
|
+
declare const SPACE_SCALES: Record<Density, Record<string, number>>;
|
|
405
|
+
declare function spaceTokens(density: Density): Record<string, string>;
|
|
406
|
+
declare function sectionTokens(density: Density): Record<string, string>;
|
|
407
|
+
/** Content widths (doc 04 §2.2). */
|
|
408
|
+
declare const WIDTH_TOKENS: {
|
|
409
|
+
readonly narrow: "720px";
|
|
410
|
+
readonly content: "1280px";
|
|
411
|
+
readonly wide: "1440px";
|
|
412
|
+
};
|
|
413
|
+
/** Radius scale + component radii (px) per shape. `full` is always a pill. */
|
|
414
|
+
declare const RADIUS_SCALES: Record<Shape, Record<string, number>>;
|
|
415
|
+
declare function radiusTokens(shape: Shape): Record<string, string>;
|
|
416
|
+
declare const SHADOW_VALUES: Record<"none" | "sm" | "md" | "lg", string>;
|
|
417
|
+
declare const BUTTON_ROLES: readonly ["primary", "secondary", "tertiary"];
|
|
418
|
+
type ButtonRole = (typeof BUTTON_ROLES)[number];
|
|
419
|
+
/** Per-role CSS properties a button reads; each becomes `--os-button-<role>-<prop>`. */
|
|
420
|
+
declare const BUTTON_PROPS: readonly ["bg", "color", "border-color", "border-width", "radius", "padding-x", "padding-y", "text-decoration", "icon", "bg-hover", "color-hover", "border-color-hover", "bg-active", "color-active", "border-color-active"];
|
|
421
|
+
type ButtonProp = (typeof BUTTON_PROPS)[number];
|
|
422
|
+
declare function buttonTokens(style: ButtonStyle): Record<ButtonRole, Record<ButtonProp, string>>;
|
|
423
|
+
declare const MOTION_PROPS: readonly ["enabled", "duration-fast", "duration-base", "duration-slow", "ease", "reveal-distance", "reveal-duration", "stagger", "hover-scale", "parallax"];
|
|
424
|
+
type MotionProp = (typeof MOTION_PROPS)[number];
|
|
425
|
+
declare const MOTION_VALUES: Record<MotionLevel, Record<MotionProp, string>>;
|
|
426
|
+
declare const IMAGE_PROPS: readonly ["radius", "clip-path", "aspect-ratio", "filter", "blend", "backdrop", "overlay", "overlay-blend", "overlay-opacity", "frame-padding", "frame-color", "frame-border"];
|
|
427
|
+
type ImageProp = (typeof IMAGE_PROPS)[number];
|
|
428
|
+
/**
|
|
429
|
+
* Image treatment tokens. The renderer reads them on the image wrapper (`radius`, `clip-path`,
|
|
430
|
+
* `aspect-ratio`, `backdrop`, frame), the `<img>` (`filter`, `blend`) and a `::after` overlay.
|
|
431
|
+
* Duotone: a grayscale image multiplied over a light accent, then a dark accent lightened in.
|
|
432
|
+
*/
|
|
433
|
+
declare const IMAGE_TREATMENT_VALUES: Record<ImageTreatment, Record<ImageProp, string>>;
|
|
434
|
+
|
|
435
|
+
interface FlatToken {
|
|
436
|
+
/** Path segments, e.g. `["type", "display", "letterSpacing"]`. */
|
|
437
|
+
path: string[];
|
|
438
|
+
token: DtcgToken;
|
|
439
|
+
/** `$type` from the token or the nearest enclosing group. */
|
|
440
|
+
type: string | undefined;
|
|
441
|
+
}
|
|
442
|
+
/** Flatten a DTCG group into tokens (sorted by path for deterministic output). */
|
|
443
|
+
declare function flattenDtcg(group: DtcgGroup | undefined): FlatToken[];
|
|
444
|
+
/** A value that is exactly one alias, e.g. `{color.accent-500}`. */
|
|
445
|
+
declare const SOLE_ALIAS_RE: RegExp;
|
|
446
|
+
/** `{color.accent-500}` → `var(--os-color-accent-500)` */
|
|
447
|
+
declare function resolveAliases(value: string): string;
|
|
448
|
+
declare function isSafeCssValue(value: string): boolean;
|
|
449
|
+
/**
|
|
450
|
+
* Convert a DTCG `$value` to a CSS value, or `null` if it can't be represented safely.
|
|
451
|
+
* Composite `typography` tokens are expanded by the caller.
|
|
452
|
+
*/
|
|
453
|
+
declare function dtcgToCss(value: DtcgValue, type: string | undefined): string | null;
|
|
454
|
+
|
|
455
|
+
interface CuratedPalette {
|
|
456
|
+
id: string;
|
|
457
|
+
name: string;
|
|
458
|
+
/** Brand color (OKLCH) the full palette is generated from. */
|
|
459
|
+
brand: string;
|
|
460
|
+
neutral: NeutralTint;
|
|
461
|
+
description: string;
|
|
462
|
+
/** Launch templates this palette was designed for (doc 04 §7). */
|
|
463
|
+
templates?: string[];
|
|
464
|
+
}
|
|
465
|
+
/** Named starting palettes for Studio's palette picker. */
|
|
466
|
+
declare const curatedPalettes: readonly CuratedPalette[];
|
|
467
|
+
declare function getCuratedPalette(id: string): CuratedPalette | undefined;
|
|
468
|
+
|
|
469
|
+
/** Every typography preset: the nine content presets plus the UI-only `button` and `nav`. */
|
|
470
|
+
declare const TYPE_PRESET_NAMES: readonly ["display", "h1", "h2", "h3", "h4", "lead", "body", "small", "eyebrow", "button", "nav"];
|
|
471
|
+
type TypePresetName = (typeof TYPE_PRESET_NAMES)[number];
|
|
472
|
+
/** Fluid scale parameters (doc 04 §4.2). Sizes interpolate between the two viewports. */
|
|
473
|
+
declare const FLUID_TYPE: {
|
|
474
|
+
readonly minViewport: 360;
|
|
475
|
+
readonly maxViewport: 1440;
|
|
476
|
+
/** Body size (px) at the min / max viewport. */
|
|
477
|
+
readonly minBase: 16;
|
|
478
|
+
readonly maxBase: 18;
|
|
479
|
+
readonly minRatio: 1.2;
|
|
480
|
+
readonly maxRatio: 1.333;
|
|
481
|
+
};
|
|
482
|
+
/** Smallest rendered size (px) for any preset — keeps captions and eyebrows legible. */
|
|
483
|
+
declare const MIN_FONT_SIZE_PX = 14;
|
|
484
|
+
/**
|
|
485
|
+
* `clamp()` that moves linearly from `minPx` at the min viewport to `maxPx` at the max viewport.
|
|
486
|
+
* Sizes are emitted in rem so they respect the reader's font-size preference.
|
|
487
|
+
*/
|
|
488
|
+
declare function fluidClamp(minPx: number, maxPx: number, minViewport?: number, maxViewport?: number): string;
|
|
489
|
+
/** Font size (px) for a scale step at the min and max viewport. */
|
|
490
|
+
declare function stepSizes(step: number): {
|
|
491
|
+
min: number;
|
|
492
|
+
max: number;
|
|
493
|
+
};
|
|
494
|
+
interface TypePresetTokens {
|
|
495
|
+
"font-family": string;
|
|
496
|
+
"font-size": string;
|
|
497
|
+
"line-height": string;
|
|
498
|
+
"letter-spacing": string;
|
|
499
|
+
"font-weight": string;
|
|
500
|
+
"text-transform": string;
|
|
501
|
+
}
|
|
502
|
+
/** Typography preset tokens for a pairing. Families reference `--os-font-*` vars. */
|
|
503
|
+
declare function typeScale(p: TypePairing): Record<TypePresetName, TypePresetTokens>;
|
|
504
|
+
/** Font family + weight tokens for a pairing. */
|
|
505
|
+
declare function fontTokens(p: TypePairing): Record<string, string>;
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* The CSS custom-property contract. Every token the compiler emits is named here; the renderer's
|
|
509
|
+
* base stylesheet and blocks must reference tokens only through these names.
|
|
510
|
+
*
|
|
511
|
+
* Naming rule (also how DTCG override paths map to properties): path segments, camelCase turned
|
|
512
|
+
* kebab-case, joined with `-`, prefixed `--os-`. `type.display.letterSpacing` →
|
|
513
|
+
* `--os-type-display-letter-spacing`; `color.accent-500` → `--os-color-accent-500`.
|
|
514
|
+
*
|
|
515
|
+
* Where each lives in the compiled CSS:
|
|
516
|
+
* - `color.*` roles: per section theme, on `:root, .os-theme-light` and `.os-theme-<name>`.
|
|
517
|
+
* - `button.*`, `image.*`: re-declared on `:root` and every `.os-theme-*` so their `var()`
|
|
518
|
+
* references resolve against the enclosing section's roles; `.os-image-<treatment>` classes
|
|
519
|
+
* override `image.*` per block.
|
|
520
|
+
* - everything else: `:root`.
|
|
521
|
+
*/
|
|
522
|
+
|
|
523
|
+
declare const VAR_PREFIX = "--os-";
|
|
524
|
+
/** `["type", "display", "letterSpacing"]` → `--os-type-display-letter-spacing` */
|
|
525
|
+
declare function varName(...path: (string | number)[]): string;
|
|
526
|
+
/** `var(--os-…)` for a path. */
|
|
527
|
+
declare const cssVar: (...path: (string | number)[]) => string;
|
|
528
|
+
/** Variable for a schema color token (`accent`, `text-muted`, `neutral-200`). */
|
|
529
|
+
declare const colorVar: (token: string) => string;
|
|
530
|
+
/** Section theme class: `.os-theme-dark` → `os-theme-dark`. */
|
|
531
|
+
declare const themeClass: (theme: ColorTheme) => string;
|
|
532
|
+
/** Per-block image treatment class. */
|
|
533
|
+
declare const imageTreatmentClass: (treatment: string) => string;
|
|
534
|
+
/** Space steps (schema `SPACE_TOKENS`). */
|
|
535
|
+
declare const SPACE_STEPS: readonly ["none", "3xs", "2xs", "xs", "sm", "md", "lg", "xl", "2xl", "3xl"];
|
|
536
|
+
/** Schema `RADIUS_TOKENS` plus component radii set by the shape choice. */
|
|
537
|
+
declare const RADIUS_NAMES: readonly ["none", "sm", "md", "lg", "xl", "full", "button", "card", "input", "image"];
|
|
538
|
+
/** Schema `SHADOW_TOKENS`. */
|
|
539
|
+
declare const SHADOW_NAMES: readonly ["none", "sm", "md", "lg"];
|
|
540
|
+
declare const TYPE_PROPS: readonly ["font-family", "font-size", "line-height", "letter-spacing", "font-weight", "text-transform"];
|
|
541
|
+
/** The documented variable map. Values are custom-property names (`--os-…`). */
|
|
542
|
+
declare const TOKEN_VARS: {
|
|
543
|
+
/** Semantic roles plus `accent-hover` / `accent-pressed` (theme-scoped, contrast-solved). */
|
|
544
|
+
readonly color: Record<ThemeRole, string>;
|
|
545
|
+
/** Accent scale `--os-color-accent-50` … `-950`. */
|
|
546
|
+
readonly accent: Record<100 | 500 | 200 | 50 | 300 | 400 | 600 | 700 | 800 | 900 | 950, string>;
|
|
547
|
+
/** Neutral scale `--os-color-neutral-50` … `-950`. */
|
|
548
|
+
readonly neutral: Record<100 | 500 | 200 | 50 | 300 | 400 | 600 | 700 | 800 | 900 | 950, string>;
|
|
549
|
+
/** The (gamut-mapped) brand color as chosen. */
|
|
550
|
+
readonly brand: string;
|
|
551
|
+
readonly font: {
|
|
552
|
+
readonly heading: string;
|
|
553
|
+
readonly body: string;
|
|
554
|
+
readonly weightHeading: string;
|
|
555
|
+
readonly weightBody: string;
|
|
556
|
+
readonly weightStrong: string;
|
|
557
|
+
};
|
|
558
|
+
/** Typography presets: `--os-type-<preset>-<css-property>`. */
|
|
559
|
+
readonly type: Record<"small" | "display" | "h1" | "h2" | "h3" | "h4" | "lead" | "body" | "eyebrow" | "button" | "nav", Record<"font-family" | "font-size" | "line-height" | "letter-spacing" | "font-weight" | "text-transform", string>>;
|
|
560
|
+
/** Max line length for running text (~70 characters). */
|
|
561
|
+
readonly measure: string;
|
|
562
|
+
readonly space: Record<"none" | "3xs" | "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl", string>;
|
|
563
|
+
readonly section: {
|
|
564
|
+
readonly padY: string;
|
|
565
|
+
readonly blockGap: string;
|
|
566
|
+
readonly gutter: string;
|
|
567
|
+
};
|
|
568
|
+
readonly width: {
|
|
569
|
+
readonly narrow: string;
|
|
570
|
+
readonly content: string;
|
|
571
|
+
readonly wide: string;
|
|
572
|
+
};
|
|
573
|
+
readonly radius: Record<"none" | "input" | "sm" | "md" | "lg" | "xl" | "full" | "image" | "button" | "card", string>;
|
|
574
|
+
readonly shadow: Record<"none" | "sm" | "md" | "lg", string>;
|
|
575
|
+
/** Buttons: `--os-button-<primary|secondary|tertiary>-<prop>`. */
|
|
576
|
+
readonly button: Record<"primary" | "secondary" | "tertiary", Record<"radius" | "color" | "icon" | "bg" | "border-color" | "border-width" | "padding-x" | "padding-y" | "text-decoration" | "bg-hover" | "color-hover" | "border-color-hover" | "bg-active" | "color-active" | "border-color-active", string>>;
|
|
577
|
+
readonly motion: Record<"stagger" | "parallax" | "enabled" | "duration-fast" | "duration-base" | "duration-slow" | "ease" | "reveal-distance" | "reveal-duration" | "hover-scale", string>;
|
|
578
|
+
readonly image: Record<"filter" | "radius" | "overlay" | "clip-path" | "aspect-ratio" | "blend" | "backdrop" | "overlay-blend" | "overlay-opacity" | "frame-padding" | "frame-color" | "frame-border", string>;
|
|
579
|
+
};
|
|
580
|
+
/** Every variable name in {@link TOKEN_VARS}, flattened, in declaration order. */
|
|
581
|
+
declare function allTokenVars(): string[];
|
|
582
|
+
/** Section theme class names, in schema order. */
|
|
583
|
+
declare const THEME_CLASSES: string[];
|
|
584
|
+
|
|
585
|
+
declare const name = "@omniharmonic/openspace-theme";
|
|
586
|
+
|
|
587
|
+
export { ACCENT_LIGHTNESS, ACHROMATIC_CHROMA, ALL_THEMES, BUTTON_PROPS, BUTTON_ROLES, type ButtonProp, type ButtonRole, type ButtonStyle, CONTRAST_MINIMUMS, CONTRAST_PAIRS, CORE_CONTRAST_PAIRS, type ColorRole, type ColorScale, type CompileOptions, type CompileWarning, type CompiledStyles, type ContrastAdjustment, type ContrastFailure, type ContrastLevel, type ContrastMode, type ContrastPair, type CuratedPalette, DEFAULT_PAIRING_ID, type Density, type Direction, type ExtractOptions, FALLBACK_STACKS, FLUID_TYPE, type FlatToken, type FontCategory, type FontFamilySpec, IMAGE_PROPS, IMAGE_TREATMENT_VALUES, type ImageProp, MIN_FONT_SIZE_PX, MOTION_PROPS, MOTION_VALUES, type MotionLevel, type MotionProp, NEUTRAL_LIGHTNESS, type NeutralTint, type Oklab, type Oklch, type Palette, type PaletteCandidate, type PaletteInput, RADIUS_NAMES, RADIUS_SCALES, type Rgb, SHADOW_NAMES, SHADOW_VALUES, SOLE_ALIAS_RE, SPACE_SCALES, SPACE_STEPS, STATE_PAIRS, STATE_ROLES, type ScaleStep, type Shape, type SolvedThemes, type StateRole, THEME_CLASSES, TOKEN_VARS, TYPE_PAIRINGS, TYPE_PRESET_NAMES, TYPE_PROPS, type ThemeColorSet, type ThemeColors, type ThemeRole, type ThemeTokens, type TypePairing, type TypePresetName, type TypePresetTokens, VAR_PREFIX, WIDTH_TOKENS, accentScale, accentStates, allTokenVars, auditContrast, bestOn, buildPalette, buttonTokens, chosenCustomFont, colorVar, compileStyles, contrastRatio, cssVar, curatedPalettes, customFontFaces, customFontPreloads, customFontStack, deltaEOK, dtcgToCss, emitCss, extractPalette, flattenDtcg, fluidClamp, fontStack, fontTokens, fontsourceCss, fontsourceId, fontsourcePackages, formatOklch, gamutMap, getCuratedPalette, getPairing, googleFontsUrl, hexToOklch, hexToRgb, imageTreatmentClass, inSrgbGamut, isColor, isSafeCssValue, linearChannelToSrgb, linearSrgbToOklab, linearToSrgb, mixHue, name, nearestAccessible, nearestStep, nearestWeight, neutralScale, normalizeHue, oklabToLinearSrgb, oklabToOklch, oklchToHex, oklchToOklab, oklchToSrgb, oklchToSrgbInGamut, onColor, pairingFamilies, parseColor, radiusTokens, relativeLuminance, resolveAliases, rgbToHex, sectionTokens, solveLightness, solveThemes, spaceTokens, srgbChannelToLinear, srgbToLinear, srgbToOklch, stepSizes, themeClass, themeRoles, toHex, typeScale, varName };
|