Merge pull request #526 from PR0M3TH3AN/codex/add-dynamic-styles-and-refactor-utilities

Add dynamic design-system stylesheet helper
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-14 13:34:52 -04:00
commit f003359b18
8 files changed
+507 -36

No files matched your search

+12
View File
@@ -121,6 +121,18 @@
@apply flex items-center justify-start py-md;
}
.ds-metric-probe {
position: absolute;
top: 0;
left: 0;
visibility: hidden;
pointer-events: none;
contain: layout size style;
inline-size: 0;
block-size: var(--ds-metric-probe-length, 0);
overflow: hidden;
}
.ds-header__logo {
@apply h-24 w-auto max-w-none;
}
+20
View File
@@ -12,6 +12,26 @@ The new design system ships behind the `FEATURE_DESIGN_SYSTEM` runtime flag defi
- **Runtime toggle:** Set `window.__BITVID_RUNTIME_FLAGS__.FEATURE_DESIGN_SYSTEM = false` (or call `setFeatureDesignSystemEnabled(false)`) to temporarily fall back to the legacy primitives. The entrypoint automatically updates every `[data-ds]` container to `data-ds="new"` and notifies controllers.
- **DOM contract:** Templates and partials must include `data-ds` on their root elements. Controllers should rely on the `designSystem` context (see `js/designSystem.js`) before attaching classes or behaviors that only exist in the new system.
## Dynamic runtime styling
Inline styles are blocked in production builds, so runtime components that need bespoke coordinates or measurements must route their overrides through `js/designSystem/dynamicStyles.js`.
- `registerScope(id, selectors)` creates (or reuses) a constructable stylesheet when supported, allocates a unique identifier, and injects empty rules scoped to `[data-ds-style-id="<id>"]`. Callers attach the attribute to any element that should inherit those rules.
- `setVariables(id, tokens)` updates CSS custom properties on the registered scope. Pass CSS variable names (`"--floating-fallback-top"`) and string or number values—`setVariables` handles serialization and removes entries when they resolve to empty strings.
- `releaseScope(id)` removes the generated rules and frees the identifier. Always invoke it from component teardown paths to avoid leaking rules between renders.
The floating panel utility and design-system metrics both lean on this helper:
- `createFloatingPanelStyles` assigns each popover a unique scope and publishes fallback coordinates through CSS variables instead of mutating `.style`. Positioning logic reads and writes the same API, and teardown releases the scope before removing dataset attributes.
- `readDesignTokenAsPixels` measures non-`px`/`rem` tokens with an offscreen probe that ships in core CSS (`.ds-metric-probe`). The probe is registered through the helper so dynamic lengths are fed into `--ds-metric-probe-length` without touching inline styles.
When introducing a new runtime feature that requires dynamic styling:
1. Register a dedicated scope during setup and attach the returned attribute to the relevant DOM node.
2. Update custom properties with `setVariables` instead of touching `.style` or appending `<style>` tags.
3. Clean up by calling `releaseScope` when the component unmounts.
4. Prefer reusable primitives (like `.ds-metric-probe`) for offscreen measurement or layout shims so multiple components can share the same CSS.
## Theming
### `[data-theme]` contract
+276
View File
@@ -0,0 +1,276 @@
const DATA_ATTRIBUTE = "data-ds-style-id";
const DEFAULT_SELECTOR = ":scope";
const SCOPE_REGISTRY = new Map();
const DOCUMENT_MANAGERS = new WeakMap();
function sanitizeScopeId(baseId) {
if (typeof baseId !== "string") {
return "ds-scope";
}
const trimmed = baseId.trim();
if (!trimmed) {
return "ds-scope";
}
const normalized = trimmed.replace(/[^a-zA-Z0-9_-]+/g, "-");
return normalized || "ds-scope";
}
function resolveDocument(documentRef) {
if (documentRef && typeof documentRef === "object" && documentRef.nodeType === 9) {
return documentRef;
}
if (typeof globalThis.document === "object" && globalThis.document?.nodeType === 9) {
return globalThis.document;
}
return null;
}
function supportsConstructableStylesheets(doc) {
return (
!!doc &&
Array.isArray(doc.adoptedStyleSheets) &&
typeof doc.adoptedStyleSheets.splice === "function" &&
typeof globalThis.CSSStyleSheet === "function"
);
}
function ensureManager(doc) {
if (!doc) {
return null;
}
let manager = DOCUMENT_MANAGERS.get(doc);
if (manager) {
return manager;
}
let sheet = null;
let styleElement = null;
const constructable = supportsConstructableStylesheets(doc);
if (constructable) {
sheet = new globalThis.CSSStyleSheet();
try {
doc.adoptedStyleSheets = [...doc.adoptedStyleSheets, sheet];
} catch (error) {
// Fall back to a <style> element if adoption fails at runtime.
sheet = null;
}
}
if (!sheet) {
styleElement = doc.createElement("style");
styleElement.setAttribute("data-ds-dynamic", "true");
doc.head?.appendChild(styleElement);
sheet = styleElement.sheet || null;
}
manager = {
document: doc,
sheet,
styleElement,
counter: 0,
scopes: new Map(),
};
DOCUMENT_MANAGERS.set(doc, manager);
return manager;
}
function generateScopeId(baseId, manager) {
const prefix = sanitizeScopeId(baseId);
if (!SCOPE_REGISTRY.has(prefix) && !manager.scopes.has(prefix)) {
return prefix;
}
let suffix = 1;
let candidate = `${prefix}-${suffix}`;
while (SCOPE_REGISTRY.has(candidate) || manager.scopes.has(candidate)) {
suffix += 1;
candidate = `${prefix}-${suffix}`;
}
return candidate;
}
function buildSelector(scopeId, selector) {
const token = `[${DATA_ATTRIBUTE}="${scopeId}"]`;
if (typeof selector !== "string" || !selector.trim()) {
return token;
}
if (selector.includes("&")) {
return selector.replace(/&/g, token);
}
if (selector.includes(":scope")) {
return selector.replace(/:scope/g, token);
}
const trimmed = selector.trim();
if (/^[>+~]/.test(trimmed)) {
return `${token}${trimmed}`;
}
return `${token} ${trimmed}`;
}
function insertEmptyRule(sheet, selectorText) {
if (!sheet || typeof sheet.insertRule !== "function") {
return null;
}
try {
const index = sheet.insertRule(`${selectorText} {}`, sheet.cssRules.length);
return sheet.cssRules[index] || null;
} catch (error) {
return null;
}
}
function findRuleIndex(sheet, rule) {
if (!sheet || !rule) {
return -1;
}
const { cssRules } = sheet;
for (let index = 0; index < cssRules.length; index += 1) {
if (cssRules[index] === rule) {
return index;
}
}
return -1;
}
function normalizeSelectors(selectors) {
if (!Array.isArray(selectors)) {
return [DEFAULT_SELECTOR];
}
const filtered = selectors
.map((selector) => (typeof selector === "string" ? selector.trim() : ""))
.filter((selector) => selector !== "");
if (filtered.length === 0) {
return [DEFAULT_SELECTOR];
}
return filtered;
}
export function registerScope(baseId, selectors = [DEFAULT_SELECTOR], options = {}) {
const doc = resolveDocument(options.documentRef);
const manager = ensureManager(doc);
if (!manager || !manager.sheet) {
return null;
}
const scopeId = generateScopeId(baseId, manager);
const normalizedSelectors = normalizeSelectors(selectors);
const records = [];
for (const selector of normalizedSelectors) {
const selectorText = buildSelector(scopeId, selector);
const rule = insertEmptyRule(manager.sheet, selectorText);
if (rule) {
records.push({ selector, rule });
}
}
const scopeRecord = {
id: scopeId,
manager,
rules: records,
};
manager.scopes.set(scopeId, scopeRecord);
SCOPE_REGISTRY.set(scopeId, scopeRecord);
return scopeId;
}
function normalizeVariableValue(value) {
if (value === undefined || value === null) {
return "";
}
if (typeof value === "number" && Number.isFinite(value)) {
return `${value}`;
}
if (typeof value === "string") {
return value.trim();
}
return "";
}
export function setVariables(scopeId, variables = {}, { selector = DEFAULT_SELECTOR } = {}) {
if (!scopeId || typeof variables !== "object" || variables === null) {
return false;
}
const scope = SCOPE_REGISTRY.get(scopeId);
if (!scope || !scope.manager?.sheet) {
return false;
}
const targetRecord = scope.rules.find((record) => record.selector === selector) ||
scope.rules.find((record) => record.selector === DEFAULT_SELECTOR);
if (!targetRecord) {
return false;
}
const rule = targetRecord.rule;
const ruleStyle = rule ? rule["style"] : null;
if (!ruleStyle) {
return false;
}
let didUpdate = false;
for (const [name, rawValue] of Object.entries(variables)) {
if (typeof name !== "string" || !name.trim().startsWith("--")) {
continue;
}
const value = normalizeVariableValue(rawValue);
if (value) {
ruleStyle.setProperty(name.trim(), value);
} else {
ruleStyle.removeProperty(name.trim());
}
didUpdate = true;
}
return didUpdate;
}
export function releaseScope(scopeId) {
if (!scopeId) {
return false;
}
const scope = SCOPE_REGISTRY.get(scopeId);
if (!scope || !scope.manager?.sheet) {
return false;
}
const { sheet } = scope.manager;
let removed = false;
for (const record of scope.rules) {
const rule = record.rule;
if (!rule) {
continue;
}
const index = findRuleIndex(sheet, rule);
if (index >= 0) {
try {
sheet.deleteRule(index);
removed = true;
} catch (error) {
// Ignore deletion failures.
}
}
}
scope.manager.scopes.delete(scopeId);
SCOPE_REGISTRY.delete(scopeId);
return removed;
}
export function getScopeAttributeName() {
return DATA_ATTRIBUTE;
}
export default {
registerScope,
setVariables,
releaseScope,
getScopeAttributeName,
};
+61 -22
View File
@@ -1,3 +1,9 @@
import {
registerScope,
setVariables,
getScopeAttributeName,
} from "./dynamicStyles.js";
const TOKEN_FALLBACKS = {
"--grid-card-min-width": "20rem",
"--popover-backdrop-blur": "12px",
@@ -7,8 +13,13 @@ const TOKEN_FALLBACKS = {
"--popup-padding-block": "0.75rem",
};
const PROBE_SCOPE_BASE = "metric-probe";
const PROBE_LENGTH_VAR = "--ds-metric-probe-length";
const PROBE_CACHE = new WeakMap();
function resolveDocument(documentRef) {
if (documentRef && typeof documentRef === "object") {
if (documentRef && typeof documentRef === "object" && documentRef.nodeType === 9) {
return documentRef;
}
return typeof globalThis.document === "object" ? globalThis.document : null;
@@ -60,10 +71,10 @@ export function readDesignToken(tokenName, { documentRef } = {}) {
}
}
if (doc?.documentElement?.style) {
const inlineValue = normalizeTokenValue(
doc.documentElement.style.getPropertyValue(token),
);
const root = doc?.documentElement || null;
const inlineStyles = root ? root["style"] : null;
if (inlineStyles) {
const inlineValue = normalizeTokenValue(inlineStyles.getPropertyValue(token));
if (inlineValue) {
return inlineValue;
}
@@ -77,6 +88,47 @@ function parsePixelValue(value) {
return Number.isFinite(numeric) ? numeric : 0;
}
function ensureProbe(documentRef) {
const doc = resolveDocument(documentRef);
if (!doc?.body) {
return null;
}
let record = PROBE_CACHE.get(doc);
if (record?.element?.isConnected) {
return record;
}
const element = doc.createElement("div");
element.className = "ds-metric-probe";
const scopeId = registerScope(PROBE_SCOPE_BASE, [":scope"], { documentRef: doc });
if (scopeId) {
element.setAttribute(getScopeAttributeName(), scopeId);
setVariables(scopeId, { [PROBE_LENGTH_VAR]: "0" });
}
doc.body.appendChild(element);
record = { element, scopeId };
PROBE_CACHE.set(doc, record);
return record;
}
function measureWithProbe(value, documentRef) {
const record = ensureProbe(documentRef);
if (!record?.element) {
return 0;
}
if (record.scopeId) {
setVariables(record.scopeId, { [PROBE_LENGTH_VAR]: value });
}
const measured = record.element.offsetHeight;
if (record.scopeId) {
setVariables(record.scopeId, { [PROBE_LENGTH_VAR]: "0" });
}
return Number.isFinite(measured) ? measured : 0;
}
function convertLengthToPixels(value, documentRef) {
const raw = normalizeTokenValue(value);
if (!raw) {
@@ -107,23 +159,9 @@ function convertLengthToPixels(value, documentRef) {
return rem * base;
}
const doc = resolveDocument(documentRef);
if (doc?.createElement && doc?.body) {
try {
const probe = doc.createElement("div");
probe.style.position = "absolute";
probe.style.visibility = "hidden";
probe.style.width = "0";
probe.style.height = raw;
doc.body.appendChild(probe);
const measured = probe.offsetHeight;
doc.body.removeChild(probe);
if (Number.isFinite(measured)) {
return measured;
}
} catch (error) {
// Ignore measurement errors and fall through to numeric parsing.
}
const measured = measureWithProbe(raw, documentRef);
if (measured > 0) {
return measured;
}
return parsePixelValue(raw);
@@ -206,3 +244,4 @@ export const designMetrics = {
};
export default designMetrics;
+42 -10
View File
@@ -1,3 +1,10 @@
import {
registerScope,
setVariables,
releaseScope,
getScopeAttributeName,
} from "../../designSystem/dynamicStyles.js";
const DEFAULT_STATE = {
placement: "bottom",
alignment: "start",
@@ -10,6 +17,8 @@ const DEFAULT_STATE = {
const FALLBACK_TOP_VAR = "--floating-fallback-top";
const FALLBACK_LEFT_VAR = "--floating-fallback-left";
const SCOPE_BASE_ID = "floating-panel";
const SCOPE_SELECTORS = [":scope"];
function toCssPixel(value) {
if (Number.isFinite(value)) {
@@ -36,18 +45,36 @@ function ensureDatasetValue(element, key, value) {
element.dataset[key] = value;
}
function applyFallbackVariables(scopeId, variables) {
if (!scopeId) {
return;
}
setVariables(scopeId, variables);
}
export function createFloatingPanelStyles(panel) {
const safePanel = panel || null;
const state = { ...DEFAULT_STATE };
const documentRef = safePanel?.ownerDocument || null;
const scopeId = safePanel
? registerScope(SCOPE_BASE_ID, SCOPE_SELECTORS, { documentRef })
: null;
if (safePanel) {
safePanel.dataset.floatingPanel = "true";
safePanel.style.setProperty(FALLBACK_TOP_VAR, "auto");
safePanel.style.setProperty(FALLBACK_LEFT_VAR, "auto");
if (scopeId) {
safePanel.setAttribute(getScopeAttributeName(), scopeId);
applyFallbackVariables(scopeId, {
[FALLBACK_TOP_VAR]: "auto",
[FALLBACK_LEFT_VAR]: "auto",
});
}
}
const api = {
element: safePanel,
scopeId,
setPlacement(value) {
state.placement = typeof value === "string" && value ? value : DEFAULT_STATE.placement;
ensureDatasetValue(safePanel, "floatingPlacement", state.placement);
@@ -91,9 +118,11 @@ export function createFloatingPanelStyles(panel) {
setFallbackPosition({ top, left }) {
state.fallbackTop = Number.isFinite(top) ? Number(top) : null;
state.fallbackLeft = Number.isFinite(left) ? Number(left) : null;
if (safePanel) {
safePanel.style.setProperty(FALLBACK_TOP_VAR, toCssPixel(state.fallbackTop));
safePanel.style.setProperty(FALLBACK_LEFT_VAR, toCssPixel(state.fallbackLeft));
if (scopeId) {
applyFallbackVariables(scopeId, {
[FALLBACK_TOP_VAR]: toCssPixel(state.fallbackTop),
[FALLBACK_LEFT_VAR]: toCssPixel(state.fallbackLeft),
});
}
return { ...api.getFallbackPosition() };
},
@@ -104,14 +133,17 @@ export function createFloatingPanelStyles(panel) {
};
},
teardown() {
if (scopeId) {
applyFallbackVariables(scopeId, {
[FALLBACK_TOP_VAR]: "auto",
[FALLBACK_LEFT_VAR]: "auto",
});
releaseScope(scopeId);
}
if (!safePanel) {
return;
}
safePanel.style.removeProperty(FALLBACK_TOP_VAR);
safePanel.style.removeProperty(FALLBACK_LEFT_VAR);
safePanel.style.removeProperty("right");
safePanel.style.removeProperty("bottom");
safePanel.style.removeProperty("position");
safePanel.removeAttribute(getScopeAttributeName());
delete safePanel.dataset.floatingPanel;
delete safePanel.dataset.floatingPlacement;
delete safePanel.dataset.floatingAlignment;
+1 -4
View File
@@ -39,10 +39,7 @@ const IGNORED_FILES = new Set([
"sw.min.js",
]);
const VIOLATION_ALLOWLIST = new Map([
["js/ui/utils/floatingPanelStyles.js", new Set(["Direct .style usage"])],
["js/designSystem/metrics.js", new Set(["Direct .style usage"])],
]);
const VIOLATION_ALLOWLIST = new Map();
const TEXT_EXTENSIONS = new Set([
".astro",
+61
View File
@@ -0,0 +1,61 @@
import test from "node:test";
import assert from "node:assert/strict";
import { JSDOM } from "jsdom";
import {
readDesignTokenAsPixels,
} from "../js/designSystem/metrics.js";
function getScopeRule(document, scopeId) {
const styleEl = document.querySelector('style[data-ds-dynamic="true"]');
const rules = [];
if (styleEl?.sheet?.cssRules) {
rules.push(...Array.from(styleEl.sheet.cssRules));
}
const adoptedSheets = document.adoptedStyleSheets;
if (Array.isArray(adoptedSheets)) {
for (const sheet of adoptedSheets) {
if (sheet?.cssRules) {
rules.push(...Array.from(sheet.cssRules));
}
}
}
return rules.find((cssRule) => cssRule.selectorText === `[data-ds-style-id="${scopeId}"]`) || null;
}
test("metric probe falls back to numeric parsing when measurement fails", () => {
const dom = new JSDOM(`<!doctype html><html><head></head><body></body></html>`, {
pretendToBeVisual: true,
});
const { document } = dom.window;
Object.defineProperty(dom.window.HTMLDivElement.prototype, "offsetHeight", {
configurable: true,
get() {
return Number.parseFloat(this.dataset.mockHeight || "0");
},
});
const rootStyle = document.documentElement["style"];
rootStyle?.setProperty("--probe-test", "12q");
const fallback = readDesignTokenAsPixels("--probe-test", { documentRef: document });
assert.equal(fallback, 12);
const probe = document.querySelector(".ds-metric-probe");
assert.ok(probe, "probe element should be created");
const scopeId = probe.getAttribute("data-ds-style-id");
assert.ok(scopeId, "probe should be registered with a dynamic scope");
probe.dataset.mockHeight = "48";
const initialRule = getScopeRule(document, scopeId);
assert.ok(initialRule, "metric probe should have a dynamic rule");
rootStyle?.setProperty("--probe-test", "3vh");
const measured = readDesignTokenAsPixels("--probe-test", { documentRef: document });
assert.equal(measured, 48);
const rule = getScopeRule(document, scopeId);
assert.ok(rule, "metric probe rule should persist");
const ruleStyle = rule ? rule["style"] : null;
assert.ok(ruleStyle, "probe rule should expose a CSSStyleDeclaration");
assert.equal(ruleStyle.getPropertyValue("--ds-metric-probe-length"), "0");
});
+34
View File
@@ -4,6 +4,23 @@ import { JSDOM } from "jsdom";
import positionFloatingPanel from "../js/ui/utils/positionFloatingPanel.js";
import { createFloatingPanelStyles } from "../js/ui/utils/floatingPanelStyles.js";
function getScopeRule(document, scopeId) {
const styleEl = document.querySelector('style[data-ds-dynamic="true"]');
const rules = [];
if (styleEl?.sheet?.cssRules) {
rules.push(...Array.from(styleEl.sheet.cssRules));
}
const adoptedSheets = document.adoptedStyleSheets;
if (Array.isArray(adoptedSheets)) {
for (const sheet of adoptedSheets) {
if (sheet?.cssRules) {
rules.push(...Array.from(sheet.cssRules));
}
}
}
return rules.find((cssRule) => cssRule.selectorText === `[data-ds-style-id="${scopeId}"]`) || null;
}
test("flips to top when bottom placement collides with viewport", () => {
const dom = new JSDOM(
`
@@ -61,10 +78,20 @@ test("flips to top when bottom placement collides with viewport", () => {
assert.equal(styles.getStrategy(), "fixed");
assert.deepEqual(styles.getFallbackPosition(), { top: 132, left: 100 });
assert.equal(panel.dataset.floatingMode, "fallback");
const scopeId = panel.getAttribute("data-ds-style-id");
assert.ok(scopeId, "panel should receive a dynamic style scope id");
const rule = getScopeRule(document, scopeId);
assert.ok(rule, "scope rule should exist for the floating panel");
const ruleStyleTop = rule ? rule["style"] : null;
assert.ok(ruleStyleTop, "floating panel rule should expose a CSSStyleDeclaration");
assert.equal(ruleStyleTop.getPropertyValue("--floating-fallback-top"), "132px");
assert.equal(ruleStyleTop.getPropertyValue("--floating-fallback-left"), "100px");
positioner.destroy();
assert.equal(panel.dataset.floatingPanel, undefined);
assert.equal(trigger.dataset.floatingAnchor, undefined);
const ruleAfterDestroy = getScopeRule(document, scopeId);
assert.equal(ruleAfterDestroy, null);
});
test("respects RTL alignment and clamps within the viewport", () => {
@@ -194,10 +221,17 @@ test("updates when scroll containers move the trigger", async () => {
assert.deepEqual(styles.getFallbackPosition(), { top: 152, left: 40 });
assert.ok(scrollHandler, "scroll listener should be registered");
const scopeId = panel.getAttribute("data-ds-style-id");
const rule = getScopeRule(document, scopeId);
assert.ok(rule, "dynamic rule should be present for scroll updates");
const fallbackRuleStyle = rule ? rule["style"] : null;
assert.ok(fallbackRuleStyle, "dynamic rule should be present for scroll updates");
assert.equal(fallbackRuleStyle.getPropertyValue("--floating-fallback-top"), "152px");
anchorTop = 60;
scrollHandler?.({ type: "scroll" });
assert.deepEqual(styles.getFallbackPosition(), { top: 112, left: 40 });
assert.equal(fallbackRuleStyle.getPropertyValue("--floating-fallback-top"), "112px");
positioner.destroy();
window.addEventListener = originalAddEventListener;