@ic-reactor/candid 3.0.11-beta.2 → 3.0.13-beta.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/dist/display-reactor.d.ts +1 -2
  2. package/dist/display-reactor.d.ts.map +1 -1
  3. package/dist/display-reactor.js +1 -1
  4. package/dist/display-reactor.js.map +1 -1
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/metadata-display-reactor.d.ts +3 -3
  9. package/dist/metadata-display-reactor.d.ts.map +1 -1
  10. package/dist/metadata-display-reactor.js +2 -2
  11. package/dist/metadata-display-reactor.js.map +1 -1
  12. package/dist/visitor/arguments/helpers.d.ts +40 -0
  13. package/dist/visitor/arguments/helpers.d.ts.map +1 -0
  14. package/dist/visitor/arguments/helpers.js +81 -0
  15. package/dist/visitor/arguments/helpers.js.map +1 -0
  16. package/dist/visitor/arguments/index.d.ts +64 -42
  17. package/dist/visitor/arguments/index.d.ts.map +1 -1
  18. package/dist/visitor/arguments/index.js +472 -85
  19. package/dist/visitor/arguments/index.js.map +1 -1
  20. package/dist/visitor/arguments/types.d.ts +481 -46
  21. package/dist/visitor/arguments/types.d.ts.map +1 -1
  22. package/dist/visitor/helpers.d.ts +1 -1
  23. package/dist/visitor/helpers.d.ts.map +1 -1
  24. package/dist/visitor/returns/index.d.ts +1 -2
  25. package/dist/visitor/returns/index.d.ts.map +1 -1
  26. package/dist/visitor/returns/index.js +2 -3
  27. package/dist/visitor/returns/index.js.map +1 -1
  28. package/dist/visitor/types.d.ts +2 -3
  29. package/dist/visitor/types.d.ts.map +1 -1
  30. package/dist/visitor/types.js +1 -2
  31. package/dist/visitor/types.js.map +1 -1
  32. package/package.json +6 -3
  33. package/src/display-reactor.ts +4 -6
  34. package/src/index.ts +0 -1
  35. package/src/metadata-display-reactor.ts +8 -8
  36. package/src/visitor/arguments/helpers.ts +104 -0
  37. package/src/visitor/arguments/index.test.ts +544 -52
  38. package/src/visitor/arguments/index.ts +590 -151
  39. package/src/visitor/arguments/schema.test.ts +215 -0
  40. package/src/visitor/arguments/types.ts +554 -62
  41. package/src/visitor/helpers.ts +1 -1
  42. package/src/visitor/returns/index.test.ts +1 -1
  43. package/src/visitor/returns/index.ts +2 -3
  44. package/src/visitor/types.ts +2 -3
@@ -1,8 +1,86 @@
1
1
  import { isQuery } from "../helpers";
2
- import { IDL } from "../types";
2
+ import { IDL } from "@icp-sdk/core/candid";
3
+ import { Principal } from "@icp-sdk/core/principal";
4
+ import * as z from "zod";
5
+ import { formatLabel } from "./helpers";
3
6
  export * from "./types";
7
+ // ════════════════════════════════════════════════════════════════════════════
8
+ // Render Hint Helpers
9
+ // ════════════════════════════════════════════════════════════════════════════
10
+ const COMPOUND_RENDER_HINT = {
11
+ isCompound: true,
12
+ isPrimitive: false,
13
+ };
14
+ const TEXT_RENDER_HINT = {
15
+ isCompound: false,
16
+ isPrimitive: true,
17
+ inputType: "text",
18
+ };
19
+ const NUMBER_RENDER_HINT = {
20
+ isCompound: false,
21
+ isPrimitive: true,
22
+ inputType: "number",
23
+ };
24
+ const CHECKBOX_RENDER_HINT = {
25
+ isCompound: false,
26
+ isPrimitive: true,
27
+ inputType: "checkbox",
28
+ };
29
+ const FILE_RENDER_HINT = {
30
+ isCompound: false,
31
+ isPrimitive: true,
32
+ inputType: "file",
33
+ };
34
+ // ════════════════════════════════════════════════════════════════════════════
35
+ // Blob Field Helpers
36
+ // ════════════════════════════════════════════════════════════════════════════
37
+ const DEFAULT_BLOB_LIMITS = {
38
+ maxHexBytes: 512,
39
+ maxFileBytes: 2 * 1024 * 1024, // 2MB
40
+ maxHexDisplayLength: 128,
41
+ };
42
+ function normalizeHex(input) {
43
+ // Remove 0x prefix and convert to lowercase
44
+ let hex = input.toLowerCase();
45
+ if (hex.startsWith("0x")) {
46
+ hex = hex.slice(2);
47
+ }
48
+ // Remove any whitespace
49
+ hex = hex.replace(/\s/g, "");
50
+ return hex;
51
+ }
52
+ function validateBlobInput(value, limits) {
53
+ if (value instanceof Uint8Array) {
54
+ if (value.length > limits.maxFileBytes) {
55
+ return {
56
+ valid: false,
57
+ error: `File size exceeds maximum of ${limits.maxFileBytes} bytes`,
58
+ };
59
+ }
60
+ return { valid: true };
61
+ }
62
+ // String input (hex)
63
+ const normalized = normalizeHex(value);
64
+ if (normalized.length === 0) {
65
+ return { valid: true }; // Empty is valid
66
+ }
67
+ if (!/^[0-9a-f]*$/.test(normalized)) {
68
+ return { valid: false, error: "Invalid hex characters" };
69
+ }
70
+ if (normalized.length % 2 !== 0) {
71
+ return { valid: false, error: "Hex string must have even length" };
72
+ }
73
+ const byteLength = normalized.length / 2;
74
+ if (byteLength > limits.maxHexBytes) {
75
+ return {
76
+ valid: false,
77
+ error: `Hex input exceeds maximum of ${limits.maxHexBytes} bytes`,
78
+ };
79
+ }
80
+ return { valid: true };
81
+ }
4
82
  /**
5
- * ArgumentFieldVisitor generates metadata for form input fields from Candid IDL types.
83
+ * FieldVisitor generates metadata for form input fields from Candid IDL types.
6
84
  *
7
85
  * ## Design Principles
8
86
  *
@@ -10,55 +88,84 @@ export * from "./types";
10
88
  * 2. **No value dependencies** - metadata is independent of actual values
11
89
  * 3. **Form-framework agnostic** - output can be used with TanStack, React Hook Form, etc.
12
90
  * 4. **Efficient** - single traversal, no runtime type checking
91
+ * 5. **TanStack Form optimized** - name paths compatible with TanStack Form patterns
13
92
  *
14
93
  * ## Output Structure
15
94
  *
16
95
  * Each field has:
17
96
  * - `type`: The field type (record, variant, text, number, etc.)
18
- * - `label`: Human-readable label from Candid
19
- * - `path`: Dot-notation path for form binding (e.g., "0.owner")
97
+ * - `label`: Raw label from Candid
98
+ * - `displayLabel`: Human-readable formatted label
99
+ * - `name`: TanStack Form compatible path (e.g., "[0]", "[0].owner", "tags[1]")
100
+ * - `component`: Suggested component type for rendering
101
+ * - `renderHint`: Hints for UI rendering strategy
20
102
  * - `defaultValue`: Initial value for the form
103
+ * - `schema`: Zod schema for validation
21
104
  * - Type-specific properties (options for variant, fields for record, etc.)
105
+ * - Helper methods for dynamic forms (getOptionDefault, getItemDefault, etc.)
106
+ *
107
+ * ## Usage with TanStack Form
22
108
  *
23
109
  * @example
24
110
  * ```typescript
25
- * const visitor = new ArgumentFieldVisitor()
26
- * const serviceMeta = service.accept(visitor, null)
111
+ * import { useForm } from '@tanstack/react-form'
112
+ * import { FieldVisitor } from '@ic-reactor/candid'
27
113
  *
28
- * // For a specific method
114
+ * const visitor = new FieldVisitor()
115
+ * const serviceMeta = service.accept(visitor, null)
29
116
  * const methodMeta = serviceMeta["icrc1_transfer"]
30
- * // methodMeta.fields = [{ type: "record", fields: [...] }]
31
- * // methodMeta.defaultValues = [{ to: "", amount: "" }]
117
+ *
118
+ * const form = useForm({
119
+ * defaultValues: methodMeta.defaultValue,
120
+ * validators: { onBlur: methodMeta.schema },
121
+ * onSubmit: async ({ value }) => {
122
+ * await actor.icrc1_transfer(...value)
123
+ * }
124
+ * })
125
+ *
126
+ * // Render fields dynamically
127
+ * methodMeta.fields.map((field, index) => (
128
+ * <form.Field key={index} name={field.name}>
129
+ * {(fieldApi) => <DynamicInput field={field} fieldApi={fieldApi} />}
130
+ * </form.Field>
131
+ * ))
32
132
  * ```
33
133
  */
34
- export class ArgumentFieldVisitor extends IDL.Visitor {
134
+ export class FieldVisitor extends IDL.Visitor {
35
135
  constructor() {
36
136
  super(...arguments);
37
- Object.defineProperty(this, "pathStack", {
137
+ Object.defineProperty(this, "recursiveSchemas", {
138
+ enumerable: true,
139
+ configurable: true,
140
+ writable: true,
141
+ value: new Map()
142
+ });
143
+ Object.defineProperty(this, "nameStack", {
38
144
  enumerable: true,
39
145
  configurable: true,
40
146
  writable: true,
41
147
  value: []
42
148
  });
43
149
  }
44
- withPath(path, fn) {
45
- this.pathStack.push(path);
150
+ /**
151
+ * Execute function with a name segment pushed onto the stack.
152
+ * Automatically manages stack cleanup.
153
+ */
154
+ withName(name, fn) {
155
+ this.nameStack.push(name);
46
156
  try {
47
157
  return fn();
48
158
  }
49
159
  finally {
50
- this.pathStack.pop();
160
+ this.nameStack.pop();
51
161
  }
52
162
  }
53
- currentPath() {
54
- return this.pathStack[this.pathStack.length - 1] ?? "";
55
- }
56
- childPath(key) {
57
- const parent = this.currentPath();
58
- if (typeof key === "number") {
59
- return parent ? `${parent}[${key}]` : String(key);
60
- }
61
- return parent ? `${parent}.${key}` : key;
163
+ /**
164
+ * Get the current full name path for form binding.
165
+ * Returns empty string for root level.
166
+ */
167
+ currentName() {
168
+ return this.nameStack.join("");
62
169
  }
63
170
  // ════════════════════════════════════════════════════════════════════════
64
171
  // Service & Function Level
@@ -72,206 +179,486 @@ export class ArgumentFieldVisitor extends IDL.Visitor {
72
179
  }
73
180
  visitFunc(t, functionName) {
74
181
  const functionType = isQuery(t) ? "query" : "update";
182
+ const argCount = t.argTypes.length;
75
183
  const fields = t.argTypes.map((arg, index) => {
76
- return this.withPath(`[${index}]`, () => arg.accept(this, `__arg${index}`));
184
+ return this.withName(`[${index}]`, () => arg.accept(this, `__arg${index}`));
77
185
  });
78
- const defaultValues = fields.map((field) => this.extractDefaultValue(field));
186
+ const defaultValues = fields.map((field) => field.defaultValue);
187
+ // Handle empty args case for schema
188
+ // For no-arg functions, use an empty array schema
189
+ // For functions with args, use a proper tuple schema
190
+ const schema = argCount === 0
191
+ ? z.tuple([])
192
+ : z.tuple(fields.map((field) => field.schema));
79
193
  return {
80
194
  functionType,
81
195
  functionName,
82
196
  fields,
83
- defaultValues,
197
+ defaultValues: defaultValues,
198
+ schema,
199
+ argCount,
200
+ isNoArgs: argCount === 0,
84
201
  };
85
202
  }
86
- extractDefaultValue(field) {
87
- if ("defaultValue" in field) {
88
- return field.defaultValue;
89
- }
90
- if ("defaultValues" in field) {
91
- return field.defaultValues;
92
- }
93
- return undefined;
94
- }
95
203
  // ════════════════════════════════════════════════════════════════════════
96
204
  // Compound Types
97
205
  // ════════════════════════════════════════════════════════════════════════
98
206
  visitRecord(_t, fields_, label) {
99
- const path = this.currentPath();
207
+ const name = this.currentName();
100
208
  const fields = [];
101
- const defaultValues = {};
209
+ const fieldMap = new Map();
210
+ const defaultValue = {};
211
+ const schemaShape = {};
102
212
  for (const [key, type] of fields_) {
103
- const field = this.withPath(this.childPath(key), () => type.accept(this, key));
213
+ const field = this.withName(name ? `.${key}` : key, () => type.accept(this, key));
104
214
  fields.push(field);
105
- defaultValues[key] = this.extractDefaultValue(field);
215
+ fieldMap.set(key, field);
216
+ defaultValue[key] = field.defaultValue;
217
+ schemaShape[key] = field.schema;
106
218
  }
219
+ const schema = z.object(schemaShape);
107
220
  return {
108
221
  type: "record",
109
222
  label,
110
- path,
223
+ displayLabel: formatLabel(label),
224
+ name,
225
+ component: "record-container",
226
+ renderHint: COMPOUND_RENDER_HINT,
111
227
  fields,
112
- defaultValues,
228
+ fieldMap,
229
+ defaultValue,
230
+ schema,
231
+ candidType: "record",
113
232
  };
114
233
  }
115
234
  visitVariant(_t, fields_, label) {
116
- const path = this.currentPath();
235
+ const name = this.currentName();
117
236
  const fields = [];
118
237
  const options = [];
238
+ const optionMap = new Map();
239
+ const variantSchemas = [];
119
240
  for (const [key, type] of fields_) {
120
- const field = this.withPath(this.childPath(key), () => type.accept(this, key));
241
+ const field = this.withName(`.${key}`, () => type.accept(this, key));
121
242
  fields.push(field);
122
243
  options.push(key);
244
+ optionMap.set(key, field);
245
+ variantSchemas.push(z.object({ [key]: field.schema }));
123
246
  }
124
247
  const defaultOption = options[0];
125
- const defaultValues = {
126
- [defaultOption]: this.extractDefaultValue(fields[0]),
248
+ const firstField = fields[0];
249
+ const defaultValue = {
250
+ [defaultOption]: firstField.defaultValue,
251
+ };
252
+ const schema = z.union(variantSchemas);
253
+ // Helper to get default value for any option
254
+ const getOptionDefault = (option) => {
255
+ const optField = optionMap.get(option);
256
+ if (!optField) {
257
+ throw new Error(`Unknown variant option: ${option}`);
258
+ }
259
+ return { [option]: optField.defaultValue };
260
+ };
261
+ // Helper to get field for a specific option
262
+ const getField = (option) => {
263
+ const optField = optionMap.get(option);
264
+ if (!optField) {
265
+ throw new Error(`Unknown variant option: ${option}`);
266
+ }
267
+ return optField;
268
+ };
269
+ // Helper to get currently selected option from a value
270
+ const getSelectedOption = (value) => {
271
+ const validKeys = Object.keys(value).filter((k) => options.includes(k));
272
+ return validKeys[0] ?? defaultOption;
273
+ };
274
+ // Helper to get selected field from a value
275
+ const getSelectedField = (value) => {
276
+ const selectedOption = getSelectedOption(value);
277
+ return getField(selectedOption);
127
278
  };
128
279
  return {
129
280
  type: "variant",
130
281
  label,
131
- path,
282
+ displayLabel: formatLabel(label),
283
+ name,
284
+ component: "variant-select",
285
+ renderHint: COMPOUND_RENDER_HINT,
132
286
  fields,
133
287
  options,
134
288
  defaultOption,
135
- defaultValues,
289
+ optionMap,
290
+ defaultValue,
291
+ schema,
292
+ getOptionDefault,
293
+ getField,
294
+ getSelectedOption,
295
+ getSelectedField,
296
+ candidType: "variant",
136
297
  };
137
298
  }
138
299
  visitTuple(_t, components, label) {
139
- const path = this.currentPath();
300
+ const name = this.currentName();
140
301
  const fields = [];
141
- const defaultValues = [];
302
+ const defaultValue = [];
303
+ const schemas = [];
142
304
  for (let index = 0; index < components.length; index++) {
143
305
  const type = components[index];
144
- const field = this.withPath(this.childPath(index), () => type.accept(this, `_${index}_`));
306
+ const field = this.withName(`[${index}]`, () => type.accept(this, `_${index}_`));
145
307
  fields.push(field);
146
- defaultValues.push(this.extractDefaultValue(field));
308
+ defaultValue.push(field.defaultValue);
309
+ schemas.push(field.schema);
147
310
  }
311
+ const schema = z.tuple(schemas);
148
312
  return {
149
313
  type: "tuple",
150
314
  label,
151
- path,
315
+ displayLabel: formatLabel(label),
316
+ name,
317
+ component: "tuple-container",
318
+ renderHint: COMPOUND_RENDER_HINT,
152
319
  fields,
153
- defaultValues,
320
+ defaultValue,
321
+ schema,
322
+ candidType: "tuple",
154
323
  };
155
324
  }
156
325
  visitOpt(_t, ty, label) {
157
- const path = this.currentPath();
158
- const innerField = this.withPath(this.childPath(0), () => ty.accept(this, label));
326
+ const name = this.currentName();
327
+ // For optional, the inner field keeps the same name path
328
+ // because the value replaces null directly (not nested)
329
+ const innerField = ty.accept(this, label);
330
+ const schema = z.union([
331
+ innerField.schema,
332
+ z.null(),
333
+ z.undefined().transform(() => null),
334
+ z.literal("").transform(() => null),
335
+ ]);
336
+ // Helper to get the inner default when enabling the optional
337
+ const getInnerDefault = () => innerField.defaultValue;
338
+ // Helper to check if a value represents an enabled optional
339
+ const isEnabled = (value) => {
340
+ return value !== null && typeof value !== "undefined";
341
+ };
159
342
  return {
160
343
  type: "optional",
161
344
  label,
162
- path,
345
+ displayLabel: formatLabel(label),
346
+ name,
347
+ component: "optional-toggle",
348
+ renderHint: COMPOUND_RENDER_HINT,
163
349
  innerField,
164
350
  defaultValue: null,
351
+ schema,
352
+ getInnerDefault,
353
+ isEnabled,
354
+ candidType: "opt",
165
355
  };
166
356
  }
167
357
  visitVec(_t, ty, label) {
168
- const path = this.currentPath();
358
+ const name = this.currentName();
169
359
  // Check if it's blob (vec nat8)
170
360
  const isBlob = ty instanceof IDL.FixedNatClass && ty._bits === 8;
171
- const itemField = this.withPath(this.childPath(0), () => ty.accept(this, label));
361
+ // Item field uses [0] as template path
362
+ const itemField = this.withName("[0]", () => ty.accept(this, `${label}_item`));
172
363
  if (isBlob) {
364
+ const schema = z.union([
365
+ z.string(),
366
+ z.array(z.number()),
367
+ z.instanceof(Uint8Array),
368
+ ]);
369
+ const limits = { ...DEFAULT_BLOB_LIMITS };
173
370
  return {
174
371
  type: "blob",
175
372
  label,
176
- path,
373
+ displayLabel: formatLabel(label),
374
+ name,
375
+ component: "blob-upload",
376
+ renderHint: FILE_RENDER_HINT,
177
377
  itemField,
178
378
  defaultValue: "",
379
+ schema,
380
+ acceptedFormats: ["hex", "base64", "file"],
381
+ limits,
382
+ normalizeHex,
383
+ validateInput: (value) => validateBlobInput(value, limits),
384
+ candidType: "blob",
179
385
  };
180
386
  }
387
+ const schema = z.array(itemField.schema);
388
+ // Helper to get a new item with default values
389
+ const getItemDefault = () => itemField.defaultValue;
390
+ // Helper to create an item field for a specific index
391
+ const createItemField = (index, overrides) => {
392
+ // Replace [0] in template with actual index
393
+ const itemName = name ? `${name}[${index}]` : `[${index}]`;
394
+ const itemLabel = overrides?.label ?? `Item ${index}`;
395
+ return {
396
+ ...itemField,
397
+ name: itemName,
398
+ label: itemLabel,
399
+ displayLabel: formatLabel(itemLabel),
400
+ };
401
+ };
181
402
  return {
182
403
  type: "vector",
183
404
  label,
184
- path,
405
+ displayLabel: formatLabel(label),
406
+ name,
407
+ component: "vector-list",
408
+ renderHint: COMPOUND_RENDER_HINT,
185
409
  itemField,
186
410
  defaultValue: [],
411
+ schema,
412
+ getItemDefault,
413
+ createItemField,
414
+ candidType: "vec",
187
415
  };
188
416
  }
189
417
  visitRec(_t, ty, label) {
190
- const path = this.currentPath();
418
+ const name = this.currentName();
419
+ const typeName = ty.name || "RecursiveType";
420
+ let schema;
421
+ if (this.recursiveSchemas.has(typeName)) {
422
+ schema = this.recursiveSchemas.get(typeName);
423
+ }
424
+ else {
425
+ schema = z.lazy(() => ty.accept(this, label).schema);
426
+ this.recursiveSchemas.set(typeName, schema);
427
+ }
428
+ // Lazy extraction to prevent infinite loops
429
+ const extract = () => this.withName(name, () => ty.accept(this, label));
430
+ // Helper to get inner default (evaluates lazily)
431
+ const getInnerDefault = () => extract().defaultValue;
191
432
  return {
192
433
  type: "recursive",
193
434
  label,
194
- path,
195
- // Lazy extraction to prevent infinite loops
196
- extract: () => this.withPath(path, () => ty.accept(this, label)),
435
+ displayLabel: formatLabel(label),
436
+ name,
437
+ component: "recursive-lazy",
438
+ renderHint: COMPOUND_RENDER_HINT,
439
+ typeName,
440
+ extract,
441
+ defaultValue: undefined,
442
+ schema,
443
+ getInnerDefault,
444
+ candidType: "rec",
197
445
  };
198
446
  }
199
447
  // ════════════════════════════════════════════════════════════════════════
200
448
  // Primitive Types
201
449
  // ════════════════════════════════════════════════════════════════════════
202
450
  visitPrincipal(_t, label) {
451
+ const schema = z.custom((val) => {
452
+ if (val instanceof Principal)
453
+ return true;
454
+ if (typeof val === "string") {
455
+ try {
456
+ Principal.fromText(val);
457
+ return true;
458
+ }
459
+ catch {
460
+ return false;
461
+ }
462
+ }
463
+ return false;
464
+ }, {
465
+ message: "Invalid Principal format",
466
+ });
467
+ const inputProps = {
468
+ type: "text",
469
+ placeholder: "aaaaa-aa or full principal ID",
470
+ minLength: 7,
471
+ maxLength: 64,
472
+ spellCheck: false,
473
+ autoComplete: "off",
474
+ };
203
475
  return {
204
476
  type: "principal",
205
477
  label,
206
- path: this.currentPath(),
478
+ displayLabel: formatLabel(label),
479
+ name: this.currentName(),
480
+ component: "principal-input",
481
+ renderHint: TEXT_RENDER_HINT,
207
482
  defaultValue: "",
208
483
  maxLength: 64,
209
484
  minLength: 7,
485
+ schema,
486
+ inputProps,
487
+ candidType: "principal",
210
488
  };
211
489
  }
212
490
  visitText(_t, label) {
491
+ const inputProps = {
492
+ type: "text",
493
+ placeholder: "Enter text...",
494
+ spellCheck: true,
495
+ };
213
496
  return {
214
497
  type: "text",
215
498
  label,
216
- path: this.currentPath(),
499
+ displayLabel: formatLabel(label),
500
+ name: this.currentName(),
501
+ component: "text-input",
502
+ renderHint: TEXT_RENDER_HINT,
217
503
  defaultValue: "",
504
+ schema: z.string().min(1, "Required"),
505
+ inputProps,
506
+ candidType: "text",
218
507
  };
219
508
  }
220
509
  visitBool(_t, label) {
510
+ const inputProps = {
511
+ type: "checkbox",
512
+ };
221
513
  return {
222
514
  type: "boolean",
223
515
  label,
224
- path: this.currentPath(),
516
+ displayLabel: formatLabel(label),
517
+ name: this.currentName(),
518
+ component: "boolean-checkbox",
519
+ renderHint: CHECKBOX_RENDER_HINT,
225
520
  defaultValue: false,
521
+ schema: z.boolean(),
522
+ inputProps,
523
+ candidType: "bool",
226
524
  };
227
525
  }
228
526
  visitNull(_t, label) {
229
527
  return {
230
528
  type: "null",
231
529
  label,
232
- path: this.currentPath(),
530
+ displayLabel: formatLabel(label),
531
+ name: this.currentName(),
532
+ component: "null-hidden",
533
+ renderHint: {
534
+ isCompound: false,
535
+ isPrimitive: true,
536
+ },
233
537
  defaultValue: null,
538
+ schema: z.null(),
539
+ candidType: "null",
234
540
  };
235
541
  }
236
- // Numbers - all use string for display format
237
- visitNumberType(label, candidType) {
542
+ // ════════════════════════════════════════════════════════════════════════
543
+ // Number Types with Constraints
544
+ // ════════════════════════════════════════════════════════════════════════
545
+ visitNumberType(label, candidType, options) {
546
+ let schema = z.string().min(1, "Required");
547
+ if (options.isFloat) {
548
+ schema = schema.refine((val) => !isNaN(Number(val)), "Must be a number");
549
+ }
550
+ else if (options.unsigned) {
551
+ schema = schema.regex(/^\d+$/, "Must be a positive number");
552
+ }
553
+ else {
554
+ schema = schema.regex(/^-?\d+$/, "Must be a number");
555
+ }
556
+ // Use "text" type for large numbers (BigInt) to ensure precision and better UI handling
557
+ // Standard number input has issues with large integers
558
+ const isBigInt = !options.isFloat && (!options.bits || options.bits > 32);
559
+ const type = isBigInt ? "text" : "number";
560
+ if (type === "text") {
561
+ const inputProps = {
562
+ type: "text",
563
+ placeholder: options.unsigned ? "e.g. 100000" : "e.g. -100000",
564
+ inputMode: "numeric",
565
+ pattern: options.unsigned ? "\\d+" : "-?\\d+",
566
+ spellCheck: false,
567
+ autoComplete: "off",
568
+ };
569
+ return {
570
+ type: "text",
571
+ label,
572
+ displayLabel: formatLabel(label),
573
+ name: this.currentName(),
574
+ component: "text-input",
575
+ renderHint: TEXT_RENDER_HINT,
576
+ defaultValue: "",
577
+ candidType,
578
+ schema,
579
+ inputProps,
580
+ };
581
+ }
582
+ const inputProps = {
583
+ type: "number",
584
+ placeholder: options.isFloat ? "0.0" : "0",
585
+ inputMode: options.isFloat ? "decimal" : "numeric",
586
+ min: options.min,
587
+ max: options.max,
588
+ step: options.isFloat ? "any" : "1",
589
+ };
238
590
  return {
239
591
  type: "number",
240
592
  label,
241
- path: this.currentPath(),
593
+ displayLabel: formatLabel(label),
594
+ name: this.currentName(),
595
+ component: "number-input",
596
+ renderHint: NUMBER_RENDER_HINT,
242
597
  defaultValue: "",
243
598
  candidType,
599
+ schema: schema,
600
+ inputProps,
601
+ ...options,
244
602
  };
245
603
  }
246
604
  visitInt(_t, label) {
247
- return this.visitNumberType(label, "int");
605
+ return this.visitNumberType(label, "int", {
606
+ unsigned: false,
607
+ isFloat: false,
608
+ });
248
609
  }
249
610
  visitNat(_t, label) {
250
- return this.visitNumberType(label, "nat");
611
+ return this.visitNumberType(label, "nat", {
612
+ unsigned: true,
613
+ isFloat: false,
614
+ });
251
615
  }
252
- visitFloat(_t, label) {
253
- return this.visitNumberType(label, "float");
616
+ visitFloat(t, label) {
617
+ return this.visitNumberType(label, `float${t._bits}`, {
618
+ unsigned: false,
619
+ isFloat: true,
620
+ bits: t._bits,
621
+ });
254
622
  }
255
623
  visitFixedInt(t, label) {
256
- return this.visitNumberType(label, `int${t._bits}`);
624
+ const bits = t._bits;
625
+ // Calculate min/max for signed integers
626
+ const max = (BigInt(2) ** BigInt(bits - 1) - BigInt(1)).toString();
627
+ const min = (-(BigInt(2) ** BigInt(bits - 1))).toString();
628
+ return this.visitNumberType(label, `int${bits}`, {
629
+ unsigned: false,
630
+ isFloat: false,
631
+ bits,
632
+ min,
633
+ max,
634
+ });
257
635
  }
258
636
  visitFixedNat(t, label) {
259
- return this.visitNumberType(label, `nat${t._bits}`);
637
+ const bits = t._bits;
638
+ // Calculate max for unsigned integers
639
+ const max = (BigInt(2) ** BigInt(bits) - BigInt(1)).toString();
640
+ return this.visitNumberType(label, `nat${bits}`, {
641
+ unsigned: true,
642
+ isFloat: false,
643
+ bits,
644
+ min: "0",
645
+ max,
646
+ });
260
647
  }
261
648
  visitType(_t, label) {
262
649
  return {
263
650
  type: "unknown",
264
651
  label,
265
- path: this.currentPath(),
652
+ displayLabel: formatLabel(label),
653
+ name: this.currentName(),
654
+ component: "unknown-fallback",
655
+ renderHint: {
656
+ isCompound: false,
657
+ isPrimitive: false,
658
+ },
266
659
  defaultValue: undefined,
660
+ schema: z.any(),
267
661
  };
268
662
  }
269
663
  }
270
- // ════════════════════════════════════════════════════════════════════════════
271
- // Legacy Exports (for backward compatibility)
272
- // ════════════════════════════════════════════════════════════════════════════
273
- /**
274
- * @deprecated Use ArgumentFieldVisitor instead
275
- */
276
- export { ArgumentFieldVisitor as VisitTanstackField };
277
664
  //# sourceMappingURL=index.js.map