diff --git a/gui-ts/src/styles.css b/gui-ts/src/styles.css
index 84690ea..820b712 100644
--- a/gui-ts/src/styles.css
+++ b/gui-ts/src/styles.css
@@ -149,6 +149,8 @@ html, body, #root {
@media (max-width: 880px) {
.tab-grid.two, .tab-grid.three { grid-template-columns: 1fr; }
.panel-row { grid-template-columns: 140px 1fr; }
+ .action-row-inline { flex-direction: column; align-items: stretch; }
+ .action-row-inline .btn { width: 100%; }
}
/* Inputs */
@@ -202,6 +204,10 @@ html, body, #root {
align-items: center;
margin: 4px 0 8px 0;
}
+.action-row-inline {
+ justify-content: flex-end;
+ margin: 0;
+}
.action-row { display: flex; justify-content: flex-end; gap: 8px; padding-top: 10px; }
/* Tables */
@@ -241,8 +247,43 @@ html, body, #root {
outline-offset: -1px;
background: rgba(56, 189, 248, 0.1);
}
+.data-table tbody tr:focus-within td {
+ background: rgba(56, 189, 248, 0.08);
+}
.data-table tbody td .panel-input { padding: 3px 6px; font-size: 11px; }
.data-table .empty-row { text-align: center; color: var(--text-muted); padding: 18px; }
+.data-table-compact thead th { padding: 4px 6px; font-size: 11px; }
+.data-table-compact tbody td { padding: 2px 4px; }
+.data-table-compact tbody td .panel-input { padding: 2px 4px; font-size: 10px; }
+
+.table-actions-col {
+ position: sticky;
+ right: 0;
+ z-index: 2;
+ background: var(--panel-3);
+ box-shadow: -1px 0 0 var(--border);
+}
+.data-table thead .table-actions-col {
+ background: var(--panel);
+ z-index: 3;
+}
+.data-table tbody tr:focus-within .table-actions-col,
+.data-table tbody tr.row-selected .table-actions-col {
+ background: #162338;
+}
+
+.data-table .btn:focus-visible,
+.data-table .panel-input:focus-visible,
+.data-table select:focus-visible,
+.data-table input[type="checkbox"]:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 1px;
+ box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.18);
+}
+
+.data-table-compact .btn {
+ min-height: 24px;
+}
/* KPI grid */
.kpi-grid {
@@ -338,6 +379,18 @@ html, body, #root {
.uplot-host .u-legend { color: var(--text-dim); font-size: 11px; }
.uplot-host .u-wrap { background: var(--panel-3); }
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
/* 3D wellbore */
.wellbore-3d-wrap {
border: 1px solid var(--border);
diff --git a/gui-ts/src/ui/common/CheckboxField.tsx b/gui-ts/src/ui/common/CheckboxField.tsx
index fd82aef..732ca42 100644
--- a/gui-ts/src/ui/common/CheckboxField.tsx
+++ b/gui-ts/src/ui/common/CheckboxField.tsx
@@ -4,6 +4,7 @@ export type CheckboxFieldProps = {
onChange: (checked: boolean) => void;
label?: string;
disabled?: boolean;
+ ariaLabel?: string;
};
export function CheckboxField(props: CheckboxFieldProps) {
@@ -12,6 +13,7 @@ export function CheckboxField(props: CheckboxFieldProps) {
props.onChange(e.target.checked)}
diff --git a/gui-ts/src/ui/tabs/FluidTab.tsx b/gui-ts/src/ui/tabs/FluidTab.tsx
index 7d8a97d..652eeeb 100644
--- a/gui-ts/src/ui/tabs/FluidTab.tsx
+++ b/gui-ts/src/ui/tabs/FluidTab.tsx
@@ -12,6 +12,7 @@ type Props = { store: CaseStore };
export function FluidTab({ store }: Props) {
const { state, update } = store;
+ const isMetric = state.unitsSelection === 1;
const estimated = useMemo(
() => ({
@@ -65,7 +66,7 @@ export function FluidTab({ store }: Props) {
label="Tubing Gradient"
htmlFor="tubingGrad"
hint={
- state.unitsSelection === 1
+ isMetric
? "Pa/m when SI (UnitsSelection=1); from XML"
: "psi/ft imperial oilfield; converted to Pa/m in the API"
}
@@ -79,16 +80,19 @@ export function FluidTab({ store }: Props) {
Heuristic bulk-liquid ρ(water cut, API, water SG) → gradient ≈{" "}
- {state.unitsSelection === 1
+ {isMetric
? `${estimated.paM.toFixed(1)} Pa/m`
: `${estimated.psiPerFt.toFixed(4)} psi/ft`}
. Not wired into the C solve yet (see COMPUTE_PLAN).
+
+ Dual reference: {estimated.psiPerFt.toFixed(4)} psi/ft · {estimated.paM.toFixed(1)} Pa/m
+
);
diff --git a/gui-ts/src/ui/tabs/PumpTab.tsx b/gui-ts/src/ui/tabs/PumpTab.tsx
index 30cf6cb..d617eba 100644
--- a/gui-ts/src/ui/tabs/PumpTab.tsx
+++ b/gui-ts/src/ui/tabs/PumpTab.tsx
@@ -14,6 +14,7 @@ type Props = { store: CaseStore };
export function PumpTab({ store }: Props) {
const { state, update } = store;
+ const isMetric = state.unitsSelection === 1;
const pumpOptions = useMemo(() => {
const mmVals = PUMP_PLUNGER_INCH_OPTIONS.map((inchVal) => pumpDiameterMmFromInches(inchVal));
@@ -37,7 +38,11 @@ export function PumpTab({ store }: Props) {
-
+
update("pumpFriction", v)}
/>
-
+
("depth");
const [selectedSegment, setSelectedSegment] = useState(null);
+ const [traceabilityQuery, setTraceabilityQuery] = useState("");
+ const dlsUnitLabel = caseState.unitsSelection === 1 ? "deg/100 m" : "deg/100 ft";
const trajectorySegments = useMemo(() => buildTrajectorySegments(caseState.survey), [caseState.survey]);
const sideLoadProfile = primary?.profiles?.sideLoadProfile ?? null;
const pumpDiag = useMemo(() => computePumpPlacement(caseState), [caseState]);
+ const filteredTraceability = useMemo(() => {
+ const fields = result?.fieldTraceability?.fields ?? [];
+ const q = traceabilityQuery.trim().toLowerCase();
+ if (!q) return fields;
+ return fields.filter((row) => {
+ return (
+ row.xmlKey.toLowerCase().includes(q) ||
+ row.category.toLowerCase().includes(q) ||
+ row.notes.toLowerCase().includes(q)
+ );
+ });
+ }, [result?.fieldTraceability?.fields, traceabilityQuery]);
const dynacardData = useMemo(() => {
const fdmSeries = toSeries(fdm ?? undefined);
@@ -240,8 +254,7 @@ export function ResultsTab({