diff --git a/ROADMAP.md b/ROADMAP.md
index 881dd79..de3fc6e 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -28,7 +28,7 @@ The slice files remain the detailed implementation plans. When an older list con
| [Slice 7a](SLICE7a.md) | Local authentication, RBAC, and application publishing | Complete | Implementation, security validation, manual acceptance, cleanup, and explicit sign-off passed |
| [Slice 7b](SLICE7b.md) | Browser-based first-run administrator setup | Complete | Fresh-install setup, takeover prevention, recovery validation, manual acceptance, and explicit sign-off passed |
| [Slice 7c](SLICE7c.md) | Multi-page applications and variable scope | Complete | Page authoring, scoped runtime, deep links, security validation, and explicit sign-off |
-| [Slice 7d](SLICE7d.md) | Visual Editor professional command ribbon | Planned | Ribbon implementation, responsive/accessibility validation, manual visual acceptance, and explicit sign-off |
+| [Slice 7d](SLICE7d.md) | Visual Editor professional command ribbon | Complete | None |
| [Slice 8](SLICE8.md) | Documentation and release packaging | Partial | Unified roadmap complete; broader documentation ownership, reconciliation, guides, and release packaging remain |
| [Slice 9](SLICE9.md) | OIDC and enterprise SSO | Post-MVP | Begins after Slice 7a; provider and provisioning decisions remain |
@@ -278,10 +278,10 @@ Controlled orchestration is not required in the v0.1.0 demonstration. Execution
### Slice 7d — Visual Editor command ribbon
-- [ ] Replace dense native-looking Visual Editor command rows with a grouped professional ribbon.
-- [ ] Distinguish project, page, page-settings, lifecycle, primary, disabled, and destructive controls.
-- [ ] Preserve Slice 7c behavior while improving desktop responsiveness, keyboard access, focus, and visual hierarchy.
-- [ ] Complete automated regression and accessibility checks plus manual visual acceptance and explicit sign-off.
+- [x] Replace dense native-looking Visual Editor command rows with a grouped professional ribbon.
+- [x] Distinguish project, page, page-settings, lifecycle, primary, disabled, and destructive controls.
+- [x] Preserve Slice 7c behavior while improving desktop responsiveness, keyboard access, focus, and visual hierarchy.
+- [x] Complete automated regression and accessibility checks plus manual visual acceptance and explicit sign-off.
## 8. Documentation, Packaging, and Release
diff --git a/docs/MVP_TRACEABILITY.md b/docs/MVP_TRACEABILITY.md
index 006da8f..f4b1d4f 100644
--- a/docs/MVP_TRACEABILITY.md
+++ b/docs/MVP_TRACEABILITY.md
@@ -26,7 +26,7 @@ This matrix maps every release-critical requirement area in `docs/REQUIREMENTS.m
| R16 | Immutable standalone published applications with public/authenticated visibility and server-owned published action execution | Slice 7a | Server-snapshot integration coverage and accepted public/restricted publishing workflows | Accepted |
| R17 | A fresh installation creates its initial administrator through a secure browser first-run flow without requiring Docker commands | Slice 7b | Atomic setup/security integration tests, frontend setup/password tests, recovery verification, and accepted manual workflow | Accepted |
| R18 | Authored and published applications support multiple deep-linked pages with page-local components and explicit global/page runtime-variable scope | Slice 7c | Schema/editor/runtime/publishing tests, multi-page browser E2E, security validation, and manual acceptance | Accepted |
-| R19 | The Visual Editor presents project, page, page-setting, and lifecycle commands in a professional, accessible, responsive command ribbon | Slice 7d | Ribbon component/regression/accessibility checks and manual visual acceptance | Planned |
+| R19 | The Visual Editor presents project, page, page-setting, and lifecycle commands in a professional, accessible, responsive command ribbon | Slice 7d | 28 frontend suites / 528 tests, production build, and accepted manual visual validation | Accepted |
## Approved acceptance workflows
diff --git a/frontend/src/components/ProjectToolbar/ProjectToolbar.module.css b/frontend/src/components/ProjectToolbar/ProjectToolbar.module.css
index e6caef4..a31ac9e 100644
--- a/frontend/src/components/ProjectToolbar/ProjectToolbar.module.css
+++ b/frontend/src/components/ProjectToolbar/ProjectToolbar.module.css
@@ -11,6 +11,15 @@
flex-shrink: 0;
}
+.toolbarRibbon {
+ min-height: 52px;
+ height: auto;
+ padding-top: 7px;
+ padding-bottom: 7px;
+ background: linear-gradient(180deg, #202832 0%, #171d24 100%);
+ box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.04);
+}
+
.nameArea {
display: flex;
align-items: center;
@@ -40,6 +49,15 @@
.nameButton:hover {
border-color: #484f58;
+ background: rgba(255, 255, 255, 0.05);
+}
+
+.nameButton:focus-visible,
+.btn:focus-visible,
+.pickerClose:focus-visible,
+.pickerItem:focus-visible {
+ outline: 2px solid #79c0ff;
+ outline-offset: 2px;
}
.nameInput {
@@ -77,16 +95,27 @@
flex-shrink: 0;
}
+.toolbarRibbon .actions {
+ padding-left: 14px;
+ border-left: 1px solid #3b4652;
+}
+
.btn {
- padding: 5px 12px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ min-height: 32px;
+ padding: 6px 13px;
background: #21262d;
border: 1px solid #30363d;
- border-radius: 4px;
+ border-radius: 6px;
font-size: 12px;
font-weight: 500;
color: #c9d1d9;
cursor: pointer;
- transition: background 0.1s, border-color 0.1s;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.18);
+ transition: background 0.12s, border-color 0.12s, transform 0.12s;
white-space: nowrap;
}
@@ -95,14 +124,22 @@
border-color: #8b949e;
}
+.btn:active:not(:disabled) { transform: translateY(1px); }
+
+.btnIcon {
+ font-size: 14px;
+ line-height: 1;
+ opacity: 0.9;
+}
+
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btnPrimary {
- background: #1f6feb;
- border-color: #1f6feb;
+ background: linear-gradient(180deg, #2f81f7 0%, #1f6feb 100%);
+ border-color: #438ef7;
color: #ffffff;
}
diff --git a/frontend/src/components/ProjectToolbar/ProjectToolbar.tsx b/frontend/src/components/ProjectToolbar/ProjectToolbar.tsx
index bd23f6e..fae3613 100644
--- a/frontend/src/components/ProjectToolbar/ProjectToolbar.tsx
+++ b/frontend/src/components/ProjectToolbar/ProjectToolbar.tsx
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { useProject } from '../../context/ProjectContext';
import styles from './ProjectToolbar.module.css';
-function ProjectToolbar(): React.ReactElement {
+function ProjectToolbar({ variant = 'default' }: { variant?: 'default' | 'ribbon' }): React.ReactElement {
const {
projectRowId,
projectName,
@@ -61,7 +61,7 @@ function ProjectToolbar(): React.ReactElement {
return (
<>
{/* ── Toolbar strip ─────────────────────────────────────────── */}
-
+
{/* Project name — click to rename */}
{editingName ? (
@@ -97,7 +97,7 @@ function ProjectToolbar(): React.ReactElement {
disabled={isLoading || isSaving}
title="Create a new project (current unsaved changes will be lost)"
>
- New
+ + New
{/* Save */}
@@ -107,7 +107,7 @@ function ProjectToolbar(): React.ReactElement {
disabled={isSaving || isLoading}
title={projectRowId === null ? 'Save project to backend' : 'Update project on backend'}
>
- {isSaving ? 'Saving…' : (projectRowId === null ? 'Save' : 'Update')}
+ ✓ {isSaving ? 'Saving…' : (projectRowId === null ? 'Save' : 'Update')}
{/* Load */}
@@ -117,7 +117,7 @@ function ProjectToolbar(): React.ReactElement {
disabled={isLoading || isSaving}
title="Load an existing project from the backend"
>
- {isLoading ? 'Loading…' : 'Load'}
+ ↥ {isLoading ? 'Loading…' : 'Load'}
diff --git a/frontend/src/components/VisualEditor/PageEventEditor.test.tsx b/frontend/src/components/VisualEditor/PageEventEditor.test.tsx
index 63955cb..53dfec5 100644
--- a/frontend/src/components/VisualEditor/PageEventEditor.test.tsx
+++ b/frontend/src/components/VisualEditor/PageEventEditor.test.tsx
@@ -40,4 +40,19 @@ describe('PageEventEditor', () => {
});
expect(onChange).toHaveBeenCalledWith([]);
});
+
+ test('configures a distinct onEnter lifecycle action', () => {
+ const onChange = jest.fn();
+ act(() => root.render(
));
+ const select = container.querySelector
('#page-onenter-action')!;
+ expect(select.getAttribute('aria-label')).toBe('On enter action');
+ act(() => {
+ Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value')!.set!.call(select, 'action_b');
+ select.dispatchEvent(new Event('change', { bubbles: true }));
+ });
+ expect(onChange).toHaveBeenCalledWith([
+ { event: 'onLoad', actionId: 'action_a' },
+ { event: 'onEnter', actionId: 'action_b' },
+ ]);
+ });
});
diff --git a/frontend/src/components/VisualEditor/PageEventEditor.tsx b/frontend/src/components/VisualEditor/PageEventEditor.tsx
index a0c42cf..61d57c3 100644
--- a/frontend/src/components/VisualEditor/PageEventEditor.tsx
+++ b/frontend/src/components/VisualEditor/PageEventEditor.tsx
@@ -24,7 +24,7 @@ export default function PageEventEditor({ events, actions, onChange }: Props): R
{actions.map((action) => )}
-