@jsenv/navi 0.0.1 → 0.1.1

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 (139) hide show
  1. package/dist/jsenv_navi.js +22959 -0
  2. package/index.js +66 -16
  3. package/package.json +23 -11
  4. package/src/actions.js +50 -26
  5. package/src/browser_integration/browser_integration.js +31 -6
  6. package/src/browser_integration/via_history.js +42 -9
  7. package/src/components/action_execution/render_actionable_component.jsx +6 -4
  8. package/src/components/action_execution/use_action.js +51 -282
  9. package/src/components/action_execution/use_execute_action.js +106 -92
  10. package/src/components/action_execution/use_run_on_mount.js +9 -0
  11. package/src/components/action_renderer.jsx +21 -32
  12. package/src/components/demos/0_button_demo.html +574 -103
  13. package/src/components/demos/10_column_reordering_debug.html +277 -0
  14. package/src/components/demos/11_table_selection_debug.html +432 -0
  15. package/src/components/demos/1_checkbox_demo.html +579 -202
  16. package/src/components/demos/2_input_textual_demo.html +81 -138
  17. package/src/components/demos/3_radio_demo.html +0 -2
  18. package/src/components/demos/4_select_demo.html +19 -23
  19. package/src/components/demos/6_tablist_demo.html +77 -0
  20. package/src/components/demos/7_table_selection_demo.html +176 -0
  21. package/src/components/demos/8_table_fixed_headers_demo.html +584 -0
  22. package/src/components/demos/9_table_column_drag_demo.html +325 -0
  23. package/src/components/demos/action/0_button_demo.html +2 -4
  24. package/src/components/demos/action/1_input_text_demo.html +643 -222
  25. package/src/components/demos/action/3_details_demo.html +146 -115
  26. package/src/components/demos/action/4_input_checkbox_demo.html +442 -322
  27. package/src/components/demos/action/5_input_checkbox_state_demo.html +270 -0
  28. package/src/components/demos/action/6_checkbox_list_demo.html +304 -72
  29. package/src/components/demos/action/7_radio_list_demo.html +310 -170
  30. package/src/components/demos/action/{8_editable_text_demo.html → 8_editable_demo.html} +65 -76
  31. package/src/components/demos/action/9_link_demo.html +84 -62
  32. package/src/components/demos/ui_transition/0_action_renderer_ui_transition_demo.html +695 -0
  33. package/src/components/demos/ui_transition/1_nested_ui_transition_demo.html +429 -0
  34. package/src/components/demos/ui_transition/2_height_transition_test.html +295 -0
  35. package/src/components/details/details.jsx +62 -64
  36. package/src/components/edition/editable.jsx +186 -0
  37. package/src/components/field/README.md +247 -0
  38. package/src/components/{input → field}/button.jsx +151 -130
  39. package/src/components/field/checkbox_list.jsx +184 -0
  40. package/src/components/{collect_form_element_values.js → field/collect_form_element_values.js} +7 -4
  41. package/src/components/{input → field}/field_css.js +4 -1
  42. package/src/components/field/form.jsx +211 -0
  43. package/src/components/{input → field}/input.jsx +1 -0
  44. package/src/components/{input → field}/input_checkbox.jsx +132 -155
  45. package/src/components/{input → field}/input_radio.jsx +135 -46
  46. package/src/components/field/input_textual.jsx +418 -0
  47. package/src/components/field/label.jsx +32 -0
  48. package/src/components/field/radio_list.jsx +182 -0
  49. package/src/components/{input → field}/select.jsx +17 -32
  50. package/src/components/field/use_action_events.js +132 -0
  51. package/src/components/field/use_form_events.js +55 -0
  52. package/src/components/field/use_ui_state_controller.js +506 -0
  53. package/src/components/item_tracker/README.md +461 -0
  54. package/src/components/item_tracker/use_isolated_item_tracker.jsx +209 -0
  55. package/src/components/item_tracker/use_isolated_item_tracker_demo.html +148 -0
  56. package/src/components/item_tracker/use_isolated_item_tracker_demo.jsx +460 -0
  57. package/src/components/item_tracker/use_item_tracker.jsx +143 -0
  58. package/src/components/item_tracker/use_item_tracker_demo.html +207 -0
  59. package/src/components/item_tracker/use_item_tracker_demo.jsx +216 -0
  60. package/src/components/keyboard_shortcuts/active_keyboard_shortcuts.jsx +87 -0
  61. package/src/components/keyboard_shortcuts/aria_key_shortcuts.js +61 -0
  62. package/src/components/keyboard_shortcuts/keyboard_key_meta.js +17 -0
  63. package/src/components/keyboard_shortcuts/keyboard_shortcuts.js +371 -0
  64. package/src/components/link/link.jsx +65 -102
  65. package/src/components/link/link_with_icon.jsx +52 -0
  66. package/src/components/loader/loader_background.jsx +85 -64
  67. package/src/components/loader/rectangle_loading.jsx +38 -19
  68. package/src/components/route.jsx +8 -4
  69. package/src/components/selection/selection.jsx +1583 -0
  70. package/src/components/svg/font_sized_svg.jsx +45 -0
  71. package/src/components/svg/icon_and_text.jsx +21 -0
  72. package/src/components/svg/svg_mask_overlay.jsx +105 -0
  73. package/src/components/table/drag/table_drag.jsx +506 -0
  74. package/src/components/table/resize/table_resize.jsx +650 -0
  75. package/src/components/table/resize/table_size.js +43 -0
  76. package/src/components/table/selection/table_selection.js +106 -0
  77. package/src/components/table/selection/table_selection.jsx +203 -0
  78. package/src/components/table/sticky/sticky_group.js +354 -0
  79. package/src/components/table/sticky/table_sticky.js +25 -0
  80. package/src/components/table/sticky/table_sticky.jsx +501 -0
  81. package/src/components/table/table.jsx +721 -0
  82. package/src/components/table/table_css.js +211 -0
  83. package/src/components/table/table_ui.jsx +49 -0
  84. package/src/components/table/use_cells_and_columns.js +90 -0
  85. package/src/components/table/use_object_array_to_cells.js +46 -0
  86. package/src/components/table/z_indexes.js +23 -0
  87. package/src/components/tablist/tablist.jsx +99 -0
  88. package/src/components/text/overflow.jsx +15 -0
  89. package/src/components/text/text_and_count.jsx +28 -0
  90. package/src/components/ui_transition.jsx +128 -0
  91. package/src/components/use_auto_focus.js +58 -7
  92. package/src/components/use_batch_during_render.js +33 -0
  93. package/src/components/use_debounce_true.js +7 -7
  94. package/src/components/use_dependencies_diff.js +35 -0
  95. package/src/components/use_focus_group.js +4 -3
  96. package/src/components/use_initial_value.js +8 -34
  97. package/src/components/use_signal_sync.js +1 -1
  98. package/src/components/use_stable_callback.js +68 -0
  99. package/src/components/use_state_array.js +16 -9
  100. package/src/docs/actions.md +22 -0
  101. package/src/notes.md +33 -12
  102. package/src/route/route.js +97 -47
  103. package/src/store/resource_graph.js +2 -1
  104. package/src/store/tests/{resource_graph_dependencies.test.js → resource_graph_dependencies.test_manual.js} +13 -13
  105. package/src/utils/is_signal.js +20 -0
  106. package/src/utils/stringify_for_display.js +4 -23
  107. package/src/validation/constraints/confirm_constraint.js +14 -0
  108. package/src/validation/constraints/create_unique_value_constraint.js +27 -0
  109. package/src/validation/constraints/native_constraints.js +313 -0
  110. package/src/validation/constraints/readonly_constraint.js +36 -0
  111. package/src/validation/constraints/single_space_constraint.js +13 -0
  112. package/src/validation/custom_constraint_validation.js +599 -0
  113. package/src/validation/custom_message.js +18 -0
  114. package/src/validation/demos/browser_style.png +0 -0
  115. package/src/validation/demos/form_validation_demo.html +142 -0
  116. package/src/validation/demos/form_validation_demo_preact.html +87 -0
  117. package/src/validation/demos/form_validation_native_popover_demo.html +168 -0
  118. package/src/validation/demos/form_validation_vs_native_demo.html +172 -0
  119. package/src/validation/demos/validation_message_demo.html +203 -0
  120. package/src/validation/hooks/use_constraints.js +23 -0
  121. package/src/validation/hooks/use_custom_validation_ref.js +73 -0
  122. package/src/validation/hooks/use_validation_message.js +19 -0
  123. package/src/validation/validation_message.js +741 -0
  124. package/src/components/editable_text/editable_text.jsx +0 -96
  125. package/src/components/form.jsx +0 -144
  126. package/src/components/input/checkbox_list.jsx +0 -294
  127. package/src/components/input/field.jsx +0 -61
  128. package/src/components/input/input_textual.jsx +0 -338
  129. package/src/components/input/radio_list.jsx +0 -283
  130. package/src/components/input/use_form_event.js +0 -20
  131. package/src/components/input/use_on_change.js +0 -12
  132. package/src/components/selection/selection.js +0 -5
  133. package/src/components/selection/selection_context.jsx +0 -262
  134. package/src/components/shortcut/shortcut_context.jsx +0 -390
  135. package/src/components/use_action_events.js +0 -37
  136. package/src/utils/iterable_weak_set.js +0 -62
  137. /package/src/components/demos/action/{11_nested_shortcuts_demo.html → 11_nested_shortcuts_demo.xhtml} +0 -0
  138. /package/src/components/{shortcut → keyboard_shortcuts}/os.js +0 -0
  139. /package/src/route/{route.test.html → route.xtest.html} +0 -0
@@ -95,6 +95,7 @@
95
95
  min-height: 40px;
96
96
  font-size: 16px;
97
97
  line-height: 1.4;
98
+ position: relative;
98
99
  }
99
100
 
100
101
  .result-display {
@@ -156,11 +157,11 @@
156
157
  import { render } from "preact";
157
158
  import { useState } from "preact/hooks";
158
159
  import { signal } from "@preact/signals";
159
- import { SINGLE_SPACE_CONSTRAINT } from "@jsenv/validation";
160
160
  import {
161
- useEditableController,
161
+ SINGLE_SPACE_CONSTRAINT,
162
+ useEditionController,
162
163
  // eslint-disable-next-line no-unused-vars
163
- EditableText,
164
+ Editable,
164
165
  createAction,
165
166
  } from "@jsenv/navi";
166
167
 
@@ -172,14 +173,14 @@
172
173
  <LoadingDemo />
173
174
  <ErrorDemo />
174
175
  <CustomStyleDemo />
175
- <SignalControlDemo />
176
+ <ActionObjectDemo />
176
177
  </div>
177
178
  );
178
179
  };
179
180
 
180
181
  // eslint-disable-next-line no-unused-vars
181
182
  const BasicDemo = () => {
182
- const { editable, startEditing, stopEditing } = useEditableController();
183
+ const { editing, startEditing, stopEditing } = useEditionController();
183
184
  const [value, setValue] = useState("Click edit to change this text");
184
185
  const [result, setResult] = useState("");
185
186
 
@@ -189,25 +190,24 @@
189
190
  <div className="control-group">
190
191
  <label>Editable Content:</label>
191
192
  <div className="editable-container">
192
- <EditableText
193
- editable={editable}
193
+ <Editable
194
+ editing={editing}
194
195
  onEditEnd={() => {
195
196
  stopEditing();
196
197
  setResult(``);
197
198
  }}
198
- name="test"
199
- action={({ test }) => {
200
- setValue(test);
201
- return `Updated to: "${test}"`;
202
- }}
203
199
  value={value}
200
+ action={(newText) => {
201
+ setValue(newText);
202
+ return `Updated to: "${newText}"`;
203
+ }}
204
204
  constraints={[SINGLE_SPACE_CONSTRAINT]}
205
205
  />
206
206
  </div>
207
207
  </div>
208
208
  <div className="button-group">
209
- <button onClick={() => startEditing()} disabled={editable}>
210
- {editable ? "Editing..." : "Start Editing"}
209
+ <button onClick={() => startEditing()} disabled={editing}>
210
+ {editing ? "Editing..." : "Start Editing"}
211
211
  </button>
212
212
  </div>
213
213
  <div
@@ -223,7 +223,7 @@
223
223
 
224
224
  // eslint-disable-next-line no-unused-vars
225
225
  const LoadingDemo = () => {
226
- const { editable, startEditing, stopEditing } = useEditableController();
226
+ const { editing, startEditing, stopEditing } = useEditionController();
227
227
  const [value, setValue] = useState("This will take 2 seconds to save");
228
228
  const [result, setResult] = useState("");
229
229
  const [isLoading, setIsLoading] = useState(false);
@@ -234,22 +234,21 @@
234
234
  <div className="control-group">
235
235
  <label>Editable Content (2s delay):</label>
236
236
  <div className="editable-container">
237
- <EditableText
238
- editable={editable}
237
+ <Editable
238
+ editing={editing}
239
239
  onEditEnd={() => {
240
240
  stopEditing();
241
241
  setIsLoading(false);
242
242
  setResult();
243
243
  }}
244
- name="test"
245
- action={async ({ test }) => {
244
+ value={value}
245
+ action={async (newText) => {
246
246
  setIsLoading(true);
247
247
  setResult("Saving...");
248
248
  await new Promise((resolve) => setTimeout(resolve, 2000));
249
- setValue(test);
250
- return `Saved: "${test}"`;
249
+ setValue(newText);
250
+ return `Saved: "${newText}"`;
251
251
  }}
252
- value={value}
253
252
  constraints={[SINGLE_SPACE_CONSTRAINT]}
254
253
  />
255
254
  </div>
@@ -257,11 +256,11 @@
257
256
  <div className="button-group">
258
257
  <button
259
258
  onClick={() => startEditing()}
260
- disabled={editable || isLoading}
259
+ disabled={editing || isLoading}
261
260
  >
262
261
  {isLoading
263
262
  ? "Saving..."
264
- : editable
263
+ : editing
265
264
  ? "Editing..."
266
265
  : "Start Editing"}
267
266
  </button>
@@ -279,7 +278,7 @@
279
278
 
280
279
  // eslint-disable-next-line no-unused-vars
281
280
  const ErrorDemo = () => {
282
- const { editable, startEditing, stopEditing } = useEditableController();
281
+ const { editing, startEditing, stopEditing } = useEditionController();
283
282
  const [value] = useState("This edit will always fail");
284
283
  const [result, setResult] = useState("");
285
284
 
@@ -289,26 +288,28 @@
289
288
  <div className="control-group">
290
289
  <label>Editable Content (always fails):</label>
291
290
  <div className="editable-container">
292
- <EditableText
293
- editable={editable}
291
+ <Editable
292
+ editing={editing}
294
293
  onEditEnd={() => {
295
294
  stopEditing();
296
295
  setResult("");
297
296
  }}
298
- name="test"
299
- action={async ({ test }) => {
297
+ value={value}
298
+ name="errorText"
299
+ action={async (newText) => {
300
300
  setResult("Processing...");
301
301
  await new Promise((resolve) => setTimeout(resolve, 500));
302
- throw new Error(`Cannot save "${test}" - simulated error`);
302
+ throw new Error(
303
+ `Cannot save "${newText}" - simulated error`,
304
+ );
303
305
  }}
304
- value={value}
305
306
  constraints={[SINGLE_SPACE_CONSTRAINT]}
306
307
  />
307
308
  </div>
308
309
  </div>
309
310
  <div className="button-group">
310
- <button onClick={() => startEditing()} disabled={editable}>
311
- {editable ? "Editing..." : "Start Editing (Will Fail)"}
311
+ <button onClick={() => startEditing()} disabled={editing}>
312
+ {editing ? "Editing..." : "Start Editing (Will Fail)"}
312
313
  </button>
313
314
  </div>
314
315
  <div
@@ -324,7 +325,7 @@
324
325
 
325
326
  // eslint-disable-next-line no-unused-vars
326
327
  const CustomStyleDemo = () => {
327
- const { editable, startEditing, stopEditing } = useEditableController();
328
+ const { editing, startEditing, stopEditing } = useEditionController();
328
329
  const [value, setValue] = useState("Bold styled text");
329
330
  const [result, setResult] = useState("");
330
331
 
@@ -334,29 +335,28 @@
334
335
  <div className="control-group">
335
336
  <label>Styled Editable Content:</label>
336
337
  <div className="editable-container">
337
- <EditableText
338
- editable={editable}
338
+ <Editable
339
+ editing={editing}
339
340
  onEditEnd={() => {
340
341
  stopEditing();
341
342
  setResult("");
342
343
  }}
343
- name="test"
344
- action={({ test }) => {
345
- setValue(test);
346
- return `Updated styled text: "${test}"`;
347
- }}
348
344
  value={value}
345
+ action={(newText) => {
346
+ setValue(newText);
347
+ return `Updated styled text: "${newText}"`;
348
+ }}
349
349
  constraints={[SINGLE_SPACE_CONSTRAINT]}
350
350
  >
351
351
  <strong style={{ color: "#e74c3c", fontSize: "18px" }}>
352
352
  {value}
353
353
  </strong>
354
- </EditableText>
354
+ </Editable>
355
355
  </div>
356
356
  </div>
357
357
  <div className="button-group">
358
- <button onClick={() => startEditing()} disabled={editable}>
359
- {editable ? "Editing..." : "Start Editing"}
358
+ <button onClick={() => startEditing()} disabled={editing}>
359
+ {editing ? "Editing..." : "Start Editing"}
360
360
  </button>
361
361
  </div>
362
362
  <div
@@ -371,38 +371,37 @@
371
371
  };
372
372
 
373
373
  // Create signal outside component to persist across renders
374
- const valueSignal = signal("Signal-controlled editable text");
375
- const action = createAction(async ({ test }) => {
374
+ const editableTextSignal = signal("Click me to edit");
375
+ const updateTextAction = createAction(async (newText) => {
376
376
  await new Promise((resolve) => setTimeout(resolve, 800));
377
- return `Signal update: "${test}"`;
377
+ editableTextSignal.value = newText;
378
+ return `Signal update: "${newText}"`;
378
379
  });
379
- const boundAction = action.bindParams({ test: valueSignal });
380
-
381
380
  // eslint-disable-next-line no-unused-vars
382
- const SignalControlDemo = () => {
381
+ const ActionObjectDemo = () => {
383
382
  const [result, setResult] = useState("");
384
- const { editable, startEditing, stopEditing } = useEditableController();
383
+ const { editing, startEditing, stopEditing } = useEditionController();
385
384
 
386
385
  return (
387
386
  <div className="demo-card">
388
- <h3 className="demo-title">Signal-Controlled Editable Text</h3>
387
+ <h3 className="demo-title">Action object editable Text</h3>
389
388
  <div className="control-group">
390
- <label>Signal-Controlled Content:</label>
389
+ <label>Content:</label>
391
390
  <div className="editable-container">
392
- <EditableText
393
- valueSignal={valueSignal}
394
- editable={editable}
391
+ <Editable
392
+ editing={editing}
395
393
  onEditEnd={() => {
396
394
  stopEditing();
397
395
  setResult("");
398
396
  }}
397
+ value={editableTextSignal.value}
398
+ name="signalText"
399
+ action={(newText) => {
400
+ updateTextAction(newText);
401
+ }}
399
402
  onActionStart={() => {
400
- setResult("Saving via signal...");
403
+ setResult("Saving...");
401
404
  }}
402
- name="coucou"
403
- action={boundAction}
404
- cancelOnEscape
405
- cancelOnBlurInvalid
406
405
  constraints={[SINGLE_SPACE_CONSTRAINT]}
407
406
  >
408
407
  <span
@@ -410,27 +409,17 @@
410
409
  startEditing();
411
410
  }}
412
411
  >
413
- {valueSignal.value}
412
+ {editableTextSignal.value}
414
413
  </span>
415
- </EditableText>
414
+ </Editable>
416
415
  </div>
417
416
  </div>
418
- <div className="button-group">
419
- <button
420
- onClick={() => {
421
- console.log("Setting editable signal to true");
422
- valueSignal.value = "toto";
423
- }}
424
- >
425
- Set signal value to "toto"
426
- </button>
427
- </div>
428
417
  <div
429
- className={`result-display ${result === "Saving via signal..." ? "result-loading" : result.startsWith("✅") ? "result-success" : result.startsWith("❌") ? "result-error" : ""}`}
418
+ className={`result-display ${result === "Saving..." ? "result-loading" : result.startsWith("✅") ? "result-success" : result.startsWith("❌") ? "result-error" : ""}`}
430
419
  >
431
- Signal value: {valueSignal.value}
420
+ Current value: {editableTextSignal.value}
432
421
  <br />
433
- {result || "Use buttons to control edit mode via signal..."}
422
+ {result || "Use buttons to control edit mode..."}
434
423
  </div>
435
424
  </div>
436
425
  );
@@ -11,16 +11,21 @@
11
11
 
12
12
  <script type="module" jsenv-type="module/jsx">
13
13
  import { render } from "preact";
14
- import { useState } from "preact/hooks";
15
- // eslint-disable-next-line no-unused-vars
16
- import { Link, SelectionProvider, defineRoutes } from "@jsenv/navi";
14
+ import { useState, useRef } from "preact/hooks";
15
+
16
+ import {
17
+ // eslint-disable-next-line no-unused-vars
18
+ Link,
19
+ useSelectionController,
20
+ // eslint-disable-next-line no-unused-vars
21
+ SelectionContext,
22
+ defineRoutes,
23
+ } from "@jsenv/navi";
17
24
 
18
25
  defineRoutes({});
19
26
 
20
27
  // eslint-disable-next-line no-unused-vars
21
28
  const App = () => {
22
- const [contextSelectedItems, setContextSelectedItems] = useState([]);
23
-
24
29
  return (
25
30
  <div style="padding: 20px; font-family: system-ui, sans-serif; max-width: 800px;">
26
31
  <h1>Link Component Demo</h1>
@@ -82,63 +87,7 @@
82
87
  </div>
83
88
  </section>
84
89
 
85
- <section style="margin-bottom: 40px;">
86
- <h2>Selectable Links</h2>
87
- <p style="color: #666; margin-bottom: 16px;">
88
- Using Selection context for centralized state management.{" "}
89
- <kbd style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 0.9em;">
90
- Cmd/Ctrl + Click
91
- </kbd>{" "}
92
- to multi-select,{" "}
93
- <kbd style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 0.9em;">
94
- Shift + Click
95
- </kbd>{" "}
96
- for range selection, and{" "}
97
- <kbd style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 0.9em;">
98
- Cmd/Ctrl + Delete
99
- </kbd>{" "}
100
- to show selected items.
101
- </p>
102
-
103
- <SelectionProvider
104
- value={contextSelectedItems}
105
- onChange={setContextSelectedItems}
106
- >
107
- <div style="border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; background: white;">
108
- <div style="display: flex; flex-direction: column; gap: 8px;">
109
- <Link href="#doc_report" name="doc" value="doc-1">
110
- 📋 Annual Report 2024
111
- </Link>
112
-
113
- <Link href="#doc_budget" name="doc" value="doc-2">
114
- 💰 Budget Proposal
115
- </Link>
116
-
117
- <Link href="#doc_presentation" name="doc" value="doc-3">
118
- 🎯 Marketing Presentation
119
- </Link>
120
-
121
- <Link href="#doc_manual" name="doc" value="doc-4">
122
- 📖 User Manual v2.0
123
- </Link>
124
- </div>
125
-
126
- {contextSelectedItems.length > 0 && (
127
- <div style="margin-top: 16px; padding: 12px; background: #e3f2fd; border-radius: 6px; border-left: 4px solid #1976d2;">
128
- <strong>Selected items:</strong>{" "}
129
- {contextSelectedItems.join(", ")}
130
- <br />
131
- <button
132
- onClick={() => setContextSelectedItems([])}
133
- style="margin-top: 8px; padding: 4px 12px; background: #1976d2; color: white; border: none; border-radius: 4px; font-size: 0.9em; cursor: pointer;"
134
- >
135
- Clear selection
136
- </button>
137
- </div>
138
- )}
139
- </div>
140
- </SelectionProvider>
141
- </section>
90
+ <SelectableLinkDemo />
142
91
 
143
92
  <section>
144
93
  <h2>Navigation Instructions</h2>
@@ -166,6 +115,79 @@
166
115
  );
167
116
  };
168
117
 
118
+ // eslint-disable-next-line no-unused-vars
119
+ const SelectableLinkDemo = () => {
120
+ const [selection, setSelection] = useState([]);
121
+ const linkContainerRef = useRef(null);
122
+ const selectionController = useSelectionController({
123
+ elementRef: linkContainerRef,
124
+ layout: "vertical",
125
+ value: selection,
126
+ onChange: setSelection,
127
+ });
128
+
129
+ return (
130
+ <section style="margin-bottom: 40px;">
131
+ <h2>Selectable Links</h2>
132
+ <p style="color: #666; margin-bottom: 16px;">
133
+ Using Selection context for centralized state management.{" "}
134
+ <kbd style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 0.9em;">
135
+ Cmd/Ctrl + Click
136
+ </kbd>{" "}
137
+ to multi-select,{" "}
138
+ <kbd style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 0.9em;">
139
+ Shift + Click
140
+ </kbd>{" "}
141
+ for range selection, and{" "}
142
+ <kbd style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 0.9em;">
143
+ Cmd/Ctrl + Delete
144
+ </kbd>{" "}
145
+ to show selected items.
146
+ </p>
147
+
148
+ <SelectionContext.Provider
149
+ value={{ selection, selectionController }}
150
+ >
151
+ <div style="border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; background: white;">
152
+ <div
153
+ style="display: flex; flex-direction: column; gap: 8px;"
154
+ ref={linkContainerRef}
155
+ >
156
+ <Link href="#doc_report" name="doc" value="doc-1">
157
+ 📋 Annual Report 2024
158
+ </Link>
159
+
160
+ <Link href="#doc_budget" name="doc" value="doc-2">
161
+ 💰 Budget Proposal
162
+ </Link>
163
+
164
+ <Link href="#doc_presentation" name="doc" value="doc-3">
165
+ 🎯 Marketing Presentation
166
+ </Link>
167
+
168
+ <Link href="#doc_manual" name="doc" value="doc-4">
169
+ 📖 User Manual v2.0
170
+ </Link>
171
+ </div>
172
+
173
+ {selection.length > 0 && (
174
+ <div style="margin-top: 16px; padding: 12px; background: #e3f2fd; border-radius: 6px; border-left: 4px solid #1976d2;">
175
+ <strong>Selected items:</strong> {selection.join(", ")}
176
+ <br />
177
+ <button
178
+ onClick={() => setSelection([])}
179
+ style="margin-top: 8px; padding: 4px 12px; background: #1976d2; color: white; border: none; border-radius: 4px; font-size: 0.9em; cursor: pointer;"
180
+ >
181
+ Clear selection
182
+ </button>
183
+ </div>
184
+ )}
185
+ </div>
186
+ </SelectionContext.Provider>
187
+ </section>
188
+ );
189
+ };
190
+
169
191
  render(<App />, document.querySelector("#root"));
170
192
  </script>
171
193
  </body>