feat(web): order the shipped presets, and edit one on its own screen

The picker listed presets alphabetically by id, so the shipped set read
cordis, minimal, standard — reverse order of capability. A preset may now
declare `order` in its metadata; the shipped three declare 1/2/3 and read
standard, minimal, cordis. A preset that declares none sorts behind those
that do, then by id, so authored presets stay stable.

Editing had nowhere good to live. Inside a card it was squeezed into a
~268px column; hanging off the end of the grid it was orphaned from the card
it edits. It now replaces the list: a back link, what is being edited, and
the form at full width. One thing on screen at a time, which is what the
form's height wanted all along.

Cards in different grid rows sized independently, so a short description made
a short card. `grid-auto-rows: 1fr` makes every row the same height.

The trust badge lost its pill when the card CSS was rewritten, and `In use`
never had one; both are tags now. Icon labels moved from `title` to a drawn
tooltip — the native one waits about a second, which reads as nothing
happening.
This commit is contained in:
Yichen Jiang
2026-08-05 17:04:57 +08:00
parent 4f41608cbc
commit 64fc536982
14 changed files with 202 additions and 45 deletions

View File

@@ -200,14 +200,19 @@ describe('the composition editor', () => {
writable: true, name: '我的预设', description: '', saving: false, error: null,
}
it('opens under the row it belongs to and edits through the controller', () => {
it('replaces the list while editing, and returns to it', () => {
const actions = renderSection({ draft })
const editor = within(rowFor('mine')).getByLabelText(en.composition)
// The form is tall and a card column is ~268px: squeezing it into one is
// unusable, and hanging it off the end orphans it from the card it edits.
expect(screen.queryByRole('heading', { name: en.builtInGroup })).toBeNull()
const editor = screen.getByLabelText(en.composition)
expect(editor).toHaveProperty('value', '- id: tool-read\n')
fireEvent.change(editor, { target: { value: '- id: tool-edit\n' } })
fireEvent.click(screen.getByRole('button', { name: `← ${en.backToList}` }))
expect(actions.setContent).toHaveBeenCalledWith('- id: tool-edit\n')
expect(actions.close).toHaveBeenCalledTimes(1)
})
it('saves and cancels through the controller', () => {
@@ -237,6 +242,20 @@ describe('the composition editor', () => {
expect(screen.getByText(en.close)).toBeTruthy()
})
it('titles the panel by what it is doing', () => {
renderSection({ draft })
expect(screen.getByText(`${en.edit} · 我的预设`)).toBeTruthy()
cleanup()
// An unnamed draft falls back to what it was copied from.
renderSection({ draft: { ...draft, name: '' } })
expect(screen.getByText(`${en.edit} · mine`)).toBeTruthy()
cleanup()
renderSection({ draft: { ...draft, writable: false } })
expect(screen.getByText(`${en.view} · 我的预设`)).toBeTruthy()
})
it('names a new preset and says what it was copied from', () => {
const actions = renderSection({
draft: { ...draft, id: '', source: 'standard', creating: true },