Skip to content

feat(setup): add HeroUI step indicator + fullscreen layout to setup wizard#22

Open
dingyi wants to merge 14 commits into
mainfrom
feature/setup-wizard-stepper-heroui
Open

feat(setup): add HeroUI step indicator + fullscreen layout to setup wizard#22
dingyi wants to merge 14 commits into
mainfrom
feature/setup-wizard-stepper-heroui

Conversation

@dingyi

@dingyi dingyi commented May 1, 2026

Copy link
Copy Markdown
Owner

Summary

The setup wizard at `/setup` had two visible problems:

  1. No step indicator. `SetupWizard` defined a `steps` array with title, description, and icon for each of the four phases (database check → admin account → usage mode → review), but never rendered it. Users only saw the current step's body content with no idea where they were in the flow or how many steps remained.
  2. Global console header leaked into `/setup`. `PageLayout` rendered `HeaderBar` (and 系统公告 / 主题 / 语言 buttons + the 首页/控制台/模型广场/文档/关于 nav links) above the wizard, even though the user has no session and the console isn't accessible yet. Visually loud, semantically nonsensical.

Before / After

The wizard step body was already HeroUI-native (Cards, Chips, Lucide icons, no Semi imports anywhere). This PR fills in the missing chrome.

Before After
Only the current step body, navbar floating above 4-step indicator above the body, no navbar
User can't tell they're in step 2 of 4 Step 1 ✓ → 2 active → 3,4 pending

Changes

1. `SetupWizard.jsx` — add `SetupStepper` component

Renders the existing `steps` metadata as a 4-column grid:

  • Active: primary border + `ring-2 ring-primary/20`, circle holds the step icon
  • Completed: `border-primary/40 bg-primary/5`, circle holds a check glyph
  • Pending: muted border + neutral surface, circle holds the step number

`sr-only` status labels (`已完成` / `进行中` / `待处理`) make the indicator screen-reader friendly without visual clutter.

Mirrors the visual language of `web/default/src/features/setup/setup-wizard.tsx` but in HeroUI / Tailwind tokens consistent with the rest of `web/classic`. Per the "web/default ownership: maintained" decision in `TODO.md`, default's setup wizard already ships this UX — this brings classic to parity.

Also retunes the outer container layout: drops the `pt-28` and `min-h-[calc(100vh-11rem)]` that were dodging a now-hidden header, switches to `min-h-dvh py-12` for proper viewport-centred placement regardless of card height.

2. `PageLayout.jsx` — short-circuit `/setup` to a minimal shell

When `location.pathname === '/setup'`:

  • No `HeaderBar`
  • No `SiderBar`
  • No console page-header row
  • No `FooterBar`

The wizard's own card already provides background gradients + branding, so dropping the chrome avoids the visual clash and keeps onboarding focused.

3. i18n

Adds `待处理` key across all 8 locale files (`Pending` / `待处理` / `待處理` / `En attente` / `待機中` / `В ожидании` / `Đang chờ`). `已完成` and `进行中` were already present.

Verification

Smoke-tested against local dev (`make dev-api && make dev-web-classic`):

  • `/setup` loads with no global navbar
  • Step indicator shows 4 cards with current step highlighted
  • Clicking 下一步 advances both the body AND the indicator (Step 1 ✓, Step 2 active)
  • Step 2 → 3 transition (admin account → usage mode) works
  • Step 3 → 4 transition (usage mode → review) works
  • Final 初始化系统 button still triggers the existing onSubmit flow
  • `bun run build` clean (`built in 14.53s`)

Diff

```
web/classic/src/components/layout/PageLayout.jsx | 24 ++++++-
web/classic/src/components/setup/SetupWizard.jsx | 87 ++++++++++++++++++++++--
web/classic/src/i18n/locales/*.json | 24 +++++--
10 files changed, 122 insertions(+), 13 deletions(-)
```


This PR was generated automatically by Cursor

Made with Cursor

dingyi added 14 commits May 1, 2026 15:17
…izard

The setup wizard had defined a `steps` array with title/description/icon
for each phase, but never rendered it — users had no way to tell where
they were in the 4-step flow without reading the body content. The
classic frontend's global header was also leaking into /setup, putting
console nav links above an onboarding screen the user couldn't act on
yet (no session, no permissions resolved).

Two changes, one visual story:

1. **`SetupWizard.jsx`**: adds a `SetupStepper` component that renders
   the existing `steps` metadata as a 4-column grid above the step
   body. Three states per item — active (primary border + ring + step
   icon in the circle), completed (primary tint + check glyph), pending
   (muted border + step number). Mirrors the v3 default-frontend
   pattern (`web/default/.../setup-wizard.tsx`) but uses HeroUI / Tailwind
   tokens consistent with the rest of `web/classic`. `sr-only` status
   labels (已完成 / 进行中 / 待处理) make it screen-reader friendly.

   Also retunes the outer container: drops the `pt-28` that was dodging
   a now-hidden header, switches to `min-h-dvh py-12` so the card
   centres around the viewport midpoint regardless of card height.

2. **`PageLayout.jsx`**: detects `/setup` and short-circuits to a
   minimal shell — no `HeaderBar`, no `SiderBar`, no console
   page-header row, no footer. The wizard owns the entire viewport,
   which matches the onboarding intent and avoids visually-loud nav
   chrome floating above an unauthenticated session.

i18n: adds `待处理` key (one per locale × 8 locales). `已完成` and
`进行中` were already present.

Build: clean (`built in 14.53s`).
Made-with: Cursor
The selected usage-mode card was rendering an empty primary-tinted
square — DOM had the lucide SVG and the right `text-white`, but the
icon container's background was transparent, so the white SVG sat
invisibly on the card surface beneath it.

Root cause: this fork's Tailwind v3 JIT is not emitting any
`bg-primary` / `border-primary` / `bg-success` rules into the dev
bundle. Verified by `grep -c bg-primary web/classic/dist/assets/*.css`
returning 0. The Tailwind config maps `primary -> var(--app-primary)`
correctly; something in the toolchain (likely a class-name collision
with one of the HeroUI / HeroUI-Pro packages) is suppressing the
generated utilities at build time.

Fix: bypass the broken utilities by setting the same CSS variable
inline. Same convention `web/classic/src/index.css`'s `.button--primary`
uses; works in both light and dark themes.

Two surfaces touched:

1. `UsageModeStep.jsx` — selected card icon container fills with
   `var(--app-primary)`, foreground `#fff` (chosen as theme-neutral —
   tying SVG colour to `--app-background` would invert with the
   theme and tank contrast on one side).

2. `SetupWizard.jsx` `SetupStepper` — active/completed step circle
   gets the same inline-style treatment for `border-color`,
   `background-color`, plus the active card's outer
   `border-color` / `box-shadow` ring. Pending step keeps its
   Tailwind classes since `bg-surface-secondary` and `border-border`
   do emit correctly.

Verified in dev with chrome-devtools `getComputedStyle`:
  iconBox_bg: oklch(0.8832 0.1488 196.96)  ← cyan ✓
  svg_stroke: rgb(255, 255, 255)            ← white ✓

Made-with: Cursor
Cyan fill + white SVG technically had enough contrast, but visually
clashed with this fork's `.button--primary` convention (cyan + dark
text — see the 下一步 button on the same screen). User feedback:
"icon 还是黑色比较好看".

Switched the selected-state icon `color` from `#fff` to `#0a0a0a`.
Stays a hardcoded value (not `var(--app-foreground)`) because
`--app-foreground` flips with the theme, but the cyan fill behind
the glyph stays the same in both themes — so a theme-dependent
glyph colour would invert one of them.

`SetupStepper`'s active-step circle keeps the white glyph: the
stepper circle is much smaller (h-7 vs h-12) and the white check on
cyan reads more crisply at that size. Also matches the typical
"primary chip" pattern (white check on cyan) the rest of the fork
already uses.

Made-with: Cursor
Stepper active/completed circle was still white-on-cyan from the
earlier "small ring reads better with white check" judgement; the
selected mode-card icon was just changed to dark-on-cyan. User
wants both consistent — switching the stepper to the same
`#0a0a0a` glyph colour.

Made-with: Cursor
The complete step's "准备完成初始化" card was using a louder visual
treatment than every other step:
  - h-14 (vs the h-12 the wizard uses elsewhere) icon on a saturated
    emerald gradient
  - chip stacked above a text-2xl title
  - chip / title / description not sharing a left edge

Layout now mirrors DatabaseStep's info-card pattern: h-12 filled icon
on the left, an `items-start` flex row, then a heading + inline chip
on one line with the description directly under. Container drops the
135deg gradient for a flat tinted card (emerald-50 / emerald-950) so
it sits quietly above the three summary cards instead of competing
with them.

Visual hierarchy preserved:
  - emerald icon + chip still flag the "ready to initialize" semantic
  - the cyan "初始化系统" CTA at the bottom now wins the eye, which
    matches what the screen is asking the user to do

Made-with: Cursor
Reproducer: user opens /setup in tab A, walks to admin step, leaves
the tab. In tab B (or via curl, devtools, another session) someone
finishes setup. Tab A's cached `setupStatus` still reads status:false
and root_init:false, so the user keeps clicking 下一步 to step 4 and
hits 初始化系统 — at which point backend rejects with success:false +
"系统已经初始化完成" because `constant.Setup` is already true.

The user is then stuck on a wizard where every action errors out and
the only escape is to manually navigate to /, which is not obvious.

Fix: when /api/setup POST comes back with success:false, re-poll
/api/setup. If the GET now reports status:true, treat the POST
rejection as a "race lost — already done" and redirect quietly with
a 系统已初始化,正在跳转... toast instead of surfacing the raw
already-completed error. Pure-frontend recovery; doesn't change
backend semantics or weaken any other failure paths (genuine
validation errors still reach the original showError).

Also converts onSubmit from a Promise chain to async/await so the
re-poll branch stays linear and readable.

i18n: adds 系统已初始化,正在跳转... across all 8 locale files.
Made-with: Cursor
Short pages (home, pricing, about) were rendering with the footer
mid-screen, floating above empty viewport space. <main> was a plain
block container with `overflow-y-auto`, so when its content height
was less than the visible height the footer just sat right below
the content with whitespace underneath.

Convert <main> to a flex column and let the inner content wrapper
take `flex-1` — that pushes the footer to the bottom edge whenever
content is shorter than the viewport, while still letting it scroll
naturally on long pages. Footer also gets `shrink-0` so it doesn't
get squeezed when content is tall enough to need scrolling.

Console routes are unaffected (their footer is hidden by
`shouldHideFooter`); same for the cardProPages / setup minimal
shell.

Made-with: Cursor
The About page (especially in its empty / loading states where the
card is only ~340px tall) was hugging the top of the viewport with
a large empty band below, looking off-balance. Adding
`flex min-h-full flex-col justify-center` to the shared PAGE_SHELL
class centres the inner card vertically in main's available height
without disturbing the existing horizontal `mx-auto + max-w-3xl`
constraint. Works with the recent layout fix that turned <main>
into a flex column with a `flex-1` content wrapper.

Made-with: Cursor
Same family of bug as the About-page centring fix — the auth shell
(login / register / password reset / oauth callback) was using
hard-coded `min-h-[calc(100vh-108px)]` + `mt-[60px]` to dodge what
used to be a fixed top header. After PageLayout switched to a flex
column with the header as a shrink-0 sibling and <main> as flex-1,
those magic numbers stretched the auth shell *taller* than its
parent — the form sat top-aligned with empty space below it AND the
footer got pushed below the fold of `/login?expired=true`.

Outer wrapper now uses `min-h-full flex flex-col` so it fills <main>'s
available height exactly, and the inner column uses `flex-1` instead
of viewport math. Net result:
  - auth form vertically centred in the available height
  - footer stays pinned to the viewport bottom on every auth route

Made-with: Cursor
Logged-out navbar was rendering hand-rolled `<span>`-styled chips for
登录 / 注册 — one a tertiary surface, the other a `bg-primary` chip
that wasn't even rendering correctly because of this fork's
JIT-doesn't-emit-bg-primary issue (same root cause as the setup
wizard icon fix earlier in this branch).

Changes:
- 登录 chip → HeroUI `<Button variant='primary' size='sm'>` with
  `onPress` navigate. Now matches the visual language of every
  other primary CTA in the app (设置关于 / 切换到新版前端 / 下一步).
- 注册 chip removed. Per request — keeps the navbar's
  unauthenticated state to a single CTA. The login screen still
  links to /register inline when sign-ups are allowed, so register
  remains reachable just one click deeper.
- `isSelfUseMode` prop removed from `UserArea` and `ActionButtons`
  signatures plus their call sites — it was only used to gate the
  now-deleted register button. `HeaderLogo`'s `isSelfUseMode`
  consumer (mode chip) is unaffected.

Made-with: Cursor
The hand-rolled implementation was a `<label>` with a `<span>`
caption, an absolute-positioned icon overlay, and an Input with
manual `focus:ring-4 focus:ring-primary/10` plus a `border-border`.
On focus that ring drew on top of the existing 1px border, looking
like the border had thickened to ~5px instead of getting a focus
treatment — which is what the user spotted as "border 尺寸不对".

Replace with the HeroUI v3 stack the docs recommend for this
exact composition:

  TextField
   └─ Label
   └─ InputGroup
        ├─ InputGroup.Prefix (the leading icon)
        ├─ InputGroup.Input
        └─ InputGroup.Suffix (the optional action slot)

Wins:
- Focus ring is owned by InputGroup's `[data-focus-within]` styling
  and matches every other input in the app instead of getting a
  custom 4px ring.
- Icon prefix is a real prefix slot, not an absolute overlay; spacing
  between icon and text is layout-driven, no `pl-10` hacks.
- TextField owns the label-for-input a11y wiring (id correlation,
  aria-required, aria-invalid) — the previous `<label>` wrap relied
  on click-to-focus only.

Bridge layer keeps existing callers (LoginForm, PasswordResetConfirm,
TwoFAVerification — they all do `event.target.value` in their
onChange) working unchanged: HeroUI gives us `(value: string)` and
we synthesise the legacy event shape before forwarding.

Made-with: Cursor
Audited the auth card surface after the InputGroup migration —
remaining plain HTML primitives were the brand name (`<div>`),
the bottom 没有账户/注册 link (`<a>`), and the user-agreement /
privacy-policy links inside the agreement checkbox (also `<a>`).

Replacements:
- `AuthBrand` system name → `<Text as='span'>` so it picks up the
  design system's text token defaults instead of an unstyled div
- `AuthLinkRow` route → HeroUI `<Link>` (same anchor semantics, but
  consistent with the rest of the design system's text + link
  components and inheriting proper hover/focus rings)
- `AuthAgreement` user agreement / privacy policy anchors →
  `<Link>` with the same `target` / `rel` attributes preserved

Card title + subtitle stay as `<h1>` / `<p>` rather than swapping
to `CardTitle` / `CardDescription` — those subcomponents come with
their own default padding/typography that would conflict with the
existing `Card` `px-5 py-6` shell, and the visual outcome would be
identical with more wiring. Keeping the simpler form.

Title / subtitle are also a-okay as semantic `<h1>` for screen
readers; CardTitle would render as `<h3>` by default which is
worse for the auth landing page's heading outline.

Made-with: Cursor
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.

1 participant