Skip to content

Commit 8f6df46

Browse files
mpstatonclaude
andcommitted
feat(shell): replace the tiling-host label with a Developers menu
The header spent a permanent slot on `tiling host · :3100` — one fact, stated always, that nobody needs at a glance and that told a developer nothing they could act on. It becomes a dropdown holding the things you actually go looking for when something is wrong, and the design system alongside them. Four items: - Design system — the Phase 2a portal. Brand guidelines, design tokens, the three-mode contract, every token on every surface with live contrast. - Workspace service — opens /config on whatever base this build points at, so "which stack am I actually talking to" is one click rather than a guess. - Identity · didi.sh — the sign-in and session issuer, same question. - Shell host · :3100 — keeps the fact the old label carried, and makes it actionable: copies this build's environment (ws url, id base, portal, active client, pinned state, auth mode, didi_id, mode, user agent) to the clipboard for a bug report. Built on the existing JumboPopdown rather than a second dropdown, so it inherits the header interaction contract already agreed — hover-open, click-toggle, Esc, click-outside, role="menu"/"menuitem". JumboPopdown gains one optional `triggerIcon` prop defaulting to the grid mark it has always used, so the flow-navigation popdown is untouched. The portal URL follows the established env convention: PUBLIC_DESIGN_PORTAL_URL with a localhost fallback, using `||` not `??` — an unset Docker ARG becomes an EMPTY STRING once assigned to ENV, and `??` would ship that. The remotes block in shell/rsbuild.config.ts documents the same trap from when it bit this repo. Deploying the portal will need that var set; until then it points at :3020. Verified in a browser: opens on click, closes on Esc and on click-outside, all four descriptions carry live values, and the old label is gone. Two things the DOM checks missed and a screenshot caught — the first glyph fell back to an asterisk in the mono stack, and a synthetic body.click() appeared to prove click-outside broken when the listener is on pointerdown, so the test was wrong rather than the component. 94 files typecheck clean, 20 packages build. Files changed: - shell/src/DevelopersMenu.svelte (new) - shell/src/JumboPopdown.svelte - shell/src/App.svelte Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019a8tSPbFdvF1pKtADnWyDg
1 parent 33d48e0 commit 8f6df46

3 files changed

Lines changed: 138 additions & 2 deletions

File tree

shell/src/App.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import FlowWidget from './FlowWidget.svelte';
66
import WorkspaceSwitcher from './WorkspaceSwitcher.svelte';
77
import DidiBadge from './DidiBadge.svelte';
8+
import DevelopersMenu from './DevelopersMenu.svelte';
89
import SignInWall from './SignInWall.svelte';
910
import JumboPopdown, { type PopdownItem } from './JumboPopdown.svelte';
1011
import ToggleHeader from '@augment-it/shared-ui/ToggleHeader__PromptOrPackage--Icons.svelte';
@@ -593,7 +594,7 @@
593594
>
594595
🔎 queue{#if queueDoneCount > 0}<span class="queue-badge">{queueDoneCount}</span>{/if}
595596
</button>
596-
<span class="muted">tiling host · :3100</span>
597+
<DevelopersMenu wsHttpBase={WS_HTTP_BASE} />
597598
<DidiBadge />
598599
<ModeToggle />
599600
{#if !workspace.pinned}

shell/src/DevelopersMenu.svelte

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<script lang="ts">
2+
// The Developers menu — header affordances only a developer wants.
3+
//
4+
// Replaces the bare `tiling host · :3100` label, which spent a permanent slot
5+
// in the header to state one fact nobody needed at a glance. That fact now
6+
// lives inside, next to the other things you actually go looking for when
7+
// something is wrong: which services this build points at, and the design
8+
// system.
9+
//
10+
// Built on JumboPopdown so it inherits the interaction contract already
11+
// agreed for header dropdowns — hover-open, click-toggle, Esc, click-outside,
12+
// role="menu"/"menuitem" — rather than inventing a second one.
13+
14+
import JumboPopdown, { type PopdownItem } from './JumboPopdown.svelte';
15+
import { workspace } from '@augment-it/workspace';
16+
17+
let { wsHttpBase }: { wsHttpBase: string } = $props();
18+
19+
// Same env convention as the federated remotes and DidiBadge: a PUBLIC_-
20+
// prefixed var inlined at build time, with a localhost fallback so local dev
21+
// needs no configuration. `||` not `??` deliberately — an unset Docker ARG
22+
// resolves to an EMPTY STRING once assigned to ENV, and `??` would ship the
23+
// empty string. That exact bug has bitten this repo before; see the remotes
24+
// block in shell/rsbuild.config.ts.
25+
const env = (import.meta as { env?: Record<string, string> }).env ?? {};
26+
const DESIGN_PORTAL = env.PUBLIC_DESIGN_PORTAL_URL || 'http://localhost:3020';
27+
const ID_BASE = env.PUBLIC_ID_BASE || 'http://localhost:4000';
28+
29+
let copied = $state(false);
30+
31+
// $derived, not const: wsHttpBase is a prop, and a const array would capture
32+
// its initial value and leave the workspace-service description stale.
33+
const items: PopdownItem[] = $derived([
34+
{
35+
id: 'design-system',
36+
title: 'Design system',
37+
description: 'Brand guidelines, design tokens, the three-mode contract — every token on every surface with live contrast.',
38+
},
39+
{
40+
id: 'workspace-service',
41+
title: 'Workspace service',
42+
description: `Session, tenancy and capability config · ${wsHttpBase}`,
43+
},
44+
{
45+
id: 'identity',
46+
title: 'Identity · didi.sh',
47+
description: `Sign-in and session issuer · ${ID_BASE}`,
48+
},
49+
{
50+
id: 'diagnostics',
51+
title: 'Shell host · :3100',
52+
description: 'Federation host. Copies this build’s environment to the clipboard for a bug report.',
53+
},
54+
]);
55+
56+
/** Everything you would otherwise have to ask someone to read off a screen. */
57+
function diagnostics(): string {
58+
return JSON.stringify(
59+
{
60+
shell: 'tiling host :3100',
61+
ws_url: wsHttpBase,
62+
id_base: ID_BASE,
63+
design_portal: DESIGN_PORTAL,
64+
active_client_id: workspace.active_client_id ?? null,
65+
pinned: workspace.pinned ?? null,
66+
didi_auth_mode: workspace.didi_auth_mode ?? null,
67+
didi_id: workspace.user?.didi_id ?? null,
68+
mode: document.documentElement.dataset.mode ?? null,
69+
user_agent: navigator.userAgent,
70+
},
71+
null,
72+
2,
73+
);
74+
}
75+
76+
function open(url: string): void {
77+
// noopener: a tab opened from here must not get a handle on the shell.
78+
window.open(url, '_blank', 'noopener,noreferrer');
79+
}
80+
81+
async function onSelect(id: string): Promise<void> {
82+
switch (id) {
83+
case 'design-system':
84+
open(DESIGN_PORTAL);
85+
break;
86+
case 'workspace-service':
87+
open(`${wsHttpBase}/config`);
88+
break;
89+
case 'identity':
90+
open(ID_BASE);
91+
break;
92+
case 'diagnostics':
93+
try {
94+
await navigator.clipboard.writeText(diagnostics());
95+
copied = true;
96+
setTimeout(() => (copied = false), 1600);
97+
} catch {
98+
// Clipboard is permission-gated and unavailable over plain http on
99+
// some origins. Falling back to the console beats failing silently —
100+
// the point is that the developer ends up holding the text.
101+
console.info('[developers] diagnostics:\n' + diagnostics());
102+
}
103+
break;
104+
}
105+
}
106+
</script>
107+
108+
<span class="dev-menu">
109+
<JumboPopdown triggerLabel="Developers" triggerIcon="⚙" {items} onSelect={(id) => void onSelect(id)} />
110+
{#if copied}
111+
<span class="copied" role="status">copied</span>
112+
{/if}
113+
</span>
114+
115+
<style>
116+
.dev-menu {
117+
display: inline-flex;
118+
align-items: center;
119+
gap: 6px;
120+
}
121+
122+
.copied {
123+
font-size: 10px;
124+
color: var(--color-ok-text);
125+
background: var(--color-ok-bg);
126+
border-radius: 2px;
127+
padding: 2px 6px;
128+
white-space: nowrap;
129+
}
130+
</style>

shell/src/JumboPopdown.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,15 @@
2929
triggerLabel,
3030
items,
3131
onSelect,
32+
// Defaults to the grid mark the flow-navigation popdown has always used, so
33+
// adding this prop changed nothing for the first caller. A second caller
34+
// (the Developers menu) wants its own glyph rather than a tiling icon.
35+
triggerIcon = '',
3236
}: {
3337
triggerLabel: string;
3438
items: PopdownItem[];
3539
onSelect: (id: string) => void;
40+
triggerIcon?: string;
3641
} = $props();
3742
3843
let open = $state(false);
@@ -97,7 +102,7 @@
97102
aria-expanded={open}
98103
onclick={toggle}
99104
>
100-
<span class="grid-mark" aria-hidden="true"></span>
105+
<span class="grid-mark" aria-hidden="true">{triggerIcon}</span>
101106
<span class="label">{triggerLabel}</span>
102107
<span class="chev" aria-hidden="true">{open ? '' : ''}</span>
103108
</button>

0 commit comments

Comments
 (0)