@open-mercato/ui 0.6.6-develop.6241.1.47c01ad4ed → 0.6.6-develop.6245.1.2be3b151f8

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.
@@ -3,7 +3,10 @@
3
3
  import * as React from 'react'
4
4
  import { Cog, GripVertical, Languages, Pencil, Plus, Trash2, X } from 'lucide-react'
5
5
  import { Button } from '../../primitives/button'
6
+ import { CheckboxField } from '../../primitives/checkbox-field'
7
+ import { FormField } from '../../primitives/form-field'
6
8
  import { IconButton } from '../../primitives/icon-button'
9
+ import { Input } from '../../primitives/input'
7
10
  import {
8
11
  Select,
9
12
  SelectContent,
@@ -72,6 +75,17 @@ const DEFAULT_KIND_OPTIONS = CUSTOM_FIELD_KINDS.map((k) => ({
72
75
  label: k.charAt(0).toUpperCase() + k.slice(1),
73
76
  }))
74
77
 
78
+ const DEFAULT_VALUE_NONE = '__open_mercato_no_default__'
79
+
80
+ function getDefaultValueNoneOptionValue(options: CustomFieldOptionDto[]): string {
81
+ const optionValues = new Set(options.map((option) => String(option.value)))
82
+ let candidate = DEFAULT_VALUE_NONE
83
+ while (optionValues.has(candidate)) {
84
+ candidate = `${candidate}_`
85
+ }
86
+ return candidate
87
+ }
88
+
75
89
  const FIELDSET_ICON_OPTIONS = [
76
90
  { value: 'layers', label: 'Layers' },
77
91
  { value: 'tag', label: 'Tag' },
@@ -331,24 +345,22 @@ export function FieldDefinitionsEditor({
331
345
  </div>
332
346
  {resolvedActiveFieldset && activeFieldsetConfig ? (
333
347
  <div className="grid grid-cols-1 md:grid-cols-4 gap-3">
334
- <div>
335
- <label className="text-xs">Code</label>
336
- <input
337
- className="border rounded w-full px-2 py-1 text-sm font-mono"
348
+ <FormField label="Code">
349
+ <Input
350
+ size="sm"
351
+ inputClassName="font-mono"
338
352
  value={activeFieldsetConfig.code}
339
353
  onChange={(event) => handleFieldsetCodeInput(activeFieldsetConfig.code, event.target.value)}
340
354
  />
341
- </div>
342
- <div>
343
- <label className="text-xs">Label</label>
344
- <input
345
- className="border rounded w-full px-2 py-1 text-sm"
355
+ </FormField>
356
+ <FormField label="Label">
357
+ <Input
358
+ size="sm"
346
359
  value={activeFieldsetConfig.label}
347
360
  onChange={(event) => handleFieldsetPatch(activeFieldsetConfig.code, { label: event.target.value })}
348
361
  />
349
- </div>
350
- <div>
351
- <label className="text-xs">Icon</label>
362
+ </FormField>
363
+ <FormField label="Icon">
352
364
  <Select
353
365
  value={activeFieldsetConfig.icon || undefined}
354
366
  onValueChange={(value) =>
@@ -368,29 +380,28 @@ export function FieldDefinitionsEditor({
368
380
  ))}
369
381
  </SelectContent>
370
382
  </Select>
371
- </div>
372
- <div>
373
- <label className="text-xs">Description</label>
374
- <input
375
- className="border rounded w-full px-2 py-1 text-sm"
383
+ </FormField>
384
+ <FormField label="Description">
385
+ <Input
386
+ size="sm"
376
387
  value={activeFieldsetConfig.description ?? ''}
377
388
  onChange={(event) => handleFieldsetPatch(activeFieldsetConfig.code, { description: event.target.value })}
378
389
  />
379
- </div>
390
+ </FormField>
380
391
  </div>
381
392
  ) : null}
382
- <div className="flex items-center gap-2 text-xs">
383
- <label className="inline-flex items-center gap-2">
384
- <input
385
- type="checkbox"
386
- disabled={!canToggleSingleFieldset}
387
- checked={singleFieldsetChecked}
388
- onChange={(event) => onSingleFieldsetPerRecordChange?.(event.target.checked)}
389
- />
390
- Single fieldset per entity
391
- </label>
393
+ <div className="flex items-center gap-2">
394
+ <CheckboxField
395
+ size="sm"
396
+ disabled={!canToggleSingleFieldset}
397
+ checked={singleFieldsetChecked}
398
+ onCheckedChange={(checked) => onSingleFieldsetPerRecordChange?.(checked === true)}
399
+ label="Single fieldset per entity"
400
+ containerClassName="text-xs"
401
+ contentClassName="gap-1 [&_label]:text-xs [&_label]:font-normal"
402
+ />
392
403
  {!canToggleSingleFieldset ? (
393
- <span className="text-muted-foreground">(add at least two fieldsets to toggle)</span>
404
+ <span className="text-xs text-muted-foreground">(add at least two fieldsets to toggle)</span>
394
405
  ) : null}
395
406
  </div>
396
407
  </div>
@@ -411,7 +422,7 @@ export function FieldDefinitionsEditor({
411
422
  )}
412
423
  {orderNotice?.dirty && (
413
424
  <div className="sticky top-0 z-sticky -mt-1 -mb-1">
414
- <div className="inline-flex items-center gap-2 text-xs px-2 py-1 rounded border bg-amber-50 text-amber-800 shadow-sm">
425
+ <div className="inline-flex items-center gap-2 text-xs px-2 py-1 rounded border border-status-warning-border bg-status-warning-bg text-status-warning-text shadow-sm">
415
426
  {orderNotice?.saving ? 'Saving order…' : (orderNotice?.message ?? 'Reordered — saving soon')}
416
427
  </div>
417
428
  </div>
@@ -481,7 +492,7 @@ export function FieldDefinitionsEditor({
481
492
  <span key={key}>
482
493
  <Button
483
494
  variant="link"
484
- className="h-auto p-0 text-blue-600"
495
+ className="h-auto p-0"
485
496
  onClick={() => onRestoreField(key)}
486
497
  >
487
498
  {key}
@@ -741,9 +752,14 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
741
752
  Drag to reorder
742
753
  </div>
743
754
  <div className="flex items-center gap-3">
744
- <label className="inline-flex items-center gap-2 text-sm">
745
- <input type="checkbox" checked={local.isActive !== false} onChange={(event) => { apply({ isActive: event.target.checked }, true) }} /> Active
746
- </label>
755
+ <CheckboxField
756
+ size="sm"
757
+ checked={local.isActive !== false}
758
+ onCheckedChange={(checked) => { apply({ isActive: checked === true }, true) }}
759
+ label="Active"
760
+ containerClassName="text-sm"
761
+ contentClassName="gap-1 [&_label]:text-sm [&_label]:font-normal"
762
+ />
747
763
  {onTranslate && (
748
764
  <IconButton variant="outline" size="sm" onClick={onTranslate} aria-label="Translate field">
749
765
  <Languages className="h-4 w-4" />
@@ -756,24 +772,21 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
756
772
  </div>
757
773
 
758
774
  <div className="mt-3 grid grid-cols-1 md:grid-cols-12 gap-3 items-center">
759
- <div className="md:col-span-6">
760
- <label className="text-xs">Key</label>
761
- <input
762
- className={`rounded w-full px-2 py-1 text-sm font-mono ${error?.key ? 'border-red-500 border' : 'border'}`}
775
+ <FormField label="Key" error={error?.key} className="md:col-span-6">
776
+ <Input
777
+ inputClassName="font-mono"
763
778
  placeholder="snake_case"
764
779
  value={local.key}
765
780
  onChange={(event) => apply({ key: event.target.value })}
766
781
  onBlur={commit}
767
782
  />
768
- {error?.key ? <div className="text-xs text-red-600 mt-1">{error.key}</div> : null}
769
- </div>
770
- <div className="md:col-span-6">
771
- <label className="text-xs">Kind</label>
783
+ </FormField>
784
+ <FormField label="Kind" error={error?.kind} className="md:col-span-6">
772
785
  <Select
773
786
  value={local.kind || undefined}
774
787
  onValueChange={(value) => { apply({ kind: value ?? '' }, true) }}
775
788
  >
776
- <SelectTrigger className={error?.kind ? 'border-destructive' : undefined}>
789
+ <SelectTrigger aria-invalid={!!error?.kind}>
777
790
  <SelectValue />
778
791
  </SelectTrigger>
779
792
  <SelectContent>
@@ -784,9 +797,8 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
784
797
  ))}
785
798
  </SelectContent>
786
799
  </Select>
787
- {error?.kind ? <div className="text-xs text-red-600 mt-1">{error.kind}</div> : null}
800
+ </FormField>
788
801
  </div>
789
- </div>
790
802
 
791
803
  {allowFieldsetSelection ? (
792
804
  <div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
@@ -852,24 +864,20 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
852
864
  ) : null}
853
865
 
854
866
  <div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
855
- <div>
856
- <label className="text-xs">Label</label>
857
- <input
858
- className="border rounded w-full px-2 py-1 text-sm"
867
+ <FormField label="Label">
868
+ <Input
859
869
  value={typeof local.configJson?.label === 'string' ? local.configJson.label : ''}
860
870
  onChange={(event) => apply({ configJson: { ...(local.configJson || {}), label: event.target.value } })}
861
871
  onBlur={commit}
862
872
  />
863
- </div>
864
- <div>
865
- <label className="text-xs">Description</label>
866
- <input
867
- className="border rounded w-full px-2 py-1 text-sm"
873
+ </FormField>
874
+ <FormField label="Description">
875
+ <Input
868
876
  value={typeof local.configJson?.description === 'string' ? local.configJson.description : ''}
869
877
  onChange={(event) => apply({ configJson: { ...(local.configJson || {}), description: event.target.value } })}
870
878
  onBlur={commit}
871
879
  />
872
- </div>
880
+ </FormField>
873
881
 
874
882
  {(local.kind === 'text' || local.kind === 'multiline') && (
875
883
  <>
@@ -891,11 +899,14 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
891
899
  </div>
892
900
  {local.kind === 'text' && (
893
901
  <>
894
- <div className="md:col-span-2">
895
- <label className="inline-flex items-center gap-2 text-xs">
896
- <input type="checkbox" checked={!!local.configJson?.multi} onChange={(event) => { apply({ configJson: { ...(local.configJson || {}), multi: event.target.checked } }, true) }} /> Multiple
897
- </label>
898
- </div>
902
+ <CheckboxField
903
+ size="sm"
904
+ checked={!!local.configJson?.multi}
905
+ onCheckedChange={(checked) => { apply({ configJson: { ...(local.configJson || {}), multi: checked === true } }, true) }}
906
+ label="Multiple"
907
+ containerClassName="md:col-span-2"
908
+ contentClassName="gap-1 [&_label]:text-xs [&_label]:font-normal"
909
+ />
899
910
  {!!local.configJson?.multi && (
900
911
  <div className="md:col-span-2">
901
912
  <label className="text-xs">Multi-select input style</label>
@@ -940,7 +951,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
940
951
  <IconButton
941
952
  variant="ghost"
942
953
  size="xs"
943
- className="text-red-500 hover:text-red-700"
954
+ className="text-destructive hover:text-destructive/80"
944
955
  onClick={() => handleRemoveOption(idx)}
945
956
  aria-label="Remove option"
946
957
  >
@@ -968,25 +979,22 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
968
979
 
969
980
  {(local.kind === 'select' || local.kind === 'relation') && (
970
981
  <>
971
- <div>
972
- <label className="text-xs">Options URL</label>
973
- <input
974
- className="border rounded w-full px-2 py-1 text-sm"
982
+ <FormField label="Options URL">
983
+ <Input
975
984
  placeholder="/api/..."
976
985
  value={typeof local.configJson?.optionsUrl === 'string' ? local.configJson.optionsUrl : ''}
977
986
  onChange={(event) => apply({ configJson: { ...(local.configJson || {}), optionsUrl: event.target.value } })}
978
987
  onBlur={commit}
979
988
  />
980
- </div>
989
+ </FormField>
981
990
  {local.kind === 'relation' && (
982
- <div>
983
- <label className="text-xs">Related Entity ID</label>
984
- <input
985
- className="border rounded w-full px-2 py-1 text-sm font-mono"
986
- placeholder="module:entity"
987
- value={typeof local.configJson?.relatedEntityId === 'string' ? local.configJson.relatedEntityId : ''}
988
- onChange={(event) => {
989
- const relatedEntityId = event.target.value
991
+ <FormField label="Related Entity ID">
992
+ <Input
993
+ inputClassName="font-mono"
994
+ placeholder="module:entity"
995
+ value={typeof local.configJson?.relatedEntityId === 'string' ? local.configJson.relatedEntityId : ''}
996
+ onChange={(event) => {
997
+ const relatedEntityId = event.target.value
990
998
  const defOptionsUrl = relatedEntityId
991
999
  ? `/api/entities/relations/options?entityId=${encodeURIComponent(relatedEntityId)}`
992
1000
  : ''
@@ -1000,7 +1008,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1000
1008
  }}
1001
1009
  onBlur={commit}
1002
1010
  />
1003
- </div>
1011
+ </FormField>
1004
1012
  )}
1005
1013
  </>
1006
1014
  )}
@@ -1015,16 +1023,14 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1015
1023
  )}
1016
1024
 
1017
1025
  {(local.kind === 'integer' || local.kind === 'float') && (
1018
- <div className="md:col-span-2">
1019
- <label className="text-xs">Units (optional)</label>
1020
- <input
1021
- className="border rounded w-full px-2 py-1 text-sm"
1026
+ <FormField label="Units (optional)" className="md:col-span-2">
1027
+ <Input
1022
1028
  placeholder="kg, cm, etc."
1023
1029
  value={typeof local.configJson?.unit === 'string' ? local.configJson.unit : ''}
1024
1030
  onChange={(event) => apply({ configJson: { ...(local.configJson || {}), unit: event.target.value } })}
1025
1031
  onBlur={commit}
1026
1032
  />
1027
- </div>
1033
+ </FormField>
1028
1034
  )}
1029
1035
  </div>
1030
1036
 
@@ -1082,8 +1088,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1082
1088
  </Select>
1083
1089
  </div>
1084
1090
  <div className="md:col-span-4">
1085
- <input
1086
- className="border rounded w-full px-2 py-1 text-sm"
1091
+ <Input
1087
1092
  placeholder={rule?.rule === 'regex' ? 'Pattern (e.g. ^[a-z]+$)' : (['lt','lte','gt','gte'].includes(rule?.rule) ? 'Number' : '—')}
1088
1093
  value={rule?.param ?? ''}
1089
1094
  onChange={(event) => {
@@ -1100,8 +1105,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1100
1105
  />
1101
1106
  </div>
1102
1107
  <div className="md:col-span-4">
1103
- <input
1104
- className="border rounded w-full px-2 py-1 text-sm"
1108
+ <Input
1105
1109
  placeholder="Error message"
1106
1110
  value={rule?.message || ''}
1107
1111
  onChange={(event) => {
@@ -1163,37 +1167,37 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1163
1167
  if (local.kind === 'boolean') {
1164
1168
  const boolDefault = currentDefault === true ? 'true' : currentDefault === false ? 'false' : ''
1165
1169
  return (
1166
- <div className="mt-3">
1167
- <label className="text-xs">{t('entities.customFields.fields.defaultValue', 'Default value')}</label>
1168
- <select
1169
- className="border rounded w-full px-2 py-1 text-sm"
1170
- value={boolDefault}
1171
- onChange={(event) => {
1172
- const raw = event.target.value
1170
+ <FormField label={t('entities.customFields.fields.defaultValue', 'Default value')} className="mt-3">
1171
+ <Select
1172
+ value={boolDefault || DEFAULT_VALUE_NONE}
1173
+ onValueChange={(raw) => {
1173
1174
  const value = raw === 'true' ? true : raw === 'false' ? false : undefined
1174
1175
  apply({ configJson: { ...(local.configJson || {}), defaultValue: value } }, true)
1175
1176
  }}
1176
1177
  >
1177
- <option value="">{t('entities.customFields.fields.defaultValueNone', 'No default')}</option>
1178
- <option value="true">{t('entities.customFields.fields.defaultValueTrue', 'Checked (true)')}</option>
1179
- <option value="false">{t('entities.customFields.fields.defaultValueFalse', 'Unchecked (false)')}</option>
1180
- </select>
1181
- </div>
1178
+ <SelectTrigger>
1179
+ <SelectValue />
1180
+ </SelectTrigger>
1181
+ <SelectContent>
1182
+ <SelectItem value={DEFAULT_VALUE_NONE}>{t('entities.customFields.fields.defaultValueNone', 'No default')}</SelectItem>
1183
+ <SelectItem value="true">{t('entities.customFields.fields.defaultValueTrue', 'Checked (true)')}</SelectItem>
1184
+ <SelectItem value="false">{t('entities.customFields.fields.defaultValueFalse', 'Unchecked (false)')}</SelectItem>
1185
+ </SelectContent>
1186
+ </Select>
1187
+ </FormField>
1182
1188
  )
1183
1189
  }
1184
1190
  // select with static options — picker
1185
1191
  if (local.kind === 'select' && !local.configJson?.multi) {
1186
1192
  const opts = normalizeCustomFieldOptions(local.configJson?.options || [])
1187
1193
  if (opts.length > 0) {
1194
+ const noneValue = getDefaultValueNoneOptionValue(opts)
1188
1195
  return (
1189
- <div className="mt-3">
1190
- <label className="text-xs">{t('entities.customFields.fields.defaultValue', 'Default value')}</label>
1191
- <select
1192
- className="border rounded w-full px-2 py-1 text-sm"
1196
+ <FormField label={t('entities.customFields.fields.defaultValue', 'Default value')} className="mt-3">
1197
+ <Select
1193
1198
  value={typeof currentDefault === 'string' || typeof currentDefault === 'number' ? String(currentDefault) : ''}
1194
- onChange={(event) => {
1195
- const raw = event.target.value
1196
- if (!raw) {
1199
+ onValueChange={(raw) => {
1200
+ if (raw === noneValue) {
1197
1201
  apply({ configJson: { ...(local.configJson || {}), defaultValue: undefined } }, true)
1198
1202
  return
1199
1203
  }
@@ -1203,26 +1207,29 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1203
1207
  apply({ configJson: { ...(local.configJson || {}), defaultValue: typed } }, true)
1204
1208
  }}
1205
1209
  >
1206
- <option value="">{t('entities.customFields.fields.defaultValueNone', 'No default')}</option>
1207
- {opts.map((option) => (
1208
- <option key={String(option.value)} value={String(option.value)}>
1209
- {option.label}
1210
- </option>
1211
- ))}
1212
- </select>
1213
- </div>
1210
+ <SelectTrigger>
1211
+ <SelectValue placeholder={t('entities.customFields.fields.defaultValueNone', 'No default')} />
1212
+ </SelectTrigger>
1213
+ <SelectContent>
1214
+ <SelectItem value={noneValue}>{t('entities.customFields.fields.defaultValueNone', 'No default')}</SelectItem>
1215
+ {opts.map((option) => (
1216
+ <SelectItem key={String(option.value)} value={String(option.value)}>
1217
+ {option.label}
1218
+ </SelectItem>
1219
+ ))}
1220
+ </SelectContent>
1221
+ </Select>
1222
+ </FormField>
1214
1223
  )
1215
1224
  }
1216
1225
  }
1217
1226
  // Numeric kinds
1218
1227
  if (local.kind === 'integer' || local.kind === 'float') {
1219
1228
  return (
1220
- <div className="mt-3">
1221
- <label className="text-xs">{t('entities.customFields.fields.defaultValue', 'Default value')}</label>
1222
- <input
1229
+ <FormField label={t('entities.customFields.fields.defaultValue', 'Default value')} className="mt-3">
1230
+ <Input
1223
1231
  type="number"
1224
1232
  step={local.kind === 'integer' ? '1' : 'any'}
1225
- className="border rounded w-full px-2 py-1 text-sm"
1226
1233
  value={typeof currentDefault === 'number' ? String(currentDefault) : ''}
1227
1234
  onChange={(event) => {
1228
1235
  const raw = event.target.value
@@ -1233,16 +1240,14 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1233
1240
  onBlur={commit}
1234
1241
  placeholder={t('entities.customFields.fields.defaultValuePlaceholder', 'No default')}
1235
1242
  />
1236
- </div>
1243
+ </FormField>
1237
1244
  )
1238
1245
  }
1239
1246
  // Text-like kinds (text, multiline, date, datetime, currency)
1240
1247
  if (['text', 'multiline', 'date', 'datetime', 'currency'].includes(local.kind)) {
1241
1248
  return (
1242
- <div className="mt-3">
1243
- <label className="text-xs">{t('entities.customFields.fields.defaultValue', 'Default value')}</label>
1244
- <input
1245
- className="border rounded w-full px-2 py-1 text-sm"
1249
+ <FormField label={t('entities.customFields.fields.defaultValue', 'Default value')} className="mt-3">
1250
+ <Input
1246
1251
  value={typeof currentDefault === 'string' ? currentDefault : ''}
1247
1252
  onChange={(event) => {
1248
1253
  const value = event.target.value
@@ -1251,7 +1256,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1251
1256
  onBlur={commit}
1252
1257
  placeholder={t('entities.customFields.fields.defaultValuePlaceholder', 'No default')}
1253
1258
  />
1254
- </div>
1259
+ </FormField>
1255
1260
  )
1256
1261
  }
1257
1262
  return null
@@ -1259,38 +1264,34 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1259
1264
 
1260
1265
  <div className="mt-3 pt-2 border-t flex flex-wrap items-center gap-4">
1261
1266
  <span className="text-xs text-muted-foreground">{t('entities.customFields.fields.visibility', 'Visibility:')}</span>
1262
- <label className="inline-flex items-center gap-2 text-xs">
1263
- <input
1264
- type="checkbox"
1265
- checked={local.configJson?.listVisible !== false}
1266
- onChange={(event) => { apply({ configJson: { ...(local.configJson || {}), listVisible: event.target.checked } }, true) }}
1267
- />
1268
- {t('entities.customFields.fields.listVisible', 'List')}
1269
- </label>
1270
- <label className="inline-flex items-center gap-2 text-xs">
1271
- <input
1272
- type="checkbox"
1273
- checked={!!local.configJson?.filterable}
1274
- onChange={(event) => { apply({ configJson: { ...(local.configJson || {}), filterable: event.target.checked } }, true) }}
1275
- />
1276
- {t('entities.customFields.fields.filterable', 'Filter')}
1277
- </label>
1278
- <label className="inline-flex items-center gap-2 text-xs">
1279
- <input
1280
- type="checkbox"
1281
- checked={local.configJson?.formEditable !== false}
1282
- onChange={(event) => { apply({ configJson: { ...(local.configJson || {}), formEditable: event.target.checked } }, true) }}
1283
- />
1284
- {t('entities.customFields.fields.formEditable', 'Form')}
1285
- </label>
1286
- <label className="inline-flex items-center gap-2 text-xs">
1287
- <input
1288
- type="checkbox"
1289
- checked={!!local.configJson?.encrypted}
1290
- onChange={(event) => { apply({ configJson: { ...(local.configJson || {}), encrypted: event.target.checked } }, true) }}
1291
- />
1292
- {t('entities.customFields.fields.encrypted', 'Encrypted')}
1293
- </label>
1267
+ <CheckboxField
1268
+ size="sm"
1269
+ checked={local.configJson?.listVisible !== false}
1270
+ onCheckedChange={(checked) => { apply({ configJson: { ...(local.configJson || {}), listVisible: checked === true } }, true) }}
1271
+ label={t('entities.customFields.fields.listVisible', 'List')}
1272
+ contentClassName="gap-1 [&_label]:text-xs [&_label]:font-normal"
1273
+ />
1274
+ <CheckboxField
1275
+ size="sm"
1276
+ checked={!!local.configJson?.filterable}
1277
+ onCheckedChange={(checked) => { apply({ configJson: { ...(local.configJson || {}), filterable: checked === true } }, true) }}
1278
+ label={t('entities.customFields.fields.filterable', 'Filter')}
1279
+ contentClassName="gap-1 [&_label]:text-xs [&_label]:font-normal"
1280
+ />
1281
+ <CheckboxField
1282
+ size="sm"
1283
+ checked={local.configJson?.formEditable !== false}
1284
+ onCheckedChange={(checked) => { apply({ configJson: { ...(local.configJson || {}), formEditable: checked === true } }, true) }}
1285
+ label={t('entities.customFields.fields.formEditable', 'Form')}
1286
+ contentClassName="gap-1 [&_label]:text-xs [&_label]:font-normal"
1287
+ />
1288
+ <CheckboxField
1289
+ size="sm"
1290
+ checked={!!local.configJson?.encrypted}
1291
+ onCheckedChange={(checked) => { apply({ configJson: { ...(local.configJson || {}), encrypted: checked === true } }, true) }}
1292
+ label={t('entities.customFields.fields.encrypted', 'Encrypted')}
1293
+ contentClassName="gap-1 [&_label]:text-xs [&_label]:font-normal"
1294
+ />
1294
1295
  </div>
1295
1296
  </div>
1296
1297
  <Dialog
@@ -1306,10 +1307,9 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1306
1307
  <DialogDescription>Provide the stored value and optional label shown to users.</DialogDescription>
1307
1308
  </DialogHeader>
1308
1309
  <div className="space-y-4 py-2">
1309
- <div>
1310
- <label className="text-xs">Value</label>
1311
- <input
1312
- className="mt-1 w-full rounded border px-2 py-1 text-sm font-mono"
1310
+ <FormField label="Value" error={optionFormError ?? undefined}>
1311
+ <Input
1312
+ inputClassName="font-mono"
1313
1313
  placeholder="unique_value"
1314
1314
  value={optionValueDraft}
1315
1315
  onChange={(event) => {
@@ -1317,17 +1317,14 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1317
1317
  setOptionValueDraft(event.target.value)
1318
1318
  }}
1319
1319
  />
1320
- {optionFormError ? <p className="mt-1 text-xs text-red-600">{optionFormError}</p> : null}
1321
- </div>
1322
- <div>
1323
- <label className="text-xs">Label</label>
1324
- <input
1325
- className="mt-1 w-full rounded border px-2 py-1 text-sm"
1320
+ </FormField>
1321
+ <FormField label="Label">
1322
+ <Input
1326
1323
  placeholder="Label shown to users (optional)"
1327
1324
  value={optionLabelDraft}
1328
1325
  onChange={(event) => setOptionLabelDraft(event.target.value)}
1329
1326
  />
1330
- </div>
1327
+ </FormField>
1331
1328
  </div>
1332
1329
  <DialogFooter>
1333
1330
  <Button variant="outline" size="sm" onClick={handleCloseOptionDialog}>
@@ -1358,10 +1355,9 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1358
1355
  </DialogDescription>
1359
1356
  </DialogHeader>
1360
1357
  <div className="space-y-3">
1361
- <div>
1362
- <label className="text-xs font-medium">Group code</label>
1363
- <input
1364
- className="mt-1 w-full rounded border px-2 py-1 text-sm font-mono disabled:cursor-not-allowed disabled:bg-muted/50 disabled:text-muted-foreground"
1358
+ <FormField label="Group code" error={groupError ?? undefined} disabled={!!editingGroupCode}>
1359
+ <Input
1360
+ inputClassName="font-mono"
1365
1361
  value={groupDraft.code}
1366
1362
  onChange={(event) => {
1367
1363
  setGroupDraft((prev) => ({ ...prev, code: event.target.value }))
@@ -1370,26 +1366,21 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1370
1366
  disabled={!!editingGroupCode}
1371
1367
  placeholder="e.g. buying_committee"
1372
1368
  />
1373
- {groupError ? <div className="text-xs text-red-600 mt-1">{groupError}</div> : null}
1374
- </div>
1375
- <div>
1376
- <label className="text-xs font-medium">Label</label>
1377
- <input
1378
- className="mt-1 w-full rounded border px-2 py-1 text-sm"
1369
+ </FormField>
1370
+ <FormField label="Label">
1371
+ <Input
1379
1372
  value={groupDraft.title}
1380
1373
  onChange={(event) => setGroupDraft((prev) => ({ ...prev, title: event.target.value }))}
1381
1374
  placeholder="Buying committee"
1382
1375
  />
1383
- </div>
1384
- <div>
1385
- <label className="text-xs font-medium">Hint</label>
1386
- <input
1387
- className="mt-1 w-full rounded border px-2 py-1 text-sm"
1376
+ </FormField>
1377
+ <FormField label="Hint">
1378
+ <Input
1388
1379
  value={groupDraft.hint}
1389
1380
  onChange={(event) => setGroupDraft((prev) => ({ ...prev, hint: event.target.value }))}
1390
1381
  placeholder="Visible to merchandisers"
1391
1382
  />
1392
- </div>
1383
+ </FormField>
1393
1384
  {currentFieldsetValue && groupOptions.length > 0 ? (
1394
1385
  <div>
1395
1386
  <div className="text-xs font-medium mb-1">Existing groups</div>
@@ -1416,7 +1407,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1416
1407
  <IconButton
1417
1408
  variant="ghost"
1418
1409
  size="xs"
1419
- className="text-red-500 hover:text-red-600"
1410
+ className="text-destructive hover:text-destructive/80"
1420
1411
  onClick={() => handleRemoveGroupEntry(group.code)}
1421
1412
  aria-label={`Delete ${group.code}`}
1422
1413
  >