-
Notifications
You must be signed in to change notification settings - Fork 14
feat(ui): inputs TextField/Select/Switch redesign, remove forbidden primitives (slice 05, issue #440) #461
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
57 commits
Select commit
Hold shift + click to select a range
8585ca7
feat(ui): inputs slice 05 — TextField/Select/Switch redesign, remove …
Astro-Han a606681
fix(ui): post-smoke fixes for slice 05 inputs
Astro-Han a634787
fix(ui): smoke feedback — switch thumb, rename dialog, revert review/…
Astro-Han b3bab1d
fix(ui): align select tokens, rename dialog, and in-progress todo spi…
Astro-Han 48b7a68
fix(ui): round-4 smoke — select tokens, dark borders, double-click re…
Astro-Han 0a46ed8
fix(ui): align switch with inputs.html spec; fix todo spinner width jump
Astro-Han 4914fdc
fix(ui): align select trigger with inputs.html spec
Astro-Han 4e9b340
fix(ui): align TextField/Select inputs with inputs.html spec (round 5)
Astro-Han 5413fb9
fix(ui): fix select icon, toast dark mode, and switch track visibility
Astro-Han d1049bc
fix(ui): fix settings trigger text clipping and list search border
Astro-Han 5573fcd
fix(ui): suppress empty section header in ungrouped select
Astro-Han 56feba7
fix(ui): fix select item height for CJK fonts
Astro-Han 6f80c00
fix(ui): fix settings select width, hover, and descender clipping
Astro-Han 33d852b
fix(ui): align select/textfield with preview specs
Astro-Han e691762
Merge remote-tracking branch 'origin/dev' into claude/i440-slice-05-i…
Astro-Han e9c063a
fix(ui): align list search bar with popover-embedded spec
Astro-Han e5e1671
fix(ui): remove divider under list search bar
Astro-Han 2108fd0
fix(ui): lighten select trigger hover and align item to sidebar standard
Astro-Han 4909456
fix(app): align sidebar row hover and active to session-row.html
Astro-Han 1b4013b
test(app): rewrite session-rename e2e for dialog flow
Astro-Han 95ef268
fix(ui): split select item hover and selected into two tiers
Astro-Han 9ba7357
fix(app): split sidebar row hover and active into two tiers
Astro-Han 981df8e
fix(app): align composer pickers to the cross-product hover token
Astro-Han c7a8f2a
fix(ui): bump hover-overlay-warm and add dark mirror
Astro-Han c511126
fix(ui,app): switch hover token to --hover-overlay-warm everywhere
Astro-Han ad8f52c
feat(ui): introduce --surface-list-hover token for two-tier hover
Astro-Han 12a0bf5
fix(ui,app): apply --surface-list-hover everywhere a list row can hover
Astro-Han df2f5e7
chore(opencode): refresh prebuilt models snapshot
Astro-Han a7d0906
feat(ui,app): introduce row overlay tokens, decouple sidebar token, f…
Astro-Han 6cc43e7
feat(ui): introduce picker contract — shared trigger/content/item CSS…
Astro-Han ca4b71b
refactor(ui): wire Select to picker contract
Astro-Han 72eabfd
refactor(ui): wire List to picker contract, make selected check-icon …
Astro-Han 493d517
refactor(app): align composer pickers to picker contract
Astro-Han c8bc205
fix(ui): align Switch primitive to ui_kits desktop mock
Astro-Han 68cc4e1
fix(ui): textfield default background uses --surface-base, not cream
Astro-Han 6820921
fix(app): flatten rename-session dialog — drop cream footer + separator
Astro-Han 7079931
refactor(ui): remove unused review-filter Select variant
Astro-Han 821b65d
fix(app): await rename promise instead of dropping it with void
Astro-Han 22e1257
fix(app): catch onConfirm failure in DialogRenameSession
Astro-Han c7529c8
fix(app): drop redundant aria-haspopup override on variant Select
Astro-Han 0e73287
fix(ui): restore visible focus-visible ring on picker items
Astro-Han 024b5be
fix(ui): scope dark-mode switch thumb override to non-disabled
Astro-Han 085bf18
test(ui): null-guard splitMatch regex in switch checked test
Astro-Han a6ce188
fix(app): use aria-label, drop label="" + hideLabel pattern
Astro-Han 86fe995
test(app): tighten new inputs e2e specs
Astro-Han bf7bbfc
test(ui,app): loosen brittle source-text assertions
Astro-Han 1fd031f
refactor(ui): drop redundant :first-child override on toast action
Astro-Han a190008
fix(app): wire diff-style toggle through review tab
Astro-Han e5bc191
test(app,e2e): align sidebar rename spec with Dialog flow
Astro-Han 1230a8f
feat(app): toast on session rename failure
Astro-Han 49fc07c
test(app,e2e): seed session storage in rename dialog spec
Astro-Han 6d55114
test(app,e2e): match todo items by data-state attribute, not filter
Astro-Han cabaeb2
test(app,e2e): open review tab via overflow menu in diff-style spec
Astro-Han 61af285
fix(ui): restore flex layout for message-part todo items
Astro-Han 4e5ec7f
fix(app): block Escape during rename save to match disabled Cancel
Astro-Han d46719c
test(app,e2e): drop redundant trackSession in review-filter spec
Astro-Han 7154319
test(ui): assert text-field css exposes [data-invalid] selector
Astro-Han File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| /** | ||
| * select-review-filter.spec.ts @smoke | ||
| * | ||
| * Golden-path: review panel diff-style toggle via IconButton pair that replaced RadioGroup. | ||
| * Creates a real file diff, opens the review panel, then verifies the unified/split | ||
| * IconButtons toggle aria-pressed correctly. | ||
| */ | ||
| import type { Page } from "@playwright/test" | ||
| import { openRightPanel, withSession } from "../actions" | ||
| import { test, expect } from "../fixtures" | ||
| import { bodyText } from "../prompt/mock" | ||
|
|
||
| async function openReviewPanel(page: Page) { | ||
| const panel = await openRightPanel(page) | ||
| const tabList = panel.getByRole("tablist").first() | ||
| const reviewTab = tabList.getByRole("tab", { name: "Review", exact: true }) | ||
|
|
||
| if (await reviewTab.isVisible().catch(() => false)) { | ||
| await reviewTab.click() | ||
| } else { | ||
| await tabList.locator("button").last().click() | ||
| await page.getByRole("menuitem", { name: "Review" }).click() | ||
| } | ||
| await expect(reviewTab).toHaveAttribute("aria-selected", "true") | ||
| } | ||
|
|
||
| test("review diff-style toggle switches between unified and split @smoke", async ({ | ||
| page, | ||
| llm, | ||
| project, | ||
| }) => { | ||
| await project.open() | ||
|
|
||
| await withSession(project.sdk, "e2e inputs review filter toggle", async (session) => { | ||
| { | ||
| const PATCH_TEXT = [ | ||
| "*** Begin Patch", | ||
| "*** Add File: review-filter-test.txt", | ||
| "+line one", | ||
| "+line two", | ||
| "*** End Patch", | ||
| ].join("\n") | ||
|
|
||
| const callsBefore = await llm.calls() | ||
| await llm.toolMatch( | ||
| (hit) => bodyText(hit).includes("Your only valid response is one apply_patch tool call."), | ||
| "apply_patch", | ||
| { patchText: PATCH_TEXT }, | ||
| ) | ||
| await project.sdk.session.prompt({ | ||
| sessionID: session.id, | ||
| agent: "build", | ||
| system: [ | ||
| "You are seeding deterministic e2e UI state.", | ||
| "Your only valid response is one apply_patch tool call.", | ||
| `Use this JSON input: ${JSON.stringify({ patchText: PATCH_TEXT })}`, | ||
| "Do not call any other tools.", | ||
| "Do not output plain text.", | ||
| ].join("\n"), | ||
| parts: [{ type: "text", text: "Apply the provided patch exactly once." }], | ||
| }) | ||
|
|
||
| await expect.poll(() => llm.calls().then((c) => c > callsBefore), { timeout: 30_000 }).toBe(true) | ||
| await expect | ||
| .poll( | ||
| async () => { | ||
| const diff = await project.sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? []) | ||
| return diff.length | ||
| }, | ||
| { timeout: 60_000 }, | ||
| ) | ||
| .toBeGreaterThan(0) | ||
|
|
||
| await project.gotoSession(session.id) | ||
| await openReviewPanel(page) | ||
|
|
||
| const unifiedBtn = page.locator('[data-component="session-review"] [aria-label*="unified" i]').first() | ||
| const splitBtn = page.locator('[data-component="session-review"] [aria-label*="split" i]').first() | ||
|
|
||
| await expect(unifiedBtn).toBeVisible({ timeout: 10_000 }) | ||
| await expect(splitBtn).toBeVisible() | ||
|
|
||
| await expect(unifiedBtn).toHaveAttribute("aria-pressed", "true") | ||
| await expect(splitBtn).toHaveAttribute("aria-pressed", "false") | ||
|
|
||
| await splitBtn.click() | ||
| await expect(splitBtn).toHaveAttribute("aria-pressed", "true") | ||
| await expect(unifiedBtn).toHaveAttribute("aria-pressed", "false") | ||
|
|
||
| await unifiedBtn.click() | ||
| await expect(unifiedBtn).toHaveAttribute("aria-pressed", "true") | ||
| await expect(splitBtn).toHaveAttribute("aria-pressed", "false") | ||
| } | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| /** | ||
| * session-rename-dialog.spec.ts @smoke | ||
| * | ||
| * Golden-path: sidebar rename opens a Dialog with a TextField, saves on Enter, | ||
| * and the new title shows up in the sidebar row. | ||
| */ | ||
| import { cleanupSession, openSidebar } from "../actions" | ||
| import { test, expect } from "../fixtures" | ||
| import { pawworkSidebarSelector } from "../selectors" | ||
|
|
||
| test("sidebar rename uses Dialog and saves on Enter @smoke", async ({ page, sdk, gotoSession }) => { | ||
| const stamp = Date.now() | ||
| const session = await sdk.session.create({ title: `Rename dialog test ${stamp}` }).then((r) => r.data) | ||
| if (!session?.id) throw new Error("Session create did not return an id") | ||
|
|
||
| try { | ||
| await gotoSession(session.id) | ||
| await openSidebar(page) | ||
|
|
||
| const sidebar = page.locator(pawworkSidebarSelector).first() | ||
| const row = sidebar.locator(`[data-session-id="${session.id}"]`).first() | ||
|
|
||
| await row.hover() | ||
| await row.locator('[data-action="session-row-menu"]').click() | ||
| await page.getByRole("menuitem", { name: /rename/i }).click() | ||
|
|
||
| const dialog = page.locator('[data-component="dialog"]') | ||
| await expect(dialog).toBeVisible() | ||
|
|
||
| const input = dialog.getByRole("textbox") | ||
| await expect(input).toBeVisible() | ||
| await expect(input).toBeFocused() | ||
|
|
||
| const newTitle = `Renamed dialog ${stamp}` | ||
| await input.fill(newTitle) | ||
| await input.press("Enter") | ||
|
|
||
| await expect(dialog).toBeHidden() | ||
| await expect(sidebar.locator(`[data-session-id="${session.id}"]`)).toContainText(newTitle) | ||
| } finally { | ||
| await cleanupSession({ sdk, sessionID: session.id }) | ||
| } | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| /** | ||
| * todo-toggle.spec.ts @smoke | ||
| * | ||
| * Golden-path: todo dock renders circle / circle-check icons based on todo status, | ||
| * after replacing the Checkbox primitive with Icon in slice 05. | ||
| */ | ||
| import type { Page } from "@playwright/test" | ||
| import { | ||
| composerEvent, | ||
| type ComposerDriverState, | ||
| type ComposerWindow, | ||
| } from "../../src/testing/session-composer" | ||
| import { cleanupSession } from "../actions" | ||
| import { test, expect } from "../fixtures" | ||
|
|
||
| async function driverWrite( | ||
| page: Page, | ||
| sessionID: string, | ||
| driver: ComposerDriverState | undefined, | ||
| ) { | ||
| await page.evaluate( | ||
| (input: { event: string; sessionID: string; driver: ComposerDriverState | undefined }) => { | ||
| const win = window as ComposerWindow | ||
| const composer = win.__opencode_e2e?.composer | ||
| if (!composer?.enabled) throw new Error("Composer e2e driver is not enabled") | ||
| composer.sessions ??= {} | ||
| const prev = composer.sessions[input.sessionID] ?? {} | ||
| if (!input.driver) { | ||
| delete composer.sessions[input.sessionID] | ||
| } else { | ||
| composer.sessions[input.sessionID] = { ...prev, driver: input.driver } | ||
| } | ||
| window.sessionStorage.setItem("__opencode_e2e_composer_sessions", JSON.stringify(composer.sessions)) | ||
| window.dispatchEvent(new CustomEvent(input.event, { detail: { sessionID: input.sessionID } })) | ||
| }, | ||
| { event: composerEvent, sessionID, driver }, | ||
| ) | ||
| } | ||
|
|
||
| test("todo dock shows circle for pending and circle-check for completed items @smoke", async ({ | ||
| page, | ||
| project, | ||
| }) => { | ||
| await page.addInitScript(() => { | ||
| const win = window as ComposerWindow | ||
| const saved = window.sessionStorage.getItem("__opencode_e2e_composer_sessions") | ||
| const sessions = saved ? JSON.parse(saved) : {} | ||
| win.__opencode_e2e = { ...win.__opencode_e2e, composer: { enabled: true, sessions } } | ||
| }) | ||
|
|
||
| await project.open() | ||
|
|
||
| const session = await project.sdk.session.create({ title: `e2e inputs todo icons` }).then((r) => r.data) | ||
| if (!session?.id) throw new Error("Session create did not return an id") | ||
| project.trackSession(session.id) | ||
|
|
||
| try { | ||
| await project.gotoSession(session.id) | ||
|
|
||
| await driverWrite(page, session.id, { | ||
| todos: [ | ||
| { content: "pending task", status: "pending", priority: "medium" }, | ||
| { content: "done task", status: "completed", priority: "high" }, | ||
| { content: "in-progress task", status: "in_progress", priority: "low" }, | ||
| ], | ||
| }) | ||
|
|
||
| const dock = page.locator('[data-component="session-todo-dock"]') | ||
| await expect(dock).toBeVisible({ timeout: 10_000 }) | ||
|
|
||
| const toggleBtn = dock.locator('[data-action="session-todo-toggle"]').first() | ||
| await toggleBtn.click() | ||
|
|
||
| const list = dock.locator('[data-slot="session-todo-list"]') | ||
| await expect(list).toBeVisible({ timeout: 5_000 }) | ||
|
|
||
| const items = list.locator('[data-slot="session-todo-item"]') | ||
| await expect(items).toHaveCount(3, { timeout: 5_000 }) | ||
|
|
||
| const pendingItem = list.locator('[data-slot="session-todo-item"][data-state="pending"]').first() | ||
| const completedItem = list.locator('[data-slot="session-todo-item"][data-state="completed"]').first() | ||
|
|
||
| await expect(pendingItem.locator('[data-component="icon"][data-size]')).toBeVisible() | ||
| await expect(completedItem.locator('[data-component="icon"][data-size]')).toBeVisible() | ||
|
|
||
| await driverWrite(page, session.id, undefined) | ||
| } finally { | ||
| await cleanupSession({ sdk: project.sdk, sessionID: session.id }) | ||
| } | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import { createSignal } from "solid-js" | ||
| import { Dialog } from "@opencode-ai/ui/dialog" | ||
| import { Button } from "@opencode-ai/ui/button" | ||
| import { TextField } from "@opencode-ai/ui/text-field" | ||
| import { useDialog } from "@opencode-ai/ui/context/dialog" | ||
| import { showToast } from "@opencode-ai/ui/toast" | ||
| import { useLanguage } from "@/context/language" | ||
|
|
||
| export function DialogRenameSession(props: { | ||
| name: string | ||
| onConfirm: (name: string) => Promise<void> | void | ||
| }) { | ||
| const language = useLanguage() | ||
| const dialog = useDialog() | ||
| const [value, setValue] = createSignal(props.name) | ||
| const [saving, setSaving] = createSignal(false) | ||
|
|
||
| const handleSave = async () => { | ||
| const next = value().trim() | ||
| if (!next || saving()) return | ||
| setSaving(true) | ||
| try { | ||
| await props.onConfirm(next) | ||
| dialog.close() | ||
| } catch { | ||
| showToast({ | ||
| title: language.t("toast.session.rename.failed.title"), | ||
| description: language.t("toast.session.rename.failed.description"), | ||
| variant: "error", | ||
| }) | ||
| } finally { | ||
| setSaving(false) | ||
| } | ||
| } | ||
|
Astro-Han marked this conversation as resolved.
|
||
|
|
||
| return ( | ||
| <Dialog title={language.t("session.rename.title")} fit class="w-full max-w-[420px] mx-auto"> | ||
| <div class="px-6 pt-2 pb-6"> | ||
| <TextField | ||
| aria-label={language.t("session.rename.title")} | ||
| autofocus | ||
| value={value()} | ||
| onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) => | ||
| setValue(e.currentTarget.value) | ||
| } | ||
| onKeyDown={(e: KeyboardEvent & { currentTarget: HTMLInputElement }) => { | ||
| if (e.key === "Enter") { | ||
| e.preventDefault() | ||
| void handleSave() | ||
| } | ||
| if (e.key === "Escape") { | ||
| e.preventDefault() | ||
| if (saving()) return | ||
| dialog.close() | ||
| } | ||
| }} | ||
| onFocus={(e: FocusEvent & { currentTarget: HTMLInputElement }) => | ||
| e.currentTarget.select() | ||
| } | ||
| /> | ||
| </div> | ||
| <div class="flex justify-end gap-2 px-6 pb-6"> | ||
| <Button variant="secondary" onClick={() => dialog.close()} disabled={saving()}> | ||
| {language.t("common.cancel")} | ||
| </Button> | ||
| <Button | ||
| variant="primary" | ||
| onClick={handleSave} | ||
| disabled={saving() || !value().trim()} | ||
| > | ||
| {language.t("common.save")} | ||
| </Button> | ||
| </div> | ||
| </Dialog> | ||
| ) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.