@ -112,7 +112,8 @@ export class ComboboxProvider {
type : ( ) = > this . opts . type . current ,
multiple : ( ) = > this . opts . type . current === 'multiple' ,
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 : {
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 ( ', ' ) ;
}
/ * *
* 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 ) ) ;
}
@ -723,9 +748,14 @@ export class ComboboxItemProvider {
// Maintain value → label registry without a ref-reading mount effect.
// Item text fallback belongs to consumers; label is the stable headless
// 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 ;
this . provider . registerLabel ( value , opts . label . current || value ) ;
$effect ( ( ) = > ( ) = > this . provider . unregisterLabel ( value ) ) ;
}
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 ─────────────────────────────────────────────────────────────
interface ComboboxClearButtonOpts extends WithRefOpts { }