feat(uix): adopt sec-dom APIs in first consumers

- floating: read the resolved content z-index via `dom.measure` instead of a raw
  `requestFrame` + manual `cancelFrame` — coalesced post-layout, disposer returned
  directly. The canonical `dom.measure` adopter.
- color-picker demo: resolve preset-swatch token colours via `eidos.resolveTokens`
  (config + `$color`, pure JS) instead of the `getComputedStyle(probe)` round-trip
  that forced the reflow. Restores the Clear footer button (`{#if showClear}`,
  was a stray `{#if false}` debug edit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
menubar-v4-safe
dev 3 months ago
parent 7433d171b9
commit e22c9501d4

@ -310,7 +310,10 @@ export class FloatingContent {
() => this.contentRef.current,
(contentNode) => {
if (!contentNode || !this.opts.enabled.current) return;
const rafId = this.provider.opts.dom.requestFrame(() => {
// Read the resolved content z-index post-layout (coalesced) via dom.measure
// instead of a raw requestFrame — a layout read must never force a sync reflow,
// and measure returns the disposer directly (no manual cancelFrame bookkeeping).
return this.provider.opts.dom.measure(() => {
if (this.contentRef.current !== contentNode || !this.opts.enabled.current) return;
const cs = this.provider.opts.dom.getWindow(contentNode).getComputedStyle(contentNode);
const zIndex = cs.zIndex;
@ -318,10 +321,6 @@ export class FloatingContent {
this.contentZIndex = zIndex;
}
}, contentNode);
return () => {
this.provider.opts.dom.cancelFrame(rafId, contentNode);
};
}
);

@ -16,9 +16,11 @@
import { colorFieldMorfo } from '@/uix/morfo/components/color-field';
import { popoverMorfo } from '@/uix/morfo/components/popover';
import { getActiveUix } from '$active-uix';
import { ActiveEidos } from '$uix/eidos';
import { colorValueFromRgb, type ColorValue, type ColorFormat } from '$libs/color';
const uix = getActiveUix();
const eidos = ActiveEidos.require();
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type TraceEntry = { event: string; family: string; intent?: string; at: number };
@ -191,42 +193,23 @@
});
// SwatchTrigger commits a parsed ColorValue, so a raw `var(--token)` can't be
// passed — resolve each token to its computed rgb() via a hidden probe. Re-run
// when :root theme attrs change so System/Intent swatches follow retints
// passed. Resolve each token to a concrete sRGB hex via the eidos colour engine
// (config + `$color`) — pure JS, NO DOM probe, so no forced reflow. Re-resolve
// when the :root theme attrs change so System/Intent swatches follow retints
// (palette scales are theme-independent but re-resolve cheaply too).
let resolvedTokens = $state<Record<string, string>>({});
$effect(() => {
const doc = uix.dom?.getDocument?.() ?? document;
const win = uix.dom?.getWindow?.() ?? window;
if (!doc?.body) return;
const probe = doc.createElement('span');
probe.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none';
doc.body.appendChild(probe);
const reprobe = () => {
const next: Record<string, string> = {};
for (const token of presetTokens) {
// Scale tokens resolve to an oklch() sibling the color parser can't read.
// color-mix(in srgb) coerces to the sRGB gamut; serialize to rgb() so the
// SwatchTrigger can both display AND commit (parse) the value.
probe.style.color = `color-mix(in srgb, var(${token}), var(${token}))`;
const computed = win.getComputedStyle(probe).color;
const srgb = computed.match(/srgb\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)/);
next[token] = srgb
? `rgb(${Math.round(+srgb[1] * 255)}, ${Math.round(+srgb[2] * 255)}, ${Math.round(+srgb[3] * 255)})`
: computed;
}
resolvedTokens = next;
const resolve = () => {
resolvedTokens = eidos.resolveTokens(presetTokens);
};
reprobe();
const themeObs = new MutationObserver(reprobe);
resolve();
const themeObs = new MutationObserver(resolve);
themeObs.observe(doc.documentElement, {
attributes: true,
attributeFilter: ['class', 'style', 'data-theme', 'data-uix-theme']
attributeFilter: ['class', 'style', 'data-theme', 'data-mode', 'data-uix-theme']
});
return () => {
themeObs.disconnect();
probe.remove();
};
return () => themeObs.disconnect();
});
const valueLabel = $derived(value?.hex ?? 'empty');
@ -277,8 +260,7 @@
showSwatches && ' <ColorPicker.SavedSwatches />',
' </PickerShell.Body>',
showFooter && ' <PickerShell.Footer>',
showFooter &&
' <ColorPicker.SaveAction />',
showFooter && ' <ColorPicker.SaveAction />',
showFooter && showClear && ' <PickerShell.Clear />',
showFooter && showCancel && ' <PickerShell.Cancel />',
showFooter && showClose && ' <PickerShell.Close />',
@ -334,8 +316,7 @@
showSwatches && ' <ColorPicker.SavedSwatches />',
' </PickerShell.Body>',
showFooter && ' <ColorPicker.Footer>',
showFooter &&
' <ColorPicker.SaveAction />',
showFooter && ' <ColorPicker.SaveAction />',
showFooter && showClear && ' <ColorPicker.Clear />',
showFooter && showCancel && ' <ColorPicker.Cancel />',
showFooter && showClose && ' <ColorPicker.Close />',
@ -421,12 +402,7 @@
<ColorPicker.Portal>
<ColorPicker.Content>
<PickerShell.Body>
<Tabs
variant="segmented"
fitted
{size}
bind:value={pickerTab}
>
<Tabs variant="segmented" fitted {size} bind:value={pickerTab}>
<Tabs.List aria-label="Vistas del selector de color">
<Tabs.Trigger value="selection">Selección</Tabs.Trigger>
<Tabs.Trigger value="presets">Presets</Tabs.Trigger>
@ -522,6 +498,7 @@
{#if showFooter}
<ColorPicker.Footer>
<ColorPicker.SaveAction />
{#if showClear}<ColorPicker.Clear />{/if}
{#if showCancel}<ColorPicker.Cancel />{/if}
{#if showClose}<ColorPicker.Close />{/if}

Loading…
Cancel
Save

Powered by TurnKey Linux.