Mission Control Dashboard - Initial implementation

This commit is contained in:
Daniel Arroyo
2026-03-27 18:36:05 +00:00
parent 257cea2c7d
commit a8fb4d4555
12516 changed files with 2307128 additions and 2 deletions

View File

@@ -0,0 +1,73 @@
import { buildHTMLStyles } from '../../html/utils/build-styles.mjs';
import { buildSVGPath } from './path.mjs';
/**
* CSS Motion Path properties that should remain as CSS styles on SVG elements.
*/
const cssMotionPathProperties = [
"offsetDistance",
"offsetPath",
"offsetRotate",
"offsetAnchor",
];
/**
* Build SVG visual attributes, like cx and style.transform
*/
function buildSVGAttrs(state, { attrX, attrY, attrScale, pathLength, pathSpacing = 1, pathOffset = 0,
// This is object creation, which we try to avoid per-frame.
...latest }, isSVGTag, transformTemplate, styleProp) {
buildHTMLStyles(state, latest, transformTemplate);
/**
* For svg tags we just want to make sure viewBox is animatable and treat all the styles
* as normal HTML tags.
*/
if (isSVGTag) {
if (state.style.viewBox) {
state.attrs.viewBox = state.style.viewBox;
}
return;
}
state.attrs = state.style;
state.style = {};
const { attrs, style } = state;
/**
* However, we apply transforms as CSS transforms.
* So if we detect a transform, transformOrigin we take it from attrs and copy it into style.
*/
if (attrs.transform) {
style.transform = attrs.transform;
delete attrs.transform;
}
if (style.transform || attrs.transformOrigin) {
style.transformOrigin = attrs.transformOrigin ?? "50% 50%";
delete attrs.transformOrigin;
}
if (style.transform) {
/**
* SVG's element transform-origin uses its own median as a reference.
* Therefore, transformBox becomes a fill-box
*/
style.transformBox = styleProp?.transformBox ?? "fill-box";
delete attrs.transformBox;
}
for (const key of cssMotionPathProperties) {
if (attrs[key] !== undefined) {
style[key] = attrs[key];
delete attrs[key];
}
}
// Render attrX/attrY/attrScale as attributes
if (attrX !== undefined)
attrs.x = attrX;
if (attrY !== undefined)
attrs.y = attrY;
if (attrScale !== undefined)
attrs.scale = attrScale;
// Build SVG path if one has been defined
if (pathLength !== undefined) {
buildSVGPath(attrs, pathLength, pathSpacing, pathOffset, false);
}
}
export { buildSVGAttrs };
//# sourceMappingURL=build-attrs.mjs.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"build-attrs.mjs","sources":["../../../../../src/render/svg/utils/build-attrs.ts"],"sourcesContent":["import type { MotionNodeOptions } from \"../../../node/types\"\nimport { buildHTMLStyles } from \"../../html/utils/build-styles\"\nimport { ResolvedValues } from \"../../types\"\nimport { SVGRenderState } from \"../types\"\nimport { buildSVGPath } from \"./path\"\n\n/**\n * CSS Motion Path properties that should remain as CSS styles on SVG elements.\n */\nconst cssMotionPathProperties = [\n \"offsetDistance\",\n \"offsetPath\",\n \"offsetRotate\",\n \"offsetAnchor\",\n]\n\n/**\n * Build SVG visual attributes, like cx and style.transform\n */\nexport function buildSVGAttrs(\n state: SVGRenderState,\n {\n attrX,\n attrY,\n attrScale,\n pathLength,\n pathSpacing = 1,\n pathOffset = 0,\n // This is object creation, which we try to avoid per-frame.\n ...latest\n }: ResolvedValues,\n isSVGTag: boolean,\n transformTemplate?: MotionNodeOptions[\"transformTemplate\"],\n styleProp?: Record<string, any>\n) {\n buildHTMLStyles(state, latest, transformTemplate)\n\n /**\n * For svg tags we just want to make sure viewBox is animatable and treat all the styles\n * as normal HTML tags.\n */\n if (isSVGTag) {\n if (state.style.viewBox) {\n state.attrs.viewBox = state.style.viewBox\n }\n return\n }\n\n state.attrs = state.style\n state.style = {}\n const { attrs, style } = state\n\n /**\n * However, we apply transforms as CSS transforms.\n * So if we detect a transform, transformOrigin we take it from attrs and copy it into style.\n */\n if (attrs.transform) {\n style.transform = attrs.transform\n delete attrs.transform\n }\n if (style.transform || attrs.transformOrigin) {\n style.transformOrigin = attrs.transformOrigin ?? \"50% 50%\"\n delete attrs.transformOrigin\n }\n\n if (style.transform) {\n /**\n * SVG's element transform-origin uses its own median as a reference.\n * Therefore, transformBox becomes a fill-box\n */\n style.transformBox = (styleProp?.transformBox as string) ?? \"fill-box\"\n delete attrs.transformBox\n }\n\n for (const key of cssMotionPathProperties) {\n if (attrs[key] !== undefined) {\n style[key] = attrs[key]\n delete attrs[key]\n }\n }\n\n // Render attrX/attrY/attrScale as attributes\n if (attrX !== undefined) attrs.x = attrX\n if (attrY !== undefined) attrs.y = attrY\n if (attrScale !== undefined) attrs.scale = attrScale\n\n // Build SVG path if one has been defined\n if (pathLength !== undefined) {\n buildSVGPath(\n attrs,\n pathLength as number,\n pathSpacing as number,\n pathOffset as number,\n false\n )\n }\n}\n"],"names":[],"mappings":";;;AAMA;;AAEG;AACH,MAAM,uBAAuB,GAAG;IAC5B,gBAAgB;IAChB,YAAY;IACZ,cAAc;IACd,cAAc;CACjB;AAED;;AAEG;AACG,SAAU,aAAa,CACzB,KAAqB,EACrB,EACI,KAAK,EACL,KAAK,EACL,SAAS,EACT,UAAU,EACV,WAAW,GAAG,CAAC,EACf,UAAU,GAAG,CAAC;AACd;AACA,GAAG,MAAM,EACI,EACjB,QAAiB,EACjB,iBAA0D,EAC1D,SAA+B,EAAA;AAE/B,IAAA,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,iBAAiB,CAAC;AAEjD;;;AAGG;IACH,IAAI,QAAQ,EAAE;AACV,QAAA,IAAI,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE;YACrB,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,OAAO;QAC7C;QACA;IACJ;AAEA,IAAA,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK;AACzB,IAAA,KAAK,CAAC,KAAK,GAAG,EAAE;AAChB,IAAA,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,KAAK;AAE9B;;;AAGG;AACH,IAAA,IAAI,KAAK,CAAC,SAAS,EAAE;AACjB,QAAA,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,SAAS;QACjC,OAAO,KAAK,CAAC,SAAS;IAC1B;IACA,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,eAAe,EAAE;QAC1C,KAAK,CAAC,eAAe,GAAG,KAAK,CAAC,eAAe,IAAI,SAAS;QAC1D,OAAO,KAAK,CAAC,eAAe;IAChC;AAEA,IAAA,IAAI,KAAK,CAAC,SAAS,EAAE;AACjB;;;AAGG;QACH,KAAK,CAAC,YAAY,GAAI,SAAS,EAAE,YAAuB,IAAI,UAAU;QACtE,OAAO,KAAK,CAAC,YAAY;IAC7B;AAEA,IAAA,KAAK,MAAM,GAAG,IAAI,uBAAuB,EAAE;AACvC,QAAA,IAAI,KAAK,CAAC,GAAG,CAAC,KAAK,SAAS,EAAE;YAC1B,KAAK,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC;AACvB,YAAA,OAAO,KAAK,CAAC,GAAG,CAAC;QACrB;IACJ;;IAGA,IAAI,KAAK,KAAK,SAAS;AAAE,QAAA,KAAK,CAAC,CAAC,GAAG,KAAK;IACxC,IAAI,KAAK,KAAK,SAAS;AAAE,QAAA,KAAK,CAAC,CAAC,GAAG,KAAK;IACxC,IAAI,SAAS,KAAK,SAAS;AAAE,QAAA,KAAK,CAAC,KAAK,GAAG,SAAS;;AAGpD,IAAA,IAAI,UAAU,KAAK,SAAS,EAAE;QAC1B,YAAY,CACR,KAAK,EACL,UAAoB,EACpB,WAAqB,EACrB,UAAoB,EACpB,KAAK,CACR;IACL;AACJ;;;;"}

View File

@@ -0,0 +1,31 @@
/**
* A set of attribute names that are always read/written as camel case.
*/
const camelCaseAttributes = new Set([
"baseFrequency",
"diffuseConstant",
"kernelMatrix",
"kernelUnitLength",
"keySplines",
"keyTimes",
"limitingConeAngle",
"markerHeight",
"markerWidth",
"numOctaves",
"targetX",
"targetY",
"surfaceScale",
"specularConstant",
"specularExponent",
"stdDeviation",
"tableValues",
"viewBox",
"gradientTransform",
"pathLength",
"startOffset",
"textLength",
"lengthAdjust",
]);
export { camelCaseAttributes };
//# sourceMappingURL=camel-case-attrs.mjs.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"camel-case-attrs.mjs","sources":["../../../../../src/render/svg/utils/camel-case-attrs.ts"],"sourcesContent":["/**\n * A set of attribute names that are always read/written as camel case.\n */\nexport const camelCaseAttributes = new Set([\n \"baseFrequency\",\n \"diffuseConstant\",\n \"kernelMatrix\",\n \"kernelUnitLength\",\n \"keySplines\",\n \"keyTimes\",\n \"limitingConeAngle\",\n \"markerHeight\",\n \"markerWidth\",\n \"numOctaves\",\n \"targetX\",\n \"targetY\",\n \"surfaceScale\",\n \"specularConstant\",\n \"specularExponent\",\n \"stdDeviation\",\n \"tableValues\",\n \"viewBox\",\n \"gradientTransform\",\n \"pathLength\",\n \"startOffset\",\n \"textLength\",\n \"lengthAdjust\",\n])\n"],"names":[],"mappings":"AAAA;;AAEG;AACI,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC;IACvC,eAAe;IACf,iBAAiB;IACjB,cAAc;IACd,kBAAkB;IAClB,YAAY;IACZ,UAAU;IACV,mBAAmB;IACnB,cAAc;IACd,aAAa;IACb,YAAY;IACZ,SAAS;IACT,SAAS;IACT,cAAc;IACd,kBAAkB;IAClB,kBAAkB;IAClB,cAAc;IACd,aAAa;IACb,SAAS;IACT,mBAAmB;IACnB,YAAY;IACZ,aAAa;IACb,YAAY;IACZ,cAAc;AACjB,CAAA;;;;"}

View File

@@ -0,0 +1,4 @@
const isSVGTag = (tag) => typeof tag === "string" && tag.toLowerCase() === "svg";
export { isSVGTag };
//# sourceMappingURL=is-svg-tag.mjs.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"is-svg-tag.mjs","sources":["../../../../../src/render/svg/utils/is-svg-tag.ts"],"sourcesContent":["export const isSVGTag = (tag: unknown) =>\n typeof tag === \"string\" && tag.toLowerCase() === \"svg\"\n"],"names":[],"mappings":"MAAa,QAAQ,GAAG,CAAC,GAAY,KACjC,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK;;;;"}

View File

@@ -0,0 +1,32 @@
const dashKeys = {
offset: "stroke-dashoffset",
array: "stroke-dasharray",
};
const camelKeys = {
offset: "strokeDashoffset",
array: "strokeDasharray",
};
/**
* Build SVG path properties. Uses the path's measured length to convert
* our custom pathLength, pathSpacing and pathOffset into stroke-dashoffset
* and stroke-dasharray attributes.
*
* This function is mutative to reduce per-frame GC.
*
* Note: We use unitless values for stroke-dasharray and stroke-dashoffset
* because Safari incorrectly scales px values when the page is zoomed.
*/
function buildSVGPath(attrs, length, spacing = 1, offset = 0, useDashCase = true) {
// Normalise path length by setting SVG attribute pathLength to 1
attrs.pathLength = 1;
// We use dash case when setting attributes directly to the DOM node and camel case
// when defining props on a React component.
const keys = useDashCase ? dashKeys : camelKeys;
// Build the dash offset (unitless to avoid Safari zoom bug)
attrs[keys.offset] = `${-offset}`;
// Build the dash array (unitless to avoid Safari zoom bug)
attrs[keys.array] = `${length} ${spacing}`;
}
export { buildSVGPath };
//# sourceMappingURL=path.mjs.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"path.mjs","sources":["../../../../../src/render/svg/utils/path.ts"],"sourcesContent":["import { ResolvedValues } from \"../../types\"\n\nconst dashKeys = {\n offset: \"stroke-dashoffset\",\n array: \"stroke-dasharray\",\n}\n\nconst camelKeys = {\n offset: \"strokeDashoffset\",\n array: \"strokeDasharray\",\n}\n\n/**\n * Build SVG path properties. Uses the path's measured length to convert\n * our custom pathLength, pathSpacing and pathOffset into stroke-dashoffset\n * and stroke-dasharray attributes.\n *\n * This function is mutative to reduce per-frame GC.\n *\n * Note: We use unitless values for stroke-dasharray and stroke-dashoffset\n * because Safari incorrectly scales px values when the page is zoomed.\n */\nexport function buildSVGPath(\n attrs: ResolvedValues,\n length: number,\n spacing = 1,\n offset = 0,\n useDashCase: boolean = true\n): void {\n // Normalise path length by setting SVG attribute pathLength to 1\n attrs.pathLength = 1\n\n // We use dash case when setting attributes directly to the DOM node and camel case\n // when defining props on a React component.\n const keys = useDashCase ? dashKeys : camelKeys\n\n // Build the dash offset (unitless to avoid Safari zoom bug)\n attrs[keys.offset] = `${-offset}`\n\n // Build the dash array (unitless to avoid Safari zoom bug)\n attrs[keys.array] = `${length} ${spacing}`\n}\n"],"names":[],"mappings":"AAEA,MAAM,QAAQ,GAAG;AACb,IAAA,MAAM,EAAE,mBAAmB;AAC3B,IAAA,KAAK,EAAE,kBAAkB;CAC5B;AAED,MAAM,SAAS,GAAG;AACd,IAAA,MAAM,EAAE,kBAAkB;AAC1B,IAAA,KAAK,EAAE,iBAAiB;CAC3B;AAED;;;;;;;;;AASG;SACa,YAAY,CACxB,KAAqB,EACrB,MAAc,EACd,OAAO,GAAG,CAAC,EACX,MAAM,GAAG,CAAC,EACV,cAAuB,IAAI,EAAA;;AAG3B,IAAA,KAAK,CAAC,UAAU,GAAG,CAAC;;;IAIpB,MAAM,IAAI,GAAG,WAAW,GAAG,QAAQ,GAAG,SAAS;;IAG/C,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAA,EAAG,CAAC,MAAM,CAAA,CAAE;;IAGjC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,OAAO,CAAA,CAAE;AAC9C;;;;"}

View File

@@ -0,0 +1,13 @@
import { camelToDash } from '../../dom/utils/camel-to-dash.mjs';
import { renderHTML } from '../../html/utils/render.mjs';
import { camelCaseAttributes } from './camel-case-attrs.mjs';
function renderSVG(element, renderState, _styleProp, projection) {
renderHTML(element, renderState, undefined, projection);
for (const key in renderState.attrs) {
element.setAttribute(!camelCaseAttributes.has(key) ? camelToDash(key) : key, renderState.attrs[key]);
}
}
export { renderSVG };
//# sourceMappingURL=render.mjs.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"render.mjs","sources":["../../../../../src/render/svg/utils/render.ts"],"sourcesContent":["import type { MotionStyle } from \"../../VisualElement\"\nimport { camelToDash } from \"../../dom/utils/camel-to-dash\"\nimport { renderHTML } from \"../../html/utils/render\"\nimport { SVGRenderState } from \"../types\"\nimport { camelCaseAttributes } from \"./camel-case-attrs\"\n\nexport function renderSVG(\n element: SVGElement,\n renderState: SVGRenderState,\n _styleProp?: MotionStyle,\n projection?: any\n) {\n renderHTML(element as any, renderState, undefined, projection)\n\n for (const key in renderState.attrs) {\n element.setAttribute(\n !camelCaseAttributes.has(key) ? camelToDash(key) : key,\n renderState.attrs[key] as string\n )\n }\n}\n"],"names":[],"mappings":";;;;AAMM,SAAU,SAAS,CACrB,OAAmB,EACnB,WAA2B,EAC3B,UAAwB,EACxB,UAAgB,EAAA;IAEhB,UAAU,CAAC,OAAc,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,CAAC;AAE9D,IAAA,KAAK,MAAM,GAAG,IAAI,WAAW,CAAC,KAAK,EAAE;AACjC,QAAA,OAAO,CAAC,YAAY,CAChB,CAAC,mBAAmB,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,GAAG,EACtD,WAAW,CAAC,KAAK,CAAC,GAAG,CAAW,CACnC;IACL;AACJ;;;;"}

View File

@@ -0,0 +1,20 @@
import { isMotionValue } from '../../../value/utils/is-motion-value.mjs';
import { transformPropOrder } from '../../utils/keys-transform.mjs';
import { scrapeMotionValuesFromProps as scrapeMotionValuesFromProps$1 } from '../../html/utils/scrape-motion-values.mjs';
function scrapeMotionValuesFromProps(props, prevProps, visualElement) {
const newValues = scrapeMotionValuesFromProps$1(props, prevProps, visualElement);
for (const key in props) {
if (isMotionValue(props[key]) ||
isMotionValue(prevProps[key])) {
const targetKey = transformPropOrder.indexOf(key) !== -1
? "attr" + key.charAt(0).toUpperCase() + key.substring(1)
: key;
newValues[targetKey] = props[key];
}
}
return newValues;
}
export { scrapeMotionValuesFromProps };
//# sourceMappingURL=scrape-motion-values.mjs.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"scrape-motion-values.mjs","sources":["../../../../../src/render/svg/utils/scrape-motion-values.ts"],"sourcesContent":["import { isMotionValue } from \"../../../value/utils/is-motion-value\"\nimport type { MotionNodeOptions } from \"../../../node/types\"\nimport { transformPropOrder } from \"../../utils/keys-transform\"\nimport { scrapeMotionValuesFromProps as scrapeHTMLMotionValuesFromProps } from \"../../html/utils/scrape-motion-values\"\nimport type { VisualElement } from \"../../VisualElement\"\n\nexport function scrapeMotionValuesFromProps(\n props: MotionNodeOptions,\n prevProps: MotionNodeOptions,\n visualElement?: VisualElement\n) {\n const newValues = scrapeHTMLMotionValuesFromProps(\n props,\n prevProps,\n visualElement\n )\n\n for (const key in props) {\n if (\n isMotionValue(props[key as keyof typeof props]) ||\n isMotionValue(prevProps[key as keyof typeof prevProps])\n ) {\n const targetKey =\n transformPropOrder.indexOf(key) !== -1\n ? \"attr\" + key.charAt(0).toUpperCase() + key.substring(1)\n : key\n\n newValues[targetKey] = props[key as keyof typeof props]\n }\n }\n\n return newValues\n}\n"],"names":["scrapeHTMLMotionValuesFromProps"],"mappings":";;;;SAMgB,2BAA2B,CACvC,KAAwB,EACxB,SAA4B,EAC5B,aAA6B,EAAA;IAE7B,MAAM,SAAS,GAAGA,6BAA+B,CAC7C,KAAK,EACL,SAAS,EACT,aAAa,CAChB;AAED,IAAA,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE;AACrB,QAAA,IACI,aAAa,CAAC,KAAK,CAAC,GAAyB,CAAC,CAAC;AAC/C,YAAA,aAAa,CAAC,SAAS,CAAC,GAA6B,CAAC,CAAC,EACzD;YACE,MAAM,SAAS,GACX,kBAAkB,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK;AAChC,kBAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC;kBACtD,GAAG;YAEb,SAAS,CAAC,SAAS,CAAC,GAAG,KAAK,CAAC,GAAyB,CAAC;QAC3D;IACJ;AAEA,IAAA,OAAO,SAAS;AACpB;;;;"}