@structuralists/scaffolding 0.5.0 → 0.6.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.
- package/.claude/skills/pr-screenshots/SKILL.md +84 -0
- package/.github/workflows/publish.yml +11 -0
- package/.storybook/main.ts +1 -1
- package/.storybook/preview.tsx +7 -0
- package/AGENTS.md +104 -0
- package/bun.lock +79 -2
- package/package.json +7 -1
- package/src/forms/CLAUDE.md +13 -5
- package/src/forms/plan.md +132 -5
- package/src/forms/useFormState/FormDebugger.module.css +41 -0
- package/src/forms/useFormState/FormDebugger.test.tsx +74 -0
- package/src/forms/useFormState/FormDebugger.tsx +72 -0
- package/src/forms/useFormState/inspectable.test.ts +42 -0
- package/src/forms/useFormState/inspectable.ts +35 -0
- package/src/forms/useFormState/snapshotStore.test.ts +56 -0
- package/src/forms/useFormState/snapshotStore.ts +31 -0
- package/src/forms/useFormState/types.ts +18 -0
- package/src/forms/useFormState/useFormState.stories.tsx +107 -0
- package/src/forms/useFormState/useFormState.ts +45 -3
- package/vitest.config.ts +35 -0
- package/CLAUDE.md +0 -55
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { createFormDebugger } from './FormDebugger';
|
|
3
|
+
import type { FormDebuggerComponent } from './FormDebugger';
|
|
4
|
+
import { createSnapshotStore } from './snapshotStore';
|
|
5
|
+
import type { SnapshotStore } from './snapshotStore';
|
|
6
|
+
import type {
|
|
7
|
+
FormDebugSnapshot,
|
|
8
|
+
FormErrors,
|
|
9
|
+
FormHelpers,
|
|
10
|
+
FormValuesObject,
|
|
11
|
+
} from './types';
|
|
3
12
|
import type { Refine, Validations } from '../validations/types';
|
|
4
13
|
|
|
5
14
|
// `const V` freezes the inferred type of an inline `constraints` object —
|
|
@@ -38,6 +47,31 @@ export const useFormState = <
|
|
|
38
47
|
|
|
39
48
|
const isValid = Object.keys(errors).length === 0;
|
|
40
49
|
|
|
50
|
+
// Debugger plumbing: one store + one component per hook instance, created
|
|
51
|
+
// lazily on first render. The component's identity must be stable across
|
|
52
|
+
// renders — recreated each render it would remount (and lose its
|
|
53
|
+
// open/closed state) on every keystroke.
|
|
54
|
+
const debugRef = useRef<{
|
|
55
|
+
store: SnapshotStore<FormDebugSnapshot<T>>;
|
|
56
|
+
Debugger: FormDebuggerComponent;
|
|
57
|
+
} | null>(null);
|
|
58
|
+
if (debugRef.current === null) {
|
|
59
|
+
const store = createSnapshotStore<FormDebugSnapshot<T>>({
|
|
60
|
+
values,
|
|
61
|
+
errors,
|
|
62
|
+
isValid,
|
|
63
|
+
submitAttempted,
|
|
64
|
+
});
|
|
65
|
+
debugRef.current = { store, Debugger: createFormDebugger(store) };
|
|
66
|
+
}
|
|
67
|
+
const { store, Debugger } = debugRef.current;
|
|
68
|
+
|
|
69
|
+
// Publish after every commit. With no debugger window subscribed this is a
|
|
70
|
+
// field write and an empty notify loop — effectively free.
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
store.publish({ values, errors, isValid, submitAttempted });
|
|
73
|
+
});
|
|
74
|
+
|
|
41
75
|
const submit = () => {
|
|
42
76
|
setSubmitAttempted(true);
|
|
43
77
|
if (!isValid) return;
|
|
@@ -46,5 +80,13 @@ export const useFormState = <
|
|
|
46
80
|
onSubmit?.(values as Refine<T, V>);
|
|
47
81
|
};
|
|
48
82
|
|
|
49
|
-
return {
|
|
83
|
+
return {
|
|
84
|
+
values,
|
|
85
|
+
onValueChanges,
|
|
86
|
+
errors,
|
|
87
|
+
isValid,
|
|
88
|
+
submitAttempted,
|
|
89
|
+
submit,
|
|
90
|
+
Debugger,
|
|
91
|
+
};
|
|
50
92
|
};
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
import { defineConfig } from 'vitest/config';
|
|
5
|
+
|
|
6
|
+
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
|
|
7
|
+
|
|
8
|
+
import { playwright } from '@vitest/browser-playwright';
|
|
9
|
+
|
|
10
|
+
const dirname =
|
|
11
|
+
typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
// Runs every story as a Vitest test in headless Chromium: each story must
|
|
14
|
+
// mount without throwing, its play function (if any) must pass, and the
|
|
15
|
+
// a11y addon reports axe-core violations (see `a11y` in .storybook/preview.tsx).
|
|
16
|
+
// More info: https://storybook.js.org/docs/writing-tests/integrations/vitest-addon
|
|
17
|
+
export default defineConfig({
|
|
18
|
+
test: {
|
|
19
|
+
projects: [
|
|
20
|
+
{
|
|
21
|
+
extends: true,
|
|
22
|
+
plugins: [storybookTest({ configDir: path.join(dirname, '.storybook') })],
|
|
23
|
+
test: {
|
|
24
|
+
name: 'storybook',
|
|
25
|
+
browser: {
|
|
26
|
+
enabled: true,
|
|
27
|
+
headless: true,
|
|
28
|
+
provider: playwright({}),
|
|
29
|
+
instances: [{ browser: 'chromium' }],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
});
|
package/CLAUDE.md
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# @structuralists/scaffolding
|
|
2
|
-
|
|
3
|
-
Generic React component library. Storybook for dev.
|
|
4
|
-
|
|
5
|
-
Designed to be used to scaffold up an app
|
|
6
|
-
|
|
7
|
-
## Conventions
|
|
8
|
-
|
|
9
|
-
### Component prop destructuring
|
|
10
|
-
|
|
11
|
-
React components must take a single argument named `props` and destructure
|
|
12
|
-
on the first line of the function body — not in the parameter list.
|
|
13
|
-
|
|
14
|
-
```tsx
|
|
15
|
-
// ✅ correct
|
|
16
|
-
export const Foo = (props: FooProps) => {
|
|
17
|
-
const { a, b, c } = props;
|
|
18
|
-
|
|
19
|
-
return <div>{a}</div>;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
// ❌ wrong — destructures in the parameter list
|
|
23
|
-
export const Foo = ({ a, b, c }: FooProps) => {
|
|
24
|
-
return <div>{a}</div>;
|
|
25
|
-
};
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
Why: the destructuring line at the top of the body acts as a quick legend
|
|
29
|
-
of what the component reads from its props, scannable without parsing the
|
|
30
|
-
function signature. Keeps the call shape uniform across the package.
|
|
31
|
-
|
|
32
|
-
### Custom hook arguments
|
|
33
|
-
|
|
34
|
-
Project-defined hooks must take a single argument named `args` (an object)
|
|
35
|
-
and destructure on the first line of the function body — same shape as the
|
|
36
|
-
component-prop rule above.
|
|
37
|
-
|
|
38
|
-
```ts
|
|
39
|
-
// ✅ correct
|
|
40
|
-
export const useFoo = (args: UseFooArgs) => {
|
|
41
|
-
const { a, b, c } = args;
|
|
42
|
-
// ...
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// ❌ wrong — positional args
|
|
46
|
-
export const useFoo = (a: string, b: number, c?: boolean) => {
|
|
47
|
-
// ...
|
|
48
|
-
};
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Why: named args read clearly at the call site (`useFoo({ a, b })`), survive
|
|
52
|
-
reordering, and let new optional fields be added without breaking callers.
|
|
53
|
-
Built-in React hooks (`useState`, `useEffect`, etc.) keep their stock
|
|
54
|
-
positional signatures — this rule applies only to hooks defined in this
|
|
55
|
-
package.
|