@jsenv/navi 0.10.1 → 0.11.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.
Files changed (207) hide show
  1. package/dist/jsenv_navi.js +13858 -23295
  2. package/dist/jsenv_navi.js.map +1281 -0
  3. package/package.json +5 -7
  4. package/index.js +0 -122
  5. package/src/action_private_properties.js +0 -11
  6. package/src/action_proxy_test.html +0 -353
  7. package/src/action_run_states.js +0 -5
  8. package/src/actions.js +0 -1401
  9. package/src/browser_integration/browser_integration.js +0 -216
  10. package/src/browser_integration/document_back_and_forward.js +0 -17
  11. package/src/browser_integration/document_loading_signal.js +0 -100
  12. package/src/browser_integration/document_state_signal.js +0 -9
  13. package/src/browser_integration/document_url_signal.js +0 -9
  14. package/src/browser_integration/use_is_visited.js +0 -19
  15. package/src/browser_integration/via_history.js +0 -232
  16. package/src/browser_integration/via_navigation.js +0 -168
  17. package/src/components/action_execution/form_context.js +0 -5
  18. package/src/components/action_execution/render_actionable_component.jsx +0 -29
  19. package/src/components/action_execution/use_action.js +0 -99
  20. package/src/components/action_execution/use_execute_action.js +0 -177
  21. package/src/components/action_execution/use_run_on_mount.js +0 -9
  22. package/src/components/action_renderer.jsx +0 -125
  23. package/src/components/callout/callout.js +0 -990
  24. package/src/components/callout/callout_demo.html +0 -201
  25. package/src/components/callout/test_dynamic_positioning.html +0 -161
  26. package/src/components/callout/test_html_document_iframe.html +0 -182
  27. package/src/components/demos/0_button_demo.html +0 -707
  28. package/src/components/demos/10_column_reordering_debug.html +0 -277
  29. package/src/components/demos/11_table_selection_debug.html +0 -432
  30. package/src/components/demos/1_checkbox_demo.html +0 -754
  31. package/src/components/demos/2_input_textual_demo.html +0 -286
  32. package/src/components/demos/3_radio_demo.html +0 -874
  33. package/src/components/demos/4_select_demo.html +0 -100
  34. package/src/components/demos/5_list_scrollable_demo.html +0 -153
  35. package/src/components/demos/6_tablist_demo.html +0 -77
  36. package/src/components/demos/7_table_selection_demo.html +0 -176
  37. package/src/components/demos/8_table_fixed_headers_demo.html +0 -584
  38. package/src/components/demos/9_table_column_drag_demo.html +0 -325
  39. package/src/components/demos/action/0_button_demo.html +0 -204
  40. package/src/components/demos/action/10_shortcuts_demo.html +0 -189
  41. package/src/components/demos/action/11_nested_shortcuts_demo.xhtml +0 -401
  42. package/src/components/demos/action/1_input_text_demo.html +0 -876
  43. package/src/components/demos/action/2_form_multiple.html +0 -303
  44. package/src/components/demos/action/3_details_demo.html +0 -203
  45. package/src/components/demos/action/4_input_checkbox_demo.html +0 -731
  46. package/src/components/demos/action/5_input_checkbox_state_demo.html +0 -270
  47. package/src/components/demos/action/6_checkbox_list_demo.html +0 -341
  48. package/src/components/demos/action/7_radio_list_demo.html +0 -357
  49. package/src/components/demos/action/8_editable_demo.html +0 -431
  50. package/src/components/demos/action/9_link_demo.html +0 -194
  51. package/src/components/demos/demo.md +0 -0
  52. package/src/components/demos/route/basic/basic.html +0 -14
  53. package/src/components/demos/route/basic/basic_route_demo.jsx +0 -224
  54. package/src/components/demos/route/multi/multi.html +0 -14
  55. package/src/components/demos/route/multi/multi_route_demo.jsx +0 -277
  56. package/src/components/demos/ui_transition/0_action_renderer_ui_transition_demo.html +0 -695
  57. package/src/components/demos/ui_transition/1_nested_ui_transition_demo.html +0 -429
  58. package/src/components/demos/ui_transition/2_height_transition_test.html +0 -295
  59. package/src/components/details/details.jsx +0 -245
  60. package/src/components/details/summary_marker.jsx +0 -141
  61. package/src/components/edition/editable.jsx +0 -186
  62. package/src/components/error_boundary_context.js +0 -9
  63. package/src/components/field/README.md +0 -247
  64. package/src/components/field/button.jsx +0 -429
  65. package/src/components/field/checkbox_list.jsx +0 -185
  66. package/src/components/field/collect_form_element_values.js +0 -82
  67. package/src/components/field/custom_field.js +0 -106
  68. package/src/components/field/form.jsx +0 -209
  69. package/src/components/field/input.jsx +0 -16
  70. package/src/components/field/input_checkbox.jsx +0 -434
  71. package/src/components/field/input_radio.jsx +0 -432
  72. package/src/components/field/input_textual.jsx +0 -389
  73. package/src/components/field/label.jsx +0 -46
  74. package/src/components/field/radio_list.jsx +0 -183
  75. package/src/components/field/select.jsx +0 -256
  76. package/src/components/field/use_action_events.js +0 -132
  77. package/src/components/field/use_form_events.js +0 -59
  78. package/src/components/field/use_ui_state_controller.js +0 -506
  79. package/src/components/item_tracker/README.md +0 -461
  80. package/src/components/item_tracker/use_isolated_item_tracker.jsx +0 -209
  81. package/src/components/item_tracker/use_isolated_item_tracker_demo.html +0 -148
  82. package/src/components/item_tracker/use_isolated_item_tracker_demo.jsx +0 -460
  83. package/src/components/item_tracker/use_item_tracker.jsx +0 -143
  84. package/src/components/item_tracker/use_item_tracker_demo.html +0 -207
  85. package/src/components/item_tracker/use_item_tracker_demo.jsx +0 -216
  86. package/src/components/keyboard_shortcuts/active_keyboard_shortcuts.jsx +0 -87
  87. package/src/components/keyboard_shortcuts/aria_key_shortcuts.js +0 -61
  88. package/src/components/keyboard_shortcuts/keyboard_key_meta.js +0 -17
  89. package/src/components/keyboard_shortcuts/keyboard_shortcuts.js +0 -371
  90. package/src/components/keyboard_shortcuts/os.js +0 -9
  91. package/src/components/layout/demos/demo_flex.html +0 -638
  92. package/src/components/layout/demos/demo_layout_style_buttons.html +0 -351
  93. package/src/components/layout/demos/demo_layout_style_input.html +0 -226
  94. package/src/components/layout/demos/demo_layout_style_text.html +0 -514
  95. package/src/components/layout/flex.jsx +0 -109
  96. package/src/components/layout/layout_context.jsx +0 -3
  97. package/src/components/layout/spacing.jsx +0 -20
  98. package/src/components/layout/use_layout_style.js +0 -249
  99. package/src/components/link/link.jsx +0 -267
  100. package/src/components/link/link_with_icon.jsx +0 -52
  101. package/src/components/loader/loader_background.jsx +0 -372
  102. package/src/components/loader/loading_spinner.jsx +0 -68
  103. package/src/components/loader/network_speed.js +0 -83
  104. package/src/components/loader/rectangle_loading.jsx +0 -244
  105. package/src/components/props_composition/demos/demo_with_props_style.html +0 -81
  106. package/src/components/props_composition/with_props_class_name.js +0 -37
  107. package/src/components/props_composition/with_props_style.js +0 -26
  108. package/src/components/route.jsx +0 -19
  109. package/src/components/selection/selection.jsx +0 -1583
  110. package/src/components/svg/font_sized_svg.jsx +0 -59
  111. package/src/components/svg/icon_and_text.jsx +0 -21
  112. package/src/components/svg/svg_mask_overlay.jsx +0 -105
  113. package/src/components/table/drag/table_drag.jsx +0 -506
  114. package/src/components/table/resize/table_resize.jsx +0 -650
  115. package/src/components/table/resize/table_size.js +0 -43
  116. package/src/components/table/selection/table_selection.js +0 -106
  117. package/src/components/table/selection/table_selection.jsx +0 -203
  118. package/src/components/table/sticky/sticky_group.js +0 -354
  119. package/src/components/table/sticky/table_sticky.js +0 -25
  120. package/src/components/table/sticky/table_sticky.jsx +0 -501
  121. package/src/components/table/table.jsx +0 -721
  122. package/src/components/table/table_css.js +0 -211
  123. package/src/components/table/table_ui.jsx +0 -49
  124. package/src/components/table/use_cells_and_columns.js +0 -90
  125. package/src/components/table/use_object_array_to_cells.js +0 -46
  126. package/src/components/table/z_indexes.js +0 -23
  127. package/src/components/tablist/tablist.jsx +0 -99
  128. package/src/components/text/demos/demo_text_and_icon.html +0 -421
  129. package/src/components/text/overflow.jsx +0 -15
  130. package/src/components/text/text.jsx +0 -83
  131. package/src/components/text/text_and_count.jsx +0 -28
  132. package/src/components/ui_transition.jsx +0 -128
  133. package/src/components/use_auto_focus.js +0 -94
  134. package/src/components/use_batch_during_render.js +0 -33
  135. package/src/components/use_debounce_true.js +0 -31
  136. package/src/components/use_dependencies_diff.js +0 -35
  137. package/src/components/use_focus_group.js +0 -20
  138. package/src/components/use_initial_value.js +0 -78
  139. package/src/components/use_is_visited.js +0 -19
  140. package/src/components/use_ref_array.js +0 -38
  141. package/src/components/use_signal_sync.js +0 -50
  142. package/src/components/use_stable_callback.js +0 -68
  143. package/src/components/use_state_array.js +0 -47
  144. package/src/docs/actions.md +0 -250
  145. package/src/docs/demos/resource/action_status.jsx +0 -42
  146. package/src/docs/demos/resource/demo.md +0 -1
  147. package/src/docs/demos/resource/resource_demo_0.html +0 -84
  148. package/src/docs/demos/resource/resource_demo_10_post_gc.html +0 -364
  149. package/src/docs/demos/resource/resource_demo_11_describe_many.html +0 -362
  150. package/src/docs/demos/resource/resource_demo_2.html +0 -173
  151. package/src/docs/demos/resource/resource_demo_3_filtered_users.html +0 -415
  152. package/src/docs/demos/resource/resource_demo_4_details.html +0 -284
  153. package/src/docs/demos/resource/resource_demo_5_renderer_lazy.html +0 -115
  154. package/src/docs/demos/resource/resource_demo_6_gc.html +0 -217
  155. package/src/docs/demos/resource/resource_demo_7_child_gc.html +0 -240
  156. package/src/docs/demos/resource/resource_demo_8_proxy_gc.html +0 -319
  157. package/src/docs/demos/resource/resource_demo_9_describe_one.html +0 -472
  158. package/src/docs/demos/resource/tata.jsx +0 -3
  159. package/src/docs/demos/resource/toto.jsx +0 -3
  160. package/src/docs/demos/user_nav/user_nav.html +0 -12
  161. package/src/docs/demos/user_nav/user_nav.jsx +0 -330
  162. package/src/docs/resource_dependencies.md +0 -103
  163. package/src/docs/resource_with_params.md +0 -80
  164. package/src/navi_css_vars.js +0 -14
  165. package/src/notes.md +0 -34
  166. package/src/route/route.js +0 -596
  167. package/src/route/route.xtest.html +0 -228
  168. package/src/store/array_signal_store.js +0 -537
  169. package/src/store/local_storage_signal.js +0 -17
  170. package/src/store/resource_graph.js +0 -1304
  171. package/src/store/tests/resource_graph_autoreload_demo.html +0 -12
  172. package/src/store/tests/resource_graph_autoreload_demo.jsx +0 -964
  173. package/src/store/tests/resource_graph_dependencies.test_manual.js +0 -95
  174. package/src/store/value_in_local_storage.js +0 -187
  175. package/src/symbol_object_signal.js +0 -1
  176. package/src/use_action_data.js +0 -10
  177. package/src/use_action_status.js +0 -47
  178. package/src/utils/add_many_event_listeners.js +0 -15
  179. package/src/utils/array_add_remove.js +0 -61
  180. package/src/utils/array_signal.js +0 -15
  181. package/src/utils/compare_two_js_values.js +0 -172
  182. package/src/utils/execute_with_cleanup.js +0 -21
  183. package/src/utils/get_caller_info.js +0 -85
  184. package/src/utils/is_signal.js +0 -20
  185. package/src/utils/js_value_weak_map.js +0 -162
  186. package/src/utils/js_value_weak_map_demo.html +0 -690
  187. package/src/utils/merge_two_js_values.js +0 -53
  188. package/src/utils/stringify_for_display.js +0 -131
  189. package/src/utils/weak_effect.js +0 -48
  190. package/src/validation/constraints/confirm_constraint.js +0 -14
  191. package/src/validation/constraints/create_unique_value_constraint.js +0 -27
  192. package/src/validation/constraints/native_constraints.js +0 -338
  193. package/src/validation/constraints/readonly_constraint.js +0 -41
  194. package/src/validation/constraints/same_as_constraint.js +0 -42
  195. package/src/validation/constraints/single_space_constraint.js +0 -13
  196. package/src/validation/custom_constraint_validation.js +0 -793
  197. package/src/validation/custom_message.js +0 -18
  198. package/src/validation/demos/browser_style.png +0 -0
  199. package/src/validation/demos/demo_same_as_constraint.html +0 -259
  200. package/src/validation/demos/form_validation_demo.html +0 -142
  201. package/src/validation/demos/form_validation_demo_preact.html +0 -87
  202. package/src/validation/demos/form_validation_native_popover_demo.html +0 -168
  203. package/src/validation/demos/form_validation_vs_native_demo.html +0 -172
  204. package/src/validation/hooks/use_constraints.js +0 -23
  205. package/src/validation/hooks/use_custom_validation_ref.js +0 -73
  206. package/src/validation/hooks/use_validation_message.js +0 -19
  207. package/src/validation/input_change_effect.js +0 -106
@@ -1,461 +0,0 @@
1
- # Item Tracker
2
-
3
- A Preact hook system for tracking dynamic lists, designed to prevent infinite re-renders while enabling component composition similar to native HTML elements.
4
-
5
- ## The Problem
6
-
7
- In React/Preact, you can wrap elements into components while maintaining the same compositional flexibility:
8
-
9
- ```jsx
10
- // Native HTML
11
- <select>
12
- <option>Eastern</option>
13
- <option>Central</option>
14
- <option disabled>Mountain</option>
15
- <option className="highlighted">Pacific</option>
16
- </select>
17
-
18
- // Component abstraction - same flexibility
19
- <TimezoneSelect>
20
- <TimezoneOption value="est">Eastern</TimezoneOption>
21
- <TimezoneOption value="cst">Central</TimezoneOption>
22
- <TimezoneOption value="mst" disabled>Mountain</TimezoneOption>
23
- <TimezoneOption value="pst" className="highlighted">Pacific</TimezoneOption>
24
- </TimezoneSelect>
25
- ```
26
-
27
- However, when building components that need to coordinate between children, the parent component needs to know about its children's properties.
28
-
29
- Consider a table where column definitions need to be shared with table cells:
30
-
31
- ```jsx
32
- // Desired API - clean composition
33
- <Table>
34
- <colgroup>
35
- <Col id="name" width="200px" sortable />
36
- <Col id="email" width="300px" resizable />
37
- <Col id="status" width="100px" />
38
- </colgroup>
39
- <tbody>
40
- <tr>
41
- <Cell column="name">{user.name}</Cell>
42
- <Cell column="email">{user.email}</Cell>
43
- <Cell column="status">{user.status}</Cell>
44
- </tr>
45
- </tbody>
46
- </Table>
47
- ```
48
-
49
- The challenge: `<Cell>` components need access to column configuration defined by `<Col>` components, but they're not in a parent-child relationship.
50
-
51
- ## Alternative Approaches
52
-
53
- Most libraries use configuration objects instead of components:
54
-
55
- ```jsx
56
- <Table
57
- columns={[
58
- { id: "name", width: "200px", sortable: true },
59
- { id: "email", width: "300px", resizable: true },
60
- { id: "status", width: "100px" },
61
- ]}
62
- data={tableData}
63
- />
64
- ```
65
-
66
- This approach works but requires complex APIs when you need to customize individual columns:
67
-
68
- ```jsx
69
- <Table
70
- columns={columns}
71
- getColumnProps={(column, index) => {
72
- if (column.id === "email") return { className: "email-column" };
73
- return {};
74
- }}
75
- cellRenderers={{
76
- email: (value) => value.toLowerCase(),
77
- }}
78
- />
79
- ```
80
-
81
- With component composition, the same customization is more straightforward:
82
-
83
- ```jsx
84
- <Table>
85
- <Colgroup>
86
- <Col id="name" width="200px" sortable />
87
- <Col id="email" width="300px" resizable className="email-column" />
88
- <Col id="status" width="100px" />
89
- </Colgroup>
90
- <Tbody>
91
- <Tr>
92
- <TableCell column="name">{user.name}</TableCell>
93
- <TableCell column="email">{user.email.toLowerCase()}</TableCell>
94
- <TableCell column="status">{user.status}</TableCell>
95
- </Tr>
96
- </Tbody>
97
- </Table>
98
- ```
99
-
100
- The problem is: how does the parent component discover its children's properties without causing infinite re-renders?
101
-
102
- ## Our Solution
103
-
104
- The Item Tracker provides hooks that enable component composition while preventing infinite re-renders. It offers two approaches depending on your component structure:
105
-
106
- **For library authors**, the system provides hooks to build components with clean APIs:
107
-
108
- ```jsx
109
- // Internal implementation
110
- function Table({ children }) {
111
- const ColTrackerProvider = useItemTrackerProvider();
112
-
113
- return (
114
- <ColTrackerProvider>
115
- <Table>
116
- <TableHeaders /> {/* Reads column data */}
117
- <Tbody>{children}</Tbody>
118
- </Table>
119
- </ColTrackerProvider>
120
- );
121
- }
122
-
123
- function Col({ id, width, sortable, ...props }) {
124
- const colIndex = useTrackItem({ id, width, sortable, ...props });
125
- return <col style={{ width }} />;
126
- }
127
-
128
- function TableCell({ column, children }) {
129
- const columns = useTrackedItems();
130
- const colData = columns.find((col) => col.id === column);
131
- return <td style={{ width: colData.width }}>{children}</td>;
132
- }
133
- ```
134
-
135
- **For end users**, this enables clean, component-based APIs:
136
-
137
- ```jsx
138
- // User-facing API
139
- <Table>
140
- <Colgroup>
141
- <Col id="name" width="200px" sortable className="name-col" />
142
- <Col id="email" width="300px" resizable />
143
- <Col id="status" width="100px" />
144
- </Colgroup>
145
- <Tbody>
146
- <Tr>
147
- <TableCell column="name">{user.name}</TableCell>
148
- <TableCell column="email">{user.email}</TableCell>
149
- <TableCell column="status">{user.status}</TableCell>
150
- </Tr>
151
- </Tbody>
152
- </Table>
153
- ```
154
-
155
- Key features:
156
-
157
- - **Prevents infinite re-renders** through ref-based item registration
158
- - **Enables true composition** - items can be registered anywhere in the component tree
159
- - **No configuration objects** - each component configures itself through props
160
- - **Dynamic lists** - supports adding, removing, and reordering items
161
-
162
- ## Two Systems for Different Use Cases
163
-
164
- We provide two complementary systems:
165
-
166
- ### 1. Simple Item Tracker (Colocated)
167
-
168
- For scenarios where registration and consumption happen in the same component tree:
169
-
170
- ```jsx
171
- import { createItemTracker } from "./use_item_tracker.jsx";
172
-
173
- const [useRowTrackerProvider, useRegisterRow, useRow, useRows] =
174
- createItemTracker();
175
-
176
- function App() {
177
- const RowTrackerProvider = useRowTrackerProvider();
178
-
179
- return (
180
- <RowTrackerProvider>
181
- <table>
182
- <tbody>
183
- {rows.map((data) => (
184
- <TableRow key={data.id} data={data}>
185
- <TableCell column="name" />
186
- <TableCell column="email" />
187
- </TableRow>
188
- ))}
189
- </tbody>
190
- </table>
191
- </RowTrackerProvider>
192
- );
193
- }
194
-
195
- function TableRow({ data, children }) {
196
- const rowIndex = useRegisterRow(data);
197
- return (
198
- <RowContext.Provider value={rowIndex}>
199
- <tr>{children}</tr>
200
- </RowContext.Provider>
201
- );
202
- }
203
-
204
- function TableCell({ column }) {
205
- const rowIndex = useContext(RowContext);
206
- const rowData = useRow(rowIndex);
207
- return <td>{rowData[column]}</td>;
208
- }
209
- ```
210
-
211
- ### 2. Isolated Item Tracker (Separated Trees)
212
-
213
- For complex scenarios where registration and consumption are in completely separate component trees:
214
-
215
- ```jsx
216
- import { createIsolatedItemTracker } from "./use_item_tracker_isolated.jsx";
217
-
218
- const [useColumnTrackerProviders, useRegisterColumn, useColumn, useColumns] =
219
- createIsolatedItemTracker();
220
-
221
- function App() {
222
- const [ColumnProducerProvider, ColumnConsumerProvider] =
223
- useColumnTrackerProviders();
224
-
225
- return (
226
- <div>
227
- {/* Producer tree: Registers column data */}
228
- <Table>
229
- <Colgroup>
230
- <ColumnProducerProvider>
231
- {columns.map((col) => (
232
- <Col key={col.id} {...col} />
233
- ))}
234
- </ColumnProducerProvider>
235
- </Colgroup>
236
- {/* Table content */}
237
- </Table>
238
-
239
- {/* Consumer tree: Reads column data */}
240
- <ColumnConsumerProvider>
241
- <TableControls />
242
- <ColumnSummary />
243
- </ColumnConsumerProvider>
244
- </div>
245
- );
246
- }
247
-
248
- function Col({ id, label, width, sortable }) {
249
- const columnIndex = useRegisterColumn({ id, label, width, sortable });
250
- return <col style={{ width }} />;
251
- }
252
-
253
- function TableControls() {
254
- const columns = useColumns(); // Access all columns
255
- return (
256
- <div>
257
- {columns.map((col, index) => (
258
- <ColumnToggle key={col.id} columnIndex={index} />
259
- ))}
260
- </div>
261
- );
262
- }
263
-
264
- function ColumnToggle({ columnIndex }) {
265
- const column = useColumn(columnIndex); // Access specific column
266
- return (
267
- <button>
268
- Toggle {column.label} ({column.width})
269
- </button>
270
- );
271
- }
272
- ```
273
-
274
- ## Key Architecture Benefits
275
-
276
- ### ✅ No Infinite Re-renders
277
-
278
- **Producer side uses refs** - Item registration doesn't trigger re-renders:
279
-
280
- ```jsx
281
- // This is safe - no state updates during render
282
- const index = useRegisterItem(data);
283
- ```
284
-
285
- **Consumer side uses state** - Only consumers re-render when data changes:
286
-
287
- ```jsx
288
- // Only this component re-renders when items change
289
- const items = useTrackedItems();
290
- ```
291
-
292
- ### ✅ True Component Composition
293
-
294
- Register items anywhere in the producer tree:
295
-
296
- ```jsx
297
- <ColumnProducerProvider>
298
- <div>
299
- <SomeWrapper>
300
- <Col id="name" width="200px" />
301
- </SomeWrapper>
302
- </div>
303
- <AnotherComponent>
304
- <Col id="email" width="300px" />
305
- </AnotherComponent>
306
- </ColumnProducerProvider>
307
- ```
308
-
309
- Consume items anywhere in the consumer tree:
310
-
311
- ```jsx
312
- <ColumnConsumerProvider>
313
- <Header>
314
- <ColumnSummary /> {/* Reads all columns */}
315
- </Header>
316
- <Sidebar>
317
- <ColumnFilter columnIndex={0} /> {/* Reads specific column */}
318
- </Sidebar>
319
- </ColumnConsumerProvider>
320
- ```
321
-
322
- ### ✅ Controlled Synchronization
323
-
324
- The isolated tracker synchronizes data at controlled moments:
325
-
326
- - After producer tree renders completely
327
- - Before consumer tree renders
328
- - No intermediate state or partial updates
329
-
330
- ### ✅ Handles Dynamic Lists
331
-
332
- Add, remove, and reorder items without breaking:
333
-
334
- ```jsx
335
- // Columns can be added/removed dynamically
336
- {
337
- dynamicColumns.map((col) => <ColumnDefinition key={col.id} {...col} />);
338
- }
339
- ```
340
-
341
- ## When to Use Which System
342
-
343
- ### Use Simple Item Tracker when:
344
-
345
- - Registration and consumption happen in the same component tree
346
- - Parent-child relationships exist between producers and consumers
347
- - You need a straightforward, lightweight solution
348
-
349
- **Examples:**
350
-
351
- - Table rows registering themselves for cell access
352
- - Navigation items registering for keyboard navigation
353
- - Form fields registering for validation
354
-
355
- ### Use Isolated Item Tracker when:
356
-
357
- - Registration and consumption happen in separate component trees
358
- - No parent-child relationship exists between producers and consumers
359
- - You need complex synchronization between distant components
360
-
361
- **Examples:**
362
-
363
- - HTML table colgroup → tbody communication
364
- - Sidebar filters reading main content structure
365
- - Toolbar controls accessing editor content
366
- - Dashboard widgets reading data source definitions
367
-
368
- ## HTML Table Use Case (Primary Motivation)
369
-
370
- The isolated tracker was specifically designed for HTML table structures:
371
-
372
- ```jsx
373
- <Table>
374
- {/* PRODUCER: Column definitions register metadata */}
375
- <ColumnProducerProvider>
376
- <Colgroup>
377
- <Col
378
- id="name"
379
- label="Full Name"
380
- width="200px"
381
- sortable={true}
382
- filterable={true}
383
- />
384
- <Col
385
- id="email"
386
- label="Email Address"
387
- width="300px"
388
- sortable={true}
389
- filterable={false}
390
- />
391
- </Colgroup>
392
- </ColumnProducerProvider>
393
-
394
- {/* CONSUMER: Table cells read column metadata */}
395
- <ColumnConsumerProvider>
396
- <Thead>
397
- <Tr>
398
- <TableCell columnIndex={0} /> {/* Reads name column */}
399
- <TableCell columnIndex={1} /> {/* Reads email column */}
400
- </Tr>
401
- </Thead>
402
- <Tbody>
403
- {data.map((row) => (
404
- <Tr key={row.id}>
405
- <TableCell columnIndex={0} value={row.name} />
406
- <TableCell columnIndex={1} value={row.email} />
407
- </Tr>
408
- ))}
409
- </Tbody>
410
- </ColumnConsumerProvider>
411
- </Table>
412
- ```
413
-
414
- This enables:
415
-
416
- - **Semantic HTML structure** - Proper `<colgroup>` usage
417
- - **Column metadata sharing** - Headers and cells access the same data
418
- - **Dynamic column management** - Add/remove columns without breaking
419
- - **Rich interactions** - Sorting, filtering, resizing based on column config
420
- - **Accessibility** - ARIA attributes based on column metadata
421
-
422
- ## API Reference
423
-
424
- ### Simple Item Tracker
425
-
426
- ```jsx
427
- const [
428
- useItemTrackerProvider, // () => Provider component
429
- useTrackItem, // (data) => index
430
- useTrackedItem, // (index) => data
431
- useTrackedItems, // () => data[]
432
- ] = createItemTracker();
433
- ```
434
-
435
- ### Isolated Item Tracker
436
-
437
- ```jsx
438
- const [
439
- useItemTrackerProviders, // () => [ProducerProvider, ConsumerProvider]
440
- useRegisterItem, // (data) => index (use in producer)
441
- useTrackedItem, // (index) => data (use in consumer)
442
- useTrackedItems, // () => data[] (use in consumer)
443
- ] = createIsolatedItemTracker();
444
- ```
445
-
446
- ## Performance Characteristics
447
-
448
- - **Producer registration**: O(1) - No re-renders, direct ref updates
449
- - **Consumer access**: O(1) - Direct array access by index
450
- - **Synchronization**: O(n) - One-time copy from refs to state
451
- - **Memory**: O(n) - Stores one copy in refs, one copy in state during sync
452
-
453
- ## Browser Support
454
-
455
- Requires modern JavaScript features:
456
-
457
- - ES Modules
458
- - Preact/React hooks
459
- - `useLayoutEffect` for synchronization timing
460
-
461
- Compatible with all modern browsers and Node.js environments.
@@ -1,209 +0,0 @@
1
- // https://github.com/reach/reach-ui/tree/b3d94d22811db6b5c0f272b9a7e2e3c1bb4699ae/packages/descendants
2
- // https://github.com/pacocoursey/use-descendants/tree/master
3
-
4
- /*
5
- * Item Tracker Isolated System - A Preact hook for tracking dynamic lists without infinite re-renders
6
- *
7
- * USE CASE:
8
- * This is specifically designed for scenarios where item registration and usage are SEPARATE,
9
- * such as HTML tables with colgroup elements where colgroup registers columns and tbody uses them.
10
- *
11
- * For simpler cases where item definition and usage are colocated (same component tree),
12
- * prefer useItemTracker instead.
13
- *
14
- * SOLUTION ARCHITECTURE:
15
- * This system uses a Producer/Consumer pattern with separate context trees:
16
- *
17
- * 1. PRODUCER SIDE (ref-based, no re-renders):
18
- * - ItemProducerProvider: Manages item registration without causing re-renders
19
- * - useTrackItem: Registers individual items using refs
20
- * - Items are stored in a mutable array via useRef
21
- *
22
- * 2. CONSUMER SIDE (state-based, re-renders when needed):
23
- * - ItemConsumerProvider: Manages reactive state for consumers
24
- * - useTrackedItems/useTrackedItem: Read the tracked items with reactivity
25
- * - State is synchronized from producer side at controlled intervals
26
- *
27
- * RENDER ORDER REQUIREMENT:
28
- * Producer MUST render before Consumer in the React tree for proper synchronization.
29
- */
30
-
31
- import { createContext } from "preact";
32
- import {
33
- useContext,
34
- useLayoutEffect,
35
- useMemo,
36
- useRef,
37
- useState,
38
- } from "preact/hooks";
39
-
40
- import { compareTwoJsValues } from "../../utils/compare_two_js_values.js";
41
-
42
- export const createIsolatedItemTracker = () => {
43
- // Producer contexts (ref-based, no re-renders)
44
- const ProducerTrackerContext = createContext();
45
- const ProducerItemCountRefContext = createContext();
46
- const ProducerListRenderIdContext = createContext();
47
-
48
- // Consumer contexts (state-based, re-renders)
49
- const ConsumerItemsContext = createContext();
50
-
51
- const useIsolatedItemTrackerProvider = () => {
52
- const itemsRef = useRef([]);
53
- const items = itemsRef.current;
54
- const itemCountRef = useRef();
55
- const pendingFlushRef = useRef(false);
56
- const producerIsRenderingRef = useRef(false);
57
-
58
- const itemTracker = useMemo(() => {
59
- const registerItem = (index, value) => {
60
- const hasValue = index in items;
61
- if (hasValue) {
62
- const currentValue = items[index];
63
- if (compareTwoJsValues(currentValue, value)) {
64
- return;
65
- }
66
- }
67
-
68
- items[index] = value;
69
-
70
- if (producerIsRenderingRef.current) {
71
- // Consumer will sync after producer render completes
72
- return;
73
- }
74
-
75
- pendingFlushRef.current = true;
76
- };
77
-
78
- const getProducerItem = (itemIndex) => {
79
- return items[itemIndex];
80
- };
81
-
82
- const ItemProducerProvider = ({ children }) => {
83
- items.length = 0;
84
- itemCountRef.current = 0;
85
- pendingFlushRef.current = false;
86
- producerIsRenderingRef.current = true;
87
- const listRenderId = {};
88
-
89
- useLayoutEffect(() => {
90
- producerIsRenderingRef.current = false;
91
- });
92
-
93
- // CRITICAL: Sync consumer state on subsequent renders
94
- const renderedOnce = useRef(false);
95
- useLayoutEffect(() => {
96
- if (!renderedOnce.current) {
97
- renderedOnce.current = true;
98
- return;
99
- }
100
- pendingFlushRef.current = true;
101
- itemTracker.flushToConsumers();
102
- }, [listRenderId]);
103
-
104
- return (
105
- <ProducerItemCountRefContext.Provider value={itemCountRef}>
106
- <ProducerListRenderIdContext.Provider value={listRenderId}>
107
- <ProducerTrackerContext.Provider value={itemTracker}>
108
- {children}
109
- </ProducerTrackerContext.Provider>
110
- </ProducerListRenderIdContext.Provider>
111
- </ProducerItemCountRefContext.Provider>
112
- );
113
- };
114
-
115
- const ItemConsumerProvider = ({ children }) => {
116
- const [consumerItems, setConsumerItems] = useState(items);
117
-
118
- const flushToConsumers = () => {
119
- if (!pendingFlushRef.current) {
120
- return;
121
- }
122
- const itemsCopy = [...items];
123
- pendingFlushRef.current = false;
124
- setConsumerItems(itemsCopy);
125
- };
126
- itemTracker.flushToConsumers = flushToConsumers;
127
-
128
- useLayoutEffect(() => {
129
- flushToConsumers();
130
- });
131
-
132
- return (
133
- <ConsumerItemsContext.Provider value={consumerItems}>
134
- {children}
135
- </ConsumerItemsContext.Provider>
136
- );
137
- };
138
-
139
- return {
140
- pendingFlushRef,
141
- registerItem,
142
- getProducerItem,
143
- ItemProducerProvider,
144
- ItemConsumerProvider,
145
- };
146
- }, []);
147
-
148
- const { ItemProducerProvider, ItemConsumerProvider } = itemTracker;
149
-
150
- return [ItemProducerProvider, ItemConsumerProvider, items];
151
- };
152
-
153
- // Hook for producers to register items (ref-based, no re-renders)
154
- const useTrackIsolatedItem = (data) => {
155
- const listRenderId = useContext(ProducerListRenderIdContext);
156
- const itemCountRef = useContext(ProducerItemCountRefContext);
157
- const itemTracker = useContext(ProducerTrackerContext);
158
- const listRenderIdRef = useRef();
159
- const itemIndexRef = useRef();
160
- const dataRef = useRef();
161
- const prevListRenderId = listRenderIdRef.current;
162
-
163
- useLayoutEffect(() => {
164
- if (itemTracker.pendingFlushRef.current) {
165
- itemTracker.flushToConsumers();
166
- }
167
- });
168
-
169
- if (prevListRenderId === listRenderId) {
170
- const itemIndex = itemIndexRef.current;
171
- itemTracker.registerItem(itemIndex, data);
172
- dataRef.current = data;
173
- return itemIndex;
174
- }
175
-
176
- listRenderIdRef.current = listRenderId;
177
- const itemCount = itemCountRef.current;
178
- const itemIndex = itemCount;
179
- itemCountRef.current = itemIndex + 1;
180
- itemIndexRef.current = itemIndex;
181
- dataRef.current = data;
182
- itemTracker.registerItem(itemIndex, data);
183
- return itemIndex;
184
- };
185
-
186
- const useTrackedIsolatedItem = (itemIndex) => {
187
- const items = useTrackedIsolatedItems();
188
- const item = items[itemIndex];
189
- return item;
190
- };
191
-
192
- // Hooks for consumers to read items (state-based, re-renders)
193
- const useTrackedIsolatedItems = () => {
194
- const consumerItems = useContext(ConsumerItemsContext);
195
- if (!consumerItems) {
196
- throw new Error(
197
- "useTrackedIsolatedItems must be used within <ItemConsumerProvider />",
198
- );
199
- }
200
- return consumerItems;
201
- };
202
-
203
- return [
204
- useIsolatedItemTrackerProvider,
205
- useTrackIsolatedItem,
206
- useTrackedIsolatedItem,
207
- useTrackedIsolatedItems,
208
- ];
209
- };