diff --git a/.changes/focus-edge-dimming.json b/.changes/focus-edge-dimming.json new file mode 100644 index 00000000..4a9d9141 --- /dev/null +++ b/.changes/focus-edge-dimming.json @@ -0,0 +1 @@ +{ "type": "user-facing", "releaseNoteId": "release:0.21.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f26ab87..2f16e931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable Loop Studio releases, newest first. Behavioral changes are pinned in versioned spec documents (see the [README](README.md#technical-reference)); this file is the narrative history, not the contract. +## v0.21.1 — 2026-10-07 + +A fix release (issue #329): Focus mode dims the connections outside the focus set, as [`docs/large-graph-readability.md`](docs/large-graph-readability.md) §LGR3.1 always said. + +- **Connections fade with the nodes.** A connection outside the focus set is drawn at 0.26, its path and its arrowhead. Before, every connection path carried an inline `opacity` that beat the dimming rule, so only the nodes were dimmed. +- **Focus wins over an activator.** An unsatisfied activator connection stays at 0.5 inside the focus set and goes to 0.26 outside it. +- **High contrast keeps its own tell.** Under forced colours a dimmed connection is not faded; its fine `1 5` dash marks it, as before. +- **Unchanged:** labels, the selection highlight, nodes, the playback tokens and cues, files, digests and simulation results. + +**No migration.** Three release-note lines in 18 languages, 16 of them without native review. The informational `meta.tool` string is now `loop-studio/0.21.1`. + ## v0.21.0 — 2026-10-07 Compact nodes (issue #325, the last of three parts): more of a large graph fits in view. The contract is [`docs/flow-colour-and-compact-nodes.md`](docs/flow-colour-and-compact-nodes.md) FC-7. diff --git a/README.md b/README.md index 4702de63..931bf5a2 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,16 @@ Additional feature-specific design documents (localization, mobile, module system, large-graph readability, simulation playback, edge routing, data import, …) live under [`docs/`](docs/). -## Latest — v0.21.0 +## Latest — v0.21.1 + +A fix release: Focus mode dims the connections too. + +- **Connections outside the focus set fade** like the nodes there, path and arrowhead, + whatever their activator says; before, only the nodes were dimmed +- **What stays at full strength is unchanged**: selection, warnings and the moving markers + of a running simulation; under high contrast a faded connection is a fine dotted line + +## v0.21.0 Compact nodes: more of a large graph fits in view. @@ -177,16 +186,8 @@ Flow colours: give nodes and connections a colour so a large graph's flows read - **Advice, never a block**: a colour that would be hard to see, or that looks like a colour the canvas uses for focus, warnings or a run, is still applied, with a note -## v0.18.2 - -A fix release: the guided tour says each step once. - -- **Each step is announced once**: step 1 as the tour opens, every later step as Next or - Back reaches it, with its number, title and text -- **Focus stays on Next or Back** while the steps change, and ending the tour returns it - to the Help button (More on a phone) - -See [`CHANGELOG.md`](CHANGELOG.md) for the full notes of these releases, v0.18.1 (one +See [`CHANGELOG.md`](CHANGELOG.md) for the full notes of these releases, v0.18.2 (the +guided tour says each step once), v0.18.1 (one keyboard contract for every menu), v0.18.0 (the third-party open-source licenses in the About dialog), v0.17.2 (the Temporary session button drawn like the menu buttons), v0.17.1 (share links compressed with the browser's own Compression Streams), v0.17.0 diff --git a/docs/large-graph-readability.md b/docs/large-graph-readability.md index 8ff79826..004f7d50 100644 --- a/docs/large-graph-readability.md +++ b/docs/large-graph-readability.md @@ -218,6 +218,15 @@ expression, type dot) are **hidden** while dimmed — they are the noise focus i removing; the in-node value / capacity bar stays. §LGR2.3's required set is exempt and stays full-strength. +**A connection outside the focus set** is drawn at 0.26, its path and its +arrowhead. This holds whatever its activator says: an unsatisfied activator +connection is 0.5 inside the focus set and 0.26 outside it, Focus winning. +Under forced colours a dimmed connection is not faded (opacity 1); the sparse +`1 5` dash is its tell (§LGR9). Issue #329: until v0.21.1 an inline `opacity` +on every connection path beat this rule, so connections were never dimmed; the +activator's 0.5 is now the `.edge-activator-off` class. The playback cues on a +dimmed connection keep their strength (their Focus policy is #330). + **A selected node is never de-emphasised.** Selection outranks focus: whatever the focus set says, a node the user has selected renders at full strength, with its badges. The focus calculation itself is unchanged and still applies to every diff --git a/e2e/focus-edge-dimming.spec.ts b/e2e/focus-edge-dimming.spec.ts new file mode 100644 index 00000000..5582e724 --- /dev/null +++ b/e2e/focus-edge-dimming.spec.ts @@ -0,0 +1,170 @@ +import { readFileSync } from 'node:fs' +import type { Page } from '@playwright/test' +import { expect, importGraph, openApp, resetAll, test } from './support/loop' + +// issue #329 — Focus mode dims the connections outside the focus set +// (docs/large-graph-readability.md §LGR3.1): the path and its arrowhead at 0.26. +// An unsatisfied activator connection is 0.5 inside the focus set and 0.26 +// outside it (Focus wins). Under forced colours a dimmed connection is not +// faded: opacity 1 and the sparse `1 5` dash are its tell. These read the +// DRAWN opacity: the class alone was asserted before, and an inline `opacity` +// on every path beat the stylesheet rule unseen. + +const STATE = readFileSync(new URL('../examples/state-verification.json', import.meta.url), 'utf8') +const EQUILIBRIUM = readFileSync(new URL('../examples/equilibrium.json', import.meta.url), 'utf8') + +type Loop = { + __loop: Record any }> & { rf: { fitView: (o: object) => void } } +} +type Drawn = { out: boolean; activatorOff: boolean; opacity: string; dash: string; inline: string; marker: string | null } + +/** commit `n` steps at the fastest beat, waiting for each transition to settle */ +const steps = (page: Page, n: number) => + page.evaluate(async (n) => { + const S = (window as unknown as Loop).__loop.sim + S.getState().setSpeed(120) + for (let i = 0; i < n; i++) { + S.getState().stepOnce() + while (S.getState().transition) await new Promise((r) => requestAnimationFrame(r)) + } + }, n) + +/** select a node by clicking it, centred first so no panel covers it */ +const clickNode = async (page: Page, nodeId: string) => { + await page.evaluate((id) => (window as unknown as Loop).__loop.rf.fitView({ nodes: [{ id }], duration: 0, maxZoom: 1, padding: 1.5 }), nodeId) + await page.locator(`.react-flow__node[data-id="${nodeId}"]`).click() +} + +const focusOn = async (page: Page, nodeId: string) => { + await page.evaluate(() => (window as unknown as Loop).__loop.ui.getState().setFocusMode(true)) + await clickNode(page, nodeId) + await expect(page.locator('.react-flow__edge.lgr-deemph').first()).toBeAttached() +} + +/** per connection: in or out of the focus set, its activator verdict, and the drawn path */ +const drawn = (page: Page): Promise> => + page.evaluate(() => + Object.fromEntries( + [...document.querySelectorAll('.react-flow__edge')].map((e) => { + const p = e.querySelector('path.react-flow__edge-path') as SVGPathElement + const c = getComputedStyle(p) + return [ + (e as HTMLElement).dataset.id, + { + out: e.classList.contains('lgr-deemph'), + activatorOff: p.classList.contains('edge-activator-off'), + opacity: c.opacity, + dash: c.strokeDasharray, + inline: p.style.opacity, + marker: p.getAttribute('marker-end'), + }, + ] + }), + ), + ) + +const openStateGraph = async (page: Page) => { + await openApp(page) + await resetAll(page) + await importGraph(page, STATE) + await page.locator('.react-flow__node[data-id="gauge_b"]').waitFor() + // three steps: `a_ga_pd` is an unsatisfied activator, `a_gb_pd` a satisfied one + await steps(page, 3) +} + +test.describe('#329 Focus mode dims the connections outside the focus set', () => { + for (const scheme of ['light', 'dark'] as const) { + test(`${scheme}: outside 0.26 (Focus wins over an unsatisfied activator's 0.5), inside 1 or 0.5, with no inline opacity`, async ({ page }) => { + await page.emulateMedia({ colorScheme: scheme }) + await openStateGraph(page) + + const off = await drawn(page) + expect(off.a_ga_pd).toMatchObject({ out: false, activatorOff: true, opacity: '0.5' }) + expect(off.a_gb_pd).toMatchObject({ out: false, activatorOff: false, opacity: '1' }) + for (const [id, e] of Object.entries(off)) { + expect(e.inline, `${id}: no inline opacity left to beat the stylesheet`).toBe('') + if (!e.activatorOff) expect(e.opacity, `${id}: Focus off, full strength`).toBe('1') + } + + // gauge_a in focus: its unsatisfied activator inside (0.5), the satisfied one outside (0.26) + await focusOn(page, 'gauge_a') + const a = await drawn(page) + expect(a.a_ga_pd).toMatchObject({ out: false, activatorOff: true, opacity: '0.5' }) + expect(a.a_gb_pd).toMatchObject({ out: true, activatorOff: false, opacity: '0.26' }) + expect(a.e_gsrc_ga).toMatchObject({ out: false, opacity: '1' }) + + // gauge_b in focus: the unsatisfied activator outside — Focus's 0.26 wins over its 0.5 + await clickNode(page, 'gauge_b') + await expect.poll(async () => (await drawn(page)).a_ga_pd.out).toBe(true) + const b = await drawn(page) + expect(b.a_ga_pd).toMatchObject({ out: true, activatorOff: true, opacity: '0.26' }) + expect(b.a_gb_pd).toMatchObject({ out: false, activatorOff: false, opacity: '1' }) + for (const [id, e] of Object.entries(b)) { + if (e.out) expect(e.opacity, `${id}: outside the focus set`).toBe('0.26') + // the arrowhead is the path's own marker, so it is painted at the path's opacity + expect(e.marker, `${id}: the arrowhead is the path's marker`).toMatch(/^url\(#/) + } + }) + } + + test.describe('forced colours', () => { + test.use({ contextOptions: { forcedColors: 'active' } }) + test('a dimmed connection is not faded: opacity 1 and the `1 5` dash, an unsatisfied activator included', async ({ page }) => { + await openStateGraph(page) + await focusOn(page, 'gauge_a') + const a = await drawn(page) + // inside the focus set an unsatisfied activator keeps its 0.5, as before + expect(a.a_ga_pd).toMatchObject({ out: false, activatorOff: true, opacity: '0.5' }) + await clickNode(page, 'gauge_b') + await expect.poll(async () => (await drawn(page)).a_ga_pd.out).toBe(true) + const b = await drawn(page) + expect(b.a_ga_pd).toMatchObject({ out: true, activatorOff: true, opacity: '1', dash: '1px, 5px' }) + for (const [id, e] of Object.entries(b)) { + if (!e.out) continue + expect(e.opacity, `${id}: not faded under forced colours`).toBe('1') + expect(e.dash, `${id}: the sparse dash is the tell`).toBe('1px, 5px') + } + }) + }) + + test('during a played step the dimmed path is 0.26 and the playback token keeps its strength and colour', async ({ page }) => { + await openApp(page) + await resetAll(page) + await importGraph(page, EQUILIBRIUM) + await page.locator('.react-flow__node[data-id="tpl-gate"]').waitFor() + await focusOn(page, 'tpl-src') + await steps(page, 1) + await page.evaluate(() => { + const s = (window as unknown as Loop).__loop.sim.getState() + s.setSpeed(2400) + s.stepOnce() + }) + await page.waitForFunction(() => { + const t = (window as unknown as Loop).__loop.sim.getState().transition + return t && t.tau > 0.5 + }, null, { polling: 'raf' }) + await page.evaluate(() => (window as unknown as Loop).__loop.sim.getState().pause()) + const rows = await page.evaluate(() => + [...document.querySelectorAll('.react-flow__edge')] + .filter((e) => e.querySelector('.pb-move')) + .map((e) => ({ + id: (e as HTMLElement).dataset.id, + out: e.classList.contains('lgr-deemph'), + path: getComputedStyle(e.querySelector('path.react-flow__edge-path')!).opacity, + token: getComputedStyle(e.querySelector('.pb-move')!).opacity, + bead: getComputedStyle(e.querySelector('.flow-bead')!).fill, + })), + ) + const inside = rows.filter((r) => !r.out) + const outside = rows.filter((r) => r.out) + expect(inside.length, 'a token on an in-focus connection').toBeGreaterThan(0) + expect(outside.length, 'a token on a dimmed connection').toBeGreaterThan(0) + for (const r of inside) expect(r.path, r.id).toBe('1') + for (const r of outside) expect(r.path, r.id).toBe('0.26') + // the playback cues are unchanged by this fix (#330 decides their Focus policy) + for (const r of rows) { + expect(r.token, `${r.id}: the token is not dimmed by this fix`).toBe('1') + expect(r.bead, `${r.id}: the token keeps the run colour`).toBe(inside[0].bead) + } + }) +}) diff --git a/e2e/whats-new.spec.ts b/e2e/whats-new.spec.ts index 01d84f7a..ef379ddb 100644 --- a/e2e/whats-new.spec.ts +++ b/e2e/whats-new.spec.ts @@ -238,7 +238,7 @@ test.describe('closing the notice and opening the panel are different things', ( expect(text).not.toContain('text unavailable') } } - expect(RELEASE_NOTES.map((n) => n.version)).toEqual(['0.21.0', '0.20.0', '0.19.0','0.18.2', '0.18.1', '0.18.0', '0.17.2', '0.17.1', '0.17.0', '0.16.0', '0.15.3', '0.15.2', '0.15.1', '0.15.0', '0.14.0']) + expect(RELEASE_NOTES.map((n) => n.version)).toEqual(['0.21.1', '0.21.0', '0.20.0', '0.19.0','0.18.2', '0.18.1', '0.18.0', '0.17.2', '0.17.1', '0.17.0', '0.16.0', '0.15.3', '0.15.2', '0.15.1', '0.15.0', '0.14.0']) await page.keyboard.press('Escape') await expect(panel).toHaveCount(0) diff --git a/package-lock.json b/package-lock.json index 43f3117a..9bcbdab8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "loop-studio", - "version": "0.21.0", + "version": "0.21.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "loop-studio", - "version": "0.21.0", + "version": "0.21.1", "dependencies": { "@fontsource/ibm-plex-mono": "^5.3.0", "@fontsource/ibm-plex-sans": "^5.3.0", diff --git a/package.json b/package.json index c4bb884f..2cf9bc2f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "loop-studio", "private": true, - "version": "0.21.0", + "version": "0.21.1", "type": "module", "engines": { "node": ">=22.12.0" diff --git a/src/components/edges/LoopEdge.tsx b/src/components/edges/LoopEdge.tsx index acabef51..f3ada45c 100644 --- a/src/components/edges/LoopEdge.tsx +++ b/src/components/edges/LoopEdge.tsx @@ -386,13 +386,17 @@ function LoopEdge({ isState ? 'edge-state' : 'edge-resource', route ? `route-${route.routeClass}${route.invalidWaypoint ? ' route-invalid' : ''}` : '', activityOp > 0 ? 'lgr-active-tint' : '', + // issue #329 — an unsatisfied activator's 0.5 is a class, not an + // inline `opacity`: an inline value beat Focus mode's 0.26 + // (`.react-flow__edge.lgr-deemph .react-flow__edge-path`), so a + // connection outside the focus set was never dimmed + activatorOn === false ? 'edge-activator-off' : '', ] .filter(Boolean) .join(' ')} style={{ stroke: baseStroke, strokeWidth: selected ? 2 : activatorOn === true ? 1.8 : isState ? 1 : 1.5, - opacity: activatorOn === false ? 0.5 : 1, ...(activityOp > 0 ? { ['--lgr-activity' as string]: activityOp } : null), }} /> diff --git a/src/i18n/itCopy.test.ts b/src/i18n/itCopy.test.ts index 9ecd0714..d6575c24 100644 --- a/src/i18n/itCopy.test.ts +++ b/src/i18n/itCopy.test.ts @@ -205,15 +205,15 @@ const IDENTICAL_TO_EN: ReadonlyArray = [ // ------------------------------------------------------------------ shape describe('it copy — the surfaces exist and are complete', () => { it('has exactly the base key set', () => { - expect(KEYS).toHaveLength(1030) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines) + expect(KEYS).toHaveLength(1033) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines); Focus dimming: +3 (the three 0.21.1 release-note lines) expect(Object.keys(IT).sort()).toEqual([...KEYS].sort()) }) it('covers all four runtime surfaces, at the measured sizes', () => { const per: Record = {} for (const r of RUNTIME) per[r.surface.split('/')[0]!] = (per[r.surface.split('/')[0]!] ?? 0) + 1 - expect(per).toEqual({ catalog: 1030, template: 196, frame: 7, module: 19 }) - expect(RUNTIME).toHaveLength(1252) + expect(per).toEqual({ catalog: 1033, template: 196, frame: 7, module: 19 }) + expect(RUNTIME).toHaveLength(1255) }) it('EVERY row has a non-empty ENGLISH side — the vacuity guard', () => { diff --git a/src/i18n/locales/ar/ui.ts b/src/i18n/locales/ar/ui.ts index 3f9c284b..69339ea2 100644 --- a/src/i18n/locales/ar/ui.ts +++ b/src/i18n/locales/ar/ui.ts @@ -592,6 +592,9 @@ const ui = { 'whatsNew.v0210.compact': 'أصبحت العقد أكثر إحكامًا: العقدة ذات السطر الواحد أقصر وأضيق قليلًا، فيتسع على الشاشة جزء أكبر من المخطط الكبير.', 'whatsNew.v0210.grows': 'العقدة التي تحتاج إلى مساحة ما زالت تكبر: تحتفظ العناوين ذات السطرين والصفوف الإضافية بارتفاعها، وتلتف العناوين الطويلة كما من قبل.', 'whatsNew.v0210.same': 'لا تتغير مواضع العقد ولا الملفات المحفوظة ولا نتائج المحاكاة؛ وأصبحت الاتصالات تتصل بكل عقدة في موضع أعلى قليلًا.', + 'whatsNew.v0211.dim': 'عند تشغيل التركيز، تخفت الآن الاتصالات خارج منطقة التركيز كما تخفت العقد هناك، فيبرز الجوار الذي تركّز عليه.', + 'whatsNew.v0211.kept': 'يبقى التحديد والتحذيرات والعلامات المتحركة لمحاكاة قيد التشغيل ظاهرة بكامل وضوحها.', + 'whatsNew.v0211.contrast': 'في التباين العالي لا يُفتَّح لون الاتصال الخافت، بل يُميَّز بخط منقّط رفيع بدلًا من ذلك.', } as const export default ui diff --git a/src/i18n/locales/de/ui.ts b/src/i18n/locales/de/ui.ts index 20b8a8aa..84a59a64 100644 --- a/src/i18n/locales/de/ui.ts +++ b/src/i18n/locales/de/ui.ts @@ -627,6 +627,9 @@ const ui = { 'whatsNew.v0210.compact': 'Knoten sind kompakter: Ein einzeiliger Knoten ist niedriger und etwas schmaler, sodass mehr von einem großen Graphen auf den Bildschirm passt.', 'whatsNew.v0210.grows': 'Ein Knoten, der Platz braucht, wächst weiterhin: Zweizeilige Titel und zusätzliche Zeilen behalten ihre Höhe, und lange Titel brechen wie bisher um.', 'whatsNew.v0210.same': 'Knotenpositionen, gespeicherte Dateien und Simulationsergebnisse bleiben unverändert; Verbindungen setzen jetzt etwas höher am Knoten an.', + 'whatsNew.v0211.dim': 'Mit eingeschaltetem Fokus werden jetzt auch die Verbindungen außerhalb des fokussierten Bereichs abgeschwächt, wie die Knoten dort, sodass die fokussierte Umgebung hervortritt.', + 'whatsNew.v0211.kept': 'Auswahl, Warnungen und die beweglichen Markierungen einer laufenden Simulation bleiben voll sichtbar.', + 'whatsNew.v0211.contrast': 'Bei hohem Kontrast wird eine abgeschwächte Verbindung nicht blasser gezeichnet, sondern durch eine feine gepunktete Linie markiert.', } satisfies Record export default ui diff --git a/src/i18n/locales/en/ui.ts b/src/i18n/locales/en/ui.ts index af1cff6a..9014ca02 100644 --- a/src/i18n/locales/en/ui.ts +++ b/src/i18n/locales/en/ui.ts @@ -536,6 +536,9 @@ const ui = { 'whatsNew.v0210.compact': 'Nodes are more compact: a one-line node is shorter and a little narrower, so more of a large graph fits in view at once.', 'whatsNew.v0210.grows': 'A node that needs the room still grows: two-line titles and extra rows keep their height, and long titles wrap as before.', 'whatsNew.v0210.same': 'Node positions, saved files and simulation results are unchanged; connections now attach a little higher on each node.', + 'whatsNew.v0211.dim': 'With Focus on, the connections outside the focused area now fade like the nodes there, so the focused neighbourhood stands out.', + 'whatsNew.v0211.kept': 'Selection, warnings and the moving markers of a running simulation still show at full strength.', + 'whatsNew.v0211.contrast': 'In high contrast, a faded connection is not made paler: a fine dotted line marks it instead.', } as const export type UiKey = keyof typeof ui diff --git a/src/i18n/locales/es-419/ui.ts b/src/i18n/locales/es-419/ui.ts index 150fd845..1bf769d2 100644 --- a/src/i18n/locales/es-419/ui.ts +++ b/src/i18n/locales/es-419/ui.ts @@ -550,6 +550,9 @@ const ui = { 'whatsNew.v0210.compact': 'Los nodos son más compactos: un nodo de una línea es más bajo y algo más estrecho, así que cabe en pantalla una parte mayor de un grafo grande.', 'whatsNew.v0210.grows': 'Un nodo que necesita espacio sigue creciendo: los títulos de dos líneas y las filas adicionales mantienen su altura, y los títulos largos se ajustan como antes.', 'whatsNew.v0210.same': 'Las posiciones de los nodos, los archivos guardados y los resultados de la simulación no cambian; las conexiones ahora se unen un poco más arriba en cada nodo.', + 'whatsNew.v0211.dim': 'Con el enfoque activado, las conexiones fuera de la zona enfocada ahora se atenúan como los nodos de esa zona, de modo que el entorno enfocado destaca.', + 'whatsNew.v0211.kept': 'La selección, los avisos y las marcas en movimiento de una simulación en curso se siguen viendo con toda su intensidad.', + 'whatsNew.v0211.contrast': 'En contraste alto, una conexión atenuada no se aclara: la marca una línea de puntos fina.', } as const export type UiKey = keyof typeof ui diff --git a/src/i18n/locales/es-ES/ui.ts b/src/i18n/locales/es-ES/ui.ts index be4a1f27..7a62071b 100644 --- a/src/i18n/locales/es-ES/ui.ts +++ b/src/i18n/locales/es-ES/ui.ts @@ -564,6 +564,9 @@ const ui = { 'whatsNew.v0210.compact': 'Los nodos son más compactos: un nodo de una línea es más bajo y algo más estrecho, así que cabe en pantalla una parte mayor de un grafo grande.', 'whatsNew.v0210.grows': 'Un nodo que necesita espacio sigue creciendo: los títulos de dos líneas y las filas adicionales mantienen su altura, y los títulos largos se ajustan como antes.', 'whatsNew.v0210.same': 'Las posiciones de los nodos, los archivos guardados y los resultados de la simulación no cambian; las conexiones ahora se unen un poco más arriba en cada nodo.', + 'whatsNew.v0211.dim': 'Con el enfoque activado, las conexiones fuera de la zona enfocada ahora se atenúan como los nodos de esa zona, de modo que el entorno enfocado destaca.', + 'whatsNew.v0211.kept': 'La selección, los avisos y las marcas en movimiento de una simulación en curso se siguen viendo con toda su intensidad.', + 'whatsNew.v0211.contrast': 'En contraste alto, una conexión atenuada no se aclara: la marca una línea de puntos fina.', } as const export type UiKey = keyof typeof ui diff --git a/src/i18n/locales/fr/ui.ts b/src/i18n/locales/fr/ui.ts index 0f3bd74c..43a7278a 100644 --- a/src/i18n/locales/fr/ui.ts +++ b/src/i18n/locales/fr/ui.ts @@ -621,6 +621,9 @@ const ui = { 'whatsNew.v0210.compact': 'Les nœuds sont plus compacts : un nœud d’une ligne est moins haut et un peu plus étroit, pour qu’une plus grande partie d’un grand graphe tienne à l’écran.', 'whatsNew.v0210.grows': 'Un nœud qui a besoin de place grandit toujours : les titres sur deux lignes et les lignes supplémentaires gardent leur hauteur, et les titres longs passent à la ligne comme avant.', 'whatsNew.v0210.same': 'Les positions des nœuds, les fichiers enregistrés et les résultats de simulation ne changent pas ; les connexions s’attachent maintenant un peu plus haut sur chaque nœud.', + 'whatsNew.v0211.dim': 'Avec le Focus activé, les connexions hors de la zone ciblée s’estompent désormais comme les nœuds qui s’y trouvent, ce qui fait ressortir le voisinage ciblé.', + 'whatsNew.v0211.kept': 'La sélection, les avertissements et les repères mobiles d’une simulation en cours restent pleinement visibles.', + 'whatsNew.v0211.contrast': 'En contraste élevé, une connexion estompée n’est pas pâlie : de fins pointillés la signalent à la place.', } satisfies Record export default ui diff --git a/src/i18n/locales/it/ui.ts b/src/i18n/locales/it/ui.ts index f1fbcb7d..1d9eb4fa 100644 --- a/src/i18n/locales/it/ui.ts +++ b/src/i18n/locales/it/ui.ts @@ -566,6 +566,9 @@ const ui = { 'whatsNew.v0210.compact': 'I nodi sono più compatti: un nodo su una riga è più basso e un po’ più stretto, così una parte maggiore di un grafo grande entra nello schermo.', 'whatsNew.v0210.grows': 'Un nodo che ha bisogno di spazio cresce comunque: i titoli su due righe e le righe in più mantengono la loro altezza, e i titoli lunghi vanno a capo come prima.', 'whatsNew.v0210.same': 'Le posizioni dei nodi, i file salvati e i risultati della simulazione non cambiano; le connessioni ora si agganciano un po’ più in alto su ogni nodo.', + 'whatsNew.v0211.dim': 'Con la messa a fuoco attiva, anche le connessioni fuori dall’area a fuoco ora si attenuano come i nodi che vi si trovano, così l’area a fuoco risalta.', + 'whatsNew.v0211.kept': 'La selezione, gli avvisi e i segni in movimento di una simulazione in corso restano ben visibili.', + 'whatsNew.v0211.contrast': 'In contrasto elevato una connessione attenuata non viene schiarita: la segnala invece una linea punteggiata sottile.', } satisfies Record export default ui diff --git a/src/i18n/locales/ja/ui.ts b/src/i18n/locales/ja/ui.ts index 86f95385..ce0b967e 100644 --- a/src/i18n/locales/ja/ui.ts +++ b/src/i18n/locales/ja/ui.ts @@ -571,6 +571,9 @@ const ui = { 'whatsNew.v0210.compact': 'ノードがよりコンパクトになりました。1 行のノードが少し低く細くなり、大きなグラフを画面に多く収められます。', 'whatsNew.v0210.grows': '場所が必要なノードはこれまでどおり大きくなります。2 行のタイトルや追加の行は高さを保ち、長いタイトルも以前と同じく折り返します。', 'whatsNew.v0210.same': 'ノードの位置、保存したファイル、シミュレーション結果は変わりません。接続は各ノードの少し上側につながるようになりました。', + 'whatsNew.v0211.dim': 'フォーカスをオンにすると、フォーカス範囲外の接続もそこのノードと同じように淡く表示され、注目している周辺が際立ちます。', + 'whatsNew.v0211.kept': '選択、警告、実行中のシミュレーションで動く印は、これまでどおりはっきり表示されます。', + 'whatsNew.v0211.contrast': '高コントラストでは、淡くなった接続を薄くせず、代わりに細い点線で示します。', } satisfies Record export default ui diff --git a/src/i18n/locales/ko/ui.ts b/src/i18n/locales/ko/ui.ts index ff252154..5ae309cc 100644 --- a/src/i18n/locales/ko/ui.ts +++ b/src/i18n/locales/ko/ui.ts @@ -539,6 +539,9 @@ const ui = { 'whatsNew.v0210.compact': '노드가 더 촘촘해졌습니다. 한 줄 노드가 조금 낮고 좁아져 큰 그래프를 화면에 더 많이 담을 수 있습니다.', 'whatsNew.v0210.grows': '공간이 필요한 노드는 그대로 커집니다. 두 줄 제목과 추가 행은 높이를 유지하고, 긴 제목도 이전처럼 줄바꿈됩니다.', 'whatsNew.v0210.same': '노드 위치, 저장된 파일, 시뮬레이션 결과는 바뀌지 않습니다. 연결은 이제 각 노드에서 조금 더 위쪽에 붙습니다.', + 'whatsNew.v0211.dim': 'Focus를 켜면 집중 영역 밖의 연결선도 그곳의 노드처럼 흐려져, 집중한 주변이 더 잘 보입니다.', + 'whatsNew.v0211.kept': '선택, 경고, 실행 중인 시뮬레이션의 움직이는 표시는 그대로 선명하게 보입니다.', + 'whatsNew.v0211.contrast': '고대비에서는 흐려진 연결선을 옅게 만들지 않고, 대신 가는 점선으로 표시합니다.', } satisfies Record export default ui diff --git a/src/i18n/locales/nl/ui.ts b/src/i18n/locales/nl/ui.ts index 3d351484..64729684 100644 --- a/src/i18n/locales/nl/ui.ts +++ b/src/i18n/locales/nl/ui.ts @@ -547,6 +547,9 @@ const ui = { 'whatsNew.v0210.compact': 'Knooppunten zijn compacter: een knooppunt van één regel is lager en iets smaller, zodat er meer van een groot diagram op het scherm past.', 'whatsNew.v0210.grows': 'Een knooppunt dat ruimte nodig heeft, groeit nog steeds: titels van twee regels en aanvullende regels houden hun hoogte, en lange titels lopen door zoals voorheen.', 'whatsNew.v0210.same': 'Posities van knooppunten, opgeslagen bestanden en simulatieresultaten veranderen niet; verbindingen sluiten nu iets hoger op elk knooppunt aan.', + 'whatsNew.v0211.dim': 'Met Focus aan worden de verbindingen buiten het gefocuste gebied nu ook gedimd, net als de knooppunten daar, zodat de gefocuste omgeving opvalt.', + 'whatsNew.v0211.kept': 'Selectie, waarschuwingen en de bewegende markeringen van een lopende simulatie blijven volledig zichtbaar.', + 'whatsNew.v0211.contrast': 'Bij hoog contrast wordt een gedimde verbinding niet lichter getekend, maar met een fijne stippellijn gemarkeerd.', } as const export default ui diff --git a/src/i18n/locales/pt-BR/ui.ts b/src/i18n/locales/pt-BR/ui.ts index 6f2a520c..5f9d6e00 100644 --- a/src/i18n/locales/pt-BR/ui.ts +++ b/src/i18n/locales/pt-BR/ui.ts @@ -552,6 +552,9 @@ const ui = { 'whatsNew.v0210.compact': 'Os nós estão mais compactos: um nó de uma linha é mais baixo e um pouco mais estreito, e assim cabe mais de um grafo grande de uma só vez.', 'whatsNew.v0210.grows': 'Um nó que precisa de espaço continua a crescer: títulos de duas linhas e linhas adicionais mantêm a altura, e títulos longos passam para a linha seguinte como antes.', 'whatsNew.v0210.same': 'As posições dos nós, os arquivos salvos e os resultados da simulação não mudam; as conexões agora se ligam um pouco mais acima em cada nó.', + 'whatsNew.v0211.dim': 'Com o Foco ativado, as conexões fora da área em foco agora ficam esmaecidas como os nós dessa área, destacando a vizinhança em foco.', + 'whatsNew.v0211.kept': 'A seleção, os avisos e as marcas em movimento durante a simulação continuam bem visíveis.', + 'whatsNew.v0211.contrast': 'Em alto contraste, uma conexão esmaecida não fica mais clara: uma linha pontilhada fina a indica.', } as const export type UiKey = keyof typeof ui diff --git a/src/i18n/locales/pt-PT/ui.ts b/src/i18n/locales/pt-PT/ui.ts index 8349aca4..23943221 100644 --- a/src/i18n/locales/pt-PT/ui.ts +++ b/src/i18n/locales/pt-PT/ui.ts @@ -566,6 +566,9 @@ const ui = { 'whatsNew.v0210.compact': 'Os nós estão mais compactos: um nó de uma linha é mais baixo e um pouco mais estreito, e assim cabe mais de um grafo grande de uma só vez.', 'whatsNew.v0210.grows': 'Um nó que precisa de espaço continua a crescer: títulos de duas linhas e linhas adicionais mantêm a altura, e títulos longos passam para a linha seguinte como antes.', 'whatsNew.v0210.same': 'As posições dos nós, os ficheiros guardados e os resultados da simulação não mudam; as ligações passam a prender-se um pouco mais acima em cada nó.', + 'whatsNew.v0211.dim': 'Com o Foco ativado, as ligações fora da área em foco passam a ficar esbatidas como os nós dessa área, destacando a vizinhança em foco.', + 'whatsNew.v0211.kept': 'A seleção, os avisos e as marcas em movimento durante a simulação continuam bem visíveis.', + 'whatsNew.v0211.contrast': 'Em alto contraste, uma ligação esbatida não fica mais clara: uma linha pontilhada fina indica-a.', } as const export type UiKey = keyof typeof ui diff --git a/src/i18n/locales/ru/ui.ts b/src/i18n/locales/ru/ui.ts index 1f99f571..bb18ed6e 100644 --- a/src/i18n/locales/ru/ui.ts +++ b/src/i18n/locales/ru/ui.ts @@ -620,6 +620,9 @@ const ui = { 'whatsNew.v0210.compact': 'Узлы стали компактнее: однострочный узел ниже и немного уже, поэтому на экране помещается больше большого графа.', 'whatsNew.v0210.grows': 'Узел, которому нужно место, по-прежнему растёт: двухстрочные заголовки и дополнительные строки сохраняют высоту, а длинные заголовки переносятся как раньше.', 'whatsNew.v0210.same': 'Положения узлов, сохранённые файлы и результаты симуляции не меняются; связи теперь присоединяются к каждому узлу чуть выше.', + 'whatsNew.v0211.dim': 'При включённом фокусе связи вне области фокуса теперь тоже приглушаются, как и узлы там, и окружение в фокусе выделяется.', + 'whatsNew.v0211.kept': 'Выделение, предупреждения и движущиеся отметки идущей симуляции по-прежнему видны полностью.', + 'whatsNew.v0211.contrast': 'При высокой контрастности приглушённая связь не становится бледнее: её отмечает тонкая пунктирная линия.', } as const export type UiKey = keyof typeof ui diff --git a/src/i18n/locales/th/ui.ts b/src/i18n/locales/th/ui.ts index b85d44f4..730e1fb4 100644 --- a/src/i18n/locales/th/ui.ts +++ b/src/i18n/locales/th/ui.ts @@ -612,6 +612,9 @@ const ui = { 'whatsNew.v0210.compact': 'โหนดกะทัดรัดขึ้น โหนดบรรทัดเดียวเตี้ยลงและแคบลงเล็กน้อย จึงแสดงกราฟขนาดใหญ่บนหน้าจอได้มากขึ้น', 'whatsNew.v0210.grows': 'โหนดที่ต้องการพื้นที่ยังขยายได้ตามเดิม ชื่อสองบรรทัดและแถวเพิ่มเติมยังคงความสูงไว้ และชื่อยาวยังตัดบรรทัดเหมือนเดิม', 'whatsNew.v0210.same': 'ตำแหน่งโหนด ไฟล์ที่บันทึกไว้ และผลการจำลองไม่เปลี่ยน ตอนนี้เส้นเชื่อมจะต่อกับแต่ละโหนดในตำแหน่งที่สูงขึ้นเล็กน้อย', + 'whatsNew.v0211.dim': 'เมื่อเปิดโฟกัส เส้นเชื่อมที่อยู่นอกบริเวณที่โฟกัสจะจางลงเช่นเดียวกับโหนดในบริเวณนั้น ทำให้บริเวณที่โฟกัสเด่นขึ้น', + 'whatsNew.v0211.kept': 'การเลือก คำเตือน และเครื่องหมายเคลื่อนไหวของการจำลองที่กำลังทำงานยังคงแสดงเต็มที่', + 'whatsNew.v0211.contrast': 'ในโหมดคอนทราสต์สูง เส้นเชื่อมที่จางลงจะไม่ถูกทำให้ซีดลง แต่จะแสดงด้วยเส้นประจุดบาง ๆ แทน', } as const export type UiKey = keyof typeof ui diff --git a/src/i18n/locales/tr/ui.ts b/src/i18n/locales/tr/ui.ts index d653124d..68df891e 100644 --- a/src/i18n/locales/tr/ui.ts +++ b/src/i18n/locales/tr/ui.ts @@ -606,6 +606,9 @@ const ui = { 'whatsNew.v0210.compact': 'Düğümler daha kompakt: tek satırlık bir düğüm daha alçak ve biraz daha dar, böylece büyük bir grafiğin daha fazlası ekrana sığar.', 'whatsNew.v0210.grows': 'Yere ihtiyacı olan bir düğüm yine büyür: iki satırlık başlıklar ve ek satırlar yüksekliklerini korur, uzun başlıklar da eskisi gibi alt satıra geçer.', 'whatsNew.v0210.same': 'Düğüm konumları, kaydedilmiş dosyalar ve simülasyon sonuçları değişmez; bağlantılar artık her düğüme biraz daha yukarıdan bağlanır.', + 'whatsNew.v0211.dim': 'Odak açıkken odak alanının dışındaki bağlantılar da artık oradaki düğümler gibi soluklaşır; böylece odaklanılan çevre öne çıkar.', + 'whatsNew.v0211.kept': 'Seçim, uyarılar ve çalışan bir simülasyonun hareket eden işaretleri tam belirginlikte kalır.', + 'whatsNew.v0211.contrast': 'Yüksek karşıtlıkta soluklaşan bir bağlantı daha açık çizilmez; bunun yerine ince noktalı bir çizgiyle belirtilir.', } as const export type UiKey = keyof typeof ui diff --git a/src/i18n/locales/vi/ui.ts b/src/i18n/locales/vi/ui.ts index de1cad1c..3876543a 100644 --- a/src/i18n/locales/vi/ui.ts +++ b/src/i18n/locales/vi/ui.ts @@ -552,6 +552,9 @@ const ui = { 'whatsNew.v0210.compact': 'Nút gọn hơn: nút một dòng thấp hơn và hẹp hơn một chút, nên màn hình chứa được nhiều hơn của một đồ thị lớn.', 'whatsNew.v0210.grows': 'Nút cần chỗ vẫn lớn ra: tiêu đề hai dòng và các dòng thêm giữ nguyên chiều cao, tiêu đề dài vẫn xuống dòng như trước.', 'whatsNew.v0210.same': 'Vị trí nút, tệp đã lưu và kết quả mô phỏng không đổi; liên kết giờ gắn vào mỗi nút ở vị trí cao hơn một chút.', + 'whatsNew.v0211.dim': 'Khi bật Tiêu điểm, các liên kết nằm ngoài vùng tiêu điểm giờ cũng mờ đi như các nút ở đó, nhờ vậy vùng đang được tập trung nổi bật hơn.', + 'whatsNew.v0211.kept': 'Lựa chọn, cảnh báo và các dấu chuyển động của một mô phỏng đang chạy vẫn hiển thị rõ ràng.', + 'whatsNew.v0211.contrast': 'Ở chế độ tương phản cao, liên kết bị mờ không bị làm nhạt đi mà được đánh dấu bằng một đường chấm mảnh.', } satisfies Record export default ui diff --git a/src/i18n/locales/zh-Hans/ui.ts b/src/i18n/locales/zh-Hans/ui.ts index 76f3f59f..a69d0c07 100644 --- a/src/i18n/locales/zh-Hans/ui.ts +++ b/src/i18n/locales/zh-Hans/ui.ts @@ -544,6 +544,9 @@ const ui = { 'whatsNew.v0210.compact': '节点更紧凑了:单行节点更矮、略窄,屏幕上能显示更多的大型图。', 'whatsNew.v0210.grows': '需要空间的节点仍会变大:两行标题和额外的行保持原有高度,长标题照常换行。', 'whatsNew.v0210.same': '节点位置、已保存的文件和模拟结果都不变;连线现在连接在每个节点稍高的位置。', + 'whatsNew.v0211.dim': '开启聚焦后,聚焦范围以外的连线也会像那里的节点一样变淡,聚焦的邻近区域更加醒目。', + 'whatsNew.v0211.kept': '选择、警告和运行中模拟的移动标记仍以完整强度显示。', + 'whatsNew.v0211.contrast': '在高对比度下,变淡的连线不会变浅,而是改以细点线标示。', } satisfies Record export default ui diff --git a/src/i18n/locales/zh-Hant/ui.ts b/src/i18n/locales/zh-Hant/ui.ts index 7942332c..e5cb4387 100644 --- a/src/i18n/locales/zh-Hant/ui.ts +++ b/src/i18n/locales/zh-Hant/ui.ts @@ -570,6 +570,9 @@ const ui = { 'whatsNew.v0210.compact': '節點更緊湊了:單行節點更矮、略窄,螢幕上能顯示更多的大型圖。', 'whatsNew.v0210.grows': '需要空間的節點仍會變大:兩行標題和額外的列保持原有高度,長標題照常換行。', 'whatsNew.v0210.same': '節點位置、已儲存的檔案和模擬結果都不變;連線現在連接在每個節點稍高的位置。', + 'whatsNew.v0211.dim': '開啟聚焦後,聚焦範圍以外的連線也會像那裡的節點一樣變淡,聚焦的鄰近區域更加醒目。', + 'whatsNew.v0211.kept': '選取、警告和執行中模擬的移動標記仍以完整強度顯示。', + 'whatsNew.v0211.contrast': '在高對比度下,變淡的連線不會變淺,而是改以細點線標示。', } satisfies Record export default ui diff --git a/src/i18n/nlCopy.test.ts b/src/i18n/nlCopy.test.ts index 0fa9a6af..e1428716 100644 --- a/src/i18n/nlCopy.test.ts +++ b/src/i18n/nlCopy.test.ts @@ -261,15 +261,15 @@ const IDENTICAL_TO_EN: ReadonlyArray = [ // ------------------------------------------------------------------ shape describe('nl copy — the surfaces exist and are complete', () => { it('has exactly the base key set', () => { - expect(KEYS).toHaveLength(1030) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines) + expect(KEYS).toHaveLength(1033) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines); Focus dimming: +3 (the three 0.21.1 release-note lines) expect(Object.keys(NL).sort()).toEqual([...KEYS].sort()) }) it('covers all four runtime surfaces, at the measured sizes', () => { const per: Record = {} for (const r of RUNTIME) per[r.surface.split('/')[0]!] = (per[r.surface.split('/')[0]!] ?? 0) + 1 - expect(per).toEqual({ catalog: 1030, template: 196, frame: 7, module: 19 }) - expect(RUNTIME).toHaveLength(1252) + expect(per).toEqual({ catalog: 1033, template: 196, frame: 7, module: 19 }) + expect(RUNTIME).toHaveLength(1255) }) it('EVERY row has a non-empty ENGLISH side — the vacuity guard', () => { diff --git a/src/i18n/ptPtCopy.test.ts b/src/i18n/ptPtCopy.test.ts index db12810a..1fdbb392 100644 --- a/src/i18n/ptPtCopy.test.ts +++ b/src/i18n/ptPtCopy.test.ts @@ -58,7 +58,7 @@ describe('pt-PT copy — the region audit over pt-BR', () => { // ---------------------------------------------------------------- shape it('has exactly the same key set as pt-BR', () => { expect(Object.keys(ptValues).sort()).toEqual(KEYS.slice().sort()) - expect(KEYS).toHaveLength(1030) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines) + expect(KEYS).toHaveLength(1033) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines); Focus dimming: +3 (the three 0.21.1 release-note lines) }) it('differs from pt-BR on exactly the audited keys', () => { @@ -142,7 +142,10 @@ describe('pt-PT copy — the region audit over pt-BR', () => { // Compact nodes (0.21.0) moved it from 274 to 275: one of its three // release-note lines - `ficheiros guardados` not `arquivos salvos`, `ligações` // not `conexões`; the other two read the same in both. - expect(DELTA).toHaveLength(275) + // Focus dimming (0.21.1) moved it from 275 to 277: two of its three + // release-note lines - `ligações` / `ligação esbatida` not `conexões` / + // `conexão esmaecida`; the third reads the same in both. + expect(DELTA).toHaveLength(277) // and it is a real audit, not a rewrite — most of the catalog agrees. // // The password strings of issue #300 are counted apart. The word for a @@ -186,7 +189,7 @@ describe('pt-PT copy — the region audit over pt-BR', () => { // outside them, the quarter bound stands as it always did const rest = KEYS.filter((k) => !isPasswordKey(k)) const restDelta = DELTA.filter((k) => !isPasswordKey(k)) - expect(restDelta).toHaveLength(247) + expect(restDelta).toHaveLength(249) expect(restDelta.length).toBeLessThan(rest.length / 4) }) diff --git a/src/i18n/ruCopy.test.ts b/src/i18n/ruCopy.test.ts index db7e5fb4..b4c9e362 100644 --- a/src/i18n/ruCopy.test.ts +++ b/src/i18n/ruCopy.test.ts @@ -43,7 +43,7 @@ describe('ru copy — the first Cyrillic catalog', () => { // ------------------------------------------------------------------ shape it('has exactly the base key set', () => { expect(Object.keys(ruValues).sort()).toEqual(KEYS.slice().sort()) - expect(KEYS).toHaveLength(1030) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines) + expect(KEYS).toHaveLength(1033) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines); Focus dimming: +3 (the three 0.21.1 release-note lines) }) // ----------------------------------------------------------------- script @@ -209,6 +209,9 @@ describe('ru copy — the first Cyrillic catalog', () => { 'введённое', 'вернётся', 'включён', + // #329: the 0.21.1 release-note lines, `при включённом фокусе` and + // `приглушённая связь` + 'включённом', 'всё', 'даёт', 'ещё', @@ -244,6 +247,7 @@ describe('ru copy — the first Cyrillic catalog', () => { 'подключён', 'прервёт', 'приведён', + 'приглушённая', 'прошёл', 'разделённые', 'растёт', diff --git a/src/i18n/thCopy.test.ts b/src/i18n/thCopy.test.ts index 7e941330..d4bfd48b 100644 --- a/src/i18n/thCopy.test.ts +++ b/src/i18n/thCopy.test.ts @@ -88,11 +88,11 @@ const englishFor = (surface: string, id: string): string => { describe('th copy — the first no-space script', () => { it('has exactly the base key set', () => { expect(Object.keys(TH).sort()).toEqual(KEYS.slice().sort()) - expect(KEYS).toHaveLength(1030) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines) + expect(KEYS).toHaveLength(1033) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines); Focus dimming: +3 (the three 0.21.1 release-note lines) }) it('covers all three runtime surfaces', () => { - expect(RUNTIME.filter(([s]) => s === 'catalog')).toHaveLength(1030) + expect(RUNTIME.filter(([s]) => s === 'catalog')).toHaveLength(1033) expect(RUNTIME.filter(([s]) => s.startsWith('template/'))).toHaveLength(196) expect(RUNTIME.filter(([s]) => s.startsWith('frame/'))).toHaveLength(7) expect(RUNTIME.filter(([s]) => s.startsWith('module/'))).toHaveLength(19) diff --git a/src/i18n/trCopy.test.ts b/src/i18n/trCopy.test.ts index 30c1adba..4446149a 100644 --- a/src/i18n/trCopy.test.ts +++ b/src/i18n/trCopy.test.ts @@ -30,7 +30,7 @@ const KEYS = Object.keys(EN) describe('tr copy — the first Turkish catalog', () => { it('has exactly the base key set', () => { expect(Object.keys(TR).sort()).toEqual(KEYS.slice().sort()) - expect(KEYS).toHaveLength(1030) // issue #300: +33; issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines) + expect(KEYS).toHaveLength(1033) // issue #300: +33; issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines); Focus dimming: +3 (the three 0.21.1 release-note lines) }) }) diff --git a/src/i18n/viCopy.test.ts b/src/i18n/viCopy.test.ts index c4508c84..0f472146 100644 --- a/src/i18n/viCopy.test.ts +++ b/src/i18n/viCopy.test.ts @@ -132,11 +132,11 @@ const englishFor = (surface: string, id: string): string => { describe('vi copy — the first locale English hides inside', () => { it('has exactly the base key set', () => { expect(Object.keys(VI).sort()).toEqual(KEYS.slice().sort()) - expect(KEYS).toHaveLength(1030) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines) + expect(KEYS).toHaveLength(1033) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines); Focus dimming: +3 (the three 0.21.1 release-note lines) }) it('covers all four runtime surfaces', () => { - expect(RUNTIME.filter(([s]) => s === 'catalog')).toHaveLength(1030) + expect(RUNTIME.filter(([s]) => s === 'catalog')).toHaveLength(1033) expect(RUNTIME.filter(([s]) => s.startsWith('template/'))).toHaveLength(196) expect(RUNTIME.filter(([s]) => s.startsWith('frame/'))).toHaveLength(7) expect(RUNTIME.filter(([s]) => s.startsWith('module/'))).toHaveLength(19) diff --git a/src/index.css b/src/index.css index 4be2e901..b5e7a494 100644 --- a/src/index.css +++ b/src/index.css @@ -2224,6 +2224,13 @@ a.sheet__row { (`showLabel`), not here. The route-invalid `!` flag (`.route-invalid-flag`, required set) is left alone. §LGR4.2 — a dimmed edge is not selectable while Focus is on. */ +/* an unsatisfied activator connection is drawn at 0.5. It is a class, not an + inline style, so the Focus rule below (higher specificity) outranks it: a + connection outside the focus set is 0.26 whatever its activator (issue #329; + the inline `opacity` this replaces beat the Focus rule on every connection). */ +.react-flow__edge-path.edge-activator-off { + opacity: 0.5; +} .react-flow__edge.lgr-deemph .react-flow__edge-path { opacity: 0.26; } @@ -2233,10 +2240,13 @@ a.sheet__row { /* §LGR9 — under a UA colour override opacity is unreliable, so a de-emphasised element gets a sparse dash instead. A state edge is already dashed `4 4`; the - de-emphasis dash is finer so the two still read apart. */ + de-emphasis dash is finer so the two still read apart. The dash is the only + tell there: a dimmed connection is not faded, and its opacity is 1 even when + its activator is unsatisfied (issue #329). */ @media (forced-colors: active) { .react-flow__edge.lgr-deemph .react-flow__edge-path { stroke-dasharray: 1 5; + opacity: 1; } .react-flow__node.lgr-deemph .nodef__stroke { stroke-dasharray: 2 4; @@ -2965,7 +2975,7 @@ a.sheet__row { } .lgr-active-tint.edge-state:not(.route-invalid) { stroke-dasharray: 8 4; - /* an activator-OFF state edge carries `opacity: 0.5` inline; composited + /* an activator-OFF state edge carries `opacity: 0.5` (`.edge-activator-off`); composited over the canvas that halved the Highlight tell to 2.0–2.6:1 (measured at zoom 0.4–0.75). Opaque here only — light / dark keep the fade. */ opacity: 1 !important; diff --git a/src/releaseNotes/releaseNotes.ts b/src/releaseNotes/releaseNotes.ts index be95f927..cd820be4 100644 --- a/src/releaseNotes/releaseNotes.ts +++ b/src/releaseNotes/releaseNotes.ts @@ -38,6 +38,15 @@ export const releaseNoteIdFor = (version: string): ReleaseNoteId => `release:${v * mechanics, no test counts, no pull-request numbers. */ export const RELEASE_NOTES: readonly ReleaseNote[] = [ + { + // issue #329 - Focus mode dims the connections outside the focus set (an + // inline opacity beat the rule); the date is the day it is deployed, set + // right before the merge + id: 'release:0.21.1', + version: '0.21.1', + date: '2026-10-07', + items: ['whatsNew.v0211.dim', 'whatsNew.v0211.kept', 'whatsNew.v0211.contrast'], + }, { // issue #325 PR 3 - compact nodes: a 56 px floor instead of 64, less side // padding, content still deciding; the date is the day it is deployed, set