Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

This inventory identifies the third-party npm packages present in the locked Lumora dependency graph. It is not a replacement for the license text shipped by each package. Release artifacts must retain any license or notice files required by those packages.

Locked package identities: **419**
Locked package identities: **420**

| Package | Version | License | Scope |
| --- | --- | --- | --- |
Expand Down Expand Up @@ -315,6 +315,7 @@ Locked package identities: **419**
| loupe | 3.2.1 | MIT | development |
| lru-cache | 10.4.3 | ISC | development |
| lru-cache | 5.1.1 | ISC | development |
| lucide-react | 1.37.0 | ISC | runtime |
| lz-string | 1.5.0 | MIT | development |
| maath | 0.10.8 | MIT | runtime |
| magic-string | 0.30.21 | MIT | development |
Expand Down
67 changes: 67 additions & 0 deletions docs/evidence/tml-563/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# TML-563 UI/UX Review Evidence

The `before` images are the original reviewer's `HOLD` matrix. The `after` images are generated from this branch by:

```sh
npx playwright test e2e/tml-563-screenshots.spec.ts --project=chromium
```

Each after image uses a fresh browser context so project persistence and runtime locks cannot leak between captures.

States 14 and 15 were regenerated after the final review gates. State 14 captures the modal log drawer with its 44px close target and focus boundary; state 15 captures Fit with zero horizontal scroll and fully reachable 44px short-shot targets. Their inert, Escape/focus-restoration, edge-hit, and geometry assertions are enforced by `e2e/ui-ux-regression.spec.ts`.

| ID | State and viewport | Before | After |
| --- | --- | --- | --- |
| 01 | Editor, selected cube, 1440x900 | [Before](before/01-desktop-editor-1440x900.png) | [After](after/01-desktop-editor-1440x900.png) |
| 01a | Narrow embedded editor, selected cube, 1024x768 | [Before](before/01a-desktop-editor-1024x768.png) | [After](after/01a-desktop-editor-1024x768.png) |
| 02 | Plugin manager, 1440x900 | [Before](before/02-desktop-plugin-manager-1440x900.png) | [After](after/02-desktop-plugin-manager-1440x900.png) |
| 03 | Command palette, 1440x900 | [Before](before/03-desktop-command-palette-1440x900.png) | [After](after/03-desktop-command-palette-1440x900.png) |
| 04 | Storyboard, 1440x900 | [Before](before/04-desktop-storyboard-1440x900.png) | [After](after/04-desktop-storyboard-1440x900.png) |
| 05 | Export workspace, 1440x900 | [Before](before/05-desktop-export-1440x900.png) | [After](after/05-desktop-export-1440x900.png) |
| 06a | Mobile editor before selection, 375x667 | [Before](before/06a-mobile-editor-before-selection-375x667.png) | [After](after/06a-mobile-editor-before-selection-375x667.png) |
| 06 | Mobile editor after selection, 375x667 | [Before](before/06-mobile-editor-375x667.png) | [After](after/06-mobile-editor-375x667.png) |
| 07 | Mobile plugin manager, 375x667 | [Before](before/07-mobile-plugin-manager-375x667.png) | [After](after/07-mobile-plugin-manager-375x667.png) |
| 08 | Mobile storyboard, 375x667 | [Before](before/08-mobile-storyboard-375x667.png) | [After](after/08-mobile-storyboard-375x667.png) |
| 09 | Mobile landscape editor, 667x375 | [Before](before/09-mobile-landscape-editor-667x375.png) | [After](after/09-mobile-landscape-editor-667x375.png) |
| 10 | Mobile landscape export, 667x375 | [Before](before/10-mobile-landscape-export-667x375.png) | [After](after/10-mobile-landscape-export-667x375.png) |
| 11 | Host sibling isolation, plugin modal, 1440x900 | [Before](before/11-portal-host-isolation-1440x900.png) | [After](after/11-portal-host-isolation-1440x900.png) |
| 12 | ShadowRoot opener focus restored, 1440x900 | [Before](before/12-shadow-root-focus-restore-1440x900.png) | [After](after/12-shadow-root-focus-restore-1440x900.png) |
| 13a | 900px Studio boundary (1240x768 host) | [Before](before/13-responsive-boundary-1240x768.png) | [After](after/13-responsive-boundary-1240x768.png) |
| 13b | 901px Studio boundary (1241x768 host) | [Before](before/13-responsive-boundary-1241x768.png) | [After](after/13-responsive-boundary-1241x768.png) |
| 13c | 1100px Studio boundary (1440x768 host) | [Before](before/13-responsive-boundary-1440x768.png) | [After](after/13-responsive-boundary-1440x768.png) |
| 13d | 1101px Studio boundary (1441x768 host) | [Before](before/13-responsive-boundary-1441x768.png) | [After](after/13-responsive-boundary-1441x768.png) |
| 14 | Mobile landscape with host log expanded, 667x375 | [Before](before/14-mobile-landscape-log-open-667x375.png) | [After](after/14-mobile-landscape-log-open-667x375.png) |
| 15 | Mobile fit zoom with 0.1s shot controls, 375x667 | [Before](before/15-mobile-fit-shot-controls-375x667.png) | [After](after/15-mobile-fit-shot-controls-375x667.png) |
| 16 | Mobile keyframe target lanes, 375x667 | [Before](before/16-mobile-keyframe-overlap-375x667.png) | [After](after/16-mobile-keyframe-overlap-375x667.png) |
| 17 | Mobile storyboard close/delete controls, 375x667 | [Before](before/17-mobile-storyboard-controls-375x667.png) | [After](after/17-mobile-storyboard-controls-375x667.png) |
| 18 | Plugin transition focus, 1440x900 | [Before](before/18-plugin-transition-focus-1440x900.png) | [After](after/18-plugin-transition-focus-1440x900.png) |
| 19 | Two-Studio top-modal stack after Escape, 1440x900 | [Before](before/19-multiple-modal-stack-1440x900.png) | [After](after/19-multiple-modal-stack-1440x900.png) |

The focused regression suite also checks WCAG A/AA violations, document-level host isolation, deep ShadowRoot focus restoration, two-Studio modal ordering, stable plugin-toggle focus, 900/901 and 1100/1101 container geometry, mobile shot/keyframe hitbox geometry, expanded landscape logs, and nonblank desktop/mobile WebGL canvas pixels.

## Round 3 hard-gate evidence

The reviewer's annotated `round3-before` images are preserved unchanged. The matching `round3-after` images are generated from the corrected branch by:

```sh
npx playwright test e2e/tml-563-screenshots.spec.ts --project=chromium
```

The capture test asserts the measured failure condition before writing each image: modal count and unique accessible IDs, Fit canvas containment, 44px shot actions, disjoint 60fps keyframe targets with correct center ownership, document-level inert/focus, mixed-modal Escape order, single-row toolbar geometry, and portrait timeline containment.

| Failure state and viewport | Before | After |
| --- | --- | --- |
| Background Studio dispatches `Ctrl+K`, 1440x900 | [Before](round3-before/hold-dual-ctrlk-scope.png) | [After](round3-after/hold-dual-ctrlk-scope.png) |
| Two command palettes expose duplicate IDs, 1440x900 | [Before](round3-before/hold-dual-palette-ids.png) | [After](round3-after/hold-dual-palette-ids.png) |
| Fit loses the full duration after a 0.1s shot, 375x667 | [Before](round3-before/hold-fit-overview-loss-375x667.png) | [After](round3-after/hold-fit-overview-loss-375x667.png) |
| One zoom-out clips selected-shot actions, 375x667 | [Before](round3-before/hold-shot-actions-clipped-375x667.png) | [After](round3-after/hold-shot-actions-clipped-375x667.png) |
| Adjacent 60fps keyframes overlap, 375x667 | [Before](round3-before/tml-563-keyframes-60fps-overlap-375x667.png) | [After](round3-after/tml-563-keyframes-60fps-overlap-375x667.png) |
| Overwrite confirmation allows host-sibling focus, 1440x900 | [Before](round3-before/hold-overwrite-host-focus.png) | [After](round3-after/hold-overwrite-host-focus.png) |
| Mixed modal Escape closes the lower dialog first, 1440x900 | [Before](round3-before/hold-mixed-modal-escape-order.png) | [After](round3-after/hold-mixed-modal-escape-order.png) |
| Studio 1240 toolbar wraps at host 1580x768 | [Before](round3-before/toolbar-studio-1240-host-1580x768.png) | [After](round3-after/toolbar-studio-1240-host-1580x768.png) |
| Studio 1241 toolbar jumps at host 1581x768 | [Before](round3-before/toolbar-studio-1241-host-1581x768.png) | [After](round3-after/toolbar-studio-1241-host-1581x768.png) |
| Expanded host log clips the timeline, 375x667 | [Before](round3-before/portrait-log-open-375x667.png) | [After](round3-after/portrait-log-open-375x667.png) |

The related Studio `1080/1081` boundary is captured at [1420x768](round3-after/toolbar-studio-1080-host-1420x768.png) and [1421x768](round3-after/toolbar-studio-1081-host-1421x768.png); both remain in the same compact single-row mode.

An additional Chromium regression loads 60 consecutive 60fps keyframes and checks both the default zoom and the minimum zoom. Dense groups stay within two 44px rows, keep the track label and shot lane visible, and expose every grouped frame through repeated activation of the cluster target.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
101 changes: 101 additions & 0 deletions docs/plans/2026-08-31-tml-563-ui-ux-hold-remediation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# TML-563 UI/UX HOLD Remediation Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use `executing-plans` to implement this plan task-by-task.

**Goal:** Resolve UI/UX review findings 1-9 from `main@51863fb`, add regression coverage, and provide the reviewer's desktop/mobile screenshot matrix for re-review.

**Architecture:** Introduce one portal-based modal primitive that owns focus trapping, Escape handling, opener restoration, and application isolation. Make the Studio root the responsive container, switch narrow embeds to explicit scene/object/properties tabs, keep primary actions in one row with an accessible overflow menu, and collapse the demo host log on small screens. Preserve editing behavior while replacing inaccessible timeline/tree interactions with named native controls, consistent Lucide icons, semantic disabled colors, and shared plugin visual tokens.

**Tech Stack:** React 19, TypeScript, CSS container queries, Lucide React, Vitest/Testing Library, Playwright, axe-core.

---

### Task 1: Modal focus boundary

**Files:**
- Create: `packages/studio/src/components/ModalDialog.tsx`
- Create: `packages/studio/test/modal-dialog.test.tsx`
- Modify: `packages/studio/src/components/PluginManager.tsx`
- Modify: `packages/studio/src/components/CommandPalette.tsx`
- Modify: `packages/studio/src/components/LumoraStudio.tsx`
- Test: `e2e/ui-ux-regression.spec.ts`

1. Write tests for `aria-modal`, portal placement, initial focus, Tab/Shift+Tab wrapping, Escape, outside-key isolation, and opener restoration.
2. Run the focused tests and confirm they fail on the existing dialogs.
3. Implement `ModalDialog`, render plugin/command dialogs through it, and set the Studio application root inert while either dialog is open.
4. Add a persistent command-search label and rerun the focused tests until green.

### Task 2: Container-responsive editor and compact host

**Files:**
- Modify: `packages/studio/src/components/LumoraStudio.tsx`
- Modify: `packages/studio/src/components/Toolbar.tsx`
- Modify: `packages/studio/src/lumora.css`
- Modify: `examples/embedded-host/src/App.tsx`
- Modify: `examples/embedded-host/src/app.css`
- Test: `e2e/ui-ux-regression.spec.ts`

1. Add E2E assertions for 900/760/600/560/480px Studio widths, stable scene height, no horizontal overflow, one-row primary actions, panel tab switching, and collapsed mobile host log.
2. Run the E2E file and confirm the current viewport-based layout fails.
3. Make `.lumora-studio` the inline-size container, replace editor `@media` layout rules with `@container`, and add scene/object/properties tabs below 900px.
4. Move secondary toolbar actions into an accessible narrow-container overflow menu and make the host log a user-controlled drawer below 900px.
5. Use `100dvh` and stable tracks so 375x667 and 667x375 preserve a usable scene viewport.

### Task 3: Accessible names and keyboard timeline

**Files:**
- Modify: `packages/studio/test/editor-components.test.tsx`
- Modify: `packages/studio/test/timeline-panel.test.tsx`
- Modify: `packages/studio/src/components/editor/ObjectTree.tsx`
- Modify: `packages/studio/src/components/editor/EditorViewport.tsx`
- Modify: `packages/studio/src/components/editor/PropertiesPanel.tsx`
- Modify: `packages/studio/src/components/editor/TimelinePanel.tsx`
- Test: `e2e/accessibility.spec.ts`

1. Add failing accessible-name assertions for the scene selector, view selector, object-name input, and command search.
2. Add failing keyboard tests for ruler Arrow/Page/Home/End seeking, track selection, and shot seeking.
3. Add labels, expose the ruler as a slider, and use separate native buttons for track/shot actions without nested interactive controls.
4. Expand editor-state axe scans so the main editor and dialogs are checked instead of only the inert export workspace.

### Task 4: Targets, contrast, and icon language

**Files:**
- Modify: `packages/studio/package.json`
- Modify: `package-lock.json`
- Modify: `packages/studio/src/components/editor/ObjectTree.tsx`
- Modify: `packages/studio/src/components/editor/TimelinePanel.tsx`
- Modify: `packages/studio/src/components/storyboard/StoryboardWorkspace.tsx`
- Modify: `packages/studio/src/components/PluginManager.tsx`
- Modify: `packages/studio/src/components/Toolbar.tsx`
- Modify: `packages/studio/src/lumora.css`
- Test: `packages/studio/test/editor-components.test.tsx`
- Test: `packages/studio/test/timeline-panel.test.tsx`

1. Add failing assertions for dynamic action names, pressed states, and icon-only control labels.
2. Add `lucide-react`, replace Unicode glyphs and single-character action abbreviations, and hide icons from the accessibility tree.
3. Keep visual keyframe dots at 8px while expanding hit boxes to 24px desktop and 44px touch; give tree and shot actions equivalent stable targets.
4. Remove whole-row disabled opacity, use readable state tokens, and keep tree actions visible without hover.

### Task 5: Mock plugin visual baseline

**Files:**
- Create: `examples/mock-plugin/src/style.css`
- Modify: `examples/mock-plugin/src/index.tsx`
- Modify: `examples/mock-plugin/src/panels/MockConsolePanel.tsx`
- Modify: `examples/mock-plugin/src/panels/MockAiChatPanel.tsx`
- Test: `e2e/ui-ux-regression.spec.ts`

1. Add a failing browser assertion that Mock controls inherit the Studio surface, text, border, focus, and disabled tokens.
2. Add plugin-owned semantic styles for headings, actions, inputs, results, empty states, and log rows.
3. Rerun the visual and accessibility scenarios until green.

### Task 6: Verification and evidence

**Files:**
- Modify: `e2e/ui-ux-regression.spec.ts`
- Generate: `test-results/tml-563-after/*.png`

1. Capture the editor at 1440x900 and 1024x768; plugin manager, command palette, storyboard, and export at 1440x900; editor/plugin/storyboard at 375x667; and editor/export at 667x375.
2. Run focused Vitest and Chromium E2E, then `npm test`, `npm run typecheck`, `npm run lint`, `npm run build`, and `git diff --check`.
3. Compare screenshots against the review attachments and inspect the critical desktop/mobile images directly.
4. Commit, push an independent `TML-563` branch, create a linked PR without merging, and report the PR, tests, screenshots, and residual limits to the project manager.
15 changes: 12 additions & 3 deletions e2e/accessibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,26 @@ import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';
import type { Page } from '@playwright/test';

async function clickToolbarItem(page: Page, testId: string) {
const item = page.getByTestId(testId);
if (!(await item.isVisible())) {
await page.getByTestId('toolbar-more').click();
await expect(item).toBeVisible();
}
await item.click();
}

async function openSampleExport(page: Page) {
await page.goto('/');
await page.getByTestId('open-sample-project').click();
await expect(page.getByTestId('tree-row-sample-cube')).toBeVisible();
await clickToolbarItem(page, 'open-sample-project');
await expect(page.getByTestId('open-export-workspace')).toBeEnabled();
await page.getByTestId('open-export-workspace').click();
await expect(page.getByTestId('export-workspace')).toBeVisible();
}

test('host event log preserves native PageDown and Space scrolling without toggling Studio playback', async ({ page }) => {
await page.goto('/');
await page.getByTestId('open-sample-project').click();
await clickToolbarItem(page, 'open-sample-project');
await expect(page.getByTestId('tree-row-sample-cube')).toBeVisible();

const log = page.getByTestId('host-event-log');
Expand Down
24 changes: 17 additions & 7 deletions e2e/editor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,15 @@ async function countGizmoPixels(page: Page): Promise<number> {

test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.getByTestId('open-sample-project').click();
const openSample = page.getByTestId('open-sample-project');
if (!(await openSample.isVisible())) {
await page.getByTestId('toolbar-more').click();
await expect(openSample).toBeVisible();
}
await openSample.click();
if (!(await page.getByTestId('tree-row-sample-group').isVisible())) {
await page.getByTestId('editor-panel-objects').click();
}
await expect(page.getByTestId('tree-row-sample-group')).toBeVisible();
});

Expand Down Expand Up @@ -342,13 +350,13 @@ test('M3 树行键盘动作:V/L 切换可见/锁定,Delete 全局快捷键
await row('sample-cube').click();
await row('sample-cube').focus();

// L:锁定(按钮文本翻转为「锁」);V:隐藏(「隐」)——行内按钮等效快捷键
// L/V 切换后,图标按钮的可访问名称同步表达下一步动作。
await page.keyboard.press('l');
await expect(page.getByTestId('tree-lock-sample-cube')).toHaveText('锁');
await expect(page.getByTestId('tree-lock-sample-cube')).toHaveAccessibleName('解锁 立方体');
await page.keyboard.press('v');
await expect(page.getByTestId('tree-visible-sample-cube')).toHaveText('隐');
await expect(page.getByTestId('tree-visible-sample-cube')).toHaveAccessibleName('显示 立方体');
await page.keyboard.press('l');
await expect(page.getByTestId('tree-lock-sample-cube')).toHaveText('开');
await expect(page.getByTestId('tree-lock-sample-cube')).toHaveAccessibleName('锁定 立方体');
// Delete:宿主全局快捷键(选中行上直接删除,撤销可恢复)
await page.keyboard.press('Delete');
await expect(row('sample-cube')).not.toBeVisible();
Expand All @@ -360,7 +368,7 @@ test('M3 按钮双击隔离:双击不绕过删除确认、不触发行重命
// 双击删除按钮:第一次点击只进入确认态,双击的第二次点击被隔离 → 对象仍在
await page.getByTestId('tree-delete-sample-light').dblclick();
await expect(page.getByTestId('tree-row-sample-light')).toBeVisible();
await expect(page.getByTestId('tree-delete-sample-light')).toHaveText('确认?');
await expect(page.getByTestId('tree-delete-sample-light')).toHaveAccessibleName('确认删除 主光');
// 双击按钮不触发行级 dblclick(不进入重命名)
await expect(page.getByTestId('tree-rename-sample-light')).toHaveCount(0);
// 单击确认 → 删除
Expand All @@ -369,7 +377,7 @@ test('M3 按钮双击隔离:双击不绕过删除确认、不触发行重命

// 双击可见性按钮:只切换一次(一次双击只产生一步「隐藏」,不因第二次点击回弹)
await page.getByTestId('tree-visible-sample-cube').dblclick();
await expect(page.getByTestId('tree-visible-sample-cube')).toHaveText('隐');
await expect(page.getByTestId('tree-visible-sample-cube')).toHaveAccessibleName('显示 立方体');
await expect(page.getByTestId('tree-rename-sample-cube')).toHaveCount(0);
});

Expand Down Expand Up @@ -543,8 +551,10 @@ test.describe('G-10 验收:窄屏布局', () => {
expect(scrollWidth).toBeLessThanOrEqual(innerWidth);

// 窄屏下列布局仍可编辑:选中对象并修改名称提交
await page.getByTestId('editor-panel-properties').click();
await page.getByTestId('inspector-name').fill('窄屏示例组');
await page.getByTestId('inspector-name').press('Enter');
await page.getByTestId('editor-panel-objects').click();
await expect(page.getByTestId('tree-row-sample-group')).toContainText('窄屏示例组');
});
});
Expand Down
Loading