feat(06-05): guidance/confirm/trip-end card builders + the token-only shell.css guidance block (GREEN)
- buildGuidanceCard: GUIDE register, verbatim headline, live telemetry row (MIN / {km} KM / ETA), 4px progressbar with full aria value set + instant inline width, no-reroute state line only when degrading, ONE btn-secondary END TRIP
- buildConfirmCard: GUIDE // CONFIRM + exact en/ms privacy copy, btn-primary YES ABANDON + btn-secondary CONTINUE, guide-confirm bottom-register hook — the ONE primary CTA in guidance chrome
- buildTripEndCard: GUIDE // END, exact deletion copy, no actions
- shell.css guidance block: banner chassis + 1px separator, CSS-only strip compaction under [data-guidance], list panel + row states + lamp, progress bar — token vars only, zero motion/depth/radius/hex (theme audit clean)
This commit is contained in:
@@ -276,3 +276,181 @@ function buildManeuverRow(row: ManeuverRowView): HTMLElement {
|
||||
el.append(glyphWrap, street, dist);
|
||||
return el;
|
||||
}
|
||||
|
||||
/* ————— the guidance card (D-06 — the route card evolved, bottom register) ————— */
|
||||
|
||||
export interface GuidanceCardView {
|
||||
/** Verbatim destination name — the continuity anchor. */
|
||||
destinationName: string;
|
||||
/** Deadband-smoothed minutes remaining (D-07 — the controller passes the
|
||||
* display values; this builder is a pure DOM composition). */
|
||||
minutesRemaining: number;
|
||||
/** Remaining kilometres (model-derived, D-07). */
|
||||
kmRemaining: number;
|
||||
/** The 24-hour HH:MM arrival clock — precomposed by the eta module. */
|
||||
etaClock: string;
|
||||
/** Snapped progress 0..100 — the bar STEPS per accepted fix, never glides. */
|
||||
progressPct: number;
|
||||
/** The no-reroute degradation is the ONLY state that adds a state line. */
|
||||
showNoReroute: boolean;
|
||||
}
|
||||
|
||||
export interface GuidanceCardActions {
|
||||
/** END TRIP → the abandon confirm card (the D-09 surface every exit reaches). */
|
||||
onEndTrip(): void;
|
||||
}
|
||||
|
||||
/** The route card's slots evolved in place: same card class, GUIDE register,
|
||||
* live telemetry row (minutes + MIN + {km} KM + ETA), the 4px progress bar,
|
||||
* the honest state line only in no-reroute, and ONE .btn-secondary —
|
||||
* NO .btn-primary here: the drive's forward action is driving. */
|
||||
export function buildGuidanceCard(
|
||||
locale: Locale,
|
||||
view: GuidanceCardView,
|
||||
actions: GuidanceCardActions,
|
||||
): HTMLElement {
|
||||
const loc = strings[locale];
|
||||
const card = document.createElement("div");
|
||||
card.className = "card";
|
||||
|
||||
const header = document.createElement("div");
|
||||
header.className = "card-header";
|
||||
const ref = document.createElement("span");
|
||||
ref.className = "card-ref";
|
||||
ref.textContent = loc.guideRegister;
|
||||
header.appendChild(ref);
|
||||
|
||||
const headline = document.createElement("h2");
|
||||
headline.className = "text-headline-md";
|
||||
headline.textContent = view.destinationName; // verbatim gazetteer name — textContent only
|
||||
|
||||
const telemetry = document.createElement("div");
|
||||
telemetry.className = "route-telemetry"; // the route card's row class, evolved
|
||||
const minutes = document.createElement("span");
|
||||
minutes.className = "text-telemetry-num";
|
||||
minutes.textContent = String(Math.round(view.minutesRemaining));
|
||||
const minUnit = document.createElement("span");
|
||||
minUnit.className = "text-label-sm";
|
||||
minUnit.textContent = "MIN"; // instrument constant, both locales
|
||||
const km = document.createElement("span");
|
||||
km.className = "text-label-sm";
|
||||
km.textContent = `${view.kmRemaining.toFixed(1)} KM`;
|
||||
const eta = document.createElement("span");
|
||||
eta.className = "text-label-sm";
|
||||
eta.textContent = `ETA ${view.etaClock}`;
|
||||
telemetry.append(minutes, minUnit, km, eta);
|
||||
|
||||
const progress = document.createElement("div");
|
||||
progress.className = "guide-progress";
|
||||
progress.setAttribute("role", "progressbar");
|
||||
progress.setAttribute("aria-valuemin", "0");
|
||||
progress.setAttribute("aria-valuemax", "100");
|
||||
progress.setAttribute("aria-valuenow", String(Math.round(view.progressPct)));
|
||||
progress.setAttribute("aria-label", loc.ariaTripProgress);
|
||||
const fill = document.createElement("div");
|
||||
fill.className = "guide-progress-fill";
|
||||
fill.style.width = `${view.progressPct}%`; // instant width set, never a transition
|
||||
progress.appendChild(fill);
|
||||
|
||||
card.append(header, headline, telemetry, progress);
|
||||
|
||||
if (view.showNoReroute) {
|
||||
const stateLine = document.createElement("p");
|
||||
stateLine.className = "text-body-sm guide-state"; // standard ink — no red, a deferral not an error
|
||||
stateLine.textContent = loc.guideNoRerouteBody;
|
||||
card.appendChild(stateLine);
|
||||
}
|
||||
|
||||
const actionsRow = document.createElement("div");
|
||||
actionsRow.className = "card-actions";
|
||||
actionsRow.appendChild(secondaryButton(locale, loc.ctaEndTrip, actions.onEndTrip));
|
||||
card.appendChild(actionsRow);
|
||||
return card;
|
||||
}
|
||||
|
||||
/* ————— the abandon-route confirm card (D-09/PX-03 — the ONE guard) ————— */
|
||||
|
||||
export interface ConfirmCardActions {
|
||||
/** YES, ABANDON → guidance teardown: chrome clears, envelope DELETE, watch stops. */
|
||||
onAbandon(): void;
|
||||
/** CONTINUE → the card dismisses, guidance continues (no dead ends). */
|
||||
onContinue(): void;
|
||||
}
|
||||
|
||||
/** One confirm surface for ALL guidance exits (END TRIP, BackButton, the
|
||||
* native-close net's return path). Bottom register (thumb-reach — the
|
||||
* mount reads the guide-confirm class). YES, ABANDON is .btn-primary —
|
||||
* the working forward path, deliberately NOT red (abandoning is a
|
||||
* deliberate act, not an error; orange = primary action semantics). */
|
||||
export function buildConfirmCard(locale: Locale, actions: ConfirmCardActions): HTMLElement {
|
||||
const loc = strings[locale];
|
||||
const card = document.createElement("div");
|
||||
card.className = "card guide-confirm"; // guide-confirm: the bottom-register placement hook
|
||||
|
||||
const header = document.createElement("div");
|
||||
header.className = "card-header";
|
||||
const ref = document.createElement("span");
|
||||
ref.className = "card-ref";
|
||||
ref.textContent = `${loc.guideRegister} // CONFIRM`; // instrument vocabulary, en both locales
|
||||
header.appendChild(ref);
|
||||
|
||||
const headline = document.createElement("h2");
|
||||
headline.className = "text-headline-md";
|
||||
headline.textContent = loc.guideConfirmHeadline;
|
||||
|
||||
const body = document.createElement("p");
|
||||
body.className = "text-body-sm";
|
||||
body.textContent = loc.guideConfirmBody;
|
||||
|
||||
const actionsRow = document.createElement("div");
|
||||
actionsRow.className = "card-actions";
|
||||
const abandon = document.createElement("button");
|
||||
abandon.type = "button";
|
||||
abandon.className = "btn-primary"; // the ONE primary CTA in all guidance chrome
|
||||
abandon.textContent = loc.ctaConfirmAbandon;
|
||||
abandon.addEventListener("click", actions.onAbandon);
|
||||
const keepGoing = secondaryButton(locale, loc.ctaConfirmContinue, actions.onContinue);
|
||||
actionsRow.append(abandon, keepGoing);
|
||||
|
||||
card.append(header, headline, body, actionsRow);
|
||||
return card;
|
||||
}
|
||||
|
||||
/* ————— the trip-ended card (D-14 — PRIV-01's in-app half, visible) ————— */
|
||||
|
||||
/** ONE card for arrival and abandon — both delete, one message. No actions:
|
||||
* the card persists until the next action (no silent auto-dismiss — the
|
||||
* honesty law); the BackButton walks to the search root. */
|
||||
export function buildTripEndCard(locale: Locale): HTMLElement {
|
||||
const loc = strings[locale];
|
||||
const card = document.createElement("div");
|
||||
card.className = "card";
|
||||
|
||||
const header = document.createElement("div");
|
||||
header.className = "card-header";
|
||||
const ref = document.createElement("span");
|
||||
ref.className = "card-ref";
|
||||
ref.textContent = `${loc.guideRegister} // END`; // instrument vocabulary, en both locales
|
||||
header.appendChild(ref);
|
||||
|
||||
const headline = document.createElement("h2");
|
||||
headline.className = "text-headline-md";
|
||||
headline.textContent = loc.guideEndHeadline;
|
||||
|
||||
const body = document.createElement("p");
|
||||
body.className = "text-body-sm";
|
||||
body.textContent = loc.guideEndBody;
|
||||
|
||||
card.append(header, headline, body);
|
||||
return card;
|
||||
}
|
||||
|
||||
/** The one secondary-button shape (btn-secondary — never primary here). */
|
||||
function secondaryButton(locale: Locale, label: string, onActivate: () => void): HTMLButtonElement {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "btn-secondary";
|
||||
button.textContent = label;
|
||||
button.addEventListener("click", onActivate);
|
||||
return button;
|
||||
}
|
||||
|
||||
@@ -137,3 +137,71 @@ main { position: fixed; inset: 0; }
|
||||
|
||||
/* No CSS transition or animation anywhere — state changes are instant hard swaps. */
|
||||
/* DEBUG-ROUND probe styles removed at the device-matrix exit (2026-10-23). */
|
||||
|
||||
/* ————— guidance cockpit (Phase 6, 06-UI-SPEC): the maneuver banner, the
|
||||
compacted strip, the maneuver list, the guidance-card rows + progress bar.
|
||||
Token vars ONLY (the sanctioned machined-geometry literal list: banner
|
||||
min-height 96px, arrow plate 48px + glyph 32px, compact strip 24px, row
|
||||
48px + glyph 24px, next-row lamp 8px, panel max 320px/480px, bar 4px, 1px
|
||||
rules). States are instant hard swaps — zero motion, zero depth, zero
|
||||
rounded corners (buttons are radius-reset in the theme base layer). ————— */
|
||||
|
||||
/* the banner band: full-bleed matte-black chassis under the safe inset; the
|
||||
whole banner is the list toggle (the 96px band is the >=44px hit target) */
|
||||
.guide-banner {
|
||||
position: absolute; top: calc(var(--safe-top, 0px) + var(--spacing-space-sm)); left: 0; right: 0;
|
||||
min-height: 96px; padding: var(--spacing-space-sm) var(--spacing-space-md);
|
||||
display: flex; flex-direction: column; gap: var(--spacing-space-xs);
|
||||
align-items: stretch; text-align: left; border: 0; font: inherit;
|
||||
background: var(--color-matte-black); color: var(--color-pure-white);
|
||||
border-bottom: 1px solid var(--color-chassis-subtle); /* banner→strip separator */
|
||||
z-index: 3; /* above the list tap-out — the banner stays the toggle (CLOSE) */
|
||||
}
|
||||
.guide-banner:focus-visible { outline: 3px solid var(--color-signal-orange); outline-offset: 2px; } /* the route-chip bracket precedent */
|
||||
.guide-banner .guide-top { display: flex; align-items: center; justify-content: space-between; color: var(--color-chassis-subtle); }
|
||||
.guide-banner .guide-body { display: flex; align-items: center; gap: var(--spacing-space-sm); flex: 1; min-width: 0; }
|
||||
.guide-banner .guide-plate { width: 48px; height: 48px; flex: none; display: flex; align-items: center; justify-content: center; background: var(--color-surface-dark); color: var(--color-signal-orange); }
|
||||
.guide-banner .guide-plate svg { width: 32px; height: 32px; display: block; }
|
||||
.guide-banner .guide-measures { display: flex; flex-direction: column; gap: var(--spacing-space-xs); flex: 1; min-width: 0; }
|
||||
.guide-banner .guide-distance-row { display: flex; align-items: baseline; gap: var(--spacing-space-xs); }
|
||||
.guide-banner .guide-distance { color: var(--color-pure-white); } /* the glance datum — maximum contrast ink */
|
||||
.guide-banner .guide-unit { color: var(--color-chassis-subtle); }
|
||||
.guide-banner .guide-street-row { display: flex; align-items: center; gap: var(--spacing-space-xs); min-width: 0; }
|
||||
.guide-banner .guide-street { color: var(--color-soft-beige); text-transform: uppercase; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.guide-banner .guide-gantry { width: 8px; height: 8px; flex: none; border: 1px solid var(--color-chassis-subtle); }
|
||||
.guide-banner .guide-gantry[data-fill="on"] { background: var(--color-signal-orange); }
|
||||
|
||||
/* strip compaction during guidance — LAYOUT ONLY (the shell root carries
|
||||
data-guidance="active"); lamp, word and degradation states byte-identical */
|
||||
[data-guidance="active"] .strip {
|
||||
top: calc(var(--safe-top, 0px) + var(--spacing-space-sm) + 96px + 1px);
|
||||
min-height: 24px;
|
||||
padding-top: var(--spacing-space-xs); padding-bottom: var(--spacing-space-xs);
|
||||
}
|
||||
[data-guidance="active"] .strip .telemetry { display: none; } /* accuracy/fix-age are not glance data at speed */
|
||||
|
||||
/* the maneuver list: transparent tap-out + card panel over the map —
|
||||
no dim layer (boot-card precedent); any map tap collapses the list */
|
||||
.guide-list-wrap { position: absolute; inset: 0; z-index: 2; display: flex; align-items: flex-start; justify-content: center; pointer-events: none; }
|
||||
.guide-list-tapout { position: absolute; inset: 0; background: transparent; pointer-events: auto; }
|
||||
.guide-list { pointer-events: auto; position: relative; width: calc(100% - 2 * var(--spacing-margin)); max-width: 320px; max-height: 480px; overflow-y: auto; padding-bottom: var(--spacing-space-sm); margin-top: calc(var(--safe-top, 0px) + var(--spacing-space-sm) + 96px + 1px + 24px + var(--spacing-space-sm)); }
|
||||
.guide-row { display: flex; align-items: center; gap: var(--spacing-space-sm); min-height: 48px; padding: var(--spacing-space-sm) var(--spacing-space-md); }
|
||||
.guide-row + .guide-row { border-top: 1px solid var(--color-matte-black); }
|
||||
[data-tg-theme="dark"] .guide-row + .guide-row { border-top-color: var(--color-inverse-on-surface); }
|
||||
.guide-row .guide-row-glyph { display: flex; align-items: center; flex: none; color: var(--color-matte-black); }
|
||||
[data-tg-theme="dark"] .guide-row .guide-row-glyph { color: var(--color-inverse-on-surface); }
|
||||
.guide-row .guide-row-glyph svg { width: 24px; height: 24px; display: block; }
|
||||
.guide-row[data-row-state="next"] .guide-row-glyph { color: var(--color-signal-orange); }
|
||||
.guide-row[data-row-state="past"] .guide-row-glyph { color: var(--color-outline); }
|
||||
.guide-row .guide-row-street { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.guide-row[data-row-state="past"] .guide-row-street,
|
||||
.guide-row[data-row-state="past"] .guide-row-dist { color: var(--color-outline); } /* muted history, not hidden */
|
||||
.guide-row .guide-row-dist { margin-left: auto; flex: none; }
|
||||
.guide-row .guide-lamp { width: 8px; height: 8px; flex: none; background: var(--color-signal-orange); }
|
||||
|
||||
/* guidance-card instruments (composing with the existing .card child rules —
|
||||
no overrides; the confirm card's bottom placement is the guide-confirm
|
||||
class hook, read by the mount) */
|
||||
.guide-progress { height: 4px; margin: var(--spacing-space-xs) var(--spacing-space-md) 0; background: var(--color-chassis-subtle); border: 1px solid var(--color-matte-black); } /* checkbox geometry — distinguishable without color */
|
||||
[data-tg-theme="dark"] .guide-progress { border-color: var(--color-inverse-on-surface); }
|
||||
.guide-progress-fill { height: 100%; background: var(--color-signal-orange); }
|
||||
|
||||
Reference in New Issue
Block a user