@adcops/autocore-react 3.3.89 → 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 (93) hide show
  1. package/dist/assets/JogXNeg.d.ts +4 -0
  2. package/dist/assets/JogXNeg.d.ts.map +1 -0
  3. package/dist/assets/JogXNeg.js +1 -0
  4. package/dist/assets/JogXPos.d.ts +4 -0
  5. package/dist/assets/JogXPos.d.ts.map +1 -0
  6. package/dist/assets/JogXPos.js +1 -0
  7. package/dist/assets/JogYNeg.d.ts +4 -0
  8. package/dist/assets/JogYNeg.d.ts.map +1 -0
  9. package/dist/assets/JogYNeg.js +1 -0
  10. package/dist/assets/JogYPos.d.ts +4 -0
  11. package/dist/assets/JogYPos.d.ts.map +1 -0
  12. package/dist/assets/JogYPos.js +1 -0
  13. package/dist/assets/JogZNeg.d.ts +4 -0
  14. package/dist/assets/JogZNeg.d.ts.map +1 -0
  15. package/dist/assets/JogZNeg.js +1 -0
  16. package/dist/assets/JogZPos.d.ts +4 -0
  17. package/dist/assets/JogZPos.d.ts.map +1 -0
  18. package/dist/assets/JogZPos.js +1 -0
  19. package/dist/assets/Off.d.ts +4 -0
  20. package/dist/assets/Off.d.ts.map +1 -0
  21. package/dist/assets/Off.js +1 -0
  22. package/dist/assets/On.d.ts +4 -0
  23. package/dist/assets/On.d.ts.map +1 -0
  24. package/dist/assets/On.js +1 -0
  25. package/dist/assets/index.d.ts +6 -0
  26. package/dist/assets/index.d.ts.map +1 -1
  27. package/dist/assets/index.js +1 -1
  28. package/dist/assets/svg/off.svg +2 -0
  29. package/dist/assets/svg/on.svg +11 -0
  30. package/dist/components/JogPanel.d.ts +2 -2
  31. package/dist/components/JogPanel.d.ts.map +1 -1
  32. package/dist/components/JogPanel.js +1 -1
  33. package/dist/components/ams/AssetDetailView.js +1 -1
  34. package/dist/components/ams/AssetEditDialog.d.ts.map +1 -1
  35. package/dist/components/ams/AssetEditDialog.js +1 -1
  36. package/dist/components/ams/AssetRegistryTable.css +12 -0
  37. package/dist/components/ams/AssetRegistryTable.d.ts +1 -0
  38. package/dist/components/ams/AssetRegistryTable.d.ts.map +1 -1
  39. package/dist/components/ams/AssetRegistryTable.js +1 -1
  40. package/dist/components/tis/ConfigurationDialog.d.ts +21 -0
  41. package/dist/components/tis/ConfigurationDialog.d.ts.map +1 -0
  42. package/dist/components/tis/ConfigurationDialog.js +1 -0
  43. package/dist/components/tis/ResultHistoryTable.js +1 -1
  44. package/dist/components/tis/TestDataView.d.ts +27 -0
  45. package/dist/components/tis/TestDataView.d.ts.map +1 -1
  46. package/dist/components/tis/TestDataView.js +1 -1
  47. package/dist/components/tis/TestSetupForm.d.ts +37 -0
  48. package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
  49. package/dist/components/tis/TestSetupForm.js +1 -1
  50. package/dist/components/tis/TisProvider.d.ts +25 -0
  51. package/dist/components/tis/TisProvider.d.ts.map +1 -1
  52. package/dist/components/tis/TisProvider.js +1 -1
  53. package/dist/components/tis-editor/TisConfigEditor.css +20 -0
  54. package/dist/components/tis-editor/editor/ConfigurationsEditor.d.ts +19 -0
  55. package/dist/components/tis-editor/editor/ConfigurationsEditor.d.ts.map +1 -0
  56. package/dist/components/tis-editor/editor/ConfigurationsEditor.js +1 -0
  57. package/dist/components/tis-editor/editor/MethodFormEditor.d.ts.map +1 -1
  58. package/dist/components/tis-editor/editor/MethodFormEditor.js +1 -1
  59. package/dist/components/tis-editor/types.d.ts +13 -0
  60. package/dist/components/tis-editor/types.d.ts.map +1 -1
  61. package/dist/components/tis-editor/validation.d.ts.map +1 -1
  62. package/dist/components/tis-editor/validation.js +1 -1
  63. package/dist/themes/adc-dark/blue/theme.css +3 -2
  64. package/dist/themes/adc-dark/blue/theme.css.map +1 -1
  65. package/package.json +2 -1
  66. package/src/assets/JogXNeg.tsx +30 -0
  67. package/src/assets/JogXPos.tsx +30 -0
  68. package/src/assets/JogYNeg.tsx +30 -0
  69. package/src/assets/JogYPos.tsx +30 -0
  70. package/src/assets/JogZNeg.tsx +30 -0
  71. package/src/assets/JogZPos.tsx +30 -0
  72. package/src/assets/Off.tsx +14 -0
  73. package/src/assets/On.tsx +26 -0
  74. package/src/assets/index.ts +6 -0
  75. package/src/assets/svg/off.svg +2 -0
  76. package/src/assets/svg/on.svg +11 -0
  77. package/src/components/JogPanel.tsx +18 -28
  78. package/src/components/ams/AssetDetailView.tsx +1 -1
  79. package/src/components/ams/AssetEditDialog.tsx +25 -10
  80. package/src/components/ams/AssetRegistryTable.css +12 -0
  81. package/src/components/ams/AssetRegistryTable.tsx +15 -4
  82. package/src/components/tis/ConfigurationDialog.tsx +128 -0
  83. package/src/components/tis/ResultHistoryTable.tsx +2 -2
  84. package/src/components/tis/TestDataView.tsx +83 -1
  85. package/src/components/tis/TestSetupForm.tsx +167 -10
  86. package/src/components/tis/TisProvider.tsx +53 -0
  87. package/src/components/tis-editor/TisConfigEditor.css +20 -0
  88. package/src/components/tis-editor/editor/ConfigurationsEditor.tsx +242 -0
  89. package/src/components/tis-editor/editor/MethodFormEditor.tsx +4 -0
  90. package/src/components/tis-editor/types.ts +14 -0
  91. package/src/components/tis-editor/validation.ts +29 -0
  92. package/src/themes/adc-dark/_extensions.scss +1 -0
  93. package/src/themes/theme-base/components/panel/_fieldset.scss +2 -2
@@ -19,6 +19,7 @@ import { Distance, JogShort, JogMedium, JogLong } from '../assets';
19
19
  import { Speed, SpeedSlow, SpeedMedium, SpeedFast } from '../assets';
20
20
  import { RotationCcwA, RotationCcwB, RotationCcwC } from '../assets';
21
21
  import { RotationCwA, RotationCwB, RotationCwC } from '../assets';
22
+ import { JogXPos, JogXNeg, JogYPos, JogYNeg, JogZPos, JogZNeg } from '../assets';
22
23
 
23
24
  /**
24
25
  * Enumerates the source button or action when an event occurs.
@@ -125,9 +126,9 @@ interface JogPanelState {
125
126
  * A default jog button configuration for linear 3D motion.
126
127
  */
127
128
  export const DefaultLinearJogButtons : (JogPanelButtonDefinition | undefined)[][] = [
128
- [{ icon: "pi pi-arrow-up-left", action: JogPanelAction.yPositive, alt: "Y Positive" }, { icon: "pi pi-arrow-up", action: JogPanelAction.zPositive, alt: "Z Positive" }, undefined],
129
+ [undefined, { icon: "pi pi-arrow-up", action: JogPanelAction.zPositive, alt: "Z Positive" }, { icon: "pi pi-arrow-down-left", action: JogPanelAction.yNegative, alt: "Y Negative" }],
129
130
  [{ icon: "pi pi-arrow-left", action: JogPanelAction.xNegative, alt: "X Negative" }, undefined, { icon: "pi pi-arrow-right", action: JogPanelAction.xPositive, alt: "X Positive" }],
130
- [undefined, { icon: "pi pi-arrow-down", action: JogPanelAction.zNegative, alt: "Z Negative" }, { icon: "pi pi-arrow-down-right", action: JogPanelAction.yNegative, alt: "Y Negative" }],
131
+ [{ icon: "pi pi-arrow-up-right", action: JogPanelAction.yPositive, alt: "Y Positive" }, { icon: "pi pi-arrow-down", action: JogPanelAction.zNegative, alt: "Z Negative" }, undefined],
131
132
  ];
132
133
 
133
134
  /**
@@ -156,9 +157,9 @@ export const DefaultRotationJogButtons : (JogPanelButtonDefinition | undefined)[
156
157
  * Example:
157
158
  * ```
158
159
  * export const kDefaultButtonDefinitions : (JogPanelButtonDefinition | undefined)[][] = [
159
- * [{ icon: "pi pi-arrow-up-left", action: JogPanelAction.yPositive, alt: "Y Positive" }, { icon: "pi pi-arrow-up", action: JogPanelAction.zPositive, alt: "Z Positive" }, undefined],
160
+ * [undefined, { icon: "pi pi-arrow-up", action: JogPanelAction.zPositive, alt: "Z Positive" }, { icon: "pi pi-arrow-down-left", action: JogPanelAction.yNegative, alt: "Y Negative" }],
160
161
  * [{ icon: "pi pi-arrow-left", action: JogPanelAction.xNegative, alt: "X Negative" }, undefined, { icon: "pi pi-arrow-right", action: JogPanelAction.xPositive, alt: "X Positive" }],
161
- * [undefined, { icon: "pi pi-arrow-down", action: JogPanelAction.zNegative, alt: "Z Negative" }, { icon: "pi pi-arrow-down-right", action: JogPanelAction.yNegative, alt: "Y Negative" }],
162
+ * [{ icon: "pi pi-arrow-up-right", action: JogPanelAction.yPositive, alt: "Y Positive" }, { icon: "pi pi-arrow-down", action: JogPanelAction.zNegative, alt: "Z Negative" }, undefined],
162
163
  * ];
163
164
  * ```
164
165
  *
@@ -249,22 +250,22 @@ export class JogPanel extends React.Component<JogPanelProps, JogPanelState> {
249
250
  return <i/>
250
251
  }
251
252
  else if (action == JogPanelAction.xNegative) {
252
- return <i className="pi pi-arrow-left" />
253
+ return <JogXNeg />
253
254
  }
254
255
  else if (action == JogPanelAction.xPositive) {
255
- return <i className="pi pi-arrow-right" />
256
+ return <JogXPos />
256
257
  }
257
258
  else if (action == JogPanelAction.yNegative) {
258
- return <i className="pi pi-arrow-up-left" />
259
+ return <JogYNeg />
259
260
  }
260
261
  else if (action == JogPanelAction.yPositive) {
261
- return <i className="pi pi-arrow-down-right" />
262
+ return <JogYPos />
262
263
  }
263
264
  else if (action == JogPanelAction.zNegative) {
264
- return <i className="pi pi-up" />
265
+ return <JogZNeg />
265
266
  }
266
267
  else if (action == JogPanelAction.zPositive) {
267
- return <i className="pi pi-down" />
268
+ return <JogZPos />
268
269
  }
269
270
  else if (action == JogPanelAction.aNegative) {
270
271
  return <RotationCcwA />
@@ -299,24 +300,13 @@ export class JogPanel extends React.Component<JogPanelProps, JogPanelState> {
299
300
 
300
301
  const action = this.props.buttonDefinitions[row][col]?.action;
301
302
 
302
- if (action !== undefined && action < JogPanelAction.aPositive ) {
303
- return <Button
304
- key={`${row}-${col}`}
305
- icon={this.props.buttonDefinitions[row][col]?.icon}
306
- tooltip={this.props.buttonDefinitions[row][col]?.alt}
307
- onClick={() => this.handleClicked(action)}
308
- className="button-item"
309
- />
310
- }
311
- else {
312
- return <Button
313
- key={`${row}-${col}`}
314
- tooltip={this.props.buttonDefinitions[row][col]?.alt}
315
- onClick={() => this.handleClicked(action)}
316
- className="button-item"
317
- icon={() => this.actionToIcon(action)}/>
318
- }
319
-
303
+ return <Button
304
+ key={`${row}-${col}`}
305
+ tooltip={this.props.buttonDefinitions[row][col]?.alt}
306
+ onClick={() => this.handleClicked(action)}
307
+ className="button-item"
308
+ icon={() => this.actionToIcon(action)}/>
309
+
320
310
  }
321
311
  else {
322
312
  return <Button icon="pi" key={`${row}-${col}`} disabled={true} className="button-item empty-slot" />
@@ -235,7 +235,7 @@ export const AssetDetailView: React.FC = () => {
235
235
  <Button label="Edit" icon="pi pi-pencil"
236
236
  outlined
237
237
  onClick={() => setEditDialogOpen(true)}
238
- tooltip="Edit role, nameplate, and per-axis values. Type, serial, and install date are immutable."
238
+ tooltip="Edit serial, role, nameplate, and per-axis values. Type and install date are immutable."
239
239
  tooltipOptions={{ position: 'left' }}
240
240
  />
241
241
  {asset.status === 'active' && (
@@ -9,12 +9,13 @@
9
9
  * `asset.current_calibration_id` is non-null. Posts via
10
10
  * `ams.update_calibration` (server enforces "current only"). Tab
11
11
  * is hidden entirely for assets that never had a calibration —
12
- * "+ Calibration" on <AssetDetailView> is the path to add one.
12
+ * "Calibration" on <AssetDetailView> is the path to add one.
13
13
  *
14
- * The server treats `asset_id`, `asset_type`, `serial`, and
15
- * `install_date` as immutable; the read-only header strip mirrors
16
- * that. Status stays out of this dialog because the Retire button on
17
- * <AssetDetailView> already owns that transition.
14
+ * The server treats `asset_id`, `asset_type`, and `install_date` as
15
+ * immutable; the read-only header strip mirrors that. `serial` is
16
+ * editable (free-form traceability metadata) so a mis-keyed serial can
17
+ * be corrected here. Status stays out of this dialog because the Retire
18
+ * button on <AssetDetailView> already owns that transition.
18
19
  *
19
20
  * Save commits the Asset tab first, then (if a calibration is loaded)
20
21
  * the Calibration tab. Either failure surfaces inline and stops; the
@@ -175,6 +176,7 @@ export const AssetEditDialog: React.FC<AssetEditDialogProps> = ({
175
176
  // effect below.
176
177
  const [roleSelection, setRoleSelection] = useState<string>('');
177
178
  const [location, setLocation] = useState<string>('');
179
+ const [serial, setSerial] = useState<string>('');
178
180
  const [customFields, setCustomFields] =
179
181
  useState<Record<string, string>>({});
180
182
  const [subLocationFields, setSubLocationFields] =
@@ -215,6 +217,9 @@ export const AssetEditDialog: React.FC<AssetEditDialogProps> = ({
215
217
  setSubmitting(false);
216
218
  setActiveTab(0);
217
219
 
220
+ // Serial: free-form, editable so operators can correct a typo.
221
+ setSerial(typeof asset.serial === 'string' ? asset.serial : '');
222
+
218
223
  // Role: pick the dropdown option when the asset's location
219
224
  // matches a declared role; otherwise route into ROLE_OTHER so
220
225
  // the operator can keep the current custom string.
@@ -365,6 +370,7 @@ export const AssetEditDialog: React.FC<AssetEditDialogProps> = ({
365
370
  const payload: any = {
366
371
  asset_id: asset.asset_id,
367
372
  location,
373
+ serial,
368
374
  custom,
369
375
  };
370
376
  if (subLocations) payload.sub_locations = subLocations;
@@ -481,10 +487,11 @@ export const AssetEditDialog: React.FC<AssetEditDialogProps> = ({
481
487
  </>
482
488
  }
483
489
  >
484
- {/* Read-only context strip — type, serial, install_date.
485
- The server treats these as immutable; surfacing them
486
- here keeps the operator oriented without inviting an
487
- edit that would silently no-op. */}
490
+ {/* Read-only context strip — type, install_date. The server
491
+ treats these as immutable; surfacing them here keeps the
492
+ operator oriented without inviting an edit that would
493
+ silently no-op. Serial moved into the editable Asset tab
494
+ below so operators can correct a mis-entered serial. */}
488
495
  <div style={{ display: 'grid',
489
496
  gridTemplateColumns: 'auto 1fr',
490
497
  gap: '0.25rem 1rem',
@@ -492,7 +499,6 @@ export const AssetEditDialog: React.FC<AssetEditDialogProps> = ({
492
499
  color: 'var(--text-secondary-color)',
493
500
  marginBottom: '0.75rem' }}>
494
501
  <span>Type</span> <span>{typeLabel}</span>
495
- <span>Serial</span> <span>{asset.serial || <em>(none)</em>}</span>
496
502
  {asset.install_date && (
497
503
  <>
498
504
  <span>Installed</span>
@@ -507,6 +513,15 @@ export const AssetEditDialog: React.FC<AssetEditDialogProps> = ({
507
513
  gridTemplateColumns: 'auto 1fr',
508
514
  gap: '0.5rem 1rem',
509
515
  alignItems: 'center' }}>
516
+ {/* Serial — free-form manufacturer metadata. Editable so
517
+ a mis-entered serial can be corrected after the fact. */}
518
+ <label>Serial</label>
519
+ <InputText
520
+ value={serial}
521
+ placeholder="(none)"
522
+ onChange={(e) => setSerial(e.target.value)}
523
+ />
524
+
510
525
  {/* Role field. Asset types referenced only by_id_field
511
526
  (no by_location asset_ref) come back with an empty
512
527
  role list — we hide the row entirely so the operator
@@ -0,0 +1,12 @@
1
+ /*
2
+ * Touch-friendly row sizing for the AMS asset list.
3
+ *
4
+ * The shop-floor HMI is a touchscreen and operators were mis-tapping
5
+ * the dense default DataTable rows. Adding vertical padding to each
6
+ * body cell enlarges the per-row touch target without changing the
7
+ * column layout or text size.
8
+ */
9
+ .ams-asset-table .p-datatable-tbody > tr > td {
10
+ padding-top: 0.9rem;
11
+ padding-bottom: 0.9rem;
12
+ }
@@ -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,
@@ -433,8 +434,11 @@ export const AssetRegistryTable: React.FC = () => {
433
434
  ? 'AMS not enabled in this project (no asset_types declared).'
434
435
  : 'No assets registered yet.'
435
436
  }
436
- size="small"
437
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"
438
442
  >
439
443
  <Column field="asset_id" header="Asset ID" />
440
444
  <Column field="asset_type" header="Type"
@@ -481,9 +485,16 @@ export const AssetRegistryTable: React.FC = () => {
481
485
  onChange={(e) => onAssetTypeChange(e.value)}
482
486
  placeholder="Choose asset type"
483
487
  />
484
- <label>Serial</label>
485
- <InputText value={addState.serial}
486
- 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
+ )}
487
498
 
488
499
  {/* Role field: only shown when this asset_type has at
489
500
  least one declared role in project.json. Asset
@@ -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.