diff --git a/TODO.md b/TODO.md index 8522381..9f61c4a 100644 --- a/TODO.md +++ b/TODO.md @@ -421,6 +421,19 @@ and the p90 tail fell from ~470ms to ~330ms. The proxy was ruled out early ## Debt +- [x] **The boot screen is one design across both frames.** The card + index.html serves and the dialog Suramadu builds are styled together + now, so the handover from the first to the second is not visible. Two + latent bugs turned up while doing it: the empty `.ws-header` was drawing + a divider above nothing on every dialog, and the first frame's spinner + was 0x0 until the loader ran, because upstream only declares the spinner + at a depth that does not exist until then. Both fixed. +- [ ] **The boot string reaches players through one locale.** It is served as + `javascript/nls/en/msg.json`, and the client only ever asks for the tag + the browser reports, shortened. `lang.js` pins the tag to `en` so the + file applies to everyone, which is right while the game is English-only + and wrong the day it is not. Whoever adds a second language has to + remove that pin and ship a directory per locale. - [x] **The local match lifecycle now runs.** `up`, `status`, `logs`, `wait-ready` and `down` were exercised against the real daemon on 2026-08-07. What the stubbed tests could not answer, answered: the diff --git a/suramadu/web/index.html b/suramadu/web/index.html index aa860d1..e6ea32a 100644 --- a/suramadu/web/index.html +++ b/suramadu/web/index.html @@ -48,6 +48,7 @@ + @@ -55,11 +56,23 @@
+Loading combat simulation
. Making the content div
+ * a flex column is what buys `order`, and `order` is what puts the mark above
+ * the text without touching the markup.
+ *
+ * Why :has() rather than plain .ws-dialog: upstream builds every dialog from
+ * the same template, including the ones with buttons and no spinner - the
+ * reconnect and error cards. Those want the ordinary card, not a badge with a
+ * status line under it. The one thing that separates them is whether the
+ * injected content put a spinner in, so that is what this selects on. It has to
+ * be `> .ws-spinner`, a direct child of the content div: the template also
+ * carries a spinner inside #ajaxProgress, which would otherwise match on every
+ * dialog and undo the distinction. A browser without :has() support falls back
+ * to the plain card, which is the old behaviour rather than a broken one.
*/
-:root .lb-wordmark {
- font-family: var(--lb-font);
- font-size: 1.35rem;
- font-weight: 600;
- letter-spacing: -0.02em;
- color: var(--lb-ink);
+:root .suramadu-element .lb-boot,
+:root .suramadu-element .suramadu-element-content .lb-boot,
+:root .suramadu-element .suramadu-element-content .ws-dialog:has(.ws-dialog-content > .ws-spinner) {
+ max-width: 300px;
+ padding: 26px 24px 24px;
+ border: 0;
+ border-top: 2px solid var(--lb-brand);
+ border-radius: 0;
+ box-shadow: none;
+ background-color: var(--lb-panel);
+ clip-path: var(--lb-notch);
+}
+
+/* Upstream gives .ws-header 20px of padding and a bottom rule. On this dialog
+ it holds nothing, so that is a divider drawn above nothing. */
+:root .suramadu-element .ws-header:empty,
+:root .suramadu-element .suramadu-element-content .ws-header:empty {
+ display: none;
+}
+
+:root .suramadu-element .lb-boot .ws-login-content,
+:root .suramadu-element .suramadu-element-content .lb-boot .ws-login-content,
+:root .suramadu-element .suramadu-element-content .ws-dialog-content:has(> .ws-spinner) {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding-bottom: 0;
+}
+
+/* The mark. An in the frame we write, a background in the one we do not;
+ `content: ""` on the
would blank the image rather than decorate it. */
+:root .lb-mark {
+ order: 1;
+ display: block;
+ width: 84px;
+ height: 105px;
+}
+
+:root .suramadu-element .suramadu-element-content .ws-dialog-content:has(> .ws-spinner)::before {
+ content: "";
+ order: 1;
+ display: block;
+ width: 84px;
+ height: 105px;
+ background: url("insignia.svg") center / contain no-repeat;
+}
+
+/* The padding-left is optical, not spacing: letter-spacing is added after the
+ last letter too, so centred caps sit half a space left of the true centre. */
+:root .lb-status,
+:root .suramadu-element .suramadu-element-content .ws-dialog-content:has(> .ws-spinner) #commonDialog-description {
+ order: 2;
+ margin: 16px 0 0;
+ padding-left: 0.22em;
+ font-family: var(--lb-display);
+ font-weight: 700;
+ font-size: 11px;
+ line-height: 1.55;
+ text-transform: uppercase;
+ letter-spacing: 0.22em;
+ text-wrap: balance;
+ color: var(--lb-brand);
+}
+
+/* Under the text, not over it: the wait is a caption here, not the subject. */
+:root .suramadu-element .lb-boot .ws-spinner,
+:root .suramadu-element .suramadu-element-content .lb-boot .ws-spinner,
+:root .suramadu-element .suramadu-element-content .ws-dialog-content:has(> .ws-spinner) > .ws-spinner {
+ order: 3;
+ width: 20px;
+ height: 20px;
+ margin: 16px auto 2px;
+}
+
+/* The first frame has to carry the spinner's own geometry, because upstream
+ only ever declares it at .suramadu-element-content depth - and that wrapper
+ does not exist yet. The loader makes it, by detaching this markup and
+ re-appending it inside one, which is also why the rules above are written at
+ both depths. Until it runs, an unstyled dot is a 0x0 box: the card sat there
+ with no spinner in it at all, for as long as the bundle took to arrive.
+
+ The keyframes are upstream's. @keyframes are not scoped, so style.css having
+ defined sk-rotate and sk-bounce is enough to name them here. */
+:root .suramadu-element .lb-boot .ws-spinner {
+ position: relative;
text-align: center;
- margin: 0.25rem 0 0.5rem;
+ animation: sk-rotate 2s infinite linear;
}
-:root .lb-wordmark .lb-dot {
- color: var(--lb-accent);
+:root .suramadu-element .lb-boot .ws-spinner-dot-1,
+:root .suramadu-element .lb-boot .ws-spinner-dot-2 {
+ position: absolute;
+ top: 0;
+ display: inline-block;
+ width: 60%;
+ height: 60%;
+ border-radius: 100%;
+ animation: sk-bounce 2s infinite ease-in-out;
+}
+
+:root .suramadu-element .lb-boot .ws-spinner-dot-2 {
+ top: auto;
+ bottom: 0;
+ animation-delay: -1s;
}
-/* Upstream sizes the spinner for a light card; centre it under the wordmark. */
-:root .suramadu-element .suramadu-element-content .ws-spinner {
- margin: 0.75rem auto 0.25rem;
+/* Below 480px upstream forces `font-size: 14px !important` on every
in the + dialog. Specificity has no answer to that, and this file is not allowed one - + tests/shell/test-theme-css.sh forbids !important, on the grounds that needing + it means the selector is wrong. Here the selector is not wrong; upstream + simply wins. + So the second frame goes to 14px on a phone whatever we do, and the only way + the two frames stay the same size is for the one we control to follow. The + caps line wraps to two at that size, which is why it is balanced above. */ +@media only screen and (max-width: 480px) { + :root .lb-status { + font-size: 14px; + } } /* ---- upstream's AGPL source offer --------------------------------------- diff --git a/tests/shell/test-theme-css.sh b/tests/shell/test-theme-css.sh index 3ffce5b..0110671 100755 --- a/tests/shell/test-theme-css.sh +++ b/tests/shell/test-theme-css.sh @@ -21,11 +21,18 @@ check() { if "${@:2}"; then echo "ok $1"; pass=$((pass+1)); else echo "FAIL # Selector lines are the ones ending in { or , outside a declaration block. # Comments and declarations are skipped; :root {...} itself is the one rule # allowed to stand alone, since it only declares custom properties. +# +# At-rule preludes are skipped too. `@media ... {` ends in a brace and would +# read as a bare selector, but it is not one: it opens a block whose selectors +# are checked on their own lines, and nothing about it can lose a specificity +# fight. The rule the check exists for is unchanged - every selector, including +# the ones nested inside an at-rule, still has to start with :root. mapfile -t bare < <( awk ' /\/\*/ { inc = 1 } inc { if (/\*\//) inc = 0; next } /^[[:space:]]*$/ { next } + /^[[:space:]]*@/ { next } /^[[:space:]]*:root[[:space:]]*\{/ { next } /[{,][[:space:]]*$/ { line = $0