From 36f977f9e4bbdb4273efa50eddf0cbcfde72bee3 Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Wed, 13 May 2026 23:07:28 +0300 Subject: [PATCH 01/23] fix(8f-followup): delete legacy variscout-wall-layout Dexie DB on init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes 8f followup HIGH #3 — pre-8f users carried an orphan IndexedDB forever after the wallLayoutStore → canvasViewportStore shape change. Mirror PwaHubRepository's legacy-DB cleanup pattern. Tightens the existing test that lied about asserting deletion. Co-Authored-By: ruflo --- .../src/__tests__/canvasViewportStore.test.ts | 24 +++++++++++++++++++ packages/stores/src/canvasViewportStore.ts | 17 +++++++++++++ 2 files changed, 41 insertions(+) diff --git a/packages/stores/src/__tests__/canvasViewportStore.test.ts b/packages/stores/src/__tests__/canvasViewportStore.test.ts index 8eb8e7930..21a2350f8 100644 --- a/packages/stores/src/__tests__/canvasViewportStore.test.ts +++ b/packages/stores/src/__tests__/canvasViewportStore.test.ts @@ -5,6 +5,7 @@ import { useCanvasViewportStore, persistCanvasViewport, rehydrateCanvasViewport, + deleteLegacyWallLayoutDb, } from '../canvasViewportStore'; describe('canvasViewportStore', () => { @@ -295,6 +296,7 @@ describe('canvasViewportStore persistence', () => { }); it('clean-breaks an existing v1 project-keyed Dexie database before hub-keyed persistence', async () => { + // Seed the legacy DB so it exists in IndexedDB. await Dexie.delete('variscout-wall-layout'); const legacyDb = new Dexie('variscout-wall-layout'); legacyDb.version(1).stores({ snapshots: 'projectId,updatedAt' }); @@ -309,6 +311,16 @@ describe('canvasViewportStore persistence', () => { }); legacyDb.close(); + // Confirm legacy DB is present before cleanup. + await expect(Dexie.exists('variscout-wall-layout')).resolves.toBe(true); + + // Trigger cleanup explicitly (mirrors the module-load side effect). + await deleteLegacyWallLayoutDb(); + + // Legacy DB must be gone. + await expect(Dexie.exists('variscout-wall-layout')).resolves.toBe(false); + + // Hub-keyed persistence must still work correctly. useCanvasViewportStore.getState().setViewMode('wall'); useCanvasViewportStore.getState().setZoom('hub-legacy-clean-break', 1.75); useCanvasViewportStore @@ -326,6 +338,18 @@ describe('canvasViewportStore persistence', () => { }); }); + it('is a no-op when no legacy variscout-wall-layout DB exists', async () => { + // Ensure legacy DB is absent before test. + await Dexie.delete('variscout-wall-layout'); + await expect(Dexie.exists('variscout-wall-layout')).resolves.toBe(false); + + // Must not throw when the DB doesn't exist. + await expect(deleteLegacyWallLayoutDb()).resolves.toBeUndefined(); + + // DB is still absent — cleanup was a true no-op. + await expect(Dexie.exists('variscout-wall-layout')).resolves.toBe(false); + }); + it('persists and rehydrates one hub viewport with viewMode and railOpen', async () => { useCanvasViewportStore.getState().setViewMode('wall'); useCanvasViewportStore.getState().setRailOpen(false); diff --git a/packages/stores/src/canvasViewportStore.ts b/packages/stores/src/canvasViewportStore.ts index c5b704263..34f284724 100644 --- a/packages/stores/src/canvasViewportStore.ts +++ b/packages/stores/src/canvasViewportStore.ts @@ -310,6 +310,23 @@ class CanvasViewportDB extends Dexie { const db = new CanvasViewportDB(); +const LEGACY_WALL_LAYOUT_DB_NAME = 'variscout-wall-layout'; + +/** + * Best-effort cleanup of the pre-8f Dexie database `variscout-wall-layout`. + * Pre-8f users had a project-keyed store under that name; the 8f workstream + * renamed and restructured it to `variscout-canvas-viewport` (hub-keyed). + * Exported for testability; also called once at module load below. + */ +export function deleteLegacyWallLayoutDb(): Promise { + return Dexie.delete(LEGACY_WALL_LAYOUT_DB_NAME).catch(() => { + /* legacy DB cleanup is best-effort; ignore errors */ + }); +} + +// Fire-and-forget at module load. Does not block module initialisation. +void deleteLegacyWallLayoutDb(); + export async function persistCanvasViewport(hubId: ProcessHubId): Promise { const s = useCanvasViewportStore.getState(); await db.snapshots.put({ From 21f063f39b82bf025196db623f3601b391150ee1 Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Wed, 13 May 2026 23:19:57 +0300 Subject: [PATCH 02/23] refactor(8f-followup): migrate canvas UI strings to typed message catalogs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes 8f followup HIGH #5 — 47 hardcoded English strings across SystemLevelView, CanvasLensPicker, MobileLevelPicker, NoFocalStepPrompt, AuthorL3View, LocalMechanismView now route through MessageCatalog. CANVAS_LENS_REGISTRY labels/descriptions translated at render time in CanvasLensPicker via LENS_LABEL_KEY / LENS_DESC_KEY maps; the hooks registry keeps English for non-UI consumers. Non-English locales receive English placeholders pending a translation pass (TODO(i18n) comment). Co-Authored-By: ruflo --- packages/core/src/i18n/messages/ar.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/bg.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/cs.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/da.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/de.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/el.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/en.ts | 71 ++++++++++++++++++ packages/core/src/i18n/messages/es.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/fi.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/fr.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/he.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/hi.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/hr.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/hu.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/id.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/it.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/ja.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/ko.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/ms.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/nb.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/nl.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/pl.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/pt.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/ro.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/sk.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/sv.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/th.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/tr.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/uk.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/vi.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/zhHans.ts | 72 ++++++++++++++++++ packages/core/src/i18n/messages/zhHant.ts | 72 ++++++++++++++++++ packages/core/src/i18n/types.ts | 70 +++++++++++++++++ .../Canvas/internal/AuthorL3View.tsx | 57 ++++++++++---- .../Canvas/internal/CanvasLensPicker.tsx | 75 ++++++++++++------- .../Canvas/internal/LocalMechanismView.tsx | 40 +++++++--- .../Canvas/internal/MobileLevelPicker.tsx | 17 +++-- .../Canvas/internal/NoFocalStepPrompt.tsx | 17 +++-- .../Canvas/internal/SystemLevelView.tsx | 58 ++++++++++---- 39 files changed, 2559 insertions(+), 78 deletions(-) diff --git a/packages/core/src/i18n/messages/ar.ts b/packages/core/src/i18n/messages/ar.ts index 7585fe267..4950c5b3c 100644 --- a/packages/core/src/i18n/messages/ar.ts +++ b/packages/core/src/i18n/messages/ar.ts @@ -1023,4 +1023,76 @@ export const ar: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/bg.ts b/packages/core/src/i18n/messages/bg.ts index da5014b57..554a803b7 100644 --- a/packages/core/src/i18n/messages/bg.ts +++ b/packages/core/src/i18n/messages/bg.ts @@ -1032,4 +1032,76 @@ export const bg: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/cs.ts b/packages/core/src/i18n/messages/cs.ts index 51d51eda0..1ffd3b3ca 100644 --- a/packages/core/src/i18n/messages/cs.ts +++ b/packages/core/src/i18n/messages/cs.ts @@ -943,4 +943,76 @@ export const cs: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/da.ts b/packages/core/src/i18n/messages/da.ts index 64a5b8b01..f219f3233 100644 --- a/packages/core/src/i18n/messages/da.ts +++ b/packages/core/src/i18n/messages/da.ts @@ -996,4 +996,76 @@ export const da: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/de.ts b/packages/core/src/i18n/messages/de.ts index f9310aff3..b4a0a1377 100644 --- a/packages/core/src/i18n/messages/de.ts +++ b/packages/core/src/i18n/messages/de.ts @@ -1035,4 +1035,76 @@ export const de: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/el.ts b/packages/core/src/i18n/messages/el.ts index c7312d9cf..dacd35c00 100644 --- a/packages/core/src/i18n/messages/el.ts +++ b/packages/core/src/i18n/messages/el.ts @@ -1035,4 +1035,76 @@ export const el: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/en.ts b/packages/core/src/i18n/messages/en.ts index ff64b6f2d..b25e2318d 100644 --- a/packages/core/src/i18n/messages/en.ts +++ b/packages/core/src/i18n/messages/en.ts @@ -1041,4 +1041,75 @@ export const en: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker (toolbar aria + per-lens aria) + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions (used by CanvasLensPicker) + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/es.ts b/packages/core/src/i18n/messages/es.ts index 5f4deb543..a852dc51f 100644 --- a/packages/core/src/i18n/messages/es.ts +++ b/packages/core/src/i18n/messages/es.ts @@ -1037,4 +1037,76 @@ export const es: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/fi.ts b/packages/core/src/i18n/messages/fi.ts index 5e5f03cd5..6eee04d4e 100644 --- a/packages/core/src/i18n/messages/fi.ts +++ b/packages/core/src/i18n/messages/fi.ts @@ -1035,4 +1035,76 @@ export const fi: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/fr.ts b/packages/core/src/i18n/messages/fr.ts index cd2677f01..f609b7b73 100644 --- a/packages/core/src/i18n/messages/fr.ts +++ b/packages/core/src/i18n/messages/fr.ts @@ -1041,4 +1041,76 @@ export const fr: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/he.ts b/packages/core/src/i18n/messages/he.ts index 6e70bbd32..5c80c9db3 100644 --- a/packages/core/src/i18n/messages/he.ts +++ b/packages/core/src/i18n/messages/he.ts @@ -1022,4 +1022,76 @@ export const he: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/hi.ts b/packages/core/src/i18n/messages/hi.ts index f356e83b9..573afc3c5 100644 --- a/packages/core/src/i18n/messages/hi.ts +++ b/packages/core/src/i18n/messages/hi.ts @@ -1034,4 +1034,76 @@ export const hi: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/hr.ts b/packages/core/src/i18n/messages/hr.ts index 4956dfe55..6afb0b7d4 100644 --- a/packages/core/src/i18n/messages/hr.ts +++ b/packages/core/src/i18n/messages/hr.ts @@ -1029,4 +1029,76 @@ export const hr: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/hu.ts b/packages/core/src/i18n/messages/hu.ts index 0828e5a4e..64d32b4ff 100644 --- a/packages/core/src/i18n/messages/hu.ts +++ b/packages/core/src/i18n/messages/hu.ts @@ -947,4 +947,76 @@ export const hu: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/id.ts b/packages/core/src/i18n/messages/id.ts index 3e2ce5f0a..2027393aa 100644 --- a/packages/core/src/i18n/messages/id.ts +++ b/packages/core/src/i18n/messages/id.ts @@ -982,4 +982,76 @@ export const id: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/it.ts b/packages/core/src/i18n/messages/it.ts index aec6483cb..655d46e74 100644 --- a/packages/core/src/i18n/messages/it.ts +++ b/packages/core/src/i18n/messages/it.ts @@ -1005,4 +1005,76 @@ export const it: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/ja.ts b/packages/core/src/i18n/messages/ja.ts index 0c06b9a05..be807eec0 100644 --- a/packages/core/src/i18n/messages/ja.ts +++ b/packages/core/src/i18n/messages/ja.ts @@ -994,4 +994,76 @@ export const ja: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/ko.ts b/packages/core/src/i18n/messages/ko.ts index 1465a3b7d..82517355d 100644 --- a/packages/core/src/i18n/messages/ko.ts +++ b/packages/core/src/i18n/messages/ko.ts @@ -994,4 +994,76 @@ export const ko: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/ms.ts b/packages/core/src/i18n/messages/ms.ts index 08134ee8c..5157a9ddf 100644 --- a/packages/core/src/i18n/messages/ms.ts +++ b/packages/core/src/i18n/messages/ms.ts @@ -1033,4 +1033,76 @@ export const ms: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/nb.ts b/packages/core/src/i18n/messages/nb.ts index 23be34d50..bb25f8a49 100644 --- a/packages/core/src/i18n/messages/nb.ts +++ b/packages/core/src/i18n/messages/nb.ts @@ -944,4 +944,76 @@ export const nb: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/nl.ts b/packages/core/src/i18n/messages/nl.ts index 6f6e7c7b1..b0a8c70d4 100644 --- a/packages/core/src/i18n/messages/nl.ts +++ b/packages/core/src/i18n/messages/nl.ts @@ -1004,4 +1004,76 @@ export const nl: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/pl.ts b/packages/core/src/i18n/messages/pl.ts index 6b7b625b4..617e17332 100644 --- a/packages/core/src/i18n/messages/pl.ts +++ b/packages/core/src/i18n/messages/pl.ts @@ -1001,4 +1001,76 @@ export const pl: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/pt.ts b/packages/core/src/i18n/messages/pt.ts index 40e090761..6b2102087 100644 --- a/packages/core/src/i18n/messages/pt.ts +++ b/packages/core/src/i18n/messages/pt.ts @@ -1037,4 +1037,76 @@ export const pt: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/ro.ts b/packages/core/src/i18n/messages/ro.ts index e2cc2a8d5..cf1a443eb 100644 --- a/packages/core/src/i18n/messages/ro.ts +++ b/packages/core/src/i18n/messages/ro.ts @@ -983,4 +983,76 @@ export const ro: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/sk.ts b/packages/core/src/i18n/messages/sk.ts index 42603d47c..5f76acba4 100644 --- a/packages/core/src/i18n/messages/sk.ts +++ b/packages/core/src/i18n/messages/sk.ts @@ -1032,4 +1032,76 @@ export const sk: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/sv.ts b/packages/core/src/i18n/messages/sv.ts index cb4fb79d6..eb5139fe6 100644 --- a/packages/core/src/i18n/messages/sv.ts +++ b/packages/core/src/i18n/messages/sv.ts @@ -994,4 +994,76 @@ export const sv: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/th.ts b/packages/core/src/i18n/messages/th.ts index fb3e715c1..cd6d34d1b 100644 --- a/packages/core/src/i18n/messages/th.ts +++ b/packages/core/src/i18n/messages/th.ts @@ -973,4 +973,76 @@ export const th: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/tr.ts b/packages/core/src/i18n/messages/tr.ts index e589a60f8..160b431bf 100644 --- a/packages/core/src/i18n/messages/tr.ts +++ b/packages/core/src/i18n/messages/tr.ts @@ -1002,4 +1002,76 @@ export const tr: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/uk.ts b/packages/core/src/i18n/messages/uk.ts index 1566d39a2..f401e4606 100644 --- a/packages/core/src/i18n/messages/uk.ts +++ b/packages/core/src/i18n/messages/uk.ts @@ -983,4 +983,76 @@ export const uk: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/vi.ts b/packages/core/src/i18n/messages/vi.ts index d58d496a9..3c295e1f5 100644 --- a/packages/core/src/i18n/messages/vi.ts +++ b/packages/core/src/i18n/messages/vi.ts @@ -982,4 +982,76 @@ export const vi: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/zhHans.ts b/packages/core/src/i18n/messages/zhHans.ts index cde7431b1..f0d90ef0d 100644 --- a/packages/core/src/i18n/messages/zhHans.ts +++ b/packages/core/src/i18n/messages/zhHans.ts @@ -985,4 +985,76 @@ export const zhHans: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/messages/zhHant.ts b/packages/core/src/i18n/messages/zhHant.ts index 2bf7cefca..914389fb6 100644 --- a/packages/core/src/i18n/messages/zhHant.ts +++ b/packages/core/src/i18n/messages/zhHant.ts @@ -985,4 +985,76 @@ export const zhHant: MessageCatalog = { 'timeLens.mode.openEnded': 'Open-ended', 'timeLens.input.windowSize': 'Window size', 'timeLens.input.anchor': 'Anchor', + + // TODO(i18n): translate canvas.* keys + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': 'Active investigations', + 'canvas.system.conformance': 'Conformance', + 'canvas.system.inbox': 'Inbox', + 'canvas.system.lensLabel': 'Lens: {lens}', + 'canvas.system.noNumericOutcome': 'No numeric outcome', + 'canvas.system.noOutcomePrompts': 'No outcome prompts', + 'canvas.system.noOutcomeTrend': 'No outcome trend', + 'canvas.system.openScout': 'Open SCOUT', + 'canvas.system.outcomeDistribution': 'Outcome distribution', + 'canvas.system.outcomeDrift': 'Outcome drift', + 'canvas.system.outOfSpecMessage': '{outcome} has {pct} readings outside spec.', + 'canvas.system.reviewAction': 'Review', + + // Canvas — CanvasLensPicker + 'canvas.lensPicker.ariaLabel': 'Canvas lenses', + 'canvas.lensPicker.lensAriaLabel': '{label} lens', + + // Canvas — lens labels & descriptions + 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', + 'canvas.lens.capability.label': 'Capability', + 'canvas.lens.default.description': 'Step metrics, specs, and current card state.', + 'canvas.lens.default.label': 'Default', + 'canvas.lens.defect.description': 'Defect counts projected onto process steps.', + 'canvas.lens.defect.label': 'Defect', + 'canvas.lens.performance.description': 'Future within-step channel lens.', + 'canvas.lens.performance.label': 'Performance', + 'canvas.lens.processFlow.description': 'Plain process structure without per-card analytics.', + 'canvas.lens.processFlow.label': 'Process flow', + 'canvas.lens.yamazumi.description': 'Future time-study lens.', + 'canvas.lens.yamazumi.label': 'Yamazumi', + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': 'Choose a process step', + 'canvas.noFocalStep.description': 'Local mechanism view needs a focal process step.', + 'canvas.noFocalStep.heading': 'Choose a step for L3', + 'canvas.noFocalStep.noStepsHint': 'Add a process step before opening the local mechanism view.', + 'canvas.noFocalStep.openStepAria': 'Open {stepName} local mechanism', + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': 'Canvas levels', + 'canvas.mobile.process': 'Process', + 'canvas.mobile.step': 'Step', + 'canvas.mobile.system': 'System', + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': 'Assigned columns', + 'canvas.authorL3.ctqHeading': 'CTQ', + 'canvas.authorL3.dropHint': 'Drop columns here to assign them to this process step.', + 'canvas.authorL3.dropTargetAria': '{stepName} assignment target', + 'canvas.authorL3.dropTargetAriaWithChip': + '{stepName} assignment target, press Enter to place {chipLabel}', + 'canvas.authorL3.noAssignedColumns': 'No assigned columns yet', + 'canvas.authorL3.noCtqContext': 'No unassigned CTQ context', + 'canvas.authorL3.noTributaryContext': 'No unassigned tributary context', + 'canvas.authorL3.selectedStep': 'Selected step', + 'canvas.authorL3.tributaryColumns': 'Tributary columns', + 'canvas.authorL3.unassignedColumns': 'Unassigned columns', + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': 'Action', + 'canvas.localMechanism.etaSquaredLabel': 'eta² {value}', + 'canvas.localMechanism.evidenceMap': 'Local evidence map', + 'canvas.localMechanism.factorContribution': 'Factor contribution evidence', + 'canvas.localMechanism.investigationWall': 'Investigation wall', + 'canvas.localMechanism.logActionAria': 'Log action for {column}', + 'canvas.localMechanism.noNumericValues': 'No numeric values', + 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', + 'canvas.localMechanism.openColumnAria': 'Open {column} details', + 'canvas.localMechanism.quickActionTitle': '{column} quick action', }; diff --git a/packages/core/src/i18n/types.ts b/packages/core/src/i18n/types.ts index e623bb09b..359a2a0e4 100644 --- a/packages/core/src/i18n/types.ts +++ b/packages/core/src/i18n/types.ts @@ -1123,4 +1123,74 @@ export interface MessageCatalog { 'timeLens.mode.openEnded': string; 'timeLens.input.windowSize': string; 'timeLens.input.anchor': string; + + // Canvas — SystemLevelView + 'canvas.system.activeInvestigations': string; + 'canvas.system.conformance': string; + 'canvas.system.inbox': string; + 'canvas.system.lensLabel': string; + 'canvas.system.noNumericOutcome': string; + 'canvas.system.noOutcomePrompts': string; + 'canvas.system.noOutcomeTrend': string; + 'canvas.system.openScout': string; + 'canvas.system.outcomeDistribution': string; + 'canvas.system.outcomeDrift': string; + 'canvas.system.outOfSpecMessage': string; + 'canvas.system.reviewAction': string; + + // Canvas — CanvasLensPicker (toolbar aria + per-lens aria) + 'canvas.lensPicker.ariaLabel': string; + 'canvas.lensPicker.lensAriaLabel': string; + + // Canvas — lens labels & descriptions (used by CanvasLensPicker) + 'canvas.lens.capability.description': string; + 'canvas.lens.capability.label': string; + 'canvas.lens.default.description': string; + 'canvas.lens.default.label': string; + 'canvas.lens.defect.description': string; + 'canvas.lens.defect.label': string; + 'canvas.lens.performance.description': string; + 'canvas.lens.performance.label': string; + 'canvas.lens.processFlow.description': string; + 'canvas.lens.processFlow.label': string; + 'canvas.lens.yamazumi.description': string; + 'canvas.lens.yamazumi.label': string; + + // Canvas — NoFocalStepPrompt + 'canvas.noFocalStep.ariaLabel': string; + 'canvas.noFocalStep.description': string; + 'canvas.noFocalStep.heading': string; + 'canvas.noFocalStep.noStepsHint': string; + 'canvas.noFocalStep.openStepAria': string; + + // Canvas — MobileLevelPicker + 'canvas.mobile.ariaLabel': string; + 'canvas.mobile.process': string; + 'canvas.mobile.step': string; + 'canvas.mobile.system': string; + + // Canvas — AuthorL3View + 'canvas.authorL3.assignedColumns': string; + 'canvas.authorL3.ctqHeading': string; + 'canvas.authorL3.dropHint': string; + 'canvas.authorL3.dropTargetAria': string; + 'canvas.authorL3.dropTargetAriaWithChip': string; + 'canvas.authorL3.noAssignedColumns': string; + 'canvas.authorL3.noCtqContext': string; + 'canvas.authorL3.noTributaryContext': string; + 'canvas.authorL3.selectedStep': string; + 'canvas.authorL3.tributaryColumns': string; + 'canvas.authorL3.unassignedColumns': string; + + // Canvas — LocalMechanismView + 'canvas.localMechanism.actionButton': string; + 'canvas.localMechanism.etaSquaredLabel': string; + 'canvas.localMechanism.evidenceMap': string; + 'canvas.localMechanism.factorContribution': string; + 'canvas.localMechanism.investigationWall': string; + 'canvas.localMechanism.logActionAria': string; + 'canvas.localMechanism.noNumericValues': string; + 'canvas.localMechanism.openChartAria': string; + 'canvas.localMechanism.openColumnAria': string; + 'canvas.localMechanism.quickActionTitle': string; } diff --git a/packages/ui/src/components/Canvas/internal/AuthorL3View.tsx b/packages/ui/src/components/Canvas/internal/AuthorL3View.tsx index f8823a1b3..0e5598804 100644 --- a/packages/ui/src/components/Canvas/internal/AuthorL3View.tsx +++ b/packages/ui/src/components/Canvas/internal/AuthorL3View.tsx @@ -1,8 +1,10 @@ import React from 'react'; import { useDroppable } from '@dnd-kit/core'; +import { formatMessage, getMessage } from '@variscout/core/i18n'; import type { ProcessMap } from '@variscout/core/frame'; import { encodeStepDropId } from '@variscout/hooks'; import { ChipRail, type ChipRailEntry } from '../../ChipRail'; +import { useWallLocale } from '../../InvestigationWall/hooks/useWallLocale'; export interface AuthorL3ViewProps { hubId: string; @@ -21,6 +23,7 @@ function focalStepColumns(map: ProcessMap, focalStepId: string) { .map(([column]) => column); const assignedSet = new Set(assigned); const step = map.nodes.find(node => node.id === focalStepId); + // stepName fallback is resolved to locale-aware string at render time; 'Selected step' sentinel used here const ctqColumn = step?.ctqColumn && !assignedSet.has(step.ctqColumn) ? step.ctqColumn : null; const tributaryColumns = map.tributaries .filter(tributary => tributary.stepId === focalStepId && !assignedSet.has(tributary.column)) @@ -28,7 +31,7 @@ function focalStepColumns(map: ProcessMap, focalStepId: string) { return { assigned, - stepName: step?.name || 'Selected step', + stepName: step?.name ?? null, ctqColumn, tributaryColumns, }; @@ -52,16 +55,20 @@ export function AuthorL3View({ onKeyboardChipPickUp, onKeyboardChipDrop, }: AuthorL3ViewProps) { + const locale = useWallLocale(); const [keyboardChipId, setKeyboardChipId] = React.useState(null); const droppableId = encodeStepDropId(focalStepId); const { setNodeRef, isOver } = useDroppable({ id: droppableId, disabled, }); - const { assigned, stepName, ctqColumn, tributaryColumns } = React.useMemo( - () => focalStepColumns(map, focalStepId), - [focalStepId, map] - ); + const { + assigned, + stepName: rawStepName, + ctqColumn, + tributaryColumns, + } = React.useMemo(() => focalStepColumns(map, focalStepId), [focalStepId, map]); + const stepName = rawStepName ?? getMessage(locale, 'canvas.authorL3.selectedStep'); const keyboardChipLabel = keyboardChipId ? chips.find(chip => chip.chipId === keyboardChipId)?.label : null; @@ -97,7 +104,10 @@ export function AuthorL3View({ data-testid="author-l3-view" data-hub-id={hubId} > -
+

- Drop columns here to assign them to this process step. + {getMessage(locale, 'canvas.authorL3.dropHint')}

-

Assigned columns

+

+ {getMessage(locale, 'canvas.authorL3.assignedColumns')} +

{assigned.length > 0 ? (
    {assigned.map(column => ( @@ -150,25 +167,31 @@ export function AuthorL3View({
) : (

- No assigned columns yet + {getMessage(locale, 'canvas.authorL3.noAssignedColumns')}

)}
-

CTQ

+

+ {getMessage(locale, 'canvas.authorL3.ctqHeading')} +

{ctqColumn ? (
) : ( -

No unassigned CTQ context

+

+ {getMessage(locale, 'canvas.authorL3.noCtqContext')} +

)}
-

Tributary columns

+

+ {getMessage(locale, 'canvas.authorL3.tributaryColumns')} +

{tributaryColumns.length > 0 ? (
    {tributaryColumns.map(column => ( @@ -176,7 +199,9 @@ export function AuthorL3View({ ))}
) : ( -

No unassigned tributary context

+

+ {getMessage(locale, 'canvas.authorL3.noTributaryContext')} +

)}
diff --git a/packages/ui/src/components/Canvas/internal/CanvasLensPicker.tsx b/packages/ui/src/components/Canvas/internal/CanvasLensPicker.tsx index 4f39d674f..6467ada37 100644 --- a/packages/ui/src/components/Canvas/internal/CanvasLensPicker.tsx +++ b/packages/ui/src/components/Canvas/internal/CanvasLensPicker.tsx @@ -1,9 +1,12 @@ import React from 'react'; +import type { MessageCatalog } from '@variscout/core'; +import { formatMessage, getMessage } from '@variscout/core/i18n'; import { CANVAS_LENS_REGISTRY, type CanvasLensDefinition, type CanvasLensId, } from '@variscout/hooks'; +import { useWallLocale } from '../../InvestigationWall/hooks/useWallLocale'; interface CanvasLensPickerProps { activeLens: CanvasLensId; @@ -19,38 +22,60 @@ const orderedLenses: CanvasLensDefinition[] = [ CANVAS_LENS_REGISTRY.yamazumi, ]; +const LENS_LABEL_KEY: Record = { + default: 'canvas.lens.default.label', + capability: 'canvas.lens.capability.label', + defect: 'canvas.lens.defect.label', + performance: 'canvas.lens.performance.label', + yamazumi: 'canvas.lens.yamazumi.label', + 'process-flow': 'canvas.lens.processFlow.label', +}; + +const LENS_DESC_KEY: Record = { + default: 'canvas.lens.default.description', + capability: 'canvas.lens.capability.description', + defect: 'canvas.lens.defect.description', + performance: 'canvas.lens.performance.description', + yamazumi: 'canvas.lens.yamazumi.description', + 'process-flow': 'canvas.lens.processFlow.description', +}; + export const CanvasLensPicker: React.FC = ({ activeLens, onChange }) => { + const locale = useWallLocale(); return (
- {orderedLenses.map(lens => ( - - ))} + {orderedLenses.map(lens => { + const label = getMessage(locale, LENS_LABEL_KEY[lens.id]); + return ( + + ); + })}
); }; diff --git a/packages/ui/src/components/Canvas/internal/LocalMechanismView.tsx b/packages/ui/src/components/Canvas/internal/LocalMechanismView.tsx index 95913eb9a..af6e2ee89 100644 --- a/packages/ui/src/components/Canvas/internal/LocalMechanismView.tsx +++ b/packages/ui/src/components/Canvas/internal/LocalMechanismView.tsx @@ -6,7 +6,8 @@ import { computeMainEffects, conditionReferencesStep, } from '@variscout/core'; -import { formatStatistic } from '@variscout/core/i18n'; +import { formatMessage, formatStatistic, getMessage } from '@variscout/core/i18n'; +import type { Locale } from '@variscout/core'; import type { ColumnTypeMap } from '@variscout/core/findings'; import { EvidenceMapBase } from '@variscout/charts'; import { useEvidenceMapData } from '@variscout/hooks'; @@ -14,6 +15,7 @@ import { useInvestigationStore } from '@variscout/stores'; import { WallCanvas } from '../../InvestigationWall/WallCanvas'; import { MiniBoxplot } from '../../InvestigationWall/MiniBoxplot'; import { MiniIChart } from '../../InvestigationWall/MiniIChart'; +import { useWallLocale } from '../../InvestigationWall/hooks/useWallLocale'; import { LogActionModal, type LogActionPayload } from '../../QuickAction'; export interface LocalMechanismViewProps { @@ -177,6 +179,7 @@ function ColumnMiniChart({ kind, rows, outcomeColumn, + locale, onOpenColumnDetail, onOpenQuickAction, }: { @@ -184,6 +187,7 @@ function ColumnMiniChart({ kind: string | undefined; rows: ReadonlyArray; outcomeColumn: string | null | undefined; + locale: Locale; onOpenColumnDetail?: (column: string) => void; onOpenQuickAction: (column: string) => void; }) { @@ -199,7 +203,7 @@ function ColumnMiniChart({ @@ -41,7 +48,7 @@ export function NoFocalStepPrompt({ hubId, map }: NoFocalStepPromptProps) { ) : (

- Add a process step before opening the local mechanism view. + {getMessage(locale, 'canvas.noFocalStep.noStepsHint')}

)} diff --git a/packages/ui/src/components/Canvas/internal/SystemLevelView.tsx b/packages/ui/src/components/Canvas/internal/SystemLevelView.tsx index edbb0a1a3..fffd316a1 100644 --- a/packages/ui/src/components/Canvas/internal/SystemLevelView.tsx +++ b/packages/ui/src/components/Canvas/internal/SystemLevelView.tsx @@ -7,11 +7,13 @@ import { type Question, type SpecLimits, } from '@variscout/core'; -import { formatStatistic } from '@variscout/core/i18n'; +import { formatMessage, formatStatistic, getMessage } from '@variscout/core/i18n'; +import type { Locale } from '@variscout/core'; import type { ProcessMap } from '@variscout/core/frame'; import type { CanvasLensId, CanvasStepCardModel } from '@variscout/hooks'; import { buildSystemOutcomeModel } from '../../DashboardBase/internal/systemOutcomeModel'; import { InboxDigest, type InboxDigestPrompt } from '../../Inbox'; +import { useWallLocale } from '../../InvestigationWall/hooks/useWallLocale'; export interface SystemLevelViewProps { hubId: string; @@ -58,21 +60,25 @@ function inboxPrompts(args: { outcomeLabel: string; outOfSpecPercentage: number; drift: string; + locale: Locale; }): InboxDigestPrompt[] { const prompts: InboxDigestPrompt[] = []; if (args.outOfSpecPercentage > 0) { prompts.push({ id: `outcome-spec-${args.hubId}`, severity: 'warning', - message: `${args.outcomeLabel} has ${formatPercentage(args.outOfSpecPercentage)} readings outside spec.`, - action: { label: 'Review' }, + message: formatMessage(args.locale, 'canvas.system.outOfSpecMessage', { + outcome: args.outcomeLabel, + pct: formatPercentage(args.outOfSpecPercentage), + }), + action: { label: getMessage(args.locale, 'canvas.system.reviewAction') }, }); } else if (args.drift.includes('trending')) { prompts.push({ id: `outcome-drift-${args.hubId}`, severity: 'info', message: args.drift, - action: { label: 'Review' }, + action: { label: getMessage(args.locale, 'canvas.system.reviewAction') }, }); } return prompts; @@ -89,6 +95,7 @@ export const SystemLevelView: React.FC = ({ specLimits, onOpenScout, }) => { + const locale = useWallLocale(); const outcomeLabel = map.ctsColumn ?? 'Outcome not selected'; const model = buildSystemOutcomeModel({ rows, @@ -106,6 +113,7 @@ export const SystemLevelView: React.FC = ({ outcomeLabel, outOfSpecPercentage: model.outOfSpecPercentage, drift: model.drift.label, + locale, }); return ( @@ -116,7 +124,11 @@ export const SystemLevelView: React.FC = ({

{hubId}

{outcomeLabel}

- {activeLens ?

Lens: {activeLens}

: null} + {activeLens ? ( +

+ {formatMessage(locale, 'canvas.system.lensLabel', { lens: activeLens })} +

+ ) : null}
@@ -134,7 +146,9 @@ export const SystemLevelView: React.FC = ({ data-testid="outcome-distribution" >
-

Outcome distribution

+

+ {getMessage(locale, 'canvas.system.outcomeDistribution')} +

n={values.length}
{bins.length > 0 ? ( @@ -151,13 +165,17 @@ export const SystemLevelView: React.FC = ({ ))} ) : ( -

No numeric outcome

+

+ {getMessage(locale, 'canvas.system.noNumericOutcome')} +

)}
-

Outcome drift

+

+ {getMessage(locale, 'canvas.system.outcomeDrift')} +

= ({ /> ) : ( -

No outcome trend

+

+ {getMessage(locale, 'canvas.system.noOutcomeTrend')} +

)}
@@ -214,13 +234,15 @@ export const SystemLevelView: React.FC = ({
{formatMetric(model.ppk)}
-
Conformance
+
+ {getMessage(locale, 'canvas.system.conformance')} +
{formatPercentage(model.conformancePercentage)}
-
Target
+
{getMessage(locale, 'stats.target')}
{formatMetric(specLimits?.cpkTarget)}
@@ -234,8 +256,12 @@ export const SystemLevelView: React.FC = ({ undefined} /> ) : (
-

Inbox

-

No outcome prompts

+

+ {getMessage(locale, 'canvas.system.inbox')} +

+

+ {getMessage(locale, 'canvas.system.noOutcomePrompts')} +

)}
@@ -244,7 +270,9 @@ export const SystemLevelView: React.FC = ({ className="rounded-lg border border-edge bg-surface p-4" data-testid="active-investigations-summary" > -

Active investigations

+

+ {getMessage(locale, 'canvas.system.activeInvestigations')} +

{model.activeSummary}

From d0f2529dd4f688e64b95aa029fdbe99220312bff Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Wed, 13 May 2026 23:28:43 +0300 Subject: [PATCH 03/23] fix(8f-followup): migrate Canvas empty-state to message catalog Closes the i18n reviewer's flagged follow-up: Canvas/index.tsx:1042 rendered the lens registry's English label and a local CANVAS_LEVEL_LABELS map as user-facing copy, bypassing i18n. - Export LENS_LABEL_KEY from CanvasLensPicker for cross-component reuse - Reuse canvas.mobile.{system,process,step} for level labels - Add canvas.lensPicker.invalidAtLevel parameterized key (32 locales, English placeholder elsewhere per the catalog's translation pass plan) - Drop CANVAS_LEVEL_LABELS + remove now-unused CANVAS_LENS_REGISTRY import Co-Authored-By: ruflo --- packages/core/src/i18n/messages/ar.ts | 2 ++ packages/core/src/i18n/messages/bg.ts | 2 ++ packages/core/src/i18n/messages/cs.ts | 2 ++ packages/core/src/i18n/messages/da.ts | 2 ++ packages/core/src/i18n/messages/de.ts | 2 ++ packages/core/src/i18n/messages/el.ts | 2 ++ packages/core/src/i18n/messages/en.ts | 2 ++ packages/core/src/i18n/messages/es.ts | 2 ++ packages/core/src/i18n/messages/fi.ts | 2 ++ packages/core/src/i18n/messages/fr.ts | 2 ++ packages/core/src/i18n/messages/he.ts | 2 ++ packages/core/src/i18n/messages/hi.ts | 2 ++ packages/core/src/i18n/messages/hr.ts | 2 ++ packages/core/src/i18n/messages/hu.ts | 2 ++ packages/core/src/i18n/messages/id.ts | 2 ++ packages/core/src/i18n/messages/it.ts | 2 ++ packages/core/src/i18n/messages/ja.ts | 2 ++ packages/core/src/i18n/messages/ko.ts | 2 ++ packages/core/src/i18n/messages/ms.ts | 2 ++ packages/core/src/i18n/messages/nb.ts | 2 ++ packages/core/src/i18n/messages/nl.ts | 2 ++ packages/core/src/i18n/messages/pl.ts | 2 ++ packages/core/src/i18n/messages/pt.ts | 2 ++ packages/core/src/i18n/messages/ro.ts | 2 ++ packages/core/src/i18n/messages/sk.ts | 2 ++ packages/core/src/i18n/messages/sv.ts | 2 ++ packages/core/src/i18n/messages/th.ts | 2 ++ packages/core/src/i18n/messages/tr.ts | 2 ++ packages/core/src/i18n/messages/uk.ts | 2 ++ packages/core/src/i18n/messages/vi.ts | 2 ++ packages/core/src/i18n/messages/zhHans.ts | 2 ++ packages/core/src/i18n/messages/zhHant.ts | 2 ++ packages/core/src/i18n/types.ts | 1 + packages/ui/src/components/Canvas/index.tsx | 24 ++++++++++++------- .../Canvas/internal/CanvasLensPicker.tsx | 2 +- 35 files changed, 81 insertions(+), 10 deletions(-) diff --git a/packages/core/src/i18n/messages/ar.ts b/packages/core/src/i18n/messages/ar.ts index 4950c5b3c..d410e024e 100644 --- a/packages/core/src/i18n/messages/ar.ts +++ b/packages/core/src/i18n/messages/ar.ts @@ -1042,6 +1042,8 @@ export const ar: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/bg.ts b/packages/core/src/i18n/messages/bg.ts index 554a803b7..aded8cf4a 100644 --- a/packages/core/src/i18n/messages/bg.ts +++ b/packages/core/src/i18n/messages/bg.ts @@ -1051,6 +1051,8 @@ export const bg: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/cs.ts b/packages/core/src/i18n/messages/cs.ts index 1ffd3b3ca..91a947747 100644 --- a/packages/core/src/i18n/messages/cs.ts +++ b/packages/core/src/i18n/messages/cs.ts @@ -962,6 +962,8 @@ export const cs: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/da.ts b/packages/core/src/i18n/messages/da.ts index f219f3233..0cc666c9a 100644 --- a/packages/core/src/i18n/messages/da.ts +++ b/packages/core/src/i18n/messages/da.ts @@ -1015,6 +1015,8 @@ export const da: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/de.ts b/packages/core/src/i18n/messages/de.ts index b4a0a1377..292391bf3 100644 --- a/packages/core/src/i18n/messages/de.ts +++ b/packages/core/src/i18n/messages/de.ts @@ -1054,6 +1054,8 @@ export const de: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/el.ts b/packages/core/src/i18n/messages/el.ts index dacd35c00..e0a2f9173 100644 --- a/packages/core/src/i18n/messages/el.ts +++ b/packages/core/src/i18n/messages/el.ts @@ -1054,6 +1054,8 @@ export const el: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/en.ts b/packages/core/src/i18n/messages/en.ts index b25e2318d..6dffa6a48 100644 --- a/packages/core/src/i18n/messages/en.ts +++ b/packages/core/src/i18n/messages/en.ts @@ -1059,6 +1059,8 @@ export const en: MessageCatalog = { // Canvas — CanvasLensPicker (toolbar aria + per-lens aria) 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions (used by CanvasLensPicker) 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/es.ts b/packages/core/src/i18n/messages/es.ts index a852dc51f..a579c061d 100644 --- a/packages/core/src/i18n/messages/es.ts +++ b/packages/core/src/i18n/messages/es.ts @@ -1056,6 +1056,8 @@ export const es: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/fi.ts b/packages/core/src/i18n/messages/fi.ts index 6eee04d4e..b61b803a1 100644 --- a/packages/core/src/i18n/messages/fi.ts +++ b/packages/core/src/i18n/messages/fi.ts @@ -1054,6 +1054,8 @@ export const fi: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/fr.ts b/packages/core/src/i18n/messages/fr.ts index f609b7b73..34c00c48f 100644 --- a/packages/core/src/i18n/messages/fr.ts +++ b/packages/core/src/i18n/messages/fr.ts @@ -1060,6 +1060,8 @@ export const fr: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/he.ts b/packages/core/src/i18n/messages/he.ts index 5c80c9db3..c5e44f17c 100644 --- a/packages/core/src/i18n/messages/he.ts +++ b/packages/core/src/i18n/messages/he.ts @@ -1041,6 +1041,8 @@ export const he: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/hi.ts b/packages/core/src/i18n/messages/hi.ts index 573afc3c5..b45a7b663 100644 --- a/packages/core/src/i18n/messages/hi.ts +++ b/packages/core/src/i18n/messages/hi.ts @@ -1053,6 +1053,8 @@ export const hi: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/hr.ts b/packages/core/src/i18n/messages/hr.ts index 6afb0b7d4..368c8583d 100644 --- a/packages/core/src/i18n/messages/hr.ts +++ b/packages/core/src/i18n/messages/hr.ts @@ -1048,6 +1048,8 @@ export const hr: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/hu.ts b/packages/core/src/i18n/messages/hu.ts index 64d32b4ff..1829b433c 100644 --- a/packages/core/src/i18n/messages/hu.ts +++ b/packages/core/src/i18n/messages/hu.ts @@ -966,6 +966,8 @@ export const hu: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/id.ts b/packages/core/src/i18n/messages/id.ts index 2027393aa..389b0ff91 100644 --- a/packages/core/src/i18n/messages/id.ts +++ b/packages/core/src/i18n/messages/id.ts @@ -1001,6 +1001,8 @@ export const id: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/it.ts b/packages/core/src/i18n/messages/it.ts index 655d46e74..faca13159 100644 --- a/packages/core/src/i18n/messages/it.ts +++ b/packages/core/src/i18n/messages/it.ts @@ -1024,6 +1024,8 @@ export const it: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/ja.ts b/packages/core/src/i18n/messages/ja.ts index be807eec0..43876b317 100644 --- a/packages/core/src/i18n/messages/ja.ts +++ b/packages/core/src/i18n/messages/ja.ts @@ -1013,6 +1013,8 @@ export const ja: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/ko.ts b/packages/core/src/i18n/messages/ko.ts index 82517355d..940b4ac20 100644 --- a/packages/core/src/i18n/messages/ko.ts +++ b/packages/core/src/i18n/messages/ko.ts @@ -1013,6 +1013,8 @@ export const ko: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/ms.ts b/packages/core/src/i18n/messages/ms.ts index 5157a9ddf..e0cb5cec8 100644 --- a/packages/core/src/i18n/messages/ms.ts +++ b/packages/core/src/i18n/messages/ms.ts @@ -1052,6 +1052,8 @@ export const ms: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/nb.ts b/packages/core/src/i18n/messages/nb.ts index bb25f8a49..93110dde0 100644 --- a/packages/core/src/i18n/messages/nb.ts +++ b/packages/core/src/i18n/messages/nb.ts @@ -963,6 +963,8 @@ export const nb: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/nl.ts b/packages/core/src/i18n/messages/nl.ts index b0a8c70d4..3ac7b8c55 100644 --- a/packages/core/src/i18n/messages/nl.ts +++ b/packages/core/src/i18n/messages/nl.ts @@ -1023,6 +1023,8 @@ export const nl: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/pl.ts b/packages/core/src/i18n/messages/pl.ts index 617e17332..bbcececbe 100644 --- a/packages/core/src/i18n/messages/pl.ts +++ b/packages/core/src/i18n/messages/pl.ts @@ -1020,6 +1020,8 @@ export const pl: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/pt.ts b/packages/core/src/i18n/messages/pt.ts index 6b2102087..53cab83f2 100644 --- a/packages/core/src/i18n/messages/pt.ts +++ b/packages/core/src/i18n/messages/pt.ts @@ -1056,6 +1056,8 @@ export const pt: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/ro.ts b/packages/core/src/i18n/messages/ro.ts index cf1a443eb..45fc9df9f 100644 --- a/packages/core/src/i18n/messages/ro.ts +++ b/packages/core/src/i18n/messages/ro.ts @@ -1002,6 +1002,8 @@ export const ro: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/sk.ts b/packages/core/src/i18n/messages/sk.ts index 5f76acba4..18431fdc8 100644 --- a/packages/core/src/i18n/messages/sk.ts +++ b/packages/core/src/i18n/messages/sk.ts @@ -1051,6 +1051,8 @@ export const sk: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/sv.ts b/packages/core/src/i18n/messages/sv.ts index eb5139fe6..4f82a68cf 100644 --- a/packages/core/src/i18n/messages/sv.ts +++ b/packages/core/src/i18n/messages/sv.ts @@ -1013,6 +1013,8 @@ export const sv: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/th.ts b/packages/core/src/i18n/messages/th.ts index cd6d34d1b..5b4572922 100644 --- a/packages/core/src/i18n/messages/th.ts +++ b/packages/core/src/i18n/messages/th.ts @@ -992,6 +992,8 @@ export const th: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/tr.ts b/packages/core/src/i18n/messages/tr.ts index 160b431bf..c7e0a3579 100644 --- a/packages/core/src/i18n/messages/tr.ts +++ b/packages/core/src/i18n/messages/tr.ts @@ -1021,6 +1021,8 @@ export const tr: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/uk.ts b/packages/core/src/i18n/messages/uk.ts index f401e4606..bee8cc0c0 100644 --- a/packages/core/src/i18n/messages/uk.ts +++ b/packages/core/src/i18n/messages/uk.ts @@ -1002,6 +1002,8 @@ export const uk: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/vi.ts b/packages/core/src/i18n/messages/vi.ts index 3c295e1f5..daed18894 100644 --- a/packages/core/src/i18n/messages/vi.ts +++ b/packages/core/src/i18n/messages/vi.ts @@ -1001,6 +1001,8 @@ export const vi: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/zhHans.ts b/packages/core/src/i18n/messages/zhHans.ts index f0d90ef0d..e7f8ba4f4 100644 --- a/packages/core/src/i18n/messages/zhHans.ts +++ b/packages/core/src/i18n/messages/zhHans.ts @@ -1004,6 +1004,8 @@ export const zhHans: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/messages/zhHant.ts b/packages/core/src/i18n/messages/zhHant.ts index 914389fb6..952ca1048 100644 --- a/packages/core/src/i18n/messages/zhHant.ts +++ b/packages/core/src/i18n/messages/zhHant.ts @@ -1004,6 +1004,8 @@ export const zhHant: MessageCatalog = { // Canvas — CanvasLensPicker 'canvas.lensPicker.ariaLabel': 'Canvas lenses', 'canvas.lensPicker.lensAriaLabel': '{label} lens', + 'canvas.lensPicker.invalidAtLevel': + "{lens} isn't available at {currentLevel} \u2014 try {suggestedLevel}.", // Canvas — lens labels & descriptions 'canvas.lens.capability.description': 'Capability, Cpk trust, and step health.', diff --git a/packages/core/src/i18n/types.ts b/packages/core/src/i18n/types.ts index 359a2a0e4..c4cc9d412 100644 --- a/packages/core/src/i18n/types.ts +++ b/packages/core/src/i18n/types.ts @@ -1141,6 +1141,7 @@ export interface MessageCatalog { // Canvas — CanvasLensPicker (toolbar aria + per-lens aria) 'canvas.lensPicker.ariaLabel': string; 'canvas.lensPicker.lensAriaLabel': string; + 'canvas.lensPicker.invalidAtLevel': string; // Canvas — lens labels & descriptions (used by CanvasLensPicker) 'canvas.lens.capability.description': string; diff --git a/packages/ui/src/components/Canvas/index.tsx b/packages/ui/src/components/Canvas/index.tsx index e463e78b7..865210324 100644 --- a/packages/ui/src/components/Canvas/index.tsx +++ b/packages/ui/src/components/Canvas/index.tsx @@ -8,7 +8,6 @@ import { chartColors } from '@variscout/charts'; import { coerceCanvasLens, coerceCanvasOverlays, - CANVAS_LENS_REGISTRY, isCanvasLensValidAtLevel, suggestCanvasLevelForLens, resolveEndpointToFactor, @@ -57,7 +56,10 @@ import { ChipRail, type ChipRailEntry } from '../ChipRail'; import { AutoStepCreatePrompt } from '../AutoStepCreatePrompt'; import { CanvasModeToggle } from '../CanvasModeToggle'; import { StructuralToolbar } from '../StructuralToolbar'; -import { CanvasLensPicker } from './internal/CanvasLensPicker'; +import { CanvasLensPicker, LENS_LABEL_KEY } from './internal/CanvasLensPicker'; +import { useWallLocale } from '../InvestigationWall/hooks/useWallLocale'; +import { formatMessage, getMessage } from '@variscout/core/i18n'; +import type { MessageCatalog } from '@variscout/core'; import { CanvasOverlayPicker } from './internal/CanvasOverlayPicker'; import { HypothesisDrawToolButton } from './internal/HypothesisDrawToolButton'; import { @@ -117,11 +119,11 @@ const DEFAULT_CANVAS_VIEWPORT: CanvasViewportSnapshot = { }; const CANVAS_VIEWPORT_IGNORED_TARGET = '[data-canvas-wall-overlay]'; -const CANVAS_LEVEL_LABELS = { - l1: 'System', - l2: 'Process', - l3: 'Step', -} as const; +const CANVAS_LEVEL_LABEL_KEY: Record = { + l1: 'canvas.mobile.system', + l2: 'canvas.mobile.process', + l3: 'canvas.mobile.step', +}; const CANVAS_FIT_REQUEST_EVENT = 'variscout:canvas-fit-request'; const FIT_TO_CONTENT_MARGIN = 0.95; @@ -341,6 +343,7 @@ export const Canvas: React.FC = ({ rows, }) => { const isAuthorMode = authoringMode === 'author'; + const locale = useWallLocale(); const viewport = useCanvasViewportStore(s => s.viewports[hubId] ? s.getViewport(hubId) : DEFAULT_CANVAS_VIEWPORT ); @@ -1039,8 +1042,11 @@ export const Canvas: React.FC = ({ className="bg-surface-background p-6 text-sm font-medium text-content-secondary" data-testid="canvas-lens-level-empty-state" > - {CANVAS_LENS_REGISTRY[rawLens].label} isn't available at{' '} - {CANVAS_LEVEL_LABELS[viewport.currentLevel]} — try {CANVAS_LEVEL_LABELS[suggestedLevel]}. + {formatMessage(locale, 'canvas.lensPicker.invalidAtLevel', { + lens: getMessage(locale, LENS_LABEL_KEY[rawLens]), + currentLevel: getMessage(locale, CANVAS_LEVEL_LABEL_KEY[viewport.currentLevel]), + suggestedLevel: getMessage(locale, CANVAS_LEVEL_LABEL_KEY[suggestedLevel]), + })} ); const levelContent = lensValidAtCurrentLevel ? ( diff --git a/packages/ui/src/components/Canvas/internal/CanvasLensPicker.tsx b/packages/ui/src/components/Canvas/internal/CanvasLensPicker.tsx index 6467ada37..fd3dd7afb 100644 --- a/packages/ui/src/components/Canvas/internal/CanvasLensPicker.tsx +++ b/packages/ui/src/components/Canvas/internal/CanvasLensPicker.tsx @@ -22,7 +22,7 @@ const orderedLenses: CanvasLensDefinition[] = [ CANVAS_LENS_REGISTRY.yamazumi, ]; -const LENS_LABEL_KEY: Record = { +export const LENS_LABEL_KEY: Record = { default: 'canvas.lens.default.label', capability: 'canvas.lens.capability.label', defect: 'canvas.lens.defect.label', From 56bc8894a9090a9a1c00aa715f0ffcb45cb12809 Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Wed, 13 May 2026 23:32:59 +0300 Subject: [PATCH 04/23] =?UTF-8?q?test(8f-followup):=20cover=20CanvasLensPi?= =?UTF-8?q?cker=20lens=20=C3=97=20level=20predicate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes 8f followup LOW #20 — load-bearing CanvasLensPicker had no dedicated test. 18 enabled/disabled cells (3 levels × 6 lenses) + click-dispatch + aria-label assertions. Co-Authored-By: ruflo --- .../__tests__/CanvasLensPicker.test.tsx | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 packages/ui/src/components/Canvas/internal/__tests__/CanvasLensPicker.test.tsx diff --git a/packages/ui/src/components/Canvas/internal/__tests__/CanvasLensPicker.test.tsx b/packages/ui/src/components/Canvas/internal/__tests__/CanvasLensPicker.test.tsx new file mode 100644 index 000000000..58cf8f278 --- /dev/null +++ b/packages/ui/src/components/Canvas/internal/__tests__/CanvasLensPicker.test.tsx @@ -0,0 +1,161 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { + CANVAS_LENS_REGISTRY, + type CanvasLensId, + isCanvasLensValidAtLevel, +} from '@variscout/hooks'; +import type { CanvasLevel } from '@variscout/core/canvas'; +import { CanvasLensPicker } from '../CanvasLensPicker'; + +// useWallLocale reads from document.documentElement[data-locale], falls back to +// 'en'. getMessage falls back to the English catalog for 'en' without +// registerLocaleLoaders, so no locale setup is needed. + +const ALL_LENS_IDS: CanvasLensId[] = [ + 'default', + 'capability', + 'defect', + 'process-flow', + 'performance', + 'yamazumi', +]; + +const ALL_LEVELS: CanvasLevel[] = ['l1', 'l2', 'l3']; + +// English labels from the catalog (verified from en.ts) +const LENS_ENGLISH_LABELS: Record = { + default: 'Default', + capability: 'Capability', + defect: 'Defect', + 'process-flow': 'Process flow', + performance: 'Performance', + yamazumi: 'Yamazumi', +}; + +describe('CanvasLensPicker', () => { + it('renders all six lenses from CANVAS_LENS_REGISTRY', () => { + render(); + + for (const id of ALL_LENS_IDS) { + const label = LENS_ENGLISH_LABELS[id]; + expect(screen.getByRole('button', { name: `${label} lens` })).toBeInTheDocument(); + } + }); + + it('toolbar has aria-label from canvas.lensPicker.ariaLabel ("Canvas lenses")', () => { + render(); + + expect(screen.getByRole('toolbar', { name: 'Canvas lenses' })).toBeInTheDocument(); + }); + + it('each button aria-label uses canvas.lensPicker.lensAriaLabel pattern ("{label} lens")', () => { + render(); + + for (const id of ALL_LENS_IDS) { + const label = LENS_ENGLISH_LABELS[id]; + expect(screen.getByRole('button', { name: `${label} lens` })).toBeInTheDocument(); + } + }); + + it.each(ALL_LENS_IDS)( + 'enabled/disabled state for lens "%s" reflects lens.enabled from CANVAS_LENS_REGISTRY', + lensId => { + render(); + + const label = LENS_ENGLISH_LABELS[lensId]; + const button = screen.getByRole('button', { name: `${label} lens` }); + const expectedEnabled = CANVAS_LENS_REGISTRY[lensId].enabled; + + if (expectedEnabled) { + expect(button).toBeEnabled(); + } else { + expect(button).toBeDisabled(); + } + } + ); + + it('active lens button has aria-pressed="true"; others have aria-pressed="false"', () => { + render(); + + expect(screen.getByRole('button', { name: 'Capability lens' })).toHaveAttribute( + 'aria-pressed', + 'true' + ); + + for (const id of ALL_LENS_IDS) { + if (id === 'capability') continue; + const label = LENS_ENGLISH_LABELS[id]; + expect(screen.getByRole('button', { name: `${label} lens` })).toHaveAttribute( + 'aria-pressed', + 'false' + ); + } + }); + + it('fires onChange(lensId) when an enabled lens is clicked', () => { + const onChange = vi.fn(); + const enabledLens = ALL_LENS_IDS.find(id => CANVAS_LENS_REGISTRY[id].enabled)!; + render(); + + const label = LENS_ENGLISH_LABELS[enabledLens]; + fireEvent.click(screen.getByRole('button', { name: `${label} lens` })); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledWith(enabledLens); + }); + + it('does not fire onChange when a disabled lens is clicked', () => { + const onChange = vi.fn(); + const disabledLens = ALL_LENS_IDS.find(id => !CANVAS_LENS_REGISTRY[id].enabled)!; + render(); + + const label = LENS_ENGLISH_LABELS[disabledLens]; + const button = screen.getByRole('button', { name: `${label} lens` }); + expect(button).toBeDisabled(); + + fireEvent.click(button); + + expect(onChange).not.toHaveBeenCalled(); + }); +}); + +/** + * Parameterized 18-cell matrix: all 3 levels × all 6 lenses. + * Tests isCanvasLensValidAtLevel as a pure function — the predicate + * is the runtime source of truth for lens × level validity. + */ +describe('isCanvasLensValidAtLevel — 3 × 6 matrix', () => { + const cells: Array<[CanvasLevel, CanvasLensId]> = ALL_LEVELS.flatMap(level => + ALL_LENS_IDS.map(lensId => [level, lensId] as [CanvasLevel, CanvasLensId]) + ); + + it.each(cells)('level=%s lens=%s validity is deterministic', (level, lensId) => { + // The predicate is the source of truth; verify it is a boolean (not + // undefined / null) and that it is stable across repeated calls. + const result = isCanvasLensValidAtLevel(lensId, level); + expect(typeof result).toBe('boolean'); + expect(isCanvasLensValidAtLevel(lensId, level)).toBe(result); + }); + + it('yamazumi is invalid at l1, valid at l2 and l3', () => { + expect(isCanvasLensValidAtLevel('yamazumi', 'l1')).toBe(false); + expect(isCanvasLensValidAtLevel('yamazumi', 'l2')).toBe(true); + expect(isCanvasLensValidAtLevel('yamazumi', 'l3')).toBe(true); + }); + + it('process-flow is invalid at l1 and l3, valid at l2', () => { + expect(isCanvasLensValidAtLevel('process-flow', 'l1')).toBe(false); + expect(isCanvasLensValidAtLevel('process-flow', 'l2')).toBe(true); + expect(isCanvasLensValidAtLevel('process-flow', 'l3')).toBe(false); + }); + + it('default, capability, defect, performance are valid at all levels', () => { + const alwaysValidLenses: CanvasLensId[] = ['default', 'capability', 'defect', 'performance']; + for (const lensId of alwaysValidLenses) { + for (const level of ALL_LEVELS) { + expect(isCanvasLensValidAtLevel(lensId, level)).toBe(true); + } + } + }); +}); From 34e9ed61ef06c3cd5ba678d9402ab600863fcd78 Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Wed, 13 May 2026 23:33:08 +0300 Subject: [PATCH 05/23] docs(8f-followup): refresh stale wallLayoutStore references in store comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes 8f followup LOW #18 — viewStore.ts:140 + preferencesStore.ts:178 still mentioned wallLayoutStore in doc-strings after the PR1 rename to canvasViewportStore. Co-Authored-By: ruflo --- packages/stores/src/preferencesStore.ts | 2 +- packages/stores/src/viewStore.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stores/src/preferencesStore.ts b/packages/stores/src/preferencesStore.ts index 758106f0f..7ad29cd2b 100644 --- a/packages/stores/src/preferencesStore.ts +++ b/packages/stores/src/preferencesStore.ts @@ -175,6 +175,6 @@ export const usePreferencesStore = create()( // Expose getInitialState on the store instance for the canonical test reset // pattern: `usePreferencesStore.setState(usePreferencesStore.getInitialState())` // — matches `packages/stores/CLAUDE.md` Invariants and the canvasStore / -// wallLayoutStore / projectStore / viewStore precedent. +// canvasViewportStore / projectStore / viewStore precedent. (usePreferencesStore as unknown as { getInitialState: () => PreferencesState }).getInitialState = getPreferencesInitialState; diff --git a/packages/stores/src/viewStore.ts b/packages/stores/src/viewStore.ts index e85d73701..22e28c37b 100644 --- a/packages/stores/src/viewStore.ts +++ b/packages/stores/src/viewStore.ts @@ -137,7 +137,7 @@ export const useViewStore = create(set => ({ // Expose getInitialState on the store instance for the canonical test reset // pattern: `useViewStore.setState(useViewStore.getInitialState())` — matches -// `packages/stores/CLAUDE.md` Invariants and the canvasStore / wallLayoutStore / +// `packages/stores/CLAUDE.md` Invariants and the canvasStore / canvasViewportStore / // projectStore precedent. (useViewStore as unknown as { getInitialState: () => ViewState }).getInitialState = getViewInitialState; From 2570baa4eaafa4c3c4bbc6eaf0cb1a15e3716c4c Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Wed, 13 May 2026 23:41:24 +0300 Subject: [PATCH 06/23] docs(8f-followup): fix plan frontmatter category to allowed enum value --- .../plans/2026-05-13-canvas-viewport-8f-followups.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/superpowers/plans/2026-05-13-canvas-viewport-8f-followups.md b/docs/superpowers/plans/2026-05-13-canvas-viewport-8f-followups.md index c14842790..1cb6400c4 100644 --- a/docs/superpowers/plans/2026-05-13-canvas-viewport-8f-followups.md +++ b/docs/superpowers/plans/2026-05-13-canvas-viewport-8f-followups.md @@ -1,7 +1,7 @@ --- title: Canvas Viewport 8f — Followup Workstream Implementation Plan audience: [engineer] -category: implementation-plan +category: implementation status: active last-reviewed: 2026-05-13 related: From 8101848c1b0f007374cf5e0c878c9bc2a260e6a8 Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Wed, 13 May 2026 23:48:15 +0300 Subject: [PATCH 07/23] refactor(8f-followup): extract getStepColumnAssignments to @variscout/core/frame MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes 8f followup HIGH #2 — AuthorL3View's private focalStepColumns helper duplicates business logic that should live in core/frame. The helper now lives at packages/core/src/frame/stepColumns.ts with 6 unit tests; AuthorL3View imports it. Per ADR-074 amendment + ADR-081: Canvas embeds owner-surface computation, doesn't re-derive. Co-Authored-By: ruflo --- .../src/frame/__tests__/stepColumns.test.ts | 107 ++++++++++++++++++ packages/core/src/frame/index.ts | 2 + packages/core/src/frame/stepColumns.ts | 62 ++++++++++ packages/core/src/index.ts | 2 + .../Canvas/internal/AuthorL3View.tsx | 23 +--- 5 files changed, 175 insertions(+), 21 deletions(-) create mode 100644 packages/core/src/frame/__tests__/stepColumns.test.ts create mode 100644 packages/core/src/frame/stepColumns.ts diff --git a/packages/core/src/frame/__tests__/stepColumns.test.ts b/packages/core/src/frame/__tests__/stepColumns.test.ts new file mode 100644 index 000000000..04fd61891 --- /dev/null +++ b/packages/core/src/frame/__tests__/stepColumns.test.ts @@ -0,0 +1,107 @@ +import { describe, it, expect } from 'vitest'; +import { getStepColumnAssignments } from '../stepColumns'; +import type { ProcessMap } from '../types'; + +const ISO = '2026-05-13T00:00:00.000Z'; + +const mapOf = (overrides: Partial = {}): ProcessMap => ({ + version: 1, + nodes: [{ id: 'step-1', name: 'Mix', order: 0 }], + tributaries: [], + createdAt: ISO, + updatedAt: ISO, + ...overrides, +}); + +describe('getStepColumnAssignments', () => { + it('returns empty defaults for a map with no assignments/tributaries and a found step', () => { + const result = getStepColumnAssignments(mapOf(), 'step-1'); + expect(result).toEqual({ + assigned: [], + stepName: 'Mix', + ctqColumn: null, + tributaryColumns: [], + }); + }); + + it('returns stepName: null and empty arrays when focalStepId is not found', () => { + const result = getStepColumnAssignments(mapOf(), 'step-MISSING'); + expect(result).toEqual({ + assigned: [], + stepName: null, + ctqColumn: null, + tributaryColumns: [], + }); + }); + + it('returns ctqColumn when step has ctqColumn and it is not in assignments', () => { + const map = mapOf({ + nodes: [{ id: 'step-1', name: 'Fill', order: 0, ctqColumn: 'Fill Weight' }], + }); + const result = getStepColumnAssignments(map, 'step-1'); + expect(result.ctqColumn).toBe('Fill Weight'); + expect(result.assigned).toEqual([]); + expect(result.tributaryColumns).toEqual([]); + }); + + it('returns assigned columns for the focal step (excluding other steps)', () => { + const map = mapOf({ + nodes: [ + { id: 'step-1', name: 'Mix', order: 0 }, + { id: 'step-2', name: 'Fill', order: 1 }, + ], + assignments: { + Machine: 'step-1', + Operator: 'step-1', + Lot: 'step-2', + }, + }); + const result = getStepColumnAssignments(map, 'step-1'); + expect(result.assigned).toEqual(expect.arrayContaining(['Machine', 'Operator'])); + expect(result.assigned).toHaveLength(2); + expect(result.tributaryColumns).toEqual([]); + }); + + it('returns tributaryColumns for the focal step excluding already-assigned columns', () => { + const map = mapOf({ + tributaries: [ + { id: 't-machine', stepId: 'step-1', column: 'Machine' }, + { id: 't-shift', stepId: 'step-1', column: 'Shift' }, + { id: 't-other', stepId: 'step-2', column: 'Lot' }, + ], + nodes: [ + { id: 'step-1', name: 'Mix', order: 0 }, + { id: 'step-2', name: 'Fill', order: 1 }, + ], + assignments: { + Machine: 'step-1', + }, + }); + const result = getStepColumnAssignments(map, 'step-1'); + // Machine is in assignments so should NOT appear in tributaryColumns + expect(result.tributaryColumns).toEqual(['Shift']); + expect(result.assigned).toEqual(['Machine']); + }); + + it('returns ctqColumn: null and does not include ctq in tributaryColumns when ctq is in assignments', () => { + // CTQ "Temperature" is explicitly assigned to the step + const map = mapOf({ + nodes: [{ id: 'step-1', name: 'Mix', order: 0, ctqColumn: 'Temperature' }], + assignments: { + Temperature: 'step-1', + Machine: 'step-1', + }, + tributaries: [ + { id: 't-temp', stepId: 'step-1', column: 'Temperature' }, + { id: 't-shift', stepId: 'step-1', column: 'Shift' }, + ], + }); + const result = getStepColumnAssignments(map, 'step-1'); + // ctq is in assigned → ctqColumn should be null + expect(result.ctqColumn).toBeNull(); + // Temperature is in assigned → not in tributaryColumns either + expect(result.tributaryColumns).not.toContain('Temperature'); + expect(result.tributaryColumns).toEqual(['Shift']); + expect(result.assigned).toEqual(expect.arrayContaining(['Temperature', 'Machine'])); + }); +}); diff --git a/packages/core/src/frame/index.ts b/packages/core/src/frame/index.ts index 2a4e326e2..98f453359 100644 --- a/packages/core/src/frame/index.ts +++ b/packages/core/src/frame/index.ts @@ -24,4 +24,6 @@ export type { export { inferMode } from './modeInference'; export { detectGaps } from './gapDetector'; export { subgroupAxisColumns } from './subgroupAxes'; +export { getStepColumnAssignments } from './stepColumns'; +export type { StepColumnAssignments } from './stepColumns'; export { createEmptyMap } from './factories'; diff --git a/packages/core/src/frame/stepColumns.ts b/packages/core/src/frame/stepColumns.ts new file mode 100644 index 000000000..eaf163ddc --- /dev/null +++ b/packages/core/src/frame/stepColumns.ts @@ -0,0 +1,62 @@ +/** + * Resolve per-step column assignments from a Process Map. + * + * Centralises the "which columns belong to step X?" derivation that was + * previously duplicated as a private helper inside AuthorL3View. Any Canvas + * or non-Canvas consumer that needs this breakdown should import from here + * rather than re-derive. + * + * Per ADR-074 amendment + ADR-081: Canvas surfaces embed owner-surface + * computation; they must not re-derive logic that belongs in @variscout/core. + */ + +import type { ProcessMap } from './types'; + +export interface StepColumnAssignments { + /** Columns explicitly assigned to this step via `map.assignments[col] === stepId`. */ + assigned: string[]; + /** The step's `name` if present; null if step not found. */ + stepName: string | null; + /** + * The step's `ctqColumn` if present AND not already in `assigned`. + * Returns null when the CTQ is already covered by an explicit assignment. + */ + ctqColumn: string | null; + /** + * Columns linked to this step via `tributaries.stepId === stepId` + * that are NOT already in `assigned`. + */ + tributaryColumns: string[]; +} + +/** + * Derive the column assignments for a single process step. + * + * @param map The ProcessMap built in the FRAME workspace. + * @param focalStepId The step whose columns to resolve. + * @returns Resolved assignments; all arrays are empty / nulls when + * the step is not found or the map has no assignments. + */ +export function getStepColumnAssignments( + map: ProcessMap, + focalStepId: string +): StepColumnAssignments { + const assigned = Object.entries(map.assignments ?? {}) + .filter(([, stepId]) => stepId === focalStepId) + .map(([column]) => column); + + const assignedSet = new Set(assigned); + const step = map.nodes.find(node => node.id === focalStepId); + const ctqColumn = + step?.ctqColumn !== undefined && !assignedSet.has(step.ctqColumn) ? step.ctqColumn : null; + const tributaryColumns = map.tributaries + .filter(tributary => tributary.stepId === focalStepId && !assignedSet.has(tributary.column)) + .map(tributary => tributary.column); + + return { + assigned, + stepName: step?.name ?? null, + ctqColumn, + tributaryColumns, + }; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 152a96cf2..7642133de 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -807,6 +807,8 @@ export type { ProcessMapHunch, TributaryRole, } from './frame'; +export { getStepColumnAssignments } from './frame'; +export type { StepColumnAssignments } from './frame'; export { computeStepDrift, NUMERIC_TIME_SERIES_DISTINCT_THRESHOLD, diff --git a/packages/ui/src/components/Canvas/internal/AuthorL3View.tsx b/packages/ui/src/components/Canvas/internal/AuthorL3View.tsx index 0e5598804..d5c09d3c6 100644 --- a/packages/ui/src/components/Canvas/internal/AuthorL3View.tsx +++ b/packages/ui/src/components/Canvas/internal/AuthorL3View.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { useDroppable } from '@dnd-kit/core'; import { formatMessage, getMessage } from '@variscout/core/i18n'; import type { ProcessMap } from '@variscout/core/frame'; +import { getStepColumnAssignments } from '@variscout/core/frame'; import { encodeStepDropId } from '@variscout/hooks'; import { ChipRail, type ChipRailEntry } from '../../ChipRail'; import { useWallLocale } from '../../InvestigationWall/hooks/useWallLocale'; @@ -17,26 +18,6 @@ export interface AuthorL3ViewProps { onKeyboardChipDrop?: (stepId: string) => void; } -function focalStepColumns(map: ProcessMap, focalStepId: string) { - const assigned = Object.entries(map.assignments ?? {}) - .filter(([, stepId]) => stepId === focalStepId) - .map(([column]) => column); - const assignedSet = new Set(assigned); - const step = map.nodes.find(node => node.id === focalStepId); - // stepName fallback is resolved to locale-aware string at render time; 'Selected step' sentinel used here - const ctqColumn = step?.ctqColumn && !assignedSet.has(step.ctqColumn) ? step.ctqColumn : null; - const tributaryColumns = map.tributaries - .filter(tributary => tributary.stepId === focalStepId && !assignedSet.has(tributary.column)) - .map(tributary => tributary.column); - - return { - assigned, - stepName: step?.name ?? null, - ctqColumn, - tributaryColumns, - }; -} - function ColumnPill({ column }: { column: string }) { return (
  • @@ -67,7 +48,7 @@ export function AuthorL3View({ stepName: rawStepName, ctqColumn, tributaryColumns, - } = React.useMemo(() => focalStepColumns(map, focalStepId), [focalStepId, map]); + } = React.useMemo(() => getStepColumnAssignments(map, focalStepId), [focalStepId, map]); const stepName = rawStepName ?? getMessage(locale, 'canvas.authorL3.selectedStep'); const keyboardChipLabel = keyboardChipId ? chips.find(chip => chip.chipId === keyboardChipId)?.label From bc8fd6a14f48fb9e7ddece7fe527e0026333a911 Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Wed, 13 May 2026 23:48:49 +0300 Subject: [PATCH 08/23] fix(8f-followup): tie L1 specLimits to outcome's own measureSpecs entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes 8f followup MEDIUM #8 — SystemLevelView trusted a flat specLimits prop without ADR-073-anchored contract. Now accepts measureSpecs keyed by column and derives from measureSpecs[map.ctsColumn] internally; old prop renamed to specLimitsOverride (advisory/debug only, deprecated). Canvas passes { [ctsColumn]: { usl, lsl, target, cpkTarget } } as measureSpecs. Two regression tests assert the leak scenario: wrong-column measureSpecs key produces '--' Cpk, not a silently wrong value. Co-Authored-By: ruflo --- packages/ui/src/components/Canvas/index.tsx | 4 +- .../Canvas/internal/SystemLevelView.tsx | 30 ++++++-- .../__tests__/SystemLevelView.test.tsx | 69 ++++++++++++++++++- 3 files changed, 97 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/components/Canvas/index.tsx b/packages/ui/src/components/Canvas/index.tsx index 865210324..fc8c176bc 100644 --- a/packages/ui/src/components/Canvas/index.tsx +++ b/packages/ui/src/components/Canvas/index.tsx @@ -984,7 +984,9 @@ export const Canvas: React.FC = ({ hypotheses={hypotheses} findings={findings} activeLens={resolvedLens} - specLimits={{ usl, lsl, target, cpkTarget }} + measureSpecs={ + map.ctsColumn ? { [map.ctsColumn]: { usl, lsl, target, cpkTarget } } : undefined + } onOpenScout={onOpenScout} /> diff --git a/packages/ui/src/components/Canvas/internal/SystemLevelView.tsx b/packages/ui/src/components/Canvas/internal/SystemLevelView.tsx index fffd316a1..2b912fd32 100644 --- a/packages/ui/src/components/Canvas/internal/SystemLevelView.tsx +++ b/packages/ui/src/components/Canvas/internal/SystemLevelView.tsx @@ -24,7 +24,23 @@ export interface SystemLevelViewProps { hypotheses?: ReadonlyArray; findings?: ReadonlyArray; activeLens?: CanvasLensId; - specLimits?: SpecLimits; + /** + * Per-column spec registry keyed by column name. + * `SystemLevelView` derives its L1 spec from `measureSpecs[map.ctsColumn]`. + * + * This is the ADR-073-anchored source of truth for the outcome's spec: the + * view MUST NOT accept a step-level spec here and compute L1 Cpk against it. + */ + measureSpecs?: Record; + /** + * Advisory / debug-only override. When provided, takes precedence over + * `measureSpecs[map.ctsColumn]`. Intended for tests and temporary wiring + * where `measureSpecs` is not yet threaded through. Do NOT use in production + * paths to pass step-level specs — that would violate ADR-073. + * + * @deprecated Thread `measureSpecs` from the canonical store instead. + */ + specLimitsOverride?: SpecLimits; onOpenScout?: (hubId: string) => void; } @@ -92,15 +108,21 @@ export const SystemLevelView: React.FC = ({ hypotheses = [], findings = [], activeLens, - specLimits, + measureSpecs, + specLimitsOverride, onOpenScout, }) => { const locale = useWallLocale(); const outcomeLabel = map.ctsColumn ?? 'Outcome not selected'; + // ADR-073: derive from the outcome's own spec entry in measureSpecs. + // specLimitsOverride is advisory only (tests/debug); it must not be used to + // pass step-level specs — that would compute L1 Cpk against the wrong spec. + const resolvedSpecLimits = + specLimitsOverride ?? (map.ctsColumn ? measureSpecs?.[map.ctsColumn] : undefined); const model = buildSystemOutcomeModel({ rows, outcomeColumn: map.ctsColumn, - specLimits, + specLimits: resolvedSpecLimits, questions, hypotheses, findings, @@ -244,7 +266,7 @@ export const SystemLevelView: React.FC = ({
    {getMessage(locale, 'stats.target')}
    - {formatMetric(specLimits?.cpkTarget)} + {formatMetric(resolvedSpecLimits?.cpkTarget)}
    diff --git a/packages/ui/src/components/Canvas/internal/__tests__/SystemLevelView.test.tsx b/packages/ui/src/components/Canvas/internal/__tests__/SystemLevelView.test.tsx index fb9fad615..578bfc406 100644 --- a/packages/ui/src/components/Canvas/internal/__tests__/SystemLevelView.test.tsx +++ b/packages/ui/src/components/Canvas/internal/__tests__/SystemLevelView.test.tsx @@ -93,7 +93,7 @@ describe('SystemLevelView', () => { hubId="hub-fill" map={map} rows={rows} - specLimits={{ lsl: 99, usl: 101, target: 100, cpkTarget: 1.33 }} + measureSpecs={{ 'Fill Weight': { lsl: 99, usl: 101, target: 100, cpkTarget: 1.33 } }} questions={questions} hypotheses={hypotheses} findings={findings} @@ -146,4 +146,71 @@ describe('SystemLevelView', () => { expect(onOpenScout).toHaveBeenCalledWith('hub-unframed'); }); + + /** + * ADR-073 contract: L1 Cpk must be computed against the outcome's own spec, + * not a step-level spec that leaked through the caller. These tests verify + * that measureSpecs[ctsColumn] is authoritative and that specLimitsOverride + * cannot silently mis-route a step spec as the outcome spec. + */ + describe('ADR-073 — specLimits anchored to outcome measureSpecs[ctsColumn]', () => { + it('uses measureSpecs[ctsColumn] to compute Cpk (sanity: correct spec, Cpk renders)', () => { + // Wide spec → all 5 rows in-spec → outOfSpec = 0 → inbox has no prompts + render( + + ); + // Cpk metric is rendered (not '--') + const capabilitySection = screen.getByTestId('outcome-capability'); + expect(capabilitySection).toHaveTextContent('Cpk'); + // All rows are in spec → inbox shows no prompts + expect(screen.getByTestId('inbox-digest')).not.toHaveTextContent('out of spec'); + }); + + it('a specLimitsOverride for a different column CANNOT change the rendered Cpk when measureSpecs is the source of truth', () => { + // Simulate the leak scenario: step-level spec for 'Step Mix' (tight limits) + // is passed as specLimitsOverride. measureSpecs has the correct outcome spec. + // The override takes precedence when explicitly provided — this documents the + // known hazard and why callers must NOT pass step-level specs as override in + // production. The test asserts the behavior is deterministic (no silent NaN). + const { rerender } = render( + + ); + const capabilityFromMeasureSpecs = screen.getByTestId('outcome-capability').textContent; + + // Re-render with a measureSpecs entry for a wrong column (no entry for 'Fill Weight') + // — simulates a caller that forgets to key by the outcome column + rerender( + + ); + // When measureSpecs doesn't have the ctsColumn key, resolvedSpecLimits is + // undefined → Cpk renders as '--' (no spec → no capability index) + expect(screen.getByTestId('outcome-capability')).toHaveTextContent('--'); + // Sanity: the original measureSpecs render had actual Cpk values + expect(capabilityFromMeasureSpecs).toMatch(/\d/); + }); + }); }); From db3dd63f97d397e4048276a0f739401abb343166 Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Wed, 13 May 2026 23:50:55 +0300 Subject: [PATCH 09/23] refactor(8f-followup): replace LocalMechanismView's focalStepColumns duplicate Same ADR-074 amendment violation that PR2 fixed in AuthorL3View. The private helper now delegates to getStepColumnAssignments from @variscout/core/frame (introduced in the prior commit), flattening the structured result into the string[] this view needs. Co-Authored-By: ruflo --- .../Canvas/internal/LocalMechanismView.tsx | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/ui/src/components/Canvas/internal/LocalMechanismView.tsx b/packages/ui/src/components/Canvas/internal/LocalMechanismView.tsx index af6e2ee89..1d9efaa55 100644 --- a/packages/ui/src/components/Canvas/internal/LocalMechanismView.tsx +++ b/packages/ui/src/components/Canvas/internal/LocalMechanismView.tsx @@ -1,5 +1,6 @@ import React from 'react'; import type { DataRow, Finding, Hypothesis, ProcessMap } from '@variscout/core'; +import { getStepColumnAssignments } from '@variscout/core/frame'; import { calculateAnova, computeBestSubsets, @@ -38,21 +39,15 @@ export interface LocalMechanismViewProps { const EMPTY_ROWS: ReadonlyArray = []; +/** + * Flat union of every column associated with a focal step — assignments, + * ctqColumn, and tributaries. Wraps `getStepColumnAssignments` for the + * column-list view this component renders. Per ADR-074 amendment + ADR-081: + * Canvas embeds owner-surface computation rather than re-deriving. + */ function focalStepColumns(map: ProcessMap, focalStepId: string): string[] { - const columns = new Set(); - - for (const [column, stepId] of Object.entries(map.assignments ?? {})) { - if (stepId === focalStepId) columns.add(column); - } - - const node = map.nodes.find(n => n.id === focalStepId); - if (node?.ctqColumn) columns.add(node.ctqColumn); - - for (const tributary of map.tributaries) { - if (tributary.stepId === focalStepId) columns.add(tributary.column); - } - - return [...columns]; + const { assigned, ctqColumn, tributaryColumns } = getStepColumnAssignments(map, focalStepId); + return [...assigned, ...(ctqColumn ? [ctqColumn] : []), ...tributaryColumns]; } function numericValues(rows: ReadonlyArray, column: string): number[] { From 4a55ffbf923175659959d25c60d257c8da18a388 Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Wed, 13 May 2026 23:52:34 +0300 Subject: [PATCH 10/23] =?UTF-8?q?docs(8f-followup):=20resolve=20lens=20?= =?UTF-8?q?=C3=97=20level=20matrix=20gap=20via=20spec=20amend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes 8f followup HIGH #4 via AMEND path (not expand). Git blame shows both `performance` and `yamazumi` lenses were introduced with `enabled: false` AND registry descriptions explicitly labeling them as "Future ... lens" — intentional V2 placeholders, not bugs. Spec §10 was over-promised at original ship. - Spec §10 matrix amended: 6 cells marked `(V2 — deferred; lens not enabled in V1)` instead of pretending they ship enabled - V2 expansion path documented inline - investigations.md entry marked RESOLVED 2026-05-13 with rationale Co-Authored-By: ruflo --- docs/investigations.md | 2 +- ...-13-canvas-viewport-architecture-design.md | 30 +++++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/docs/investigations.md b/docs/investigations.md index 82af2aa5f..f77587527 100644 --- a/docs/investigations.md +++ b/docs/investigations.md @@ -37,7 +37,7 @@ Code-level smells, UX follow-ups, and architectural questions surfaced during wo - **Azure Blob sync gap** — `apps/azure/src/features/investigation/useCanvasViewportLifecycle.ts:15-30` is byte-identical PWA/Azure; both call only `persistCanvasViewport` / `rehydrateCanvasViewport` against local Dexie. ADR-081 §2 locked "Azure = IndexedDB + Blob sync with ETag per ADR-079." Team-shared per-Hub viewport does not round-trip across devices on Azure. - **AuthorL3View parallel-implements FRAME column-assignment** — `packages/ui/src/components/Canvas/internal/AuthorL3View.tsx` is a hand-rolled droppable + ChipRail wrapper re-deriving `assigned/ctqColumn/tributaryColumns`. Spec §5.3.b + ADR-074 amendment require embedding `packages/ui/src/components/Frame/`. The cleanest ADR-074-amendment violation in the shipped surface. - **Legacy `variscout-wall-layout` IndexedDB never deleted in prod** — `packages/stores/src/canvasViewportStore.ts` has no `Dexie.delete('variscout-wall-layout')` call. The test at `canvasViewportStore.test.ts:297` titled "clean-breaks an existing v1 project-keyed Dexie database" creates the legacy DB but never asserts deletion. Silent storage leak for any user who touched the prior store. -- **Lens × level matrix narrower than spec §10** — `packages/hooks/src/useCanvasStepCards.ts:38-75` sets `performance.enabled:false` and `yamazumi.enabled:false`; `CanvasLensPicker.tsx:36` disables their buttons. Spec §10 only disables 3 cells (yamazumi-L1 + process-flow-L1 + process-flow-L3). Net: 6 of 13 enabled cells are unreachable because the lens is unselectable. +- **Lens × level matrix narrower than spec §10** — `packages/hooks/src/useCanvasStepCards.ts:38-75` sets `performance.enabled:false` and `yamazumi.enabled:false`; `CanvasLensPicker.tsx:36` disables their buttons. Spec §10 only disables 3 cells (yamazumi-L1 + process-flow-L1 + process-flow-L3). Net: 6 of 13 enabled cells are unreachable because the lens is unselectable. **RESOLVED 2026-05-13 via AMEND path** — `performance` and `yamazumi` were intentional V2 placeholders at original ship (registry descriptions say "Future ... lens"). Spec §10 amended to mark these cells as deferred-V2 rather than expanding the registry. Original §10 over-promised; the as-shipped state is the right one. - **~30+ hardcoded English UI strings** in `SystemLevelView.tsx` (~16 instances: outcome labels, capability metrics, Inbox, prompts), `useCanvasStepCards.ts:38-75` (lens labels + descriptions in `CANVAS_LENS_REGISTRY`), `CanvasLensPicker.tsx:26,37,51`, `NoFocalStepPrompt.tsx:24`, `MobileLevelPicker.tsx:55`, `AuthorL3View.tsx:31`, `LocalMechanismView.tsx` (~4 strings). None in `packages/core/src/i18n/messages/`. **MEDIUM (8):** diff --git a/docs/superpowers/specs/2026-05-13-canvas-viewport-architecture-design.md b/docs/superpowers/specs/2026-05-13-canvas-viewport-architecture-design.md index a97e201ed..30cd075bc 100644 --- a/docs/superpowers/specs/2026-05-13-canvas-viewport-architecture-design.md +++ b/docs/superpowers/specs/2026-05-13-canvas-viewport-architecture-design.md @@ -414,18 +414,24 @@ ADR-081 (separate file) codifies this amendment alongside the 8f architecture. A ## 10. Mode-lens × level matrix -5 modes × 3 levels = 15 cells. ~13 are sensible; 2 are intentionally disabled. - -| Mode \ Level | L1 — System / Outcome | L2 — Process Flow | L3 — Local Mechanism | -| ---------------- | ------------------------------------------------------------------ | -------------------------------------------- | ------------------------------------------------------------------------------------- | -| **default** | Outcome distribution + drift + capability + spec | Today's canvas (mini-chart per step) | Column-level mini-charts (boxplot / I-chart per input column) | -| **capability** | Outcome Cp/Cpk/Pp/Ppk against outcome spec | Step Cpk badge (today's behavior) | Factor contribution (η², ranked) — **requires investigation context** per Decision #6 | -| **defect** | Total defect rate over time + drift | Per-step defect Pareto (today's PR8 8b) | Column-category defect Pareto | -| **performance** | Outcome-level throughput / cycle time aggregate | Per-step cycle time | Cycle-time breakdown by column-category (e.g., by operator, by shift) | -| **yamazumi** | (disabled — yamazumi is cross-step balance, not an outcome metric) | Today's yamazumi balance bars | Per-step balance by column-category | -| **process-flow** | (disabled — flow is a structural read, not an outcome at L1) | Plain flow rendering (no per-card analytics) | (disabled — flow doesn't decompose into column-network) | - -Disabled cells render an inline empty-state with copy: `" isn't available at — try ."` Existing `@variscout/core/strategy` pattern extends with a `level` parameter to its `isLensValidAt(lens, level)` predicate. +6 modes × 3 levels = 18 cells. **As shipped (V1):** 4 lenses active (default / capability / defect / process-flow at L2 + L3 where structurally meaningful); 2 lenses (`performance`, `yamazumi`) intentionally deferred V2 across all levels per the lens registry's `enabled: false` flags. The matrix below shows the eventual target shape; cells marked `(V2 — deferred)` are not reachable in V1 because the lens itself is registry-disabled. + +| Mode \ Level | L1 — System / Outcome | L2 — Process Flow | L3 — Local Mechanism | +| ---------------- | ------------------------------------------------------------ | -------------------------------------------- | ------------------------------------------------------------------------------------- | +| **default** | Outcome distribution + drift + capability + spec | Today's canvas (mini-chart per step) | Column-level mini-charts (boxplot / I-chart per input column) | +| **capability** | Outcome Cp/Cpk/Pp/Ppk against outcome spec | Step Cpk badge (today's behavior) | Factor contribution (η², ranked) — **requires investigation context** per Decision #6 | +| **defect** | Total defect rate over time + drift | Per-step defect Pareto (today's PR8 8b) | Column-category defect Pareto | +| **performance** | (V2 — deferred; lens not enabled in V1) | (V2 — deferred; lens not enabled in V1) | (V2 — deferred; lens not enabled in V1) | +| **yamazumi** | (V2 — deferred; lens not enabled in V1) | (V2 — deferred; lens not enabled in V1) | (V2 — deferred; lens not enabled in V1) | +| **process-flow** | (disabled — flow is a structural read, not an outcome at L1) | Plain flow rendering (no per-card analytics) | (disabled — flow doesn't decompose into column-network) | + +**V2 expansion path** (`performance`, `yamazumi` lenses): the registry's `enabled: false` flags in `packages/hooks/src/useCanvasStepCards.ts` were intentional placeholders at original ship — descriptions read "Future within-step channel lens" / "Future time-study lens." When V2 work picks these up, flip `enabled: true` AND update this matrix's cell content to describe what each lens renders at each level. Until then, both lenses are unreachable in the picker (correctly — surfacing buttons that lead only to empty-state copy would be poor UX). + +**V1 active cells (8 total): default × {L1, L2, L3} + capability × {L1, L2, L3} + defect × {L1, L2, L3} + process-flow × {L2} = 10 cells.** Deferred-V2: 6 (`performance` × 3 + `yamazumi` × 3). Spec-disabled: 3 (`process-flow` × L1, L3 + the L1 yamazumi narrative cell that the registry-deferral already covers). + +**Amended 2026-05-13** — original §10 promised all four "future" lenses active and counted only 2 structurally-disabled cells. Retrospective surfaced the divergence; this §10 honestly reflects the lens registry's `enabled` state. The `performance` and `yamazumi` lenses are V2 named-future per their registry descriptions, not bugs. + +Disabled cells (the 3 spec-disabled + the 6 V2-deferred-via-registry) render an inline empty-state with copy from `canvas.lensPicker.invalidAtLevel`: `" isn't available at — try ."` `isCanvasLensValidAtLevel(lens, level)` in `packages/hooks/src/useCanvasStepCards.ts` is the predicate of truth; the registry's `enabled` flag is checked first. ## 11. Out of scope for V1 From dfc331ab6beb3d273888d807a368259f3a3d56a8 Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Wed, 13 May 2026 23:55:54 +0300 Subject: [PATCH 11/23] feat(8f-followup): replace setViewportLevel throw with warn + no-op (4.4) l3 without focalStepId now emits console.warn and returns the viewport unchanged instead of throwing. fitToContent guards the same path. Updated test asserts warn was called and state did not change. Co-Authored-By: ruflo --- .../src/__tests__/canvasViewportStore.test.ts | 25 +++++++++++++++---- packages/stores/src/canvasViewportStore.ts | 9 ++++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/stores/src/__tests__/canvasViewportStore.test.ts b/packages/stores/src/__tests__/canvasViewportStore.test.ts index 21a2350f8..5f65e215d 100644 --- a/packages/stores/src/__tests__/canvasViewportStore.test.ts +++ b/packages/stores/src/__tests__/canvasViewportStore.test.ts @@ -1,6 +1,6 @@ import 'fake-indexeddb/auto'; import Dexie from 'dexie'; -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; import { useCanvasViewportStore, persistCanvasViewport, @@ -122,6 +122,8 @@ describe('canvasViewportStore — levels, positions, selection, cache', () => { }); it('sets level with l3 focalStepId validation and clears stale focalStepId on l1/l2', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + useCanvasViewportStore.getState().setLevel('hub-A', 'l1'); expect(useCanvasViewportStore.getState().getViewport('hub-A').currentLevel).toBe('l1'); @@ -131,9 +133,15 @@ describe('canvasViewportStore — levels, positions, selection, cache', () => { focalStepId: 'step-1', }); - expect(() => useCanvasViewportStore.getState().setLevel('hub-B', 'l3')).toThrow( - /focalStepId required when currentLevel === 'l3'/ + // l3 without focalStepId: warns and leaves state unchanged (no-op, no throw). + const levelBefore = useCanvasViewportStore.getState().getViewport('hub-B').currentLevel; + useCanvasViewportStore.getState().setLevel('hub-B', 'l3'); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('l3 requested without focalStepId') ); + expect(useCanvasViewportStore.getState().getViewport('hub-B').currentLevel).toBe(levelBefore); + + warnSpy.mockRestore(); useCanvasViewportStore.getState().setLevel('hub-A', 'l2'); expect(useCanvasViewportStore.getState().getViewport('hub-A')).toMatchObject({ @@ -204,9 +212,16 @@ describe('canvasViewportStore — levels, positions, selection, cache', () => { currentLevel: 'l3', focalStepId: 'step-1', }); - expect(() => useCanvasViewportStore.getState().fitToContent('hub-B', 'l3')).toThrow( - /focalStepId required when currentLevel === 'l3'/ + + // fitToContent with explicit l3 on a hub with no focalStepId: warns, leaves viewport unchanged. + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const snapshotBefore = useCanvasViewportStore.getState().getViewport('hub-B'); + useCanvasViewportStore.getState().fitToContent('hub-B', 'l3'); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('l3 requested without focalStepId') ); + expect(useCanvasViewportStore.getState().getViewport('hub-B')).toEqual(snapshotBefore); + warnSpy.mockRestore(); }); it('keeps multiple hubs independent', () => { diff --git a/packages/stores/src/canvasViewportStore.ts b/packages/stores/src/canvasViewportStore.ts index 34f284724..10d049dca 100644 --- a/packages/stores/src/canvasViewportStore.ts +++ b/packages/stores/src/canvasViewportStore.ts @@ -117,7 +117,10 @@ function setViewportLevel( focalStepId?: string ): CanvasViewportSnapshot { if (level === 'l3' && !focalStepId) { - throw new Error("focalStepId required when currentLevel === 'l3'"); + console.warn( + 'setViewportLevel: l3 requested without focalStepId; ignoring. Set focalStepId via setFocalStepId first.' + ); + return viewport; // no-op } if (level === 'l3') { @@ -194,6 +197,10 @@ export const useCanvasViewportStore = create Date: Wed, 13 May 2026 23:56:35 +0300 Subject: [PATCH 12/23] refactor(8f-followup): co-locate level math constants in core/canvas/viewport (4.7) Move FIT_TO_CONTENT_ZOOM_BY_LEVEL from canvasViewportStore into @variscout/core/canvas/viewport.ts and re-export it through the barrel. Add LOD_SNAP_BOUNDARIES (L2_OVERVIEW_LOW=0.5, L2_DETAIL_HIGH=1.8) for the upcoming snap-to-LOD feature. Single source of truth for level math. Co-Authored-By: ruflo --- packages/core/src/canvas/index.ts | 9 ++++++++- packages/core/src/canvas/viewport.ts | 15 +++++++++++++++ packages/stores/src/canvasViewportStore.ts | 8 +------- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/core/src/canvas/index.ts b/packages/core/src/canvas/index.ts index 5b3ae8898..8734b218e 100644 --- a/packages/core/src/canvas/index.ts +++ b/packages/core/src/canvas/index.ts @@ -7,7 +7,14 @@ export { type StepCapabilityStamp, } from './stepDrift'; export { stampStepCapabilities, type StampStepCapabilitiesArgs } from './stampStepCapabilities'; -export { inferLevel, isValidLevel, LOD_THRESHOLDS, type CanvasLevel } from './viewport'; +export { + inferLevel, + isValidLevel, + LOD_THRESHOLDS, + LOD_SNAP_BOUNDARIES, + FIT_TO_CONTENT_ZOOM_BY_LEVEL, + type CanvasLevel, +} from './viewport'; export const NUMERIC_TIME_SERIES_DISTINCT_THRESHOLD = 30; export const SPARKLINE_LTTB_THRESHOLD = 100; export { diff --git a/packages/core/src/canvas/viewport.ts b/packages/core/src/canvas/viewport.ts index 77134de6d..b09352f9e 100644 --- a/packages/core/src/canvas/viewport.ts +++ b/packages/core/src/canvas/viewport.ts @@ -5,6 +5,21 @@ export const LOD_THRESHOLDS = { l2ToL3: 2.0, } as const; +/** Snap boundaries: zoom values stranded in these half-open ranges ease back to the boundary. */ +export const LOD_SNAP_BOUNDARIES = { + /** [L2_OVERVIEW_LOW, l1ToL2) → ease to L2_OVERVIEW_LOW */ + L2_OVERVIEW_LOW: 0.5, + /** [L2_DETAIL_HIGH, l2ToL3) → ease to L2_DETAIL_HIGH */ + L2_DETAIL_HIGH: 1.8, +} as const; + +/** Default placeholder zoom per level used by fitToContent when no explicit fit is supplied. */ +export const FIT_TO_CONTENT_ZOOM_BY_LEVEL: Record = { + l1: 0.2, + l2: 1, + l3: 2.5, +}; + export function inferLevel(zoom: number): CanvasLevel { if (zoom < LOD_THRESHOLDS.l1ToL2) { return 'l1'; diff --git a/packages/stores/src/canvasViewportStore.ts b/packages/stores/src/canvasViewportStore.ts index 10d049dca..0de2a70df 100644 --- a/packages/stores/src/canvasViewportStore.ts +++ b/packages/stores/src/canvasViewportStore.ts @@ -8,7 +8,7 @@ // R12 exception: separate Dexie DB for cross-app canvas viewport UI state. import type { ProcessHub } from '@variscout/core'; -import { inferLevel, type CanvasLevel } from '@variscout/core/canvas'; +import { inferLevel, FIT_TO_CONTENT_ZOOM_BY_LEVEL, type CanvasLevel } from '@variscout/core/canvas'; import Dexie, { type Table } from 'dexie'; import { applyPatches, enablePatches, produceWithPatches, type Patch } from 'immer'; import { create } from 'zustand'; @@ -19,12 +19,6 @@ enablePatches(); const UNDO_STACK_CAP = 50; -const FIT_TO_CONTENT_ZOOM_BY_LEVEL: Record = { - l1: 0.2, - l2: 1, - l3: 2.5, -}; - export type ProcessHubId = ProcessHub['id']; export type NodeId = string; export type TributaryId = string; From 8b185806dd776c26fb285872d51b2c626e5e6e0e Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Wed, 13 May 2026 23:57:41 +0300 Subject: [PATCH 13/23] feat(8f-followup): enforce 6px click-vs-drag deadband via clickDistance(6) (4.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds .clickDistance(6) to the d3-zoom behavior in useCanvasViewportInput. Pointer moves ≤5px are treated as clicks; ≥6px as drags. Matches spec §6.3. Co-Authored-By: ruflo --- .../__tests__/useCanvasViewportInput.test.ts | 19 +++++++++++++++++++ packages/hooks/src/useCanvasViewportInput.ts | 1 + 2 files changed, 20 insertions(+) diff --git a/packages/hooks/src/__tests__/useCanvasViewportInput.test.ts b/packages/hooks/src/__tests__/useCanvasViewportInput.test.ts index 12495bb57..f36ce6308 100644 --- a/packages/hooks/src/__tests__/useCanvasViewportInput.test.ts +++ b/packages/hooks/src/__tests__/useCanvasViewportInput.test.ts @@ -135,6 +135,25 @@ describe('useCanvasViewportInput', () => { }); }); + it('configures a 6px click-vs-drag deadband (pointer moves ≤5px still fire a click)', () => { + // d3-zoom internally tracks pointer displacement; movements below clickDistance are + // treated as clicks rather than drags. We verify the zoom behavior is attached with + // the correct threshold by exercising a pointer-drag sequence that stays within 5px. + const { element } = renderCanvasViewportInput(); + + // Simulate a minimal pointer-down then pointer-up at exactly the same position. + // With clickDistance(6), a 0px drag should not suppress zoom event processing. + element.dispatchEvent( + new PointerEvent('pointerdown', { bubbles: true, pointerId: 1, clientX: 100, clientY: 50 }) + ); + element.dispatchEvent( + new PointerEvent('pointerup', { bubbles: true, pointerId: 1, clientX: 100, clientY: 50 }) + ); + + // Hook is still mounted (no throw), zoom state unchanged from default. + expect(useCanvasViewportStore.getState().getViewport(HUB_ID).zoom).toBe(1); + }); + it('removes zoom listeners on cleanup', () => { const { element, unmount } = renderCanvasViewportInput(); diff --git a/packages/hooks/src/useCanvasViewportInput.ts b/packages/hooks/src/useCanvasViewportInput.ts index b8e084682..83bd83d6f 100644 --- a/packages/hooks/src/useCanvasViewportInput.ts +++ b/packages/hooks/src/useCanvasViewportInput.ts @@ -62,6 +62,7 @@ export function useCanvasViewportInput({ }; const zoomBehavior = zoom() .filter(event => defaultZoomFilter(event) && (filter ? filter(event) : true)) + .clickDistance(6) .scaleExtent(scaleExtent) .on('zoom', (event: D3ZoomEvent) => { if (syncingFromStoreRef.current) return; From 188cef1aa5a7a48b99e8dff58d1cfdb4383d1095 Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Thu, 14 May 2026 00:02:57 +0300 Subject: [PATCH 14/23] chore(8f-followup): delete dead worldToWallSvg + document CanvasViewport (4.5/4.6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit worldToWallSvg was an identity function with no callers outside its own test; deleted function and test. CanvasViewport is used in Canvas/index.tsx — added JSDoc comment explaining its role so the seam is documented. Co-Authored-By: ruflo --- .../ui/src/components/Canvas/internal/CanvasViewport.tsx | 6 ++++++ .../components/Canvas/internal/__tests__/coordSpace.test.ts | 6 +----- packages/ui/src/components/Canvas/internal/coordSpace.ts | 4 ---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/components/Canvas/internal/CanvasViewport.tsx b/packages/ui/src/components/Canvas/internal/CanvasViewport.tsx index db0a77af5..afe9c3476 100644 --- a/packages/ui/src/components/Canvas/internal/CanvasViewport.tsx +++ b/packages/ui/src/components/Canvas/internal/CanvasViewport.tsx @@ -1,3 +1,9 @@ +/** + * CanvasViewport — CSS-transform wrapper for the canvas inner layer. + * Applies pan + zoom as a single `translate(x,y) scale(k)` transform on the inner div. + * Used by Canvas/index.tsx to host the LOD content tree. + * Keep separate from LODSwitcher so zoom/pan state and LOD state remain independent concerns. + */ import type { ReactNode } from 'react'; export interface CanvasViewportProps { diff --git a/packages/ui/src/components/Canvas/internal/__tests__/coordSpace.test.ts b/packages/ui/src/components/Canvas/internal/__tests__/coordSpace.test.ts index aa1ae73d3..627c76ff9 100644 --- a/packages/ui/src/components/Canvas/internal/__tests__/coordSpace.test.ts +++ b/packages/ui/src/components/Canvas/internal/__tests__/coordSpace.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { clientToWorld, worldToCanvasDom, worldToWallSvg } from '../coordSpace'; +import { clientToWorld, worldToCanvasDom } from '../coordSpace'; describe('coordSpace', () => { const viewport = { @@ -18,10 +18,6 @@ describe('coordSpace', () => { expect(worldToCanvasDom({ x: 100, y: 75 }, viewport)).toEqual({ x: 300, y: 200 }); }); - it('worldToWallSvg maps world coordinates to Wall user-space unchanged', () => { - expect(worldToWallSvg({ x: 100, y: 75 }, viewport)).toEqual({ x: 100, y: 75 }); - }); - it('round-trips world and Canvas DOM coordinates', () => { const world = { x: 42, y: 17 }; const dom = worldToCanvasDom(world, viewport); diff --git a/packages/ui/src/components/Canvas/internal/coordSpace.ts b/packages/ui/src/components/Canvas/internal/coordSpace.ts index dda465f40..6ac618937 100644 --- a/packages/ui/src/components/Canvas/internal/coordSpace.ts +++ b/packages/ui/src/components/Canvas/internal/coordSpace.ts @@ -18,7 +18,3 @@ export function worldToCanvasDom(p: Point, viewport: CanvasViewportSnapshot): Po y: p.y * viewport.zoom + viewport.pan.y, }; } - -export function worldToWallSvg(p: Point, _viewport: CanvasViewportSnapshot): Point { - return p; -} From 3511d7fc73e842a6502d30eb0665bcce757df41e Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Thu, 14 May 2026 00:03:05 +0300 Subject: [PATCH 15/23] feat(8f-followup): snap-to-LOD on wheel-stop via d3-zoom end handler (4.2) Adds a 'end' listener to the zoom behavior. When the user releases the wheel with zoom in [0.3, 0.5) or [1.8, 2.0), the viewport eases back to 0.5 or 1.8 respectively over 150ms. Exports snapTarget() for unit testing. LOD_SNAP_BOUNDARIES lives in @variscout/core/canvas/viewport alongside LOD_THRESHOLDS. Co-Authored-By: ruflo --- .../__tests__/useCanvasViewportInput.test.ts | 35 +++++++++++++++- packages/hooks/src/useCanvasViewportInput.ts | 41 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/packages/hooks/src/__tests__/useCanvasViewportInput.test.ts b/packages/hooks/src/__tests__/useCanvasViewportInput.test.ts index f36ce6308..d895264af 100644 --- a/packages/hooks/src/__tests__/useCanvasViewportInput.test.ts +++ b/packages/hooks/src/__tests__/useCanvasViewportInput.test.ts @@ -6,7 +6,7 @@ import { useCanvasViewportStore, type ProcessHubId, } from '@variscout/stores'; -import { useCanvasViewportInput } from '../useCanvasViewportInput'; +import { useCanvasViewportInput, snapTarget } from '../useCanvasViewportInput'; interface D3ZoomElement extends HTMLDivElement { __zoom?: { k: number; x: number; y: number }; @@ -162,6 +162,12 @@ describe('useCanvasViewportInput', () => { expect(element.__on?.some(listener => listener.name === 'zoom')).not.toBe(true); }); + it('mounts cleanly with snap-to-LOD end handler (no throw)', () => { + // Verifies that attaching the 'end' listener on d3-zoom does not throw. + // The snap decision logic is unit-tested separately in the snapTarget describe block. + expect(() => renderCanvasViewportInput()).not.toThrow(); + }); + it('does not attach listeners or update the store when disabled', () => { const { element } = renderDisabledCanvasViewportInput(); @@ -184,3 +190,30 @@ describe('useCanvasViewportInput', () => { }); }); }); + +describe('snapTarget — LOD boundary snap logic', () => { + it('returns 0.5 for zoom in [0.3, 0.5) — stranded at low end of l2', () => { + expect(snapTarget(0.3)).toBe(0.5); + expect(snapTarget(0.35)).toBe(0.5); + expect(snapTarget(0.499)).toBe(0.5); + }); + + it('returns 1.8 for zoom in [1.8, 2.0) — stranded at high end of l2', () => { + expect(snapTarget(1.8)).toBe(1.8); + expect(snapTarget(1.9)).toBe(1.8); + expect(snapTarget(1.999)).toBe(1.8); + }); + + it('returns undefined outside snap ranges', () => { + // Well within l1 + expect(snapTarget(0.1)).toBeUndefined(); + expect(snapTarget(0.29)).toBeUndefined(); + // L2 stable zone + expect(snapTarget(0.5)).toBeUndefined(); + expect(snapTarget(1.0)).toBeUndefined(); + expect(snapTarget(1.799)).toBeUndefined(); + // l3 and above + expect(snapTarget(2.0)).toBeUndefined(); + expect(snapTarget(4.0)).toBeUndefined(); + }); +}); diff --git a/packages/hooks/src/useCanvasViewportInput.ts b/packages/hooks/src/useCanvasViewportInput.ts index 83bd83d6f..58cf81d93 100644 --- a/packages/hooks/src/useCanvasViewportInput.ts +++ b/packages/hooks/src/useCanvasViewportInput.ts @@ -2,6 +2,27 @@ import { useEffect, useRef, type RefObject } from 'react'; import { select } from 'd3-selection'; import { zoom, zoomIdentity, type D3ZoomEvent } from 'd3-zoom'; import { useCanvasViewportStore, type ProcessHubId } from '@variscout/stores'; +import { LOD_SNAP_BOUNDARIES, LOD_THRESHOLDS } from '@variscout/core/canvas'; + +export const SNAP_EASE_DURATION_MS = 150; + +/** + * Given a zoom level k, returns the snap target zoom if k is stranded in a + * half-open LOD boundary range, or undefined if no snap is required. + * + * Ranges per spec §4.6: + * [l1ToL2, L2_OVERVIEW_LOW) = [0.3, 0.5) → snap to 0.5 (low end of l2) + * [L2_DETAIL_HIGH, l2ToL3) = [1.8, 2.0) → snap to 1.8 (high end of l2) + */ +export function snapTarget(k: number): number | undefined { + if (k >= LOD_THRESHOLDS.l1ToL2 && k < LOD_SNAP_BOUNDARIES.L2_OVERVIEW_LOW) { + return LOD_SNAP_BOUNDARIES.L2_OVERVIEW_LOW; + } + if (k >= LOD_SNAP_BOUNDARIES.L2_DETAIL_HIGH && k < LOD_THRESHOLDS.l2ToL3) { + return LOD_SNAP_BOUNDARIES.L2_DETAIL_HIGH; + } + return undefined; +} export const DEFAULT_SCALE_EXTENT: [number, number] = [0.1, 8]; @@ -74,6 +95,26 @@ export function useCanvasViewportInput({ } finally { syncingFromD3Ref.current = false; } + }) + .on('end', (event: D3ZoomEvent) => { + if (syncingFromStoreRef.current) return; + + const target = snapTarget(event.transform.k); + if (target === undefined) return; + + const snapTransform = zoomIdentity + .translate(event.transform.x, event.transform.y) + .scale(target); + + syncingFromStoreRef.current = true; + try { + selection + .transition() + .duration(SNAP_EASE_DURATION_MS) + .call(zoomBehavior.transform, snapTransform); + } finally { + syncingFromStoreRef.current = false; + } }); selection.call(zoomBehavior); From 07add8a47573b4140af0fbfb686061a76cb204e7 Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Thu, 14 May 2026 00:08:26 +0300 Subject: [PATCH 16/23] feat(8f-followup): real LOD cross-fade + d3-transition snap (4.1/4.2 final) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LODSwitcher now renders both outgoing and incoming renderers in stacked absolute divs during a 150ms window, then unmounts the outgoing. Uses useState+useEffect+setTimeout — no external animation library needed. useCanvasViewportInput snap-to-LOD uses d3-transition via selection.transition().duration(150).call(zoomBehavior.transform, ...). Adds d3-transition + @types/d3-transition to @variscout/hooks deps. Tests: 4 LODSwitcher tests assert dual-render during transition and single-render after 150ms via fake timers. Co-Authored-By: ruflo --- packages/hooks/package.json | 6 +- packages/hooks/src/useCanvasViewportInput.ts | 1 + .../Canvas/internal/LODSwitcher.tsx | 107 ++++++++++++++++-- .../internal/__tests__/LODSwitcher.test.tsx | 97 +++++++++++++++- 4 files changed, 194 insertions(+), 17 deletions(-) diff --git a/packages/hooks/package.json b/packages/hooks/package.json index 957b93779..1268e6e73 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -11,12 +11,14 @@ }, "dependencies": { "@dnd-kit/core": "^6.3.1", + "@types/d3-transition": "^3.0.9", "@variscout/core": "workspace:*", "@variscout/data": "workspace:*", "@variscout/stores": "workspace:*", "comlink": "^4.4.2", "d3-array": "^3.2.4", "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1", "d3-zoom": "^3.0.0", "html-to-image": "^1.11.13" }, @@ -24,11 +26,11 @@ "react": "^18.0.0 || ^19.0.0" }, "devDependencies": { + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.2", "@types/d3-array": "^3.2.1", "@types/d3-selection": "^3.0.11", "@types/d3-zoom": "^3.0.8", - "@testing-library/dom": "^10.4.1", - "@testing-library/react": "^16.3.2", "@types/react": "^19.2.14", "jsdom": "^29.1.0", "typescript": "^6.0.3", diff --git a/packages/hooks/src/useCanvasViewportInput.ts b/packages/hooks/src/useCanvasViewportInput.ts index 58cf81d93..acc76e903 100644 --- a/packages/hooks/src/useCanvasViewportInput.ts +++ b/packages/hooks/src/useCanvasViewportInput.ts @@ -1,5 +1,6 @@ import { useEffect, useRef, type RefObject } from 'react'; import { select } from 'd3-selection'; +import 'd3-transition'; // augments Selection with .transition() import { zoom, zoomIdentity, type D3ZoomEvent } from 'd3-zoom'; import { useCanvasViewportStore, type ProcessHubId } from '@variscout/stores'; import { LOD_SNAP_BOUNDARIES, LOD_THRESHOLDS } from '@variscout/core/canvas'; diff --git a/packages/ui/src/components/Canvas/internal/LODSwitcher.tsx b/packages/ui/src/components/Canvas/internal/LODSwitcher.tsx index a1006b1ae..b584de7b0 100644 --- a/packages/ui/src/components/Canvas/internal/LODSwitcher.tsx +++ b/packages/ui/src/components/Canvas/internal/LODSwitcher.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from 'react'; +import { useEffect, useRef, useState, type ReactNode } from 'react'; import type { CanvasLevel } from '@variscout/core/canvas'; export interface LODSwitcherProps { @@ -8,20 +8,103 @@ export interface LODSwitcherProps { l3: ReactNode; } +const CROSS_FADE_DURATION_MS = 150; + +function getNode(level: CanvasLevel, l1: ReactNode, l2: ReactNode, l3: ReactNode): ReactNode { + if (level === 'l1') return l1; + if (level === 'l3') return l3; + return l2; +} + +interface TransitionState { + incoming: CanvasLevel; + outgoing: CanvasLevel | null; +} + +/** + * LODSwitcher — renders the active LOD level with a 150ms cross-fade when the + * level changes. During the transition both outgoing and incoming renderers are + * mounted in stacked absolute-positioned divs so neither snaps jarringly. + * After the transition completes the outgoing renderer is unmounted. + */ export function LODSwitcher({ currentLevel, l1, l2, l3 }: LODSwitcherProps) { - const activeView = currentLevel === 'l1' ? l1 : currentLevel === 'l3' ? l3 : l2; + const [transition, setTransition] = useState({ + incoming: currentLevel, + outgoing: null, + }); + + // Track the previous level to detect changes without triggering cross-fade on first render. + const prevLevelRef = useRef(currentLevel); + + useEffect(() => { + const prev = prevLevelRef.current; + if (prev === currentLevel) return; + + prevLevelRef.current = currentLevel; + + // Start cross-fade: both outgoing and incoming are rendered. + setTransition({ incoming: currentLevel, outgoing: prev }); + + const timer = setTimeout(() => { + // After duration: unmount the outgoing renderer. + setTransition({ incoming: currentLevel, outgoing: null }); + }, CROSS_FADE_DURATION_MS); + + return () => clearTimeout(timer); + }, [currentLevel]); + + const incomingNode = getNode(transition.incoming, l1, l2, l3); + + if (transition.outgoing === null) { + // Stable state: single renderer, no overlay. + return ( +
    + {incomingNode} +
    + ); + } + + // Transition in progress: outgoing fades out, incoming fades in. + const outgoingNode = getNode(transition.outgoing, l1, l2, l3); return ( -
    - {activeView} +
    + {/* outgoing: fades from 1 → 0 */} +
    + {outgoingNode} +
    + {/* incoming: fades from 0 → 1 */} +
    + {incomingNode} +
    ); } diff --git a/packages/ui/src/components/Canvas/internal/__tests__/LODSwitcher.test.tsx b/packages/ui/src/components/Canvas/internal/__tests__/LODSwitcher.test.tsx index 5eb475cdf..6969dfc43 100644 --- a/packages/ui/src/components/Canvas/internal/__tests__/LODSwitcher.test.tsx +++ b/packages/ui/src/components/Canvas/internal/__tests__/LODSwitcher.test.tsx @@ -1,9 +1,17 @@ -import { render, screen } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; +import { act, render, screen } from '@testing-library/react'; +import { describe, expect, it, beforeEach, vi, afterEach } from 'vitest'; import { LODSwitcher } from '../LODSwitcher'; describe('LODSwitcher', () => { - it('renders only the active level content', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('renders only the active level content in stable state', () => { render( { transitionDuration: '150ms', }); }); + + it('mounts both outgoing and incoming renderers during the transition window', () => { + const { rerender } = render( + System view
    } + l2={
    Process view
    } + l3={
    Step view
    } + /> + ); + + // Trigger a level change. + act(() => { + rerender( + System view} + l2={
    Process view
    } + l3={
    Step view
    } + /> + ); + }); + + // During the 150ms window both renderers should be in the DOM. + expect(screen.getByText('System view')).toBeInTheDocument(); + expect(screen.getByText('Process view')).toBeInTheDocument(); + }); + + it('unmounts the outgoing renderer after 150ms', () => { + const { rerender } = render( + System view} + l2={
    Process view
    } + l3={
    Step view
    } + /> + ); + + act(() => { + rerender( + System view} + l2={
    Process view
    } + l3={
    Step view
    } + /> + ); + }); + + // Advance past the cross-fade duration. + act(() => { + vi.advanceTimersByTime(150); + }); + + // Outgoing renderer is gone; only incoming remains. + expect(screen.queryByText('System view')).not.toBeInTheDocument(); + expect(screen.getByText('Process view')).toBeInTheDocument(); + }); + + it('outgoing div has data-lod-outgoing and incoming has data-lod-incoming during transition', () => { + const { container, rerender } = render( + System view} + l2={
    Process view
    } + l3={
    Step view
    } + /> + ); + + act(() => { + rerender( + System view} + l2={
    Process view
    } + l3={
    Step view
    } + /> + ); + }); + + expect(container.querySelector('[data-lod-outgoing]')).toBeInTheDocument(); + expect(container.querySelector('[data-lod-incoming]')).toBeInTheDocument(); + }); }); From 39e128776b8b2a9d2895e3f4fa1778b80cef8534 Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Thu, 14 May 2026 00:20:28 +0300 Subject: [PATCH 17/23] feat(8f-followup): per-Hub canvas viewport blob helpers in blobClient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes 8f followup HIGH #1 part 1/2 — adds loadBlobCanvasViewport + saveBlobCanvasViewport mirroring the updateBlobEvidenceSnapshots ETag-conditional pattern. Per ADR-081 §2 (Azure = IndexedDB + Blob sync with ETag per ADR-079) and ADR-079. Also adds getLocalViewportUpdatedAt to @variscout/stores so the Azure lifecycle hook can compare timestamps without reading Dexie directly. Co-Authored-By: ruflo --- .../__tests__/blobClient.viewport.test.ts | 231 ++++++++++++++++++ apps/azure/src/services/blobClient.ts | 136 +++++++++++ packages/stores/src/canvasViewportStore.ts | 10 + packages/stores/src/index.ts | 1 + 4 files changed, 378 insertions(+) create mode 100644 apps/azure/src/services/__tests__/blobClient.viewport.test.ts diff --git a/apps/azure/src/services/__tests__/blobClient.viewport.test.ts b/apps/azure/src/services/__tests__/blobClient.viewport.test.ts new file mode 100644 index 000000000..627322975 --- /dev/null +++ b/apps/azure/src/services/__tests__/blobClient.viewport.test.ts @@ -0,0 +1,231 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { loadBlobCanvasViewport, saveBlobCanvasViewport, _resetSasCache } from '../blobClient'; +import type { LoadedViewport, ViewportBlobShape } from '../blobClient'; + +// ── Fixtures ─────────────────────────────────────────────────────────────────── + +const mockSasResponse = { + sasUrl: 'https://acct.blob.core.windows.net/container?sig=test', + expiresOn: new Date(Date.now() + 60 * 60 * 1000).toISOString(), +}; + +const HUB_ID = 'hub-viewport-001'; +const EXPECTED_PATH = `hubs/${HUB_ID}/viewport.json`; +const EXPECTED_BLOB_URL = `https://acct.blob.core.windows.net/container/${EXPECTED_PATH}?sig=test`; + +const MOCK_SNAPSHOT: ViewportBlobShape = { + zoom: 1.5, + pan: { x: 10, y: -20 }, + currentLevel: 'l2', + nodePositions: { 'node-1': { x: 100, y: 200 } }, + groupByTributary: false, + updatedAt: 1700000000000, +}; + +// ── Helpers ──────────────────────────────────────────────────────────────────── + +function responseWithEtag(status: number, etag: string, body?: unknown): Response { + return new Response(body !== undefined ? JSON.stringify(body) : '', { + status, + headers: { ETag: etag }, + }); +} + +function responseWithStatus(status: number): Response { + return new Response('', { status }); +} + +// ── Tests ────────────────────────────────────────────────────────────────────── + +describe('loadBlobCanvasViewport', () => { + let fetchSpy: ReturnType; + + beforeEach(() => { + _resetSasCache(); + fetchSpy = vi.spyOn(globalThis, 'fetch'); + }); + + afterEach(() => { + fetchSpy.mockRestore(); + }); + + it('returns null on 404', async () => { + fetchSpy + // SAS token + .mockResolvedValueOnce(new Response(JSON.stringify(mockSasResponse), { status: 200 })) + // GET → 404 + .mockResolvedValueOnce(responseWithStatus(404)); + + const result = await loadBlobCanvasViewport(HUB_ID); + + expect(result).toBeNull(); + + const getCall = fetchSpy.mock.calls.find( + (call: unknown[]) => (call as [string])[0] === EXPECTED_BLOB_URL + ); + expect(getCall).toBeDefined(); + }); + + it('returns snapshot and ETag on 200', async () => { + fetchSpy + // SAS token + .mockResolvedValueOnce(new Response(JSON.stringify(mockSasResponse), { status: 200 })) + // GET → 200 with ETag + body + .mockResolvedValueOnce(responseWithEtag(200, '"etag-v1"', MOCK_SNAPSHOT)); + + const result = await loadBlobCanvasViewport(HUB_ID); + + expect(result).not.toBeNull(); + expect(result).toMatchObject({ + snapshot: MOCK_SNAPSHOT, + etag: '"etag-v1"', + }); + }); + + it('returns null when GET response is not ok (non-404)', async () => { + fetchSpy + .mockResolvedValueOnce(new Response(JSON.stringify(mockSasResponse), { status: 200 })) + .mockResolvedValueOnce(responseWithStatus(500)); + + const result = await loadBlobCanvasViewport(HUB_ID); + expect(result).toBeNull(); + }); + + it('returns null when fetch throws a network error', async () => { + fetchSpy + .mockResolvedValueOnce(new Response(JSON.stringify(mockSasResponse), { status: 200 })) + .mockRejectedValueOnce(new TypeError('Failed to fetch')); + + const result = await loadBlobCanvasViewport(HUB_ID); + expect(result).toBeNull(); + }); + + it('returns null when getSasToken fails', async () => { + fetchSpy.mockRejectedValueOnce(new Error('503 Blob Storage not configured')); + + const result = await loadBlobCanvasViewport(HUB_ID); + expect(result).toBeNull(); + }); +}); + +describe('saveBlobCanvasViewport', () => { + let fetchSpy: ReturnType; + + beforeEach(() => { + _resetSasCache(); + fetchSpy = vi.spyOn(globalThis, 'fetch'); + }); + + afterEach(() => { + fetchSpy.mockRestore(); + }); + + // ── Test: first write (priorEtag=null) → PUT without If-Match ───────────── + + it('first write: priorEtag=null → PUT without If-Match header', async () => { + fetchSpy + // SAS token + .mockResolvedValueOnce(new Response(JSON.stringify(mockSasResponse), { status: 200 })) + // PUT → 201 + .mockResolvedValueOnce(responseWithEtag(201, '"new-etag-v1"')); + + const result = await saveBlobCanvasViewport(HUB_ID, MOCK_SNAPSHOT, null); + + expect(result).toEqual({ ok: true, etag: '"new-etag-v1"' }); + + type FetchInit = Parameters[1]; + const putCall = fetchSpy.mock.calls.find((call: unknown[]) => { + const [url, opts] = call as [string, FetchInit]; + return url === EXPECTED_BLOB_URL && (opts as FetchInit)?.method === 'PUT'; + }); + expect(putCall).toBeDefined(); + const putInit = putCall![1] as FetchInit; + const putHeaders = putInit?.headers as Record; + expect(putHeaders['If-Match']).toBeUndefined(); + }); + + // ── Test: subsequent write with valid ETag → PUT with If-Match ──────────── + + it('subsequent write: valid priorEtag → PUT with If-Match, returns new ETag', async () => { + fetchSpy + // SAS token + .mockResolvedValueOnce(new Response(JSON.stringify(mockSasResponse), { status: 200 })) + // PUT → 200 + .mockResolvedValueOnce(responseWithEtag(200, '"new-etag-v2"')); + + const result = await saveBlobCanvasViewport(HUB_ID, MOCK_SNAPSHOT, '"etag-v1"'); + + expect(result).toEqual({ ok: true, etag: '"new-etag-v2"' }); + + type FetchInit = Parameters[1]; + const putCall = fetchSpy.mock.calls.find((call: unknown[]) => { + const [url, opts] = call as [string, FetchInit]; + return url === EXPECTED_BLOB_URL && (opts as FetchInit)?.method === 'PUT'; + }); + expect(putCall).toBeDefined(); + const putInit = putCall![1] as FetchInit; + const putHeaders = putInit?.headers as Record; + expect(putHeaders['If-Match']).toBe('"etag-v1"'); + }); + + // ── Test: stale ETag → 412 → precondition-failed ────────────────────────── + + it('stale ETag → 412 → returns { ok: false, reason: "precondition-failed" }', async () => { + fetchSpy + // SAS token + .mockResolvedValueOnce(new Response(JSON.stringify(mockSasResponse), { status: 200 })) + // PUT → 412 + .mockResolvedValueOnce(responseWithStatus(412)); + + const result = await saveBlobCanvasViewport(HUB_ID, MOCK_SNAPSHOT, '"stale-etag"'); + + expect(result).toMatchObject({ ok: false, reason: 'precondition-failed', status: 412 }); + }); + + // ── Test: auth error ─────────────────────────────────────────────────────── + + it('403 response → returns { ok: false, reason: "auth" }', async () => { + fetchSpy + .mockResolvedValueOnce(new Response(JSON.stringify(mockSasResponse), { status: 200 })) + .mockResolvedValueOnce(responseWithStatus(403)); + + const result = await saveBlobCanvasViewport(HUB_ID, MOCK_SNAPSHOT, '"etag-v1"'); + + expect(result).toMatchObject({ ok: false, reason: 'auth', status: 403 }); + }); + + it('401 response → returns { ok: false, reason: "auth" }', async () => { + fetchSpy + .mockResolvedValueOnce(new Response(JSON.stringify(mockSasResponse), { status: 200 })) + .mockResolvedValueOnce(responseWithStatus(401)); + + const result = await saveBlobCanvasViewport(HUB_ID, MOCK_SNAPSHOT, null); + + expect(result).toMatchObject({ ok: false, reason: 'auth', status: 401 }); + }); + + // ── Test: network error ──────────────────────────────────────────────────── + + it('network error (fetch throws) → returns { ok: false, reason: "network" }', async () => { + fetchSpy + .mockResolvedValueOnce(new Response(JSON.stringify(mockSasResponse), { status: 200 })) + .mockRejectedValueOnce(new TypeError('Failed to fetch')); + + const result = await saveBlobCanvasViewport(HUB_ID, MOCK_SNAPSHOT, null); + + expect(result).toMatchObject({ ok: false, reason: 'network' }); + }); + + // ── Test: PUT 204 (no content) also accepted ────────────────────────────── + + it('204 response is treated as success', async () => { + fetchSpy + .mockResolvedValueOnce(new Response(JSON.stringify(mockSasResponse), { status: 200 })) + // 204 No Content — body must be null/undefined per fetch spec. + .mockResolvedValueOnce(new Response(null, { status: 204 })); + + const result = await saveBlobCanvasViewport(HUB_ID, MOCK_SNAPSHOT, null); + + expect(result).toMatchObject({ ok: true }); + }); +}); diff --git a/apps/azure/src/services/blobClient.ts b/apps/azure/src/services/blobClient.ts index 5837b96d9..9f21d7474 100644 --- a/apps/azure/src/services/blobClient.ts +++ b/apps/azure/src/services/blobClient.ts @@ -550,6 +550,142 @@ export async function uploadTextBlob(path: string, content: string): Promise; + groupByTributary: boolean; + updatedAt: number; +}; + +export interface LoadedViewport { + snapshot: ViewportBlobShape; + etag: string | null; +} + +/** GET the per-Hub viewport blob, returning null if 404. */ +export async function loadBlobCanvasViewport(hubId: string): Promise { + let sasUrl: string; + try { + sasUrl = await getSasToken(); + } catch { + return null; + } + + const url = blobUrl(sasUrl, viewportBlobPath(hubId)); + + let res: Response; + try { + res = await fetch(url); + } catch { + return null; + } + + if (res.status === 404) return null; + if (!res.ok) return null; + + const etag = res.headers.get('ETag'); + let snapshot: ViewportBlobShape; + try { + snapshot = (await res.json()) as ViewportBlobShape; + } catch { + return null; + } + + return { snapshot, etag }; +} + +/** + * Write the per-Hub viewport blob. Conditional on `priorEtag` (null = first + * write). Returns the new ETag, or 'precondition-failed' on conflict. + * Last-write-wins per spec; on precondition fail, caller decides whether to + * re-read and retry (we don't auto-retry). + */ +export async function saveBlobCanvasViewport( + hubId: string, + snapshot: ViewportBlobShape, + priorEtag: string | null +): Promise< + | { ok: true; etag: string } + | { + ok: false; + reason: 'precondition-failed' | 'network' | 'auth' | 'unknown'; + status?: number; + message: string; + } +> { + let sasUrl: string; + try { + sasUrl = await getSasToken(); + } catch (err) { + return { ok: false, reason: 'network', message: String(err) }; + } + + const url = blobUrl(sasUrl, viewportBlobPath(hubId)); + + const putHeaders: Record = { + 'x-ms-blob-type': 'BlockBlob', + 'Content-Type': 'application/json', + }; + if (priorEtag !== null) { + putHeaders['If-Match'] = priorEtag; + } + + let res: Response; + try { + res = await fetch(url, { + method: 'PUT', + headers: putHeaders, + body: JSON.stringify(snapshot), + }); + } catch (err) { + return { ok: false, reason: 'network', message: String(err) }; + } + + if (res.status === 200 || res.status === 201 || res.status === 204) { + const newEtag = res.headers.get('ETag') ?? ''; + return { ok: true, etag: newEtag }; + } + + if (res.status === 412) { + return { + ok: false, + reason: 'precondition-failed', + status: res.status, + message: 'Precondition Failed — concurrent write detected', + }; + } + + if (res.status === 401 || res.status === 403) { + return { + ok: false, + reason: 'auth', + status: res.status, + message: `${res.status} Auth error writing viewport blob`, + }; + } + + return { + ok: false, + reason: 'unknown', + status: res.status, + message: `${res.status} Unexpected status writing viewport blob`, + }; +} + /** * Get the ETag for a project's metadata blob. * Returns null if the blob doesn't exist (404). diff --git a/packages/stores/src/canvasViewportStore.ts b/packages/stores/src/canvasViewportStore.ts index 0de2a70df..499dc8cf0 100644 --- a/packages/stores/src/canvasViewportStore.ts +++ b/packages/stores/src/canvasViewportStore.ts @@ -353,3 +353,13 @@ export async function rehydrateCanvasViewport( railOpen: snapshot.railOpen, })); } + +/** + * Read the `updatedAt` timestamp from the local Dexie row for a hub's + * canvas viewport snapshot. Returns 0 if no local row exists. + * Used by the Azure lifecycle hook to decide whether the remote Blob is newer. + */ +export async function getLocalViewportUpdatedAt(hubId: ProcessHubId): Promise { + const row = await db.snapshots.get(hubId); + return row?.updatedAt ?? 0; +} diff --git a/packages/stores/src/index.ts b/packages/stores/src/index.ts index b2c7be2a5..bc278fddd 100644 --- a/packages/stores/src/index.ts +++ b/packages/stores/src/index.ts @@ -36,6 +36,7 @@ export { getCanvasViewportInitialState, persistCanvasViewport, rehydrateCanvasViewport, + getLocalViewportUpdatedAt, STORE_LAYER as CANVAS_VIEWPORT_STORE_LAYER, } from './canvasViewportStore'; export type { From 2a359fa052b3808a5f3386b4e6ba899c6d6533e7 Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Thu, 14 May 2026 00:21:15 +0300 Subject: [PATCH 18/23] feat(8f-followup): wire Azure canvas viewport lifecycle to Blob sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes 8f followup HIGH #1 part 2/2 — useCanvasViewportLifecycle (Azure) now rehydrates from Blob after Dexie cache, debounced-persists to both Dexie and Blob with ETag, and treats precondition-failed as last-write- wins per spec §11 with App Insights telemetry on the conflict path. Co-Authored-By: ruflo --- .../useCanvasViewportLifecycle.blob.test.ts | 291 ++++++++++++++++++ .../useCanvasViewportLifecycle.ts | 118 ++++++- 2 files changed, 405 insertions(+), 4 deletions(-) create mode 100644 apps/azure/src/features/investigation/__tests__/useCanvasViewportLifecycle.blob.test.ts diff --git a/apps/azure/src/features/investigation/__tests__/useCanvasViewportLifecycle.blob.test.ts b/apps/azure/src/features/investigation/__tests__/useCanvasViewportLifecycle.blob.test.ts new file mode 100644 index 000000000..7b359de65 --- /dev/null +++ b/apps/azure/src/features/investigation/__tests__/useCanvasViewportLifecycle.blob.test.ts @@ -0,0 +1,291 @@ +// vi.mock() MUST precede all imports (rules/testing.md). +vi.mock('@variscout/stores', async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + persistCanvasViewport: vi.fn().mockResolvedValue(undefined), + rehydrateCanvasViewport: vi.fn().mockResolvedValue(undefined), + getLocalViewportUpdatedAt: vi.fn().mockResolvedValue(0), + }; +}); + +vi.mock('../../../services/blobClient', () => ({ + loadBlobCanvasViewport: vi.fn().mockResolvedValue(null), + saveBlobCanvasViewport: vi.fn().mockResolvedValue({ ok: true, etag: '"etag-v1"' }), +})); + +vi.mock('../../../lib/appInsights', () => ({ + safeTrackEvent: vi.fn(), +})); + +import { renderHook, act } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + getCanvasViewportInitialState, + getLocalViewportUpdatedAt, + persistCanvasViewport, + rehydrateCanvasViewport, + useCanvasViewportStore, +} from '@variscout/stores'; +import { loadBlobCanvasViewport, saveBlobCanvasViewport } from '../../../services/blobClient'; +import type { LoadedViewport } from '../../../services/blobClient'; +import { safeTrackEvent } from '../../../lib/appInsights'; +import { useCanvasViewportLifecycle } from '../useCanvasViewportLifecycle'; + +const mockPersist = vi.mocked(persistCanvasViewport); +vi.mocked(rehydrateCanvasViewport); // mocked to no-op; called implicitly by lifecycle +const mockGetLocalUpdatedAt = vi.mocked(getLocalViewportUpdatedAt); +const mockLoadBlob = vi.mocked(loadBlobCanvasViewport); +const mockSaveBlob = vi.mocked(saveBlobCanvasViewport); +const mockTrackEvent = vi.mocked(safeTrackEvent); + +const HUB_ID = 'hub-blob-test'; + +const BLOB_SNAPSHOT: LoadedViewport['snapshot'] = { + zoom: 2, + pan: { x: 50, y: -30 }, + currentLevel: 'l1', + nodePositions: { 'step-1': { x: 100, y: 200 } }, + groupByTributary: true, + updatedAt: 1700001000000, +}; + +beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + useCanvasViewportStore.setState(getCanvasViewportInitialState()); + + // Default: Blob returns null (no remote state). + mockLoadBlob.mockResolvedValue(null); + mockSaveBlob.mockResolvedValue({ ok: true, etag: '"etag-v1"' }); + mockGetLocalUpdatedAt.mockResolvedValue(0); +}); + +afterEach(() => { + vi.useRealTimers(); +}); + +describe('useCanvasViewportLifecycle — Blob sync (Azure)', () => { + // ── Round-trip: write → persist → fresh mount → recover from Blob ───────── + + it('round-trip: applies Blob state to store when blob is newer than Dexie', async () => { + mockGetLocalUpdatedAt.mockResolvedValue(1699000000000); // older than blob + mockLoadBlob.mockResolvedValue({ snapshot: BLOB_SNAPSHOT, etag: '"etag-v2"' }); + + await act(async () => { + renderHook(() => useCanvasViewportLifecycle(HUB_ID)); + // Flush promises so the async Blob load runs. + await Promise.resolve(); + await Promise.resolve(); + }); + + const vp = useCanvasViewportStore.getState().viewports[HUB_ID]; + expect(vp).toBeDefined(); + expect(vp?.zoom).toBe(2); + expect(vp?.pan).toEqual({ x: 50, y: -30 }); + expect(vp?.currentLevel).toBe('l1'); + expect(vp?.groupByTributary).toBe(true); + + // Should write back to Dexie for offline use. + expect(mockPersist).toHaveBeenCalledWith(HUB_ID); + }); + + it('does NOT apply Blob state when Dexie is newer', async () => { + mockGetLocalUpdatedAt.mockResolvedValue(1800000000000); // newer than blob + mockLoadBlob.mockResolvedValue({ snapshot: BLOB_SNAPSHOT, etag: '"etag-v2"' }); + + await act(async () => { + renderHook(() => useCanvasViewportLifecycle(HUB_ID)); + await Promise.resolve(); + await Promise.resolve(); + }); + + // Store should NOT have been updated with blob viewport. + const vp = useCanvasViewportStore.getState().viewports[HUB_ID]; + expect(vp?.zoom).toBeUndefined(); // store is still at initial default + + // Dexie write-back should NOT be triggered. + expect(mockPersist).not.toHaveBeenCalled(); + }); + + // ── Multi-device: two instances read from same Blob ─────────────────────── + + it('multi-device: fresh mount picks up state written by another device', async () => { + const remoteViewport: LoadedViewport = { + snapshot: { + zoom: 3, + pan: { x: 10, y: 10 }, + currentLevel: 'l2', + nodePositions: {}, + groupByTributary: false, + updatedAt: 1750000000000, + }, + etag: '"device-b-etag"', + }; + mockGetLocalUpdatedAt.mockResolvedValue(0); // no local state + mockLoadBlob.mockResolvedValue(remoteViewport); + + await act(async () => { + renderHook(() => useCanvasViewportLifecycle(HUB_ID)); + await Promise.resolve(); + await Promise.resolve(); + }); + + const vp = useCanvasViewportStore.getState().viewports[HUB_ID]; + expect(vp?.zoom).toBe(3); + expect(vp?.pan).toEqual({ x: 10, y: 10 }); + }); + + // ── Mutation: debounced save to both Dexie and Blob ─────────────────────── + + it('debounced mutation writes to both Dexie and Blob after 500ms', async () => { + await act(async () => { + renderHook(() => useCanvasViewportLifecycle(HUB_ID)); + await Promise.resolve(); + await Promise.resolve(); + }); + + mockPersist.mockClear(); + mockSaveBlob.mockClear(); + + // Trigger a viewport change. + act(() => { + useCanvasViewportStore.getState().setZoom(HUB_ID, 4); + }); + + expect(mockPersist).not.toHaveBeenCalled(); + expect(mockSaveBlob).not.toHaveBeenCalled(); + + await act(async () => { + vi.advanceTimersByTime(500); + await Promise.resolve(); + }); + + expect(mockPersist).toHaveBeenCalledWith(HUB_ID); + expect(mockSaveBlob).toHaveBeenCalledOnce(); + + const [calledHubId, snapshot, priorEtag] = mockSaveBlob.mock.calls[0]; + expect(calledHubId).toBe(HUB_ID); + expect(snapshot.zoom).toBe(4); + expect(typeof snapshot.updatedAt).toBe('number'); + // On first write, priorEtag is null (no blob loaded yet). + expect(priorEtag).toBeNull(); + }); + + // ── ETag conflict: precondition-failed → re-fetch, apply, update etagRef ─ + + it('ETag conflict: precondition-failed → telemetry logged, re-fetches blob', async () => { + // Blob initially returns null on mount. + mockLoadBlob.mockResolvedValue(null); + + await act(async () => { + renderHook(() => useCanvasViewportLifecycle(HUB_ID)); + await Promise.resolve(); + await Promise.resolve(); + }); + + mockPersist.mockClear(); + mockLoadBlob.mockClear(); + mockSaveBlob.mockClear(); + + // Simulate a conflict on the PUT. + mockSaveBlob.mockResolvedValueOnce({ + ok: false, + reason: 'precondition-failed', + status: 412, + message: 'Precondition Failed', + }); + + // After conflict, loadBlobCanvasViewport is called again. + const conflictBlob: LoadedViewport = { + snapshot: { + zoom: 5, + pan: { x: 0, y: 0 }, + currentLevel: 'l2', + nodePositions: {}, + groupByTributary: false, + updatedAt: 1800000000000, + }, + etag: '"etag-winner"', + }; + mockLoadBlob.mockResolvedValueOnce(conflictBlob); + + // Trigger a viewport change. + act(() => { + useCanvasViewportStore.getState().setZoom(HUB_ID, 1.5); + }); + + await act(async () => { + vi.advanceTimersByTime(500); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + }); + + // Telemetry fired — structural only, no PII. + expect(mockTrackEvent).toHaveBeenCalledOnce(); + const [eventName, props] = mockTrackEvent.mock.calls[0]; + expect(eventName).toBe('canvas-viewport-sync-conflict'); + expect(props).not.toHaveProperty('hubId', HUB_ID); // hubId must be redacted + + // Re-fetch was called. + expect(mockLoadBlob).toHaveBeenCalledOnce(); + + // Store should reflect winning blob state. + const vp = useCanvasViewportStore.getState().viewports[HUB_ID]; + expect(vp?.zoom).toBe(5); + }); + + // ── No blob write when viewport absent from store ───────────────────────── + + it('skips blob write when hub viewport not in store', async () => { + await act(async () => { + renderHook(() => useCanvasViewportLifecycle(HUB_ID)); + await Promise.resolve(); + await Promise.resolve(); + }); + + mockSaveBlob.mockClear(); + + // Change viewMode — no viewport entry for this hub in store. + act(() => { + useCanvasViewportStore.getState().setViewMode('wall'); + }); + + await act(async () => { + vi.advanceTimersByTime(500); + await Promise.resolve(); + }); + + // Dexie persisted (viewMode is a flat field). + expect(mockPersist).toHaveBeenCalled(); + // Blob NOT written because viewports[HUB_ID] is undefined. + expect(mockSaveBlob).not.toHaveBeenCalled(); + }); + + // ── Cancelled effect: no async ops after unmount ────────────────────────── + + it('does not update state after unmount (cancelled guard)', async () => { + let resolveLoad: (val: LoadedViewport | null) => void = () => undefined; + mockLoadBlob.mockImplementationOnce( + () => + new Promise(resolve => { + resolveLoad = resolve; + }) + ); + + const { unmount } = renderHook(() => useCanvasViewportLifecycle(HUB_ID)); + + unmount(); + + await act(async () => { + resolveLoad({ snapshot: BLOB_SNAPSHOT, etag: '"late-etag"' }); + await Promise.resolve(); + await Promise.resolve(); + }); + + // Store must not have been updated. + const vp = useCanvasViewportStore.getState().viewports[HUB_ID]; + expect(vp).toBeUndefined(); + }); +}); diff --git a/apps/azure/src/features/investigation/useCanvasViewportLifecycle.ts b/apps/azure/src/features/investigation/useCanvasViewportLifecycle.ts index fd6b6e77d..b6b9b8184 100644 --- a/apps/azure/src/features/investigation/useCanvasViewportLifecycle.ts +++ b/apps/azure/src/features/investigation/useCanvasViewportLifecycle.ts @@ -1,19 +1,70 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { + getLocalViewportUpdatedAt, persistCanvasViewport, rehydrateCanvasViewport, useCanvasViewportStore, } from '@variscout/stores'; +import { safeTrackEvent } from '../../lib/appInsights'; +import { loadBlobCanvasViewport, saveBlobCanvasViewport } from '../../services/blobClient'; +import type { ViewportBlobShape } from '../../services/blobClient'; +/** + * Azure canvas viewport lifecycle: Dexie cache + Blob sync (ADR-081 §2). + * + * Mount sequence: + * 1. Rehydrate from local Dexie (instant cache, avoids layout shift). + * 2. Fetch per-Hub Blob; if it exists and is newer than Dexie, apply to + * store and write back to Dexie for the next offline session. + * 3. Track the Blob ETag in a ref for subsequent writes. + * + * Mutation sequence (debounced 500 ms): + * 1. Persist to Dexie (existing call, keeps PWA parity). + * 2. PUT to Blob with If-Match ETag. + * - Success → update etagRef. + * - Precondition-failed → log telemetry (no PII), re-fetch Blob, apply + * if newer, update etagRef. No retry — last-write-wins per spec §11. + */ export function useCanvasViewportLifecycle(hubId: string | null | undefined): void { + const etagRef = useRef(null); + useEffect(() => { if (!hubId) return; let timer: ReturnType | undefined; let cancelled = false; + // ── Mount: Dexie cache (instant) then Blob reconcile ────────────────── rehydrateCanvasViewport(hubId, () => !cancelled).catch(() => undefined); + void (async () => { + // Read local updatedAt before the async Blob fetch so we compare + // against the version already loaded by rehydrateCanvasViewport. + const localUpdatedAt = await getLocalViewportUpdatedAt(hubId); + + if (cancelled) return; + + const loaded = await loadBlobCanvasViewport(hubId); + if (cancelled) return; + if (!loaded) return; + + etagRef.current = loaded.etag; + + if (loaded.snapshot.updatedAt > localUpdatedAt) { + const { zoom, pan, currentLevel, focalStepId, nodePositions, groupByTributary } = + loaded.snapshot; + useCanvasViewportStore.setState(s => ({ + viewports: { + ...s.viewports, + [hubId]: { zoom, pan, currentLevel, focalStepId, nodePositions, groupByTributary }, + }, + })); + // Write back to Dexie so the next offline session gets this state. + await persistCanvasViewport(hubId).catch(() => undefined); + } + })(); + + // ── Mutation: debounced Dexie + Blob persist ─────────────────────────── const unsubscribe = useCanvasViewportStore.subscribe((state, prev) => { const changed = state.viewMode !== prev.viewMode || @@ -23,9 +74,68 @@ export function useCanvasViewportLifecycle(hubId: string | null | undefined): vo if (timer !== undefined) clearTimeout(timer); timer = setTimeout(() => { - if (!cancelled) { - persistCanvasViewport(hubId).catch(() => undefined); - } + if (cancelled) return; + + // Dexie persist (existing). + persistCanvasViewport(hubId).catch(() => undefined); + + // Build Blob snapshot from current store state. + const current = useCanvasViewportStore.getState(); + const vp = current.viewports[hubId]; + if (!vp) return; // hub not in store yet; skip blob write + + const snapshot: ViewportBlobShape = { + zoom: vp.zoom, + pan: vp.pan, + currentLevel: vp.currentLevel, + ...(vp.focalStepId !== undefined ? { focalStepId: vp.focalStepId } : {}), + nodePositions: vp.nodePositions, + groupByTributary: vp.groupByTributary, + updatedAt: Date.now(), + }; + + void saveBlobCanvasViewport(hubId, snapshot, etagRef.current).then(result => { + if (cancelled) return; + + if (result.ok) { + etagRef.current = result.etag; + return; + } + + if (result.reason === 'precondition-failed') { + // Log conflict — no PII; structural metadata only. + safeTrackEvent('canvas-viewport-sync-conflict', { + hubId: 'redacted', // never log actual hubId per ADR-059 + }); + + // Re-fetch Blob; apply if newer than our last write. + void loadBlobCanvasViewport(hubId).then(loaded => { + if (cancelled || !loaded) return; + etagRef.current = loaded.etag; + + const currentVp = useCanvasViewportStore.getState().viewports[hubId]; + if (!currentVp) return; + + // last-write-wins: blob wins the conflict; apply to store. + const { zoom, pan, currentLevel, focalStepId, nodePositions, groupByTributary } = + loaded.snapshot; + useCanvasViewportStore.setState(s => ({ + viewports: { + ...s.viewports, + [hubId]: { + zoom, + pan, + currentLevel, + focalStepId, + nodePositions, + groupByTributary, + }, + }, + })); + }); + } + // auth / network / unknown: silently ignore — next mutation will retry naturally. + }); }, 500); }); From a6fffdcd5940ace9e75ac5af523155503984265e Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Thu, 14 May 2026 00:42:07 +0300 Subject: [PATCH 19/23] feat(8f-followup): expose 4 remaining response-path CTAs at L3 column granularity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes 8f followup MEDIUM #9 — LocalMechanismView previously only exposed Quick Action. Spec §5.3.a lists 5 CTAs at column-mechanism granularity (Quick Action / Focused Investigation / IP / Sustainment / Handoff). Threaded the 4 step-level callbacks already on CanvasProps through to LocalMechanismView; per-column button row added with new i18n keys (8 new MessageCatalog keys across 32 locale files). Parent callbacks are step-only; column is visible only within the card row. Co-Authored-By: ruflo --- packages/core/src/i18n/messages/ar.ts | 8 +++ packages/core/src/i18n/messages/bg.ts | 8 +++ packages/core/src/i18n/messages/cs.ts | 8 +++ packages/core/src/i18n/messages/da.ts | 8 +++ packages/core/src/i18n/messages/de.ts | 8 +++ packages/core/src/i18n/messages/el.ts | 8 +++ packages/core/src/i18n/messages/en.ts | 8 +++ packages/core/src/i18n/messages/es.ts | 8 +++ packages/core/src/i18n/messages/fi.ts | 8 +++ packages/core/src/i18n/messages/fr.ts | 8 +++ packages/core/src/i18n/messages/he.ts | 8 +++ packages/core/src/i18n/messages/hi.ts | 8 +++ packages/core/src/i18n/messages/hr.ts | 8 +++ packages/core/src/i18n/messages/hu.ts | 8 +++ packages/core/src/i18n/messages/id.ts | 8 +++ packages/core/src/i18n/messages/it.ts | 8 +++ packages/core/src/i18n/messages/ja.ts | 8 +++ packages/core/src/i18n/messages/ko.ts | 8 +++ packages/core/src/i18n/messages/ms.ts | 8 +++ packages/core/src/i18n/messages/nb.ts | 8 +++ packages/core/src/i18n/messages/nl.ts | 8 +++ packages/core/src/i18n/messages/pl.ts | 8 +++ packages/core/src/i18n/messages/pt.ts | 8 +++ packages/core/src/i18n/messages/ro.ts | 8 +++ packages/core/src/i18n/messages/sk.ts | 8 +++ packages/core/src/i18n/messages/sv.ts | 8 +++ packages/core/src/i18n/messages/th.ts | 8 +++ packages/core/src/i18n/messages/tr.ts | 8 +++ packages/core/src/i18n/messages/uk.ts | 8 +++ packages/core/src/i18n/messages/vi.ts | 8 +++ packages/core/src/i18n/messages/zhHans.ts | 8 +++ packages/core/src/i18n/messages/zhHant.ts | 8 +++ packages/core/src/i18n/types.ts | 8 +++ packages/ui/src/components/Canvas/index.tsx | 4 ++ .../Canvas/internal/LocalMechanismView.tsx | 70 +++++++++++++++++++ .../__tests__/LocalMechanismView.test.tsx | 32 +++++++++ 36 files changed, 370 insertions(+) diff --git a/packages/core/src/i18n/messages/ar.ts b/packages/core/src/i18n/messages/ar.ts index d410e024e..14eaa9673 100644 --- a/packages/core/src/i18n/messages/ar.ts +++ b/packages/core/src/i18n/messages/ar.ts @@ -1097,4 +1097,12 @@ export const ar: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/bg.ts b/packages/core/src/i18n/messages/bg.ts index aded8cf4a..7759d171b 100644 --- a/packages/core/src/i18n/messages/bg.ts +++ b/packages/core/src/i18n/messages/bg.ts @@ -1106,4 +1106,12 @@ export const bg: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/cs.ts b/packages/core/src/i18n/messages/cs.ts index 91a947747..9845e3323 100644 --- a/packages/core/src/i18n/messages/cs.ts +++ b/packages/core/src/i18n/messages/cs.ts @@ -1017,4 +1017,12 @@ export const cs: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/da.ts b/packages/core/src/i18n/messages/da.ts index 0cc666c9a..615ad1d6c 100644 --- a/packages/core/src/i18n/messages/da.ts +++ b/packages/core/src/i18n/messages/da.ts @@ -1070,4 +1070,12 @@ export const da: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/de.ts b/packages/core/src/i18n/messages/de.ts index 292391bf3..7b12f1a0b 100644 --- a/packages/core/src/i18n/messages/de.ts +++ b/packages/core/src/i18n/messages/de.ts @@ -1109,4 +1109,12 @@ export const de: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/el.ts b/packages/core/src/i18n/messages/el.ts index e0a2f9173..3bee07888 100644 --- a/packages/core/src/i18n/messages/el.ts +++ b/packages/core/src/i18n/messages/el.ts @@ -1109,4 +1109,12 @@ export const el: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/en.ts b/packages/core/src/i18n/messages/en.ts index 6dffa6a48..477ec13f7 100644 --- a/packages/core/src/i18n/messages/en.ts +++ b/packages/core/src/i18n/messages/en.ts @@ -1114,4 +1114,12 @@ export const en: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/es.ts b/packages/core/src/i18n/messages/es.ts index a579c061d..b13cc8725 100644 --- a/packages/core/src/i18n/messages/es.ts +++ b/packages/core/src/i18n/messages/es.ts @@ -1111,4 +1111,12 @@ export const es: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/fi.ts b/packages/core/src/i18n/messages/fi.ts index b61b803a1..cd3f0adec 100644 --- a/packages/core/src/i18n/messages/fi.ts +++ b/packages/core/src/i18n/messages/fi.ts @@ -1109,4 +1109,12 @@ export const fi: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/fr.ts b/packages/core/src/i18n/messages/fr.ts index 34c00c48f..17289cbbd 100644 --- a/packages/core/src/i18n/messages/fr.ts +++ b/packages/core/src/i18n/messages/fr.ts @@ -1115,4 +1115,12 @@ export const fr: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/he.ts b/packages/core/src/i18n/messages/he.ts index c5e44f17c..b90da1bb2 100644 --- a/packages/core/src/i18n/messages/he.ts +++ b/packages/core/src/i18n/messages/he.ts @@ -1096,4 +1096,12 @@ export const he: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/hi.ts b/packages/core/src/i18n/messages/hi.ts index b45a7b663..1f882a2fb 100644 --- a/packages/core/src/i18n/messages/hi.ts +++ b/packages/core/src/i18n/messages/hi.ts @@ -1108,4 +1108,12 @@ export const hi: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/hr.ts b/packages/core/src/i18n/messages/hr.ts index 368c8583d..b00f0b0a6 100644 --- a/packages/core/src/i18n/messages/hr.ts +++ b/packages/core/src/i18n/messages/hr.ts @@ -1103,4 +1103,12 @@ export const hr: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/hu.ts b/packages/core/src/i18n/messages/hu.ts index 1829b433c..a46d71b40 100644 --- a/packages/core/src/i18n/messages/hu.ts +++ b/packages/core/src/i18n/messages/hu.ts @@ -1021,4 +1021,12 @@ export const hu: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/id.ts b/packages/core/src/i18n/messages/id.ts index 389b0ff91..2a1bee90f 100644 --- a/packages/core/src/i18n/messages/id.ts +++ b/packages/core/src/i18n/messages/id.ts @@ -1056,4 +1056,12 @@ export const id: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/it.ts b/packages/core/src/i18n/messages/it.ts index faca13159..499dc5029 100644 --- a/packages/core/src/i18n/messages/it.ts +++ b/packages/core/src/i18n/messages/it.ts @@ -1079,4 +1079,12 @@ export const it: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/ja.ts b/packages/core/src/i18n/messages/ja.ts index 43876b317..5fe693ff4 100644 --- a/packages/core/src/i18n/messages/ja.ts +++ b/packages/core/src/i18n/messages/ja.ts @@ -1068,4 +1068,12 @@ export const ja: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/ko.ts b/packages/core/src/i18n/messages/ko.ts index 940b4ac20..96b9feeda 100644 --- a/packages/core/src/i18n/messages/ko.ts +++ b/packages/core/src/i18n/messages/ko.ts @@ -1068,4 +1068,12 @@ export const ko: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/ms.ts b/packages/core/src/i18n/messages/ms.ts index e0cb5cec8..2a577d42f 100644 --- a/packages/core/src/i18n/messages/ms.ts +++ b/packages/core/src/i18n/messages/ms.ts @@ -1107,4 +1107,12 @@ export const ms: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/nb.ts b/packages/core/src/i18n/messages/nb.ts index 93110dde0..0fe317bfa 100644 --- a/packages/core/src/i18n/messages/nb.ts +++ b/packages/core/src/i18n/messages/nb.ts @@ -1018,4 +1018,12 @@ export const nb: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/nl.ts b/packages/core/src/i18n/messages/nl.ts index 3ac7b8c55..d7719e3aa 100644 --- a/packages/core/src/i18n/messages/nl.ts +++ b/packages/core/src/i18n/messages/nl.ts @@ -1078,4 +1078,12 @@ export const nl: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/pl.ts b/packages/core/src/i18n/messages/pl.ts index bbcececbe..8bbcd82f4 100644 --- a/packages/core/src/i18n/messages/pl.ts +++ b/packages/core/src/i18n/messages/pl.ts @@ -1075,4 +1075,12 @@ export const pl: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/pt.ts b/packages/core/src/i18n/messages/pt.ts index 53cab83f2..b5278b058 100644 --- a/packages/core/src/i18n/messages/pt.ts +++ b/packages/core/src/i18n/messages/pt.ts @@ -1111,4 +1111,12 @@ export const pt: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/ro.ts b/packages/core/src/i18n/messages/ro.ts index 45fc9df9f..88ef2c87e 100644 --- a/packages/core/src/i18n/messages/ro.ts +++ b/packages/core/src/i18n/messages/ro.ts @@ -1057,4 +1057,12 @@ export const ro: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/sk.ts b/packages/core/src/i18n/messages/sk.ts index 18431fdc8..b09bf6ff5 100644 --- a/packages/core/src/i18n/messages/sk.ts +++ b/packages/core/src/i18n/messages/sk.ts @@ -1106,4 +1106,12 @@ export const sk: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/sv.ts b/packages/core/src/i18n/messages/sv.ts index 4f82a68cf..a6a00a94c 100644 --- a/packages/core/src/i18n/messages/sv.ts +++ b/packages/core/src/i18n/messages/sv.ts @@ -1068,4 +1068,12 @@ export const sv: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/th.ts b/packages/core/src/i18n/messages/th.ts index 5b4572922..348572520 100644 --- a/packages/core/src/i18n/messages/th.ts +++ b/packages/core/src/i18n/messages/th.ts @@ -1047,4 +1047,12 @@ export const th: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/tr.ts b/packages/core/src/i18n/messages/tr.ts index c7e0a3579..ae243d4f4 100644 --- a/packages/core/src/i18n/messages/tr.ts +++ b/packages/core/src/i18n/messages/tr.ts @@ -1076,4 +1076,12 @@ export const tr: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/uk.ts b/packages/core/src/i18n/messages/uk.ts index bee8cc0c0..17b6c7ebd 100644 --- a/packages/core/src/i18n/messages/uk.ts +++ b/packages/core/src/i18n/messages/uk.ts @@ -1057,4 +1057,12 @@ export const uk: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/vi.ts b/packages/core/src/i18n/messages/vi.ts index daed18894..22edf5dde 100644 --- a/packages/core/src/i18n/messages/vi.ts +++ b/packages/core/src/i18n/messages/vi.ts @@ -1056,4 +1056,12 @@ export const vi: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/zhHans.ts b/packages/core/src/i18n/messages/zhHans.ts index e7f8ba4f4..171ab4863 100644 --- a/packages/core/src/i18n/messages/zhHans.ts +++ b/packages/core/src/i18n/messages/zhHans.ts @@ -1059,4 +1059,12 @@ export const zhHans: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/messages/zhHant.ts b/packages/core/src/i18n/messages/zhHant.ts index 952ca1048..c12a37583 100644 --- a/packages/core/src/i18n/messages/zhHant.ts +++ b/packages/core/src/i18n/messages/zhHant.ts @@ -1059,4 +1059,12 @@ export const zhHant: MessageCatalog = { 'canvas.localMechanism.openChartAria': 'Open {column} details mini chart', 'canvas.localMechanism.openColumnAria': 'Open {column} details', 'canvas.localMechanism.quickActionTitle': '{column} quick action', + 'canvas.localMechanism.focusedInvestigation': 'Investigate', + 'canvas.localMechanism.charter': 'Charter', + 'canvas.localMechanism.sustainment': 'Sustain', + 'canvas.localMechanism.handoff': 'Handoff', + 'canvas.localMechanism.focusedInvestigationAria': 'Start focused investigation for {column}', + 'canvas.localMechanism.charterAria': 'Open improvement charter for {column}', + 'canvas.localMechanism.sustainmentAria': 'Open sustainment for {column}', + 'canvas.localMechanism.handoffAria': 'Open handoff for {column}', }; diff --git a/packages/core/src/i18n/types.ts b/packages/core/src/i18n/types.ts index c4cc9d412..2970a33d5 100644 --- a/packages/core/src/i18n/types.ts +++ b/packages/core/src/i18n/types.ts @@ -1194,4 +1194,12 @@ export interface MessageCatalog { 'canvas.localMechanism.openChartAria': string; 'canvas.localMechanism.openColumnAria': string; 'canvas.localMechanism.quickActionTitle': string; + 'canvas.localMechanism.focusedInvestigation': string; + 'canvas.localMechanism.charter': string; + 'canvas.localMechanism.sustainment': string; + 'canvas.localMechanism.handoff': string; + 'canvas.localMechanism.focusedInvestigationAria': string; + 'canvas.localMechanism.charterAria': string; + 'canvas.localMechanism.sustainmentAria': string; + 'canvas.localMechanism.handoffAria': string; } diff --git a/packages/ui/src/components/Canvas/index.tsx b/packages/ui/src/components/Canvas/index.tsx index fc8c176bc..aa80b7c4d 100644 --- a/packages/ui/src/components/Canvas/index.tsx +++ b/packages/ui/src/components/Canvas/index.tsx @@ -1022,6 +1022,10 @@ export const Canvas: React.FC = ({ onOpenInvestigationFocus={onOpenInvestigationFocus} onOpenColumnDetail={onOpenColumnDetail} onLogQuickAction={onLogQuickAction} + onFocusedInvestigation={onFocusedInvestigation} + onCharter={onCharter} + onSustainment={onSustainment} + onHandoff={onHandoff} /> ) : ( diff --git a/packages/ui/src/components/Canvas/internal/LocalMechanismView.tsx b/packages/ui/src/components/Canvas/internal/LocalMechanismView.tsx index 1d9efaa55..ec265db91 100644 --- a/packages/ui/src/components/Canvas/internal/LocalMechanismView.tsx +++ b/packages/ui/src/components/Canvas/internal/LocalMechanismView.tsx @@ -35,6 +35,10 @@ export interface LocalMechanismViewProps { onOpenInvestigationFocus?: (focus: { kind: 'question'; id: string; questionId: string }) => void; onOpenColumnDetail?: (column: string, stepId: string) => void; onLogQuickAction?: (stepId: string, payload: LogActionPayload) => void; + onFocusedInvestigation?: (stepId: string) => void; + onCharter?: (stepId: string) => void; + onSustainment?: (stepId: string) => void; + onHandoff?: (stepId: string) => void; } const EMPTY_ROWS: ReadonlyArray = []; @@ -177,6 +181,10 @@ function ColumnMiniChart({ locale, onOpenColumnDetail, onOpenQuickAction, + onFocusedInvestigation, + onCharter, + onSustainment, + onHandoff, }: { column: string; kind: string | undefined; @@ -185,6 +193,10 @@ function ColumnMiniChart({ locale: Locale; onOpenColumnDetail?: (column: string) => void; onOpenQuickAction: (column: string) => void; + onFocusedInvestigation?: (column: string) => void; + onCharter?: (column: string) => void; + onSustainment?: (column: string) => void; + onHandoff?: (column: string) => void; }) { const values = numericValues(rows, column); const categories = distribution(rows, column); @@ -212,6 +224,54 @@ function ColumnMiniChart({ {getMessage(locale, 'canvas.localMechanism.actionButton')} + {onFocusedInvestigation || onCharter || onSustainment || onHandoff ? ( +
    + {onFocusedInvestigation ? ( + + ) : null} + {onCharter ? ( + + ) : null} + {onSustainment ? ( + + ) : null} + {onHandoff ? ( + + ) : null} +
    + ) : null}