refactor(combobox + search-field): ClearButton/Clear render <Button>

Closes #120. The two icon-only clear affordances now render as eidos
`<Button>` via soma's `child` snippet, retiring the per-recipe chrome
(border, padding, focus ring, hover, dimensions) in favour of
button.css. The X glyph is provided through Button's `icon` snippet
slot; `iconOnly` collapses the body and keeps the label sr-only.

Both narrow `variant` to ControlVariant (`'surface' | 'outline' | 'ghost'`)
per DEMO_AUTHORING_GUIDE §12.7 — same 3-set as DropdownMenu.Trigger
and AlertDialog.Cancel. Default `variant='ghost'` (subtle X that reveals
chrome on hover) which is the canonical clear affordance.

Defaults:
- Combobox.ClearButton: ghost · xs · iconOnly · color=neutral
- SearchField.ClearTrigger: ghost · sm · iconOnly · color=neutral

Per-component CSS gutted to the empty-state visibility rule only —
`[data-…][data-empty] [data-…-clear-…] { opacity: 0; pointer-events: none }`
stays because hiding the X when there's nothing to clear is
component-specific behavior, not Button chrome.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent 32598d7cf3
commit 0b55be5c36

@ -1,18 +1,47 @@
<script lang="ts">
/**
* Eidos `Combobox.ClearButton` — auto-X icon when no children are provided.
* Inline SVG keeps the wrapper self-contained (mirrors `Drawer.Close`).
* Eidos `Combobox.ClearButton` — icon-only X that clears the input.
* Renders an eidos `<Button>` via soma's `child` snippet (chrome,
* focus ring, hover, size all come from button.css). Default
* `variant='ghost' size='xs' iconOnly` — the typical subtle clear
* affordance.
*
* Visibility (hidden when the combobox is empty) is still handled by
* `combobox.css` via the `[data-combobox][data-empty]` ancestor rule.
*
* Composition: callers can pass their own `children` snippet (custom
* glyph) or `child` snippet (full bypass of Button).
*/
import * as Combobox from '$soma/components/combobox';
import { Button } from '$uix/eidos/components/button';
import type { ComboboxClearButtonProps } from './types';
let { children, ...rest }: ComboboxClearButtonProps = $props();
let {
children,
child: outerChild,
variant = 'ghost',
size = 'xs',
rounded,
...rest
}: ComboboxClearButtonProps = $props();
</script>
<Combobox.ClearButton {...rest}>
{#if children}
{@render children()}
{#snippet child({ props })}
{#if outerChild}
{@render outerChild({ props })}
{:else}
<Button
{...props}
{variant}
{size}
{rounded}
intent="neutral"
color="neutral"
iconOnly
aria-label="Clear"
>
{#snippet icon()}
<svg
width="12"
height="12"
@ -25,5 +54,9 @@
>
<path d="M3 3l8 8M11 3l-8 8" />
</svg>
{/snippet}
{#if children}{@render children()}{/if}
</Button>
{/if}
{/snippet}
</Combobox.ClearButton>

@ -241,38 +241,10 @@
transition: transform var(--duration-fast) var(--ease-default);
}
[data-combobox-clear-button] {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
inline-size: var(--control-height-xs);
block-size: var(--control-height-xs);
padding: 0;
border: 0;
border-radius: var(--radius-full);
background: transparent;
color: var(--color-content-secondary);
font: inherit;
cursor: pointer;
outline: none;
transition:
background var(--duration-fast) var(--ease-default),
color var(--duration-fast) var(--ease-default),
opacity var(--duration-fast) var(--ease-default);
}
[data-combobox-clear-button]:hover:not(:disabled):not([data-disabled]) {
background: var(--color-surface-raised);
color: var(--color-content-primary);
}
[data-combobox-clear-button]:focus-visible {
box-shadow:
0 0 0 var(--focus-ring-offset) var(--color-surface-default),
0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
}
/* ClearButton chrome (border, focus ring, padding, hover) comes from
* `button.css` since the eidos wrapper renders an icon-only `<Button>`.
* Only the empty-state visibility rule stays here — it's combobox-
* specific behavior (hide the X when there's nothing to clear). */
[data-combobox-clear-button][data-disabled],
[data-combobox-clear-button]:disabled,
[data-combobox][data-empty] [data-combobox-clear-button] {

@ -1,3 +1,8 @@
import type {
ButtonVariant,
ButtonSize,
ButtonRounded
} from '$uix/eidos/components/button';
import type {
ArrowProps,
ClearButtonProps,
@ -72,7 +77,24 @@ export type ComboboxGroupProps = GroupProps
export type ComboboxGroupHeadingProps = GroupHeadingProps
export type ComboboxSeparatorProps = SeparatorProps
export type ComboboxArrowProps = ArrowProps
export type ComboboxClearButtonProps = ClearButtonProps
/**
* ClearButton renders an eidos `<Button>` internally (icon-only X by
* default). `variant` is narrowed to ControlVariant
* (`'surface' | 'outline' | 'ghost'`) per DEMO_AUTHORING_GUIDE §12.7 —
* same 3-set the rest of the form-control surface uses. Default
* `variant='ghost'` because the typical clear affordance is a subtle X
* that only reveals chrome on hover.
*/
export type ComboboxClearButtonVariant = Extract<ButtonVariant, ControlVariant>;
export type ComboboxClearButtonProps = ClearButtonProps & {
/** Button visual variant, narrowed to ControlVariant. @default 'ghost' */
variant?: ComboboxClearButtonVariant;
/** Button size scale. @default 'xs' */
size?: ResponsiveProp<ButtonSize>;
/** Button corner radius. */
rounded?: ResponsiveProp<ButtonRounded>;
};
export type ComboboxSelectedTagsProps = SelectedTagsProps
export type ComboboxSelectedTagProps = SelectedTagProps
export type ComboboxSelectedTagRemoveProps = SelectedTagRemoveProps

@ -1,10 +1,60 @@
<script lang="ts">
/**
* Eidos `SearchField.ClearTrigger` — icon-only X that clears the input.
* Renders an eidos `<Button>` via soma's `child` snippet. Default
* `variant='ghost' iconOnly` — subtle clear affordance.
*
* Visibility (hidden when the field is empty) is still handled by
* `search-field.css` via the `[data-search-field][data-empty]`
* ancestor rule.
*
* Composition: callers can pass their own `children` snippet (custom
* glyph) or `child` snippet (full bypass of Button).
*/
import * as SearchField from '$soma/components/search-field';
import { Button } from '$uix/eidos/components/button';
import type { SearchFieldClearTriggerProps } from './types';
let { children, ...rest }: SearchFieldClearTriggerProps = $props();
let {
children,
child: outerChild,
variant = 'ghost',
size,
rounded,
...rest
}: SearchFieldClearTriggerProps = $props();
</script>
<SearchField.ClearTrigger {...rest}>
{@render children?.()}
{#snippet child({ props })}
{#if outerChild}
{@render outerChild({ props })}
{:else}
<Button
{...props}
{variant}
size={size ?? 'sm'}
{rounded}
intent="neutral"
color="neutral"
iconOnly
>
{#snippet icon()}
<svg
width="12"
height="12"
viewBox="0 0 14 14"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
aria-hidden="true"
>
<path d="M3 3l8 8M11 3l-8 8" />
</svg>
{/snippet}
{#if children}{@render children()}{/if}
</Button>
{/if}
{/snippet}
</SearchField.ClearTrigger>

@ -163,36 +163,10 @@
cursor: not-allowed;
}
[data-search-field-clear-trigger] {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
inline-size: var(--search-field-clear-size);
block-size: var(--search-field-clear-size);
border: var(--search-field-clear-border-width) solid var(--search-field-clear-border);
border-radius: var(--search-field-clear-radius);
background: var(--search-field-clear-bg);
color: var(--search-field-clear-color);
font: inherit;
cursor: pointer;
outline: none;
transition:
background var(--search-field-transition-duration) var(--search-field-transition-ease),
border-color var(--search-field-transition-duration) var(--search-field-transition-ease),
color var(--search-field-transition-duration) var(--search-field-transition-ease);
}
[data-search-field-clear-trigger]:hover:not([data-disabled]) {
border-color: var(--search-field-clear-border-hover);
background: var(--search-field-clear-bg-hover);
color: var(--search-field-clear-color-hover);
}
[data-search-field-clear-trigger]:focus-visible {
box-shadow: var(--search-field-clear-focus-shadow);
}
/* ClearTrigger chrome (border, focus ring, padding, hover) comes from
* `button.css` since the eidos wrapper renders an icon-only `<Button>`.
* Only the empty-state visibility rule stays here — it's search-field-
* specific behavior (hide the X when there's nothing to clear). */
[data-search-field-clear-trigger][data-disabled],
[data-search-field][data-empty] [data-search-field-clear-trigger] {
cursor: default;

@ -6,6 +6,7 @@ import type {
ProviderProps,
ProviderSnippetProps as SearchFieldProviderSnippetProps
} from '$soma/components/search-field';
import type { ButtonVariant, ButtonRounded } from '$uix/eidos/components/button';
import type { ColorRole, ControlVariant, ResponsiveProp, Size } from '$uix/eidos/lib/types';
export type SearchFieldSize = Extract<Size, 'xs' | 'sm' | 'md' | 'lg' | 'xl'>;
@ -28,7 +29,24 @@ export type SearchFieldProps = ProviderProps & {
};
export type SearchFieldInputProps = InputProps;
export type SearchFieldClearTriggerProps = ClearTriggerProps;
/**
* ClearTrigger renders an eidos `<Button>` internally (icon-only X by
* default). `variant` is narrowed to ControlVariant
* (`'surface' | 'outline' | 'ghost'`) per DEMO_AUTHORING_GUIDE §12.7.
* Default `variant='ghost'` — subtle clear affordance that reveals
* chrome on hover. Inherits the field's `size` automatically.
*/
export type SearchFieldClearTriggerVariant = Extract<ButtonVariant, ControlVariant>;
export type SearchFieldClearTriggerProps = ClearTriggerProps & {
/** Button visual variant, narrowed to ControlVariant. @default 'ghost' */
variant?: SearchFieldClearTriggerVariant;
/** Button size scale. @default inherits from parent SearchField */
size?: ResponsiveProp<SearchFieldSize>;
/** Button corner radius. */
rounded?: ResponsiveProp<ButtonRounded>;
};
export type SearchFieldIconProps = IconProps;
export type SearchFieldLoadingIndicatorProps = LoadingIndicatorProps;
export type { SearchFieldProviderSnippetProps };

Loading…
Cancel
Save

Powered by TurnKey Linux.