@valancex/style 0.0.0-stage → 0.15.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 +21 -0
- package/README.md +33 -2
- package/contract/style-contract-v1.json +207 -0
- package/dist/check.d.ts +31 -0
- package/dist/check.js +250 -0
- package/dist/color.d.ts +17 -0
- package/dist/color.js +82 -0
- package/dist/contrast.d.ts +5 -0
- package/dist/contrast.js +13 -0
- package/dist/coverage.d.ts +21 -0
- package/dist/coverage.js +60 -0
- package/dist/css.d.ts +9 -0
- package/dist/css.js +201 -0
- package/dist/desktop.d.ts +33 -0
- package/dist/desktop.js +112 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +11 -0
- package/dist/load.d.ts +5 -0
- package/dist/load.js +8 -0
- package/dist/primitives.d.ts +2 -0
- package/dist/primitives.js +364 -0
- package/dist/suggest.d.ts +2 -0
- package/dist/suggest.js +22 -0
- package/dist/theme.d.ts +5 -0
- package/dist/theme.js +129 -0
- package/dist/tokens.d.ts +21 -0
- package/dist/tokens.js +257 -0
- package/dist/types.d.ts +114 -0
- package/dist/types.js +3 -0
- package/package.json +64 -3
- package/schema/dtcg-2025.10/NOTICE.md +6 -0
- package/schema/dtcg-2025.10/format/group.json +54 -0
- package/schema/dtcg-2025.10/format/groupOrToken.json +14 -0
- package/schema/dtcg-2025.10/format/token.json +450 -0
- package/schema/dtcg-2025.10/format/tokenType.json +22 -0
- package/schema/dtcg-2025.10/format/values/border.json +44 -0
- package/schema/dtcg-2025.10/format/values/color.json +570 -0
- package/schema/dtcg-2025.10/format/values/cubicBezier.json +56 -0
- package/schema/dtcg-2025.10/format/values/dimension.json +34 -0
- package/schema/dtcg-2025.10/format/values/duration.json +34 -0
- package/schema/dtcg-2025.10/format/values/fontFamily.json +33 -0
- package/schema/dtcg-2025.10/format/values/fontWeight.json +38 -0
- package/schema/dtcg-2025.10/format/values/gradient.json +52 -0
- package/schema/dtcg-2025.10/format/values/number.json +7 -0
- package/schema/dtcg-2025.10/format/values/shadow.json +102 -0
- package/schema/dtcg-2025.10/format/values/strokeStyle.json +63 -0
- package/schema/dtcg-2025.10/format/values/transition.json +44 -0
- package/schema/dtcg-2025.10/format/values/typography.json +72 -0
- package/schema/dtcg-2025.10/format.json +105 -0
- package/schema/recipe-v1.schema.json +43 -0
- package/schema/style-contract-v1.schema.json +171 -0
- package/schema/theme-v1.schema.json +78 -0
- package/themes/default/recipes/button.json +87 -0
- package/themes/default/recipes/checkbox.json +26 -0
- package/themes/default/recipes/column.json +10 -0
- package/themes/default/recipes/heading.json +11 -0
- package/themes/default/recipes/row.json +10 -0
- package/themes/default/recipes/text.json +12 -0
- package/themes/default/recipes/textfield.json +37 -0
- package/themes/default/theme.json +61 -0
- package/themes/default/tokens/base.tokens.json +193 -0
- package/themes/default/tokens/contrast.tokens.json +127 -0
- package/themes/default/tokens/dark.tokens.json +127 -0
- package/themes/default/tokens/light.tokens.json +127 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Inputs } from "./check.js";
|
|
2
|
+
import type { LoadedTheme } from "./types.js";
|
|
3
|
+
export type Target = "web" | "desktop";
|
|
4
|
+
export interface NotApplied {
|
|
5
|
+
readonly primitive: string;
|
|
6
|
+
readonly kind: "property" | "condition" | "state";
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly support: "partial" | "unsupported";
|
|
9
|
+
readonly note?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface Coverage {
|
|
12
|
+
readonly target: Target;
|
|
13
|
+
/** The primitives the target has a hook for, and a recipe styles. */
|
|
14
|
+
readonly styled: ReadonlyArray<string>;
|
|
15
|
+
/** The primitives the target has a hook for, and no recipe styles. */
|
|
16
|
+
readonly unstyled: ReadonlyArray<string>;
|
|
17
|
+
readonly notApplied: ReadonlyArray<NotApplied>;
|
|
18
|
+
}
|
|
19
|
+
/** The portable-core primitives: the ones a theme must be able to style (they carry the web's hook). */
|
|
20
|
+
export declare const styleable: (inputs: Inputs) => ReadonlyArray<string>;
|
|
21
|
+
export declare const coverageOf: (loaded: LoadedTheme, inputs: Inputs, target: Target) => Coverage;
|
package/dist/coverage.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Which primitives a theme styles, and what it asks that a target does not apply. A target that cannot honor something says so here, instead of the theme silently doing less (decision S5: the Desktop
|
|
2
|
+
// honors the viewport and the color scheme first, and reports the rest as unsupported).
|
|
3
|
+
import { hookOf } from "./check.js";
|
|
4
|
+
/** The portable-core primitives: the ones a theme must be able to style (they carry the web's hook). */
|
|
5
|
+
export const styleable = (inputs) => Object.keys(inputs.primitives).filter((name) => hookOf(inputs, name) !== undefined && inputs.primitives[name].status === "portable");
|
|
6
|
+
export const coverageOf = (loaded, inputs, target) => {
|
|
7
|
+
const recipes = new Map(loaded.recipes.map(({ recipe }) => [recipe.target.primitive, recipe]));
|
|
8
|
+
const all = styleable(inputs);
|
|
9
|
+
const notApplied = [];
|
|
10
|
+
if (target === "desktop") {
|
|
11
|
+
const note = inputs.contract.desktopNotes ?? {};
|
|
12
|
+
for (const name of all) {
|
|
13
|
+
const recipe = recipes.get(name);
|
|
14
|
+
if (recipe === undefined) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const properties = new Set();
|
|
18
|
+
const add = (declarations) => Object.keys(declarations ?? {}).forEach((property) => properties.add(property));
|
|
19
|
+
add(recipe.base);
|
|
20
|
+
Object.values(recipe.variants ?? {}).forEach((values) => Object.values(values).forEach(add));
|
|
21
|
+
for (const property of properties) {
|
|
22
|
+
const support = inputs.contract.properties[property]?.desktop;
|
|
23
|
+
if (support !== undefined && support !== "supported") {
|
|
24
|
+
notApplied.push({ primitive: name, kind: "property", name: property, support, ...(note[property] === undefined ? {} : { note: note[property] }) });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (const [state, declarations] of Object.entries(recipe.states ?? {})) {
|
|
28
|
+
const support = inputs.contract.desktopStates?.[state] ?? "unsupported";
|
|
29
|
+
if (support !== "supported") {
|
|
30
|
+
notApplied.push({ primitive: name, kind: "state", name: state, support, ...(note["states"] === undefined ? {} : { note: note["states"] }) });
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
// a supported state still applies only the properties the Desktop honors
|
|
34
|
+
for (const property of Object.keys(declarations)) {
|
|
35
|
+
const propertySupport = inputs.contract.properties[property]?.desktop;
|
|
36
|
+
if (propertySupport !== undefined && propertySupport !== "supported") {
|
|
37
|
+
notApplied.push({ primitive: name, kind: "property", name: `${state}: ${property}`, support: propertySupport });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const condition of Object.keys(recipe.when ?? {})) {
|
|
43
|
+
const [kind] = condition.split(":");
|
|
44
|
+
const support = inputs.contract.conditions[kind]?.desktop;
|
|
45
|
+
if (support === "unsupported") {
|
|
46
|
+
notApplied.push({ primitive: name, kind: "condition", name: condition, support: "unsupported" });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// modes the Desktop cannot choose (it has light and dark; high contrast is the person's setting, not reported to it)
|
|
51
|
+
for (const mode of Object.keys(loaded.modes)) {
|
|
52
|
+
const condition = inputs.contract.modeConditions?.[mode];
|
|
53
|
+
const kind = condition?.split(":")[0];
|
|
54
|
+
if (kind !== undefined && inputs.contract.conditions[kind]?.desktop === "unsupported") {
|
|
55
|
+
notApplied.push({ primitive: "(theme)", kind: "condition", name: `mode ${mode}`, support: "unsupported" });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { target, styled: all.filter((name) => recipes.has(name)), unstyled: all.filter((name) => !recipes.has(name)), notApplied };
|
|
60
|
+
};
|
package/dist/css.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Inputs } from "./check.js";
|
|
2
|
+
import type { LoadedTheme, RecipeValue, ResolvedToken } from "./types.js";
|
|
3
|
+
export declare const propertyName: (path: string) => string;
|
|
4
|
+
/** The custom properties a token is: one for most, five for a typography. */
|
|
5
|
+
export declare const propertiesOf: (token: ResolvedToken) => ReadonlyArray<readonly [name: string, value: string]>;
|
|
6
|
+
/** The CSS declarations of one property of the contract. */
|
|
7
|
+
export declare const declarationOf: (property: string, value: RecipeValue) => ReadonlyArray<string>;
|
|
8
|
+
/** The stylesheet of a theme whose problems are none. */
|
|
9
|
+
export declare const compileWeb: (loaded: LoadedTheme, inputs: Inputs) => string;
|
package/dist/css.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// The web's realization of a theme: one static stylesheet, the same on the server and in the browser, so nothing is inserted at run time and nothing differs at hydration.
|
|
2
|
+
//
|
|
3
|
+
// @layer vlx-library, vlx-theme, vlx-app; the layers, library first: a library's styles lose to a theme's, and a theme's to the application's own (unlayered CSS beats any layer)
|
|
4
|
+
// @layer vlx-theme { … } the tokens as custom properties, per mode; then each recipe's rules
|
|
5
|
+
//
|
|
6
|
+
// A rule reaches a primitive only by what the contract names: its hook (`[data-vlx="button"]`), a variant's attribute (`[data-intent="primary"]`), a state's selector (`:hover`,
|
|
7
|
+
// `[aria-invalid="true"]`) and a media condition. Never an element, a class, or a descendant.
|
|
8
|
+
import { toCss } from "./color.js";
|
|
9
|
+
import { hookOf, statesOf } from "./check.js";
|
|
10
|
+
const GENERIC_FAMILIES = new Set([
|
|
11
|
+
"serif",
|
|
12
|
+
"sans-serif",
|
|
13
|
+
"monospace",
|
|
14
|
+
"cursive",
|
|
15
|
+
"fantasy",
|
|
16
|
+
"system-ui",
|
|
17
|
+
"ui-serif",
|
|
18
|
+
"ui-sans-serif",
|
|
19
|
+
"ui-monospace",
|
|
20
|
+
"ui-rounded",
|
|
21
|
+
"emoji",
|
|
22
|
+
"math",
|
|
23
|
+
"fangsong",
|
|
24
|
+
]);
|
|
25
|
+
export const propertyName = (path) => `--vlx-${path.replaceAll(".", "-")}`;
|
|
26
|
+
const dimension = (value) => {
|
|
27
|
+
const { value: amount, unit } = value;
|
|
28
|
+
return `${amount}${unit}`;
|
|
29
|
+
};
|
|
30
|
+
const family = (value) => (Array.isArray(value) ? value : [value])
|
|
31
|
+
.map((name) => (GENERIC_FAMILIES.has(name) || /^[A-Za-z][A-Za-z0-9-]*$/.test(name) ? name : JSON.stringify(name)))
|
|
32
|
+
.join(", ");
|
|
33
|
+
const shadow = (value) => (Array.isArray(value) ? value : [value])
|
|
34
|
+
.map((each) => {
|
|
35
|
+
const s = each;
|
|
36
|
+
return `${s.inset === true ? "inset " : ""}${dimension(s.offsetX)} ${dimension(s.offsetY)} ${dimension(s.blur)} ${dimension(s.spread)} ${toCss(s.color)}`;
|
|
37
|
+
})
|
|
38
|
+
.join(", ");
|
|
39
|
+
/** The custom properties a token is: one for most, five for a typography. */
|
|
40
|
+
export const propertiesOf = (token) => {
|
|
41
|
+
const name = propertyName(token.path);
|
|
42
|
+
switch (token.type) {
|
|
43
|
+
case "color":
|
|
44
|
+
return [[name, toCss(token.value)]];
|
|
45
|
+
case "dimension":
|
|
46
|
+
return [[name, dimension(token.value)]];
|
|
47
|
+
case "number":
|
|
48
|
+
return [[name, String(token.value)]];
|
|
49
|
+
case "fontWeight":
|
|
50
|
+
return [[name, String(token.value)]];
|
|
51
|
+
case "fontFamily":
|
|
52
|
+
return [[name, family(token.value)]];
|
|
53
|
+
case "duration":
|
|
54
|
+
return [[name, dimension(token.value)]];
|
|
55
|
+
case "shadow":
|
|
56
|
+
return [[name, shadow(token.value)]];
|
|
57
|
+
case "typography": {
|
|
58
|
+
const t = token.value;
|
|
59
|
+
return [
|
|
60
|
+
[`${name}-family`, family(t.fontFamily)],
|
|
61
|
+
[`${name}-size`, dimension(t.fontSize)],
|
|
62
|
+
[`${name}-weight`, String(t.fontWeight)],
|
|
63
|
+
[`${name}-letter-spacing`, t.letterSpacing === undefined ? "normal" : dimension(t.letterSpacing)],
|
|
64
|
+
[`${name}-line-height`, t.lineHeight === undefined ? "normal" : String(t.lineHeight)],
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
default:
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const variable = (path) => `var(${propertyName(path)})`;
|
|
72
|
+
const stroke = (value) => `${variable(value["width"])} ${value["style"] ?? "solid"} ${variable(value["color"])}`;
|
|
73
|
+
/** The CSS declarations of one property of the contract. */
|
|
74
|
+
export const declarationOf = (property, value) => {
|
|
75
|
+
switch (property) {
|
|
76
|
+
case "background":
|
|
77
|
+
return [`background-color: ${variable(value)}`];
|
|
78
|
+
case "text":
|
|
79
|
+
return [`color: ${variable(value)}`];
|
|
80
|
+
case "border":
|
|
81
|
+
return [value === "none" ? "border: none" : `border: ${stroke(value)}`];
|
|
82
|
+
case "radius":
|
|
83
|
+
return [`border-radius: ${value === "0" ? "0" : variable(value)}`];
|
|
84
|
+
case "padding":
|
|
85
|
+
return [`padding: ${(Array.isArray(value) ? value : [value]).map((each) => (each === "0" ? "0" : variable(each))).join(" ")}`];
|
|
86
|
+
case "gap":
|
|
87
|
+
return [`gap: ${value === "0" ? "0" : variable(value)}`];
|
|
88
|
+
case "min-size":
|
|
89
|
+
return [`min-height: ${variable(value)}`, `min-width: ${variable(value)}`];
|
|
90
|
+
case "type": {
|
|
91
|
+
const name = propertyName(value);
|
|
92
|
+
return [
|
|
93
|
+
`font-family: var(${name}-family)`,
|
|
94
|
+
`font-size: var(${name}-size)`,
|
|
95
|
+
`font-weight: var(${name}-weight)`,
|
|
96
|
+
`line-height: var(${name}-line-height)`,
|
|
97
|
+
`letter-spacing: var(${name}-letter-spacing)`,
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
case "elevation":
|
|
101
|
+
return [value === "none" ? "box-shadow: none" : `box-shadow: ${variable(value)}`];
|
|
102
|
+
case "opacity":
|
|
103
|
+
return [`opacity: ${value}`];
|
|
104
|
+
case "outline": {
|
|
105
|
+
if (value === "none") {
|
|
106
|
+
return ["outline: none"];
|
|
107
|
+
}
|
|
108
|
+
const o = value;
|
|
109
|
+
return [`outline: ${stroke(o)}`, ...(typeof o["offset"] === "string" ? [`outline-offset: ${variable(o["offset"])}`] : [])];
|
|
110
|
+
}
|
|
111
|
+
case "align":
|
|
112
|
+
return [`align-items: ${value}`];
|
|
113
|
+
default:
|
|
114
|
+
throw new Error(`no CSS for the property "${property}"`);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const declarations = (properties) => Object.entries(properties).flatMap(([property, value]) => declarationOf(property, value));
|
|
118
|
+
const block = (selector, lines, indent = " ") => lines.length === 0 ? "" : `${indent}${selector} {\n${lines.map((line) => `${indent} ${line};`).join("\n")}\n${indent}}\n`;
|
|
119
|
+
const same = (a, b) => JSON.stringify(a?.value) === JSON.stringify(b?.value);
|
|
120
|
+
/** The stylesheet of a theme whose problems are none. */
|
|
121
|
+
export const compileWeb = (loaded, inputs) => {
|
|
122
|
+
const out = [
|
|
123
|
+
`/* Generated by @valancex/style from the theme "${loaded.theme.name}". Do not edit: it is rewritten whenever the theme changes. */\n`,
|
|
124
|
+
"@layer vlx-library, vlx-theme, vlx-app;\n\n",
|
|
125
|
+
"@layer vlx-theme {\n",
|
|
126
|
+
];
|
|
127
|
+
const modes = Object.keys(loaded.modes);
|
|
128
|
+
const light = loaded.modes["light"];
|
|
129
|
+
// tokens: those that are the same in every mode once, in :root; the others per mode
|
|
130
|
+
const varying = new Set([...light.keys()].filter((path) => modes.some((mode) => !same(light.get(path), loaded.modes[mode].get(path)))));
|
|
131
|
+
const props = (tokens, only) => [...tokens.values()].filter((token) => only(token.path)).flatMap((token) => propertiesOf(token).map(([name, value]) => `${name}: ${value}`));
|
|
132
|
+
out.push(block(":root", [...props(light, (path) => !varying.has(path)), ...props(light, (path) => varying.has(path))]));
|
|
133
|
+
for (const mode of modes.filter((each) => each !== "light")) {
|
|
134
|
+
const condition = inputs.contract.modeConditions?.[mode];
|
|
135
|
+
if (condition !== undefined) {
|
|
136
|
+
const [name, value] = condition.split(":");
|
|
137
|
+
const query = inputs.contract.conditions[name]?.web?.[value];
|
|
138
|
+
out.push(` @media ${query} {\n${block(":root", props(loaded.modes[mode], (path) => varying.has(path)), " ")} }\n`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const mode of modes) {
|
|
142
|
+
out.push(block(`[data-theme="${mode}"]`, props(loaded.modes[mode], (path) => varying.has(path))));
|
|
143
|
+
}
|
|
144
|
+
// the page under a modal layer (I14): it does not scroll behind it, and the scrollbar's width stays so the content does not shift; the layer itself does not chain its scroll to the page
|
|
145
|
+
out.push("}\n\n@layer vlx-library {\n");
|
|
146
|
+
out.push(block(":root:has(:modal)", ["overflow: hidden", "scrollbar-gutter: stable"]));
|
|
147
|
+
out.push(block(":modal", ["overscroll-behavior: contain"]));
|
|
148
|
+
out.push("}\n\n@layer vlx-theme {\n");
|
|
149
|
+
// recipes, by primitive
|
|
150
|
+
for (const { recipe } of [...loaded.recipes].sort((a, b) => a.recipe.target.primitive.localeCompare(b.recipe.target.primitive))) {
|
|
151
|
+
const primitive = recipe.target.primitive;
|
|
152
|
+
const hook = hookOf(inputs, primitive);
|
|
153
|
+
if (hook === undefined) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const info = inputs.primitives[primitive];
|
|
157
|
+
out.push(`\n /* ${primitive} */\n`);
|
|
158
|
+
out.push(block(hook, declarations(recipe.base ?? {})));
|
|
159
|
+
for (const [variant, values] of Object.entries(recipe.variants ?? {})) {
|
|
160
|
+
const name = info.variants.find((each) => each.name === variant).attribute;
|
|
161
|
+
const defaulted = inputs.contract.variants[variant].default;
|
|
162
|
+
for (const [value, properties] of Object.entries(values)) {
|
|
163
|
+
const selector = value === defaulted ? `${hook}:is([${name}="${value}"], :not([${name}]))` : `${hook}[${name}="${value}"]`;
|
|
164
|
+
out.push(block(selector, declarations(properties)));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const interaction = inputs.contract.interactionStates;
|
|
168
|
+
const states = statesOf(inputs, primitive);
|
|
169
|
+
const stateSelector = (state) => interaction[state] ?? info.stateSelectors[state] ?? "";
|
|
170
|
+
for (const state of states) {
|
|
171
|
+
const properties = recipe.states?.[state];
|
|
172
|
+
if (properties !== undefined) {
|
|
173
|
+
const media = inputs.contract.interactionMedia?.[state];
|
|
174
|
+
out.push(media === undefined
|
|
175
|
+
? block(`${hook}${stateSelector(state)}`, declarations(properties))
|
|
176
|
+
: ` @media ${media} {\n${block(`${hook}${stateSelector(state)}`, declarations(properties), " ")} }\n`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const [condition, properties] of Object.entries(recipe.when ?? {})) {
|
|
180
|
+
const [name, value] = condition.split(":");
|
|
181
|
+
if (name === "target") {
|
|
182
|
+
if (value === "web") {
|
|
183
|
+
out.push(block(hook, declarations(properties)));
|
|
184
|
+
}
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const query = inputs.contract.conditions[name]?.web?.[value];
|
|
188
|
+
if (query !== undefined) {
|
|
189
|
+
out.push(` @media ${query} {\n${block(hook, declarations(properties), " ")} }\n`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (recipe.escape?.web !== undefined) {
|
|
193
|
+
out.push(block(hook, recipe.escape.web.css
|
|
194
|
+
.split(";")
|
|
195
|
+
.map((line) => line.trim())
|
|
196
|
+
.filter((line) => line !== "")));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
out.push("}\n");
|
|
200
|
+
return out.join("").replace(/\n{3,}/g, "\n\n");
|
|
201
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type Inputs } from "./check.js";
|
|
2
|
+
import type { LoadedTheme } from "./types.js";
|
|
3
|
+
export interface DesktopStyle {
|
|
4
|
+
background?: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
border?: {
|
|
7
|
+
width: number;
|
|
8
|
+
color: string;
|
|
9
|
+
} | "none";
|
|
10
|
+
radius?: number;
|
|
11
|
+
/** Top, right, bottom, left. */
|
|
12
|
+
padding?: [number, number, number, number];
|
|
13
|
+
gap?: number;
|
|
14
|
+
minSize?: number;
|
|
15
|
+
fontSize?: number;
|
|
16
|
+
fontWeight?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface DesktopRecipe {
|
|
19
|
+
base?: DesktopStyle;
|
|
20
|
+
variants?: Record<string, Record<string, DesktopStyle>>;
|
|
21
|
+
states?: Record<string, DesktopStyle>;
|
|
22
|
+
when?: Record<string, DesktopStyle>;
|
|
23
|
+
}
|
|
24
|
+
export interface DesktopTheme {
|
|
25
|
+
readonly version: 1;
|
|
26
|
+
readonly name: string;
|
|
27
|
+
/** Each variant's default value (the one that applies when a template does not say). */
|
|
28
|
+
readonly defaults: Record<string, string>;
|
|
29
|
+
/** Mode name to primitive name to its recipe. */
|
|
30
|
+
readonly modes: Record<string, Record<string, DesktopRecipe>>;
|
|
31
|
+
}
|
|
32
|
+
/** The theme as the Desktop is told it. */
|
|
33
|
+
export declare const resolveForDesktop: (loaded: LoadedTheme, inputs: Inputs) => DesktopTheme;
|
package/dist/desktop.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// The Desktop's realization of a theme: resolved values, not references. The Desktop draws its own pixels (egui) and keeps a typed map; it is told it once, in the `appearance` message it already has.
|
|
2
|
+
// Colors are `#rrggbb[aa]`, lengths are points (a rem is 16), and only what the Desktop honors is in it: a property it does not apply is left out here and listed in the coverage report, so the window never
|
|
3
|
+
// has to guess. A mode the Desktop cannot choose (high contrast is the person's setting; the window is not told it) is left out too.
|
|
4
|
+
import { toRgb } from "./color.js";
|
|
5
|
+
import { hookOf, statesOf } from "./check.js";
|
|
6
|
+
const byte = (value) => Math.round(Math.min(1, Math.max(0, value)) * 255)
|
|
7
|
+
.toString(16)
|
|
8
|
+
.padStart(2, "0");
|
|
9
|
+
const color = (token) => {
|
|
10
|
+
const rgb = toRgb(token.value);
|
|
11
|
+
return `#${byte(rgb.r)}${byte(rgb.g)}${byte(rgb.b)}${rgb.a === 1 ? "" : byte(rgb.a)}`;
|
|
12
|
+
};
|
|
13
|
+
const length = (token) => {
|
|
14
|
+
const { value, unit } = token.value;
|
|
15
|
+
return unit === "rem" ? value * 16 : value;
|
|
16
|
+
};
|
|
17
|
+
const resolveStyle = (declarations, tokens, inputs) => {
|
|
18
|
+
const out = {};
|
|
19
|
+
const token = (path) => tokens.get(path);
|
|
20
|
+
for (const [property, value] of Object.entries(declarations ?? {})) {
|
|
21
|
+
if (inputs.contract.properties[property]?.desktop === "unsupported") {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
switch (property) {
|
|
25
|
+
case "background":
|
|
26
|
+
out.background = color(token(value));
|
|
27
|
+
break;
|
|
28
|
+
case "text":
|
|
29
|
+
out.text = color(token(value));
|
|
30
|
+
break;
|
|
31
|
+
case "border": {
|
|
32
|
+
const stroke = value;
|
|
33
|
+
out.border = stroke === "none" ? "none" : { width: length(token(stroke.width)), color: color(token(stroke.color)) };
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
case "radius":
|
|
37
|
+
out.radius = value === "0" ? 0 : length(token(value));
|
|
38
|
+
break;
|
|
39
|
+
case "padding": {
|
|
40
|
+
const list = (Array.isArray(value) ? value : [value]).map((each) => (each === "0" ? 0 : length(token(each))));
|
|
41
|
+
const [a = 0, b = a, c = a, d = b] = list;
|
|
42
|
+
out.padding = [a, b, c, d];
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
case "gap":
|
|
46
|
+
out.gap = value === "0" ? 0 : length(token(value));
|
|
47
|
+
break;
|
|
48
|
+
case "min-size":
|
|
49
|
+
out.minSize = length(token(value));
|
|
50
|
+
break;
|
|
51
|
+
case "type": {
|
|
52
|
+
const type = token(value).value;
|
|
53
|
+
out.fontSize = type.fontSize.unit === "rem" ? type.fontSize.value * 16 : type.fontSize.value;
|
|
54
|
+
if (typeof type.fontWeight === "number") {
|
|
55
|
+
out.fontWeight = type.fontWeight;
|
|
56
|
+
}
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
default:
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
};
|
|
65
|
+
/** The theme as the Desktop is told it. */
|
|
66
|
+
export const resolveForDesktop = (loaded, inputs) => {
|
|
67
|
+
const modes = {};
|
|
68
|
+
for (const [mode, tokens] of Object.entries(loaded.modes)) {
|
|
69
|
+
const condition = inputs.contract.modeConditions?.[mode];
|
|
70
|
+
const kind = condition?.split(":")[0];
|
|
71
|
+
if (kind !== undefined && inputs.contract.conditions[kind]?.desktop === "unsupported") {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const recipes = {};
|
|
75
|
+
for (const { recipe } of loaded.recipes) {
|
|
76
|
+
const primitive = recipe.target.primitive;
|
|
77
|
+
if (hookOf(inputs, primitive) === undefined) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const out = {};
|
|
81
|
+
const base = resolveStyle(recipe.base, tokens, inputs);
|
|
82
|
+
// `target:desktop` is the Desktop's own refinement of the base; `target:web` is not for it
|
|
83
|
+
Object.assign(base, resolveStyle(recipe.when?.["target:desktop"], tokens, inputs));
|
|
84
|
+
out.base = base;
|
|
85
|
+
for (const [variant, values] of Object.entries(recipe.variants ?? {})) {
|
|
86
|
+
out.variants = {
|
|
87
|
+
...out.variants,
|
|
88
|
+
[variant]: Object.fromEntries(Object.entries(values).map(([value, declarations]) => [value, resolveStyle(declarations, tokens, inputs)])),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
for (const state of statesOf(inputs, primitive)) {
|
|
92
|
+
const style = inputs.contract.desktopStates?.[state] === "supported" ? resolveStyle(recipe.states?.[state], tokens, inputs) : {};
|
|
93
|
+
if (Object.keys(style).length > 0) {
|
|
94
|
+
out.states = { ...out.states, [state]: style };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const [condition, declarations] of Object.entries(recipe.when ?? {})) {
|
|
98
|
+
if (condition.startsWith("viewport:")) {
|
|
99
|
+
out.when = { ...out.when, [condition]: resolveStyle(declarations, tokens, inputs) };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
recipes[primitive] = out;
|
|
103
|
+
}
|
|
104
|
+
modes[mode] = recipes;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
version: 1,
|
|
108
|
+
name: loaded.theme.name,
|
|
109
|
+
defaults: Object.fromEntries(Object.entries(inputs.contract.variants).map(([name, definition]) => [name, definition.default])),
|
|
110
|
+
modes,
|
|
111
|
+
};
|
|
112
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type * from "./types.js";
|
|
2
|
+
export { loadContract, loadSchema, readJson, root } from "./load.js";
|
|
3
|
+
export { dtcgValidator, resolveDocument, validateDocument } from "./tokens.js";
|
|
4
|
+
export type { Resolved } from "./tokens.js";
|
|
5
|
+
export { toCss, toRgb } from "./color.js";
|
|
6
|
+
export type { DtcgColor, Rgb } from "./color.js";
|
|
7
|
+
export { composite, luminance, ratio } from "./contrast.js";
|
|
8
|
+
export { loadTheme, validateRecipe } from "./theme.js";
|
|
9
|
+
export { checkRecipe, checkTheme, contrastOf, defaultInputs, hookOf, statesOf } from "./check.js";
|
|
10
|
+
export { primitives } from "./primitives.js";
|
|
11
|
+
export type { Checked, ContrastResult, Inputs } from "./check.js";
|
|
12
|
+
export { didYouMean, nearest } from "./suggest.js";
|
|
13
|
+
export { compileWeb, declarationOf, propertiesOf, propertyName } from "./css.js";
|
|
14
|
+
export { coverageOf, styleable } from "./coverage.js";
|
|
15
|
+
export type { Coverage, NotApplied, Target } from "./coverage.js";
|
|
16
|
+
export { resolveForDesktop } from "./desktop.js";
|
|
17
|
+
export type { DesktopRecipe, DesktopStyle, DesktopTheme } from "./desktop.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { loadContract, loadSchema, readJson, root } from "./load.js";
|
|
2
|
+
export { dtcgValidator, resolveDocument, validateDocument } from "./tokens.js";
|
|
3
|
+
export { toCss, toRgb } from "./color.js";
|
|
4
|
+
export { composite, luminance, ratio } from "./contrast.js";
|
|
5
|
+
export { loadTheme, validateRecipe } from "./theme.js";
|
|
6
|
+
export { checkRecipe, checkTheme, contrastOf, defaultInputs, hookOf, statesOf } from "./check.js";
|
|
7
|
+
export { primitives } from "./primitives.js";
|
|
8
|
+
export { didYouMean, nearest } from "./suggest.js";
|
|
9
|
+
export { compileWeb, declarationOf, propertiesOf, propertyName } from "./css.js";
|
|
10
|
+
export { coverageOf, styleable } from "./coverage.js";
|
|
11
|
+
export { resolveForDesktop } from "./desktop.js";
|
package/dist/load.d.ts
ADDED
package/dist/load.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Where the package's data is: the directory that holds `contract/`, `schema/` and `themes/` (one level above `dist/` or `src/`).
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
export const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
|
+
export const readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
7
|
+
export const loadContract = () => readJson(join(root, "contract", "style-contract-v1.json"));
|
|
8
|
+
export const loadSchema = (name) => readJson(join(root, "schema", name));
|