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