@ic-reactor/candid 3.0.13-beta.0 → 3.0.14-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.
@@ -2,4 +2,18 @@ export type FieldType = "functionRecord" | "function" | "record" | "variant" | "
2
2
  export type { Principal } from "@icp-sdk/core/principal";
3
3
  import type { IDL } from "@icp-sdk/core/candid";
4
4
  export type AllNumberTypes = IDL.NatClass | IDL.IntClass | IDL.FixedNatClass | IDL.FixedIntClass | IDL.FloatClass;
5
+ /**
6
+ * The core Candid type category used across visitors.
7
+ */
8
+ export type VisitorDataType = "record" | "variant" | "tuple" | "optional" | "vector" | "blob" | "recursive" | "principal" | "number" | "text" | "boolean" | "null" | "unknown";
9
+ /**
10
+ * Detected format for text fields based on label heuristics.
11
+ * Used to provide format-specific validation and display.
12
+ */
13
+ export type TextFormat = "plain" | "timestamp" | "uuid" | "url" | "email" | "phone" | "btc" | "eth" | "account-id" | "principal" | "cycle";
14
+ /**
15
+ * Detected format for number fields based on label heuristics.
16
+ * Used to provide format-specific validation and display.
17
+ */
18
+ export type NumberFormat = "timestamp" | "cycle" | "value" | "token" | "normal";
5
19
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/visitor/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GACjB,gBAAgB,GAChB,UAAU,GACV,QAAQ,GACR,SAAS,GACT,OAAO,GACP,UAAU,GACV,QAAQ,GACR,OAAO,GACP,MAAM,GACN,YAAY,GACZ,WAAW,GACX,SAAS,GACT,MAAM,GACN,QAAQ,GACR,WAAW,GACX,SAAS,GACT,MAAM,CAAA;AAEV,YAAY,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAExD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAA;AAC/C,MAAM,MAAM,cAAc,GACtB,GAAG,CAAC,QAAQ,GACZ,GAAG,CAAC,QAAQ,GACZ,GAAG,CAAC,aAAa,GACjB,GAAG,CAAC,aAAa,GACjB,GAAG,CAAC,UAAU,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/visitor/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GACjB,gBAAgB,GAChB,UAAU,GACV,QAAQ,GACR,SAAS,GACT,OAAO,GACP,UAAU,GACV,QAAQ,GACR,OAAO,GACP,MAAM,GACN,YAAY,GACZ,WAAW,GACX,SAAS,GACT,MAAM,GACN,QAAQ,GACR,WAAW,GACX,SAAS,GACT,MAAM,CAAA;AAEV,YAAY,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAExD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAA;AAC/C,MAAM,MAAM,cAAc,GACtB,GAAG,CAAC,QAAQ,GACZ,GAAG,CAAC,QAAQ,GACZ,GAAG,CAAC,aAAa,GACjB,GAAG,CAAC,aAAa,GACjB,GAAG,CAAC,UAAU,CAAA;AAMlB;;GAEG;AACH,MAAM,MAAM,eAAe,GACvB,QAAQ,GACR,SAAS,GACT,OAAO,GACP,UAAU,GACV,QAAQ,GACR,MAAM,GACN,WAAW,GACX,WAAW,GACX,QAAQ,GACR,MAAM,GACN,SAAS,GACT,MAAM,GACN,SAAS,CAAA;AAEb;;;GAGG;AACH,MAAM,MAAM,UAAU,GAClB,OAAO,GACP,WAAW,GACX,MAAM,GACN,KAAK,GACL,OAAO,GACP,OAAO,GACP,KAAK,GACL,KAAK,GACL,YAAY,GACZ,WAAW,GACX,OAAO,CAAA;AAEX;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ic-reactor/candid",
3
- "version": "3.0.13-beta.0",
3
+ "version": "3.0.14-beta.0",
4
4
  "description": "IC Reactor Candid Adapter - Fetch and parse Candid definitions from Internet Computer canisters",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -44,7 +44,7 @@
44
44
  "dependencies": {
45
45
  "@noble/hashes": "^2.0.1",
46
46
  "zod": "^4.3.5",
47
- "@ic-reactor/core": "^3.0.13-beta.0"
47
+ "@ic-reactor/core": "^3.0.14-beta.0"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "zod": "^4.3.5",
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ArgumentFieldType,
3
3
  CompoundField,
4
- Field,
4
+ FieldNode,
5
5
  FieldByType,
6
6
  PrimitiveField,
7
7
  RecordField,
@@ -28,14 +28,14 @@ import {
28
28
  * ```
29
29
  */
30
30
  export function isFieldType<T extends ArgumentFieldType>(
31
- field: Field,
31
+ field: FieldNode,
32
32
  type: T
33
33
  ): field is FieldByType<T> {
34
34
  return field.type === type
35
35
  }
36
36
 
37
37
  /** Check if a field is a compound type (contains other fields) */
38
- export function isCompoundField(field: Field): field is CompoundField {
38
+ export function isCompoundField(field: FieldNode): field is CompoundField {
39
39
  return [
40
40
  "record",
41
41
  "variant",
@@ -47,13 +47,13 @@ export function isCompoundField(field: Field): field is CompoundField {
47
47
  }
48
48
 
49
49
  /** Check if a field is a primitive type */
50
- export function isPrimitiveField(field: Field): field is PrimitiveField {
50
+ export function isPrimitiveField(field: FieldNode): field is PrimitiveField {
51
51
  return ["principal", "number", "text", "boolean", "null"].includes(field.type)
52
52
  }
53
53
 
54
54
  /** Check if a field has children (for iteration) */
55
55
  export function hasChildFields(
56
- field: Field
56
+ field: FieldNode
57
57
  ): field is RecordField | VariantField | TupleField {
58
58
  return "fields" in field && Array.isArray((field as RecordField).fields)
59
59
  }
@@ -1,6 +1,12 @@
1
1
  import { describe, it, expect } from "vitest"
2
2
  import { IDL } from "@icp-sdk/core/candid"
3
- import { FieldVisitor, VectorField } from "./index"
3
+ import {
4
+ FieldVisitor,
5
+ OptionalField,
6
+ RecordField,
7
+ VariantField,
8
+ VectorField,
9
+ } from "./index"
4
10
 
5
11
  describe("ArgumentFieldVisitor", () => {
6
12
  const visitor = new FieldVisitor()
@@ -186,7 +192,9 @@ describe("ArgumentFieldVisitor", () => {
186
192
  expect(field.type).toBe("record")
187
193
  expect(field.fields).toHaveLength(2)
188
194
 
189
- const addressField = field.fields.find((f) => f.label === "address")
195
+ const addressField = field.fields.find(
196
+ (f) => f.label === "address"
197
+ ) as RecordField
190
198
  if (!addressField || addressField.type !== "record") {
191
199
  throw new Error("Address field not found or not record")
192
200
  }
@@ -235,7 +243,7 @@ describe("ArgumentFieldVisitor", () => {
235
243
  expect(field.fields).toHaveLength(5)
236
244
 
237
245
  // Check 'to' field
238
- const toField = field.fields.find((f) => f.label === "to")
246
+ const toField = field.fields.find((f) => f.label === "to") as RecordField
239
247
  if (!toField || toField.type !== "record") {
240
248
  throw new Error("To field not found or not record")
241
249
  }
@@ -251,7 +259,9 @@ describe("ArgumentFieldVisitor", () => {
251
259
  expect(amountField.candidType).toBe("nat")
252
260
 
253
261
  // Check optional 'fee' field
254
- const feeField = field.fields.find((f) => f.label === "fee")
262
+ const feeField = field.fields.find(
263
+ (f) => f.label === "fee"
264
+ ) as OptionalField
255
265
  if (!feeField || feeField.type !== "optional") {
256
266
  throw new Error("Fee field not found or not optional")
257
267
  }
@@ -289,8 +299,8 @@ describe("ArgumentFieldVisitor", () => {
289
299
  })
290
300
 
291
301
  // Test getOptionDefault helper
292
- expect(field.getOptionDefault("Active")).toEqual({ Active: null })
293
- expect(field.getOptionDefault("Pending")).toEqual({ Pending: null })
302
+ expect(field.getOptionDefault("Active")).toEqual({ _type: "Active" })
303
+ expect(field.getOptionDefault("Pending")).toEqual({ _type: "Pending" })
294
304
  })
295
305
 
296
306
  it("should handle variant with payloads", () => {
@@ -330,7 +340,9 @@ describe("ArgumentFieldVisitor", () => {
330
340
  expect(field.type).toBe("variant")
331
341
  expect(field.options).toEqual(["Approve", "Burn", "Transfer"]) // Sorted order
332
342
 
333
- const transferField = field.fields.find((f) => f.label === "Transfer")
343
+ const transferField = field.fields.find(
344
+ (f) => f.label === "Transfer"
345
+ ) as RecordField
334
346
  if (!transferField || transferField.type !== "record") {
335
347
  throw new Error("Transfer field not found or not record")
336
348
  }
@@ -429,7 +441,7 @@ describe("ArgumentFieldVisitor", () => {
429
441
  expect(field.type).toBe("optional")
430
442
  expect(field.innerField.type).toBe("record")
431
443
  expect(field.innerField.type).toBe("record")
432
- const inner = field.innerField
444
+ const inner = field.innerField as RecordField
433
445
  if (inner.type === "record") {
434
446
  expect(inner.fields).toHaveLength(2)
435
447
  } else {
@@ -448,7 +460,7 @@ describe("ArgumentFieldVisitor", () => {
448
460
  expect(field.type).toBe("optional")
449
461
  expect(field.innerField.type).toBe("optional")
450
462
  expect(field.innerField.type).toBe("optional")
451
- const inner = field.innerField
463
+ const inner = field.innerField as OptionalField
452
464
  if (inner.type === "optional") {
453
465
  expect(inner.innerField.type).toBe("text")
454
466
  } else {
@@ -479,7 +491,7 @@ describe("ArgumentFieldVisitor", () => {
479
491
 
480
492
  expect(field.type).toBe("vector")
481
493
  expect(field.itemField.type).toBe("record")
482
- const item = field.itemField
494
+ const item = field.itemField as RecordField
483
495
  if (item.type === "record") {
484
496
  expect(item.fields).toHaveLength(2)
485
497
  } else {
@@ -509,7 +521,7 @@ describe("ArgumentFieldVisitor", () => {
509
521
 
510
522
  expect(field.type).toBe("vector")
511
523
  expect(field.itemField.type).toBe("vector")
512
- const item = field.itemField
524
+ const item = field.itemField as VectorField
513
525
  if (item.type === "vector") {
514
526
  expect(item.itemField.type).toBe("text")
515
527
  } else {
@@ -550,7 +562,7 @@ describe("ArgumentFieldVisitor", () => {
550
562
  expect(typeof field.getInnerDefault).toBe("function")
551
563
 
552
564
  // Extract should return a variant
553
- const extracted = field.extract()
565
+ const extracted = field.extract() as VariantField
554
566
  if (extracted.type !== "variant") {
555
567
  throw new Error("Extracted field is not variant")
556
568
  }
@@ -585,14 +597,16 @@ describe("ArgumentFieldVisitor", () => {
585
597
 
586
598
  expect(field.type).toBe("recursive")
587
599
 
588
- const extracted = field.extract()
600
+ const extracted = field.extract() as VariantField
589
601
  if (extracted.type !== "variant") {
590
602
  throw new Error("Extracted field is not variant")
591
603
  }
592
604
  expect(extracted.type).toBe("variant")
593
605
  expect(extracted.options).toEqual(["Nil", "Cons"])
594
606
 
595
- const consField = extracted.fields.find((f) => f.label === "Cons")
607
+ const consField = extracted.fields.find(
608
+ (f) => f.label === "Cons"
609
+ ) as RecordField
596
610
  if (!consField || consField.type !== "record") {
597
611
  throw new Error("Cons field not found or not record")
598
612
  }
@@ -642,7 +656,7 @@ describe("ArgumentFieldVisitor", () => {
642
656
  expect(meta.fields).toHaveLength(1)
643
657
  expect(meta.fields[0].type).toBe("record")
644
658
 
645
- const recordField = meta.fields[0]
659
+ const recordField = meta.fields[0] as RecordField
646
660
  if (recordField.type !== "record") {
647
661
  throw new Error("Expected record field")
648
662
  }
@@ -754,13 +768,15 @@ describe("ArgumentFieldVisitor", () => {
754
768
  )
755
769
  const meta = visitor.visitFunc(funcType, "updateUser")
756
770
 
757
- const argRecord = meta.fields[0]
771
+ const argRecord = meta.fields[0] as RecordField
758
772
  if (argRecord.type !== "record") {
759
773
  throw new Error("Expected record field")
760
774
  }
761
775
  expect(argRecord.name).toBe("[0]")
762
776
 
763
- const userRecord = argRecord.fields.find((f) => f.label === "user")
777
+ const userRecord = argRecord.fields.find(
778
+ (f) => f.label === "user"
779
+ ) as RecordField
764
780
  if (!userRecord || userRecord.type !== "record") {
765
781
  throw new Error("User record not found or not record")
766
782
  }
@@ -783,7 +799,7 @@ describe("ArgumentFieldVisitor", () => {
783
799
  const funcType = IDL.Func([IDL.Vec(IDL.Text)], [], [])
784
800
  const meta = visitor.visitFunc(funcType, "addTags")
785
801
 
786
- const vecField = meta.fields[0]
802
+ const vecField = meta.fields[0] as VectorField
787
803
  if (vecField.type !== "vector") {
788
804
  throw new Error("Expected vector field")
789
805
  }
@@ -837,7 +853,9 @@ describe("ArgumentFieldVisitor", () => {
837
853
  expect(field.fields.length).toBeGreaterThan(5)
838
854
 
839
855
  // Check spender field
840
- const spenderField = field.fields.find((f) => f.label === "spender")
856
+ const spenderField = field.fields.find(
857
+ (f) => f.label === "spender"
858
+ ) as RecordField
841
859
  if (!spenderField || spenderField.type !== "record") {
842
860
  throw new Error("Spender field not found or not record")
843
861
  }
@@ -906,7 +924,9 @@ describe("ArgumentFieldVisitor", () => {
906
924
  expect(field.options).toContain("UpgradeSnsControlledCanister")
907
925
 
908
926
  // Check Motion variant
909
- const motionField = field.fields.find((f) => f.label === "Motion")
927
+ const motionField = field.fields.find(
928
+ (f) => f.label === "Motion"
929
+ ) as RecordField
910
930
  if (!motionField || motionField.type !== "record") {
911
931
  throw new Error("Motion field not found or not record")
912
932
  }
@@ -916,7 +936,7 @@ describe("ArgumentFieldVisitor", () => {
916
936
  // Check TransferSnsTreasuryFunds variant
917
937
  const transferField = field.fields.find(
918
938
  (f) => f.label === "TransferSnsTreasuryFunds"
919
- )
939
+ ) as RecordField
920
940
  if (!transferField || transferField.type !== "record") {
921
941
  throw new Error("Transfer field not found or not record")
922
942
  }
@@ -944,8 +964,9 @@ describe("ArgumentFieldVisitor", () => {
944
964
  "status"
945
965
  )
946
966
 
947
- expect(field.getOptionDefault("Active")).toEqual({ Active: null })
967
+ expect(field.getOptionDefault("Active")).toEqual({ _type: "Active" })
948
968
  expect(field.getOptionDefault("Pending")).toEqual({
969
+ _type: "Pending",
949
970
  Pending: { reason: "" },
950
971
  })
951
972
  })
@@ -1,6 +1,7 @@
1
1
  import { isQuery } from "../helpers"
2
+ import { checkTextFormat, checkNumberFormat } from "../constants"
2
3
  import type {
3
- Field,
4
+ FieldNode,
4
5
  RecordField,
5
6
  VariantField,
6
7
  TupleField,
@@ -20,6 +21,8 @@ import type {
20
21
  PrimitiveInputProps,
21
22
  BlobLimits,
22
23
  BlobValidationResult,
24
+ TextFormat,
25
+ NumberFormat,
23
26
  } from "./types"
24
27
 
25
28
  import { IDL } from "@icp-sdk/core/candid"
@@ -29,6 +32,8 @@ import * as z from "zod"
29
32
  import { formatLabel } from "./helpers"
30
33
 
31
34
  export * from "./types"
35
+ export * from "./helpers"
36
+ export { checkTextFormat, checkNumberFormat } from "../constants"
32
37
 
33
38
  // ════════════════════════════════════════════════════════════════════════════
34
39
  // Render Hint Helpers
@@ -177,7 +182,7 @@ function validateBlobInput(
177
182
  */
178
183
  export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
179
184
  string,
180
- Field | ArgumentsMeta<A> | ArgumentsServiceMeta<A>
185
+ FieldNode | ArgumentsMeta<A> | ArgumentsServiceMeta<A>
181
186
  > {
182
187
  public recursiveSchemas: Map<string, z.ZodTypeAny> = new Map()
183
188
 
@@ -231,7 +236,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
231
236
  const fields = t.argTypes.map((arg, index) => {
232
237
  return this.withName(`[${index}]`, () =>
233
238
  arg.accept(this, `__arg${index}`)
234
- ) as Field
239
+ ) as FieldNode
235
240
  })
236
241
 
237
242
  const defaultValues = fields.map((field) => field.defaultValue)
@@ -272,15 +277,15 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
272
277
  label: string
273
278
  ): RecordField {
274
279
  const name = this.currentName()
275
- const fields: Field[] = []
276
- const fieldMap = new Map<string, Field>()
280
+ const fields: FieldNode[] = []
281
+ const fieldMap = new Map<string, FieldNode>()
277
282
  const defaultValue: Record<string, unknown> = {}
278
283
  const schemaShape: Record<string, z.ZodTypeAny> = {}
279
284
 
280
285
  for (const [key, type] of fields_) {
281
286
  const field = this.withName(name ? `.${key}` : key, () =>
282
287
  type.accept(this, key)
283
- ) as Field
288
+ ) as FieldNode
284
289
 
285
290
  fields.push(field)
286
291
  fieldMap.set(key, field)
@@ -311,29 +316,44 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
311
316
  label: string
312
317
  ): VariantField {
313
318
  const name = this.currentName()
314
- const fields: Field[] = []
319
+ const fields: FieldNode[] = []
315
320
  const options: string[] = []
316
- const optionMap = new Map<string, Field>()
321
+ const optionMap = new Map<string, FieldNode>()
317
322
  const variantSchemas: z.ZodTypeAny[] = []
318
323
 
319
324
  for (const [key, type] of fields_) {
320
325
  const field = this.withName(`.${key}`, () =>
321
326
  type.accept(this, key)
322
- ) as Field
327
+ ) as FieldNode
323
328
 
324
329
  fields.push(field)
325
330
  options.push(key)
326
331
  optionMap.set(key, field)
327
- variantSchemas.push(z.object({ [key]: field.schema }))
332
+
333
+ if (field.type === "null") {
334
+ variantSchemas.push(z.object({ _type: z.literal(key) }))
335
+ } else {
336
+ variantSchemas.push(
337
+ z.object({
338
+ _type: z.literal(key),
339
+ [key]: field.schema,
340
+ })
341
+ )
342
+ }
328
343
  }
329
344
 
330
345
  const defaultOption = options[0]
331
346
  const firstField = fields[0]
332
- const defaultValue = {
333
- [defaultOption]: firstField.defaultValue,
334
- }
335
347
 
336
- const schema = z.union(variantSchemas as [z.ZodTypeAny, ...z.ZodTypeAny[]])
348
+ const defaultValue =
349
+ firstField.type === "null"
350
+ ? { _type: defaultOption }
351
+ : {
352
+ _type: defaultOption,
353
+ [defaultOption]: firstField.defaultValue,
354
+ }
355
+
356
+ const schema = z.union(variantSchemas)
337
357
 
338
358
  // Helper to get default value for any option
339
359
  const getOptionDefault = (option: string): Record<string, unknown> => {
@@ -341,11 +361,13 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
341
361
  if (!optField) {
342
362
  throw new Error(`Unknown variant option: ${option}`)
343
363
  }
344
- return { [option]: optField.defaultValue }
364
+ return optField.type === "null"
365
+ ? { _type: option }
366
+ : { _type: option, [option]: optField.defaultValue }
345
367
  }
346
368
 
347
369
  // Helper to get field for a specific option
348
- const getField = (option: string): Field => {
370
+ const getField = (option: string): FieldNode => {
349
371
  const optField = optionMap.get(option)
350
372
  if (!optField) {
351
373
  throw new Error(`Unknown variant option: ${option}`)
@@ -355,12 +377,15 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
355
377
 
356
378
  // Helper to get currently selected option from a value
357
379
  const getSelectedOption = (value: Record<string, unknown>): string => {
380
+ if (value._type && typeof value._type === "string") {
381
+ return value._type
382
+ }
358
383
  const validKeys = Object.keys(value).filter((k) => options.includes(k))
359
384
  return validKeys[0] ?? defaultOption
360
385
  }
361
386
 
362
387
  // Helper to get selected field from a value
363
- const getSelectedField = (value: Record<string, unknown>): Field => {
388
+ const getSelectedField = (value: Record<string, unknown>): FieldNode => {
364
389
  const selectedOption = getSelectedOption(value)
365
390
  return getField(selectedOption)
366
391
  }
@@ -392,7 +417,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
392
417
  label: string
393
418
  ): TupleField {
394
419
  const name = this.currentName()
395
- const fields: Field[] = []
420
+ const fields: FieldNode[] = []
396
421
  const defaultValue: unknown[] = []
397
422
  const schemas: z.ZodTypeAny[] = []
398
423
 
@@ -400,7 +425,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
400
425
  const type = components[index]
401
426
  const field = this.withName(`[${index}]`, () =>
402
427
  type.accept(this, `_${index}_`)
403
- ) as Field
428
+ ) as FieldNode
404
429
 
405
430
  fields.push(field)
406
431
  defaultValue.push(field.defaultValue)
@@ -432,7 +457,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
432
457
 
433
458
  // For optional, the inner field keeps the same name path
434
459
  // because the value replaces null directly (not nested)
435
- const innerField = ty.accept(this, label) as Field
460
+ const innerField = ty.accept(this, label) as FieldNode
436
461
 
437
462
  const schema = z.union([
438
463
  innerField.schema,
@@ -478,7 +503,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
478
503
  // Item field uses [0] as template path
479
504
  const itemField = this.withName("[0]", () =>
480
505
  ty.accept(this, `${label}_item`)
481
- ) as Field
506
+ ) as FieldNode
482
507
 
483
508
  if (isBlob) {
484
509
  const schema = z.union([
@@ -517,7 +542,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
517
542
  const createItemField = (
518
543
  index: number,
519
544
  overrides?: { label?: string }
520
- ): Field => {
545
+ ): FieldNode => {
521
546
  // Replace [0] in template with actual index
522
547
  const itemName = name ? `${name}[${index}]` : `[${index}]`
523
548
  const itemLabel = overrides?.label ?? `Item ${index}`
@@ -559,13 +584,13 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
559
584
  if (this.recursiveSchemas.has(typeName)) {
560
585
  schema = this.recursiveSchemas.get(typeName)!
561
586
  } else {
562
- schema = z.lazy(() => (ty.accept(this, label) as Field).schema)
587
+ schema = z.lazy(() => (ty.accept(this, label) as FieldNode).schema)
563
588
  this.recursiveSchemas.set(typeName, schema)
564
589
  }
565
590
 
566
591
  // Lazy extraction to prevent infinite loops
567
- const extract = (): Field =>
568
- this.withName(name, () => ty.accept(this, label)) as Field
592
+ const extract = (): FieldNode =>
593
+ this.withName(name, () => ty.accept(this, label)) as FieldNode
569
594
 
570
595
  // Helper to get inner default (evaluates lazily)
571
596
  const getInnerDefault = (): unknown => extract().defaultValue
@@ -628,6 +653,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
628
653
  defaultValue: "",
629
654
  maxLength: 64,
630
655
  minLength: 7,
656
+ format: checkTextFormat(label) as TextFormat,
631
657
  schema,
632
658
  inputProps,
633
659
  candidType: "principal",
@@ -635,11 +661,13 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
635
661
  }
636
662
 
637
663
  public visitText(_t: IDL.TextClass, label: string): TextField {
638
- const inputProps: PrimitiveInputProps = {
639
- type: "text",
640
- placeholder: "Enter text...",
641
- spellCheck: true,
642
- }
664
+ const format = checkTextFormat(label) as TextFormat
665
+
666
+ // Generate format-specific inputProps
667
+ const inputProps = this.getTextInputProps(format)
668
+
669
+ // Generate format-specific schema
670
+ const schema = this.getTextSchema(format)
643
671
 
644
672
  return {
645
673
  type: "text",
@@ -649,12 +677,109 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
649
677
  component: "text-input",
650
678
  renderHint: TEXT_RENDER_HINT,
651
679
  defaultValue: "",
652
- schema: z.string().min(1, "Required"),
680
+ format,
681
+ schema,
653
682
  inputProps,
654
683
  candidType: "text",
655
684
  }
656
685
  }
657
686
 
687
+ /**
688
+ * Generate format-specific input props for text fields.
689
+ */
690
+ private getTextInputProps(format: TextFormat): PrimitiveInputProps {
691
+ switch (format) {
692
+ case "email":
693
+ return {
694
+ type: "email",
695
+ placeholder: "email@example.com",
696
+ inputMode: "email",
697
+ autoComplete: "email",
698
+ spellCheck: false,
699
+ }
700
+ case "url":
701
+ return {
702
+ type: "url",
703
+ placeholder: "https://example.com",
704
+ inputMode: "url",
705
+ autoComplete: "url",
706
+ spellCheck: false,
707
+ }
708
+ case "phone":
709
+ return {
710
+ type: "tel",
711
+ placeholder: "+1 (555) 123-4567",
712
+ inputMode: "tel",
713
+ autoComplete: "tel",
714
+ spellCheck: false,
715
+ }
716
+ case "uuid":
717
+ return {
718
+ type: "text",
719
+ placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
720
+ pattern:
721
+ "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}",
722
+ spellCheck: false,
723
+ autoComplete: "off",
724
+ }
725
+ case "btc":
726
+ return {
727
+ type: "text",
728
+ placeholder: "bc1... or 1... or 3...",
729
+ spellCheck: false,
730
+ autoComplete: "off",
731
+ }
732
+ case "eth":
733
+ return {
734
+ type: "text",
735
+ placeholder: "0x...",
736
+ pattern: "0x[0-9a-fA-F]{40}",
737
+ spellCheck: false,
738
+ autoComplete: "off",
739
+ }
740
+ case "account-id":
741
+ return {
742
+ type: "text",
743
+ placeholder: "64-character hex string",
744
+ pattern: "[0-9a-fA-F]{64}",
745
+ maxLength: 64,
746
+ spellCheck: false,
747
+ autoComplete: "off",
748
+ }
749
+ case "principal":
750
+ return {
751
+ type: "text",
752
+ placeholder: "aaaaa-aa or full principal ID",
753
+ minLength: 7,
754
+ maxLength: 64,
755
+ spellCheck: false,
756
+ autoComplete: "off",
757
+ }
758
+ default:
759
+ return {
760
+ type: "text",
761
+ placeholder: "Enter text...",
762
+ spellCheck: true,
763
+ }
764
+ }
765
+ }
766
+
767
+ /**
768
+ * Generate format-specific zod schema for text fields.
769
+ */
770
+ private getTextSchema(format: TextFormat): z.ZodTypeAny {
771
+ switch (format) {
772
+ case "email":
773
+ return z.email("Invalid email address")
774
+ case "url":
775
+ return z.url("Invalid URL")
776
+ case "uuid":
777
+ return z.uuid("Invalid UUID")
778
+ default:
779
+ return z.string().min(1, "Required")
780
+ }
781
+ }
782
+
658
783
  public visitBool(_t: IDL.BoolClass, label: string): BooleanField {
659
784
  const inputProps: PrimitiveInputProps = {
660
785
  type: "checkbox",
@@ -706,6 +831,8 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
706
831
  max?: string
707
832
  }
708
833
  ): NumberField | TextField {
834
+ const format = checkNumberFormat(label) as NumberFormat
835
+
709
836
  let schema = z.string().min(1, "Required")
710
837
 
711
838
  if (options.isFloat) {
@@ -722,6 +849,11 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
722
849
  const type = isBigInt ? "text" : "number"
723
850
 
724
851
  if (type === "text") {
852
+ // Propagate timestamp/cycle format if detected, otherwise default to plain
853
+ let textFormat: TextFormat = "plain"
854
+ if (format === "timestamp") textFormat = "timestamp"
855
+ if (format === "cycle") textFormat = "cycle"
856
+
725
857
  const inputProps: PrimitiveInputProps = {
726
858
  type: "text",
727
859
  placeholder: options.unsigned ? "e.g. 100000" : "e.g. -100000",
@@ -739,6 +871,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
739
871
  component: "text-input",
740
872
  renderHint: TEXT_RENDER_HINT,
741
873
  defaultValue: "",
874
+ format: textFormat,
742
875
  candidType,
743
876
  schema,
744
877
  inputProps,
@@ -763,7 +896,8 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
763
896
  renderHint: NUMBER_RENDER_HINT,
764
897
  defaultValue: "",
765
898
  candidType,
766
- schema: schema,
899
+ format,
900
+ schema,
767
901
  inputProps,
768
902
  ...options,
769
903
  }
@@ -838,6 +972,7 @@ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
838
972
  isPrimitive: false,
839
973
  },
840
974
  defaultValue: undefined,
975
+ candidType: "unknown",
841
976
  schema: z.any(),
842
977
  }
843
978
  }