Autocore-React Component Library
The previous chapter covered the connection layer of @adcops/autocore-react
— hooks like useMemoryStore, useGnv, and useServeletData for talking to
the server. This chapter covers the presentation layer: the provider tree
that supplies context, the component families ready to drop into an HMI, and
the styling system (most importantly, the ac-form layout primitives) used
to keep forms consistent across an autocore project.
If you’ve ever stared at a half-styled <TestSetupForm /> wondering why
nothing lines up, this chapter is for you.
Provider tree
Every autocore-react HMI mounts a stack of context providers near the root. The order matters — outer providers must be in place before inner ones read from them. The canonical layout, from the 3830 traction tester:
<EventEmitterProvider> {/* IPC / hub primitives */}
<PrimeReactProvider> {/* PrimeReact theming */}
<AutoCoreTagProvider {/* GM tag spec + scales */}
tags={acTagSpec}
scales={acScales}
eagerRead
>
<TisProvider defaultMethodId="translational_traction">
<AmsProvider>
<App />
</AmsProvider>
</TisProvider>
</AutoCoreTagProvider>
</PrimeReactProvider>
</EventEmitterProvider>
What each one provides:
| Provider | Source | Provides |
|---|---|---|
EventEmitterProvider | core/EventEmitterContext | The invoke, subscribe, unsubscribe, dispatch, write primitives that every other provider builds on. |
PrimeReactProvider | primereact/api | PrimeReact’s own context (locale, ripple, etc.). |
AutoCoreTagProvider | core/AutoCoreTagContext | GM tag registry, type-aware reads/writes, scale conversions. Backs useAutoCoreTag(). |
TisProvider | components/tis/TisProvider | Test Information System — schemas, selection (project/method/sample/run), staged config draft, run cache. Backs every TIS component. |
AmsProvider | components/ams/AmsProvider | Asset Management System — asset registry, schemas, roles, alerts. Backs every AMS component. |
TisProvider and AmsProvider are independent — you can mount one without
the other. If your project doesn’t use AMS, drop AmsProvider; AMS-aware
components fall back to empty asset lists gracefully.
Component families
TIS components
Render the test workflow on the operator HMI. All read from TisProvider
context — none take props for the project/method selection.
| Component | Purpose |
|---|---|
<ProjectSelector /> | Project tab: list known projects, pick or create one. Pins selection.projectId. |
<TestSetupForm /> | Test tab: per-test sample ID, method picker, config_fields. Auto-stages on every valid edit. |
<TestMethodDialog /> | Modal picker for test method, used by TestSetupForm. |
<ResultHistoryTable /> | Project tab: every recorded run for the active project, with quick-load to TestDataView. |
<TestDataView /> | Live cycle scatter charts driven by the active method’s views block. |
<TestRawDataView /> | Raw waveform plots driven by raw_data.columns. |
<TisConfigEditor /> | Authoring tool — edit the test_methods block of project.json in-app (fields, views, raw_data shape, asset_refs, analysis). See the dedicated section below. |
AMS components
Render the asset registry. Drop them into a Settings panel or a dedicated tab.
| Component | Purpose |
|---|---|
<AssetRegistryTable /> | List every asset, filter by type/status, “+ Add” to register. |
<AssetDetailView /> | Selected asset’s details — calibration history, usage counters, sub-locations. |
<CalibrationEntryDialog /> | Form to add a new calibration record. |
<SubLocationPicker /> | Edit per-instance state on multi-position assets (e.g., surface lanes). |
Toolbar widgets
| Component | Purpose |
|---|---|
<IndicatorButton /> | Two-state colored button (motor on/off, error reset, etc.). |
<IndicatorColor> enum | Standard color names — IndicatorGreen, IndicatorRed, IndicatorOrange, IndicatorOff. |
<ToggleGroup /> | Mutually-exclusive button group for mode selection. |
Editing test methods: <TisConfigEditor />
Everywhere else in this chapter, the TIS components consume the schema —
they read test_methods from project.json and render the test workflow
against it. <TisConfigEditor /> is the other side of that contract: it
writes the schema. Open it in an admin tab, click around, save, and the
HMI’s test workflow picks up the new methods on the next mount.
If you’ve ever hand-edited project.json with VS Code while squinting at
the TestMethod struct in project.rs to make sure your braces line up,
this is for you.
What it edits
The entire test_methods block of project.json — every field on every
TestMethod, including:
label/description(the picker UX)- Four field arrays:
project_fields,config_fields,cycle_fields,results_fields views(chart definitions consumed by<TestDataView>and<TestRawDataView>)raw_data(the DAQ blob shape —blob_nameplus per-column source)asset_refs(AMS dependencies snapshotted at start_test time)analysis(post-cycle Python hook)
Unknown top-level fields in project.json (anything outside test_methods)
are preserved on save — the editor does JSON-level surgery, not a full
round-trip through the Rust Project struct, so any forward-compatible
keys your team adds survive.
Mounting
import { TisConfigEditor } from '@adcops/autocore-react/components/tis-editor/TisConfigEditor';
<TisConfigEditor projectId="3830_traction" />
That’s the whole API. projectId is required and threads through every
IPC call, but today the server resolves it to the loaded project — so any
non-empty string is accepted. (See Multi-project posture below.)
The editor pulls live data via the standard provider tree
(EventEmitterProvider for IPC, plus AmsProvider upstream if you want
the asset_type dropdown to populate). No additional context required.
For testing or for an offline playground, pass a custom invoker:
<TisConfigEditor projectId="demo" invoker={mockInvoker} />
Where mockInvoker is a function of type TisIpcInvoker —
(topic: string, payload: object) => Promise<{ success, data, error_message }>.
The autocore-react/playground repo includes a working mock you can copy.
The staged-then-saved model
This is the most important concept to grasp. Edits don’t land on disk immediately. They land in an in-memory copy on the server, and stay there until you click Save.
HMI types in editor → tis.put_method ┐
HMI deletes a method → tis.remove_method├→ server-side stage (in memory)
HMI clicks "Revert" → tis.discard_… ┘ │
HMI clicks "Save" ──────────────────────→ tis.save_config
↓
atomic write of project.json
(+ project.json.bak)
Several consequences flow from this:
- The dirty pill in the top-right of the editor (an orange “unsaved” badge) means: the server has staged edits that don’t match disk yet. Save and Revert are only enabled when this is true.
- Stage survives across HMI page reloads — the server holds it. If you refresh the browser, your edits are still there.
- Stage does not survive an autocore-server restart. If you bounce the server with unsaved edits, they’re gone.
- Two HMIs editing the same server share one stage. Last writer wins. In practice the editor is used by one operator at a time on a station; if you need per-user drafts, that’s a future addition.
Layout
┌─ Test Methods [unsaved] [Save…] [Revert] ─┐
│ │
│ ┌──── sidebar ────┐ ┌──── detail pane ─────────────────────┐ │
│ │ [New][Dup][Del] │ │ rotational_traction • [Apply] │ │
│ │ ─────────────── │ │ ──────────────────────────────────── │ │
│ │ Method ID Label │ │ [Identity][Fields][Views][Raw…]… │ │
│ │ ─────────────── │ │ │ │
│ │ rotational_… ● │ (the active tab's subform) │ │
│ │ translational… │ │ │ │
│ │ │ │ │ │
│ └─────────────────┘ └──────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
- Top action bar: dirty indicator, Save (opens diff dialog), Revert.
- Sidebar: master list of methods. Click a row to load it. The three buttons up top scope to “the entire methods map” (add a new one, duplicate the selected one, delete the selected one).
- Detail pane: seven tabs editing one method’s contents at a time.
The
•next to the method ID lights up when the in-tab form has unapplied edits. - Apply button: pushes the current tab’s edits to the server stage
via
tis.put_method. Until you hit Apply, your typing is in the browser only.
The two-step (“Apply, then Save”) is intentional — Apply runs server-side validation per method, Save runs the full whole-config validation and writes to disk. You get fast per-method feedback and a single atomic persist.
Walking through the tabs
Identity
Two fields: label (pretty name for the Test Method picker) and
description (long-form guidance shown below the dropdown). Both are
optional — when absent, the picker falls back to the canonical
method_id.
Fields
Four field arrays, each rendered by the same component:
| Array | Lifecycle | One row per |
|---|---|---|
project_fields | Filled once by the HMI when the method is selected. Not cycled. | Test record (top of test.json) |
config_fields | Operator-input speeds, loads, surface IDs. Snapshotted at start_test. | Test record (test.json::config) |
cycle_fields | Per-cycle measurements. Written by the control program each cycle. | Cycle (cycles.jsonl line) |
results_fields | Post-test summary (min/max/avg, pass/fail). Written once at finish. | Test record (test.json::results) |
Click Add field or the row’s pencil icon to open the field dialog.
Each TestField has:
- Name (required) — the canonical wire-format key. Must be a valid identifier. Also becomes the CSV column header.
- Type —
string,i32/i64/u32/u64,f32/f64,bool. The dropdown is editable in case you need a type that’s not in the list yet. - Units — display string appended to form labels (e.g.,
m/s). - Label — pretty form label. Falls back to
namewhen empty. - Required — gates form validation in
<TestSetupForm>. - Source — optional
gm.<var>binding. When set, the control program reads/writes this GM variable as the field’s storage. - Scale — display-time multiplier.
display = raw × scale. Storage is always raw; only the HMI display and CSV export apply the scale. Leave blank for1.0(no conversion). - Description — hover tooltip in the form.
Row reorder (up/down arrows) is purely cosmetic — field order in the form matches array order; on-disk storage doesn’t care.
Views
The named chart definitions. Each entry is a ChartView keyed by a
stable view ID (the same key your control program references when
emitting cycle data).
The dialog asks for:
- View ID — stable key like
cof_scatter. Must be unique within the method. - Type —
cycle_scatter(one point per cycle, plotted by<TestDataView>) orraw_trace(waveform plot, by<TestRawDataView>). - X axis — either a field name (cycle_scatter) or a column name (raw_trace). The dialog auto-switches the input label based on the selected type. Both inputs offer autocomplete from the method’s known fields and raw_data columns.
- Y series — one or more entries, each with the same field/column
picker plus a
y_axis: left | rightselector for dual-Y charts.
If you reference a field or column that doesn’t exist, the editor flags it as a validation issue immediately — see Validation below.
Raw Data
Toggleable. The header checkbox enables or disables raw data capture
entirely; when off, the method writes no raw_data/ blobs and views
of type raw_trace won’t render anything.
When enabled, you set:
- Blob name — base filename under
raw_data/. Defaults totrace. Files end up asraw_data/<sample_id>_<n>_cycleNNNN.json. - Columns — one row per column. Each carries:
- The column name (rename in place, blur to commit).
- The source — one of:
time— synthesized linear time axis from the DAQ’s actual_samples and sample_rate. No physical channel.ni.<daq>.channels.<channel_name>— a channel from a NI DAQ capture. The codegen resolves the channel index fromproject.json’smodules.ni.config.daq[<daq>].channelsarray.derived— computed at codegen time from other declared columns via the formula field. Supports+ - * /withabs(...),sqrt(...), and a top-levelddt(<column>)for the per-sample derivative.
- The formula — required when source is
derived, ignored otherwise. Identifiers reference other columns in the same map (forward references included; derived columns emit in a second pass).
Assets
Edits the method’s asset_refs — AMS dependencies resolved at
tis.start_test and snapshotted into test.json::asset_snapshot.<field>.
Each ref carries:
- Field — key under
asset_snapshot(e.g.,load_cell_z). - Asset type — populated from
ams.list_schemasif AmsProvider is upstream. Falls back to a free-form text input otherwise. - Select —
by_location(asset whose location matches a fixed value) orby_id_field(asset whose ID is read from a config field at start-time). - Location or From — surfaces conditionally based on the Select
value.
Locationis an AMS location string (e.g.,tsdr);Fromis a dotted config path (e.g.,config.surface_asset_id). - Calibration policy —
ignore/warn(default) /require. The start_test resolver emits warnings or hard-errors per this setting if the matched asset’s calibration is missing or expired.
Method-level asset_refs are combined with project-level asset_refs
at start_test time. Method overrides win on field-name collisions. If
your project declares surface at the project level, you don’t redeclare
it here — but you can override its calibration policy by adding the same
field name with a stricter setting.
Analysis
Toggleable hook into autocore-python. When enabled:
- Script — path under
autocore-python’s scripts directory. - Function — entry-point name within the script.
The codegen emits a run_analysis(ctx) helper on this method’s
TestManager that dispatches python.run_analysis with the configured
script + function. Whether/when to fire it is up to your control program.
JSON
Always available, every method. Shows the working copy as pretty-printed JSON in a Monaco editor with full syntax highlighting and folding.
This is the escape hatch. Use it when:
- The form doesn’t (yet) expose a field you need.
- You want to paste in a method definition from somewhere else.
- You’re diagnosing a “the form claims this is invalid but I think it’s fine” situation — read the literal JSON.
Edits in the JSON tab feed back into the form tabs in real time (parse errors are shown inline; if it doesn’t parse, the form tabs keep their last-good state). The reverse is also true — form edits update the JSON text. The two views stay in sync.
Validation and the issue badge
Two layers of validation, by design:
- Client-side, live. Mirrors the server’s
validate_methodexactly. Runs on every keystroke. Shows up as a red badge in the detail header (“3 issues” — hover for the list). Disables Apply. - Server-side, at Apply. Same checks plus the schema deserialization (catches “type” wasn’t a string, etc.). Returns the error list in the IPC response; surfaces in the detail pane.
- Server-side, at Save. Full whole-config validation across all methods. Catches “your view references a field, the field exists in this method, but it’s the wrong type.” Surfaces in the save dialog with a clear error message.
Checks performed by both layers:
- Field names are non-empty and unique within each array.
viewsaxes reference known field or column names.raw_data.blob_nameis non-empty when raw_data is enabled.
Checks performed only server-side:
- The full TestMethod payload matches the Rust struct (catches type-coercion drift if the schema evolves).
Checks not performed yet (worth knowing — possible Phase-4 work):
raw_data.columnssources reference real NI DAQ channels.analysis.scriptexists on disk.asset_refs.asset_typeexists in the AMS catalog (the dropdown helps, but doesn’t enforce).
Save flow: the diff dialog
Clicking Save… doesn’t immediately write to disk. It opens the diff dialog, which:
- Fetches a fresh disk-state via
tis.list_schemas(the read endpoint that bypasses staging). - Compares to the current staged methods.
- Shows a summary: Added / Removed / Modified by method ID.
- For modified methods, shows expandable before/after JSON panes (red for before, green for after — line-level diffing is intentionally not done, the JSON dump is enough for “did I actually delete that field by accident?”).
- On Confirm, calls
tis.save_config. The server writesproject.json.bak, then atomically replacesproject.json, then clears its stage.
If something goes wrong server-side (validation, write failure, active-test guard), the error surfaces in the dialog — Save remains pending.
Active-test guards
You cannot edit a method while a run is open against it. The guards fire in three places:
| Action | Server check | What you see |
|---|---|---|
tis.put_method | The active_tests map contains <project_id>:<method_id> for the method being edited. | The Apply button returns an error; the badge shows the message. |
tis.remove_method | Same. | Same. |
tis.save_config | ANY active run, regardless of method. (A method removal here would orphan an open run schema-side.) | The save dialog rejects with the active key list. Operator finishes/cancels the runs and retries. |
Conservative by design. Editing the recipe of a test that’s mid-flight is a recipe for inconsistent on-disk data — better to make the operator explicitly finish or cancel.
IPC surface
For integrators who want to drive the editor functions from outside the component (e.g., from a CLI tool or a sync script), the wire contract is:
| Topic | Request data | Response data | Side effects |
|---|---|---|---|
tis.show_config | { project_id } | { project_id, test_methods, default_method_id, dirty } | None |
tis.put_method | { project_id, method_id, method } | { status: "added" | "updated", method_id, dirty } | Server stage gains the method. Validates first; rejects on schema or cross-field errors, or while a run is active against method_id. |
tis.remove_method | { project_id, method_id } | { status: "removed", method_id, dirty } | Server stage loses the method. Same active-run rejection. 404 if the method wasn’t in the stage. |
tis.discard_config_changes | { project_id } | { status: "discarded", dirty: false } | Server stage cleared. Idempotent — succeeds when nothing was staged. |
tis.save_config | { project_id } | { status: "saved" | "noop", dirty: false } | Atomic write of project.json (+ project.json.bak). Rejected while any run is active anywhere. noop when nothing was staged. |
tis.list_schemas (already documented in chapter 15) still reads
straight from disk — bypasses staging. Useful when you want the canonical
on-disk version, e.g., for the diff dialog or for external diffing tools.
Multi-project posture
projectId is plumbed end-to-end through the React component, the
useTisConfig hook, and every IPC payload — but the server currently
resolves every value to “the loaded project.” The current single-project
binding lives in main.rs where Project::load(&project_path) is called
once.
When the time comes for real runtime project switching, the change is
isolated to tis_servelet.rs — load alternate project.json files on
demand keyed by project_id. The React side already speaks the right
contract; no editor rewrite needed.
Gotchas
“My Apply succeeded but the badge says ‘unsaved’.” That’s expected — Apply pushes to the stage; the stage is dirty until you Save. The badge is a config-level dirty indicator, not a per-method one.
“I’m in the JSON tab and the form tabs look stale.” Switch tabs once. The form tabs re-parse the working copy on activation. (If the JSON doesn’t parse, the form tabs keep their last-good state until you fix the JSON.)
“The asset_type dropdown is just a text field.” That happens when
no ams.list_schemas response was available — usually because
AmsProvider isn’t mounted upstream. The editor degrades to a free-form
input rather than blocking. Mount AmsProvider to get the dropdown back.
“I deleted a method by accident, what now?” If you haven’t clicked
Save yet, hit Revert — that clears the entire server-side stage,
including the deletion. If you have saved, restore from
project.json.bak (sitting next to project.json). The editor always
writes the backup first; it’s only one save behind.
“Two operators got into the editor at the same time and now I’m confused.” Stage is shared. Whichever one hits Save first wins; the other’s pending edits land too unless someone reverts. Phase-4 enhancement: per-client draft isolation. Until then, treat the editor as single-operator.
“I want to script a bulk schema change.” Skip the editor — talk to
the IPC directly. Send a sequence of tis.put_method calls followed by
one tis.save_config. Errors surface in each response. The active-test
guard still applies.
Charting test data
The cycle and raw-trace charts on the HMI are driven entirely by the
views block under each test method in project.json. There are two
view types, served by two different components — pick the one that
matches the axis of your data, not the chart shape:
type | Component that renders it | x/y refer to | Best for |
|---|---|---|---|
cycle_scatter | <TestDataView /> main panel | cycle_fields (one point per cycle) | Trends across many cycles in a single run — wear-in, drift, COF degradation. |
raw_trace | <TestRawDataView /> | raw_data.columns (one point per sample) | Per-cycle waveforms — force vs displacement, channel-vs-channel cross-plots. |
cycle_scatter: one point per cycle
Lists cycle_fields-bound axes. The control program is responsible for
calling record_cycle() (or the per-method add_cycle()) once per
cycle; each call appends one row to cycles.jsonl and, if the run is
active, broadcasts tis.cycle_added so the chart updates live.
"cof_per_cycle": {
"title": "COF by Cycle",
"type": "cycle_scatter",
"x": { "field": "cycle_index", "label": "Cycle" },
"y": [
{ "field": "friction_coefficient", "label": "COF" }
]
}
Multi-series with split axes works the same way as raw_trace —
y_axis: "left" (default) or "right". Useful when the series have
different units; e.g. plotting actual_load (N) on the left axis and
friction_coefficient (unitless) on the right.
A cycle_scatter view with cycle_count == 1 per run renders as a
single dot. That’s a signal you should be using raw_trace instead —
the data you care about lives at sample resolution, not cycle
resolution. (Single-cycle test methods can still use cycle_scatter
to track averages across many runs in the History tab, but the live
chart in TestDataView will look unhelpfully sparse.)
raw_trace: one point per sample
Lists raw_data.columns-bound axes. The whole capture is sent in a
single columnar JSON blob (raw_data/<blob_name>.json); axes index
into the column map by name.
"loads_vs_time": {
"title": "All Load Channels over Time",
"type": "raw_trace",
"x": { "column": "t", "label": "Time (s)" },
"y": [
{ "column": "tsdr_fx", "label": "Fx (N)", "y_axis": "left" },
{ "column": "tsdr_fy", "label": "Fy (N)", "y_axis": "left" },
{ "column": "tsdr_fz", "label": "Fz (N)", "y_axis": "left" },
{ "column": "tsdr_mx", "label": "Mx (Nm)", "y_axis": "right" },
{ "column": "tsdr_my", "label": "My (Nm)", "y_axis": "right" },
{ "column": "tsdr_mz", "label": "Mz (Nm)", "y_axis": "right" }
]
}
Cross-plots (Y over X where neither axis is time) are just as easy —
point both x.column and the entries in y[*].column at any column
declared in raw_data.columns:
"fz_vs_fx": {
"title": "Normal vs Friction Force",
"type": "raw_trace",
"x": { "column": "tsdr_fx", "label": "Fx (N)" },
"y": [{ "column": "tsdr_fz", "label": "Fz (N)" }]
}
<TestRawDataView> lazy-fetches the trace blob on mount (or whenever
the pinned run changes). Pan, scroll-zoom, pinch-zoom and shift-drag-
zoom are wired by default; the “Reset Zoom” button restores the auto
range. Charts with multi-axis series automatically split labels into
“left axis / right axis” titles.
Mixing both view types in one method
A method can declare both kinds and the components ignore the views
they don’t render. Stack <TestDataView /> and <TestRawDataView />
in the same tab to give the operator scatter + waveform side by side:
<div className="vblock">
<TestDataView />
<TestRawDataView chartHeight="50vh" />
</div>
The ac-form layout system
Forms in autocore HMIs use one of two complementary CSS class families. Don’t mix them. They have different markup expectations and trying to nest one inside the other gives you alignment that almost works but doesn’t quite — the trap that bit ManualControlView.
.ac-form + .ac-form-row — flexbox, row by row
A vertical flex column where each child is its own horizontally-laid-out row. Easiest to reason about; columns aren’t aligned across rows.
<div className="ac-form">
<div className="ac-form-row">
<span className="ac-form-label">Send X</span>
<InputNumber className="ac-form-field" ... />
<Button label="GO!" />
</div>
<div className="ac-form-row">
<span className="ac-form-label">Send Y</span>
<InputNumber className="ac-form-field" ... />
<Button label="GO!" />
</div>
</div>
When to use it:
- Rows have different shapes (different widget types, different counts of cells)
- You don’t care that “Send X”’s input doesn’t line up vertically with “Send Y”’s
- You want a quick form without thinking about a grid
CSS recap (from themes/adc-dark/_extensions.scss):
.ac-form {
display: flex;
flex-direction: column;
gap: 10px;
}
.ac-form-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.ac-form-field { flex: 1; min-width: 0; }
.ac-form-label { font-weight: 600; flex-shrink: 0; }
.ac-form-grid — CSS Grid, columns aligned across rows
A 4-column CSS Grid. Children flow into cells row-by-row, no row wrappers. Every label sits in column 1, every input in column 2, every units/suffix in column 3, every validity-check icon in column 4. Cross-row alignment is automatic.
<div className="ac-form-grid">
<span className="ac-form-label">Send X</span>
<InputNumber ... />
<span>{positionUnits}</span>
<i className="pi pi-check" /> {/* validity */}
<span className="ac-form-label">Send Y</span>
<InputNumber ... />
<span>{positionUnits}</span>
<i className="pi pi-check" />
</div>
When to use it:
- Multiple rows of similar shape (data entry, parameter setup)
- Labels and inputs must align vertically (it’s how operators scan a form)
- You have a “validity check” or “info icon” cell — the 4th column was designed for this
CSS recap (from themes/theme-base/_common.scss):
.ac-form-grid {
display: grid;
grid-template-columns: auto 1fr auto auto; // label | input | units | check
gap: 16px 24px;
align-items: center;
max-width: 600px;
}
Override the column template when 4 isn’t right
For a 3-column form (label, input, action button), override
gridTemplateColumns inline:
<div className="ac-form-grid"
style={{ gridTemplateColumns: 'auto 1fr auto' }}>
<span className="ac-form-label">Send X</span>
<InputNumber ... />
<Button label="GO!" />
...
</div>
<TestSetupForm /> does this for its 4-column case where the per-field
columns are: label, input, info-icon, validity-check. See its gridStyle
declaration for the pattern.
Spanning cells
When a row needs to span multiple columns — a section header, a long description, a wide note — use one of the span helpers:
| Class | Effect |
|---|---|
.ac-form-section | Spans all columns. Renders as a small uppercase header with an underline. Use for “Test Configuration”, “Network Settings”, section dividers. |
.ac-form-wide | Spans all columns. No styling — for content that shouldn’t look like a section header. |
.ac-form-span | Spans from column 2 to the end (skips the label column). Useful for descriptive text under an input that aligns with the input rather than the label. |
<div className="ac-form-grid">
<h3 className="ac-form-section">Press Configuration</h3>
<span className="ac-form-label">Pre-load</span>
<InputNumber ... />
<span>N</span>
<i />
<p className="ac-form-span">
Force applied before the cycle begins. Set near zero for unloaded tests.
</p>
<h3 className="ac-form-section">Cycle</h3>
...
</div>
<h3 className="ac-form-section"> is the single most common element after
labels — every form on the HMI uses it for visual structure.
The big mistake: nesting
Don’t put .ac-form-row inside .ac-form-grid:
{/* DON'T do this */}
<div className="ac-form-grid">
<div className="ac-form-row"> {/* ← entire row collapses */}
<span className="ac-form-label">Send X</span> {/* into a single grid cell */}
<InputNumber ... />
<Button label="GO!" />
</div>
...
</div>
The grid sees the <div class="ac-form-row"> as one child, places it in
column 1 of row 1, and lets the inner flex layout sort itself out. It looks
close to right but the columns aren’t aligned across rows because each row’s
cells live inside their own flex container, not in the grid’s columns.
Pick one system per form. If you need cross-row alignment, use the grid with direct children. If you need per-row independence, use the flex form.
ac-toolbar — icon-button toolbars
For operator toolbars (PANIC, motor power, axis status), use the
ac-toolbar-* classes. They ensure consistent square button sizing and
SVG-icon rendering weights across mixed icon sources (Lucide SVGs,
PrimeIcons, custom).
| Class | Size | Use for |
|---|---|---|
.ac-toolbar-icon-btn | ~19mm × 12.7mm (compact) | Standard toolbar action buttons |
.ac-toolbar-icon-lg | ~25.4mm × 19mm | Larger toolbar actions where touch target matters |
.ac-toolbar-icon-panic | 25.4mm × 25.4mm | The PANIC button specifically — squarer, larger, severity=danger |
.ac-toolbar-group | flex row, gap: 2px | Group of related buttons (motor on/off cluster, etc.) |
.ac-toolbar-tool-list | flex column | Stacked text-button list inside an OverlayPanel |
.ac-toolbar-tool-item | left-justified | Single text button inside a tool list |
The toolbar classes also normalize SVG icons so Lucide icons (<PanelBottomOpen />)
render at the same visible weight as PrimeIcons (<i class="pi pi-power-off" />).
See the ac-toolbar-svg-normalize mixin in _extensions.scss for what gets applied.
Theme overrides
@adcops/autocore-react ships with the adc-dark theme as the default
visual style. To override:
- Create a project-local SCSS file.
- Import it after the autocore-react theme so your rules win cascade ties (or use higher specificity).
- Match PrimeReact’s selector specificity when overriding their
components —
.p-dialog .p-dialog-contentbeats.p-dialog-content.
Common overrides land in the project’s CSS rather than the package — for
single-machine customization (custom logo, brand colors). For shared
overrides used across multiple HMIs, contribute back to
autocore-react/src/themes/adc-dark/_extensions.scss.
A worked example of an override that DIDN’T work the first time, and why, appears in the autocore-react commit history under “Dialog padding never applied” — TL;DR: PrimeReact’s nested-class selectors silently outrank a single-class override, and you have to match their specificity to win.
Hooks reference (cross-context)
These belong to specific providers but are useful to know exist:
| Hook | From | Returns |
|---|---|---|
useAutoCoreTag(name) | AutoCoreTagProvider | { value, write, tap } for a GM tag |
useTis() | TisProvider | full TIS context (selection, schemas, stagedConfig, …) |
useTisSelection() | TisProvider | [selection, setSelection] tuple |
useTisSchemas() | TisProvider | the test method registry |
useAms() | AmsProvider | full AMS context |
useAmsSchemas() | AmsProvider | the asset_type registry |
useAmsRoles() | AmsProvider | roles per asset_type (from project.json’s by_location asset_refs) |
useAmsAssets() | AmsProvider | the live asset list, sorted/filtered already |
useAmsSelection() | AmsProvider | [selection, setSelection] for assetType/assetId pinning |
useMemoryStore(name) | core hooks | { value, write, refresh } for a memory-store entry |
useGnv(group, key) | core hooks | { value, write, refresh } for a gnv key |
See chapter 10 for usage examples on the connection-layer hooks.