pcm-shared-components 2.1.390 → 2.1.392

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.
@@ -54,6 +54,22 @@ const REQUIRED_ERROR_KEY_BY_TYPE = {
54
54
  yes_no: 'animal.required_error_yes_no'
55
55
  };
56
56
 
57
+ // A native <input type="date"> only opens its calendar popup on its own when the browser's tiny
58
+ // built-in icon is clicked -- clicking anywhere else in the field just places the text caret
59
+ // (still lets someone type the year/month/day directly, which showPicker() doesn't change). Most
60
+ // people expect clicking anywhere in the box to open the calendar. showPicker() is Chrome/Edge/
61
+ // Firefox only as of this writing (not Safari) and throws if called on a disabled/read-only input
62
+ // or outside a user gesture, so this fails silently rather than breaking the click -- on an
63
+ // unsupported browser, the field just falls back to manual typing and the browser's own
64
+ // icon-click affordance, exactly as before.
65
+ const openNativeDatePicker = e => {
66
+ try {
67
+ e.target.showPicker?.();
68
+ } catch {
69
+ // ignore -- unsupported browser or transient DOM state, typing/icon-click still work
70
+ }
71
+ };
72
+
57
73
  /**
58
74
  * Tab-per-item editor for a lot's animal or vehicle detail fields: one tab per item (green
59
75
  * check when every required field is answered, amber warning triangle otherwise), an "Add"
@@ -285,234 +301,241 @@ export const AnimalVehicleDetailsEditor = props => {
285
301
  onClick: () => requestRemove(selectedItem)
286
302
  }, /*#__PURE__*/React.createElement(DeleteOutlineIcon, {
287
303
  fontSize: "small"
288
- }))), /*#__PURE__*/React.createElement("div", {
289
- ref: fieldsContainerRef,
290
- className: "grid grid-cols-2",
291
- style: {
292
- columnGap: 12,
293
- rowGap: 16,
294
- gridAutoFlow: 'row dense'
295
- }
296
- }, fieldConfig.map(field => {
297
- const rawValue = selectedItem.values?.[field.key] ?? '';
298
- const revealKey = `${selectedItem.id}_${field.key}`;
299
- const wasDrifted = isFieldDrifted(field, rawValue);
300
- const showAsDrifted = wasDrifted && !revealedDriftKeys.has(revealKey);
301
- // Once a drifted field is revealed, the live control gets an EMPTY value, never
302
- // the mismatched raw one -- a drifted answer is never editable-in-place, only
303
- // replaceable. Not drifted (or already revealed): behaves exactly as before.
304
- const value = showAsDrifted ? rawValue : wasDrifted ? field.type === 'select_multiple' ? [] : '' : rawValue;
305
- const showError = field.required && !isFieldAnswered(field.type, value);
306
- // No "(Optional)" suffix -- required fields already say so via the asterisk, so
307
- // spelling out the alternative on every other field was redundant noise, and the
308
- // extra length was long enough to wrap onto a second line in a narrow column,
309
- // throwing that field's input out of vertical alignment with its paired neighbor.
310
- const labelText = field.required ? `${field.label} *` : field.label;
311
- const wrapperClassName = ['select', 'textarea', 'select_multiple'].includes(field.type) ? 'col-span-full' : undefined;
312
- let control;
313
- if (showAsDrifted) {
314
- // Generic stringify -- this renders whatever type the field used to be, which
315
- // may no longer match field.type at all, so it can't reuse any type-specific
316
- // control (a boolean/array/string all need to display as plain read text here).
317
- const displayText = typeof rawValue === 'boolean' ? rawValue ? i18next.t('common.app.yes') : i18next.t('common.app.no') : Array.isArray(rawValue) ? rawValue.join(', ') : String(rawValue);
318
- control = /*#__PURE__*/React.createElement("div", {
319
- className: "flex flex-col",
320
- style: {
321
- gap: 4
322
- }
323
- }, /*#__PURE__*/React.createElement(Paper, {
324
- variant: "outlined",
325
- className: "flex items-center",
326
- sx: {
327
- gap: 6,
328
- px: 1.5,
329
- py: 1,
330
- bgcolor: 'action.hover'
331
- }
332
- }, /*#__PURE__*/React.createElement(WarningAmberIcon, {
333
- fontSize: "small",
334
- color: "warning"
335
- }), /*#__PURE__*/React.createElement(Typography, {
304
+ }))), (() => {
305
+ const pairableFields = fieldConfig.filter(f => !['select', 'textarea', 'select_multiple'].includes(f.type));
306
+ const orphanField = pairableFields.length % 2 === 1 ? pairableFields[pairableFields.length - 1] : null;
307
+ return /*#__PURE__*/React.createElement("div", {
308
+ ref: fieldsContainerRef,
309
+ className: "grid grid-cols-2",
310
+ style: {
311
+ columnGap: 12,
312
+ rowGap: 16,
313
+ gridAutoFlow: 'row dense'
314
+ }
315
+ }, fieldConfig.map(field => {
316
+ const rawValue = selectedItem.values?.[field.key] ?? '';
317
+ const revealKey = `${selectedItem.id}_${field.key}`;
318
+ const wasDrifted = isFieldDrifted(field, rawValue);
319
+ const showAsDrifted = wasDrifted && !revealedDriftKeys.has(revealKey);
320
+ // Once a drifted field is revealed, the live control gets an EMPTY value, never
321
+ // the mismatched raw one -- a drifted answer is never editable-in-place, only
322
+ // replaceable. Not drifted (or already revealed): behaves exactly as before.
323
+ const value = showAsDrifted ? rawValue : wasDrifted ? field.type === 'select_multiple' ? [] : '' : rawValue;
324
+ const showError = field.required && !isFieldAnswered(field.type, value);
325
+ // No "(Optional)" suffix -- required fields already say so via the asterisk, so
326
+ // spelling out the alternative on every other field was redundant noise, and the
327
+ // extra length was long enough to wrap onto a second line in a narrow column,
328
+ // throwing that field's input out of vertical alignment with its paired neighbor.
329
+ const labelText = field.required ? `${field.label} *` : field.label;
330
+ const wrapperClassName = ['select', 'textarea', 'select_multiple'].includes(field.type) || field === orphanField ? 'col-span-full' : undefined;
331
+ let control;
332
+ if (showAsDrifted) {
333
+ // Generic stringify -- this renders whatever type the field used to be, which
334
+ // may no longer match field.type at all, so it can't reuse any type-specific
335
+ // control (a boolean/array/string all need to display as plain read text here).
336
+ const displayText = typeof rawValue === 'boolean' ? rawValue ? i18next.t('common.app.yes') : i18next.t('common.app.no') : Array.isArray(rawValue) ? rawValue.join(', ') : String(rawValue);
337
+ control = /*#__PURE__*/React.createElement("div", {
338
+ className: "flex flex-col",
339
+ style: {
340
+ gap: 4
341
+ }
342
+ }, /*#__PURE__*/React.createElement(Paper, {
343
+ variant: "outlined",
344
+ className: "flex items-center",
345
+ sx: {
346
+ gap: 6,
347
+ px: 1.5,
348
+ py: 1,
349
+ bgcolor: 'action.hover'
350
+ }
351
+ }, /*#__PURE__*/React.createElement(WarningAmberIcon, {
352
+ fontSize: "small",
353
+ color: "warning"
354
+ }), /*#__PURE__*/React.createElement(Typography, {
355
+ variant: "body2",
356
+ sx: {
357
+ flex: 1,
358
+ wordBreak: 'break-word'
359
+ }
360
+ }, displayText)), /*#__PURE__*/React.createElement("div", {
361
+ className: "flex items-center",
362
+ style: {
363
+ gap: 4
364
+ }
365
+ }, /*#__PURE__*/React.createElement(Typography, {
366
+ variant: "caption",
367
+ color: "text.secondary"
368
+ }, i18next.t('animal.field_drifted_hint')), /*#__PURE__*/React.createElement(Button, {
369
+ size: "small",
370
+ onClick: () => revealDrift(revealKey),
371
+ sx: {
372
+ textTransform: 'none',
373
+ minWidth: 0,
374
+ p: 0
375
+ }
376
+ }, i18next.t('animal.field_drifted_replace'))));
377
+ } else if (field.type === 'number') {
378
+ control = /*#__PURE__*/React.createElement(TextField, {
379
+ name: field.key,
380
+ value: value,
381
+ onChange: e => onFieldChange(selectedItem.id, field.key, e.target.value),
382
+ onKeyDown: e => {
383
+ if (e.key === 'e' || e.key === 'E') e.preventDefault();
384
+ },
385
+ variant: "outlined",
386
+ size: "small",
387
+ error: showError,
388
+ helperText: showError ? requiredErrorText(field.type) : '',
389
+ inputProps: {
390
+ maxLength: field.maxLength || 45
391
+ },
392
+ type: "number",
393
+ placeholder: field.placeholder,
394
+ fullWidth: true,
395
+ InputProps: {
396
+ endAdornment: field.unit ? /*#__PURE__*/React.createElement(InputAdornment, {
397
+ position: "end"
398
+ }, field.unit) : undefined
399
+ }
400
+ });
401
+ } else if (field.type === 'select') {
402
+ control = /*#__PURE__*/React.createElement(TextField, {
403
+ select: true,
404
+ name: field.key,
405
+ value: value,
406
+ onChange: e => onFieldChange(selectedItem.id, field.key, e.target.value),
407
+ variant: "outlined",
408
+ size: "small",
409
+ error: showError,
410
+ helperText: showError ? requiredErrorText(field.type) : '',
411
+ fullWidth: true
412
+ }, /*#__PURE__*/React.createElement(MenuItem, {
413
+ value: ""
414
+ }, /*#__PURE__*/React.createElement("em", null, field.placeholder || i18next.t('common.app.select'))), (field.options || []).map(opt => /*#__PURE__*/React.createElement(MenuItem, {
415
+ key: opt,
416
+ value: opt
417
+ }, opt)));
418
+ } else if (field.type === 'textarea') {
419
+ control = /*#__PURE__*/React.createElement(TextField, {
420
+ name: field.key,
421
+ value: value,
422
+ onChange: e => onFieldChange(selectedItem.id, field.key, e.target.value),
423
+ variant: "outlined",
424
+ size: "small",
425
+ error: showError,
426
+ helperText: showError ? requiredErrorText(field.type) : '',
427
+ inputProps: {
428
+ maxLength: field.maxLength || 45
429
+ },
430
+ placeholder: field.placeholder,
431
+ multiline: true,
432
+ rows: 3,
433
+ fullWidth: true
434
+ });
435
+ } else if (field.type === 'select_multiple') {
436
+ const selectedValues = Array.isArray(value) ? value : [];
437
+ control = /*#__PURE__*/React.createElement(TextField, {
438
+ select: true,
439
+ name: field.key,
440
+ value: selectedValues,
441
+ onChange: e => onFieldChange(selectedItem.id, field.key, e.target.value),
442
+ variant: "outlined",
443
+ size: "small",
444
+ error: showError,
445
+ helperText: showError ? requiredErrorText(field.type) : '',
446
+ fullWidth: true,
447
+ SelectProps: {
448
+ multiple: true,
449
+ renderValue: selected => /*#__PURE__*/React.createElement("div", {
450
+ className: "flex flex-wrap gap-4"
451
+ }, selected.map(opt => /*#__PURE__*/React.createElement(Chip, {
452
+ key: opt,
453
+ label: opt,
454
+ size: "small"
455
+ })))
456
+ }
457
+ }, (field.options || []).map(opt => /*#__PURE__*/React.createElement(MenuItem, {
458
+ key: opt,
459
+ value: opt
460
+ }, /*#__PURE__*/React.createElement(Checkbox, {
461
+ checked: selectedValues.indexOf(opt) > -1
462
+ }), /*#__PURE__*/React.createElement(ListItemText, {
463
+ primary: opt
464
+ }))));
465
+ } else if (field.type === 'yes_no') {
466
+ const toggleValue = value === true ? 'yes' : value === false ? 'no' : null;
467
+ control = /*#__PURE__*/React.createElement(ToggleButtonGroup, {
468
+ value: toggleValue,
469
+ exclusive: true,
470
+ onChange: (_, val) => {
471
+ if (val !== null) onFieldChange(selectedItem.id, field.key, val === 'yes');
472
+ },
473
+ fullWidth: true,
474
+ size: "small",
475
+ color: showError ? 'error' : 'standard'
476
+ }, /*#__PURE__*/React.createElement(ToggleButton, {
477
+ value: "yes"
478
+ }, i18next.t('common.app.yes')), /*#__PURE__*/React.createElement(ToggleButton, {
479
+ value: "no"
480
+ }, i18next.t('common.app.no')));
481
+ } else if (field.type === 'date') {
482
+ // Native date input, not each host app's own local DateTimeField wrapper --
483
+ // this component can't depend on either app's app-local component tree.
484
+ // Value is a plain 'YYYY-MM-DD' string in and out, same convention both apps
485
+ // already use for this field type.
486
+ control = /*#__PURE__*/React.createElement(TextField, {
487
+ name: field.key,
488
+ type: "date",
489
+ value: value,
490
+ onChange: e => onFieldChange(selectedItem.id, field.key, e.target.value),
491
+ variant: "outlined",
492
+ size: "small",
493
+ error: showError,
494
+ helperText: showError ? requiredErrorText(field.type) : '',
495
+ fullWidth: true,
496
+ InputLabelProps: {
497
+ shrink: true
498
+ },
499
+ inputProps: {
500
+ onClick: openNativeDatePicker
501
+ }
502
+ });
503
+ } else {
504
+ // 'text' (default/unknown type too, for backward compat)
505
+ control = /*#__PURE__*/React.createElement(TextField, {
506
+ name: field.key,
507
+ value: value,
508
+ onChange: e => onFieldChange(selectedItem.id, field.key, e.target.value),
509
+ variant: "outlined",
510
+ size: "small",
511
+ error: showError,
512
+ helperText: showError ? requiredErrorText(field.type) : '',
513
+ inputProps: {
514
+ maxLength: field.maxLength || 45
515
+ },
516
+ type: "text",
517
+ placeholder: field.placeholder,
518
+ fullWidth: true
519
+ });
520
+ }
521
+ return /*#__PURE__*/React.createElement("div", {
522
+ className: wrapperClassName,
523
+ key: revealKey,
524
+ "data-field-invalid": showError ? 'true' : undefined
525
+ }, /*#__PURE__*/React.createElement(Typography, {
336
526
  variant: "body2",
337
527
  sx: {
338
- flex: 1,
339
- wordBreak: 'break-word'
340
- }
341
- }, displayText)), /*#__PURE__*/React.createElement("div", {
342
- className: "flex items-center",
343
- style: {
344
- gap: 4
528
+ fontWeight: 500,
529
+ mb: 0.5,
530
+ color: showError ? 'error.main' : 'text.primary'
345
531
  }
346
- }, /*#__PURE__*/React.createElement(Typography, {
532
+ }, labelText), control, field.type === 'yes_no' && !showAsDrifted && showError && /*#__PURE__*/React.createElement(Typography, {
347
533
  variant: "caption",
348
- color: "text.secondary"
349
- }, i18next.t('animal.field_drifted_hint')), /*#__PURE__*/React.createElement(Button, {
350
- size: "small",
351
- onClick: () => revealDrift(revealKey),
352
- sx: {
353
- textTransform: 'none',
354
- minWidth: 0,
355
- p: 0
356
- }
357
- }, i18next.t('animal.field_drifted_replace'))));
358
- } else if (field.type === 'number') {
359
- control = /*#__PURE__*/React.createElement(TextField, {
360
- name: field.key,
361
- value: value,
362
- onChange: e => onFieldChange(selectedItem.id, field.key, e.target.value),
363
- onKeyDown: e => {
364
- if (e.key === 'e' || e.key === 'E') e.preventDefault();
365
- },
366
- variant: "outlined",
367
- size: "small",
368
- error: showError,
369
- helperText: showError ? requiredErrorText(field.type) : '',
370
- inputProps: {
371
- maxLength: field.maxLength || 45
372
- },
373
- type: "number",
374
- placeholder: field.placeholder,
375
- fullWidth: true,
376
- InputProps: {
377
- endAdornment: field.unit ? /*#__PURE__*/React.createElement(InputAdornment, {
378
- position: "end"
379
- }, field.unit) : undefined
380
- }
381
- });
382
- } else if (field.type === 'select') {
383
- control = /*#__PURE__*/React.createElement(TextField, {
384
- select: true,
385
- name: field.key,
386
- value: value,
387
- onChange: e => onFieldChange(selectedItem.id, field.key, e.target.value),
388
- variant: "outlined",
389
- size: "small",
390
- error: showError,
391
- helperText: showError ? requiredErrorText(field.type) : '',
392
- fullWidth: true
393
- }, /*#__PURE__*/React.createElement(MenuItem, {
394
- value: ""
395
- }, /*#__PURE__*/React.createElement("em", null, field.placeholder || i18next.t('common.app.select'))), (field.options || []).map(opt => /*#__PURE__*/React.createElement(MenuItem, {
396
- key: opt,
397
- value: opt
398
- }, opt)));
399
- } else if (field.type === 'textarea') {
400
- control = /*#__PURE__*/React.createElement(TextField, {
401
- name: field.key,
402
- value: value,
403
- onChange: e => onFieldChange(selectedItem.id, field.key, e.target.value),
404
- variant: "outlined",
405
- size: "small",
406
- error: showError,
407
- helperText: showError ? requiredErrorText(field.type) : '',
408
- inputProps: {
409
- maxLength: field.maxLength || 45
410
- },
411
- placeholder: field.placeholder,
412
- multiline: true,
413
- rows: 3,
414
- fullWidth: true
415
- });
416
- } else if (field.type === 'select_multiple') {
417
- const selectedValues = Array.isArray(value) ? value : [];
418
- control = /*#__PURE__*/React.createElement(TextField, {
419
- select: true,
420
- name: field.key,
421
- value: selectedValues,
422
- onChange: e => onFieldChange(selectedItem.id, field.key, e.target.value),
423
- variant: "outlined",
424
- size: "small",
425
- error: showError,
426
- helperText: showError ? requiredErrorText(field.type) : '',
427
- fullWidth: true,
428
- SelectProps: {
429
- multiple: true,
430
- renderValue: selected => /*#__PURE__*/React.createElement("div", {
431
- className: "flex flex-wrap gap-4"
432
- }, selected.map(opt => /*#__PURE__*/React.createElement(Chip, {
433
- key: opt,
434
- label: opt,
435
- size: "small"
436
- })))
437
- }
438
- }, (field.options || []).map(opt => /*#__PURE__*/React.createElement(MenuItem, {
439
- key: opt,
440
- value: opt
441
- }, /*#__PURE__*/React.createElement(Checkbox, {
442
- checked: selectedValues.indexOf(opt) > -1
443
- }), /*#__PURE__*/React.createElement(ListItemText, {
444
- primary: opt
445
- }))));
446
- } else if (field.type === 'yes_no') {
447
- const toggleValue = value === true ? 'yes' : value === false ? 'no' : null;
448
- control = /*#__PURE__*/React.createElement(ToggleButtonGroup, {
449
- value: toggleValue,
450
- exclusive: true,
451
- onChange: (_, val) => {
452
- if (val !== null) onFieldChange(selectedItem.id, field.key, val === 'yes');
453
- },
454
- fullWidth: true,
455
- size: "small",
456
- color: showError ? 'error' : 'standard'
457
- }, /*#__PURE__*/React.createElement(ToggleButton, {
458
- value: "yes"
459
- }, i18next.t('common.app.yes')), /*#__PURE__*/React.createElement(ToggleButton, {
460
- value: "no"
461
- }, i18next.t('common.app.no')));
462
- } else if (field.type === 'date') {
463
- // Native date input, not each host app's own local DateTimeField wrapper --
464
- // this component can't depend on either app's app-local component tree.
465
- // Value is a plain 'YYYY-MM-DD' string in and out, same convention both apps
466
- // already use for this field type.
467
- control = /*#__PURE__*/React.createElement(TextField, {
468
- name: field.key,
469
- type: "date",
470
- value: value,
471
- onChange: e => onFieldChange(selectedItem.id, field.key, e.target.value),
472
- variant: "outlined",
473
- size: "small",
474
- error: showError,
475
- helperText: showError ? requiredErrorText(field.type) : '',
476
- fullWidth: true,
477
- InputLabelProps: {
478
- shrink: true
479
- }
480
- });
481
- } else {
482
- // 'text' (default/unknown type too, for backward compat)
483
- control = /*#__PURE__*/React.createElement(TextField, {
484
- name: field.key,
485
- value: value,
486
- onChange: e => onFieldChange(selectedItem.id, field.key, e.target.value),
487
- variant: "outlined",
488
- size: "small",
489
- error: showError,
490
- helperText: showError ? requiredErrorText(field.type) : '',
491
- inputProps: {
492
- maxLength: field.maxLength || 45
493
- },
494
- type: "text",
495
- placeholder: field.placeholder,
496
- fullWidth: true
497
- });
498
- }
499
- return /*#__PURE__*/React.createElement("div", {
500
- className: wrapperClassName,
501
- key: revealKey,
502
- "data-field-invalid": showError ? 'true' : undefined
503
- }, /*#__PURE__*/React.createElement(Typography, {
504
- variant: "body2",
505
- sx: {
506
- fontWeight: 500,
507
- mb: 0.5,
508
- color: showError ? 'error.main' : 'text.primary'
509
- }
510
- }, labelText), control, field.type === 'yes_no' && !showAsDrifted && showError && /*#__PURE__*/React.createElement(Typography, {
511
- variant: "caption",
512
- color: "error",
513
- className: "block mt-4"
514
- }, requiredErrorText(field.type)));
515
- })))), showHistoricalAnswers && selectedItem && (() => {
534
+ color: "error",
535
+ className: "block mt-4"
536
+ }, requiredErrorText(field.type)));
537
+ }));
538
+ })())), showHistoricalAnswers && selectedItem && (() => {
516
539
  const liveKeys = new Set(fieldConfig.map(f => f.key));
517
540
  const historicalEntries = Object.keys(selectedItem.values || {}).filter(k => !liveKeys.has(k)).map(k => ({
518
541
  key: k,
@@ -2452,6 +2452,7 @@
2452
2452
  "configure": "Configure Fields",
2453
2453
  "preview_hint": "How guests see it",
2454
2454
  "preview_empty": "Add a field to see it here.",
2455
+ "preview_reset_hint": "Clear your test answers to see how this looks when it first loads.",
2455
2456
  "field_type": "Field Type",
2456
2457
  "field_type_text": "Text",
2457
2458
  "field_type_select": "Dropdown (Select One)",
@@ -2452,6 +2452,7 @@
2452
2452
  "configure": "Configurar campos",
2453
2453
  "preview_hint": "Vista del cliente",
2454
2454
  "preview_empty": "Añade un campo para verlo aquí.",
2455
+ "preview_reset_hint": "Borra tus respuestas de prueba para ver cómo se ve cuando se carga por primera vez.",
2455
2456
  "field_type": "Tipo de campo",
2456
2457
  "field_type_text": "Texto",
2457
2458
  "field_type_select": "Desplegable (Seleccionar uno)",
@@ -2452,6 +2452,7 @@
2452
2452
  "configure": "Configurer les champs",
2453
2453
  "preview_hint": "Vue côté client",
2454
2454
  "preview_empty": "Ajoutez un champ pour le voir ici.",
2455
+ "preview_reset_hint": "Effacez vos réponses de test pour voir comment cela s'affiche au premier chargement.",
2455
2456
  "field_type": "Type de champ",
2456
2457
  "field_type_text": "Texte",
2457
2458
  "field_type_select": "Liste déroulante (Sélectionner un)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pcm-shared-components",
3
- "version": "2.1.390",
3
+ "version": "2.1.392",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "babel": {