@ic-reactor/candid 3.0.12-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.
@@ -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,
@@ -16,17 +17,119 @@ import type {
16
17
  UnknownField,
17
18
  ArgumentsMeta,
18
19
  ArgumentsServiceMeta,
20
+ RenderHint,
21
+ PrimitiveInputProps,
22
+ BlobLimits,
23
+ BlobValidationResult,
24
+ TextFormat,
25
+ NumberFormat,
19
26
  } from "./types"
20
27
 
21
28
  import { IDL } from "@icp-sdk/core/candid"
22
29
  import { Principal } from "@icp-sdk/core/principal"
23
30
  import { BaseActor, FunctionName } from "@ic-reactor/core"
24
31
  import * as z from "zod"
32
+ import { formatLabel } from "./helpers"
25
33
 
26
34
  export * from "./types"
35
+ export * from "./helpers"
36
+ export { checkTextFormat, checkNumberFormat } from "../constants"
37
+
38
+ // ════════════════════════════════════════════════════════════════════════════
39
+ // Render Hint Helpers
40
+ // ════════════════════════════════════════════════════════════════════════════
41
+
42
+ const COMPOUND_RENDER_HINT: RenderHint = {
43
+ isCompound: true,
44
+ isPrimitive: false,
45
+ }
46
+
47
+ const TEXT_RENDER_HINT: RenderHint = {
48
+ isCompound: false,
49
+ isPrimitive: true,
50
+ inputType: "text",
51
+ }
52
+
53
+ const NUMBER_RENDER_HINT: RenderHint = {
54
+ isCompound: false,
55
+ isPrimitive: true,
56
+ inputType: "number",
57
+ }
58
+
59
+ const CHECKBOX_RENDER_HINT: RenderHint = {
60
+ isCompound: false,
61
+ isPrimitive: true,
62
+ inputType: "checkbox",
63
+ }
64
+
65
+ const FILE_RENDER_HINT: RenderHint = {
66
+ isCompound: false,
67
+ isPrimitive: true,
68
+ inputType: "file",
69
+ }
70
+
71
+ // ════════════════════════════════════════════════════════════════════════════
72
+ // Blob Field Helpers
73
+ // ════════════════════════════════════════════════════════════════════════════
74
+
75
+ const DEFAULT_BLOB_LIMITS: BlobLimits = {
76
+ maxHexBytes: 512,
77
+ maxFileBytes: 2 * 1024 * 1024, // 2MB
78
+ maxHexDisplayLength: 128,
79
+ }
80
+
81
+ function normalizeHex(input: string): string {
82
+ // Remove 0x prefix and convert to lowercase
83
+ let hex = input.toLowerCase()
84
+ if (hex.startsWith("0x")) {
85
+ hex = hex.slice(2)
86
+ }
87
+ // Remove any whitespace
88
+ hex = hex.replace(/\s/g, "")
89
+ return hex
90
+ }
91
+
92
+ function validateBlobInput(
93
+ value: string | Uint8Array,
94
+ limits: BlobLimits
95
+ ): BlobValidationResult {
96
+ if (value instanceof Uint8Array) {
97
+ if (value.length > limits.maxFileBytes) {
98
+ return {
99
+ valid: false,
100
+ error: `File size exceeds maximum of ${limits.maxFileBytes} bytes`,
101
+ }
102
+ }
103
+ return { valid: true }
104
+ }
105
+
106
+ // String input (hex)
107
+ const normalized = normalizeHex(value)
108
+ if (normalized.length === 0) {
109
+ return { valid: true } // Empty is valid
110
+ }
111
+
112
+ if (!/^[0-9a-f]*$/.test(normalized)) {
113
+ return { valid: false, error: "Invalid hex characters" }
114
+ }
115
+
116
+ if (normalized.length % 2 !== 0) {
117
+ return { valid: false, error: "Hex string must have even length" }
118
+ }
119
+
120
+ const byteLength = normalized.length / 2
121
+ if (byteLength > limits.maxHexBytes) {
122
+ return {
123
+ valid: false,
124
+ error: `Hex input exceeds maximum of ${limits.maxHexBytes} bytes`,
125
+ }
126
+ }
127
+
128
+ return { valid: true }
129
+ }
27
130
 
28
131
  /**
29
- * ArgumentFieldVisitor generates metadata for form input fields from Candid IDL types.
132
+ * FieldVisitor generates metadata for form input fields from Candid IDL types.
30
133
  *
31
134
  * ## Design Principles
32
135
  *
@@ -40,8 +143,11 @@ export * from "./types"
40
143
  *
41
144
  * Each field has:
42
145
  * - `type`: The field type (record, variant, text, number, etc.)
43
- * - `label`: Human-readable label from Candid
146
+ * - `label`: Raw label from Candid
147
+ * - `displayLabel`: Human-readable formatted label
44
148
  * - `name`: TanStack Form compatible path (e.g., "[0]", "[0].owner", "tags[1]")
149
+ * - `component`: Suggested component type for rendering
150
+ * - `renderHint`: Hints for UI rendering strategy
45
151
  * - `defaultValue`: Initial value for the form
46
152
  * - `schema`: Zod schema for validation
47
153
  * - Type-specific properties (options for variant, fields for record, etc.)
@@ -52,9 +158,9 @@ export * from "./types"
52
158
  * @example
53
159
  * ```typescript
54
160
  * import { useForm } from '@tanstack/react-form'
55
- * import { ArgumentFieldVisitor } from '@ic-reactor/candid'
161
+ * import { FieldVisitor } from '@ic-reactor/candid'
56
162
  *
57
- * const visitor = new ArgumentFieldVisitor()
163
+ * const visitor = new FieldVisitor()
58
164
  * const serviceMeta = service.accept(visitor, null)
59
165
  * const methodMeta = serviceMeta["icrc1_transfer"]
60
166
  *
@@ -74,9 +180,9 @@ export * from "./types"
74
180
  * ))
75
181
  * ```
76
182
  */
77
- export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
183
+ export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
78
184
  string,
79
- Field | ArgumentsMeta<A> | ArgumentsServiceMeta<A>
185
+ FieldNode | ArgumentsMeta<A> | ArgumentsServiceMeta<A>
80
186
  > {
81
187
  public recursiveSchemas: Map<string, z.ZodTypeAny> = new Map()
82
188
 
@@ -130,7 +236,7 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
130
236
  const fields = t.argTypes.map((arg, index) => {
131
237
  return this.withName(`[${index}]`, () =>
132
238
  arg.accept(this, `__arg${index}`)
133
- ) as Field
239
+ ) as FieldNode
134
240
  })
135
241
 
136
242
  const defaultValues = fields.map((field) => field.defaultValue)
@@ -171,15 +277,15 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
171
277
  label: string
172
278
  ): RecordField {
173
279
  const name = this.currentName()
174
- const fields: Field[] = []
175
- const fieldMap = new Map<string, Field>()
280
+ const fields: FieldNode[] = []
281
+ const fieldMap = new Map<string, FieldNode>()
176
282
  const defaultValue: Record<string, unknown> = {}
177
283
  const schemaShape: Record<string, z.ZodTypeAny> = {}
178
284
 
179
285
  for (const [key, type] of fields_) {
180
286
  const field = this.withName(name ? `.${key}` : key, () =>
181
287
  type.accept(this, key)
182
- ) as Field
288
+ ) as FieldNode
183
289
 
184
290
  fields.push(field)
185
291
  fieldMap.set(key, field)
@@ -192,7 +298,10 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
192
298
  return {
193
299
  type: "record",
194
300
  label,
301
+ displayLabel: formatLabel(label),
195
302
  name,
303
+ component: "record-container",
304
+ renderHint: COMPOUND_RENDER_HINT,
196
305
  fields,
197
306
  fieldMap,
198
307
  defaultValue,
@@ -207,29 +316,44 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
207
316
  label: string
208
317
  ): VariantField {
209
318
  const name = this.currentName()
210
- const fields: Field[] = []
319
+ const fields: FieldNode[] = []
211
320
  const options: string[] = []
212
- const optionMap = new Map<string, Field>()
321
+ const optionMap = new Map<string, FieldNode>()
213
322
  const variantSchemas: z.ZodTypeAny[] = []
214
323
 
215
324
  for (const [key, type] of fields_) {
216
325
  const field = this.withName(`.${key}`, () =>
217
326
  type.accept(this, key)
218
- ) as Field
327
+ ) as FieldNode
219
328
 
220
329
  fields.push(field)
221
330
  options.push(key)
222
331
  optionMap.set(key, field)
223
- 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
+ }
224
343
  }
225
344
 
226
345
  const defaultOption = options[0]
227
346
  const firstField = fields[0]
228
- const defaultValue = {
229
- [defaultOption]: firstField.defaultValue,
230
- }
231
347
 
232
- 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)
233
357
 
234
358
  // Helper to get default value for any option
235
359
  const getOptionDefault = (option: string): Record<string, unknown> => {
@@ -237,13 +361,42 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
237
361
  if (!optField) {
238
362
  throw new Error(`Unknown variant option: ${option}`)
239
363
  }
240
- return { [option]: optField.defaultValue }
364
+ return optField.type === "null"
365
+ ? { _type: option }
366
+ : { _type: option, [option]: optField.defaultValue }
367
+ }
368
+
369
+ // Helper to get field for a specific option
370
+ const getField = (option: string): FieldNode => {
371
+ const optField = optionMap.get(option)
372
+ if (!optField) {
373
+ throw new Error(`Unknown variant option: ${option}`)
374
+ }
375
+ return optField
376
+ }
377
+
378
+ // Helper to get currently selected option from a value
379
+ const getSelectedOption = (value: Record<string, unknown>): string => {
380
+ if (value._type && typeof value._type === "string") {
381
+ return value._type
382
+ }
383
+ const validKeys = Object.keys(value).filter((k) => options.includes(k))
384
+ return validKeys[0] ?? defaultOption
385
+ }
386
+
387
+ // Helper to get selected field from a value
388
+ const getSelectedField = (value: Record<string, unknown>): FieldNode => {
389
+ const selectedOption = getSelectedOption(value)
390
+ return getField(selectedOption)
241
391
  }
242
392
 
243
393
  return {
244
394
  type: "variant",
245
395
  label,
396
+ displayLabel: formatLabel(label),
246
397
  name,
398
+ component: "variant-select",
399
+ renderHint: COMPOUND_RENDER_HINT,
247
400
  fields,
248
401
  options,
249
402
  defaultOption,
@@ -251,6 +404,9 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
251
404
  defaultValue,
252
405
  schema,
253
406
  getOptionDefault,
407
+ getField,
408
+ getSelectedOption,
409
+ getSelectedField,
254
410
  candidType: "variant",
255
411
  }
256
412
  }
@@ -261,7 +417,7 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
261
417
  label: string
262
418
  ): TupleField {
263
419
  const name = this.currentName()
264
- const fields: Field[] = []
420
+ const fields: FieldNode[] = []
265
421
  const defaultValue: unknown[] = []
266
422
  const schemas: z.ZodTypeAny[] = []
267
423
 
@@ -269,7 +425,7 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
269
425
  const type = components[index]
270
426
  const field = this.withName(`[${index}]`, () =>
271
427
  type.accept(this, `_${index}_`)
272
- ) as Field
428
+ ) as FieldNode
273
429
 
274
430
  fields.push(field)
275
431
  defaultValue.push(field.defaultValue)
@@ -281,7 +437,10 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
281
437
  return {
282
438
  type: "tuple",
283
439
  label,
440
+ displayLabel: formatLabel(label),
284
441
  name,
442
+ component: "tuple-container",
443
+ renderHint: COMPOUND_RENDER_HINT,
285
444
  fields,
286
445
  defaultValue,
287
446
  schema,
@@ -298,7 +457,7 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
298
457
 
299
458
  // For optional, the inner field keeps the same name path
300
459
  // because the value replaces null directly (not nested)
301
- const innerField = ty.accept(this, label) as Field
460
+ const innerField = ty.accept(this, label) as FieldNode
302
461
 
303
462
  const schema = z.union([
304
463
  innerField.schema,
@@ -310,14 +469,23 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
310
469
  // Helper to get the inner default when enabling the optional
311
470
  const getInnerDefault = (): unknown => innerField.defaultValue
312
471
 
472
+ // Helper to check if a value represents an enabled optional
473
+ const isEnabled = (value: unknown): boolean => {
474
+ return value !== null && typeof value !== "undefined"
475
+ }
476
+
313
477
  return {
314
478
  type: "optional",
315
479
  label,
480
+ displayLabel: formatLabel(label),
316
481
  name,
482
+ component: "optional-toggle",
483
+ renderHint: COMPOUND_RENDER_HINT,
317
484
  innerField,
318
485
  defaultValue: null,
319
486
  schema,
320
487
  getInnerDefault,
488
+ isEnabled,
321
489
  candidType: "opt",
322
490
  }
323
491
  }
@@ -335,7 +503,7 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
335
503
  // Item field uses [0] as template path
336
504
  const itemField = this.withName("[0]", () =>
337
505
  ty.accept(this, `${label}_item`)
338
- ) as Field
506
+ ) as FieldNode
339
507
 
340
508
  if (isBlob) {
341
509
  const schema = z.union([
@@ -343,14 +511,24 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
343
511
  z.array(z.number()),
344
512
  z.instanceof(Uint8Array),
345
513
  ])
514
+
515
+ const limits = { ...DEFAULT_BLOB_LIMITS }
516
+
346
517
  return {
347
518
  type: "blob",
348
519
  label,
520
+ displayLabel: formatLabel(label),
349
521
  name,
522
+ component: "blob-upload",
523
+ renderHint: FILE_RENDER_HINT,
350
524
  itemField,
351
525
  defaultValue: "",
352
526
  schema,
353
527
  acceptedFormats: ["hex", "base64", "file"],
528
+ limits,
529
+ normalizeHex,
530
+ validateInput: (value: string | Uint8Array) =>
531
+ validateBlobInput(value, limits),
354
532
  candidType: "blob",
355
533
  }
356
534
  }
@@ -360,14 +538,35 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
360
538
  // Helper to get a new item with default values
361
539
  const getItemDefault = (): unknown => itemField.defaultValue
362
540
 
541
+ // Helper to create an item field for a specific index
542
+ const createItemField = (
543
+ index: number,
544
+ overrides?: { label?: string }
545
+ ): FieldNode => {
546
+ // Replace [0] in template with actual index
547
+ const itemName = name ? `${name}[${index}]` : `[${index}]`
548
+ const itemLabel = overrides?.label ?? `Item ${index}`
549
+
550
+ return {
551
+ ...itemField,
552
+ name: itemName,
553
+ label: itemLabel,
554
+ displayLabel: formatLabel(itemLabel),
555
+ }
556
+ }
557
+
363
558
  return {
364
559
  type: "vector",
365
560
  label,
561
+ displayLabel: formatLabel(label),
366
562
  name,
563
+ component: "vector-list",
564
+ renderHint: COMPOUND_RENDER_HINT,
367
565
  itemField,
368
566
  defaultValue: [],
369
567
  schema,
370
568
  getItemDefault,
569
+ createItemField,
371
570
  candidType: "vec",
372
571
  }
373
572
  }
@@ -385,13 +584,13 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
385
584
  if (this.recursiveSchemas.has(typeName)) {
386
585
  schema = this.recursiveSchemas.get(typeName)!
387
586
  } else {
388
- schema = z.lazy(() => (ty.accept(this, label) as Field).schema)
587
+ schema = z.lazy(() => (ty.accept(this, label) as FieldNode).schema)
389
588
  this.recursiveSchemas.set(typeName, schema)
390
589
  }
391
590
 
392
591
  // Lazy extraction to prevent infinite loops
393
- const extract = (): Field =>
394
- this.withName(name, () => ty.accept(this, label)) as Field
592
+ const extract = (): FieldNode =>
593
+ this.withName(name, () => ty.accept(this, label)) as FieldNode
395
594
 
396
595
  // Helper to get inner default (evaluates lazily)
397
596
  const getInnerDefault = (): unknown => extract().defaultValue
@@ -399,7 +598,10 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
399
598
  return {
400
599
  type: "recursive",
401
600
  label,
601
+ displayLabel: formatLabel(label),
402
602
  name,
603
+ component: "recursive-lazy",
604
+ renderHint: COMPOUND_RENDER_HINT,
403
605
  typeName,
404
606
  extract,
405
607
  defaultValue: undefined,
@@ -432,42 +634,167 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
432
634
  }
433
635
  )
434
636
 
637
+ const inputProps: PrimitiveInputProps = {
638
+ type: "text",
639
+ placeholder: "aaaaa-aa or full principal ID",
640
+ minLength: 7,
641
+ maxLength: 64,
642
+ spellCheck: false,
643
+ autoComplete: "off",
644
+ }
645
+
435
646
  return {
436
647
  type: "principal",
437
648
  label,
649
+ displayLabel: formatLabel(label),
438
650
  name: this.currentName(),
651
+ component: "principal-input",
652
+ renderHint: TEXT_RENDER_HINT,
439
653
  defaultValue: "",
440
654
  maxLength: 64,
441
655
  minLength: 7,
656
+ format: checkTextFormat(label) as TextFormat,
442
657
  schema,
658
+ inputProps,
443
659
  candidType: "principal",
444
- ui: {
445
- placeholder: "aaaaa-aa or full principal ID",
446
- },
447
660
  }
448
661
  }
449
662
 
450
663
  public visitText(_t: IDL.TextClass, label: string): TextField {
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)
671
+
451
672
  return {
452
673
  type: "text",
453
674
  label,
675
+ displayLabel: formatLabel(label),
454
676
  name: this.currentName(),
677
+ component: "text-input",
678
+ renderHint: TEXT_RENDER_HINT,
455
679
  defaultValue: "",
456
- schema: z.string().min(1, "Required"),
680
+ format,
681
+ schema,
682
+ inputProps,
457
683
  candidType: "text",
458
- ui: {
459
- placeholder: "Enter text...",
460
- },
684
+ }
685
+ }
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")
461
780
  }
462
781
  }
463
782
 
464
783
  public visitBool(_t: IDL.BoolClass, label: string): BooleanField {
784
+ const inputProps: PrimitiveInputProps = {
785
+ type: "checkbox",
786
+ }
787
+
465
788
  return {
466
789
  type: "boolean",
467
790
  label,
791
+ displayLabel: formatLabel(label),
468
792
  name: this.currentName(),
793
+ component: "boolean-checkbox",
794
+ renderHint: CHECKBOX_RENDER_HINT,
469
795
  defaultValue: false,
470
796
  schema: z.boolean(),
797
+ inputProps,
471
798
  candidType: "bool",
472
799
  }
473
800
  }
@@ -476,7 +803,13 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
476
803
  return {
477
804
  type: "null",
478
805
  label,
806
+ displayLabel: formatLabel(label),
479
807
  name: this.currentName(),
808
+ component: "null-hidden",
809
+ renderHint: {
810
+ isCompound: false,
811
+ isPrimitive: true,
812
+ },
480
813
  defaultValue: null,
481
814
  schema: z.null(),
482
815
  candidType: "null",
@@ -498,6 +831,8 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
498
831
  max?: string
499
832
  }
500
833
  ): NumberField | TextField {
834
+ const format = checkNumberFormat(label) as NumberFormat
835
+
501
836
  let schema = z.string().min(1, "Required")
502
837
 
503
838
  if (options.isFloat) {
@@ -514,30 +849,57 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
514
849
  const type = isBigInt ? "text" : "number"
515
850
 
516
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
+
857
+ const inputProps: PrimitiveInputProps = {
858
+ type: "text",
859
+ placeholder: options.unsigned ? "e.g. 100000" : "e.g. -100000",
860
+ inputMode: "numeric",
861
+ pattern: options.unsigned ? "\\d+" : "-?\\d+",
862
+ spellCheck: false,
863
+ autoComplete: "off",
864
+ }
865
+
517
866
  return {
518
867
  type: "text",
519
868
  label,
869
+ displayLabel: formatLabel(label),
520
870
  name: this.currentName(),
871
+ component: "text-input",
872
+ renderHint: TEXT_RENDER_HINT,
521
873
  defaultValue: "",
874
+ format: textFormat,
522
875
  candidType,
523
876
  schema,
524
- ui: {
525
- placeholder: options.unsigned ? "e.g. 100000" : "e.g. -100000",
526
- },
877
+ inputProps,
527
878
  }
528
879
  }
529
880
 
881
+ const inputProps: PrimitiveInputProps = {
882
+ type: "number",
883
+ placeholder: options.isFloat ? "0.0" : "0",
884
+ inputMode: options.isFloat ? "decimal" : "numeric",
885
+ min: options.min,
886
+ max: options.max,
887
+ step: options.isFloat ? "any" : "1",
888
+ }
889
+
530
890
  return {
531
891
  type: "number",
532
892
  label,
893
+ displayLabel: formatLabel(label),
533
894
  name: this.currentName(),
895
+ component: "number-input",
896
+ renderHint: NUMBER_RENDER_HINT,
534
897
  defaultValue: "",
535
898
  candidType,
536
- schema: schema,
899
+ format,
900
+ schema,
901
+ inputProps,
537
902
  ...options,
538
- ui: {
539
- placeholder: options.isFloat ? "0.0" : "0",
540
- },
541
903
  }
542
904
  }
543
905
 
@@ -602,8 +964,15 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
602
964
  return {
603
965
  type: "unknown",
604
966
  label,
967
+ displayLabel: formatLabel(label),
605
968
  name: this.currentName(),
969
+ component: "unknown-fallback",
970
+ renderHint: {
971
+ isCompound: false,
972
+ isPrimitive: false,
973
+ },
606
974
  defaultValue: undefined,
975
+ candidType: "unknown",
607
976
  schema: z.any(),
608
977
  }
609
978
  }