@noya-app/noya-designsystem 0.1.44 → 0.1.46

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 (51) hide show
  1. package/.turbo/turbo-build.log +13 -10
  2. package/CHANGELOG.md +18 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +1194 -878
  5. package/dist/index.d.ts +1194 -878
  6. package/dist/index.js +11432 -10219
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +11556 -10360
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +4 -4
  11. package/src/__tests__/combobox.test.ts +137 -89
  12. package/src/components/AnimatePresence.tsx +10 -1
  13. package/src/components/Avatar.tsx +2 -0
  14. package/src/components/Breadcrumbs.tsx +29 -0
  15. package/src/components/Checkbox.tsx +6 -1
  16. package/src/components/Collection.tsx +74 -0
  17. package/src/components/Combobox.tsx +75 -56
  18. package/src/components/ComboboxMenu.tsx +61 -28
  19. package/src/components/CommandPalette.tsx +69 -0
  20. package/src/components/ContextMenu.tsx +33 -134
  21. package/src/components/DropdownMenu.tsx +46 -155
  22. package/src/components/EditableText.tsx +203 -0
  23. package/src/components/Fade.tsx +62 -0
  24. package/src/components/Grid.tsx +243 -0
  25. package/src/components/GridView.tsx +86 -96
  26. package/src/components/InputField.tsx +109 -133
  27. package/src/components/Label.tsx +81 -7
  28. package/src/components/LabeledField.tsx +112 -0
  29. package/src/components/List.tsx +268 -0
  30. package/src/components/ListView.tsx +65 -61
  31. package/src/components/Popover.tsx +12 -1
  32. package/src/components/SearchCompletionMenu.tsx +210 -0
  33. package/src/components/SegmentedControl.tsx +12 -6
  34. package/src/components/SelectMenu.tsx +110 -126
  35. package/src/components/Slider.tsx +18 -26
  36. package/src/components/Text.tsx +1 -0
  37. package/src/components/TextArea.tsx +4 -1
  38. package/src/components/Toolbar.tsx +9 -4
  39. package/src/components/TreeView.tsx +4 -0
  40. package/src/components/WorkspaceLayout.tsx +64 -20
  41. package/src/components/internal/Menu.tsx +107 -18
  42. package/src/components/internal/MenuViewport.tsx +152 -0
  43. package/src/components/internal/SelectItem.tsx +111 -0
  44. package/src/hooks/useIndent.ts +12 -0
  45. package/src/hooks/useLabel.ts +51 -0
  46. package/src/hooks/usePreservePanelSize.tsx +50 -19
  47. package/src/index.tsx +15 -6
  48. package/src/utils/combobox.ts +116 -101
  49. package/src/utils/createSectionedMenu.ts +11 -14
  50. package/src/utils/fuzzyScorer.ts +11 -8
  51. package/src/utils/selection.ts +48 -0
@@ -1,80 +1,76 @@
1
1
  import { chunkBy, partition } from "@noya-app/noya-utils";
2
- import { ReactNode, useSyncExternalStore } from "react";
2
+ import { useSyncExternalStore } from "react";
3
+ import {
4
+ isMenuItemSectionHeader,
5
+ isNonSelectableMenuItem,
6
+ isSelectableMenuItem,
7
+ MenuItem,
8
+ ScoredMenuItem,
9
+ SectionHeaderMenuItem,
10
+ SelectableMenuItem,
11
+ } from "../components/internal/Menu";
3
12
  import { fuzzyFilter, IScoredItem } from "./fuzzyScorer";
4
13
 
5
- export type ComboboxOption = {
6
- type?: undefined;
7
- id: string;
8
- name: string;
9
- icon?: ReactNode;
10
- alwaysInclude?: boolean;
11
- };
12
-
13
- export type ComboboxSectionHeader = {
14
- type: "sectionHeader";
15
- id: string;
16
- name: string;
17
- maxVisibleItems?: number;
18
- };
19
-
20
- export type ComboboxItem = ComboboxOption | ComboboxSectionHeader;
21
-
22
- export type InternalComboboxItem =
23
- | (ComboboxOption & IScoredItem)
24
- | ComboboxSectionHeader;
25
-
26
- export type ComboboxStateSnapshot = {
14
+ export type ComboboxStateSnapshot<T extends string> = {
27
15
  filter: string;
28
16
  selectedIndex: number;
29
- filteredItems: InternalComboboxItem[];
17
+ filteredItems: ScoredMenuItem<T>[];
30
18
  lastSubmittedValue: { filter: string; itemName: string };
31
19
  };
32
20
 
33
- export class ComboboxState {
21
+ export class ComboboxState<T extends string> {
34
22
  private filter: string;
35
23
  private selectedIndex: number;
36
- private items: ComboboxItem[];
24
+ private items: MenuItem<T>[];
37
25
  private lastSubmittedValue: { filter: string; itemName: string };
38
26
  private subscribers: (() => void)[] = [];
39
27
  private version = 0;
40
- private _cachedSnapshot: [number, ComboboxStateSnapshot] | undefined;
28
+ private _cachedSnapshot: [number, ComboboxStateSnapshot<T>] | undefined;
41
29
 
42
30
  constructor(
43
- items: ComboboxItem[],
44
- initialValue: string,
31
+ items: MenuItem<T>[],
32
+ initialTitle: string,
45
33
  allowArbitraryValues: boolean
46
34
  ) {
47
35
  this.items = items;
48
36
  this.lastSubmittedValue = {
49
- filter: initialValue,
50
- itemName: initialValue,
37
+ filter: initialTitle,
38
+ itemName: initialTitle,
51
39
  };
52
40
 
53
41
  // Initialize with the initial value
54
- this.filter = initialValue;
42
+ this.filter = initialTitle;
55
43
  this.selectedIndex = 0;
56
44
 
57
45
  // Create initial snapshot
58
46
  this._cachedSnapshot = [
59
47
  0,
60
48
  {
61
- filter: initialValue,
49
+ filter: initialTitle,
62
50
  selectedIndex: 0,
63
- filteredItems: items.map((item, index) => {
64
- if (item.type === "sectionHeader") return item;
65
- return { ...item, index, score: 0 };
66
- }),
51
+
52
+ filteredItems: this.items.flatMap((item, index) =>
53
+ isMenuItemSectionHeader(item)
54
+ ? item
55
+ : isSelectableMenuItem(item)
56
+ ? {
57
+ ...item,
58
+ index,
59
+ score: 0,
60
+ }
61
+ : []
62
+ ),
67
63
  lastSubmittedValue: this.lastSubmittedValue,
68
64
  },
69
65
  ];
70
66
 
71
67
  // If there's an initial value, validate it
72
- if (initialValue) {
68
+ if (initialTitle) {
73
69
  // Check if the initial value exists in items
74
- const hasMatch = items.some(
70
+ const hasMatch = this.items.some(
75
71
  (item) =>
76
- item.type !== "sectionHeader" &&
77
- item.name.toLowerCase() === initialValue.toLowerCase()
72
+ item.type === undefined &&
73
+ item.title?.toString().toLowerCase() === initialTitle.toLowerCase()
78
74
  );
79
75
 
80
76
  // Get filtered items to check for fuzzy matches
@@ -89,10 +85,20 @@ export class ComboboxState {
89
85
  {
90
86
  filter: "",
91
87
  selectedIndex: 0,
92
- filteredItems: items.map((item, index) => {
93
- if (item.type === "sectionHeader") return item;
94
- return { ...item, index, score: 0 };
95
- }),
88
+
89
+ filteredItems: this.items.flatMap((item): ScoredMenuItem<T>[] =>
90
+ isSelectableMenuItem(item)
91
+ ? [
92
+ {
93
+ ...item,
94
+ index: 0,
95
+ score: 0,
96
+ },
97
+ ]
98
+ : item.type === "sectionHeader"
99
+ ? [item]
100
+ : []
101
+ ),
96
102
  lastSubmittedValue: this.lastSubmittedValue,
97
103
  },
98
104
  ];
@@ -113,11 +119,11 @@ export class ComboboxState {
113
119
  this.subscribers.forEach((listener) => listener());
114
120
  }
115
121
 
116
- private getFilteredItems(): InternalComboboxItem[] {
122
+ private getFilteredItems(): ScoredMenuItem<T>[] {
117
123
  return filterWithGroupedSections(this.items, this.filter);
118
124
  }
119
125
 
120
- getSnapshot = (): ComboboxStateSnapshot => {
126
+ getSnapshot = (): ComboboxStateSnapshot<T> => {
121
127
  if (this._cachedSnapshot && this._cachedSnapshot[0] === this.version) {
122
128
  return this._cachedSnapshot[1];
123
129
  }
@@ -133,7 +139,7 @@ export class ComboboxState {
133
139
  return snapshot;
134
140
  };
135
141
 
136
- setItems(items: ComboboxItem[]) {
142
+ setItems(items: MenuItem<T>[]) {
137
143
  this.items = items;
138
144
  this.notifyChange();
139
145
  }
@@ -145,13 +151,13 @@ export class ComboboxState {
145
151
  // Find exact match if it exists (case insensitive)
146
152
  const exactMatchIndex = filteredItems.findIndex(
147
153
  (item) =>
148
- item.type !== "sectionHeader" &&
149
- item.name.toLowerCase().startsWith(filter.toLowerCase())
154
+ isSelectableMenuItem(item) &&
155
+ item.title?.toString().toLowerCase().startsWith(filter.toLowerCase())
150
156
  );
151
157
 
152
158
  // Find the first non-header item
153
- const firstSelectableIndex = filteredItems.findIndex(
154
- (item) => item.type !== "sectionHeader"
159
+ const firstSelectableIndex = filteredItems.findIndex((item): boolean =>
160
+ isSelectableMenuItem(item)
155
161
  );
156
162
 
157
163
  this.selectedIndex =
@@ -166,29 +172,28 @@ export class ComboboxState {
166
172
  filteredItems,
167
173
  this.selectedIndex,
168
174
  direction === "down" ? "next" : "previous",
169
- (item) => item.type === "sectionHeader"
175
+ (item) => isNonSelectableMenuItem(item)
170
176
  );
171
177
  this.notifyChange();
172
178
  }
173
179
 
174
180
  selectCurrentItem(
175
- item?: InternalComboboxItem
176
- ): InternalComboboxItem | undefined {
181
+ item?: ScoredMenuItem<T>
182
+ ): SelectableMenuItem<T> | undefined {
177
183
  const itemToSelect = item || this.getFilteredItems()[this.selectedIndex];
178
- if (!itemToSelect || itemToSelect.type === "sectionHeader") {
184
+ if (itemToSelect && isSelectableMenuItem(itemToSelect)) {
185
+ this.lastSubmittedValue = {
186
+ filter: itemToSelect.title!.toString(),
187
+ itemName: itemToSelect.title!.toString(),
188
+ };
189
+ this.filter = itemToSelect.title!.toString();
190
+ this.notifyChange();
191
+ return itemToSelect;
192
+ } else {
179
193
  // If no valid item to select and arbitrary values aren't allowed,
180
194
  // restore the last valid value
181
195
  this.restoreLastSubmittedValue();
182
- return;
183
196
  }
184
-
185
- this.lastSubmittedValue = {
186
- filter: itemToSelect.name,
187
- itemName: itemToSelect.name,
188
- };
189
- this.filter = itemToSelect.name;
190
- this.notifyChange();
191
- return itemToSelect;
192
197
  }
193
198
 
194
199
  restoreLastSubmittedValue() {
@@ -202,25 +207,25 @@ export class ComboboxState {
202
207
  this.notifyChange();
203
208
  }
204
209
 
205
- getSelectedItem(): InternalComboboxItem | undefined {
210
+ getSelectedItem(): ScoredMenuItem<T> | undefined {
206
211
  const filteredItems = this.getFilteredItems();
207
212
  return filteredItems[this.selectedIndex];
208
213
  }
209
214
 
210
215
  getTypeaheadValue(): string | undefined {
211
216
  const selectedItem = this.getSelectedItem();
212
- if (!selectedItem || selectedItem.type === "sectionHeader") return;
213
- return selectedItem.name;
217
+ if (selectedItem && isSelectableMenuItem(selectedItem))
218
+ return selectedItem.title!.toString();
214
219
  }
215
220
 
216
221
  setSelectedIndex(index: number) {
217
222
  const filteredItems = this.getFilteredItems();
218
223
 
219
- // If trying to select a header, find the next selectable item
220
- if (filteredItems[index]?.type === "sectionHeader") {
221
- // Look for the next non-header item
224
+ // If trying to select a header or separator, find the next selectable item
225
+ if (isNonSelectableMenuItem(filteredItems[index])) {
226
+ // Look for the next selectable item
222
227
  for (let i = index + 1; i < filteredItems.length; i++) {
223
- if (filteredItems[i]?.type !== "sectionHeader") {
228
+ if (isSelectableMenuItem(filteredItems[i])) {
224
229
  this.selectedIndex = i;
225
230
  this.notifyChange();
226
231
  return;
@@ -228,7 +233,7 @@ export class ComboboxState {
228
233
  }
229
234
  // If no next item, look backwards
230
235
  for (let i = index - 1; i >= 0; i--) {
231
- if (filteredItems[i]?.type !== "sectionHeader") {
236
+ if (isSelectableMenuItem(filteredItems[i])) {
232
237
  this.selectedIndex = i;
233
238
  this.notifyChange();
234
239
  return;
@@ -252,18 +257,18 @@ export class ComboboxState {
252
257
  }
253
258
  }
254
259
 
255
- function filterWithGroupedSections(
256
- items: ComboboxItem[],
260
+ function filterWithGroupedSections<T extends string>(
261
+ items: MenuItem<T>[],
257
262
  query: string
258
- ): InternalComboboxItem[] {
259
- const sections = chunkBy(items, (_a, b) => b.type !== "sectionHeader");
260
- let result: InternalComboboxItem[] = [];
263
+ ): ScoredMenuItem<T>[] {
264
+ const sections = chunkBy(items, (_a, b) => isSelectableMenuItem(b));
265
+ let result: ScoredMenuItem<T>[] = [];
261
266
  let currentIndex = 0;
262
267
 
263
268
  const createExtraItem = (
264
- item: ComboboxOption,
269
+ item: SelectableMenuItem<T>,
265
270
  index: number
266
- ): InternalComboboxItem => ({
271
+ ): ScoredMenuItem<T> => ({
267
272
  ...item,
268
273
  index: currentIndex + index,
269
274
  score: 0,
@@ -271,62 +276,72 @@ function filterWithGroupedSections(
271
276
 
272
277
  const createScoredItem = (
273
278
  item: IScoredItem,
274
- regular: ComboboxOption[]
275
- ): InternalComboboxItem => ({
276
- ...item,
277
- ...regular[item.index],
278
- index: currentIndex + item.index,
279
- });
279
+ selectableItems: Array<SelectableMenuItem<T>>
280
+ ): ScoredMenuItem<T> => {
281
+ const menuItem = selectableItems[item.index];
282
+ return {
283
+ ...item,
284
+ ...menuItem,
285
+ index: currentIndex + item.index,
286
+ };
287
+ };
280
288
 
281
289
  for (const section of sections) {
282
- const [headers, regular] = partition(
283
- section,
284
- (item): item is ComboboxSectionHeader => item.type === "sectionHeader"
290
+ const [sectionHeaders, selectableItems] = partition(
291
+ section.filter(
292
+ (item): item is SelectableMenuItem<T> | SectionHeaderMenuItem =>
293
+ item.type !== "submenu" && item.type !== "separator"
294
+ ),
295
+ (item): item is SectionHeaderMenuItem => isMenuItemSectionHeader(item)
285
296
  );
286
297
 
287
- let newItems: InternalComboboxItem[];
298
+ let newItems: ScoredMenuItem<T>[];
288
299
 
289
300
  if (!query.trim()) {
290
301
  // For empty query, include all items with score 0
291
- newItems = regular.map((item, index) => createExtraItem(item, index));
302
+ newItems = selectableItems.map((item, index) =>
303
+ createExtraItem(item, index)
304
+ );
292
305
  } else {
293
306
  const scoredItems = fuzzyFilter({
294
- items: regular.map((item) => item.name),
307
+ items: selectableItems.map((item) => item.title.toString()),
295
308
  query,
296
309
  });
297
310
 
298
311
  const usedIndexes = new Set(scoredItems.map((item) => item.index));
299
- const extraItems = regular.flatMap(
300
- (item, index): InternalComboboxItem[] =>
301
- item.alwaysInclude && !usedIndexes.has(index)
312
+ const extraItems = selectableItems.flatMap(
313
+ (item, index): ScoredMenuItem<T>[] =>
314
+ item?.alwaysInclude && !usedIndexes.has(index)
302
315
  ? [createExtraItem(item, index)]
303
316
  : []
304
317
  );
305
318
 
306
319
  newItems = scoredItems
307
- .map((item) => createScoredItem(item, regular))
320
+ .map((item) => createScoredItem(item, selectableItems))
308
321
  .concat(extraItems);
309
322
  }
310
323
 
311
324
  // If we have items or this is an empty query
312
325
  if (newItems.length > 0 || !query.trim()) {
313
326
  const maxVisibleItems =
314
- headers.length > 0 ? headers[0].maxVisibleItems : undefined;
327
+ sectionHeaders.length > 0 && sectionHeaders[0]?.maxVisibleItems
328
+ ? sectionHeaders[0]?.maxVisibleItems
329
+ : undefined;
315
330
 
316
331
  if (maxVisibleItems !== undefined) {
317
332
  newItems = newItems.slice(0, maxVisibleItems);
318
333
  }
319
334
 
320
- result.push(...headers, ...newItems);
335
+ result.push(...sectionHeaders, ...newItems);
321
336
  }
322
337
 
323
- currentIndex += regular.length;
338
+ currentIndex += selectableItems.length;
324
339
  }
325
340
 
326
341
  return result;
327
342
  }
328
343
 
329
- function getNextIndex<T>(
344
+ export function getNextIndex<T>(
330
345
  items: T[],
331
346
  currentIndex: number,
332
347
  direction: "next" | "previous",
@@ -360,7 +375,7 @@ function getNextIndex<T>(
360
375
  return nextIndex;
361
376
  }
362
377
 
363
- export function useComboboxState(state: ComboboxState) {
378
+ export function useComboboxState<T extends string>(state: ComboboxState<T>) {
364
379
  return useSyncExternalStore(
365
380
  state.subscribe,
366
381
  state.getSnapshot,
@@ -1,16 +1,15 @@
1
- import {
2
- MenuItem,
3
- RegularMenuItem,
4
- SEPARATOR_ITEM,
5
- } from '../components/internal/Menu';
1
+ import { MenuItem, SeparatorItem } from "../components/internal/Menu";
6
2
 
7
3
  export type Optional<T> = T | false | null | undefined;
8
4
 
9
- function withSeparators<T>(elements: T[], separator: T) {
10
- const result: T[] = [];
5
+ function withSeparators<T extends string>(
6
+ elements: MenuItem<T>[][],
7
+ separator: SeparatorItem
8
+ ): MenuItem<T>[] {
9
+ const result: MenuItem<T>[] = [];
11
10
 
12
11
  for (let i = 0; i < elements.length; i++) {
13
- result.push(elements[i]);
12
+ result.push(...elements[i]);
14
13
 
15
14
  if (i !== elements.length - 1) {
16
15
  result.push(separator);
@@ -20,9 +19,7 @@ function withSeparators<T>(elements: T[], separator: T) {
20
19
  return result;
21
20
  }
22
21
 
23
- export type MenuConfig<T extends string> = Optional<
24
- Optional<RegularMenuItem<T>>[]
25
- >[];
22
+ export type MenuConfig<T extends string> = Optional<Optional<MenuItem<T>>[]>[];
26
23
 
27
24
  export function createSectionedMenu<T extends string>(
28
25
  ...sections: MenuConfig<T>
@@ -32,7 +29,7 @@ export function createSectionedMenu<T extends string>(
32
29
  .map((section) => section.flatMap((item) => (item ? [item] : [])))
33
30
  .filter((section) => section.length > 0);
34
31
 
35
- return withSeparators<MenuItem<T>[]>(nonEmptySections, [
36
- SEPARATOR_ITEM,
37
- ]).flat();
32
+ return withSeparators(nonEmptySections, {
33
+ type: "separator",
34
+ }).flat();
38
35
  }
@@ -1,4 +1,4 @@
1
- import { IItemScore, scoreFilePathFuzzy } from 'vscode-fuzzy-scorer';
1
+ import { IItemScore, scoreFilePathFuzzy } from "vscode-fuzzy-scorer";
2
2
 
3
3
  export function fuzzyScore({ item, query }: { item: string; query: string }) {
4
4
  return scoreFilePathFuzzy({ path: item, query });
@@ -21,7 +21,7 @@ export function fuzzyFilter({
21
21
  (text, index): IScoredItem => ({
22
22
  index,
23
23
  ...fuzzyScore({ item: text, query }),
24
- }),
24
+ })
25
25
  );
26
26
 
27
27
  if (!query) return scoredItems;
@@ -32,8 +32,8 @@ export function fuzzyFilter({
32
32
  }
33
33
 
34
34
  export type IToken =
35
- | { type: 'text'; text: string }
36
- | { type: 'match'; text: string };
35
+ | { type: "text"; text: string }
36
+ | { type: "match"; text: string };
37
37
 
38
38
  export type MatchRange = { start: number; end: number };
39
39
 
@@ -44,8 +44,11 @@ export function fuzzyTokenize({
44
44
  item: string;
45
45
  itemScore: IItemScore;
46
46
  }): IToken[] {
47
- const tokens: IToken[] = [];
47
+ if (typeof item !== "string") {
48
+ return [{ type: "text", text: String(item) }];
49
+ }
48
50
 
51
+ const tokens: IToken[] = [];
49
52
  let lastMatchIndex = 0;
50
53
 
51
54
  const matches: MatchRange[] = mergeRanges([
@@ -56,13 +59,13 @@ export function fuzzyTokenize({
56
59
  for (const match of matches) {
57
60
  if (match.start > lastMatchIndex) {
58
61
  tokens.push({
59
- type: 'text',
62
+ type: "text",
60
63
  text: item.slice(lastMatchIndex, match.start),
61
64
  });
62
65
  }
63
66
 
64
67
  tokens.push({
65
- type: 'match',
68
+ type: "match",
66
69
  text: item.slice(match.start, match.end),
67
70
  });
68
71
 
@@ -71,7 +74,7 @@ export function fuzzyTokenize({
71
74
 
72
75
  if (lastMatchIndex < item.length) {
73
76
  tokens.push({
74
- type: 'text',
77
+ type: "text",
75
78
  text: item.slice(lastMatchIndex),
76
79
  });
77
80
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Updates the selection state based on user interaction.
3
+ * Handles single selection, multi-selection with meta/ctrl, and range selection with shift.
4
+ */
5
+ export function updateSelection(
6
+ allIds: string[],
7
+ selectedIds: string[],
8
+ clickedId: string,
9
+ event?: { shiftKey?: boolean; metaKey?: boolean; ctrlKey?: boolean }
10
+ ): string[] {
11
+ const metaOrCtrl = event?.metaKey || event?.ctrlKey;
12
+
13
+ if (!event || (!event.shiftKey && !metaOrCtrl)) {
14
+ // Store this as the anchor point for future shift selections
15
+ return [clickedId];
16
+ }
17
+
18
+ if (metaOrCtrl) {
19
+ const isSelected = selectedIds.includes(clickedId);
20
+ return isSelected
21
+ ? selectedIds.filter((id) => id !== clickedId)
22
+ : [...selectedIds, clickedId];
23
+ }
24
+
25
+ if (event.shiftKey && selectedIds.length > 0) {
26
+ // Find the anchor based on which end of the current selection is closer to the clicked item
27
+ const firstSelectedIndex = allIds.indexOf(selectedIds[0]);
28
+ const lastSelectedIndex = allIds.indexOf(
29
+ selectedIds[selectedIds.length - 1]
30
+ );
31
+ const clickedIndex = allIds.indexOf(clickedId);
32
+
33
+ // Use the endpoint of the current selection that's furthest from the clicked item as the anchor
34
+ const anchorId =
35
+ Math.abs(clickedIndex - firstSelectedIndex) >
36
+ Math.abs(clickedIndex - lastSelectedIndex)
37
+ ? selectedIds[0]
38
+ : selectedIds[selectedIds.length - 1];
39
+
40
+ const anchorIndex = allIds.indexOf(anchorId);
41
+ const start = Math.min(anchorIndex, clickedIndex);
42
+ const end = Math.max(anchorIndex, clickedIndex);
43
+
44
+ return allIds.slice(start, end + 1);
45
+ }
46
+
47
+ return [clickedId];
48
+ }