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); }); 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/docs/investigations.md b/docs/investigations.md index 82af2aa5f..256a672be 100644 --- a/docs/investigations.md +++ b/docs/investigations.md @@ -32,12 +32,14 @@ Code-level smells, UX follow-ups, and architectural questions surfaced during wo **Description:** 20 findings total — 5 HIGH that qualify the "shipped" claim, 8 MEDIUM spec-vs-shipped drift, 7 LOW cleanups. Followup workstream plan at [`docs/superpowers/plans/2026-05-13-canvas-viewport-8f-followups.md`](superpowers/plans/2026-05-13-canvas-viewport-8f-followups.md). Decision-log "8f canvas viewport SHIPPED" entry has been amended to reference these gaps. Roadmap continues to mark 8f shipped; the followups are a separate cleanup sequence. +**STATUS 2026-05-14:** 19 of 20 findings RESOLVED on branch `canvas-viewport-8f-followups` (22 commits, single PR pending). HIGH #4 resolved via spec AMEND (intentional V2 placeholders); HIGH #1/#2/#3/#5 resolved via implementation; all 8 MEDIUM resolved (including the spec §10 amend); 6 of 7 LOW resolved. Deferred: T1.1 brand `ProcessHubId` (LOW #19) — 18-file refactor, low value vs risk, moved to its own future micro-PR. This entry stays open until the followup PR merges; close on merge. + **HIGH (5):** - **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/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: 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 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/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/i18n/messages/ar.ts b/packages/core/src/i18n/messages/ar.ts index 7585fe267..14eaa9673 100644 --- a/packages/core/src/i18n/messages/ar.ts +++ b/packages/core/src/i18n/messages/ar.ts @@ -1023,4 +1023,86 @@ 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.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.', + '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', + '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 da5014b57..7759d171b 100644 --- a/packages/core/src/i18n/messages/bg.ts +++ b/packages/core/src/i18n/messages/bg.ts @@ -1032,4 +1032,86 @@ 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.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.', + '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', + '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 51d51eda0..9845e3323 100644 --- a/packages/core/src/i18n/messages/cs.ts +++ b/packages/core/src/i18n/messages/cs.ts @@ -943,4 +943,86 @@ 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.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.', + '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', + '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 64a5b8b01..615ad1d6c 100644 --- a/packages/core/src/i18n/messages/da.ts +++ b/packages/core/src/i18n/messages/da.ts @@ -996,4 +996,86 @@ 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.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.', + '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', + '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 f9310aff3..7b12f1a0b 100644 --- a/packages/core/src/i18n/messages/de.ts +++ b/packages/core/src/i18n/messages/de.ts @@ -1035,4 +1035,86 @@ 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.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.', + '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', + '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 c7312d9cf..3bee07888 100644 --- a/packages/core/src/i18n/messages/el.ts +++ b/packages/core/src/i18n/messages/el.ts @@ -1035,4 +1035,86 @@ 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.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.', + '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', + '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 ff64b6f2d..477ec13f7 100644 --- a/packages/core/src/i18n/messages/en.ts +++ b/packages/core/src/i18n/messages/en.ts @@ -1041,4 +1041,85 @@ 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.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.', + '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', + '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 5f4deb543..b13cc8725 100644 --- a/packages/core/src/i18n/messages/es.ts +++ b/packages/core/src/i18n/messages/es.ts @@ -1037,4 +1037,86 @@ 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.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.', + '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', + '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 5e5f03cd5..cd3f0adec 100644 --- a/packages/core/src/i18n/messages/fi.ts +++ b/packages/core/src/i18n/messages/fi.ts @@ -1035,4 +1035,86 @@ 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.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.', + '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', + '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 cd2677f01..17289cbbd 100644 --- a/packages/core/src/i18n/messages/fr.ts +++ b/packages/core/src/i18n/messages/fr.ts @@ -1041,4 +1041,86 @@ 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.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.', + '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', + '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 6e70bbd32..b90da1bb2 100644 --- a/packages/core/src/i18n/messages/he.ts +++ b/packages/core/src/i18n/messages/he.ts @@ -1022,4 +1022,86 @@ 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.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.', + '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', + '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 f356e83b9..1f882a2fb 100644 --- a/packages/core/src/i18n/messages/hi.ts +++ b/packages/core/src/i18n/messages/hi.ts @@ -1034,4 +1034,86 @@ 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.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.', + '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', + '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 4956dfe55..b00f0b0a6 100644 --- a/packages/core/src/i18n/messages/hr.ts +++ b/packages/core/src/i18n/messages/hr.ts @@ -1029,4 +1029,86 @@ 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.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.', + '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', + '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 0828e5a4e..a46d71b40 100644 --- a/packages/core/src/i18n/messages/hu.ts +++ b/packages/core/src/i18n/messages/hu.ts @@ -947,4 +947,86 @@ 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.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.', + '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', + '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 3e2ce5f0a..2a1bee90f 100644 --- a/packages/core/src/i18n/messages/id.ts +++ b/packages/core/src/i18n/messages/id.ts @@ -982,4 +982,86 @@ 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.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.', + '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', + '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 aec6483cb..499dc5029 100644 --- a/packages/core/src/i18n/messages/it.ts +++ b/packages/core/src/i18n/messages/it.ts @@ -1005,4 +1005,86 @@ 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.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.', + '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', + '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 0c06b9a05..5fe693ff4 100644 --- a/packages/core/src/i18n/messages/ja.ts +++ b/packages/core/src/i18n/messages/ja.ts @@ -994,4 +994,86 @@ 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.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.', + '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', + '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 1465a3b7d..96b9feeda 100644 --- a/packages/core/src/i18n/messages/ko.ts +++ b/packages/core/src/i18n/messages/ko.ts @@ -994,4 +994,86 @@ 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.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.', + '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', + '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 08134ee8c..2a577d42f 100644 --- a/packages/core/src/i18n/messages/ms.ts +++ b/packages/core/src/i18n/messages/ms.ts @@ -1033,4 +1033,86 @@ 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.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.', + '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', + '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 23be34d50..0fe317bfa 100644 --- a/packages/core/src/i18n/messages/nb.ts +++ b/packages/core/src/i18n/messages/nb.ts @@ -944,4 +944,86 @@ 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.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.', + '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', + '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 6f6e7c7b1..d7719e3aa 100644 --- a/packages/core/src/i18n/messages/nl.ts +++ b/packages/core/src/i18n/messages/nl.ts @@ -1004,4 +1004,86 @@ 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.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.', + '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', + '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 6b7b625b4..8bbcd82f4 100644 --- a/packages/core/src/i18n/messages/pl.ts +++ b/packages/core/src/i18n/messages/pl.ts @@ -1001,4 +1001,86 @@ 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.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.', + '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', + '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 40e090761..b5278b058 100644 --- a/packages/core/src/i18n/messages/pt.ts +++ b/packages/core/src/i18n/messages/pt.ts @@ -1037,4 +1037,86 @@ 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.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.', + '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', + '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 e2cc2a8d5..88ef2c87e 100644 --- a/packages/core/src/i18n/messages/ro.ts +++ b/packages/core/src/i18n/messages/ro.ts @@ -983,4 +983,86 @@ 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.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.', + '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', + '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 42603d47c..b09bf6ff5 100644 --- a/packages/core/src/i18n/messages/sk.ts +++ b/packages/core/src/i18n/messages/sk.ts @@ -1032,4 +1032,86 @@ 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.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.', + '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', + '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 cb4fb79d6..a6a00a94c 100644 --- a/packages/core/src/i18n/messages/sv.ts +++ b/packages/core/src/i18n/messages/sv.ts @@ -994,4 +994,86 @@ 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.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.', + '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', + '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 fb3e715c1..348572520 100644 --- a/packages/core/src/i18n/messages/th.ts +++ b/packages/core/src/i18n/messages/th.ts @@ -973,4 +973,86 @@ 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.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.', + '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', + '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 e589a60f8..ae243d4f4 100644 --- a/packages/core/src/i18n/messages/tr.ts +++ b/packages/core/src/i18n/messages/tr.ts @@ -1002,4 +1002,86 @@ 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.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.', + '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', + '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 1566d39a2..17b6c7ebd 100644 --- a/packages/core/src/i18n/messages/uk.ts +++ b/packages/core/src/i18n/messages/uk.ts @@ -983,4 +983,86 @@ 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.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.', + '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', + '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 d58d496a9..22edf5dde 100644 --- a/packages/core/src/i18n/messages/vi.ts +++ b/packages/core/src/i18n/messages/vi.ts @@ -982,4 +982,86 @@ 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.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.', + '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', + '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 cde7431b1..171ab4863 100644 --- a/packages/core/src/i18n/messages/zhHans.ts +++ b/packages/core/src/i18n/messages/zhHans.ts @@ -985,4 +985,86 @@ 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.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.', + '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', + '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 2bf7cefca..c12a37583 100644 --- a/packages/core/src/i18n/messages/zhHant.ts +++ b/packages/core/src/i18n/messages/zhHant.ts @@ -985,4 +985,86 @@ 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.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.', + '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', + '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 e623bb09b..2970a33d5 100644 --- a/packages/core/src/i18n/types.ts +++ b/packages/core/src/i18n/types.ts @@ -1123,4 +1123,83 @@ 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.lensPicker.invalidAtLevel': 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; + '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/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/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/__tests__/useCanvasViewportInput.test.ts b/packages/hooks/src/__tests__/useCanvasViewportInput.test.ts index 12495bb57..7fdc202b0 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 }; @@ -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(); @@ -143,6 +162,28 @@ describe('useCanvasViewportInput', () => { expect(element.__on?.some(listener => listener.name === 'zoom')).not.toBe(true); }); + it('does not call syncElementToStoreViewport when an unrelated store mutation fires', () => { + // setRailOpen mutates railOpen — the viewports[hubId] reference is unchanged. + // The subscribe handler should short-circuit and leave d3 element transform untouched. + const { element } = renderCanvasViewportInput(); + + // Capture current d3 transform state. + const zoomBefore = { ...element.__zoom }; + + act(() => { + useCanvasViewportStore.getState().setRailOpen(false); + }); + + // d3 element's __zoom must remain unchanged — no unnecessary sync work fired. + expect(element.__zoom).toMatchObject(zoomBefore); + }); + + 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(); @@ -165,3 +206,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 b8e084682..ac931d1ef 100644 --- a/packages/hooks/src/useCanvasViewportInput.ts +++ b/packages/hooks/src/useCanvasViewportInput.ts @@ -1,7 +1,29 @@ 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'; + +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]; @@ -62,6 +84,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; @@ -73,13 +96,40 @@ 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); syncElementToStoreViewport(); - const unsubscribe = useCanvasViewportStore.subscribe(() => { + // Subscribe to the full store but short-circuit on reference equality of the + // hub's viewport slice — avoids running syncElementToStoreViewport on every + // unrelated mutation (e.g. setRailOpen, setViewMode, openChartCluster). + let prevViewportRef = useCanvasViewportStore.getState().viewports[hubId]; + const unsubscribe = useCanvasViewportStore.subscribe(state => { if (syncingFromD3Ref.current) return; + const nextViewport = state.viewports[hubId]; + if (nextViewport === prevViewportRef) return; + prevViewportRef = nextViewport; syncElementToStoreViewport(); }); diff --git a/packages/stores/CLAUDE.md b/packages/stores/CLAUDE.md index a01b38ac2..f641b3de5 100644 --- a/packages/stores/CLAUDE.md +++ b/packages/stores/CLAUDE.md @@ -2,14 +2,14 @@ 6 Zustand stores across 3 layers (ADR-078 + F4, 2026-05-07): -| Layer | Store | Persistence | -| --------------- | ------------------------ | ----------------------------------------------------------- | -| Document (×3) | `useProjectStore` | consumer-side serialisation via `useProjectActions` | -| Document | `useInvestigationStore` | session-only today; future `HubRepository.dispatch` | -| Document | `useCanvasStore` | `dispatch(CanvasAction)` + hub repository | -| Annotation hub | `useCanvasViewportStore` | Dexie DB `variscout-canvas-viewport` (R12 ESLint exception) | -| Annotation user | `usePreferencesStore` | idb-keyval, key `'variscout-preferences'` | -| View | `useViewStore` | NONE — transient | +| Layer | Store | Persistence | +| --------------- | ------------------------ | --------------------------------------------------------------------------------------------- | +| Document (×3) | `useProjectStore` | consumer-side serialisation via `useProjectActions` | +| Document | `useInvestigationStore` | session-only today; future `HubRepository.dispatch` | +| Document | `useCanvasStore` | `dispatch(CanvasAction)` + hub repository | +| Annotation hub | `useCanvasViewportStore` | Dexie DB `variscout-canvas-viewport` (R12 ESLint exception, STORE_LAYER='annotation-per-hub') | +| Annotation user | `usePreferencesStore` | idb-keyval, key `'variscout-preferences'` | +| View | `useViewStore` | NONE — transient | **Boundary rule (portability test):** another analyst importing this hub needs it? Yes → Document. Survives reload but not portable → Annotation. Doesn't survive reload → View. `__tests__/layerBoundary.test.ts` enforces middleware presence/absence. diff --git a/packages/stores/src/__tests__/canvasViewportStore.test.ts b/packages/stores/src/__tests__/canvasViewportStore.test.ts index 8eb8e7930..5f65e215d 100644 --- a/packages/stores/src/__tests__/canvasViewportStore.test.ts +++ b/packages/stores/src/__tests__/canvasViewportStore.test.ts @@ -1,10 +1,11 @@ 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, rehydrateCanvasViewport, + deleteLegacyWallLayoutDb, } from '../canvasViewportStore'; describe('canvasViewportStore', () => { @@ -121,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'); @@ -130,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({ @@ -203,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', () => { @@ -295,6 +311,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 +326,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 +353,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/__tests__/layerBoundary.test.ts b/packages/stores/src/__tests__/layerBoundary.test.ts index 50b5ba17b..9353f568e 100644 --- a/packages/stores/src/__tests__/layerBoundary.test.ts +++ b/packages/stores/src/__tests__/layerBoundary.test.ts @@ -8,11 +8,11 @@ * * STORE_LAYER enum has 6 values; today 4 are realised in code: * - 'document' (projectStore, investigationStore, canvasStore) - * - 'annotation-per-project' (canvasViewportStore) + * - 'annotation-per-hub' (canvasViewportStore) * - 'annotation-per-user' (preferencesStore) * - 'view' (viewStore) * Reserved for future use (no test coverage today, intentional): - * - 'annotation-per-hub' + * - 'annotation-per-project' * - 'annotation-per-investigation' */ import { describe, it, expect } from 'vitest'; @@ -109,10 +109,10 @@ describe('layer boundary', () => { }); }); - it('canvasViewportStore is the only annotation-per-project store and uses Dexie', () => { - const annotationPerProject = files.filter(f => f.layer === 'annotation-per-project'); - expect(annotationPerProject).toHaveLength(1); - expect(annotationPerProject[0].filename).toBe('canvasViewportStore.ts'); - expect(annotationPerProject[0].source).toMatch(/from\s+['"]dexie['"]/); + it('canvasViewportStore is the only annotation-per-hub store and uses Dexie', () => { + const annotationPerHub = files.filter(f => f.layer === 'annotation-per-hub'); + expect(annotationPerHub).toHaveLength(1); + expect(annotationPerHub[0].filename).toBe('canvasViewportStore.ts'); + expect(annotationPerHub[0].source).toMatch(/from\s+['"]dexie['"]/); }); }); diff --git a/packages/stores/src/canvasViewportStore.ts b/packages/stores/src/canvasViewportStore.ts index c5b704263..ecb4f394d 100644 --- a/packages/stores/src/canvasViewportStore.ts +++ b/packages/stores/src/canvasViewportStore.ts @@ -8,23 +8,17 @@ // 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'; -export const STORE_LAYER = 'annotation-per-project' as const; +export const STORE_LAYER = 'annotation-per-hub' as const; 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; @@ -117,7 +111,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 +191,10 @@ export const useCanvasViewportStore = create { + 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({ @@ -335,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 { 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; diff --git a/packages/ui/src/components/Canvas/index.tsx b/packages/ui/src/components/Canvas/index.tsx index e463e78b7..aa80b7c4d 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 ); @@ -981,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} /> @@ -1017,6 +1022,10 @@ export const Canvas: React.FC = ({ onOpenInvestigationFocus={onOpenInvestigationFocus} onOpenColumnDetail={onOpenColumnDetail} onLogQuickAction={onLogQuickAction} + onFocusedInvestigation={onFocusedInvestigation} + onCharter={onCharter} + onSustainment={onSustainment} + onHandoff={onHandoff} /> ) : ( @@ -1039,8 +1048,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/AuthorL3View.tsx b/packages/ui/src/components/Canvas/internal/AuthorL3View.tsx index f8823a1b3..d5c09d3c6 100644 --- a/packages/ui/src/components/Canvas/internal/AuthorL3View.tsx +++ b/packages/ui/src/components/Canvas/internal/AuthorL3View.tsx @@ -1,8 +1,11 @@ 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'; export interface AuthorL3ViewProps { hubId: string; @@ -15,25 +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); - 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 || 'Selected step', - ctqColumn, - tributaryColumns, - }; -} - function ColumnPill({ column }: { column: string }) { return (
  • @@ -52,16 +36,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(() => getStepColumnAssignments(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 +85,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 +148,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 +180,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..fd3dd7afb 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, ]; +export 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/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/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/LocalMechanismView.tsx b/packages/ui/src/components/Canvas/internal/LocalMechanismView.tsx index 95913eb9a..ec265db91 100644 --- a/packages/ui/src/components/Canvas/internal/LocalMechanismView.tsx +++ b/packages/ui/src/components/Canvas/internal/LocalMechanismView.tsx @@ -1,12 +1,14 @@ import React from 'react'; import type { DataRow, Finding, Hypothesis, ProcessMap } from '@variscout/core'; +import { getStepColumnAssignments } from '@variscout/core/frame'; import { calculateAnova, computeBestSubsets, 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 +16,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 { @@ -32,25 +35,23 @@ 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 = []; +/** + * 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[] { @@ -177,15 +178,25 @@ function ColumnMiniChart({ kind, rows, outcomeColumn, + locale, onOpenColumnDetail, onOpenQuickAction, + onFocusedInvestigation, + onCharter, + onSustainment, + onHandoff, }: { column: string; kind: string | undefined; rows: ReadonlyArray; outcomeColumn: string | null | undefined; + 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); @@ -199,7 +210,7 @@ function ColumnMiniChart({
    + {onFocusedInvestigation || onCharter || onSustainment || onHandoff ? ( +
    + {onFocusedInvestigation ? ( + + ) : null} + {onCharter ? ( + + ) : null} + {onSustainment ? ( + + ) : null} + {onHandoff ? ( + + ) : null} +
    + ) : null} @@ -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..2b912fd32 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; @@ -22,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; } @@ -58,21 +76,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; @@ -86,14 +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, @@ -106,6 +135,7 @@ export const SystemLevelView: React.FC = ({ outcomeLabel, outOfSpecPercentage: model.outOfSpecPercentage, drift: model.drift.label, + locale, }); return ( @@ -116,7 +146,11 @@ export const SystemLevelView: React.FC = ({

    {hubId}

    {outcomeLabel}

    - {activeLens ?

    Lens: {activeLens}

    : null} + {activeLens ? ( +

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

    + ) : null}
    @@ -134,7 +168,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 +187,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,15 +256,17 @@ 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)} + {formatMetric(resolvedSpecLimits?.cpkTarget)}
    @@ -234,8 +278,12 @@ export const SystemLevelView: React.FC = ({ undefined} /> ) : (
    -

    Inbox

    -

    No outcome prompts

    +

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

    +

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

    )} @@ -244,7 +292,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}

    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); + } + } + }); +}); 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(); + }); }); diff --git a/packages/ui/src/components/Canvas/internal/__tests__/LocalMechanismView.test.tsx b/packages/ui/src/components/Canvas/internal/__tests__/LocalMechanismView.test.tsx index 29ca8f8c0..0570c82d6 100644 --- a/packages/ui/src/components/Canvas/internal/__tests__/LocalMechanismView.test.tsx +++ b/packages/ui/src/components/Canvas/internal/__tests__/LocalMechanismView.test.tsx @@ -268,6 +268,38 @@ describe('LocalMechanismView', () => { expect(onOpenColumnDetail).toHaveBeenCalledWith('Machine', 'mix'); }); + it('renders 4 additional response-path CTA buttons per column when callbacks provided', () => { + const onFocusedInvestigation = vi.fn(); + const onCharter = vi.fn(); + const onSustainment = vi.fn(); + const onHandoff = vi.fn(); + renderView({ onFocusedInvestigation, onCharter, onSustainment, onHandoff }); + + const ctaGroups = screen.getAllByTestId('response-path-ctas'); + // 4 columns for the 'mix' step — each column card gets the CTA row + expect(ctaGroups).toHaveLength(4); + + // Use within first CTA group to target a single column's buttons. + const firstGroup = ctaGroups[0]; + + fireEvent.click(within(firstGroup).getByText('Investigate')); + expect(onFocusedInvestigation).toHaveBeenCalledWith('mix'); + + fireEvent.click(within(firstGroup).getByText('Charter')); + expect(onCharter).toHaveBeenCalledWith('mix'); + + fireEvent.click(within(firstGroup).getByText('Sustain')); + expect(onSustainment).toHaveBeenCalledWith('mix'); + + fireEvent.click(within(firstGroup).getByText('Handoff')); + expect(onHandoff).toHaveBeenCalledWith('mix'); + }); + + it('does not render the response-path CTA row when no callbacks are provided', () => { + renderView(); + expect(screen.queryAllByTestId('response-path-ctas')).toHaveLength(0); + }); + it('submits quick action payload with focal step and column context', () => { const onLogQuickAction = vi.fn(); renderView({ onLogQuickAction }); diff --git a/packages/ui/src/components/Canvas/internal/__tests__/MobileLevelPicker.test.tsx b/packages/ui/src/components/Canvas/internal/__tests__/MobileLevelPicker.test.tsx index e46b7ed96..58fe9d34b 100644 --- a/packages/ui/src/components/Canvas/internal/__tests__/MobileLevelPicker.test.tsx +++ b/packages/ui/src/components/Canvas/internal/__tests__/MobileLevelPicker.test.tsx @@ -34,7 +34,7 @@ describe('MobileLevelPicker', () => { }); }); - it('enters placeholder l3 without a focal step so Canvas can choose the first step', () => { + it('navigates directly to l3 without focal step so canvas renders step-list (spec §7)', () => { render(); const stepButton = screen.getByRole('button', { name: 'Step' }); @@ -45,6 +45,7 @@ describe('MobileLevelPicker', () => { currentLevel: 'l3', zoom: 2.5, }); + // No focalStepId — canvas NoFocalStepPrompt step-list will render at l3. expect(useCanvasViewportStore.getState().getViewport('hub-mobile').focalStepId).toBeUndefined(); }); 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/); + }); + }); }); 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; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82c462641..30fed5811 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -504,6 +504,9 @@ importers: '@dnd-kit/core': specifier: ^6.3.1 version: 6.3.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@types/d3-transition': + specifier: ^3.0.9 + version: 3.0.9 '@variscout/core': specifier: workspace:* version: link:../core @@ -522,6 +525,9 @@ importers: d3-selection: specifier: ^3.0.0 version: 3.0.0 + d3-transition: + specifier: ^3.0.1 + version: 3.0.1(d3-selection@3.0.0) d3-zoom: specifier: ^3.0.0 version: 3.0.0 @@ -8288,6 +8294,7 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@9.0.1: @@ -14378,7 +14385,7 @@ snapshots: strict-event-emitter-types: 2.0.0 undici: 7.24.0 uri-templates: 0.2.0 - xsschema: 0.4.4(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6) + xsschema: 0.4.4(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@4.3.6) yargs: 18.0.0 zod: 4.3.6 zod-to-json-schema: 3.25.2(zod@4.3.6) @@ -18683,7 +18690,7 @@ snapshots: xmlchars@2.2.0: {} - xsschema@0.4.4(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6): + xsschema@0.4.4(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@4.3.6): optionalDependencies: zod: 4.3.6 zod-to-json-schema: 3.25.2(zod@4.3.6)