fix(demos): reachable nav + real interactive Ambient testbed + visible fog/mesh

The catalog work shipped but was unreachable and impoverished:

- **Nav**: the sidebar IA (web/routes/uix/+layout@.svelte) is a manual list;
  the new components were never added, so nothing linked to them. Add a
  "Text effects" group (TextGradient/Circular/Blur/Focus/Scramble), CountUp
  under Utilities, and a "Packs" group (Ambient · backgrounds).
- **Ambient demo → real testbed**: rebuild web/routes/uix/packs/ambient as a
  proper interactive page — effect selector (32, grouped Agentic/Tier A/B),
  live param controls AUTO-GENERATED from each effect's `defaults`
  (number→slider, boolean→toggle, colors→editable token list, string→text),
  a stage-background toggle (surface/black/white/transparent), reset, and a
  copyable usage snippet. The old page only switched effect, no tuning.
- **fog + mesh painted nothing**: their seeded colors were near-invisible on
  the dark base theme — fog used neutral-bg2 (L 0.21) + neutral-element
  (L 0.25), mesh used four -element slots (L ~0.26) against surface L 0.18.
  Reseed with high-contrast tokens (fog: surface bg + neutral-text; mesh:
  -solid stops). Verified in a real browser: aurora/mesh/fog all paint.
- Repoint the six demo ledes from the ephemeral plan to
  docs/decisions/design-text-effects.md (matches the READMEs).

check 0 own errors. Verified live (real Chrome): all effects paint, controls
tune params, nav reaches every new page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
menubar-v4-safe
dev 3 months ago
parent 63b371d5ce
commit 0567e893f4

@ -299,7 +299,8 @@
{ slug: '/uix/components/trans', label: 'Trans' },
{ slug: '/uix/components/format-number', label: 'FormatNumber' },
{ slug: '/uix/components/format-date', label: 'FormatDate' },
{ slug: '/uix/components/relative-time', label: 'RelativeTime' }
{ slug: '/uix/components/relative-time', label: 'RelativeTime' },
{ slug: '/uix/components/count-up', label: 'CountUp' }
]
},
{
@ -339,6 +340,22 @@
{ slug: '/uix/components/link', label: 'Link' }
]
},
{
kind: 'group',
heading: 'Text effects',
items: [
{ slug: '/uix/components/text-gradient', label: 'TextGradient' },
{ slug: '/uix/components/text-circular', label: 'TextCircular' },
{ slug: '/uix/components/text-blur', label: 'TextBlur' },
{ slug: '/uix/components/text-focus', label: 'TextFocus' },
{ slug: '/uix/components/text-scramble', label: 'TextScramble' }
]
},
{
kind: 'group',
heading: 'Packs',
items: [{ slug: '/uix/packs/ambient', label: 'Ambient · backgrounds', tag: 'pack' }]
},
{
kind: 'group',
heading: 'Layout',

@ -58,8 +58,7 @@
muelle al entrar en viewport, renderizando CADA frame a través de
<code>uix.format.numbers</code> (locale reactivo del runtime — nada de <code>en-US</code>
fijo). Hermano de <a href="/uix/components/format-number">&lt;FormatNumber&gt;</a>: formatear
es suyo, contar es de este. Familia del plan
<code>docs/process/PLAN-text-effects.md</code>.
es suyo, contar es de este. Familia (diseño): <code>docs/decisions/design-text-effects.md</code>.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill>

@ -47,8 +47,7 @@
Entrada de contenido: los segmentos (palabras o letras) condensan de borroso→nítido con
stagger al entrar en viewport. <strong>SR endurecido</strong>: el texto real vive
visually-hidden y los segmentos animados son <code>aria-hidden</code> — los lectores leen una
cadena continua, no fragmentos. Familia del plan
<code>docs/process/PLAN-text-effects.md</code>.
cadena continua, no fragmentos. Familia (diseño): <code>docs/decisions/design-text-effects.md</code>.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill>

@ -49,7 +49,7 @@
accesible: <code>role="img"</code> + <code>aria-label</code>. La rotación es rAF del
ecosistema (persistencia de ángulo al cambiar velocidad — una animación CSS no puede).
<strong>Nota Aura</strong>: texto orbital = pieza candidata de la periferia del indicador de
agente. Familia del plan <code>docs/process/PLAN-text-effects.md</code>.
agente. Familia (diseño): <code>docs/decisions/design-text-effects.md</code>.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill>

@ -52,8 +52,7 @@
Todas las palabras difuminadas salvo la activa; un marco de esquinas viaja hasta ella —
cadencia automática (<code>uix.timers.interval</code>) o <code>manualMode</code> por hover.
Los rects se leen con <code>dom.measure</code> (coalescido post-layout). A11y fix sobre el
seed: fuera los <code>role="button"</code> falsos. Familia del plan
<code>docs/process/PLAN-text-effects.md</code>.
seed: fuera los <code>role="button"</code> falsos. Familia (diseño): <code>docs/decisions/design-text-effects.md</code>.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill>

@ -55,8 +55,7 @@
Gradiente animado pintado A TRAVÉS del texto (<code>background-clip: text</code>), con borde
opcional en el mismo gradiente. El texto queda intacto en el DOM — efecto solo de pintura.
Los stops aceptan tokens del tema (re-tintan al cambiar de modo) y <code>preset</code>
consume los gradientes con nombre del theming (S9). Familia del plan
<code>docs/process/PLAN-text-effects.md</code>.
consume los gradientes con nombre del theming (S9). Familia (diseño): <code>docs/decisions/design-text-effects.md</code>.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill>

@ -44,7 +44,7 @@
real. <strong>SR endurecido</strong> (los lectores nunca ven los glifos scrambled) y
<strong>cache de medición</strong>: centros por carácter cacheados vía
<code>dom.measure</code> — 1 lectura de layout por pointermove (el seed hacía N). Familia del
plan <code>docs/process/PLAN-text-effects.md</code>.
diseño de familia <code>docs/decisions/design-text-effects.md</code>.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill>

@ -1,11 +1,11 @@
<script lang="ts">
/**
* Ambient pack — catalog. One route for the whole pack (packs live outside
* the component matrix and its 9-tab template): pick an effect, see it run
* with THEME-token colors (P-4) — switch the shell's light/dark mode and
* the token-driven effects re-tint live.
* Ambient pack — interactive catalog. Pick an effect, tune EVERY param
* live (controls auto-generated from the effect's `defaults`), pick the
* stage background, copy the usage snippet. Colors default to theme tokens
* (P-4) so a light/dark switch re-tints the token-driven effects.
*/
import { Ambient, type AmbientEffectName } from '$packs/ambient';
import { Ambient, resolveAmbientEffect, type AmbientEffectName } from '$packs/ambient';
import '$packs/ambient/effects/mesh';
import '$packs/ambient/effects/aurora';
import '$packs/ambient/effects/dot-grid';
@ -39,145 +39,128 @@
import '$packs/ambient/effects/pixel-blast';
import '$packs/ambient/effects/hyperspeed';
// Agentic set first (aurora/silk = thinking · orb = listening · lumen =
// acting · radar = searching · threads = streaming), then Tier A
// decoratives, then Tier B.
const effects: readonly AmbientEffectName[] = [
'aurora',
'silk',
'orb',
'lumen',
'radar',
'threads',
'waves',
'mesh',
'dot-grid',
'fog',
'grainient',
'veil',
'dots',
'beam',
'ray',
'snow-pixels',
'galaxy',
'eter',
'hyperspeed',
'particles',
'pixel-blast',
'grid',
'prismatic',
'pillar',
'dither',
'blinds',
'balastro',
'iridiscence',
'liquid-metal',
'line-waves',
'floating-lines',
'letters'
];
let active = $state<AmbientEffectName>('aurora');
// Token-first params: concrete theming, not hardcoded palettes (P-4).
const paramsByEffect: Record<AmbientEffectName, Record<string, unknown>> = {
aurora: {
colorStops: ['--color-primary-solid', '--color-tertiary-solid', '--color-affirm-solid'],
amplitude: 1,
blend: 0.5
},
silk: {
color: '--color-neutral-solid'
},
orb: {
backgroundColor: '--color-neutral-bg2'
},
lumen: {
bgColor: '--color-neutral-bg2',
trailColorA: '--color-primary-solid',
trailColorB: '--color-tertiary-solid'
},
radar: {
color: '--color-affirm-solid'
// Effect groups (for the selector) — agentic first, then Tier A, then Tier B.
const GROUPS: { heading: string; names: AmbientEffectName[] }[] = [
{ heading: 'Agentic', names: ['aurora', 'silk', 'orb', 'lumen', 'radar', 'threads'] },
{
heading: 'Tier A',
names: ['waves', 'mesh', 'dot-grid', 'fog', 'grainient', 'veil', 'dots', 'beam', 'ray', 'snow-pixels']
},
threads: {
color: '--color-neutral-solid'
},
waves: {
lineColor: '--color-neutral-solid'
},
mesh: {
colors: [
'--color-primary-element',
'--color-secondary-element',
'--color-tertiary-element',
'--color-neutral-element'
{
heading: 'Tier B',
names: [
'galaxy', 'eter', 'hyperspeed', 'particles', 'pixel-blast', 'grid', 'prismatic', 'pillar',
'dither', 'blinds', 'balastro', 'iridiscence', 'liquid-metal', 'line-waves', 'floating-lines', 'letters'
]
},
'dot-grid': {
baseColor: '--color-neutral-border',
activeColor: '--color-primary-solid'
},
fog: {
bgColor: '--color-neutral-bg2',
fogColor: '--color-neutral-element'
},
grainient: {
color1: '--color-primary-solid',
color2: '--color-tertiary-solid',
color3: '--color-secondary-element'
},
veil: {},
dots: {
gradientFrom: '--color-primary-solid',
gradientTo: '--color-tertiary-solid',
glowColor: '--color-neutral-bg2'
},
beam: {},
ray: {},
'snow-pixels': {
color: '--color-neutral-text'
},
galaxy: {},
eter: {
colors: ['--color-primary-solid', '--color-secondary-solid', '--color-tertiary-solid']
},
hyperspeed: {},
particles: {
particleColors: ['--color-primary-solid', '--color-tertiary-solid', '--color-neutral-text']
},
'pixel-blast': {
color: '--color-primary-solid'
},
grid: {
linesColor: '--color-neutral-border',
scanColor: '--color-primary-solid'
},
prismatic: {},
pillar: {
topColor: '--color-primary-solid',
bottomColor: '--color-tertiary-solid'
},
dither: {
waveColor: '--color-neutral-solid'
},
blinds: {
colors: ['--color-primary-solid', '--color-tertiary-solid']
},
balastro: {},
iridiscence: {},
'liquid-metal': {},
'line-waves': {
color1: '--color-primary-solid',
color2: '--color-secondary-solid',
color3: '--color-tertiary-solid'
},
'floating-lines': {
linesGradient: ['--color-primary-solid', '--color-tertiary-solid']
},
letters: {
glitchColors: ['--color-neutral-element', '--color-primary-solid', '--color-tertiary-solid']
}
];
// Curated VISIBLE token colors per effect (the base theme is dark, so these
// separate from the stage; the auto-controls let you change them). Only the
// color params are seeded here; every other param comes from the effect's
// own `defaults`.
const COLOR_SEED: Partial<Record<AmbientEffectName, Record<string, unknown>>> = {
aurora: { colorStops: ['--color-primary-solid', '--color-tertiary-solid', '--color-affirm-solid'] },
silk: { color: '--color-primary-solid' },
orb: { backgroundColor: '--color-surface-default' },
lumen: { bgColor: '--color-surface-default', trailColorA: '--color-primary-solid', trailColorB: '--color-tertiary-solid' },
radar: { color: '--color-affirm-solid' },
threads: { color: '--color-neutral-text' },
waves: { lineColor: '--color-neutral-text' },
mesh: { colors: ['--color-primary-solid', '--color-secondary-solid', '--color-tertiary-solid', '--color-affirm-solid'] },
'dot-grid': { baseColor: '--color-neutral-text', activeColor: '--color-primary-solid' },
fog: { bgColor: '--color-surface-default', fogColor: '--color-neutral-text' },
grainient: { color1: '--color-primary-solid', color2: '--color-tertiary-solid', color3: '--color-affirm-solid' },
dots: { gradientFrom: '--color-primary-solid', gradientTo: '--color-tertiary-solid', glowColor: '--color-primary-solid' },
'snow-pixels': { color: '--color-neutral-text' },
eter: { colors: ['--color-primary-solid', '--color-secondary-solid', '--color-tertiary-solid'] },
particles: { particleColors: ['--color-primary-solid', '--color-tertiary-solid', '--color-neutral-text'] },
'pixel-blast': { color: '--color-primary-solid' },
grid: { linesColor: '--color-neutral-text', scanColor: '--color-primary-solid' },
pillar: { topColor: '--color-primary-solid', bottomColor: '--color-tertiary-solid' },
dither: { waveColor: '--color-primary-solid' },
blinds: { colors: ['--color-primary-solid', '--color-tertiary-solid'] },
'line-waves': { color1: '--color-primary-solid', color2: '--color-secondary-solid', color3: '--color-tertiary-solid' },
'floating-lines': { linesGradient: ['--color-primary-solid', '--color-tertiary-solid'] },
letters: { glitchColors: ['--color-primary-solid', '--color-affirm-solid', '--color-tertiary-solid'] }
};
let active = $state<AmbientEffectName>('aurora');
let params = $state<Record<string, unknown>>({});
let stageBg = $state<'surface' | 'black' | 'white' | 'transparent'>('surface');
const STAGE_BG: Record<typeof stageBg, string> = {
surface: 'var(--color-surface-default)',
black: '#000',
white: '#fff',
transparent: 'transparent'
};
// The selected effect's authoring defaults (typed `never` at the registry
// boundary; the runtime object is the real defaults).
const defaults = $derived(
(resolveAmbientEffect(active)?.defaults ?? {}) as Record<string, unknown>
);
const driver = $derived(resolveAmbientEffect(active)?.driver ?? '—');
// Reseed params whenever the effect changes: effect defaults + curated colors.
function seed(name: AmbientEffectName) {
const base = { ...((resolveAmbientEffect(name)?.defaults ?? {}) as Record<string, unknown>) };
params = { ...base, ...(COLOR_SEED[name] ?? {}) };
}
$effect(() => {
seed(active);
});
// ── Control heuristics ────────────────────────────────────────────────
type ControlKind = 'number' | 'boolean' | 'colors' | 'string';
const kindOf = (v: unknown): ControlKind =>
typeof v === 'number' ? 'number'
: typeof v === 'boolean' ? 'boolean'
: Array.isArray(v) ? 'colors'
: 'string';
const numBounds = (v: number) => {
const abs = Math.abs(v);
const max = v === 0 ? 1 : Math.max(abs * 3, abs + 1);
const min = v < 0 ? -max : 0;
const step = max <= 2 ? 0.01 : max <= 50 ? 0.1 : 1;
return { min, max, step };
};
const isColorish = (k: string) =>
/color|bg|stop|tint|glow|gradient|line/i.test(k);
const entries = $derived(Object.entries(params));
function setParam(key: string, value: unknown) {
params = { ...params, [key]: value };
}
function setColorList(key: string, csv: string) {
setParam(key, csv.split(',').map((s) => s.trim()).filter(Boolean));
}
// ── Snippet ───────────────────────────────────────────────────────────
const stripDefault = $derived.by(() => {
// Only include params that differ from the effect's own default.
const out: Record<string, unknown> = {};
for (const [k, v] of Object.entries(params)) {
if (JSON.stringify(v) !== JSON.stringify(defaults[k])) out[k] = v;
}
return out;
});
const snippet = $derived(
[
"<script lang='ts'>",
` import { Ambient } from '$packs/ambient';`,
` import '$packs/ambient/effects/${active}'; // import = opt-in`,
'</' + 'script>',
'',
Object.keys(stripDefault).length === 0
? `<Ambient effect="${active}" style="height: 320px" />`
: `<Ambient\n effect="${active}"\n params={${JSON.stringify(stripDefault)}}\n style="height: 320px"\n/>`
].join('\n')
);
</script>
<div data-uix-canvas-inner>
@ -185,28 +168,115 @@
<div data-uix-eyebrow>Packs · Ambient</div>
<h1 data-uix-page-title>Ambient</h1>
<p data-uix-page-lede>
Fondos animados como pack encapsulado sobre <code>arts/scene</code> — colores por token del
tema (cambia el modo del shell y se re-tintan), reduced-motion y pausa fuera de vista de
serie. Contrato P: <code>src/packs/ambient/README.md</code>.
Fondos animados como pack encapsulado sobre <code>arts/scene</code> — 32 efectos.
Elige uno, ajusta cada parámetro en vivo (los controles se generan del
<code>defaults</code> del efecto), cambia el fondo del escenario y copia el snippet.
Colores por token del tema (P-4): cambia el modo del shell y los efectos token-aware se
re-tintan. Contrato P: <code>src/packs/ambient/README.md</code>.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill><span data-uix-meta-key>effects</span>32</span>
<span data-uix-meta-pill><span data-uix-meta-key>driver</span>{driver}</span>
<span data-uix-meta-pill><span data-uix-meta-key>tier</span>pack</span>
</div>
</header>
<div style="display:flex; flex-wrap: wrap; gap: var(--space-2); margin-block: var(--space-4)">
{#each effects as name (name)}
<button
type="button"
data-button
data-selected={active === name ? '' : undefined}
onclick={() => (active = name)}
>
{name}
</button>
{/each}
</div>
<!-- Live stage -->
<Ambient
effect={active}
params={paramsByEffect[active] as never}
style="height: 420px; border-radius: var(--radius-lg); overflow: hidden; background: var(--color-surface-default)"
params={params as never}
style={`height: 420px; border-radius: var(--radius-lg); overflow: hidden; background: ${STAGE_BG[stageBg]}`}
/>
<!-- Effect selector -->
<section data-uix-section>
<h2 data-uix-section-title>Efecto</h2>
{#each GROUPS as group (group.heading)}
<div data-uix-subsection-head>{group.heading}</div>
<div data-uix-chips role="radiogroup" style="flex-wrap: wrap;">
{#each group.names as name (name)}
<button data-uix-chip data-active={active === name} onclick={() => (active = name)}>{name}</button>
{/each}
</div>
{/each}
</section>
<!-- Auto-generated live controls -->
<section data-uix-section>
<h2 data-uix-section-title>Parámetros <span data-uix-tab-count>{entries.length}</span></h2>
<p data-uix-section-desc>
Generados del <code>defaults</code> del efecto. Los campos de color aceptan tokens
(<code>--color-*</code> → resueltos vía <code>eidos.resolveToken</code>) o cualquier color CSS.
<button data-uix-chip onclick={() => seed(active)} style="margin-inline-start: var(--space-2);">↺ reset</button>
<label style="margin-inline-start: var(--space-3);">
fondo:
<span data-uix-chips role="radiogroup">
{#each ['surface', 'black', 'white', 'transparent'] as const as bg (bg)}
<button data-uix-chip data-active={stageBg === bg} onclick={() => (stageBg = bg)}>{bg}</button>
{/each}
</span>
</label>
</p>
<div data-uix-controls>
{#each entries as [key, value] (key)}
{@const kind = kindOf(value)}
<label data-uix-control>
<span data-uix-control-label>
{key}
{#if isColorish(key)}<span data-uix-control-hint>token o color CSS</span>{/if}
</span>
{#if kind === 'number'}
{@const b = numBounds(defaults[key] as number)}
<span style="display:flex; align-items:center; gap: var(--space-2);">
<input
type="range"
min={b.min}
max={b.max}
step={b.step}
value={value as number}
oninput={(e) => setParam(key, Number(e.currentTarget.value))}
/>
<input
type="number"
step={b.step}
value={value as number}
oninput={(e) => setParam(key, Number(e.currentTarget.value))}
style="inline-size: 6rem;"
/>
</span>
{:else if kind === 'boolean'}
<span data-uix-chips role="radiogroup">
<button data-uix-chip data-active={value === true} onclick={() => setParam(key, true)}>true</button>
<button data-uix-chip data-active={value === false} onclick={() => setParam(key, false)}>false</button>
</span>
{:else if kind === 'colors'}
<input
type="text"
value={(value as string[]).join(', ')}
oninput={(e) => setColorList(key, e.currentTarget.value)}
style="inline-size: 24rem; max-inline-size: 100%;"
/>
{:else}
<input
type="text"
value={value as string}
oninput={(e) => setParam(key, e.currentTarget.value)}
style="inline-size: 16rem;"
/>
{/if}
</label>
{/each}
</div>
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="eidos">pack</span>
<span>import = opt-in (tree-shaken) · params por token</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{snippet}</code></pre>
</div>
</section>
</div>

Loading…
Cancel
Save

Powered by TurnKey Linux.