react-store-input 0.2.6 → 0.5.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 (44) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +211 -199
  3. package/dist/index.d.mts +127 -11
  4. package/dist/index.d.ts +127 -11
  5. package/dist/index.js +542 -280
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +529 -272
  8. package/dist/index.mjs.map +1 -0
  9. package/dist/text-editor.d.mts +11 -0
  10. package/dist/text-editor.d.ts +11 -0
  11. package/dist/text-editor.js +203 -0
  12. package/dist/text-editor.js.map +1 -0
  13. package/dist/text-editor.mjs +178 -0
  14. package/dist/text-editor.mjs.map +1 -0
  15. package/package.json +78 -18
  16. package/dist/create_render.d.mts +0 -9
  17. package/dist/create_render.d.ts +0 -9
  18. package/dist/create_render.js +0 -51
  19. package/dist/create_render.mjs +0 -25
  20. package/dist/text_editor.d.mts +0 -13
  21. package/dist/text_editor.d.ts +0 -13
  22. package/dist/text_editor.js +0 -135
  23. package/dist/text_editor.mjs +0 -112
  24. package/dist/use_form_store.d.mts +0 -16
  25. package/dist/use_form_store.d.ts +0 -16
  26. package/dist/use_form_store.js +0 -365
  27. package/dist/use_form_store.mjs +0 -340
  28. package/dist/use_store_component.d.mts +0 -23
  29. package/dist/use_store_component.d.ts +0 -23
  30. package/dist/use_store_component.js +0 -337
  31. package/dist/use_store_component.mjs +0 -311
  32. package/dist/use_store_controller.d.mts +0 -11
  33. package/dist/use_store_controller.d.ts +0 -11
  34. package/dist/use_store_controller.js +0 -57
  35. package/dist/use_store_controller.mjs +0 -32
  36. package/dist/use_store_input.d.mts +0 -22
  37. package/dist/use_store_input.d.ts +0 -22
  38. package/dist/use_store_input.js +0 -151
  39. package/dist/use_store_input.mjs +0 -126
  40. package/dist/use_store_input_with_name.d.mts +0 -17
  41. package/dist/use_store_input_with_name.d.ts +0 -17
  42. package/dist/use_store_input_with_name.js +0 -177
  43. package/dist/use_store_input_with_name.mjs +0 -150
  44. package/tsup.config.ts +0 -9
package/CHANGELOG.md ADDED
@@ -0,0 +1,136 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project uses [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [0.5.0] - 2026-07-31
9
+
10
+ ### Changed
11
+
12
+ - Replaced `useStoreController` with `useStoreBinding`, a Lens/Codec-based
13
+ adapter for synchronizing external controls without exposing whole-state
14
+ subscription and draft callbacks.
15
+ - Reimplemented `useStoreHTMLElement` and the optional `TextEditor` integration
16
+ on top of `useStoreBinding`.
17
+ - Renamed `useStoreComponent` to `useStoreInput`. It accepts an existing
18
+ `gw-store` `Store` and returns bound `input`, `select`, and `textarea`
19
+ components.
20
+ - Renamed the low-level DOM binding hook from `useStoreInput` to
21
+ `useStoreHTMLElement`.
22
+ - Removed `gw-store` API re-exports. Import stores, selectors, and their types
23
+ directly from `gw-store`.
24
+
25
+ ### Removed
26
+
27
+ - Removed the deprecated `useFormStore`, `FormStore`, and `FormComponents`
28
+ APIs.
29
+ - Removed `useStoreController`. Non-input actions should call
30
+ `store.dispatch` directly.
31
+
32
+ ## [0.4.0] - 2026-07-22
33
+
34
+ ### Added
35
+
36
+ - Typed `Lens`, `Codec`, and `InputBinding` primitives for custom controls.
37
+ - `stateLens` for generating a getter and setter from one typed state path.
38
+ - `gw-result@0.3.0`-based codec parsing with typed failure metadata from
39
+ `useStoreInput`.
40
+ - `assertLensLaws` and `assertCodecLaws` helpers for testing mapping and
41
+ round-trip invariants.
42
+ - Top-level `ok`, `err`, and `Result` re-exports for codec implementations.
43
+
44
+ ### Changed
45
+
46
+ - Removed the deprecated `getter`, `setter`, `toInputValue`, and
47
+ `toStateValue` APIs. Nested and converted controls now use bindings.
48
+ - Reimplemented named controls internally with a generated Lens and default
49
+ Codec while preserving built-in conversion and reset behavior.
50
+ - Expanded the custom currency example to demonstrate validation failures and
51
+ accessible error feedback.
52
+
53
+ ### Fixed
54
+
55
+ - Preserved invalid raw control input while unrelated store fields update.
56
+ - Cleared binding parse errors and restored the initial domain value on native
57
+ form reset.
58
+
59
+ ## [0.3.0] - 2026-07-22
60
+
61
+ ### Added
62
+
63
+ - Standalone `Input`, `Select`, and `Textarea` components with typed named or
64
+ custom getter/setter bindings.
65
+ - `useStoreInput`, `useStoreController`, `createRender`, and complete
66
+ `gw-store` API re-exports.
67
+ - An optional `react-store-input/text-editor` entry point that keeps editor
68
+ dependencies out of the core bundle.
69
+ - Automatic native form reset synchronization, including batched store
70
+ notifications and multiple-select, date, checkbox, radio, and file values.
71
+ - Runtime, package-consumer, type, and example build tests.
72
+ - A complete interactive example covering the public components, hooks, and
73
+ supported input value types.
74
+
75
+ ### Changed
76
+
77
+ - Updated the store integration to use exactly `gw-store@0.2.0`.
78
+ - Published separate CommonJS and ESM entry points with matching declaration
79
+ files.
80
+ - Made `gw-react-text-editor` an optional peer dependency.
81
+ - Expanded React peer support to React 18 and 19.
82
+ - Organized package source, examples, styles, and runtime tests by
83
+ responsibility.
84
+
85
+ ### Fixed
86
+
87
+ - Prevented bundled duplicate React copies from causing invalid hook calls.
88
+ - Preserved numeric radio values and mapped empty numeric inputs to
89
+ `undefined`.
90
+ - Stored all selected values from multiple selects and normalized empty file
91
+ inputs to `null`.
92
+ - Kept rendered JSON synchronized after a native form reset.
93
+ - Reduced reset notifications to one batch and eliminated notifications for
94
+ semantically unchanged arrays and `Date` values.
95
+
96
+ ## [0.2.6] - 2026-04-28
97
+
98
+ ### Added
99
+
100
+ - Re-exported the `gw-store` API.
101
+
102
+ ## [0.2.5] - 2026-04-27
103
+
104
+ ### Changed
105
+
106
+ - Added `gw-store` as a package dependency.
107
+
108
+ ## [0.2.4] - 2026-03-16
109
+
110
+ ### Fixed
111
+
112
+ - Improved store value comparison behavior.
113
+
114
+ ## [0.2.3] - 2026-03-09
115
+
116
+ ### Changed
117
+
118
+ - Applied package maintenance updates.
119
+
120
+ ## [0.2.2] - 2026-03-09
121
+
122
+ ### Changed
123
+
124
+ - Added compatible React 18 type and peer dependency ranges.
125
+
126
+ ## [0.2.1] - 2026-01-21
127
+
128
+ ### Changed
129
+
130
+ - Updated the JSX runtime configuration.
131
+
132
+ ## [0.2.0] - 2026-01-13
133
+
134
+ ### Fixed
135
+
136
+ - Corrected forwarded ref behavior.
package/README.md CHANGED
@@ -1,285 +1,297 @@
1
1
  # React Store Input
2
2
 
3
- The goal of this package is to make state management easier when using input elements in React.
3
+ Typed, store-backed form controls for React 18 and 19. The package uses
4
+ [`gw-store`](https://www.npmjs.com/package/gw-store) 0.2.0, so state snapshots
5
+ are immutable and updates are made through Immer recipes.
4
6
 
5
- It eliminates repetitive code required to implement state changes and subscriptions for input elements, and provides a simple interface.
7
+ ## Install
6
8
 
7
- At the same time, it allows you to use all the attributes originally provided by the input tag as-is, without needing to learn this package.
8
-
9
- ## Get Started
9
+ ```sh
10
+ npm install react-store-input
11
+ ```
10
12
 
11
- This is a simple example of how to use this package.
13
+ ## Quick start
12
14
 
13
15
  ```tsx
14
- import { useFormStore } from "dn-react-input";
16
+ import { useStore } from "gw-store";
17
+ import { Input } from "react-store-input";
15
18
 
16
- export default function App() {
17
- const store = useFormStore({
19
+ export default function LoginForm() {
20
+ const store = useStore({
18
21
  email: "",
19
22
  password: "",
23
+ rememberMe: false,
20
24
  });
21
25
 
22
- const submit = async () => {
23
- const { email, password } = store.state;
24
-
25
- alert(`Email: ${email}\nPassword: ${password}`);
26
- };
27
-
28
26
  return (
29
27
  <form
30
- onSubmit={(e) => {
31
- e.preventDefault();
32
- submit();
28
+ onSubmit={(event) => {
29
+ event.preventDefault();
30
+ console.log(store.state);
33
31
  }}
34
32
  >
35
- <store.input name="email" type="email" />
36
- <store.input name="password" type="password" />
37
- <button type="submit">Submit</button>
33
+ <Input store={store} name="email" type="email" />
34
+ <Input store={store} name="password" type="password" />
35
+ <Input store={store} name="rememberMe" type="checkbox" />
36
+ <button type="submit">Sign in</button>
38
37
  </form>
39
38
  );
40
39
  }
41
40
  ```
42
41
 
43
- ## How to define state?
42
+ Create and own stores with `gw-store`. This package only binds that store to
43
+ input controls.
44
44
 
45
- You can define any state you want as an object when calling `useStore`.
45
+ ## Components
46
46
 
47
- ```tsx
48
- function Component() {
49
- ...
50
-
51
- const store = useStore({
52
- email: "",
53
- password: "",
54
- rememberMe: false,
55
- });
47
+ You can use the standalone components when the store is passed from elsewhere:
56
48
 
57
- ...
58
- }
49
+ ```tsx
50
+ import { useStore } from "gw-store";
51
+ import { Input, Select, Textarea } from "react-store-input";
52
+
53
+ const store = useStore({ role: "user", bio: "" });
54
+
55
+ <Input store={store} name="role" type="radio" value="admin" />;
56
+ <Input store={store} name="role" type="radio" value="user" />;
57
+ <Select store={store} name="role">
58
+ <option value="admin">Admin</option>
59
+ <option value="user">User</option>
60
+ </Select>;
61
+ <Textarea store={store} name="bio" rows={5} />;
59
62
  ```
60
63
 
61
- It's a single source of truth for your form state.
62
-
63
- ## How to get input values?
64
-
65
- You can access the current values of the input elements through the `state` property of the store.
64
+ When passing `store` to every control would be repetitive, `useStoreInput`
65
+ returns stable components already bound to an existing store:
66
66
 
67
67
  ```tsx
68
- function Component() {
69
- ...
68
+ import { useStore } from "gw-store";
69
+ import { useStoreInput } from "react-store-input";
70
70
 
71
- const submit = () => {
72
- const { email, password, rememberMe } = store.state;
73
- };
71
+ const store = useStore({ role: "user", bio: "" });
72
+ const controls = useStoreInput(store);
74
73
 
75
- ...
76
- }
74
+ <controls.input name="role" />;
75
+ <controls.select name="role">...</controls.select>;
76
+ <controls.textarea name="bio" />;
77
77
  ```
78
78
 
79
- ## How to add input elements?
79
+ A named `Input`, `Select`, or `Textarea` requires a valid top-level state key.
80
+ Use `useStoreHTMLElement` with a typed binding for nested or converted values.
80
81
 
81
- You can add input elements using the `Input` component provided by the store. There are 'Select' and 'Textarea' components as well.
82
+ ## Value conversion
82
83
 
83
- ```tsx
84
- import { Input } from "dn-react-input";
85
-
86
- function Component() {
87
- ...
88
-
89
- return (
90
- <form>
91
- <Input store={store} name="email" type="email" />
92
- <Input store={store} name="password" type="password" />
93
- <Input store={store} name="rememberMe" type="checkbox" />
94
- </form>
95
- );
96
- }
97
- ```
84
+ The default conversions are:
98
85
 
99
- If you want to avoid passing the store to each input component, use `useStoreComponent`. This hook provides input components that are already connected to the store.
86
+ - checkbox `boolean`
87
+ - radio → the original `value` prop, preserving numbers and strings
88
+ - number/range → `number`, or `undefined` when empty
89
+ - datetime-local → `Date`, or `undefined` when empty or invalid
90
+ - multiple select → `string[]`
91
+ - file → `FileList | null`
92
+ - other controls → `string`
100
93
 
101
- ```tsx
102
- import { useStoreComponent } from "dn-react-input";
103
-
104
- function Component() {
105
- ...
106
- const component = useStoreComponent(store);
107
-
108
- return (
109
- <form>
110
- <component.input name="email" type="email" />
111
- <component.input name="password" type="password" />
112
- <component.input name="rememberMe" type="checkbox" />
113
- </form>
114
- );
115
- }
116
- ```
94
+ Domain-specific conversions use a Codec as described below.
117
95
 
118
- `useFormStore` is a facade that combines `useStore` and `useStoreInput` for convenience.
96
+ The controls are store-backed uncontrolled inputs. An explicit `value` or
97
+ `checked` prop is respected as an externally controlled value and is not
98
+ overwritten by store subscriptions. Native form reset is synchronized back to
99
+ the store.
119
100
 
120
- ```tsx
121
- import { useFormStore } from "dn-react-input";
122
-
123
- function Component() {
124
- ...
125
- const store = useFormStore({
126
- email: "",
127
- password: "",
128
- rememberMe: false,
129
- });
101
+ ## Lens, codec, and binding
130
102
 
131
- return (
132
- <form>
133
- <store.input name="email" type="email" />
134
- <store.input name="password" type="password" />
135
- <store.input name="rememberMe" type="checkbox" />
136
- </form>
137
- );
138
- }
139
- ```
103
+ A custom control is defined from three small pieces:
140
104
 
141
- ## How to render components on state changes?
105
+ - A `Lens<TState, TValue>` selects and updates one domain value in the store.
106
+ - A `Codec<TValue, TInput, TError>` formats that domain value for the control
107
+ and parses input back to a `gw-result` `Result`.
108
+ - An `InputBinding` combines a lens and codec whose `TValue` types must match.
142
109
 
143
- If you want to render a component only when specific parts of the state change, use the `useSelector` hook.
110
+ Keeping the lens and codec separate lets one state field use different UI
111
+ representations, and lets one codec be reused for the same domain type in
112
+ different stores. `InputBinding` itself is not tied to an HTML element.
144
113
 
145
114
  ```tsx
146
- import { useSelector } from "dn-react-input";
115
+ import {
116
+ defineBinding,
117
+ defineCodec,
118
+ err,
119
+ ok,
120
+ stateLens,
121
+ } from "react-store-input";
122
+
123
+ type FormState = { profile: { budget?: number } };
124
+ type BudgetError = { code: "INVALID_BUDGET"; input: string };
125
+
126
+ const budgetBinding = defineBinding({
127
+ lens: stateLens<FormState>().prop("profile").prop("budget"),
128
+ codec: defineCodec<number | undefined, string, BudgetError>({
129
+ format: (value) => value?.toString() ?? "",
130
+ parse: (input) => {
131
+ if (input === "") return ok(undefined);
132
+
133
+ const value = Number(input);
134
+ return Number.isFinite(value)
135
+ ? ok(value)
136
+ : err({ code: "INVALID_BUDGET", input });
137
+ },
138
+ }),
139
+ });
140
+ ```
147
141
 
148
- function Component() {
149
- ...
150
- const email = useSelector(store, (state) => state.email);
142
+ `stateLens().prop(...)` creates `get` and `set` from the same typed path, so
143
+ they cannot accidentally target different fields. `defineLens` is also
144
+ available for computed or otherwise non-path mappings.
151
145
 
152
- return <div>Your email is: {email}</div>;
153
- }
154
- ```
146
+ `ok`, `err`, and the `Result` type are re-exported from `gw-result@0.3.0` for
147
+ codec implementations.
148
+
149
+ ## Rendering selected state
155
150
 
156
- If you want to render components in an inline manner, use the `createRender` function. By using this, you can avoid creating separate components for each part of the state you want to track.
151
+ Use `gw-store` selectors with this package's rendering helper.
157
152
 
158
153
  ```tsx
159
- import { createRender } from "dn-react-input";
160
-
161
- function Component() {
162
- ...
163
- return (
164
- <div>
165
- {createRender(store, (state) => <p>{state.email}</p>)}
166
- {createRender(store, (state) => <p>{state.password}</p>)}
167
- </div>
168
- );
169
- }
154
+ import { useSelector } from "gw-store";
155
+ import { createRender } from "react-store-input";
156
+
157
+ const email = useSelector(store, (state) => state.email);
158
+
159
+ return (
160
+ <>
161
+ <p>{email}</p>
162
+ {createRender(store, (state) => <p>{state.password.length} characters</p>)}
163
+ {createRender(store, (state) => (
164
+ <p>{state.rememberMe ? "Remember" : "Forget"}</p>
165
+ ))}
166
+ </>
167
+ );
170
168
  ```
171
169
 
172
- `Store.render` is a shortcut for `createRender` when you use `useFormStore`.
170
+ ## Custom controls
171
+
172
+ Use `useStoreHTMLElement` with a binding for custom elements that expose a normal
173
+ form-control DOM node. The ref is deliberately explicit. A parse failure keeps
174
+ the last valid store value, preserves the user's raw input, and exposes the
175
+ typed error through `meta`.
173
176
 
174
177
  ```tsx
175
- function Component() {
176
- const store = useFormStore({
177
- email: "",
178
- password: "",
178
+ import { useRef } from "react";
179
+ import type { Store } from "gw-store";
180
+ import { useStoreHTMLElement } from "react-store-input";
181
+
182
+ function BudgetInput({ store }: { store: Store<FormState> }) {
183
+ const ref = useRef<HTMLInputElement>(null);
184
+ const field = useStoreHTMLElement(ref, store, budgetBinding, {
185
+ type: "text",
179
186
  });
180
187
 
181
188
  return (
182
- <div>
183
- {store.render((state) => (
184
- <p>{state.email}</p>
185
- ))}
186
- {store.render((state) => (
187
- <p>{state.password}</p>
188
- ))}
189
- </div>
189
+ <label>
190
+ Budget
191
+ <input
192
+ ref={ref}
193
+ type="text"
194
+ inputMode="decimal"
195
+ aria-invalid={!field.meta.valid}
196
+ {...field.inputProps}
197
+ />
198
+ {!field.meta.valid && <span>{field.meta.error.code}</span>}
199
+ </label>
190
200
  );
191
201
  }
192
202
  ```
193
203
 
194
- ## How to subscribe to state changes?
195
-
196
- You can subscribe to state changes using the `subscribe` method of the store.
204
+ For an imperative editor or another non-DOM control, use the same binding
205
+ through `useStoreBinding`. It parses commits, exposes validation metadata, and
206
+ synchronizes external store changes without echoing its own dispatch:
197
207
 
198
208
  ```tsx
199
- function Component() {
200
- ...
201
- useEffect(() => {
202
- const unsubscribe = store.subscribe((state) => {
203
- console.log(`State changed`, state);
204
- });
205
-
206
- return () => {
207
- unsubscribe();
208
- };
209
- }, []);
210
-
211
- ...
212
- }
209
+ const field = useStoreBinding(store, budgetBinding, {
210
+ onStoreChange: (input) => editorRef.current?.setValue(input),
211
+ });
212
+
213
+ <ExternalEditor
214
+ defaultValue={field.initialValue}
215
+ onChange={(input) => field.commit(input)}
216
+ />;
213
217
  ```
214
218
 
215
- ## How to update state manually?
216
-
217
- You can update the state manually using the `dispatch` method of the store.
219
+ Generated lenses and codecs can be checked with the exported law assertions in
220
+ unit tests:
218
221
 
219
- ```tsx
220
- function Component() {
221
- ...
222
- const updateEmail = () => {
223
- store.dispatch({ email: "ohjinsu98@icloud.com" });
224
- };
222
+ ```ts
223
+ assertLensLaws(budgetBinding.lens, {
224
+ state: { profile: { budget: 10 } },
225
+ values: [undefined, 0, 25],
226
+ });
225
227
 
226
- return <button onClick={updateEmail}>Update Email</button>;
227
- }
228
+ assertCodecLaws(budgetBinding.codec, {
229
+ values: [undefined, 0, 25],
230
+ inputs: ["", "0", "25"],
231
+ });
228
232
  ```
229
233
 
230
- The `dispatch` method uses immerjs internally to update the state, so you can also use a function to update the state based on the previous state.
234
+ The lens assertions verify get-after-set, set-current-value, and last-set-wins.
235
+ The codec assertion verifies `parse(format(value))` for representative domain
236
+ values, plus `format(parse(input).value)` for successful canonical inputs when
237
+ `inputs` are supplied. Normalizing or lossy codecs may supply domain-specific
238
+ `equals` and `equalsInput` functions.
239
+
240
+ For non-input actions, dispatch directly to the store:
231
241
 
232
242
  ```tsx
233
- function Component() {
234
- ...
243
+ import type { Store } from "gw-store";
235
244
 
236
- const updateEmail = () => {
237
- store.dispatch((state) => {
238
- state.email = "ohjinsu98@icloud.com";
239
- });
240
- };
245
+ function Counter({ store }: { store: Store<{ count: number }> }) {
246
+ const increment = () =>
247
+ store.dispatch((state) => {
248
+ state.count += 1;
249
+ });
241
250
 
242
- return <button onClick={updateEmail}>Update Email</button>;
251
+ return <button onClick={increment}>Increment</button>;
243
252
  }
244
253
  ```
245
254
 
246
- ## How to create custom input components?
255
+ ## Optional text editor
247
256
 
248
- You can create custom input components using the `useStoreInput` hook. This hook provides the necessary props to connect your custom input component to the store: `name`, `value`, `defaultValue`, `defaultChecked`, `onChange`, and `ref` which already subscribed to the store.
257
+ The ProseMirror-based editor is a separate entry point so normal forms do not
258
+ download or bundle editor dependencies. It requires React 19 and an explicit
259
+ optional peer installation:
249
260
 
250
- ```tsx
251
- import { useStoreInput } from "dn-react-input";
261
+ ```sh
262
+ npm install gw-react-text-editor
263
+ ```
252
264
 
253
- function CustomInput({ store }: { store: Store<{ email: string }> }) {
254
- const inputProps = useStoreInput(store, {
255
- name: "email",
256
- });
265
+ ```tsx
266
+ import { TextEditor } from "react-store-input/text-editor";
257
267
 
258
- return <input {...inputProps} />;
259
- }
268
+ <TextEditor store={store} name="content" />;
260
269
  ```
261
270
 
262
- ## How to creatre custom controller components?
271
+ ## Development
263
272
 
264
- If your custom component is not an html input element, you can use the `useStoreController` hook. This hook provides the necessary props to connect your custom controller component to the store: `ref`, `onSubscribe`, and `onDispatch`.
273
+ Release history is tracked in [CHANGELOG.md](./CHANGELOG.md). The manual
274
+ pre-release checks are documented in [docs/PUBLISHING.md](./docs/PUBLISHING.md).
265
275
 
266
- ```tsx
267
- import { useStoreController } from "dn-react-input";
276
+ Source code is grouped by responsibility:
268
277
 
269
- type State = {
270
- count: number;
271
- };
278
+ ```text
279
+ src/
280
+ ├─ binding/ Lens, Codec, Binding, and law assertions
281
+ ├─ input/ DOM value conversion, reset coordination, and element hooks
282
+ ├─ form/ standalone and store-bound input components
283
+ ├─ store/ controller and render helpers
284
+ └─ editor/ optional text-editor integration
272
285
 
273
- function CustomController({ store }: { store: Store<State> }) {
274
- const controllerProps = useStoreController<HTMLDivElement, State>(store, {
275
- onSubscribe: (state, element) => {
276
- element.textContent = `Count: ${state.count}`;
277
- },
278
- onDispatch: (state, element) => {
279
- state.count += Number(element.textContent.replace("Count: ", ""));
280
- },
281
- });
286
+ example/src/
287
+ ├─ components/ reusable demo UI
288
+ ├─ sections/ one catalog section per capability group
289
+ ├─ demo/ state model and initial data
290
+ └─ styles/ layout, fields, toolbar, state panel, and responsive rules
291
+ ```
282
292
 
283
- return <div {...controllerProps} />;
284
- }
293
+ ```sh
294
+ npm run typecheck
295
+ npm test
296
+ npm pack --dry-run
285
297
  ```