feat(06-05): the 8-glyph committed maneuver set + glyphs.ts + the 18 guidance strings keys
- 8 hand-authored 32x32 SVGs per the UI-SPEC geometry table (3px stroke, square caps, orthogonal/45° only; uturn = machined staple, no arc) - glyphs.ts: GLYPH_KINDS/GLYPHS via Vite ?raw (committed files are the single source) + svgHost — the ONE sanctioned innerHTML site, a static import value - strings.ts: 18 ShellStrings keys appended in en + ms, values verbatim from the 06-UI-SPEC copywriting contract - copy-gate: COPY1 floor 40→58 + COPY8 pins all 18 guidance keys verbatim in both locales
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* glyphs.ts — the 8-glyph committed maneuver set (06-UI-SPEC "The 8-glyph set").
|
||||
* ------------------------------------------------------------------------------
|
||||
* The committed SVG files under ./glyphs/ are the SINGLE SOURCE: this module
|
||||
* imports them verbatim with Vite's `?raw` and never re-states their geometry.
|
||||
* The same files rasterize once to 256×256 PNGs for the chat turn-cards
|
||||
* (maintainer `build-glyphs.mjs`, plan 07) — one geometry, two renderings.
|
||||
* In-app the raw SVG renders inline: 32×32 on the banner's arrow plate,
|
||||
* 24×24 in the maneuver list. Strokes ride `currentColor` (signal-orange on
|
||||
* the banner plate, matte-black/outline per list row state) — the PNG build
|
||||
* injects the token literals; nothing here hardcodes a color (DSGN-01).
|
||||
*
|
||||
* svgHost is the shell's ONE sanctioned innerHTML site: the template parses
|
||||
* a STATIC import value (GLYPHS[kind], committed at build time), never a
|
||||
* name-derived or runtime string — the surface gate whitelists exactly this
|
||||
* line (guidance-surface structural case).
|
||||
*/
|
||||
import depart from "./glyphs/depart.svg?raw";
|
||||
import straight from "./glyphs/straight.svg?raw";
|
||||
import left from "./glyphs/left.svg?raw";
|
||||
import right from "./glyphs/right.svg?raw";
|
||||
import slightLeft from "./glyphs/slight-left.svg?raw";
|
||||
import slightRight from "./glyphs/slight-right.svg?raw";
|
||||
import uturn from "./glyphs/uturn.svg?raw";
|
||||
import arrive from "./glyphs/arrive.svg?raw";
|
||||
|
||||
/** The maneuver vocabulary the UI-SPEC commits (8 kinds, trip-order names —
|
||||
* RouteStep.kind maps onto these in the controller). */
|
||||
export type GlyphKind =
|
||||
| "depart"
|
||||
| "straight"
|
||||
| "left"
|
||||
| "right"
|
||||
| "slight-left"
|
||||
| "slight-right"
|
||||
| "uturn"
|
||||
| "arrive";
|
||||
|
||||
/** The 8 kinds in committed order (depart → arrive). */
|
||||
export const GLYPH_KINDS: readonly GlyphKind[] = [
|
||||
"depart",
|
||||
"straight",
|
||||
"left",
|
||||
"right",
|
||||
"slight-left",
|
||||
"slight-right",
|
||||
"uturn",
|
||||
"arrive",
|
||||
];
|
||||
|
||||
/** kind → raw SVG string of the committed file (the single source). */
|
||||
export const GLYPHS: Record<GlyphKind, string> = {
|
||||
depart,
|
||||
straight,
|
||||
left,
|
||||
right,
|
||||
"slight-left": slightLeft,
|
||||
"slight-right": slightRight,
|
||||
uturn,
|
||||
arrive,
|
||||
};
|
||||
|
||||
/** Parse a committed glyph into a live inline-SVG element. The caller sizes
|
||||
* the host (banner 32×32, list 24×24); color rides currentColor. */
|
||||
export function svgHost(kind: GlyphKind): SVGSVGElement {
|
||||
const template = document.createElement("template");
|
||||
// THE one sanctioned innerHTML site: a static import value — never a
|
||||
// name-derived or runtime string (the surface gate's exact whitelist).
|
||||
template.innerHTML = GLYPHS[kind];
|
||||
return template.content.firstElementChild as SVGSVGElement;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter" aria-hidden="true"><path d="M16 2 V8"/><rect x="10" y="8" width="12" height="12" fill="currentColor" stroke="none"/><polygon points="14.5,20 17.5,20 16,28" fill="currentColor" stroke="none"/></svg>
|
||||
|
After Width: | Height: | Size: 357 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter" aria-hidden="true"><rect x="14" y="24" width="4" height="4" fill="currentColor" stroke="none"/><path d="M16 24 V4"/></svg>
|
||||
|
After Width: | Height: | Size: 282 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter" aria-hidden="true"><path d="M16 28 V14 H8"/><path d="M13 9 L8 14 L13 19"/></svg>
|
||||
|
After Width: | Height: | Size: 240 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter" aria-hidden="true"><path d="M16 28 V14 H24"/><path d="M19 9 L24 14 L19 19"/></svg>
|
||||
|
After Width: | Height: | Size: 242 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter" aria-hidden="true"><path d="M20 28 L10 18"/><path d="M10 23 L10 18 L15 18"/></svg>
|
||||
|
After Width: | Height: | Size: 242 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter" aria-hidden="true"><path d="M12 28 L22 18"/><path d="M22 23 L22 18 L17 18"/></svg>
|
||||
|
After Width: | Height: | Size: 242 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter" aria-hidden="true"><path d="M16 28 V4"/><path d="M8 12 L16 4 L24 12"/></svg>
|
||||
|
After Width: | Height: | Size: 236 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter" aria-hidden="true"><path d="M22 28 V8 H10 V28"/></svg>
|
||||
|
After Width: | Height: | Size: 214 B |
@@ -6,7 +6,8 @@
|
||||
* module, and the copy gate (web/test/copy-gate.test.mjs COPY1..COPY7) keeps
|
||||
* it verbatim against the approved UI-SPEC table in en + ms. Register labels
|
||||
* (`BOOT-01 //`, `BOOT-02 //`, `GEO // LOCATION`, `SYS // POSITION`, `ROUTE`,
|
||||
* `ROUTE // ROUTER`) are instrument vocabulary: identical in both locales by
|
||||
* `ROUTE // ROUTER`, `GUIDE`) are instrument vocabulary: identical in both
|
||||
* locales by
|
||||
* design. Tone is clinical + honest (D-01): plain statements of what is
|
||||
* stored and what is not.
|
||||
*
|
||||
@@ -58,6 +59,48 @@ export interface ShellStrings {
|
||||
ariaRouteAlternatives: string;
|
||||
/** Template — {n}/{min}/{km} placeholders are replaced in code. */
|
||||
ariaRouteChip: string;
|
||||
|
||||
/* — Phase 6 guidance chrome (06-UI-SPEC §Copywriting Contract; D-06/D-09/D-14).
|
||||
Every guidance word lives here in en + ms — never inline (the copy gates).
|
||||
Register labels (GUIDE, GUIDE // NEXT/REROUTING/…, GUIDE // T-nn) are
|
||||
instrument vocabulary composed in code from guideRegister. Tone: clinical,
|
||||
honest — the privacy promise stated once, at the exact moment it executes. */
|
||||
/** The banner/card register base word — composed with the state word in code. */
|
||||
guideRegister: string;
|
||||
/** Fallback street line for an unnamed OSM segment (D-06 street slot). */
|
||||
guideStreetUnnamed: string;
|
||||
/** REROUTING calm-state line — no blame, no apology (D-04). */
|
||||
guideReroutingLine: string;
|
||||
/** RESUMING projected-state line until the first live fix (D-06/D-11). */
|
||||
guideResumingLine: string;
|
||||
/** NO REROUTE banner line — router-pending degradation (D-03). */
|
||||
guideNoRerouteLine: string;
|
||||
/** NO REROUTE card state line — the longer honest copy (D-03). */
|
||||
guideNoRerouteBody: string;
|
||||
/** Banner toggle word, collapsed state (real-word affordance, no chevron). */
|
||||
ctaBannerList: string;
|
||||
/** Banner toggle word, expanded state. */
|
||||
ctaBannerClose: string;
|
||||
/** The guidance card's ONLY action — opens the abandon confirm card (D-09). */
|
||||
ctaEndTrip: string;
|
||||
/** The abandon-route confirm card (D-09/PX-03) — the one destructive guard. */
|
||||
guideConfirmHeadline: string;
|
||||
/** The privacy clause stated at the exact moment it executes (D-14/PRIV-01). */
|
||||
guideConfirmBody: string;
|
||||
/** Confirm primary CTA — deliberately NOT red (deliberate, not an error). */
|
||||
ctaConfirmAbandon: string;
|
||||
/** Confirm dismiss — guidance continues. */
|
||||
ctaConfirmContinue: string;
|
||||
/** Trip-ended card (D-14) — ONE card for arrival and abandon. */
|
||||
guideEndHeadline: string;
|
||||
/** Trip-end deletion copy — the TTL law's promise, visible. */
|
||||
guideEndBody: string;
|
||||
/** The maneuver list region label (list = labelled region of real text rows). */
|
||||
ariaManeuverList: string;
|
||||
/** The banner button label (one toggle: aria-expanded + aria-controls). */
|
||||
ariaBannerToggle: string;
|
||||
/** The guidance card's progress bar label. */
|
||||
ariaTripProgress: string;
|
||||
}
|
||||
|
||||
export type Locale = "en" | "ms";
|
||||
@@ -106,6 +149,26 @@ export const strings: Record<Locale, ShellStrings> = {
|
||||
ctaSearchAgain: "SEARCH AGAIN",
|
||||
ariaRouteAlternatives: "Alternative routes",
|
||||
ariaRouteChip: "Route {n}: {min} minutes, {km} kilometres",
|
||||
|
||||
guideRegister: "GUIDE",
|
||||
guideStreetUnnamed: "Unnamed road",
|
||||
guideReroutingLine: "Finding a new route from where you are.",
|
||||
guideResumingLine: "Resuming from your last known progress.",
|
||||
guideNoRerouteLine: "Rerouting unavailable on this server.",
|
||||
guideNoRerouteBody: "No road graph here — follow the drawn route; recalculation is unavailable.",
|
||||
ctaBannerList: "LIST",
|
||||
ctaBannerClose: "CLOSE",
|
||||
ctaEndTrip: "END TRIP",
|
||||
guideConfirmHeadline: "Abandon route?",
|
||||
guideConfirmBody: "Guidance stops and this trip's record is deleted. Nothing is stored.",
|
||||
ctaConfirmAbandon: "YES, ABANDON",
|
||||
ctaConfirmContinue: "CONTINUE",
|
||||
guideEndHeadline: "Trip ended",
|
||||
guideEndBody:
|
||||
"Route guidance stopped and the trip record is deleted — nothing about this drive remains.",
|
||||
ariaManeuverList: "Full maneuver list",
|
||||
ariaBannerToggle: "Next maneuver",
|
||||
ariaTripProgress: "Trip progress",
|
||||
},
|
||||
ms: {
|
||||
bootValidatingRegister: "BOOT-01 // SESSION",
|
||||
@@ -150,6 +213,26 @@ export const strings: Record<Locale, ShellStrings> = {
|
||||
ctaSearchAgain: "CARI SEMULA",
|
||||
ariaRouteAlternatives: "Laluan alternatif",
|
||||
ariaRouteChip: "Laluan {n}: {min} minit, {km} kilometer",
|
||||
|
||||
guideRegister: "GUIDE",
|
||||
guideStreetUnnamed: "Jalan tanpa nama",
|
||||
guideReroutingLine: "Mencari laluan baharu dari kedudukan anda.",
|
||||
guideResumingLine: "Sambung semula dari kemajuan terakhir anda.",
|
||||
guideNoRerouteLine: "Pengiraan semula tiada pada pelayan ini.",
|
||||
guideNoRerouteBody: "Tiada graf jalan di sini — ikut laluan yang dilukis; pengiraan semula tidak tersedia.",
|
||||
ctaBannerList: "SENARAI",
|
||||
ctaBannerClose: "TUTUP",
|
||||
ctaEndTrip: "TAMAT PERJALANAN",
|
||||
guideConfirmHeadline: "Tinggalkan laluan?",
|
||||
guideConfirmBody: "Panduan berhenti dan rekod perjalanan ini dipadam. Tiada apa-apa disimpan.",
|
||||
ctaConfirmAbandon: "YA, TINGGALKAN",
|
||||
ctaConfirmContinue: "TERUSKAN",
|
||||
guideEndHeadline: "Perjalanan tamat",
|
||||
guideEndBody:
|
||||
"Panduan laluan dihentikan dan rekod perjalanan dipadam — tiada apa-apa tentang pemanduan ini kekal.",
|
||||
ariaManeuverList: "Senarai penuh manuver",
|
||||
ariaBannerToggle: "Manuver seterusnya",
|
||||
ariaTripProgress: "Kemajuan perjalanan",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -24,12 +24,15 @@ test("COPY1 en and ms carry identical key-sets (parity gate, both locales)", ()
|
||||
const enKeys = Object.keys(strings.en).sort();
|
||||
const msKeys = Object.keys(strings.ms).sort();
|
||||
assert.deepEqual(msKeys, enKeys, "ms missing/duplicated keys vs en (D-05 locale parity)");
|
||||
assert.ok(enKeys.length >= 40, `expected the full ShellStrings surface (40 keys incl. the 10 route keys), found ${enKeys.length}`);
|
||||
assert.ok(enKeys.length >= 58, `expected the full ShellStrings surface (58 keys incl. the 10 route + 18 guidance keys), found ${enKeys.length}`);
|
||||
assert.ok(enKeys.includes("deniedRegister"), "deniedRegister is a keys — cards.ts renders GEO //");
|
||||
assert.ok(enKeys.includes("lostRegister"), "lostRegister is a key — cards.ts renders SYS //");
|
||||
assert.ok(enKeys.includes("routeRegister"), "routeRegister is a key — route-card.ts renders ROUTE");
|
||||
assert.ok(enKeys.includes("routePendingRegister"), "routePendingRegister is a key — the pending card renders ROUTE // ROUTER");
|
||||
assert.ok(enKeys.includes("ctaSearchAgain"), "ctaSearchAgain is a key — the D-10 restart affordance");
|
||||
assert.ok(enKeys.includes("guideRegister"), "guideRegister is a key — the banner/card renders GUIDE");
|
||||
assert.ok(enKeys.includes("ctaEndTrip"), "ctaEndTrip is a key — the guidance card's only action");
|
||||
assert.ok(enKeys.includes("guideConfirmHeadline"), "guideConfirmHeadline is a key — the D-09 confirm guard");
|
||||
});
|
||||
|
||||
/* ————— COPY2 — denied copy names location ————— */
|
||||
@@ -131,3 +134,68 @@ test("COPY7 route copy verbatim (the 10 route keys, en + ms)", () => {
|
||||
"instrument vocabulary — identical in both locales",
|
||||
);
|
||||
});
|
||||
|
||||
/* ————— COPY8 — guidance copy verbatim (the 18 guidance keys, en + ms) ————— */
|
||||
|
||||
test("COPY8 guidance copy verbatim (the 18 guidance keys, en + ms, 06-UI-SPEC table)", () => {
|
||||
// register + street fallback
|
||||
assert.equal(strings.en.guideRegister, "GUIDE");
|
||||
assert.equal(strings.ms.guideRegister, "GUIDE", "instrument vocabulary — identical in both locales");
|
||||
assert.equal(strings.en.guideStreetUnnamed, "Unnamed road");
|
||||
assert.equal(strings.ms.guideStreetUnnamed, "Jalan tanpa nama");
|
||||
// the calm banner states (D-04/D-06/D-03)
|
||||
assert.equal(strings.en.guideReroutingLine, "Finding a new route from where you are.");
|
||||
assert.equal(strings.ms.guideReroutingLine, "Mencari laluan baharu dari kedudukan anda.");
|
||||
assert.equal(strings.en.guideResumingLine, "Resuming from your last known progress.");
|
||||
assert.equal(strings.ms.guideResumingLine, "Sambung semula dari kemajuan terakhir anda.");
|
||||
assert.equal(strings.en.guideNoRerouteLine, "Rerouting unavailable on this server.");
|
||||
assert.equal(strings.ms.guideNoRerouteLine, "Pengiraan semula tiada pada pelayan ini.");
|
||||
assert.equal(
|
||||
strings.en.guideNoRerouteBody,
|
||||
"No road graph here — follow the drawn route; recalculation is unavailable.",
|
||||
);
|
||||
assert.equal(
|
||||
strings.ms.guideNoRerouteBody,
|
||||
"Tiada graf jalan di sini — ikut laluan yang dilukis; pengiraan semula tidak tersedia.",
|
||||
);
|
||||
// banner toggle + the guidance card's single action
|
||||
assert.equal(strings.en.ctaBannerList, "LIST");
|
||||
assert.equal(strings.ms.ctaBannerList, "SENARAI");
|
||||
assert.equal(strings.en.ctaBannerClose, "CLOSE");
|
||||
assert.equal(strings.ms.ctaBannerClose, "TUTUP");
|
||||
assert.equal(strings.en.ctaEndTrip, "END TRIP");
|
||||
assert.equal(strings.ms.ctaEndTrip, "TAMAT PERJALANAN");
|
||||
// the D-09 confirm guard — the exact privacy copy at the moment it executes
|
||||
assert.equal(strings.en.guideConfirmHeadline, "Abandon route?");
|
||||
assert.equal(strings.ms.guideConfirmHeadline, "Tinggalkan laluan?");
|
||||
assert.equal(
|
||||
strings.en.guideConfirmBody,
|
||||
"Guidance stops and this trip's record is deleted. Nothing is stored.",
|
||||
);
|
||||
assert.equal(
|
||||
strings.ms.guideConfirmBody,
|
||||
"Panduan berhenti dan rekod perjalanan ini dipadam. Tiada apa-apa disimpan.",
|
||||
);
|
||||
assert.equal(strings.en.ctaConfirmAbandon, "YES, ABANDON");
|
||||
assert.equal(strings.ms.ctaConfirmAbandon, "YA, TINGGALKAN");
|
||||
assert.equal(strings.en.ctaConfirmContinue, "CONTINUE");
|
||||
assert.equal(strings.ms.ctaConfirmContinue, "TERUSKAN");
|
||||
// the D-14 trip-end card — ONE card for arrival and abandon
|
||||
assert.equal(strings.en.guideEndHeadline, "Trip ended");
|
||||
assert.equal(strings.ms.guideEndHeadline, "Perjalanan tamat");
|
||||
assert.equal(
|
||||
strings.en.guideEndBody,
|
||||
"Route guidance stopped and the trip record is deleted — nothing about this drive remains.",
|
||||
);
|
||||
assert.equal(
|
||||
strings.ms.guideEndBody,
|
||||
"Panduan laluan dihentikan dan rekod perjalanan dipadam — tiada apa-apa tentang pemanduan ini kekal.",
|
||||
);
|
||||
// aria labels
|
||||
assert.equal(strings.en.ariaManeuverList, "Full maneuver list");
|
||||
assert.equal(strings.ms.ariaManeuverList, "Senarai penuh manuver");
|
||||
assert.equal(strings.en.ariaBannerToggle, "Next maneuver");
|
||||
assert.equal(strings.ms.ariaBannerToggle, "Manuver seterusnya");
|
||||
assert.equal(strings.en.ariaTripProgress, "Trip progress");
|
||||
assert.equal(strings.ms.ariaTripProgress, "Kemajuan perjalanan");
|
||||
});
|
||||
|
||||