feat(combobox): SelectedTags inline chips for multi mode

New parts `<Combobox.SelectedTags>`, `<Combobox.SelectedTag>` and
`<Combobox.SelectedTagRemove>` render the current selection as
removable chips inside `<Combobox.Control>` when `type='multiple'`
(Material UI Autocomplete pattern):

- Morfo: 3 new public parts with kebab names + texts entries.
- Langs: es/en entries `selected-tags` / `selected-tag-remove`.
- Soma: `ComboboxSelectedTagsProvider` exposes `items` derived from
  the provider's label registry; `ComboboxSelectedTagProvider`
  surfaces `data-value` / `data-label`; `ComboboxSelectedTagRemoveProvider`
  resolves its parent tag via context and calls `provider.removeValue`
  on click. Provider gains `getSelectedItems()` and `removeValue()`
  helpers; `selectedTagsEmpty` runtime prop drives `[data-empty]`.
- Soma item registry: stop unregistering labels on Item cleanup so
  chips keep proper labels after the listbox closes/lazy-unmounts.
- Eidos: default snippet loops items and renders a chip + X icon
  per entry; consumer can override the snippet for custom rendering.
- CSS: chip styling, wrap-friendly Control with breathing padding
  when chips are present, ClearButton pinned right via auto margin,
  input flex basis reduced to 4rem so it shares a row with chips.
- Demo: renders `<Combobox.SelectedTags />` inside Control when
  `mode === 'multiple'`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent 26daa9e22e
commit 30e9517a9a

@ -0,0 +1,29 @@
<script lang="ts">
/**
* Eidos `Combobox.SelectedTagRemove` — X button inside a chip. Inline SVG
* keeps the wrapper self-contained (mirrors `Combobox.ClearButton`).
*/
import * as Combobox from '$soma/components/combobox';
import type { ComboboxSelectedTagRemoveProps } from './types';
let { children, ...rest }: ComboboxSelectedTagRemoveProps = $props();
</script>
<Combobox.SelectedTagRemove {...rest}>
{#if children}
{@render children()}
{:else}
<svg
width="10"
height="10"
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>
{/if}
</Combobox.SelectedTagRemove>

@ -0,0 +1,14 @@
<script lang="ts">
/**
* Eidos `Combobox.SelectedTag` — visual chip wrapper. Pure pass-through;
* styling lives in `combobox.css` via `[data-combobox-selected-tag]`.
*/
import * as Combobox from '$soma/components/combobox';
import type { ComboboxSelectedTagProps } from './types';
let { children, ...rest }: ComboboxSelectedTagProps = $props();
</script>
<Combobox.SelectedTag {...rest}>
{@render children?.()}
</Combobox.SelectedTag>

@ -0,0 +1,30 @@
<script lang="ts">
/**
* Eidos `Combobox.SelectedTags` — renders inline chips for every selected
* value when `type='multiple'`. If the consumer doesn't supply a children
* snippet, a default chip + remove button is rendered per item (Material
* UI Autocomplete pattern). Consumers can override the rendering by
* passing their own snippet that receives `{ items }`.
*/
import * as Combobox from '$soma/components/combobox';
import SelectedTag from './combobox-selected-tag.svelte';
import SelectedTagRemove from './combobox-selected-tag-remove.svelte';
import type { ComboboxSelectedTagsProps } from './types';
let { children, ...rest }: ComboboxSelectedTagsProps = $props();
</script>
<Combobox.SelectedTags {...rest}>
{#snippet children(snippetProps)}
{#if children}
{@render children(snippetProps)}
{:else}
{#each snippetProps.items as item (item.value)}
<SelectedTag value={item.value} label={item.label}>
<span>{item.label}</span>
<SelectedTagRemove />
</SelectedTag>
{/each}
{/if}
{/snippet}
</Combobox.SelectedTags>

@ -88,6 +88,7 @@
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap;
gap: var(--_combobox-control-gap); gap: var(--_combobox-control-gap);
inline-size: 100%; inline-size: 100%;
min-inline-size: 0; min-inline-size: 0;
@ -164,9 +165,11 @@
[data-combobox-input] { [data-combobox-input] {
box-sizing: border-box; box-sizing: border-box;
flex: 1 1 auto; /* Basis `4rem` lets the input share a wrapped row with chips +
inline-size: 100%; * clear/trigger affordances while still growing to fill any remaining
min-inline-size: 0; * space. Pure `inline-size: 100%` forces a new line under flex-wrap. */
flex: 1 1 4rem;
min-inline-size: 4rem;
border: 0; border: 0;
background: transparent; background: transparent;
color: inherit; color: inherit;
@ -489,3 +492,85 @@
transition: none; transition: none;
} }
} }
/* SelectedTags — inline chip strip inside the Control for multi mode.
* Hidden when empty so the control collapses back to plain input chrome. */
[data-combobox-selected-tags] {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-1-5);
min-inline-size: 0;
}
[data-combobox-selected-tags][data-empty] {
display: none;
}
/* Make the Control breathe vertically when chips are present so the
* input + chips stack visually instead of clipping. */
[data-combobox-control]:has([data-combobox-selected-tags]:not([data-empty])) {
padding-block: var(--space-1);
}
/* When chips are present, pin the right-side affordances (ClearButton +
* Trigger) to the end of the row so they don't visually float in the
* middle when the input + chips wrap. */
[data-combobox-control]:has([data-combobox-selected-tags]:not([data-empty]))
[data-combobox-clear-button] {
margin-inline-start: auto;
}
[data-combobox-selected-tag] {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding-inline: var(--space-2) var(--space-0-5);
min-block-size: calc(var(--_combobox-control-height) - var(--space-2));
border: var(--border-width) solid var(--color-border-default);
border-radius: var(--radius-sm);
background: var(--color-surface-raised);
color: var(--color-content-primary);
font-size: calc(var(--_combobox-font-size) - 1px);
line-height: var(--leading-ui);
white-space: nowrap;
}
[data-combobox-selected-tag][data-disabled] {
opacity: 0.56;
}
[data-combobox-selected-tag-remove] {
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: var(--space-4);
block-size: var(--space-4);
margin-inline-start: var(--space-0-5);
border: 0;
border-radius: var(--radius-sm);
background: transparent;
color: inherit;
opacity: 0.7;
cursor: pointer;
transition:
background var(--duration-fast) var(--ease-default),
opacity var(--duration-fast) var(--ease-default);
}
[data-combobox-selected-tag-remove]:hover {
background: var(--color-surface-overlay);
opacity: 1;
}
[data-combobox-selected-tag-remove]:focus-visible {
outline: var(--focus-ring-width) solid var(--color-primary-border);
outline-offset: var(--focus-ring-offset);
opacity: 1;
}
[data-combobox-selected-tag-remove][data-disabled],
[data-combobox-selected-tag-remove]:disabled {
cursor: not-allowed;
opacity: 0.4;
}

@ -12,6 +12,9 @@ import GroupHeading from './combobox-group-heading.svelte'
import Separator from './combobox-separator.svelte' import Separator from './combobox-separator.svelte'
import Arrow from './combobox-arrow.svelte' import Arrow from './combobox-arrow.svelte'
import ClearButton from './combobox-clear-button.svelte' import ClearButton from './combobox-clear-button.svelte'
import SelectedTags from './combobox-selected-tags.svelte'
import SelectedTag from './combobox-selected-tag.svelte'
import SelectedTagRemove from './combobox-selected-tag-remove.svelte'
import { Portal } from '$soma/components/internal' import { Portal } from '$soma/components/internal'
type ComboboxNamespace = typeof ComboboxComponent & { type ComboboxNamespace = typeof ComboboxComponent & {
@ -29,6 +32,9 @@ type ComboboxNamespace = typeof ComboboxComponent & {
Separator: typeof Separator Separator: typeof Separator
Arrow: typeof Arrow Arrow: typeof Arrow
ClearButton: typeof ClearButton ClearButton: typeof ClearButton
SelectedTags: typeof SelectedTags
SelectedTag: typeof SelectedTag
SelectedTagRemove: typeof SelectedTagRemove
} }
const Combobox = ComboboxComponent as ComboboxNamespace const Combobox = ComboboxComponent as ComboboxNamespace
@ -46,6 +52,9 @@ Combobox.GroupHeading = GroupHeading
Combobox.Separator = Separator Combobox.Separator = Separator
Combobox.Arrow = Arrow Combobox.Arrow = Arrow
Combobox.ClearButton = ClearButton Combobox.ClearButton = ClearButton
Combobox.SelectedTags = SelectedTags
Combobox.SelectedTag = SelectedTag
Combobox.SelectedTagRemove = SelectedTagRemove
export { Combobox } export { Combobox }
@ -66,6 +75,9 @@ export type {
ComboboxSeparatorProps as SeparatorProps, ComboboxSeparatorProps as SeparatorProps,
ComboboxArrowProps as ArrowProps, ComboboxArrowProps as ArrowProps,
ComboboxClearButtonProps as ClearButtonProps, ComboboxClearButtonProps as ClearButtonProps,
ComboboxSelectedTagsProps as SelectedTagsProps,
ComboboxSelectedTagProps as SelectedTagProps,
ComboboxSelectedTagRemoveProps as SelectedTagRemoveProps,
ComboboxSize, ComboboxSize,
ComboboxVariant, ComboboxVariant,
ComboboxColor ComboboxColor

@ -7,6 +7,9 @@ import type {
InputProps, InputProps,
ItemProps, ItemProps,
ProviderProps, ProviderProps,
SelectedTagsProps,
SelectedTagProps,
SelectedTagRemoveProps,
SeparatorProps, SeparatorProps,
TriggerProps, TriggerProps,
ViewportProps ViewportProps
@ -70,3 +73,6 @@ export type ComboboxGroupHeadingProps = GroupHeadingProps
export type ComboboxSeparatorProps = SeparatorProps export type ComboboxSeparatorProps = SeparatorProps
export type ComboboxArrowProps = ArrowProps export type ComboboxArrowProps = ArrowProps
export type ComboboxClearButtonProps = ClearButtonProps export type ComboboxClearButtonProps = ClearButtonProps
export type ComboboxSelectedTagsProps = SelectedTagsProps
export type ComboboxSelectedTagProps = SelectedTagProps
export type ComboboxSelectedTagRemoveProps = SelectedTagRemoveProps

@ -19,5 +19,13 @@ export const comboboxLangs = {
clear: { clear: {
es: 'Borrar valor', es: 'Borrar valor',
en: 'Clear value' en: 'Clear value'
},
'selected-tags': {
es: 'Valores seleccionados',
en: 'Selected values'
},
'selected-tag-remove': {
es: 'Quitar',
en: 'Remove'
} }
} satisfies LangNode; } satisfies LangNode;

@ -9,7 +9,9 @@ export const comboboxMorfo = {
texts: { texts: {
label: '#?components.combobox.label|Combobox', label: '#?components.combobox.label|Combobox',
toggle: '#?components.combobox.toggle|Toggle', toggle: '#?components.combobox.toggle|Toggle',
clear: '#?components.combobox.clear|Clear value' clear: '#?components.combobox.clear|Clear value',
selectedTags: '#?components.combobox.selected-tags|Selected values',
selectedTagRemove: '#?components.combobox.selected-tag-remove|Remove'
}, },
events: [ events: [
{ {
@ -242,6 +244,57 @@ export const comboboxMorfo = {
optional: true, optional: true,
data: [], data: [],
aria: [] aria: []
},
{
name: 'SelectedTags',
kebab: 'selected-tags',
archetype: 'group',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: true,
data: [
{ attr: 'data-empty', value: v.propRef('selectedTagsEmpty'), severity: 'optional' }
],
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.combobox.selected-tags|Selected values'),
severity: 'recommended'
}
]
},
{
name: 'SelectedTag',
kebab: 'selected-tag',
archetype: 'item',
kind: 'public',
defaultElement: 'span',
optional: true,
data: [
{ attr: 'data-value' },
{ attr: 'data-label', severity: 'optional' },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
},
{
name: 'SelectedTagRemove',
kebab: 'selected-tag-remove',
archetype: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.combobox.selected-tag-remove|Remove'),
severity: 'recommended'
}
]
} }
] ]
} as const satisfies Morfo; } as const satisfies Morfo;

@ -112,7 +112,8 @@ export class ComboboxProvider {
type: () => this.opts.type.current, type: () => this.opts.type.current,
multiple: () => this.opts.type.current === 'multiple', multiple: () => this.opts.type.current === 'multiple',
empty: () => this.isEmpty, empty: () => this.isEmpty,
clearDisabled: () => this.opts.disabled.current || this.isEmpty clearDisabled: () => this.opts.disabled.current || this.isEmpty,
selectedTagsEmpty: () => this.opts.value.current.length === 0
}, },
parts: { parts: {
input: () => this.inputId.current || undefined, input: () => this.inputId.current || undefined,
@ -298,6 +299,30 @@ export class ComboboxProvider {
return this.opts.value.current.map((v) => this.labelRegistry.get(v) ?? v).join(', '); return this.opts.value.current.map((v) => this.labelRegistry.get(v) ?? v).join(', ');
} }
/**
* Snapshot of currently-selected `{ value, label }` pairs in selection
* order. Drives `<Combobox.SelectedTags>` chip rendering. Reads the
* label registry so chips reflect the same labels the menu shows.
*/
getSelectedItems(): Array<{ value: string; label: string }> {
return this.opts.value.current.map((v) => ({
value: v,
label: this.labelRegistry.get(v) ?? v
}));
}
/**
* Remove a single value from the selection. Used by
* `<Combobox.SelectedTagRemove>` chip X buttons. No-op if the combobox
* is disabled.
*/
removeValue(value: string) {
if (this.opts.disabled.current) return;
const current = this.opts.value.current;
if (!current.includes(value)) return;
this.opts.value.current = current.filter((v) => v !== value);
}
readonly props = $derived.by(() => this.runtimePart.assert({ ...this.runtimePart.props } as const)); readonly props = $derived.by(() => this.runtimePart.assert({ ...this.runtimePart.props } as const));
} }
@ -723,9 +748,14 @@ export class ComboboxItemProvider {
// Maintain value → label registry without a ref-reading mount effect. // Maintain value → label registry without a ref-reading mount effect.
// Item text fallback belongs to consumers; label is the stable headless // Item text fallback belongs to consumers; label is the stable headless
// source for display text when content unmounts. // source for display text when content unmounts.
//
// We register on mount but DO NOT unregister on cleanup — keeping the
// label around after the listbox closes/lazy-unmounts lets
// `<Combobox.SelectedTags>` render the proper label for previously-
// rendered values even when their `Item` is no longer in the DOM.
// Re-registration on remount is idempotent (SvelteMap.set).
const value = opts.value.current; const value = opts.value.current;
this.provider.registerLabel(value, opts.label.current || value); this.provider.registerLabel(value, opts.label.current || value);
$effect(() => () => this.provider.unregisterLabel(value));
} }
readonly isSelected = $derived.by(() => this.provider.isSelected(this.opts.value.current)); readonly isSelected = $derived.by(() => this.provider.isSelected(this.opts.value.current));
@ -893,6 +923,131 @@ export class ComboboxArrowProvider {
); );
} }
// ── SelectedTags ────────────────────────────────────────────────────────────
interface ComboboxSelectedTagsOpts extends WithRefOpts {}
export class ComboboxSelectedTagsProvider {
readonly opts: ComboboxSelectedTagsOpts;
readonly runtimePart: SomaRuntimePart;
static create(opts: ComboboxSelectedTagsOpts) {
return new ComboboxSelectedTagsProvider(opts);
}
readonly provider: ComboboxProvider;
private constructor(opts: ComboboxSelectedTagsOpts) {
this.opts = opts;
this.provider = ComboboxProvider.require();
this.runtimePart = this.provider.runtime.part('selected-tags', {
id: opts.id,
ref: opts.ref,
owner: this
});
}
/** Live snapshot of `{ value, label }` for every selected entry. Drives the chip loop. */
readonly items = $derived.by(() => this.provider.getSelectedItems());
readonly props = $derived.by(() => this.runtimePart.assert({ ...this.runtimePart.props } as const));
}
// ── SelectedTag ─────────────────────────────────────────────────────────────
interface ComboboxSelectedTagOpts
extends
WithRefOpts,
ActiveProps<{
value: string;
label: string;
disabled: boolean;
}> {}
export class ComboboxSelectedTagProvider {
readonly opts: ComboboxSelectedTagOpts;
readonly runtimePart: SomaRuntimePart;
static readonly ctx = context<ComboboxSelectedTagProvider>('ComboboxSelectedTag');
static get(): ComboboxSelectedTagProvider | undefined {
return this.ctx.getOr(undefined) as ComboboxSelectedTagProvider | undefined;
}
static require(): ComboboxSelectedTagProvider {
return this.ctx.get();
}
static create(opts: ComboboxSelectedTagOpts) {
return new ComboboxSelectedTagProvider(opts);
}
readonly provider: ComboboxProvider;
private constructor(opts: ComboboxSelectedTagOpts) {
this.opts = opts;
this.provider = ComboboxProvider.require();
this.runtimePart = this.provider.runtime.part('selected-tag', {
id: opts.id,
ref: opts.ref,
owner: this,
context: ComboboxSelectedTagProvider.ctx
});
}
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.props,
'data-value': this.opts.value.current,
'data-label': this.opts.label.current || undefined,
'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current)
} as const)
);
}
// ── SelectedTagRemove ───────────────────────────────────────────────────────
interface ComboboxSelectedTagRemoveOpts extends WithRefOpts {}
export class ComboboxSelectedTagRemoveProvider {
readonly opts: ComboboxSelectedTagRemoveOpts;
readonly runtimePart: SomaRuntimePart;
static create(opts: ComboboxSelectedTagRemoveOpts) {
return new ComboboxSelectedTagRemoveProvider(opts);
}
readonly provider: ComboboxProvider;
readonly tag: ComboboxSelectedTagProvider;
private constructor(opts: ComboboxSelectedTagRemoveOpts) {
this.opts = opts;
this.tag = ComboboxSelectedTagProvider.require();
this.provider = this.tag.provider;
this.runtimePart = this.provider.runtime.part('selected-tag-remove', {
id: opts.id,
ref: opts.ref,
owner: this
});
}
readonly disabled = $derived.by(
() => this.provider.opts.disabled.current || this.tag.opts.disabled.current
);
readonly onclick = (e: SomaMouseEvent<HTMLElement>) => {
if (this.disabled) return;
e.preventDefault();
e.stopPropagation();
this.provider.removeValue(this.tag.opts.value.current);
};
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.props,
type: 'button' as const,
'data-disabled': boolToEmptyStrOrUndef(this.disabled),
disabled: this.disabled || undefined,
onclick: this.onclick
} as const)
);
}
// ── ClearButton ───────────────────────────────────────────────────────────── // ── ClearButton ─────────────────────────────────────────────────────────────
interface ComboboxClearButtonOpts extends WithRefOpts {} interface ComboboxClearButtonOpts extends WithRefOpts {}

@ -0,0 +1,35 @@
<script lang="ts">
import { readableActive, writableActive } from '$libs/reactive';
import { mergeProps } from '../../../props';
import { createId } from '../../../id';
import { ComboboxSelectedTagRemoveProvider } from '../combobox-provider.svelte';
import type { ComboboxSelectedTagRemoveProps } from '../types';
const uid = $props.id();
let {
ref = $bindable(null),
id = createId(uid, 'combobox-selected-tag-remove'),
children,
child,
...restProps
}: ComboboxSelectedTagRemoveProps = $props();
const state = ComboboxSelectedTagRemoveProvider.create({
id: readableActive(() => id),
ref: writableActive(
() => ref,
(v) => (ref = v)
)
});
const mergedProps = $derived(mergeProps(restProps, state.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<button {...mergedProps}>
{@render children?.()}
</button>
{/if}

@ -0,0 +1,41 @@
<script lang="ts">
import { readableActive, writableActive } from '$libs/reactive';
import { mergeProps } from '../../../props';
import { createId } from '../../../id';
import { ComboboxSelectedTagProvider } from '../combobox-provider.svelte';
import type { ComboboxSelectedTagProps } from '../types';
const uid = $props.id();
let {
ref = $bindable(null),
id = createId(uid, 'combobox-selected-tag'),
value,
label = '',
disabled = false,
children,
child,
...restProps
}: ComboboxSelectedTagProps = $props();
const state = ComboboxSelectedTagProvider.create({
id: readableActive(() => id),
ref: writableActive(
() => ref,
(v) => (ref = v)
),
value: readableActive(() => value),
label: readableActive(() => label),
disabled: readableActive(() => disabled)
});
const mergedProps = $derived(mergeProps(restProps, state.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<span {...mergedProps}>
{@render children?.()}
</span>
{/if}

@ -0,0 +1,36 @@
<script lang="ts">
import { readableActive, writableActive } from '$libs/reactive';
import { mergeProps } from '../../../props';
import { createId } from '../../../id';
import { ComboboxSelectedTagsProvider } from '../combobox-provider.svelte';
import type { ComboboxSelectedTagsProps } from '../types';
const uid = $props.id();
let {
ref = $bindable(null),
id = createId(uid, 'combobox-selected-tags'),
children,
child,
...restProps
}: ComboboxSelectedTagsProps = $props();
const state = ComboboxSelectedTagsProvider.create({
id: readableActive(() => id),
ref: writableActive(
() => ref,
(v) => (ref = v)
)
});
const mergedProps = $derived(mergeProps(restProps, state.props));
const snippetProps = $derived({ items: state.items });
</script>
{#if child}
{@render child({ props: mergedProps, ...snippetProps })}
{:else}
<div {...mergedProps}>
{@render children?.(snippetProps)}
</div>
{/if}

@ -9,6 +9,9 @@ export { default as GroupHeading } from './components/combobox-group-heading.sve
export { default as Separator } from './components/combobox-separator.svelte'; export { default as Separator } from './components/combobox-separator.svelte';
export { default as Arrow } from './components/combobox-arrow.svelte'; export { default as Arrow } from './components/combobox-arrow.svelte';
export { default as ClearButton } from './components/combobox-clear-button.svelte'; export { default as ClearButton } from './components/combobox-clear-button.svelte';
export { default as SelectedTags } from './components/combobox-selected-tags.svelte';
export { default as SelectedTag } from './components/combobox-selected-tag.svelte';
export { default as SelectedTagRemove } from './components/combobox-selected-tag-remove.svelte';
export type { export type {
ComboboxType, ComboboxType,
@ -22,5 +25,8 @@ export type {
ComboboxGroupHeadingProps as GroupHeadingProps, ComboboxGroupHeadingProps as GroupHeadingProps,
ComboboxSeparatorProps as SeparatorProps, ComboboxSeparatorProps as SeparatorProps,
ComboboxArrowProps as ArrowProps, ComboboxArrowProps as ArrowProps,
ComboboxClearButtonProps as ClearButtonProps ComboboxClearButtonProps as ClearButtonProps,
ComboboxSelectedTagsProps as SelectedTagsProps,
ComboboxSelectedTagProps as SelectedTagProps,
ComboboxSelectedTagRemoveProps as SelectedTagRemoveProps
} from './types'; } from './types';

@ -166,3 +166,41 @@ export type ComboboxClearButtonProps = WithChild<{
id?: string; id?: string;
}> & }> &
Without<PrimitiveButtonAttributes, {}>; Without<PrimitiveButtonAttributes, {}>;
/**
* Container for the inline selected-value chips that render inside
* `<Combobox.Control>` when `type === 'multiple'`. The `children` snippet
* receives `{ items: Array<{ value, label }> }` — the consumer iterates
* and composes one `<Combobox.SelectedTag>` per entry.
*
* In multi mode, hides itself (via `[data-empty]`) when no values are
* selected so the control collapses back to plain input chrome.
*/
export type ComboboxSelectedTagsProps = WithChild<
{
/** Unique identifier. Auto-generated if omitted. */
id?: string;
},
{ items: Array<{ value: string; label: string }> }
> &
Without<PrimitiveDivAttributes, {}>;
/** A single chip rendered for one selected value. Hosts label + remove button. */
export type ComboboxSelectedTagProps = WithChild<{
/** Unique identifier. Auto-generated if omitted. */
id?: string;
/** Required. The value this chip represents. */
value: string;
/** Display label. Falls back to `value`. */
label?: string;
/** @default false */
disabled?: boolean;
}> &
Without<PrimitiveDivAttributes, {}>;
/** X button inside a `<Combobox.SelectedTag>` that removes its value from the selection. */
export type ComboboxSelectedTagRemoveProps = WithChild<{
/** Unique identifier. Auto-generated if omitted. */
id?: string;
}> &
Without<PrimitiveButtonAttributes, {}>;

@ -266,6 +266,9 @@
name="framework" name="framework"
> >
<Combobox.Control> <Combobox.Control>
{#if mode === 'multiple'}
<Combobox.SelectedTags />
{/if}
<Combobox.Input <Combobox.Input
placeholder="Search framework..." placeholder="Search framework..."
data-perm-step={!disabled ? '0' : undefined} data-perm-step={!disabled ? '0' : undefined}

Loading…
Cancel
Save

Powered by TurnKey Linux.