fix(blocks): /blocks habla prefs — modo y densidad por uix.prefs, preview sin persistencia, muere el segundo motor de densidad (P2 #5-D)

Firmado por el autor tras C («arregla también /blocks»). El shell de `/blocks`
(`web/routes/blocks/+layout@.svelte`) y el boot de cada preview
(`web/routes/blocks/_lib/BootUix.svelte`) mantenían su propio `$state` de modo
y lo pasaban a eidos por `modeSource` (muerto en f875f60b9), así que su toggle
había dejado de mover a eidos — y como el sobre `uix.prefs` es compartido con
`/uix`, la divergencia estaba a UN CLIC del toggle de docs (adversarial C, R-1).
Además implementaban la DENSIDAD a mano (`:root{--density-space-scale…}` por
`uix.dom.writeStyle`) mientras eidos ya estampa `data-density` desde prefs y la
hoja generada aplica las escalas: un segundo motor de densidad.

- Shell: `mode` y `density` DERIVADOS de las ranuras de `uix.prefs`; los toggles
  escriben `setIntent`; mueren `modeSource`, `modeListeners`, `DENSITY_SCALES` y
  el `writeStyle`; migración ÚNICA del blob `uix-blocks-prefs` al sobre de prefs
  (detalle en el cuerpo del informe / handoff).
- Preview (`BootUix`): la raíz nace con `intent` desde la URL (`mode`, `language`,
  `direction`, `density`) y `storage: false` — la preview es una PROYECCIÓN de su
  URL y ya no hidrata ni escribe el sobre compartido del documento padre
  (contaminación cruzada latente, cerrada).
- `BlockDemo`: prosa que citaba `modeSource` corregida; los ejes de la URL se leen
  de prefs.
- Ledger `check-debt.ts`: desaparecen las dos entradas de `blocks` (solo mengua).

Verificado en Chrome por el coordinador (SO oscuro sin blob ⇒ `/blocks`
coherente; toggles de modo y densidad mueven `<html data-mode/data-density>` y
el wrapper; preview `?mode=light` clara con el shell en oscuro sin tocar el sobre).
Adversarial Opus independiente (informe en el handoff). Constructor + adversarial
Opus 5; la sesión coordina.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
alpha-0.1-background
dev 3 weeks ago
parent 13293fe500
commit 2ed9c54937

@ -37,10 +37,6 @@ export const CHECK_DEBT: Record<string, number> = {
// 0 → 2 (2026-09-14): retired `*Source` options — excess property + handler.
'web/routes/alpha/sheet/[variant]/+layout@.svelte': 2,
'web/routes/alpha/sheet/[variant]/+page.svelte': 1,
// 0 → 2 (2026-09-14): retired `*Source` options — excess property + handler.
'web/routes/blocks/+layout@.svelte': 2,
// 0 → 2 (2026-09-14): retired `*Source` options — excess property + handler.
'web/routes/blocks/_lib/BootUix.svelte': 2,
'web/routes/demos/animations/background/balastro/+page.svelte': 1,
'web/routes/demos/animations/background/beam/beam.svelte': 2,
'web/routes/demos/animations/background/bends/+page.svelte': 1,

@ -13,7 +13,7 @@
import { onDestroy } from 'svelte';
import { page } from '$app/state';
import { createActiveUix, setActiveUix } from '$active-uix';
import { standardPrefsDimensions } from '$prefs';
import { readActivePrefsSlot, standardPrefsDimensions } from '$prefs';
import { Soma } from '$soma/core/soma.svelte';
import { siumLangs } from '$sium/langs/langs';
import { secsLangs } from '$libs/secs';
@ -65,57 +65,45 @@
Soma.create();
// ── Axes ──────────────────────────────────────────────────────────────
let mode = $state<'light' | 'dark'>('light');
// `mode` and `density` are prefs DIMENSIONS: `uix.prefs` is the one engine
// that resolves them (stored intent → OS hint → default) and eidos stamps
// `<html data-mode>` / `<html data-density>` from those same slots, with the
// generated sheet rebinding the density scales per `[data-density='…']`. The
// shell READS the effective value — so its wrapper cannot contradict the
// document the way a second local state did — and WRITES intent from the
// topbar. Language and direction stay local for now; this batch does not
// move them.
let language = $state<Language>('es');
let dir = $state<'ltr' | 'rtl'>('ltr');
let density = $state<Density>('comfortable');
const DENSITY_SCALES: Record<Density, { space: number; control: number }> = {
compact: { space: 0.85, control: 0.9 },
comfortable: { space: 1, control: 1 },
spacious: { space: 1.15, control: 1.1 }
};
const activeEidos = ActiveEidos.create({ applyDom: true, theme: 'base' });
const modeSlot = readActivePrefsSlot<'light' | 'dark'>(uix.prefs, 'mode');
const densitySlot = readActivePrefsSlot<Density>(uix.prefs, 'density');
const mode = $derived(modeSlot?.get() ?? 'light');
const density = $derived(densitySlot?.get() ?? 'comfortable');
const modeListeners = new Set<(value: 'light' | 'dark') => void>();
const activeEidos = ActiveEidos.create({
applyDom: true,
theme: 'base',
modeSource: {
get: () => mode,
onChange(handler) {
modeListeners.add(handler);
return () => modeListeners.delete(handler);
}
}
});
// Restore once: a stored choice wins, otherwise the OS scheme decides.
// One-shot migration of the section's own blob: `mode` and `density` hand
// themselves over to the prefs envelope, which persists them from here on
// and follows the OS by itself when nothing is stored. The blob below stops
// writing the two keys, so the migration finds nothing on the next visit.
$effect(() => {
if (typeof localStorage === 'undefined') return;
const stored = localStorage.getItem('uix-blocks-prefs');
if (stored) {
try {
const blob = JSON.parse(stored);
if (blob.mode === 'dark' || blob.mode === 'light') mode = blob.mode;
if (blob.language) language = blob.language;
if (blob.dir) dir = blob.dir;
if (blob.density) density = blob.density;
return;
} catch {
/* ignore a corrupt blob */
}
if (!stored) return;
try {
const blob = JSON.parse(stored);
if (blob.language) language = blob.language;
if (blob.dir) dir = blob.dir;
if (blob.mode === 'dark' || blob.mode === 'light') uix.prefs.setIntent('mode', blob.mode);
if (blob.density) uix.prefs.setIntent('density', blob.density);
} catch {
/* ignore a corrupt blob */
}
mode = window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
});
$effect(() => {
if (typeof localStorage === 'undefined') return;
localStorage.setItem('uix-blocks-prefs', JSON.stringify({ mode, language, dir, density }));
});
$effect(() => {
const current = mode;
for (const listener of modeListeners) listener(current);
localStorage.setItem('uix-blocks-prefs', JSON.stringify({ language, dir }));
});
// Language and direction are PREFS intents: they reach langs, formats and the
@ -125,15 +113,6 @@
uix.prefs.setIntent('direction', dir);
});
// Density on :root, so portaled overlays get it too.
$effect(() => {
const d = DENSITY_SCALES[density];
uix.dom.writeStyle(
'uix-blocks-display',
`:root{--density-space-scale:${d.space};--density-control-scale:${d.control};}`
);
});
const currentSlug = $derived(page.url.pathname.replace(/^\/blocks\/?/, ''));
onDestroy(() => {
@ -183,7 +162,7 @@
size="sm"
attached
value={[density]}
onValueChange={(v) => (density = (v[0] ?? density) as Density)}
onValueChange={(v) => uix.prefs.setIntent('density', v[0] ?? density)}
aria-label="Densidad"
>
<ToggleGroup.Item value="compact">−</ToggleGroup.Item>
@ -211,7 +190,7 @@
size="sm"
variant="ghost"
aria-label={mode === 'light' ? 'Cambiar a modo oscuro' : 'Cambiar a modo claro'}
onclick={() => (mode = mode === 'light' ? 'dark' : 'light')}
onclick={() => uix.prefs.setIntent('mode', mode === 'light' ? 'dark' : 'light')}
>
{#if mode === 'light'}
<Icon.Moon size="sm" />

@ -60,12 +60,14 @@
// The preview is another document with its own runtime (`BootUix` runs its
// own `createActiveUix`), so the section's live state does not cross the
// frame: the URL is the serialization the second runtime boots from. The
// axes are read from the FRAMEWORK's live state — the prefs dimension reads
// are rune-backed (per-dimension change counters) and `getThemeContext()`
// reads the shell's `modeSource` — so flipping a topbar toggle re-keys the
// iframe with the new params. The shell is the single owner of these axes
// (the demos' own "dir (preview only)" controls were retired with this):
// a demo's query string carries only its BLOCK props.
// axes are read from the FRAMEWORK's live state — all three are prefs
// dimensions and those reads are rune-backed (per-dimension change
// counters), `getThemeContext().mode` included, since eidos resolves the
// mode through the same `uix.prefs` slot the shell's toggle writes — so
// flipping a topbar toggle re-keys the iframe with the new params. The
// shell is the single owner of these axes (the demos' own "dir (preview
// only)" controls were retired with this): a demo's query string carries
// only its BLOCK props.
const uix = getActiveUix();
const eidos = ActiveEidos.require();
const resolvedSrc = $derived.by(() => {

@ -9,7 +9,7 @@
* `?mode=dark&dir=rtl&lang=en` is what the demo's iframe passes.
*/
import './reset.css';
import { onDestroy } from 'svelte';
import { onDestroy, untrack } from 'svelte';
import { createActiveUix, setActiveUix } from '$active-uix';
import { standardPrefsDimensions } from '$prefs';
import { Soma } from '$soma/core/soma.svelte';
@ -37,12 +37,6 @@
children: Snippet;
} = $props();
const DENSITY_SCALES: Record<BootDensity, { space: number; control: number }> = {
compact: { space: 0.85, control: 0.9 },
comfortable: { space: 1, control: 1 },
spacious: { space: 1.15, control: 1.1 }
};
const prefsSchema = standardPrefsDimensions({
languages: ['es', 'en', 'fr', 'de'] as const,
locales: ['es-ES', 'en-US', 'en-GB', 'de-DE'] as const,
@ -60,40 +54,34 @@
// still stamps `data-event-*` correctly and nothing is ever audible —
// which is how the section shipped from F0.
events: { sound: true, haptic: true, components: semaPacks },
prefs: { schema: prefsSchema }
prefs: {
schema: prefsSchema,
// The four axes enter as INTENT, so the first resolution is already the
// one the URL asked for — no flash, no second engine. A SNAPSHOT on
// purpose (`untrack`): the engine is built once and the `$effect`
// below owns every later move of a prop.
intent: untrack(() => ({ mode, language, direction: dir, density })),
// A preview is a PROJECTION of its URL and persists nothing. The iframe
// shares its origin with the section shell, so the canonical envelope is
// the SHELL's: with the default adapter this boot hydrated the shell's
// choices over the URL's and then wrote the URL's back into them.
storage: false
}
});
setActiveUix(uix);
Soma.create();
const modeListeners = new Set<(value: 'light' | 'dark') => void>();
const activeEidos = ActiveEidos.create({
applyDom: true,
theme: 'base',
modeSource: {
get: () => mode,
onChange(handler) {
modeListeners.add(handler);
return () => modeListeners.delete(handler);
}
}
});
$effect(() => {
const current = mode;
for (const listener of modeListeners) listener(current);
});
// Mode, theme, density and scaling reach `<html>` from the prefs slots eidos
// reads — the density scales included, rebound by the generated sheet per
// `[data-density='…']`.
const activeEidos = ActiveEidos.create({ applyDom: true, theme: 'base' });
// Re-assert the axes when a prop moves without remounting the boot.
$effect(() => {
uix.prefs.setIntent('mode', mode);
uix.prefs.setIntent('language', language);
uix.prefs.setIntent('direction', dir);
});
$effect(() => {
const d = DENSITY_SCALES[density];
uix.dom.writeStyle(
'uix-blocks-display',
`:root{--density-space-scale:${d.space};--density-control-scale:${d.control};}`
);
uix.prefs.setIntent('density', density);
});
onDestroy(() => {

Loading…
Cancel
Save

Powered by TurnKey Linux.