diff --git a/src/uix/eidos/components/button/button-spinner.svelte b/src/uix/eidos/components/button/button-spinner.svelte index 5bda6dac6..19c1646eb 100644 --- a/src/uix/eidos/components/button/button-spinner.svelte +++ b/src/uix/eidos/components/button/button-spinner.svelte @@ -1,33 +1,22 @@ {#if children} {@render children?.()} {:else} - - + {/if} diff --git a/src/uix/eidos/components/button/button.css b/src/uix/eidos/components/button/button.css index e8558315f..76355e81e 100644 --- a/src/uix/eidos/components/button/button.css +++ b/src/uix/eidos/components/button/button.css @@ -52,6 +52,13 @@ align-items: center; justify-content: center; gap: var(--_button-gap); + /* Defend the intrinsic box: a button is content-sized and NEVER stretched by a + flex/grid parent. As a flex/grid item `inline-flex` blockifies to `flex`, and an + `auto` main/cross size would stretch to fill the cell (filling H+W) — `fit-content` + pins the button to its own size while the container's alignment still positions it. + `[data-block]` opts INTO full width; `[data-icon-only]` overrides to a square. */ + inline-size: fit-content; + block-size: fit-content; min-block-size: var(--_button-height); padding-inline: var(--_button-padding-inline); border: var(--button-border-width) solid var(--_button-border); @@ -81,12 +88,22 @@ } [data-button]:active:not([data-disabled]):not([data-loading]) { - transform: translateY(1px); + /* Canonical press — the shared scale-down "squeeze" (`--press-*`), not a + bespoke translateY. `transform: scale` composes with flex-centred content; + absolutely-centred consumers (e.g. carousel triggers) centre via the + `translate` property so the scale doesn't wipe their offset. */ + transform: scale(var(--press-scale)); + transition-duration: var(--press-duration); } [data-button]:focus-visible { - outline: var(--border-width-medium) solid var(--button-palette-border); - outline-offset: 2px; + /* Canonical focus ring — `outline` (survives forced-colors/HCM, unlike box-shadow) + with the shared `--focus-ring-*` tokens. A single primary-tinted accent ring, + not per-color: a consistent, high-contrast focus indicator (a11y), same as + badge / card / carousel. If `--focus-ring-color` ever becomes intent-driven at + the canon level, the Button inherits it for free. */ + outline: var(--focus-ring-width) solid var(--focus-ring-color); + outline-offset: var(--focus-ring-offset); } /* ── Size scale ────────────────────────────────────────────────────────── */ @@ -179,13 +196,14 @@ } [data-button][data-variant='plain'] { + /* Pure-emphasis variant (text button): transparent chrome + underline on hover, + but the SAME control box as every other variant — `variant` is the EMPHASIS + axis only, never layout. Inline link-like actions belong to , not here. */ --_button-bg: transparent; --_button-fg: var(--button-palette-text); --_button-border: transparent; --_button-hover-bg: transparent; --_button-hover-border: transparent; - min-block-size: auto; - padding-inline: 0; } [data-button][data-variant='plain']:hover:not([data-disabled]):not([data-loading]) { @@ -283,30 +301,16 @@ /* ── Spinner ───────────────────────────────────────────────────────────── */ [data-button-spinner] { + /* Presentational slot — the composed (variant ring, color=inherit) + sizes + animates itself. The slot just centres it in the icon flow. */ display: inline-flex; align-items: center; justify-content: center; - inline-size: var(--_button-icon-size, 1em); - block-size: var(--_button-icon-size, 1em); - color: currentColor; -} - -.eidos-button-spinner-ring { - inline-size: 100%; - block-size: 100%; - animation: button-spinner-spin var(--button-spinner-duration) linear infinite; -} - -@keyframes button-spinner-spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } } @media (prefers-reduced-motion: reduce) { - [data-button], - .eidos-button-spinner-ring { + [data-button] { transition: none; - animation: none; } } diff --git a/src/uix/eidos/components/button/button.svelte b/src/uix/eidos/components/button/button.svelte index f656f081c..367824f13 100644 --- a/src/uix/eidos/components/button/button.svelte +++ b/src/uix/eidos/components/button/button.svelte @@ -13,16 +13,19 @@ import { ActiveEidos } from '$uix/eidos'; import * as Button from '$soma/components/button'; import ButtonSpinner from './button-spinner.svelte'; + import { getButtonGroupEidosCtx } from '$uix/eidos/components/button-group/context'; import type { ButtonProps } from './types'; let { - variant = 'solid', - size = 'md', + variant, + size, rounded, shape, block = false, iconOnly = false, loading = false, + color, + disabled, icon, endIcon, spinner, @@ -33,16 +36,27 @@ }: ButtonProps = $props(); const eidos = ActiveEidos.require(); - const resolvedSize = $derived(eidos.resolve(size, 'md')); + + // Optional context supplies shared visual DEFAULTS. An + // explicit prop on this Button always wins; outside a group the getter is + // undefined and Button falls back to its own defaults (backward-compatible). + const group = getButtonGroupEidosCtx(); + + const resolvedVariant = $derived(variant ?? group?.variant ?? 'solid'); + const resolvedSize = $derived(eidos.resolve(size ?? group?.size, 'md')); const resolvedRounded = $derived(eidos.resolve(rounded)); + const resolvedColor = $derived(color ?? group?.color); + const resolvedDisabled = $derived(disabled ?? group?.disabled); {#if loading && spinnerPlacement === 'start'} - {#if spinner}{@render spinner()}{:else}{/if} + {#if spinner}{@render spinner()}{:else}{/if} {:else if icon}