feat(words): complete image visual panel (R5 / POLISH-1b)

The drawer image panel already had a corner-radius slider and
shadow/border toggles. Adds the controls that were still missing so
the V2 `block.visual` sidecar is fully editable from the UI:

- shadow blur slider (shown when shadow is on) → visual.shadow.blur
- shadow color picker → visual.shadow.color
- border color picker (shown when border is on) → visual.borderColor
- background color picker (always, with clear) → visual.background

All route through a small reusable `imageColorField` snippet (preset
swatches + native <input type=color> + optional clear), mirroring the
format panel's text-color UI, and dispatch `setBlockVisual`. The
engine already merges + renders these via visualToStyle onto the
<figure>.

Verified in browser: panel renders all four new controls when an image
is selected; clicking a Background swatch updates the figure
(background-color:#22c55e) while preserving radius/border/shadow
(shallow-merge intact). npm run check: 0 errors (26 pre-existing
warnings, none new).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 4a0661a53b
commit cc4412b8e9

@ -120,24 +120,35 @@ asignado al `value` bindeado rompería los `$derived`).
sobre el union V2, `exportWordsDocument` + `importDraftDocument`
reescritos, prosa de la API-tab limpiada de striped/tone)
## Pendiente para mañana
### Limpieza de huérfanos del drop D2 (fuera de scope de F6)
Los literales de comando borrados (`toggle-table-striped`,
`toggle-table-compact`, `table-cell-tone-*`) todavía aparecen como
**selectores CSS huérfanos** en
`src/uix/eidos/components/words/words.css` y como menciones en docs
(README / `ARCHITECTURE_PROPOSAL.md` / este `continue.md` viejo). No
rompen nada (selectores muertos), pero conviene barrerlos. Audit:
grep `striped|compact|cell-tone` en `src/uix/eidos` + docs.
## Cerrado en la continuación (2026-05-29)
### ✅ Limpieza de huérfanos del drop D2
Borrados los selectores CSS muertos de `words.css`
(`[data-command='table-cell-tone-*']`, `table[data-words-table-striped]`,
`[data-words-table-compact]`, `[data-words-cell-tone]` + focus-within, y
los `[data-words-cell-header]` huérfanos — los headers se renderizan como
`<th>`). README actualizado a la realidad V2 (tabla lista solo formato
vivo; HTML/MD export-only; import JSON + texto). `ARCHITECTURE_PROPOSAL.md`
intacto: documenta la *decisión* de eliminarlos, no features stale.
Verificado en browser (tabla 2×2 renderiza, `<th>` conserva su tinte vía
`:where(th)`). Commit `4a0661a5`.
### ✅ R5 — POLISH-1b (image visuals)
El panel de imagen del drawer
(`eidos/components/words/words-drawer.svelte > imagePanel`) ya tenía radius
slider + shadow/border toggles. Añadido lo que faltaba: **shadow blur
slider**, **shadow color picker**, **border color picker** y **background
color picker**, vía un snippet reutilizable `imageColorField` (presets +
`<input type=color>` + clear opcional), mismo patrón que el color de texto
del panel format. Todos despachan `setBlockVisual` → `block.visual.*` →
`visualToStyle` → inline style en el `<figure>`. Verificado en browser: el
panel renderiza los 4 controles nuevos; click en swatch de Background
actualiza el figure (`background-color:#22c55e`) preservando
radius/border/shadow (shallow-merge correcto). `npm run check` 0 errors.
### R5 — POLISH-1b (image visuals)
Drawer image panel: sliders (radius, shadow blur), color pickers
(shadow color, borderColor, background), toggles (shadow, border).
La base existe: `setBlockVisual(blockIndex, patch)` ya es operación V2
con whitelist P8. Falta el panel UI en el drawer + cablear el comando.
## Pendiente para mañana
### P1 — Drag handle FUERA del rail sobre bloques `<code>` (heredado)

@ -829,6 +829,75 @@
<span>Border</span>
</label>
</div>
{@const shadow = 'shadow' in visual ? visual.shadow : undefined}
{@const shadowBlur = shadow?.blur ?? 12}
{@const shadowColor = shadow?.color ?? '#000000'}
{@const borderColor = 'borderColor' in visual ? visual.borderColor ?? '' : ''}
{@const background = 'background' in visual ? visual.background ?? '' : ''}
{#if hasShadow}
<div data-words-drawer-section-subtitle>Shadow blur</div>
<div data-words-drawer-actions>
<Slider
value={[shadowBlur]}
min={0}
max={40}
step={1}
onValueChange={(next) => {
const b = next[0] ?? 0;
s.applyCommand({
type: 'setBlockVisual',
blockIndex,
visual: {
...visual,
shadow: { ...(shadow ?? { x: 0, y: 4, color: '#00000033' }), blur: b }
}
});
}}
aria-label="Image shadow blur"
size="sm"
>
<Slider.Range />
<Slider.Thumb />
</Slider>
<span data-words-drawer-readonly-text>{shadowBlur}px</span>
</div>
<div data-words-drawer-section-subtitle>Shadow color</div>
{@render imageColorField(shadowColor, '#000000', (v) =>
s.applyCommand({
type: 'setBlockVisual',
blockIndex,
visual: {
...visual,
shadow: { ...(shadow ?? { x: 0, y: 4, blur: 12 }), color: v }
}
}))}
{/if}
{#if hasBorder}
<div data-words-drawer-section-subtitle>Border color</div>
{@render imageColorField(borderColor, '#cccccc', (v) =>
s.applyCommand({
type: 'setBlockVisual',
blockIndex,
visual: { ...visual, borderColor: v }
}))}
{/if}
<div data-words-drawer-section-subtitle>Background</div>
{@render imageColorField(
background,
'#ffffff',
(v) =>
s.applyCommand({
type: 'setBlockVisual',
blockIndex,
visual: { ...visual, background: v }
}),
() =>
s.applyCommand({
type: 'setBlockVisual',
blockIndex,
visual: { ...visual, background: undefined }
})
)}
{@const status = block.id ? s.imageStatus.get(block.id) : undefined}
{#if status}
<div data-words-drawer-section-subtitle>Status</div>
@ -841,6 +910,45 @@
{/if}
{/snippet}
{#snippet imageColorField(
current: string,
fallback: string,
onPick: (value: string) => void,
onClear?: () => void
)}
<div data-words-drawer-color-picker>
{#each COLOR_PRESETS as preset (preset)}
<button
type="button"
data-words-drawer-color-swatch
data-active={current === preset ? '' : undefined}
style="background-color:{preset}"
title={preset}
aria-label={`Set color to ${preset}`}
onmousedown={(e) => e.preventDefault()}
onclick={() => onPick(preset)}
></button>
{/each}
<input
type="color"
data-words-drawer-color-input
value={current || fallback}
aria-label="Pick custom color"
onchange={(e) => onPick((e.currentTarget as HTMLInputElement).value)}
/>
{#if onClear && current}
<button
type="button"
data-words-drawer-color-clear
title="Clear"
aria-label="Clear color"
onmousedown={(e) => e.preventDefault()}
onclick={() => onClear()}
>×</button>
{/if}
</div>
{/snippet}
<script context="module" lang="ts">
import type { ProviderSnippetProps as PSP } from '$soma/components/words';
type Warning = {

Loading…
Cancel
Save

Powered by TurnKey Linux.