feat(eidos): Lote A closes — popover + banner compose IconButton; clean drawer orphans

DIM 1 composition (component-coherence audit), close family:

- popover-close + banner-close now compose the canonical <IconButton>/<Button>
  (via soma child snippet for popover; direct for the eidos-only banner) instead
  of bespoke chrome. They inherit Button's variant/size/color/focus-ring; the
  aria-label routes through langs ("Cerrar" / "Descartar"). Consistent with the
  already-migrated dialog/drawer closes (solid/primary default; consumer overrides
  for subtle).
- Removed the redundant [data-popover-close] / [data-banner-close] chrome from CSS
  and the now-orphaned --popover-close-* / --popover-transition-* recipe tokens;
  regenerated base.css. banner keeps only `margin-inline-start: auto` for end
  placement, scoped under [data-banner] so it out-ranks [data-button] (composing
  IconButton flips CSS load order — the equal-specificity bare selector loses).
- Cleaned the 15 orphaned --drawer-close-* recipe tokens the earlier drawer-close
  migration left behind, and fixed the component-visual-attrs manifest: drawer +
  popover closes delegate icon-only to IconButton, so they stamp no wrapper attr.

eidos suite 9->8 failures (the 8 remaining are inherited: spin-field + palabras +
icon, outside this work). svelte-check clean for all touched files. Verified in
browser. Excludes words/palabras/chronos.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
active-uix
dev 4 months ago
parent ebc742e73b
commit 0a43225ec2

@ -709,10 +709,22 @@ time-picker + morfo: **65/65**. Navegador: langs ES + transición de los 4 input
### Pendiente (orden sugerido) ### Pendiente (orden sugerido)
1. **DIM 1 composición — Lote A** (Button-consumer): closes dialog/drawer/popover/ 1. **DIM 1 composición — Lote A** (Button-consumer) — **EN CURSO**:
banner/toast → `IconButton`+`SvgX`; calendar/range prev/next → `IconButton`+ - ✅ **popover-close** + **banner-close** → `IconButton`+`SvgX` (slices completos:
`SvgChevron`; **code-block copy → componer `<Clipboard>`** (cierra también su wrapper + types + borrado de chrome CSS + eliminación de tokens recipe huérfanos
hardcode de langs). Borrar el chrome `--*-control-*`/`--*-close-*` redundante. + regen; verificados en navegador, consistentes con dialog/drawer-close).
- ✅ **drawer-close**: limpiados los 15 tokens recipe huérfanos que la migración
previa dejó + arreglado el manifiesto de `component-visual-attrs` (drawer +
popover componen IconButton → no estampan `data-icon-only`, sin entrada). Suite
eidos 9→8 fallas (las 8 restantes son heredadas: spin-field, palabras, icon).
- **Gotcha confirmado** ([[iconbutton-lote0]]): migrar a IconButton voltea el orden
de carga CSS → `[data-button]{margin:0}` (igual especificidad, carga después) pisa
el positioning del consumidor. Fix: subir especificidad con ancestro
(`[data-banner] [data-banner-close]`).
- ⏳ **Pendiente del Lote A**: **toast-close** (19 refs CSS — chrome más complejo),
**float-panel-close** (chrome COMPARTIDO con `action`+`stage-trigger` → migrar los
tres o extraer con cuidado), **calendar/range prev/next** → `IconButton`+`SvgChevron`
(+ borrar `--calendar-control-*`), **code-block copy** → componer `<Clipboard>`.
2. **DIM 1 — Lote B** (estructural): 6 `<select>` nativos → `<Select>` (calendar 2. **DIM 1 — Lote B** (estructural): 6 `<select>` nativos → `<Select>` (calendar
month/year, range-calendar month/year, color-field/color-picker format). month/year, range-calendar month/year, color-field/color-picker format).
3. **Barrido de langs — cola** (deferidos arriba): avatar-group conteo, date-range-picker 3. **Barrido de langs — cola** (deferidos arriba): avatar-group conteo, date-range-picker

@ -31,11 +31,11 @@ const VISUAL_ATTRS = [
], ],
['checkbox/checkbox.svelte', ['data-size', 'data-variant', 'data-color']], ['checkbox/checkbox.svelte', ['data-size', 'data-variant', 'data-color']],
['checkbox/checkbox-group.svelte', ['data-columns']], ['checkbox/checkbox-group.svelte', ['data-columns']],
// The close composes <IconButton>, which owns the icon-only layout — the // The close composes <IconButton>, which owns the icon-only layout. dialog
// wrapper only wires the dialog-specific floating `data-position`. // adds the floating `data-position`; drawer + popover stamp no wrapper-specific
// visual attr (they fully delegate to IconButton) so they have no entry.
['dialog/dialog-close.svelte', ['data-position']], ['dialog/dialog-close.svelte', ['data-position']],
['dialog/dialog-content.svelte', ['data-size', 'data-position', 'data-sheet']], ['dialog/dialog-content.svelte', ['data-size', 'data-position', 'data-sheet']],
['drawer/drawer-close.svelte', ['data-icon-only']],
['drawer/drawer-content.svelte', ['data-size']], ['drawer/drawer-content.svelte', ['data-size']],
['editable/editable.svelte', ['data-size', 'data-variant', 'data-color']], ['editable/editable.svelte', ['data-size', 'data-variant', 'data-color']],
['field/field.svelte', ['data-size', 'data-variant', 'data-orientation', 'data-color']], ['field/field.svelte', ['data-size', 'data-variant', 'data-orientation', 'data-color']],
@ -44,7 +44,6 @@ const VISUAL_ATTRS = [
['meter/meter.svelte', ['data-size', 'data-shape']], ['meter/meter.svelte', ['data-size', 'data-shape']],
['number-field/number-field.svelte', ['data-size', 'data-variant', 'data-color']], ['number-field/number-field.svelte', ['data-size', 'data-variant', 'data-color']],
['pagination/pagination.svelte', ['data-size']], ['pagination/pagination.svelte', ['data-size']],
['popover/popover-close.svelte', ['data-icon-only']],
['popover/popover-content.svelte', ['data-size']], ['popover/popover-content.svelte', ['data-size']],
['progress/progress.svelte', ['data-size', 'data-shape']], ['progress/progress.svelte', ['data-size', 'data-shape']],
['radio-group/radio-group.svelte', ['data-size', 'data-variant', 'data-color', 'data-columns']], ['radio-group/radio-group.svelte', ['data-size', 'data-variant', 'data-color', 'data-columns']],

@ -1,50 +1,38 @@
<script lang="ts"> <script lang="ts">
/** /**
* Eidos `<Banner.Close>` — visually-styled dismiss button. Pure * Eidos `<Banner.Close>` — the banner dismiss control. Composes the canonical
* eidos-only part: meets the four rules in * `<IconButton>` (eidos-only part — Banner has no soma close; the consumer
* `eidos/components/README.md` (no behavior, no aria contract, no * wires `onclick` to their own visibility state). Inherits Button's chrome,
* event-target role, no keyboard). The consumer wires `onclick` to * focus ring and disabled treatment; only the positioning
* their own visibility state — Banner does not own the dismissal. * (`margin-inline-start: auto`) stays in `banner.css`. Default `variant='ghost'
* * color='neutral'` — a subtle dismiss. The glyph defaults to ✕.
* Defaults to a localized "Dismiss" `aria-label` (via langs) so consumers
* don't have to remember, but the consumer can override.
*/ */
import { ActiveEidos } from '$uix/eidos'; import { ActiveEidos } from '$uix/eidos';
import { IconButton } from '$uix/eidos/components/icon-button';
import { SvgX } from '$uix/eidos/components/svg';
import type { BannerCloseProps } from './types'; import type { BannerCloseProps } from './types';
let { let {
'aria-label': ariaLabel, 'aria-label': ariaLabel,
class: className, variant = 'ghost',
size,
color = 'neutral',
children, children,
...restProps ...restProps
}: BannerCloseProps = $props(); }: BannerCloseProps = $props();
const eidos = ActiveEidos.require(); const eidos = ActiveEidos.require();
// Accessible name routes through langs — explicit prop wins, else the // Accessible name routes through langs — explicit prop wins, else localized.
// localized default. No hardcoded English.
const resolvedLabel = $derived(ariaLabel ?? eidos.langs.ts('#?common.buttons.dismiss|Dismiss')); const resolvedLabel = $derived(ariaLabel ?? eidos.langs.ts('#?common.buttons.dismiss|Dismiss'));
</script> </script>
<button <IconButton
type="button"
{...restProps} {...restProps}
{variant}
size={size ?? 'sm'}
{color}
aria-label={resolvedLabel} aria-label={resolvedLabel}
class={className}
data-banner-close="" data-banner-close=""
> >
{#if children}{@render children()}{:else}<svg {#if children}{@render children()}{:else}<SvgX />{/if}
viewBox="0 0 16 16" </IconButton>
width="14"
height="14"
aria-hidden="true"
focusable="false"
>
<path
d="M3 3l10 10M13 3L3 13"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
</svg>{/if}
</button>

@ -141,30 +141,12 @@
/* ── Close button (eidos-only part) ────────────────────────────────── */ /* ── Close button (eidos-only part) ────────────────────────────────── */
[data-banner-close] { /* Positioning only — `Banner.Close` composes the canonical `<IconButton>`, so
display: inline-flex; its chrome (size, bg, border, hover, focus ring) comes from button.css. The
align-items: center; `margin-inline-start: auto` pushes the dismiss to the end of the banner row.
justify-content: center; Scoped under `[data-banner]` (0,2,0) so it out-ranks `[data-button]`'s own
`margin` (0,1,0) — composing IconButton flips the CSS load order, so an
equal-specificity bare selector would lose the tie. */
[data-banner] [data-banner-close] {
margin-inline-start: auto; margin-inline-start: auto;
inline-size: var(--space-6);
block-size: var(--space-6);
padding: 0;
background: transparent;
color: inherit;
border: 0;
border-radius: var(--radius-sm, var(--space-1));
cursor: pointer;
opacity: var(--opacity-subtle);
transition: opacity var(--duration-fast) ease, background-color var(--duration-fast) ease;
}
[data-banner-close]:hover {
opacity: 1;
background: color-mix(in srgb, currentColor 12%, transparent);
}
[data-banner-close]:focus-visible {
outline: var(--border-width-medium) solid currentColor;
outline-offset: 1px;
opacity: 1;
} }

@ -2,6 +2,7 @@ import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from 'svelte/elements';
import type { ColorRole, ResponsiveProp, Size } from '$uix/eidos/lib/types'; import type { ColorRole, ResponsiveProp, Size } from '$uix/eidos/lib/types';
import type { ChipVariant } from '$uix/eidos/lib/types'; import type { ChipVariant } from '$uix/eidos/lib/types';
import type { IconButtonProps } from '$uix/eidos/components/icon-button';
/** /**
* Banner intent vocabulary. Mirrors the canonical UIX `ColorRole` set * Banner intent vocabulary. Mirrors the canonical UIX `ColorRole` set
@ -49,8 +50,15 @@ export type BannerProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
children?: Snippet; children?: Snippet;
}; };
export type BannerCloseProps = Omit<HTMLAttributes<HTMLButtonElement>, 'children'> & { /**
/** Accessible label for the dismiss button. @default 'Dismiss' */ * `Banner.Close` composes the canonical `<IconButton>`, so it inherits its
* full prop surface (variant / size / color / onclick / disabled / …). It only
* relaxes the two IconButton requirements: `aria-label` (defaults to a
* localized "Dismiss") and `children` (the glyph, defaults to ✕).
*/
export type BannerCloseProps = Omit<IconButtonProps, 'aria-label' | 'children'> & {
/** Accessible label for the dismiss button. @default localized 'Dismiss' */
'aria-label'?: string; 'aria-label'?: string;
/** Glyph override (defaults to the ✕). */
children?: Snippet; children?: Snippet;
}; };

@ -1,31 +1,50 @@
<script lang="ts"> <script lang="ts">
/** /**
* Eidos `Popover.Close` — passes through to headless. Optional auto-X * Eidos `Popover.Close` — composes the canonical button primitives via soma's
* icon when no children are provided (matches the air baseline + * `child` snippet (no bespoke close chrome — it inherits Button's variant /
* Dialog/Drawer pattern). The recipe styles a square button with * focus-ring / disabled):
* focus ring. *
* - no text children → icon close: an `<IconButton>` with the ✕ glyph and a
* localized "Close" `aria-label`.
* - text children → text close: a `<Button>` with the label inline.
*
* A `child` snippet bypasses both primitives.
*/ */
import { ActiveEidos } from '$uix/eidos';
import * as Popover from '$soma/components/popover'; import * as Popover from '$soma/components/popover';
import { Button } from '$uix/eidos/components/button';
import { IconButton } from '$uix/eidos/components/icon-button';
import { SvgX } from '$uix/eidos/components/svg';
import type { PopoverCloseProps } from './types'; import type { PopoverCloseProps } from './types';
let { children, ...rest }: PopoverCloseProps = $props(); let {
variant,
size,
color,
'aria-label': ariaLabel,
child: outerChild,
children,
...rest
}: PopoverCloseProps = $props();
const eidos = ActiveEidos.require();
const resolvedSize = $derived(size ?? 'md');
// Accessible name routes through langs — explicit prop wins, else localized.
const closeLabel = $derived(ariaLabel ?? eidos.langs.ts('#?common.buttons.close|Close'));
</script> </script>
<Popover.Close {...rest} data-icon-only={!children ? '' : undefined}> <Popover.Close {...rest}>
{#if children} {#snippet child({ props })}
{@render children()} {#if outerChild}
{:else} {@render outerChild({ props })}
<svg {:else if children}
width="12" <Button {...props} {variant} size={resolvedSize} {color} aria-label={ariaLabel ?? undefined}>
height="12" {@render children()}
viewBox="0 0 14 14" </Button>
fill="none" {:else}
stroke="currentColor" <IconButton {...props} {variant} size={resolvedSize} {color} aria-label={closeLabel}>
stroke-width="1.6" <SvgX />
stroke-linecap="round" </IconButton>
aria-hidden="true" {/if}
> {/snippet}
<path d="M3 3l8 8M11 3l-8 8" />
</svg>
{/if}
</Popover.Close> </Popover.Close>

@ -224,45 +224,9 @@
} }
/* ── Close button ────────────────────────────────────────────────────── /* ── Close button ──────────────────────────────────────────────────────
* Default sizing is auto-fit (text labels like "Close" or "Dismiss" fit * `Popover.Close` composes the canonical `<IconButton>` / `<Button>` (eidos
* naturally). For icon-only close buttons (× glyph), the consumer sets * wrapper), so all chrome — size, border, bg, hover, focus ring — comes from
* `data-icon-only` on `<Popover.Close>` to switch to a fixed square. */ * `button.css`. No bespoke `[data-popover-close]` recipe here. */
[data-popover-close] {
display: inline-flex;
align-items: center;
justify-content: center;
min-block-size: var(--popover-close-size);
padding-inline: var(--space-3);
border: var(--popover-close-border-width) solid var(--popover-close-border);
border-radius: var(--popover-close-radius);
background: var(--popover-close-bg);
color: var(--popover-close-color);
box-shadow: none;
font: inherit;
font-size: var(--popover-close-font-size);
cursor: pointer;
transition:
background var(--popover-transition-duration) var(--popover-transition-ease),
border-color var(--popover-transition-duration) var(--popover-transition-ease),
color var(--popover-transition-duration) var(--popover-transition-ease),
box-shadow var(--popover-transition-duration) var(--popover-transition-ease);
}
[data-popover-close][data-icon-only] {
inline-size: var(--popover-close-size);
block-size: var(--popover-close-size);
padding: 0;
}
[data-popover-close]:hover {
background: var(--popover-close-bg-hover);
border-color: var(--popover-close-border-hover);
}
[data-popover-close]:focus-visible {
outline: none;
box-shadow: var(--focus-ring);
}
/* ── data-last-action exit tinting (mirrors Dialog/Drawer) ────────────── /* ── data-last-action exit tinting (mirrors Dialog/Drawer) ──────────────
* Prewrite writes `data-last-action` BEFORE state flips to 'closed'. * Prewrite writes `data-last-action` BEFORE state flips to 'closed'.

@ -1,6 +1,7 @@
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import type { ResponsiveProp, Size } from '$uix/eidos/lib/types'; import type { ResponsiveProp, Size } from '$uix/eidos/lib/types';
import type { MotionPresetName } from '$uix/eidos/lib/motion/registry'; import type { MotionPresetName } from '$uix/eidos/lib/motion/registry';
import type { ButtonVariant, ButtonSize, ButtonProps } from '$uix/eidos/components/button';
import type { import type {
ProviderProps, ProviderProps,
TriggerProps, TriggerProps,
@ -26,7 +27,17 @@ export type PopoverTriggerProps = TriggerProps;
export type PopoverArrowProps = ArrowProps; export type PopoverArrowProps = ArrowProps;
export type PopoverTitleProps = TitleProps; export type PopoverTitleProps = TitleProps;
export type PopoverDescriptionProps = DescriptionProps; export type PopoverDescriptionProps = DescriptionProps;
export type PopoverCloseProps = CloseProps; export type PopoverCloseProps = CloseProps & {
/**
* Visual variant of the underlying button. The close composes the canonical
* `<Button>` / `<IconButton>`, so it inherits the shared chrome + focus ring.
*/
variant?: ButtonVariant;
/** Button size of the close control. @default 'md' */
size?: ResponsiveProp<ButtonSize>;
/** Hierarchical color accent — defers to Button's canonical default. */
color?: ButtonProps['color'];
};
export type PopoverAnchorProps = AnchorProps; export type PopoverAnchorProps = AnchorProps;
export type PopoverOverlayProps = OverlayProps & { export type PopoverOverlayProps = OverlayProps & {
/** /**

@ -1174,21 +1174,6 @@
--drawer-description-font-size: var(--font-size-sm); --drawer-description-font-size: var(--font-size-sm);
--drawer-description-line-height: var(--leading-ui); --drawer-description-line-height: var(--leading-ui);
--drawer-description-color: var(--color-content-secondary); --drawer-description-color: var(--color-content-secondary);
--drawer-close-size: var(--control-height-sm);
--drawer-close-px: var(--space-2);
--drawer-close-radius: var(--radius-md);
--drawer-close-bg: transparent;
--drawer-close-bg-hover: color-mix(in srgb, var(--color-neutral-track) 72%, transparent);
--drawer-close-color: var(--color-content-secondary);
--drawer-close-color-hover: var(--color-content-primary);
--drawer-close-border: transparent;
--drawer-close-border-width: var(--border-width);
--drawer-close-transition-duration: var(--duration-fast);
--drawer-close-transition-ease: var(--ease-default);
--drawer-close-focus-ring-offset: 1px;
--drawer-close-focus-ring-width: var(--focus-ring-width);
--drawer-close-focus-ring-color: var(--focus-ring-color);
--drawer-close-disabled-opacity: 0.5;
--icon-size: var(--icon-size-md); --icon-size: var(--icon-size-md);
--icon-stroke-width: var(--icon-stroke-width-md); --icon-stroke-width: var(--icon-stroke-width-md);
--image-size-xs: 64px; --image-size-xs: 64px;
@ -1819,17 +1804,6 @@
--popover-color: var(--color-content-primary); --popover-color: var(--color-content-primary);
--popover-arrow-width: var(--space-5); --popover-arrow-width: var(--space-5);
--popover-arrow-height: var(--space-2); --popover-arrow-height: var(--space-2);
--popover-transition-duration: var(--duration-fast);
--popover-transition-ease: var(--ease-default);
--popover-close-size: var(--control-height-xs);
--popover-close-font-size: var(--font-size-sm);
--popover-close-radius: var(--radius-md);
--popover-close-border-width: var(--border-width);
--popover-close-bg: transparent;
--popover-close-bg-hover: color-mix( in srgb, var(--color-neutral-track) 72%, var(--popover-bg) );
--popover-close-color: var(--color-content-secondary);
--popover-close-border: color-mix(in srgb, var(--color-border-default) 72%, transparent);
--popover-close-border-hover: var(--color-border-strong);
--progress-height-xs: 2px; --progress-height-xs: 2px;
--progress-height-sm: 4px; --progress-height-sm: 4px;
--progress-height-md: 6px; --progress-height-md: 6px;

@ -938,22 +938,9 @@ export const THEME_BASE_RECIPE_TOKENS = {
'description-font-family': 'var(--font-ui)', 'description-font-family': 'var(--font-ui)',
'description-font-size': 'var(--font-size-sm)', 'description-font-size': 'var(--font-size-sm)',
'description-line-height': 'var(--leading-ui)', 'description-line-height': 'var(--leading-ui)',
'description-color': 'var(--color-content-secondary)', 'description-color': 'var(--color-content-secondary)'
'close-size': 'var(--control-height-sm)', // `close-*` tokens removed: Drawer.Close composes the canonical
'close-px': 'var(--space-2)', // <IconButton>/<Button>, so its chrome lives in button.css.
'close-radius': 'var(--radius-md)',
'close-bg': 'transparent',
'close-bg-hover': 'color-mix(in srgb, var(--color-neutral-track) 72%, transparent)',
'close-color': 'var(--color-content-secondary)',
'close-color-hover': 'var(--color-content-primary)',
'close-border': 'transparent',
'close-border-width': 'var(--border-width)',
'close-transition-duration': 'var(--duration-fast)',
'close-transition-ease': 'var(--ease-default)',
'close-focus-ring-offset': '1px',
'close-focus-ring-width': 'var(--focus-ring-width)',
'close-focus-ring-color': 'var(--focus-ring-color)',
'close-disabled-opacity': '0.5'
}, },
icon: { icon: {
size: 'var(--icon-size-md)', size: 'var(--icon-size-md)',
@ -1805,18 +1792,9 @@ export const THEME_BASE_RECIPE_TOKENS = {
// Arrow size — canonical density-aware tokens (NOT soma's hardcoded 10×5). // Arrow size — canonical density-aware tokens (NOT soma's hardcoded 10×5).
// width ≈ 2.5× height for a balanced triangle; both scale with density/scaling. // width ≈ 2.5× height for a balanced triangle; both scale with density/scaling.
'arrow-width': 'var(--space-5)', 'arrow-width': 'var(--space-5)',
'arrow-height': 'var(--space-2)', 'arrow-height': 'var(--space-2)'
'transition-duration': 'var(--duration-fast)', // `close-*` + `transition-*` tokens removed: Popover.Close composes the
'transition-ease': 'var(--ease-default)', // canonical <IconButton>/<Button>, so its chrome lives in button.css.
'close-size': 'var(--control-height-xs)',
'close-font-size': 'var(--font-size-sm)',
'close-radius': 'var(--radius-md)',
'close-border-width': 'var(--border-width)',
'close-bg': 'transparent',
'close-bg-hover': 'color-mix( in srgb, var(--color-neutral-track) 72%, var(--popover-bg) )',
'close-color': 'var(--color-content-secondary)',
'close-border': 'color-mix(in srgb, var(--color-border-default) 72%, transparent)',
'close-border-hover': 'var(--color-border-strong)'
}, },
progress: { progress: {
'height-xs': '2px', 'height-xs': '2px',

Loading…
Cancel
Save

Powered by TurnKey Linux.