feat(eidos): dialog/drawer modal-plane + h3 title role + framework-Button trigger/close + density testbed

- dialog + drawer adopt data-depth="modal" (surface·border·base-shadow from the plane);
  recipes pruned of orphan content-bg/border/shadow. The bundle's on-surface font fixes
  the Times New Roman of portaled content (Decisión 8).
- Dialog.Close inherits the dialog size via context.ts, capped at md (sm→sm, md/lg/xl/full→md;
  full is layout, not a bigger control — Radix/Mantine/MD3). THEMING §5 container→part norm.
- Dialog title → h3 typography role (--dialog-title-* points at --style-h3-*, override knob kept).
- Dialog trigger + close compose the framework <Button> via the asChild `child` snippet
  (the trigger was a passthrough that rendered soma's native <button>); variant/color/size
  defer to the canonical Button defaults (solid/primary) — no hardcoded ghost/neutral.
- demo layout (web/routes/uix/+layout@.svelte): "Display" control — density·scaling·border-width
  applied to :root (reaches portaled overlays) to observe the interrelated theming axes live.
- update the depth-plane test (z is token-only; on-surface font) + the archetype audit hand-off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 27012a5c68
commit 7d31cd3fd7

@ -31,6 +31,36 @@ consuman por defecto**.
---
## ESTADO DE IMPLEMENTACIÓN — hand-off 2026-06-20
**Hecho hoy (sobre el A1 de ayer):**
- **A1 piloto dialog + drawer (plano `modal`)** — `data-depth="modal"` en ambos `Content`;
surface·border·base-shadow vienen del plano; recipes podados (`content-bg/border/
border-width/shadow` huérfanos; el drawer conserva `content-bg` = gap del focus-ring +
`content-shadow-dragging`). El **on-surface font** del bundle (Decisión 8) arregla el
Times New Roman de contenidos portalizados.
- **Norma contenedor→parte: cap en `md`** (THEMING §5). `Dialog.Close` hereda el size del
dialog vía `context.ts` capando en `md` (sm→sm; md/lg/xl/full→md — `full` es layout, no
control mayor; alineado Radix/Mantine/MD3). Verificado runtime.
- **Dialog title → rol `h3`** (Arq. 5): el recipe apunta `--dialog-title-*` a `--style-h3-*`
(perilla de override viva); mueve el título de Lora (off-scale) a `primary` (sans).
- **Dialog trigger + close = el Button del framework** (patrón `asChild`): el wrapper eidos
del **trigger era passthrough** → renderizaba el `<button>` nativo del soma; arreglado
componiendo `<Button>` vía el snippet `child`. variant/color/size defieren al canónico
(solid/primary). Memoria `reference_soma_native_aschild_button_composition`. **Tarea
spawneada**: auditar los trigger wrappers de los demás overlays (passthrough probable).
- **Testbed**: control "Display" en `web/routes/uix/+layout@.svelte` — densidad·scaling·
border-width sobre `:root` (alcanza overlays portalizados) para observar los ejes en vivo.
**Button audit (source):** bien construido (Radix variante×color, font 1:1, hover por-variante,
disabled/transición canónicos). 2 hallazgos: **focus forkea el `--focus-ring-*` canónico**
(`outline + palette-border`; ítem Arq. 8 / §9.3) · **active hardcodeado** (`translateY(1px)`).
**Siguiente:** (1) decidir foco del Button (unificar `--focus-ring-*` + per-intent vs primary-tint)
+ active; (2) B2 rollout del plano a los overlays restantes; (3) la tarea de los trigger passthrough.
---
## ESTADO DE IMPLEMENTACIÓN — hand-off 2026-06-19
**Hecho:**

@ -557,6 +557,27 @@ recipe soporta. Categorías:
La categorización vive en
[`web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md §12.8`](../../../web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md).
### Derivación contenedor→parte: cap en `md` (norma 2026-06-19)
Cuando una parte deriva su `size` de su **contenedor** (p. ej. `Dialog.Close`
hereda el size del dialog), la parte **sigue el size del contenedor solo en los
pasos por debajo de `md`**; en `md` y por encima (`lg` / `xl` / `full`) **cap a
la densidad normal `md`**. Un contenedor más ancho — o `full` — **NO engorda sus
controles**: `full` es layout (llena el viewport), no un tamaño de control mayor.
| size del contenedor | size de la parte |
|---|---|
| `xs` / `sm` | `xs` / `sm` (sigue el paso) |
| `md` / `lg` / `xl` / `full` | `md` (normal) |
Coincide con las referencias: Radix separa `size` (densidad) de `width` (full);
Mantine `fullScreen` **ignora** `size`; Material 3 full-screen es un **tipo de
layout** (top app bar) con controles estándar. Ninguno agranda los controles por
ser el dialog `full`.
Primer consumidor: `Dialog.Close` — `components/dialog/context.ts` publica el size
del dialog y el Close lo deriva. Una prop `size` explícita en la parte siempre gana.
### Mapeo size→fuente: 1:1 universal (override 2026-06-17)
> **Supersede los arquetipos `control`/`compact`/`dense` de la Fase 7.** El usuario

@ -491,7 +491,7 @@ describe('ActiveEidos config', () => {
eidos.clearSpacing();
});
it('emits depth plane tokens + [data-depth] rules (composing surface/shadow/halo/z)', () => {
it('emits depth plane tokens + [data-depth] rules (surface/border/shadow/halo + on-surface font; z token-only)', () => {
const css = createThemeBaseEidos().renderStaticCss();
// surface/shadow/z compose the existing primitives — no new math there
expect(css).toContain('--depth-overlay-shadow: var(--shadow-overlay);');
@ -501,10 +501,15 @@ describe('ActiveEidos config', () => {
expect(css).toContain(
'--depth-overlay-halo: inset 0 1px 0 0 color-mix(in oklab, white 7%, transparent);'
);
// the rule applies the safe additive cues; box-shadow composes the drop shadow + the rim
// The rule paints the visual elevation bundle (surface · border · shadow ·
// halo) + anchors the on-surface UI font (portal-safe typography, Decisión 8).
expect(css).toContain("[data-depth='overlay'] {");
expect(css).toContain('box-shadow: var(--depth-overlay-shadow), var(--depth-overlay-halo);');
expect(css).toContain('z-index: var(--depth-overlay-z);');
expect(css).toContain('font-family: var(--font-ui);');
// z stays a TOKEN (asserted above) but is NOT applied as `z-index` in the
// bundle rule — positioning is the floating wrapper's job, not the elevation
// bundle (Decisión 8). Lock that the rule never re-adds it.
expect(css).not.toContain('z-index: var(--depth-overlay-z);');
// a plane without a halo (flush) keeps a single-value box-shadow — no dangling comma
expect(css).toContain('box-shadow: var(--depth-flush-shadow);');
});

@ -0,0 +1,31 @@
import { getContext, setContext } from 'svelte';
import type { DialogSize } from './types';
/**
* Eidos context for `<Dialog.*>` parts — the dialog publishes its resolved
* Content `size` so the parts it owns (close button, and any size-aware part)
* derive their own size from the container instead of hardcoding one.
*
* The size norm (THEMING.md §5 "Subset por componente"): a part follows the
* container's size, clamped to the part's own subset. The close composes
* `Button`, whose subset tops out at `xl`, so a `full` dialog resolves the
* close to `xl`. The reader owns that clamp; the context just carries the
* dialog's resolved size verbatim.
*
* Property getter so reads stay reactive — when the dialog's `size` prop
* changes, parts observing `ctx.size` see the new value on the next read.
*/
const KEY = Symbol('dialog-eidos-ctx');
export interface DialogEidosCtx {
readonly size: DialogSize;
}
export function setDialogEidosCtx(ctx: DialogEidosCtx): void {
setContext(KEY, ctx);
}
export function getDialogEidosCtx(): DialogEidosCtx | undefined {
return getContext<DialogEidosCtx | undefined>(KEY);
}

@ -11,22 +11,28 @@
* - text close — children given, no `position` → renders a `<Button>` with
* the label flowing inline (e.g. a "Cancel" action inside a Footer).
*
* Default chrome is `variant='ghost' size='sm'` (matches the old close
* tokens: transparent bg, neutral hover, secondary→primary text). A `child`
* snippet bypasses both primitives for full control.
* `variant` and `color` defer to the canonical Button defaults (`solid` /
* `primary`) — no hardcoded ghost/neutral. The `size` follows the
* dialog's size only for steps BELOW `md`; at `md` and above (lg / xl / full)
* it caps at the normal `md` density — a wider or `full` dialog does NOT
* enlarge its controls (`full` is layout, not a bigger size — matches Radix
* `size`≠`width`, Mantine `fullScreen` ignoring `size`, MD3 full-screen). An
* explicit `size` prop overrides. A `child` snippet bypasses both primitives.
*/
import { ActiveEidos } from '$uix/eidos';
import * as Dialog from '$soma/components/dialog';
import { Button } from '$uix/eidos/components/button';
import type { ButtonSize } from '$uix/eidos/components/button';
import { IconButton } from '$uix/eidos/components/icon-button';
import { SvgX } from '$uix/eidos/components/svg';
import { getDialogEidosCtx } from './context';
import type { DialogCloseProps } from './types';
let {
position,
variant = 'ghost',
size = 'sm',
color = 'neutral',
variant,
size,
color,
'aria-label': ariaLabel,
child: outerChild,
children,
@ -34,6 +40,14 @@
}: DialogCloseProps = $props();
const eidos = ActiveEidos.require();
const dialogCtx = getDialogEidosCtx();
// Size norm: a part follows the dialog size only for steps BELOW `md`; at `md`
// and above (lg / xl / full) it caps at the normal `md` density. A wider/full
// dialog does not grow its controls — `full` is layout, not a bigger control
// size. DialogSize below `md` is only `sm`. No context (standalone) → `md`.
// An explicit `size` prop always wins.
const inheritedSize = $derived<ButtonSize>(dialogCtx?.size === 'sm' ? 'sm' : 'md');
const resolvedSize = $derived(size ?? inheritedSize);
const resolvedPosition = $derived(position !== undefined ? eidos.resolve(position) : undefined);
// Icon mode = floating corner dismiss: an explicit `position`, or no label.
const iconMode = $derived(resolvedPosition !== undefined || !children);
@ -45,11 +59,11 @@
{#if outerChild}
{@render outerChild({ props })}
{:else if iconMode}
<IconButton {...props} {variant} {size} {color} aria-label={closeLabel}>
<IconButton {...props} {variant} size={resolvedSize} {color} aria-label={closeLabel}>
{#if children}{@render children()}{:else}<SvgX />{/if}
</IconButton>
{:else}
<Button {...props} {variant} {size} {color} aria-label={ariaLabel}>
<Button {...props} {variant} size={resolvedSize} {color} aria-label={ariaLabel ?? undefined}>
{@render children?.()}
</Button>
{/if}

@ -17,6 +17,7 @@
import { ActiveEidos } from '$uix/eidos';
import { composeInlineStyle } from '$uix/eidos/lib/style';
import * as Dialog from '$soma/components/dialog';
import { setDialogEidosCtx } from './context';
import type { DialogContentProps } from './types';
// `children` is renamed to `bodyContent` so the inner `{#snippet children}`
@ -41,6 +42,13 @@
const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md'));
// Publish the resolved size so the parts the dialog owns (close, footer
// actions) follow it instead of hardcoding their own — see context.ts.
setDialogEidosCtx({
get size() {
return resolvedSize;
}
});
const resolvedPosition = $derived(eidos.resolve(position, 'middle-center'));
const isSheet = $derived(resolvedSize !== 'full' && eidos.isBelow('sm'));
@ -81,6 +89,7 @@
data-size={resolvedSize}
data-position={resolvedPosition}
data-sheet={isSheet ? '' : undefined}
data-depth="modal"
data-animation-style={motion === 'none' ? undefined : motion}
>
{#snippet children(snippetProps)}

@ -1,10 +1,27 @@
<script lang="ts">
/**
* Eidos `<Dialog.Trigger>` — composes the canonical `<Button>` via soma's
* `child` snippet, so the trigger is a framework Button (not soma's native
* `<button>` fallback). `variant` / `size` / `color` default to nothing and
* therefore defer to Button's canonical defaults (`solid` / `md` / `primary`
* — the primary "open" action). A `child` snippet bypasses the Button for a
* fully custom trigger element.
*/
import * as Dialog from '$soma/components/dialog';
import { Button } from '$uix/eidos/components/button';
import type { DialogTriggerProps } from './types';
let { children, ...rest }: DialogTriggerProps = $props();
let { children, variant, size, color, child: outerChild, ...rest }: DialogTriggerProps = $props();
</script>
<Dialog.Trigger {...rest}>
{#snippet child({ props })}
{#if outerChild}
{@render outerChild({ props })}
{:else}
<Button {...props} {variant} {size} {color}>
{@render children?.()}
</Button>
{/if}
{/snippet}
</Dialog.Trigger>

@ -100,11 +100,12 @@
max-height: var(--_dialog-content-max-height-override, var(--dialog-content-max-height));
padding: var(--_dialog-padding);
overflow: auto;
border: var(--dialog-content-border-width) solid var(--dialog-content-border);
/* Elevation bundle (surface · border · shadow) comes from the `modal` plane
via `data-depth` (A1, Decisión 8). The panel keeps only its own radius /
color — radius is a separate axis (Radix factor, A2). The intent variants
below override `border-color` on top of the plane's border. */
border-radius: var(--dialog-content-radius);
background: var(--dialog-content-bg);
color: var(--dialog-content-color);
box-shadow: var(--dialog-content-shadow);
outline: none;
}
@ -238,9 +239,7 @@
* a dialog at all. Each intent tints the panel border and (for evaluative
* intents) the title color. The body background stays neutral so long-form
* content doesn't compete with the intent signal. */
[data-dialog-content][data-color='neutral'] {
border-color: var(--dialog-content-border);
}
/* neutral = the plane's own border (`--depth-modal-border`); no override needed. */
[data-dialog-content][data-color='risk'] {
border-color: var(--color-risk-border);
@ -294,10 +293,17 @@
[data-dialog-title] {
margin: 0;
/* Typography role (Arq. 5 / THEMING §3): the title reads its own
`--dialog-title-*` knobs, which the recipe defaults to the canonical `h3`
heading role (primary/sans · lg · semibold). The role gives coherence by
default; the `--dialog-title-*` tokens stay the override surface — a theme,
the recipe config, or one `<Dialog.Content style>` can restyle dialog titles
WITHOUT retuning every `h3`. The intent tints below still override color. */
font-family: var(--dialog-title-font-family);
font-size: var(--dialog-title-font-size);
font-weight: var(--dialog-title-font-weight);
line-height: var(--dialog-title-line-height);
letter-spacing: var(--dialog-title-letter-spacing);
color: var(--dialog-title-color);
text-wrap: balance;
}

@ -31,7 +31,21 @@ export type DialogPosition = Position;
export type DialogCssLength = string | number;
export type DialogProps = ProviderProps;
export type DialogTriggerProps = TriggerProps;
export type DialogTriggerProps = TriggerProps & {
/**
* Visual variant of the underlying button. The trigger composes the canonical
* `<Button>`, so with no value it defers to Button's canonical default (`solid`).
*/
variant?: ButtonVariant;
/** Button size of the trigger. Defers to Button's canonical default (`md`). */
size?: ResponsiveProp<ButtonSize>;
/**
* Hierarchical color accent. The trigger is the primary "open" action, so with
* no value it defers to Button's canonical default (`primary`).
*/
color?: ButtonProps['color'];
};
export type DialogContentProps = ContentProps & {
/**
@ -133,11 +147,9 @@ export type DialogCloseProps = CloseProps & {
*/
size?: ResponsiveProp<ButtonSize>;
/**
* Hierarchical color accent. The close is a chromeless dismiss, so it
* defaults to `'neutral'` (the old close was `content-secondary` grey) —
* not Button's promoted `'primary'`.
*
* @default 'neutral'
* Hierarchical color accent. With no value it defers to Button's canonical
* default (`'primary'`) — the close IS the framework Button. An app that wants
* a subtle dismiss overrides per-usage (`color="neutral"` or a ghost variant).
*/
color?: ButtonProps['color'];
};

@ -69,6 +69,7 @@
{...rest}
style={composedStyle}
data-size={resolvedSize}
data-depth="modal"
data-animation-style={motion === 'none' ? undefined : motion}
>
{#snippet children(snippetProps)}

@ -82,11 +82,11 @@
max-inline-size: var(--_drawer-content-max-width-override, none);
min-block-size: var(--_drawer-content-min-height-override, auto);
max-block-size: var(--_drawer-content-max-height-override, none);
border: var(--drawer-content-border-width) solid var(--drawer-content-border);
/* Elevation bundle (surface · border · shadow) from the `modal` plane via
`data-depth` (A1, Decisión 8). Keeps its own radius/color; the
[data-dragging] box-shadow override below still wins while dragging. */
border-radius: var(--drawer-content-radius);
background: var(--drawer-content-bg);
color: var(--drawer-content-color);
box-shadow: var(--drawer-content-shadow);
outline: none;
}

@ -737,12 +737,11 @@ export const THEME_BASE_RECIPE_TOKENS = {
'overlay-opacity': '62%',
'overlay-blur': 'var(--blur-lg)',
'overlay-z': '70',
'content-bg': 'var(--color-surface-overlay)',
// Elevation (bg · border · shadow) now comes from the `modal` plane via
// `data-depth` (A1, Decisión 8). The recipe keeps only non-elevation:
// content color + the panel radius (separate axis — Radix factor, A2).
'content-color': 'var(--color-content-primary)',
'content-border': 'var(--color-border-default)',
'content-border-width': 'var(--border-width)',
'content-radius': 'var(--radius-xl)',
'content-shadow': 'var(--depth-overlay-shadow), var(--depth-overlay-halo)',
'content-max-height': 'calc(100dvh - var(--space-8))',
'content-max-height-sheet': '85dvh',
'content-width-sm': '420px',
@ -761,11 +760,17 @@ export const THEME_BASE_RECIPE_TOKENS = {
'header-gap': 'var(--space-3)',
'footer-gap': 'var(--space-3)',
'footer-margin-top': 'var(--space-2)',
'title-font-family': 'var(--font-heading)',
'title-font-size': 'var(--font-size-lg)',
'title-font-weight': 'var(--font-weight-semibold)',
'title-line-height': 'var(--leading-heading)',
'title-color': 'var(--color-content-primary)',
// Title = the canonical `h3` heading role (Arq. 5 / THEMING §3). These stay
// public knobs (a theme / instance overrides them to restyle dialog titles
// without retuning every h3); their DEFAULT is the role. Adopting `h3` moves
// the title off the old `--font-heading` (Lora) — off the type scale at an
// h3 size — onto `primary` (sans), per h3–h6 = primary.
'title-font-family': 'var(--style-h3-font-family)',
'title-font-size': 'var(--style-h3-font-size)',
'title-font-weight': 'var(--style-h3-font-weight)',
'title-line-height': 'var(--style-h3-line-height)',
'title-letter-spacing': 'var(--style-h3-letter-spacing)',
'title-color': 'var(--style-h3-color)',
'description-font-family': 'var(--font-ui)',
'description-font-size': 'var(--font-size-sm)',
'description-line-height': 'var(--leading-ui)',
@ -908,12 +913,12 @@ export const THEME_BASE_RECIPE_TOKENS = {
'handle-active-scale': '1.18',
'handle-transition-duration': 'var(--duration-fast)',
'handle-transition-ease': 'var(--ease-out)',
// Surface · border · base-shadow now come from the `modal` plane (data-depth,
// A1). `content-bg` survives only as the close focus-ring gap color (= the
// plane surface, same value); the in-flight drag shadow keeps its own token.
'content-bg': 'var(--color-surface-overlay)',
'content-color': 'var(--color-content-primary)',
'content-border': 'var(--color-border-default)',
'content-border-width': 'var(--border-width)',
'content-radius': 'var(--radius-xl)',
'content-shadow': 'var(--depth-overlay-shadow), var(--depth-overlay-halo)',
'content-shadow-dragging':
'var(--shadow-overlay-strong, var(--depth-overlay-shadow)), var(--depth-overlay-halo)',
'content-ring-dragging-width': 'var(--border-width-medium)',

@ -378,6 +378,20 @@
let motionOn = $state(true);
let hapticOn = $state(true);
// ── Display axes — the interrelated theming knobs (density · scaling · border
// width). Projected onto :root (below) via a managed stylesheet so EVERY demo —
// including portaled overlays (dialog/popover) — reflects the change live.
type Density = 'compact' | 'comfortable' | 'spacious';
const DENSITIES: readonly Density[] = ['compact', 'comfortable', 'spacious'];
const DENSITY_SCALES: Record<Density, { space: number; control: number }> = {
compact: { space: 0.84, control: 0.9 },
comfortable: { space: 1, control: 1 },
spacious: { space: 1.16, control: 1.12 }
};
let density = $state<Density>('comfortable');
let scaling = $state(1);
let borderWidth = $state(1);
const LANGUAGE_LABELS: Record<Language, string> = {
es: 'Español',
en: 'English',
@ -413,6 +427,10 @@
if (typeof p.soundOn === 'boolean') soundOn = p.soundOn;
if (typeof p.motionOn === 'boolean') motionOn = p.motionOn;
if (typeof p.hapticOn === 'boolean') hapticOn = p.hapticOn;
if (p.density === 'compact' || p.density === 'comfortable' || p.density === 'spacious')
density = p.density;
if (typeof p.scaling === 'number') scaling = p.scaling;
if (typeof p.borderWidth === 'number') borderWidth = p.borderWidth;
} catch {
/* ignore corrupt prefs blob */
}
@ -420,7 +438,19 @@
$effect(() => {
if (typeof localStorage === 'undefined') return;
const blob = { dir, language, currency, units, numberFormat, soundOn, motionOn, hapticOn };
const blob = {
dir,
language,
currency,
units,
numberFormat,
soundOn,
motionOn,
hapticOn,
density,
scaling,
borderWidth
};
localStorage.setItem('uix-docs-prefs', JSON.stringify(blob));
});
@ -435,6 +465,17 @@
uix.prefs.setIntent('haptic', hapticOn ? 'allow' : 'reduce');
});
// Project the display axes onto :root through a managed <style> (id-keyed, so
// it replaces in place). :root — not the docs shell — so portaled overlays,
// which render outside the shell, inherit the same density/scaling/border.
$effect(() => {
const d = DENSITY_SCALES[density];
uix.dom.writeStyle(
'uix-docs-display',
`:root{--density-space-scale:${d.space};--density-control-scale:${d.control};--scaling:${scaling};--border-width:${borderWidth}px;}`
);
});
const filteredNav = $derived.by(() => {
const q = search.trim().toLowerCase();
if (!q) return nav;
@ -664,6 +705,101 @@
</Popover.Content>
</Popover>
<!-- DISPLAY: density / scaling / border width — the interrelated
theming axes, applied to :root so every demo (including
portaled overlays) reflects the change live. -->
<Popover>
<Popover.Trigger>
<span data-uix-label-btn>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<line x1="4" y1="7" x2="20" y2="7" />
<circle cx="9" cy="7" r="2.5" />
<line x1="4" y1="17" x2="20" y2="17" />
<circle cx="15" cy="17" r="2.5" />
</svg>
Display
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
data-caret
aria-hidden="true"
>
<path d="m6 9 6 6 6-6" />
</svg>
</span>
</Popover.Trigger>
<Popover.Content side="bottom" align="end" sideOffset={6}>
<div data-uix-menu style="min-inline-size: 252px;">
<div data-uix-menu-section>
<div data-uix-menu-label>Density</div>
{#each DENSITIES as d}
<button
type="button"
data-uix-menu-item
data-checked={density === d ? 'true' : 'false'}
onclick={() => (density = d)}
>
<span style="text-transform: capitalize;">{d}</span>
<span data-uix-menu-item-meta
>{DENSITY_SCALES[d].space}× · {DENSITY_SCALES[d].control}×</span
>
</button>
{/each}
</div>
<div data-uix-menu-section>
<div data-uix-menu-label>Scaling · {scaling.toFixed(2)}×</div>
<input
data-uix-range
type="range"
min="0.85"
max="1.25"
step="0.05"
bind:value={scaling}
aria-label="Scaling factor"
/>
</div>
<div data-uix-menu-section>
<div data-uix-menu-label>Border width · {borderWidth}px</div>
<input
data-uix-range
type="range"
min="0"
max="4"
step="0.5"
bind:value={borderWidth}
aria-label="Border width"
/>
</div>
<div data-uix-menu-section>
<button
type="button"
data-uix-menu-item
onclick={() => {
density = 'comfortable';
scaling = 1;
borderWidth = 1;
}}
>
<span>Reset</span>
<span data-uix-menu-item-meta>comfortable · 1× · 1px</span>
</button>
</div>
</div>
</Popover.Content>
</Popover>
<!-- SEMANTICS: sound / motion / haptic toggles. These will hook
into the sema engine channels' enabled state once the
channels expose runtime setEnabled APIs; today they
@ -807,8 +943,3 @@
</main>
</div>
</div>
<style>
/* This layout owns its UIX context directly so components declared in
* the same layout hydrate with ActiveUix/Soma available. */
</style>

@ -403,6 +403,12 @@
padding-block-start: 4px;
border-block-start: 1px solid var(--uix-line);
}
[data-uix-range] {
inline-size: 100%;
margin-block-start: 6px;
accent-color: var(--uix-accent, currentColor);
cursor: pointer;
}
[data-uix-menu-label] {
padding: 4px 10px 6px;
font-family: var(--uix-font-mono);

Loading…
Cancel
Save

Powered by TurnKey Linux.