@adcops/autocore-react 3.5.9 → 3.5.13

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 (76) hide show
  1. package/dist/components/index.d.ts +4 -0
  2. package/dist/components/index.d.ts.map +1 -1
  3. package/dist/components/index.js +1 -1
  4. package/dist/components/tis-editor/TisConfigEditor.css +11 -0
  5. package/dist/components/tis-editor/TisConfigEditor.d.ts.map +1 -1
  6. package/dist/components/tis-editor/TisConfigEditor.js +1 -1
  7. package/dist/components/tis-editor/editor/AuthoringContext.d.ts +37 -0
  8. package/dist/components/tis-editor/editor/AuthoringContext.d.ts.map +1 -0
  9. package/dist/components/tis-editor/editor/AuthoringContext.js +1 -0
  10. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts +11 -0
  11. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts.map +1 -1
  12. package/dist/components/tis-editor/editor/ChartViewDialog.js +1 -1
  13. package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts.map +1 -1
  14. package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -1
  15. package/dist/components/tis-editor/editor/RawDataEditor.d.ts.map +1 -1
  16. package/dist/components/tis-editor/editor/RawDataEditor.js +1 -1
  17. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +7 -0
  18. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -1
  19. package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -1
  20. package/dist/components/tis-editor/editor/ViewsEditor.d.ts.map +1 -1
  21. package/dist/components/tis-editor/editor/ViewsEditor.js +1 -1
  22. package/dist/components/varpick/VariableInput.d.ts +41 -0
  23. package/dist/components/varpick/VariableInput.d.ts.map +1 -0
  24. package/dist/components/varpick/VariableInput.js +1 -0
  25. package/dist/components/varpick/VariablePicker.d.ts +45 -0
  26. package/dist/components/varpick/VariablePicker.d.ts.map +1 -0
  27. package/dist/components/varpick/VariablePicker.js +1 -0
  28. package/dist/components/varpick/index.d.ts +16 -0
  29. package/dist/components/varpick/index.d.ts.map +1 -0
  30. package/dist/components/varpick/index.js +1 -0
  31. package/dist/components/varpick/tisCatalogs.d.ts +85 -0
  32. package/dist/components/varpick/tisCatalogs.d.ts.map +1 -0
  33. package/dist/components/varpick/tisCatalogs.js +1 -0
  34. package/dist/components/varpick/varpick.css +167 -0
  35. package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
  36. package/dist/core/AutoCoreTagContext.js +1 -1
  37. package/dist/core/AutoCoreTagTypes.d.ts +13 -0
  38. package/dist/core/AutoCoreTagTypes.d.ts.map +1 -1
  39. package/dist/hooks/index.d.ts +4 -0
  40. package/dist/hooks/index.d.ts.map +1 -1
  41. package/dist/hooks/index.js +1 -1
  42. package/dist/hooks/useGmVariables.d.ts +57 -0
  43. package/dist/hooks/useGmVariables.d.ts.map +1 -0
  44. package/dist/hooks/useGmVariables.js +1 -0
  45. package/dist/hooks/useSequenceProducers.d.ts +38 -0
  46. package/dist/hooks/useSequenceProducers.d.ts.map +1 -0
  47. package/dist/hooks/useSequenceProducers.js +1 -0
  48. package/package.json +5 -2
  49. package/src/components/index.ts +17 -0
  50. package/src/components/tis-editor/TisConfigEditor.css +11 -0
  51. package/src/components/tis-editor/TisConfigEditor.tsx +31 -0
  52. package/src/components/tis-editor/editor/AuthoringContext.tsx +43 -0
  53. package/src/components/tis-editor/editor/ChartViewDialog.tsx +24 -7
  54. package/src/components/tis-editor/editor/FieldArrayEditor.tsx +1 -0
  55. package/src/components/tis-editor/editor/RawDataEditor.tsx +7 -2
  56. package/src/components/tis-editor/editor/TestFieldDialog.tsx +66 -12
  57. package/src/components/tis-editor/editor/ViewsEditor.tsx +25 -0
  58. package/src/components/varpick/VariableInput.tsx +126 -0
  59. package/src/components/varpick/VariablePicker.tsx +168 -0
  60. package/src/components/varpick/index.ts +22 -0
  61. package/src/components/varpick/tisCatalogs.ts +235 -0
  62. package/src/components/varpick/varpick.css +167 -0
  63. package/src/core/AutoCoreTagContext.tsx +3 -1
  64. package/src/core/AutoCoreTagTypes.ts +14 -0
  65. package/src/hooks/index.ts +11 -0
  66. package/src/hooks/useGmVariables.ts +142 -0
  67. package/src/hooks/useSequenceProducers.ts +104 -0
  68. package/tools/tests/dist/TestFieldDialog-C4mKVh7q.js +32817 -0
  69. package/tools/tests/dist/autocore-react.css +257 -0
  70. package/tools/tests/dist/tis-editor.entry.js +8431 -40567
  71. package/tools/tests/dist/varpick.entry.js +113 -0
  72. package/tools/tests/live-server.probe.mjs +172 -0
  73. package/tools/tests/varpick.entry.tsx +81 -0
  74. package/tools/tests/varpick.test.mjs +208 -0
  75. package/tools/tests/vite.config.mjs +7 -2
  76. package/tools/tests/dist/tis-editor.entry.css +0 -90
@@ -8,6 +8,8 @@ import { FormRow } from '../../forms/FormRow';
8
8
  import { useEditDraft } from '../../forms/useEditDraft';
9
9
  import { DEFAULT_X_DECIMALS } from '../../tis/TestDataView';
10
10
  import type { ChartAxis, ChartSeries, ChartView } from '../types';
11
+ import { VariableInput } from '../../varpick';
12
+ import type { Catalog } from '@adcops/autocore-varpick';
11
13
 
12
14
  /** Upper bound on the editor's X-decimals spinner. Past ~6 the tick labels
13
15
  * are wider than the gap between them, which is the problem this setting
@@ -31,6 +33,16 @@ export interface ChartViewDialogProps {
31
33
  onSave: (viewId: string, view: ChartView) => void;
32
34
  /** Known field/column names; offered as datalist suggestions. */
33
35
  knownKeys: string[];
36
+ /**
37
+ * The same names, grouped and described, for the picker button.
38
+ *
39
+ * Takes the view TYPE because it decides which list is the likely answer:
40
+ * a `raw_trace` plots trace columns, a `cycle_scatter` plots cycle fields.
41
+ * A datalist alone was not enough — it only appears once you start typing,
42
+ * which is no help when you do not know what to type, and it does not open
43
+ * at all on a touchscreen.
44
+ */
45
+ fieldCatalog?: (viewType: string) => Catalog;
34
46
  /** Other view ids already in use — for uniqueness validation. */
35
47
  siblingIds: string[];
36
48
  }
@@ -42,7 +54,7 @@ const blank = (): ChartView => ({
42
54
  });
43
55
 
44
56
  export const ChartViewDialog: React.FC<ChartViewDialogProps> = ({
45
- visible, initial, onCancel, onSave, knownKeys, siblingIds,
57
+ visible, initial, onCancel, onSave, knownKeys, siblingIds, fieldCatalog,
46
58
  }) => {
47
59
  // One draft covering both edited values (the id is editable too), seeded
48
60
  // once per open. Keyed on the view id rather than on `initial` — the
@@ -124,10 +136,13 @@ export const ChartViewDialog: React.FC<ChartViewDialogProps> = ({
124
136
  />
125
137
  </FormRow>
126
138
  <FormRow label={isRawTrace ? 'X column' : 'X field'} required>
127
- <InputText
139
+ <VariableInput
128
140
  value={(draft.x[axisKey] as string) ?? ''}
129
- onChange={(e) => setAxis({ [axisKey]: e.target.value } as ChartAxis)}
130
- list="known-keys"
141
+ onChange={(v) => setAxis({ [axisKey]: v } as ChartAxis)}
142
+ catalog={() => fieldCatalog?.(draft.type) ?? []}
143
+ ariaLabel="Choose the X axis column"
144
+ pickerTitle="X axis"
145
+ pickerHint="A chart naming a column that does not exist draws an empty plot with no error, which reads as “the machine recorded nothing”."
131
146
  />
132
147
  </FormRow>
133
148
  <FormRow label="X label">
@@ -173,11 +188,13 @@ export const ChartViewDialog: React.FC<ChartViewDialogProps> = ({
173
188
  alignItems: 'center',
174
189
  }}
175
190
  >
176
- <InputText
191
+ <VariableInput
177
192
  placeholder={isRawTrace ? 'column' : 'field'}
178
193
  value={(s[axisKey] as string) ?? ''}
179
- onChange={(e) => setSeries(i, { [axisKey]: e.target.value } as ChartSeries)}
180
- list="known-keys"
194
+ onChange={(v) => setSeries(i, { [axisKey]: v } as ChartSeries)}
195
+ catalog={() => fieldCatalog?.(draft.type) ?? []}
196
+ ariaLabel={`Choose the column for series ${i + 1}`}
197
+ pickerTitle="Y series"
181
198
  />
182
199
  <InputText
183
200
  placeholder="label"
@@ -148,6 +148,7 @@ export const FieldArrayEditor: React.FC<FieldArrayEditorProps> = ({ arrayKey, me
148
148
  siblingNames={fields.map(f => f.name)}
149
149
  isResultsField={arrayKey === 'results_fields'}
150
150
  isCycleConfigField={arrayKey === 'cycle_config_fields'}
151
+ isCycleField={arrayKey === 'cycle_fields'}
151
152
  cycleFieldNames={(method.cycle_fields ?? []).map(f => f.name).filter(Boolean)}
152
153
  resultsFieldNames={(method.results_fields ?? []).map(f => f.name).filter(Boolean)}
153
154
  onCancel={() => setDialogOpen(false)}
@@ -4,6 +4,7 @@ import { Checkbox } from 'primereact/checkbox';
4
4
  import { FormSection } from '../../forms/FormSection';
5
5
  import { FormRow } from '../../forms/FormRow';
6
6
  import type { RawColumn, RawDataShape, TestMethod } from '../types';
7
+ import { VariableInput, rawSourceCatalog } from '../../varpick';
7
8
 
8
9
  export interface RawDataEditorProps {
9
10
  method: TestMethod;
@@ -89,10 +90,14 @@ export const RawDataEditor: React.FC<RawDataEditorProps> = ({ method, onChange }
89
90
  defaultValue={name}
90
91
  onBlur={(e) => renameColumn(name, e.target.value.trim())}
91
92
  />
92
- <InputText
93
+ <VariableInput
93
94
  placeholder="source (time / ni.<daq>.channels.<name> / derived)"
94
95
  value={col.source}
95
- onChange={(e) => updateColumn(name, { ...col, source: e.target.value })}
96
+ onChange={(v) => updateColumn(name, { ...col, source: v })}
97
+ catalog={() => rawSourceCatalog()}
98
+ ariaLabel={`Choose the source for column ${name}`}
99
+ pickerTitle="Column source"
100
+ pickerHint="A DAQ channel is named ni.<daq>.channels.<name> and is not listed here — the editor has no channel catalog, so type it."
96
101
  />
97
102
  <InputText
98
103
  placeholder="formula (only when source=derived)"
@@ -13,6 +13,9 @@ import type { TestField, AggFn, AggFrom, AggScope, AggregateSpec } from '../type
13
13
  // form so the editor and the runtime can never disagree on what a bound is.
14
14
  import { boundText, parseBound, boundInvalid } from '../../tis/TestFieldRow';
15
15
  import { AutoCoreTagContext } from '../../../core/AutoCoreTagContext';
16
+ import { VariableInput } from '../../varpick';
17
+ import { boundCatalog, configSourceCatalog, cycleSourceCatalog } from '../../varpick';
18
+ import { useAuthoringCatalogs } from './AuthoringContext';
16
19
 
17
20
 
18
21
  const FIELD_TYPES: { label: string; value: string }[] = [
@@ -74,15 +77,25 @@ export interface TestFieldDialogProps {
74
77
  /** Candidate target field names for an aggregate's `of`, by source. */
75
78
  cycleFieldNames?: string[];
76
79
  resultsFieldNames?: string[];
80
+ /**
81
+ * True when this field belongs to `cycle_fields`. Only there does Source
82
+ * have two possible producers — a GM tag read at record time, or the
83
+ * sequence storing a value under this column's NAME — so only there is the
84
+ * picker a producer picker rather than a variable list.
85
+ */
86
+ isCycleField?: boolean;
77
87
  }
78
88
 
79
89
  const blank: TestField = { name: '', type: 'f32' };
80
90
 
81
91
  export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
82
92
  visible, initial, onCancel, onSave, siblingNames,
83
- isResultsField = false, isCycleConfigField = false,
93
+ isResultsField = false, isCycleConfigField = false, isCycleField = false,
84
94
  cycleFieldNames = [], resultsFieldNames = [],
85
95
  }) => {
96
+ // Supplied by the editor above; see AuthoringContext for why this is not
97
+ // four levels of props.
98
+ const { variables, producedNames } = useAuthoringCatalogs();
86
99
  // The field under edit plus its free-text Default, as one draft seeded
87
100
  // once per open. `default` is edited as free text so a numeric literal, a
88
101
  // string, or an `${fqdn}` token can all be entered in one field; it's
@@ -120,6 +133,24 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
120
133
  [scales],
121
134
  );
122
135
 
136
+ // What the pickers offer. Units come from the machine's resolved scales, so
137
+ // an entry is labelled in the operator's active system rather than in
138
+ // whatever the variable's backend unit happens to be.
139
+ const catalogCtx = useMemo(
140
+ () => ({
141
+ variables,
142
+ unitFor: (q: string) => scales?.[q]?.label,
143
+ }),
144
+ [variables, scales],
145
+ );
146
+
147
+ // A cycle field's Source is the one that needs BOTH producers offered; every
148
+ // other array only ever binds a GM tag the setup form writes.
149
+ const sourceCatalog = () =>
150
+ isCycleField
151
+ ? cycleSourceCatalog(catalogCtx, { producedNames, fieldName: draft.name })
152
+ : configSourceCatalog(catalogCtx);
153
+
123
154
  // Turn the free-text Default into its stored form: an `${fqdn}` token and
124
155
  // other non-numerics stay strings; numeric/bool literals are stored typed.
125
156
  const coerceDefault = (s: string): unknown => {
@@ -275,10 +306,21 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
275
306
  />
276
307
  </FormRow>
277
308
  )}
278
- <FormRow label="Source" hint="Optional gm.* variable to bind this field to.">
279
- <InputText
309
+ <FormRow
310
+ label="Source"
311
+ hint={isCycleField
312
+ ? 'Where this column\u2019s value comes from. Leave empty when the sequence records it \u2014 a store_value step is matched to this column by NAME, not by a binding.'
313
+ : 'Optional gm.* variable this field reads and writes.'}
314
+ >
315
+ <VariableInput
280
316
  value={draft.source ?? ''}
281
- onChange={(e) => setDraft({ ...draft, source: e.target.value || undefined })}
317
+ onChange={(v) => setDraft({ ...draft, source: v || undefined })}
318
+ catalog={sourceCatalog}
319
+ ariaLabel="Choose a source for this field"
320
+ pickerTitle="Where does this value come from?"
321
+ pickerHint={isCycleField
322
+ ? 'A column filled by the sequence needs no Source \u2014 choosing one below clears it, which is correct.'
323
+ : 'The Test Setup form writes the operator\u2019s entry to this variable, so only variables a UI may write are listed.'}
282
324
  placeholder="gm.<variable_name>"
283
325
  />
284
326
  </FormRow>
@@ -297,10 +339,14 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
297
339
  label="Min"
298
340
  hint="Optional lower bound the operator can enter (display units). Blank = no minimum. Or bind it to the machine with an FQDN token like ${gm.press_force_min_n} — for a bound that moves, e.g. a load-cell force ceiling."
299
341
  >
300
- <InputText
342
+ <VariableInput
301
343
  value={boundText(draft.min)}
302
- onChange={(e) => setDraft({ ...draft, min: parseBound(e.target.value) })}
303
- className={boundInvalid(draft.min) ? 'p-invalid' : ''}
344
+ onChange={(v) => setDraft({ ...draft, min: parseBound(v) })}
345
+ invalid={boundInvalid(draft.min)}
346
+ catalog={() => boundCatalog(catalogCtx)}
347
+ ariaLabel="Bind Min to a machine variable"
348
+ pickerTitle="Bind Min to a machine value"
349
+ pickerHint="The bound then follows the machine — a load-cell ceiling that changes with the fitted cell, rather than a number that goes stale."
304
350
  placeholder="number or ${gm.…}"
305
351
  />
306
352
  </FormRow>
@@ -308,17 +354,25 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
308
354
  label="Max"
309
355
  hint="Optional upper bound the operator can enter (display units). Blank = no maximum. Or bind it to the machine with an FQDN token like ${gm.press_force_max_n}."
310
356
  >
311
- <InputText
357
+ <VariableInput
312
358
  value={boundText(draft.max)}
313
- onChange={(e) => setDraft({ ...draft, max: parseBound(e.target.value) })}
314
- className={boundInvalid(draft.max) ? 'p-invalid' : ''}
359
+ onChange={(v) => setDraft({ ...draft, max: parseBound(v) })}
360
+ invalid={boundInvalid(draft.max)}
361
+ catalog={() => boundCatalog(catalogCtx)}
362
+ ariaLabel="Bind Max to a machine variable"
363
+ pickerTitle="Bind Max to a machine value"
364
+ pickerHint="The bound then follows the machine — a load-cell ceiling that changes with the fitted cell, rather than a number that goes stale."
315
365
  placeholder="number or ${gm.…}"
316
366
  />
317
367
  </FormRow>
318
368
  <FormRow label="Default" hint="Applied every time the method loads (operator can override). A literal (display units) like 5, or an FQDN token like ${gm.safe_speed} that snapshots that value on load.">
319
- <InputText
369
+ <VariableInput
320
370
  value={defaultText}
321
- onChange={(e) => setDefaultText(e.target.value)}
371
+ onChange={setDefaultText}
372
+ catalog={() => boundCatalog(catalogCtx)}
373
+ ariaLabel="Use a machine variable as the default"
374
+ pickerTitle="Default from a machine value"
375
+ pickerHint="A token default SNAPSHOTS the value when the method loads. It is not a live binding — that is what Source is."
322
376
  placeholder="e.g. 5 or ${gm.safe_speed}"
323
377
  />
324
378
  </FormRow>
@@ -6,6 +6,8 @@ import { FormSection } from '../../forms/FormSection';
6
6
  import { moveView, orderedViews } from '../../tis/chartViews';
7
7
  import { ChartViewDialog } from './ChartViewDialog';
8
8
  import type { ChartView, TestField, TestMethod } from '../types';
9
+ import { chartFieldCatalog } from '../../varpick';
10
+ import type { Catalog } from '@adcops/autocore-varpick';
9
11
 
10
12
  export interface ViewsEditorProps {
11
13
  method: TestMethod;
@@ -47,6 +49,28 @@ export const ViewsEditor: React.FC<ViewsEditorProps> = ({ method, onChange }) =>
47
49
  'timestamp',
48
50
  ];
49
51
 
52
+ // The same names, grouped and described, for the picker button. Built from
53
+ // `method` rather than from `knownKeys` so each group knows what it IS —
54
+ // a flat list cannot say that `cycle_index` is per-cycle and `operator` is
55
+ // per-test, which is the distinction that makes a chart plot a straight
56
+ // line by accident.
57
+ const fieldCatalog = (viewType: string): Catalog => chartFieldCatalog({
58
+ viewType,
59
+ cycleFields: [
60
+ ...((method.cycle_config_fields ?? []) as TestField[]).map(f => f.name),
61
+ ...((method.cycle_fields ?? []) as TestField[]).map(f => f.name),
62
+ // Written onto every row by the server, declared nowhere.
63
+ 'cycle_index',
64
+ 'timestamp',
65
+ ].filter(Boolean),
66
+ resultsFields: ((method.results_fields ?? []) as TestField[]).map(f => f.name).filter(Boolean),
67
+ rawColumns: Object.keys((method.raw_data as any)?.columns ?? {}),
68
+ testFields: [
69
+ ...((method.project_fields ?? []) as TestField[]).map(f => f.name),
70
+ ...((method.config_fields ?? []) as TestField[]).map(f => f.name),
71
+ ].filter(Boolean),
72
+ });
73
+
50
74
  const openNew = () => { setEditingId(null); setDialogOpen(true); };
51
75
  const openEdit = (id: string) => { setEditingId(id); setDialogOpen(true); };
52
76
 
@@ -125,6 +149,7 @@ export const ViewsEditor: React.FC<ViewsEditorProps> = ({ method, onChange }) =>
125
149
  visible={dialogOpen}
126
150
  initial={editingId ? { viewId: editingId, view: views[editingId] } : null}
127
151
  knownKeys={knownKeys}
152
+ fieldCatalog={fieldCatalog}
128
153
  siblingIds={Object.keys(views)}
129
154
  onCancel={() => setDialogOpen(false)}
130
155
  onSave={handleSave}
@@ -0,0 +1,126 @@
1
+ /**
2
+ * VariableInput — a text input with a picker button.
3
+ *
4
+ * A drop-in replacement for the `<InputText>` inside a `FormRow`, so adding a
5
+ * picker to a field is a one-line change and the field keeps its label, hint and
6
+ * error handling.
7
+ *
8
+ * Typing still works exactly as before. That is the point: an author who knows
9
+ * the name types it, and an author who does not gets a list instead of a guess.
10
+ */
11
+
12
+ import * as React from 'react';
13
+ import { Button } from 'primereact/button';
14
+ import { InputText } from 'primereact/inputtext';
15
+ import {
16
+ applyInsert,
17
+ queryFromCaret,
18
+ type Catalog,
19
+ type InsertMode,
20
+ type VariableEntry,
21
+ } from '@adcops/autocore-varpick';
22
+ import { VariablePicker } from './VariablePicker';
23
+ import './varpick.css';
24
+
25
+ export interface VariableInputProps {
26
+ value: string;
27
+ onChange: (value: string) => void;
28
+ /**
29
+ * Built when the dialog opens, not on every render — a machine's variable
30
+ * table is ~100 rows and this sits inside a dialog that re-renders on live
31
+ * tag traffic.
32
+ */
33
+ catalog: () => Catalog;
34
+ /**
35
+ * `replace` when the whole field IS one binding (a `source`, a bound);
36
+ * `caret` when the field is an expression and the pick is one term of it.
37
+ * Defaults to `replace`, which is what every TIS field is.
38
+ */
39
+ mode?: InsertMode;
40
+ /** Dialog heading and the line under it. */
41
+ pickerTitle?: string;
42
+ pickerHint?: React.ReactNode;
43
+ /** Accessible name for the button — it is icon-only, so this is required. */
44
+ ariaLabel: string;
45
+ placeholder?: string;
46
+ className?: string;
47
+ disabled?: boolean;
48
+ invalid?: boolean;
49
+ }
50
+
51
+ export const VariableInput: React.FC<VariableInputProps> = ({
52
+ value, onChange, catalog, mode = 'replace', pickerTitle, pickerHint,
53
+ ariaLabel, placeholder, className, disabled, invalid,
54
+ }) => {
55
+ const inputRef = React.useRef<HTMLInputElement>(null);
56
+ const caret = React.useRef<{ start: number; end: number }>({ start: 0, end: 0 });
57
+ const [open, setOpen] = React.useState(false);
58
+ const [built, setBuilt] = React.useState<Catalog>([]);
59
+
60
+ // Captured on every selection change rather than read when the button is
61
+ // clicked: clicking blurs the input, and a blur can reset `selectionStart`
62
+ // to the end of the value — which would silently turn "replace the word I
63
+ // am typing" into "append to it".
64
+ const rememberCaret = () => {
65
+ const el = inputRef.current;
66
+ if (!el) return;
67
+ caret.current = {
68
+ start: el.selectionStart ?? el.value.length,
69
+ end: el.selectionEnd ?? el.value.length,
70
+ };
71
+ };
72
+
73
+ const pick = (entry: VariableEntry) => {
74
+ const { start, end } = caret.current;
75
+ const next = applyInsert(value, entry, mode, start, end);
76
+ onChange(next.value);
77
+ setOpen(false);
78
+ requestAnimationFrame(() => {
79
+ const el = inputRef.current;
80
+ if (!el) return;
81
+ el.focus();
82
+ el.setSelectionRange(next.caret, next.caret);
83
+ caret.current = { start: next.caret, end: next.caret };
84
+ });
85
+ };
86
+
87
+ return (
88
+ <div className="ac-varpick__input">
89
+ <InputText
90
+ ref={inputRef}
91
+ value={value}
92
+ placeholder={placeholder}
93
+ disabled={disabled}
94
+ className={`${className ?? ''}${invalid ? ' p-invalid' : ''}`}
95
+ onChange={(e) => onChange(e.target.value)}
96
+ onSelect={rememberCaret}
97
+ onFocus={rememberCaret}
98
+ />
99
+ <Button
100
+ type="button"
101
+ icon="pi pi-list"
102
+ aria-label={ariaLabel}
103
+ disabled={disabled}
104
+ className="p-button-outlined"
105
+ onClick={() => { setBuilt(catalog()); setOpen(true); }}
106
+ />
107
+ {open && (
108
+ <VariablePicker
109
+ visible
110
+ catalog={built}
111
+ title={pickerTitle}
112
+ hint={pickerHint}
113
+ initialQuery={
114
+ mode === 'caret'
115
+ ? queryFromCaret(value, caret.current.start, caret.current.end)
116
+ : ''
117
+ }
118
+ onCancel={() => setOpen(false)}
119
+ onPick={pick}
120
+ />
121
+ )}
122
+ </div>
123
+ );
124
+ };
125
+
126
+ export default VariableInput;
@@ -0,0 +1,168 @@
1
+ /**
2
+ * VariablePicker — a searchable, described list of the machine's variables.
3
+ *
4
+ * ## Why
5
+ *
6
+ * Every place the TIS method editor asks an author to name a machine value was
7
+ * a bare text box: a field's `Source` hinting `gm.<variable_name>`, a bound
8
+ * hinting `${gm.…}`. Nobody can type a variable out of a program they cannot
9
+ * see, and a wrong name fails SILENTLY in both directions — a `source` that
10
+ * names nothing never binds, a bound that resolves to nothing is simply
11
+ * unbounded.
12
+ *
13
+ * Typing is still allowed. The button is additive: it opens the list, shows what
14
+ * each variable MEANS, and writes the right spelling for the field it came from.
15
+ *
16
+ * ## Rules honoured here
17
+ *
18
+ * - **No tooltips, no `title`.** Half the installed base is a touchscreen with
19
+ * no hover, so the description is on the row, not behind it.
20
+ * - **Pick-then-Insert.** A stray tap in a list must not rewrite a method.
21
+ * - **A disabled entry is shown with its reason**, never omitted — hiding it
22
+ * answers "where is my variable?" with the same silence as a text box.
23
+ *
24
+ * The dialog's own state is deliberately trivial (a query and a selection) and
25
+ * it is unmounted between openings, so there is no draft here to reseed — see
26
+ * `useEditDraft` for the fields that DO have one.
27
+ */
28
+
29
+ import * as React from 'react';
30
+ import { Dialog } from 'primereact/dialog';
31
+ import { Button } from 'primereact/button';
32
+ import { InputText } from 'primereact/inputtext';
33
+ import {
34
+ firstChoosable,
35
+ searchCatalog,
36
+ type Catalog,
37
+ type VariableEntry,
38
+ } from '@adcops/autocore-varpick';
39
+ import './varpick.css';
40
+
41
+ export interface VariablePickerProps {
42
+ visible: boolean;
43
+ catalog: Catalog;
44
+ /** Dialog heading. */
45
+ title?: string;
46
+ /** One line saying what picking will do to the field. */
47
+ hint?: React.ReactNode;
48
+ /** Seeds the search box — usually the word the caret was sitting in. */
49
+ initialQuery?: string;
50
+ onCancel: () => void;
51
+ onPick: (entry: VariableEntry) => void;
52
+ }
53
+
54
+ export const VariablePicker: React.FC<VariablePickerProps> = ({
55
+ visible, catalog, title, hint, initialQuery, onCancel, onPick,
56
+ }) => {
57
+ const [query, setQuery] = React.useState(initialQuery ?? '');
58
+ const [selected, setSelected] = React.useState<string | null>(null);
59
+
60
+ // Reset on each opening. The dialog is cheap and stateless by design; the
61
+ // alternative — remembering the last search — means opening the picker on a
62
+ // different field shows a list filtered for the previous one.
63
+ React.useEffect(() => {
64
+ if (visible) setQuery(initialQuery ?? '');
65
+ }, [visible, initialQuery]);
66
+
67
+ const filtered = React.useMemo(() => searchCatalog(catalog, query), [catalog, query]);
68
+
69
+ // The highlight follows the search, so Enter always takes the top hit
70
+ // rather than a stale selection that has scrolled out of view.
71
+ React.useEffect(() => {
72
+ setSelected(firstChoosable(filtered)?.id ?? null);
73
+ }, [filtered]);
74
+
75
+ const entryById = (id: string | null): VariableEntry | undefined =>
76
+ id === null ? undefined : filtered.flatMap(g => g.entries).find(e => e.id === id);
77
+
78
+ const confirm = (entry?: VariableEntry) => {
79
+ const chosen = entry ?? entryById(selected);
80
+ if (!chosen || chosen.disabled) return;
81
+ onPick(chosen);
82
+ };
83
+
84
+ const footer = (
85
+ <div className="ac-varpick__foot">
86
+ <Button label="Cancel" className="p-button-text" onClick={onCancel} />
87
+ <Button
88
+ label="Insert"
89
+ disabled={entryById(selected) === undefined}
90
+ onClick={() => confirm()}
91
+ />
92
+ </div>
93
+ );
94
+
95
+ return (
96
+ <Dialog
97
+ header={title ?? 'Insert a variable'}
98
+ visible={visible}
99
+ onHide={onCancel}
100
+ footer={footer}
101
+ style={{ width: '44rem', maxWidth: '95vw' }}
102
+ >
103
+ {hint && <div className="ac-varpick__hint">{hint}</div>}
104
+
105
+ <InputText
106
+ className="ac-varpick__search"
107
+ value={query}
108
+ autoFocus
109
+ placeholder="name, or what it means — try “load” or “EL3356”"
110
+ onChange={(e) => setQuery(e.target.value)}
111
+ onKeyDown={(e) => {
112
+ if (e.key === 'Enter') { e.preventDefault(); confirm(); }
113
+ }}
114
+ />
115
+
116
+ {filtered.length === 0 ? (
117
+ <div className="ac-varpick__empty">
118
+ {catalog.length === 0
119
+ ? 'Nothing to pick from. The machine published no variables — the server may be an older build without gm.get_variables.'
120
+ : `No variable matches “${query}”.`}
121
+ </div>
122
+ ) : (
123
+ <div className="ac-varpick__list">
124
+ {filtered.map(group => (
125
+ <div className="ac-varpick__group" key={group.id}>
126
+ <div className="ac-varpick__grouphead">
127
+ <span className="ac-varpick__grouptitle">{group.title}</span>
128
+ {group.hint && <span className="ac-varpick__grouphint">{group.hint}</span>}
129
+ </div>
130
+ {group.entries.map(entry => (
131
+ <button
132
+ type="button"
133
+ key={entry.id}
134
+ className={
135
+ 'ac-varpick__row'
136
+ + (entry.id === selected ? ' ac-varpick__row--on' : '')
137
+ + (entry.disabled ? ' ac-varpick__row--off' : '')
138
+ }
139
+ aria-pressed={entry.id === selected}
140
+ disabled={!!entry.disabled}
141
+ onClick={() => setSelected(entry.id)}
142
+ onDoubleClick={() => confirm(entry)}
143
+ >
144
+ <span className="ac-varpick__line">
145
+ <code className="ac-varpick__name">{entry.name}</code>
146
+ {entry.label && <span className="ac-varpick__label">{entry.label}</span>}
147
+ {entry.unit && <span className="ac-varpick__chip">{entry.unit}</span>}
148
+ {entry.type && <span className="ac-varpick__chip">{entry.type}</span>}
149
+ {entry.live !== undefined && (
150
+ <span className="ac-varpick__live">now {entry.live}</span>
151
+ )}
152
+ </span>
153
+ {entry.description && (
154
+ <span className="ac-varpick__desc">{entry.description}</span>
155
+ )}
156
+ {entry.disabled && <span className="ac-varpick__why">{entry.disabled}</span>}
157
+ {entry.note && <span className="ac-varpick__note">{entry.note}</span>}
158
+ </button>
159
+ ))}
160
+ </div>
161
+ ))}
162
+ </div>
163
+ )}
164
+ </Dialog>
165
+ );
166
+ };
167
+
168
+ export default VariablePicker;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The variable picker: a field with a button that opens a searchable, described
3
+ * list of the machine's variables.
4
+ *
5
+ * The model (catalog shape, search ranking, caret-aware insertion, the `${fqdn}`
6
+ * token grammar) lives in `@adcops/autocore-varpick`, which has no UI in it —
7
+ * the sequence editor renders the same model with a different widget layer, and
8
+ * neither package may import the other's.
9
+ */
10
+
11
+ export { VariablePicker } from './VariablePicker';
12
+ export type { VariablePickerProps } from './VariablePicker';
13
+ export { VariableInput } from './VariableInput';
14
+ export type { VariableInputProps } from './VariableInput';
15
+ export {
16
+ boundCatalog,
17
+ chartFieldCatalog,
18
+ configSourceCatalog,
19
+ cycleSourceCatalog,
20
+ rawSourceCatalog,
21
+ } from './tisCatalogs';
22
+ export type { TisCatalogContext } from './tisCatalogs';