You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
90 lines
2.9 KiB
90 lines
2.9 KiB
<script lang="ts">
|
|
import './button.css';
|
|
/**
|
|
* Eidos `<Button>` — visual wrapper over Soma's headless button.
|
|
*
|
|
* Soma owns the click flow, `data-state`, `aria-busy`, `aria-disabled`
|
|
* and the `contact-activate` perceptual event (book cap. 22 §10).
|
|
* Eidos adds the visual API on top — `variant`, `size`, `rounded`,
|
|
* `block`, `iconOnly`, the `icon` / `endIcon` snippet slots, and the
|
|
* loading Spinner — and wires those to the `data-*` selectors that
|
|
* `button.css` reacts to.
|
|
*/
|
|
import { ActiveEidos } from '$uix/eidos';
|
|
import * as Button from '$soma/components/button';
|
|
import ButtonSpinner from './button-spinner.svelte';
|
|
import type { ButtonProps } from './types';
|
|
|
|
let {
|
|
variant = 'solid',
|
|
size = 'md',
|
|
rounded,
|
|
shape,
|
|
block = false,
|
|
iconOnly = false,
|
|
loading = false,
|
|
icon,
|
|
endIcon,
|
|
spinner,
|
|
spinnerPlacement = 'start',
|
|
loadingText,
|
|
children: bodyContent,
|
|
...headlessProps
|
|
}: ButtonProps = $props();
|
|
|
|
const eidos = ActiveEidos.require();
|
|
const resolvedSize = $derived(eidos.resolve(size, 'md'));
|
|
const resolvedRounded = $derived(eidos.resolve(rounded));
|
|
</script>
|
|
|
|
<Button.Provider
|
|
{...headlessProps}
|
|
{loading}
|
|
{loadingText}
|
|
{spinnerPlacement}
|
|
data-variant={variant}
|
|
data-size={resolvedSize}
|
|
data-rounded={resolvedRounded ?? undefined}
|
|
data-shape={shape}
|
|
data-block={block ? '' : undefined}
|
|
data-icon-only={iconOnly ? '' : undefined}
|
|
data-has-icon={icon || (loading && spinnerPlacement === 'start') ? '' : undefined}
|
|
data-spinner-placement={spinnerPlacement}
|
|
>
|
|
{#snippet children(snippetProps)}
|
|
<!-- Start slot: spinner (when loading) wins over icon at start placement. -->
|
|
{#if loading && spinnerPlacement === 'start'}
|
|
{#if spinner}<ButtonSpinner>{@render spinner()}</ButtonSpinner>{:else}<ButtonSpinner />{/if}
|
|
{:else if icon}
|
|
<span class="eidos-button-icon" data-button-icon="start" aria-hidden="true">
|
|
{@render icon(snippetProps)}
|
|
</span>
|
|
{/if}
|
|
|
|
<!--
|
|
* Body wrapper holds the text label. When `iconOnly` is on, the
|
|
* recipe makes it visually-hidden but keeps it in the accessibility
|
|
* tree so screen readers still announce the label. When loading
|
|
* with `loadingText`, the original children remain in DOM (sr-only)
|
|
* and the loadingText takes the visual slot.
|
|
-->
|
|
<span class="eidos-button-body">
|
|
{#if loading && loadingText}
|
|
<span class="eidos-button-loading-text">{loadingText}</span>
|
|
<span class="eidos-button-sr-only">{@render bodyContent?.(snippetProps)}</span>
|
|
{:else}
|
|
{@render bodyContent?.(snippetProps)}
|
|
{/if}
|
|
</span>
|
|
|
|
<!-- End slot: spinner (when loading) wins over endIcon at end placement. -->
|
|
{#if loading && spinnerPlacement === 'end'}
|
|
{#if spinner}<ButtonSpinner>{@render spinner()}</ButtonSpinner>{:else}<ButtonSpinner />{/if}
|
|
{:else if endIcon}
|
|
<span class="eidos-button-icon" data-button-icon="end" aria-hidden="true">
|
|
{@render endIcon(snippetProps)}
|
|
</span>
|
|
{/if}
|
|
{/snippet}
|
|
</Button.Provider>
|