@typecad/ui 1.0.0-alpha.11 → 1.0.0-alpha.13
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/README.md +21 -1
- package/assets/fonts/dejavu/DejaVuSans-Bold.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSans-Oblique.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSans.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSansMono-Bold.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSansMono.ttf +0 -0
- package/assets/fonts/dejavu/LICENSE +187 -0
- package/assets/fonts/dejavu/README.md +49 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +74 -0
- package/dist/engine-index.js +2 -0
- package/dist/preview/build-program.d.ts +7 -1
- package/dist/preview/build-program.js +94 -15
- package/dist/preview/host-gfx.d.ts +3 -0
- package/dist/preview/host-gfx.js +30 -3
- package/dist/preview/host-ui-runtime.d.ts +115 -0
- package/dist/preview/host-ui-runtime.js +928 -46
- package/dist/ui-engine/block-layout.js +25 -11
- package/dist/ui-engine/color.d.ts +6 -0
- package/dist/ui-engine/color.js +80 -2
- package/dist/ui-engine/compat-report.d.ts +8 -0
- package/dist/ui-engine/compat-report.js +124 -0
- package/dist/ui-engine/css-imports.d.ts +4 -0
- package/dist/ui-engine/css-imports.js +70 -0
- package/dist/ui-engine/css-parser.js +58 -14
- package/dist/ui-engine/default-font.d.ts +25 -0
- package/dist/ui-engine/default-font.js +80 -0
- package/dist/ui-engine/font-assets.d.ts +2 -2
- package/dist/ui-engine/font-assets.js +22 -5
- package/dist/ui-engine/html-parser.d.ts +4 -0
- package/dist/ui-engine/html-parser.js +241 -26
- package/dist/ui-engine/image-assets.d.ts +8 -1
- package/dist/ui-engine/image-assets.js +35 -2
- package/dist/ui-engine/image-decode.d.ts +22 -0
- package/dist/ui-engine/image-decode.js +194 -0
- package/dist/ui-engine/inline-parser.js +1 -1
- package/dist/ui-engine/layout-engine.d.ts +19 -0
- package/dist/ui-engine/layout-engine.js +59 -3
- package/dist/ui-engine/model.d.ts +18 -0
- package/dist/ui-engine/model.js +76 -8
- package/dist/ui-engine/runtime-header/blend-bodies.js +29 -9
- package/dist/ui-engine/runtime-header/canvas-helpers.js +5 -2
- package/dist/ui-engine/runtime-header/canvas-scrollbar.js +115 -111
- package/dist/ui-engine/runtime-header/cuttlefish-gfx.js +21 -0
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +182 -179
- package/dist/ui-engine/runtime-header/forward-decls.js +215 -167
- package/dist/ui-engine/runtime-header/init-press-input.js +138 -135
- package/dist/ui-engine/runtime-header/keyboard.js +682 -380
- package/dist/ui-engine/runtime-header/node-draw-body.js +1656 -1377
- package/dist/ui-engine/runtime-header/paint-order-coords.js +259 -214
- package/dist/ui-engine/runtime-header/paint-rects-repair.js +736 -568
- package/dist/ui-engine/runtime-header/scroll-physics.js +25 -1
- package/dist/ui-engine/runtime-header/state-bindings-nav.js +9 -0
- package/dist/ui-engine/runtime-header/structs.js +228 -216
- package/dist/ui-engine/runtime-header/text-rendering.js +888 -770
- package/dist/ui-engine/runtime-header/tick/bindings-phase.js +118 -116
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +897 -633
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +24 -25
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +79 -23
- package/dist/ui-engine/runtime-header/types-defines.js +110 -93
- package/dist/ui-engine/shadcn-kit.d.ts +1 -0
- package/dist/ui-engine/shadcn-kit.js +502 -0
- package/dist/ui-engine/style-resolver.d.ts +4 -0
- package/dist/ui-engine/style-resolver.js +58 -6
- package/dist/ui-engine/transpile-ui.js +12 -5
- package/dist/ui-engine/ua-stylesheet.d.ts +22 -1
- package/dist/ui-engine/ua-stylesheet.js +119 -32
- package/dist/ui-engine/ui-lowering.js +37 -3
- package/dist/ui-engine/ui-registry.js +32 -4
- package/dist/ui-engine/yoga-layout.js +57 -52
- package/dist/wizard/config-writer.d.ts +49 -0
- package/dist/wizard/config-writer.js +366 -0
- package/dist/wizard/display-catalog.d.ts +75 -0
- package/dist/wizard/display-catalog.js +207 -0
- package/dist/wizard/index.d.ts +7 -0
- package/dist/wizard/index.js +10 -0
- package/dist/wizard/integration-wizard.d.ts +15 -0
- package/dist/wizard/integration-wizard.js +486 -0
- package/dist/wizard/prompts.d.ts +32 -0
- package/dist/wizard/prompts.js +95 -0
- package/dist/wizard/starter-ui.d.ts +1 -0
- package/dist/wizard/starter-ui.js +56 -0
- package/package.json +18 -5
- package/src/cli.ts +87 -0
- package/src/engine-index.ts +53 -51
- package/src/preview/build-program.ts +810 -734
- package/src/preview/host-gfx.ts +30 -3
- package/src/preview/host-ui-runtime.ts +4133 -3289
- package/src/ui-engine/block-layout.ts +24 -11
- package/src/ui-engine/color.ts +77 -2
- package/src/ui-engine/compat-report.ts +139 -0
- package/src/ui-engine/css-imports.ts +69 -0
- package/src/ui-engine/css-parser.ts +64 -15
- package/src/ui-engine/default-font.ts +95 -0
- package/src/ui-engine/font-assets.ts +545 -525
- package/src/ui-engine/html-parser.ts +615 -397
- package/src/ui-engine/image-assets.ts +37 -2
- package/src/ui-engine/image-decode.ts +244 -0
- package/src/ui-engine/inline-parser.ts +1 -1
- package/src/ui-engine/layout-engine.ts +61 -3
- package/src/ui-engine/model.ts +1317 -1230
- package/src/ui-engine/runtime-header/blend-bodies.ts +29 -9
- package/src/ui-engine/runtime-header/canvas-helpers.ts +5 -2
- package/src/ui-engine/runtime-header/canvas-scrollbar.ts +121 -117
- package/src/ui-engine/runtime-header/cuttlefish-gfx.ts +21 -0
- package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +188 -185
- package/src/ui-engine/runtime-header/forward-decls.ts +227 -179
- package/src/ui-engine/runtime-header/init-press-input.ts +144 -141
- package/src/ui-engine/runtime-header/keyboard.ts +689 -386
- package/src/ui-engine/runtime-header/node-draw-body.ts +1681 -1402
- package/src/ui-engine/runtime-header/paint-order-coords.ts +265 -220
- package/src/ui-engine/runtime-header/paint-rects-repair.ts +742 -574
- package/src/ui-engine/runtime-header/scroll-physics.ts +25 -1
- package/src/ui-engine/runtime-header/state-bindings-nav.ts +9 -0
- package/src/ui-engine/runtime-header/structs.ts +234 -222
- package/src/ui-engine/runtime-header/text-rendering.ts +894 -776
- package/src/ui-engine/runtime-header/tick/bindings-phase.ts +124 -122
- package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +902 -638
- package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +30 -31
- package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +79 -23
- package/src/ui-engine/runtime-header/types-defines.ts +116 -99
- package/src/ui-engine/shadcn-kit.ts +503 -0
- package/src/ui-engine/style-resolver.ts +471 -416
- package/src/ui-engine/transpile-ui.ts +12 -5
- package/src/ui-engine/ua-stylesheet.ts +137 -31
- package/src/ui-engine/ui-lowering.ts +486 -452
- package/src/ui-engine/ui-registry.ts +556 -524
- package/src/ui-engine/yoga-layout.ts +307 -309
- package/src/wizard/config-writer.ts +404 -0
- package/src/wizard/display-catalog.ts +273 -0
- package/src/wizard/index.ts +38 -0
- package/src/wizard/integration-wizard.ts +619 -0
- package/src/wizard/prompts.ts +145 -0
- package/src/wizard/starter-ui.ts +58 -0
|
@@ -7,15 +7,26 @@
|
|
|
7
7
|
// This is the fallback when no `display: flex` is present. YogaLayoutEngine
|
|
8
8
|
// handles flexbox layouts.
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
|
-
import { isDisplayNone, parseAspectRatio } from "./layout-engine.js";
|
|
11
|
-
/** Parse a CSS value string ("8px", "8") to a number
|
|
10
|
+
import { isDisplayNone, parseAspectRatio, cssDimValue, expandBoxShorthand } from "./layout-engine.js";
|
|
11
|
+
/** Parse a CSS value string ("8px", "8") to a number (percent folds to its
|
|
12
|
+
* numeric part — block layout has no parent-relative resolution). */
|
|
12
13
|
function cssNum(val) {
|
|
13
14
|
if (val === undefined)
|
|
14
15
|
return 0;
|
|
15
16
|
if (typeof val === "number")
|
|
16
17
|
return val;
|
|
17
|
-
const
|
|
18
|
-
return
|
|
18
|
+
const d = cssDimValue(val);
|
|
19
|
+
return typeof d === "number" ? d : parseFloat(String(d));
|
|
20
|
+
}
|
|
21
|
+
/** Resolve a dimension against a reference size: px passes through, a
|
|
22
|
+
* percentage resolves against `total` (0 when unresolvable). */
|
|
23
|
+
function resolveDim(val, total) {
|
|
24
|
+
const d = cssDimValue(val);
|
|
25
|
+
if (typeof d === "number")
|
|
26
|
+
return d;
|
|
27
|
+
if (typeof d === "string" && d.endsWith("%"))
|
|
28
|
+
return Math.round((parseFloat(d) * total) / 100);
|
|
29
|
+
return 0;
|
|
19
30
|
}
|
|
20
31
|
export class BlockLayoutEngine {
|
|
21
32
|
id = "block";
|
|
@@ -32,12 +43,15 @@ export class BlockLayoutEngine {
|
|
|
32
43
|
out.push(box);
|
|
33
44
|
if (node.children.length === 0)
|
|
34
45
|
return;
|
|
35
|
-
|
|
46
|
+
// Padding: full 1-4 value TRBL expansion (vertical/horizontal pairs).
|
|
47
|
+
const [padT, , padB, padL] = expandBoxShorthand(node.style.padding);
|
|
48
|
+
const padV = (typeof padT === "number" ? padT : 0) + (typeof padB === "number" ? padB : 0);
|
|
49
|
+
const padH = typeof padL === "number" ? padL * 2 : 0;
|
|
36
50
|
const content = {
|
|
37
|
-
x: box.x +
|
|
38
|
-
y: box.y +
|
|
39
|
-
w: box.w -
|
|
40
|
-
h: box.h -
|
|
51
|
+
x: box.x + (typeof padL === "number" ? padL : 0),
|
|
52
|
+
y: box.y + (typeof padT === "number" ? padT : 0),
|
|
53
|
+
w: box.w - padH,
|
|
54
|
+
h: box.h - padV,
|
|
41
55
|
};
|
|
42
56
|
let cursorY = content.y;
|
|
43
57
|
for (const child of node.children) {
|
|
@@ -55,8 +69,8 @@ export class BlockLayoutEngine {
|
|
|
55
69
|
? intrinsic.w + childPad * 2
|
|
56
70
|
: content.w;
|
|
57
71
|
const childH = intrinsic.h > 0 ? intrinsic.h + (isButton ? childPad * 2 : 0) : 16;
|
|
58
|
-
const explicitW =
|
|
59
|
-
const explicitH =
|
|
72
|
+
const explicitW = resolveDim(child.style.width, content.w);
|
|
73
|
+
const explicitH = resolveDim(child.style.height, content.h);
|
|
60
74
|
const aspectRatio = parseAspectRatio(child.style.aspectRatio);
|
|
61
75
|
let resolvedW = childW;
|
|
62
76
|
let resolvedH = childH;
|
|
@@ -3,6 +3,12 @@ export interface RGB {
|
|
|
3
3
|
g: number;
|
|
4
4
|
b: number;
|
|
5
5
|
}
|
|
6
|
+
/** Is this color fully transparent (the `transparent` keyword, alpha-0 rgba/
|
|
7
|
+
* hsla, or #rrggbb00)? Callers treat it as "no fill" rather than black. */
|
|
8
|
+
export declare function isTransparentColor(input: string): boolean;
|
|
9
|
+
/** Does this color carry a partial (non-zero, non-one) alpha that the engine
|
|
10
|
+
* will ignore? (No alpha blending on bare metal.) Used for warnings. */
|
|
11
|
+
export declare function hasIgnoredAlpha(input: string): boolean;
|
|
6
12
|
/** Parse any CSS color string to {r,g,b}. Alpha is ignored (no blending). */
|
|
7
13
|
export declare function parseColor(input: string): RGB;
|
|
8
14
|
/** @deprecated Use parseColor instead — parseHexColor only handles #rrggbb. */
|
package/dist/ui-engine/color.js
CHANGED
|
@@ -85,8 +85,41 @@ const NAMED_COLORS = {
|
|
|
85
85
|
violet: { r: 238, g: 130, b: 238 }, wheat: { r: 245, g: 222, b: 179 },
|
|
86
86
|
white: { r: 255, g: 255, b: 255 }, whitesmoke: { r: 245, g: 245, b: 245 },
|
|
87
87
|
yellow: { r: 255, g: 255, b: 0 }, yellowgreen: { r: 154, g: 205, b: 50 },
|
|
88
|
-
transparent: { r: 0, g: 0, b: 0 },
|
|
89
88
|
};
|
|
89
|
+
/** Is this color fully transparent (the `transparent` keyword, alpha-0 rgba/
|
|
90
|
+
* hsla, or #rrggbb00)? Callers treat it as "no fill" rather than black. */
|
|
91
|
+
export function isTransparentColor(input) {
|
|
92
|
+
const s = input.trim().toLowerCase();
|
|
93
|
+
if (s === "transparent" || s === "none")
|
|
94
|
+
return true;
|
|
95
|
+
const rgbaM = /^rgba?\([^)]*,\s*(0(?:\.0+)?)\s*\)$/.exec(s);
|
|
96
|
+
if (rgbaM)
|
|
97
|
+
return true;
|
|
98
|
+
const hslaM = /^hsla?\([^)]*\/\s*(0(?:\.0+)?|0%)\s*\)$/.exec(s);
|
|
99
|
+
if (hslaM)
|
|
100
|
+
return true;
|
|
101
|
+
const hex8M = /^#[0-9a-f]{8}$/.exec(s);
|
|
102
|
+
if (hex8M)
|
|
103
|
+
return s.endsWith("00");
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
/** Does this color carry a partial (non-zero, non-one) alpha that the engine
|
|
107
|
+
* will ignore? (No alpha blending on bare metal.) Used for warnings. */
|
|
108
|
+
export function hasIgnoredAlpha(input) {
|
|
109
|
+
const s = input.trim().toLowerCase();
|
|
110
|
+
const rgbaM = /^rgba?\([^)]*,\s*([\d.]+)\s*\)$/.exec(s);
|
|
111
|
+
if (rgbaM)
|
|
112
|
+
return parseFloat(rgbaM[1]) < 1;
|
|
113
|
+
const hslaM = /^hsla?\([^)]*\/\s*([\d.]+)%?\s*\)$/.exec(s);
|
|
114
|
+
if (hslaM)
|
|
115
|
+
return (hslaM[1].endsWith("%") ? parseFloat(hslaM[1]) / 100 : parseFloat(hslaM[1])) < 1;
|
|
116
|
+
const hex8M = /^#[0-9a-f]{8}$/.exec(s);
|
|
117
|
+
if (hex8M) {
|
|
118
|
+
const a = parseInt(s.slice(7, 9), 16) / 255;
|
|
119
|
+
return a > 0 && a < 1;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
90
123
|
/** Parse any CSS color string to {r,g,b}. Alpha is ignored (no blending). */
|
|
91
124
|
export function parseColor(input) {
|
|
92
125
|
const s = input.trim().toLowerCase();
|
|
@@ -136,7 +169,52 @@ export function parseColor(input) {
|
|
|
136
169
|
// Named colors
|
|
137
170
|
if (NAMED_COLORS[s])
|
|
138
171
|
return NAMED_COLORS[s];
|
|
139
|
-
|
|
172
|
+
// transparent resolves to black as a COLOR (e.g. gradient stops); fill
|
|
173
|
+
// sites check isTransparentColor() first and skip painting entirely.
|
|
174
|
+
if (s === "transparent")
|
|
175
|
+
return { r: 0, g: 0, b: 0 };
|
|
176
|
+
// oklch(L C H [/ a]) — CSS Color 4, the dialect of Tailwind v4 era shadcn
|
|
177
|
+
// themes. Alpha ignored like everywhere else; L accepts % form.
|
|
178
|
+
const oklchM = /^oklch\(\s*([\d.]+|none)(%)?\s+([\d.]+|none)\s+([\d.]+|none)(?:deg|turn|rad)?(?:\s*\/\s*[\d.%]+)?\s*\)$/i.exec(s);
|
|
179
|
+
if (oklchM) {
|
|
180
|
+
const l = oklchM[1] === "none" ? 0 : parseFloat(oklchM[1]) / (oklchM[2] ? 100 : 1);
|
|
181
|
+
const c = oklchM[3] === "none" ? 0 : parseFloat(oklchM[3]);
|
|
182
|
+
let h = oklchM[4] === "none" ? 0 : parseFloat(oklchM[4]);
|
|
183
|
+
if (/turn\b/i.test(s))
|
|
184
|
+
h = h * 360;
|
|
185
|
+
return oklchToRgb(l, c, h);
|
|
186
|
+
}
|
|
187
|
+
// shadcn theme dialect: bare HSL channel triplets ("222.2 47.4% 11.2%") —
|
|
188
|
+
// the format stock shadcn themes store in CSS variables, normally consumed
|
|
189
|
+
// as hsl(var(--x)). The shape (number % number%) is unambiguous with any
|
|
190
|
+
// valid CSS color, so both dialects work through var() substitution.
|
|
191
|
+
const tripletM = /^([\d.]+)(?:deg)?\s+([\d.]+)%\s+([\d.]+)%$/.exec(s);
|
|
192
|
+
if (tripletM)
|
|
193
|
+
return hslToRgb(parseFloat(tripletM[1]), parseFloat(tripletM[2]), parseFloat(tripletM[3]));
|
|
194
|
+
throw new Error(`Unsupported color format "${input}" — use #hex, rgb(), hsl(), oklch(), or a named color`);
|
|
195
|
+
}
|
|
196
|
+
/** Convert oklch (L 0-1, C 0-~0.4, H degrees) to sRGB via OkLab — Björn
|
|
197
|
+
* Ottosson's reference matrices, then the sRGB transfer function. Values
|
|
198
|
+
* outside the sRGB gamut clamp per channel (compile-time approximation of
|
|
199
|
+
* CSS gamut mapping — good enough for opaque 16-bit panel output). */
|
|
200
|
+
function oklchToRgb(l, c, h) {
|
|
201
|
+
const rad = (h * Math.PI) / 180;
|
|
202
|
+
const a = c * Math.cos(rad);
|
|
203
|
+
const b = c * Math.sin(rad);
|
|
204
|
+
const l_ = l + 0.3963377774 * a + 0.2158037573 * b;
|
|
205
|
+
const m_ = l - 0.1055613458 * a - 0.0638541728 * b;
|
|
206
|
+
const s_ = l - 0.0894841775 * a - 1.2914855480 * b;
|
|
207
|
+
const L = l_ * l_ * l_;
|
|
208
|
+
const M = m_ * m_ * m_;
|
|
209
|
+
const S = s_ * s_ * s_;
|
|
210
|
+
const lr = 4.0767416621 * L - 3.3077115913 * M + 0.2309699292 * S;
|
|
211
|
+
const lg = -1.2684380046 * L + 2.6097574011 * M - 0.3413193965 * S;
|
|
212
|
+
const lb = -0.0041960863 * L - 0.7034186147 * M + 1.7076147010 * S;
|
|
213
|
+
const gamma = (v) => {
|
|
214
|
+
const x = Math.min(1, Math.max(0, v));
|
|
215
|
+
return Math.round(255 * (x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055));
|
|
216
|
+
};
|
|
217
|
+
return { r: gamma(lr), g: gamma(lg), b: gamma(lb) };
|
|
140
218
|
}
|
|
141
219
|
/** Convert HSL (h: 0-360, s/l: 0-100) to RGB. Standard CSS algorithm. */
|
|
142
220
|
function hslToRgb(h, s, l) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Diagnostic } from "@typecad/cuttlefish/api/shared";
|
|
2
|
+
import type { StyledNode } from "./style-resolver.js";
|
|
3
|
+
import type { UIFontAssetModel } from "./font-assets.js";
|
|
4
|
+
/** Produce CSS-compatibility diagnostics for the lowered tree. */
|
|
5
|
+
export declare function cssCompatDiagnostics(screens: StyledNode[], fontAssets: UIFontAssetModel[], viewport: {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}, colorFormat: string | undefined, sourceFile: string): Diagnostic[];
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// CSS compatibility report — post-layout diagnostics that make the engine's
|
|
3
|
+
// divergences from browser behavior VISIBLE instead of silent.
|
|
4
|
+
//
|
|
5
|
+
// Every check here corresponds to a place where the engine approximates or
|
|
6
|
+
// quantizes what a browser would do (font-size bucketing on the stock font,
|
|
7
|
+
// ignored alpha, display-anchored UA scale, touch-target minimums). Surfaces
|
|
8
|
+
// as ordinary warnings in the build output; `--strict-css` upgrades them to
|
|
9
|
+
// errors (see the cuttlefish CLI).
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
import { selectFontAssetForStyle } from "./font-assets.js";
|
|
12
|
+
import { hasIgnoredAlpha } from "./color.js";
|
|
13
|
+
import { uaScaleFor } from "./ua-stylesheet.js";
|
|
14
|
+
const COLOR_PROPS = [
|
|
15
|
+
"color", "background", "borderColor", "borderTopColor", "borderRightColor",
|
|
16
|
+
"borderBottomColor", "borderLeftColor", "outline",
|
|
17
|
+
];
|
|
18
|
+
/** Stock-font size buckets (model.ts textSizeOf): every font-size in a bucket
|
|
19
|
+
* renders at the same pixel size when no @font-face matches. */
|
|
20
|
+
function bucketOf(px) {
|
|
21
|
+
if (px <= 12)
|
|
22
|
+
return "1–12";
|
|
23
|
+
if (px <= 20)
|
|
24
|
+
return "13–20";
|
|
25
|
+
if (px <= 28)
|
|
26
|
+
return "21–28";
|
|
27
|
+
return "29+";
|
|
28
|
+
}
|
|
29
|
+
function labelOf(node) {
|
|
30
|
+
if (node.id)
|
|
31
|
+
return `#${node.id}`;
|
|
32
|
+
if (node.origTag)
|
|
33
|
+
return `<${node.origTag}>`;
|
|
34
|
+
return `<${node.tag}>`;
|
|
35
|
+
}
|
|
36
|
+
/** Produce CSS-compatibility diagnostics for the lowered tree. */
|
|
37
|
+
export function cssCompatDiagnostics(screens, fontAssets, viewport, colorFormat, sourceFile) {
|
|
38
|
+
const out = [];
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
const push = (key, d) => {
|
|
41
|
+
if (seen.has(key))
|
|
42
|
+
return;
|
|
43
|
+
seen.add(key);
|
|
44
|
+
out.push(d);
|
|
45
|
+
};
|
|
46
|
+
const mono = colorFormat === "mono";
|
|
47
|
+
const scale = uaScaleFor(viewport.height, mono);
|
|
48
|
+
// Surface the display-anchored UA scale once when it differs from the
|
|
49
|
+
// browser's classic 16px — "why is my text smaller than on desktop" answered
|
|
50
|
+
// up front instead of discovered pixel-by-pixel.
|
|
51
|
+
if (scale.root !== 16 || mono) {
|
|
52
|
+
out.push({
|
|
53
|
+
severity: "info",
|
|
54
|
+
code: "css-ua-scale",
|
|
55
|
+
message: `Display-anchored UA defaults active: root text ${scale.root}px` +
|
|
56
|
+
(mono ? ` (monochrome; stock-font sizes are bucketed 1–12/13–20/21–28/29+)` : ``) +
|
|
57
|
+
`, headings ${scale.h.join("/")}, touch-target min-height ${scale.controlMinHeight}px.`,
|
|
58
|
+
hint: `Override per element with explicit px font-sizes, or restyle the scale in your own CSS.`,
|
|
59
|
+
source: sourceFile,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const walk = (node) => {
|
|
63
|
+
const style = node.style;
|
|
64
|
+
const label = labelOf(node);
|
|
65
|
+
// Partial alpha is silently dropped (no blending on bare metal).
|
|
66
|
+
for (const prop of COLOR_PROPS) {
|
|
67
|
+
const v = style[prop];
|
|
68
|
+
if (v && hasIgnoredAlpha(v)) {
|
|
69
|
+
push(`alpha:${v}`, {
|
|
70
|
+
severity: "warning",
|
|
71
|
+
code: "css-alpha-ignored",
|
|
72
|
+
message: `${label}: alpha in "${v}" is ignored — the color renders fully opaque (${prop}).`,
|
|
73
|
+
hint: `There is no alpha blending on bare metal; blend against the target background color instead.`,
|
|
74
|
+
source: sourceFile,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// font-size sanity against the viewport and the stock-font buckets.
|
|
79
|
+
const fs = style.fontSize;
|
|
80
|
+
if (fs) {
|
|
81
|
+
const px = parseFloat(fs);
|
|
82
|
+
if (Number.isFinite(px) && px > 0) {
|
|
83
|
+
if (px >= viewport.height * 0.15) {
|
|
84
|
+
push(`fsvp:${px}`, {
|
|
85
|
+
severity: "warning",
|
|
86
|
+
code: "css-font-size-viewport",
|
|
87
|
+
message: `${label}: font-size ${px}px is ${Math.round((px / viewport.height) * 100)}% of the ${viewport.height}px display height.`,
|
|
88
|
+
hint: `Large fills of a small screen are sometimes intended — if not, scale it down.`,
|
|
89
|
+
source: sourceFile,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// Quantization only bites on the stock font (no matching @font-face).
|
|
93
|
+
if (mono && selectFontAssetForStyle(fontAssets, node.style) === undefined) {
|
|
94
|
+
push(`bucket:${px}`, {
|
|
95
|
+
severity: "warning",
|
|
96
|
+
code: "css-font-size-quantized",
|
|
97
|
+
message: `${label}: font-size ${px}px renders at the stock-font bucket for ${bucketOf(px)}px — every size in that range draws identically on monochrome displays.`,
|
|
98
|
+
hint: `Pick sizes from the bucket edges (12/20/28) or register an @font-face for exact sizes.`,
|
|
99
|
+
source: sourceFile,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Touch-target minimums that swallow half the screen.
|
|
105
|
+
const mh = style.minHeight;
|
|
106
|
+
if (mh) {
|
|
107
|
+
const px = parseFloat(mh);
|
|
108
|
+
if (Number.isFinite(px) && px >= viewport.height * 0.5) {
|
|
109
|
+
push(`mhvp:${px}`, {
|
|
110
|
+
severity: "warning",
|
|
111
|
+
code: "css-min-height-viewport",
|
|
112
|
+
message: `${label}: min-height ${px}px is ${Math.round((px / viewport.height) * 100)}% of the ${viewport.height}px display height.`,
|
|
113
|
+
hint: `Reduce the min-height for very small panels (the UA default already scales down).`,
|
|
114
|
+
source: sourceFile,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const child of node.children)
|
|
119
|
+
walk(child);
|
|
120
|
+
};
|
|
121
|
+
for (const screen of screens)
|
|
122
|
+
walk(screen);
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Inline local @import statements in `cssText`. `baseDir` is the directory
|
|
2
|
+
* of the stylesheet the text came from. `seen` tracks normalized absolute
|
|
3
|
+
* paths already inlined (cycle guard) across the whole expansion. */
|
|
4
|
+
export declare function expandCssImports(cssText: string, baseDir: string, seen?: Set<string>): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// CSS @import expansion — build-time stylesheet inclusion.
|
|
3
|
+
//
|
|
4
|
+
// Local @import statements are inlined before parsing so shared stylesheets
|
|
5
|
+
// (e.g. the shadcn preset added by `cuttlefish add shadcn`) compose with
|
|
6
|
+
// per-module CSS, like a browser resolving imports. Relative paths resolve
|
|
7
|
+
// against the importing stylesheet's directory; recursion is supported with a
|
|
8
|
+
// cycle guard. Remote (http/data:) imports and unreadable files are left in
|
|
9
|
+
// place — the CSS parser's existing "@import is not supported" warning then
|
|
10
|
+
// reports them instead of failing silently.
|
|
11
|
+
//
|
|
12
|
+
// This is compile-time inclusion only: each firmware build still targets one
|
|
13
|
+
// display, so there is no runtime fetch.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { createRequire } from "node:module";
|
|
18
|
+
/** Guard against runaway include chains (distinct-file budget across the
|
|
19
|
+
* whole expansion; cycles are caught by the seen-set, this bounds depth). */
|
|
20
|
+
const MAX_INCLUDED_FILES = 32;
|
|
21
|
+
const IMPORT_RE = /@import\s+(?:url\(\s*)?["']?([^"'()\s;]+)["']?\s*\)?\s*[^;]*;/g;
|
|
22
|
+
/** Resolve a bare package specifier ("@typecad/ui/themes/blue.css") through
|
|
23
|
+
* node_modules relative to baseDir — how pre-packaged themes are imported.
|
|
24
|
+
* Returns null when it doesn't resolve (e.g. the Tailwind scaffolding
|
|
25
|
+
* specifier "tailwindcss" inside stock theme exports). */
|
|
26
|
+
function resolvePackageSpecifier(spec, baseDir) {
|
|
27
|
+
try {
|
|
28
|
+
const req = createRequire(path.join(baseDir, "package.json"));
|
|
29
|
+
return req.resolve(spec);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Inline local @import statements in `cssText`. `baseDir` is the directory
|
|
36
|
+
* of the stylesheet the text came from. `seen` tracks normalized absolute
|
|
37
|
+
* paths already inlined (cycle guard) across the whole expansion. */
|
|
38
|
+
export function expandCssImports(cssText, baseDir, seen = new Set()) {
|
|
39
|
+
return cssText.replace(IMPORT_RE, (match, spec) => {
|
|
40
|
+
if (/^(https?|data):/i.test(spec))
|
|
41
|
+
return match;
|
|
42
|
+
// Package specifiers ("@typecad/ui/themes/blue.css", but also the
|
|
43
|
+
// "tailwindcss" scaffolding inside stock exports) start with @ or a bare
|
|
44
|
+
// name — NOT "./", "../", "/", or a drive letter. Resolvable ones load
|
|
45
|
+
// from node_modules (pre-packaged themes); the rest drop silently so
|
|
46
|
+
// pasted tweakcn/ui.shadcn.com files import cleanly.
|
|
47
|
+
const pathLike = spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || /^[a-zA-Z]:/.test(spec);
|
|
48
|
+
let abs;
|
|
49
|
+
if (!pathLike) {
|
|
50
|
+
const resolved = resolvePackageSpecifier(spec, baseDir);
|
|
51
|
+
if (!resolved)
|
|
52
|
+
return "";
|
|
53
|
+
abs = resolved;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
abs = path.normalize(path.isAbsolute(spec) ? spec : path.join(baseDir, spec));
|
|
57
|
+
}
|
|
58
|
+
if (seen.has(abs) || seen.size >= MAX_INCLUDED_FILES)
|
|
59
|
+
return "";
|
|
60
|
+
seen.add(abs);
|
|
61
|
+
let text;
|
|
62
|
+
try {
|
|
63
|
+
text = fs.readFileSync(abs, "utf-8");
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return match; // unreadable: keep the statement so the parser warns
|
|
67
|
+
}
|
|
68
|
+
return expandCssImports(text, path.dirname(abs), seen);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -279,16 +279,23 @@ function resolveCalc(value) {
|
|
|
279
279
|
return out;
|
|
280
280
|
}
|
|
281
281
|
/** Evaluate a simple arithmetic expression of lengths to a length string.
|
|
282
|
-
*
|
|
283
|
-
*
|
|
282
|
+
* Supports + - * /. rem/em terms normalize to px (×16), so any length
|
|
283
|
+
* expression reduces to px: "calc(0.5rem - 2px)" → "6px".
|
|
284
|
+
* Percent + length mixes ("calc(50% - 10px)") cannot resolve numerically at
|
|
285
|
+
* parse time — those stay as literal calc() text. Unrecognized → as-is. */
|
|
284
286
|
function evalCalcExpr(expr) {
|
|
285
287
|
// Tokenize into numbers-with-units and operators.
|
|
286
288
|
const tokens = expr.match(/(?:[\d.]+(?:rem|em|px|%)?|[-+*/])/g);
|
|
287
289
|
if (!tokens || tokens.length === 0)
|
|
288
290
|
return `calc(${expr})`;
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
291
|
+
const hasPercent = /[\d.]+%/.test(expr);
|
|
292
|
+
const hasLength = /[\d.]+(?:rem|em|px)/.test(expr);
|
|
293
|
+
if (hasPercent && hasLength)
|
|
294
|
+
return `calc(${expr})`;
|
|
295
|
+
// rem/em already fold to px in toNum, so the output unit is determined by
|
|
296
|
+
// the term kinds — never "the first unit seen" (that produced 6rem from
|
|
297
|
+
// 0.5rem - 2px).
|
|
298
|
+
const unit = hasPercent ? "%" : hasLength ? "px" : "";
|
|
292
299
|
// Convert each token to a plain number (rem/em × 16).
|
|
293
300
|
const toNum = (tok) => {
|
|
294
301
|
const remM = /^(-?[\d.]+)rem$/.exec(tok);
|
|
@@ -728,10 +735,12 @@ function parseFontShorthand(props, val) {
|
|
|
728
735
|
if (sizeIndex < 0)
|
|
729
736
|
return;
|
|
730
737
|
const beforeSize = parts.slice(0, sizeIndex);
|
|
731
|
-
const size = parts[sizeIndex].split("/")
|
|
738
|
+
const [size, lineHeight] = parts[sizeIndex].split("/");
|
|
732
739
|
const family = parts.slice(sizeIndex + 1).join(" ").trim();
|
|
733
740
|
if (size)
|
|
734
741
|
props.fontSize = size;
|
|
742
|
+
if (lineHeight)
|
|
743
|
+
props.lineHeight = lineHeight;
|
|
735
744
|
if (family)
|
|
736
745
|
props.fontFamily = family;
|
|
737
746
|
for (const part of beforeSize) {
|
|
@@ -743,12 +752,18 @@ function parseFontShorthand(props, val) {
|
|
|
743
752
|
}
|
|
744
753
|
}
|
|
745
754
|
/** Assign a CSS property to the CSSProperty object. Unknown properties emit a warning
|
|
746
|
-
* (forward-compatible: they are dropped from output, but the author is notified).
|
|
755
|
+
* (forward-compatible: they are dropped from output, but the author is notified).
|
|
756
|
+
* Known properties with values the engine can only partially honor also warn —
|
|
757
|
+
* a visible approximation beats a silent one. */
|
|
747
758
|
function assignProp(props, prop, val, diagnostics) {
|
|
748
759
|
const warn = (message, hint) => {
|
|
749
760
|
if (diagnostics)
|
|
750
761
|
diagnostics.push({ severity: "warning", message, hint, code: "unknown-css-property", source: prop });
|
|
751
762
|
};
|
|
763
|
+
const warnValue = (code, message, hint) => {
|
|
764
|
+
if (diagnostics)
|
|
765
|
+
diagnostics.push({ severity: "warning", message, hint, code, source: prop });
|
|
766
|
+
};
|
|
752
767
|
switch (prop) {
|
|
753
768
|
// Box model
|
|
754
769
|
case "padding":
|
|
@@ -791,6 +806,9 @@ function assignProp(props, prop, val, diagnostics) {
|
|
|
791
806
|
props.aspectRatio = val;
|
|
792
807
|
break;
|
|
793
808
|
case "box-sizing":
|
|
809
|
+
if (val.trim().toLowerCase() === "content-box") {
|
|
810
|
+
warnValue("css-box-sizing", `box-sizing: content-box is ignored — the engine always lays out border-box (borders and padding inside width/height).`, `Remove the declaration, or size elements accounting for padding/border.`);
|
|
811
|
+
}
|
|
794
812
|
props.boxSizing = val;
|
|
795
813
|
break;
|
|
796
814
|
case "overflow":
|
|
@@ -812,9 +830,23 @@ function assignProp(props, prop, val, diagnostics) {
|
|
|
812
830
|
case "font-family":
|
|
813
831
|
props.fontFamily = val;
|
|
814
832
|
break;
|
|
815
|
-
case "font-size":
|
|
816
|
-
|
|
833
|
+
case "font-size": {
|
|
834
|
+
const v = val.trim();
|
|
835
|
+
if (v.endsWith("%")) {
|
|
836
|
+
warnValue("css-font-size-unit", `font-size "${val}" — percent sizes are not supported; use px. The declaration is ignored.`, `font-size accepts px (or a bare number, treated as px).`);
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
const em = /^([\d.]+)(em|rem)$/.exec(v);
|
|
840
|
+
if (em) {
|
|
841
|
+
props.fontSize = `${Math.round(parseFloat(em[1]) * 16)}px`;
|
|
842
|
+
warnValue("css-font-size-unit", `font-size "${val}" — em/rem resolve against the root (16px), not the parent; wrote ${props.fontSize}.`, `Use px to size text exactly.`);
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
props.fontSize = val;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
817
848
|
break;
|
|
849
|
+
}
|
|
818
850
|
case "text-align":
|
|
819
851
|
props.textAlign = val;
|
|
820
852
|
break;
|
|
@@ -836,6 +868,9 @@ function assignProp(props, prop, val, diagnostics) {
|
|
|
836
868
|
props.fontSubset = val;
|
|
837
869
|
break;
|
|
838
870
|
case "line-height":
|
|
871
|
+
if (/^\d+(?:\.\d+)?$/.test(val.trim())) {
|
|
872
|
+
warnValue("css-line-height", `line-height "${val}" — unitless multipliers are treated as px, not a multiple of the font size.`, `Write an explicit px value (e.g. line-height: 22px).`);
|
|
873
|
+
}
|
|
839
874
|
props.lineHeight = val;
|
|
840
875
|
break;
|
|
841
876
|
case "letter-spacing":
|
|
@@ -874,6 +909,9 @@ function assignProp(props, prop, val, diagnostics) {
|
|
|
874
909
|
break;
|
|
875
910
|
// Flexbox / layout
|
|
876
911
|
case "display":
|
|
912
|
+
if (["inline", "inline-block", "inline-flex", "grid", "flow-root", "table"].includes(val.trim().toLowerCase())) {
|
|
913
|
+
warnValue("css-display", `display: ${val.trim()} is not supported — elements stack vertically like a flex column (children use display:flex semantics).`, `Use the default stacking, or a row container with flex-direction: row + gap for inline flows.`);
|
|
914
|
+
}
|
|
877
915
|
props.display = val;
|
|
878
916
|
break;
|
|
879
917
|
case "flex-direction":
|
|
@@ -925,6 +963,9 @@ function assignProp(props, prop, val, diagnostics) {
|
|
|
925
963
|
props.order = val;
|
|
926
964
|
break;
|
|
927
965
|
case "position":
|
|
966
|
+
if (["fixed", "sticky"].includes(val.trim().toLowerCase())) {
|
|
967
|
+
warnValue("css-position", `position: ${val.trim()} is not supported — treated as static.`, `Use position: absolute (against the nearest ancestor) or keep flow layout.`);
|
|
968
|
+
}
|
|
928
969
|
props.position = val;
|
|
929
970
|
break;
|
|
930
971
|
case "z-index":
|
|
@@ -1023,12 +1064,14 @@ function parseFlexShorthand(props, val) {
|
|
|
1023
1064
|
props.flexBasis = parts[2];
|
|
1024
1065
|
}
|
|
1025
1066
|
}
|
|
1026
|
-
/** Parse the `border` shorthand: "2px solid #808080" → width, style, color.
|
|
1067
|
+
/** Parse the `border` shorthand: "2px solid #808080" → width, style, color.
|
|
1068
|
+
* Width accepts px, bare numbers (unitless = px), and decimals. */
|
|
1027
1069
|
function parseBorderShorthand(props, val) {
|
|
1028
1070
|
const parts = val.trim().split(/\s+/);
|
|
1029
1071
|
for (const p of parts) {
|
|
1030
|
-
|
|
1031
|
-
|
|
1072
|
+
const w = /^(\d+(?:\.\d+)?)(?:px)?$/.exec(p);
|
|
1073
|
+
if (w)
|
|
1074
|
+
props.borderWidth = `${w[1]}px`;
|
|
1032
1075
|
else if (["solid", "dashed", "dotted", "double", "none"].includes(p))
|
|
1033
1076
|
props.borderStyle = p;
|
|
1034
1077
|
// Color: #hex, rgb()/hsl(), a CSS named color, or a var() reference (the
|
|
@@ -1048,8 +1091,9 @@ function parsePerSideBorder(props, prop, val) {
|
|
|
1048
1091
|
const cKey = `border${cap}Color`;
|
|
1049
1092
|
const parts = val.trim().split(/\s+/);
|
|
1050
1093
|
for (const p of parts) {
|
|
1051
|
-
|
|
1052
|
-
|
|
1094
|
+
const w = /^(\d+(?:\.\d+)?)(?:px)?$/.exec(p);
|
|
1095
|
+
if (w)
|
|
1096
|
+
props[wKey] = `${w[1]}px`;
|
|
1053
1097
|
else if (["solid", "dashed", "dotted", "double", "none"].includes(p))
|
|
1054
1098
|
props[sKey] = p;
|
|
1055
1099
|
else if (p.startsWith("#") || p.startsWith("rgb") || p.startsWith("hsl") || p.startsWith("var(") || /^[a-z]+$/i.test(p))
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CSSFontFace } from "./css-parser.js";
|
|
2
|
+
/** The sans family the default faces register under. It matches the font's
|
|
3
|
+
* real name (unmodified file, original name — license-clean). */
|
|
4
|
+
export declare const DEFAULT_FONT_FAMILY = "DejaVu Sans";
|
|
5
|
+
/** The monospace family for <pre>/<code>/<kbd> UA styling. */
|
|
6
|
+
export declare const DEFAULT_MONO_FAMILY = "DejaVu Sans Mono";
|
|
7
|
+
/** Absolute source paths of the bundled faces. */
|
|
8
|
+
export declare function bundledFontPath(file: string): string;
|
|
9
|
+
/** True when the bundled font assets are present and usable. */
|
|
10
|
+
export declare function defaultFontAvailable(): boolean;
|
|
11
|
+
/** True when the display profile should use the bundled font. Color displays
|
|
12
|
+
* only: monochrome targets keep the built-in bitmap font (no AA). An absent
|
|
13
|
+
* profile defaults to color — the common case and the current build target. */
|
|
14
|
+
export declare function defaultFontApplies(colorFormat: string | undefined): boolean;
|
|
15
|
+
/** The implicit @font-face declarations for the bundled default fonts. */
|
|
16
|
+
export declare function defaultFontFaces(): CSSFontFace[];
|
|
17
|
+
/** Merge the bundled default faces into a user's @font-face list. Checked
|
|
18
|
+
* per family: if the author registered any face for "DejaVu Sans" (or the
|
|
19
|
+
* mono family), theirs wins and that family's bundle faces are skipped.
|
|
20
|
+
* Injected faces append so user faces sort first in weight/style matching. */
|
|
21
|
+
export declare function injectDefaultFontFaces(userFaces: CSSFontFace[], colorFormat: string | undefined): CSSFontFace[];
|
|
22
|
+
/** Attribution lines for generated font tables whose glyphs came from the
|
|
23
|
+
* bundled DejaVu faces (satisfies the license's binary-notice requirement
|
|
24
|
+
* and makes firmware images auditable). */
|
|
25
|
+
export declare function defaultFontAttribution(sourcePath: string): string | undefined;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Default bundled font — DejaVu Sans 2.37 (Bitstream Vera License).
|
|
3
|
+
//
|
|
4
|
+
// Every color display gets an implicit @font-face pair (regular + bold) so
|
|
5
|
+
// antialiased, continuously-sized text works out of the box: users don't have
|
|
6
|
+
// to source a TTF before their UI renders like the browser preview. Devices
|
|
7
|
+
// never receive the TTF itself — font-assets.ts rasterizes the exact
|
|
8
|
+
// (family, px, weight, style, characters) subsets a UI uses into glyph tables.
|
|
9
|
+
//
|
|
10
|
+
// Monochrome displays (1-bit OLEDs/e-ink) keep the engine's built-in bitmap
|
|
11
|
+
// font: alpha blending is disabled there, so rasterized AA glyphs add flash
|
|
12
|
+
// cost without a visual payoff.
|
|
13
|
+
//
|
|
14
|
+
// License: the unmodified TTFs live in packages/ui/assets/fonts/dejavu with
|
|
15
|
+
// the full text (LICENSE) and checksums (README.md). They must be redistributed
|
|
16
|
+
// with that license file and must not be renamed or modified (Bitstream Vera
|
|
17
|
+
// License reserved-name terms). Generated font tables carry an attribution
|
|
18
|
+
// comment (see ui-lowering.ts emitFontTables).
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
/** The sans family the default faces register under. It matches the font's
|
|
24
|
+
* real name (unmodified file, original name — license-clean). */
|
|
25
|
+
export const DEFAULT_FONT_FAMILY = "DejaVu Sans";
|
|
26
|
+
/** The monospace family for <pre>/<code>/<kbd> UA styling. */
|
|
27
|
+
export const DEFAULT_MONO_FAMILY = "DejaVu Sans Mono";
|
|
28
|
+
const FONT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../assets/fonts/dejavu");
|
|
29
|
+
/** Faces injected by default: sans + mono at the workhorse weights. */
|
|
30
|
+
const DEFAULT_FACES = [
|
|
31
|
+
{ family: DEFAULT_FONT_FAMILY, file: "DejaVuSans.ttf", weight: "400", style: "normal" },
|
|
32
|
+
{ family: DEFAULT_FONT_FAMILY, file: "DejaVuSans-Bold.ttf", weight: "700", style: "normal" },
|
|
33
|
+
{ family: DEFAULT_MONO_FAMILY, file: "DejaVuSansMono.ttf", weight: "400", style: "normal" },
|
|
34
|
+
{ family: DEFAULT_MONO_FAMILY, file: "DejaVuSansMono-Bold.ttf", weight: "700", style: "normal" },
|
|
35
|
+
];
|
|
36
|
+
/** Absolute source paths of the bundled faces. */
|
|
37
|
+
export function bundledFontPath(file) {
|
|
38
|
+
return path.join(FONT_DIR, file);
|
|
39
|
+
}
|
|
40
|
+
/** True when the bundled font assets are present and usable. */
|
|
41
|
+
export function defaultFontAvailable() {
|
|
42
|
+
return DEFAULT_FACES.every((f) => fs.existsSync(bundledFontPath(f.file)));
|
|
43
|
+
}
|
|
44
|
+
/** True when the display profile should use the bundled font. Color displays
|
|
45
|
+
* only: monochrome targets keep the built-in bitmap font (no AA). An absent
|
|
46
|
+
* profile defaults to color — the common case and the current build target. */
|
|
47
|
+
export function defaultFontApplies(colorFormat) {
|
|
48
|
+
return colorFormat !== "mono" && defaultFontAvailable();
|
|
49
|
+
}
|
|
50
|
+
/** The implicit @font-face declarations for the bundled default fonts. */
|
|
51
|
+
export function defaultFontFaces() {
|
|
52
|
+
return DEFAULT_FACES.map((f) => ({
|
|
53
|
+
fontFamily: f.family,
|
|
54
|
+
src: bundledFontPath(f.file),
|
|
55
|
+
fontWeight: f.weight,
|
|
56
|
+
fontStyle: f.style,
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
/** Merge the bundled default faces into a user's @font-face list. Checked
|
|
60
|
+
* per family: if the author registered any face for "DejaVu Sans" (or the
|
|
61
|
+
* mono family), theirs wins and that family's bundle faces are skipped.
|
|
62
|
+
* Injected faces append so user faces sort first in weight/style matching. */
|
|
63
|
+
export function injectDefaultFontFaces(userFaces, colorFormat) {
|
|
64
|
+
if (!defaultFontApplies(colorFormat))
|
|
65
|
+
return userFaces;
|
|
66
|
+
const declared = new Set(userFaces.map((f) => f.fontFamily.toLowerCase()));
|
|
67
|
+
const bundled = defaultFontFaces().filter((f) => !declared.has(f.fontFamily.toLowerCase()));
|
|
68
|
+
return bundled.length > 0 ? [...userFaces, ...bundled] : userFaces;
|
|
69
|
+
}
|
|
70
|
+
/** Attribution lines for generated font tables whose glyphs came from the
|
|
71
|
+
* bundled DejaVu faces (satisfies the license's binary-notice requirement
|
|
72
|
+
* and makes firmware images auditable). */
|
|
73
|
+
export function defaultFontAttribution(sourcePath) {
|
|
74
|
+
const resolved = path.resolve(sourcePath);
|
|
75
|
+
if (path.dirname(resolved) !== FONT_DIR)
|
|
76
|
+
return undefined;
|
|
77
|
+
return `${path.basename(resolved)} - DejaVu fonts v2.37` +
|
|
78
|
+
" - (c) 2003 Bitstream, Inc., (c) 2006 Tavmjong Bah." +
|
|
79
|
+
" Bitstream Vera License - see @typecad/ui assets/fonts/dejavu/LICENSE.";
|
|
80
|
+
}
|