@zeno-lib/forms 0.2.1 → 0.2.2

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.
@@ -99,7 +99,7 @@ type CreateZenoFormConfig<FC extends Record<string, AnyComponent>, FMC extends R
99
99
  * the `createFormHook` primitives plus the schema-aware `useForm` and the typed
100
100
  * `useAppFields` wrappers (prop types are inferred from the injected components).
101
101
  */
102
- declare function createZenoForm<FC extends Record<string, AnyComponent>, FMC extends Record<string, AnyComponent>>(config: CreateZenoFormConfig<FC, FMC>): {
102
+ export declare function createZenoForm<FC extends Record<string, AnyComponent>, FMC extends Record<string, AnyComponent>>(config: CreateZenoFormConfig<FC, FMC>): {
103
103
  useAppFields: <TForm extends AnyAppForm>(form: TForm) => FieldWrappers<FC, FormDataOf<TForm>>;
104
104
  useAppForm: <TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnBlur extends undefined | FormValidateOrFn<TFormData>, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnSubmit extends undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnDynamic extends undefined | FormValidateOrFn<TFormData>, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, TSubmitMeta>(props: FormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>) => import("@tanstack/react-form").AppFieldExtendedReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, FC, FMC>;
105
105
  useForm: <TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData> = undefined, TOnChange extends undefined | FormValidateOrFn<TFormData> = undefined, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined, TOnBlur extends undefined | FormValidateOrFn<TFormData> = undefined, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined, TOnSubmit extends undefined | FormValidateOrFn<TFormData> = undefined, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined, TOnDynamic extends undefined | FormValidateOrFn<TFormData> = undefined, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData> = undefined, TSubmitMeta = never>(options: UseFormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>) => import("@tanstack/react-form").FormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta> & import("@tanstack/react-form").ReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta> & NoInfer<FMC> & {
@@ -114,5 +114,4 @@ declare function createZenoForm<FC extends Record<string, AnyComponent>, FMC ext
114
114
  form: import("@tanstack/react-form").AppFieldExtendedReactFormApi<[unknown] extends [TFormData] ? any : TFormData, [FormValidateOrFn<TFormData> | undefined] extends [TOnMount] ? [TOnMount] extends [TOnMount & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnMount : TOnMount, [FormValidateOrFn<TFormData> | undefined] extends [TOnChange] ? [TOnChange] extends [TOnChange & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnChange : TOnChange, [FormValidateOrFn<TFormData> | undefined] extends [TOnChangeAsync] ? [TOnChangeAsync] extends [TOnChangeAsync & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnChangeAsync : TOnChangeAsync, [FormValidateOrFn<TFormData> | undefined] extends [TOnBlur] ? [TOnBlur] extends [TOnBlur & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnBlur : TOnBlur, [FormValidateOrFn<TFormData> | undefined] extends [TOnBlurAsync] ? [TOnBlurAsync] extends [TOnBlurAsync & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnBlurAsync : TOnBlurAsync, [FormValidateOrFn<TFormData> | undefined] extends [TOnSubmit] ? [TOnSubmit] extends [TOnSubmit & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnSubmit : TOnSubmit, [FormValidateOrFn<TFormData> | undefined] extends [TOnSubmitAsync] ? [TOnSubmitAsync] extends [TOnSubmitAsync & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnSubmitAsync : TOnSubmitAsync, [FormValidateOrFn<TFormData> | undefined] extends [TOnDynamic] ? [TOnDynamic] extends [TOnDynamic & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnDynamic : TOnDynamic, [FormValidateOrFn<TFormData> | undefined] extends [TOnDynamicAsync] ? [TOnDynamicAsync] extends [TOnDynamicAsync & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnDynamicAsync : TOnDynamicAsync, [FormValidateOrFn<TFormData> | undefined] extends [TOnServer] ? [TOnServer] extends [TOnServer & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnServer : TOnServer, [unknown] extends [TSubmitMeta] ? any : TSubmitMeta, [unknown] extends [FC] ? any : FC, [unknown] extends [FMC] ? any : FMC>;
115
115
  }>>;
116
116
  };
117
- //#endregion
118
- export { createZenoForm };
117
+ //#endregion
@@ -6,8 +6,8 @@ type FormProviderProps = {
6
6
  handleSubmit: () => unknown;
7
7
  };
8
8
  };
9
- declare function FormProvider({ children, form }: FormProviderProps): import("react/jsx-runtime").JSX.Element;
9
+ declare function FormProvider({ children, form }: FormProviderProps): import("react").JSX.Element;
10
10
  type FormProps = Omit<ComponentProps<"form">, "onSubmit">;
11
- declare function Form({ children, className, ...props }: FormProps): import("react/jsx-runtime").JSX.Element;
11
+ declare function Form({ children, className, ...props }: FormProps): import("react").JSX.Element;
12
12
  //#endregion
13
13
  export { Form, FormProvider };
@@ -1,4 +1,3 @@
1
1
  //#region src/lib/aria.d.ts
2
- declare function describedBy(...entries: [unknown, string][]): string | undefined;
3
- //#endregion
4
- export { describedBy };
2
+ export declare function describedBy(...entries: [unknown, string][]): string | undefined;
3
+ //#endregion
@@ -1,5 +1,5 @@
1
1
  //#region src/lib/contexts.d.ts
2
2
  declare const fieldContext: import("react").Context<import("@tanstack/react-form").AnyFieldApi>, formContext: import("react").Context<import("@tanstack/react-form").AnyFormApi>, useFieldContext: <TData>() => import("@tanstack/react-form").FieldApi<any, string, TData, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any>, useFormContext: () => import("@tanstack/react-form").ReactFormExtendedApi<Record<string, never>, any, any, any, any, any, any, any, any, any, any, any>;
3
- declare const FormProvider: import("react").Provider<import("@tanstack/react-form").AnyFormApi>;
3
+ export declare const FormProvider: import("react").Provider<import("@tanstack/react-form").AnyFormApi>;
4
4
  //#endregion
5
- export { FormProvider, fieldContext, formContext, useFieldContext, useFormContext };
5
+ export { fieldContext, formContext, useFieldContext, useFormContext };
@@ -26,7 +26,7 @@ function normalizeMinus(text) {
26
26
  }
27
27
  /** Format a number for display (on blur, on reset, on first render). */
28
28
  function formatNumber(value, { locale, maximumFractionDigits, minimumFractionDigits = 0, useGrouping = true }) {
29
- if (value == null || !Number.isFinite(value)) return "";
29
+ if (value === null || value === void 0 || !Number.isFinite(value)) return "";
30
30
  return normalizeMinus(new Intl.NumberFormat(locale, {
31
31
  maximumFractionDigits,
32
32
  minimumFractionDigits: Math.min(minimumFractionDigits, maximumFractionDigits),
@@ -102,19 +102,19 @@ function parseLocaleNumber(text, locale) {
102
102
  }
103
103
  function countSignificant(text, end, decimal) {
104
104
  let count = 0;
105
- for (let i = 0; i < end && i < text.length; i++) {
105
+ for (let i = 0; i < end && i < text.length; i += 1) {
106
106
  const char = text[i];
107
- if (DIGIT.test(char) || char === decimal || MINUS_SIGNS.test(char)) count++;
107
+ if (DIGIT.test(char) || char === decimal || MINUS_SIGNS.test(char)) count += 1;
108
108
  }
109
109
  return count;
110
110
  }
111
111
  function caretForSignificant(text, significant, decimal) {
112
112
  if (significant <= 0) return 0;
113
113
  let count = 0;
114
- for (let i = 0; i < text.length; i++) {
114
+ for (let i = 0; i < text.length; i += 1) {
115
115
  const char = text[i];
116
116
  if (DIGIT.test(char) || char === decimal || char === "-") {
117
- count++;
117
+ count += 1;
118
118
  if (count === significant) return i + 1;
119
119
  }
120
120
  }
@@ -5,7 +5,7 @@ function getDef(node) {
5
5
  }
6
6
  function extractDefaultForField(node) {
7
7
  let current = node;
8
- for (let depth = 0; depth < MAX_WRAPPER_DEPTH; depth++) {
8
+ for (let depth = 0; depth < MAX_WRAPPER_DEPTH; depth += 1) {
9
9
  const def = getDef(current);
10
10
  if (!def?.type) return { ok: false };
11
11
  switch (def.type) {
@@ -26,7 +26,7 @@ function probeContainer(expected) {
26
26
  if (typeof expected !== "string") return;
27
27
  const kind = expected.toLowerCase();
28
28
  if (kind === "object") return {};
29
- if (kind === "array") return [{}];
29
+ return kind === "array" ? [{}] : void 0;
30
30
  }
31
31
  function fillAt(root, keys, value) {
32
32
  let node = root;
@@ -53,7 +53,7 @@ function validateSync(schema, value) {
53
53
  function getRequiredPaths(schema) {
54
54
  const required = /* @__PURE__ */ new Set();
55
55
  const probe = {};
56
- for (let depth = 0; depth < MAX_PROBE_DEPTH; depth++) {
56
+ for (let depth = 0; depth < MAX_PROBE_DEPTH; depth += 1) {
57
57
  const issues = validateSync(schema, probe);
58
58
  if (!issues) return required;
59
59
  let descended = false;
package/package.json CHANGED
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "name": "@zeno-lib/forms",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
+ "repository": {
5
+ "url": "https://github.com/zeno-lib/zeno.git",
6
+ "directory": "packages/forms"
7
+ },
4
8
  "type": "module",
5
9
  "exports": {
6
10
  ".": {
@@ -25,12 +29,23 @@
25
29
  "dist",
26
30
  "src"
27
31
  ],
28
- "publishConfig": {
29
- "access": "public"
32
+ "dependencies": {
33
+ "@tanstack/react-form": "1.33.5",
34
+ "@tanstack/react-form-nextjs": "1.33.5"
30
35
  },
31
- "repository": {
32
- "url": "https://github.com/zeno-lib/zeno.git",
33
- "directory": "packages/forms"
36
+ "devDependencies": {
37
+ "@types/node": "26.6.4",
38
+ "@types/react": "19.3.0",
39
+ "@types/react-dom": "19.3.0",
40
+ "@zeno-lib/test": "^0.1.1",
41
+ "@zeno-lib/typescript": "^1.1.0",
42
+ "@zeno-lib/ui": "^0.0.1",
43
+ "lucide-react": "^1.52.0",
44
+ "tsdown": "^0.23.0",
45
+ "typescript": "7.0.2",
46
+ "vite": "8.3.3",
47
+ "vitest": "5.0.3",
48
+ "zod": "^4.6.5"
34
49
  },
35
50
  "peerDependencies": {
36
51
  "@tanstack/react-devtools": ">=0.10",
@@ -48,26 +63,12 @@
48
63
  "optional": true
49
64
  }
50
65
  },
51
- "dependencies": {
52
- "@tanstack/react-form": "1.33.5",
53
- "@tanstack/react-form-nextjs": "1.33.5"
54
- },
55
- "devDependencies": {
56
- "@types/node": "25.6.0",
57
- "@types/react": "19.2.14",
58
- "@types/react-dom": "19.2.3",
59
- "lucide-react": "^1.23.0",
60
- "tsdown": "^0.22.14",
61
- "typescript": "7.0.2",
62
- "vite": "8.0.16",
63
- "vitest": "5.0.2",
64
- "zod": "^4.3.6",
65
- "@zeno-lib/test": "^0.1.0",
66
- "@zeno-lib/typescript": "^1.1.0",
67
- "@zeno-lib/ui": "^0.0.1"
66
+ "publishConfig": {
67
+ "access": "public"
68
68
  },
69
69
  "scripts": {
70
70
  "build": "tsdown",
71
+ "dev": "tsdown --watch --no-clean",
71
72
  "test": "vitest --run",
72
73
  "test:watch": "vitest watch",
73
74
  "types:check": "tsc --noEmit"
@@ -124,7 +124,7 @@ function ComboboxField<T = string>({
124
124
  items={items}
125
125
  onInputValueChange={onInputValueChange}
126
126
  onValueChange={(next) => {
127
- if (next == null) {
127
+ if (next === null) {
128
128
  field.handleChange(undefined)
129
129
  return
130
130
  }
@@ -143,7 +143,7 @@ function ComboboxField<T = string>({
143
143
  name={field.name}
144
144
  onBlur={field.handleBlur}
145
145
  placeholder={placeholder}
146
- showClear={showClear && value != null && value !== "" && !loading}
146
+ showClear={showClear && value !== null && value !== "" && !loading}
147
147
  >
148
148
  {loading && (
149
149
  <InputGroupAddon align="inline-end">
@@ -68,7 +68,7 @@ function DatePickerField({
68
68
  const schemaRequired = useIsFieldRequired(field)
69
69
  const isRequired = required ?? schemaRequired
70
70
 
71
- const value = field.state.value
71
+ const { value } = field.state
72
72
  const empty = !value
73
73
 
74
74
  return (
@@ -11,7 +11,7 @@ type NumberFieldProps = Omit<
11
11
 
12
12
  function NumberField(props: NumberFieldProps) {
13
13
  const field = useFieldContext<number | undefined>()
14
- const value = field.state.value
14
+ const { value } = field.state
15
15
  const inputValue =
16
16
  typeof value === "number" && !Number.isNaN(value) ? value : ""
17
17
 
@@ -51,12 +51,14 @@ function SliderField({
51
51
  const schemaRequired = useIsFieldRequired(field)
52
52
  const isRequired = required ?? schemaRequired
53
53
 
54
- const value = field.state.value
54
+ const { value } = field.state
55
55
  const readout = formatValue && value !== undefined ? formatValue(value) : null
56
+ // Not a truthiness check: a formatter may return the number 0, which must still render.
57
+ const hasReadout = readout !== null && readout !== undefined
56
58
 
57
59
  return (
58
60
  <Field data-field={field.name} data-invalid={isInvalid}>
59
- {(label || readout) && (
61
+ {(label || hasReadout) && (
60
62
  <FieldContent className="flex-row items-center justify-between">
61
63
  {label && (
62
64
  <FieldLabel htmlFor={field.name}>
@@ -64,7 +66,7 @@ function SliderField({
64
66
  {isRequired && <RequiredIndicator />}
65
67
  </FieldLabel>
66
68
  )}
67
- {readout && (
69
+ {hasReadout && (
68
70
  <span className="text-muted-foreground text-sm tabular-nums">
69
71
  {readout}
70
72
  </span>
@@ -81,9 +83,9 @@ function SliderField({
81
83
  name={field.name}
82
84
  onBlur={field.handleBlur}
83
85
  onValueChange={(next) => {
84
- const value: SliderValue =
86
+ const nextValue: SliderValue =
85
87
  typeof next === "number" ? next : Array.from(next)
86
- field.handleChange(value)
88
+ field.handleChange(nextValue)
87
89
  }}
88
90
  value={value}
89
91
  {...props}
@@ -63,8 +63,8 @@ type DialogForm<TValues> = {
63
63
  }
64
64
  }
65
65
  readonly state: DialogFormState
66
- handleSubmit(): unknown
67
- reset(values?: TValues): void
66
+ handleSubmit: () => unknown
67
+ reset: (values?: TValues) => void
68
68
  }
69
69
 
70
70
  type FormDialogProps<TValues> = {
@@ -84,7 +84,7 @@ function formatNumber(
84
84
  useGrouping = true,
85
85
  }: FormatNumberOptions
86
86
  ): string {
87
- if (value == null || !Number.isFinite(value)) {
87
+ if (value === null || value === undefined || !Number.isFinite(value)) {
88
88
  return ""
89
89
  }
90
90
  return normalizeMinus(
@@ -236,10 +236,10 @@ function parseLocaleNumber(text: string, locale?: string): number | null {
236
236
  // the same logical place after separators were inserted or removed.
237
237
  function countSignificant(text: string, end: number, decimal: string): number {
238
238
  let count = 0
239
- for (let i = 0; i < end && i < text.length; i++) {
239
+ for (let i = 0; i < end && i < text.length; i += 1) {
240
240
  const char = text[i] as string
241
241
  if (DIGIT.test(char) || char === decimal || MINUS_SIGNS.test(char)) {
242
- count++
242
+ count += 1
243
243
  }
244
244
  }
245
245
  return count
@@ -254,10 +254,10 @@ function caretForSignificant(
254
254
  return 0
255
255
  }
256
256
  let count = 0
257
- for (let i = 0; i < text.length; i++) {
257
+ for (let i = 0; i < text.length; i += 1) {
258
258
  const char = text[i] as string
259
259
  if (DIGIT.test(char) || char === decimal || char === "-") {
260
- count++
260
+ count += 1
261
261
  if (count === significant) {
262
262
  return i + 1
263
263
  }
@@ -37,7 +37,7 @@ function getDef(node: SchemaNode | undefined): SchemaDef | undefined {
37
37
 
38
38
  function extractDefaultForField(node: SchemaNode): ExtractResult {
39
39
  let current: SchemaNode | undefined = node
40
- for (let depth = 0; depth < MAX_WRAPPER_DEPTH; depth++) {
40
+ for (let depth = 0; depth < MAX_WRAPPER_DEPTH; depth += 1) {
41
41
  const def: SchemaDef | undefined = getDef(current)
42
42
  if (!def?.type) {
43
43
  return { ok: false }
@@ -166,7 +166,7 @@ describe("getRequiredPaths", () => {
166
166
  const schema: StandardSchemaLike = {
167
167
  "~standard": {
168
168
  validate: (value) => {
169
- const profile = (value as { profile?: unknown }).profile
169
+ const { profile } = value as { profile?: unknown }
170
170
  return profile === undefined
171
171
  ? { issues: [{ expected: "Object", path: [{ key: "profile" }] }] }
172
172
  : {
@@ -50,6 +50,7 @@ const INDEX_SEGMENT = /^\d+$/
50
50
  const INDEX_IN_NAME = /\[\d+\]|\.(\d+)(?=\.|\[|$)/g
51
51
 
52
52
  function pathKey(part: PathPart): PropertyKey {
53
+ // biome-ignore lint/suspicious/noUnnecessaryConditions: issue paths come from third-party validators at runtime, and a non-conforming `null` segment would make `in` throw.
53
54
  if (typeof part === "object" && part !== null && "key" in part) {
54
55
  return part.key
55
56
  }
@@ -93,10 +94,7 @@ function probeContainer(expected: unknown): unknown {
93
94
  if (kind === "object") {
94
95
  return {}
95
96
  }
96
- if (kind === "array") {
97
- return [{}]
98
- }
99
- return
97
+ return kind === "array" ? [{}] : undefined
100
98
  }
101
99
 
102
100
  // Write `value` at `keys` inside `root`, only where nothing is set yet.
@@ -140,7 +138,7 @@ function validateSync(
140
138
  function getRequiredPaths(schema: StandardSchemaLike): Set<string> {
141
139
  const required = new Set<string>()
142
140
  const probe: Record<PropertyKey, unknown> = {}
143
- for (let depth = 0; depth < MAX_PROBE_DEPTH; depth++) {
141
+ for (let depth = 0; depth < MAX_PROBE_DEPTH; depth += 1) {
144
142
  const issues = validateSync(schema, probe)
145
143
  if (!issues) {
146
144
  return required
@@ -32,7 +32,6 @@ function makeRun() {
32
32
  const runValidation = vi.fn(
33
33
  (props: { form: AnyFormApi; validators: ObservedValidator[] }) => {
34
34
  observed.push(props.validators)
35
- return
36
35
  }
37
36
  )
38
37
  return { observed, runValidation }
@@ -0,0 +1,30 @@
1
+ import { cleanup, render, screen } from "@zeno-lib/test/testing-library"
2
+ import { afterEach, describe, expect, test, vi } from "vitest"
3
+ import { Form, FormProvider, useForm } from "./create-form"
4
+
5
+ afterEach(() => {
6
+ cleanup()
7
+ })
8
+
9
+ describe("SliderField", () => {
10
+ function Harness() {
11
+ const form = useForm({ defaultValues: { volume: 0 }, onSubmit: vi.fn() })
12
+ const { SliderField } = form
13
+ return (
14
+ <FormProvider form={form}>
15
+ <Form>
16
+ <SliderField
17
+ formatValue={(value) => value}
18
+ label="Volume"
19
+ name="volume"
20
+ />
21
+ </Form>
22
+ </FormProvider>
23
+ )
24
+ }
25
+
26
+ test("renders a numeric readout of 0", () => {
27
+ render(<Harness />)
28
+ expect(screen.getByText("0").tagName).toBe("SPAN")
29
+ })
30
+ })