@hypen-space/web 0.4.980 → 0.5.2
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 +20 -13
- package/dist/canvas/events.js +46 -7
- package/dist/canvas/events.js.map +1 -1
- package/dist/canvas/index.d.ts +0 -1
- package/dist/canvas/index.js +0 -1
- package/dist/canvas/index.js.map +1 -1
- package/dist/canvas/layout.js +68 -98
- package/dist/canvas/layout.js.map +1 -1
- package/dist/canvas/paint.js +168 -71
- package/dist/canvas/paint.js.map +1 -1
- package/dist/canvas/renderer.d.ts +8 -3
- package/dist/canvas/renderer.js +32 -43
- package/dist/canvas/renderer.js.map +1 -1
- package/dist/canvas/text.js +2 -9
- package/dist/canvas/text.js.map +1 -1
- package/dist/canvas/types.d.ts +3 -0
- package/dist/canvas/utils.d.ts +0 -23
- package/dist/canvas/utils.js +3 -67
- package/dist/canvas/utils.js.map +1 -1
- package/dist/canvas/variants.d.ts +49 -0
- package/dist/canvas/variants.js +137 -0
- package/dist/canvas/variants.js.map +1 -0
- package/dist/dom/applicators/events.js +1 -0
- package/dist/dom/applicators/events.js.map +1 -1
- package/dist/dom/applicators/font.d.ts +11 -6
- package/dist/dom/applicators/font.js +16 -27
- package/dist/dom/applicators/font.js.map +1 -1
- package/dist/dom/applicators/index.js +47 -45
- package/dist/dom/applicators/index.js.map +1 -1
- package/dist/dom/canvas/index.js +2 -2
- package/dist/dom/canvas/index.js.map +1 -1
- package/dist/dom/components/app.js +2 -2
- package/dist/dom/components/app.js.map +1 -1
- package/dist/dom/components/audio.js +2 -2
- package/dist/dom/components/audio.js.map +1 -1
- package/dist/dom/components/avatar.js +3 -4
- package/dist/dom/components/avatar.js.map +1 -1
- package/dist/dom/components/badge.js +3 -4
- package/dist/dom/components/badge.js.map +1 -1
- package/dist/dom/components/button.js +2 -2
- package/dist/dom/components/button.js.map +1 -1
- package/dist/dom/components/card.js +2 -2
- package/dist/dom/components/card.js.map +1 -1
- package/dist/dom/components/center.js +2 -2
- package/dist/dom/components/center.js.map +1 -1
- package/dist/dom/components/checkbox.js +4 -5
- package/dist/dom/components/checkbox.js.map +1 -1
- package/dist/dom/components/column.js +2 -2
- package/dist/dom/components/column.js.map +1 -1
- package/dist/dom/components/container.js +2 -2
- package/dist/dom/components/container.js.map +1 -1
- package/dist/dom/components/divider.js +2 -2
- package/dist/dom/components/divider.js.map +1 -1
- package/dist/dom/components/grid.js +10 -10
- package/dist/dom/components/grid.js.map +1 -1
- package/dist/dom/components/heading.js +4 -6
- package/dist/dom/components/heading.js.map +1 -1
- package/dist/dom/components/hypenapp.js +2 -2
- package/dist/dom/components/hypenapp.js.map +1 -1
- package/dist/dom/components/icon.js +4 -5
- package/dist/dom/components/icon.js.map +1 -1
- package/dist/dom/components/image.js +2 -2
- package/dist/dom/components/image.js.map +1 -1
- package/dist/dom/components/index.d.ts +2 -2
- package/dist/dom/components/index.js +2 -2
- package/dist/dom/components/index.js.map +1 -1
- package/dist/dom/components/input.js +2 -2
- package/dist/dom/components/input.js.map +1 -1
- package/dist/dom/components/link.js +2 -2
- package/dist/dom/components/link.js.map +1 -1
- package/dist/dom/components/list.js +2 -2
- package/dist/dom/components/list.js.map +1 -1
- package/dist/dom/components/paragraph.js +3 -4
- package/dist/dom/components/paragraph.js.map +1 -1
- package/dist/dom/components/progressbar.js +3 -3
- package/dist/dom/components/progressbar.js.map +1 -1
- package/dist/dom/components/route.js +2 -2
- package/dist/dom/components/route.js.map +1 -1
- package/dist/dom/components/router.js +2 -2
- package/dist/dom/components/router.js.map +1 -1
- package/dist/dom/components/row.js +10 -10
- package/dist/dom/components/row.js.map +1 -1
- package/dist/dom/components/select.js +3 -4
- package/dist/dom/components/select.js.map +1 -1
- package/dist/dom/components/slider.js +2 -2
- package/dist/dom/components/slider.js.map +1 -1
- package/dist/dom/components/spacer.js +2 -2
- package/dist/dom/components/spacer.js.map +1 -1
- package/dist/dom/components/spinner.js +4 -4
- package/dist/dom/components/spinner.js.map +1 -1
- package/dist/dom/components/stack.js +10 -10
- package/dist/dom/components/stack.js.map +1 -1
- package/dist/dom/components/switch.js +5 -6
- package/dist/dom/components/switch.js.map +1 -1
- package/dist/dom/components/text.js +4 -7
- package/dist/dom/components/text.js.map +1 -1
- package/dist/dom/components/textarea.js +2 -2
- package/dist/dom/components/textarea.js.map +1 -1
- package/dist/dom/components/video.js +2 -2
- package/dist/dom/components/video.js.map +1 -1
- package/dist/dom/debug.js +6 -8
- package/dist/dom/debug.js.map +1 -1
- package/dist/dom/index.d.ts +0 -1
- package/dist/dom/index.js +0 -1
- package/dist/dom/index.js.map +1 -1
- package/dist/dom/renderer.js +2 -3
- package/dist/dom/renderer.js.map +1 -1
- package/dist/variants.d.ts +90 -0
- package/dist/variants.js +206 -0
- package/dist/variants.js.map +1 -0
- package/package.json +2 -2
- package/src/canvas/QUICKSTART.md +12 -7
- package/src/canvas/README.md +8 -5
- package/src/canvas/events.ts +49 -7
- package/src/canvas/index.ts +0 -8
- package/src/canvas/layout.ts +68 -103
- package/src/canvas/paint.ts +172 -69
- package/src/canvas/renderer.ts +38 -46
- package/src/canvas/text.ts +2 -9
- package/src/canvas/types.ts +14 -0
- package/src/canvas/utils.ts +3 -69
- package/src/canvas/variants.ts +153 -0
- package/src/dom/README.md +8 -7
- package/src/dom/applicators/events.ts +1 -0
- package/src/dom/applicators/font.ts +16 -29
- package/src/dom/applicators/index.ts +56 -46
- package/src/dom/canvas/index.ts +2 -2
- package/src/dom/components/app.ts +2 -2
- package/src/dom/components/audio.ts +2 -2
- package/src/dom/components/avatar.ts +3 -4
- package/src/dom/components/badge.ts +3 -4
- package/src/dom/components/button.ts +2 -2
- package/src/dom/components/card.ts +2 -2
- package/src/dom/components/center.ts +2 -2
- package/src/dom/components/checkbox.ts +4 -5
- package/src/dom/components/column.ts +2 -2
- package/src/dom/components/container.ts +2 -2
- package/src/dom/components/divider.ts +2 -2
- package/src/dom/components/grid.ts +10 -10
- package/src/dom/components/heading.ts +4 -6
- package/src/dom/components/hypenapp.ts +2 -2
- package/src/dom/components/icon.ts +4 -5
- package/src/dom/components/image.ts +2 -2
- package/src/dom/components/index.ts +3 -3
- package/src/dom/components/input.ts +2 -2
- package/src/dom/components/link.ts +2 -2
- package/src/dom/components/list.ts +2 -2
- package/src/dom/components/paragraph.ts +3 -4
- package/src/dom/components/progressbar.ts +3 -3
- package/src/dom/components/route.ts +2 -2
- package/src/dom/components/router.ts +2 -2
- package/src/dom/components/row.ts +10 -10
- package/src/dom/components/select.ts +3 -4
- package/src/dom/components/slider.ts +2 -2
- package/src/dom/components/spacer.ts +2 -2
- package/src/dom/components/spinner.ts +4 -4
- package/src/dom/components/stack.ts +10 -10
- package/src/dom/components/switch.ts +5 -6
- package/src/dom/components/text.ts +4 -7
- package/src/dom/components/textarea.ts +2 -2
- package/src/dom/components/video.ts +2 -2
- package/src/dom/debug.ts +6 -8
- package/src/dom/index.ts +0 -8
- package/src/dom/renderer.ts +2 -3
- package/src/variants.ts +249 -0
- package/dist/client.d.ts +0 -36
- package/dist/client.js +0 -47
- package/dist/client.js.map +0 -1
- package/src/client.ts +0 -88
package/src/canvas/paint.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { renderText } from "./text.js";
|
|
|
9
9
|
import { ScrollManager, isScrollable } from "./scroll.js";
|
|
10
10
|
import { getVisibleChildren, VIRTUALIZE_THRESHOLD } from "./virtualize.js";
|
|
11
11
|
import type { SelectionManager } from "./selection.js";
|
|
12
|
-
import { cssLengthToPx
|
|
12
|
+
import { cssLengthToPx } from "./utils.js";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Module-level reference to the active SelectionManager so paintText
|
|
@@ -205,14 +205,9 @@ function paintContainer(ctx: CanvasRenderingContext2D, node: VirtualNode): void
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
// Draw background
|
|
208
|
-
const
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
if (typeof backgroundColor === "string" && backgroundColor.includes("gradient")) {
|
|
212
|
-
ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
|
|
213
|
-
} else {
|
|
214
|
-
ctx.fillStyle = backgroundColor;
|
|
215
|
-
}
|
|
208
|
+
const background = resolveBackgroundPaint(props);
|
|
209
|
+
if (background) {
|
|
210
|
+
ctx.fillStyle = resolveCanvasPaint(ctx, background, x, y, width, height);
|
|
216
211
|
|
|
217
212
|
if (radius > 0) {
|
|
218
213
|
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
@@ -271,14 +266,13 @@ function paintText(ctx: CanvasRenderingContext2D, node: VirtualNode): void {
|
|
|
271
266
|
const layout = node.layout!;
|
|
272
267
|
const props = node.props;
|
|
273
268
|
|
|
274
|
-
|
|
275
|
-
let text = String(props[0] ?? props.text ?? "");
|
|
269
|
+
let text = String(props[0] || props.text || "");
|
|
276
270
|
const color = props.color || "#000000";
|
|
277
271
|
const fontSize = cssLengthToPx(props.fontSize) ?? 16;
|
|
278
272
|
const fontWeight = props.fontWeight || "normal";
|
|
279
273
|
const fontFamily = props.fontFamily || "system-ui, sans-serif";
|
|
280
274
|
const textAlign = props.textAlign || "left";
|
|
281
|
-
const lineHeight =
|
|
275
|
+
const lineHeight = cssLengthToPx(props.lineHeight) ?? fontSize * 1.2;
|
|
282
276
|
const textDecoration = props.textDecoration || "none";
|
|
283
277
|
const textTransform = props.textTransform || "none";
|
|
284
278
|
const letterSpacing = cssLengthToPx(props.letterSpacing) ?? 0;
|
|
@@ -384,7 +378,7 @@ function paintButton(ctx: CanvasRenderingContext2D, node: VirtualNode): void {
|
|
|
384
378
|
const y = layout.y;
|
|
385
379
|
const width = layout.width;
|
|
386
380
|
const height = layout.height;
|
|
387
|
-
const radius = layout.border.radius;
|
|
381
|
+
const radius = layout.border.radius || 4;
|
|
388
382
|
|
|
389
383
|
// Apply shadow if specified
|
|
390
384
|
const shadow = props.shadow || props.boxShadow;
|
|
@@ -403,16 +397,12 @@ function paintButton(ctx: CanvasRenderingContext2D, node: VirtualNode): void {
|
|
|
403
397
|
backgroundColor = props.hoverColor;
|
|
404
398
|
} else if (node.focused && props.focusColor !== undefined) {
|
|
405
399
|
backgroundColor = props.focusColor;
|
|
406
|
-
} else
|
|
407
|
-
backgroundColor = props
|
|
400
|
+
} else {
|
|
401
|
+
backgroundColor = resolveBackgroundPaint(props);
|
|
408
402
|
}
|
|
409
403
|
|
|
410
404
|
if (backgroundColor !== undefined) {
|
|
411
|
-
|
|
412
|
-
ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
|
|
413
|
-
} else {
|
|
414
|
-
ctx.fillStyle = backgroundColor;
|
|
415
|
-
}
|
|
405
|
+
ctx.fillStyle = resolveCanvasPaint(ctx, backgroundColor, x, y, width, height);
|
|
416
406
|
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
417
407
|
ctx.fill();
|
|
418
408
|
}
|
|
@@ -450,7 +440,7 @@ function paintInput(ctx: CanvasRenderingContext2D, node: VirtualNode): void {
|
|
|
450
440
|
const y = layout.y;
|
|
451
441
|
const width = layout.width;
|
|
452
442
|
const height = layout.height;
|
|
453
|
-
const radius = layout.border.radius;
|
|
443
|
+
const radius = layout.border.radius || 4;
|
|
454
444
|
|
|
455
445
|
// Background
|
|
456
446
|
ctx.fillStyle = props.backgroundColor || "#ffffff";
|
|
@@ -475,7 +465,7 @@ function paintInput(ctx: CanvasRenderingContext2D, node: VirtualNode): void {
|
|
|
475
465
|
const fontSize = cssLengthToPx(props.fontSize) ?? 16;
|
|
476
466
|
const fontWeight = props.fontWeight || "normal";
|
|
477
467
|
const fontFamily = props.fontFamily || "system-ui, sans-serif";
|
|
478
|
-
const lineHeight =
|
|
468
|
+
const lineHeight = cssLengthToPx(props.lineHeight) ?? fontSize * 1.2;
|
|
479
469
|
|
|
480
470
|
renderText(
|
|
481
471
|
ctx,
|
|
@@ -725,14 +715,6 @@ function drawRoundedRect(
|
|
|
725
715
|
radius: number
|
|
726
716
|
): void {
|
|
727
717
|
if (radius <= 0 || width <= 0 || height <= 0) {
|
|
728
|
-
// beginPath is critical here — without it, this rect appends to the
|
|
729
|
-
// existing path and the next `ctx.fill()` re-fills every previously
|
|
730
|
-
// queued rect with the current fillStyle. That manifested as a
|
|
731
|
-
// calculator grid where every button painted in the last button's
|
|
732
|
-
// colour and only the last button's child text survived (each
|
|
733
|
-
// subsequent fill repainted over earlier children). Mirrors the
|
|
734
|
-
// beginPath() the rounded branch already does below.
|
|
735
|
-
ctx.beginPath();
|
|
736
718
|
ctx.rect(x, y, width, height);
|
|
737
719
|
return;
|
|
738
720
|
}
|
|
@@ -783,6 +765,45 @@ function applyShadow(ctx: CanvasRenderingContext2D, shadow: any): void {
|
|
|
783
765
|
}
|
|
784
766
|
}
|
|
785
767
|
|
|
768
|
+
/**
|
|
769
|
+
* Resolve DOM background applicator props into the CSS-like paint strings
|
|
770
|
+
* Canvas understands. DOM applicators map `.linearGradient("...")` to
|
|
771
|
+
* `background-image: linear-gradient(...)`; Canvas receives the original prop
|
|
772
|
+
* and must do the same translation before painting.
|
|
773
|
+
*/
|
|
774
|
+
function resolveBackgroundPaint(props: Record<string, any>): string | undefined {
|
|
775
|
+
if (props.linearGradient !== undefined) {
|
|
776
|
+
return `linear-gradient(${props.linearGradient})`;
|
|
777
|
+
}
|
|
778
|
+
if (props.radialGradient !== undefined) {
|
|
779
|
+
return `radial-gradient(${props.radialGradient})`;
|
|
780
|
+
}
|
|
781
|
+
if (props.conicGradient !== undefined) {
|
|
782
|
+
return `conic-gradient(${props.conicGradient})`;
|
|
783
|
+
}
|
|
784
|
+
if (props.backgroundImage !== undefined) {
|
|
785
|
+
return String(props.backgroundImage);
|
|
786
|
+
}
|
|
787
|
+
if (props.backgroundColor !== undefined) {
|
|
788
|
+
return String(props.backgroundColor);
|
|
789
|
+
}
|
|
790
|
+
if (props.background !== undefined) {
|
|
791
|
+
return String(props.background);
|
|
792
|
+
}
|
|
793
|
+
return undefined;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function resolveCanvasPaint(
|
|
797
|
+
ctx: CanvasRenderingContext2D,
|
|
798
|
+
paint: string,
|
|
799
|
+
x: number,
|
|
800
|
+
y: number,
|
|
801
|
+
width: number,
|
|
802
|
+
height: number,
|
|
803
|
+
): CanvasGradient | string {
|
|
804
|
+
return paint.includes("gradient(") ? parseGradient(ctx, paint, x, y, width, height) : paint;
|
|
805
|
+
}
|
|
806
|
+
|
|
786
807
|
/**
|
|
787
808
|
* Parse gradient string and create canvas gradient
|
|
788
809
|
* Supports:
|
|
@@ -803,7 +824,7 @@ function parseGradient(
|
|
|
803
824
|
const match = gradientStr.match(/linear-gradient\((.*)\)/);
|
|
804
825
|
if (!match) return gradientStr;
|
|
805
826
|
|
|
806
|
-
const parts = match[1]
|
|
827
|
+
const parts = splitCssArgs(match[1]);
|
|
807
828
|
|
|
808
829
|
// Determine direction (default to bottom)
|
|
809
830
|
let x0 = x, y0 = y, x1 = x, y1 = y + height;
|
|
@@ -813,29 +834,20 @@ function parseGradient(
|
|
|
813
834
|
colorStart = 1;
|
|
814
835
|
const direction = parts[0];
|
|
815
836
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
x1 =
|
|
822
|
-
|
|
823
|
-
y1 = y;
|
|
824
|
-
} else if (direction.includes("to top") || direction === "0deg") {
|
|
825
|
-
y0 = y + height;
|
|
826
|
-
y1 = y;
|
|
837
|
+
const angle = parseCssGradientAngle(direction);
|
|
838
|
+
if (angle !== null) {
|
|
839
|
+
const line = gradientLineForAngle(angle, x, y, width, height);
|
|
840
|
+
x0 = line.x0;
|
|
841
|
+
y0 = line.y0;
|
|
842
|
+
x1 = line.x1;
|
|
843
|
+
y1 = line.y1;
|
|
827
844
|
}
|
|
828
|
-
// Default is "to bottom" which we already set
|
|
829
845
|
}
|
|
830
846
|
|
|
831
847
|
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
|
|
832
848
|
|
|
833
849
|
// Add color stops
|
|
834
|
-
|
|
835
|
-
colors.forEach((color, i) => {
|
|
836
|
-
const stop = i / (colors.length - 1);
|
|
837
|
-
gradient.addColorStop(stop, color.trim());
|
|
838
|
-
});
|
|
850
|
+
addColorStops(gradient, parts.slice(colorStart));
|
|
839
851
|
|
|
840
852
|
return gradient;
|
|
841
853
|
} else if (gradientStr.startsWith("radial-gradient")) {
|
|
@@ -843,7 +855,7 @@ function parseGradient(
|
|
|
843
855
|
const match = gradientStr.match(/radial-gradient\((.*)\)/);
|
|
844
856
|
if (!match) return gradientStr;
|
|
845
857
|
|
|
846
|
-
const parts = match[1]
|
|
858
|
+
const parts = splitCssArgs(match[1]);
|
|
847
859
|
|
|
848
860
|
// Create radial gradient from center
|
|
849
861
|
const centerX = x + width / 2;
|
|
@@ -853,17 +865,116 @@ function parseGradient(
|
|
|
853
865
|
const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
|
|
854
866
|
|
|
855
867
|
// Add color stops
|
|
856
|
-
|
|
857
|
-
const stop = i / (parts.length - 1);
|
|
858
|
-
gradient.addColorStop(stop, color.trim());
|
|
859
|
-
});
|
|
868
|
+
addColorStops(gradient, parts);
|
|
860
869
|
|
|
870
|
+
return gradient;
|
|
871
|
+
} else if (gradientStr.startsWith("conic-gradient")) {
|
|
872
|
+
const match = gradientStr.match(/conic-gradient\((.*)\)/);
|
|
873
|
+
if (!match) return gradientStr;
|
|
874
|
+
const createConicGradient = (ctx as CanvasRenderingContext2D & {
|
|
875
|
+
createConicGradient?: (startAngle: number, x: number, y: number) => CanvasGradient;
|
|
876
|
+
}).createConicGradient;
|
|
877
|
+
if (!createConicGradient) return firstGradientColor(match[1]) ?? gradientStr;
|
|
878
|
+
const centerX = x + width / 2;
|
|
879
|
+
const centerY = y + height / 2;
|
|
880
|
+
const gradient = createConicGradient.call(ctx, 0, centerX, centerY);
|
|
881
|
+
addColorStops(gradient, splitCssArgs(match[1]));
|
|
861
882
|
return gradient;
|
|
862
883
|
}
|
|
863
884
|
|
|
864
885
|
return gradientStr;
|
|
865
886
|
}
|
|
866
887
|
|
|
888
|
+
function splitCssArgs(input: string): string[] {
|
|
889
|
+
const parts: string[] = [];
|
|
890
|
+
let depth = 0;
|
|
891
|
+
let start = 0;
|
|
892
|
+
for (let i = 0; i < input.length; i++) {
|
|
893
|
+
const ch = input[i];
|
|
894
|
+
if (ch === "(") depth++;
|
|
895
|
+
else if (ch === ")") depth = Math.max(0, depth - 1);
|
|
896
|
+
else if (ch === "," && depth === 0) {
|
|
897
|
+
const part = input.slice(start, i).trim();
|
|
898
|
+
if (part) parts.push(part);
|
|
899
|
+
start = i + 1;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const tail = input.slice(start).trim();
|
|
903
|
+
if (tail) parts.push(tail);
|
|
904
|
+
return parts;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function parseCssGradientAngle(direction: string): number | null {
|
|
908
|
+
const trimmed = direction.trim().toLowerCase();
|
|
909
|
+
const deg = trimmed.match(/^(-?(?:\d+|\d*\.\d+))deg$/);
|
|
910
|
+
if (deg) return Number(deg[1]);
|
|
911
|
+
if (!trimmed.startsWith("to ")) return null;
|
|
912
|
+
|
|
913
|
+
const words = new Set(trimmed.slice(3).split(/\s+/).filter(Boolean));
|
|
914
|
+
if (words.has("top") && words.has("right")) return 45;
|
|
915
|
+
if (words.has("right") && words.has("bottom")) return 135;
|
|
916
|
+
if (words.has("bottom") && words.has("left")) return 225;
|
|
917
|
+
if (words.has("left") && words.has("top")) return 315;
|
|
918
|
+
if (words.has("top")) return 0;
|
|
919
|
+
if (words.has("right")) return 90;
|
|
920
|
+
if (words.has("bottom")) return 180;
|
|
921
|
+
if (words.has("left")) return 270;
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function gradientLineForAngle(
|
|
926
|
+
cssDegrees: number,
|
|
927
|
+
x: number,
|
|
928
|
+
y: number,
|
|
929
|
+
width: number,
|
|
930
|
+
height: number,
|
|
931
|
+
): { x0: number; y0: number; x1: number; y1: number } {
|
|
932
|
+
const radians = (cssDegrees * Math.PI) / 180;
|
|
933
|
+
const cx = x + width / 2;
|
|
934
|
+
const cy = y + height / 2;
|
|
935
|
+
const half = Math.sqrt(width * width + height * height) / 2;
|
|
936
|
+
const dx = Math.sin(radians) * half;
|
|
937
|
+
const dy = -Math.cos(radians) * half;
|
|
938
|
+
return {
|
|
939
|
+
x0: cx - dx,
|
|
940
|
+
y0: cy - dy,
|
|
941
|
+
x1: cx + dx,
|
|
942
|
+
y1: cy + dy,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function parseColorStop(raw: string, fallbackStop: number): { color: string; stop: number } {
|
|
947
|
+
const trimmed = raw.trim();
|
|
948
|
+
const match = trimmed.match(/^(.*\S)\s+(-?(?:\d+|\d*\.\d+)%)$/);
|
|
949
|
+
if (!match) return { color: trimmed, stop: fallbackStop };
|
|
950
|
+
return {
|
|
951
|
+
color: match[1].trim(),
|
|
952
|
+
stop: Math.max(0, Math.min(1, parseFloat(match[2]) / 100)),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function addColorStops(gradient: CanvasGradient, rawStops: string[]): void {
|
|
957
|
+
const stops = rawStops.filter(Boolean);
|
|
958
|
+
if (stops.length === 0) return;
|
|
959
|
+
if (stops.length === 1) {
|
|
960
|
+
const parsed = parseColorStop(stops[0], 0);
|
|
961
|
+
gradient.addColorStop(0, parsed.color);
|
|
962
|
+
gradient.addColorStop(1, parsed.color);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
stops.forEach((raw, i) => {
|
|
966
|
+
const fallback = i / (stops.length - 1);
|
|
967
|
+
const { color, stop } = parseColorStop(raw, fallback);
|
|
968
|
+
gradient.addColorStop(stop, color);
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function firstGradientColor(input: string): string | undefined {
|
|
973
|
+
const first = splitCssArgs(input)[0];
|
|
974
|
+
if (!first) return undefined;
|
|
975
|
+
return parseColorStop(first, 0).color;
|
|
976
|
+
}
|
|
977
|
+
|
|
867
978
|
/**
|
|
868
979
|
* Apply CSS-like transforms to canvas context
|
|
869
980
|
* Supports:
|
|
@@ -1277,19 +1388,15 @@ function paintCard(ctx: CanvasRenderingContext2D, node: VirtualNode): void {
|
|
|
1277
1388
|
const y = layout.y;
|
|
1278
1389
|
const width = layout.width;
|
|
1279
1390
|
const height = layout.height;
|
|
1280
|
-
const radius = layout.border.radius;
|
|
1391
|
+
const radius = layout.border.radius || 8;
|
|
1281
1392
|
|
|
1282
1393
|
// Default card shadow
|
|
1283
1394
|
const shadow = props.shadow || props.boxShadow || "0 2 8 rgba(0,0,0,0.1)";
|
|
1284
1395
|
applyShadow(ctx, shadow);
|
|
1285
1396
|
|
|
1286
1397
|
// Background
|
|
1287
|
-
const backgroundColor = props
|
|
1288
|
-
|
|
1289
|
-
ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
|
|
1290
|
-
} else {
|
|
1291
|
-
ctx.fillStyle = backgroundColor;
|
|
1292
|
-
}
|
|
1398
|
+
const backgroundColor = resolveBackgroundPaint(props) || "#ffffff";
|
|
1399
|
+
ctx.fillStyle = resolveCanvasPaint(ctx, backgroundColor, x, y, width, height);
|
|
1293
1400
|
|
|
1294
1401
|
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
1295
1402
|
ctx.fill();
|
|
@@ -1333,8 +1440,8 @@ function paintBadge(ctx: CanvasRenderingContext2D, node: VirtualNode): void {
|
|
|
1333
1440
|
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
1334
1441
|
ctx.fill();
|
|
1335
1442
|
|
|
1336
|
-
//
|
|
1337
|
-
const text = String(props[0]
|
|
1443
|
+
// Text content
|
|
1444
|
+
const text = String(props[0] || props.text || "");
|
|
1338
1445
|
if (text) {
|
|
1339
1446
|
ctx.fillStyle = props.color || "#ffffff";
|
|
1340
1447
|
ctx.font = `${props.fontWeight || "bold"} ${props.fontSize || 10}px ${props.fontFamily || "sans-serif"}`;
|
|
@@ -1369,8 +1476,8 @@ function paintAvatar(ctx: CanvasRenderingContext2D, node: VirtualNode): void {
|
|
|
1369
1476
|
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
1370
1477
|
ctx.fill();
|
|
1371
1478
|
|
|
1372
|
-
//
|
|
1373
|
-
const text = String(props[0]
|
|
1479
|
+
// Text initials if provided
|
|
1480
|
+
const text = String(props[0] || props.text || props.initials || "");
|
|
1374
1481
|
if (text) {
|
|
1375
1482
|
ctx.fillStyle = props.color || "#ffffff";
|
|
1376
1483
|
ctx.font = `${props.fontWeight || "bold"} ${props.fontSize || size / 2.5}px ${props.fontFamily || "sans-serif"}`;
|
|
@@ -1510,8 +1617,7 @@ function paintLink(ctx: CanvasRenderingContext2D, node: VirtualNode): void {
|
|
|
1510
1617
|
const layout = node.layout!;
|
|
1511
1618
|
const props = node.props;
|
|
1512
1619
|
|
|
1513
|
-
|
|
1514
|
-
const text = String(props[0] ?? props.text ?? "");
|
|
1620
|
+
const text = String(props[0] || props.text || "");
|
|
1515
1621
|
const color = node.hovered ? (props.hoverColor || "#0056b3") : (props.color || "#007bff");
|
|
1516
1622
|
const fontSize = cssLengthToPx(props.fontSize) ?? 16;
|
|
1517
1623
|
const fontWeight = props.fontWeight || "normal";
|
|
@@ -1641,6 +1747,3 @@ function drawStar(
|
|
|
1641
1747
|
|
|
1642
1748
|
|
|
1643
1749
|
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
package/src/canvas/renderer.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
refreshApplicator,
|
|
36
36
|
parseApplicatorBase,
|
|
37
37
|
} from "./props.js";
|
|
38
|
+
import { applyVariants, invalidateVariantCache, deriveNodeComputed } from "./variants.js";
|
|
38
39
|
|
|
39
40
|
const DEFAULT_OPTIONS: CanvasRendererOptions = {
|
|
40
41
|
devicePixelRatio: typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1,
|
|
@@ -73,10 +74,6 @@ export class CanvasRenderer implements Renderer {
|
|
|
73
74
|
private rafId: number | null = null;
|
|
74
75
|
private needsRedraw = false;
|
|
75
76
|
|
|
76
|
-
private resizeObserver: ResizeObserver | null = null;
|
|
77
|
-
private dprMediaQuery: MediaQueryList | null = null;
|
|
78
|
-
private dprChangeHandler: (() => void) | null = null;
|
|
79
|
-
|
|
80
77
|
private frameCount = 0;
|
|
81
78
|
private lastFrameTime = 0;
|
|
82
79
|
|
|
@@ -142,30 +139,6 @@ export class CanvasRenderer implements Renderer {
|
|
|
142
139
|
// Eagerly initialise Taffy WASM for layout (non-blocking — fallback used until ready)
|
|
143
140
|
initTaffyLayout();
|
|
144
141
|
|
|
145
|
-
// Observe size changes — without this, the canvas backing store stays
|
|
146
|
-
// pinned at construction-time dimensions while CSS stretches the
|
|
147
|
-
// display box, and content gets bitmap-stretched / hit-testing drifts.
|
|
148
|
-
if (typeof ResizeObserver !== "undefined") {
|
|
149
|
-
this.resizeObserver = new ResizeObserver(() => {
|
|
150
|
-
this.setupHiDPI();
|
|
151
|
-
this.scheduleRedraw();
|
|
152
|
-
});
|
|
153
|
-
this.resizeObserver.observe(this.canvas);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Track DPR changes (e.g. dragging window across monitors with
|
|
157
|
-
// different scaling). matchMedia fires once per crossing.
|
|
158
|
-
if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
|
|
159
|
-
const dpr = window.devicePixelRatio || 1;
|
|
160
|
-
this.dprMediaQuery = window.matchMedia(`(resolution: ${dpr}dppx)`);
|
|
161
|
-
this.dprChangeHandler = () => {
|
|
162
|
-
this.options.devicePixelRatio = window.devicePixelRatio || 1;
|
|
163
|
-
this.setupHiDPI();
|
|
164
|
-
this.scheduleRedraw();
|
|
165
|
-
};
|
|
166
|
-
this.dprMediaQuery.addEventListener("change", this.dprChangeHandler);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
142
|
// Don't schedule initial render - wait for patches
|
|
170
143
|
}
|
|
171
144
|
|
|
@@ -176,18 +149,15 @@ export class CanvasRenderer implements Renderer {
|
|
|
176
149
|
const dpr = this.options.devicePixelRatio || 1;
|
|
177
150
|
const rect = this.canvas.getBoundingClientRect();
|
|
178
151
|
|
|
179
|
-
// Skip when the host is collapsed — a 0-pixel backing store throws
|
|
180
|
-
// in some 2D contexts and the next non-zero resize will reinitialise.
|
|
181
|
-
if (rect.width === 0 || rect.height === 0) return;
|
|
182
|
-
|
|
183
152
|
this.canvas.width = rect.width * dpr;
|
|
184
153
|
this.canvas.height = rect.height * dpr;
|
|
185
154
|
|
|
186
|
-
// Reset transform first — `scale()` is cumulative across calls, so on
|
|
187
|
-
// a resize the second call would compound (dpr²) and shrink content.
|
|
188
|
-
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
189
155
|
this.ctx.scale(dpr, dpr);
|
|
190
156
|
|
|
157
|
+
// Update canvas display size
|
|
158
|
+
this.canvas.style.width = `${rect.width}px`;
|
|
159
|
+
this.canvas.style.height = `${rect.height}px`;
|
|
160
|
+
|
|
191
161
|
// Sync dirty tracker with new canvas size
|
|
192
162
|
if (this.dirtyTracker) {
|
|
193
163
|
this.dirtyTracker.setCanvasSize(rect.width, rect.height);
|
|
@@ -325,6 +295,10 @@ export class CanvasRenderer implements Renderer {
|
|
|
325
295
|
|
|
326
296
|
node.props[name] = value;
|
|
327
297
|
|
|
298
|
+
// A new key may introduce a variant marker (e.g. `padding@md.0`) — drop the
|
|
299
|
+
// cached variant-base set so the next frame rescans.
|
|
300
|
+
invalidateVariantCache(node);
|
|
301
|
+
|
|
328
302
|
// If this is an applicator-namespaced key (e.g. `flex.0`, `onClick.to`),
|
|
329
303
|
// rebuild the derived flat/aggregate entry under the base name so layout,
|
|
330
304
|
// paint, and event dispatch see the updated value.
|
|
@@ -373,6 +347,8 @@ export class CanvasRenderer implements Renderer {
|
|
|
373
347
|
|
|
374
348
|
delete node.props[name];
|
|
375
349
|
|
|
350
|
+
invalidateVariantCache(node);
|
|
351
|
+
|
|
376
352
|
const base = parseApplicatorBase(name);
|
|
377
353
|
if (base !== null) {
|
|
378
354
|
refreshApplicator(node.props, base);
|
|
@@ -611,6 +587,20 @@ export class CanvasRenderer implements Renderer {
|
|
|
611
587
|
}
|
|
612
588
|
}
|
|
613
589
|
|
|
590
|
+
/**
|
|
591
|
+
* Re-derive cached computed fields that may have been overridden by variant
|
|
592
|
+
* resolution. `node.opacity` is read straight off `node.props.opacity` in
|
|
593
|
+
* paint, but it is cached on the node at create/setProp time — after a
|
|
594
|
+
* variant pass changes `props.opacity` (e.g. `opacity:disabled`), the cache
|
|
595
|
+
* must be refreshed or the paint would use the stale value.
|
|
596
|
+
*/
|
|
597
|
+
private refreshComputedProps(node: VirtualNode): void {
|
|
598
|
+
deriveNodeComputed(node);
|
|
599
|
+
for (const child of node.children) {
|
|
600
|
+
this.refreshComputedProps(child);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
614
604
|
/**
|
|
615
605
|
* Full canvas repaint (default behavior when dirty rects disabled)
|
|
616
606
|
*/
|
|
@@ -626,10 +616,17 @@ export class CanvasRenderer implements Renderer {
|
|
|
626
616
|
|
|
627
617
|
// Layout and paint
|
|
628
618
|
if (this.rootNode) {
|
|
619
|
+
// Resolve responsive + state variants against the current content width
|
|
620
|
+
// BEFORE layout so spacing/size winners feed the layout engine and
|
|
621
|
+
// colour/opacity winners feed paint.
|
|
622
|
+
const contentWidth = this.canvas.width / dpr;
|
|
623
|
+
applyVariants(this.rootNode, contentWidth);
|
|
624
|
+
this.refreshComputedProps(this.rootNode);
|
|
625
|
+
|
|
629
626
|
computeLayout(
|
|
630
627
|
this.ctx,
|
|
631
628
|
this.rootNode,
|
|
632
|
-
|
|
629
|
+
contentWidth,
|
|
633
630
|
this.canvas.height / dpr
|
|
634
631
|
);
|
|
635
632
|
|
|
@@ -649,10 +646,14 @@ export class CanvasRenderer implements Renderer {
|
|
|
649
646
|
private renderWithDirtyRects(dpr: number): void {
|
|
650
647
|
// Always run layout so nodes have up-to-date bounds
|
|
651
648
|
if (this.rootNode) {
|
|
649
|
+
const contentWidth = this.canvas.width / dpr;
|
|
650
|
+
applyVariants(this.rootNode, contentWidth);
|
|
651
|
+
this.refreshComputedProps(this.rootNode);
|
|
652
|
+
|
|
652
653
|
computeLayout(
|
|
653
654
|
this.ctx,
|
|
654
655
|
this.rootNode,
|
|
655
|
-
|
|
656
|
+
contentWidth,
|
|
656
657
|
this.canvas.height / dpr
|
|
657
658
|
);
|
|
658
659
|
ScrollManager.updateScrollBounds(this.rootNode);
|
|
@@ -764,15 +765,6 @@ export class CanvasRenderer implements Renderer {
|
|
|
764
765
|
if (this.rafId !== null) {
|
|
765
766
|
cancelAnimationFrame(this.rafId);
|
|
766
767
|
}
|
|
767
|
-
if (this.resizeObserver) {
|
|
768
|
-
this.resizeObserver.disconnect();
|
|
769
|
-
this.resizeObserver = null;
|
|
770
|
-
}
|
|
771
|
-
if (this.dprMediaQuery && this.dprChangeHandler) {
|
|
772
|
-
this.dprMediaQuery.removeEventListener("change", this.dprChangeHandler);
|
|
773
|
-
this.dprMediaQuery = null;
|
|
774
|
-
this.dprChangeHandler = null;
|
|
775
|
-
}
|
|
776
768
|
this.eventManager.destroy();
|
|
777
769
|
this.scrollManager.destroy();
|
|
778
770
|
this.selectionManager.destroy();
|
package/src/canvas/text.ts
CHANGED
|
@@ -191,13 +191,7 @@ export function renderText(
|
|
|
191
191
|
ctx.save();
|
|
192
192
|
ctx.font = font;
|
|
193
193
|
ctx.fillStyle = style.color;
|
|
194
|
-
|
|
195
|
-
// of its line box. With `top`, canvas draws from the top of the EM square
|
|
196
|
-
// and any extra `line-height − font-size` space lands as padding BELOW the
|
|
197
|
-
// glyph — visible as `+` inside a round FAB drifting toward the top of
|
|
198
|
-
// the circle. CSS centres glyphs within the line box; matching that
|
|
199
|
-
// requires `middle` + half-lineHeight offset.
|
|
200
|
-
ctx.textBaseline = "middle";
|
|
194
|
+
ctx.textBaseline = "top";
|
|
201
195
|
|
|
202
196
|
const metrics = measureText(ctx, text, style, width, maxLines, textOverflow);
|
|
203
197
|
|
|
@@ -212,8 +206,7 @@ export function renderText(
|
|
|
212
206
|
// Render each line
|
|
213
207
|
for (let i = 0; i < metrics.lines.length; i++) {
|
|
214
208
|
const line = metrics.lines[i];
|
|
215
|
-
|
|
216
|
-
const lineY = startY + i * metrics.lineHeight + metrics.lineHeight / 2;
|
|
209
|
+
const lineY = startY + i * metrics.lineHeight;
|
|
217
210
|
|
|
218
211
|
// Calculate X based on text alignment
|
|
219
212
|
let lineX = x;
|
package/src/canvas/types.ts
CHANGED
|
@@ -27,6 +27,20 @@ export interface VirtualNode {
|
|
|
27
27
|
|
|
28
28
|
// Scroll state (managed by ScrollManager, not serialised)
|
|
29
29
|
scrollState?: ScrollState;
|
|
30
|
+
|
|
31
|
+
// Pointer-pressed (active) state, tracked by the event manager from
|
|
32
|
+
// mousedown/mouseup so `:active` paint variants can resolve.
|
|
33
|
+
pressed?: boolean;
|
|
34
|
+
|
|
35
|
+
// --- Variant resolution bookkeeping (managed by applyVariants) ---
|
|
36
|
+
// Set of applicator base names that have at least one `@bp`/`:state` variant
|
|
37
|
+
// key on this node. Computed lazily; null means "not yet scanned", an empty
|
|
38
|
+
// set means "scanned, no variants".
|
|
39
|
+
variantBases?: Set<string> | null;
|
|
40
|
+
// Snapshot of the node's original (variant-free) base values, captured the
|
|
41
|
+
// first time a variant override is applied so each frame resolves from the
|
|
42
|
+
// un-overridden base instead of compounding overrides.
|
|
43
|
+
variantOriginals?: Record<string, unknown>;
|
|
30
44
|
}
|
|
31
45
|
|
|
32
46
|
export interface Layout {
|
package/src/canvas/utils.ts
CHANGED
|
@@ -12,42 +12,6 @@ import type { BoxSpacing, Rectangle, Point, VirtualNode } from "./types.js";
|
|
|
12
12
|
// and treating `3.5rem` as 3.5px, collapsing whole layouts to 1/16 size.
|
|
13
13
|
const ROOT_FONT_PX = 16;
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
* Active layout viewport — used to resolve `vw` / `vh` correctly during a
|
|
17
|
-
* layout pass. Set by `computeLayout` (layout.ts) at entry and cleared on
|
|
18
|
-
* exit. Layout traversal is synchronous, so a single module-level slot is
|
|
19
|
-
* sufficient and avoids threading a `viewport` parameter through every
|
|
20
|
-
* `cssLengthToPx` call site (there are dozens).
|
|
21
|
-
*
|
|
22
|
-
* Falls back to `window.innerWidth` / `window.innerHeight` outside a layout
|
|
23
|
-
* pass (e.g. for direct callers that resolve sizes ahead of layout). When
|
|
24
|
-
* neither is available — e.g. headless tests — `vw`/`vh` degrade to the
|
|
25
|
-
* pre-fix unitless-pixel behavior.
|
|
26
|
-
*/
|
|
27
|
-
let currentViewport: { width: number; height: number } | null = null;
|
|
28
|
-
|
|
29
|
-
export function setCurrentViewport(viewport: { width: number; height: number } | null): void {
|
|
30
|
-
currentViewport = viewport;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function getCurrentViewport(): { width: number; height: number } | null {
|
|
34
|
-
return currentViewport;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function resolveViewportPx(unit: "vw" | "vh", n: number): number {
|
|
38
|
-
if (currentViewport) {
|
|
39
|
-
const dim = unit === "vw" ? currentViewport.width : currentViewport.height;
|
|
40
|
-
return (n / 100) * dim;
|
|
41
|
-
}
|
|
42
|
-
if (typeof window !== "undefined") {
|
|
43
|
-
const dim = unit === "vw" ? window.innerWidth : window.innerHeight;
|
|
44
|
-
if (Number.isFinite(dim) && dim > 0) return (n / 100) * dim;
|
|
45
|
-
}
|
|
46
|
-
// Last resort: treat as unitless px so the layout still gets a numeric
|
|
47
|
-
// value (matches pre-fix behavior in the no-viewport case).
|
|
48
|
-
return n;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
15
|
/**
|
|
52
16
|
* Parse a CSS length string (or number) into pixels.
|
|
53
17
|
* Returns `null` for `"auto"` or unparseable input; returns `null` for `%`
|
|
@@ -69,8 +33,10 @@ export function cssLengthToPx(value: any): number | null {
|
|
|
69
33
|
case "px": return n;
|
|
70
34
|
case "rem":
|
|
71
35
|
case "em": return n * ROOT_FONT_PX;
|
|
36
|
+
// We don't have a viewport reference here; treat vw/vh as unitless px
|
|
37
|
+
// fallback so the layout gets _some_ numeric value rather than zero.
|
|
72
38
|
case "vw":
|
|
73
|
-
case "vh": return
|
|
39
|
+
case "vh": return n;
|
|
74
40
|
// Typographic point — 1/72 inch at CSS reference density (96dpi).
|
|
75
41
|
case "pt": return n * (96 / 72);
|
|
76
42
|
// Density-independent (dp) and scale-independent (sp) logical pixels
|
|
@@ -86,38 +52,6 @@ export function cssLengthToPx(value: any): number | null {
|
|
|
86
52
|
}
|
|
87
53
|
}
|
|
88
54
|
|
|
89
|
-
/**
|
|
90
|
-
* Resolve a CSS `line-height` value to a pixel value.
|
|
91
|
-
*
|
|
92
|
-
* Unitless numbers (e.g. `1`, `1.5`, `"1.25"`) are CSS multipliers of the
|
|
93
|
-
* font-size — `line-height: 1` (Tailwind's `leading-none`) means line box =
|
|
94
|
-
* 1 × font-size, NOT 1 pixel. Without this branch, `cssLengthToPx("1")`
|
|
95
|
-
* returned literally `1`, and a Column of `leading-none` Texts collapsed
|
|
96
|
-
* each line to 1px tall — emoji + label in a bottom-tab button rendered at
|
|
97
|
-
* the same y and overlapped.
|
|
98
|
-
*
|
|
99
|
-
* Anything with an explicit unit (`16px`, `1.5em`, `1rem`, `120%` is
|
|
100
|
-
* rejected → null since Taffy/measure expects px) falls through to
|
|
101
|
-
* `cssLengthToPx`.
|
|
102
|
-
*/
|
|
103
|
-
export function resolveLineHeight(value: any, fontSize: number): number | null {
|
|
104
|
-
if (value === undefined || value === null) return null;
|
|
105
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
106
|
-
// Unitless number: multiplier of font-size (matches CSS spec).
|
|
107
|
-
return value * fontSize;
|
|
108
|
-
}
|
|
109
|
-
if (typeof value === "string") {
|
|
110
|
-
const s = value.trim();
|
|
111
|
-
if (s === "" || s === "auto" || s === "normal") return null;
|
|
112
|
-
// Pure numeric string with no unit → unitless multiplier.
|
|
113
|
-
if (/^-?\d*\.?\d+$/.test(s)) {
|
|
114
|
-
const n = parseFloat(s);
|
|
115
|
-
return Number.isFinite(n) ? n * fontSize : null;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return cssLengthToPx(value);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
55
|
/**
|
|
122
56
|
* Parse a CSS length string for Taffy's Dimension type. Keeps `%` values as
|
|
123
57
|
* the tagged percentage string, turns `"auto"`/empty/invalid into `"auto"`,
|