Skip to content

feat(ui-web): draw each settings section's own shape while it waits - #682

Merged
gloryfromca merged 1 commit into
mainfrom
feat/settings_page_skeletons
Sep 23, 2026
Merged

gloryfromca merged 1 commit into
mainfrom
feat/settings_page_skeletons

Conversation

@arelchan

Copy link
Copy Markdown
Contributor

Summary

Every settings section waited behind the same dashed box with "Loading" in it.
That box is a line of display text and 34px of padding, so the panel was a 90px
strip that became whatever the section is the moment the snapshot landed, and a
section is rarely one line: the usage page is four cards and a chart, the
provider catalogue is a full-height two-column grid.

The waits the pages own drew even less. The usage totals and the archive list
each replaced a card's worth of rows with a single grey word inside it, and the
card shrank to fit the word. Channels, schedules and the schedules' run history
drew nothing at all, and an empty column beside an empty pane is what "there are
no channels" looks like.

So a section draws its own anatomy instead, in the section's own card, row and
list classes: the skeleton is the page with its words replaced by bars, which is
what makes the swap a fill-in rather than a resize. Twelve places in all:

  • the settings panel, with a shape per section (general, usage, providers,
    models, skills, tools, plugins, archive, about);
  • the onboarding wizard's two step bodies, which wait on the same snapshot;
  • the five a page or a drawer fetches for itself: usage totals, archive list,
    skill body, plugin credential fields, vendor model list;
  • the four lists the two-pane frame holds (channels, schedules, memory) and the
    schedules' run history.

Key decisions:

  • The usage range picker stays live through the page's own wait: the range is
    the reader's state, not the counter's, so only what the counter answers is
    drawn as bars.
  • The vendor list keeps the line naming the vendor being asked, with the rows
    under it: a shape cannot say which vendor.
  • The bar is the one shape added, and it is added three times. A class belongs
    to one namespace (scripts/check-class-namespace.mjs) and the existing
    skel/sk pair is pinned at the two domains that already name it, so the
    settings sheet, the two-pane frame and the cron sheet each carry their own.
    All three are a translucent wash rather than a named surface, because these
    sit on the card, on the catalogue's rail and inside the model popover and no
    one named colour reads against all three.
  • Channels and schedules draw the wait in the unentered state too: the
    condition is the same !loaded && !rows.length either way, the box is hidden
    until the section opens, and opening it starts the load. This is what grows
    both boot goldens by the channels list's wait.

Also here:

  • _resetForTests clears the settings store's in-flight load. A case that
    mounted the dialog on a load it never resolved left refresh handing the same
    promise to every later caller, so one failed assertion turned the four cases
    after it into timeouts.
  • The run history says the schedules' own word (gui.cron.reading) rather than
    the hub's. gui.hub is said by nothing now, so its ALIAS row in the i18n gate
    goes.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

  • npm test --prefix ui-web -- 194 files, 2695 tests, all pass

  • make lint-ui -- gen:check, eslint and tsc clean (4 pre-existing
    react-hooks/exhaustive-deps warnings, none in changed files)

  • npm run lint:i18n --prefix ui-tui -- generated catalogue up to date

  • npm run --prefix ui-web build && python3 ui-web/build.py -- both boot
    goldens match (264 and 263 nodes)

  • node ui-web/scripts/check-page.mjs, check-css.mjs,
    check-class-namespace.mjs -- all OK

  • Mutation check: 14 mutations applied one at a time (each wait reverted to its
    old word or to null, each shimmer and each reduced-motion rule removed, one
    section's shape deleted so it falls through to the default). Every one turns a
    case red.

  • Real browser: every wait photographed in light and dark through Playwright on
    the vite dev page, with the fixture transport's replies held open so the wait
    states stay on screen.

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

User-visible: every settings wait changes from a word to a shape. No behaviour,
no state and no wire shape moves; what a section draws once its values are in is
unchanged. The three new bar classes are additive and scoped to their own
sheets, and all three hold still under prefers-reduced-motion.

Two changes reach past the waits: the settings store's test-only reset now
clears its in-flight load, and the i18n gate loses the gui.hub ALIAS row that
nothing measures any more.

Rollback: revert the single commit. Nothing is persisted and no generated file
other than the two boot goldens and the TUI catalogue moves.

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

Related Issues

N/A

Every settings section waited behind the same dashed box with "Loading"
in it. That box is a line of display text and 34px of padding, so the
panel was a 90px strip that became whatever the section is the moment
the snapshot landed, and a section is rarely one line: the usage page is
four cards and a chart, the provider catalogue is a full-height
two-column grid.

The waits the pages own drew even less. The usage totals and the archive
list each replaced a card's worth of rows with a single grey word inside
it, and the card shrank to fit the word. Channels, schedules and the
schedules' run history drew nothing at all, and an empty column beside
an empty pane is what "there are no channels" looks like.

So a section draws its own anatomy instead, in the section's own card,
row and list classes: the skeleton is the page with its words replaced
by bars, which is what makes the swap a fill-in rather than a resize.
Twelve places in all: the settings panel, with a shape per section; the
onboarding wizard's two step bodies; the five a page or a drawer fetches
for itself (usage totals, archive list, skill body, plugin credential
fields, vendor model list); and the four lists the two-pane frame and
the run history hold.

The bar is the one shape added, and it is added three times. A class
belongs to one namespace (scripts/check-class-namespace.mjs) and the
existing skel/sk pair is pinned at the two domains that already name it,
so the settings sheet, the two-pane frame and the cron sheet each carry
their own. All three are a translucent wash rather than a named surface,
because these sit on the card, on the catalogue's rail and inside the
model popover and no one named colour reads against all three; all three
hold still under prefers-reduced-motion, which the new
scripts/gates/wait-shimmer-css.test.mjs pins along with the shimmer.

Also here:

- _resetForTests clears the settings store's in-flight load. A case that
  mounted the dialog on a load it never resolved left refresh handing
  the same promise to every later caller, so one failed assertion turned
  the four cases after it into timeouts.
- The run history says the schedules' own word (gui.cron.reading) rather
  than the hub's. gui.hub is said by nothing now, so its ALIAS row goes.
- Both boot goldens grow the channels list's wait: that box is mounted
  and unentered in the settled boot tree, and hidden.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

Reviewed the complete one-commit diff against the refreshed github/main, the affected settings/two-pane/cron callers, and the relevant history. I also checked the repository rules and Web UI architecture vocabulary, backward compatibility of the replaced loading states, generated i18n and boot-golden changes, and that tests were added without weakening prior assertions.

Verification:

  • focused loading-state suites: 122 passed
  • full ui-web suite: 2,695 passed
  • npm run type-check: passed
  • npm run lint: passed with four existing hook warnings and no errors
  • production build, both boot snapshots, and check-page: passed
  • TUI generated i18n catalogue check: passed
  • source-language, large-file, and diff-whitespace checks: passed

@arelchan
arelchan requested a review from 0xKT September 23, 2026 03:35
@gloryfromca
gloryfromca merged commit 03303a3 into main Sep 23, 2026
23 checks passed
@gloryfromca
gloryfromca deleted the feat/settings_page_skeletons branch September 23, 2026 03:36
LivXue pushed a commit that referenced this pull request Oct 12, 2026
…682)

## Summary

Every settings section waited behind the same dashed box with "Loading"
in it.
That box is a line of display text and 34px of padding, so the panel was
a 90px
strip that became whatever the section is the moment the snapshot
landed, and a
section is rarely one line: the usage page is four cards and a chart,
the
provider catalogue is a full-height two-column grid.

The waits the pages own drew even less. The usage totals and the archive
list
each replaced a card's worth of rows with a single grey word inside it,
and the
card shrank to fit the word. Channels, schedules and the schedules' run
history
drew nothing at all, and an empty column beside an empty pane is what
"there are
no channels" looks like.

So a section draws its own anatomy instead, in the section's own card,
row and
list classes: the skeleton is the page with its words replaced by bars,
which is
what makes the swap a fill-in rather than a resize. Twelve places in
all:

- the settings panel, with a shape per section (general, usage,
providers,
  models, skills, tools, plugins, archive, about);
- the onboarding wizard's two step bodies, which wait on the same
snapshot;
- the five a page or a drawer fetches for itself: usage totals, archive
list,
  skill body, plugin credential fields, vendor model list;
- the four lists the two-pane frame holds (channels, schedules, memory)
and the
  schedules' run history.

Key decisions:

- The usage range picker stays live through the page's own wait: the
range is
the reader's state, not the counter's, so only what the counter answers
is
  drawn as bars.
- The vendor list keeps the line naming the vendor being asked, with the
rows
  under it: a shape cannot say which vendor.
- The bar is the one shape added, and it is added three times. A class
belongs
to one namespace (`scripts/check-class-namespace.mjs`) and the existing
`skel`/`sk` pair is pinned at the two domains that already name it, so
the
settings sheet, the two-pane frame and the cron sheet each carry their
own.
All three are a translucent wash rather than a named surface, because
these
sit on the card, on the catalogue's rail and inside the model popover
and no
  one named colour reads against all three.
- Channels and schedules draw the wait in the unentered state too: the
condition is the same `!loaded && !rows.length` either way, the box is
hidden
until the section opens, and opening it starts the load. This is what
grows
  both boot goldens by the channels list's wait.

Also here:

- `_resetForTests` clears the settings store's in-flight load. A case
that
mounted the dialog on a load it never resolved left `refresh` handing
the same
promise to every later caller, so one failed assertion turned the four
cases
  after it into timeouts.
- The run history says the schedules' own word (`gui.cron.reading`)
rather than
the hub's. `gui.hub` is said by nothing now, so its ALIAS row in the
i18n gate
  goes.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

- `npm test --prefix ui-web` -- 194 files, 2695 tests, all pass
- `make lint-ui` -- gen:check, eslint and tsc clean (4 pre-existing
  react-hooks/exhaustive-deps warnings, none in changed files)
- `npm run lint:i18n --prefix ui-tui` -- generated catalogue up to date
- `npm run --prefix ui-web build && python3 ui-web/build.py` -- both
boot
  goldens match (264 and 263 nodes)
- `node ui-web/scripts/check-page.mjs`, `check-css.mjs`,
  `check-class-namespace.mjs` -- all OK
- Mutation check: 14 mutations applied one at a time (each wait reverted
to its
old word or to `null`, each shimmer and each reduced-motion rule
removed, one
section's shape deleted so it falls through to the default). Every one
turns a
  case red.
- Real browser: every wait photographed in light and dark through
Playwright on
the vite dev page, with the fixture transport's replies held open so the
wait
  states stay on screen.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

## Risk

User-visible: every settings wait changes from a word to a shape. No
behaviour,
no state and no wire shape moves; what a section draws once its values
are in is
unchanged. The three new bar classes are additive and scoped to their
own
sheets, and all three hold still under `prefers-reduced-motion`.

Two changes reach past the waits: the settings store's test-only reset
now
clears its in-flight load, and the i18n gate loses the `gui.hub` ALIAS
row that
nothing measures any more.

Rollback: revert the single commit. Nothing is persisted and no
generated file
other than the two boot goldens and the TUI catalogue moves.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

N/A

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants