From 8a1e79e46605291234314bd1a02dcab7a8c9bdc1 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 21 Sep 2026 16:06:48 +0800 Subject: feat(ui): replace id form with clickable model library cards --- public/App.js | 47 +++++++++++++-- public/index.html | 41 ++++--------- public/styles.css | 55 +++++++++++++++++ src/SomhairlesDream.Frontend/App.fs | 68 +++++++++++++++++++--- .../FrontendContractTests.fs | 4 +- 5 files changed, 169 insertions(+), 46 deletions(-) diff --git a/public/App.js b/public/App.js index 4716bb3..f63ae7e 100644 --- a/public/App.js +++ b/public/App.js @@ -6,7 +6,7 @@ import { sortBy, map, equalsWith, tryFind, iterateIndexed, item } from "./fable_ import { join, isNullOrWhiteSpace, printf, toText } from "./fable_modules/fable-library-js.4.24.0/String.js"; import { MeshData_triangleIndices, MeshData_positionData } from "./SomhairlesDream.Shared/Domain.js"; import { min, max } from "./fable_modules/fable-library-js.4.24.0/Double.js"; -import { defaultArg } from "./fable_modules/fable-library-js.4.24.0/Option.js"; +import { some, defaultArg } from "./fable_modules/fable-library-js.4.24.0/Option.js"; import { awaitPromise, startImmediate } from "./fable_modules/fable-library-js.4.24.0/Async.js"; import { singleton } from "./fable_modules/fable-library-js.4.24.0/AsyncBuilder.js"; @@ -867,8 +867,29 @@ export function inputValue(id) { return element(id).value; } +export const probeSelection = new RunSelection("probe-001", "run-probe-real-2", "probe-base-000", "probe-target-007", false); + +export const shipSelection = new RunSelection("ship-001", "run-ship-001", "ship-base-000", "ship-target-007", false); + +export let activeModel = createAtom(probeSelection); + export function selectedRun() { - return new RunSelection(inputValue("project-id-input"), inputValue("run-id-input"), inputValue("base-id-input"), inputValue("target-id-input"), element("run-render-input").checked); + return activeModel(); +} + +export function highlightActiveCard(selection) { + const library = element("model-library"); + for (let index = 0; index <= (library.children.length - 1); index++) { + const card = library.children.item(some(index)); + if (card.classList.contains("model-card")) { + if ((card.getAttribute("data-project") === selection.ProjectId) && (card.getAttribute("data-run") === selection.RunId)) { + card.classList.add("is-active"); + } + else { + card.classList.remove("is-active"); + } + } + } } export function freshLiveRunId(base_) { @@ -993,6 +1014,14 @@ export function loadBootHistory() { loadHistoryFor(selectedRun()); } +export function selectModel(card) { + const selection = new RunSelection(card.getAttribute("data-project"), card.getAttribute("data-run"), card.getAttribute("data-base"), card.getAttribute("data-target"), false); + activeModel(selection); + highlightActiveCard(selection); + setText("run-message", "载入模型历史"); + loadHistoryFor(selection); +} + export function boot() { showFrameMetadata(item(0, frames)); try { @@ -1010,10 +1039,16 @@ export function boot() { element("viewport-note").addEventListener("click", (_arg_2) => { retryFailedLoad(); }); - element("run-controls").addEventListener("submit", (event) => { - event.preventDefault(); - startRun(selectionForLiveStart(selectedRun())); - }); + const library = element("model-library"); + for (let index = 0; index <= (library.children.length - 1); index++) { + const card = library.children.item(some(index)); + if (card.classList.contains("model-card")) { + card.addEventListener("click", (_arg_3) => { + selectModel(card); + }); + } + } + highlightActiveCard(activeModel()); loadBootHistory(); renderTimeline(); } diff --git a/public/index.html b/public/index.html index b2f8703..63fa727 100644 --- a/public/index.html +++ b/public/index.html @@ -54,34 +54,17 @@

当前没有下一版正在开发

-
-
- - - - -
-
- - -
- -
+
+

模型库 · 点击浏览

+ + +
设计版本 @@ -196,6 +179,6 @@ heritage 参考线保留占位演示 · F# / OPEN CODE - + diff --git a/public/styles.css b/public/styles.css index a925e58..d6cfbb1 100644 --- a/public/styles.css +++ b/public/styles.css @@ -229,6 +229,61 @@ button:focus-visible { display: none !important; } +.model-library { + display: grid; + gap: 9px; + margin: 0 0 22px; + padding: 14px 0 20px; + border-top: 1px solid var(--line); + border-bottom: 1px solid var(--line); +} + +.library-title { + color: var(--muted); + font-family: "Fira Code", "SFMono-Regular", Consolas, monospace; + font-size: 0.62rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.model-card { + display: grid; + gap: 5px; + padding: 11px 13px; + border: 1px solid var(--line-bright); + border-radius: 0; + background: var(--panel-raised); + color: var(--ivory); + text-align: left; + cursor: pointer; + transition: border-color 0.15s ease, background-color 0.15s ease; +} + +.model-card:hover { + border-color: var(--gold-soft); + background: rgba(200, 168, 92, 0.08); +} + +.model-card:focus-visible { + outline: 2px solid var(--gold-soft); + outline-offset: 2px; +} + +.model-card.is-active { + border-color: var(--gold-soft); + background: rgba(200, 168, 92, 0.1); +} + +.model-name { + font-size: 0.95rem; + font-weight: 600; +} + +.model-desc { + color: var(--muted); + font-size: 0.68rem; +} + .run-controls { display: grid; gap: 14px; diff --git a/src/SomhairlesDream.Frontend/App.fs b/src/SomhairlesDream.Frontend/App.fs index 0e6bcd4..dbb267c 100644 --- a/src/SomhairlesDream.Frontend/App.fs +++ b/src/SomhairlesDream.Frontend/App.fs @@ -687,12 +687,38 @@ let connectToServer (selection: RunSelection) = let inputValue id = (element id :?> HtmlInput).value -let selectedRun () = - { ProjectId = inputValue "project-id-input" - RunId = inputValue "run-id-input" - BaseId = inputValue "base-id-input" - TargetId = inputValue "target-id-input" - Render = (element "run-render-input" :?> HtmlInput).``checked`` } +let probeSelection : RunSelection = + { ProjectId = "probe-001" + RunId = "run-probe-real-2" + BaseId = "probe-base-000" + TargetId = "probe-target-007" + Render = false } + +let shipSelection : RunSelection = + { ProjectId = "ship-001" + RunId = "run-ship-001" + BaseId = "ship-base-000" + TargetId = "ship-target-007" + Render = false } + +let mutable activeModel : RunSelection = probeSelection + +let selectedRun () = activeModel + +let highlightActiveCard (selection: RunSelection) = + let library: HTMLElement = unbox (element "model-library") + + for index in 0 .. (library.children.length - 1) do + let card: HTMLElement = unbox (library.children.item index) + + if card.classList.contains "model-card" then + let project = card.getAttribute "data-project" + let run = card.getAttribute "data-run" + + if project = selection.ProjectId && run = selection.RunId then + card.classList.add "is-active" + else + card.classList.remove "is-active" // The boot run id names a pre-published historical replay that already has an // on-disk manifest; re-running it can only fail ("run directory already @@ -801,6 +827,24 @@ let startRun (selection: RunSelection) = let loadBootHistory () = loadHistoryFor (selectedRun ()) +let selectModel (card: HTMLElement) = + let project = card.getAttribute "data-project" + let run = card.getAttribute "data-run" + let baseId = card.getAttribute "data-base" + let targetId = card.getAttribute "data-target" + + let selection : RunSelection = + { ProjectId = project + RunId = run + BaseId = baseId + TargetId = targetId + Render = false } + + activeModel <- selection + highlightActiveCard selection + setText "run-message" "载入模型历史" + loadHistoryFor selection + let boot () = showFrameMetadata frames[0] @@ -812,10 +856,16 @@ let boot () = (element "replay-button").addEventListener("click", fun _ -> toggleReplay ()) (element "follow-button").addEventListener("click", fun _ -> followLatestStep ()) (element "viewport-note").addEventListener("click", fun _ -> retryFailedLoad ()) - (element "run-controls").addEventListener("submit", fun event -> - event.preventDefault() - startRun (selectionForLiveStart (selectedRun ()))) + let library: HTMLElement = unbox (element "model-library") + + for index in 0 .. (library.children.length - 1) do + let card: HTMLElement = unbox (library.children.item index) + + if card.classList.contains "model-card" then + card.addEventListener("click", fun _ -> selectModel card) + + highlightActiveCard activeModel loadBootHistory () renderTimeline () diff --git a/tests/SomhairlesDream.Shared.Tests/FrontendContractTests.fs b/tests/SomhairlesDream.Shared.Tests/FrontendContractTests.fs index d961d8b..62187fa 100644 --- a/tests/SomhairlesDream.Shared.Tests/FrontendContractTests.fs +++ b/tests/SomhairlesDream.Shared.Tests/FrontendContractTests.fs @@ -22,8 +22,8 @@ let ``frontend source and bundle use the selected artifact run lifecycle`` () = |> assertBrowserContract let html = readRelative "public/index.html" - Assert.Contains("id=\"run-start-button\"", html) - Assert.Contains("id=\"artifact-manifest-link\"", html) + Assert.Contains("id=\"model-library\"", html) + Assert.Contains("data-project=\"ship-001\"", html) [] let ``solution contains every project in the artifact pipeline`` () = -- cgit v1.2.3