fix: simplify plugin settings layout (#3914)
Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
@@ -227,7 +227,9 @@ describe("PluginsSettingsPage", () => {
|
||||
render(<PluginsSettingsPage />);
|
||||
|
||||
expect(screen.getByText(PLUGIN_DISPLAY_NAME)).toBeTruthy();
|
||||
expect(screen.getByText(/acme-tools.*v1\.0\.0/)).toBeTruthy();
|
||||
expect(screen.getByTestId(`plugin-row-${PLUGIN_ID}`).textContent).toMatch(
|
||||
/acme-tools.*v1\.0\.0/,
|
||||
);
|
||||
expect(screen.getAllByText(/active/i).length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByTestId("plugin-unsigned-badge")).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -120,7 +120,7 @@ export function PluginRow({
|
||||
return (
|
||||
<div
|
||||
data-testid={`plugin-row-${plugin.id}`}
|
||||
className="group relative rounded-lg border border-border/70 bg-background p-4 transition-colors hover:border-border hover:bg-muted"
|
||||
className="group relative min-w-0 py-4 transition-colors hover:bg-muted/40"
|
||||
>
|
||||
<PluginRowContent
|
||||
plugin={plugin}
|
||||
@@ -214,7 +214,7 @@ function PluginRowContent({
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<PluginRowIdentity plugin={plugin} needsSetup={needsSetup} update={update} />
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<div className="flex min-w-0 max-w-full items-center gap-2">
|
||||
{canManage && (
|
||||
<PluginRowActions
|
||||
plugin={plugin}
|
||||
@@ -320,7 +320,7 @@ function PluginRowIdentity({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="min-w-0 max-w-full space-y-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-medium text-foreground truncate group-hover:underline">
|
||||
{plugin.display_name}
|
||||
@@ -349,9 +349,10 @@ function PluginRowIdentity({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
||||
<span className="font-mono truncate">
|
||||
{plugin.id} · v{plugin.version}
|
||||
<span className="font-mono truncate" title={plugin.id}>
|
||||
{plugin.id}
|
||||
</span>
|
||||
<span className="shrink-0 font-mono">v{plugin.version}</span>
|
||||
<PluginRepoLink url={plugin.repo_url} className="relative z-10" />
|
||||
<PluginUpdateInfo pluginId={plugin.id} update={update} />
|
||||
</div>
|
||||
@@ -493,7 +494,7 @@ function PluginRowActions({
|
||||
const { isMobile } = useResponsiveBreakpoint();
|
||||
const updateEntry = update?.hasUpdate ? update.latest : undefined;
|
||||
return (
|
||||
<div className="relative z-10 flex flex-wrap items-center gap-2 shrink-0">
|
||||
<div className="relative z-10 flex min-w-0 flex-wrap items-center gap-2">
|
||||
{updateEntry && onUpdate && (
|
||||
<PluginUpdateButton
|
||||
pluginId={plugin.id}
|
||||
|
||||
@@ -22,7 +22,8 @@ import { PluginUpdateStatus } from "./plugin-update-status";
|
||||
import { usePluginActions } from "./use-plugin-actions";
|
||||
import { usePluginUpdateAction } from "./use-plugin-update-action";
|
||||
import { settingsActionClassName } from "@/components/settings/settings-control";
|
||||
import { SettingsGroup } from "@/components/settings/settings-group";
|
||||
import { SettingsRow } from "@/components/settings/settings-group";
|
||||
import { SETTINGS_TYPOGRAPHY } from "@/components/settings/settings-typography";
|
||||
|
||||
/**
|
||||
* Operator UI to browse, install, enable, disable, uninstall, and update kandev
|
||||
@@ -60,8 +61,9 @@ export function PluginsSettings() {
|
||||
saveStatus="idle"
|
||||
onSave={() => undefined}
|
||||
showSaveButton={false}
|
||||
contentFrame="none"
|
||||
>
|
||||
<Tabs defaultValue="installed" className="space-y-6">
|
||||
<Tabs defaultValue="installed" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger
|
||||
value="installed"
|
||||
@@ -84,7 +86,7 @@ export function PluginsSettings() {
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="installed" className="space-y-6">
|
||||
<TabsContent value="installed" className="space-y-4">
|
||||
<InstalledTab
|
||||
list={list}
|
||||
actions={actions}
|
||||
@@ -141,46 +143,9 @@ function InstalledTab({
|
||||
updateAction,
|
||||
isFinePointer,
|
||||
}: InstalledTabProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsGroup
|
||||
title={t("plugins:installedPlugins")}
|
||||
contentClassName="space-y-4 divide-y-0"
|
||||
action={
|
||||
canManage ? (
|
||||
<div className="flex w-full flex-col gap-2 md:w-auto md:flex-row">
|
||||
<Button
|
||||
data-testid="plugins-sync-button"
|
||||
variant="secondary"
|
||||
disabled={actions.syncBusy}
|
||||
onClick={actions.handleSync}
|
||||
className={settingsActionClassName("cursor-pointer")}
|
||||
>
|
||||
<IconRefresh className={`h-4 w-4 ${actions.syncBusy ? "animate-spin" : ""}`} />
|
||||
{t("plugins:sync")}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="plugins-check-updates-button"
|
||||
variant="secondary"
|
||||
disabled={updates.checking}
|
||||
onClick={updates.checkForUpdates}
|
||||
className={settingsActionClassName("cursor-pointer")}
|
||||
>
|
||||
<IconRefresh className={`h-4 w-4 ${updates.checking ? "animate-spin" : ""}`} />
|
||||
{t("plugins:checkForUpdates")}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="install-plugin-trigger"
|
||||
onClick={actions.openInstall}
|
||||
className={settingsActionClassName("cursor-pointer")}
|
||||
>
|
||||
{t("plugins:installPlugin")}
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<section className="min-w-0 space-y-3" aria-labelledby="installed-plugins-heading">
|
||||
<InstalledPluginsToolbar actions={actions} updates={updates} canManage={canManage} />
|
||||
{canManage && <GlobalAutoUpdateToggle settings={autoUpdate} />}
|
||||
|
||||
<PluginUpdateStatus
|
||||
@@ -211,7 +176,61 @@ function InstalledTab({
|
||||
updateAction={updateAction}
|
||||
isFinePointer={isFinePointer}
|
||||
/>
|
||||
</SettingsGroup>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function InstalledPluginsToolbar({
|
||||
actions,
|
||||
updates,
|
||||
canManage,
|
||||
}: Pick<InstalledTabProps, "actions" | "updates" | "canManage">) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
<h3 id="installed-plugins-heading" className={SETTINGS_TYPOGRAPHY.sectionTitle}>
|
||||
{t("plugins:installedPlugins")}
|
||||
</h3>
|
||||
{canManage && (
|
||||
<>
|
||||
<Button
|
||||
data-testid="install-plugin-trigger"
|
||||
onClick={actions.openInstall}
|
||||
className={settingsActionClassName("ml-auto cursor-pointer md:ml-0")}
|
||||
>
|
||||
{t("plugins:installPlugin")}
|
||||
</Button>
|
||||
<div className="flex w-full flex-wrap items-center gap-1 md:ml-auto md:w-auto">
|
||||
<Button
|
||||
data-testid="plugins-sync-button"
|
||||
variant="ghost"
|
||||
disabled={actions.syncBusy}
|
||||
onClick={actions.handleSync}
|
||||
className={settingsActionClassName("cursor-pointer text-muted-foreground")}
|
||||
>
|
||||
<IconRefresh
|
||||
className={`size-4 ${actions.syncBusy ? "animate-spin" : ""}`}
|
||||
aria-hidden
|
||||
/>
|
||||
{t("plugins:sync")}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="plugins-check-updates-button"
|
||||
variant="ghost"
|
||||
disabled={updates.checking}
|
||||
onClick={updates.checkForUpdates}
|
||||
className={settingsActionClassName("cursor-pointer text-muted-foreground")}
|
||||
>
|
||||
<IconRefresh
|
||||
className={`size-4 ${updates.checking ? "animate-spin" : ""}`}
|
||||
aria-hidden
|
||||
/>
|
||||
{t("plugins:checkForUpdates")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -227,25 +246,24 @@ function GlobalAutoUpdateToggle({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4 rounded-lg border border-border/70 bg-background p-4">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<label
|
||||
htmlFor="plugins-auto-update-default"
|
||||
className="text-sm font-medium text-foreground cursor-pointer"
|
||||
>
|
||||
{t("plugins:autoUpdateTitle")}
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">{t("plugins:autoUpdateDescription")}</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="plugins-auto-update-default"
|
||||
data-testid="plugins-auto-update-default"
|
||||
checked={settings.autoUpdateDefault}
|
||||
disabled={!settings.loaded}
|
||||
onCheckedChange={settings.setDefault}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<SettingsRow
|
||||
label={<span className="text-sm">{t("plugins:autoUpdateTitle")}</span>}
|
||||
description={t("plugins:autoUpdateDescription")}
|
||||
controlId="plugins-auto-update-default"
|
||||
touchTarget="switch"
|
||||
className="flex-row items-center gap-4 border-y border-border/60"
|
||||
controlWrapperClassName="max-md:w-auto"
|
||||
control={
|
||||
<Switch
|
||||
id="plugins-auto-update-default"
|
||||
data-testid="plugins-auto-update-default"
|
||||
checked={settings.autoUpdateDefault}
|
||||
disabled={!settings.loaded}
|
||||
onCheckedChange={settings.setDefault}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -297,7 +315,7 @@ function PluginList({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="divide-y divide-border/60">
|
||||
{items.map((plugin) => {
|
||||
const rowUpdate: PluginRowUpdateState = {
|
||||
latest: updates.latestById.get(plugin.id),
|
||||
|
||||
@@ -62,6 +62,24 @@ import {
|
||||
const NAV_ITEM_ID = "e2e-hello";
|
||||
const PLUGIN_ROUTE = "/plugins/e2e-hello";
|
||||
|
||||
// @covers AC-UI-CONTROL-SIZING-001.1, AC-UI-CONTROL-SIZING-001.3, AC-UI-CONTROL-SIZING-001.8
|
||||
test("plugin settings toolbar stays compact on desktop", async ({ testPage }) => {
|
||||
await testPage.goto("/settings/plugins");
|
||||
const install = testPage.getByTestId("install-plugin-trigger");
|
||||
const sync = testPage.getByTestId("plugins-sync-button");
|
||||
const check = testPage.getByTestId("plugins-check-updates-button");
|
||||
await expect(install).toBeVisible();
|
||||
const boxes = await Promise.all([install.boundingBox(), sync.boundingBox(), check.boundingBox()]);
|
||||
for (const box of boxes) {
|
||||
expect(box).not.toBeNull();
|
||||
expect(box!.height).toBeCloseTo(28, 0);
|
||||
expect(box!.y).toBeCloseTo(boxes[0]!.y, 0);
|
||||
}
|
||||
await install.focus();
|
||||
await testPage.keyboard.press("Enter");
|
||||
await expect(testPage.getByTestId("install-plugin-dialog")).toBeVisible();
|
||||
});
|
||||
|
||||
/** Every deliveries.jsonl `event_type` recorded so far, read straight off
|
||||
* disk from the plugin's real KANDEV_PLUGIN_DATA_DIR (no in-process mock —
|
||||
* this is the fixture Go binary's own gRPC OnEvent handler writing to its
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
uploadPackage,
|
||||
} from "../plugins/plugin-test-helpers";
|
||||
|
||||
// @covers AC-UI-CONTROL-SIZING-001.3, AC-UI-CONTROL-SIZING-001.4, AC-UI-CONTROL-SIZING-001.5, AC-UI-CONTROL-SIZING-001.8
|
||||
test.describe("Mobile plugin updates", () => {
|
||||
test.afterEach(async ({ apiClient }) => {
|
||||
await uninstallPluginFixture(apiClient);
|
||||
@@ -14,6 +15,7 @@ test.describe("Mobile plugin updates", () => {
|
||||
|
||||
test("checks and updates marketplace state from the mobile settings flow", async ({
|
||||
testPage,
|
||||
prCapture,
|
||||
}) => {
|
||||
await openInstallDialog(testPage);
|
||||
await uploadPackage(testPage, PACKAGE_PATH);
|
||||
@@ -81,6 +83,19 @@ test.describe("Mobile plugin updates", () => {
|
||||
expect(buttonBox).not.toBeNull();
|
||||
expect(buttonBox!.height).toBeGreaterThanOrEqual(44);
|
||||
|
||||
await prCapture.screenshot("plugin-settings", { caption: "Plugin settings on a phone" });
|
||||
const syncBox = await testPage.getByTestId("plugins-sync-button").boundingBox();
|
||||
const installBox = await testPage.getByTestId("install-plugin-trigger").boundingBox();
|
||||
const headingBox = await testPage
|
||||
.getByRole("heading", { name: "Installed plugins", exact: true })
|
||||
.boundingBox();
|
||||
expect(syncBox).not.toBeNull();
|
||||
expect(installBox).not.toBeNull();
|
||||
expect(headingBox).not.toBeNull();
|
||||
expect(syncBox!.y).toBeCloseTo(buttonBox!.y, 0);
|
||||
expect(installBox!.y).toBeLessThan(headingBox!.y + headingBox!.height);
|
||||
expect(installBox!.width).toBeLessThan(testPage.viewportSize()!.width / 2);
|
||||
|
||||
await checkButton.tap();
|
||||
|
||||
await expect.poll(() => refreshRequests).toBe(1);
|
||||
@@ -106,5 +121,27 @@ test.describe("Mobile plugin updates", () => {
|
||||
expect(
|
||||
await testPage.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth),
|
||||
).toBe(true);
|
||||
|
||||
for (const width of [320, 767, 768]) {
|
||||
await testPage.setViewportSize({ width, height: 851 });
|
||||
const sync = testPage.getByTestId("plugins-sync-button");
|
||||
const install = testPage.getByTestId("install-plugin-trigger");
|
||||
await expect(sync).toBeVisible();
|
||||
await expect(install).toBeVisible();
|
||||
const [syncBounds, checkBounds, installBounds] = await Promise.all([
|
||||
sync.boundingBox(),
|
||||
checkButton.boundingBox(),
|
||||
install.boundingBox(),
|
||||
]);
|
||||
expect(syncBounds!.y).toBeCloseTo(checkBounds!.y, 0);
|
||||
for (const box of [syncBounds!, checkBounds!, installBounds!]) {
|
||||
expect(box.height).toBeGreaterThanOrEqual(44);
|
||||
expect(box.x).toBeGreaterThanOrEqual(0);
|
||||
expect(box.x + box.width).toBeLessThanOrEqual(width);
|
||||
}
|
||||
expect(
|
||||
await testPage.evaluate(() => document.documentElement.scrollWidth),
|
||||
).toBeLessThanOrEqual(width);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
created: 2026-09-24
|
||||
status: done
|
||||
requirements:
|
||||
- REQ-UI-CONTROL-SIZING-001
|
||||
system_design:
|
||||
- ../../specs/ui/system-design/control-sizing.md
|
||||
legacy_specs: []
|
||||
---
|
||||
|
||||
# Implementation Plan: Plugin settings layout
|
||||
|
||||
## Overview
|
||||
|
||||
Flatten the host plugin settings page and compact its actions while preserving
|
||||
the existing plugin lifecycle and permission behavior. This is a presentation
|
||||
refactor under the existing [control sizing contract](../../specs/ui/requirements/control-sizing.md).
|
||||
The user authorized implementation, screenshots, and PR delivery in one session.
|
||||
|
||||
## Scope and technical approach
|
||||
|
||||
Use the unframed SettingsPageTemplate variant, a compact installed-list toolbar,
|
||||
an inline SettingsRow for automatic updates, and divided PluginRow entries.
|
||||
Keep the current hooks, translated copy, direct row navigation, errors, and
|
||||
confirmation surfaces. No backend, persistence, plugin SDK, or release changes.
|
||||
|
||||
## ASCII UI preview
|
||||
|
||||
UI-01: Settings > Plugins > Installed, populated state. Current phone layout
|
||||
stacks three full-width buttons and nests the list and preference in cards.
|
||||
|
||||
```text
|
||||
Phone Desktop
|
||||
[Installed] [Browse] [Installed] [Browse]
|
||||
Installed plugins [Install plugin] Installed plugins [Install plugin] Sync Check for updates
|
||||
Sync Check for updates Automatic updates description [switch]
|
||||
Automatic updates [switch] Last-check status
|
||||
Description ---------------------------------------------------------
|
||||
Last-check status Plugin name / status / version / actions
|
||||
-------------------------------- Description and categories
|
||||
Plugin name / status / version Auto-update [switch]
|
||||
Actions ---------------------------------------------------------
|
||||
Description and categories Next plugin
|
||||
Auto-update [switch]
|
||||
--------------------------------
|
||||
Next plugin
|
||||
```
|
||||
|
||||
Structural requirements: retain labels and touch targets while removing nested
|
||||
frames. The existing settings page owns scrolling and safe areas. Use shared
|
||||
SettingsRow and control-sizing primitives; retain the existing phone uninstall
|
||||
drawer. Spacing in the preview is illustrative. Empty/loading/error and pending
|
||||
actions retain their current content and behavior. Long labels may wrap the
|
||||
toolbar without causing horizontal scrolling.
|
||||
|
||||
## Tests
|
||||
|
||||
Existing page and PluginRow unit coverage protects actions and permissions.
|
||||
Mobile Playwright checks aligned secondary actions, compact Install placement,
|
||||
44px controls, responsive boundaries, and reachable row actions. Desktop
|
||||
Playwright checks 28px toolbar controls and the installation lifecycle.
|
||||
These cover AC-UI-CONTROL-SIZING-001.1, .3, .4, .5, .6, .7, and .8.
|
||||
|
||||
## Work orders
|
||||
|
||||
- [x] [Task 01: Flatten plugin settings](task-01-flatten-settings.md)
|
||||
|
||||
## Verification results
|
||||
|
||||
Completed on 2026-09-24. The mobile regression first failed with secondary
|
||||
actions separated vertically by 52px. After implementation: 2 mobile and 17
|
||||
desktop Playwright scenarios passed, as did 54 unit tests, typecheck, focused
|
||||
ESLint, localization, public-doc and spec validators, and diff checks. See the
|
||||
[work-order results](task-01-flatten-settings.md#results). Four fresh PR screenshots
|
||||
cover Installed and Browse on phone and desktop with synthetic records; the
|
||||
disposable capture test passed and was removed afterward.
|
||||
The public plugin guide uses the new desktop Installed screenshot and no longer
|
||||
repeats an illustration of the previous layout.
|
||||
|
||||
## Risks
|
||||
|
||||
Row overlay links must not intercept inline actions. Removing padding must not
|
||||
clip long plugin IDs or touch targets. All plugin controls retain their existing
|
||||
permission checks.
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
id: "01-flatten-settings"
|
||||
title: "Flatten plugin settings"
|
||||
status: done
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
requirements:
|
||||
- REQ-UI-CONTROL-SIZING-001
|
||||
acceptance_criteria:
|
||||
- AC-UI-CONTROL-SIZING-001.1
|
||||
- AC-UI-CONTROL-SIZING-001.3
|
||||
- AC-UI-CONTROL-SIZING-001.4
|
||||
- AC-UI-CONTROL-SIZING-001.5
|
||||
- AC-UI-CONTROL-SIZING-001.6
|
||||
- AC-UI-CONTROL-SIZING-001.7
|
||||
- AC-UI-CONTROL-SIZING-001.8
|
||||
system_design:
|
||||
- ../../specs/ui/system-design/control-sizing.md
|
||||
---
|
||||
|
||||
# Task 01: Flatten plugin settings
|
||||
|
||||
## Summary and scope
|
||||
|
||||
Remove redundant frames, place Install beside the heading, keep secondary
|
||||
actions compact, and render installed plugins as a divided list. Own the host
|
||||
settings layout, focused browser regressions, documentation, and PR screenshots.
|
||||
Preserve every existing plugin action and permission boundary. API, SDK,
|
||||
persistence, and plugin implementation changes are out of scope.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Install sits beside the heading; Sync and update checks share a compact row
|
||||
on phones and join the heading on wider screens. Controls stay touch-safe.
|
||||
- Automatic updates and plugin rows are no longer enclosed in nested cards;
|
||||
names, versions, descriptions, state, and all actions remain available.
|
||||
- Existing install, check, disable, settings-navigation, and uninstall flows
|
||||
pass on mobile and desktop, with no horizontal overflow.
|
||||
|
||||
## ASCII UI preview
|
||||
|
||||
UI-01, excerpt of the [combined preview](plan.md#ascii-ui-preview):
|
||||
|
||||
```text
|
||||
Phone Desktop
|
||||
Installed plugins [Install plugin] Installed plugins [Install plugin] Sync Check updates
|
||||
Sync Check for updates Automatic updates [switch]
|
||||
Automatic updates [switch] -----------------------------------------------------
|
||||
-------------------------------- Plugin name / state / version / controls
|
||||
Plugin name / state / controls -----------------------------------------------------
|
||||
--------------------------------
|
||||
```
|
||||
|
||||
The existing settings surface owns vertical scrolling. Keep 44px phone/coarse
|
||||
targets and 28px desktop toolbar controls per the listed acceptance criteria.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
(cd apps/web && pnpm e2e:run --host --project mobile-chrome tests/settings/mobile-plugin-updates.spec.ts tests/plugins/mobile-plugin-settings-row.spec.ts --retries=0)
|
||||
(cd apps/web && pnpm e2e:run --host --no-build --project chromium tests/plugins/plugins.spec.ts --retries=0)
|
||||
(cd apps/web && pnpm exec vitest run app/settings/plugins/page.test.tsx components/settings/plugins/plugin-row.test.tsx)
|
||||
(cd apps/web && pnpm run typecheck)
|
||||
(cd apps/web && pnpm exec eslint components/settings/plugins/plugins-settings.tsx components/settings/plugins/plugin-row.tsx e2e/tests/settings/mobile-plugin-updates.spec.ts e2e/tests/plugins/plugins.spec.ts --max-warnings 0)
|
||||
(cd apps/web && pnpm run i18n:check)
|
||||
node scripts/validate-public-docs.mjs
|
||||
python3 scripts/list-docs.py validate
|
||||
python3 scripts/lint-spec-files.py --all
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/web/components/settings/plugins/plugins-settings.tsx`
|
||||
- `apps/web/components/settings/plugins/plugin-row.tsx`
|
||||
- `apps/web/e2e/tests/settings/mobile-plugin-updates.spec.ts`
|
||||
- `apps/web/e2e/tests/plugins/plugins.spec.ts`
|
||||
- `docs/public/plugins.md`
|
||||
- `docs/screenshots/plugin-settings.png`
|
||||
|
||||
## Dependencies and parallelism
|
||||
|
||||
None. Sequential execution in the primary session.
|
||||
|
||||
## Inputs and risks
|
||||
|
||||
Read the linked control-sizing design, existing settings primitives, and plugin
|
||||
E2E fixtures. Keep overlay links below inline controls; check translated label
|
||||
wrapping and plugin IDs after removing padding.
|
||||
|
||||
## Results
|
||||
|
||||
Completed on 2026-09-24. All commands in Verification passed:
|
||||
|
||||
- Mobile Playwright: 2 passed, including 320px, 767px, and 768px containment and
|
||||
touch-control checks in addition to the canonical phone viewport.
|
||||
- Desktop Playwright: 17 passed, including 28px toolbar geometry, keyboard
|
||||
activation, installation, synchronization, updates, persistence, and uninstall.
|
||||
- Focused Vitest: 54 passed. The existing version assertion now checks the row's
|
||||
combined text because ID and version occupy separate wrapping spans.
|
||||
- Typecheck, focused ESLint, and all localization checks passed.
|
||||
- Public docs: 47 published pages validated. Catalog validation checked 304
|
||||
decisions and 1138 specifications; all specification files passed lint.
|
||||
- Documentation coverage validator: covered; no reference errors.
|
||||
- `git diff --check` passed.
|
||||
|
||||
RED evidence: the original mobile toolbar failed the same-row assertion with
|
||||
Sync at y=314.5 and Check for updates at y=366.5. The final page preserves both
|
||||
44px touch targets while placing these actions side by side.
|
||||
|
||||
A disposable capture spec passed against the production build with isolated,
|
||||
synthetic plugin records. Four compressed screenshots show Installed and Browse
|
||||
on phone and desktop; PR comparison assets are published on a separate media
|
||||
branch. The rendered phone composition matches UI-01. The public plugin guide
|
||||
describes the compact toolbar and flat list and includes the new desktop
|
||||
Installed screenshot. Its duplicate illustration of the previous layout was
|
||||
removed during PR review.
|
||||
@@ -167,14 +167,15 @@ Once installed, the plugin appears in the list with its category, a status
|
||||
badge (`active`), a signing badge (`unsigned` today), and **Disable** and
|
||||
**Uninstall** actions. Selecting the row anywhere opens that plugin's own
|
||||
settings page; a `Setup required` badge marks a plugin whose manifest declares
|
||||
a required setting that has no value yet:
|
||||
|
||||

|
||||
a required setting that has no value yet.
|
||||
|
||||
The Installed tab also gives you an overview of automatic updates, installed
|
||||
versions, available updates, and per-plugin controls:
|
||||
versions, available updates, and per-plugin controls. **Install plugin** sits
|
||||
beside the list heading. **Sync** and **Check for updates** share a compact
|
||||
toolbar, with the automatic-update preference and plugin list below. On phones,
|
||||
the secondary actions wrap below the heading and retain touch-sized targets:
|
||||
|
||||

|
||||

|
||||
|
||||
<details>
|
||||
<summary>Filesystem sideload and synchronization</summary>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 416 KiB After Width: | Height: | Size: 42 KiB |
Reference in New Issue
Block a user