diff --git a/web/scripts/hangar.test.mjs b/web/scripts/hangar.test.mjs index 02684e5..7b1e85c 100644 --- a/web/scripts/hangar.test.mjs +++ b/web/scripts/hangar.test.mjs @@ -417,6 +417,29 @@ test("a chassis with a space is searchable by name", () => { ); }); +test("the filter reference says what is not supported yet, and marks it", async () => { + // A player who tries equipment:"ER PPC" and gets nothing deserves to know + // whether they got the syntax wrong or asked the library for something it + // does not carry. + const hangar = await readFile(`${web}src/screens/hangar.ts`, "utf8"); + for (const missing of ["equipment", "quirks", "alpha", "engine", "faction"]) { + assert.match( + hangar, + new RegExp(`soon\\("${missing}"`), + `${missing} is not listed as unsupported`, + ); + } + assert.match(hangar, /coming soon/, "nothing marks the unsupported terms"); + // And they are not silently accepted by the parser either. + assert.equal(parseQuery('equipment:"ER PPC"').filters.length, 0); +}); + +test("the reference is a dialog rather than a caption", async () => { + const hangar = await readFile(`${web}src/screens/hangar.ts`, "utf8"); + assert.match(hangar, /el\("dialog", \{ className: "hangar-help" \}/); + assert.match(hangar, /syntaxDialog\.showModal\(\)/); +}); + test("every filter the builder can make is a term the box can hold", async () => { // The builder writes into the query rather than holding a filter of its // own, which is what keeps "expressible as text" true by construction diff --git a/web/src/screens/hangar.ts b/web/src/screens/hangar.ts index 9a4b206..e62a94f 100644 --- a/web/src/screens/hangar.ts +++ b/web/src/screens/hangar.ts @@ -63,6 +63,23 @@ const PAGE = 12; /** How much accent each segment keeps, stepping down along the strip. */ const RAMP = [100, 86, 72, 58, 46, 36, 28]; +/** One worked example: the query, and what it asks for. */ +function example(query: string, means: string): HTMLElement { + return el("li", {}, [ + el("code", { textContent: query }), + el("span", { className: "hint", textContent: ` — ${means}` }), + ]); +} + +/** One question the library cannot answer yet, and what it would look like. */ +function soon(key: string, query: string, means: string): HTMLElement { + return el("li", {}, [ + el("code", { className: "hangar-soon-term", textContent: query }), + el("span", { className: "hint", textContent: ` — ${means}` }), + el("span", { className: "hangar-soon-mark", textContent: "coming soon" }), + ]); +} + /** A design's picture, or a plate where the release has none. */ function sprite(unit: Unit, size: number): HTMLElement { if (!unit.sprite) { @@ -138,24 +155,66 @@ export function hangarScreen(): Node[] { /** The filters in the box, as something to read and remove. */ const pills = el("div", { className: "hangar-pills" }); - /** What the box takes, said once rather than guessed at. */ - const syntax = el("p", { className: "hint hangar-syntax" }, [ - document.createTextNode("Filters: "), - el("code", { textContent: "era<=3025" }), - document.createTextNode(" "), - el("code", { textContent: "bv<=1500" }), - document.createTextNode(" "), - el("code", { textContent: "class:heavy" }), - document.createTextNode(" "), - el("code", { textContent: "tech:clan" }), - document.createTextNode(" "), - el("code", { textContent: "jump>0" }), - document.createTextNode( - `. Any of ${FILTER_KEYS.join(", ")}, with : = < <= > >=.`, - ), - ]); + const closeHelp = el("button", { + type: "button", + className: "secondary small-btn", + textContent: "Close", + }); + + const help = el("button", { + type: "button", + className: "secondary small-btn hangar-help-open", + textContent: "Filters", + }); - /** What the target is imposing on the search, when it is locked. */ + /** + * What the box takes, and what it does not take yet. + * + * A dialog rather than a line of small print under the search: the list is + * twenty keys long and the screen is a workbench, so it belongs where it + * can be read and then put away. The unsupported half is in it on purpose — + * a player who tries `equipment:"ER PPC"` and gets nothing deserves to know + * whether they got the syntax wrong or asked for something the library does + * not carry. + */ + const syntaxDialog = el("dialog", { className: "hangar-help" }, [ + el("div", { className: "hangar-help-body" }, [ + el("h2", { textContent: "Searching the library" }), + el("p", { + textContent: + "Bare words match a design's name. A term filters a column: key, then one of : = < <= > >=, then a value. Everything combines with and.", + }), + el("ul", { className: "hangar-help-examples" }, [ + example("atlas", "every Atlas"), + example("bv<=1500", "nothing dearer than 1,500 battle value"), + example( + "era<=3025 tech:inner", + "Inner Sphere designs of 3025 or earlier", + ), + example("class:heavy jump>0", "heavies that jump"), + example('chassis:"black hawk"', "a name with a space in it, quoted"), + example("role:brawler mass>=70", "brawlers of 70 tons and up"), + ]), + el("h3", { textContent: "Every key" }), + el("p", { className: "hangar-help-keys" }, [ + el("code", { textContent: FILTER_KEYS.join(", ") }), + ]), + el("h3", { textContent: "Not yet" }), + el("p", { + textContent: + "These are questions the library cannot answer yet. Each one needs a column helm does not publish in the index — the data exists in its database, so the work is deciding what a browser should carry rather than finding it.", + }), + el("ul", { className: "hangar-help-soon" }, [ + soon("equipment", 'equipment:"ER PPC"', "what a design carries"), + soon("quirks", "quirks:battlefists", "MegaMek's per-design quirks"), + soon("damage", "damage9>=20", "damage at a range band"), + soon("alpha", "alpha:3/3/2", "the Alpha Strike conversion"), + soon("engine", "engine:xl", "engine, structure and armour types"), + soon("faction", "faction:steiner", "who fielded it, and when"), + ]), + el("div", { className: "row hangar-help-actions" }, [closeHelp]), + ]), + ]); /** What the target is imposing on the search, when it is locked. */ const imposed = el("div", { className: "hangar-imposed" }); /** Put a query back in the box and redraw from it. */ @@ -596,12 +655,12 @@ export function hangarScreen(): Node[] { el("div", { className: "hangar-meter" }, [totals, strip, warning]), el("div", { className: "hangar-panes" }, [ el("section", { className: "card hangar-library" }, [ - el("div", { className: "hangar-search-row" }, [searchBox, found]), + el("div", { className: "hangar-search-row" }, [searchBox, help, found]), pills, builder, imposed, - syntax, results, + syntaxDialog, el("div", { className: "hangar-ways-in" }, [ el( "a", @@ -634,6 +693,13 @@ export function hangarScreen(): Node[] { ]), ]); + help.addEventListener("click", () => { + syntaxDialog.showModal(); + }); + closeHelp.addEventListener("click", () => { + syntaxDialog.close(); + }); + draw(); void currentSession() diff --git a/web/src/styles.css b/web/src/styles.css index e299e5f..f7aa2fd 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -6152,15 +6152,11 @@ footer .debug { margin-top: 0.35rem; } -/* What the box takes, and what the target is imposing on it. Both are small - print under the search: a player who knows the syntax should not have to - read them, and one who does not should not have to guess. */ -.hangar-syntax { - margin: 0.1rem 0 0.35rem; - font-size: 0.78rem; -} - -.hangar-syntax code, +/* What the target is imposing on the search. Small print under the box: a + player who knows the syntax should not have to read it, and one who does + not should not have to guess why a search answered the way it did. The + syntax itself is a dialog behind the Filters button — twenty keys is a + reference, not a caption. */ .hangar-term { font-family: var(--font-data); font-size: 0.9em; @@ -6255,3 +6251,90 @@ footer .debug { .hangar-value { width: 9rem; } + +/* The filter reference. A dialog because it is a page of it, and because a + workbench should be able to put its manual away. */ +.hangar-help::backdrop { + background: rgb(0 0 0 / 55%); +} + +.hangar-help { + max-width: min(42rem, calc(100vw - 2rem)); + color: var(--ink); + background: var(--panel); + border: 1px solid var(--line); + border-radius: var(--radius); +} + +.hangar-help-body { + display: flex; + flex-direction: column; + gap: 0.6rem; + padding: 1.25rem; +} + +.hangar-help-body h2 { + margin: 0; + font-size: 1.1rem; +} + +.hangar-help-body h3 { + margin: 0.4rem 0 0; + font-size: 0.85rem; +} + +.hangar-help-body p { + margin: 0; + font-size: 0.9rem; +} + +.hangar-help-examples, +.hangar-help-soon { + display: flex; + flex-direction: column; + gap: 0.3rem; + margin: 0; + padding: 0; + list-style: none; + font-size: 0.9rem; +} + +.hangar-help-examples code, +.hangar-help-soon code, +.hangar-help-keys code { + font-family: var(--font-data); + font-size: 0.85em; + padding: 0.05rem 0.3rem; + background: var(--bg); + border: 1px solid var(--line); + border-radius: 3px; +} + +.hangar-help-keys code { + display: block; + padding: 0.4rem 0.5rem; + line-height: 1.5; +} + +/* Dimmed, and marked: a term that cannot work yet should not read like one + that can. */ +.hangar-help-soon li { + opacity: 0.75; +} + +.hangar-soon-mark { + margin-left: 0.4rem; + font-family: var(--font-labels); + font-size: 0.58rem; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--stamp); + border: 1px solid var(--stamp); + border-radius: 3px; + padding: 0.05rem 0.3rem; +} + +.hangar-help-actions { + justify-content: flex-end; + margin-top: 0.4rem; +}