From 98584ad70098cc77208eb0d51894d8907ee3763f Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:02:33 -0400 Subject: [PATCH] Fix: take a label's ellipsis side from the name in Safari unicode-bidi:plaintext moves the cut in Blink and is ignored by WebKit, where a Latin name on a right-to-left dashboard was clipped at both ends. The labels are centred, so the direction can go on the block itself. --- CHANGELOG.md | 4 ++-- ui/admin/index.html | 2 +- ui/css/dashboard.css | 12 ++++------ ui/index.html | 4 ++-- ui/js/admin-app-form.js | 6 ++--- ui/js/admin-auth.js | 4 ++-- ui/js/admin-color-control.js | 4 ++-- ui/js/admin-settings.js | 4 ++-- ui/js/admin-shared.js | 2 +- ui/js/admin-widget-form.js | 6 ++--- ui/js/admin.js | 14 +++++------ ui/js/dashboard.js | 7 +++--- ui/js/spotlight.js | 2 +- ui/js/ui.js | 7 +++++- ui/js/utils.js | 4 ++++ ui/js/widget-config-form.js | 6 ++--- ui/test/dashboard-localisation.test.mjs | 31 ++++++++++++++++++------- 17 files changed, 70 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f958c746..6f836063 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,8 +72,8 @@ 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. +- A tile label is now cut at the end of its own name in Safari too. 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. diff --git a/ui/admin/index.html b/ui/admin/index.html index ff5c9732..d5242f48 100644 --- a/ui/admin/index.html +++ b/ui/admin/index.html @@ -323,6 +323,6 @@

General

- + diff --git a/ui/css/dashboard.css b/ui/css/dashboard.css index 72f51baf..dc1fa696 100644 --- a/ui/css/dashboard.css +++ b/ui/css/dashboard.css @@ -215,14 +215,10 @@ } /* ── 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; unicode-bidi:plaintext; + white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } /* Labels measured against a light background. The tone is set on the @@ -740,8 +736,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; 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-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-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%); } @@ -755,7 +751,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; unicode-bidi:plaintext; 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; 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 ffdf86da..2ed1551b 100644 --- a/ui/index.html +++ b/ui/index.html @@ -25,7 +25,7 @@ Stackyard - +

Stackyard

@@ -53,5 +53,5 @@ - + diff --git a/ui/js/admin-app-form.js b/ui/js/admin-app-form.js index cf093632..900f1ef8 100644 --- a/ui/js/admin-app-form.js +++ b/ui/js/admin-app-form.js @@ -1,4 +1,4 @@ -import { clr as rc, el, inp as inpById, q as qSel, qa, qi, tgt } from '/js/utils.js?v=26566e09'; +import { clr as rc, el, inp as inpById, q as qSel, qa, qi, tgt } from '/js/utils.js?v=b9a5b134'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { loadLocalIcons, resolveIcon, iconChain, cdnIconName } from '/js/icons.js?v=69c2b9bd'; import { state } from '/js/admin-state.js?v=c23e6346'; @@ -13,9 +13,9 @@ import { initInlineEdit, setTogDisabled, wireChecklist, -} from '/js/admin-shared.js?v=e4464b78'; +} from '/js/admin-shared.js?v=6ca8f3dd'; import { MAX_LABELS } from '/js/badge-logic.js?v=41a929ac'; -import { renderColorControl, BADGE_SWATCHES, BADGE_DEFAULT } from '/js/admin-color-control.js?v=49b9c909'; +import { renderColorControl, BADGE_SWATCHES, BADGE_DEFAULT } from '/js/admin-color-control.js?v=ff330e63'; import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=10f3cdb1'; export function buildFolderForm(body, item) { diff --git a/ui/js/admin-auth.js b/ui/js/admin-auth.js index c1d9db63..858b3d51 100644 --- a/ui/js/admin-auth.js +++ b/ui/js/admin-auth.js @@ -1,7 +1,7 @@ -import { ag, ap } from '/js/admin-shared.js?v=e4464b78'; +import { ag, ap } from '/js/admin-shared.js?v=6ca8f3dd'; import { t } from '/js/i18n.js?v=83239bf4'; import { pwStrength } from '/js/password-strength.js?v=42f45ac7'; -import { el, inp as inpById, qa } from '/js/utils.js?v=26566e09'; +import { el, inp as inpById, qa } from '/js/utils.js?v=b9a5b134'; export async function checkAuth(onLogin) { try { diff --git a/ui/js/admin-color-control.js b/ui/js/admin-color-control.js index ee160977..5d9f102f 100644 --- a/ui/js/admin-color-control.js +++ b/ui/js/admin-color-control.js @@ -1,7 +1,7 @@ -import { PE_SVG, initInlineEdit, toast } from '/js/admin-shared.js?v=e4464b78'; +import { PE_SVG, initInlineEdit, toast } from '/js/admin-shared.js?v=6ca8f3dd'; import { t } from '/js/i18n.js?v=83239bf4'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; -import { qa, q } from '/js/utils.js?v=26566e09'; +import { qa, q } from '/js/utils.js?v=b9a5b134'; const CC_SWATCHES = ['#1c1c1e', '#8e8e93', '#f2f2f7', '#ff393c', '#ffcd00', '#35c759', '#0289ff', '#cb30df']; export const BADGE_DEFAULT = '#1e6ef4'; diff --git a/ui/js/admin-settings.js b/ui/js/admin-settings.js index e715a825..f22da3e2 100644 --- a/ui/js/admin-settings.js +++ b/ui/js/admin-settings.js @@ -1,8 +1,8 @@ -import { toast, ag, ap } from '/js/admin-shared.js?v=e4464b78'; +import { toast, ag, ap } from '/js/admin-shared.js?v=6ca8f3dd'; import { pwStrength } from '/js/password-strength.js?v=42f45ac7'; import { t } from '/js/i18n.js?v=83239bf4'; import { shouldWritePassword, settingsSaveBlocker, clearsStoredPassword, BLOCK } from '/js/admin-logic.js?v=d17394da'; -import { el, inp, q, qa, setUserText } from '/js/utils.js?v=26566e09'; +import { el, inp, q, qa, setUserText } from '/js/utils.js?v=b9a5b134'; /* Mirrors the server's rule: auth cannot be switched on with no password. */ let _passwordSet = false; diff --git a/ui/js/admin-shared.js b/ui/js/admin-shared.js index ac0af973..3b22209d 100644 --- a/ui/js/admin-shared.js +++ b/ui/js/admin-shared.js @@ -1,6 +1,6 @@ /* Stateless helpers shared by the admin modules. Mutable state stays out. */ import { nextActiveIndex, recoversSession, toastHoldMs } from '/js/admin-logic.js?v=d17394da'; -import { el, qa, q } from '/js/utils.js?v=26566e09'; +import { el, qa, q } from '/js/utils.js?v=b9a5b134'; import { t } from '/js/i18n.js?v=83239bf4'; export const API = ''; diff --git a/ui/js/admin-widget-form.js b/ui/js/admin-widget-form.js index ed61c841..919b7991 100644 --- a/ui/js/admin-widget-form.js +++ b/ui/js/admin-widget-form.js @@ -1,10 +1,10 @@ import { state } from '/js/admin-state.js?v=c23e6346'; -import { PE_SVG, CHEV_SVG, initInlineEdit } from '/js/admin-shared.js?v=e4464b78'; -import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=adbc3f62'; +import { PE_SVG, CHEV_SVG, initInlineEdit } from '/js/admin-shared.js?v=6ca8f3dd'; +import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=0d82ab23'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { sizesForView, widgetConfigMode, rejectionLines, carriesTypedValues } from '/js/admin-logic.js?v=d17394da'; import { t } from '/js/i18n.js?v=83239bf4'; -import { q, qi, qa } from '/js/utils.js?v=26566e09'; +import { q, qi, qa } from '/js/utils.js?v=b9a5b134'; const SIZE_ICONS = { small: diff --git a/ui/js/admin.js b/ui/js/admin.js index 1a1c7485..4c1ac203 100644 --- a/ui/js/admin.js +++ b/ui/js/admin.js @@ -1,5 +1,5 @@ -import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=6aa6302b'; -import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=d19a6fdf'; +import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=ab1a8e24'; +import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=245d301f'; import { applyDrop, canJoinFolder, folderRowZone } from '/js/admin-drag-logic.js?v=ebe3e806'; import { reorderItems, resolveAdminSection } from '/js/admin-logic.js?v=d17394da'; import { @@ -10,10 +10,10 @@ import { snapshotItems, upsertItem, } from '/js/admin-save-logic.js?v=48a9e055'; -import { loadSettings, showBgFields, showBgFit, showWallpaperFile } from '/js/admin-settings.js?v=4638d447'; -import { ag, ap, initInlineEdit, setReauthHandler, toast } from '/js/admin-shared.js?v=e4464b78'; +import { loadSettings, showBgFields, showBgFit, showWallpaperFile } from '/js/admin-settings.js?v=4aead8ca'; +import { ag, ap, initInlineEdit, setReauthHandler, toast } from '/js/admin-shared.js?v=6ca8f3dd'; import { state } from '/js/admin-state.js?v=c23e6346'; -import { buildWidgetForm } from '/js/admin-widget-form.js?v=892f9ffe'; +import { buildWidgetForm } from '/js/admin-widget-form.js?v=1033c33a'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { initI18n, LANGUAGES, t } from '/js/i18n.js?v=83239bf4'; import { iconChain, loadLocalIcons, resolveIcon } from '/js/icons.js?v=69c2b9bd'; @@ -29,9 +29,9 @@ import { import { isMobileLayout, onLayoutChange } from '/js/layout.js?v=28416a75'; import { confirmModal, openModal as openDialog, promptModal } from '/js/modal.js?v=ff76dc56'; import { readMode, watchSystemTheme, writeMode } from '/js/theme.js?v=db4192cd'; -import { el, inp, q, qa, clr as rc, sanitizeCssUrl, setUserText, tgt } from '/js/utils.js?v=26566e09'; +import { el, inp, q, qa, clr as rc, sanitizeCssUrl, setUserText, tgt } from '/js/utils.js?v=b9a5b134'; import { widgetGlyph } from '/js/widget-glyphs.js?v=b5036986'; -import { normalizeColorInput } from '/js/admin-color-control.js?v=49b9c909'; +import { normalizeColorInput } from '/js/admin-color-control.js?v=ff330e63'; import { parseYamlTolerant, YamlLiteError } from '/js/yaml-lite.js?v=cceca788'; /* A class rather than a bare media query. Some phones report a wider CSS diff --git a/ui/js/dashboard.js b/ui/js/dashboard.js index 7033ec21..51df2b82 100644 --- a/ui/js/dashboard.js +++ b/ui/js/dashboard.js @@ -21,13 +21,13 @@ import { setUserText, teardownWidgets, titleWhenTruncated, -} from '/js/utils.js?v=26566e09'; -import { initSpotlight } from '/js/spotlight.js?v=71c04e24'; +} from '/js/utils.js?v=b9a5b134'; +import { initSpotlight } from '/js/spotlight.js?v=00545746'; import { html, setHtml, raw } from '/js/html.js?v=c71f8903'; import { initI18n, t, currentLang } from '/js/i18n.js?v=83239bf4'; import { pwStrength, passwordMismatch } from '/js/password-strength.js?v=42f45ac7'; import { sanitizeItemLinks } from '/js/link-url.js?v=54adb40f'; -import { initUI, mkFolder, openFolderDesktop, openFolderMobile, buildMobile } from '/js/ui.js?v=8390a5b8'; +import { initUI, mkFolder, openFolderDesktop, openFolderMobile, buildMobile } from '/js/ui.js?v=b400beaf'; import { badgeMinimum, badgeSignature, computeBadgeVisual, readBadgeUpdate } from '/js/badge-logic.js?v=41a929ac'; import { formatNumber } from '/js/format-number.js?v=e2165e12'; import { closeBadgePopover, wireBadgePopover } from '/js/badge-popover.js?v=08aae50f'; @@ -309,6 +309,7 @@ function mkIcon(item) { if (showLabel) { const l = mk('div'); l.className = 'ilabel'; + l.dir = 'auto'; l.style.width = iw + 12 + 'px'; setUserText(l, item.label || item.id); a.appendChild(l); diff --git a/ui/js/spotlight.js b/ui/js/spotlight.js index 467c880a..1e0dce42 100644 --- a/ui/js/spotlight.js +++ b/ui/js/spotlight.js @@ -1,4 +1,4 @@ -import { mk, clr, el, inp as inpById, q, qa, setUserText } from '/js/utils.js?v=26566e09'; +import { mk, clr, el, inp as inpById, q, qa, setUserText } from '/js/utils.js?v=b9a5b134'; import { wrapTab } from '/js/dialog.js?v=05935547'; import { t } from '/js/i18n.js?v=83239bf4'; diff --git a/ui/js/ui.js b/ui/js/ui.js index b5062eed..906a1237 100644 --- a/ui/js/ui.js +++ b/ui/js/ui.js @@ -10,7 +10,7 @@ import { q, qa, setUserText, -} from '/js/utils.js?v=26566e09'; +} from '/js/utils.js?v=b9a5b134'; import { t, currentLang } from '/js/i18n.js?v=83239bf4'; import { trapFocus } from '/js/dialog.js?v=05935547'; import { toneForColor } from '/js/label-contrast.js?v=38adb276'; @@ -139,6 +139,7 @@ export function mkFolder(item) { if (showLabel) { const l = mk('div'); l.className = 'ilabel'; + l.dir = 'auto'; l.style.width = iw + 12 + 'px'; setUserText(l, item.label || t('type.folder')); a.appendChild(l); @@ -188,6 +189,7 @@ export function openFolderDesktop(folder) { if (showLabel) { const l = mk('div'); l.className = 'ilabel'; + l.dir = 'auto'; l.style.width = iw + 12 + 'px'; setUserText(l, child.label || child.id); a.appendChild(l); @@ -316,6 +318,7 @@ function mFolder(item, cw, rh, isz, ir, im, sc) { if (showLabel) { const l = mk('div'); l.className = 'dyn-fold-label'; + l.dir = 'auto'; css(l, { '--lfs': Math.max(9, Math.round(9 * sc)) + 'px', '--lw': cw - 4 + 'px' }); setUserText(l, item.label || t('type.folder')); a.appendChild(l); @@ -452,6 +455,7 @@ export function openFolderMobile(folder, isz, _ir, _im, sc) { if (showLabel) { const l = mk('div'); l.className = 'dyn-fold-inner-label'; + l.dir = 'auto'; css(l, { '--lfs': Math.max(11, Math.round(11 * ptScale)) + 'px', '--fiw': folderIconW + 'px' }); setUserText(l, child.label || child.id); a.appendChild(l); @@ -695,6 +699,7 @@ export function buildMobile() { if (showLabel) { const l = mk('div'); l.className = 'dyn-mob-label'; + l.dir = 'auto'; css(l, { '--lfs': Math.max(9, Math.round(9 * sc)) + 'px', '--lw': '100%' }); setUserText(l, item.label || item.id); a.appendChild(l); diff --git a/ui/js/utils.js b/ui/js/utils.js index b33c4f87..127765e2 100644 --- a/ui/js/utils.js +++ b/ui/js/utils.js @@ -37,6 +37,10 @@ export { esc } from '/js/html.js?v=c71f8903'; alignment as well as bidi, so a Latin name on the block dragged its whole row to the other edge: a title and its subtitle ended up on opposite sides. + A centred label sets `dir` on the block as well, because alignment cannot move + centred text. Without it the block's direction decides which end is cut, and + WebKit ignores unicode-bidi:plaintext for that. + @param {HTMLElement} node @param {string} text @returns {HTMLElement} */ export const setUserText = (node, text) => { node.textContent = ''; diff --git a/ui/js/widget-config-form.js b/ui/js/widget-config-form.js index 661b869f..1e682200 100644 --- a/ui/js/widget-config-form.js +++ b/ui/js/widget-config-form.js @@ -4,8 +4,8 @@ import { t } from '/js/i18n.js?v=83239bf4'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; -import { wireChecklist } from '/js/admin-shared.js?v=e4464b78'; -import { renderColorControl } from '/js/admin-color-control.js?v=49b9c909'; +import { wireChecklist } from '/js/admin-shared.js?v=6ca8f3dd'; +import { renderColorControl } from '/js/admin-color-control.js?v=ff330e63'; import { seedCarried, applyOptionSet, @@ -15,7 +15,7 @@ import { visibleFieldFlags, } from '/js/admin-logic.js?v=d17394da'; import { optionsErrorAdvice, TONE } from '/js/admin-error.js?v=10f3cdb1'; -import { qi } from '/js/utils.js?v=26566e09'; +import { qi } from '/js/utils.js?v=b9a5b134'; const PE = ''; diff --git a/ui/test/dashboard-localisation.test.mjs b/ui/test/dashboard-localisation.test.mjs index 9e9e00cb..64ef5870 100644 --- a/ui/test/dashboard-localisation.test.mjs +++ b/ui/test/dashboard-localisation.test.mjs @@ -196,20 +196,35 @@ test('the first-run prompt and API error are translated', () => { }); /* 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". + language. Without a direction of its own the block's direction decides which + end is cut, and a Latin name on a right-to-left dashboard lost its beginning: + "Backup and Storage" read "...ckup and St". + + unicode-bidi:plaintext fixes it in Blink and does nothing in WebKit, so the + attribute is what the label carries. It is safe here only because these labels + are centred: dir sets alignment too, which moves a name off its own icon. */ +test('every tile label carries its own direction', () => { + const ui = read('js/ui.js'); + const dashboard = read('js/dashboard.js'); + for (const [src, name] of [ + [ui, 'ui.js'], + [dashboard, 'dashboard.js'], + ]) { + for (const m of src.matchAll( + /className = '(ilabel|dyn-mob-label|dyn-fold-label|dyn-fold-inner-label)';\n(\s*)(.*)/g, + )) { + assert.match(m[3], /^l\.dir = 'auto';$/, `${name}: .${m[1]} is cut by the page direction`); + } + } +}); - 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', () => { +test('the labels the direction is set on are 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`); + assert.match(rule[2], /text-align:center/, `.${cls} is not centred, so dir would move it`); } });