@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,246 +1,311 @@
1
1
  import type { BaseActor, FunctionName, FunctionType } from "@ic-reactor/core"
2
2
  import * as z from "zod"
3
+ import type { VisitorDataType, TextFormat, NumberFormat } from "../types"
4
+
5
+ export type { TextFormat, NumberFormat }
3
6
 
4
7
  // ════════════════════════════════════════════════════════════════════════════
5
8
  // Field Type Union
6
9
  // ════════════════════════════════════════════════════════════════════════════
7
10
 
8
- export type ArgumentFieldType =
9
- | "record"
10
- | "variant"
11
- | "tuple"
12
- | "optional"
13
- | "vector"
14
- | "blob"
15
- | "recursive"
16
- | "principal"
17
- | "number"
11
+ export type ArgumentFieldType = VisitorDataType
12
+
13
+ // ════════════════════════════════════════════════════════════════════════════
14
+ // Component Type Hints
15
+ // ════════════════════════════════════════════════════════════════════════════
16
+
17
+ /**
18
+ * Suggested component type for rendering the field.
19
+ * This eliminates the need for switch statements in the frontend.
20
+ */
21
+ export type FieldComponentType =
22
+ | "record-container"
23
+ | "tuple-container"
24
+ | "variant-select"
25
+ | "optional-toggle"
26
+ | "vector-list"
27
+ | "blob-upload"
28
+ | "principal-input"
29
+ | "text-input"
30
+ | "number-input"
31
+ | "boolean-checkbox"
32
+ | "null-hidden"
33
+ | "recursive-lazy"
34
+ | "unknown-fallback"
35
+
36
+ // ════════════════════════════════════════════════════════════════════════════
37
+ // Render Hints for UI Rendering Strategy
38
+ // ════════════════════════════════════════════════════════════════════════════
39
+
40
+ /**
41
+ * Input type hints for HTML input elements.
42
+ * Used by primitive fields to suggest the appropriate input type.
43
+ */
44
+ export type InputType =
18
45
  | "text"
19
- | "boolean"
20
- | "null"
21
- | "unknown"
46
+ | "number"
47
+ | "checkbox"
48
+ | "select"
49
+ | "file"
50
+ | "textarea"
51
+
52
+ /**
53
+ * Rendering hints for the UI.
54
+ * Eliminates the need for frontend to maintain COMPLEX_TYPES arrays.
55
+ */
56
+ export interface RenderHint {
57
+ /** Whether this field has its own container/card styling (compound types) */
58
+ isCompound: boolean
59
+ /** Whether this is a leaf input (primitive types) */
60
+ isPrimitive: boolean
61
+ /** Suggested input type for HTML input elements */
62
+ inputType?: InputType
63
+ /** Description or help text for the field (derived from Candid) */
64
+ description?: string
65
+ }
22
66
 
23
67
  // ════════════════════════════════════════════════════════════════════════════
24
- // UI Hints for Form Rendering
68
+ // Primitive Input Props
25
69
  // ════════════════════════════════════════════════════════════════════════════
26
70
 
27
- export interface FieldUIHints {
28
- /** Placeholder text for the input */
71
+ /**
72
+ * Pre-computed HTML input props for primitive fields.
73
+ * Can be spread directly onto an input element.
74
+ */
75
+ export interface PrimitiveInputProps {
76
+ /** HTML input type - includes format-specific types */
77
+ type?: "text" | "number" | "checkbox" | "email" | "url" | "tel"
78
+ /** Placeholder text */
29
79
  placeholder?: string
30
- /** Description or help text for the field */
31
- description?: string
32
- /** Whether the field is required */
33
- required?: boolean
34
- /** Whether the field should be disabled */
35
- disabled?: boolean
36
- /** Additional CSS class names */
37
- className?: string
80
+ /** Minimum value for number inputs */
81
+ min?: string | number
82
+ /** Maximum value for number inputs */
83
+ max?: string | number
84
+ /** Step value for number inputs */
85
+ step?: string | number
86
+ /** Pattern for text inputs (e.g., phone numbers) */
87
+ pattern?: string
88
+ /** Input mode for virtual keyboards */
89
+ inputMode?: "text" | "numeric" | "decimal" | "email" | "tel" | "url"
90
+ /** Autocomplete hint */
91
+ autoComplete?: string
92
+ /** Whether to check spelling */
93
+ spellCheck?: boolean
94
+ /** Minimum length for text inputs */
95
+ minLength?: number
96
+ /** Maximum length for text inputs */
97
+ maxLength?: number
38
98
  }
39
99
 
40
100
  // ════════════════════════════════════════════════════════════════════════════
41
101
  // Base Field Interface
42
102
  // ════════════════════════════════════════════════════════════════════════════
43
103
 
44
- export interface FieldBase<TValue = unknown> {
104
+ interface FieldBase<T extends VisitorDataType = VisitorDataType> {
45
105
  /** The field type */
46
- type: ArgumentFieldType
47
- /** Human-readable label from Candid */
106
+ type: T
107
+ /** Raw label from Candid: "__arg0", "_0_" */
48
108
  label: string
49
- /**
50
- * Form field name path for binding.
51
- * Uses bracket notation for array indices: `[0]`, `args[0].owner`, `tags[1]`
52
- * Compatible with TanStack Form's `form.Field` name prop.
53
- *
54
- * @example
55
- * ```tsx
56
- * <form.Field name={field.name}>
57
- * {(fieldApi) => <input {...} />}
58
- * </form.Field>
59
- * ```
60
- */
109
+ /** Pre-formatted display label for UI rendering */
110
+ displayLabel: string
111
+ /** Form field name path for binding */
61
112
  name: string
113
+ /** Suggested component type for rendering this field */
114
+ component: FieldComponentType
115
+ /** Rendering hints for UI strategy */
116
+ renderHint: RenderHint
62
117
  /** Zod schema for field validation */
63
118
  schema: z.ZodTypeAny
64
- /** Default value for the field */
65
- defaultValue: TValue
66
119
  /** Original Candid type name for reference */
67
- candidType?: string
68
- /** UI rendering hints */
69
- ui?: FieldUIHints
70
- }
71
-
72
- // ════════════════════════════════════════════════════════════════════════════
73
- // Compound Types
74
- // ════════════════════════════════════════════════════════════════════════════
75
-
76
- export interface RecordField extends FieldBase<Record<string, unknown>> {
77
- type: "record"
78
- /** Child fields in the record */
79
- fields: Field[]
80
- /** Map of field label to its metadata for quick lookup */
81
- fieldMap: Map<string, Field>
82
- }
83
-
84
- export interface VariantField extends FieldBase<Record<string, unknown>> {
85
- type: "variant"
86
- /** All variant option fields */
87
- fields: Field[]
88
- /** List of variant option names */
89
- options: string[]
90
- /** Default selected option */
91
- defaultOption: string
92
- /** Map of option name to its field metadata */
93
- optionMap: Map<string, Field>
94
- /**
95
- * Get default value for a specific option.
96
- * Useful when switching between variant options.
97
- *
98
- * @example
99
- * ```tsx
100
- * const handleOptionChange = (newOption: string) => {
101
- * const newDefault = field.getOptionDefault(newOption)
102
- * fieldApi.handleChange(newDefault)
103
- * }
104
- * ```
105
- */
106
- getOptionDefault: (option: string) => Record<string, unknown>
107
- }
108
-
109
- export interface TupleField extends FieldBase<unknown[]> {
110
- type: "tuple"
111
- /** Tuple element fields in order */
112
- fields: Field[]
113
- }
114
-
115
- export interface OptionalField extends FieldBase<null> {
116
- type: "optional"
117
- /** The inner field when value is present */
118
- innerField: Field
119
- /**
120
- * Get default value when enabling the optional.
121
- * Returns the inner field's default value.
122
- *
123
- * @example
124
- * ```tsx
125
- * const handleToggle = (enabled: boolean) => {
126
- * if (enabled) {
127
- * fieldApi.handleChange(field.getInnerDefault())
128
- * } else {
129
- * fieldApi.handleChange(null)
130
- * }
131
- * }
132
- * ```
133
- */
134
- getInnerDefault: () => unknown
135
- }
136
-
137
- export interface VectorField extends FieldBase<unknown[]> {
138
- type: "vector"
139
- /** Template field for vector items */
140
- itemField: Field
141
- /**
142
- * Get a new item with default values.
143
- * Used when adding items to the vector.
144
- *
145
- * @example
146
- * ```tsx
147
- * <button onClick={() => fieldApi.pushValue(field.getItemDefault())}>
148
- * Add Item
149
- * </button>
150
- * ```
151
- */
152
- getItemDefault: () => unknown
153
- }
154
-
155
- export interface BlobField extends FieldBase<string> {
156
- type: "blob"
157
- /** Item field for individual bytes (nat8) */
158
- itemField: Field
159
- /** Accepted input formats */
160
- acceptedFormats: ("hex" | "base64" | "file")[]
161
- }
162
-
163
- export interface RecursiveField extends FieldBase<undefined> {
164
- type: "recursive"
165
- /** Type name for the recursive type */
166
- typeName: string
167
- /** Lazily extract the inner field to prevent infinite loops */
168
- extract: () => Field
169
- /**
170
- * Get default value for the recursive type.
171
- * Evaluates the inner type on demand.
172
- */
173
- getInnerDefault: () => unknown
120
+ candidType: string
174
121
  }
175
122
 
176
123
  // ════════════════════════════════════════════════════════════════════════════
177
- // Primitive Types
124
+ // Type-Specific Extras
178
125
  // ════════════════════════════════════════════════════════════════════════════
179
126
 
180
- export interface PrincipalField extends FieldBase<string> {
181
- type: "principal"
182
- maxLength: number
183
- minLength: number
184
- }
185
-
186
- export interface NumberField extends FieldBase<string> {
187
- type: "number"
188
- /**
189
- * Original Candid type: nat, int, nat8, nat16, nat32, nat64, int8, int16, int32, int64, float32, float64
190
- */
191
- candidType: string
192
- /** Whether this is an unsigned type */
193
- unsigned: boolean
194
- /** Whether this is a floating point type */
195
- isFloat: boolean
196
- /** Bit width if applicable (8, 16, 32, 64, or undefined for unbounded) */
197
- bits?: number
198
- /** Minimum value constraint (for bounded types) */
199
- min?: string
200
- /** Maximum value constraint (for bounded types) */
201
- max?: string
202
- }
203
-
204
- export interface TextField extends FieldBase<string> {
205
- type: "text"
206
- /** Minimum length constraint */
207
- minLength?: number
208
- /** Maximum length constraint */
209
- maxLength?: number
210
- /** Whether to render as multiline textarea */
211
- multiline?: boolean
212
- }
213
-
214
- export interface BooleanField extends FieldBase<boolean> {
215
- type: "boolean"
216
- }
217
-
218
- export interface NullField extends FieldBase<null> {
219
- type: "null"
127
+ /**
128
+ * Blob field size limits.
129
+ */
130
+ export interface BlobLimits {
131
+ /** Maximum bytes when entering as hex (e.g., 512 bytes) */
132
+ maxHexBytes: number
133
+ /** Maximum file size in bytes (e.g., 2MB ICP limit) */
134
+ maxFileBytes: number
135
+ /** Maximum hex display length before truncation */
136
+ maxHexDisplayLength: number
220
137
  }
221
138
 
222
- export interface UnknownField extends FieldBase<undefined> {
223
- type: "unknown"
139
+ /**
140
+ * Validation result for blob input.
141
+ */
142
+ export interface BlobValidationResult {
143
+ /** Whether the input is valid */
144
+ valid: boolean
145
+ /** Error message if invalid */
146
+ error?: string
224
147
  }
225
148
 
226
- // ════════════════════════════════════════════════════════════════════════════
227
- // Union Type
228
- // ════════════════════════════════════════════════════════════════════════════
149
+ type FieldExtras<T extends VisitorDataType> = T extends "record"
150
+ ? {
151
+ /** Child fields in the record */
152
+ fields: FieldNode[]
153
+ /** Map of field label to its metadata for quick lookup */
154
+ fieldMap: Map<string, FieldNode>
155
+ defaultValue: Record<string, unknown>
156
+ }
157
+ : T extends "variant"
158
+ ? {
159
+ /** All variant option fields */
160
+ fields: FieldNode[]
161
+ /** List of variant option names */
162
+ options: string[]
163
+ /** Default selected option */
164
+ defaultOption: string
165
+ /** Map of option name to its field metadata */
166
+ optionMap: Map<string, FieldNode>
167
+ defaultValue: Record<string, unknown>
168
+ /** Get default value for a specific option */
169
+ getOptionDefault: (option: string) => Record<string, unknown>
170
+ /** Get the field for a specific option */
171
+ getField: (option: string) => FieldNode
172
+ /** Get the currently selected option from a value */
173
+ getSelectedOption: (value: Record<string, unknown>) => string
174
+ /** Get the selected field from a value */
175
+ getSelectedField: (value: Record<string, unknown>) => FieldNode
176
+ }
177
+ : T extends "tuple"
178
+ ? {
179
+ /** Tuple element fields in order */
180
+ fields: FieldNode[]
181
+ defaultValue: unknown[]
182
+ }
183
+ : T extends "optional"
184
+ ? {
185
+ /** The inner field when value is present */
186
+ innerField: FieldNode
187
+ defaultValue: null
188
+ /** Get default value when enabling the optional */
189
+ getInnerDefault: () => unknown
190
+ /** Check if a value represents an enabled optional */
191
+ isEnabled: (value: unknown) => boolean
192
+ }
193
+ : T extends "vector"
194
+ ? {
195
+ /** Template field for vector items */
196
+ itemField: FieldNode
197
+ defaultValue: unknown[]
198
+ /** Get a new item with default values */
199
+ getItemDefault: () => unknown
200
+ /** Create a properly configured item field for a specific index */
201
+ createItemField: (
202
+ index: number,
203
+ overrides?: { label?: string }
204
+ ) => FieldNode
205
+ }
206
+ : T extends "blob"
207
+ ? {
208
+ /** Item field for individual bytes (nat8) */
209
+ itemField: FieldNode
210
+ /** Accepted input formats */
211
+ acceptedFormats: ("hex" | "base64" | "file")[]
212
+ /** Size limits for blob input */
213
+ limits: BlobLimits
214
+ /** Normalize hex input */
215
+ normalizeHex: (input: string) => string
216
+ /** Validate blob input value */
217
+ validateInput: (
218
+ value: string | Uint8Array
219
+ ) => BlobValidationResult
220
+ defaultValue: string
221
+ }
222
+ : T extends "recursive"
223
+ ? {
224
+ /** Type name for the recursive type */
225
+ typeName: string
226
+ /** Lazily extract the inner field to prevent infinite loops */
227
+ extract: () => FieldNode
228
+ /** Get default value for the recursive type (evaluates lazily) */
229
+ getInnerDefault: () => unknown
230
+ defaultValue: undefined
231
+ }
232
+ : T extends "principal"
233
+ ? {
234
+ maxLength: number
235
+ minLength: number
236
+ /** Detected format based on label heuristics */
237
+ format: TextFormat
238
+ /** Pre-computed HTML input props */
239
+ inputProps: PrimitiveInputProps
240
+ defaultValue: string
241
+ }
242
+ : T extends "number"
243
+ ? {
244
+ /** Whether this is an unsigned type */
245
+ unsigned: boolean
246
+ /** Whether this is a floating point type */
247
+ isFloat: boolean
248
+ /** Bit width if applicable (8, 16, 32, 64, or undefined for unbounded) */
249
+ bits?: number
250
+ /** Minimum value constraint (for bounded types) */
251
+ min?: string
252
+ /** Maximum value constraint (for bounded types) */
253
+ max?: string
254
+ /** Detected format based on label heuristics */
255
+ format: NumberFormat
256
+ /** Pre-computed HTML input props */
257
+ inputProps: PrimitiveInputProps
258
+ defaultValue: string
259
+ }
260
+ : T extends "text"
261
+ ? {
262
+ /** Minimum length constraint */
263
+ minLength?: number
264
+ /** Maximum length constraint */
265
+ maxLength?: number
266
+ /** Whether to render as multiline textarea */
267
+ multiline?: boolean
268
+ /** Detected format based on label heuristics */
269
+ format: TextFormat
270
+ /** Pre-computed HTML input props */
271
+ inputProps: PrimitiveInputProps
272
+ defaultValue: string
273
+ }
274
+ : T extends "boolean"
275
+ ? {
276
+ /** Pre-computed HTML input props */
277
+ inputProps: PrimitiveInputProps
278
+ defaultValue: boolean
279
+ }
280
+ : T extends "null"
281
+ ? {
282
+ defaultValue: null
283
+ }
284
+ : T extends "unknown"
285
+ ? {
286
+ defaultValue: undefined
287
+ }
288
+ : {}
229
289
 
230
- export type Field =
231
- | RecordField
232
- | VariantField
233
- | TupleField
234
- | OptionalField
235
- | VectorField
236
- | BlobField
237
- | RecursiveField
238
- | PrincipalField
239
- | NumberField
240
- | TextField
241
- | BooleanField
242
- | NullField
243
- | UnknownField
290
+ /**
291
+ * A unified field node that contains all metadata needed for rendering.
292
+ */
293
+ export type FieldNode<T extends VisitorDataType = VisitorDataType> =
294
+ FieldBase<T> & FieldExtras<T>
295
+
296
+ export type RecordField = FieldNode<"record">
297
+ export type VariantField = FieldNode<"variant">
298
+ export type TupleField = FieldNode<"tuple">
299
+ export type OptionalField = FieldNode<"optional">
300
+ export type VectorField = FieldNode<"vector">
301
+ export type BlobField = FieldNode<"blob">
302
+ export type RecursiveField = FieldNode<"recursive">
303
+ export type PrincipalField = FieldNode<"principal">
304
+ export type NumberField = FieldNode<"number">
305
+ export type TextField = FieldNode<"text">
306
+ export type BooleanField = FieldNode<"boolean">
307
+ export type NullField = FieldNode<"null">
308
+ export type UnknownField = FieldNode<"unknown">
244
309
 
245
310
  // ════════════════════════════════════════════════════════════════════════════
246
311
  // Form Metadata - TanStack Form Integration
@@ -249,31 +314,6 @@ export type Field =
249
314
  /**
250
315
  * Form metadata for a Candid method.
251
316
  * Contains all information needed to create a TanStack Form instance.
252
- *
253
- * @example
254
- * ```tsx
255
- * import { useForm } from '@tanstack/react-form'
256
- *
257
- * function MethodForm({ meta }: { meta: FormMeta }) {
258
- * const form = useForm({
259
- * ...meta.formOptions,
260
- * onSubmit: async ({ value }) => {
261
- * await actor[meta.functionName](...value)
262
- * }
263
- * })
264
- *
265
- * return (
266
- * <form onSubmit={(e) => { e.preventDefault(); form.handleSubmit() }}>
267
- * {meta.fields.map(field => (
268
- * <form.Field key={field.name} name={field.name}>
269
- * {(fieldApi) => <DynamicInput field={field} fieldApi={fieldApi} />}
270
- * </form.Field>
271
- * ))}
272
- * <button type="submit">Submit</button>
273
- * </form>
274
- * )
275
- * }
276
- * ```
277
317
  */
278
318
  export interface ArgumentsMeta<
279
319
  A = BaseActor,
@@ -284,7 +324,7 @@ export interface ArgumentsMeta<
284
324
  /** The function name */
285
325
  functionName: Name
286
326
  /** Argument field definitions for rendering */
287
- fields: Field[]
327
+ fields: FieldNode[]
288
328
  /** Default values for all arguments (as a tuple) */
289
329
  defaultValues: unknown[]
290
330
  /** Combined Zod schema for all arguments */
@@ -323,10 +363,19 @@ export type ArgumentsServiceMeta<A = BaseActor> = {
323
363
 
324
364
  /** Extract a specific field type */
325
365
  export type FieldByType<T extends ArgumentFieldType> = Extract<
326
- Field,
366
+ FieldNode,
327
367
  { type: T }
328
368
  >
329
369
 
370
+ /**
371
+ * Props type helper for field components.
372
+ * Use this to type your field components for better DX.
373
+ */
374
+ export type FieldProps<T extends ArgumentFieldType> = {
375
+ field: FieldByType<T>
376
+ renderField?: (child: FieldNode) => React.ReactNode
377
+ }
378
+
330
379
  /** Compound field types that contain other fields */
331
380
  export type CompoundField =
332
381
  | RecordField
@@ -343,52 +392,3 @@ export type PrimitiveField =
343
392
  | TextField
344
393
  | BooleanField
345
394
  | NullField
346
-
347
- /**
348
- * Type guard for checking specific field types.
349
- *
350
- * @example
351
- * ```tsx
352
- * function FieldInput({ field }: { field: Field }) {
353
- * if (isFieldType(field, 'record')) {
354
- * // field is now typed as RecordField
355
- * return <RecordInput field={field} />
356
- * }
357
- * if (isFieldType(field, 'text')) {
358
- * // field is now typed as TextField
359
- * return <TextInput field={field} />
360
- * }
361
- * // ...
362
- * }
363
- * ```
364
- */
365
- export function isFieldType<T extends ArgumentFieldType>(
366
- field: Field,
367
- type: T
368
- ): field is FieldByType<T> {
369
- return field.type === type
370
- }
371
-
372
- /** Check if a field is a compound type (contains other fields) */
373
- export function isCompoundField(field: Field): field is CompoundField {
374
- return [
375
- "record",
376
- "variant",
377
- "tuple",
378
- "optional",
379
- "vector",
380
- "recursive",
381
- ].includes(field.type)
382
- }
383
-
384
- /** Check if a field is a primitive type */
385
- export function isPrimitiveField(field: Field): field is PrimitiveField {
386
- return ["principal", "number", "text", "boolean", "null"].includes(field.type)
387
- }
388
-
389
- /** Check if a field has children (for iteration) */
390
- export function hasChildFields(
391
- field: Field
392
- ): field is RecordField | VariantField | TupleField {
393
- return "fields" in field && Array.isArray((field as RecordField).fields)
394
- }
@@ -4,25 +4,15 @@ import type {
4
4
  FunctionType,
5
5
  ActorMethodReturnType,
6
6
  } from "@ic-reactor/core"
7
+ import type { VisitorDataType, TextFormat, NumberFormat } from "../types"
8
+
9
+ export type { TextFormat, NumberFormat }
7
10
 
8
11
  // ════════════════════════════════════════════════════════════════════════════
9
12
  // Core Types & Formats
10
13
  // ════════════════════════════════════════════════════════════════════════════
11
14
 
12
- export type NodeType =
13
- | "record"
14
- | "variant"
15
- | "tuple"
16
- | "optional"
17
- | "vector"
18
- | "blob"
19
- | "recursive"
20
- | "principal"
21
- | "number"
22
- | "text"
23
- | "boolean"
24
- | "null"
25
- | "unknown"
15
+ export type NodeType = VisitorDataType
26
16
 
27
17
  export type DisplayType =
28
18
  | "string"
@@ -38,19 +28,6 @@ export type DisplayType =
38
28
  | "blob"
39
29
  | "unknown"
40
30
 
41
- export type NumberFormat = "timestamp" | "cycle" | "value" | "token" | "normal"
42
- export type TextFormat =
43
- | "plain"
44
- | "timestamp"
45
- | "uuid"
46
- | "url"
47
- | "email"
48
- | "phone"
49
- | "btc"
50
- | "eth"
51
- | "account-id"
52
- | "principal"
53
-
54
31
  // ════════════════════════════════════════════════════════════════════════════
55
32
  // Unified Result Node - Single Structure for Schema & Resolved Data
56
33
  // ════════════════════════════════════════════════════════════════════════════