mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
fix(studio): make flat inspector's Stroke width free of style-name typing
Stroke width committed border-width and border-style together from one free-text field, so setting a style meant typing an exact CSS keyword (e.g. "dashed") with no indication of which ones were valid — the row also duplicated the discoverable Stroke style select directly below it. Stroke width now only commits border-width; style changes go exclusively through the existing select.
This commit is contained in:
@@ -1,23 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatStrokeSummary, parseStrokeSummary } from "./propertyPanelFlatStyleHelpers";
|
||||
|
||||
describe("formatStrokeSummary", () => {
|
||||
it("formats width and style into one string", () => {
|
||||
expect(formatStrokeSummary(1, "solid")).toBe("1px solid");
|
||||
expect(formatStrokeSummary(2.5, "dashed")).toBe("2.5px dashed");
|
||||
expect(formatStrokeSummary(0, "none")).toBe("0px none");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseStrokeSummary", () => {
|
||||
it("parses a well-formed summary back into width and style", () => {
|
||||
expect(parseStrokeSummary("1px solid")).toEqual({ widthPx: 1, style: "solid" });
|
||||
expect(parseStrokeSummary(" 2.5px dashed ")).toEqual({ widthPx: 2.5, style: "dashed" });
|
||||
});
|
||||
|
||||
it("returns null for unparseable input", () => {
|
||||
expect(parseStrokeSummary("garbage")).toBeNull();
|
||||
expect(parseStrokeSummary("")).toBeNull();
|
||||
expect(parseStrokeSummary("1px")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -12,16 +12,3 @@ export const STROKE_STYLE_OPTIONS: string[] = [
|
||||
"inset",
|
||||
"outset",
|
||||
];
|
||||
|
||||
export function formatStrokeSummary(widthPx: number, style: string): string {
|
||||
return `${widthPx}px ${style}`;
|
||||
}
|
||||
|
||||
export function parseStrokeSummary(text: string): { widthPx: number; style: string } | null {
|
||||
const match = /^\s*(-?\d+(?:\.\d+)?)px\s+(\S+)\s*$/.exec(text);
|
||||
if (!match) return null;
|
||||
const widthPx = Number.parseFloat(match[1]);
|
||||
const style = match[2];
|
||||
if (!Number.isFinite(widthPx) || !style) return null;
|
||||
return { widthPx, style };
|
||||
}
|
||||
|
||||
@@ -175,31 +175,43 @@ function setInputValue(input: HTMLInputElement, nextValue: string) {
|
||||
}
|
||||
|
||||
describe("FlatStyleSection — Stroke and Radius", () => {
|
||||
it("renders the combined stroke row and commits width+style together on blur", () => {
|
||||
it("renders a width-only Stroke width row — style is only ever set through the Stroke style select below it", () => {
|
||||
const { host, root } = renderSection(STROKE_STYLES);
|
||||
expect(host.textContent).toContain("Stroke");
|
||||
expect(getFlatRowInput(host, "Stroke").value).toBe("1px solid");
|
||||
expect(host.textContent).toContain("Stroke width");
|
||||
expect(host.textContent).toContain("Stroke style");
|
||||
expect(getFlatRowInput(host, "Stroke width").value).toBe("1px");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("commits the stroke row's new width and style together on blur", async () => {
|
||||
it("commits a new stroke width without touching the existing style", async () => {
|
||||
const { host, root, onSetStyle } = renderSection(STROKE_STYLES);
|
||||
await commitFlatRowInput(host, "Stroke", "2px dashed");
|
||||
await commitFlatRowInput(host, "Stroke width", "2px");
|
||||
expect(onSetStyle).toHaveBeenCalledWith("border-width", "2px");
|
||||
expect(onSetStyle).toHaveBeenCalledWith("border-style", "dashed");
|
||||
expect(onSetStyle).not.toHaveBeenCalledWith("border-style", expect.anything());
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("defaults a from-zero width commit to solid, since a width with no style renders no visible border", async () => {
|
||||
const { host, root, onSetStyle } = renderSection({
|
||||
"border-width": "0px",
|
||||
"border-style": "none",
|
||||
"border-color": "rgba(255,255,255,.12)",
|
||||
});
|
||||
await commitFlatRowInput(host, "Stroke width", "3px");
|
||||
expect(onSetStyle).toHaveBeenCalledWith("border-style", "solid");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("clamps an out-of-range stroke width commit to 200px (fix 2)", async () => {
|
||||
const { host, root, onSetStyle } = renderSection(STROKE_STYLES);
|
||||
await commitFlatRowInput(host, "Stroke", "9999px solid");
|
||||
await commitFlatRowInput(host, "Stroke width", "9999px");
|
||||
expect(onSetStyle).toHaveBeenCalledWith("border-width", "200px");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("rejects a stroke commit whose style token is not a valid border-style (fix 2)", async () => {
|
||||
it("ignores an unparseable stroke width commit", async () => {
|
||||
const { host, root, onSetStyle } = renderSection(STROKE_STYLES);
|
||||
await commitFlatRowInput(host, "Stroke", "12px bogus");
|
||||
await commitFlatRowInput(host, "Stroke width", "bogus");
|
||||
expect(onSetStyle).not.toHaveBeenCalled();
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
@@ -3,11 +3,7 @@ import { useEffect, useState } from "react";
|
||||
import { isTextEditableSelection, type DomEditSelection } from "./domEditing";
|
||||
import { buildDefaultGradientModel, serializeGradient } from "./gradientValue";
|
||||
import { BorderRadiusEditor } from "./BorderRadiusEditor";
|
||||
import {
|
||||
formatStrokeSummary,
|
||||
parseStrokeSummary,
|
||||
STROKE_STYLE_OPTIONS,
|
||||
} from "./propertyPanelFlatStyleHelpers";
|
||||
import { STROKE_STYLE_OPTIONS } from "./propertyPanelFlatStyleHelpers";
|
||||
import {
|
||||
buildBoxShadowPresetValue,
|
||||
buildClipPathValue,
|
||||
@@ -147,10 +143,9 @@ function FlatFillFields({
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Flat Stroke row — combined width+style+color */
|
||||
/* Flat Stroke row — width (numeric), style (select), color */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
function FlatStrokeRow({
|
||||
styles,
|
||||
disabled,
|
||||
@@ -167,52 +162,40 @@ function FlatStrokeRow({
|
||||
const borderStyleValue = styles["border-style"] || styles["border-top-style"] || "none";
|
||||
const borderColorValue =
|
||||
styles["border-color"] || styles["border-top-color"] || "rgba(255, 255, 255, 0.18)";
|
||||
const summary = formatStrokeSummary(borderWidthValue, borderStyleValue);
|
||||
const tier = resolveValueTier(
|
||||
styles["border-width"] != null || styles["border-style"] != null ? summary : undefined,
|
||||
formatStrokeSummary(0, "none"),
|
||||
);
|
||||
const widthDisplay = formatPxMetricValue(borderWidthValue);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FlatRow
|
||||
label="Stroke"
|
||||
value={summary}
|
||||
tier={tier}
|
||||
label="Stroke width"
|
||||
value={widthDisplay}
|
||||
tier={resolveValueTier(styles["border-width"], "0px")}
|
||||
disabled={disabled}
|
||||
onCommit={async (next) => {
|
||||
const parsed = parseStrokeSummary(next);
|
||||
if (!parsed) return;
|
||||
if (!STROKE_STYLE_OPTIONS.includes(parsed.style)) return;
|
||||
const normalizedWidth = normalizePanelPxValue(`${parsed.widthPx}px`, {
|
||||
const normalizedWidth = normalizePanelPxValue(next, {
|
||||
min: 0,
|
||||
max: 200,
|
||||
fallback: borderWidthValue,
|
||||
});
|
||||
if (!normalizedWidth) return;
|
||||
// buildStrokeWidthStyleUpdates already covers "a width typed in
|
||||
// from 0 needs a style to actually render a visible border" —
|
||||
// it only defaults to solid when the current style is none/hidden,
|
||||
// never clobbering an already-chosen style (dashed, dotted, …).
|
||||
for (const [property, value] of buildStrokeWidthStyleUpdates(
|
||||
normalizedWidth,
|
||||
parsed.style,
|
||||
borderStyleValue,
|
||||
)) {
|
||||
await onSetStyle(property, value);
|
||||
}
|
||||
for (const [property, value] of buildStrokeStyleUpdates(parsed.style, normalizedWidth)) {
|
||||
await onSetStyle(property, value);
|
||||
}
|
||||
}}
|
||||
suffix={
|
||||
<>
|
||||
<span
|
||||
className="h-4 w-4 flex-shrink-0 rounded border border-panel-border-input"
|
||||
style={{ backgroundColor: borderColorValue }}
|
||||
/>
|
||||
<span className="font-mono text-[10px] text-panel-text-3">{borderColorValue}</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<FlatSelectRow
|
||||
label="Stroke style"
|
||||
value={borderStyleValue}
|
||||
// Valid border-style keywords — the ONLY way to set this, since a
|
||||
// free-text field here would require typing an exact CSS keyword
|
||||
// (e.g. "dashed") with no indication of which ones are valid.
|
||||
options={STROKE_STYLE_OPTIONS}
|
||||
tier={resolveValueTier(styles["border-style"], "none")}
|
||||
disabled={disabled}
|
||||
|
||||
Reference in New Issue
Block a user