feat(setup): add HeroUI step indicator + fullscreen layout to setup wizard#22
Open
dingyi wants to merge 14 commits into
Open
feat(setup): add HeroUI step indicator + fullscreen layout to setup wizard#22dingyi wants to merge 14 commits into
dingyi wants to merge 14 commits into
Conversation
…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
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
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
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
The setup wizard at `/setup` had two visible problems:
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.
Changes
1. `SetupWizard.jsx` — add `SetupStepper` component
Renders the existing `steps` metadata as a 4-column grid:
`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'`:
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`):
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