feat: polish rod and trajectory UX interactions
Improve Majic Rod Solver usability with denser table controls, clearer unit-aware guidance, and stronger accessibility affordances for keyboard/screen-reader workflows. Made-with: Cursor
This commit is contained in:
@@ -149,6 +149,8 @@ html, body, #root {
|
|||||||
@media (max-width: 880px) {
|
@media (max-width: 880px) {
|
||||||
.tab-grid.two, .tab-grid.three { grid-template-columns: 1fr; }
|
.tab-grid.two, .tab-grid.three { grid-template-columns: 1fr; }
|
||||||
.panel-row { grid-template-columns: 140px 1fr; }
|
.panel-row { grid-template-columns: 140px 1fr; }
|
||||||
|
.action-row-inline { flex-direction: column; align-items: stretch; }
|
||||||
|
.action-row-inline .btn { width: 100%; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inputs */
|
/* Inputs */
|
||||||
@@ -202,6 +204,10 @@ html, body, #root {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 4px 0 8px 0;
|
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; }
|
.action-row { display: flex; justify-content: flex-end; gap: 8px; padding-top: 10px; }
|
||||||
|
|
||||||
/* Tables */
|
/* Tables */
|
||||||
@@ -241,8 +247,43 @@ html, body, #root {
|
|||||||
outline-offset: -1px;
|
outline-offset: -1px;
|
||||||
background: rgba(56, 189, 248, 0.1);
|
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 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 .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 */
|
||||||
.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-legend { color: var(--text-dim); font-size: 11px; }
|
||||||
.uplot-host .u-wrap { background: var(--panel-3); }
|
.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 */
|
/* 3D wellbore */
|
||||||
.wellbore-3d-wrap {
|
.wellbore-3d-wrap {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type CheckboxFieldProps = {
|
|||||||
onChange: (checked: boolean) => void;
|
onChange: (checked: boolean) => void;
|
||||||
label?: string;
|
label?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
ariaLabel?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CheckboxField(props: CheckboxFieldProps) {
|
export function CheckboxField(props: CheckboxFieldProps) {
|
||||||
@@ -12,6 +13,7 @@ export function CheckboxField(props: CheckboxFieldProps) {
|
|||||||
<input
|
<input
|
||||||
id={props.id}
|
id={props.id}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
aria-label={props.ariaLabel}
|
||||||
checked={props.checked}
|
checked={props.checked}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
onChange={(e) => props.onChange(e.target.checked)}
|
onChange={(e) => props.onChange(e.target.checked)}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type Props = { store: CaseStore };
|
|||||||
|
|
||||||
export function FluidTab({ store }: Props) {
|
export function FluidTab({ store }: Props) {
|
||||||
const { state, update } = store;
|
const { state, update } = store;
|
||||||
|
const isMetric = state.unitsSelection === 1;
|
||||||
|
|
||||||
const estimated = useMemo(
|
const estimated = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -65,7 +66,7 @@ export function FluidTab({ store }: Props) {
|
|||||||
label="Tubing Gradient"
|
label="Tubing Gradient"
|
||||||
htmlFor="tubingGrad"
|
htmlFor="tubingGrad"
|
||||||
hint={
|
hint={
|
||||||
state.unitsSelection === 1
|
isMetric
|
||||||
? "Pa/m when SI (UnitsSelection=1); from XML"
|
? "Pa/m when SI (UnitsSelection=1); from XML"
|
||||||
: "psi/ft imperial oilfield; converted to Pa/m in the API"
|
: "psi/ft imperial oilfield; converted to Pa/m in the API"
|
||||||
}
|
}
|
||||||
@@ -79,16 +80,19 @@ export function FluidTab({ store }: Props) {
|
|||||||
</Row>
|
</Row>
|
||||||
<div className="button-row" style={{ flexWrap: "wrap", gap: 8 }}>
|
<div className="button-row" style={{ flexWrap: "wrap", gap: 8 }}>
|
||||||
<button type="button" className="btn" onClick={applyEstimate}>
|
<button type="button" className="btn" onClick={applyEstimate}>
|
||||||
Apply liquid hydrostatic estimate
|
Apply estimate to Tubing Gradient
|
||||||
</button>
|
</button>
|
||||||
<span className="panel-note">
|
<span className="panel-note">
|
||||||
Heuristic bulk-liquid ρ(water cut, API, water SG) → gradient ≈{" "}
|
Heuristic bulk-liquid ρ(water cut, API, water SG) → gradient ≈{" "}
|
||||||
{state.unitsSelection === 1
|
{isMetric
|
||||||
? `${estimated.paM.toFixed(1)} Pa/m`
|
? `${estimated.paM.toFixed(1)} Pa/m`
|
||||||
: `${estimated.psiPerFt.toFixed(4)} psi/ft`}
|
: `${estimated.psiPerFt.toFixed(4)} psi/ft`}
|
||||||
. Not wired into the C solve yet (see COMPUTE_PLAN).
|
. Not wired into the C solve yet (see COMPUTE_PLAN).
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="panel-note" style={{ marginTop: 4 }}>
|
||||||
|
Dual reference: {estimated.psiPerFt.toFixed(4)} psi/ft · {estimated.paM.toFixed(1)} Pa/m
|
||||||
|
</p>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type Props = { store: CaseStore };
|
|||||||
|
|
||||||
export function PumpTab({ store }: Props) {
|
export function PumpTab({ store }: Props) {
|
||||||
const { state, update } = store;
|
const { state, update } = store;
|
||||||
|
const isMetric = state.unitsSelection === 1;
|
||||||
|
|
||||||
const pumpOptions = useMemo(() => {
|
const pumpOptions = useMemo(() => {
|
||||||
const mmVals = PUMP_PLUNGER_INCH_OPTIONS.map((inchVal) => pumpDiameterMmFromInches(inchVal));
|
const mmVals = PUMP_PLUNGER_INCH_OPTIONS.map((inchVal) => pumpDiameterMmFromInches(inchVal));
|
||||||
@@ -37,7 +38,11 @@ export function PumpTab({ store }: Props) {
|
|||||||
<Row
|
<Row
|
||||||
label="Plunger diameter"
|
label="Plunger diameter"
|
||||||
htmlFor="plungerDiam"
|
htmlFor="plungerDiam"
|
||||||
hint="Stored as mm in the case file when above 2 (same convention as rod OD)."
|
hint={
|
||||||
|
isMetric
|
||||||
|
? "Metric-first display with imperial equivalent in each option."
|
||||||
|
: "Imperial-first display with metric equivalent in each option."
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<SelectField
|
<SelectField
|
||||||
id="plungerDiam"
|
id="plungerDiam"
|
||||||
@@ -47,7 +52,11 @@ export function PumpTab({ store }: Props) {
|
|||||||
ariaLabel="Pump plunger diameter"
|
ariaLabel="Pump plunger diameter"
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Row label="Pump Friction" htmlFor="pumpFric" hint="lbf (imperial)">
|
<Row
|
||||||
|
label="Pump Friction"
|
||||||
|
htmlFor="pumpFric"
|
||||||
|
hint={isMetric ? "N (SI case)" : "lbf (imperial oilfield case)"}
|
||||||
|
>
|
||||||
<NumberField
|
<NumberField
|
||||||
id="pumpFric"
|
id="pumpFric"
|
||||||
value={state.pumpFriction}
|
value={state.pumpFriction}
|
||||||
@@ -55,7 +64,11 @@ export function PumpTab({ store }: Props) {
|
|||||||
onChange={(v) => update("pumpFriction", v)}
|
onChange={(v) => update("pumpFriction", v)}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Row label="Pump Intake Pressure" htmlFor="pumpIntake" hint="psi (imperial)">
|
<Row
|
||||||
|
label="Pump Intake Pressure"
|
||||||
|
htmlFor="pumpIntake"
|
||||||
|
hint={isMetric ? "Pa (SI case)" : "psi (imperial oilfield case)"}
|
||||||
|
>
|
||||||
<NumberField
|
<NumberField
|
||||||
id="pumpIntake"
|
id="pumpIntake"
|
||||||
value={state.pumpIntakePressure}
|
value={state.pumpIntakePressure}
|
||||||
|
|||||||
@@ -89,9 +89,23 @@ export function ResultsTab({
|
|||||||
const fdm = result?.solvers?.fdm ?? primary ?? null;
|
const fdm = result?.solvers?.fdm ?? primary ?? null;
|
||||||
const [overlayMode, setOverlayMode] = useState<OverlayMode>("depth");
|
const [overlayMode, setOverlayMode] = useState<OverlayMode>("depth");
|
||||||
const [selectedSegment, setSelectedSegment] = useState<number | null>(null);
|
const [selectedSegment, setSelectedSegment] = useState<number | null>(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 trajectorySegments = useMemo(() => buildTrajectorySegments(caseState.survey), [caseState.survey]);
|
||||||
const sideLoadProfile = primary?.profiles?.sideLoadProfile ?? null;
|
const sideLoadProfile = primary?.profiles?.sideLoadProfile ?? null;
|
||||||
const pumpDiag = useMemo(() => computePumpPlacement(caseState), [caseState]);
|
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<AlignedData | null>(() => {
|
const dynacardData = useMemo<AlignedData | null>(() => {
|
||||||
const fdmSeries = toSeries(fdm ?? undefined);
|
const fdmSeries = toSeries(fdm ?? undefined);
|
||||||
@@ -240,8 +254,7 @@ export function ResultsTab({
|
|||||||
</Fieldset>
|
</Fieldset>
|
||||||
<Fieldset legend="Trajectory Analytics">
|
<Fieldset legend="Trajectory Analytics">
|
||||||
<p className="panel-note" style={{ marginBottom: 8 }}>
|
<p className="panel-note" style={{ marginBottom: 8 }}>
|
||||||
Click a row or 3D segment to cross-highlight. DLS is shown numerically only (deg per
|
Click a row or 3D segment to cross-highlight. DLS is shown numerically only ({dlsUnitLabel}).
|
||||||
100 ft MD).
|
|
||||||
</p>
|
</p>
|
||||||
<div className="table-scroll" style={{ maxHeight: 250 }}>
|
<div className="table-scroll" style={{ maxHeight: 250 }}>
|
||||||
<table className="data-table">
|
<table className="data-table">
|
||||||
@@ -251,7 +264,7 @@ export function ResultsTab({
|
|||||||
<th>MD start</th>
|
<th>MD start</th>
|
||||||
<th>MD end</th>
|
<th>MD end</th>
|
||||||
<th>ΔMD</th>
|
<th>ΔMD</th>
|
||||||
<th>DLS (deg/100)</th>
|
<th>DLS ({dlsUnitLabel})</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -449,6 +462,18 @@ export function ResultsTab({
|
|||||||
Categories mirror <code>docs/engineering/field-traceability.md</code> (
|
Categories mirror <code>docs/engineering/field-traceability.md</code> (
|
||||||
physics, metadata, parseCalibration, payloadInactive, parsedUnused).
|
physics, metadata, parseCalibration, payloadInactive, parsedUnused).
|
||||||
</p>
|
</p>
|
||||||
|
<div className="button-row" style={{ gap: 8, alignItems: "center" }}>
|
||||||
|
<input
|
||||||
|
className="panel-input"
|
||||||
|
value={traceabilityQuery}
|
||||||
|
placeholder="Filter by XML key/category/notes"
|
||||||
|
aria-label="Filter traceability rows"
|
||||||
|
onChange={(e) => setTraceabilityQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
<span className="panel-note">
|
||||||
|
Showing {filteredTraceability.length} / {result.fieldTraceability.fields.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div className="table-scroll" style={{ maxHeight: 280 }}>
|
<div className="table-scroll" style={{ maxHeight: 280 }}>
|
||||||
<table className="data-table">
|
<table className="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -456,14 +481,16 @@ export function ResultsTab({
|
|||||||
<th>XML field</th>
|
<th>XML field</th>
|
||||||
<th>Category</th>
|
<th>Category</th>
|
||||||
<th>In file</th>
|
<th>In file</th>
|
||||||
|
<th>Notes</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{result.fieldTraceability.fields.map((row) => (
|
{filteredTraceability.map((row) => (
|
||||||
<tr key={row.xmlKey} title={row.notes || undefined}>
|
<tr key={row.xmlKey} title={row.notes || undefined}>
|
||||||
<td className="mono">{row.xmlKey}</td>
|
<td className="mono">{row.xmlKey}</td>
|
||||||
<td>{row.category}</td>
|
<td>{row.category}</td>
|
||||||
<td>{row.presentInXml ? "yes" : "—"}</td>
|
<td>{row.presentInXml ? "yes" : "—"}</td>
|
||||||
|
<td>{row.notes}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ function diameterOptionsFor(rowDiameter: number) {
|
|||||||
export function RodStringTab({ store }: Props) {
|
export function RodStringTab({ store }: Props) {
|
||||||
const { state, addTaperRow, removeTaperRow, updateTaperRow, setTaper, update } = store;
|
const { state, addTaperRow, removeTaperRow, updateTaperRow, setTaper, update } = store;
|
||||||
const [taperLengthMode, setTaperLengthMode] = useState<"length" | "count">("length");
|
const [taperLengthMode, setTaperLengthMode] = useState<"length" | "count">("length");
|
||||||
|
const [compactRows, setCompactRows] = useState(false);
|
||||||
|
|
||||||
const depthUnit = state.unitsSelection === 1 ? "m" : "ft";
|
const depthUnit = state.unitsSelection === 1 ? "m" : "ft";
|
||||||
|
|
||||||
@@ -102,6 +103,13 @@ export function RodStringTab({ store }: Props) {
|
|||||||
<abbr title="Approximate catalog values for workflow only">heuristic</abbr> — verify for
|
<abbr title="Approximate catalog values for workflow only">heuristic</abbr> — verify for
|
||||||
your vendor tables.
|
your vendor tables.
|
||||||
</p>
|
</p>
|
||||||
|
{taperLengthMode === "count" && (
|
||||||
|
<p className="panel-note">
|
||||||
|
Joint counts are converted using <code>RodLengthForSteel</code> /{" "}
|
||||||
|
<code>RodLengthForFiberglass</code> from the case (fallbacks: 25 ft / 37.5 ft in
|
||||||
|
imperial). Non-integer equivalent counts are allowed.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="button-row">
|
<div className="button-row">
|
||||||
<span className="panel-note">Section length:</span>
|
<span className="panel-note">Section length:</span>
|
||||||
@@ -119,6 +127,15 @@ export function RodStringTab({ store }: Props) {
|
|||||||
>
|
>
|
||||||
Joint count
|
Joint count
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn ${compactRows ? "btn-primary" : ""}`}
|
||||||
|
onClick={() => setCompactRows((v) => !v)}
|
||||||
|
aria-pressed={compactRows}
|
||||||
|
aria-label={compactRows ? "Disable compact rows in rod table" : "Enable compact rows in rod table"}
|
||||||
|
>
|
||||||
|
{compactRows ? "Compact rows: ON" : "Compact rows: OFF"}
|
||||||
|
</button>
|
||||||
<button type="button" className="btn" onClick={() => addTaperRow()}>
|
<button type="button" className="btn" onClick={() => addTaperRow()}>
|
||||||
Add Section
|
Add Section
|
||||||
</button>
|
</button>
|
||||||
@@ -133,9 +150,12 @@ export function RodStringTab({ store }: Props) {
|
|||||||
{totals.length.toFixed(1)} {depthUnit}
|
{totals.length.toFixed(1)} {depthUnit}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="sr-only" aria-live="polite">
|
||||||
|
Rod table compact mode is {compactRows ? "enabled" : "disabled"}.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="table-scroll">
|
<div className="table-scroll">
|
||||||
<table className="data-table">
|
<table className={`data-table ${compactRows ? "data-table-compact" : ""}`}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={{ width: 36 }}>#</th>
|
<th style={{ width: 36 }}>#</th>
|
||||||
@@ -148,8 +168,8 @@ export function RodStringTab({ store }: Props) {
|
|||||||
<th>Guide</th>
|
<th>Guide</th>
|
||||||
<th># guides</th>
|
<th># guides</th>
|
||||||
<th>Guide type</th>
|
<th>Guide type</th>
|
||||||
<th>Rod guide wt</th>
|
<th>Guide wt (lb/ft)</th>
|
||||||
<th style={{ width: 56 }}></th>
|
<th className="table-actions-col" style={{ width: 56 }}></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -221,6 +241,7 @@ export function RodStringTab({ store }: Props) {
|
|||||||
<CheckboxField
|
<CheckboxField
|
||||||
id={`taper-${i}-guides`}
|
id={`taper-${i}-guides`}
|
||||||
label=""
|
label=""
|
||||||
|
ariaLabel={`Enable guides for taper ${i + 1}`}
|
||||||
checked={row.guidesEnabled}
|
checked={row.guidesEnabled}
|
||||||
onChange={(checked) =>
|
onChange={(checked) =>
|
||||||
updateTaperRow(i, {
|
updateTaperRow(i, {
|
||||||
@@ -244,6 +265,7 @@ export function RodStringTab({ store }: Props) {
|
|||||||
<SelectField
|
<SelectField
|
||||||
value={row.guideTypeToken}
|
value={row.guideTypeToken}
|
||||||
options={GUIDE_TYPE_OPTIONS}
|
options={GUIDE_TYPE_OPTIONS}
|
||||||
|
disabled={!row.guidesEnabled}
|
||||||
onChange={(v) => updateTaperRow(i, { guideTypeToken: v })}
|
onChange={(v) => updateTaperRow(i, { guideTypeToken: v })}
|
||||||
ariaLabel={`Taper ${i + 1} guide type`}
|
ariaLabel={`Taper ${i + 1} guide type`}
|
||||||
/>
|
/>
|
||||||
@@ -252,16 +274,18 @@ export function RodStringTab({ store }: Props) {
|
|||||||
<NumberField
|
<NumberField
|
||||||
value={row.rodGuideWeightLbfPerFt}
|
value={row.rodGuideWeightLbfPerFt}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
|
disabled={!row.guidesEnabled}
|
||||||
onChange={(v) => updateTaperRow(i, { rodGuideWeightLbfPerFt: v })}
|
onChange={(v) => updateTaperRow(i, { rodGuideWeightLbfPerFt: v })}
|
||||||
ariaLabel={`Taper ${i + 1} rod guide weight`}
|
ariaLabel={`Taper ${i + 1} rod guide weight`}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td className="table-actions-col">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-danger"
|
className="btn btn-danger"
|
||||||
onClick={() => removeTaperRow(i)}
|
onClick={() => removeTaperRow(i)}
|
||||||
aria-label={`Remove taper ${i + 1}`}
|
aria-label={`Remove taper ${i + 1}`}
|
||||||
|
title={`Remove taper row ${i + 1}`}
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import type { CaseStore } from "../../state/useCaseStore";
|
import type { CaseStore } from "../../state/useCaseStore";
|
||||||
import { Fieldset } from "../common/Fieldset";
|
import { Fieldset } from "../common/Fieldset";
|
||||||
import { NumberField } from "../common/NumberField";
|
import { NumberField } from "../common/NumberField";
|
||||||
@@ -7,6 +8,7 @@ type Props = { store: CaseStore };
|
|||||||
export function TrajectoryTab({ store }: Props) {
|
export function TrajectoryTab({ store }: Props) {
|
||||||
const { state, addSurveyRow, insertSurveyRowBelow, removeSurveyRow, updateSurveyRow, setSurvey } =
|
const { state, addSurveyRow, insertSurveyRowBelow, removeSurveyRow, updateSurveyRow, setSurvey } =
|
||||||
store;
|
store;
|
||||||
|
const [compactRows, setCompactRows] = useState(false);
|
||||||
|
|
||||||
function loadVertical() {
|
function loadVertical() {
|
||||||
const depth = state.pumpDepth || 1727;
|
const depth = state.pumpDepth || 1727;
|
||||||
@@ -40,6 +42,10 @@ export function TrajectoryTab({ store }: Props) {
|
|||||||
Enter survey stations from surface to TD. Minimum curvature method (API
|
Enter survey stations from surface to TD. Minimum curvature method (API
|
||||||
Bulletin D20) is applied in the solver. First row should be 0 MD, 0 Inc, 0 Az.
|
Bulletin D20) is applied in the solver. First row should be 0 MD, 0 Inc, 0 Az.
|
||||||
</p>
|
</p>
|
||||||
|
<p className="panel-note">
|
||||||
|
Use <strong>Insert below</strong> to add stations mid-trajectory without rebuilding the
|
||||||
|
full table.
|
||||||
|
</p>
|
||||||
<div className="button-row">
|
<div className="button-row">
|
||||||
<button type="button" className="btn" onClick={() => addSurveyRow()}>
|
<button type="button" className="btn" onClick={() => addSurveyRow()}>
|
||||||
Add Station
|
Add Station
|
||||||
@@ -53,20 +59,34 @@ export function TrajectoryTab({ store }: Props) {
|
|||||||
<button type="button" className="btn" onClick={loadDeviatedExample}>
|
<button type="button" className="btn" onClick={loadDeviatedExample}>
|
||||||
Load Deviated Example
|
Load Deviated Example
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn ${compactRows ? "btn-primary" : ""}`}
|
||||||
|
onClick={() => setCompactRows((v) => !v)}
|
||||||
|
aria-pressed={compactRows}
|
||||||
|
aria-label={
|
||||||
|
compactRows ? "Disable compact rows in trajectory table" : "Enable compact rows in trajectory table"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{compactRows ? "Compact rows: ON" : "Compact rows: OFF"}
|
||||||
|
</button>
|
||||||
<span className="panel-note" style={{ marginLeft: "auto" }}>
|
<span className="panel-note" style={{ marginLeft: "auto" }}>
|
||||||
{state.survey.length} station{state.survey.length === 1 ? "" : "s"}
|
{state.survey.length} station{state.survey.length === 1 ? "" : "s"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="sr-only" aria-live="polite">
|
||||||
|
Trajectory table compact mode is {compactRows ? "enabled" : "disabled"}.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="table-scroll">
|
<div className="table-scroll">
|
||||||
<table className="data-table">
|
<table className={`data-table ${compactRows ? "data-table-compact" : ""}`}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={{ width: 40 }}>#</th>
|
<th style={{ width: 40 }}>#</th>
|
||||||
<th>MD</th>
|
<th>MD</th>
|
||||||
<th>Inclination (°)</th>
|
<th>Inclination (°)</th>
|
||||||
<th>Azimuth (°)</th>
|
<th>Azimuth (°)</th>
|
||||||
<th style={{ width: 120 }}>Actions</th>
|
<th className="table-actions-col" style={{ width: 120 }}>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -100,13 +120,14 @@ export function TrajectoryTab({ store }: Props) {
|
|||||||
ariaLabel={`Azi ${i + 1}`}
|
ariaLabel={`Azi ${i + 1}`}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td className="table-actions-col">
|
||||||
<div className="button-row" style={{ gap: 4 }}>
|
<div className="button-row action-row-inline" style={{ gap: 4 }}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn"
|
className="btn"
|
||||||
onClick={() => insertSurveyRowBelow(i)}
|
onClick={() => insertSurveyRowBelow(i)}
|
||||||
title="Insert a station after this row"
|
title="Insert a station after this row"
|
||||||
|
aria-label={`Insert station below row ${i + 1}`}
|
||||||
>
|
>
|
||||||
Insert below
|
Insert below
|
||||||
</button>
|
</button>
|
||||||
@@ -124,7 +145,7 @@ export function TrajectoryTab({ store }: Props) {
|
|||||||
))}
|
))}
|
||||||
{state.survey.length === 0 && (
|
{state.survey.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} className="empty-row">
|
<td colSpan={5} className="empty-row">
|
||||||
No survey stations. Add rows or load a preset.
|
No survey stations. Add rows or load a preset.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user