@adcops/autocore-react 3.3.124 → 3.5.5

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 (77) hide show
  1. package/dist/components/UnitSystemSelector.d.ts +17 -0
  2. package/dist/components/UnitSystemSelector.d.ts.map +1 -0
  3. package/dist/components/UnitSystemSelector.js +1 -0
  4. package/dist/components/UnitsEditor.d.ts +11 -0
  5. package/dist/components/UnitsEditor.d.ts.map +1 -0
  6. package/dist/components/UnitsEditor.js +1 -0
  7. package/dist/components/ams/AmsProvider.d.ts +14 -0
  8. package/dist/components/ams/AmsProvider.d.ts.map +1 -1
  9. package/dist/components/ams/AmsProvider.js +1 -1
  10. package/dist/components/ams/AssetDetailView.d.ts.map +1 -1
  11. package/dist/components/ams/AssetDetailView.js +1 -1
  12. package/dist/components/ams/AssetNameplateGrid.d.ts +13 -0
  13. package/dist/components/ams/AssetNameplateGrid.d.ts.map +1 -0
  14. package/dist/components/ams/AssetNameplateGrid.js +1 -0
  15. package/dist/components/ams/InstalledAssetPicker.d.ts +35 -0
  16. package/dist/components/ams/InstalledAssetPicker.d.ts.map +1 -0
  17. package/dist/components/ams/InstalledAssetPicker.js +1 -0
  18. package/dist/components/ams/index.d.ts +5 -0
  19. package/dist/components/ams/index.d.ts.map +1 -1
  20. package/dist/components/ams/index.js +1 -1
  21. package/dist/components/ams/useInstalledAsset.d.ts +26 -0
  22. package/dist/components/ams/useInstalledAsset.d.ts.map +1 -0
  23. package/dist/components/ams/useInstalledAsset.js +1 -0
  24. package/dist/components/index.d.ts +4 -0
  25. package/dist/components/index.d.ts.map +1 -1
  26. package/dist/components/index.js +1 -1
  27. package/dist/components/tis/ResultHistoryTable.d.ts.map +1 -1
  28. package/dist/components/tis/ResultHistoryTable.js +1 -1
  29. package/dist/components/tis/ScienceTable.d.ts.map +1 -1
  30. package/dist/components/tis/ScienceTable.js +1 -1
  31. package/dist/components/tis/TestDataView.d.ts +16 -4
  32. package/dist/components/tis/TestDataView.d.ts.map +1 -1
  33. package/dist/components/tis/TestDataView.js +1 -1
  34. package/dist/components/tis/TestFieldRow.d.ts +88 -7
  35. package/dist/components/tis/TestFieldRow.d.ts.map +1 -1
  36. package/dist/components/tis/TestFieldRow.js +1 -1
  37. package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
  38. package/dist/components/tis/TestSetupForm.js +1 -1
  39. package/dist/components/tis/TisProvider.d.ts.map +1 -1
  40. package/dist/components/tis/TisProvider.js +1 -1
  41. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -1
  42. package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -1
  43. package/dist/components/tis-editor/types.d.ts +6 -2
  44. package/dist/components/tis-editor/types.d.ts.map +1 -1
  45. package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
  46. package/dist/core/AutoCoreTagContext.js +1 -1
  47. package/dist/core/AutoCoreTagTypes.d.ts +82 -7
  48. package/dist/core/AutoCoreTagTypes.d.ts.map +1 -1
  49. package/dist/core/formatScaled.d.ts +16 -0
  50. package/dist/core/formatScaled.d.ts.map +1 -0
  51. package/dist/core/formatScaled.js +1 -0
  52. package/dist/hooks/useAutoCoreTag.d.ts +11 -3
  53. package/dist/hooks/useAutoCoreTag.d.ts.map +1 -1
  54. package/dist/hooks/useAutoCoreTag.js +1 -1
  55. package/package.json +1 -1
  56. package/src/components/UnitSystemSelector.tsx +88 -0
  57. package/src/components/UnitsEditor.tsx +317 -0
  58. package/src/components/ams/AmsProvider.tsx +36 -1
  59. package/src/components/ams/AssetDetailView.tsx +2 -20
  60. package/src/components/ams/AssetNameplateGrid.tsx +62 -0
  61. package/src/components/ams/InstalledAssetPicker.tsx +209 -0
  62. package/src/components/ams/index.ts +7 -0
  63. package/src/components/ams/useInstalledAsset.ts +108 -0
  64. package/src/components/index.ts +4 -0
  65. package/src/components/tis/ResultHistoryTable.tsx +89 -6
  66. package/src/components/tis/ScienceTable.tsx +13 -0
  67. package/src/components/tis/TestDataView.tsx +153 -35
  68. package/src/components/tis/TestFieldRow.tsx +157 -19
  69. package/src/components/tis/TestSetupForm.tsx +35 -7
  70. package/src/components/tis/TisProvider.tsx +46 -3
  71. package/src/components/tis-editor/editor/TestFieldDialog.tsx +29 -18
  72. package/src/components/tis-editor/types.ts +6 -2
  73. package/src/core/AutoCoreTagContext.tsx +165 -16
  74. package/src/core/AutoCoreTagTypes.ts +91 -7
  75. package/src/core/formatScaled.ts +72 -0
  76. package/src/hooks/useAutoCoreTag.ts +51 -3
  77. package/todo.md +6 -0
@@ -147,17 +147,63 @@ export interface SubscriptionOptions {
147
147
  */
148
148
  args?: Record<string, unknown>;
149
149
  }
150
+ /** How a resolved scale's `precision` is interpreted. */
151
+ export type PrecisionMode = "decimals" | "significant";
150
152
  /**
151
- * Configuration for a named scale group.
152
- * Apply by setting `scale: "<name>"` on numeric tags.
153
+ * One system's display treatment of one quantity, as it appears in `units.json`.
154
+ * Mirrors `mechutil::units::SystemScale`.
155
+ */
156
+ export interface UnitSystemScale {
157
+ /** Display label ("mm", "lbf", "°C"). */
158
+ label: string;
159
+ /** `display = backend * scalar (+ offset)`. */
160
+ scalar: number;
161
+ /** Additive term — absolute quantities (temperature) only. */
162
+ offset?: number;
163
+ /** Digits, interpreted per `precision_mode`. */
164
+ precision?: number;
165
+ precision_mode?: PrecisionMode;
166
+ }
167
+ /** One quantity row of `units.json`. Mirrors `mechutil::units::Scale`. */
168
+ export interface UnitScale {
169
+ /** The unit GM/GNV actually holds on this machine. */
170
+ backend: string;
171
+ /** `"absolute"` takes the offset; `"delta"` (default) does not. */
172
+ kind?: "delta" | "absolute";
173
+ /** system name → display treatment. Flattened in the JSON. */
174
+ [system: string]: unknown;
175
+ }
176
+ /**
177
+ * The whole `units.json` document — the single definition of units for a
178
+ * machine. Fetched from `system.get_units`; see `autocore-server/UNITS_PLAN.md`.
179
+ */
180
+ export interface UnitsTable {
181
+ /** Named systems, in display order. Any names, not a fixed pair. */
182
+ systems: string[];
183
+ default_system: string;
184
+ /** quantity name → row. Quantities are DATA, not a fixed enum. */
185
+ scales: Record<string, UnitScale>;
186
+ }
187
+ /**
188
+ * A quantity resolved against the active system — what the UI actually renders
189
+ * with. Produced by the provider from `UnitsTable` + active system.
190
+ *
191
+ * `scale`/`label` keep their historical names so the provider's existing
192
+ * rescale-from-raw machinery is unchanged; `precision` is new and is what makes
193
+ * "too many decimals" configurable rather than hard-coded.
153
194
  */
154
195
  export interface ScaleConfig {
155
- /** Unique name for this scale group (e.g., "position", "load") */
196
+ /** Quantity name this resolves (e.g., "position", "force") */
156
197
  name: string;
157
198
  /** Current scale factor — incoming values ×= scale; outgoing values /= scale */
158
199
  scale: number;
159
200
  /** Units/label for display (e.g., "mm", "in", "lbs") */
160
201
  label: string;
202
+ /** Additive term for absolute quantities (temperature). Default 0. */
203
+ offset?: number;
204
+ /** Display digits. */
205
+ precision?: number;
206
+ precisionMode?: PrecisionMode;
161
207
  /** Optional description for debugging/documentation */
162
208
  description?: string;
163
209
  /**
@@ -200,10 +246,21 @@ export type TagConfig<K extends string = string, T extends PrimitiveKind = Primi
200
246
  /** Optional initial **raw** value before first server update */
201
247
  initialValue?: TagValueOf<T>;
202
248
  /**
203
- * Optional scale group for numeric tags.
204
- * If present, provider will:
205
- * - Multiply incoming numbers by the group's `scale`
206
- * - Divide outgoing numbers by the group's `scale`
249
+ * The QUANTITY this tag measures — a row name in `units.json`
250
+ * ("position", "force", "torque", …). If present, the provider will:
251
+ * - Multiply incoming numbers by the active system's `scalar` (+ offset)
252
+ * - Divide outgoing numbers back to backend units
253
+ * - Render them at the active system's `precision`
254
+ *
255
+ * Quantities are data, not an enum: a machine with a channel nobody
256
+ * anticipated just gets a new row in `units.json`.
257
+ */
258
+ quantity?: string;
259
+ /**
260
+ * @deprecated Renamed to {@link quantity} in 4.0, when hand-authored
261
+ * `acScales` was replaced by the server's `units.json`. Still read as a
262
+ * fallback so a project can be migrated without its HMI going dark, but it
263
+ * will be removed — regenerate with `acctl codegen-tags`.
207
264
  */
208
265
  scale?: string;
209
266
  /**
@@ -258,6 +315,24 @@ export type BindingOf<Spec extends readonly TagConfig[], K extends Spec[number][
258
315
  export interface BaseContextValue<VMap extends Record<string, any>> {
259
316
  /** Current app-visible tag values (already scaled/decoded). */
260
317
  values: Partial<VMap>;
318
+ /**
319
+ * The machine's units table (`units.json`), or `null` when the machine has
320
+ * none — in which case values render in backend units.
321
+ */
322
+ units?: UnitsTable | null;
323
+ /** The display system currently in effect (e.g. "Metric"). */
324
+ activeSystem?: string | null;
325
+ /**
326
+ * Switch the machine's display system. Persisted server-side and broadcast,
327
+ * so every connected client switches live with no reload.
328
+ */
329
+ setUnitSystem?: (system: string) => Promise<void>;
330
+ /**
331
+ * Format a BACKEND value for display in the active system, honouring that
332
+ * quantity's label precision. Rounding is a rendering step only — this never
333
+ * changes a stored value.
334
+ */
335
+ formatQuantity?: (quantity: string, backendValue: number) => string;
261
336
  /** Last raw (controller) values, as received (pre-scale, pre-codec). */
262
337
  rawValues: Record<string, unknown>;
263
338
  /** Whether initial reads are pending. */
@@ -1 +1 @@
1
- {"version":3,"file":"AutoCoreTagTypes.d.ts","sourceRoot":"","sources":["../../src/core/AutoCoreTagTypes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AutoCoreTagTypes.d.ts","sourceRoot":"","sources":["../../src/core/AutoCoreTagTypes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { PrecisionMode, ScaleConfig } from "./AutoCoreTagTypes";
2
+ /** Digits used when a scale declares no precision. */
3
+ export declare const DEFAULT_PRECISION = 3;
4
+ /**
5
+ * Format an already-converted (display-unit) value.
6
+ *
7
+ * Precision is a **cap, not padding**: a clean 0/1/2 axis stays "0", "1", "2"
8
+ * rather than becoming "0.000", which is what made fixed `toFixed` unusable for
9
+ * chart ticks.
10
+ */
11
+ export declare function formatValue(value: number, precision?: number, mode?: PrecisionMode): string;
12
+ /** Format a display-unit value using a resolved scale's precision settings. */
13
+ export declare function formatScaled(displayValue: number, scale?: ScaleConfig): string;
14
+ /** `"12.5 mm"` — value plus the active system's label, when there is one. */
15
+ export declare function formatScaledWithLabel(displayValue: number, scale?: ScaleConfig): string;
16
+ //# sourceMappingURL=formatScaled.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatScaled.d.ts","sourceRoot":"","sources":["../../src/core/formatScaled.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAErE,sDAAsD;AACtD,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAEnC;;;;;;GAMG;AACH,wBAAgB,WAAW,CACvB,KAAK,EAAE,MAAM,EACb,SAAS,GAAE,MAA0B,EACrC,IAAI,GAAE,aAA0B,GACjC,MAAM,CAwBR;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,MAAM,CAM9E;AAED,6EAA6E;AAC7E,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,MAAM,CAIvF"}
@@ -0,0 +1 @@
1
+ export const DEFAULT_PRECISION=3;export function formatValue(t,e=3,a="decimals"){if(!Number.isFinite(t))return String(t);let i;if("significant"===a){const a=Math.max(1,e);if(0===t)i="0";else{const e=Math.floor(Math.log10(Math.abs(t))),r=Math.max(0,a-1-e);i=t.toFixed(Math.min(r,100))}}else i=t.toFixed(Math.max(0,Math.min(e,100)));return i.includes(".")&&(i=i.replace(/0+$/,"").replace(/\.$/,""),""!==i&&"-"!==i||(i="0")),i}export function formatScaled(t,e){return formatValue(t,e?.precision??3,e?.precisionMode??"decimals")}export function formatScaledWithLabel(t,e){const a=formatScaled(t,e),i=e?.label?.trim();return i?`${a} ${i}`:a}
@@ -1,4 +1,4 @@
1
- import type { BaseContextValue, TagConfig, TagValueMap } from "../core/AutoCoreTagTypes";
1
+ import type { BaseContextValue, ScaleConfig, TagConfig, TagValueMap } from "../core/AutoCoreTagTypes";
2
2
  export declare function makeAutoCoreTagHooks<Spec extends readonly TagConfig[]>(Context: React.Context<BaseContextValue<TagValueMap<Spec>>>, tags: Spec): {
3
3
  readonly useAutoCoreTag: <K extends Spec[number]["tagName"]>(tagName: K) => {
4
4
  readonly value: TagValueMap<Spec>[K];
@@ -20,9 +20,17 @@ export declare function makeAutoCoreTagHooks<Spec extends readonly TagConfig[]>(
20
20
  readonly isLoading: boolean;
21
21
  };
22
22
  readonly useScales: () => {
23
- readonly scales: Record<string, import("../core/AutoCoreTagTypes").ScaleConfig>;
23
+ readonly scales: Record<string, ScaleConfig>;
24
24
  readonly updateScale: (scaleName: string, newScale: number, newLabel: string) => Promise<void>;
25
- readonly getScale: (scaleName: string) => import("../core/AutoCoreTagTypes").ScaleConfig;
25
+ readonly getScale: (scaleName: string) => ScaleConfig;
26
+ };
27
+ readonly useUnits: () => {
28
+ readonly units: import("../core/AutoCoreTagTypes").UnitsTable | null;
29
+ readonly activeSystem: string | null;
30
+ readonly systems: string[];
31
+ readonly setUnitSystem: ((system: string) => Promise<void>) | undefined;
32
+ readonly label: (quantity: string) => string;
33
+ readonly format: (quantity: string, backendValue: number) => string;
26
34
  };
27
35
  };
28
36
  //# sourceMappingURL=useAutoCoreTag.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useAutoCoreTag.d.ts","sourceRoot":"","sources":["../../src/hooks/useAutoCoreTag.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEzF,wBAAgB,oBAAoB,CAAC,IAAI,SAAS,SAAS,SAAS,EAAE,EAClE,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAC3D,IAAI,EAAE,IAAI;8BAYc,CAAC,2CAA4B,CAAC;;;;;;;;;+BA6B7B,CAAC,4CAA6B,SAAS,CAAC,EAAE;4BAO/C,CAAC;;kCAYyB,CAAC;;;iCAUpB,CAAC,YAAY,CAAC,GAAG,EAAE,QAAQ,CAAC,OAAO,mBAAM,CAAC,KAAK,CAAC,SAAQ,GAAG,EAAE;;;;;;;uCAW3C,MAAM;;EAKtD"}
1
+ {"version":3,"file":"useAutoCoreTag.d.ts","sourceRoot":"","sources":["../../src/hooks/useAutoCoreTag.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEtG,wBAAgB,oBAAoB,CAAC,IAAI,SAAS,SAAS,SAAS,EAAE,EAClE,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAC3D,IAAI,EAAE,IAAI;8BAYc,CAAC,2CAA4B,CAAC;;;;;;;;;+BA6B7B,CAAC,4CAA6B,SAAS,CAAC,EAAE;4BAO/C,CAAC;;kCAYyB,CAAC;;;iCAUpB,CAAC,YAAY,CAAC,GAAG,EAAE,QAAQ,CAAC,OAAO,mBAAM,CAAC,KAAK,CAAC,SAAQ,GAAG,EAAE;;;;;;;uCAmBpE,MAAM,KAAG,WAAW;;;;;;;mCAoBrB,MAAM;oCAIN,MAAM,gBAAgB,MAAM;;EAqBlD"}
@@ -1 +1 @@
1
- import{useCallback,useContext,useMemo}from"react";export function makeAutoCoreTagHooks(e,a){return{useAutoCoreTag:s=>{const{values:t,rawValues:u,isLoading:o,write:r,tap:l,press:n,release:c}=useContext(e),i=(e=>a.find(a=>a.tagName===e))(s);if(!i)throw new Error(`useAutoCoreTag: unknown tagName '${String(s)}'`);const C=useCallback(e=>r(s,e),[r,s]),g=useCallback(()=>l?.(s),[l,s]),m=useCallback(()=>n?.(s),[n,s]),w=useCallback(()=>c?.(s),[c,s]);return{value:t[s],rawValue:u[s],write:C,tap:g,press:m,release:w,isLoading:o}},useAutoCoreTags:a=>{const{values:s,rawValues:t,isLoading:u,write:o}=useContext(e);return{values:useMemo(()=>Object.fromEntries(a.map(e=>[e,s[e]])),[s,a]),rawValues:useMemo(()=>Object.fromEntries(a.map(e=>[e,t[e]])),[t,a]),write:useCallback(async(e,a)=>{await o(e,a)},[o]),isLoading:u}},useAutoCoreSelect:(a,s=[])=>{const{values:t,isLoading:u}=useContext(e);return{selected:useMemo(()=>a(t),[t,...s]),isLoading:u}},useScales:()=>{const{scales:a,updateScale:s}=useContext(e),t=useCallback(e=>a[e],[a]);return{scales:a,updateScale:s,getScale:t}}}}
1
+ import{useCallback,useContext,useMemo}from"react";export function makeAutoCoreTagHooks(e,s){return{useAutoCoreTag:a=>{const{values:t,rawValues:u,isLoading:l,write:n,tap:o,press:r,release:i}=useContext(e),c=(e=>s.find(s=>s.tagName===e))(a);if(!c)throw new Error(`useAutoCoreTag: unknown tagName '${String(a)}'`);const m=useCallback(e=>n(a,e),[n,a]),C=useCallback(()=>o?.(a),[o,a]),g=useCallback(()=>r?.(a),[r,a]),b=useCallback(()=>i?.(a),[i,a]);return{value:t[a],rawValue:u[a],write:m,tap:C,press:g,release:b,isLoading:l}},useAutoCoreTags:s=>{const{values:a,rawValues:t,isLoading:u,write:l}=useContext(e);return{values:useMemo(()=>Object.fromEntries(s.map(e=>[e,a[e]])),[a,s]),rawValues:useMemo(()=>Object.fromEntries(s.map(e=>[e,t[e]])),[t,s]),write:useCallback(async(e,s)=>{await l(e,s)},[l]),isLoading:u}},useAutoCoreSelect:(s,a=[])=>{const{values:t,isLoading:u}=useContext(e);return{selected:useMemo(()=>s(t),[t,...a]),isLoading:u}},useScales:()=>{const{scales:s,updateScale:a}=useContext(e),t=useCallback(e=>s[e]??{name:e,scale:1,label:""},[s]);return{scales:s,updateScale:a,getScale:t}},useUnits:()=>{const{units:s,activeSystem:a,setUnitSystem:t,formatQuantity:u,scales:l}=useContext(e),n=useCallback(e=>l[e]?.label??"",[l]),o=useCallback((e,s)=>u?u(e,s):String(s),[u]);return{units:s??null,activeSystem:a??null,systems:s?.systems??[],setUnitSystem:t,label:n,format:o}}}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcops/autocore-react",
3
- "version": "3.3.124",
3
+ "version": "3.5.5",
4
4
  "description": "A React component library for industrial user interfaces.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -0,0 +1,88 @@
1
+ /*
2
+ * Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
3
+ *
4
+ * UnitSystemSelector — the operator's Metric/Imperial (or any named system)
5
+ * switch.
6
+ *
7
+ * Selecting a system writes it server-side, so it is machine-wide and survives
8
+ * a restart, and so the server's CSV export follows it rather than disagreeing
9
+ * with the screen. The server broadcasts the change and every connected client
10
+ * re-derives its scales, which means the switch is **live**: readouts, tables,
11
+ * charts and the sequence view all update with no page reload and no
12
+ * control-program restart. Nothing stored is converted — only what is rendered.
13
+ *
14
+ * See autocore-server/UNITS_PLAN.md §6.3.
15
+ */
16
+
17
+ import React, { useContext } from "react";
18
+ import { SelectButton } from "primereact/selectbutton";
19
+ import { Dropdown } from "primereact/dropdown";
20
+ import { AutoCoreTagContext } from "../core/AutoCoreTagContext";
21
+
22
+ export interface UnitSystemSelectorProps {
23
+ /**
24
+ * `"buttons"` (default) renders a segmented control — right for a
25
+ * touchscreen with two or three systems. `"dropdown"` is better past that.
26
+ */
27
+ variant?: "buttons" | "dropdown";
28
+ /** Optional label rendered above the control. */
29
+ label?: string;
30
+ /** Disable interaction (e.g. while a test is running). */
31
+ disabled?: boolean;
32
+ className?: string;
33
+ style?: React.CSSProperties;
34
+ }
35
+
36
+ export const UnitSystemSelector: React.FC<UnitSystemSelectorProps> = ({
37
+ variant = "buttons",
38
+ label,
39
+ disabled = false,
40
+ className,
41
+ style,
42
+ }) => {
43
+ const ctx = useContext(AutoCoreTagContext);
44
+ const units = ctx.units;
45
+ const active = ctx.activeSystem ?? units?.default_system ?? null;
46
+
47
+ // A machine with no units.json renders nothing rather than an empty control
48
+ // the operator can't use — the HMI still works, in backend units.
49
+ if (!units || !Array.isArray(units.systems) || units.systems.length === 0) {
50
+ return null;
51
+ }
52
+
53
+ const options = units.systems.map((s) => ({ label: s, value: s }));
54
+
55
+ const onChange = (next: string | null | undefined) => {
56
+ if (!next || next === active) return;
57
+ void ctx.setUnitSystem?.(next);
58
+ };
59
+
60
+ return (
61
+ <div className={className} style={style}>
62
+ {label ? (
63
+ <div style={{ marginBottom: "0.35rem", fontWeight: 600 }}>{label}</div>
64
+ ) : null}
65
+ {variant === "dropdown" ? (
66
+ <Dropdown
67
+ value={active}
68
+ options={options}
69
+ disabled={disabled}
70
+ onChange={(e) => onChange(e.value)}
71
+ style={{ minWidth: "10rem" }}
72
+ />
73
+ ) : (
74
+ <SelectButton
75
+ value={active}
76
+ options={options}
77
+ disabled={disabled}
78
+ // PrimeReact clears the value when the active option is
79
+ // re-clicked; ignore that so there is always a system.
80
+ onChange={(e) => onChange(e.value)}
81
+ allowEmpty={false}
82
+ />
83
+ )}
84
+ </div>
85
+ );
86
+ };
87
+
88
+ export default UnitSystemSelector;
@@ -0,0 +1,317 @@
1
+ /*
2
+ * Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
3
+ *
4
+ * UnitsEditor — operator-facing editor for the machine's `units.json`.
5
+ *
6
+ * This is the UI half of the units design (autocore-server/UNITS_PLAN.md). The
7
+ * file it edits is a SIDECAR next to project.json precisely so that this
8
+ * operator work is not wiped by an engineering project.json push.
9
+ *
10
+ * What the operator may change (all display-only, none of it reaches the control
11
+ * program): the display `label`, the `scalar` and `offset` that convert the
12
+ * backend value, and the display `precision` / `precision_mode`. They may also
13
+ * add and remove named systems.
14
+ *
15
+ * What they may NOT change here: `backend` — the unit GM/GNV actually holds. It
16
+ * is a commissioning declaration; changing it on a machine with recorded data is
17
+ * a data migration, not a settings tweak. It is shown, read-only, because you
18
+ * cannot sanely set a scalar without knowing what you are converting *from*.
19
+ *
20
+ * Precision is a first-class column, not an advanced setting: wrong precision is
21
+ * one of the most common customer complaints, and the whole point of putting this
22
+ * screen on the machine is that nobody has to wait for a release to fix it.
23
+ */
24
+
25
+ import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
26
+ import { DataTable } from "primereact/datatable";
27
+ import { Column } from "primereact/column";
28
+ import { InputText } from "primereact/inputtext";
29
+ import { InputNumber } from "primereact/inputnumber";
30
+ import { Dropdown } from "primereact/dropdown";
31
+ import { Button } from "primereact/button";
32
+ import { SelectButton } from "primereact/selectbutton";
33
+ import { Message } from "primereact/message";
34
+ import { EventEmitterContext } from "../core/EventEmitterContext";
35
+ import { MessageType } from "../hub/CommandMessage";
36
+ import type { PrecisionMode, UnitsTable } from "../core/AutoCoreTagTypes";
37
+
38
+ const PRECISION_MODES: { label: string; value: PrecisionMode }[] = [
39
+ { label: "Decimals", value: "decimals" },
40
+ { label: "Significant", value: "significant" },
41
+ ];
42
+
43
+ /** One editable row: a quantity, seen through the system being edited. */
44
+ interface Row {
45
+ quantity: string;
46
+ backend: string;
47
+ kind: "delta" | "absolute";
48
+ label: string;
49
+ scalar: number;
50
+ offset: number;
51
+ precision: number;
52
+ precisionMode: PrecisionMode;
53
+ }
54
+
55
+ export interface UnitsEditorProps {
56
+ /** Heading text. Omit for none. */
57
+ title?: string;
58
+ /** Called after a successful save. */
59
+ onSaved?: (units: UnitsTable) => void;
60
+ }
61
+
62
+ export const UnitsEditor: React.FC<UnitsEditorProps> = ({ title = "Units", onSaved }) => {
63
+ const { invoke } = useContext(EventEmitterContext);
64
+
65
+ const [units, setUnits] = useState<UnitsTable | null>(null);
66
+ const [editSystem, setEditSystem] = useState<string | null>(null);
67
+ const [dirty, setDirty] = useState(false);
68
+ const [busy, setBusy] = useState(false);
69
+ const [error, setError] = useState<string | null>(null);
70
+ const [notice, setNotice] = useState<string | null>(null);
71
+
72
+ const load = useCallback(async () => {
73
+ setBusy(true);
74
+ setError(null);
75
+ try {
76
+ const resp: any = await invoke("system.get_units" as any, MessageType.Request, {} as any);
77
+ const payload = resp?.data ?? resp;
78
+ const table = payload?.units as UnitsTable | null;
79
+ if (!table) {
80
+ setError(
81
+ "This machine has no units.json. Display values are shown in backend " +
82
+ "units until one is installed.",
83
+ );
84
+ setUnits(null);
85
+ } else {
86
+ setUnits(table);
87
+ setEditSystem((prev) =>
88
+ prev && table.systems.includes(prev) ? prev : table.default_system,
89
+ );
90
+ setDirty(false);
91
+ }
92
+ } catch (e) {
93
+ setError(`Could not read units: ${e instanceof Error ? e.message : String(e)}`);
94
+ }
95
+ setBusy(false);
96
+ }, [invoke]);
97
+
98
+ useEffect(() => { void load(); }, [load]);
99
+
100
+ /** Flatten the table into rows for the system being edited. */
101
+ const rows = useMemo<Row[]>(() => {
102
+ if (!units || !editSystem) return [];
103
+ return Object.entries(units.scales)
104
+ .map(([quantity, scale]: [string, any]) => {
105
+ const entry = scale?.[editSystem] ?? {};
106
+ return {
107
+ quantity,
108
+ backend: scale?.backend ?? "",
109
+ kind: (scale?.kind ?? "delta") as "delta" | "absolute",
110
+ label: entry.label ?? "",
111
+ scalar: typeof entry.scalar === "number" ? entry.scalar : 1,
112
+ offset: typeof entry.offset === "number" ? entry.offset : 0,
113
+ precision: typeof entry.precision === "number" ? entry.precision : 3,
114
+ precisionMode: (entry.precision_mode ?? "decimals") as PrecisionMode,
115
+ };
116
+ })
117
+ .sort((a, b) => a.quantity.localeCompare(b.quantity));
118
+ }, [units, editSystem]);
119
+
120
+ /** Write one field of one (quantity, system) entry. */
121
+ const patch = (quantity: string, field: keyof Row, value: any) => {
122
+ if (!units || !editSystem) return;
123
+ const next: UnitsTable = JSON.parse(JSON.stringify(units));
124
+ const scale: any = next.scales[quantity];
125
+ if (!scale) return;
126
+ const entry = { ...(scale[editSystem] ?? {}) };
127
+ switch (field) {
128
+ case "label": entry.label = value; break;
129
+ case "scalar": entry.scalar = value; break;
130
+ case "offset": entry.offset = value; break;
131
+ case "precision": entry.precision = value; break;
132
+ case "precisionMode": entry.precision_mode = value; break;
133
+ default: return;
134
+ }
135
+ scale[editSystem] = entry;
136
+ setUnits(next);
137
+ setDirty(true);
138
+ setNotice(null);
139
+ };
140
+
141
+ const save = async () => {
142
+ if (!units) return;
143
+ setBusy(true);
144
+ setError(null);
145
+ setNotice(null);
146
+ try {
147
+ // The server validates authoritatively and REFUSES a bad table —
148
+ // an invalid scale would break every display on the machine, so a
149
+ // rejected save must leave the stored file untouched.
150
+ const resp: any = await invoke(
151
+ "system.save_units" as any, MessageType.Request, { units } as any,
152
+ );
153
+ if (!resp?.success) {
154
+ setError(resp?.error_message ?? "Save was refused.");
155
+ return;
156
+ }
157
+ setDirty(false);
158
+ setNotice("Saved.");
159
+ onSaved?.(units);
160
+ } catch (e) {
161
+ setError(`Save failed: ${e instanceof Error ? e.message : String(e)}`);
162
+ } finally {
163
+ setBusy(false);
164
+ }
165
+ };
166
+
167
+ if (!units) {
168
+ return (
169
+ <div>
170
+ {title ? <h3 style={{ marginTop: 0 }}>{title}</h3> : null}
171
+ {error ? <Message severity="warn" text={error} /> : null}
172
+ <div style={{ marginTop: "0.75rem" }}>
173
+ <Button label="Reload" icon="pi pi-refresh" onClick={() => void load()} loading={busy} />
174
+ </div>
175
+ </div>
176
+ );
177
+ }
178
+
179
+ return (
180
+ <div>
181
+ {title ? <h3 style={{ marginTop: 0 }}>{title}</h3> : null}
182
+
183
+ <div style={{
184
+ display: "flex", alignItems: "center", gap: "1rem",
185
+ flexWrap: "wrap", marginBottom: "0.75rem",
186
+ }}>
187
+ <div>
188
+ <div style={{ fontWeight: 600, marginBottom: "0.3rem" }}>Editing system</div>
189
+ <SelectButton
190
+ value={editSystem}
191
+ options={units.systems.map((s) => ({ label: s, value: s }))}
192
+ onChange={(e) => { if (e.value) setEditSystem(e.value); }}
193
+ allowEmpty={false}
194
+ />
195
+ </div>
196
+ <div style={{ marginLeft: "auto", display: "flex", gap: "0.5rem" }}>
197
+ <Button
198
+ label="Revert"
199
+ icon="pi pi-undo"
200
+ className="p-button-text"
201
+ disabled={!dirty || busy}
202
+ onClick={() => void load()}
203
+ />
204
+ <Button
205
+ label="Save"
206
+ icon="pi pi-check"
207
+ disabled={!dirty || busy}
208
+ loading={busy}
209
+ onClick={() => void save()}
210
+ />
211
+ </div>
212
+ </div>
213
+
214
+ {error ? <Message severity="error" text={error} style={{ marginBottom: "0.5rem" }} /> : null}
215
+ {notice ? <Message severity="success" text={notice} style={{ marginBottom: "0.5rem" }} /> : null}
216
+
217
+ <Message
218
+ severity="info"
219
+ style={{ marginBottom: "0.75rem" }}
220
+ text={
221
+ "Display only — these settings never change what the machine stores or " +
222
+ "how it moves. 'Backend' is the unit the controller holds and is set at " +
223
+ "commissioning."
224
+ }
225
+ />
226
+
227
+ <DataTable value={rows} size="small" stripedRows scrollable scrollHeight="26rem">
228
+ <Column field="quantity" header="Quantity" style={{ minWidth: "11rem" }} />
229
+ <Column
230
+ field="backend"
231
+ header="Backend"
232
+ style={{ width: "7rem" }}
233
+ body={(r: Row) => (
234
+ <span style={{ opacity: 0.7 }}>
235
+ {r.backend || "—"}
236
+ {r.kind === "absolute" ? " (abs)" : ""}
237
+ </span>
238
+ )}
239
+ />
240
+ <Column
241
+ header="Label"
242
+ style={{ width: "8rem" }}
243
+ body={(r: Row) => (
244
+ <InputText
245
+ value={r.label}
246
+ style={{ width: "100%" }}
247
+ onChange={(e) => patch(r.quantity, "label", e.target.value)}
248
+ />
249
+ )}
250
+ />
251
+ <Column
252
+ header="Scalar"
253
+ style={{ width: "11rem" }}
254
+ body={(r: Row) => (
255
+ <InputNumber
256
+ value={r.scalar}
257
+ minFractionDigits={0}
258
+ maxFractionDigits={10}
259
+ style={{ width: "100%" }}
260
+ inputStyle={{ width: "100%" }}
261
+ onValueChange={(e) => patch(r.quantity, "scalar", e.value ?? 1)}
262
+ />
263
+ )}
264
+ />
265
+ <Column
266
+ header="Offset"
267
+ style={{ width: "8rem" }}
268
+ body={(r: Row) =>
269
+ // Only an absolute quantity takes an offset. A temperature
270
+ // DIFFERENCE must not: a 10 °C rise is 18 °F, not 50 °F.
271
+ r.kind === "absolute" ? (
272
+ <InputNumber
273
+ value={r.offset}
274
+ minFractionDigits={0}
275
+ maxFractionDigits={6}
276
+ style={{ width: "100%" }}
277
+ inputStyle={{ width: "100%" }}
278
+ onValueChange={(e) => patch(r.quantity, "offset", e.value ?? 0)}
279
+ />
280
+ ) : (
281
+ <span style={{ opacity: 0.4 }}>n/a</span>
282
+ )
283
+ }
284
+ />
285
+ <Column
286
+ header="Precision"
287
+ style={{ width: "7rem" }}
288
+ body={(r: Row) => (
289
+ <InputNumber
290
+ value={r.precision}
291
+ min={0}
292
+ max={12}
293
+ showButtons
294
+ style={{ width: "100%" }}
295
+ inputStyle={{ width: "100%" }}
296
+ onValueChange={(e) => patch(r.quantity, "precision", e.value ?? 0)}
297
+ />
298
+ )}
299
+ />
300
+ <Column
301
+ header="Mode"
302
+ style={{ width: "9rem" }}
303
+ body={(r: Row) => (
304
+ <Dropdown
305
+ value={r.precisionMode}
306
+ options={PRECISION_MODES}
307
+ style={{ width: "100%" }}
308
+ onChange={(e) => patch(r.quantity, "precisionMode", e.value)}
309
+ />
310
+ )}
311
+ />
312
+ </DataTable>
313
+ </div>
314
+ );
315
+ };
316
+
317
+ export default UnitsEditor;