@boxcustodia/library 2.0.0-alpha.22 → 2.0.0-alpha.24

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 (110) hide show
  1. package/dist/components/calendar/calendar.cjs.js +1 -1
  2. package/dist/components/calendar/calendar.es.js +43 -44
  3. package/dist/components/date-picker/date-input.cjs.js +1 -1
  4. package/dist/components/date-picker/date-input.es.js +160 -140
  5. package/dist/components/pagination/pagination.cjs.js +1 -1
  6. package/dist/components/pagination/pagination.es.js +37 -35
  7. package/dist/components/popover/popover.cjs.js +1 -1
  8. package/dist/components/popover/popover.es.js +1 -1
  9. package/dist/components/scroll-area/scroll-area.cjs.js +1 -1
  10. package/dist/components/scroll-area/scroll-area.es.js +4 -4
  11. package/dist/components/select/select.cjs.js +1 -1
  12. package/dist/components/select/select.es.js +94 -90
  13. package/dist/components/tag/tag.cjs.js +1 -1
  14. package/dist/components/tag/tag.es.js +37 -18
  15. package/dist/hooks/use-action/use-action.cjs.js +1 -0
  16. package/dist/hooks/use-action/use-action.es.js +41 -0
  17. package/dist/hooks/use-pagination/use-pagination.cjs.js +1 -1
  18. package/dist/hooks/use-pagination/use-pagination.es.js +77 -32
  19. package/dist/hooks/use-range-pagination/use-range-pagination.cjs.js +1 -1
  20. package/dist/hooks/use-range-pagination/use-range-pagination.es.js +8 -5
  21. package/dist/hooks/use-selection/use-selection.cjs.js +1 -1
  22. package/dist/hooks/use-selection/use-selection.es.js +95 -33
  23. package/dist/hooks/use-session-storage/use-session-storage.cjs.js +1 -0
  24. package/dist/hooks/use-session-storage/use-session-storage.es.js +57 -0
  25. package/dist/index.cjs.js +1 -1
  26. package/dist/index.es.js +61 -63
  27. package/dist/src/components/select/select.d.ts +9 -2
  28. package/dist/src/components/tag/tag.d.ts +2 -1
  29. package/dist/src/hooks/index.d.ts +2 -3
  30. package/dist/src/hooks/internal/index.d.ts +1 -0
  31. package/dist/src/hooks/internal/serializer.d.ts +4 -0
  32. package/dist/src/hooks/use-action/index.d.ts +1 -0
  33. package/dist/src/hooks/use-action/use-action.d.ts +22 -0
  34. package/dist/src/hooks/use-local-storage/use-local-storage.d.ts +2 -4
  35. package/dist/src/hooks/use-pagination/use-pagination.d.ts +47 -32
  36. package/dist/src/hooks/use-range-pagination/use-range-pagination.d.ts +16 -10
  37. package/dist/src/hooks/use-selection/use-selection.d.ts +39 -45
  38. package/dist/src/hooks/use-session-storage/index.d.ts +1 -0
  39. package/dist/src/hooks/use-session-storage/use-session-storage.d.ts +11 -0
  40. package/package.json +1 -1
  41. package/src/components/calendar/calendar.tsx +10 -8
  42. package/src/components/combobox/combobox.stories.tsx +16 -0
  43. package/src/components/date-picker/date-input.tsx +23 -2
  44. package/src/components/form/form.tsx +3 -2
  45. package/src/components/pagination/pagination.tsx +5 -3
  46. package/src/components/popover/popover.tsx +1 -1
  47. package/src/components/scroll-area/scroll-area.tsx +2 -2
  48. package/src/components/select/select.tsx +14 -3
  49. package/src/components/tag/tag.stories.tsx +47 -2
  50. package/src/components/tag/tag.tsx +28 -6
  51. package/src/hooks/index.ts +2 -3
  52. package/src/hooks/internal/index.ts +1 -0
  53. package/src/hooks/internal/serializer.ts +4 -0
  54. package/src/hooks/use-action/index.ts +1 -0
  55. package/src/hooks/{use-mutation/use-mutation.stories.tsx → use-action/use-action.stories.tsx} +34 -34
  56. package/src/hooks/{use-mutation/use-mutation.test.ts → use-action/use-action.test.ts} +53 -53
  57. package/src/hooks/{use-mutation/use-mutation.ts → use-action/use-action.ts} +20 -20
  58. package/src/hooks/use-click-outside/use-click-outside.stories.tsx +0 -1
  59. package/src/hooks/use-clipboard/use-clipboard.stories.tsx +0 -1
  60. package/src/hooks/use-document-title/use-document-title.stories.tsx +0 -1
  61. package/src/hooks/use-is-visible/use-is-visible.test.tsx +1 -1
  62. package/src/hooks/use-local-storage/use-local-storage.stories.tsx +0 -1
  63. package/src/hooks/use-local-storage/use-local-storage.ts +2 -5
  64. package/src/hooks/use-media-query/use-media-query.stories.tsx +0 -1
  65. package/src/hooks/use-pagination/use-pagination.stories.tsx +720 -57
  66. package/src/hooks/use-pagination/use-pagination.test.tsx +560 -48
  67. package/src/hooks/use-pagination/use-pagination.ts +266 -0
  68. package/src/hooks/use-prevent-page-close/use-prevent-page-close.stories.tsx +0 -1
  69. package/src/hooks/use-range-pagination/use-range-pagination.test.tsx +2 -2
  70. package/src/hooks/use-range-pagination/use-range-pagination.tsx +24 -21
  71. package/src/hooks/use-selection/use-selection.stories.tsx +339 -84
  72. package/src/hooks/use-selection/use-selection.test.tsx +417 -2
  73. package/src/hooks/use-selection/use-selection.ts +212 -102
  74. package/src/hooks/use-session-storage/index.ts +1 -0
  75. package/src/hooks/use-session-storage/use-session-storage.stories.tsx +122 -0
  76. package/src/hooks/use-session-storage/use-session-storage.test.ts +164 -0
  77. package/src/hooks/use-session-storage/use-session-storage.ts +115 -0
  78. package/dist/hooks/use-async/use-async.cjs.js +0 -1
  79. package/dist/hooks/use-async/use-async.es.js +0 -57
  80. package/dist/hooks/use-focus-trap/scope-tab.cjs.js +0 -1
  81. package/dist/hooks/use-focus-trap/scope-tab.es.js +0 -21
  82. package/dist/hooks/use-focus-trap/tabbable.cjs.js +0 -1
  83. package/dist/hooks/use-focus-trap/tabbable.es.js +0 -38
  84. package/dist/hooks/use-focus-trap/use-focus-trap.cjs.js +0 -1
  85. package/dist/hooks/use-focus-trap/use-focus-trap.es.js +0 -34
  86. package/dist/hooks/use-mutation/use-mutation.cjs.js +0 -1
  87. package/dist/hooks/use-mutation/use-mutation.es.js +0 -41
  88. package/dist/src/hooks/use-async/index.d.ts +0 -1
  89. package/dist/src/hooks/use-async/use-async.d.ts +0 -21
  90. package/dist/src/hooks/use-focus-trap/index.d.ts +0 -1
  91. package/dist/src/hooks/use-focus-trap/scope-tab.d.ts +0 -1
  92. package/dist/src/hooks/use-focus-trap/tabbable.d.ts +0 -4
  93. package/dist/src/hooks/use-focus-trap/use-focus-trap.d.ts +0 -1
  94. package/dist/src/hooks/use-mutation/index.d.ts +0 -1
  95. package/dist/src/hooks/use-mutation/use-mutation.d.ts +0 -22
  96. package/dist/src/hooks/use-mutation/use-mutation.test.d.ts +0 -1
  97. package/src/hooks/use-async/index.ts +0 -1
  98. package/src/hooks/use-async/use-async.stories.tsx +0 -272
  99. package/src/hooks/use-async/use-async.test.ts +0 -397
  100. package/src/hooks/use-async/use-async.ts +0 -135
  101. package/src/hooks/use-focus-trap/index.ts +0 -1
  102. package/src/hooks/use-focus-trap/scope-tab.ts +0 -38
  103. package/src/hooks/use-focus-trap/tabbable.ts +0 -70
  104. package/src/hooks/use-focus-trap/use-focus-trap.stories.tsx +0 -37
  105. package/src/hooks/use-focus-trap/use-focus-trap.test.ts +0 -355
  106. package/src/hooks/use-focus-trap/use-focus-trap.ts +0 -78
  107. package/src/hooks/use-mutation/index.ts +0 -1
  108. package/src/hooks/use-pagination/use-pagination.tsx +0 -84
  109. /package/dist/src/hooks/{use-async/use-async.test.d.ts → use-action/use-action.test.d.ts} +0 -0
  110. /package/dist/src/hooks/{use-focus-trap/use-focus-trap.test.d.ts → use-session-storage/use-session-storage.test.d.ts} +0 -0
@@ -1,14 +1,13 @@
1
1
  import { Meta, StoryObj } from "@storybook/react-vite";
2
- import { Trash } from "lucide-react";
3
- import { useMemo, useState } from "react";
2
+ import { useState } from "react";
4
3
  import { Button, Checkbox } from "../../components";
5
4
  import { cn } from "../../lib";
6
5
  import { useSelection } from "../use-selection";
7
6
 
8
7
  /**
9
- * Gestión de la selección de elementos en una lista.
10
- * Optimizado para un rendimiento eficiente, utiliza un conjunto (Set) interno para almacenar los elementos seleccionados, permitiendo consultas rápidas para verificar si un elemento está seleccionado.
11
- * Proporciona funciones simples para seleccionar, deseleccionar, alternar la selección de un elemento, seleccionar todos los elementos, limpiar la selección y obtener el estado actual de la selección
8
+ * `useSelection` manages item selection in a list. v2 adds key-based identity,
9
+ * `defaultSelected`, `onChange`, `selectedKeys`, `selectAll`, `invert`, and
10
+ * `selectRange` while keeping all v1 actions stable across renders.
12
11
  */
13
12
  const meta: Meta = {
14
13
  title: "hooks/useSelection",
@@ -18,122 +17,378 @@ const meta: Meta = {
18
17
  export default meta;
19
18
  type Story = StoryObj<typeof meta>;
20
19
 
20
+ // ─── Default ──────────────────────────────────────────────────────────────────
21
+
22
+ /**
23
+ * Basic string list showcasing select/toggle/clear/toggleAll/selectAll.
24
+ * All flags (`isAllSelected`, `isSomeSelected`, `isNoneSelected`) are shown live.
25
+ */
26
+
27
+ const fruits = ["Apple", "Banana", "Cherry", "Dragonfruit", "Elderberry"];
21
28
  export const Default: Story = {
22
29
  render: () => {
23
- const items = useMemo(() => ["🍆", "🍅", "🥑", "🥬"], []);
24
30
  const {
25
31
  selected,
32
+ isSelected,
26
33
  toggle,
27
- toggleAll,
28
34
  clear,
29
- isSelected,
35
+ toggleAll,
36
+ selectAll,
30
37
  isAllSelected,
38
+ isSomeSelected,
31
39
  isNoneSelected,
32
- setSelected,
33
- } = useSelection(items);
40
+ } = useSelection(fruits);
34
41
 
35
42
  return (
36
- <div className="space-y-4">
37
- <div className="flex gap-2">
43
+ <div className="space-y-4 p-4">
44
+ <div className="space-y-1">
45
+ {fruits.map((fruit) => (
46
+ <div key={fruit} className="flex items-center gap-2">
47
+ <Checkbox
48
+ name={fruit}
49
+ checked={isSelected(fruit)}
50
+ onCheckedChange={() => toggle(fruit)}
51
+ />
52
+ <span className="text-sm">{fruit}</span>
53
+ </div>
54
+ ))}
55
+ </div>
56
+
57
+ <div className="flex flex-wrap gap-2">
58
+ <Button size="sm" variant="outline" onClick={toggleAll}>
59
+ Toggle all
60
+ </Button>
61
+ <Button size="sm" variant="outline" onClick={selectAll}>
62
+ Select all
63
+ </Button>
64
+ <Button size="sm" variant="outline" onClick={clear}>
65
+ Clear
66
+ </Button>
67
+ </div>
68
+
69
+ <pre className="rounded-md bg-slate-950 p-4 text-xs text-white">
70
+ <code>
71
+ {JSON.stringify(
72
+ { selected, isAllSelected, isSomeSelected, isNoneSelected },
73
+ null,
74
+ 2,
75
+ )}
76
+ </code>
77
+ </pre>
78
+ </div>
79
+ );
80
+ },
81
+ };
82
+
83
+ // ─── WithKeyFn ────────────────────────────────────────────────────────────────
84
+
85
+ /**
86
+ * Demonstrates key-based identity. Clicking "Refetch" replaces the items array
87
+ * with structurally-equal but referentially-new objects. With `keyFn`, selection
88
+ * survives the re-creation; without it, selection would be silently lost.
89
+ */
90
+ export const WithKeyFn: Story = {
91
+ render: () => {
92
+ const [items, setItems] = useState([
93
+ { id: 1, name: "Alice" },
94
+ { id: 2, name: "Bob" },
95
+ { id: 3, name: "Carol" },
96
+ ]);
97
+
98
+ const { isSelected, toggle, selected, selectedKeys } = useSelection(items, {
99
+ keyFn: (item) => item.id,
100
+ });
101
+
102
+ const handleRefetch = () => {
103
+ // Simulate a re-fetch: same data, brand-new object references
104
+ setItems((prev) => prev.map((item) => ({ ...item })));
105
+ };
106
+
107
+ return (
108
+ <div className="space-y-4 p-4">
109
+ <p className="text-sm text-slate-500">
110
+ Click "Refetch" to replace the array with new object references.
111
+ Selection persists because <code>keyFn</code> uses stable IDs.
112
+ </p>
113
+
114
+ <div className="space-y-1">
38
115
  {items.map((item) => (
116
+ <div key={item.id} className="flex items-center gap-2">
117
+ <Checkbox
118
+ name={String(item.id)}
119
+ checked={isSelected(item)}
120
+ onCheckedChange={() => toggle(item)}
121
+ />
122
+ <span className="text-sm">{item.name}</span>
123
+ </div>
124
+ ))}
125
+ </div>
126
+
127
+ <Button size="sm" variant="outline" onClick={handleRefetch}>
128
+ Refetch (re-create object references)
129
+ </Button>
130
+
131
+ <pre className="rounded-md bg-slate-950 p-4 text-xs text-white">
132
+ <code>
133
+ {JSON.stringify(
134
+ {
135
+ selected: selected.map((i) => i.name),
136
+ selectedKeys: [...selectedKeys],
137
+ },
138
+ null,
139
+ 2,
140
+ )}
141
+ </code>
142
+ </pre>
143
+ </div>
144
+ );
145
+ },
146
+ };
147
+
148
+ // ─── SelectRange ──────────────────────────────────────────────────────────────
149
+
150
+ /**
151
+ * Shift-click range selection. Click a row to set the anchor, then
152
+ * Shift+click another row to select the inclusive range between them.
153
+ */
154
+ export const SelectRange: Story = {
155
+ render: () => {
156
+ const items = [
157
+ "Document 1",
158
+ "Document 2",
159
+ "Document 3",
160
+ "Document 4",
161
+ "Document 5",
162
+ "Document 6",
163
+ ];
164
+
165
+ const [anchor, setAnchor] = useState<number | null>(null);
166
+ const { isSelected, toggle, selectRange, selected, clear } =
167
+ useSelection(items);
168
+
169
+ const handleClick = (index: number, shiftKey: boolean) => {
170
+ if (shiftKey && anchor !== null) {
171
+ selectRange(anchor, index);
172
+ } else {
173
+ toggle(items[index]);
174
+ setAnchor(index);
175
+ }
176
+ };
177
+
178
+ return (
179
+ <div className="space-y-4 p-4">
180
+ <p className="text-sm text-slate-500">
181
+ Click to toggle. Hold Shift and click to select a range from the last
182
+ clicked row.
183
+ </p>
184
+
185
+ <div className="rounded-md border">
186
+ {items.map((item, index) => (
39
187
  <div
40
188
  key={item}
41
- onClick={() => toggle(item)}
42
189
  className={cn(
43
- "rounded transition-colors w-32 h-32 shadow text-3xl grid place-items-center cursor-pointer",
44
- { "bg-accent": isSelected(item) },
190
+ "flex cursor-pointer select-none items-center gap-3 border-b px-3 py-2 text-sm last:border-b-0",
191
+ isSelected(item) ? "bg-blue-50" : "hover:bg-slate-50",
45
192
  )}
193
+ onClick={(e) => handleClick(index, e.shiftKey)}
46
194
  >
47
- {item}
195
+ <Checkbox
196
+ name={item}
197
+ checked={isSelected(item)}
198
+ onCheckedChange={() => handleClick(index, false)}
199
+ />
200
+ <span>{item}</span>
201
+ {anchor === index && (
202
+ <span className="ml-auto text-xs text-slate-400">anchor</span>
203
+ )}
48
204
  </div>
49
205
  ))}
50
206
  </div>
51
207
 
52
- <div className="flex gap-4 my-2">
53
- <Button onClick={clear}>Limpiar</Button>
54
- <Button onClick={toggleAll}>Toggle all</Button>
55
- <Button onClick={() => setSelected(["🍅"])}>set 🍅</Button>
208
+ <div className="flex gap-2">
209
+ <Button size="sm" variant="outline" onClick={clear}>
210
+ Clear
211
+ </Button>
56
212
  </div>
57
213
 
58
- <pre className="p-4 text-white rounded-md bg-slate-950">
59
- <code className="block">
60
- Selected:{" "}
61
- <span className="text-blue-300">
62
- {JSON.stringify(selected, null, 2)}
63
- </span>
64
- </code>
65
- <code className="block">
66
- isAllSelected:{" "}
67
- <span className={isAllSelected ? "text-green-500" : "text-red-500"}>
68
- {JSON.stringify(isAllSelected)}
69
- </span>
70
- </code>
71
- <code className="block">
72
- isNoneSelected:{" "}
73
- <span
74
- className={isNoneSelected ? "text-green-500" : "text-red-500"}
214
+ <pre className="rounded-md bg-slate-950 p-4 text-xs text-white">
215
+ <code>{JSON.stringify({ selected }, null, 2)}</code>
216
+ </pre>
217
+ </div>
218
+ );
219
+ },
220
+ };
221
+
222
+ // ─── Invert ───────────────────────────────────────────────────────────────────
223
+
224
+ /**
225
+ * Shows `invert()` which flips the current selection relative to `items`.
226
+ * None → All, All → None, Partial → complement.
227
+ */
228
+ export const Invert: Story = {
229
+ render: () => {
230
+ const tags = ["React", "TypeScript", "Tailwind", "Vite", "Biome", "Vitest"];
231
+ const { isSelected, toggle, invert, selectAll, clear, selected } =
232
+ useSelection(tags);
233
+
234
+ return (
235
+ <div className="space-y-4 p-4">
236
+ <div className="flex flex-wrap gap-2">
237
+ {tags.map((tag) => (
238
+ <button
239
+ key={tag}
240
+ type="button"
241
+ onClick={() => toggle(tag)}
242
+ className={cn(
243
+ "rounded-full border px-3 py-1 text-sm transition-colors",
244
+ isSelected(tag)
245
+ ? "border-blue-500 bg-blue-500 text-white"
246
+ : "border-slate-300 bg-white text-slate-700 hover:border-slate-400",
247
+ )}
75
248
  >
76
- {JSON.stringify(isNoneSelected)}
77
- </span>
78
- </code>
249
+ {tag}
250
+ </button>
251
+ ))}
252
+ </div>
253
+
254
+ <div className="flex flex-wrap gap-2">
255
+ <Button size="sm" variant="outline" onClick={invert}>
256
+ Invert selection
257
+ </Button>
258
+ <Button size="sm" variant="outline" onClick={selectAll}>
259
+ Select all
260
+ </Button>
261
+ <Button size="sm" variant="outline" onClick={clear}>
262
+ Clear
263
+ </Button>
264
+ </div>
265
+
266
+ <pre className="rounded-md bg-slate-950 p-4 text-xs text-white">
267
+ <code>{JSON.stringify({ selected }, null, 2)}</code>
79
268
  </pre>
80
269
  </div>
81
270
  );
82
271
  },
83
272
  };
84
273
 
85
- export const DynamicArray: Story = {
86
- name: "Array dinamico",
274
+ // ─── DefaultSelected ──────────────────────────────────────────────────────────
275
+
276
+ /**
277
+ * Pre-initializes selection via `defaultSelected`. The hook seeds the
278
+ * selection once on mount and ignores further changes to the option.
279
+ */
280
+ export const DefaultSelected: Story = {
87
281
  render: () => {
88
- // state o useMemo
89
- const [items, setItems] = useState([
90
- { id: 1, name: "Item 1" },
91
- { id: 2, name: "Item 2" },
92
- { id: 3, name: "Item 3" },
93
- ]);
282
+ const items = ["Option A", "Option B", "Option C", "Option D"];
283
+ const { isSelected, toggle, selected } = useSelection(items, {
284
+ defaultSelected: ["Option B", "Option D"],
285
+ });
94
286
 
95
- const { selected, toggle, isSelected, unselect } = useSelection(items);
287
+ return (
288
+ <div className="space-y-4 p-4">
289
+ <p className="text-sm text-slate-500">
290
+ Options B and D are pre-selected via <code>defaultSelected</code>.
291
+ </p>
96
292
 
97
- const addNewItem = () => {
98
- const id = new Date().getTime();
99
- setItems((prevItems) => [...prevItems, { id, name: `Item ${id}` }]);
100
- };
293
+ <div className="space-y-1">
294
+ {items.map((item) => (
295
+ <div key={item} className="flex items-center gap-2">
296
+ <Checkbox
297
+ name={item}
298
+ checked={isSelected(item)}
299
+ onCheckedChange={() => toggle(item)}
300
+ />
301
+ <span className="text-sm">{item}</span>
302
+ </div>
303
+ ))}
304
+ </div>
101
305
 
102
- const removeItem = (item: { id: number; name: string }) => {
103
- setItems((prevItems) => prevItems.filter((i) => i.id !== item.id));
104
- unselect(item);
105
- };
306
+ <pre className="rounded-md bg-slate-950 p-4 text-xs text-white">
307
+ <code>{JSON.stringify({ selected }, null, 2)}</code>
308
+ </pre>
309
+ </div>
310
+ );
311
+ },
312
+ };
313
+
314
+ // ─── Controlled ───────────────────────────────────────────────────────────────
315
+
316
+ /**
317
+ * External state drives selection via `setSelected`. `onChange` fires after
318
+ * each change and logs the derived `selected` items (orphaned keys excluded).
319
+ */
320
+ export const Controlled: Story = {
321
+ render: () => {
322
+ const items = [
323
+ { id: 1, label: "Task 1" },
324
+ { id: 2, label: "Task 2" },
325
+ { id: 3, label: "Task 3" },
326
+ { id: 4, label: "Task 4" },
327
+ ];
328
+
329
+ const [log, setLog] = useState<string[]>([]);
330
+
331
+ const { isSelected, toggle, setSelected, selected, clear } = useSelection(
332
+ items,
333
+ {
334
+ keyFn: (item) => item.id,
335
+ onChange: (newSelected) => {
336
+ setLog((prev) => [
337
+ `onChange → [${newSelected.map((i) => i.label).join(", ")}]`,
338
+ ...prev.slice(0, 4),
339
+ ]);
340
+ },
341
+ },
342
+ );
106
343
 
107
344
  return (
108
- <div>
109
- {items.map((item) => (
110
- <div key={item.id} className="flex items-center gap-2">
111
- <Checkbox
112
- name={item.id.toString()}
113
- checked={isSelected(item)}
114
- onCheckedChange={() => toggle(item)}
115
- />
116
- <span className="text-sm">{item.name}</span>
117
- <Button
118
- size="icon"
119
- variant="ghost"
120
- onClick={() => removeItem(item)}
121
- >
122
- <Trash />
123
- </Button>
124
- </div>
125
- ))}
126
- <Button className="my-2" onClick={addNewItem}>
127
- Añadir nuevo elemento
128
- </Button>
129
- <pre className="p-4 text-white rounded-md bg-slate-950">
130
- <code className="block">
131
- Selected:{" "}
132
- <span className="text-blue-300">
133
- {JSON.stringify(selected, null, 2)}
134
- </span>
345
+ <div className="space-y-4 p-4">
346
+ <div className="space-y-1">
347
+ {items.map((item) => (
348
+ <div key={item.id} className="flex items-center gap-2">
349
+ <Checkbox
350
+ name={String(item.id)}
351
+ checked={isSelected(item)}
352
+ onCheckedChange={() => toggle(item)}
353
+ />
354
+ <span className="text-sm">{item.label}</span>
355
+ </div>
356
+ ))}
357
+ </div>
358
+
359
+ <div className="flex flex-wrap gap-2">
360
+ <Button
361
+ size="sm"
362
+ variant="outline"
363
+ onClick={() => setSelected([items[0], items[2]])}
364
+ >
365
+ Set [Task 1, Task 3]
366
+ </Button>
367
+ <Button size="sm" variant="outline" onClick={clear}>
368
+ Clear
369
+ </Button>
370
+ </div>
371
+
372
+ <pre className="rounded-md bg-slate-950 p-4 text-xs text-white">
373
+ <code>
374
+ {JSON.stringify(
375
+ { selected: selected.map((i) => i.label) },
376
+ null,
377
+ 2,
378
+ )}
135
379
  </code>
136
380
  </pre>
381
+
382
+ {log.length > 0 && (
383
+ <div className="space-y-1">
384
+ <p className="text-xs font-medium text-slate-500">onChange log:</p>
385
+ {log.map((entry, i) => (
386
+ <p key={i} className="font-mono text-xs text-slate-600">
387
+ {entry}
388
+ </p>
389
+ ))}
390
+ </div>
391
+ )}
137
392
  </div>
138
393
  );
139
394
  },