baseline-foundry 0.1.3
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 +588 -0
- package/config/experiments/ibm-plex-engine-smoke.json +115 -0
- package/config/experiments/ubuntu-engine-smoke.json +115 -0
- package/config/foundation-theme.json +117 -0
- package/config/tiers/app.json +120 -0
- package/config/tiers/documentation.json +120 -0
- package/config/tiers/editorial.json +120 -0
- package/config/tiers/os.json +120 -0
- package/dist/accordion.d.ts +5 -0
- package/dist/accordion.js +32 -0
- package/dist/application-layout.d.ts +4 -0
- package/dist/application-layout.js +181 -0
- package/dist/baseline-grid-overlay.d.ts +5 -0
- package/dist/baseline-grid-overlay.js +50 -0
- package/dist/baseline-grid-theme.d.ts +3 -0
- package/dist/baseline-grid-theme.js +3 -0
- package/dist/baseline-grid.d.ts +7 -0
- package/dist/baseline-grid.js +35 -0
- package/dist/build.d.ts +21 -0
- package/dist/build.js +440 -0
- package/dist/code-snippet.d.ts +6 -0
- package/dist/code-snippet.js +95 -0
- package/dist/contextual-menu.d.ts +4 -0
- package/dist/contextual-menu.js +156 -0
- package/dist/css-app-tier.d.ts +1 -0
- package/dist/css-app-tier.js +77 -0
- package/dist/css-components/article-pagination.d.ts +13 -0
- package/dist/css-components/article-pagination.js +151 -0
- package/dist/css-components/button-actions.d.ts +7 -0
- package/dist/css-components/button-actions.js +165 -0
- package/dist/css-components/cards-options.d.ts +6 -0
- package/dist/css-components/cards-options.js +180 -0
- package/dist/css-components/chip-badge-status.d.ts +10 -0
- package/dist/css-components/chip-badge-status.js +162 -0
- package/dist/css-components/content-card.d.ts +25 -0
- package/dist/css-components/content-card.js +517 -0
- package/dist/css-components/control-row.d.ts +2 -0
- package/dist/css-components/control-row.js +18 -0
- package/dist/css-components/cta-figure-aspect.d.ts +1 -0
- package/dist/css-components/cta-figure-aspect.js +123 -0
- package/dist/css-components/document-navigation.d.ts +13 -0
- package/dist/css-components/document-navigation.js +400 -0
- package/dist/css-components/editorial-content.d.ts +6 -0
- package/dist/css-components/editorial-content.js +52 -0
- package/dist/css-components/icon.d.ts +1 -0
- package/dist/css-components/icon.js +75 -0
- package/dist/css-components/interactive-feedback.d.ts +2 -0
- package/dist/css-components/interactive-feedback.js +230 -0
- package/dist/css-components/interactive-tables.d.ts +7 -0
- package/dist/css-components/interactive-tables.js +164 -0
- package/dist/css-components/linked-logo-site-layout.d.ts +21 -0
- package/dist/css-components/linked-logo-site-layout.js +214 -0
- package/dist/css-components/list-tree.d.ts +5 -0
- package/dist/css-components/list-tree.js +98 -0
- package/dist/css-components/list.d.ts +3 -0
- package/dist/css-components/list.js +119 -0
- package/dist/css-components/logo-media.d.ts +16 -0
- package/dist/css-components/logo-media.js +219 -0
- package/dist/css-components/navigation-layout.d.ts +5 -0
- package/dist/css-components/navigation-layout.js +151 -0
- package/dist/css-components/panel.d.ts +6 -0
- package/dist/css-components/panel.js +148 -0
- package/dist/css-components/search-box-and-filter.d.ts +9 -0
- package/dist/css-components/search-box-and-filter.js +267 -0
- package/dist/css-components/sites-editorial-ports.d.ts +19 -0
- package/dist/css-components/sites-editorial-ports.js +264 -0
- package/dist/css-components/sites-foundation.d.ts +17 -0
- package/dist/css-components/sites-foundation.js +173 -0
- package/dist/css-components/sites-rich-lists.d.ts +18 -0
- package/dist/css-components/sites-rich-lists.js +364 -0
- package/dist/css-components/static-content-ports.d.ts +14 -0
- package/dist/css-components/static-content-ports.js +206 -0
- package/dist/css-components/tab-section.d.ts +19 -0
- package/dist/css-components/tab-section.js +103 -0
- package/dist/css-components/table.d.ts +7 -0
- package/dist/css-components/table.js +79 -0
- package/dist/css-components/tabs-choice-breadcrumbs.d.ts +9 -0
- package/dist/css-components/tabs-choice-breadcrumbs.js +307 -0
- package/dist/css-components/tiered-list-equal-height-row.d.ts +1 -0
- package/dist/css-components/tiered-list-equal-height-row.js +286 -0
- package/dist/css-components.d.ts +2 -0
- package/dist/css-components.js +2701 -0
- package/dist/css-grid.d.ts +1 -0
- package/dist/css-grid.js +179 -0
- package/dist/css.d.ts +6 -0
- package/dist/css.js +431 -0
- package/dist/experiments/ibm-plex-engine-smoke/styles.css +8969 -0
- package/dist/experiments/ibm-plex-engine-smoke/surfaces.json +604 -0
- package/dist/experiments/ibm-plex-engine-smoke/tokens.json +217 -0
- package/dist/in-page-navigation.d.ts +7 -0
- package/dist/in-page-navigation.js +136 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +18 -0
- package/dist/interactive-feedback.d.ts +14 -0
- package/dist/interactive-feedback.js +96 -0
- package/dist/interactive-tables.d.ts +28 -0
- package/dist/interactive-tables.js +184 -0
- package/dist/list-tree.d.ts +4 -0
- package/dist/list-tree.js +44 -0
- package/dist/panel-drawer.d.ts +4 -0
- package/dist/panel-drawer.js +171 -0
- package/dist/presets/app-tier/styles.css +9459 -0
- package/dist/presets/app-tier/surfaces.json +1225 -0
- package/dist/presets/app-tier/tokens.json +221 -0
- package/dist/presets/prose/styles.css +9442 -0
- package/dist/presets/prose/surfaces.json +1225 -0
- package/dist/presets/prose/tokens.json +221 -0
- package/dist/presets.d.ts +6 -0
- package/dist/presets.js +27 -0
- package/dist/range-controls.d.ts +9 -0
- package/dist/range-controls.js +73 -0
- package/dist/resizable-aside.d.ts +4 -0
- package/dist/resizable-aside.js +328 -0
- package/dist/side-navigation.d.ts +4 -0
- package/dist/side-navigation.js +219 -0
- package/dist/styles.css +9442 -0
- package/dist/surfaces.json +1225 -0
- package/dist/tabs.d.ts +4 -0
- package/dist/tabs.js +88 -0
- package/dist/tier-registry.d.ts +10 -0
- package/dist/tier-registry.js +25 -0
- package/dist/tiers/app/styles.css +9459 -0
- package/dist/tiers/app/surfaces.json +1225 -0
- package/dist/tiers/app/tokens.json +221 -0
- package/dist/tiers/documentation/styles.css +9442 -0
- package/dist/tiers/documentation/surfaces.json +1225 -0
- package/dist/tiers/documentation/tokens.json +221 -0
- package/dist/tiers/editorial/styles.css +9442 -0
- package/dist/tiers/editorial/surfaces.json +1225 -0
- package/dist/tiers/editorial/tokens.json +221 -0
- package/dist/tiers/os/styles.css +9442 -0
- package/dist/tiers/os/surfaces.json +1225 -0
- package/dist/tiers/os/tokens.json +221 -0
- package/dist/tokens.json +221 -0
- package/dist/tooltip.d.ts +4 -0
- package/dist/tooltip.js +70 -0
- package/dist/top-navigation.d.ts +4 -0
- package/dist/top-navigation.js +354 -0
- package/dist/types.d.ts +155 -0
- package/dist/types.js +1 -0
- package/dist/vanilla-theme-colors.d.ts +5 -0
- package/dist/vanilla-theme-colors.js +208 -0
- package/docs/publishing.md +66 -0
- package/package.json +108 -0
package/dist/build.js
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { generateFoundryCss } from "./css.js";
|
|
4
|
+
import { normalizeBuiltInThemeName, presetNames, resolveBuiltInThemePath, resolvePresetPath, resolveTierPath, tierNames } from "./presets.js";
|
|
5
|
+
function parseRem(remValue) {
|
|
6
|
+
return Number.parseFloat(remValue.replace("rem", ""));
|
|
7
|
+
}
|
|
8
|
+
function roundRemValue(value) {
|
|
9
|
+
return Math.round(value * 100000) / 100000;
|
|
10
|
+
}
|
|
11
|
+
function toRem(value) {
|
|
12
|
+
return `${roundRemValue(value)}rem`;
|
|
13
|
+
}
|
|
14
|
+
async function ensureDirectory(dirPath) {
|
|
15
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
async function emptyDirectory(dirPath) {
|
|
18
|
+
await ensureDirectory(dirPath);
|
|
19
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
await emptyDirectory(entryPath);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
await fs.rm(entryPath, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function writeJsonFileAtomic(filePath, value) {
|
|
30
|
+
const tempFilePath = `${filePath}.tmp`;
|
|
31
|
+
await fs.writeFile(tempFilePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
32
|
+
await fs.rm(filePath, { force: true });
|
|
33
|
+
await fs.rename(tempFilePath, filePath);
|
|
34
|
+
}
|
|
35
|
+
function assertFiniteNumberConfigField(value, fieldPath) {
|
|
36
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
37
|
+
throw new Error(`Theme config field "${fieldPath}" must be a finite number.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const REQUIRED_LAYOUT_FIELDS = [
|
|
41
|
+
"contentMaxWidthRem",
|
|
42
|
+
"contentPaddingInlineRem",
|
|
43
|
+
"measureRem",
|
|
44
|
+
"sectionSpaceBaselineUnits",
|
|
45
|
+
"sectionSpaceShallowBaselineUnits",
|
|
46
|
+
"sectionSpaceDeepBaselineUnits",
|
|
47
|
+
"stripSpaceBaselineUnits",
|
|
48
|
+
"gridGapInlineBaselineUnits",
|
|
49
|
+
"gridGapBlockBaselineUnits",
|
|
50
|
+
"pageMarginBaselineUnits"
|
|
51
|
+
];
|
|
52
|
+
const REQUIRED_COMPONENT_FIELDS = [
|
|
53
|
+
"borderWidthPx",
|
|
54
|
+
"radiusRem",
|
|
55
|
+
"controlBlockPaddingRem",
|
|
56
|
+
"controlInlinePaddingRem",
|
|
57
|
+
"controlVisualSizeRem",
|
|
58
|
+
"fieldGapBaselineUnits",
|
|
59
|
+
"panelPaddingInlineBaselineUnits",
|
|
60
|
+
"panelPaddingBlockBaselineUnits",
|
|
61
|
+
"accordionIndentBaselineUnits"
|
|
62
|
+
];
|
|
63
|
+
const OPTIONAL_COMPONENT_FIELDS = [
|
|
64
|
+
"controlCompactBlockPaddingRem",
|
|
65
|
+
"controlInlinePaddingActionRem",
|
|
66
|
+
"controlInlinePaddingFieldRem"
|
|
67
|
+
];
|
|
68
|
+
function assertNoDuplicateRoleKeys(raw, configPath) {
|
|
69
|
+
const rolesMatch = raw.match(/"roles"\s*:\s*\{([\s\S]*?)\}/);
|
|
70
|
+
if (!rolesMatch) {
|
|
71
|
+
throw new Error(`Theme config "${configPath}" requires a roles object.`);
|
|
72
|
+
}
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
for (const match of rolesMatch[1].matchAll(/^\s*"([^"]+)"\s*:/gm)) {
|
|
75
|
+
const roleName = match[1];
|
|
76
|
+
if (seen.has(roleName)) {
|
|
77
|
+
throw new Error(`Theme config "${configPath}" contains duplicate role key "${roleName}".`);
|
|
78
|
+
}
|
|
79
|
+
seen.add(roleName);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function validateConfig(config) {
|
|
83
|
+
if (!config.fontFiles.length) {
|
|
84
|
+
throw new Error("Theme config requires at least one font file.");
|
|
85
|
+
}
|
|
86
|
+
if (!config.fontFiles.some(fontFile => !fontFile.runtimeOnly)) {
|
|
87
|
+
throw new Error("Theme config requires at least one build-time font file.");
|
|
88
|
+
}
|
|
89
|
+
if (!config.elements.length) {
|
|
90
|
+
throw new Error("Theme config requires at least one typography element.");
|
|
91
|
+
}
|
|
92
|
+
if (!config.roles.body) {
|
|
93
|
+
throw new Error('Theme config requires a "body" role.');
|
|
94
|
+
}
|
|
95
|
+
if (!config.components) {
|
|
96
|
+
throw new Error("Theme config requires a components block.");
|
|
97
|
+
}
|
|
98
|
+
assertFiniteNumberConfigField(config.baselineUnit, "baselineUnit");
|
|
99
|
+
if (config.baselineUnit <= 0) {
|
|
100
|
+
throw new Error('Theme config field "baselineUnit" must be greater than zero.');
|
|
101
|
+
}
|
|
102
|
+
for (const field of REQUIRED_LAYOUT_FIELDS) {
|
|
103
|
+
assertFiniteNumberConfigField(config.layout[field], `layout.${field}`);
|
|
104
|
+
}
|
|
105
|
+
for (const field of REQUIRED_COMPONENT_FIELDS) {
|
|
106
|
+
assertFiniteNumberConfigField(config.components[field], `components.${field}`);
|
|
107
|
+
}
|
|
108
|
+
for (const field of OPTIONAL_COMPONENT_FIELDS) {
|
|
109
|
+
const value = config.components[field];
|
|
110
|
+
if (value !== undefined) {
|
|
111
|
+
assertFiniteNumberConfigField(value, `components.${field}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const elementIdentifiers = new Set();
|
|
115
|
+
for (const [index, element] of config.elements.entries()) {
|
|
116
|
+
if (elementIdentifiers.has(element.identifier)) {
|
|
117
|
+
throw new Error(`Theme config contains duplicate element identifier "${element.identifier}".`);
|
|
118
|
+
}
|
|
119
|
+
elementIdentifiers.add(element.identifier);
|
|
120
|
+
assertFiniteNumberConfigField(element.fontSize, `elements[${index}].fontSize`);
|
|
121
|
+
assertFiniteNumberConfigField(element.lineHeight, `elements[${index}].lineHeight`);
|
|
122
|
+
assertFiniteNumberConfigField(element.spaceAfter, `elements[${index}].spaceAfter`);
|
|
123
|
+
if (element.fontWeight !== undefined) {
|
|
124
|
+
assertFiniteNumberConfigField(element.fontWeight, `elements[${index}].fontWeight`);
|
|
125
|
+
}
|
|
126
|
+
if (element.fontSize <= 0 || element.lineHeight <= 0) {
|
|
127
|
+
throw new Error(`Theme element "${element.identifier}" requires positive fontSize and lineHeight values.`);
|
|
128
|
+
}
|
|
129
|
+
if (element.spaceAfter < 1) {
|
|
130
|
+
throw new Error(`Theme element "${element.identifier}" requires spaceAfter >= 1 baseline unit so semantic margin is non-negative.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
for (const [roleName, identifier] of Object.entries(config.roles)) {
|
|
134
|
+
if (!elementIdentifiers.has(identifier)) {
|
|
135
|
+
throw new Error(`Role "${roleName}" points to missing element "${identifier}".`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export async function readThemeConfig(configPath) {
|
|
140
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
141
|
+
assertNoDuplicateRoleKeys(raw, configPath);
|
|
142
|
+
const config = JSON.parse(raw);
|
|
143
|
+
validateConfig(config);
|
|
144
|
+
return config;
|
|
145
|
+
}
|
|
146
|
+
function createBaselineConfig(config, sourceConfigPath, baselineConfigPath) {
|
|
147
|
+
const sourceConfigDir = path.dirname(sourceConfigPath);
|
|
148
|
+
const baselineConfigDir = path.dirname(baselineConfigPath);
|
|
149
|
+
return {
|
|
150
|
+
baselineUnit: config.baselineUnit,
|
|
151
|
+
fontFiles: config.fontFiles.filter(fontFile => !fontFile.runtimeOnly).map(fontFile => {
|
|
152
|
+
const absoluteFontPath = path.resolve(sourceConfigDir, fontFile.path);
|
|
153
|
+
const relativeFontPath = path.relative(baselineConfigDir, absoluteFontPath).replace(/\\/g, "/");
|
|
154
|
+
return {
|
|
155
|
+
family: fontFile.family,
|
|
156
|
+
path: relativeFontPath
|
|
157
|
+
};
|
|
158
|
+
}),
|
|
159
|
+
elements: config.elements.map((element) => ({
|
|
160
|
+
identifier: element.identifier,
|
|
161
|
+
fontSize: element.fontSize,
|
|
162
|
+
lineHeight: element.lineHeight,
|
|
163
|
+
spaceAfter: element.spaceAfter,
|
|
164
|
+
fontFamily: element.fontFamily,
|
|
165
|
+
fontWeight: element.fontWeight,
|
|
166
|
+
fontStyle: element.fontStyle
|
|
167
|
+
}))
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function createRuntimeFontFiles(config, sourceConfigPath, outputDir) {
|
|
171
|
+
const sourceConfigDir = path.dirname(sourceConfigPath);
|
|
172
|
+
return config.fontFiles.map(fontFile => {
|
|
173
|
+
const absoluteFontPath = path.resolve(sourceConfigDir, fontFile.path);
|
|
174
|
+
const relativeFontPath = path.relative(outputDir, absoluteFontPath).replace(/\\/g, "/");
|
|
175
|
+
return {
|
|
176
|
+
...fontFile,
|
|
177
|
+
path: relativeFontPath
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
async function generateBaselineTokens(baselineConfigPath, outputDir) {
|
|
182
|
+
const moduleNamespace = await import("@lyubomir-popov/baseline-nudge-generator");
|
|
183
|
+
const generateFromConfig = (moduleNamespace.generateFromConfig ?? moduleNamespace.default?.generateFromConfig);
|
|
184
|
+
if (!generateFromConfig) {
|
|
185
|
+
throw new Error("Unable to load generateFromConfig from @lyubomir-popov/baseline-nudge-generator.");
|
|
186
|
+
}
|
|
187
|
+
await generateFromConfig(baselineConfigPath, outputDir);
|
|
188
|
+
const tokensPath = path.join(outputDir, "tokens.json");
|
|
189
|
+
const raw = await fs.readFile(tokensPath, "utf8");
|
|
190
|
+
return JSON.parse(raw);
|
|
191
|
+
}
|
|
192
|
+
function toTypographyToken(identifier, token, config) {
|
|
193
|
+
const elementConfig = config.elements.find(element => element.identifier === identifier);
|
|
194
|
+
const fontFamily = token.fontFamily ?? config.fontFiles[0]?.family ?? "sans";
|
|
195
|
+
const fontStack = config.fontStacks[fontFamily] ?? fontFamily;
|
|
196
|
+
const marginBottom = toRem(config.baselineUnit - parseRem(token.nudgeTop));
|
|
197
|
+
return {
|
|
198
|
+
...token,
|
|
199
|
+
identifier,
|
|
200
|
+
fontFamily,
|
|
201
|
+
fontStack,
|
|
202
|
+
marginBottom,
|
|
203
|
+
fontVariantCaps: elementConfig?.fontVariantCaps,
|
|
204
|
+
letterSpacing: elementConfig?.letterSpacing,
|
|
205
|
+
textTransform: elementConfig?.textTransform
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function buildComponentTokens(config) {
|
|
209
|
+
const controlInlinePaddingActionRem = config.components.controlInlinePaddingActionRem ?? config.components.controlInlinePaddingRem;
|
|
210
|
+
const controlInlinePaddingFieldRem = config.components.controlInlinePaddingFieldRem ?? (controlInlinePaddingActionRem / 2);
|
|
211
|
+
return {
|
|
212
|
+
borderWidth: `${config.components.borderWidthPx}px`,
|
|
213
|
+
// Vanilla's shared $bar-thickness: 3px at a 16px root, expressed in rem
|
|
214
|
+
// so emphasis bars scale with root text sizing while thin borders do not.
|
|
215
|
+
barThickness: toRem(3 / 16),
|
|
216
|
+
radius: toRem(config.components.radiusRem),
|
|
217
|
+
controlBlockPadding: toRem(config.components.controlBlockPaddingRem),
|
|
218
|
+
controlCompactBlockPadding: toRem(config.components.controlCompactBlockPaddingRem ?? config.components.controlBlockPaddingRem),
|
|
219
|
+
controlInlinePadding: toRem(controlInlinePaddingActionRem),
|
|
220
|
+
controlInlinePaddingAction: toRem(controlInlinePaddingActionRem),
|
|
221
|
+
controlInlinePaddingField: toRem(controlInlinePaddingFieldRem),
|
|
222
|
+
controlVisualSize: toRem(config.components.controlVisualSizeRem),
|
|
223
|
+
fieldGap: toRem(config.components.fieldGapBaselineUnits * config.baselineUnit),
|
|
224
|
+
panelPaddingInline: toRem(config.components.panelPaddingInlineBaselineUnits * config.baselineUnit),
|
|
225
|
+
panelPaddingBlock: toRem(config.components.panelPaddingBlockBaselineUnits * config.baselineUnit),
|
|
226
|
+
accordionIndent: toRem(config.components.accordionIndentBaselineUnits * config.baselineUnit)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function buildThemeTokens(config, baselineTokens) {
|
|
230
|
+
const elements = Object.fromEntries(Object.entries(baselineTokens.elements).map(([identifier, token]) => [identifier, toTypographyToken(identifier, token, config)]));
|
|
231
|
+
const roles = Object.fromEntries(Object.entries(config.roles).map(([roleName, identifier]) => {
|
|
232
|
+
const roleToken = elements[identifier];
|
|
233
|
+
if (!roleToken) {
|
|
234
|
+
throw new Error(`Missing generated token for role "${roleName}" / identifier "${identifier}".`);
|
|
235
|
+
}
|
|
236
|
+
return [roleName, roleToken];
|
|
237
|
+
}));
|
|
238
|
+
return {
|
|
239
|
+
baselineUnit: baselineTokens.baselineUnit,
|
|
240
|
+
fontFiles: config.fontFiles,
|
|
241
|
+
fontStacks: config.fontStacks,
|
|
242
|
+
roles,
|
|
243
|
+
elements,
|
|
244
|
+
layout: {
|
|
245
|
+
contentMaxWidth: toRem(config.layout.contentMaxWidthRem),
|
|
246
|
+
contentPaddingInline: toRem(config.layout.contentPaddingInlineRem),
|
|
247
|
+
measure: toRem(config.layout.measureRem),
|
|
248
|
+
sectionSpace: toRem(config.layout.sectionSpaceBaselineUnits * config.baselineUnit),
|
|
249
|
+
sectionSpaceShallow: toRem(config.layout.sectionSpaceShallowBaselineUnits * config.baselineUnit),
|
|
250
|
+
sectionSpaceDeep: toRem(config.layout.sectionSpaceDeepBaselineUnits * config.baselineUnit),
|
|
251
|
+
stripSpace: toRem(config.layout.stripSpaceBaselineUnits * config.baselineUnit),
|
|
252
|
+
gridGapInline: toRem(config.layout.gridGapInlineBaselineUnits * config.baselineUnit),
|
|
253
|
+
gridGapBlock: toRem(config.layout.gridGapBlockBaselineUnits * config.baselineUnit),
|
|
254
|
+
pageMargin: toRem(config.layout.pageMarginBaselineUnits * config.baselineUnit)
|
|
255
|
+
},
|
|
256
|
+
components: buildComponentTokens(config)
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function inferBuiltInPresetName(resolvedConfigPath) {
|
|
260
|
+
for (const tierName of tierNames) {
|
|
261
|
+
if (resolvedConfigPath === resolveTierPath(tierName)) {
|
|
262
|
+
return tierName;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (const presetName of presetNames) {
|
|
266
|
+
if (resolvedConfigPath === resolveBuiltInThemePath(presetName)) {
|
|
267
|
+
return presetName;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
function inferSurfaceName(resolvedConfigPath) {
|
|
273
|
+
const builtInName = inferBuiltInPresetName(resolvedConfigPath);
|
|
274
|
+
if (builtInName) {
|
|
275
|
+
return normalizeBuiltInThemeName(builtInName);
|
|
276
|
+
}
|
|
277
|
+
return path.parse(resolvedConfigPath).name;
|
|
278
|
+
}
|
|
279
|
+
function surfaceClassName(surfaceName) {
|
|
280
|
+
return tierNames.includes(surfaceName) ? `bf-tier-${surfaceName}` : undefined;
|
|
281
|
+
}
|
|
282
|
+
function runtimeMetricsTokens(baselineTokens, runtimeFontFiles) {
|
|
283
|
+
const metricFamilies = new Set(Object.values(baselineTokens.elements)
|
|
284
|
+
.map(token => token.fontFamily)
|
|
285
|
+
.filter((family) => typeof family === "string"));
|
|
286
|
+
return {
|
|
287
|
+
...baselineTokens,
|
|
288
|
+
fontFiles: runtimeFontFiles
|
|
289
|
+
.filter(fontFile => !fontFile.runtimeOnly)
|
|
290
|
+
.filter(fontFile => metricFamilies.size === 0 || metricFamilies.has(fontFile.family))
|
|
291
|
+
.map(fontFile => ({ ...fontFile }))
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function buildSurfaceManifest(defaultSurface, surfaces) {
|
|
295
|
+
return {
|
|
296
|
+
defaultSurface,
|
|
297
|
+
surfaces: Object.fromEntries(surfaces.map(surface => [surface.name, {
|
|
298
|
+
label: surface.label,
|
|
299
|
+
className: surface.className,
|
|
300
|
+
engine: surface.engine,
|
|
301
|
+
tokens: surface.tokens,
|
|
302
|
+
metrics: surface.metrics
|
|
303
|
+
}]))
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
async function buildThemeSurface(name, resolvedConfigPath, baselineDir, outputDir, options = {}) {
|
|
307
|
+
const config = await readThemeConfig(resolvedConfigPath);
|
|
308
|
+
const resolvedBaselineDir = path.resolve(baselineDir);
|
|
309
|
+
const resolvedOutputDir = path.resolve(outputDir);
|
|
310
|
+
await ensureDirectory(resolvedBaselineDir);
|
|
311
|
+
const baselineConfigFileName = `${path.parse(resolvedConfigPath).name}.baseline.json`;
|
|
312
|
+
const baselineConfigPath = path.join(resolvedBaselineDir, baselineConfigFileName);
|
|
313
|
+
await writeJsonFileAtomic(baselineConfigPath, createBaselineConfig(config, resolvedConfigPath, baselineConfigPath));
|
|
314
|
+
const baselineTokens = await generateBaselineTokens(baselineConfigPath, resolvedBaselineDir);
|
|
315
|
+
const runtimeConfig = {
|
|
316
|
+
...config,
|
|
317
|
+
fontFiles: createRuntimeFontFiles(config, resolvedConfigPath, resolvedOutputDir)
|
|
318
|
+
};
|
|
319
|
+
return {
|
|
320
|
+
name,
|
|
321
|
+
label: options.label,
|
|
322
|
+
className: options.className,
|
|
323
|
+
engine: options.engine ?? "metrics-compensated",
|
|
324
|
+
configPath: resolvedConfigPath,
|
|
325
|
+
baselineConfigPath,
|
|
326
|
+
baselineTokensPath: path.join(resolvedBaselineDir, "tokens.json"),
|
|
327
|
+
tokens: buildThemeTokens(runtimeConfig, baselineTokens),
|
|
328
|
+
metrics: runtimeMetricsTokens(baselineTokens, runtimeConfig.fontFiles)
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
async function buildRelatedTierSurfaces(resolvedConfigPath, currentSurfaceName, baselineDir, outputDir) {
|
|
332
|
+
if (!inferBuiltInPresetName(resolvedConfigPath)) {
|
|
333
|
+
return [];
|
|
334
|
+
}
|
|
335
|
+
const surfaces = [];
|
|
336
|
+
for (const tierName of tierNames) {
|
|
337
|
+
if (tierName === currentSurfaceName) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
surfaces.push(await buildThemeSurface(tierName, resolveTierPath(tierName), path.join(baselineDir, "surfaces", tierName), outputDir, {
|
|
341
|
+
className: `bf-tier-${tierName}`
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
return surfaces;
|
|
345
|
+
}
|
|
346
|
+
async function buildAdditionalThemeSurfaces(surfaceConfigs, baselineDir, outputDir) {
|
|
347
|
+
const surfaces = [];
|
|
348
|
+
for (const surfaceConfig of surfaceConfigs) {
|
|
349
|
+
surfaces.push(await buildThemeSurface(surfaceConfig.name, path.resolve(surfaceConfig.configPath), path.join(baselineDir, "surfaces", surfaceConfig.name), outputDir, {
|
|
350
|
+
className: surfaceConfig.className,
|
|
351
|
+
label: surfaceConfig.label
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
return surfaces;
|
|
355
|
+
}
|
|
356
|
+
function assertUniqueSurfaceNames(surfaces) {
|
|
357
|
+
const names = new Set();
|
|
358
|
+
for (const surface of surfaces) {
|
|
359
|
+
if (names.has(surface.name)) {
|
|
360
|
+
throw new Error(`Duplicate surface name "${surface.name}" in build output.`);
|
|
361
|
+
}
|
|
362
|
+
names.add(surface.name);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function buildTheme(resolvedConfigPath, distDir, baselineDir, options = {}) {
|
|
366
|
+
const resolvedDistDir = path.resolve(distDir);
|
|
367
|
+
const resolvedBaselineDir = path.resolve(baselineDir);
|
|
368
|
+
await ensureDirectory(resolvedDistDir);
|
|
369
|
+
await emptyDirectory(resolvedBaselineDir);
|
|
370
|
+
const builtInName = inferBuiltInPresetName(resolvedConfigPath);
|
|
371
|
+
const defaultSurfaceName = inferSurfaceName(resolvedConfigPath);
|
|
372
|
+
const defaultSurface = await buildThemeSurface(defaultSurfaceName, resolvedConfigPath, resolvedBaselineDir, resolvedDistDir, {
|
|
373
|
+
label: options.surfaceLabel,
|
|
374
|
+
className: surfaceClassName(defaultSurfaceName)
|
|
375
|
+
});
|
|
376
|
+
const relatedSurfaces = await buildRelatedTierSurfaces(resolvedConfigPath, defaultSurfaceName, resolvedBaselineDir, resolvedDistDir);
|
|
377
|
+
const additionalSurfaces = await buildAdditionalThemeSurfaces(options.additionalSurfaces ?? [], resolvedBaselineDir, resolvedDistDir);
|
|
378
|
+
const surfaces = [defaultSurface, ...relatedSurfaces, ...additionalSurfaces];
|
|
379
|
+
assertUniqueSurfaceNames(surfaces);
|
|
380
|
+
const surfaceManifest = buildSurfaceManifest(defaultSurfaceName, surfaces);
|
|
381
|
+
const css = generateFoundryCss(defaultSurface.tokens, {
|
|
382
|
+
presetName: builtInName,
|
|
383
|
+
themeSurfaces: surfaces.filter(surface => surface.className)
|
|
384
|
+
});
|
|
385
|
+
const tokensPath = path.join(resolvedDistDir, "tokens.json");
|
|
386
|
+
const cssPath = path.join(resolvedDistDir, "styles.css");
|
|
387
|
+
const surfaceManifestPath = path.join(resolvedDistDir, "surfaces.json");
|
|
388
|
+
await fs.writeFile(tokensPath, `${JSON.stringify(defaultSurface.tokens, null, 2)}\n`, "utf8");
|
|
389
|
+
await fs.writeFile(cssPath, css, "utf8");
|
|
390
|
+
await fs.writeFile(surfaceManifestPath, `${JSON.stringify(surfaceManifest, null, 2)}\n`, "utf8");
|
|
391
|
+
return {
|
|
392
|
+
configPath: resolvedConfigPath,
|
|
393
|
+
baselineConfigPath: defaultSurface.baselineConfigPath,
|
|
394
|
+
baselineTokensPath: defaultSurface.baselineTokensPath,
|
|
395
|
+
tokensPath,
|
|
396
|
+
cssPath,
|
|
397
|
+
surfaceManifestPath,
|
|
398
|
+
tokens: defaultSurface.tokens,
|
|
399
|
+
css,
|
|
400
|
+
surfaces: surfaceManifest
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
export async function buildThemeFromConfig(configPath = path.resolve("config/tiers/editorial.json"), options = {}) {
|
|
404
|
+
const resolvedConfigPath = path.resolve(configPath);
|
|
405
|
+
const distDir = options.distDir ?? "dist";
|
|
406
|
+
const baselineDir = options.baselineDir ?? "generated/baseline";
|
|
407
|
+
return buildTheme(resolvedConfigPath, distDir, baselineDir, {
|
|
408
|
+
surfaceLabel: options.surfaceLabel,
|
|
409
|
+
additionalSurfaces: options.additionalSurfaces
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
export async function buildThemeFromTier(tier) {
|
|
413
|
+
return buildThemeFromConfig(resolveTierPath(tier), {
|
|
414
|
+
distDir: path.join("dist", "tiers", tier),
|
|
415
|
+
baselineDir: path.join("generated", "baseline", "tiers", tier)
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
export async function buildThemeFromPreset(preset) {
|
|
419
|
+
return buildThemeFromConfig(resolvePresetPath(preset), {
|
|
420
|
+
distDir: path.join("dist", "presets", preset),
|
|
421
|
+
baselineDir: path.join("generated", "baseline", preset)
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
export async function deriveBaselineTokensFromConfig(configPath = path.resolve("config/tiers/editorial.json"), options = {}) {
|
|
425
|
+
const resolvedConfigPath = path.resolve(configPath);
|
|
426
|
+
const baselineDir = options.baselineDir ?? "generated/baseline";
|
|
427
|
+
const resolvedBaselineDir = path.resolve(baselineDir);
|
|
428
|
+
await emptyDirectory(resolvedBaselineDir);
|
|
429
|
+
const config = await readThemeConfig(resolvedConfigPath);
|
|
430
|
+
const baselineConfigFileName = `${path.parse(resolvedConfigPath).name}.baseline.json`;
|
|
431
|
+
const baselineConfigPath = path.join(resolvedBaselineDir, baselineConfigFileName);
|
|
432
|
+
await writeJsonFileAtomic(baselineConfigPath, createBaselineConfig(config, resolvedConfigPath, baselineConfigPath));
|
|
433
|
+
const tokens = await generateBaselineTokens(baselineConfigPath, resolvedBaselineDir);
|
|
434
|
+
return {
|
|
435
|
+
configPath: resolvedConfigPath,
|
|
436
|
+
baselineConfigPath,
|
|
437
|
+
baselineTokensPath: path.join(resolvedBaselineDir, "tokens.json"),
|
|
438
|
+
tokens
|
|
439
|
+
};
|
|
440
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const COPY_SELECTOR = ".bf-code-snippet-block.is-icon";
|
|
2
|
+
function getCopyBlock(target) {
|
|
3
|
+
if (!(target instanceof Element)) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
return target.closest(COPY_SELECTOR);
|
|
7
|
+
}
|
|
8
|
+
async function copySnippetText(copyBlock) {
|
|
9
|
+
const text = copyBlock.textContent?.trim() ?? "";
|
|
10
|
+
if (!text) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
if (navigator.clipboard?.writeText) {
|
|
15
|
+
await navigator.clipboard.writeText(text);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Fall through to the legacy command path.
|
|
21
|
+
}
|
|
22
|
+
const textarea = document.createElement("textarea");
|
|
23
|
+
textarea.value = text;
|
|
24
|
+
textarea.setAttribute("readonly", "");
|
|
25
|
+
textarea.style.position = "fixed";
|
|
26
|
+
textarea.style.inset = "-9999px";
|
|
27
|
+
document.body.append(textarea);
|
|
28
|
+
textarea.select();
|
|
29
|
+
try {
|
|
30
|
+
return document.execCommand("copy");
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
textarea.remove();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function markCopied(copyBlock, copiedLabel, copiedDurationMs) {
|
|
40
|
+
const originalTitle = copyBlock.getAttribute("title");
|
|
41
|
+
const existingTimer = Number.parseInt(copyBlock.dataset.bfCopiedTimer ?? "", 10);
|
|
42
|
+
if (Number.isFinite(existingTimer)) {
|
|
43
|
+
window.clearTimeout(existingTimer);
|
|
44
|
+
}
|
|
45
|
+
copyBlock.classList.add("is-copied");
|
|
46
|
+
copyBlock.setAttribute("title", copiedLabel);
|
|
47
|
+
const timer = window.setTimeout(() => {
|
|
48
|
+
copyBlock.classList.remove("is-copied");
|
|
49
|
+
if (originalTitle) {
|
|
50
|
+
copyBlock.setAttribute("title", originalTitle);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
copyBlock.removeAttribute("title");
|
|
54
|
+
}
|
|
55
|
+
delete copyBlock.dataset.bfCopiedTimer;
|
|
56
|
+
}, copiedDurationMs);
|
|
57
|
+
copyBlock.dataset.bfCopiedTimer = String(timer);
|
|
58
|
+
}
|
|
59
|
+
export function initCodeSnippets(options = {}) {
|
|
60
|
+
const root = options.root ?? document;
|
|
61
|
+
const copiedLabel = options.copiedLabel ?? "Copied";
|
|
62
|
+
const copiedDurationMs = options.copiedDurationMs ?? 2000;
|
|
63
|
+
const handleCopy = async (event) => {
|
|
64
|
+
const copyBlock = getCopyBlock(event.target);
|
|
65
|
+
if (!copyBlock) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const copied = await copySnippetText(copyBlock);
|
|
69
|
+
if (copied) {
|
|
70
|
+
markCopied(copyBlock, copiedLabel, copiedDurationMs);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const onClick = (event) => {
|
|
74
|
+
void handleCopy(event);
|
|
75
|
+
};
|
|
76
|
+
const onKeyDown = (event) => {
|
|
77
|
+
if (!(event instanceof KeyboardEvent)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const copyBlock = getCopyBlock(event.target);
|
|
81
|
+
if (!copyBlock) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
void handleCopy(event);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
root.addEventListener("click", onClick);
|
|
90
|
+
root.addEventListener("keydown", onKeyDown);
|
|
91
|
+
return () => {
|
|
92
|
+
root.removeEventListener("click", onClick);
|
|
93
|
+
root.removeEventListener("keydown", onKeyDown);
|
|
94
|
+
};
|
|
95
|
+
}
|