sheer-ui 0.3.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/lib/components/accordion/components/accordion-item.svelte +3 -3
- package/src/lib/components/accordion/components/accordion.svelte +29 -13
- package/src/lib/components/accordion/types.ts +3 -3
- package/src/lib/components/checkbox/components/checkbox-group.svelte +7 -5
- package/src/lib/components/checkbox/components/checkbox.svelte +12 -8
- package/src/lib/components/combobox/components/combobox.svelte +2 -2
- package/src/lib/components/combobox/select/components/select.svelte +2 -2
- package/src/lib/components/context-menu/components/context-menu-trigger.svelte +1 -1
- package/src/lib/components/data-table/table-faceted-filter.svelte +2 -2
- package/src/lib/components/data-table/table-toolbar.svelte +5 -5
- package/src/lib/components/date-field/date-field.svelte.ts +2 -3
- package/src/lib/components/menu/components/menu-checkbox-group.svelte +6 -4
- package/src/lib/components/menu/components/menu-checkbox-item.svelte +11 -7
- package/src/lib/components/menu/menu.svelte.ts +9 -18
- package/src/lib/components/menubar/menubar.svelte.ts +7 -6
- package/src/lib/components/navigation-menu/components/navigation-menu-content.svelte +1 -1
- package/src/lib/components/navigation-menu/components/navigation-menu-indicator.svelte +2 -2
- package/src/lib/components/navigation-menu/components/navigation-menu-trigger.svelte +1 -1
- package/src/lib/components/navigation-menu/navigation-menu.svelte.ts +57 -60
- package/src/lib/components/pagination/components/pagination-next-button.svelte +12 -7
- package/src/lib/components/pagination/components/pagination-page.svelte +15 -8
- package/src/lib/components/pagination/components/pagination-prev-button.svelte +12 -7
- package/src/lib/components/pagination/components/pagination.svelte +31 -18
- package/src/lib/components/pagination/pagination.svelte.ts +48 -64
- package/src/lib/components/pin-input/pin-input.svelte.ts +7 -8
- package/src/lib/components/pin-input/usePasswordManager.svelte.ts +2 -2
- package/src/lib/components/scroll-area/scroll-area.svelte.ts +9 -10
- package/src/lib/components/slider/components/slider.svelte +2 -1
- package/src/lib/components/tabs/components/tabs-content.svelte +12 -7
- package/src/lib/components/tabs/components/tabs-list.svelte +9 -6
- package/src/lib/components/tabs/components/tabs-trigger.svelte +15 -8
- package/src/lib/components/tabs/components/tabs.svelte +28 -17
- package/src/lib/components/tabs/tabs.svelte.ts +51 -67
- package/src/lib/components/time-field/time-field.svelte.ts +2 -2
- package/src/lib/components/toggle-group/components/toggle-group-item.svelte +15 -8
- package/src/lib/components/toggle-group/components/toggle-group.svelte +28 -16
- package/src/lib/components/toggle-group/toggle-group.svelte.ts +33 -35
- package/src/lib/components/toolbar/components/toolbar-group-item.svelte +15 -8
- package/src/lib/components/toolbar/components/toolbar-group.svelte +19 -14
- package/src/lib/components/toolbar/toolbar.svelte.ts +28 -30
- package/src/lib/components/tooltip/tooltip.svelte.ts +6 -27
- package/src/lib/internal/dismissible-layer/use-dismissable-layer.svelte.ts +3 -4
- package/src/lib/internal/floating-layer/use-floating-layer.svelte.ts +26 -30
- package/src/lib/internal/{roving-focus-group.ts → roving-focus-group.svelte.ts} +17 -19
- package/src/lib/internal/roving-focus-item.svelte.ts +12 -17
- package/src/lib/internal/selection.svelte.ts +55 -41
- package/src/lib/internal/{state-machine.ts → state-machine.svelte.ts} +6 -8
- package/src/lib/internal/table/table.svelte.ts +23 -43
- package/src/lib/internal/timeout-fn.svelte.ts +6 -4
- package/src/lib/internal/tools/box.svelte.ts +5 -114
- package/src/lib/internal/tools/index.ts +1 -11
- package/src/lib/internal/tools/types.ts +0 -3
- package/src/lib/internal/typeahead.svelte.ts +9 -13
- package/src/lib/internal/types.ts +6 -0
- package/src/lib/internal/vaul/use-drawer-content.svelte.ts +2 -2
- package/src/lib/internal/vaul/use-drawer-root.svelte.ts +12 -7
- package/src/lib/components/accordion/accordion.svelte.ts +0 -43
|
@@ -1,83 +1,95 @@
|
|
|
1
|
-
import type { ReadableBox, ReadableBoxedValues, WritableBox } from './tools/index.js';
|
|
2
1
|
import { boolToStr, boolToEmptyStrOrUndef, boolToTrueOrUndef, getAriaChecked } from './attrs.js';
|
|
3
2
|
import { kbd } from './kbd.js';
|
|
4
3
|
import type { Orientation } from './index.js';
|
|
5
|
-
import type { BitsKeyboardEvent, BitsMouseEvent
|
|
6
|
-
import type { RovingFocusGroup } from './roving-focus-group.js';
|
|
4
|
+
import type { BitsKeyboardEvent, BitsMouseEvent } from './types.js';
|
|
5
|
+
import type { RovingFocusGroup } from './roving-focus-group.svelte.js';
|
|
7
6
|
import { RovingFocusItem } from './roving-focus-item.svelte.js';
|
|
8
7
|
|
|
9
8
|
export type SelectionType = 'single' | 'multiple';
|
|
10
9
|
|
|
11
|
-
export
|
|
10
|
+
export type Selection = string | string[];
|
|
11
|
+
|
|
12
|
+
export const emptySelection = (type: SelectionType): Selection => (type === 'single' ? '' : []);
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* Membership over a bindable that is a string in single mode and a string array
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Membership over a consumer's bindable that is a string in single mode and a string array
|
|
16
|
+
* in multiple mode. `value` reads and writes the root's prop through the closures it was
|
|
17
|
+
* built with; the mode is fixed at construction, so `with` always keeps the declared shape.
|
|
17
18
|
*/
|
|
18
19
|
export class SelectionValue {
|
|
19
20
|
readonly isMulti: boolean;
|
|
20
|
-
readonly #
|
|
21
|
+
readonly #read: () => Selection;
|
|
22
|
+
readonly #write: (value: Selection) => void;
|
|
21
23
|
|
|
22
|
-
constructor(type: SelectionType, value:
|
|
24
|
+
constructor(type: SelectionType, read: () => Selection, write: (value: Selection) => void) {
|
|
23
25
|
this.isMulti = type === 'multiple';
|
|
24
|
-
this.#
|
|
26
|
+
this.#read = read;
|
|
27
|
+
this.#write = write;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get value(): Selection {
|
|
31
|
+
return this.#read();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
set value(next: Selection) {
|
|
35
|
+
this.#write(next);
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
includes(item: string): boolean {
|
|
28
|
-
const value = this.#
|
|
39
|
+
const value = this.#read();
|
|
29
40
|
return this.isMulti ? (value as string[]).includes(item) : value === item;
|
|
30
41
|
}
|
|
31
42
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
this.#value.current = selected ? item : '';
|
|
40
|
-
}
|
|
41
|
-
return selected;
|
|
43
|
+
/** The value with `item` present or absent; the same reference when it already is. */
|
|
44
|
+
with(item: string, selected: boolean): Selection {
|
|
45
|
+
const value = this.#read();
|
|
46
|
+
const present = this.isMulti ? (value as string[]).includes(item) : value === item;
|
|
47
|
+
if (present === selected) return value;
|
|
48
|
+
if (!this.isMulti) return selected ? item : '';
|
|
49
|
+
return selected ? [...(value as string[]), item] : (value as string[]).filter((v) => v !== item);
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* What a selection item needs from the group that owns it. Reactive members are accessors on the
|
|
55
|
+
* implementing class, read at use, never copied out.
|
|
56
|
+
*/
|
|
46
57
|
export interface SelectionGroup {
|
|
47
58
|
readonly selection: SelectionValue;
|
|
48
|
-
readonly disabled:
|
|
49
|
-
readonly orientation:
|
|
59
|
+
readonly disabled: boolean;
|
|
60
|
+
readonly orientation: Orientation;
|
|
50
61
|
readonly rovingFocusGroup: RovingFocusGroup;
|
|
51
62
|
/** Whether arrow keys move between the items; absent means always. */
|
|
52
|
-
readonly rovingFocus?:
|
|
63
|
+
readonly rovingFocus?: boolean;
|
|
53
64
|
/** A selection takes the roving tab stop in a toggle group; a toolbar's stays where it is. */
|
|
54
65
|
readonly selectionTakesTabStop: boolean;
|
|
55
66
|
/** Beyond the roving group's candidate attribute, which every item carries. */
|
|
56
67
|
readonly extraItemAttrs?: Record<string, ''>;
|
|
57
68
|
}
|
|
58
69
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
/** The item component's props as accessors over its `$props()`; `ref` writes back to its bindable. */
|
|
71
|
+
export interface SelectionItemOpts {
|
|
72
|
+
readonly id: string;
|
|
73
|
+
readonly value: string;
|
|
74
|
+
readonly disabled: boolean;
|
|
75
|
+
ref: HTMLElement | null;
|
|
76
|
+
}
|
|
66
77
|
|
|
67
78
|
export class SelectionItemState {
|
|
68
79
|
readonly opts: SelectionItemOpts;
|
|
69
80
|
readonly group: SelectionGroup;
|
|
70
81
|
readonly rovingItem: RovingFocusItem;
|
|
71
|
-
readonly #isDisabled = $derived.by(() => this.opts.disabled
|
|
72
|
-
readonly isPressed = $derived.by(() => this.group.selection.includes(this.opts.value
|
|
82
|
+
readonly #isDisabled = $derived.by(() => this.opts.disabled || this.group.disabled);
|
|
83
|
+
readonly isPressed = $derived.by(() => this.group.selection.includes(this.opts.value));
|
|
73
84
|
|
|
74
85
|
constructor(opts: SelectionItemOpts, group: SelectionGroup) {
|
|
75
86
|
this.opts = opts;
|
|
76
87
|
this.group = group;
|
|
77
88
|
this.rovingItem = new RovingFocusItem({
|
|
78
89
|
group: group.rovingFocusGroup,
|
|
79
|
-
ref: opts.ref,
|
|
80
|
-
|
|
90
|
+
ref: () => opts.ref,
|
|
91
|
+
setRef: (v) => (opts.ref = v),
|
|
92
|
+
enabled: () => group.rovingFocus ?? true,
|
|
81
93
|
});
|
|
82
94
|
|
|
83
95
|
this.onclick = this.onclick.bind(this);
|
|
@@ -86,8 +98,10 @@ export class SelectionItemState {
|
|
|
86
98
|
|
|
87
99
|
#activate() {
|
|
88
100
|
if (this.#isDisabled) return;
|
|
89
|
-
const
|
|
90
|
-
|
|
101
|
+
const item = this.opts.value;
|
|
102
|
+
const selected = !this.group.selection.includes(item);
|
|
103
|
+
this.group.selection.value = this.group.selection.with(item, selected);
|
|
104
|
+
if (selected && this.group.selectionTakesTabStop) this.group.rovingFocusGroup.setCurrentTabStopId(this.opts.id);
|
|
91
105
|
}
|
|
92
106
|
|
|
93
107
|
onclick(_: BitsMouseEvent) {
|
|
@@ -109,12 +123,12 @@ export class SelectionItemState {
|
|
|
109
123
|
readonly props = $derived.by(
|
|
110
124
|
() =>
|
|
111
125
|
({
|
|
112
|
-
id: this.opts.id
|
|
126
|
+
id: this.opts.id,
|
|
113
127
|
role: this.group.selection.isMulti ? undefined : 'radio',
|
|
114
|
-
'data-orientation': this.group.orientation
|
|
128
|
+
'data-orientation': this.group.orientation,
|
|
115
129
|
'data-disabled': boolToEmptyStrOrUndef(this.#isDisabled),
|
|
116
130
|
'data-state': this.isPressed ? 'on' : 'off',
|
|
117
|
-
'data-value': this.opts.value
|
|
131
|
+
'data-value': this.opts.value,
|
|
118
132
|
'aria-pressed': this.group.selection.isMulti ? boolToStr(this.isPressed) : undefined,
|
|
119
133
|
'aria-checked': this.group.selection.isMulti ? undefined : getAriaChecked(this.isPressed, false),
|
|
120
134
|
disabled: boolToTrueOrUndef(this.#isDisabled),
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { simpleBox, type WritableBox } from './tools/index.js';
|
|
2
|
-
|
|
3
1
|
interface Machine<S> {
|
|
4
2
|
[k: string]: { [k: string]: S };
|
|
5
3
|
}
|
|
@@ -11,22 +9,22 @@ type MachineEvent<T> = keyof UnionToIntersection<T[keyof T]>;
|
|
|
11
9
|
type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (x: infer R) => any ? R : never;
|
|
12
10
|
|
|
13
11
|
export class StateMachine<M> {
|
|
14
|
-
|
|
12
|
+
state: MachineState<M>;
|
|
15
13
|
readonly #machine: M & Machine<MachineState<M>>;
|
|
16
14
|
|
|
17
15
|
constructor(initialState: MachineState<M>, machine: M & Machine<MachineState<M>>) {
|
|
18
|
-
this.state =
|
|
16
|
+
this.state = $state.raw(initialState);
|
|
19
17
|
this.#machine = machine;
|
|
20
18
|
this.dispatch = this.dispatch.bind(this);
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
#reducer(event: MachineEvent<M>) {
|
|
24
|
-
// @ts-expect-error state
|
|
25
|
-
const nextState = this.#machine[this.state
|
|
26
|
-
return nextState ?? this.state
|
|
22
|
+
// @ts-expect-error state is keyof M
|
|
23
|
+
const nextState = this.#machine[this.state][event];
|
|
24
|
+
return nextState ?? this.state;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
dispatch(event: MachineEvent<M>) {
|
|
30
|
-
this.state
|
|
28
|
+
this.state = this.#reducer(event);
|
|
31
29
|
}
|
|
32
30
|
}
|
|
@@ -20,13 +20,16 @@ export class DataTable<TData> {
|
|
|
20
20
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
21
|
readonly #defs: () => ColumnDef<TData, any>[];
|
|
22
22
|
readonly #paginated: boolean;
|
|
23
|
-
readonly
|
|
23
|
+
readonly selectedRowIds = new SvelteSet<string>();
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// Raw so a change is always a reassignment: the page record below keys on their identity.
|
|
26
|
+
sorting = $state.raw<ColumnSort[]>([]);
|
|
27
|
+
columnFilters = $state.raw<ColumnFilter[]>([]);
|
|
27
28
|
columnVisibility = $state<Record<string, boolean>>({});
|
|
28
|
-
#pageIndex = $state(0);
|
|
29
29
|
#pageSize = $state(10);
|
|
30
|
+
// A page index holds for the sorting and filters it was set under: a sort or filter change reads
|
|
31
|
+
// as page 0 without a write, while a data refetch underneath keeps the page.
|
|
32
|
+
#page = $state.raw({ index: 0, sorting: this.sorting, columnFilters: this.columnFilters });
|
|
30
33
|
|
|
31
34
|
constructor(options: DataTableOptions<TData>) {
|
|
32
35
|
this.#data = options.data;
|
|
@@ -40,7 +43,7 @@ export class DataTable<TData> {
|
|
|
40
43
|
readonly #columns: Column<TData, any>[] = $derived.by(() => this.#defs().map((def) => new Column(this, def)));
|
|
41
44
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
45
|
readonly #columnsById = $derived(new Map<string, Column<TData, any>>(this.#columns.map((column) => [column.id, column])));
|
|
43
|
-
readonly #coreRows: readonly Row<TData>[] = $derived.by(() => this.#data().map((original, index) => new Row(this, this
|
|
46
|
+
readonly #coreRows: readonly Row<TData>[] = $derived.by(() => this.#data().map((original, index) => new Row(this, this.selectedRowIds, original, index)));
|
|
44
47
|
|
|
45
48
|
readonly #activeFilters = $derived(
|
|
46
49
|
this.columnFilters.flatMap((filter) => {
|
|
@@ -65,7 +68,9 @@ export class DataTable<TData> {
|
|
|
65
68
|
|
|
66
69
|
readonly pageCount = $derived.by(() => (this.#paginated ? Math.max(1, Math.ceil(this.#sorted.length / this.#pageSize)) : 1));
|
|
67
70
|
// Clamped on read, never written back: data shrinking under a stale index lands on the last page.
|
|
68
|
-
readonly #safeIndex = $derived(
|
|
71
|
+
readonly #safeIndex = $derived(
|
|
72
|
+
this.#page.sorting === this.sorting && this.#page.columnFilters === this.columnFilters ? Math.min(this.#page.index, this.pageCount - 1) : 0,
|
|
73
|
+
);
|
|
69
74
|
|
|
70
75
|
readonly rows: readonly Row<TData>[] = $derived.by(() =>
|
|
71
76
|
this.#paginated ? this.#sorted.slice(this.#safeIndex * this.#pageSize, (this.#safeIndex + 1) * this.#pageSize) : this.#sorted,
|
|
@@ -79,7 +84,7 @@ export class DataTable<TData> {
|
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
setPageIndex(index: number) {
|
|
82
|
-
this.#
|
|
87
|
+
this.#page = { index: Math.min(Math.max(index, 0), this.pageCount - 1), sorting: this.sorting, columnFilters: this.columnFilters };
|
|
83
88
|
}
|
|
84
89
|
nextPage() {
|
|
85
90
|
this.setPageIndex(this.#safeIndex + 1);
|
|
@@ -90,23 +95,10 @@ export class DataTable<TData> {
|
|
|
90
95
|
setPageSize(size: number) {
|
|
91
96
|
const next = Math.max(1, size);
|
|
92
97
|
// Keep the current top row visible, matching upstream's paging math.
|
|
93
|
-
this.#
|
|
98
|
+
this.#page = { index: Math.floor((this.#safeIndex * this.#pageSize) / next), sorting: this.sorting, columnFilters: this.columnFilters };
|
|
94
99
|
this.#pageSize = next;
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
// User-initiated sort and filter changes restart paging; a data refetch underneath does not.
|
|
98
|
-
setSorting(sorting: ColumnSort[]) {
|
|
99
|
-
this.sorting = sorting;
|
|
100
|
-
this.#pageIndex = 0;
|
|
101
|
-
}
|
|
102
|
-
setColumnFilters(filters: ColumnFilter[]) {
|
|
103
|
-
this.columnFilters = filters;
|
|
104
|
-
this.#pageIndex = 0;
|
|
105
|
-
}
|
|
106
|
-
resetColumnFilters() {
|
|
107
|
-
this.setColumnFilters([]);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
102
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
111
103
|
get allColumns(): readonly Column<TData, any>[] {
|
|
112
104
|
return this.#columns;
|
|
@@ -122,15 +114,6 @@ export class DataTable<TData> {
|
|
|
122
114
|
{ id: '0', headers: this.visibleColumns.map((column) => new Header(this, column)) },
|
|
123
115
|
]);
|
|
124
116
|
|
|
125
|
-
get selectedRowIds(): ReadonlySet<string> {
|
|
126
|
-
return this.#selection;
|
|
127
|
-
}
|
|
128
|
-
setSelectedRowIds(ids: Iterable<string>) {
|
|
129
|
-
const next = new Set(ids);
|
|
130
|
-
for (const id of this.#selection) if (!next.has(id)) this.#selection.delete(id);
|
|
131
|
-
for (const id of next) this.#selection.add(id);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
117
|
readonly selectedRows: readonly Row<TData>[] = $derived(this.#coreRows.filter((row) => row.isSelected));
|
|
135
118
|
readonly filteredSelectedRows: readonly Row<TData>[] = $derived(this.filteredRows.filter((row) => row.isSelected));
|
|
136
119
|
|
|
@@ -194,26 +177,23 @@ export class Column<TData, TValue = unknown> {
|
|
|
194
177
|
const existingIndex = old.findIndex((sort) => sort.id === this.id);
|
|
195
178
|
|
|
196
179
|
if (old.length > 0 && this.#canMultiSort && multi) {
|
|
197
|
-
this.#table.
|
|
198
|
-
|
|
199
|
-
?
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
: [...old, { id: this.id, desc: nextDesc }],
|
|
203
|
-
);
|
|
180
|
+
this.#table.sorting = existing
|
|
181
|
+
? !hasManualValue && nextOrder === false
|
|
182
|
+
? old.filter((sort) => sort.id !== this.id)
|
|
183
|
+
: old.map((sort) => (sort.id === this.id ? { ...sort, desc: nextDesc } : sort))
|
|
184
|
+
: [...old, { id: this.id, desc: nextDesc }];
|
|
204
185
|
return;
|
|
205
186
|
}
|
|
206
187
|
|
|
207
188
|
// Single mode: only the most recently applied sort toggles through the cycle; anything else replaces.
|
|
208
189
|
if (existing !== undefined && existingIndex === old.length - 1) {
|
|
209
|
-
this.#table.
|
|
190
|
+
this.#table.sorting =
|
|
210
191
|
!hasManualValue && nextOrder === false
|
|
211
192
|
? old.filter((sort) => sort.id !== this.id)
|
|
212
|
-
: old.map((sort) => (sort.id === this.id ? { ...sort, desc: nextDesc } : sort))
|
|
213
|
-
);
|
|
193
|
+
: old.map((sort) => (sort.id === this.id ? { ...sort, desc: nextDesc } : sort));
|
|
214
194
|
return;
|
|
215
195
|
}
|
|
216
|
-
this.#table.
|
|
196
|
+
this.#table.sorting = [{ id: this.id, desc: nextDesc }];
|
|
217
197
|
}
|
|
218
198
|
|
|
219
199
|
get #canMultiSort(): boolean {
|
|
@@ -239,9 +219,9 @@ export class Column<TData, TValue = unknown> {
|
|
|
239
219
|
get filterValue(): unknown {
|
|
240
220
|
return this.#table.columnFilters.find((filter) => filter.id === this.id)?.value;
|
|
241
221
|
}
|
|
242
|
-
|
|
222
|
+
set filterValue(value: unknown) {
|
|
243
223
|
const others = this.#table.columnFilters.filter((filter) => filter.id !== this.id);
|
|
244
|
-
this.#table.
|
|
224
|
+
this.#table.columnFilters = shouldAutoRemoveFilter(value) ? others : [...others, { id: this.id, value }];
|
|
245
225
|
}
|
|
246
226
|
|
|
247
227
|
get facetedUniqueValues(): ReadonlyMap<unknown, number> {
|
|
@@ -4,20 +4,22 @@ import type { AnyFn } from './types.js';
|
|
|
4
4
|
* A self-cancelling timeout. `start()` (re)schedules `cb` after `getInterval()` ms,
|
|
5
5
|
* `stop()` clears it; auto-stops when the owning component/effect is destroyed.
|
|
6
6
|
*/
|
|
7
|
-
export function createEffectTimeout<T extends AnyFn>(cb: T, getDelay: () => number) {
|
|
7
|
+
export function createEffectTimeout<T extends AnyFn>(cb: T, getDelay: () => number, getWindow: () => typeof globalThis = () => globalThis) {
|
|
8
8
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
9
|
+
let timerWindow: typeof globalThis;
|
|
9
10
|
|
|
10
11
|
const stop = () => {
|
|
11
12
|
if (timer === undefined) return;
|
|
12
13
|
|
|
13
|
-
clearTimeout(timer);
|
|
14
|
+
timerWindow.clearTimeout(timer);
|
|
14
15
|
timer = undefined;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
const start = (...args: Parameters<T>) => {
|
|
18
19
|
stop();
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
timerWindow = getWindow();
|
|
22
|
+
timer = timerWindow.setTimeout(() => {
|
|
21
23
|
timer = undefined;
|
|
22
24
|
cb(...args);
|
|
23
25
|
}, getDelay());
|
|
@@ -33,4 +35,4 @@ export function createEffectTimeout<T extends AnyFn>(cb: T, getDelay: () => numb
|
|
|
33
35
|
return timer !== undefined;
|
|
34
36
|
},
|
|
35
37
|
};
|
|
36
|
-
}
|
|
38
|
+
}
|
|
@@ -1,57 +1,19 @@
|
|
|
1
1
|
import { untrack } from 'svelte';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
const isWritableSymbol = Symbol('is-writable');
|
|
5
|
-
|
|
3
|
+
/** What a vendored engine reads a reactive option through. Any object with a `current` qualifies. */
|
|
6
4
|
export type ReadableBox<T> = {
|
|
7
|
-
readonly [BoxSymbol]: true;
|
|
8
5
|
readonly current: T;
|
|
9
6
|
};
|
|
10
7
|
|
|
11
|
-
export type WritableBox<T> =
|
|
12
|
-
readonly [isWritableSymbol]: true;
|
|
8
|
+
export type WritableBox<T> = {
|
|
13
9
|
current: T;
|
|
14
10
|
};
|
|
15
11
|
|
|
16
|
-
function
|
|
17
|
-
function
|
|
18
|
-
function
|
|
19
|
-
let current = $state(initialValue);
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
[BoxSymbol]: true,
|
|
23
|
-
[isWritableSymbol]: true,
|
|
24
|
-
get current() {
|
|
25
|
-
return current as unknown;
|
|
26
|
-
},
|
|
27
|
-
set current(v: unknown) {
|
|
28
|
-
current = v;
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Creates a readonly box.
|
|
35
|
-
*
|
|
36
|
-
* @param getter Function to get the value of the box.
|
|
37
|
-
* @returns A box with a `current` property whose value is the result of the getter.
|
|
38
|
-
*/
|
|
39
|
-
function boxWith<T>(getter: () => T): ReadableBox<T>;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Creates a writable box.
|
|
43
|
-
*
|
|
44
|
-
* @param getter Function to get the value of the box.
|
|
45
|
-
* @param setter Function to set the value of the box.
|
|
46
|
-
* @returns A box with a `current` property which can be set to a new value.
|
|
47
|
-
*/
|
|
48
|
-
function boxWith<T>(getter: () => T, setter: (v: T) => void): WritableBox<T>;
|
|
49
|
-
|
|
50
|
-
function boxWith<T>(getter: () => T, setter?: (v: T) => void) {
|
|
12
|
+
export function boxWith<T>(getter: () => T): ReadableBox<T>;
|
|
13
|
+
export function boxWith<T>(getter: () => T, setter: (v: T) => void): WritableBox<T>;
|
|
14
|
+
export function boxWith<T>(getter: () => T, setter?: (v: T) => void) {
|
|
51
15
|
if (setter) {
|
|
52
16
|
return {
|
|
53
|
-
[BoxSymbol]: true,
|
|
54
|
-
[isWritableSymbol]: true,
|
|
55
17
|
get current() {
|
|
56
18
|
return getter();
|
|
57
19
|
},
|
|
@@ -62,32 +24,12 @@ function boxWith<T>(getter: () => T, setter?: (v: T) => void) {
|
|
|
62
24
|
}
|
|
63
25
|
|
|
64
26
|
return {
|
|
65
|
-
[BoxSymbol]: true,
|
|
66
27
|
get current() {
|
|
67
28
|
return getter();
|
|
68
29
|
},
|
|
69
30
|
};
|
|
70
31
|
}
|
|
71
32
|
|
|
72
|
-
/**
|
|
73
|
-
* The one bridge for a stateful `$bindable` prop: write the prop, then notify.
|
|
74
|
-
*
|
|
75
|
-
* `set` must write unconditionally — equality/idempotence guards belong to the
|
|
76
|
-
* caller (the *State class), so a write through this box always means a real
|
|
77
|
-
* change and `notify` fires exactly once per change. Pass `notify` as a closure
|
|
78
|
-
* (`(v) => onValueChange?.(v)`) so the latest callback prop is read at call
|
|
79
|
-
* time; a bare reference freezes the prop's value at bundle creation.
|
|
80
|
-
*
|
|
81
|
-
* Convention: writable bridges (value/open/checked/ref/...) use `bindableWith`;
|
|
82
|
-
* `boxWith` is for read-only views. A two-argument `boxWith` is legacy.
|
|
83
|
-
*/
|
|
84
|
-
export function bindableWith<T>(get: () => T, set: (v: T) => void, notify?: (v: T) => void): WritableBox<T> {
|
|
85
|
-
return boxWith(get, (v) => {
|
|
86
|
-
set(v);
|
|
87
|
-
notify?.(v);
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
33
|
/**
|
|
92
34
|
* Repair a bindable value synchronously for setup/SSR, then repair it again
|
|
93
35
|
* before DOM updates whenever the tracked source changes.
|
|
@@ -100,54 +42,3 @@ export function repairBindable(track: () => unknown, repair: () => void) {
|
|
|
100
42
|
untrack(repair);
|
|
101
43
|
});
|
|
102
44
|
}
|
|
103
|
-
|
|
104
|
-
export const simpleBox = createWritableBox;
|
|
105
|
-
|
|
106
|
-
export { boxWith };
|
|
107
|
-
|
|
108
|
-
type BoxAutoResetOptions<T> = {
|
|
109
|
-
afterMs?: number;
|
|
110
|
-
onChange?: (value: T) => void;
|
|
111
|
-
getWindow: () => Window & typeof globalThis;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const defaultBoxAutoResetOptions: Partial<BoxAutoResetOptions<unknown>> = {
|
|
115
|
-
afterMs: 10000,
|
|
116
|
-
onChange: () => {},
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Creates a box which will be reset to the default value after some time.
|
|
121
|
-
*
|
|
122
|
-
* @param defaultValue The value which will be set.
|
|
123
|
-
* @param afterMs A zero-or-greater delay in milliseconds.
|
|
124
|
-
*/
|
|
125
|
-
export function boxAutoReset<T>(defaultValue: T, options: BoxAutoResetOptions<T>): WritableBox<T> {
|
|
126
|
-
const { afterMs, onChange, getWindow } = { ...defaultBoxAutoResetOptions, ...options };
|
|
127
|
-
|
|
128
|
-
let timeout: number | null = null;
|
|
129
|
-
let value = $state(defaultValue);
|
|
130
|
-
|
|
131
|
-
function resetAfter() {
|
|
132
|
-
return getWindow().setTimeout(() => {
|
|
133
|
-
value = defaultValue;
|
|
134
|
-
onChange?.(defaultValue);
|
|
135
|
-
}, afterMs);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
$effect(() => {
|
|
139
|
-
return () => {
|
|
140
|
-
if (timeout) getWindow().clearTimeout(timeout);
|
|
141
|
-
};
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
return boxWith(
|
|
145
|
-
() => value,
|
|
146
|
-
(v) => {
|
|
147
|
-
value = v;
|
|
148
|
-
onChange?.(v);
|
|
149
|
-
if (timeout) getWindow().clearTimeout(timeout);
|
|
150
|
-
timeout = resetAfter();
|
|
151
|
-
},
|
|
152
|
-
);
|
|
153
|
-
}
|
|
@@ -1,19 +1,9 @@
|
|
|
1
|
-
export {
|
|
2
|
-
bindableWith,
|
|
3
|
-
boxWith,
|
|
4
|
-
boxAutoReset,
|
|
5
|
-
repairBindable,
|
|
6
|
-
simpleBox,
|
|
7
|
-
type WritableBox,
|
|
8
|
-
type ReadableBox,
|
|
9
|
-
} from './box.svelte.js';
|
|
10
|
-
export { get } from './types.js';
|
|
1
|
+
export { boxWith, repairBindable, type WritableBox, type ReadableBox } from './box.svelte.js';
|
|
11
2
|
export type {
|
|
12
3
|
AnyFn,
|
|
13
4
|
Box,
|
|
14
5
|
Expand,
|
|
15
6
|
Getter,
|
|
16
|
-
MaybeGetter,
|
|
17
7
|
ReadableBoxedValues,
|
|
18
8
|
WithChildren,
|
|
19
9
|
WithRefProps,
|
|
@@ -4,9 +4,6 @@ import type { ReadableBox, WritableBox } from './box.svelte.js';
|
|
|
4
4
|
import { BROWSER } from 'esm-env';
|
|
5
5
|
|
|
6
6
|
export type Getter<T> = () => T;
|
|
7
|
-
export type MaybeGetter<T> = T | Getter<T>;
|
|
8
|
-
|
|
9
|
-
export const get = <T>(value: MaybeGetter<T>): T => (typeof value === 'function' ? (value as Getter<T>)() : value);
|
|
10
7
|
|
|
11
8
|
export type Box<T> = ReadableBox<T> | WritableBox<T>;
|
|
12
9
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createEffectTimeout } from './timeout-fn.svelte.js';
|
|
2
2
|
import { getNextMatch } from './arrays.js';
|
|
3
3
|
|
|
4
4
|
export const textContentOf = (node: HTMLElement) => node.textContent?.trim() ?? '';
|
|
@@ -12,37 +12,33 @@ type TypeaheadOpts<T> = {
|
|
|
12
12
|
|
|
13
13
|
export class Typeahead<T> {
|
|
14
14
|
readonly #opts: TypeaheadOpts<T>;
|
|
15
|
-
|
|
15
|
+
search = $state.raw('');
|
|
16
|
+
readonly #resetTimer: ReturnType<typeof createEffectTimeout<() => void>>;
|
|
16
17
|
|
|
17
18
|
constructor(opts: TypeaheadOpts<T>) {
|
|
18
19
|
this.#opts = opts;
|
|
19
|
-
this.#
|
|
20
|
-
afterMs: 1000,
|
|
21
|
-
getWindow: opts.getWindow,
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
get search() {
|
|
26
|
-
return this.#search.current;
|
|
20
|
+
this.#resetTimer = createEffectTimeout(() => this.reset(), () => 1000, opts.getWindow);
|
|
27
21
|
}
|
|
28
22
|
|
|
29
23
|
handleKey(key: string, candidates: T[]) {
|
|
30
24
|
if (!candidates.length) return;
|
|
31
25
|
|
|
32
|
-
this
|
|
26
|
+
this.search += key;
|
|
27
|
+
this.#resetTimer.start();
|
|
33
28
|
|
|
34
29
|
const currentCandidate = this.#opts.getCurrentCandidate();
|
|
35
30
|
const current = candidates.find((candidate) => candidate === currentCandidate);
|
|
36
31
|
const currentMatch = current === undefined ? '' : this.#opts.getSearchText(current);
|
|
37
32
|
|
|
38
33
|
const values = candidates.map((candidate) => this.#opts.getSearchText(candidate));
|
|
39
|
-
const nextMatch = getNextMatch(values, this
|
|
34
|
+
const nextMatch = getNextMatch(values, this.search, currentMatch);
|
|
40
35
|
const newCandidate = candidates.find((candidate) => this.#opts.getSearchText(candidate) === nextMatch);
|
|
41
36
|
if (newCandidate) this.#opts.onMatch(newCandidate);
|
|
42
37
|
return newCandidate;
|
|
43
38
|
}
|
|
44
39
|
|
|
45
40
|
reset() {
|
|
46
|
-
this
|
|
41
|
+
this.search = '';
|
|
42
|
+
this.#resetTimer.stop();
|
|
47
43
|
}
|
|
48
44
|
}
|
|
@@ -79,6 +79,12 @@ export type AnyFn = (...args: any[]) => any;
|
|
|
79
79
|
|
|
80
80
|
export type WithRefOpts<T = {}> = T & ReadableBoxedValues<{ id: string }> & WritableBoxedValues<{ ref: HTMLElement | null }>;
|
|
81
81
|
|
|
82
|
+
/** A part's `id` and bindable `ref` as accessors over its `$props()`; engines read them, the attachment writes `ref`. */
|
|
83
|
+
export type RefOpts = {
|
|
84
|
+
readonly id: string;
|
|
85
|
+
ref: HTMLElement | null;
|
|
86
|
+
};
|
|
87
|
+
|
|
82
88
|
export type BitsEvent<T extends Event = Event, U extends HTMLElement = HTMLElement> = T & {
|
|
83
89
|
currentTarget: U;
|
|
84
90
|
};
|