From b772aef8dc69d0ccb60b9d22c9d10afa0cdd1e7f Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:46:03 -0400 Subject: [PATCH] Fix: cut a tile label at the end of its own name A tile label took its direction from the page, so a Latin name on a right-to-left dashboard was truncated at its start. The labels are centred, so taking the direction from the name moves no text. --- CHANGELOG.md | 3 +++ ui/css/dashboard.css | 12 ++++++++---- ui/index.html | 2 +- ui/test/dashboard-localisation.test.mjs | 19 +++++++++++++++++++ 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bae219b..54531952 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 concept, and no longer leave "Logging", "API key", "Collection ID" or "Authentication Proxy" in English. +- A tile label is now cut at the end of its own name. On a right-to-left + dashboard a Latin name lost its beginning instead. + - Opening and saving an app no longer rewrites a colour that was not changed. - Admin form toggles now carry accessible names. diff --git a/ui/css/dashboard.css b/ui/css/dashboard.css index dc1fa696..72f51baf 100644 --- a/ui/css/dashboard.css +++ b/ui/css/dashboard.css @@ -215,10 +215,14 @@ } /* ── Icon label ── */ + /* A name is user text, so its language need not be the interface language. + plaintext takes the direction from the name itself, so the ellipsis falls + at the name's own end. These labels are centred, so it moves no text: dir + on the block would, and drags a row to the other edge. */ .ilabel { font-size:var(--fs-footnote);line-height:var(--lh-footnote);letter-spacing:var(--tr-footnote); font-weight:500; color:var(--label-color); text-align:center; text-shadow:var(--label-shadow); margin-top:6px; - white-space:nowrap; overflow:hidden; text-overflow:ellipsis; + white-space:nowrap; overflow:hidden; text-overflow:ellipsis; unicode-bidi:plaintext; } /* Labels measured against a light background. The tone is set on the @@ -736,8 +740,8 @@ .dyn-grid { display:grid; grid-template-columns:repeat(6,var(--iw)); gap:24px; justify-content:center; } .dyn-mob-btn { flex:1; height:var(--rh); background:none; border:none; padding:0; display:flex; flex-direction:column; align-items:center; justify-content:center; cursor:pointer; flex-shrink:0; -webkit-tap-highlight-color:transparent; -webkit-appearance:none; } .dyn-mob-icon { display:flex; flex-direction:column; align-items:center; justify-content:center; width:var(--cw); height:var(--rh); cursor:pointer; text-decoration:none; flex-shrink:0; -webkit-tap-highlight-color:transparent; } - .dyn-mob-label { font-weight:500; color:rgba(255,255,255,.88); text-align:center; margin-top:2px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; text-shadow:0 1px 3px rgba(0,0,0,.65); font-size:var(--lfs); width:var(--lw); } - .dyn-fold-label { font-weight:500; color:rgba(255,255,255,.90); text-align:center; margin-top:3px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; text-shadow:0 1px 3px rgba(0,0,0,.70); font-size:var(--lfs); width:var(--lw); } + .dyn-mob-label { font-weight:500; color:rgba(255,255,255,.88); text-align:center; margin-top:2px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; unicode-bidi:plaintext; text-shadow:0 1px 3px rgba(0,0,0,.65); font-size:var(--lfs); width:var(--lw); } + .dyn-fold-label { font-weight:500; color:rgba(255,255,255,.90); text-align:center; margin-top:3px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; unicode-bidi:plaintext; text-shadow:0 1px 3px rgba(0,0,0,.70); font-size:var(--lfs); width:var(--lw); } .dyn-fold-wrap { position:absolute; inset:0; pointer-events:none; border-radius:var(--br); background:rgba(120,130,160,.35); display:grid; grid-template-columns:repeat(3,1fr); grid-template-rows:repeat(3,1fr); gap:var(--gap); padding:var(--pad); overflow:hidden; -webkit-backdrop-filter:blur(7px); backdrop-filter:blur(7px); box-shadow:inset 1px 1px 0 rgba(255,255,255,.22),inset -1px -1px 0 rgba(0,0,0,.12); } .dyn-fold-cell { display:flex; align-items:center; justify-content:center; z-index:1; pointer-events:none; min-width:0; min-height:0; } .dyn-fold-sheen { position:absolute; inset:0; pointer-events:none; z-index:2; border-radius:inherit; background:linear-gradient(135deg,rgba(255,255,255,.12) 0%,transparent 55%); } @@ -751,7 +755,7 @@ .folder-overlay-mobile:focus, .dyn-fold-anchor:focus, .folder-overlay:focus, .folder-icon-link:focus { outline:none; } .dyn-fold-anchor { display:flex; flex-direction:column; align-items:center; gap:4px; text-decoration:none; -webkit-tap-highlight-color:transparent; } - .dyn-fold-inner-label { font-weight:400; color:rgba(255,255,255,.90); text-align:center; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; text-shadow:0 1px 3px rgba(0,0,0,.6); font-size:var(--lfs); width:var(--fiw); } + .dyn-fold-inner-label { font-weight:400; color:rgba(255,255,255,.90); text-align:center; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; unicode-bidi:plaintext; text-shadow:0 1px 3px rgba(0,0,0,.6); font-size:var(--lfs); width:var(--fiw); } .dyn-title-mob { position:absolute; pointer-events:none; font-size:var(--tfs); } .dyn-box-mob { position:absolute; display:flex; flex-direction:column; align-items:center; left:var(--left); width:var(--bw); height:var(--bh); top:var(--top); border-radius:var(--br); padding:var(--pt) var(--ph) var(--pb); } .dyn-dock-icon { display:flex; align-items:center; justify-content:center; flex:1; height:100%; cursor:pointer; text-decoration:none; -webkit-tap-highlight-color:transparent; overflow:visible; position:relative; } diff --git a/ui/index.html b/ui/index.html index 4389818c..ffdf86da 100644 --- a/ui/index.html +++ b/ui/index.html @@ -25,7 +25,7 @@ Stackyard - +

Stackyard

diff --git a/ui/test/dashboard-localisation.test.mjs b/ui/test/dashboard-localisation.test.mjs index e64f7ea5..9e9e00cb 100644 --- a/ui/test/dashboard-localisation.test.mjs +++ b/ui/test/dashboard-localisation.test.mjs @@ -194,3 +194,22 @@ test('the first-run prompt and API error are translated', () => { assert.ok(src.includes(key), `${key} is not used`); } }); + +/* An app's name is user text, so its language can differ from the interface + language. Without this the block's direction decides where the line is cut, + and a Latin name on a right-to-left dashboard lost its beginning: "Backup and + Storage" read "...nd Storage". + + dir on the block would set alignment too, which moves a name away from its own + icon. These labels are centred, so plaintext moves nothing. */ +test('every tile label is cut at the end of its own name, and stays centred', () => { + const css = read('css/dashboard.css'); + for (const cls of ['ilabel', 'dyn-mob-label', 'dyn-fold-label', 'dyn-fold-inner-label']) { + const rule = [...css.matchAll(/([^{}]+)\{([^{}]*)\}/g)].find( + m => new RegExp(`\\.${cls}\\s*[,{]|\\.${cls}$`).test(m[1].trim()) && m[2].includes('text-overflow'), + ); + assert.ok(rule, `no truncating rule for .${cls}`); + assert.match(rule[2], /unicode-bidi:plaintext/, `.${cls} is cut by the page direction`); + assert.match(rule[2], /text-align:center/, `.${cls} is not centred`); + } +});