@jsenv/navi 0.0.1 → 0.1.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 (138) hide show
  1. package/dist/jsenv_navi.js +22954 -0
  2. package/index.js +66 -16
  3. package/package.json +22 -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/{input → field}/input_textual.jsx +247 -173
  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/radio_list.jsx +0 -283
  129. package/src/components/input/use_form_event.js +0 -20
  130. package/src/components/input/use_on_change.js +0 -12
  131. package/src/components/selection/selection.js +0 -5
  132. package/src/components/selection/selection_context.jsx +0 -262
  133. package/src/components/shortcut/shortcut_context.jsx +0 -390
  134. package/src/components/use_action_events.js +0 -37
  135. package/src/utils/iterable_weak_set.js +0 -62
  136. /package/src/components/demos/action/{11_nested_shortcuts_demo.html → 11_nested_shortcuts_demo.xhtml} +0 -0
  137. /package/src/components/{shortcut → keyboard_shortcuts}/os.js +0 -0
  138. /package/src/route/{route.test.html → route.xtest.html} +0 -0
@@ -61,6 +61,24 @@
61
61
  border-color: #bee5eb;
62
62
  color: #0c5460;
63
63
  }
64
+ nav a {
65
+ color: #007bff;
66
+ text-decoration: none;
67
+ }
68
+ nav a:hover {
69
+ text-decoration: underline;
70
+ }
71
+ h2 {
72
+ margin-top: 40px;
73
+ margin-bottom: 20px;
74
+ padding-top: 20px;
75
+ border-top: 2px solid #e0e0e0;
76
+ }
77
+ h2:first-of-type {
78
+ margin-top: 0;
79
+ border-top: none;
80
+ padding-top: 0;
81
+ }
64
82
  .button-group {
65
83
  display: flex;
66
84
  gap: 10px;
@@ -77,23 +95,17 @@
77
95
  button:hover {
78
96
  background: #f8f9fa;
79
97
  }
80
- button[type="submit"] {
81
- background: #007bff;
82
- color: white;
83
- border-color: #007bff;
84
- }
85
- button[type="submit"]:hover {
86
- background: #0056b3;
87
- }
88
- button[type="reset"] {
89
- background: #6c757d;
90
- color: white;
91
- border-color: #6c757d;
98
+ .control-group {
99
+ margin-bottom: 15px;
92
100
  }
93
- button[type="reset"]:hover {
94
- background: #545b62;
101
+ label {
102
+ margin-bottom: 15px;
103
+ font-weight: 500;
104
+ color: #555;
95
105
  }
96
- input {
106
+ label input {
107
+ display: block;
108
+ margin-top: 5px;
97
109
  width: 100%;
98
110
  padding: 8px 12px;
99
111
  border: 1px solid #ccc;
@@ -101,15 +113,6 @@
101
113
  font-size: 14px;
102
114
  box-sizing: border-box;
103
115
  }
104
- .control-group {
105
- margin-bottom: 15px;
106
- }
107
- label {
108
- display: block;
109
- margin-bottom: 5px;
110
- font-weight: 500;
111
- color: #555;
112
- }
113
116
  </style>
114
117
  </head>
115
118
  <body>
@@ -119,8 +122,14 @@
119
122
  <script type="module" jsenv-type="module/jsx">
120
123
  import { render } from "preact";
121
124
  import { useState } from "preact/hooks";
122
- // eslint-disable-next-line no-unused-vars
123
- import { Field, Input, Form, Button, createAction } from "@jsenv/navi";
125
+ import {
126
+ // eslint-disable-next-line no-unused-vars
127
+ Input,
128
+ // eslint-disable-next-line no-unused-vars
129
+ Form,
130
+ // eslint-disable-next-line no-unused-vars
131
+ Button,
132
+ } from "@jsenv/navi";
124
133
 
125
134
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
126
135
 
@@ -128,18 +137,501 @@
128
137
  const App = () => {
129
138
  return (
130
139
  <>
140
+ <div className="demo-card" style={{ marginBottom: "30px" }}>
141
+ <h2>Input Text Demo - Overview</h2>
142
+ <p>
143
+ This demo showcases different ways to use the Input component
144
+ with text inputs.
145
+ </p>
146
+ <nav>
147
+ <h3>Sections:</h3>
148
+ <ul style={{ marginLeft: "20px" }}>
149
+ <li>
150
+ <a href="#basic-inputs">
151
+ 1. Basic Input (no action/no form)
152
+ </a>{" "}
153
+ - Simple inputs with value handling
154
+ </li>
155
+ <li>
156
+ <a href="#input-actions">2. Input with Actions</a> - Inputs
157
+ that trigger actions on value change, including error
158
+ handling
159
+ </li>
160
+ <li>
161
+ <a href="#input-forms">3. Input inside Form</a> - Form
162
+ integration patterns
163
+ </li>
164
+ <li>
165
+ <a href="#datetime-local">4. Datetime-Local Inputs</a> -
166
+ Special handling for datetime inputs with timezone
167
+ conversion
168
+ </li>
169
+ </ul>
170
+ </nav>
171
+ </div>
172
+
173
+ <h2 id="basic-inputs">1. Basic Input (no action/no form)</h2>
174
+ <div className="demo-grid">
175
+ <BasicInputNoActionDemo />
176
+ <InputWithDefaultValueDemo />
177
+ </div>
178
+
179
+ <h2 id="input-actions">2. Input with Actions</h2>
131
180
  <div className="demo-grid">
132
181
  <BasicInputDemo />
133
182
  <InputWithInitialValueDemo />
134
183
  <InputWithExternalControlDemo />
184
+ <InputWithErrorDemo />
185
+ <InputWithValidationErrorDemo />
186
+ </div>
187
+
188
+ <h2 id="input-forms">3. Input inside Form</h2>
189
+ <div className="demo-grid">
135
190
  <FormInputDemo />
136
- <FormWithExternalControlDemo />
137
- <SharedActionDemo />
191
+ <FormInputErrorDemo />
192
+ </div>
193
+
194
+ <h2 id="datetime-local">4. Datetime-Local Inputs</h2>
195
+ <div className="demo-grid">
196
+ <DateTimeLocalDemo />
197
+ <DateTimeLocalFormDemo />
138
198
  </div>
139
199
  </>
140
200
  );
141
201
  };
142
202
 
203
+ // eslint-disable-next-line no-unused-vars
204
+ const DateTimeLocalDemo = () => {
205
+ // External state (what the server/app knows) - stored as UTC ISO string
206
+ const [externalDateTime, setExternalDateTime] = useState(
207
+ "2024-01-15T14:30:00Z", // UTC time
208
+ );
209
+
210
+ // UI state tracking
211
+ const [uiDateTime, setUiDateTime] = useState(externalDateTime);
212
+ const [actionValue, setActionValue] = useState("");
213
+ const [result, setResult] = useState("");
214
+ const [isLoading, setIsLoading] = useState(false);
215
+
216
+ const saveDateTime = async (dateTimeValue) => {
217
+ setIsLoading(true);
218
+ setActionValue(dateTimeValue || "(empty)");
219
+ setResult("Saving datetime...");
220
+ try {
221
+ await delay(1000);
222
+
223
+ // Simulate server response
224
+ const response = {
225
+ saved: dateTimeValue,
226
+ message: `Datetime saved successfully`,
227
+ };
228
+
229
+ // Update external state to match what was saved
230
+ setExternalDateTime(dateTimeValue);
231
+ setResult(`✅ ${response.message}\\nSaved: ${dateTimeValue}`);
232
+ return response;
233
+ } catch (error) {
234
+ setResult(`❌ Save failed: ${error.message}`);
235
+ throw error;
236
+ } finally {
237
+ setIsLoading(false);
238
+ }
239
+ };
240
+
241
+ return (
242
+ <div className="demo-card">
243
+ <h3 className="demo-title">Datetime-Local State Demo</h3>
244
+
245
+ {/* External State Control */}
246
+ <div
247
+ style={{
248
+ marginBottom: "20px",
249
+ padding: "12px",
250
+ background: "#e3f2fd",
251
+ borderRadius: "4px",
252
+ }}
253
+ >
254
+ <h4
255
+ style={{
256
+ margin: "0 0 10px 0",
257
+ fontSize: "14px",
258
+ color: "#1976d2",
259
+ }}
260
+ >
261
+ 📊 External State (UTC)
262
+ </h4>
263
+ <div
264
+ style={{
265
+ fontFamily: "monospace",
266
+ fontSize: "12px",
267
+ marginBottom: "10px",
268
+ }}
269
+ >
270
+ Current: {externalDateTime || "(not set)"}
271
+ </div>
272
+ <div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
273
+ <button
274
+ onClick={() => setExternalDateTime("2024-01-15T14:30:00Z")}
275
+ style={{ padding: "4px 8px", fontSize: "12px" }}
276
+ >
277
+ Set Jan 15, 2:30 PM UTC
278
+ </button>
279
+ <button
280
+ onClick={() => setExternalDateTime("2024-12-25T09:00:00Z")}
281
+ style={{ padding: "4px 8px", fontSize: "12px" }}
282
+ >
283
+ Set Dec 25, 9:00 AM UTC
284
+ </button>
285
+ <button
286
+ onClick={() => setExternalDateTime(null)}
287
+ style={{ padding: "4px 8px", fontSize: "12px" }}
288
+ >
289
+ Clear
290
+ </button>
291
+ </div>
292
+ </div>
293
+
294
+ {/* UI Input */}
295
+ <div className="control-group">
296
+ <label>
297
+ Datetime Input (Local Time):
298
+ <Input
299
+ type="datetime-local"
300
+ name="datetime"
301
+ value={externalDateTime} // External state controls the input
302
+ onUIStateChange={(datetime) => {
303
+ setUiDateTime(datetime);
304
+ }}
305
+ action={saveDateTime}
306
+ />
307
+ </label>
308
+ </div>
309
+
310
+ {/* State Display */}
311
+ <div style={{ marginBottom: "15px" }}>
312
+ <div
313
+ style={{ fontSize: "12px", color: "#666", marginBottom: "8px" }}
314
+ >
315
+ <strong>UI State:</strong> {uiDateTime || "(empty)"}
316
+ </div>
317
+ <div
318
+ style={{ fontSize: "12px", color: "#666", marginBottom: "8px" }}
319
+ >
320
+ <strong>Action receives:</strong>{" "}
321
+ {actionValue || "(waiting for action...)"}
322
+ </div>
323
+ </div>
324
+
325
+ <div
326
+ className={`result-display ${
327
+ isLoading
328
+ ? "result-loading"
329
+ : result.startsWith("✅")
330
+ ? "result-success"
331
+ : result.startsWith("❌")
332
+ ? "result-error"
333
+ : ""
334
+ }`}
335
+ >
336
+ {result ||
337
+ "Change the datetime and blur/enter to trigger action..."}
338
+ </div>
339
+ </div>
340
+ );
341
+ };
342
+
343
+ // eslint-disable-next-line no-unused-vars
344
+ const DateTimeLocalFormDemo = () => {
345
+ const [externalDateTime, setExternalDateTime] = useState(
346
+ "2024-06-01T10:00:00Z",
347
+ );
348
+ const [uiDateTime, setUiDateTime] = useState(externalDateTime);
349
+ const [result, setResult] = useState("");
350
+ const [isLoading, setIsLoading] = useState(false);
351
+
352
+ const submitForm = async (formData) => {
353
+ setIsLoading(true);
354
+ setResult("Submitting form...");
355
+ try {
356
+ await delay(1200);
357
+
358
+ const response = {
359
+ datetime: formData.appointmentTime,
360
+ message: "Appointment scheduled successfully",
361
+ };
362
+
363
+ // Update external state
364
+ setExternalDateTime(formData.appointmentTime);
365
+ setResult(
366
+ `✅ ${response.message}\\nScheduled for: ${formData.appointmentTime}`,
367
+ );
368
+ return response;
369
+ } catch (error) {
370
+ setResult(`❌ Form error: ${error.message}`);
371
+ throw error;
372
+ } finally {
373
+ setIsLoading(false);
374
+ }
375
+ };
376
+
377
+ const getResetValue = () => {
378
+ return externalDateTime || "(not set)";
379
+ };
380
+
381
+ return (
382
+ <div className="demo-card">
383
+ <h3 className="demo-title">Datetime-Local in Form</h3>
384
+
385
+ {/* External State Control */}
386
+ <div
387
+ style={{
388
+ marginBottom: "20px",
389
+ padding: "12px",
390
+ background: "#e3f2fd",
391
+ borderRadius: "4px",
392
+ }}
393
+ >
394
+ <h4
395
+ style={{
396
+ margin: "0 0 10px 0",
397
+ fontSize: "14px",
398
+ color: "#1976d2",
399
+ }}
400
+ >
401
+ 📊 External State Control (UTC)
402
+ </h4>
403
+ <div
404
+ style={{
405
+ fontFamily: "monospace",
406
+ fontSize: "12px",
407
+ marginBottom: "10px",
408
+ }}
409
+ >
410
+ Current: {externalDateTime || "(not set)"}
411
+ </div>
412
+ <div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
413
+ <button
414
+ onClick={() => setExternalDateTime("2024-03-15T09:00:00Z")}
415
+ style={{ padding: "4px 8px", fontSize: "12px" }}
416
+ >
417
+ Set Mar 15, 9:00 AM UTC
418
+ </button>
419
+ <button
420
+ onClick={() => setExternalDateTime("2024-08-20T16:30:00Z")}
421
+ style={{ padding: "4px 8px", fontSize: "12px" }}
422
+ >
423
+ Set Aug 20, 4:30 PM UTC
424
+ </button>
425
+ <button
426
+ onClick={() => setExternalDateTime("2024-12-31T23:59:00Z")}
427
+ style={{ padding: "4px 8px", fontSize: "12px" }}
428
+ >
429
+ Set New Year's Eve
430
+ </button>
431
+ <button
432
+ onClick={() => setExternalDateTime(null)}
433
+ style={{ padding: "4px 8px", fontSize: "12px" }}
434
+ >
435
+ Clear
436
+ </button>
437
+ </div>
438
+ </div>
439
+
440
+ <Form action={submitForm}>
441
+ <div className="control-group">
442
+ <label>
443
+ Appointment Time:
444
+ <Input
445
+ type="datetime-local"
446
+ name="appointmentTime"
447
+ value={externalDateTime}
448
+ onUIStateChange={setUiDateTime}
449
+ />
450
+ </label>
451
+ </div>
452
+
453
+ <div
454
+ style={{
455
+ fontSize: "12px",
456
+ color: "#666",
457
+ marginBottom: "10px",
458
+ }}
459
+ >
460
+ <strong>UI State:</strong> {uiDateTime || "(empty)"}
461
+ </div>
462
+
463
+ <div className="button-group">
464
+ <Button type="submit">
465
+ Schedule Appointment
466
+ {uiDateTime && (
467
+ <span style={{ fontSize: "11px", opacity: "0.7" }}>
468
+ {" "}
469
+ ({uiDateTime})
470
+ </span>
471
+ )}
472
+ </Button>
473
+ <Button type="reset">
474
+ Reset to Saved
475
+ <span style={{ fontSize: "11px", opacity: "0.7" }}>
476
+ {" "}
477
+ ({getResetValue()})
478
+ </span>
479
+ </Button>
480
+ </div>
481
+ </Form>
482
+
483
+ <div
484
+ className={`result-display ${
485
+ isLoading
486
+ ? "result-loading"
487
+ : result.startsWith("✅")
488
+ ? "result-success"
489
+ : result.startsWith("❌")
490
+ ? "result-error"
491
+ : ""
492
+ }`}
493
+ >
494
+ {result || "Select a datetime and submit..."}
495
+ </div>
496
+ </div>
497
+ );
498
+ };
499
+
500
+ // eslint-disable-next-line no-unused-vars
501
+ const BasicInputNoActionDemo = () => {
502
+ const [result, setResult] = useState("");
503
+
504
+ return (
505
+ <div className="demo-card">
506
+ <h3 className="demo-title">Basic Input (No Action)</h3>
507
+ <div className="control-group">
508
+ <label>
509
+ Enter some text:
510
+ <Input
511
+ name="text"
512
+ placeholder="Type something..."
513
+ onUIStateChange={(text) => {
514
+ setResult(text ? `Current value: "${text}"` : "");
515
+ }}
516
+ />
517
+ </label>
518
+ </div>
519
+ <div className="result-display">
520
+ {result || "Type in the input to see the value..."}
521
+ </div>
522
+ </div>
523
+ );
524
+ };
525
+
526
+ // eslint-disable-next-line no-unused-vars
527
+ const InputWithDefaultValueDemo = () => {
528
+ return (
529
+ <div className="demo-card">
530
+ <h3 className="demo-title">Input with Default Value</h3>
531
+ <div className="control-group">
532
+ <label>
533
+ Input with default value:
534
+ <Input
535
+ name="text"
536
+ defaultValue="Hello World!"
537
+ placeholder="Enter text..."
538
+ />
539
+ </label>
540
+ </div>
541
+ <div className="result-display">
542
+ Input starts with default value.
543
+ </div>
544
+ </div>
545
+ );
546
+ };
547
+
548
+ // eslint-disable-next-line no-unused-vars
549
+ const InputWithErrorDemo = () => {
550
+ const [result, setResult] = useState("");
551
+ const [isLoading, setIsLoading] = useState(false);
552
+
553
+ return (
554
+ <div className="demo-card">
555
+ <h3 className="demo-title">Input with Action (Always Fails)</h3>
556
+ <div className="control-group">
557
+ <label>
558
+ Enter text (will always fail):
559
+ <Input
560
+ name="text"
561
+ placeholder="Type something..."
562
+ action={async (text) => {
563
+ setIsLoading(true);
564
+ setResult("Processing...");
565
+ try {
566
+ await delay(1200);
567
+ throw new Error(`Processing failed for input: "${text}"`);
568
+ } catch (error) {
569
+ setResult(`❌ ${error.message}`);
570
+ throw error;
571
+ } finally {
572
+ setIsLoading(false);
573
+ }
574
+ }}
575
+ />
576
+ </label>
577
+ </div>
578
+ <div
579
+ className={`result-display ${isLoading ? "result-loading" : result.startsWith("❌") ? "result-error" : ""}`}
580
+ >
581
+ {result || "This input action will always fail..."}
582
+ </div>
583
+ </div>
584
+ );
585
+ };
586
+
587
+ // eslint-disable-next-line no-unused-vars
588
+ const InputWithValidationErrorDemo = () => {
589
+ const [result, setResult] = useState("");
590
+ const [isLoading, setIsLoading] = useState(false);
591
+
592
+ return (
593
+ <div className="demo-card">
594
+ <h3 className="demo-title">
595
+ Input with Validation (Fails on Short Text)
596
+ </h3>
597
+ <div className="control-group">
598
+ <label>
599
+ Enter text (min 5 characters):
600
+ <Input
601
+ name="text"
602
+ placeholder="Must be at least 5 characters..."
603
+ action={async (text) => {
604
+ setIsLoading(true);
605
+ setResult("Validating...");
606
+ try {
607
+ await delay(800);
608
+ if (!text || text.length < 5) {
609
+ throw new Error(
610
+ "Text must be at least 5 characters long",
611
+ );
612
+ }
613
+ const response = `✅ Valid input: "${text}" (${text.length} characters)`;
614
+ setResult(response);
615
+ return response;
616
+ } catch (error) {
617
+ setResult(`❌ Validation failed: ${error.message}`);
618
+ throw error;
619
+ } finally {
620
+ setIsLoading(false);
621
+ }
622
+ }}
623
+ />
624
+ </label>
625
+ </div>
626
+ <div
627
+ className={`result-display ${isLoading ? "result-loading" : result.startsWith("✅") ? "result-success" : result.startsWith("❌") ? "result-error" : ""}`}
628
+ >
629
+ {result || "Enter at least 5 characters to pass validation..."}
630
+ </div>
631
+ </div>
632
+ );
633
+ };
634
+
143
635
  // eslint-disable-next-line no-unused-vars
144
636
  const BasicInputDemo = () => {
145
637
  const [result, setResult] = useState("");
@@ -149,27 +641,28 @@
149
641
  <div className="demo-card">
150
642
  <h3 className="demo-title">Basic Input with Action</h3>
151
643
  <div className="control-group">
152
- <label htmlFor="basic-input">Enter some text:</label>
153
- <Input
154
- id="basic-input"
155
- name="text"
156
- placeholder="Type something..."
157
- action={async ({ text }) => {
158
- setIsLoading(true);
159
- setResult("Processing...");
160
- try {
161
- await delay(1000);
162
- const response = `✅ Processed: "${text}"`;
163
- setResult(response);
164
- return response;
165
- } catch (error) {
166
- setResult(`❌ Error: ${error.message}`);
167
- throw error;
168
- } finally {
169
- setIsLoading(false);
170
- }
171
- }}
172
- />
644
+ <label>
645
+ Enter some text:
646
+ <Input
647
+ name="text"
648
+ placeholder="Type something..."
649
+ action={async (text) => {
650
+ setIsLoading(true);
651
+ setResult("Processing...");
652
+ try {
653
+ await delay(1000);
654
+ const response = `✅ Processed: "${text}"`;
655
+ setResult(response);
656
+ return response;
657
+ } catch (error) {
658
+ setResult(`❌ Error: ${error.message}`);
659
+ throw error;
660
+ } finally {
661
+ setIsLoading(false);
662
+ }
663
+ }}
664
+ />
665
+ </label>
173
666
  </div>
174
667
  <div
175
668
  className={`result-display ${isLoading ? "result-loading" : result.startsWith("✅") ? "result-success" : result.startsWith("❌") ? "result-error" : ""}`}
@@ -182,25 +675,28 @@
182
675
 
183
676
  // eslint-disable-next-line no-unused-vars
184
677
  const InputWithInitialValueDemo = () => {
678
+ const [value, setValue] = useState("Hello World!");
185
679
  const [result, setResult] = useState("");
186
680
 
187
681
  return (
188
682
  <div className="demo-card">
189
683
  <h3 className="demo-title">Input with Initial Value</h3>
190
684
  <div className="control-group">
191
- <label htmlFor="initial-input">Input with preset value:</label>
192
- <Input
193
- id="initial-input"
194
- name="text"
195
- value="Hello World!"
196
- action={async ({ text }) => {
197
- setResult("Processing...");
198
- await delay(800);
199
- const response = `✅ Submitted: "${text}"`;
200
- setResult(response);
201
- return response;
202
- }}
203
- />
685
+ <label>
686
+ Input with preset value:
687
+ <Input
688
+ name="text"
689
+ value={value}
690
+ onUIStateChange={setValue}
691
+ action={async (text) => {
692
+ setResult("Processing...");
693
+ await delay(800);
694
+ const response = `✅ Submitted: "${text}"`;
695
+ setResult(response);
696
+ return response;
697
+ }}
698
+ />
699
+ </label>
204
700
  </div>
205
701
  <div
206
702
  className={`result-display ${result.startsWith("✅") ? "result-success" : ""}`}
@@ -213,48 +709,48 @@
213
709
 
214
710
  // eslint-disable-next-line no-unused-vars
215
711
  const InputWithExternalControlDemo = () => {
216
- const [inputValue, setInputValue] = useState(undefined);
712
+ const [value, setValue] = useState(undefined);
217
713
  const [result, setResult] = useState("");
218
714
 
219
715
  return (
220
716
  <div className="demo-card">
221
717
  <h3 className="demo-title">Input with External Control</h3>
222
718
  <div className="control-group">
223
- <label htmlFor="controlled-input">
719
+ <label>
224
720
  Externally controlled input:
721
+ <Input
722
+ name="text"
723
+ value={value}
724
+ placeholder="Use buttons below to set value"
725
+ action={async (text) => {
726
+ setResult("Processing...");
727
+ await delay(600);
728
+ const response = `✅ Action executed with: "${text}"`;
729
+ setResult(response);
730
+ setValue(text);
731
+ return response;
732
+ }}
733
+ />
225
734
  </label>
226
- <Input
227
- id="controlled-input"
228
- name="text"
229
- value={inputValue}
230
- placeholder="Use buttons below to set value"
231
- action={async ({ text }) => {
232
- setResult("Processing...");
233
- await delay(600);
234
- const response = `✅ Action executed with: "${text}"`;
235
- setResult(response);
236
- return response;
237
- }}
238
- />
239
735
  </div>
240
736
  <div className="button-group">
241
737
  <button
242
738
  onClick={() => {
243
- setInputValue("Preset Value 1");
739
+ setValue("Preset Value 1");
244
740
  }}
245
741
  >
246
742
  Set Value 1
247
743
  </button>
248
744
  <button
249
745
  onClick={() => {
250
- setInputValue("Preset Value 2");
746
+ setValue("Preset Value 2");
251
747
  }}
252
748
  >
253
749
  Set Value 2
254
750
  </button>
255
751
  <button
256
752
  onClick={() => {
257
- setInputValue("");
753
+ setValue("");
258
754
  }}
259
755
  >
260
756
  Clear
@@ -263,7 +759,7 @@
263
759
  <div
264
760
  className={`result-display ${result.startsWith("✅") ? "result-success" : ""}`}
265
761
  >
266
- Current value: "{inputValue}"<br />
762
+ Current value: "{value}"<br />
267
763
  {result || "Click buttons to change value, then submit..."}
268
764
  </div>
269
765
  </div>
@@ -275,43 +771,33 @@
275
771
  const [result, setResult] = useState("");
276
772
  const [isLoading, setIsLoading] = useState(false);
277
773
 
774
+ const formDemoAction = async ({ username, email }) => {
775
+ setIsLoading(true);
776
+ setResult("Submitting form...");
777
+ try {
778
+ await delay(1200);
779
+ const response = `✅ Form submitted!\nUsername: ${username}\nEmail: ${email}`;
780
+ setResult(response);
781
+ return response;
782
+ } catch (error) {
783
+ setResult(`❌ Form error: ${error.message}`);
784
+ throw error;
785
+ } finally {
786
+ setIsLoading(false);
787
+ }
788
+ };
789
+
278
790
  return (
279
791
  <div className="demo-card">
280
792
  <h3 className="demo-title">Input inside Form</h3>
281
- <Form
282
- action={async ({ username, email }) => {
283
- setIsLoading(true);
284
- setResult("Submitting form...");
285
- try {
286
- await delay(1200);
287
- const response = `✅ Form submitted!\nUsername: ${username}\nEmail: ${email}`;
288
- setResult(response);
289
- return response;
290
- } catch (error) {
291
- setResult(`❌ Form error: ${error.message}`);
292
- throw error;
293
- } finally {
294
- setIsLoading(false);
295
- }
296
- }}
297
- >
793
+ <Form action={formDemoAction}>
298
794
  <div className="control-group">
299
- <label htmlFor="form-username">Username:</label>
300
- <Input
301
- id="form-username"
302
- name="username"
303
- placeholder="Enter username"
304
- />
305
- </div>
306
- <div className="control-group">
307
- <label htmlFor="form-email">Email:</label>
308
- <Input
309
- id="form-email"
310
- name="email"
311
- type="email"
312
- placeholder="Enter email"
313
- />
795
+ <label>
796
+ Username:
797
+ <Input name="username" placeholder="Enter username" />
798
+ </label>
314
799
  </div>
800
+ <div className="control-group"></div>
315
801
  <div className="button-group">
316
802
  <Button type="submit">Submit Form</Button>
317
803
  <Button type="reset">Reset</Button>
@@ -327,69 +813,54 @@
327
813
  };
328
814
 
329
815
  // eslint-disable-next-line no-unused-vars
330
- const FormWithExternalControlDemo = () => {
331
- const [username, setUsername] = useState(undefined);
332
- const [email, setEmail] = useState(undefined);
816
+ const FormInputErrorDemo = () => {
333
817
  const [result, setResult] = useState("");
818
+ const [isLoading, setIsLoading] = useState(false);
819
+
820
+ const formErrorAction = async ({ username, email }) => {
821
+ setIsLoading(true);
822
+ setResult("Submitting form...");
823
+ try {
824
+ await delay(1000);
825
+ // Simulate server-side validation errors
826
+ if (!username || username.length < 3) {
827
+ throw new Error("Username must be at least 3 characters long");
828
+ }
829
+ if (username === "admin") {
830
+ throw new Error(
831
+ "Username 'admin' is reserved and cannot be used",
832
+ );
833
+ }
834
+ // This will never be reached due to the conditions above
835
+ const response = `✅ Form submitted!\nUsername: ${username}\nEmail: ${email}`;
836
+ setResult(response);
837
+ return response;
838
+ } catch (error) {
839
+ setResult(`❌ Form error: ${error.message}`);
840
+ throw error;
841
+ } finally {
842
+ setIsLoading(false);
843
+ }
844
+ };
334
845
 
335
846
  return (
336
847
  <div className="demo-card">
337
- <h3 className="demo-title">Form with External Control</h3>
338
- <Form
339
- action={async (params) => {
340
- setResult("Submitting form...");
341
- await delay(1000);
342
- const response = `✅ Form submitted!\nUsername: ${params.username}\nEmail: ${params.email}`;
343
- setResult(response);
344
- return response;
345
- }}
848
+ <h3 className="demo-title">Form with Validation Errors</h3>
849
+ <p
850
+ style={{ fontSize: "14px", color: "#666", marginBottom: "15px" }}
346
851
  >
852
+ This form demonstrates server-side validation errors. Try: empty
853
+ fields, username &lt; 3 chars, invalid email, or "admin" username.
854
+ </p>
855
+ <Form action={formErrorAction}>
347
856
  <div className="control-group">
348
- <label htmlFor="controlled-form-username">Username:</label>
349
- <Input
350
- id="controlled-form-username"
351
- name="username"
352
- value={username}
353
- placeholder="Controlled by buttons"
354
- />
355
- </div>
356
- <div className="control-group">
357
- <label htmlFor="controlled-form-email">Email:</label>
358
- <Input
359
- id="controlled-form-email"
360
- name="email"
361
- value={email}
362
- placeholder="Controlled by buttons"
363
- />
364
- </div>
365
- <div className="button-group">
366
- <button
367
- type="button"
368
- onClick={() => {
369
- setUsername("john_doe");
370
- setEmail("john@example.com");
371
- }}
372
- >
373
- Set User 1
374
- </button>
375
- <button
376
- type="button"
377
- onClick={() => {
378
- setUsername("jane_smith");
379
- setEmail("jane@example.com");
380
- }}
381
- >
382
- Set User 2
383
- </button>
384
- <button
385
- type="button"
386
- onClick={() => {
387
- setUsername("");
388
- setEmail("");
389
- }}
390
- >
391
- Clear All
392
- </button>
857
+ <label>
858
+ Username:
859
+ <Input
860
+ name="username"
861
+ placeholder="Enter username (try 'admin' or < 3 chars)"
862
+ />
863
+ </label>
393
864
  </div>
394
865
  <div className="button-group">
395
866
  <Button type="submit">Submit Form</Button>
@@ -397,64 +868,14 @@
397
868
  </div>
398
869
  </Form>
399
870
  <div
400
- className={`result-display ${result.startsWith("✅") ? "result-success" : ""}`}
401
- >
402
- Current: {username} / {email}
403
- <br />
404
- {result || "Use buttons to set values, then submit..."}
405
- </div>
406
- </div>
407
- );
408
- };
409
-
410
- // eslint-disable-next-line no-unused-vars
411
- const SharedActionDemo = () => {
412
- const [result, setResult] = useState("");
413
-
414
- return (
415
- <div className="demo-card">
416
- <h3 className="demo-title">Shared Action Example</h3>
417
- <div className="control-group">
418
- <label htmlFor="shared-input-1">Input 1 (shared action):</label>
419
- <Input
420
- id="shared-input-1"
421
- name="name"
422
- placeholder="Enter name for input 1"
423
- action={sharedAction}
424
- onActionStart={() => {
425
- setResult("Processing...");
426
- }}
427
- onActionEnd={(e) => {
428
- setResult(e.detail.data);
429
- }}
430
- />
431
- </div>
432
- <div className="control-group">
433
- <label htmlFor="shared-input-2">
434
- Input 2 (same shared action):
435
- </label>
436
- <Input
437
- id="shared-input-2"
438
- name="name"
439
- placeholder="Enter name for input 2"
440
- action={sharedAction}
441
- />
442
- </div>
443
- <div
444
- className={`result-display ${result.startsWith("✅") ? "result-success" : ""}`}
871
+ className={`result-display ${isLoading ? "result-loading" : result.startsWith("✅") ? "result-success" : result.startsWith("") ? "result-error" : ""}`}
445
872
  >
446
- {result || "Type in either input and submit..."}
873
+ {result || "Fill form and submit to see validation errors..."}
447
874
  </div>
448
875
  </div>
449
876
  );
450
877
  };
451
878
 
452
- const sharedAction = createAction(async ({ name }) => {
453
- await delay(800);
454
- const response = `✅ Hello ${name}! (from shared action)`;
455
- return response;
456
- });
457
-
458
879
  render(<App />, document.querySelector("#root"));
459
880
  </script>
460
881
  </body>