@juantroconisf/lib 11.6.0 → 11.7.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 (4) hide show
  1. package/README.md +180 -437
  2. package/dist/index.js +150 -151
  3. package/dist/index.mjs +150 -151
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -1,437 +1,180 @@
1
- # @juantroconisf/lib
2
-
3
- A type-safe form management library for **React** and **HeroUI**. It eliminates boilerplate through a declarative `on.*` API that bridges your [Yup](https://github.com/jquense/yup) schema directly to HeroUI component props — complete with validation, error messages, dirty tracking, and ID-based array management.
4
-
5
- ---
6
-
7
- ## Table of Contents
8
-
9
- - [Installation](#installation)
10
- - [Quick Start](#quick-start)
11
- - [Core Concepts](#core-concepts)
12
- - [Schema Definition](#schema-definition)
13
- - [Scalar Fields](#scalar-fields)
14
- - [Nested Objects](#nested-objects)
15
- - [Array Fields](#array-fields)
16
- - [The `on` API Component Bindings](#the-on-api--component-bindings)
17
- - [Manual Updates](#manual-updates)
18
- - [Array Helpers](#array-helpers)
19
- - [Form Submission](#form-submission)
20
- - [API Reference](#api-reference)
21
- - [Localization](#localization)
22
-
23
- ---
24
-
25
- ## Installation
26
-
27
- ```bash
28
- pnpm add @juantroconisf/lib yup
29
- ```
30
-
31
- ---
32
-
33
- ## Quick Start
34
-
35
- ```tsx
36
- import { useForm } from "@juantroconisf/lib";
37
- import { string, boolean } from "yup";
38
- import { Input, Switch, Button } from "@heroui/react";
39
-
40
- const MyForm = () => {
41
- const { on, ControlledForm } = useForm({
42
- fullName: string().required().default(""),
43
- darkMode: boolean().default(false),
44
- });
45
-
46
- return (
47
- <ControlledForm onSubmit={(data) => console.log(data)}>
48
- <Input {...on.input("fullName")} label='Full Name' />
49
- <Switch {...on.switch("darkMode")}>Dark Mode</Switch>
50
- <Button type='submit' color='primary'>
51
- Submit
52
- </Button>
53
- </ControlledForm>
54
- );
55
- };
56
- ```
57
-
58
- > `on.input("fullName")` spreads `value`, `onValueChange`, `isInvalid`, `errorMessage`, `isRequired`, `onBlur`, and `id` directly onto the HeroUI component. Zero boilerplate.
59
-
60
- ---
61
-
62
- ## Core Concepts
63
-
64
- ### Schema Definition
65
-
66
- Pass a plain object of Yup schemas (or raw values for unvalidated state) to `useForm`. The hook infers your TypeScript types automatically.
67
-
68
- ```ts
69
- const { on, state, helpers, ControlledForm } = useForm({
70
- email: string().email().required().default(""),
71
- age: number().min(18).default(18),
72
- settings: {
73
- theme: string().oneOf(["light", "dark"]).default("dark"),
74
- notifications: boolean().default(true),
75
- },
76
- tags: array().of(string()).default([]),
77
- users: array()
78
- .of(object({ id: number().required(), role: string().required() }))
79
- .default([]),
80
- });
81
- ```
82
-
83
- ### Scalar Fields
84
-
85
- Top-level primitive fields. Use dot notation to reach into nested objects.
86
-
87
- ```tsx
88
- <Input {...on.input("email")} label="Email" />
89
- <Input {...on.numberInput("age")} label="Age" />
90
- <Select {...on.select("settings.theme")} label="Theme">
91
- <SelectItem key="light">Light</SelectItem>
92
- <SelectItem key="dark">Dark</SelectItem>
93
- </Select>
94
- ```
95
-
96
- ### Nested Objects
97
-
98
- Dot notation reaches arbitrarily deep. TypeScript validates that the path exists and has the correct type.
99
-
100
- ```tsx
101
- <Switch {...on.switch("settings.notifications")}>
102
- Enable Notifications
103
- </Switch>
104
- <Autocomplete {...on.autocomplete("settings.theme")} label="Theme" />
105
- ```
106
-
107
- ### Array Fields
108
-
109
- Arrays of objects are tracked by item `id` (configurable via `arrayIdentifiers`). This means inputs hold their state correctly even after re-ordering or deletions.
110
-
111
- ```tsx
112
- {
113
- state.users.map((user) => (
114
- <div key={user.id}>
115
- {/* Bind to a field inside the array item using: path + itemId */}
116
- <Input {...on.input("users.name", user.id)} label='Name' />
117
- <Select {...on.select("users.role", user.id)} label='Role'>
118
- <SelectItem key='admin'>Admin</SelectItem>
119
- <SelectItem key='guest'>Guest</SelectItem>
120
- </Select>
121
- <Button onPress={() => helpers.removeById("users", user.id)}>
122
- Remove
123
- </Button>
124
- </div>
125
- ));
126
- }
127
- <Button
128
- onPress={() => helpers.addItem("users", { id: Date.now(), role: "guest" })}
129
- >
130
- Add User
131
- </Button>;
132
- ```
133
-
134
- ### Deeply Nested Arrays (N-Level Depth)
135
-
136
- If your schema contains arrays inside objects inside arrays (infinite depth), you can pass a variadic sequence of arguments alternating between structural paths and array IDs (or indexes).
137
-
138
- ```tsx
139
- // Schema: items[].form_response.input_values[].value
140
- <Input
141
- {...on.input("items", itemId, "form_response.input_values", inputId, "value")}
142
- label='Deep Value'
143
- />
144
- ```
145
-
146
- The library dynamically traverses your state, mapping IDs securely to their array indices natively, regardless of depth.
147
-
148
- ---
149
-
150
- ## The `on` API Component Bindings
151
-
152
- Each `on.*` method returns a set of props that you spread directly onto a HeroUI component. Every method automatically handles:
153
-
154
- - **Controlled value** synced with form state
155
- - **Validation** runs on change and blur against your Yup schema
156
- - **Error display** `isInvalid` and `errorMessage` from the schema
157
- - **Required indicator** — `isRequired` derived from `.required()` in your schema
158
-
159
- All methods support **scalar/nested** paths, standard **array item** paths (composite `"array.field"` + `itemId`), and **variadic sequences** for N-level deep structures.
160
-
161
- | Method | HeroUI Component | Key props returned |
162
- | -------------------------- | ------------------- | ----------------------------------------------- |
163
- | `on.input(...args)` | `Input`, `Textarea` | `value: string`, `onValueChange(string)` |
164
- | `on.numberInput(...args)` | `NumberInput` | `value: number`, `onValueChange(number)` |
165
- | `on.select(...args)` | `Select` | `selectedKeys`, `onSelectionChange` |
166
- | `on.autocomplete(...args)` | `Autocomplete` | `selectedKey`, `onSelectionChange` |
167
- | `on.checkbox(...args)` | `Checkbox` | `isSelected: boolean`, `onValueChange(boolean)` |
168
- | `on.switch(...args)` | `Switch` | `isSelected: boolean`, `onValueChange(boolean)` |
169
- | `on.radio(...args)` | `RadioGroup` | `value: string`, `onValueChange(string)` |
170
-
171
- > **Why separate methods?** Each HeroUI component has a different prop contract (e.g. `isSelected` vs `value`, `onSelectionChange` vs `onValueChange`). Separate methods give accurate intellisense for each component.
172
-
173
- ### Examples
174
-
175
- ```tsx
176
- {/* Text input */}
177
- <Input {...on.input("firstName")} label="First Name" />
178
-
179
- {/* Numeric input (NumberInput-specific) */}
180
- <NumberInput {...on.numberInput("age")} label="Age" />
181
-
182
- {/* Boolean toggle */}
183
- <Checkbox {...on.checkbox("agreed")}>I agree to the terms</Checkbox>
184
- <Switch {...on.switch("notifications")}>Notifications</Switch>
185
-
186
- {/* String radio selection */}
187
- <RadioGroup {...on.radio("gender")} label="Gender">
188
- <Radio value="male">Male</Radio>
189
- <Radio value="female">Female</Radio>
190
- </RadioGroup>
191
-
192
- {/* Dropdown selection */}
193
- <Select {...on.select("country")} label="Country">
194
- <SelectItem key="us">United States</SelectItem>
195
- <SelectItem key="ca">Canada</SelectItem>
196
- </Select>
197
-
198
- {/* Inside an array — pass item ID as 2nd arg */}
199
- <Input {...on.input("users.name", user.id)} label="Name" />
200
- <Switch {...on.switch("users.active", user.id)}>Active</Switch>
201
- ```
202
-
203
- ---
204
-
205
- ## Manual Updates
206
-
207
- For updating state outside of a component (e.g. in an event handler, after an API call), use the manual functions returned by `useForm`.
208
-
209
- | Function | Use for | Signature |
210
- | ------------------------ | ------------------------------- | --------------------- |
211
- | `onFieldChange` | Scalar / nested object field | `(id, value)` |
212
- | `onArrayItemChange` | Object array item field | `({ at, id, value })` |
213
- | `onSelectionChange` | Scalar / nested selection field | `(id, value)` |
214
- | `onArraySelectionChange` | Array item selection field | `({ at, id, value })` |
215
-
216
- ```ts
217
- const { onFieldChange, onArrayItemChange, onSelectionChange } = useForm(schema);
218
-
219
- // Update a scalar field
220
- onFieldChange("email", "user@example.com");
221
-
222
- // Update a field inside an array item
223
- onArrayItemChange({ at: "users.name", id: userId, value: "Alice" });
224
-
225
- // Update a selection field inside an array item
226
- onArraySelectionChange({ at: "users.role", id: userId, value: "admin" });
227
- ```
228
-
229
- > **`at`** is the composite dot-notation path (`"array.field"`), **`id`** is the item's unique identifier, **`value`** is the new value to set. These are direct setters — call them imperatively, not as event listeners.
230
-
231
- ---
232
-
233
- ## Array Helpers
234
-
235
- Extracted from `helpers` on the `useForm` return value. Items in object arrays are tracked by ID (default field: `id`).
236
-
237
- | Method | Description |
238
- | ------------------------------------------- | -------------------------------------------------- |
239
- | `addItem(path, item, index?)` | Adds an item to the end (or at `index`) |
240
- | `removeItemByIndex(path, index)` | Removes by index |
241
- | `removeById(path, id)` | Removes by item ID |
242
- | `updateByIndex(path, index, partial)` | Partially updates an item by index (shallow merge) |
243
- | `updateById(path, id, partial)` | Partially updates an item by ID (shallow merge) |
244
- | `moveItemByIndex(path, fromIndex, toIndex)` | Reorders by index |
245
- | `moveById(path, fromId, toId)` | Reorders by ID |
246
- | `getItemById(path, id)` | O(1) lookup by ID |
247
-
248
- ```ts
249
- const { helpers } = useForm(schema);
250
-
251
- helpers.addItem("users", { id: Date.now(), name: "", role: "guest" });
252
- helpers.removeById("users", 123);
253
- helpers.updateById("users", 123, { name: "Alice" }); // partial update — other fields preserved
254
- helpers.moveById("users", 123, 456); // moves item 123 to where item 456 is
255
- ```
256
-
257
- ### Custom Array Identifier
258
-
259
- By default, the library tracks array items by their `id` field. If your items use a different field (e.g. `uuid`, `slug`, `code`), configure it via `arrayIdentifiers` in the options.
260
-
261
- ```ts
262
- const { on, helpers, onArrayItemChange } = useForm(
263
- {
264
- employees: array()
265
- .of(
266
- object({
267
- uuid: string().required(),
268
- name: string().required(),
269
- role: string().required(),
270
- }),
271
- )
272
- .default([]),
273
- },
274
- {
275
- arrayIdentifiers: {
276
- employees: "uuid", // tells the library to use `uuid` instead of `id`
277
- },
278
- },
279
- );
280
- ```
281
-
282
- **Every ID-based function picks this up automatically:**
283
-
284
- ```ts
285
- // on.* bindings — the second arg is the item identifier
286
- <Input {...on.input("employees.name", employee.uuid)} label="Name" />
287
- <Select {...on.select("employees.role", employee.uuid)} label="Role">...</Select>
288
-
289
- // Manual setters — `id` is the item identifier
290
- onArrayItemChange({ at: "employees.name", id: employee.uuid, value: "Alice" });
291
- onArraySelectionChange({ at: "employees.role", id: employee.uuid, value: "admin" });
292
-
293
- // Helpers — all `*ById` methods use the configured identifier
294
- helpers.removeById("employees", employee.uuid);
295
- helpers.updateById("employees", employee.uuid, { name: "Alice" }); // partial update
296
- helpers.moveById("employees", fromUuid, toUuid);
297
- helpers.getItemById("employees", employee.uuid);
298
- ```
299
-
300
- > The identifier field you configure must be a **scalar** (string or number) present in every item. The library uses it to build an internal O(1) index map, which is why array operations are fast regardless of list size.
301
-
302
- **Type inference:** `arrayIdentifiers` is typed so only valid scalar keys of each array element are accepted:
303
-
304
- ```ts
305
- // ✅ uuid is a string field on each employee
306
- {
307
- arrayIdentifiers: {
308
- employees: "uuid";
309
- }
310
- }
311
-
312
- // ❌ TypeScript error — name is not a valid identifier (may not be unique)
313
- {
314
- arrayIdentifiers: {
315
- employees: "name";
316
- }
317
- } // error if "name" is not in type
318
- ```
319
-
320
- ---
321
-
322
- ## Form Submission
323
-
324
- ### Option 1: `ControlledForm` (recommended)
325
-
326
- `ControlledForm` is a drop-in replacement for HeroUI's `<Form>`. It automatically validates on submit and only calls `onSubmit` if the schema passes.
327
-
328
- ```tsx
329
- const { ControlledForm } = useForm(schema);
330
-
331
- return (
332
- <ControlledForm onSubmit={(data, event) => api.post("/submit", data)}>
333
- <Input {...on.input("email")} label='Email' />
334
- <Button type='submit'>Submit</Button>
335
- </ControlledForm>
336
- );
337
- ```
338
-
339
- `data` is fully typed from your schema. No manual type annotations needed.
340
-
341
- ### Option 2: `onFormSubmit`
342
-
343
- For use with plain HTML `<form>` elements:
344
-
345
- ```tsx
346
- const { onFormSubmit } = useForm(schema);
347
-
348
- const handleSubmit = onFormSubmit((data, event) => {
349
- api.post("/submit", data);
350
- });
351
-
352
- return (
353
- <form onSubmit={handleSubmit}>
354
- <Input {...on.input("email")} label='Email' />
355
- <button type='submit'>Submit</button>
356
- </form>
357
- );
358
- ```
359
-
360
- ### Option 3: External handler with `FormSubmit`
361
-
362
- When you need to define the submit handler outside the JSX, use the `FormSubmit` utility type:
363
-
364
- ```tsx
365
- import { useForm, FormSubmit } from "@juantroconisf/lib";
366
-
367
- const { ControlledForm } = useForm({
368
- email: string().required().default(""),
369
- });
370
-
371
- const handleSubmit: FormSubmit<typeof ControlledForm> = (data, event) => {
372
- // data.email is correctly typed as string — no schema re-declaration needed
373
- console.log(data.email);
374
- };
375
- ```
376
-
377
- `FormSubmit<typeof ControlledForm>` is a shorthand for `React.ComponentProps<typeof ControlledForm>["onSubmit"]`.
378
-
379
- ---
380
-
381
- ## API Reference
382
-
383
- ### `useForm(schema, options?)`
384
-
385
- | Parameter | Type | Description |
386
- | --------- | ---------------------------- | -------------------------------------------------------- |
387
- | `schema` | `Record<string, ConfigType>` | Yup schemas or primitive values defining your form shape |
388
- | `options` | `FormOptions` | Optional configuration |
389
-
390
- #### `FormOptions`
391
-
392
- | Option | Type | Description |
393
- | ------------------ | ------------------------ | --------------------------------------------------------- |
394
- | `arrayIdentifiers` | `Record<string, string>` | Override the ID field for object arrays (default: `"id"`) |
395
- | `resetOnSubmit` | `boolean` | Reset form to defaults after successful submit |
396
- | `onFormSubmit` | `Function` | Default submit handler passed via options |
397
- | `keepValues` | `(keyof State)[]` | Fields to preserve during reset |
398
-
399
- ### `useForm` returns
400
-
401
- | Property | Type | Description |
402
- | ------------------------ | ----------------------------- | -------------------------------------------------- |
403
- | `state` | `InferState<S>` | Current form values |
404
- | `setState` | `Dispatch<SetStateAction<S>>` | Direct state setter |
405
- | `metadata` | `Map<string, FieldMetadata>` | Per-field `isTouched`, `isInvalid`, `errorMessage` |
406
- | `on` | `OnMethods<S>` | Component binding methods |
407
- | `helpers` | `HelpersFunc<S>` | Array manipulation methods |
408
- | `isDirty` | `boolean` | `true` if any field has been touched |
409
- | `onFieldChange` | Function | Manual scalar field update |
410
- | `onArrayItemChange` | Function | Manual array item field update |
411
- | `onSelectionChange` | Function | Manual scalar selection update |
412
- | `onArraySelectionChange` | Function | Manual array item selection update |
413
- | `onFieldBlur` | Function | Manual blur trigger |
414
- | `onFormReset` | Function | Resets state and clears metadata |
415
- | `onFormSubmit` | Function | Wraps a submit handler with validation |
416
- | `ControlledForm` | Component | Validated `<Form>` wrapper |
417
-
418
- ---
419
-
420
- ## Localization
421
-
422
- Validation error messages are automatically localized. The library reads a `LOCALE` cookie on initialization.
423
-
424
- - **Supported**: `en` (English — default), `es` (Spanish)
425
- - **Setting the locale**:
426
-
427
- ```ts
428
- document.cookie = "LOCALE=es; path=/; max-age=31536000";
429
- ```
430
-
431
- If no cookie is found, or the value is unrecognized, English is used.
432
-
433
- ---
434
-
435
- ## License
436
-
437
- ISC © Juan T
1
+ # @juantroconisf/lib
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@juantroconisf/lib.svg)](https://www.npmjs.com/package/@juantroconisf/lib)
4
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
5
+
6
+ A type-safe form management library for **React** and **HeroUI**. It eliminates boilerplate through a declarative `on.*` API that bridges your [Yup](https://github.com/jquense/yup) schema directly to HeroUI component props — complete with validation, error messages, dirty tracking, and ID-based array management.
7
+
8
+ ## Why this library?
9
+
10
+ Managing forms in React often involves manual state syncing and verbose validation logic. This library acts as a **bridge**, automatically mapping your [Yup](https://github.com/jquense/yup) schema's constraints directly to your HeroUI components.
11
+
12
+ ### Key Benefits
13
+ - **Zero Boilerplate:** No more manual `value`, `onChange`, and `onBlur` wiring.
14
+ - **Type-Safe:** Automatic inference from your schema with full Intellisense.
15
+ - **Auto-Validation:** Errors and `isRequired` states flow naturally from your schema.
16
+ - **ID-Based Arrays:** Seamless handling of dynamic lists with $O(1)$ performance.
17
+
18
+ ## Table of Contents
19
+
20
+ - [Quick Start](#quick-start)
21
+ - [How-to Guides](#how-to-guides)
22
+ - [1. Working with Nested Objects](#1-working-with-nested-objects)
23
+ - [2. Dynamic Arrays](#2-managing-dynamic-arrays)
24
+ - [3. N-Level Structures](#3-n-level-deep-structures)
25
+ - [4. Manual Updates](#4-performing-manual-updates)
26
+ - [Reference](#reference)
27
+ - [useForm API](#useformschema-options)
28
+ - [on.* API](#the-on-api)
29
+ - [Array Helpers](#array-helpers-helpers)
30
+ - [Explanation](#explanation)
31
+
32
+ ---
33
+
34
+ ## Quick Start
35
+
36
+ Get up and running in under a minute.
37
+
38
+ ### 1. Install
39
+ ```bash
40
+ pnpm add @juantroconisf/lib yup
41
+ ```
42
+
43
+ ### 2. Implementation
44
+ ```tsx
45
+ import { useForm } from "@juantroconisf/lib";
46
+ import { string, boolean } from "yup";
47
+ import { Input, Switch, Button } from "@heroui/react";
48
+
49
+ const MyForm = () => {
50
+ const { on, ControlledForm } = useForm({
51
+ fullName: string().required().default(""),
52
+ darkMode: boolean().default(false),
53
+ });
54
+
55
+ return (
56
+ <ControlledForm onSubmit={(data) => console.log(data)}>
57
+ {/* on.input() automatically handles value, onValueChange, isInvalid, and errorMessage */}
58
+ <Input {...on.input("fullName")} label='Full Name' />
59
+
60
+ <Switch {...on.switch("darkMode")}>Dark Mode</Switch>
61
+
62
+ <Button type='submit' color='primary'>
63
+ Submit
64
+ </Button>
65
+ </ControlledForm>
66
+ );
67
+ };
68
+ ```
69
+
70
+ ---
71
+
72
+ ## How-to Guides
73
+
74
+ Practical recipes for common form scenarios.
75
+
76
+ ### 1. Working with Nested Objects
77
+ Dot notation reaches arbitrarily deep. TypeScript validates that the path exists and has the correct type.
78
+
79
+ ```tsx
80
+ <Input {...on.input("settings.profile.username")} label="Username" />
81
+ <Switch {...on.switch("settings.notifications")}>
82
+ Enable Notifications
83
+ </Switch>
84
+ ```
85
+
86
+ ### 2. Managing Dynamic Arrays
87
+ Arrays of objects are tracked by item `id` (or a custom identifier). This ensures inputs hold their state correctly even after re-ordering or deletions.
88
+
89
+ ```tsx
90
+ {state.users.map((user) => (
91
+ <div key={user.id}>
92
+ {/* Bind to a field inside the array item using: path + itemId */}
93
+ <Input {...on.input("users.name", user.id)} label='Name' />
94
+
95
+ <Button onPress={() => helpers.removeById("users", user.id)}>
96
+ Remove
97
+ </Button>
98
+ </div>
99
+ ))}
100
+
101
+ <Button onPress={() => helpers.addItem("users", { id: Date.now(), name: "" })}>
102
+ Add User
103
+ </Button>
104
+ ```
105
+
106
+ ### 3. N-Level Deep Structures
107
+ For arrays inside objects inside arrays, pass a variadic sequence alternating between structural paths and identifiers.
108
+
109
+ ```tsx
110
+ // Schema: items[].form_response.input_values[].value
111
+ <Input
112
+ {...on.input("items", itemId, "form_response.input_values", inputId, "value")}
113
+ label='Deep Value'
114
+ />
115
+ ```
116
+
117
+ ### 4. Performing Manual Updates
118
+ To update state outside of a component (e.g., in an API response handler), use the manual setters.
119
+
120
+ ```ts
121
+ const { onFieldChange, onArrayItemChange } = useForm(schema);
122
+
123
+ // Update a top-level or nested field
124
+ onFieldChange("settings.theme", "dark");
125
+
126
+ // Update a field inside an array item
127
+ onArrayItemChange({ at: "users.name", id: userId, value: "Alice" });
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Reference
133
+
134
+ ### `useForm(schema, options?)`
135
+
136
+ | Option | Type | Description |
137
+ | :--- | :--- | :--- |
138
+ | `arrayIdentifiers` | `Record<string, string>` | Override the ID field (default: `"id"`) |
139
+ | `resetOnSubmit` | `boolean` | Reset form after success (default: `false`) |
140
+ | `keepValues` | `(keyof State)[]` | Fields to exclude from reset |
141
+
142
+ ### The `on.*` API
143
+ Methods that bridge schema logic to HeroUI components.
144
+
145
+ | Method | HeroUI Component | Key Props Returned |
146
+ | :--- | :--- | :--- |
147
+ | `on.input()` | `Input`, `Textarea` | `value`, `onValueChange` |
148
+ | `on.select()` | `Select` | `selectedKeys`, `onSelectionChange` |
149
+ | `on.switch()` | `Switch` | `isSelected`, `onValueChange` |
150
+ | `on.checkbox()` | `Checkbox` | `isSelected`, `onValueChange` |
151
+ | `on.radio()` | `RadioGroup` | `value`, `onValueChange` |
152
+
153
+ ### Array Helpers (`helpers.*`)
154
+ | Method | Description |
155
+ | :--- | :--- |
156
+ | `addItem(path, item)` | Append a new item |
157
+ | `removeById(path, id)` | Fast $O(1)$ removal by ID |
158
+ | `updateById(path, id, partial)` | Shallow merge by ID |
159
+ | `moveById(path, fromId, toId)` | Reorder items |
160
+
161
+ ---
162
+
163
+ ## Explanation
164
+
165
+ ### The Bridge Architecture
166
+ Traditional form libraries often require you to manually map state to component props (`value={state.foo} onChange={...}`).
167
+
168
+ This library uses a **Bridge Pattern**:
169
+ 1. Your **Schema** defines the "Source of Truth".
170
+ 2. The **`on.*` methods** act as the bridge, translating that truth into the specific prop contract needed by each HeroUI component.
171
+ 3. This ensures that validation errors, required indicators, and values are always perfectly in sync without manual wiring.
172
+
173
+ ### Localization
174
+ Validation messages are automatically localized by reading the `LOCALE` cookie (`en` or `es`).
175
+ ```ts
176
+ document.cookie = "LOCALE=es; path=/;";
177
+ ```
178
+
179
+ ## License
180
+ ISC © Juan T