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.
Files changed (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +588 -0
  3. package/config/experiments/ibm-plex-engine-smoke.json +115 -0
  4. package/config/experiments/ubuntu-engine-smoke.json +115 -0
  5. package/config/foundation-theme.json +117 -0
  6. package/config/tiers/app.json +120 -0
  7. package/config/tiers/documentation.json +120 -0
  8. package/config/tiers/editorial.json +120 -0
  9. package/config/tiers/os.json +120 -0
  10. package/dist/accordion.d.ts +5 -0
  11. package/dist/accordion.js +32 -0
  12. package/dist/application-layout.d.ts +4 -0
  13. package/dist/application-layout.js +181 -0
  14. package/dist/baseline-grid-overlay.d.ts +5 -0
  15. package/dist/baseline-grid-overlay.js +50 -0
  16. package/dist/baseline-grid-theme.d.ts +3 -0
  17. package/dist/baseline-grid-theme.js +3 -0
  18. package/dist/baseline-grid.d.ts +7 -0
  19. package/dist/baseline-grid.js +35 -0
  20. package/dist/build.d.ts +21 -0
  21. package/dist/build.js +440 -0
  22. package/dist/code-snippet.d.ts +6 -0
  23. package/dist/code-snippet.js +95 -0
  24. package/dist/contextual-menu.d.ts +4 -0
  25. package/dist/contextual-menu.js +156 -0
  26. package/dist/css-app-tier.d.ts +1 -0
  27. package/dist/css-app-tier.js +77 -0
  28. package/dist/css-components/article-pagination.d.ts +13 -0
  29. package/dist/css-components/article-pagination.js +151 -0
  30. package/dist/css-components/button-actions.d.ts +7 -0
  31. package/dist/css-components/button-actions.js +165 -0
  32. package/dist/css-components/cards-options.d.ts +6 -0
  33. package/dist/css-components/cards-options.js +180 -0
  34. package/dist/css-components/chip-badge-status.d.ts +10 -0
  35. package/dist/css-components/chip-badge-status.js +162 -0
  36. package/dist/css-components/content-card.d.ts +25 -0
  37. package/dist/css-components/content-card.js +517 -0
  38. package/dist/css-components/control-row.d.ts +2 -0
  39. package/dist/css-components/control-row.js +18 -0
  40. package/dist/css-components/cta-figure-aspect.d.ts +1 -0
  41. package/dist/css-components/cta-figure-aspect.js +123 -0
  42. package/dist/css-components/document-navigation.d.ts +13 -0
  43. package/dist/css-components/document-navigation.js +400 -0
  44. package/dist/css-components/editorial-content.d.ts +6 -0
  45. package/dist/css-components/editorial-content.js +52 -0
  46. package/dist/css-components/icon.d.ts +1 -0
  47. package/dist/css-components/icon.js +75 -0
  48. package/dist/css-components/interactive-feedback.d.ts +2 -0
  49. package/dist/css-components/interactive-feedback.js +230 -0
  50. package/dist/css-components/interactive-tables.d.ts +7 -0
  51. package/dist/css-components/interactive-tables.js +164 -0
  52. package/dist/css-components/linked-logo-site-layout.d.ts +21 -0
  53. package/dist/css-components/linked-logo-site-layout.js +214 -0
  54. package/dist/css-components/list-tree.d.ts +5 -0
  55. package/dist/css-components/list-tree.js +98 -0
  56. package/dist/css-components/list.d.ts +3 -0
  57. package/dist/css-components/list.js +119 -0
  58. package/dist/css-components/logo-media.d.ts +16 -0
  59. package/dist/css-components/logo-media.js +219 -0
  60. package/dist/css-components/navigation-layout.d.ts +5 -0
  61. package/dist/css-components/navigation-layout.js +151 -0
  62. package/dist/css-components/panel.d.ts +6 -0
  63. package/dist/css-components/panel.js +148 -0
  64. package/dist/css-components/search-box-and-filter.d.ts +9 -0
  65. package/dist/css-components/search-box-and-filter.js +267 -0
  66. package/dist/css-components/sites-editorial-ports.d.ts +19 -0
  67. package/dist/css-components/sites-editorial-ports.js +264 -0
  68. package/dist/css-components/sites-foundation.d.ts +17 -0
  69. package/dist/css-components/sites-foundation.js +173 -0
  70. package/dist/css-components/sites-rich-lists.d.ts +18 -0
  71. package/dist/css-components/sites-rich-lists.js +364 -0
  72. package/dist/css-components/static-content-ports.d.ts +14 -0
  73. package/dist/css-components/static-content-ports.js +206 -0
  74. package/dist/css-components/tab-section.d.ts +19 -0
  75. package/dist/css-components/tab-section.js +103 -0
  76. package/dist/css-components/table.d.ts +7 -0
  77. package/dist/css-components/table.js +79 -0
  78. package/dist/css-components/tabs-choice-breadcrumbs.d.ts +9 -0
  79. package/dist/css-components/tabs-choice-breadcrumbs.js +307 -0
  80. package/dist/css-components/tiered-list-equal-height-row.d.ts +1 -0
  81. package/dist/css-components/tiered-list-equal-height-row.js +286 -0
  82. package/dist/css-components.d.ts +2 -0
  83. package/dist/css-components.js +2701 -0
  84. package/dist/css-grid.d.ts +1 -0
  85. package/dist/css-grid.js +179 -0
  86. package/dist/css.d.ts +6 -0
  87. package/dist/css.js +431 -0
  88. package/dist/experiments/ibm-plex-engine-smoke/styles.css +8969 -0
  89. package/dist/experiments/ibm-plex-engine-smoke/surfaces.json +604 -0
  90. package/dist/experiments/ibm-plex-engine-smoke/tokens.json +217 -0
  91. package/dist/in-page-navigation.d.ts +7 -0
  92. package/dist/in-page-navigation.js +136 -0
  93. package/dist/index.d.ts +37 -0
  94. package/dist/index.js +18 -0
  95. package/dist/interactive-feedback.d.ts +14 -0
  96. package/dist/interactive-feedback.js +96 -0
  97. package/dist/interactive-tables.d.ts +28 -0
  98. package/dist/interactive-tables.js +184 -0
  99. package/dist/list-tree.d.ts +4 -0
  100. package/dist/list-tree.js +44 -0
  101. package/dist/panel-drawer.d.ts +4 -0
  102. package/dist/panel-drawer.js +171 -0
  103. package/dist/presets/app-tier/styles.css +9459 -0
  104. package/dist/presets/app-tier/surfaces.json +1225 -0
  105. package/dist/presets/app-tier/tokens.json +221 -0
  106. package/dist/presets/prose/styles.css +9442 -0
  107. package/dist/presets/prose/surfaces.json +1225 -0
  108. package/dist/presets/prose/tokens.json +221 -0
  109. package/dist/presets.d.ts +6 -0
  110. package/dist/presets.js +27 -0
  111. package/dist/range-controls.d.ts +9 -0
  112. package/dist/range-controls.js +73 -0
  113. package/dist/resizable-aside.d.ts +4 -0
  114. package/dist/resizable-aside.js +328 -0
  115. package/dist/side-navigation.d.ts +4 -0
  116. package/dist/side-navigation.js +219 -0
  117. package/dist/styles.css +9442 -0
  118. package/dist/surfaces.json +1225 -0
  119. package/dist/tabs.d.ts +4 -0
  120. package/dist/tabs.js +88 -0
  121. package/dist/tier-registry.d.ts +10 -0
  122. package/dist/tier-registry.js +25 -0
  123. package/dist/tiers/app/styles.css +9459 -0
  124. package/dist/tiers/app/surfaces.json +1225 -0
  125. package/dist/tiers/app/tokens.json +221 -0
  126. package/dist/tiers/documentation/styles.css +9442 -0
  127. package/dist/tiers/documentation/surfaces.json +1225 -0
  128. package/dist/tiers/documentation/tokens.json +221 -0
  129. package/dist/tiers/editorial/styles.css +9442 -0
  130. package/dist/tiers/editorial/surfaces.json +1225 -0
  131. package/dist/tiers/editorial/tokens.json +221 -0
  132. package/dist/tiers/os/styles.css +9442 -0
  133. package/dist/tiers/os/surfaces.json +1225 -0
  134. package/dist/tiers/os/tokens.json +221 -0
  135. package/dist/tokens.json +221 -0
  136. package/dist/tooltip.d.ts +4 -0
  137. package/dist/tooltip.js +70 -0
  138. package/dist/top-navigation.d.ts +4 -0
  139. package/dist/top-navigation.js +354 -0
  140. package/dist/types.d.ts +155 -0
  141. package/dist/types.js +1 -0
  142. package/dist/vanilla-theme-colors.d.ts +5 -0
  143. package/dist/vanilla-theme-colors.js +208 -0
  144. package/docs/publishing.md +66 -0
  145. package/package.json +108 -0
@@ -0,0 +1 @@
1
+ export declare function gridCss(): string;
@@ -0,0 +1,179 @@
1
+ export function gridCss() {
2
+ const spanRule = (span, indent = "") => `${indent}:where(.bf-theme) :where(.bf-grid) > :where(.bf-span-${span}) { grid-column: auto / span ${span}; }`;
3
+ const baseSpans = [1, 2, 4].map(span => spanRule(span)).join("\n");
4
+ const mediumSpans = [1, 2, 4, 8].map(span => spanRule(span, " ")).join("\n");
5
+ const largeSpans = [1, 2, 4, 8, 16].map(span => spanRule(span, " ")).join("\n");
6
+ return `/* ------------------------------------------------------------------ */
7
+ /* Gutter and margin escalation — viewport-based, all tiers */
8
+ /* Grid spec v0.3 §2.3: gutters set by global viewport breakpoint. */
9
+ /* Baseline Foundry models the spec's 460px x-small row as part of the */
10
+ /* default <620px bracket because the values do not change at 460px. */
11
+ /* ------------------------------------------------------------------ */
12
+
13
+ :where(.bf-theme) {
14
+ --bf-grid-gap-inline: 1rem;
15
+ --bf-grid-gap-block: 1rem;
16
+ --bf-page-margin: 1rem;
17
+ }
18
+
19
+ @media (width >= 38.75rem) {
20
+ :where(.bf-theme) {
21
+ --bf-grid-gap-inline: 1.5rem;
22
+ --bf-grid-gap-block: 1.5rem;
23
+ --bf-page-margin: 1.5rem;
24
+ }
25
+ }
26
+
27
+ @media (width >= 64.75rem) {
28
+ :where(.bf-theme) {
29
+ --bf-grid-gap-inline: 2rem;
30
+ --bf-grid-gap-block: 2rem;
31
+ --bf-page-margin: 2rem;
32
+ }
33
+
34
+ :where(.bf-theme.bf-tier-app) {
35
+ --bf-grid-gap-inline: 1.5rem;
36
+ --bf-grid-gap-block: 1.5rem;
37
+ }
38
+
39
+ :where(.bf-theme) :where(.bf-application),
40
+ :where(.bf-theme):where(.bf-application) {
41
+ --bf-grid-gap-inline: 1.5rem;
42
+ --bf-grid-gap-block: 1.5rem;
43
+ }
44
+ }
45
+
46
+ /* ------------------------------------------------------------------ */
47
+ /* Container query contexts */
48
+ /* */
49
+ /* All tiers use container queries for column switching. In editorial */
50
+ /* and docs, .bf-page is the single grid container — centered and */
51
+ /* capped at --bf-content-max-width — so the container width mirrors */
52
+ /* the viewport (spec §3: "viewport-based grid"). In app tier, */
53
+ /* .bf-page is fluid (no max-width) and multiple concurrent containers */
54
+ /* can each establish their own grid context (spec §5). */
55
+ /* ------------------------------------------------------------------ */
56
+
57
+ :where(.bf-theme) :where(.bf-page, .bf-grid-scope, .bf-section, .bf-strip, .bf-fixed-width, .bf-panel-content, .bf-accordion-panel, .bf-main, .bf-aside),
58
+ :where(.bf-theme):where(.bf-page, .bf-grid-scope, .bf-section, .bf-strip, .bf-fixed-width, .bf-panel-content, .bf-accordion-panel, .bf-main, .bf-aside) {
59
+ container-type: inline-size;
60
+ }
61
+
62
+ /* ------------------------------------------------------------------ */
63
+ /* Page-level layout */
64
+ /* Editorial/docs: centered + max-width (spec §3.2) */
65
+ /* App: fluid edge-to-edge (spec §5.2) */
66
+ /* ------------------------------------------------------------------ */
67
+
68
+ :where(.bf-theme) :where(.bf-fixed-width) {
69
+ margin-inline: auto;
70
+ max-inline-size: var(--bf-content-max-width);
71
+ padding-inline: max(var(--bf-page-margin), var(--bf-content-padding-inline));
72
+ width: 100%;
73
+ }
74
+
75
+ /* Keep a capped content row at the logical start of a wider panel. */
76
+ :where(.bf-theme) :where(.bf-fixed-width.is-start-aligned) {
77
+ margin-inline-end: auto;
78
+ margin-inline-start: 0;
79
+ }
80
+
81
+ :where(.bf-theme.bf-tier-app) :where(.bf-page) {
82
+ max-inline-size: none;
83
+ }
84
+
85
+ :where(.bf-theme) :where(.bf-panel-content, .bf-accordion-panel) :where(.bf-fixed-width) {
86
+ padding-inline: 0;
87
+ }
88
+
89
+ :where(.bf-theme) :where(.bf-grid) :where(.bf-fixed-width) {
90
+ padding-inline: 0;
91
+ }
92
+
93
+ /* ------------------------------------------------------------------ */
94
+ /* Grid base — 4 columns, all tiers */
95
+ /* ------------------------------------------------------------------ */
96
+
97
+ :where(.bf-theme) :where(.bf-grid) {
98
+ --bf-grid-columns: 4;
99
+ display: grid;
100
+ /* bf-grid keeps row-gap at 0; the block token stays available for */
101
+ /* two-dimensional layouts and other structural layout-gap cases. */
102
+ gap: 0 var(--bf-grid-gap-inline);
103
+ grid-template-columns: repeat(var(--bf-grid-columns), minmax(0, 1fr));
104
+ }
105
+
106
+ :where(.bf-theme) :where(ul.bf-grid, ol.bf-grid) {
107
+ list-style: none;
108
+ margin: 0;
109
+ padding: 0;
110
+ }
111
+
112
+ :where(.bf-theme) :where(.bf-grid.is-guide) > * {
113
+ background: color-mix(in srgb, var(--bf-color-accent) 18%, var(--bf-color-background-default));
114
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--bf-color-accent) 50%, var(--bf-color-rule));
115
+ min-block-size: calc(var(--bf-baseline) * 4);
116
+ }
117
+
118
+ :where(.bf-theme) :where(.bf-grid) > * {
119
+ grid-column: auto / span var(--bf-grid-columns);
120
+ min-inline-size: 0;
121
+ }
122
+
123
+ :where(.bf-theme) :where(.bf-grid.bf-grid.is-controls) {
124
+ container-type: inline-size;
125
+ gap: var(--bf-field-gap);
126
+ }
127
+
128
+ :where(.bf-theme) :where(.bf-grid.bf-grid.is-controls) > :where(.bf-grid-item.is-control, .bf-grid-item.is-control-pair) {
129
+ grid-column: auto / span 4;
130
+ }
131
+
132
+ ${baseSpans}
133
+
134
+ /* ------------------------------------------------------------------ */
135
+ /* Container-based column switching — all tiers */
136
+ /* Editorial/docs containers are .bf-page (centered, max-width), so */
137
+ /* container width tracks viewport. App containers can be any layout */
138
+ /* region (panels, drawers, main area). */
139
+ /* ------------------------------------------------------------------ */
140
+
141
+ @container (width >= 38.75rem) {
142
+ :where(.bf-theme) :where(.bf-grid) {
143
+ --bf-grid-columns: 8;
144
+ }
145
+
146
+ :where(.bf-theme) :where(.bf-grid.bf-grid.is-controls) > :where(.bf-grid-item.is-control) {
147
+ grid-column: auto / span 2;
148
+ }
149
+
150
+ :where(.bf-theme) :where(.bf-grid.bf-grid.is-controls) > :where(.bf-grid-item.is-control-pair) {
151
+ grid-column: auto / span 4;
152
+ }
153
+
154
+ ${mediumSpans}
155
+ }
156
+
157
+ @container (width >= 105.0625rem) {
158
+ :where(.bf-theme) :where(.bf-grid) {
159
+ --bf-grid-columns: 16;
160
+ }
161
+
162
+ :where(.bf-theme) :where(.bf-grid.bf-grid.is-controls) > :where(.bf-grid-item.is-control) {
163
+ grid-column: auto / span 4;
164
+ }
165
+
166
+ :where(.bf-theme) :where(.bf-grid.bf-grid.is-controls) > :where(.bf-grid-item.is-control-pair) {
167
+ grid-column: auto / span 8;
168
+ }
169
+
170
+ ${largeSpans}
171
+ }
172
+
173
+ /* Full span follows every generated numbered-span rule so normal cascade
174
+ * order is sufficient; consumers never need cascade escalation. */
175
+ :where(.bf-theme) :where(.bf-grid) > :where(.bf-span-full) {
176
+ grid-column: 1 / -1;
177
+ }
178
+ `;
179
+ }
package/dist/css.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { BuiltInThemeName } from "./presets.js";
2
+ import type { ThemeSurface, ThemeTokens } from "./types.js";
3
+ export declare function generateFoundryCss(tokens: ThemeTokens, options?: {
4
+ presetName?: BuiltInThemeName;
5
+ themeSurfaces?: ThemeSurface[];
6
+ }): string;
package/dist/css.js ADDED
@@ -0,0 +1,431 @@
1
+ import { gridCss } from "./css-grid.js";
2
+ import { componentsCss } from "./css-components.js";
3
+ import { appTierPresetCss } from "./css-app-tier.js";
4
+ import { generateBaselineGridOverlayCss, generateBaselineGridThemeOverrideCss } from "./baseline-grid-overlay.js";
5
+ import { foundryThemeRootColorVars, vanillaThemeColorVars } from "./vanilla-theme-colors.js";
6
+ function parseRemValue(rem) {
7
+ return Number.parseFloat(rem.replace("rem", ""));
8
+ }
9
+ function toRemLiteral(value) {
10
+ return `${Math.round(value * 100000) / 100000}rem`;
11
+ }
12
+ function fontFormat(path) {
13
+ const extension = path.split(".").pop()?.toLowerCase();
14
+ if (extension === "woff2") {
15
+ return "woff2";
16
+ }
17
+ if (extension === "woff") {
18
+ return "woff";
19
+ }
20
+ if (extension === "ttf") {
21
+ return "truetype";
22
+ }
23
+ if (extension === "otf") {
24
+ return "opentype";
25
+ }
26
+ return extension ?? "woff";
27
+ }
28
+ function fontFaceRule(fontFile) {
29
+ if (!fontFile.cssFamily || fontFile.emitFontFace === false) {
30
+ return "";
31
+ }
32
+ return `@font-face {\n font-family: "${fontFile.cssFamily}";\n src: url("${fontFile.path}") format("${fontFormat(fontFile.path)}");\n font-style: ${fontFile.fontStyle ?? "normal"};\n font-weight: ${fontFile.fontWeight ?? "400"};\n font-stretch: ${fontFile.fontStretch ?? "normal"};\n font-display: ${fontFile.fontDisplay ?? "swap"};\n}\n`;
33
+ }
34
+ function baselineCompensation(nudgeTop, baselineUnit) {
35
+ return toRemLiteral(parseRemValue(baselineUnit) - parseRemValue(nudgeTop));
36
+ }
37
+ function roleFontFamilyVar(roleName, fallback) {
38
+ return fallback ? `var(--bf-${roleName}-font-family, ${fallback})` : `var(--bf-${roleName}-font-family)`;
39
+ }
40
+ function roleFontSizeVar(roleName, fallback) {
41
+ return fallback ? `var(--bf-${roleName}-font-size, ${fallback})` : `var(--bf-${roleName}-font-size)`;
42
+ }
43
+ function roleFontStyleVar(roleName, fallback) {
44
+ return fallback ? `var(--bf-${roleName}-font-style, ${fallback})` : `var(--bf-${roleName}-font-style)`;
45
+ }
46
+ function roleFontWeightVar(roleName, fallback) {
47
+ return fallback !== undefined ? `var(--bf-${roleName}-font-weight, ${fallback})` : `var(--bf-${roleName}-font-weight)`;
48
+ }
49
+ function roleLineHeightVar(roleName, fallback) {
50
+ return fallback ? `var(--bf-${roleName}-line-height, ${fallback})` : `var(--bf-${roleName}-line-height)`;
51
+ }
52
+ function roleFontVariantCapsVar(roleName, fallback = "normal") {
53
+ return `var(--bf-${roleName}-font-variant-caps, ${fallback})`;
54
+ }
55
+ function roleLetterSpacingVar(roleName, fallback = "normal") {
56
+ return `var(--bf-${roleName}-letter-spacing, ${fallback})`;
57
+ }
58
+ function roleTextTransformVar(roleName, fallback = "none") {
59
+ return `var(--bf-${roleName}-text-transform, ${fallback})`;
60
+ }
61
+ function roleMarginBottomVar(roleName, fallback) {
62
+ return `var(--bf-${roleName}-margin-bottom, ${fallback})`;
63
+ }
64
+ function roleNudgeStartVar(roleName, fallback) {
65
+ return `var(--bf-${roleName}-nudge-start, ${fallback})`;
66
+ }
67
+ const ROLE_STYLE_DEFAULTS = {
68
+ h1: { letterSpacing: "-0.03em" },
69
+ h2: { letterSpacing: "-0.025em" },
70
+ h3: { letterSpacing: "-0.02em" },
71
+ h4: { letterSpacing: "-0.015em" }
72
+ };
73
+ function textRule(roleName, selectors, token, baselineUnit, extra = "") {
74
+ const styleDefaults = ROLE_STYLE_DEFAULTS[roleName] ?? {};
75
+ const marginBottom = token.marginBottom ?? baselineCompensation(token.nudgeTop, baselineUnit);
76
+ const nudgeStart = token.nudgeTop;
77
+ return `${selectors.join(",\n")} {\n font-family: ${roleFontFamilyVar(roleName, token.fontStack)};\n font-size: ${roleFontSizeVar(roleName, token.fontSize)};\n font-style: ${roleFontStyleVar(roleName, token.fontStyle ?? "normal")};\n font-weight: ${roleFontWeightVar(roleName, token.fontWeight ?? 400)};\n font-variant-caps: ${roleFontVariantCapsVar(roleName, token.fontVariantCaps ?? styleDefaults.fontVariantCaps ?? "normal")};\n letter-spacing: ${roleLetterSpacingVar(roleName, token.letterSpacing ?? styleDefaults.letterSpacing ?? "normal")};\n text-transform: ${roleTextTransformVar(roleName, token.textTransform ?? styleDefaults.textTransform ?? "none")};\n line-height: ${roleLineHeightVar(roleName, token.lineHeight)};\n margin-bottom: ${roleMarginBottomVar(roleName, marginBottom)};\n padding-block-end: 0rem;\n padding-block-start: ${roleNudgeStartVar(roleName, nudgeStart)};\n${extra}}\n`;
78
+ }
79
+ function roleVarDeclarations(roleName, token, baselineUnit) {
80
+ const styleDefaults = ROLE_STYLE_DEFAULTS[roleName] ?? {};
81
+ const compensation = token.marginBottom ?? baselineCompensation(token.nudgeTop, baselineUnit);
82
+ return ` --bf-${roleName}-font-family: ${token.fontStack};\n --bf-${roleName}-font-size: ${token.fontSize};\n --bf-${roleName}-font-style: ${token.fontStyle ?? "normal"};\n --bf-${roleName}-font-weight: ${token.fontWeight ?? 400};\n --bf-${roleName}-font-variant-caps: ${token.fontVariantCaps ?? styleDefaults.fontVariantCaps ?? "normal"};\n --bf-${roleName}-letter-spacing: ${token.letterSpacing ?? styleDefaults.letterSpacing ?? "normal"};\n --bf-${roleName}-text-transform: ${token.textTransform ?? styleDefaults.textTransform ?? "none"};\n --bf-${roleName}-line-height: ${token.lineHeight};\n --bf-${roleName}-space-after: ${token.spaceAfter};\n --bf-${roleName}-baseline-compensation: ${compensation};\n --bf-${roleName}-margin-bottom: ${compensation};\n --bf-${roleName}-nudge-start: ${token.nudgeTop};\n --bf-${roleName}-nudge-end: ${compensation};\n`;
83
+ }
84
+ function themeSurfaceRule(selector, tokens) {
85
+ const body = tokens.roles.body;
86
+ const smallFontFallback = body?.fontSize ?? tokens.roles.h6?.fontSize ?? "1rem";
87
+ return `${selector} {\n --bf-baseline: ${tokens.baselineUnit};\n --bf-space-0: 0rem;\n --bf-space-half: calc(var(--bf-baseline) / 2);\n --bf-space-1: var(--bf-baseline);\n --bf-space-2: calc(var(--bf-baseline) * 2);\n --bf-space-3: calc(var(--bf-baseline) * 3);\n --bf-space-4: calc(var(--bf-baseline) * 4);\n --bf-space-6: calc(var(--bf-baseline) * 6);\n --bf-space-8: calc(var(--bf-baseline) * 8);\n --bf-space-12: calc(var(--bf-baseline) * 12);\n --bf-space-16: calc(var(--bf-baseline) * 16);\n --bf-content-max-width: ${tokens.layout.contentMaxWidth};\n --bf-content-padding-inline: ${tokens.layout.contentPaddingInline};\n --bf-measure: ${tokens.layout.measure};\n --bf-section-space: ${tokens.layout.sectionSpace};\n --bf-section-space-shallow: ${tokens.layout.sectionSpaceShallow};\n --bf-section-space-deep: ${tokens.layout.sectionSpaceDeep};\n --bf-strip-space: ${tokens.layout.stripSpace};\n --bf-grid-gap-inline: ${tokens.layout.gridGapInline};\n --bf-grid-gap-block: ${tokens.layout.gridGapBlock};\n --bf-page-margin: ${tokens.layout.pageMargin};\n${Object.entries(tokens.roles).map(([roleName, token]) => roleVarDeclarations(roleName, token, tokens.baselineUnit)).join("")} --bf-font-size-small: var(--bf-body-font-size, ${smallFontFallback});\n }\n`;
88
+ }
89
+ function collectFontFiles(tokens, themeSurfaces) {
90
+ const seen = new Set();
91
+ const fontFiles = [tokens, ...themeSurfaces.map(surface => surface.tokens)]
92
+ .flatMap(surfaceTokens => surfaceTokens.fontFiles)
93
+ .filter(fontFile => Boolean(fontFile.cssFamily));
94
+ return fontFiles.filter(fontFile => {
95
+ const key = [
96
+ fontFile.cssFamily,
97
+ fontFile.path,
98
+ fontFile.fontStyle ?? "normal",
99
+ fontFile.fontWeight ?? "400",
100
+ fontFile.fontStretch ?? "normal",
101
+ fontFile.fontDisplay ?? "swap"
102
+ ].join("|");
103
+ if (seen.has(key)) {
104
+ return false;
105
+ }
106
+ seen.add(key);
107
+ return true;
108
+ });
109
+ }
110
+ function capEngineDemoRule(selectors, token) {
111
+ const capPosition = `calc((${token.lineHeight} + 1cap) / 2)`;
112
+ const capStartNudge = `calc(var(--bf-baseline) - mod(${capPosition}, var(--bf-baseline)))`;
113
+ const capEndNudge = `calc(var(--bf-baseline) - ${capStartNudge})`;
114
+ const scopedSelectors = selectors.map(s => s.replace(":where(.bf-theme)", ":where(.bf-engine-cap)"));
115
+ return `${scopedSelectors.join(",\n")} {\n margin-block-end: 0;\n padding-block-start: ${capStartNudge};\n padding-block-end: ${capEndNudge};\n}\n`;
116
+ }
117
+ const SEMANTIC_SELECTORS_BY_ROLE = {
118
+ body: [":where(.bf-theme) :where(p)"],
119
+ h1: [":where(.bf-theme) :where(h1)"],
120
+ h2: [":where(.bf-theme) :where(h2)"],
121
+ h3: [":where(.bf-theme) :where(h3)"],
122
+ h4: [":where(.bf-theme) :where(h4)"],
123
+ h5: [":where(.bf-theme) :where(h5)"],
124
+ h6: [":where(.bf-theme) :where(h6)"],
125
+ meta: [":where(.bf-theme) :where(figcaption)"]
126
+ };
127
+ const EXTRA_STYLES_BY_ROLE = {
128
+ body: " max-inline-size: var(--bf-measure);\n",
129
+ lead: " color: var(--bf-color-muted);\n max-inline-size: calc(var(--bf-measure) + var(--bf-space-4));\n",
130
+ h1: " max-inline-size: calc(var(--bf-measure) + var(--bf-space-6));\n",
131
+ h2: " max-inline-size: calc(var(--bf-measure) + var(--bf-space-4));\n",
132
+ meta: " color: var(--bf-color-muted);\n"
133
+ };
134
+ function selectorsForRole(roleName) {
135
+ const semanticSelectors = SEMANTIC_SELECTORS_BY_ROLE[roleName] ?? [];
136
+ return [...semanticSelectors, `:where(.bf-theme) .bf-${roleName}`];
137
+ }
138
+ export function generateFoundryCss(tokens, options = {}) {
139
+ const body = tokens.roles.body;
140
+ const baselineUnit = tokens.baselineUnit;
141
+ const themeSurfaces = options.themeSurfaces ?? [];
142
+ const hasAppDefault = options.presetName === "app" || options.presetName === "app-tier";
143
+ const hasAppClassSurface = themeSurfaces.some(surface => surface.className === "bf-tier-app");
144
+ const includesAppSurface = hasAppDefault || hasAppClassSurface;
145
+ const fontFaces = options.presetName
146
+ ? ""
147
+ : collectFontFiles(tokens, themeSurfaces).map(fontFaceRule).filter(Boolean).join("\n");
148
+ const roleRules = Object.entries(tokens.roles)
149
+ .map(([roleName, token]) => textRule(roleName, selectorsForRole(roleName), token, baselineUnit, EXTRA_STYLES_BY_ROLE[roleName] ?? ""))
150
+ .join("\n");
151
+ const capEngineDemo = `/* DEMO ONLY — Cap-derived nudges are unreliable (ascender ≠ cap height).\n The approximation (line-height + 1cap) / 2 drifts at larger sizes.\n Kept as a reference for why metrics-derived nudges are used instead. */\n` +
152
+ Object.entries(tokens.roles)
153
+ .map(([roleName, token]) => capEngineDemoRule(selectorsForRole(roleName), token))
154
+ .join("\n");
155
+ const scopedThemeCss = themeSurfaces
156
+ .filter(surface => surface.className)
157
+ .map(surface => themeSurfaceRule(`:where(.bf-theme.${surface.className})`, surface.tokens))
158
+ .join("\n");
159
+ const appScopes = [
160
+ ...(hasAppDefault ? [":where(.bf-theme)"] : []),
161
+ ...(hasAppClassSurface ? [":where(.bf-theme.bf-tier-app)"] : [])
162
+ ];
163
+ const presetCss = includesAppSurface ? `\n${appTierPresetCss(appScopes)}` : "";
164
+ if (!body) {
165
+ throw new Error("Theme tokens require a body role.");
166
+ }
167
+ return `${fontFaces}${fontFaces ? "\n" : ""}${generateBaselineGridOverlayCss({ baselineUnit: tokens.baselineUnit })}
168
+
169
+ ${themeSurfaceRule(":where(.bf-theme)", tokens)}
170
+ ${scopedThemeCss}
171
+
172
+ :where(.bf-theme) {
173
+ ${vanillaThemeColorVars("light")}${foundryThemeRootColorVars("light")}
174
+ background: var(--bf-color-bg);
175
+ color: var(--bf-color-text);
176
+ font-family: var(--bf-body-font-family, ${body.fontStack});
177
+ font-size: var(--bf-body-font-size, ${body.fontSize});
178
+ font-style: var(--bf-body-font-style, ${body.fontStyle ?? "normal"});
179
+ font-weight: var(--bf-body-font-weight, ${body.fontWeight ?? 400});
180
+ line-height: var(--bf-body-line-height, ${body.lineHeight});
181
+ }
182
+
183
+ :where(.bf-theme.is-dark) {
184
+ ${vanillaThemeColorVars("dark")}${foundryThemeRootColorVars("dark")}
185
+ color-scheme: dark;
186
+ }
187
+
188
+ :where(.bf-theme.is-light) {
189
+ color-scheme: light;
190
+ }
191
+
192
+ :where(.bf-theme),
193
+ :where(.bf-theme) * {
194
+ box-sizing: border-box;
195
+ }
196
+
197
+ ${generateBaselineGridThemeOverrideCss()}
198
+
199
+ :where(.bf-theme) :where(h1, h2, h3, h4, h5, h6, p, blockquote, figure, ul, ol, dl, pre) {
200
+ margin: 0;
201
+ }
202
+
203
+ :where(.bf-theme) :where(img, picture, svg, video) {
204
+ block-size: auto;
205
+ display: block;
206
+ inline-size: auto;
207
+ max-inline-size: 100%;
208
+ }
209
+
210
+ :where(.bf-theme) :where(a) {
211
+ color: var(--bf-color-link);
212
+ text-decoration: none;
213
+ text-decoration-thickness: 1px;
214
+ text-underline-offset: 0.12em;
215
+ }
216
+
217
+ :where(.bf-theme) :where(a:is(:hover, :active)) {
218
+ text-decoration: underline;
219
+ }
220
+
221
+ :where(.bf-theme) :where(a:visited) {
222
+ color: var(--bf-color-link-visited);
223
+ }
224
+
225
+ :where(.bf-theme) :where(a:focus-visible) {
226
+ outline: 2px solid var(--bf-color-focus);
227
+ outline-offset: 2px;
228
+ }
229
+
230
+ :where(.bf-theme) :where(code) {
231
+ font-family: "Ubuntu Sans Mono", ui-monospace, SFMono-Regular, Consolas, monospace;
232
+ font-size: 0.95em;
233
+ }
234
+
235
+ :where(.bf-theme) :where(.bf-page) {
236
+ margin-inline: auto;
237
+ max-inline-size: var(--bf-content-max-width);
238
+ padding-inline: max(var(--bf-page-margin), var(--bf-content-padding-inline));
239
+ }
240
+
241
+ :where(.bf-theme) :where(.bf-page.is-fill) {
242
+ min-block-size: 100vh;
243
+ padding-block-end: var(--bf-section-space);
244
+ }
245
+
246
+ :where(.bf-theme) :where(.bf-section) {
247
+ margin-block-end: 0;
248
+ }
249
+
250
+ :where(.bf-theme) :where(.bf-section.is-shallow) {
251
+ margin-block-end: 0;
252
+ }
253
+
254
+ :where(.bf-theme) :where(.bf-section.is-deep) {
255
+ margin-block-end: 0;
256
+ }
257
+
258
+ :where(.bf-theme) :where(.bf-strip) {
259
+ padding-block-end: var(--bf-strip-space);
260
+ }
261
+
262
+ :where(.bf-theme) :where(.bf-fixed-width, .bf-measure) {
263
+ inline-size: min(100%, var(--bf-measure));
264
+ }
265
+
266
+ :where(.bf-theme) :where(.bf-inline-size) {
267
+ --bf-inline-size: 18rem;
268
+ flex: 0 1 var(--bf-inline-size);
269
+ inline-size: min(100%, var(--bf-inline-size));
270
+ min-inline-size: min(100%, var(--bf-inline-size));
271
+ }
272
+
273
+ :where(.bf-theme) :where(.bf-inline-size.is-compact) {
274
+ --bf-inline-size: 12rem;
275
+ }
276
+
277
+ :where(.bf-theme) :where(.bf-inline-size.is-regular) {
278
+ --bf-inline-size: 18rem;
279
+ }
280
+
281
+ :where(.bf-theme) :where(.bf-inline-size.is-medium) {
282
+ --bf-inline-size: 20rem;
283
+ }
284
+
285
+ :where(.bf-theme) :where(.bf-inline-size.is-wide) {
286
+ --bf-inline-size: 24rem;
287
+ }
288
+
289
+ :where(.bf-theme) :where(.bf-inline-size.is-x-wide) {
290
+ --bf-inline-size: 28rem;
291
+ }
292
+
293
+ :where(.bf-theme) :where(.bf-stage-shell) {
294
+ --bf-stage-shell-gap: var(--bf-space-3);
295
+ align-content: center;
296
+ align-items: center;
297
+ display: grid;
298
+ gap: var(--bf-stage-shell-gap);
299
+ justify-items: center;
300
+ min-block-size: 100%;
301
+ min-inline-size: 0;
302
+ padding-block: var(--bf-space-4);
303
+ }
304
+
305
+ :where(.bf-theme) :where(.bf-stage-shell.is-tight) {
306
+ --bf-stage-shell-gap: var(--bf-space-2);
307
+ }
308
+
309
+ :where(.bf-theme) :where(.bf-stage-shell.is-loose) {
310
+ --bf-stage-shell-gap: var(--bf-space-4);
311
+ }
312
+
313
+ :where(.bf-theme) :where(.bf-stack) {
314
+ --bf-stack-space: var(--bf-section-space-shallow);
315
+ align-content: start;
316
+ display: grid;
317
+ gap: var(--bf-stack-space);
318
+ }
319
+
320
+ :where(.bf-theme) :where(.bf-stack.is-flush) {
321
+ --bf-stack-space: 0px;
322
+ }
323
+
324
+ :where(.bf-theme) :where(.bf-stack.is-extra-dense) {
325
+ --bf-stack-space: var(--bf-space-half);
326
+ }
327
+
328
+ :where(.bf-theme) :where(.bf-stack.is-dense) {
329
+ --bf-stack-space: var(--bf-space-1);
330
+ }
331
+
332
+ :where(.bf-theme) :where(.bf-stack.is-loose) {
333
+ --bf-stack-space: var(--bf-space-2);
334
+ }
335
+
336
+ :where(.bf-theme) :where(.bf-stack.is-section-shallow) {
337
+ --bf-stack-space: var(--bf-section-space-shallow);
338
+ }
339
+
340
+ :where(.bf-theme) :where(.bf-stack.is-section) {
341
+ --bf-stack-space: var(--bf-section-space);
342
+ }
343
+
344
+ :where(.bf-theme) :where(.bf-stack.is-section-deep) {
345
+ --bf-stack-space: var(--bf-section-space-deep);
346
+ }
347
+
348
+ :where(.bf-theme) :where(.bf-cluster) {
349
+ align-items: flex-start;
350
+ display: flex;
351
+ flex-wrap: wrap;
352
+ gap: var(--bf-space-2);
353
+ }
354
+
355
+ :where(.bf-theme) :where(.bf-cluster.is-split) {
356
+ justify-content: space-between;
357
+ }
358
+
359
+ :where(.bf-theme) :where(.bf-prose) {
360
+ display: grid;
361
+ gap: var(--bf-section-space-shallow);
362
+ inline-size: min(100%, var(--bf-measure));
363
+ }
364
+
365
+ ${roleRules}
366
+
367
+ ${capEngineDemo}
368
+
369
+ :where(.bf-theme) :where(ul, ol) {
370
+ margin-bottom: 0;
371
+ }
372
+
373
+ :where(.bf-theme) :where(.bf-prose ul, .bf-prose ol) {
374
+ padding-inline-start: var(--bf-space-4);
375
+ }
376
+
377
+ :where(.bf-theme) :where(.bf-prose li) {
378
+ margin: 0 0 ${roleMarginBottomVar("body", baselineCompensation(body.nudgeTop, baselineUnit))};
379
+ padding-block-end: 0rem;
380
+ padding-block-start: ${roleNudgeStartVar("body", body.nudgeTop)};
381
+ }
382
+
383
+ :where(.bf-theme) :where(.bf-prose blockquote) {
384
+ border-inline-start: 1px solid var(--bf-color-rule);
385
+ color: var(--bf-color-muted);
386
+ font-family: ${roleFontFamilyVar("body", body.fontStack)};
387
+ font-size: ${roleFontSizeVar("body", body.fontSize)};
388
+ font-style: ${roleFontStyleVar("body", body.fontStyle ?? "normal")};
389
+ font-weight: ${roleFontWeightVar("body", body.fontWeight ?? 400)};
390
+ line-height: ${roleLineHeightVar("body", body.lineHeight)};
391
+ margin-bottom: ${roleMarginBottomVar("body", baselineCompensation(body.nudgeTop, baselineUnit))};
392
+ max-inline-size: calc(var(--bf-measure) + var(--bf-space-4));
393
+ padding-block-end: 0rem;
394
+ padding-block-start: ${roleNudgeStartVar("body", body.nudgeTop)};
395
+ padding-inline-start: var(--bf-space-3);
396
+ }
397
+
398
+ :where(.bf-theme) :where(.bf-rule, .bf-prose hr) {
399
+ background: var(--bf-color-rule);
400
+ block-size: 1px;
401
+ border: 0;
402
+ inline-size: 100%;
403
+ /* Reserve one half-rem rhythm step after the rule, including its */
404
+ /* thickness, so borderless content does not touch the divider. */
405
+ margin: 0 0 calc(0.5rem - 1px);
406
+ }
407
+
408
+ /* Highlight rules share the same scalable emphasis-bar geometry as active
409
+ * navigation, tabs, notifications, and document-navigation markers. */
410
+ :where(.bf-theme) :where(.bf-rule.is-highlighted, .bf-prose hr.is-highlighted) {
411
+ block-size: var(--bf-bar-thickness);
412
+ margin-block-end: calc(0.5rem - var(--bf-bar-thickness));
413
+ }
414
+
415
+ :where(.bf-theme) :where(.bf-token-row) {
416
+ border-top: 1px solid var(--bf-color-rule);
417
+ display: grid;
418
+ gap: var(--bf-space-1);
419
+ padding-top: var(--bf-space-2);
420
+ }
421
+
422
+ :where(.bf-theme) :where(.bf-token-row:first-child) {
423
+ border-top: 0;
424
+ padding-top: 0;
425
+ }
426
+
427
+ ${componentsCss(tokens, themeSurfaces)}
428
+
429
+ ${gridCss()}${presetCss}
430
+ `;
431
+ }