@adcops/autocore-react 3.3.87 → 3.3.90

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 (112) hide show
  1. package/dist/assets/AxisC.d.ts +4 -0
  2. package/dist/assets/AxisC.d.ts.map +1 -0
  3. package/dist/assets/AxisC.js +1 -0
  4. package/dist/assets/AxisX.js +1 -1
  5. package/dist/assets/AxisY.js +1 -1
  6. package/dist/assets/AxisZ.js +1 -1
  7. package/dist/assets/JogXNeg.d.ts +4 -0
  8. package/dist/assets/JogXNeg.d.ts.map +1 -0
  9. package/dist/assets/JogXNeg.js +1 -0
  10. package/dist/assets/JogXPos.d.ts +4 -0
  11. package/dist/assets/JogXPos.d.ts.map +1 -0
  12. package/dist/assets/JogXPos.js +1 -0
  13. package/dist/assets/JogYNeg.d.ts +4 -0
  14. package/dist/assets/JogYNeg.d.ts.map +1 -0
  15. package/dist/assets/JogYNeg.js +1 -0
  16. package/dist/assets/JogYPos.d.ts +4 -0
  17. package/dist/assets/JogYPos.d.ts.map +1 -0
  18. package/dist/assets/JogYPos.js +1 -0
  19. package/dist/assets/JogZNeg.d.ts +4 -0
  20. package/dist/assets/JogZNeg.d.ts.map +1 -0
  21. package/dist/assets/JogZNeg.js +1 -0
  22. package/dist/assets/JogZPos.d.ts +4 -0
  23. package/dist/assets/JogZPos.d.ts.map +1 -0
  24. package/dist/assets/JogZPos.js +1 -0
  25. package/dist/assets/Off.d.ts +4 -0
  26. package/dist/assets/Off.d.ts.map +1 -0
  27. package/dist/assets/Off.js +1 -0
  28. package/dist/assets/On.d.ts +4 -0
  29. package/dist/assets/On.d.ts.map +1 -0
  30. package/dist/assets/On.js +1 -0
  31. package/dist/assets/index.d.ts +6 -0
  32. package/dist/assets/index.d.ts.map +1 -1
  33. package/dist/assets/index.js +1 -1
  34. package/dist/assets/svg/off.svg +2 -0
  35. package/dist/assets/svg/on.svg +11 -0
  36. package/dist/components/JogPanel.d.ts +2 -2
  37. package/dist/components/JogPanel.d.ts.map +1 -1
  38. package/dist/components/JogPanel.js +1 -1
  39. package/dist/components/ams/AmsProvider.d.ts +10 -0
  40. package/dist/components/ams/AmsProvider.d.ts.map +1 -1
  41. package/dist/components/ams/AssetDetailView.js +1 -1
  42. package/dist/components/ams/AssetEditDialog.d.ts.map +1 -1
  43. package/dist/components/ams/AssetEditDialog.js +1 -1
  44. package/dist/components/ams/AssetRegistryTable.css +12 -0
  45. package/dist/components/ams/AssetRegistryTable.d.ts +1 -0
  46. package/dist/components/ams/AssetRegistryTable.d.ts.map +1 -1
  47. package/dist/components/ams/AssetRegistryTable.js +1 -1
  48. package/dist/components/forms/FormRow.js +1 -1
  49. package/dist/components/forms/FormSection.js +1 -1
  50. package/dist/components/forms/forms.css +18 -18
  51. package/dist/components/tis/ConfigurationDialog.d.ts +21 -0
  52. package/dist/components/tis/ConfigurationDialog.d.ts.map +1 -0
  53. package/dist/components/tis/ConfigurationDialog.js +1 -0
  54. package/dist/components/tis/ResultHistoryTable.js +1 -1
  55. package/dist/components/tis/TestDataView.d.ts +27 -0
  56. package/dist/components/tis/TestDataView.d.ts.map +1 -1
  57. package/dist/components/tis/TestDataView.js +1 -1
  58. package/dist/components/tis/TestSetupForm.d.ts +37 -0
  59. package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
  60. package/dist/components/tis/TestSetupForm.js +1 -1
  61. package/dist/components/tis/TisProvider.d.ts +25 -0
  62. package/dist/components/tis/TisProvider.d.ts.map +1 -1
  63. package/dist/components/tis/TisProvider.js +1 -1
  64. package/dist/components/tis-editor/TisConfigEditor.css +20 -0
  65. package/dist/components/tis-editor/editor/ConfigurationsEditor.d.ts +19 -0
  66. package/dist/components/tis-editor/editor/ConfigurationsEditor.d.ts.map +1 -0
  67. package/dist/components/tis-editor/editor/ConfigurationsEditor.js +1 -0
  68. package/dist/components/tis-editor/editor/MethodFormEditor.d.ts.map +1 -1
  69. package/dist/components/tis-editor/editor/MethodFormEditor.js +1 -1
  70. package/dist/components/tis-editor/types.d.ts +13 -0
  71. package/dist/components/tis-editor/types.d.ts.map +1 -1
  72. package/dist/components/tis-editor/validation.d.ts.map +1 -1
  73. package/dist/components/tis-editor/validation.js +1 -1
  74. package/dist/themes/adc-dark/blue/theme.css +3 -2
  75. package/dist/themes/adc-dark/blue/theme.css.map +1 -1
  76. package/package.json +2 -1
  77. package/src/assets/AxisC.tsx +38 -0
  78. package/src/assets/AxisX.tsx +32 -32
  79. package/src/assets/AxisY.tsx +34 -34
  80. package/src/assets/AxisZ.tsx +31 -31
  81. package/src/assets/JogXNeg.tsx +30 -0
  82. package/src/assets/JogXPos.tsx +30 -0
  83. package/src/assets/JogYNeg.tsx +30 -0
  84. package/src/assets/JogYPos.tsx +30 -0
  85. package/src/assets/JogZNeg.tsx +30 -0
  86. package/src/assets/JogZPos.tsx +30 -0
  87. package/src/assets/Off.tsx +14 -0
  88. package/src/assets/On.tsx +26 -0
  89. package/src/assets/index.ts +6 -0
  90. package/src/assets/svg/off.svg +2 -0
  91. package/src/assets/svg/on.svg +11 -0
  92. package/src/components/JogPanel.tsx +18 -28
  93. package/src/components/ams/AmsProvider.tsx +10 -0
  94. package/src/components/ams/AssetDetailView.tsx +1 -1
  95. package/src/components/ams/AssetEditDialog.tsx +25 -10
  96. package/src/components/ams/AssetRegistryTable.css +12 -0
  97. package/src/components/ams/AssetRegistryTable.tsx +68 -12
  98. package/src/components/forms/FormRow.tsx +6 -6
  99. package/src/components/forms/FormSection.tsx +6 -6
  100. package/src/components/forms/forms.css +18 -18
  101. package/src/components/tis/ConfigurationDialog.tsx +128 -0
  102. package/src/components/tis/ResultHistoryTable.tsx +2 -2
  103. package/src/components/tis/TestDataView.tsx +83 -1
  104. package/src/components/tis/TestSetupForm.tsx +167 -10
  105. package/src/components/tis/TisProvider.tsx +53 -0
  106. package/src/components/tis-editor/TisConfigEditor.css +20 -0
  107. package/src/components/tis-editor/editor/ConfigurationsEditor.tsx +242 -0
  108. package/src/components/tis-editor/editor/MethodFormEditor.tsx +4 -0
  109. package/src/components/tis-editor/types.ts +14 -0
  110. package/src/components/tis-editor/validation.ts +29 -0
  111. package/src/themes/adc-dark/_extensions.scss +1 -0
  112. package/src/themes/theme-base/components/panel/_fieldset.scss +2 -2
@@ -15,6 +15,7 @@ import { Dialog } from 'primereact/dialog';
15
15
  import { EventEmitterContext } from '../../core/EventEmitterContext';
16
16
  import { MessageType } from '../../hub/CommandMessage';
17
17
  import { useAms, type AmsAssetEntry, type AmsRole } from './AmsProvider';
18
+ import './AssetRegistryTable.css';
18
19
 
19
20
  // Sentinel value for the "Other..." dropdown option, which lets the
20
21
  // operator type a free-form role for the rare case (custom builds,
@@ -111,6 +112,32 @@ function subLocationsFor(schemas: any, assetType: string): SubLocationsSchema |
111
112
  return sl as SubLocationsSchema;
112
113
  }
113
114
 
115
+ /** Build a fresh `customFields` seed from a role's `defaults` map,
116
+ * narrowed to fields the asset_type schema actually declares. Server
117
+ * validation rejects unknown default keys at project load, so in
118
+ * normal operation every key will match a declared field; the
119
+ * narrowing here just protects against schema/defaults drift while a
120
+ * project is being edited.
121
+ *
122
+ * Returns string-typed values because the dialog's inputs are text-
123
+ * based; the existing `coerceField` helper converts back on submit.
124
+ * Empty / missing defaults are skipped (rather than emitting `""`) so
125
+ * required-field gating still fires for fields the role didn't seed. */
126
+ function seedCustomFromRoleDefaults(
127
+ role: AmsRole | undefined,
128
+ fields: SchemaField[],
129
+ ): Record<string, string> {
130
+ const out: Record<string, string> = {};
131
+ if (!role || !role.defaults) return out;
132
+ const declared = new Set(fields.map(f => f.name));
133
+ for (const [k, v] of Object.entries(role.defaults)) {
134
+ if (!declared.has(k)) continue;
135
+ if (v === undefined || v === null) continue;
136
+ out[k] = String(v);
137
+ }
138
+ return out;
139
+ }
140
+
114
141
  /** Coerce a per-cell string from the matrix input back to its declared
115
142
  * type. Numeric strings → JSON numbers; booleans → bool; everything
116
143
  * else stays as a string. Matches the top-level fields coercion. */
@@ -142,6 +169,8 @@ export const AssetRegistryTable: React.FC = () => {
142
169
  // when an operator clicks Register on one of its rows. Catch it
143
170
  // here and open the Add dialog pre-populated with the asset_type
144
171
  // and role — the operator just fills in the nameplate values.
172
+ // If the matching role declares `defaults`, those seed the
173
+ // nameplate inputs too (same path as onRoleChange).
145
174
  useEffect(() => {
146
175
  const handler = (e: Event) => {
147
176
  const detail = (e as CustomEvent).detail as
@@ -149,17 +178,20 @@ export const AssetRegistryTable: React.FC = () => {
149
178
  const assetType = detail?.assetType ?? '';
150
179
  const location = detail?.location ?? '';
151
180
  if (!assetType) return;
181
+ const role = (roles[assetType] ?? []).find(r => r.location === location);
182
+ const declaredFields = fieldsFor(schemas, assetType);
152
183
  setAddState({
153
184
  ...EMPTY_ADD,
154
185
  open: true,
155
186
  assetType,
156
187
  roleSelection: location,
157
188
  location,
189
+ customFields: seedCustomFromRoleDefaults(role, declaredFields),
158
190
  });
159
191
  };
160
192
  window.addEventListener('ams:prefill-add', handler);
161
193
  return () => window.removeEventListener('ams:prefill-add', handler);
162
- }, []);
194
+ }, [roles, schemas]);
163
195
 
164
196
  const typeOptions = useMemo(
165
197
  () => Object.keys(schemas).map(k => ({ label: schemas[k]?.label ?? k, value: k })),
@@ -285,15 +317,20 @@ export const AssetRegistryTable: React.FC = () => {
285
317
  * hardware), or leave it empty so they make an explicit choice
286
318
  * when there are multiple. Hide the field entirely when none.
287
319
  * Also resets the nameplate-field map — fields are type-specific
288
- * and the previous type's inputs would not be relevant. */
320
+ * and the previous type's inputs would not be relevant. When the
321
+ * auto-selected role declares `defaults`, seed those into the
322
+ * nameplate inputs so the operator only has to confirm or edit. */
289
323
  const onAssetTypeChange = (newType: string) => {
290
324
  const list = roles[newType] ?? [];
291
- const base = list.length === 1
292
- ? { roleSelection: list[0].location, location: list[0].location }
325
+ const autoRole = list.length === 1 ? list[0] : undefined;
326
+ const base = autoRole
327
+ ? { roleSelection: autoRole.location, location: autoRole.location }
293
328
  : { roleSelection: '', location: '' };
329
+ const newFields = fieldsFor(schemas, newType);
294
330
  setAddState(s => ({
295
331
  ...s, assetType: newType, ...base,
296
- customFields: {}, subLocationFields: {},
332
+ customFields: seedCustomFromRoleDefaults(autoRole, newFields),
333
+ subLocationFields: {},
297
334
  }));
298
335
  };
299
336
 
@@ -315,13 +352,22 @@ export const AssetRegistryTable: React.FC = () => {
315
352
  );
316
353
 
317
354
  /** Role dropdown change handler. ROLE_OTHER puts us into free-form
318
- * mode where the operator types into a text field. */
355
+ * mode where the operator types into a text field. When picking a
356
+ * known role with declared `defaults`, seed the nameplate inputs
357
+ * from those values so the operator only has to confirm or edit.
358
+ * Switching role is treated as an explicit reseed; values typed
359
+ * into the previous role's inputs are replaced — switching role
360
+ * is a discrete intent, not a continuous edit. */
319
361
  const onRoleChange = (value: string) => {
320
362
  if (value === ROLE_OTHER) {
321
363
  setAddState(s => ({ ...s, roleSelection: ROLE_OTHER, location: '' }));
322
- } else {
323
- setAddState(s => ({ ...s, roleSelection: value, location: value }));
364
+ return;
324
365
  }
366
+ const role = rolesForType.find(r => r.location === value);
367
+ setAddState(s => ({
368
+ ...s, roleSelection: value, location: value,
369
+ customFields: seedCustomFromRoleDefaults(role, fieldsForType),
370
+ }));
325
371
  };
326
372
 
327
373
  /** Disable Create until the operator has picked a role (or supplied
@@ -388,8 +434,11 @@ export const AssetRegistryTable: React.FC = () => {
388
434
  ? 'AMS not enabled in this project (no asset_types declared).'
389
435
  : 'No assets registered yet.'
390
436
  }
391
- size="small"
392
437
  stripedRows
438
+ /* Extra vertical padding on body cells (see the CSS) enlarges
439
+ the touch target for each row — operators on the shop-floor
440
+ touchscreen were mis-tapping the dense default rows. */
441
+ className="ams-asset-table"
393
442
  >
394
443
  <Column field="asset_id" header="Asset ID" />
395
444
  <Column field="asset_type" header="Type"
@@ -436,9 +485,16 @@ export const AssetRegistryTable: React.FC = () => {
436
485
  onChange={(e) => onAssetTypeChange(e.value)}
437
486
  placeholder="Choose asset type"
438
487
  />
439
- <label>Serial</label>
440
- <InputText value={addState.serial}
441
- onChange={(e) => setAddState(s => ({ ...s, serial: e.target.value }))} />
488
+ {/* Serial: hidden until a type is chosen. Showing it
489
+ before the type led operators to fill it in for a
490
+ not-yet-selected asset, which read as confusing. */}
491
+ {addState.assetType && (
492
+ <>
493
+ <label>Serial</label>
494
+ <InputText value={addState.serial}
495
+ onChange={(e) => setAddState(s => ({ ...s, serial: e.target.value }))} />
496
+ </>
497
+ )}
442
498
 
443
499
  {/* Role field: only shown when this asset_type has at
444
500
  least one declared role in project.json. Asset
@@ -20,15 +20,15 @@ export interface FormRowProps {
20
20
 
21
21
  export const FormRow: React.FC<FormRowProps> = ({ label, required, hint, error, htmlFor, children }) => {
22
22
  return (
23
- <div className={`ac-form-row${error ? ' ac-form-row--error' : ''}`}>
24
- <label className="ac-form-row__label" htmlFor={htmlFor}>
23
+ <div className={`ac-formrow${error ? ' ac-formrow--error' : ''}`}>
24
+ <label className="ac-formrow__label" htmlFor={htmlFor}>
25
25
  {label}
26
- {required && <span className="ac-form-row__required" aria-hidden> *</span>}
27
- {hint && <small className="ac-form-row__hint">{hint}</small>}
26
+ {required && <span className="ac-formrow__required" aria-hidden> *</span>}
27
+ {hint && <small className="ac-formrow__hint">{hint}</small>}
28
28
  </label>
29
- <div className="ac-form-row__field">
29
+ <div className="ac-formrow__field">
30
30
  {children}
31
- {error && <small className="ac-form-row__error">{error}</small>}
31
+ {error && <small className="ac-formrow__error">{error}</small>}
32
32
  </div>
33
33
  </div>
34
34
  );
@@ -19,17 +19,17 @@ export interface FormSectionProps {
19
19
 
20
20
  export const FormSection: React.FC<FormSectionProps> = ({ title, description, actions, children }) => {
21
21
  return (
22
- <section className="ac-form-section">
22
+ <section className="ac-formsection">
23
23
  {(title || actions) && (
24
- <header className="ac-form-section__header">
24
+ <header className="ac-formsection__header">
25
25
  <div>
26
- {title && <h3 className="ac-form-section__title">{title}</h3>}
27
- {description && <small className="ac-form-section__desc">{description}</small>}
26
+ {title && <h3 className="ac-formsection__title">{title}</h3>}
27
+ {description && <small className="ac-formsection__desc">{description}</small>}
28
28
  </div>
29
- {actions && <div className="ac-form-section__actions">{actions}</div>}
29
+ {actions && <div className="ac-formsection__actions">{actions}</div>}
30
30
  </header>
31
31
  )}
32
- <div className="ac-form-section__body">
32
+ <div className="ac-formsection__body">
33
33
  {children}
34
34
  </div>
35
35
  </section>
@@ -1,11 +1,11 @@
1
- .ac-form-section {
1
+ .ac-formsection {
2
2
  border: 1px solid var(--surface-d, #e2e8f0);
3
3
  border-radius: 6px;
4
4
  background: var(--surface-card, #fff);
5
5
  margin-bottom: 1rem;
6
6
  }
7
7
 
8
- .ac-form-section__header {
8
+ .ac-formsection__header {
9
9
  display: flex;
10
10
  align-items: flex-start;
11
11
  justify-content: space-between;
@@ -16,74 +16,74 @@
16
16
  border-top-right-radius: 6px;
17
17
  }
18
18
 
19
- .ac-form-section__title {
19
+ .ac-formsection__title {
20
20
  margin: 0;
21
21
  font-size: 0.95rem;
22
22
  font-weight: 600;
23
23
  }
24
24
 
25
- .ac-form-section__desc {
25
+ .ac-formsection__desc {
26
26
  color: var(--text-color-secondary, #64748b);
27
27
  display: block;
28
28
  margin-top: 0.15rem;
29
29
  }
30
30
 
31
- .ac-form-section__actions {
31
+ .ac-formsection__actions {
32
32
  display: flex;
33
33
  gap: 0.5rem;
34
34
  }
35
35
 
36
- .ac-form-section__body {
36
+ .ac-formsection__body {
37
37
  padding: 0.75rem 1rem;
38
38
  display: flex;
39
39
  flex-direction: column;
40
40
  gap: 0.5rem;
41
41
  }
42
42
 
43
- .ac-form-row {
43
+ .ac-formrow {
44
44
  display: grid;
45
45
  grid-template-columns: minmax(8rem, 14rem) 1fr;
46
46
  gap: 0.5rem 1rem;
47
47
  align-items: start;
48
48
  }
49
49
 
50
- .ac-form-row--error .ac-form-row__field input,
51
- .ac-form-row--error .ac-form-row__field .p-inputtext {
50
+ .ac-formrow--error .ac-formrow__field input,
51
+ .ac-formrow--error .ac-formrow__field .p-inputtext {
52
52
  border-color: #dc2626;
53
53
  }
54
54
 
55
- .ac-form-row__label {
55
+ .ac-formrow__label {
56
56
  font-weight: 500;
57
57
  padding-top: 0.4rem;
58
58
  display: flex;
59
59
  flex-direction: column;
60
60
  }
61
61
 
62
- .ac-form-row__required {
62
+ .ac-formrow__required {
63
63
  color: #dc2626;
64
64
  }
65
65
 
66
- .ac-form-row__hint {
66
+ .ac-formrow__hint {
67
67
  color: var(--text-color-secondary, #64748b);
68
68
  font-weight: 400;
69
69
  font-size: 0.75rem;
70
70
  margin-top: 0.15rem;
71
71
  }
72
72
 
73
- .ac-form-row__field {
73
+ .ac-formrow__field {
74
74
  display: flex;
75
75
  flex-direction: column;
76
76
  gap: 0.25rem;
77
77
  }
78
78
 
79
- .ac-form-row__field > input,
80
- .ac-form-row__field > .p-inputtext,
81
- .ac-form-row__field > .p-dropdown,
82
- .ac-form-row__field > .p-inputtextarea {
79
+ .ac-formrow__field > input,
80
+ .ac-formrow__field > .p-inputtext,
81
+ .ac-formrow__field > .p-dropdown,
82
+ .ac-formrow__field > .p-inputtextarea {
83
83
  width: 100%;
84
84
  }
85
85
 
86
- .ac-form-row__error {
86
+ .ac-formrow__error {
87
87
  color: #dc2626;
88
88
  font-size: 0.75rem;
89
89
  }
@@ -0,0 +1,128 @@
1
+ /*
2
+ * Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
3
+ *
4
+ * <ConfigurationDialog> — picker UI for choosing a method's named
5
+ * configuration (see `TestMethod.configurations`). Mirrors
6
+ * <TestMethodDialog>: a dropdown of the configurations declared on the
7
+ * active method plus the long-form description for whichever entry is
8
+ * highlighted. OK applies the choice via the supplied callback (which
9
+ * writes the configuration's `defaults` into the config_fields); Cancel
10
+ * discards.
11
+ *
12
+ * Only mounted by <TestSetupForm> when the active method declares one or
13
+ * more configurations, so the empty case is informational only.
14
+ */
15
+
16
+ import React, { useEffect, useMemo, useState } from 'react';
17
+ import { Button } from 'primereact/button';
18
+ import { Dialog } from 'primereact/dialog';
19
+ import { Dropdown } from 'primereact/dropdown';
20
+ import type { TestConfiguration } from './TestSetupForm';
21
+
22
+ /** Display name for one configuration: prefer `label`, fall back to `name`. */
23
+ export const configLabelOf = (cfg: TestConfiguration | undefined): string =>
24
+ (cfg?.label && cfg.label.length > 0) ? cfg.label : (cfg?.name ?? '');
25
+
26
+ export interface ConfigurationDialogProps {
27
+ visible: boolean;
28
+ onHide: () => void;
29
+ /** Configurations declared on the active method. */
30
+ configurations: TestConfiguration[];
31
+ /** `name` of the configuration currently applied on the form. The
32
+ * dropdown opens pointing at this so the dialog reflects state. */
33
+ currentConfigName: string;
34
+ /**
35
+ * Called with the chosen configuration `name` when the operator
36
+ * clicks OK. Cancel does not fire this. The parent applies the
37
+ * configuration's defaults to the fields.
38
+ */
39
+ onSelected: (configName: string) => void;
40
+ }
41
+
42
+ export const ConfigurationDialog: React.FC<ConfigurationDialogProps> = ({
43
+ visible, onHide, configurations, currentConfigName, onSelected,
44
+ }) => {
45
+ // Local "draft" selection — the dropdown writes here; OK applies it,
46
+ // so a Cancel really cancels (matches <TestMethodDialog>).
47
+ const [draftName, setDraftName] = useState<string>(currentConfigName);
48
+
49
+ useEffect(() => {
50
+ if (visible) setDraftName(currentConfigName);
51
+ }, [visible, currentConfigName]);
52
+
53
+ const options = useMemo(
54
+ () => configurations.map(cfg => ({ label: configLabelOf(cfg), value: cfg.name })),
55
+ [configurations],
56
+ );
57
+
58
+ const draftCfg = configurations.find(c => c.name === draftName);
59
+ const draftDescription =
60
+ (draftCfg?.description && draftCfg.description.length > 0)
61
+ ? draftCfg.description
62
+ : null;
63
+
64
+ const handleOk = () => {
65
+ if (draftName && draftName !== currentConfigName) {
66
+ onSelected(draftName);
67
+ }
68
+ onHide();
69
+ };
70
+
71
+ const footer = (
72
+ <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.5rem' }}>
73
+ <Button label="Cancel" icon="pi pi-times" onClick={onHide} text />
74
+ <Button label="OK" icon="pi pi-check" onClick={handleOk} disabled={!draftName} />
75
+ </div>
76
+ );
77
+
78
+ return (
79
+ <Dialog
80
+ header="Select Configuration"
81
+ visible={visible}
82
+ onHide={onHide}
83
+ footer={footer}
84
+ modal
85
+ style={{ width: 'min(560px, 90vw)' }}
86
+ >
87
+ {options.length === 0 ? (
88
+ <p style={{ color: 'var(--text-secondary-color)' }}>
89
+ This test method declares no configurations.
90
+ </p>
91
+ ) : (
92
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
93
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
94
+ <label htmlFor="acConfigurationDropdown" style={{ flexShrink: 0 }}>
95
+ Configuration:
96
+ </label>
97
+ <Dropdown
98
+ inputId="acConfigurationDropdown"
99
+ value={draftName}
100
+ options={options}
101
+ onChange={(e) => setDraftName(e.value)}
102
+ placeholder="Select a configuration"
103
+ style={{ flex: 1 }}
104
+ />
105
+ </div>
106
+ {/* Stable-height description region, matching
107
+ <TestMethodDialog>: render a muted placeholder
108
+ rather than collapsing the dialog. */}
109
+ <div
110
+ style={{
111
+ padding: '0.75rem 1rem',
112
+ background: 'var(--surface-100)',
113
+ borderRadius: '6px',
114
+ minHeight: '4.5rem',
115
+ color: draftDescription
116
+ ? 'var(--text-color)'
117
+ : 'var(--text-secondary-color)',
118
+ fontStyle: draftDescription ? 'normal' : 'italic',
119
+ whiteSpace: 'pre-wrap',
120
+ }}
121
+ >
122
+ {draftDescription ?? 'No description provided for this configuration.'}
123
+ </div>
124
+ </div>
125
+ )}
126
+ </Dialog>
127
+ );
128
+ };
@@ -300,7 +300,7 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
300
300
  <div style={{ display: 'flex', gap: '0.4rem' }}>
301
301
  <Button
302
302
  icon={isDataBusy ? 'pi pi-spin pi-spinner' : 'pi pi-download'}
303
- label="Data"
303
+ label="Raw"
304
304
  size="small"
305
305
  outlined
306
306
  disabled={anyBusy}
@@ -310,7 +310,7 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
310
310
  />
311
311
  <Button
312
312
  icon={isReportBusy ? 'pi pi-spin pi-spinner' : 'pi pi-file'}
313
- label="Report"
313
+ label="Results"
314
314
  size="small"
315
315
  outlined
316
316
  disabled={anyBusy}
@@ -37,6 +37,7 @@ import { Chart as ChartJS,
37
37
  Title, Tooltip, Legend,
38
38
  } from 'chart.js';
39
39
  import zoomPlugin from 'chartjs-plugin-zoom';
40
+ import annotationPlugin from 'chartjs-plugin-annotation';
40
41
  import { Line } from 'react-chartjs-2';
41
42
 
42
43
  import { EventEmitterContext } from '../../core/EventEmitterContext';
@@ -46,7 +47,7 @@ import { useRawCycleData } from './useRawCycleData';
46
47
 
47
48
  ChartJS.register(
48
49
  CategoryScale, LinearScale, PointElement, LineElement,
49
- Title, Tooltip, Legend, zoomPlugin,
50
+ Title, Tooltip, Legend, zoomPlugin, annotationPlugin,
50
51
  );
51
52
 
52
53
  // -------------------------------------------------------------------------
@@ -69,11 +70,38 @@ export interface TestFieldDef {
69
70
 
70
71
  export interface ChartAxis { field?: string; column?: string; label?: string; }
71
72
  export interface ChartSeries { field?: string; column?: string; label?: string; y_axis?: 'left' | 'right'; }
73
+ /**
74
+ * A shaded X-range band drawn over the plot (rendered as a
75
+ * chartjs-plugin-annotation `box` annotation spanning the full Y height).
76
+ * `xMin`/`xMax` are in the same units as the view's x data — for a
77
+ * `raw_trace` that's the x column's value, for a `cycle_scatter` it's the
78
+ * category index. Used to indicate a processed / region-of-interest span.
79
+ */
80
+ export interface ChartRegion {
81
+ /** Start of the band on the X axis. */
82
+ xMin: number;
83
+ /** End of the band on the X axis. */
84
+ xMax: number;
85
+ /** Optional caption drawn at the top-center of the band. */
86
+ label?: string;
87
+ /** Fill color (any CSS color). Default: translucent theme accent. */
88
+ color?: string;
89
+ /** Optional band-edge border color. Default: no border. */
90
+ borderColor?: string;
91
+ }
72
92
  export interface ChartView {
73
93
  title?: string;
74
94
  type: 'cycle_scatter' | 'raw_trace';
75
95
  x: ChartAxis;
76
96
  y: ChartSeries[];
97
+ /**
98
+ * Optional shaded X-range bands drawn over the plot. Declared per view
99
+ * in project.json (static). The dynamic case — bands computed at
100
+ * runtime from processed/results data — would feed the same
101
+ * `buildRegionAnnotations()` helper via a future prop; see its seam in
102
+ * the `chartOptions` memo.
103
+ */
104
+ regions?: ChartRegion[];
77
105
  }
78
106
  export interface RawColumn { source: string; }
79
107
  export interface RawDataShape {
@@ -375,6 +403,12 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
375
403
  mode: 'xy' as const,
376
404
  },
377
405
  },
406
+ // Region bands. Today these come from the view's static
407
+ // `regions` declaration; a future dynamic source (processed
408
+ // data) can merge its own annotations into this same map.
409
+ annotation: {
410
+ annotations: buildRegionAnnotations(selectedViewDef?.regions),
411
+ },
378
412
  },
379
413
  };
380
414
  }, [selectedViewDef, usesRightAxis]);
@@ -1010,6 +1044,54 @@ const CHART_COLORS = [
1010
1044
  ];
1011
1045
  const palette = (i: number) => CHART_COLORS[i % CHART_COLORS.length];
1012
1046
 
1047
+ // Default translucent fill for region bands (theme accent at low alpha).
1048
+ const DEFAULT_REGION_FILL = 'rgba(78, 168, 222, 0.15)';
1049
+ // Label color must be a concrete canvas color — CSS variables don't
1050
+ // resolve when chart.js paints to the 2D context.
1051
+ const REGION_LABEL_COLOR = 'rgba(226, 232, 240, 0.85)';
1052
+
1053
+ /**
1054
+ * Translate the view's `regions` into chartjs-plugin-annotation `box`
1055
+ * annotations keyed by a stable id. Each band spans the full Y height
1056
+ * (yMin/yMax left unset) between `xMin` and `xMax` on the x scale.
1057
+ * Returns an empty map when there are no regions, which the plugin
1058
+ * treats as "draw nothing".
1059
+ *
1060
+ * This is the single seam through which both the static schema path and a
1061
+ * future dynamic (processed-data) path produce annotations.
1062
+ */
1063
+ function buildRegionAnnotations(regions?: ChartRegion[]): Record<string, any> {
1064
+ if (!regions || regions.length === 0) return {};
1065
+ const out: Record<string, any> = {};
1066
+ regions.forEach((r, i) => {
1067
+ const hasBorder = !!r.borderColor;
1068
+ out[`region-${i}`] = {
1069
+ type: 'box',
1070
+ xScaleID: 'x',
1071
+ xMin: r.xMin,
1072
+ xMax: r.xMax,
1073
+ // yMin/yMax intentionally omitted → band spans the plot height.
1074
+ backgroundColor: r.color ?? DEFAULT_REGION_FILL,
1075
+ borderColor: hasBorder ? r.borderColor : 'transparent',
1076
+ borderWidth: hasBorder ? 1 : 0,
1077
+ // Sit behind the data line so the trace stays readable.
1078
+ drawTime: 'beforeDatasetsDraw' as const,
1079
+ ...(r.label
1080
+ ? {
1081
+ label: {
1082
+ display: true,
1083
+ content: r.label,
1084
+ position: { x: 'center' as const, y: 'start' as const },
1085
+ color: REGION_LABEL_COLOR,
1086
+ font: { size: 11 },
1087
+ },
1088
+ }
1089
+ : {}),
1090
+ };
1091
+ });
1092
+ return out;
1093
+ }
1094
+
1013
1095
  // Loading / error wash drawn over the chart area while a raw_trace
1014
1096
  // fetch is in flight. Centered, pointer-events-none so the operator
1015
1097
  // can still interact with the dropdown above.