From 29dec5d57db6123b4cac497e6cd6497e6be2e502 Mon Sep 17 00:00:00 2001 From: Hanrim <148833226+MerciHanrim@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:28:36 +0900 Subject: [PATCH] fix(canvas): Focus mode dims the connections outside the focus set (v0.21.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bug reported in #329; part of #330. Version 0.21.1. Cause: src/components/edges/LoopEdge.tsx passed `opacity: activatorOn === false ? 0.5 : 1` as an inline style on every connection path (since e7a401d). The Focus rule `.react-flow__edge.lgr-deemph .react-flow__edge-path { opacity: 0.26 }` came later (986eed5), and an inline style beats a stylesheet rule, so a connection outside the focus set was never dimmed; only the nodes were. The tests asserted the class, not the drawn opacity. Fix: the unsatisfied activator's 0.5 is now the `.edge-activator-off` class (src/index.css), and the inline `opacity` is gone. The Focus rule has the higher specificity, so a connection outside the focus set is 0.26, its path and its arrowhead (the arrowhead is the path's own marker), whatever its activator says; an unsatisfied activator inside the focus set keeps its 0.5. Under forced colours a dimmed connection is not faded: opacity 1, and the existing sparse `1 5` dash stays its tell. Labels, the selection underlay, nodes, the playback tokens and cues, files, digests and the simulation are unchanged. Measured before and after on the same dev server with the same script, light, dark and forced colours: the only differences are the dimmed paths' opacity (1 to 0.26 in light and dark; an unsatisfied activator outside the focus set 0.5 to 0.26, and 0.5 to 1 under forced colours), the removed inline value, the new class and the arrowhead pixels. The first comparison had no selected connection, so the selection underlay was measured separately on the old and the new code: unchanged in all three (1 inside the focus set, 0.26 outside, same colour). Tests: e2e/focus-edge-dimming.spec.ts reads the drawn opacity: Focus inside and outside times activator satisfied and unsatisfied in light and dark, forced colours (opacity 1 and the `1 5` dash), and a played step (the dimmed path at 0.26 while the token keeps its opacity and colour). All four fail on the old code. e2e/whats-new.spec.ts pins 0.21.1. No visual baseline changes: no existing baseline is taken with Focus mode on. Release: three release-note lines in 18 languages, 16 without native review; the copy guards move only their exact pins (catalog 1030 to 1033, runtime 1252 to 1255, pt-PT's difference from pt-BR 275 to 277, two new ё words for ru), no bound widened. pt-PT's difference outside the password keys is now 249 against its quarter bound of 249.75; one of the three lines reads the same in pt-BR and pt-PT so it stays there. Docs: docs/large-graph-readability.md §LGR3.1, CHANGELOG.md and README.md. --- .changes/focus-edge-dimming.json | 1 + CHANGELOG.md | 11 ++ README.md | 23 ++-- docs/large-graph-readability.md | 9 ++ e2e/focus-edge-dimming.spec.ts | 170 ++++++++++++++++++++++++++++++ e2e/whats-new.spec.ts | 2 +- package-lock.json | 4 +- package.json | 2 +- src/components/edges/LoopEdge.tsx | 6 +- src/i18n/itCopy.test.ts | 6 +- src/i18n/locales/ar/ui.ts | 3 + src/i18n/locales/de/ui.ts | 3 + src/i18n/locales/en/ui.ts | 3 + src/i18n/locales/es-419/ui.ts | 3 + src/i18n/locales/es-ES/ui.ts | 3 + src/i18n/locales/fr/ui.ts | 3 + src/i18n/locales/it/ui.ts | 3 + src/i18n/locales/ja/ui.ts | 3 + src/i18n/locales/ko/ui.ts | 3 + src/i18n/locales/nl/ui.ts | 3 + src/i18n/locales/pt-BR/ui.ts | 3 + src/i18n/locales/pt-PT/ui.ts | 3 + src/i18n/locales/ru/ui.ts | 3 + src/i18n/locales/th/ui.ts | 3 + src/i18n/locales/tr/ui.ts | 3 + src/i18n/locales/vi/ui.ts | 3 + src/i18n/locales/zh-Hans/ui.ts | 3 + src/i18n/locales/zh-Hant/ui.ts | 3 + src/i18n/nlCopy.test.ts | 6 +- src/i18n/ptPtCopy.test.ts | 9 +- src/i18n/ruCopy.test.ts | 6 +- src/i18n/thCopy.test.ts | 4 +- src/i18n/trCopy.test.ts | 2 +- src/i18n/viCopy.test.ts | 4 +- src/index.css | 14 ++- src/releaseNotes/releaseNotes.ts | 9 ++ 36 files changed, 309 insertions(+), 33 deletions(-) create mode 100644 .changes/focus-edge-dimming.json create mode 100644 e2e/focus-edge-dimming.spec.ts 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