lithesome 0.23.6 → 0.24.0

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/README.md CHANGED
@@ -9,10 +9,18 @@ An unstyled component library for Svelte 5.
9
9
 
10
10
  ## Install
11
11
 
12
+ ```bash
13
+ npm i -D lithesome
14
+ ```
15
+
12
16
  ```bash
13
17
  pnpm i -D lithesome
14
18
  ```
15
19
 
20
+ ```bash
21
+ bun add -D lithesome
22
+ ```
23
+
16
24
  ## Usage
17
25
 
18
26
  ```svelte
@@ -21,9 +29,7 @@ pnpm i -D lithesome
21
29
  </script>
22
30
 
23
31
  <Menu>
24
- <MenuTrigger>
25
- <button>Auth</button>
26
- </MenuTrigger>
32
+ <MenuTrigger>Auth</MenuTrigger>
27
33
  <MenuContent>
28
34
  <MenuItem>My Profile</MenuItem>
29
35
  <MenuItem>Account Settings</MenuItem>
@@ -14,8 +14,8 @@
14
14
  disabled = $bindable(false),
15
15
  portalTarget = 'body',
16
16
  floatingConfig = {},
17
- value = $bindable(),
18
- multiple = false,
17
+ value = $bindable(null!),
18
+ unselectable = false,
19
19
  onValueChanged
20
20
  }: SelectProps = $props();
21
21
 
@@ -40,7 +40,7 @@
40
40
  ),
41
41
  portalTarget: stateValue(() => portalTarget),
42
42
  floatingConfig: stateValue(() => floatingConfig),
43
- multiple: stateValue(() => multiple)
43
+ unselectable: stateValue(() => unselectable)
44
44
  });
45
45
  </script>
46
46
 
@@ -1,4 +1,4 @@
1
1
  import type { SelectProps } from '../../types/index.js';
2
- declare const Select: import("svelte").Component<SelectProps<any>, {}, "disabled" | "visible" | "value">;
2
+ declare const Select: import("svelte").Component<SelectProps, {}, "disabled" | "visible" | "value">;
3
3
  type Select = ReturnType<typeof Select>;
4
4
  export default Select;
@@ -7,7 +7,7 @@ declare const SelectOption: import("svelte").Component<SelectOptionProps<{
7
7
  role: string;
8
8
  tabindex: number;
9
9
  'aria-selected': boolean;
10
- 'data-value': import("../../internals/index.js").JsonValue;
10
+ 'data-value': string;
11
11
  'data-label': string;
12
12
  }> & Record<string, any>, {}, "label" | "disabled" | "ref" | "value">;
13
13
  type SelectOption = ReturnType<typeof SelectOption>;
@@ -9,9 +9,11 @@
9
9
  let {
10
10
  id = parseId(uid),
11
11
  ref = $bindable(),
12
- placeholder = $bindable(''),
12
+ placeholder = $bindable('Select an option...'),
13
+ children,
14
+ custom,
13
15
  ...props
14
- }: SelectValueProps & Record<string, any> = $props();
16
+ }: SelectValueProps<typeof ctx.props> & Record<string, any> = $props();
15
17
 
16
18
  let ctx = useSelectValue({
17
19
  id: stateValue(() => id),
@@ -20,6 +22,6 @@
20
22
  });
21
23
  </script>
22
24
 
23
- <Element bind:ref {ctx} {...props} as="span">
24
- {ctx.label}
25
+ <Element bind:ref {custom} {ctx} {...props} as="span">
26
+ {ctx.state.placeholderVisible ? placeholder : ctx.state.selectedLabels.join(', ')}
25
27
  </Element>
@@ -1,5 +1,8 @@
1
1
  import type { SelectValueProps } from '../../types/index.js';
2
- type $$ComponentProps = SelectValueProps & Record<string, any>;
3
- declare const SelectValue: import("svelte").Component<$$ComponentProps, {}, "ref" | "placeholder">;
2
+ declare const SelectValue: import("svelte").Component<SelectValueProps<{
3
+ id: string;
4
+ "data-select-value": string;
5
+ 'data-placeholder': true | undefined;
6
+ }> & Record<string, any>, {}, "ref" | "placeholder">;
4
7
  type SelectValue = ReturnType<typeof SelectValue>;
5
8
  export default SelectValue;
@@ -2,19 +2,17 @@ import { SvelteMap } from 'svelte/reactivity';
2
2
  import { Floating } from '../../internals/index.js';
3
3
  import type { CalcIndexAction, GetInternalProps, JsonValue } from '../../internals/index.js';
4
4
  import type { SelectArrowProps, SelectArrowState, SelectContentProps, SelectContentState, SelectOptionProps, SelectOptionState, SelectProps, SelectState, SelectTriggerProps, SelectTriggerState, SelectValueProps, SelectValueState } from '../../types/index.js';
5
- interface InternalSelectOption {
6
- value: JsonValue;
7
- label: string;
8
- }
9
5
  type RootProps = GetInternalProps<SelectProps>;
10
6
  declare class SelectRoot extends Floating {
11
7
  $$: RootProps;
12
8
  hoveredIndex: number;
13
- options: InternalSelectOption[];
14
- selectedOptions: InternalSelectOption[];
9
+ options: HTMLElement[];
15
10
  sharedIds: SvelteMap<"content" | "trigger", string>;
16
11
  mounted: boolean;
17
- HoveredOption: InternalSelectOption | undefined;
12
+ searchable: boolean;
13
+ serachTerm: string;
14
+ HoveredOption: HTMLElement | undefined;
15
+ SelectedOptions: HTMLElement[];
18
16
  constructor(props: RootProps);
19
17
  /**
20
18
  * Toggle the visible state of the content
@@ -30,28 +28,20 @@ declare class SelectRoot extends Floating {
30
28
  */
31
29
  close: () => void;
32
30
  /**
33
- * Tells the components that checks have been completed, render as normal.
34
- */
35
- doneMounting: () => void;
36
- /**
37
- * Get the element node of the current hovered option.
31
+ * Get all non-disabled options from the content.
38
32
  */
39
- getHoveredElement: () => HTMLElement | undefined;
33
+ getAvilableOptions: () => Promise<void>;
40
34
  /**
41
35
  * Get any element by its value inside the contents element.
42
36
  * @param value The value to search for.
43
37
  */
44
38
  getElementByValue: (value: JsonValue) => HTMLElement | null;
45
- /**
46
- * Get either the hovered or first selected option id.
47
- */
48
- getHoveredOrFirstSelectedId: () => Promise<string | undefined>;
49
39
  /**
50
40
  * Append the option to the parent array, used for keeping track of options.
51
41
  * @param value The value of the option. This must be unique.
52
42
  * @param label The label of the option. If no label is found, the text content of the node is used.
53
43
  */
54
- registerOption: (value: JsonValue, label: string) => void;
44
+ registerOption: (option: HTMLElement) => void;
55
45
  /**
56
46
  * Move the focus to another element based on the action used.
57
47
  * @param action The direction in which to travel.
@@ -61,17 +51,13 @@ declare class SelectRoot extends Floating {
61
51
  * Sets the hovered option.
62
52
  * @param value The unique value of the option.
63
53
  */
64
- setHovered: (value: JsonValue) => void;
54
+ setHovered: (value: string) => void;
65
55
  /**
66
56
  * Sets the selected option.
67
57
  *
68
58
  * Handles if singular or mulitple values.
69
59
  */
70
60
  setSelected: () => void;
71
- /**
72
- * Sets the trigger label to the selected value, only if it's found in the options array.
73
- */
74
- setInitialSelected: () => Promise<void>;
75
61
  state: SelectState;
76
62
  }
77
63
  type TriggerProps = GetInternalProps<SelectTriggerProps>;
@@ -140,7 +126,7 @@ declare class SelectOption {
140
126
  role: string;
141
127
  tabindex: number;
142
128
  'aria-selected': boolean;
143
- 'data-value': JsonValue;
129
+ 'data-value': string;
144
130
  'data-label': string;
145
131
  };
146
132
  state: SelectOptionState;
@@ -151,7 +137,6 @@ declare class SelectValue {
151
137
  _root: SelectRoot;
152
138
  PlaceholderVisible: boolean;
153
139
  constructor(root: SelectRoot, props: ValueProps);
154
- label: string;
155
140
  props: {
156
141
  id: string;
157
142
  "data-select-value": string;
@@ -1,30 +1,50 @@
1
- import { tick } from 'svelte';
1
+ import { onMount, tick } from 'svelte';
2
2
  import { SvelteMap } from 'svelte/reactivity';
3
3
  import { outside } from '../../attachments/outside.js';
4
4
  import { portal } from '../../attachments/portal.js';
5
- import { addEvents, attach, buildContext, calculateIndex, createAttributes, floating, Floating, KEYS, visuallyHidden } from '../../internals/index.js';
6
- const { attrs, selectors } = createAttributes('select', ['root', 'trigger', 'content', 'arrow', 'option', 'value']);
5
+ import { addEvents, attach, buildContext, calculateIndex, createAttributes, floating, Floating, KEYS, removeDisabledElements, visuallyHidden } from '../../internals/index.js';
6
+ const { attrs, selectors } = createAttributes('select', [
7
+ 'root',
8
+ 'trigger',
9
+ 'content',
10
+ 'arrow',
11
+ 'option',
12
+ 'value',
13
+ 'search'
14
+ ]);
7
15
  class SelectRoot extends Floating {
8
16
  $$;
9
17
  hoveredIndex = $state(-1);
10
18
  options = $state([]);
11
- selectedOptions = $state([]);
12
19
  sharedIds = new SvelteMap();
13
20
  mounted = $state(false);
21
+ searchable = $state(false);
22
+ serachTerm = $state('');
14
23
  HoveredOption = $derived.by(() => this.options[this.hoveredIndex] || undefined);
24
+ SelectedOptions = $derived(this.options.filter((opt) => {
25
+ if (!opt.dataset.value)
26
+ return false;
27
+ return Array.isArray(this.$$.value.val)
28
+ ? this.$$.value.val.includes(opt.dataset.value)
29
+ : this.$$.value.val === opt.dataset.value;
30
+ }));
15
31
  constructor(props) {
16
32
  super();
17
33
  this.$$ = props;
18
- if (this.$$.value.val)
19
- this.setInitialSelected();
20
- else
21
- this.doneMounting();
34
+ onMount(async () => {
35
+ await tick();
36
+ this.mounted = true;
37
+ this.$$.visible.val = false;
38
+ });
22
39
  }
23
40
  /**
24
41
  * Toggle the visible state of the content
25
42
  */
26
43
  toggle = () => {
27
- this.$$.visible.val = !this.$$.visible.val;
44
+ if (this.$$.visible.val)
45
+ this.close();
46
+ else
47
+ this.open();
28
48
  };
29
49
  /**
30
50
  * Set the visible state of the content to true
@@ -38,27 +58,15 @@ class SelectRoot extends Floating {
38
58
  */
39
59
  close = () => {
40
60
  this.$$.visible.val = false;
41
- this.options = [];
42
61
  this.hoveredIndex = -1;
43
62
  };
44
63
  /**
45
- * Tells the components that checks have been completed, render as normal.
46
- */
47
- doneMounting = () => {
48
- this.mounted = true;
49
- this.$$.visible.val = false;
50
- };
51
- /**
52
- * Get the element node of the current hovered option.
64
+ * Get all non-disabled options from the content.
53
65
  */
54
- getHoveredElement = () => {
55
- const hovered = this.options[this.hoveredIndex];
56
- if (hovered) {
57
- const option = document.querySelector(`#${this.$$.id.val} ${selectors.option}[data-value=${hovered.value}]`);
58
- if (option)
59
- return option;
60
- }
61
- return undefined;
66
+ getAvilableOptions = async () => {
67
+ const contentElement = document.querySelector(`#${this.sharedIds.get('content')}`);
68
+ if (contentElement)
69
+ this.options = removeDisabledElements(contentElement.querySelectorAll(selectors.option));
62
70
  };
63
71
  /**
64
72
  * Get any element by its value inside the contents element.
@@ -67,28 +75,15 @@ class SelectRoot extends Floating {
67
75
  getElementByValue = (value) => {
68
76
  return document.querySelector(`#${this.sharedIds.get('content')} ${selectors.option}[data-value="${value}"]`);
69
77
  };
70
- /**
71
- * Get either the hovered or first selected option id.
72
- */
73
- getHoveredOrFirstSelectedId = async () => {
74
- await tick();
75
- const selectedOption = this.HoveredOption || this.selectedOptions[0];
76
- if (selectedOption) {
77
- const option = this.getElementByValue(selectedOption.value);
78
- if (option)
79
- return option.id;
80
- }
81
- return undefined;
82
- };
83
78
  /**
84
79
  * Append the option to the parent array, used for keeping track of options.
85
80
  * @param value The value of the option. This must be unique.
86
81
  * @param label The label of the option. If no label is found, the text content of the node is used.
87
82
  */
88
- registerOption = (value, label) => {
89
- const find = this.options.find((option) => option.value === value);
83
+ registerOption = (option) => {
84
+ const find = this.options.find((option) => option.dataset.value === option.dataset.value);
90
85
  if (!find)
91
- this.options.push({ value, label });
86
+ this.options.push(option);
92
87
  };
93
88
  /**
94
89
  * Move the focus to another element based on the action used.
@@ -96,16 +91,15 @@ class SelectRoot extends Floating {
96
91
  */
97
92
  navigate = (action) => {
98
93
  this.hoveredIndex = calculateIndex(action, this.options, this.hoveredIndex);
99
- const element = this.getHoveredElement();
100
- if (element)
101
- element.scrollIntoView({ block: 'nearest' });
94
+ if (this.HoveredOption)
95
+ this.HoveredOption.scrollIntoView({ block: 'nearest' });
102
96
  };
103
97
  /**
104
98
  * Sets the hovered option.
105
99
  * @param value The unique value of the option.
106
100
  */
107
101
  setHovered = (value) => {
108
- this.hoveredIndex = this.options.findIndex((el) => el.value === value);
102
+ this.hoveredIndex = this.options.findIndex((el) => el.dataset.value === value);
109
103
  };
110
104
  /**
111
105
  * Sets the selected option.
@@ -113,38 +107,22 @@ class SelectRoot extends Floating {
113
107
  * Handles if singular or mulitple values.
114
108
  */
115
109
  setSelected = () => {
116
- if (!this.HoveredOption)
110
+ if (!this.HoveredOption || !this.HoveredOption.dataset.value)
117
111
  return;
118
- if (this.$$.multiple.val) {
119
- if (this.selectedOptions.find((el) => el.value === this.HoveredOption?.value)) {
120
- this.selectedOptions = this.selectedOptions.filter((el) => el.value !== this.HoveredOption?.value);
121
- }
122
- else {
123
- this.selectedOptions.push(this.HoveredOption);
124
- }
112
+ const newVal = this.HoveredOption.dataset.value;
113
+ if (Array.isArray(this.$$.value.val)) {
114
+ if (this.$$.value.val.includes(newVal))
115
+ this.$$.value.val = this.$$.value.val.filter((el) => el !== newVal);
116
+ else
117
+ this.$$.value.val.push(newVal);
125
118
  }
126
119
  else {
127
- this.selectedOptions[0] = this.HoveredOption;
120
+ if (this.$$.unselectable.val && this.$$.value.val === newVal)
121
+ this.$$.value.val = '';
122
+ else
123
+ this.$$.value.val = newVal;
124
+ this.close();
128
125
  }
129
- if (!this.$$.multiple.val)
130
- this.$$.visible.val = false;
131
- this.$$.value.val = this.$$.multiple.val
132
- ? this.selectedOptions.map((el) => el.value)
133
- : this.selectedOptions[0].value;
134
- };
135
- /**
136
- * Sets the trigger label to the selected value, only if it's found in the options array.
137
- */
138
- setInitialSelected = async () => {
139
- await tick();
140
- const value = this.$$.value.val;
141
- this.selectedOptions = this.options.filter((el) => {
142
- if (!Array.isArray(value) && el.value === value)
143
- return el;
144
- else if (Array.isArray(value) && value.includes(el.value))
145
- return el;
146
- });
147
- this.doneMounting();
148
126
  };
149
127
  state = $derived.by(() => ({
150
128
  visible: this.$$.visible.val
@@ -168,18 +146,17 @@ class SelectTrigger {
168
146
  ...attach((node) => {
169
147
  this._root.trigger = node;
170
148
  // Such a hacky way, but it works :\
171
- if (this._root.HoveredOption || this._root.selectedOptions[0]) {
172
- tick().then(async () => {
149
+ if (this._root.HoveredOption || this._root.SelectedOptions[0]) {
150
+ tick().then(() => {
173
151
  if (this._root.$$.visible.val) {
174
- const id = await this._root.getHoveredOrFirstSelectedId();
152
+ const { id } = this._root.HoveredOption || this._root.SelectedOptions[0];
175
153
  if (id)
176
154
  node.setAttribute('aria-activedescendant', id);
177
155
  }
178
- else {
179
- node.removeAttribute('aria-activedescendant');
180
- }
181
156
  });
182
157
  }
158
+ if (!this._root.$$.visible.val)
159
+ node.removeAttribute('aria-activedescendant');
183
160
  return addEvents(node, {
184
161
  click: () => {
185
162
  if (this._root.$$.disabled.val)
@@ -203,8 +180,8 @@ class SelectTrigger {
203
180
  if (key === KEYS.enter) {
204
181
  e.preventDefault();
205
182
  if (this._root.HoveredOption && this._root.$$.visible.val) {
206
- this._root.getElementByValue(this._root.HoveredOption.value)?.click();
207
- if (!this._root.$$.multiple.val)
183
+ this._root.HoveredOption?.click();
184
+ if (!Array.isArray(this._root.$$.value.val))
208
185
  this._root.close();
209
186
  }
210
187
  else {
@@ -228,6 +205,20 @@ class SelectContent {
228
205
  this._root = root;
229
206
  this.$$ = props;
230
207
  this._root.sharedIds.set('content', this.$$.id.val);
208
+ // Get element references but wait for label to be populated.
209
+ $effect(() => {
210
+ if (this._root.$$.visible.val) {
211
+ tick().then(() => {
212
+ this._root.getAvilableOptions();
213
+ });
214
+ }
215
+ });
216
+ // Set first selected value as highlighted option
217
+ $effect(() => {
218
+ if (this._root.$$.visible.val && this._root.SelectedOptions.length && !this._root.HoveredOption) {
219
+ this._root.hoveredIndex = this._root.options.findIndex((el) => el.dataset.selected === 'true');
220
+ }
221
+ });
231
222
  }
232
223
  props = $derived.by(() => ({
233
224
  id: this.$$.id.val,
@@ -273,17 +264,11 @@ class SelectArrow {
273
264
  class SelectOption {
274
265
  $$;
275
266
  _root;
276
- Hovered = $derived.by(() => this._root.HoveredOption?.value === this.$$.value.val);
277
- Selected = $derived.by(() => !!this._root.selectedOptions.find((el) => el.value === this.$$.value.val));
267
+ Hovered = $derived.by(() => this._root.HoveredOption?.dataset.value === this.$$.value.val);
268
+ Selected = $derived.by(() => !!this._root.SelectedOptions.find((el) => el.dataset.value === this.$$.value.val));
278
269
  constructor(root, props) {
279
270
  this._root = root;
280
271
  this.$$ = props;
281
- $effect(() => {
282
- if (this.$$.disabled.val)
283
- return;
284
- const label = this.$$.label.val || this.$$.ref.val.textContent.trim();
285
- this._root.registerOption(this.$$.value.val, label);
286
- });
287
272
  }
288
273
  props = $derived.by(() => ({
289
274
  id: this.$$.id.val,
@@ -294,11 +279,11 @@ class SelectOption {
294
279
  tabindex: 0,
295
280
  'aria-selected': this.Selected,
296
281
  'data-value': this.$$.value.val,
297
- 'data-label': this.$$.label.val,
282
+ 'data-label': this.$$.ref.val && !this.$$.label.val ? this.$$.ref.val.textContent.trim() : this.$$.label.val,
298
283
  ...attach((node) => {
299
284
  // Set the hovered index to the active item, if that item is "selected".
300
285
  if (this._root.$$.value.val === this.$$.value.val) {
301
- this._root.hoveredIndex = this._root.options.findIndex((el) => el.value === this.$$.value.val);
286
+ this._root.hoveredIndex = this._root.options.findIndex((el) => el.dataset.value === this.$$.value.val);
302
287
  }
303
288
  return addEvents(node, {
304
289
  click: () => {
@@ -322,12 +307,11 @@ class SelectOption {
322
307
  class SelectValue {
323
308
  $$;
324
309
  _root;
325
- PlaceholderVisible = $derived.by(() => this._root.selectedOptions.length === 0);
310
+ PlaceholderVisible = $derived.by(() => !this._root.$$.value.val.length);
326
311
  constructor(root, props) {
327
312
  this._root = root;
328
313
  this.$$ = props;
329
314
  }
330
- label = $derived.by(() => this.PlaceholderVisible ? this.$$.placeholder.val : this._root.selectedOptions.map((el) => el.label).join(','));
331
315
  props = $derived.by(() => ({
332
316
  id: this.$$.id.val,
333
317
  [attrs.value]: '',
@@ -335,7 +319,8 @@ class SelectValue {
335
319
  }));
336
320
  state = $derived.by(() => ({
337
321
  visible: this._root.$$.visible.val,
338
- placeholderVisible: this.PlaceholderVisible
322
+ placeholderVisible: this.PlaceholderVisible,
323
+ selectedLabels: this._root.SelectedOptions.map((el) => el.dataset.label)
339
324
  }));
340
325
  }
341
326
  //
@@ -1,20 +1,22 @@
1
- import type { FloatingContent, JsonValue, Props, PropsNoChildren, PropsNoRender } from '../../internals/index.js';
2
- export interface SelectProps<V extends JsonValue = any> extends PropsNoRender<SelectState>, FloatingContent {
1
+ import type { FloatingContent, Props, PropsNoRender } from '../../internals/index.js';
2
+ export interface SelectProps extends PropsNoRender<SelectState>, FloatingContent {
3
3
  /**
4
4
  * The currently selected option(s).
5
5
  *
6
6
  * ### `$bindable`
7
7
  */
8
- value?: V;
8
+ value?: string | string[];
9
9
  /**
10
- * Allows multiple options to be selected at once.
10
+ * Allows a non-array value to be unselected.
11
+ *
12
+ * @default false
11
13
  */
12
- multiple?: boolean;
14
+ unselectable?: boolean;
13
15
  /**
14
16
  * Fires whenever the `value` prop changes.
15
17
  * @param value The new value
16
18
  */
17
- onValueChanged?: (value: V) => void;
19
+ onValueChanged?: (value: string | string[]) => void;
18
20
  }
19
21
  export interface SelectState {
20
22
  /**
@@ -50,7 +52,7 @@ export interface SelectOptionProps<P = any> extends Props<HTMLButtonElement, P,
50
52
  /**
51
53
  * The value of the option.
52
54
  */
53
- value: JsonValue;
55
+ value: string;
54
56
  /**
55
57
  * Disables the option, skipping mouse/keyboard navigation and stopping events from firing.
56
58
  *
@@ -74,7 +76,7 @@ export interface SelectOptionState {
74
76
  */
75
77
  selected: boolean;
76
78
  }
77
- export interface SelectValueProps extends PropsNoChildren<HTMLSpanElement, SelectValueState> {
79
+ export interface SelectValueProps<P = any> extends Props<HTMLSpanElement, P, SelectValueState> {
78
80
  /**
79
81
  * The value displayed when no option(s) is selected.
80
82
  */
@@ -89,4 +91,8 @@ export interface SelectValueState {
89
91
  * True if no options are selected.
90
92
  */
91
93
  placeholderVisible: boolean;
94
+ /**
95
+ * The currently selected options.
96
+ */
97
+ selectedLabels: string[];
92
98
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lithesome",
3
- "version": "0.23.6",
3
+ "version": "0.24.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "bun --bun vite build && npm run prepack",