sheer-ui 0.4.0 → 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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/src/lib/components/accordion/components/accordion-item.svelte +3 -3
  3. package/src/lib/components/accordion/components/accordion.svelte +29 -13
  4. package/src/lib/components/accordion/types.ts +3 -3
  5. package/src/lib/components/checkbox/components/checkbox-group.svelte +7 -5
  6. package/src/lib/components/checkbox/components/checkbox.svelte +12 -8
  7. package/src/lib/components/combobox/components/combobox.svelte +2 -2
  8. package/src/lib/components/combobox/select/components/select.svelte +2 -2
  9. package/src/lib/components/context-menu/components/context-menu-trigger.svelte +1 -1
  10. package/src/lib/components/date-field/date-field.svelte.ts +2 -3
  11. package/src/lib/components/menu/components/menu-checkbox-group.svelte +6 -4
  12. package/src/lib/components/menu/components/menu-checkbox-item.svelte +11 -7
  13. package/src/lib/components/menu/menu.svelte.ts +9 -18
  14. package/src/lib/components/menubar/menubar.svelte.ts +7 -6
  15. package/src/lib/components/navigation-menu/components/navigation-menu-content.svelte +1 -1
  16. package/src/lib/components/navigation-menu/components/navigation-menu-indicator.svelte +2 -2
  17. package/src/lib/components/navigation-menu/components/navigation-menu-trigger.svelte +1 -1
  18. package/src/lib/components/navigation-menu/navigation-menu.svelte.ts +57 -60
  19. package/src/lib/components/pagination/components/pagination-next-button.svelte +12 -7
  20. package/src/lib/components/pagination/components/pagination-page.svelte +15 -8
  21. package/src/lib/components/pagination/components/pagination-prev-button.svelte +12 -7
  22. package/src/lib/components/pagination/components/pagination.svelte +31 -18
  23. package/src/lib/components/pagination/pagination.svelte.ts +48 -64
  24. package/src/lib/components/pin-input/pin-input.svelte.ts +7 -8
  25. package/src/lib/components/pin-input/usePasswordManager.svelte.ts +2 -2
  26. package/src/lib/components/scroll-area/scroll-area.svelte.ts +9 -10
  27. package/src/lib/components/slider/components/slider.svelte +2 -1
  28. package/src/lib/components/tabs/components/tabs-content.svelte +12 -7
  29. package/src/lib/components/tabs/components/tabs-list.svelte +9 -6
  30. package/src/lib/components/tabs/components/tabs-trigger.svelte +15 -8
  31. package/src/lib/components/tabs/components/tabs.svelte +28 -17
  32. package/src/lib/components/tabs/tabs.svelte.ts +51 -67
  33. package/src/lib/components/time-field/time-field.svelte.ts +2 -2
  34. package/src/lib/components/toggle-group/components/toggle-group-item.svelte +15 -8
  35. package/src/lib/components/toggle-group/components/toggle-group.svelte +28 -16
  36. package/src/lib/components/toggle-group/toggle-group.svelte.ts +33 -35
  37. package/src/lib/components/toolbar/components/toolbar-group-item.svelte +15 -8
  38. package/src/lib/components/toolbar/components/toolbar-group.svelte +19 -14
  39. package/src/lib/components/toolbar/toolbar.svelte.ts +28 -30
  40. package/src/lib/components/tooltip/tooltip.svelte.ts +6 -27
  41. package/src/lib/internal/dismissible-layer/use-dismissable-layer.svelte.ts +3 -4
  42. package/src/lib/internal/floating-layer/use-floating-layer.svelte.ts +26 -30
  43. package/src/lib/internal/{roving-focus-group.ts → roving-focus-group.svelte.ts} +17 -19
  44. package/src/lib/internal/roving-focus-item.svelte.ts +12 -17
  45. package/src/lib/internal/selection.svelte.ts +55 -41
  46. package/src/lib/internal/{state-machine.ts → state-machine.svelte.ts} +6 -8
  47. package/src/lib/internal/timeout-fn.svelte.ts +6 -4
  48. package/src/lib/internal/tools/box.svelte.ts +5 -114
  49. package/src/lib/internal/tools/index.ts +1 -11
  50. package/src/lib/internal/tools/types.ts +0 -3
  51. package/src/lib/internal/typeahead.svelte.ts +9 -13
  52. package/src/lib/internal/types.ts +6 -0
  53. package/src/lib/internal/vaul/use-drawer-content.svelte.ts +2 -2
  54. package/src/lib/internal/vaul/use-drawer-root.svelte.ts +12 -7
  55. 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, WithRefOpts } from './types.js';
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 const emptySelection = (type: SelectionType): string | string[] => (type === 'single' ? '' : []);
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 in multiple
15
- * mode. The mode is fixed at construction, so a write always keeps the shape the consumer's
16
- * prop was declared with.
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 #value: WritableBox<string | string[]>;
21
+ readonly #read: () => Selection;
22
+ readonly #write: (value: Selection) => void;
21
23
 
22
- constructor(type: SelectionType, value: WritableBox<string | string[]>) {
24
+ constructor(type: SelectionType, read: () => Selection, write: (value: Selection) => void) {
23
25
  this.isMulti = type === 'multiple';
24
- this.#value = value;
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.#value.current;
39
+ const value = this.#read();
29
40
  return this.isMulti ? (value as string[]).includes(item) : value === item;
30
41
  }
31
42
 
32
- /** Flips membership and returns whether the item is selected afterwards. */
33
- toggle(item: string): boolean {
34
- const selected = !this.includes(item);
35
- if (this.isMulti) {
36
- const value = this.#value.current as string[];
37
- this.#value.current = selected ? [...value, item] : value.filter((v) => v !== item);
38
- } else {
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
- /** What a selection item needs from the group that owns it. */
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: ReadableBox<boolean>;
49
- readonly orientation: ReadableBox<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?: ReadableBox<boolean>;
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
- export interface SelectionItemOpts
60
- extends
61
- WithRefOpts,
62
- ReadableBoxedValues<{
63
- value: string;
64
- disabled: boolean;
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.current || this.group.disabled.current);
72
- readonly isPressed = $derived.by(() => this.group.selection.includes(this.opts.value.current));
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
- enabled: group.rovingFocus,
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 selected = this.group.selection.toggle(this.opts.value.current);
90
- if (selected && this.group.selectionTakesTabStop) this.group.rovingFocusGroup.setCurrentTabStopId(this.opts.id.current);
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.current,
126
+ id: this.opts.id,
113
127
  role: this.group.selection.isMulti ? undefined : 'radio',
114
- 'data-orientation': this.group.orientation.current,
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.current,
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
- readonly state: WritableBox<MachineState<M>>;
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 = simpleBox(initialState);
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.current is keyof M
25
- const nextState = this.#machine[this.state.current][event];
26
- return nextState ?? this.state.current;
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.current = this.#reducer(event);
28
+ this.state = this.#reducer(event);
31
29
  }
32
30
  }
@@ -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
- timer = setTimeout(() => {
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
- const BoxSymbol = Symbol('box');
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> = ReadableBox<T> & {
12
- readonly [isWritableSymbol]: true;
8
+ export type WritableBox<T> = {
13
9
  current: T;
14
10
  };
15
11
 
16
- function createWritableBox<T>(): WritableBox<T | undefined>;
17
- function createWritableBox<T>(initialValue: T): WritableBox<T>;
18
- function createWritableBox(initialValue?: unknown) {
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 { boxAutoReset, type WritableBox } from './tools/index.js';
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
- readonly #search: WritableBox<string>;
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.#search = boxAutoReset('', {
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.#search.current = this.#search.current + key;
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.#search.current, currentMatch);
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.#search.current = '';
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
  };
@@ -144,8 +144,8 @@ export function useDrawerContent(opts: UseDrawerContentProps) {
144
144
  return;
145
145
  }
146
146
 
147
- if (ctx.keyboardIsOpen.current) {
148
- ctx.keyboardIsOpen.current = false;
147
+ if (ctx.keyboardIsOpen) {
148
+ ctx.keyboardIsOpen = false;
149
149
  }
150
150
  }
151
151
 
@@ -1,4 +1,4 @@
1
- import { simpleBox, type ReadableBoxedValues, type WritableBoxedValues } from '../tools/index.js';
1
+ import type { ReadableBoxedValues, WritableBoxedValues } from '../tools/index.js';
2
2
  import type { DrawerDirection } from './types.js';
3
3
  import { useSnapPoints } from './use-snap-points.svelte.js';
4
4
  import { isInput, usePreventScroll } from './use-prevent-scroll.svelte.js';
@@ -64,7 +64,7 @@ export function useDrawerRoot(opts: UseDrawerRootProps) {
64
64
  let isAllowedToDrag = false;
65
65
  let nestedTransitionCleanup: (() => void) | null = null;
66
66
  let pointerStart = 0;
67
- let keyboardIsOpen = simpleBox(false);
67
+ let keyboardIsOpen = $state.raw(false);
68
68
  let shouldAnimate = $state(!opts.open.current);
69
69
  let previousDiffFromInitial = 0;
70
70
  let drawerHeight = 0;
@@ -307,7 +307,7 @@ export function useDrawerRoot(opts: UseDrawerRootProps) {
307
307
  if (!drawerNode || !opts.repositionInputs.current) return;
308
308
 
309
309
  const focusedElement = document.activeElement as HTMLElement;
310
- if (isInput(focusedElement) || keyboardIsOpen.current) {
310
+ if (isInput(focusedElement) || keyboardIsOpen) {
311
311
  const visualViewportHeight = window.visualViewport?.height || 0;
312
312
  const totalHeight = window.innerHeight;
313
313
  // This is the height of the keyboard
@@ -323,7 +323,7 @@ export function useDrawerRoot(opts: UseDrawerRootProps) {
323
323
 
324
324
  // visualViewport height may change due to some subtle changes to the keyboard. Checking if the height changed by 60 or more will make sure that they keyboard really changed its open state.
325
325
  if (Math.abs(previousDiffFromInitial - diffFromInitial) > 60) {
326
- keyboardIsOpen.current = !keyboardIsOpen.current;
326
+ keyboardIsOpen = !keyboardIsOpen;
327
327
  }
328
328
 
329
329
  if (
@@ -337,7 +337,7 @@ export function useDrawerRoot(opts: UseDrawerRootProps) {
337
337
  }
338
338
  previousDiffFromInitial = diffFromInitial;
339
339
  // We don't have to change the height if the input is in view, when we are here we are in the opened keyboard state so we can correctly check if the input is in view
340
- if (drawerHeight > visualViewportHeight || keyboardIsOpen.current) {
340
+ if (drawerHeight > visualViewportHeight || keyboardIsOpen) {
341
341
  const height = drawerNode.getBoundingClientRect().height;
342
342
  let newDrawerHeight = height;
343
343
 
@@ -354,7 +354,7 @@ export function useDrawerRoot(opts: UseDrawerRootProps) {
354
354
  drawerNode.style.height = `${initialDrawerHeight}px`;
355
355
  }
356
356
 
357
- if (opts.snapPoints.current && opts.snapPoints.current.length > 0 && !keyboardIsOpen.current) {
357
+ if (opts.snapPoints.current && opts.snapPoints.current.length > 0 && !keyboardIsOpen) {
358
358
  drawerNode.style.bottom = `0px`;
359
359
  } else {
360
360
  // Negative bottom value would never make sense
@@ -653,7 +653,12 @@ export function useDrawerRoot(opts: UseDrawerRootProps) {
653
653
  handleOnly: opts.handleOnly,
654
654
  container: opts.container,
655
655
  autoFocus: opts.autoFocus,
656
- keyboardIsOpen,
656
+ get keyboardIsOpen() {
657
+ return keyboardIsOpen;
658
+ },
659
+ set keyboardIsOpen(v: boolean) {
660
+ keyboardIsOpen = v;
661
+ },
657
662
  setDrawerNode,
658
663
  setOverlayNode,
659
664
  onPress,
@@ -1,43 +0,0 @@
1
- import { createContext } from 'svelte';
2
-
3
- export type AccordionType = 'single' | 'multiple';
4
-
5
- type AccordionStateProps = {
6
- type: () => AccordionType;
7
- value: () => string | string[];
8
- setValue: (value: string | string[]) => void;
9
- };
10
-
11
- /**
12
- * Each item is a `<details>`; this state owns the open set (bindable `value`) and
13
- * single-type exclusivity — not the native `name` attribute, which would unrender a
14
- * force-closed sibling before its closing transition can play. Items intercept summary
15
- * clicks and report intent here; the grid-track animation lives in ui.css.
16
- */
17
- export class AccordionState {
18
- readonly props: AccordionStateProps;
19
-
20
- constructor(props: AccordionStateProps) {
21
- this.props = props;
22
- }
23
-
24
- includes(item: string): boolean {
25
- const value = this.props.value();
26
- return Array.isArray(value) ? value.includes(item) : value === item;
27
- }
28
-
29
- /** Open/close intent from an item — intercepted summary clicks and native force-opens alike. */
30
- report(item: string, open: boolean): void {
31
- const value = this.props.value();
32
- if (Array.isArray(value)) {
33
- if (open && !value.includes(item)) this.props.setValue([...value, item]);
34
- else if (!open && value.includes(item)) this.props.setValue(value.filter((v) => v !== item));
35
- } else if (open) {
36
- if (value !== item) this.props.setValue(item);
37
- } else if (value === item) {
38
- this.props.setValue('');
39
- }
40
- }
41
- }
42
-
43
- export const [useAccordion, setAccordion] = createContext<AccordionState>();