@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
|
@@ -8,15 +8,25 @@
|
|
|
8
8
|
// handles flexbox layouts.
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
|
|
11
|
-
import { Box, IntrinsicSize, isDisplayNone, LayoutEngine, parseAspectRatio } from "./layout-engine.js";
|
|
11
|
+
import { Box, IntrinsicSize, isDisplayNone, LayoutEngine, parseAspectRatio, cssDimValue, expandBoxShorthand } from "./layout-engine.js";
|
|
12
12
|
import { StyledNode } from "./style-resolver.js";
|
|
13
13
|
|
|
14
|
-
/** Parse a CSS value string ("8px", "8") to a number
|
|
14
|
+
/** Parse a CSS value string ("8px", "8") to a number (percent folds to its
|
|
15
|
+
* numeric part — block layout has no parent-relative resolution). */
|
|
15
16
|
function cssNum(val: string | number | undefined): number {
|
|
16
17
|
if (val === undefined) return 0;
|
|
17
18
|
if (typeof val === "number") return val;
|
|
18
|
-
const
|
|
19
|
-
return
|
|
19
|
+
const d = cssDimValue(val);
|
|
20
|
+
return typeof d === "number" ? d : parseFloat(String(d));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Resolve a dimension against a reference size: px passes through, a
|
|
24
|
+
* percentage resolves against `total` (0 when unresolvable). */
|
|
25
|
+
function resolveDim(val: string | undefined, total: number): number {
|
|
26
|
+
const d = cssDimValue(val);
|
|
27
|
+
if (typeof d === "number") return d;
|
|
28
|
+
if (typeof d === "string" && d.endsWith("%")) return Math.round((parseFloat(d) * total) / 100);
|
|
29
|
+
return 0;
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
export class BlockLayoutEngine implements LayoutEngine {
|
|
@@ -42,12 +52,15 @@ export class BlockLayoutEngine implements LayoutEngine {
|
|
|
42
52
|
out.push(box);
|
|
43
53
|
if (node.children.length === 0) return;
|
|
44
54
|
|
|
45
|
-
|
|
55
|
+
// Padding: full 1-4 value TRBL expansion (vertical/horizontal pairs).
|
|
56
|
+
const [padT, , padB, padL] = expandBoxShorthand(node.style.padding);
|
|
57
|
+
const padV = (typeof padT === "number" ? padT : 0) + (typeof padB === "number" ? padB : 0);
|
|
58
|
+
const padH = typeof padL === "number" ? padL * 2 : 0;
|
|
46
59
|
const content: Box = {
|
|
47
|
-
x: box.x +
|
|
48
|
-
y: box.y +
|
|
49
|
-
w: box.w -
|
|
50
|
-
h: box.h -
|
|
60
|
+
x: box.x + (typeof padL === "number" ? padL : 0),
|
|
61
|
+
y: box.y + (typeof padT === "number" ? padT : 0),
|
|
62
|
+
w: box.w - padH,
|
|
63
|
+
h: box.h - padV,
|
|
51
64
|
};
|
|
52
65
|
let cursorY = content.y;
|
|
53
66
|
|
|
@@ -67,8 +80,8 @@ export class BlockLayoutEngine implements LayoutEngine {
|
|
|
67
80
|
? intrinsic.w + childPad * 2
|
|
68
81
|
: content.w;
|
|
69
82
|
const childH = intrinsic.h > 0 ? intrinsic.h + (isButton ? childPad * 2 : 0) : 16;
|
|
70
|
-
const explicitW =
|
|
71
|
-
const explicitH =
|
|
83
|
+
const explicitW = resolveDim(child.style.width, content.w);
|
|
84
|
+
const explicitH = resolveDim(child.style.height, content.h);
|
|
72
85
|
const aspectRatio = parseAspectRatio(child.style.aspectRatio);
|
|
73
86
|
let resolvedW = childW;
|
|
74
87
|
let resolvedH = childH;
|
package/src/ui-engine/color.ts
CHANGED
|
@@ -88,9 +88,38 @@ const NAMED_COLORS: Record<string, RGB> = {
|
|
|
88
88
|
violet: { r: 238, g: 130, b: 238 }, wheat: { r: 245, g: 222, b: 179 },
|
|
89
89
|
white: { r: 255, g: 255, b: 255 }, whitesmoke: { r: 245, g: 245, b: 245 },
|
|
90
90
|
yellow: { r: 255, g: 255, b: 0 }, yellowgreen: { r: 154, g: 205, b: 50 },
|
|
91
|
-
transparent: { r: 0, g: 0, b: 0 },
|
|
92
91
|
};
|
|
93
92
|
|
|
93
|
+
/** Is this color fully transparent (the `transparent` keyword, alpha-0 rgba/
|
|
94
|
+
* hsla, or #rrggbb00)? Callers treat it as "no fill" rather than black. */
|
|
95
|
+
export function isTransparentColor(input: string): boolean {
|
|
96
|
+
const s = input.trim().toLowerCase();
|
|
97
|
+
if (s === "transparent" || s === "none") return true;
|
|
98
|
+
const rgbaM = /^rgba?\([^)]*,\s*(0(?:\.0+)?)\s*\)$/.exec(s);
|
|
99
|
+
if (rgbaM) return true;
|
|
100
|
+
const hslaM = /^hsla?\([^)]*\/\s*(0(?:\.0+)?|0%)\s*\)$/.exec(s);
|
|
101
|
+
if (hslaM) return true;
|
|
102
|
+
const hex8M = /^#[0-9a-f]{8}$/.exec(s);
|
|
103
|
+
if (hex8M) return s.endsWith("00");
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Does this color carry a partial (non-zero, non-one) alpha that the engine
|
|
108
|
+
* will ignore? (No alpha blending on bare metal.) Used for warnings. */
|
|
109
|
+
export function hasIgnoredAlpha(input: string): boolean {
|
|
110
|
+
const s = input.trim().toLowerCase();
|
|
111
|
+
const rgbaM = /^rgba?\([^)]*,\s*([\d.]+)\s*\)$/.exec(s);
|
|
112
|
+
if (rgbaM) return parseFloat(rgbaM[1]) < 1;
|
|
113
|
+
const hslaM = /^hsla?\([^)]*\/\s*([\d.]+)%?\s*\)$/.exec(s);
|
|
114
|
+
if (hslaM) return (hslaM[1].endsWith("%") ? parseFloat(hslaM[1]) / 100 : parseFloat(hslaM[1])) < 1;
|
|
115
|
+
const hex8M = /^#[0-9a-f]{8}$/.exec(s);
|
|
116
|
+
if (hex8M) {
|
|
117
|
+
const a = parseInt(s.slice(7, 9), 16) / 255;
|
|
118
|
+
return a > 0 && a < 1;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
94
123
|
/** Parse any CSS color string to {r,g,b}. Alpha is ignored (no blending). */
|
|
95
124
|
export function parseColor(input: string): RGB {
|
|
96
125
|
const s = input.trim().toLowerCase();
|
|
@@ -144,7 +173,53 @@ export function parseColor(input: string): RGB {
|
|
|
144
173
|
// Named colors
|
|
145
174
|
if (NAMED_COLORS[s]) return NAMED_COLORS[s];
|
|
146
175
|
|
|
147
|
-
|
|
176
|
+
// transparent resolves to black as a COLOR (e.g. gradient stops); fill
|
|
177
|
+
// sites check isTransparentColor() first and skip painting entirely.
|
|
178
|
+
if (s === "transparent") return { r: 0, g: 0, b: 0 };
|
|
179
|
+
|
|
180
|
+
// oklch(L C H [/ a]) — CSS Color 4, the dialect of Tailwind v4 era shadcn
|
|
181
|
+
// themes. Alpha ignored like everywhere else; L accepts % form.
|
|
182
|
+
const oklchM = /^oklch\(\s*([\d.]+|none)(%)?\s+([\d.]+|none)\s+([\d.]+|none)(?:deg|turn|rad)?(?:\s*\/\s*[\d.%]+)?\s*\)$/i.exec(s);
|
|
183
|
+
if (oklchM) {
|
|
184
|
+
const l = oklchM[1] === "none" ? 0 : parseFloat(oklchM[1]) / (oklchM[2] ? 100 : 1);
|
|
185
|
+
const c = oklchM[3] === "none" ? 0 : parseFloat(oklchM[3]);
|
|
186
|
+
let h = oklchM[4] === "none" ? 0 : parseFloat(oklchM[4]);
|
|
187
|
+
if (/turn\b/i.test(s)) h = h * 360;
|
|
188
|
+
return oklchToRgb(l, c, h);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// shadcn theme dialect: bare HSL channel triplets ("222.2 47.4% 11.2%") —
|
|
192
|
+
// the format stock shadcn themes store in CSS variables, normally consumed
|
|
193
|
+
// as hsl(var(--x)). The shape (number % number%) is unambiguous with any
|
|
194
|
+
// valid CSS color, so both dialects work through var() substitution.
|
|
195
|
+
const tripletM = /^([\d.]+)(?:deg)?\s+([\d.]+)%\s+([\d.]+)%$/.exec(s);
|
|
196
|
+
if (tripletM) return hslToRgb(parseFloat(tripletM[1]), parseFloat(tripletM[2]), parseFloat(tripletM[3]));
|
|
197
|
+
|
|
198
|
+
throw new Error(`Unsupported color format "${input}" — use #hex, rgb(), hsl(), oklch(), or a named color`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Convert oklch (L 0-1, C 0-~0.4, H degrees) to sRGB via OkLab — Björn
|
|
202
|
+
* Ottosson's reference matrices, then the sRGB transfer function. Values
|
|
203
|
+
* outside the sRGB gamut clamp per channel (compile-time approximation of
|
|
204
|
+
* CSS gamut mapping — good enough for opaque 16-bit panel output). */
|
|
205
|
+
function oklchToRgb(l: number, c: number, h: number): RGB {
|
|
206
|
+
const rad = (h * Math.PI) / 180;
|
|
207
|
+
const a = c * Math.cos(rad);
|
|
208
|
+
const b = c * Math.sin(rad);
|
|
209
|
+
const l_ = l + 0.3963377774 * a + 0.2158037573 * b;
|
|
210
|
+
const m_ = l - 0.1055613458 * a - 0.0638541728 * b;
|
|
211
|
+
const s_ = l - 0.0894841775 * a - 1.2914855480 * b;
|
|
212
|
+
const L = l_ * l_ * l_;
|
|
213
|
+
const M = m_ * m_ * m_;
|
|
214
|
+
const S = s_ * s_ * s_;
|
|
215
|
+
const lr = 4.0767416621 * L - 3.3077115913 * M + 0.2309699292 * S;
|
|
216
|
+
const lg = -1.2684380046 * L + 2.6097574011 * M - 0.3413193965 * S;
|
|
217
|
+
const lb = -0.0041960863 * L - 0.7034186147 * M + 1.7076147010 * S;
|
|
218
|
+
const gamma = (v: number): number => {
|
|
219
|
+
const x = Math.min(1, Math.max(0, v));
|
|
220
|
+
return Math.round(255 * (x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055));
|
|
221
|
+
};
|
|
222
|
+
return { r: gamma(lr), g: gamma(lg), b: gamma(lb) };
|
|
148
223
|
}
|
|
149
224
|
|
|
150
225
|
/** Convert HSL (h: 0-360, s/l: 0-100) to RGB. Standard CSS algorithm. */
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
|
|
12
|
+
import type { Diagnostic } from "@typecad/cuttlefish/api/shared";
|
|
13
|
+
import type { StyledNode } from "./style-resolver.js";
|
|
14
|
+
import type { Box } from "./layout-engine.js";
|
|
15
|
+
import type { UIFontAssetModel } from "./font-assets.js";
|
|
16
|
+
import { selectFontAssetForStyle } from "./font-assets.js";
|
|
17
|
+
import { hasIgnoredAlpha } from "./color.js";
|
|
18
|
+
import { uaScaleFor } from "./ua-stylesheet.js";
|
|
19
|
+
|
|
20
|
+
const COLOR_PROPS = [
|
|
21
|
+
"color", "background", "borderColor", "borderTopColor", "borderRightColor",
|
|
22
|
+
"borderBottomColor", "borderLeftColor", "outline",
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
/** Stock-font size buckets (model.ts textSizeOf): every font-size in a bucket
|
|
26
|
+
* renders at the same pixel size when no @font-face matches. */
|
|
27
|
+
function bucketOf(px: number): string {
|
|
28
|
+
if (px <= 12) return "1–12";
|
|
29
|
+
if (px <= 20) return "13–20";
|
|
30
|
+
if (px <= 28) return "21–28";
|
|
31
|
+
return "29+";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function labelOf(node: StyledNode): string {
|
|
35
|
+
if (node.id) return `#${node.id}`;
|
|
36
|
+
if (node.origTag) return `<${node.origTag}>`;
|
|
37
|
+
return `<${node.tag}>`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Produce CSS-compatibility diagnostics for the lowered tree. */
|
|
41
|
+
export function cssCompatDiagnostics(
|
|
42
|
+
screens: StyledNode[],
|
|
43
|
+
fontAssets: UIFontAssetModel[],
|
|
44
|
+
viewport: { width: number; height: number },
|
|
45
|
+
colorFormat: string | undefined,
|
|
46
|
+
sourceFile: string,
|
|
47
|
+
): Diagnostic[] {
|
|
48
|
+
const out: Diagnostic[] = [];
|
|
49
|
+
const seen = new Set<string>();
|
|
50
|
+
const push = (key: string, d: Diagnostic): void => {
|
|
51
|
+
if (seen.has(key)) return;
|
|
52
|
+
seen.add(key);
|
|
53
|
+
out.push(d);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const mono = colorFormat === "mono";
|
|
57
|
+
const scale = uaScaleFor(viewport.height, mono);
|
|
58
|
+
|
|
59
|
+
// Surface the display-anchored UA scale once when it differs from the
|
|
60
|
+
// browser's classic 16px — "why is my text smaller than on desktop" answered
|
|
61
|
+
// up front instead of discovered pixel-by-pixel.
|
|
62
|
+
if (scale.root !== 16 || mono) {
|
|
63
|
+
out.push({
|
|
64
|
+
severity: "info",
|
|
65
|
+
code: "css-ua-scale",
|
|
66
|
+
message: `Display-anchored UA defaults active: root text ${scale.root}px` +
|
|
67
|
+
(mono ? ` (monochrome; stock-font sizes are bucketed 1–12/13–20/21–28/29+)` : ``) +
|
|
68
|
+
`, headings ${scale.h.join("/")}, touch-target min-height ${scale.controlMinHeight}px.`,
|
|
69
|
+
hint: `Override per element with explicit px font-sizes, or restyle the scale in your own CSS.`,
|
|
70
|
+
source: sourceFile,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const walk = (node: StyledNode): void => {
|
|
75
|
+
const style = node.style as Record<string, string | undefined>;
|
|
76
|
+
const label = labelOf(node);
|
|
77
|
+
|
|
78
|
+
// Partial alpha is silently dropped (no blending on bare metal).
|
|
79
|
+
for (const prop of COLOR_PROPS) {
|
|
80
|
+
const v = style[prop];
|
|
81
|
+
if (v && hasIgnoredAlpha(v)) {
|
|
82
|
+
push(`alpha:${v}`, {
|
|
83
|
+
severity: "warning",
|
|
84
|
+
code: "css-alpha-ignored",
|
|
85
|
+
message: `${label}: alpha in "${v}" is ignored — the color renders fully opaque (${prop}).`,
|
|
86
|
+
hint: `There is no alpha blending on bare metal; blend against the target background color instead.`,
|
|
87
|
+
source: sourceFile,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// font-size sanity against the viewport and the stock-font buckets.
|
|
93
|
+
const fs = style.fontSize;
|
|
94
|
+
if (fs) {
|
|
95
|
+
const px = parseFloat(fs);
|
|
96
|
+
if (Number.isFinite(px) && px > 0) {
|
|
97
|
+
if (px >= viewport.height * 0.15) {
|
|
98
|
+
push(`fsvp:${px}`, {
|
|
99
|
+
severity: "warning",
|
|
100
|
+
code: "css-font-size-viewport",
|
|
101
|
+
message: `${label}: font-size ${px}px is ${Math.round((px / viewport.height) * 100)}% of the ${viewport.height}px display height.`,
|
|
102
|
+
hint: `Large fills of a small screen are sometimes intended — if not, scale it down.`,
|
|
103
|
+
source: sourceFile,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// Quantization only bites on the stock font (no matching @font-face).
|
|
107
|
+
if (mono && selectFontAssetForStyle(fontAssets, node.style) === undefined) {
|
|
108
|
+
push(`bucket:${px}`, {
|
|
109
|
+
severity: "warning",
|
|
110
|
+
code: "css-font-size-quantized",
|
|
111
|
+
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.`,
|
|
112
|
+
hint: `Pick sizes from the bucket edges (12/20/28) or register an @font-face for exact sizes.`,
|
|
113
|
+
source: sourceFile,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Touch-target minimums that swallow half the screen.
|
|
120
|
+
const mh = style.minHeight;
|
|
121
|
+
if (mh) {
|
|
122
|
+
const px = parseFloat(mh);
|
|
123
|
+
if (Number.isFinite(px) && px >= viewport.height * 0.5) {
|
|
124
|
+
push(`mhvp:${px}`, {
|
|
125
|
+
severity: "warning",
|
|
126
|
+
code: "css-min-height-viewport",
|
|
127
|
+
message: `${label}: min-height ${px}px is ${Math.round((px / viewport.height) * 100)}% of the ${viewport.height}px display height.`,
|
|
128
|
+
hint: `Reduce the min-height for very small panels (the UA default already scales down).`,
|
|
129
|
+
source: sourceFile,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (const child of node.children) walk(child);
|
|
135
|
+
};
|
|
136
|
+
for (const screen of screens) walk(screen);
|
|
137
|
+
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { createRequire } from "node:module";
|
|
19
|
+
|
|
20
|
+
/** Guard against runaway include chains (distinct-file budget across the
|
|
21
|
+
* whole expansion; cycles are caught by the seen-set, this bounds depth). */
|
|
22
|
+
const MAX_INCLUDED_FILES = 32;
|
|
23
|
+
|
|
24
|
+
const IMPORT_RE = /@import\s+(?:url\(\s*)?["']?([^"'()\s;]+)["']?\s*\)?\s*[^;]*;/g;
|
|
25
|
+
|
|
26
|
+
/** Resolve a bare package specifier ("@typecad/ui/themes/blue.css") through
|
|
27
|
+
* node_modules relative to baseDir — how pre-packaged themes are imported.
|
|
28
|
+
* Returns null when it doesn't resolve (e.g. the Tailwind scaffolding
|
|
29
|
+
* specifier "tailwindcss" inside stock theme exports). */
|
|
30
|
+
function resolvePackageSpecifier(spec: string, baseDir: string): string | null {
|
|
31
|
+
try {
|
|
32
|
+
const req = createRequire(path.join(baseDir, "package.json"));
|
|
33
|
+
return req.resolve(spec);
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Inline local @import statements in `cssText`. `baseDir` is the directory
|
|
40
|
+
* of the stylesheet the text came from. `seen` tracks normalized absolute
|
|
41
|
+
* paths already inlined (cycle guard) across the whole expansion. */
|
|
42
|
+
export function expandCssImports(cssText: string, baseDir: string, seen: Set<string> = new Set()): string {
|
|
43
|
+
return cssText.replace(IMPORT_RE, (match, spec: string) => {
|
|
44
|
+
if (/^(https?|data):/i.test(spec)) return match;
|
|
45
|
+
// Package specifiers ("@typecad/ui/themes/blue.css", but also the
|
|
46
|
+
// "tailwindcss" scaffolding inside stock exports) start with @ or a bare
|
|
47
|
+
// name — NOT "./", "../", "/", or a drive letter. Resolvable ones load
|
|
48
|
+
// from node_modules (pre-packaged themes); the rest drop silently so
|
|
49
|
+
// pasted tweakcn/ui.shadcn.com files import cleanly.
|
|
50
|
+
const pathLike = spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || /^[a-zA-Z]:/.test(spec);
|
|
51
|
+
let abs: string;
|
|
52
|
+
if (!pathLike) {
|
|
53
|
+
const resolved = resolvePackageSpecifier(spec, baseDir);
|
|
54
|
+
if (!resolved) return "";
|
|
55
|
+
abs = resolved;
|
|
56
|
+
} else {
|
|
57
|
+
abs = path.normalize(path.isAbsolute(spec) ? spec : path.join(baseDir, spec));
|
|
58
|
+
}
|
|
59
|
+
if (seen.has(abs) || seen.size >= MAX_INCLUDED_FILES) return "";
|
|
60
|
+
seen.add(abs);
|
|
61
|
+
let text: string;
|
|
62
|
+
try {
|
|
63
|
+
text = fs.readFileSync(abs, "utf-8");
|
|
64
|
+
} catch {
|
|
65
|
+
return match; // unreadable: keep the statement so the parser warns
|
|
66
|
+
}
|
|
67
|
+
return expandCssImports(text, path.dirname(abs), seen);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -442,15 +442,21 @@ function resolveCalc(value: string): string {
|
|
|
442
442
|
}
|
|
443
443
|
|
|
444
444
|
/** Evaluate a simple arithmetic expression of lengths to a length string.
|
|
445
|
-
*
|
|
446
|
-
*
|
|
445
|
+
* Supports + - * /. rem/em terms normalize to px (×16), so any length
|
|
446
|
+
* expression reduces to px: "calc(0.5rem - 2px)" → "6px".
|
|
447
|
+
* Percent + length mixes ("calc(50% - 10px)") cannot resolve numerically at
|
|
448
|
+
* parse time — those stay as literal calc() text. Unrecognized → as-is. */
|
|
447
449
|
function evalCalcExpr(expr: string): string {
|
|
448
450
|
// Tokenize into numbers-with-units and operators.
|
|
449
451
|
const tokens = expr.match(/(?:[\d.]+(?:rem|em|px|%)?|[-+*/])/g);
|
|
450
452
|
if (!tokens || tokens.length === 0) return `calc(${expr})`;
|
|
451
|
-
|
|
452
|
-
const
|
|
453
|
-
|
|
453
|
+
const hasPercent = /[\d.]+%/.test(expr);
|
|
454
|
+
const hasLength = /[\d.]+(?:rem|em|px)/.test(expr);
|
|
455
|
+
if (hasPercent && hasLength) return `calc(${expr})`;
|
|
456
|
+
// rem/em already fold to px in toNum, so the output unit is determined by
|
|
457
|
+
// the term kinds — never "the first unit seen" (that produced 6rem from
|
|
458
|
+
// 0.5rem - 2px).
|
|
459
|
+
const unit = hasPercent ? "%" : hasLength ? "px" : "";
|
|
454
460
|
// Convert each token to a plain number (rem/em × 16).
|
|
455
461
|
const toNum = (tok: string): number => {
|
|
456
462
|
const remM = /^(-?[\d.]+)rem$/.exec(tok);
|
|
@@ -843,9 +849,10 @@ function parseFontShorthand(props: CSSProperty, val: string): void {
|
|
|
843
849
|
const sizeIndex = parts.findIndex((part) => /^\d+(?:\.\d+)?(?:px|pt|em|rem)?(?:\/.+)?$/.test(part));
|
|
844
850
|
if (sizeIndex < 0) return;
|
|
845
851
|
const beforeSize = parts.slice(0, sizeIndex);
|
|
846
|
-
const size = parts[sizeIndex].split("/")
|
|
852
|
+
const [size, lineHeight] = parts[sizeIndex].split("/");
|
|
847
853
|
const family = parts.slice(sizeIndex + 1).join(" ").trim();
|
|
848
854
|
if (size) props.fontSize = size;
|
|
855
|
+
if (lineHeight) props.lineHeight = lineHeight;
|
|
849
856
|
if (family) props.fontFamily = family;
|
|
850
857
|
for (const part of beforeSize) {
|
|
851
858
|
const lower = part.toLowerCase();
|
|
@@ -855,11 +862,16 @@ function parseFontShorthand(props: CSSProperty, val: string): void {
|
|
|
855
862
|
}
|
|
856
863
|
|
|
857
864
|
/** Assign a CSS property to the CSSProperty object. Unknown properties emit a warning
|
|
858
|
-
* (forward-compatible: they are dropped from output, but the author is notified).
|
|
865
|
+
* (forward-compatible: they are dropped from output, but the author is notified).
|
|
866
|
+
* Known properties with values the engine can only partially honor also warn —
|
|
867
|
+
* a visible approximation beats a silent one. */
|
|
859
868
|
function assignProp(props: CSSProperty, prop: string, val: string, diagnostics?: Diagnostic[]): void {
|
|
860
869
|
const warn = (message: string, hint?: string): void => {
|
|
861
870
|
if (diagnostics) diagnostics.push({ severity: "warning", message, hint, code: "unknown-css-property", source: prop });
|
|
862
871
|
};
|
|
872
|
+
const warnValue = (code: string, message: string, hint?: string): void => {
|
|
873
|
+
if (diagnostics) diagnostics.push({ severity: "warning", message, hint, code, source: prop });
|
|
874
|
+
};
|
|
863
875
|
switch (prop) {
|
|
864
876
|
// Box model
|
|
865
877
|
case "padding": props.padding = val; break;
|
|
@@ -875,7 +887,12 @@ function assignProp(props: CSSProperty, prop: string, val: string, diagnostics?:
|
|
|
875
887
|
case "min-height": props.minHeight = val; break;
|
|
876
888
|
case "max-height": props.maxHeight = val; break;
|
|
877
889
|
case "aspect-ratio": props.aspectRatio = val; break;
|
|
878
|
-
case "box-sizing":
|
|
890
|
+
case "box-sizing":
|
|
891
|
+
if (val.trim().toLowerCase() === "content-box") {
|
|
892
|
+
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.`);
|
|
893
|
+
}
|
|
894
|
+
props.boxSizing = val;
|
|
895
|
+
break;
|
|
879
896
|
case "overflow": props.overflow = val; break;
|
|
880
897
|
// Colors
|
|
881
898
|
case "color": props.color = val; break;
|
|
@@ -884,7 +901,21 @@ function assignProp(props: CSSProperty, prop: string, val: string, diagnostics?:
|
|
|
884
901
|
// Text
|
|
885
902
|
case "font": props.font = val; parseFontShorthand(props, val); break;
|
|
886
903
|
case "font-family": props.fontFamily = val; break;
|
|
887
|
-
case "font-size":
|
|
904
|
+
case "font-size": {
|
|
905
|
+
const v = val.trim();
|
|
906
|
+
if (v.endsWith("%")) {
|
|
907
|
+
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).`);
|
|
908
|
+
} else {
|
|
909
|
+
const em = /^([\d.]+)(em|rem)$/.exec(v);
|
|
910
|
+
if (em) {
|
|
911
|
+
props.fontSize = `${Math.round(parseFloat(em[1]) * 16)}px`;
|
|
912
|
+
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.`);
|
|
913
|
+
} else {
|
|
914
|
+
props.fontSize = val;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
888
919
|
case "text-align": props.textAlign = val; break;
|
|
889
920
|
case "text-decoration": props.textDecoration = val; break;
|
|
890
921
|
case "font-weight": props.fontWeight = val; break;
|
|
@@ -893,7 +924,12 @@ function assignProp(props: CSSProperty, prop: string, val: string, diagnostics?:
|
|
|
893
924
|
case "font-smooth":
|
|
894
925
|
case "-webkit-font-smoothing": props.fontSmoothing = val; break;
|
|
895
926
|
case "font-subset": props.fontSubset = val; break;
|
|
896
|
-
case "line-height":
|
|
927
|
+
case "line-height":
|
|
928
|
+
if (/^\d+(?:\.\d+)?$/.test(val.trim())) {
|
|
929
|
+
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).`);
|
|
930
|
+
}
|
|
931
|
+
props.lineHeight = val;
|
|
932
|
+
break;
|
|
897
933
|
case "letter-spacing": props.letterSpacing = val; break;
|
|
898
934
|
case "white-space": props.whiteSpace = val; break;
|
|
899
935
|
case "text-transform": props.textTransform = val; break;
|
|
@@ -907,7 +943,12 @@ function assignProp(props: CSSProperty, prop: string, val: string, diagnostics?:
|
|
|
907
943
|
case "animation-delay": props.animationDelay = val; break;
|
|
908
944
|
case "animation-timing-function": props.animationTimingFunction = val; break;
|
|
909
945
|
// Flexbox / layout
|
|
910
|
-
case "display":
|
|
946
|
+
case "display":
|
|
947
|
+
if (["inline", "inline-block", "inline-flex", "grid", "flow-root", "table"].includes(val.trim().toLowerCase())) {
|
|
948
|
+
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.`);
|
|
949
|
+
}
|
|
950
|
+
props.display = val;
|
|
951
|
+
break;
|
|
911
952
|
case "flex-direction": props.flexDirection = val; break;
|
|
912
953
|
case "gap": props.gap = val; props.rowGap = val; props.columnGap = val; break;
|
|
913
954
|
case "row-gap": props.rowGap = val; if (!props.columnGap) props.columnGap = val; break;
|
|
@@ -922,7 +963,12 @@ function assignProp(props: CSSProperty, prop: string, val: string, diagnostics?:
|
|
|
922
963
|
case "justify-content": props.justifyContent = val; break;
|
|
923
964
|
case "flex-wrap": props.flexWrap = val; break;
|
|
924
965
|
case "order": props.order = val; break;
|
|
925
|
-
case "position":
|
|
966
|
+
case "position":
|
|
967
|
+
if (["fixed", "sticky"].includes(val.trim().toLowerCase())) {
|
|
968
|
+
warnValue("css-position", `position: ${val.trim()} is not supported — treated as static.`, `Use position: absolute (against the nearest ancestor) or keep flow layout.`);
|
|
969
|
+
}
|
|
970
|
+
props.position = val;
|
|
971
|
+
break;
|
|
926
972
|
case "z-index": props.zIndex = val; break;
|
|
927
973
|
case "top": props.top = val; break;
|
|
928
974
|
case "right": props.right = val; break;
|
|
@@ -973,11 +1019,13 @@ function parseFlexShorthand(props: CSSProperty, val: string): void {
|
|
|
973
1019
|
}
|
|
974
1020
|
}
|
|
975
1021
|
|
|
976
|
-
/** Parse the `border` shorthand: "2px solid #808080" → width, style, color.
|
|
1022
|
+
/** Parse the `border` shorthand: "2px solid #808080" → width, style, color.
|
|
1023
|
+
* Width accepts px, bare numbers (unitless = px), and decimals. */
|
|
977
1024
|
function parseBorderShorthand(props: CSSProperty, val: string): void {
|
|
978
1025
|
const parts = val.trim().split(/\s+/);
|
|
979
1026
|
for (const p of parts) {
|
|
980
|
-
|
|
1027
|
+
const w = /^(\d+(?:\.\d+)?)(?:px)?$/.exec(p);
|
|
1028
|
+
if (w) props.borderWidth = `${w[1]}px`;
|
|
981
1029
|
else if (["solid", "dashed", "dotted", "double", "none"].includes(p)) props.borderStyle = p;
|
|
982
1030
|
// Color: #hex, rgb()/hsl(), a CSS named color, or a var() reference (the
|
|
983
1031
|
// token is substituted later by substituteVars, like other properties).
|
|
@@ -996,7 +1044,8 @@ function parsePerSideBorder(props: CSSProperty, prop: string, val: string): void
|
|
|
996
1044
|
const cKey = `border${cap}Color` as keyof CSSProperty;
|
|
997
1045
|
const parts = val.trim().split(/\s+/);
|
|
998
1046
|
for (const p of parts) {
|
|
999
|
-
|
|
1047
|
+
const w = /^(\d+(?:\.\d+)?)(?:px)?$/.exec(p);
|
|
1048
|
+
if (w) (props[wKey] as string | undefined) = `${w[1]}px`;
|
|
1000
1049
|
else if (["solid", "dashed", "dotted", "double", "none"].includes(p)) (props[sKey] as string | undefined) = p;
|
|
1001
1050
|
else if (p.startsWith("#") || p.startsWith("rgb") || p.startsWith("hsl") || p.startsWith("var(") || /^[a-z]+$/i.test(p)) (props[cKey] as string | undefined) = p;
|
|
1002
1051
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import type { CSSFontFace } from "./css-parser.js";
|
|
25
|
+
|
|
26
|
+
/** The sans family the default faces register under. It matches the font's
|
|
27
|
+
* real name (unmodified file, original name — license-clean). */
|
|
28
|
+
export const DEFAULT_FONT_FAMILY = "DejaVu Sans";
|
|
29
|
+
|
|
30
|
+
/** The monospace family for <pre>/<code>/<kbd> UA styling. */
|
|
31
|
+
export const DEFAULT_MONO_FAMILY = "DejaVu Sans Mono";
|
|
32
|
+
|
|
33
|
+
const FONT_DIR = path.resolve(
|
|
34
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
35
|
+
"../../assets/fonts/dejavu",
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
/** Faces injected by default: sans + mono at the workhorse weights. */
|
|
39
|
+
const DEFAULT_FACES: Array<{ family: string; file: string; weight: string; style: string }> = [
|
|
40
|
+
{ family: DEFAULT_FONT_FAMILY, file: "DejaVuSans.ttf", weight: "400", style: "normal" },
|
|
41
|
+
{ family: DEFAULT_FONT_FAMILY, file: "DejaVuSans-Bold.ttf", weight: "700", style: "normal" },
|
|
42
|
+
{ family: DEFAULT_MONO_FAMILY, file: "DejaVuSansMono.ttf", weight: "400", style: "normal" },
|
|
43
|
+
{ family: DEFAULT_MONO_FAMILY, file: "DejaVuSansMono-Bold.ttf", weight: "700", style: "normal" },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/** Absolute source paths of the bundled faces. */
|
|
47
|
+
export function bundledFontPath(file: string): string {
|
|
48
|
+
return path.join(FONT_DIR, file);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** True when the bundled font assets are present and usable. */
|
|
52
|
+
export function defaultFontAvailable(): boolean {
|
|
53
|
+
return DEFAULT_FACES.every((f) => fs.existsSync(bundledFontPath(f.file)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** True when the display profile should use the bundled font. Color displays
|
|
57
|
+
* only: monochrome targets keep the built-in bitmap font (no AA). An absent
|
|
58
|
+
* profile defaults to color — the common case and the current build target. */
|
|
59
|
+
export function defaultFontApplies(colorFormat: string | undefined): boolean {
|
|
60
|
+
return colorFormat !== "mono" && defaultFontAvailable();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The implicit @font-face declarations for the bundled default fonts. */
|
|
64
|
+
export function defaultFontFaces(): CSSFontFace[] {
|
|
65
|
+
return DEFAULT_FACES.map((f) => ({
|
|
66
|
+
fontFamily: f.family,
|
|
67
|
+
src: bundledFontPath(f.file),
|
|
68
|
+
fontWeight: f.weight,
|
|
69
|
+
fontStyle: f.style,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Merge the bundled default faces into a user's @font-face list. Checked
|
|
74
|
+
* per family: if the author registered any face for "DejaVu Sans" (or the
|
|
75
|
+
* mono family), theirs wins and that family's bundle faces are skipped.
|
|
76
|
+
* Injected faces append so user faces sort first in weight/style matching. */
|
|
77
|
+
export function injectDefaultFontFaces(userFaces: CSSFontFace[], colorFormat: string | undefined): CSSFontFace[] {
|
|
78
|
+
if (!defaultFontApplies(colorFormat)) return userFaces;
|
|
79
|
+
const declared = new Set(userFaces.map((f) => f.fontFamily.toLowerCase()));
|
|
80
|
+
const bundled = defaultFontFaces().filter(
|
|
81
|
+
(f) => !declared.has(f.fontFamily.toLowerCase()),
|
|
82
|
+
);
|
|
83
|
+
return bundled.length > 0 ? [...userFaces, ...bundled] : userFaces;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Attribution lines for generated font tables whose glyphs came from the
|
|
87
|
+
* bundled DejaVu faces (satisfies the license's binary-notice requirement
|
|
88
|
+
* and makes firmware images auditable). */
|
|
89
|
+
export function defaultFontAttribution(sourcePath: string): string | undefined {
|
|
90
|
+
const resolved = path.resolve(sourcePath);
|
|
91
|
+
if (path.dirname(resolved) !== FONT_DIR) return undefined;
|
|
92
|
+
return `${path.basename(resolved)} - DejaVu fonts v2.37` +
|
|
93
|
+
" - (c) 2003 Bitstream, Inc., (c) 2006 Tavmjong Bah." +
|
|
94
|
+
" Bitstream Vera License - see @typecad/ui assets/fonts/dejavu/LICENSE.";
|
|
95
|
+
}
|