feat(editor): session entry points in the File and Account menus

The editor menubar's Account menu hosted only the shared `user-menu`
slot, so it had no way in or out of a session: the bottom-left dropdown
renders My projects / Sign in from its own markup, which this menubar
never sees. Signed-in users had no route to their saved work from the
editor, and anonymous ones had no way to sign in without leaving it.

Declares two account-scoped command ids and adds an `optional` item flag
that HIDES a row whose command is unregistered instead of the default
"render disabled". Absence here is not "temporarily unavailable" but
"does not apply": OSS has no accounts, and in pro exactly one of the two
is meaningful at a time — a greyed-out My projects would read as broken.

My projects sits third in File, at the head of the project group rather
than tacked onto the two "new …" rows: it opens saved work, which is
what Open/Save beneath it are for.

Labels reuse header.auth.{myProjects,signIn}, already translated in all
nine locales — no new strings.
This commit is contained in:
David Montero Crespo 2026-08-10 19:47:20 +02:00
parent 8fd81fa080
commit 28eca48168
2 changed files with 72 additions and 10 deletions

View File

@ -35,7 +35,20 @@ import {
import './EditorMenuBar.css';
type Item =
| { kind: 'command'; id: EditorCommandId; label: string; shortcut?: string; pro?: boolean }
| {
kind: 'command';
id: EditorCommandId;
label: string;
shortcut?: string;
pro?: boolean;
/** Hide the row entirely when no handler is registered, instead of
* the default "render disabled". For account-scoped items the
* absence of a handler is not "temporarily unavailable" but "does
* not apply here": OSS has no accounts at all, and in pro exactly
* one of Sign in / My projects is meaningful at a time. A greyed-out
* "My projects" would read as a broken feature in both. */
optional?: boolean;
}
| { kind: 'link'; href: string; label: string }
| { kind: 'separator' };
@ -90,6 +103,15 @@ export const EditorMenuBar: React.FC = () => {
{ kind: 'command', id: 'project.new', label: t('editor.menu.newProject', 'New workspace') },
{ kind: 'command', id: 'file.new', label: t('editor.menu.newFile', 'New file') },
{ kind: 'separator' },
// Third item, and deliberately at the head of the project group rather
// than tacked onto the "new …" pair above: it opens the user's saved
// work, which is what Open/Save below are about.
{
kind: 'command',
id: 'account.myProjects',
label: t('header.auth.myProjects', 'My projects'),
optional: true,
},
{ kind: 'command', id: 'project.open', label: t('editor.menu.open', 'Open project…') },
{
kind: 'command',
@ -116,6 +138,25 @@ export const EditorMenuBar: React.FC = () => {
{ kind: 'command', id: 'sim.record', label: t('editor.toolbar.recordLabel', 'Record simulation'), pro: true },
];
// Sign in / My projects for the Account menu. The bottom-left account
// dropdown gets these from its own (pro) markup; this menubar only ever
// hosted the shared `user-menu` slot, which is why the editor's Account
// menu had no way in or out of a session.
const accountItems: Item[] = [
{
kind: 'command',
id: 'account.myProjects',
label: t('header.auth.myProjects', 'My projects'),
optional: true,
},
{
kind: 'command',
id: 'account.login',
label: t('header.auth.signIn', 'Sign in'),
optional: true,
},
];
const helpItems: Item[] = [
{ kind: 'link', href: `${SITE}/docs`, label: t('header.nav.documentation', 'Documentation') },
{ kind: 'link', href: '/examples', label: t('header.nav.examples', 'Examples') },
@ -224,6 +265,16 @@ export const EditorMenuBar: React.FC = () => {
)}
{which === 'account' && (
<>
{/* Session entry points, above the pro extras. Exactly one of
the two is registered at a time (pro registers by session
state) and neither exists in OSS, so this block renders
nothing in an OSS build. */}
{accountItems
.filter((item) => item.kind !== 'command' || hasEditorCommand(item.id))
.map((item) => (item.kind === 'command' ? renderCommand(item) : null))}
{accountItems.some(
(item) => item.kind === 'command' && hasEditorCommand(item.id),
) && <div className="emb-separator" />}
{/* Pro account items (PRO badge, Subscribe / Manage
subscription, licenses, history, replays, Privacy) mount
here via the SAME user-menu slot the bottom-left account
@ -291,15 +342,20 @@ export const EditorMenuBar: React.FC = () => {
</button>
</>
)}
{items.map((item, i) =>
item.kind === 'separator' ? (
<div key={`sep-${i}`} className="emb-separator" />
) : item.kind === 'link' ? (
renderLink(item)
) : (
renderCommand(item)
),
)}
{items
.filter(
(item) =>
item.kind !== 'command' || !item.optional || hasEditorCommand(item.id),
)
.map((item, i) =>
item.kind === 'separator' ? (
<div key={`sep-${i}`} className="emb-separator" />
) : item.kind === 'link' ? (
renderLink(item)
) : (
renderCommand(item)
),
)}
</div>
)}
</div>

View File

@ -25,6 +25,12 @@ export type EditorCommandId =
| 'project.exportBom'
| 'project.exportScreenshot'
| 'file.new'
// Account-scoped, registered ONLY by the pro overlay and only in the
// matching session state (myProjects when signed in, login when not).
// OSS has no auth at all, so both stay unregistered here and the menu
// hides them outright — see `optional` in EditorMenuBar.
| 'account.myProjects'
| 'account.login'
| 'project.share'
| 'project.githubSync'
| 'firmware.upload'