@k3-tech/react-kit 0.0.59 → 0.0.61

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 (62) hide show
  1. package/dist/index.js +978 -851
  2. package/dist/kit/builder/data-table/components/DataTable.d.ts +7 -1
  3. package/dist/kit/builder/data-table/components/DataTable.d.ts.map +1 -1
  4. package/dist/kit/builder/form/components/FormBuilderField.d.ts.map +1 -1
  5. package/dist/kit/builder/form/components/fields/ArrayField.d.ts.map +1 -1
  6. package/dist/kit/builder/form/components/fields/CheckboxField.d.ts.map +1 -1
  7. package/dist/kit/builder/form/components/fields/DateField.d.ts.map +1 -1
  8. package/dist/kit/builder/form/components/fields/DatePickerField.d.ts.map +1 -1
  9. package/dist/kit/builder/form/components/fields/DateRangePickerField.d.ts.map +1 -1
  10. package/dist/kit/builder/form/components/fields/DateTimePickerField.d.ts.map +1 -1
  11. package/dist/kit/builder/form/components/fields/DateTimeRangePickerField.d.ts.map +1 -1
  12. package/dist/kit/builder/form/components/fields/FileField.d.ts.map +1 -1
  13. package/dist/kit/builder/form/components/fields/MonthPickerField.d.ts.map +1 -1
  14. package/dist/kit/builder/form/components/fields/MonthRangePickerField.d.ts.map +1 -1
  15. package/dist/kit/builder/form/components/fields/NumberField.d.ts.map +1 -1
  16. package/dist/kit/builder/form/components/fields/RadioField.d.ts.map +1 -1
  17. package/dist/kit/builder/form/components/fields/SelectField.d.ts.map +1 -1
  18. package/dist/kit/builder/form/components/fields/SwitchField.d.ts.map +1 -1
  19. package/dist/kit/builder/form/components/fields/TextField.d.ts.map +1 -1
  20. package/dist/kit/builder/form/components/fields/TextareaField.d.ts.map +1 -1
  21. package/dist/kit/builder/form/components/fields/TimePickerField.d.ts.map +1 -1
  22. package/dist/kit/builder/form/components/fields/TimeRangePickerField.d.ts.map +1 -1
  23. package/dist/kit/builder/form/types.d.ts +6 -1
  24. package/dist/kit/builder/form/types.d.ts.map +1 -1
  25. package/dist/kit/components/autocomplete/Autocomplete.d.ts +2 -1
  26. package/dist/kit/components/autocomplete/Autocomplete.d.ts.map +1 -1
  27. package/dist/kit/components/fileuploader/FileUploader.d.ts +1 -1
  28. package/dist/kit/components/fileuploader/FileUploader.d.ts.map +1 -1
  29. package/dist/kit/components/fileuploader/types.d.ts +1 -0
  30. package/dist/kit/components/fileuploader/types.d.ts.map +1 -1
  31. package/dist/kit/components/info-tooltip/InfoTooltip.d.ts +6 -0
  32. package/dist/kit/components/info-tooltip/InfoTooltip.d.ts.map +1 -0
  33. package/dist/kit/themes/clean-slate.css +6 -2
  34. package/dist/kit/themes/default.css +6 -2
  35. package/dist/kit/themes/minimal-modern.css +6 -2
  36. package/dist/kit/themes/spotify.css +6 -2
  37. package/package.json +1 -1
  38. package/src/kit/builder/data-table/components/DataTable.tsx +110 -97
  39. package/src/kit/builder/form/components/FormBuilderField.tsx +17 -2
  40. package/src/kit/builder/form/components/fields/ArrayField.tsx +1 -0
  41. package/src/kit/builder/form/components/fields/CheckboxField.tsx +21 -8
  42. package/src/kit/builder/form/components/fields/DateField.tsx +1 -0
  43. package/src/kit/builder/form/components/fields/DatePickerField.tsx +1 -0
  44. package/src/kit/builder/form/components/fields/DateRangePickerField.tsx +3 -2
  45. package/src/kit/builder/form/components/fields/DateTimePickerField.tsx +1 -0
  46. package/src/kit/builder/form/components/fields/DateTimeRangePickerField.tsx +1 -0
  47. package/src/kit/builder/form/components/fields/FileField.tsx +1 -0
  48. package/src/kit/builder/form/components/fields/MonthPickerField.tsx +1 -0
  49. package/src/kit/builder/form/components/fields/MonthRangePickerField.tsx +1 -0
  50. package/src/kit/builder/form/components/fields/NumberField.tsx +70 -2
  51. package/src/kit/builder/form/components/fields/RadioField.tsx +1 -0
  52. package/src/kit/builder/form/components/fields/SelectField.tsx +1 -0
  53. package/src/kit/builder/form/components/fields/SwitchField.tsx +11 -0
  54. package/src/kit/builder/form/components/fields/TextField.tsx +1 -0
  55. package/src/kit/builder/form/components/fields/TextareaField.tsx +2 -1
  56. package/src/kit/builder/form/components/fields/TimePickerField.tsx +1 -0
  57. package/src/kit/builder/form/components/fields/TimeRangePickerField.tsx +1 -0
  58. package/src/kit/builder/form/types.ts +6 -1
  59. package/src/kit/components/autocomplete/Autocomplete.tsx +6 -3
  60. package/src/kit/components/fileuploader/FileUploader.tsx +2 -0
  61. package/src/kit/components/fileuploader/types.ts +2 -0
  62. package/src/kit/components/info-tooltip/InfoTooltip.tsx +18 -0
@@ -82,6 +82,12 @@ export interface DataTableProps<TData, TValue> {
82
82
  filterWrapper?: 'accordion' | 'card' | 'none';
83
83
  filterTitle?: string;
84
84
  filterShowActionsSeparator?: boolean;
85
+ // Custom header component
86
+ /**
87
+ * Should only be used if you intend to replace the default filter.
88
+ * Will hide default filter and actions, and also disable some props.
89
+ */
90
+ CustomHeaderComponent?: () => React.ReactNode;
85
91
  }
86
92
 
87
93
  export function DataTable<TData, TValue>({
@@ -117,6 +123,7 @@ export function DataTable<TData, TValue>({
117
123
  filterWrapper,
118
124
  filterTitle,
119
125
  filterShowActionsSeparator,
126
+ CustomHeaderComponent,
120
127
  }: DataTableProps<TData, TValue>) {
121
128
  const [rowSelection, setRowSelection] = React.useState<
122
129
  Record<string, boolean>
@@ -359,105 +366,111 @@ export function DataTable<TData, TValue>({
359
366
 
360
367
  return (
361
368
  <div className={cn('space-y-3', className)}>
362
- {formFilters?.length ? (
363
- effectiveFilterWrapper === 'accordion' ? (
364
- <div className="rounded-md border border-border">
365
- <Accordion
366
- type="single"
367
- collapsible
368
- className="w-full"
369
- defaultValue="filters"
370
- >
371
- <AccordionItem value="filters">
372
- <AccordionTrigger className="px-4 py-3 border-b border-border text-md">
373
- {effectiveFilterTitle}
374
- </AccordionTrigger>
375
- <AccordionContent className="px-4 pb-4 pt-5">
376
- <FormBuilder
377
- key={JSON.stringify(formFilterValues ?? {})}
378
- sections={formFilters as FormBuilderSectionConfig[]}
379
- defaultValues={formFilterValues}
380
- onSubmit={(data) =>
381
- onFormFilterChange?.(data as Record<string, unknown>)
382
- }
383
- onReset={() => onFormFilterChange?.({})}
384
- showActions
385
- submitLabel="Apply"
386
- resetLabel="Clear"
387
- showActionsSeparator={filterShowActionsSeparator}
388
- />
389
- </AccordionContent>
390
- </AccordionItem>
391
- </Accordion>
392
- </div>
393
- ) : effectiveFilterWrapper === 'card' ? (
394
- <div className="rounded-md border border-border p-4">
395
- <FormBuilder
396
- key={JSON.stringify(formFilterValues ?? {})}
397
- sections={formFilters as FormBuilderSectionConfig[]}
398
- defaultValues={formFilterValues}
399
- onSubmit={(data) =>
400
- onFormFilterChange?.(data as Record<string, unknown>)
401
- }
402
- onReset={() => onFormFilterChange?.({})}
403
- showActions
404
- submitLabel="Apply"
405
- resetLabel="Clear"
406
- showActionsSeparator={filterShowActionsSeparator}
407
- />
408
- </div>
409
- ) : (
410
- <FormBuilder
411
- key={JSON.stringify(formFilterValues ?? {})}
412
- sections={formFilters as FormBuilderSectionConfig[]}
413
- defaultValues={formFilterValues}
414
- onSubmit={(data) =>
415
- onFormFilterChange?.(data as Record<string, unknown>)
416
- }
417
- onReset={() => onFormFilterChange?.({})}
418
- showActions
419
- submitLabel="Apply"
420
- resetLabel="Clear"
421
- showActionsSeparator={filterShowActionsSeparator}
422
- />
423
- )
424
- ) : null}
425
- {/* Actions Bar */}
426
- {(safeActions.length ||
427
- showStandardActions ||
428
- (selectable &&
429
- table.getSelectedRowModel().rows.length > 0 &&
430
- safeBatchActions.length)) && (
431
- <div className="rounded-md p-2 mt-6">
432
- <div className="flex items-center justify-between">
433
- <div className="flex items-center gap-2">
434
- {selectable &&
435
- table.getSelectedRowModel().rows.length > 0 &&
436
- safeBatchActions.map((a) => renderBatchButton(a, a.key))}
437
- </div>
438
- <div className="flex items-center gap-2">
439
- {showStandardActions && onRefresh && (
440
- <Button
441
- variant="outline"
442
- size="sm"
443
- onClick={handleRefresh}
444
- disabled={loading}
445
- className="h-8"
446
- title="Refresh"
369
+ {CustomHeaderComponent ? (
370
+ CustomHeaderComponent()
371
+ ) : (
372
+ <>
373
+ {formFilters?.length ? (
374
+ effectiveFilterWrapper === 'accordion' ? (
375
+ <div className="rounded-md border border-border">
376
+ <Accordion
377
+ type="single"
378
+ collapsible
379
+ className="w-full"
380
+ defaultValue="filters"
447
381
  >
448
- <RefreshCw
449
- className={cn('h-4 w-4', loading && 'animate-spin')}
450
- />{' '}
451
- Refresh
452
- </Button>
453
- )}
454
- {isColumnVisibilityEnabled ? (
455
- <DataTableViewOptions table={table} />
456
- ) : null}
457
- {safeActions.map((a) => renderActionButton(a, a.key))}
382
+ <AccordionItem value="filters">
383
+ <AccordionTrigger className="px-4 py-3 border-b border-border text-md">
384
+ {effectiveFilterTitle}
385
+ </AccordionTrigger>
386
+ <AccordionContent className="px-4 pb-4 pt-5">
387
+ <FormBuilder
388
+ key={JSON.stringify(formFilterValues ?? {})}
389
+ sections={formFilters as FormBuilderSectionConfig[]}
390
+ defaultValues={formFilterValues}
391
+ onSubmit={(data) =>
392
+ onFormFilterChange?.(data as Record<string, unknown>)
393
+ }
394
+ onReset={() => onFormFilterChange?.({})}
395
+ showActions
396
+ submitLabel="Apply"
397
+ resetLabel="Clear"
398
+ showActionsSeparator={filterShowActionsSeparator}
399
+ />
400
+ </AccordionContent>
401
+ </AccordionItem>
402
+ </Accordion>
403
+ </div>
404
+ ) : effectiveFilterWrapper === 'card' ? (
405
+ <div className="rounded-md border border-border p-4">
406
+ <FormBuilder
407
+ key={JSON.stringify(formFilterValues ?? {})}
408
+ sections={formFilters as FormBuilderSectionConfig[]}
409
+ defaultValues={formFilterValues}
410
+ onSubmit={(data) =>
411
+ onFormFilterChange?.(data as Record<string, unknown>)
412
+ }
413
+ onReset={() => onFormFilterChange?.({})}
414
+ showActions
415
+ submitLabel="Apply"
416
+ resetLabel="Clear"
417
+ showActionsSeparator={filterShowActionsSeparator}
418
+ />
419
+ </div>
420
+ ) : (
421
+ <FormBuilder
422
+ key={JSON.stringify(formFilterValues ?? {})}
423
+ sections={formFilters as FormBuilderSectionConfig[]}
424
+ defaultValues={formFilterValues}
425
+ onSubmit={(data) =>
426
+ onFormFilterChange?.(data as Record<string, unknown>)
427
+ }
428
+ onReset={() => onFormFilterChange?.({})}
429
+ showActions
430
+ submitLabel="Apply"
431
+ resetLabel="Clear"
432
+ showActionsSeparator={filterShowActionsSeparator}
433
+ />
434
+ )
435
+ ) : null}
436
+ {/* Actions Bar */}
437
+ {(safeActions.length ||
438
+ showStandardActions ||
439
+ (selectable &&
440
+ table.getSelectedRowModel().rows.length > 0 &&
441
+ safeBatchActions.length)) && (
442
+ <div className="rounded-md p-2 mt-6">
443
+ <div className="flex items-center justify-between">
444
+ <div className="flex items-center gap-2">
445
+ {selectable &&
446
+ table.getSelectedRowModel().rows.length > 0 &&
447
+ safeBatchActions.map((a) => renderBatchButton(a, a.key))}
448
+ </div>
449
+ <div className="flex items-center gap-2">
450
+ {showStandardActions && onRefresh && (
451
+ <Button
452
+ variant="outline"
453
+ size="sm"
454
+ onClick={handleRefresh}
455
+ disabled={loading}
456
+ className="h-8"
457
+ title="Refresh"
458
+ >
459
+ <RefreshCw
460
+ className={cn('h-4 w-4', loading && 'animate-spin')}
461
+ />{' '}
462
+ Refresh
463
+ </Button>
464
+ )}
465
+ {isColumnVisibilityEnabled ? (
466
+ <DataTableViewOptions table={table} />
467
+ ) : null}
468
+ {safeActions.map((a) => renderActionButton(a, a.key))}
469
+ </div>
470
+ </div>
458
471
  </div>
459
- </div>
460
- </div>
472
+ )}
473
+ </>
461
474
  )}
462
475
  <div
463
476
  className="relative overflow-hidden rounded-md border border-border"
@@ -26,6 +26,7 @@ import {
26
26
  ObjectField,
27
27
  ArrayField,
28
28
  } from './fields';
29
+ import InfoTooltip from '@/kit/components/info-tooltip/InfoTooltip';
29
30
 
30
31
  export interface FormBuilderFieldProps<
31
32
  TFieldValues extends FieldValues = FieldValues,
@@ -68,8 +69,10 @@ export function FormBuilderField<
68
69
  (value: unknown, ...extras: unknown[]) => {
69
70
  // Check for onChangeOverride, block any change to RHF
70
71
  // if onChangeOverride returns exactly false
71
- if (typeof (field.onChangeOverride) === "function"
72
- && field.onChangeOverride(value, ...extras) === false)
72
+ if (
73
+ typeof field.onChangeOverride === 'function' &&
74
+ field.onChangeOverride(value, ...extras) === false
75
+ )
73
76
  return;
74
77
  // Only patch the RHF value with the first argument (the canonical value)
75
78
  controllerField.onChange(value);
@@ -410,6 +413,12 @@ export function FormBuilderField<
410
413
  <Label htmlFor={fieldPath} className="text-sm font-medium">
411
414
  {field.label}
412
415
  {field.required && <span className="text-destructive ml-1">*</span>}
416
+ {field.infoTooltip && (
417
+ <InfoTooltip
418
+ iconSize={field.infoTooltipIconSize}
419
+ content={field.infoTooltip}
420
+ />
421
+ )}
413
422
  </Label>
414
423
  {renderField()}
415
424
  </div>
@@ -440,6 +449,12 @@ export function FormBuilderField<
440
449
  <Label htmlFor={fieldPath} className="text-sm font-medium">
441
450
  {field.label}
442
451
  {field.required && <span className="text-destructive ml-1">*</span>}
452
+ {field.infoTooltip && (
453
+ <InfoTooltip
454
+ iconSize={field.infoTooltipIconSize}
455
+ content={field.infoTooltip}
456
+ />
457
+ )}
443
458
  </Label>
444
459
  {renderField()}
445
460
  {field.description && (
@@ -299,6 +299,7 @@ export function ArrayField({
299
299
  }}
300
300
  placeholder={`Item ${index + 1}`}
301
301
  disabled={field.disabled}
302
+ data-testid={field.dataTestID}
302
303
  />
303
304
  )}
304
305
  </CardContent>
@@ -2,6 +2,7 @@ import { Checkbox } from '../../../../../shadcn/ui/checkbox';
2
2
  import { Label } from '../../../../../shadcn/ui/label';
3
3
  import { cn } from '../../../../../shadcn/lib/utils';
4
4
  import type { FieldRenderProps } from './types';
5
+ import InfoTooltip from '@/kit/components/info-tooltip/InfoTooltip';
5
6
 
6
7
  export function CheckboxField({
7
8
  field,
@@ -19,15 +20,25 @@ export function CheckboxField({
19
20
  <Label id={labelId} className="text-sm font-medium">
20
21
  {field.label}
21
22
  {field.required && <span className="text-destructive ml-1">*</span>}
23
+ {field.infoTooltip && (
24
+ <InfoTooltip
25
+ iconSize={field.infoTooltipIconSize}
26
+ content={field.infoTooltip}
27
+ />
28
+ )}
22
29
  </Label>
23
- <Checkbox
24
- aria-labelledby={labelId}
25
- id={fieldPath}
26
- checked={(value as boolean) || false}
27
- onCheckedChange={onChange as (val: boolean) => void}
28
- disabled={field.disabled || field.readOnly}
29
- className={cn(className)}
30
- />
30
+ <div className="flex items-center space-x-4">
31
+ <Checkbox
32
+ aria-labelledby={labelId}
33
+ id={fieldPath}
34
+ checked={(value as boolean) || false}
35
+ onCheckedChange={onChange as (val: boolean) => void}
36
+ disabled={field.disabled || field.readOnly}
37
+ className={cn(className)}
38
+ data-testid={field.dataTestID}
39
+ />
40
+ {field.subLabel}
41
+ </div>
31
42
  </div>
32
43
  );
33
44
  }
@@ -40,6 +51,7 @@ export function CheckboxField({
40
51
  onCheckedChange={onChange as (val: boolean) => void}
41
52
  disabled={field.disabled}
42
53
  className={cn(className)}
54
+ data-testid={field.dataTestID}
43
55
  />
44
56
  );
45
57
  }
@@ -52,6 +64,7 @@ export function CheckboxField({
52
64
  onCheckedChange={onChange as (val: boolean) => void}
53
65
  disabled={field.disabled}
54
66
  className={cn(className)}
67
+ data-testid={field.dataTestID}
55
68
  />
56
69
  <Label
57
70
  htmlFor={fieldPath}
@@ -49,6 +49,7 @@ export function DateField({
49
49
  onChange={(e) =>
50
50
  onChange(e.target.value ? new Date(e.target.value) : null)
51
51
  }
52
+ data-testid={field.dataTestID}
52
53
  />
53
54
  );
54
55
  }
@@ -42,6 +42,7 @@ export function DatePickerField({
42
42
  format={field.dateFormat}
43
43
  placeholder={field.placeholder}
44
44
  buttonVariant="outline"
45
+ data-testid={field.dataTestID}
45
46
  />
46
47
  );
47
48
  }
@@ -1,8 +1,8 @@
1
1
  import * as React from 'react';
2
- import type { FieldRenderProps } from './types';
3
- import { DateRangePicker } from '../../../../components/datepicker/DateRangePicker';
4
2
  import type { DateRange } from 'react-day-picker';
5
3
  import { useWatch } from 'react-hook-form';
4
+ import { DateRangePicker } from '../../../../components/datepicker/DateRangePicker';
5
+ import type { FieldRenderProps } from './types';
6
6
 
7
7
  function coerceDate(input: unknown): Date | undefined {
8
8
  if (!input) return undefined;
@@ -66,6 +66,7 @@ export function DateRangePickerField({
66
66
  showFooter={field.showFooter}
67
67
  cancelLabel={field.cancelLabel}
68
68
  applyLabel={field.applyLabel}
69
+ data-testid={field.dataTestID}
69
70
  />
70
71
  );
71
72
  }
@@ -47,6 +47,7 @@ export function DateTimePickerField({
47
47
  showFooter={field.showFooter}
48
48
  cancelLabel={field.cancelLabel}
49
49
  applyLabel={field.applyLabel}
50
+ data-testid={field.dataTestID}
50
51
  />
51
52
  );
52
53
  }
@@ -57,6 +57,7 @@ export function DateTimeRangePickerField({
57
57
  showFooter={field.showFooter}
58
58
  cancelLabel={field.cancelLabel}
59
59
  applyLabel={field.applyLabel}
60
+ data-testid={field.dataTestID}
60
61
  />
61
62
  );
62
63
  }
@@ -32,6 +32,7 @@ export function FileField({
32
32
  onRemove={field.fileOnRemove}
33
33
  onRetry={field.fileOnRetry}
34
34
  onRetryAll={field.fileOnRetryAll}
35
+ dataTestID={field.dataTestID}
35
36
  />
36
37
  );
37
38
  }
@@ -28,6 +28,7 @@ export function MonthPickerField({
28
28
  showFooter
29
29
  clearLabel={field.cancelLabel}
30
30
  closeLabel={field.applyLabel}
31
+ data-testid={field.dataTestID}
31
32
  />
32
33
  );
33
34
  }
@@ -36,6 +36,7 @@ export function MonthRangePickerField({
36
36
  showFooter
37
37
  cancelLabel={field.cancelLabel}
38
38
  applyLabel={field.applyLabel}
39
+ data-testid={field.dataTestID}
39
40
  />
40
41
  );
41
42
  }
@@ -1,3 +1,4 @@
1
+ import { useState } from 'react';
1
2
  import { Input } from '../../../../../shadcn/ui/input';
2
3
  import type { FieldRenderProps } from './types';
3
4
 
@@ -7,14 +8,81 @@ export function NumberField({
7
8
  onChange,
8
9
  className,
9
10
  }: FieldRenderProps) {
11
+ const innerValue = value as number;
12
+ const [displayValue, setDisplayValue] = useState(
13
+ (Number(innerValue) || "").toLocaleString('id-ID'),
14
+ );
15
+
16
+ if (field.numberMode === 'currency') {
17
+ const parseAndFormatIDRInput = (input: string) => {
18
+ const cleaned = input.replace(/[^\d.,]/g, '');
19
+ if (!cleaned) {
20
+ return { display: '', numeric: 0 };
21
+ }
22
+
23
+ // IDR uses "," as decimal separator and "." for thousands.
24
+ const hasDecimalSeparator = cleaned.includes(',');
25
+ const sepIndex = cleaned.lastIndexOf(',');
26
+ const rawInteger = hasDecimalSeparator
27
+ ? cleaned.slice(0, Math.max(0, sepIndex))
28
+ : cleaned;
29
+ const rawFraction = hasDecimalSeparator
30
+ ? cleaned.slice(sepIndex + 1)
31
+ : '';
32
+
33
+ const integerDigits = rawInteger.replace(/\D/g, '');
34
+ const fractionDigits = rawFraction.replace(/\D/g, '');
35
+ const safeInteger = integerDigits || '0';
36
+
37
+ const groupedInteger = Number(safeInteger).toLocaleString('id-ID');
38
+ const display = hasDecimalSeparator
39
+ ? `${groupedInteger},${fractionDigits}`
40
+ : groupedInteger;
41
+ const numeric = Number(
42
+ fractionDigits ? `${safeInteger}.${fractionDigits}` : safeInteger,
43
+ );
44
+
45
+ return {
46
+ display,
47
+ numeric: Number.isFinite(numeric) ? numeric : 0,
48
+ };
49
+ };
50
+
51
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
52
+ const { display, numeric } = parseAndFormatIDRInput(
53
+ e.currentTarget.value,
54
+ );
55
+ setDisplayValue(display);
56
+ onChange(numeric);
57
+ };
58
+
59
+ return (
60
+ <Input
61
+ type="text"
62
+ inputMode="decimal"
63
+ value={displayValue}
64
+ onChange={handleInputChange}
65
+ className="min-w-[120px] text-right bg-background text-foreground"
66
+ data-testid={field.dataTestID}
67
+ />
68
+ );
69
+ }
70
+
10
71
  return (
11
72
  <Input
73
+ data-testid={field.dataTestID}
12
74
  className={className}
13
75
  disabled={field.disabled || field.readOnly}
14
76
  placeholder={field.placeholder}
15
77
  type="number"
16
- value={(value as number | string) ?? ''}
17
- onChange={(e) => onChange(Number(e.target.value))}
78
+ value={innerValue ?? ''}
79
+ onChange={(e) =>
80
+ onChange(
81
+ typeof e.currentTarget.valueAsNumber === 'number'
82
+ ? e.currentTarget.valueAsNumber
83
+ : null,
84
+ )
85
+ }
18
86
  />
19
87
  );
20
88
  }
@@ -27,6 +27,7 @@ export function RadioField({
27
27
  onValueChange={(val) => onChange(fromUiValue(val))}
28
28
  disabled={field.disabled || field.readOnly}
29
29
  className={className}
30
+ data-testid={field.dataTestID}
30
31
  >
31
32
  {field.options?.map((option) => (
32
33
  <div
@@ -27,6 +27,7 @@ export function SelectField({
27
27
  value={toUiValue(value)}
28
28
  onValueChange={(val) => onChange(fromUiValue(val))}
29
29
  disabled={field.disabled || field.readOnly}
30
+ data-testid={field.dataTestID}
30
31
  >
31
32
  <SelectTrigger className={className}>
32
33
  <SelectValue placeholder={field.placeholder} />
@@ -2,6 +2,7 @@ import { Switch } from '../../../../../shadcn/ui/switch';
2
2
  import { Label } from '../../../../../shadcn/ui/label';
3
3
  import { cn } from '../../../../../shadcn/lib/utils';
4
4
  import type { FieldRenderProps } from './types';
5
+ import InfoTooltip from '@/kit/components/info-tooltip/InfoTooltip';
5
6
 
6
7
  export function SwitchField({
7
8
  field,
@@ -19,6 +20,12 @@ export function SwitchField({
19
20
  <Label id={labelId} className="text-sm font-medium">
20
21
  {field.label}
21
22
  {field.required && <span className="text-destructive ml-1">*</span>}
23
+ {field.infoTooltip && (
24
+ <InfoTooltip
25
+ iconSize={field.infoTooltipIconSize}
26
+ content={field.infoTooltip}
27
+ />
28
+ )}
22
29
  </Label>
23
30
  <Switch
24
31
  aria-labelledby={labelId}
@@ -27,7 +34,9 @@ export function SwitchField({
27
34
  onCheckedChange={onChange as (val: boolean) => void}
28
35
  disabled={field.disabled || field.readOnly}
29
36
  className={cn(className)}
37
+ data-testid={field.dataTestID}
30
38
  />
39
+ {field.subLabel}
31
40
  </div>
32
41
  );
33
42
  }
@@ -40,6 +49,7 @@ export function SwitchField({
40
49
  onCheckedChange={onChange as (val: boolean) => void}
41
50
  disabled={field.disabled}
42
51
  className={cn(className)}
52
+ data-testid={field.dataTestID}
43
53
  />
44
54
  );
45
55
  }
@@ -52,6 +62,7 @@ export function SwitchField({
52
62
  onCheckedChange={onChange as (val: boolean) => void}
53
63
  disabled={field.disabled}
54
64
  className={cn(className)}
65
+ data-testid={field.dataTestID}
55
66
  />
56
67
  <Label
57
68
  htmlFor={fieldPath}
@@ -20,6 +20,7 @@ export function TextField({
20
20
  type={type}
21
21
  value={(value as string) || ''}
22
22
  onChange={(e) => onChange(e.target.value)}
23
+ data-testid={field.dataTestID}
23
24
  />
24
25
  );
25
26
  }
@@ -14,7 +14,8 @@ export function TextareaField({
14
14
  placeholder={field.placeholder}
15
15
  value={(value as string) || ''}
16
16
  onChange={(e) => onChange(e.target.value)}
17
- rows={4}
17
+ rows={field.rows || 4}
18
+ data-testid={field.dataTestID}
18
19
  />
19
20
  );
20
21
  }
@@ -28,6 +28,7 @@ export function TimePickerField({
28
28
  showFooter={field.showFooter}
29
29
  cancelLabel={field.cancelLabel}
30
30
  applyLabel={field.applyLabel}
31
+ data-testid={field.dataTestID}
31
32
  />
32
33
  );
33
34
  }
@@ -36,6 +36,7 @@ export function TimeRangePickerField({
36
36
  showFooter={field.showFooter}
37
37
  cancelLabel={field.cancelLabel}
38
38
  applyLabel={field.applyLabel}
39
+ data-testid={field.dataTestID}
39
40
  />
40
41
  );
41
42
  }
@@ -73,13 +73,18 @@ export interface FormBuilderFieldConfig<
73
73
  > {
74
74
  id?: string;
75
75
  name: TName;
76
- label: string;
77
76
  type: FieldType;
77
+ label: React.ReactNode;
78
78
  placeholder?: string;
79
79
  description?: string;
80
+ subLabel?: React.ReactNode;
81
+ infoTooltip?: React.ReactNode;
82
+ infoTooltipIconSize?: string | number;
80
83
  required?: boolean;
81
84
  disabled?: boolean;
82
85
  readOnly?: boolean;
86
+ dataTestID?: string;
87
+ numberMode?: "currency" | undefined;
83
88
  options?: { label: string; value: string | number | boolean | null }[];
84
89
  autocompleteMode?: 'client' | 'server';
85
90
  fetcher?: AutocompleteFetcher;