Skip to content

Commit c366ef7

Browse files
committed
feat(web): polish mobile editor shell and branch picker
1 parent c36f024 commit c366ef7

35 files changed

Lines changed: 2040 additions & 662 deletions

packages/web/src/components/ui/MIGRATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343

4444
`Popover` now completes the bounded feature inventory for desktop custom-content dropdowns: the desktop terminal selector and workspace branch quick pick both use the shared primitive from the public UI barrel for click-to-toggle or ArrowDown-to-open trigger semantics, portaled non-modal dialog content, and outside-click / `Escape` dismissal while preserving legacy hooks such as `terminal-selector-btn`, `terminal-selector-dropdown`, `terminal-selector-item*`, `git-panel-status-strip__branch`, and `branch-quick-pick*`. No additional bounded feature callers remain in the current `features/components` inventory. The mobile fullscreen terminal switcher and mobile branch quick pick intentionally stay on their existing `Select` + `MobileSelectSheet` and global `MobileSelectSheet` paths by design, while menu-list composition now moves into the shared `ActionMenu` wrapper.
4545

46-
`ActionMenu` now completes the bounded feature inventory for shared menu-list triggers: the mobile workspace topbar replaces its direct settings icon with a shared more-actions trigger that opens a mobile `Sheet`-backed action list containing `Settings` and `Quick Actions`. The primitive owns the bounded action-list chrome and desktop menu/mobile sheet presentation, while the feature layer still owns trigger styling and command wiring such as `commandPaletteOpenAtom` and the existing settings navigation callback. No additional bounded feature callers remain in the current inventory.
46+
`ActionMenu` now completes the bounded feature inventory for shared menu-list triggers used by bounded desktop/mobile overflow menus. The primitive owns the shared action-list chrome and desktop menu/mobile sheet presentation where those menus remain intentionally part of the product. The mobile workspace topbar no longer uses this pattern and instead exposes a direct settings icon button to keep the shell chrome aligned with the current mobile IDE navigation model.
4747

4848
`Notice` now completes the bounded legacy `.settings-page__notice*` migration inventory: the settings-page load-error shell uses the shared primitive from the public UI barrel while preserving the legacy `settings-page__notice*` compatibility classes and the caller-owned `settings-link` refresh action styling.
4949

packages/web/src/components/ui/kbd/index.module.css

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,29 @@
11
.kbd,
22
:global(.shortcuts-key) {
3+
display: inline-flex;
4+
align-items: center;
5+
justify-content: center;
36
min-width: 80px;
47
padding: var(--sp-2) var(--sp-3);
58
border: 1px solid var(--border);
6-
border-radius: var(--radius-md);
7-
background: var(--bg-hover);
8-
color: var(--text-secondary);
9+
border-radius: var(--radius-sm);
10+
background: color-mix(in srgb, var(--bg-input) 82%, var(--bg-surface) 18%);
11+
color: var(--text-primary);
912
font-family: var(--font-mono);
1013
font-size: var(--text-sm);
1114
text-align: center;
1215
}
1316

1417
.interactive:hover,
1518
:global(.shortcuts-key):hover {
16-
border-color: var(--accent-blue);
19+
border-color: var(--border-focus);
20+
background: var(--bg-active);
1721
}
1822

1923
.kbd:focus-visible,
2024
:global(.shortcuts-key):focus-visible {
2125
outline: none;
22-
border-color: var(--accent-blue);
26+
border-color: var(--border-focus);
2327
box-shadow: 0 0 0 1px rgba(108, 182, 255, 0.4);
2428
}
2529

packages/web/src/components/ui/notice/index.module.css

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,55 +2,59 @@
22
display: flex;
33
align-items: flex-start;
44
justify-content: space-between;
5-
gap: var(--sp-4);
6-
margin: 0 0 var(--sp-5);
7-
padding: var(--sp-3) var(--sp-4);
5+
gap: var(--sp-3);
6+
margin: 0 0 var(--sp-4);
7+
padding: var(--sp-2) var(--sp-3);
88
border: 1px solid var(--border);
9-
border-radius: var(--radius-md);
10-
background: var(--bg-surface);
9+
border-radius: var(--radius-sm);
10+
background: color-mix(in srgb, var(--bg-page) 42%, var(--bg-surface) 58%);
1111
}
1212

1313
.info {
1414
border-color: color-mix(in srgb, var(--color-info) 24%, transparent);
15-
background: color-mix(in srgb, var(--color-info) 6%, var(--bg-surface));
15+
background: color-mix(in srgb, var(--color-info) 5%, var(--bg-surface));
1616
}
1717

1818
.success {
1919
border-color: color-mix(in srgb, var(--color-success) 24%, transparent);
20-
background: color-mix(in srgb, var(--color-success) 6%, var(--bg-surface));
20+
background: color-mix(in srgb, var(--color-success) 5%, var(--bg-surface));
2121
}
2222

2323
.warning {
24-
border-color: color-mix(in srgb, var(--accent-amber) 35%, transparent);
25-
background: color-mix(in srgb, var(--accent-amber) 8%, var(--bg-surface));
24+
border-color: color-mix(in srgb, var(--accent-amber) 28%, transparent);
25+
background: color-mix(in srgb, var(--accent-amber) 6%, var(--bg-surface));
2626
}
2727

2828
.error {
29-
border-color: color-mix(in srgb, var(--color-error) 36%, transparent);
30-
background: color-mix(in srgb, var(--color-error) 10%, var(--bg-surface));
29+
border-color: color-mix(in srgb, var(--color-error) 28%, transparent);
30+
background: color-mix(in srgb, var(--color-error) 7%, var(--bg-surface));
3131
}
3232

3333
.copy {
3434
display: flex;
3535
min-width: 0;
3636
flex: 1;
3737
flex-direction: column;
38-
gap: var(--sp-1);
38+
gap: 2px;
3939
}
4040

4141
.title {
42-
font-size: var(--text-sm);
42+
font-size: var(--text-xs);
4343
font-weight: var(--font-semibold);
44+
letter-spacing: 0.06em;
45+
text-transform: uppercase;
4446
color: var(--text-primary);
4547
}
4648

4749
.message {
48-
font-size: var(--text-xs);
50+
font-size: var(--text-sm);
51+
line-height: 1.45;
4952
color: var(--text-secondary);
5053
word-break: break-word;
5154
}
5255

5356
.action {
5457
flex-shrink: 0;
55-
align-self: center;
58+
display: inline-flex;
59+
align-self: flex-start;
5660
}

packages/web/src/components/ui/pill/index.module.css

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
.pill {
22
display: inline-flex;
33
align-items: center;
4+
justify-content: center;
5+
min-height: 36px;
6+
min-width: 0;
47
gap: var(--sp-2);
5-
padding: var(--sp-2) var(--sp-4);
6-
border: 1px solid var(--border);
7-
border-radius: var(--radius-full);
8-
background: var(--bg-surface);
8+
padding: var(--sp-2) var(--sp-3);
9+
border: 1px solid transparent;
10+
border-radius: var(--radius-md);
11+
background: transparent;
912
color: var(--text-secondary);
1013
font-family: var(--font-sans);
11-
font-size: var(--text-base);
14+
font-size: var(--text-sm);
15+
line-height: 1.4;
1216
transition:
1317
background var(--duration-fast) var(--ease-out),
1418
border-color var(--duration-fast) var(--ease-out),
@@ -17,24 +21,26 @@
1721

1822
.pill:hover:not(:disabled) {
1923
background: var(--bg-hover);
20-
border-color: var(--border-light);
24+
border-color: color-mix(in srgb, var(--border-light) 72%, transparent);
25+
color: var(--text-primary);
2126
}
2227

2328
.pill:focus-visible {
2429
outline: none;
25-
border-color: var(--accent-blue);
26-
box-shadow: 0 0 0 1px rgba(108, 182, 255, 0.4);
30+
border-color: var(--border-focus);
31+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-blue) 35%, transparent);
2732
}
2833

2934
.active {
30-
background: var(--accent-blue);
31-
border-color: var(--accent-blue);
32-
color: var(--text-inverse);
35+
background: var(--bg-active);
36+
border-color: var(--border-focus);
37+
color: var(--text-primary);
3338
}
3439

3540
.icon {
3641
display: inline-flex;
3742
align-items: center;
43+
color: inherit;
3844
}
3945

4046
.pill:disabled {

packages/web/src/components/ui/segmented-control/index.module.css

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@
6060

6161
:global(.settings-provider-tabs) {
6262
margin-bottom: var(--sp-6);
63+
padding: 0;
64+
border: none;
65+
border-radius: 0;
66+
background: transparent;
6367
}
6468

6569
:global(.settings-provider-subnav) {
@@ -76,35 +80,42 @@
7680
padding-left: 0;
7781
padding-right: 0;
7882
padding-top: 0;
83+
background: transparent;
7984
}
8085

8186
:global(.settings-provider-tab) {
82-
background: var(--bg-surface);
83-
border-color: var(--border);
87+
background: transparent;
88+
border-color: transparent;
8489
}
8590

8691
:global(.settings-provider-tab.active) {
87-
border-color: var(--accent-blue);
92+
background: var(--bg-active);
93+
border-color: var(--border-focus);
8894
}
8995

9096
:global(.settings-provider-subnav-button) {
9197
background: transparent;
9298
}
9399

94100
:global(.settings-provider-subnav-button.active) {
95-
background: var(--bg-hover);
96-
border-color: var(--border-light);
101+
background: var(--bg-active);
102+
border-color: var(--border-focus);
97103
}
98104

99105
:global(.shortcuts-category-tabs) {
100106
margin-bottom: var(--sp-4);
107+
padding: 0;
108+
border: none;
109+
border-radius: 0;
110+
background: transparent;
101111
}
102112

103113
:global(.shortcuts-category-tab) {
104-
background: var(--bg-hover);
114+
background: transparent;
115+
border-color: transparent;
105116
}
106117

107118
:global(.shortcuts-category-tab.active) {
108-
background: color-mix(in srgb, var(--accent-blue) 18%, var(--bg-surface));
109-
border-color: transparent;
119+
background: var(--bg-active);
120+
border-color: var(--border-focus);
110121
}

packages/web/src/components/ui/sheet/index.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ describe("Sheet", () => {
3030
expect(document.querySelector(".mobile-sheet__body")).toBeTruthy();
3131
expect(document.querySelector(".mobile-sheet__footer")).toBeTruthy();
3232
expect(screen.getByText("Body")).toBeInTheDocument();
33+
expect(screen.getByText("Quick Actions")).toBeInTheDocument();
34+
expect(screen.queryByText("COMMAND PALETTE")).not.toBeInTheDocument();
3335
expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument();
3436
});
3537

@@ -89,6 +91,7 @@ describe("Sheet", () => {
8991

9092
expect(document.querySelector(".mobile-sheet--fullscreen")).toBeTruthy();
9193
expect(document.querySelector(".mobile-sheet--launch")).toBeTruthy();
94+
expect(document.querySelector(".mobile-sheet__header .mobile-page-header")).toBeTruthy();
9295
expect(document.querySelector(".mobile-sheet__body--flush")).toBeTruthy();
9396
expect(document.querySelector(".mobile-sheet__body--fullscreen")).toBeTruthy();
9497
expect(document.querySelector(".mobile-launch-sheet")).toBeTruthy();

packages/web/src/components/ui/sheet/index.tsx

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
11
import clsx from "clsx";
2-
import { ArrowLeft } from "lucide-react";
32
import type { ReactNode } from "react";
3+
import { MobilePageHeader } from "../../../features/shared/components/mobile-page-header";
44
import { useTranslation } from "../../../lib/i18n";
55

6-
interface SheetHeaderProps {
7-
readonly title: string;
8-
readonly kicker?: string;
9-
readonly onBack: () => void;
10-
readonly backLabel: string;
11-
readonly headerAction?: ReactNode;
12-
}
13-
146
export interface SheetProps {
157
readonly title: string;
168
readonly body: ReactNode;
@@ -25,24 +17,6 @@ export interface SheetProps {
2517
readonly backLabel?: string;
2618
}
2719

28-
function SheetHeader({ title, kicker, onBack, backLabel, headerAction }: SheetHeaderProps) {
29-
return (
30-
<div className="page-header">
31-
<div className="page-header__leading">
32-
<button type="button" className="page-header__back" onClick={onBack} aria-label={backLabel}>
33-
<ArrowLeft size={16} />
34-
<span>{backLabel}</span>
35-
</button>
36-
<div className="page-header__copy">
37-
{kicker ? <div className="page-header__kicker">{kicker}</div> : null}
38-
<h2 className="page-header__title">{title}</h2>
39-
</div>
40-
</div>
41-
{headerAction ? <div className="page-header__actions">{headerAction}</div> : null}
42-
</div>
43-
);
44-
}
45-
4620
export function Sheet({
4721
title,
4822
body,
@@ -79,12 +53,13 @@ export function Sheet({
7953
>
8054
{fullscreen ? null : <div className="mobile-sheet__handle" aria-hidden="true" />}
8155
<div className="mobile-sheet__header">
82-
<SheetHeader
56+
<MobilePageHeader
8357
title={title}
8458
kicker={kicker}
8559
onBack={handleBack}
8660
backLabel={resolvedBackLabel}
87-
headerAction={headerAction}
61+
rightSlot={headerAction}
62+
titleAs="h2"
8863
/>
8964
</div>
9065
<div className={clsx("mobile-sheet__body", bodyClassName)}>{body}</div>

packages/web/src/features/mobile-select/components/mobile-select-sheet.test.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ describe("MobileSelectSheet", () => {
232232
/>
233233
);
234234

235-
const header = document.querySelector(".mobile-inline-sheet__header .page-header");
235+
const header = document.querySelector(".mobile-inline-sheet__header .mobile-page-header");
236236
const leading = header?.querySelector(".page-header__leading");
237237

238238
expect(header).not.toBeNull();
@@ -448,7 +448,8 @@ describe("MobileSelectSheet", () => {
448448
sections={[{ kind: "options", id: "branches", items: [] }]}
449449
create={{
450450
visible: true,
451-
label: (query) => `Create branch: ${query}`,
451+
label: () => "Create branch",
452+
description: (query) => query,
452453
onCreate,
453454
}}
454455
onSelect={vi.fn()}
@@ -459,7 +460,10 @@ describe("MobileSelectSheet", () => {
459460
fireEvent.change(screen.getByPlaceholderText("Search branches"), {
460461
target: { value: "feature/mobile-select" },
461462
});
462-
await user.click(screen.getByRole("button", { name: "Create branch: feature/mobile-select" }));
463+
expect(screen.getByRole("button", { name: "Create branch" })).toHaveAccessibleDescription(
464+
"feature/mobile-select"
465+
);
466+
await user.click(screen.getByRole("button", { name: "Create branch" }));
463467

464468
expect(onCreate).toHaveBeenCalledWith("feature/mobile-select");
465469
});
@@ -556,7 +560,8 @@ describe("MobileSelectSheet", () => {
556560
]}
557561
create={{
558562
visible: true,
559-
label: (query) => `Create branch: ${query}`,
563+
label: () => "Create branch",
564+
description: (query) => query,
560565
disabled: (query) => query.includes("main"),
561566
onCreate,
562567
}}
@@ -571,12 +576,13 @@ describe("MobileSelectSheet", () => {
571576

572577
const option = screen.getByRole("button", { name: "main" });
573578
const action = screen.getByRole("button", { name: "Refresh" });
574-
const create = screen.getByRole("button", { name: "Create branch: main" });
579+
const create = screen.getByRole("button", { name: "Create branch" });
575580

576581
expect(option).toBeDisabled();
577582
expect(option).toHaveAccessibleDescription("Protected branch");
578583
expect(action).toBeDisabled();
579584
expect(create).toBeDisabled();
585+
expect(create).toHaveAccessibleDescription("main");
580586

581587
await user.click(option);
582588
await user.click(action);

0 commit comments

Comments
 (0)