/** CSS border styles that may be resolved from the themed Page surface. */ export const WIDGET_FRAME_RESOLVED_BORDER_STYLES = [ "none", "hidden", "dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", ] as const; export type WidgetFrameResolvedBorderStyle = (typeof WIDGET_FRAME_RESOLVED_BORDER_STYLES)[number]; export type WidgetFrameResolvedSrgbColor = { colorSpace: "srgb"; /** Integer sRGB channel in the inclusive range 0-255. */ red: number; /** Integer sRGB channel in the inclusive range 0-255. */ green: number; /** Integer sRGB channel in the inclusive range 0-255. */ blue: number; /** Alpha channel in the inclusive range 0-1. */ alpha: number; }; export type WidgetFrameResolvedBorderColor = WidgetFrameResolvedSrgbColor; export type WidgetFrameResolvedBorder = { color: WidgetFrameResolvedSrgbColor; style: WidgetFrameResolvedBorderStyle; /** Resolved CSS pixel width. */ widthPx: number; }; export type WidgetFrameResolvedBorderRadiusCorner = { /** Resolved horizontal radius in CSS pixels. */ horizontalPx: number; /** Resolved vertical radius in CSS pixels. */ verticalPx: number; }; /** Effective border-box corner radii after relative lengths and overlap scaling. */ export type WidgetFrameResolvedBorderRadius = { topLeft: WidgetFrameResolvedBorderRadiusCorner; topRight: WidgetFrameResolvedBorderRadiusCorner; bottomRight: WidgetFrameResolvedBorderRadiusCorner; bottomLeft: WidgetFrameResolvedBorderRadiusCorner; }; /** One resolved layer from the CSS `box-shadow` list, in declaration order. */ export type WidgetFrameResolvedBoxShadowLayer = { color: WidgetFrameResolvedSrgbColor; /** Resolved horizontal offset in CSS pixels. */ offsetXPx: number; /** Resolved vertical offset in CSS pixels. */ offsetYPx: number; /** Resolved non-negative blur radius in CSS pixels. */ blurRadiusPx: number; /** Resolved spread radius in CSS pixels. */ spreadRadiusPx: number; inset: boolean; }; /** Read-only runtime frame appearance state derived from the themed Page surface. */ export type WidgetFrameAppearance = | { kind: "none"; } | { kind: "page"; border: string; borderRadius: string; boxShadow: string; resolvedBorder?: WidgetFrameResolvedBorder; resolvedBorderRadius?: WidgetFrameResolvedBorderRadius; /** Omitted when unresolved; an empty list explicitly represents `box-shadow: none`. */ resolvedBoxShadow?: WidgetFrameResolvedBoxShadowLayer[]; }; const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value); const hasOnlyKeys = ( value: Record, keys: readonly string[], ): boolean => { const valueKeys = Object.keys(value); return ( valueKeys.length === keys.length && keys.every((key) => Object.hasOwn(value, key)) ); }; const isFiniteNumber = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value); const isRgbChannel = (value: unknown): value is number => isFiniteNumber(value) && Number.isInteger(value) && value >= 0 && value <= 255; const isAlpha = (value: unknown): value is number => isFiniteNumber(value) && value >= 0 && value <= 1; const isWidgetFrameResolvedBorderStyle = ( value: unknown, ): value is WidgetFrameResolvedBorderStyle => typeof value === "string" && WIDGET_FRAME_RESOLVED_BORDER_STYLES.some((style) => style === value); const isWidgetFrameResolvedSrgbColor = ( value: unknown, ): value is WidgetFrameResolvedSrgbColor => { if ( !isRecord(value) || !hasOnlyKeys(value, ["colorSpace", "red", "green", "blue", "alpha"]) ) { return false; } return ( value.colorSpace === "srgb" && isRgbChannel(value.red) && isRgbChannel(value.green) && isRgbChannel(value.blue) && isAlpha(value.alpha) ); }; const isWidgetFrameResolvedBorder = ( value: unknown, ): value is WidgetFrameResolvedBorder => { if (!isRecord(value) || !hasOnlyKeys(value, ["color", "style", "widthPx"])) { return false; } return ( isWidgetFrameResolvedSrgbColor(value.color) && isWidgetFrameResolvedBorderStyle(value.style) && isFiniteNumber(value.widthPx) && value.widthPx >= 0 ); }; const isWidgetFrameResolvedBorderRadiusCorner = ( value: unknown, ): value is WidgetFrameResolvedBorderRadiusCorner => isRecord(value) && hasOnlyKeys(value, ["horizontalPx", "verticalPx"]) && isFiniteNumber(value.horizontalPx) && value.horizontalPx >= 0 && isFiniteNumber(value.verticalPx) && value.verticalPx >= 0; const isWidgetFrameResolvedBorderRadius = ( value: unknown, ): value is WidgetFrameResolvedBorderRadius => isRecord(value) && hasOnlyKeys(value, ["topLeft", "topRight", "bottomRight", "bottomLeft"]) && isWidgetFrameResolvedBorderRadiusCorner(value.topLeft) && isWidgetFrameResolvedBorderRadiusCorner(value.topRight) && isWidgetFrameResolvedBorderRadiusCorner(value.bottomRight) && isWidgetFrameResolvedBorderRadiusCorner(value.bottomLeft); const isWidgetFrameResolvedBoxShadowLayer = ( value: unknown, ): value is WidgetFrameResolvedBoxShadowLayer => isRecord(value) && hasOnlyKeys(value, [ "color", "offsetXPx", "offsetYPx", "blurRadiusPx", "spreadRadiusPx", "inset", ]) && isWidgetFrameResolvedSrgbColor(value.color) && isFiniteNumber(value.offsetXPx) && isFiniteNumber(value.offsetYPx) && isFiniteNumber(value.blurRadiusPx) && value.blurRadiusPx >= 0 && isFiniteNumber(value.spreadRadiusPx) && typeof value.inset === "boolean"; const isWidgetFrameResolvedBoxShadow = ( value: unknown, ): value is WidgetFrameResolvedBoxShadowLayer[] => { if (!Array.isArray(value) || Object.keys(value).length !== value.length) { return false; } for (let index = 0; index < value.length; index += 1) { if ( !Object.hasOwn(value, index) || !isWidgetFrameResolvedBoxShadowLayer(value[index]) ) { return false; } } return true; }; export function isWidgetFrameAppearance( value: unknown, ): value is WidgetFrameAppearance { if (!isRecord(value)) { return false; } if (value.kind === "none") { return hasOnlyKeys(value, ["kind"]); } const requiredPageKeys = [ "kind", "border", "borderRadius", "boxShadow", ] as const; const optionalPageKeys = [ "resolvedBorder", "resolvedBorderRadius", "resolvedBoxShadow", ] as const; const allowedPageKeys = [...requiredPageKeys, ...optionalPageKeys]; const pageKeys = Object.keys(value); const hasRequiredPageKeys = requiredPageKeys.every((key) => pageKeys.some((pageKey) => pageKey === key), ); const hasOnlyPageKeys = pageKeys.every((key) => allowedPageKeys.some((allowedKey) => allowedKey === key), ); const hasResolvedBorder = Object.hasOwn(value, "resolvedBorder"); const hasResolvedBorderRadius = Object.hasOwn(value, "resolvedBorderRadius"); const hasEnumerableResolvedBorder = pageKeys.includes("resolvedBorder"); const hasEnumerableResolvedBorderRadius = pageKeys.includes( "resolvedBorderRadius", ); const hasResolvedBoxShadow = Object.hasOwn(value, "resolvedBoxShadow"); const hasEnumerableResolvedBoxShadow = pageKeys.includes("resolvedBoxShadow"); return ( value.kind === "page" && hasRequiredPageKeys && hasOnlyPageKeys && typeof value.border === "string" && typeof value.borderRadius === "string" && typeof value.boxShadow === "string" && (hasResolvedBorder ? hasEnumerableResolvedBorder && (value.resolvedBorder === undefined || isWidgetFrameResolvedBorder(value.resolvedBorder)) : value.resolvedBorder === undefined) && (hasResolvedBorderRadius ? hasEnumerableResolvedBorderRadius && (value.resolvedBorderRadius === undefined || isWidgetFrameResolvedBorderRadius(value.resolvedBorderRadius)) : value.resolvedBorderRadius === undefined) && (hasResolvedBoxShadow ? hasEnumerableResolvedBoxShadow && (value.resolvedBoxShadow === undefined || isWidgetFrameResolvedBoxShadow(value.resolvedBoxShadow)) : value.resolvedBoxShadow === undefined) ); } const areWidgetFrameResolvedSrgbColorsEqual = ( a: WidgetFrameResolvedSrgbColor, b: WidgetFrameResolvedSrgbColor, ): boolean => a.colorSpace === b.colorSpace && a.red === b.red && a.green === b.green && a.blue === b.blue && a.alpha === b.alpha; const areWidgetFrameResolvedBordersEqual = ( a: WidgetFrameResolvedBorder | undefined, b: WidgetFrameResolvedBorder | undefined, ): boolean => { if (a === undefined || b === undefined) { return a === b; } return ( areWidgetFrameResolvedSrgbColorsEqual(a.color, b.color) && a.style === b.style && a.widthPx === b.widthPx ); }; const areWidgetFrameResolvedBorderRadiusCornersEqual = ( a: WidgetFrameResolvedBorderRadiusCorner, b: WidgetFrameResolvedBorderRadiusCorner, ): boolean => a.horizontalPx === b.horizontalPx && a.verticalPx === b.verticalPx; const areWidgetFrameResolvedBorderRadiiEqual = ( a: WidgetFrameResolvedBorderRadius | undefined, b: WidgetFrameResolvedBorderRadius | undefined, ): boolean => { if (a === undefined || b === undefined) { return a === b; } return ( areWidgetFrameResolvedBorderRadiusCornersEqual(a.topLeft, b.topLeft) && areWidgetFrameResolvedBorderRadiusCornersEqual(a.topRight, b.topRight) && areWidgetFrameResolvedBorderRadiusCornersEqual( a.bottomRight, b.bottomRight, ) && areWidgetFrameResolvedBorderRadiusCornersEqual(a.bottomLeft, b.bottomLeft) ); }; const areWidgetFrameResolvedBoxShadowLayersEqual = ( a: WidgetFrameResolvedBoxShadowLayer, b: WidgetFrameResolvedBoxShadowLayer, ): boolean => areWidgetFrameResolvedSrgbColorsEqual(a.color, b.color) && a.offsetXPx === b.offsetXPx && a.offsetYPx === b.offsetYPx && a.blurRadiusPx === b.blurRadiusPx && a.spreadRadiusPx === b.spreadRadiusPx && a.inset === b.inset; const areWidgetFrameResolvedBoxShadowsEqual = ( a: WidgetFrameResolvedBoxShadowLayer[] | undefined, b: WidgetFrameResolvedBoxShadowLayer[] | undefined, ): boolean => { if (a === undefined || b === undefined) { return a === b; } return ( a.length === b.length && a.every((layer, index) => { const otherLayer = b[index]; return ( otherLayer !== undefined && areWidgetFrameResolvedBoxShadowLayersEqual(layer, otherLayer) ); }) ); }; export function areWidgetFrameAppearancesEqual( a: WidgetFrameAppearance | undefined, b: WidgetFrameAppearance | undefined, ): boolean { if (a === undefined || b === undefined) { return a === b; } if (a.kind !== b.kind) { return false; } if (a.kind === "none") { return true; } if (b.kind !== "page") { return false; } return ( a.border === b.border && a.borderRadius === b.borderRadius && a.boxShadow === b.boxShadow && areWidgetFrameResolvedBordersEqual(a.resolvedBorder, b.resolvedBorder) && areWidgetFrameResolvedBorderRadiiEqual( a.resolvedBorderRadius, b.resolvedBorderRadius, ) && areWidgetFrameResolvedBoxShadowsEqual( a.resolvedBoxShadow, b.resolvedBoxShadow, ) ); } export function cloneWidgetFrameAppearance( appearance: WidgetFrameAppearance, ): WidgetFrameAppearance; export function cloneWidgetFrameAppearance( appearance: WidgetFrameAppearance | undefined, ): WidgetFrameAppearance | undefined; export function cloneWidgetFrameAppearance( appearance: WidgetFrameAppearance | undefined, ): WidgetFrameAppearance | undefined { if (appearance === undefined || appearance.kind === "none") { return appearance === undefined ? undefined : { kind: "none" }; } return { ...appearance, ...(appearance.resolvedBorder === undefined ? {} : { resolvedBorder: { ...appearance.resolvedBorder, color: { ...appearance.resolvedBorder.color }, }, }), ...(appearance.resolvedBorderRadius === undefined ? {} : { resolvedBorderRadius: { topLeft: { ...appearance.resolvedBorderRadius.topLeft }, topRight: { ...appearance.resolvedBorderRadius.topRight }, bottomRight: { ...appearance.resolvedBorderRadius.bottomRight }, bottomLeft: { ...appearance.resolvedBorderRadius.bottomLeft }, }, }), ...(appearance.resolvedBoxShadow === undefined ? {} : { resolvedBoxShadow: appearance.resolvedBoxShadow.map((layer) => ({ ...layer, color: { ...layer.color }, })), }), }; }