Skip to content

HeroUI v3 console refactor: tables, modals, drawers, density#16

Closed
dingyi wants to merge 3 commits into
mainfrom
feature/heroui-v3-console-refactor
Closed

HeroUI v3 console refactor: tables, modals, drawers, density#16
dingyi wants to merge 3 commits into
mainfrom
feature/heroui-v3-console-refactor

Conversation

@dingyi

@dingyi dingyi commented Apr 30, 2026

Copy link
Copy Markdown
Owner

Summary

End-to-end migration of every console list page (tokens, channels, users, redemptions, subscriptions, models, usage-logs, model-pricing) and the related modals/drawers/settings pages onto HeroUI v3 + canonical anatomy. The goal: enforce visual parity through shared primitives instead of copy-paste, and standardize the row rhythm, density, button hierarchy, and overlay behavior across the entire admin.

What changed

1. Shared infrastructure (commit 1)

  • New HeroTable wrapper around HeroUI Table — sticky-right operations column, selection adapter (Antd-style ↔ React Aria Set), nested dataIndex support, stable string keys to dodge React Aria reconciliation errors, inline empty state via renderEmptyState.
  • New SideSheet wrapper around HeroUI Drawer — uses React Portal to escape parent backdrop-filter containing blocks (the bug behind every "modal leaks into card" report), plus a CSS custom property for reliable dynamic widths under Tailwind v4 JIT.
  • ConfirmDialog / ColumnSelectorDialog / CardPro / TableFilterForm / TablePagination / JSONEditor: switch to canonical HeroUI Modal anatomy (ModalHeading wraps title text), full Checkbox anatomy, --field-background token, whitespace-nowrap on tabs, etc.
  • Global CSS:
    • body font-size → 12px (per-component density was already targeting this)
    • .button--sm / .toggle-button--sm → 12px
    • .switch__thumb locked to white in both states (HeroUI default flips to near-black on cyan accent)
    • Published .ct-compact-table / .ct-compact-alert / .ct-compact-select utility classes — replaces Tailwind v4 arbitrary BEM selectors that silently mangled __ separators into descendant spaces

2. List pages (commit 2)

  • 6 admin tables (tokens / channels / users / redemptions / subscriptions / models) all sit on HeroTable now, with consistent ~44px rows and 11px compact operation buttons (!h-7 !px-2.5 !text-[11px]) under inline-flex whitespace-nowrap.
  • Vendor tab strips on /console/channel + /console/models rewritten as ToggleButtonGroup + ToggleButton split-pill, each toggle paired with a ClickMenu for vendor edit/delete actions. !justify-start overrides HeroUI's default centered group so wrapped tabs left-align.
  • Every "Add X" CTA standardized on variant='primary' (cyan + black text). Destructive batch actions on variant='danger-soft'.
  • Edit modals migrated to the shared SideSheet: drop IconTile decorations, drop <X> / <Save> icons from footer buttons, replace native inputs/selects with HeroUI Input / Select / TextArea / InputGroup, swap manual chip badges for shared StatusChip.
  • /console/models actions and filters split into CardPro's dedicated actionsArea and searchArea slots — they used to fight for one flex row at medium widths.
  • /console/subscription and /console/models page-level notices use HeroUI Alert (status='warning' + AlertTriangle + ct-compact-alert).
  • Column headers globally inherit whitespace-nowrap from HeroTable so titles like "已用/剩余" no longer wrap mid-character.

3. Settings (commit 3)

  • /console/setting top-level tab strip rewritten as ToggleButtonGroup (replaces hand-rolled <button> pills with no pointer cursor and a black-pill selected state).
  • 3 settings pages (SettingGlobalModel, two payment gateways) replace custom warning banners with HeroUI Alert.
  • GroupRatioSettings "分组设置使用说明" sidebar moves to shared SideSheet.
  • SecureVerificationModal title wrapped in ModalHeading for proper heading sizing.

Test plan

  • /console/token: rows ~44px, sticky right ops column, edit modal opens/closes via SideSheet
  • /console/channel: vendor tab split-pill, "Add channel" cyan/black primary, "Delete selected channels" red soft
  • /console/user: AddUser / EditUser modals render, table rows compact
  • /console/redemption: add/edit modal placement (left for new, right for edit), date picker works
  • /console/subscription: warning Alert next to "Create plan" vertically centered, table empty state inline
  • /console/models: actions row, search row, vendor tabs, model edit/delete confirm dialog title in ModalHeading
  • /console/log: filters + Select all line up pixel-equal in light AND dark mode
  • /console/setting: tab pills cyan accent-soft when selected, pointer cursor, wrapped tabs left-align
  • Switch (settings page): white thumb in both on/off states
  • Confirm any delete action: title 16px medium, body 14px muted, red Delete button

This PR was generated automatically by Cursor

Made with Cursor

dingyi added 3 commits April 30, 2026 10:30
Build the foundation every console list page sits on so per-page glue
stays thin and visual parity is enforced through shared primitives
instead of copy-paste.

New shared components

- HeroTable: HeroUI Table wrapper with sticky-right operations column,
  selection adapter (Antd-style ↔ React Aria Set), nested dataIndex
  support, stable string keys to avoid React Aria reconciliation
  errors, and an inline empty state via renderEmptyState.
- SideSheet: HeroUI Drawer wrapper that uses React Portal to escape
  parent backdrop-filter containing blocks (the bug behind the
  "modal leaks into card" reports). Drives a CSS custom property for
  reliable dynamic widths under Tailwind v4's JIT.

Refactored shared primitives

- CardPro: drop the hard-coded `border-t` between table and pagination
  (the card already has its own surface chrome) and tighten footer
  padding.
- ConfirmDialog / ColumnSelectorDialog: switch to canonical HeroUI
  Modal anatomy (ModalHeading wraps title text — bare children fall
  to body font-size and read smaller than description), drop manual
  border dividers, swap Button color='danger'/'primary' for
  variant=. ColumnSelectorDialog also adopts the full Checkbox
  anatomy so the actual box renders.
- TableFilterForm: FilterInput uses HeroUI Input, FilterSelect drops
  hand-rolled Tailwind overrides and lets HeroUI's --field-background
  paint the primary variant for pixel-equal rows with adjacent inputs.
- TablePagination: switch to ct-compact-select utility class.
- JSONEditor: whitespace-nowrap on tabs to prevent CJK wrap.

Global CSS (index.css)

- Body font-size lowered to 12px to match per-component density and
  surface this as the default for bare prose / labels.
- .button--sm and .toggle-button--sm font-size dropped to 12px so
  toolbar pills don't read larger than surrounding 12px text.
- .switch__thumb locked to white in both states (HeroUI default flips
  to --accent-foreground when checked, which on our cyan accent
  resolves to near-black and looked like a different control).
- .ct-compact-table / .ct-compact-alert / .ct-compact-select utility
  classes published to replace Tailwind v4 arbitrary selectors that
  silently mangled BEM `__` separators into descendant spaces.
- .ct-compact-alert.alert__content gets justify-content:center so
  single-child Alerts vertically center the description with the icon.

Made-with: Cursor
Sweep every console list page (tokens, channels, users, redemptions,
subscriptions, models, usage-logs, model-pricing) onto the shared
HeroTable + SideSheet primitives so they share the same row rhythm,
sticky-right operations column, hover state, empty state, loading
spinner and edit-modal anatomy.

Per-page tables

- TokensTable / ChannelsTable / UsersTable / RedemptionsTable /
  SubscriptionsTable / ModelsTable: thin wrappers around HeroTable.
  Adapt each hook's legacy {selectedRowKeys, onChange} selection
  shape to HeroTable's {selectionMode, selectedRows,
  onSelectionChange} contract via a useMemo adapter so downstream
  consumers (batch delete, copy, prefill management) stay untouched.
- All operations cells switch from `flex flex-wrap` to
  `inline-flex whitespace-nowrap` + compact buttons
  (`!h-7 !px-2.5 !text-[11px]`) so rows stay ~44px tall and the
  sticky-right column stays a single line under horizontal scroll.

Tabs and toolbars

- ChannelsTabs / ModelsTabs: rewrite vendor pills as HeroUI
  ToggleButtonGroup + ToggleButton split-pill (toggle paired with
  ClickMenu for vendor edit/delete). `!justify-start` overrides
  HeroUI's default centered group so wrapped tabs left-align.
- ChannelsActions: switch local ClickDropdown to shared ClickMenu;
  fix `color='danger'` → `variant='danger-soft'` so the disabled
  "delete selected channels" button reads as destructive.
- ChannelsFilters: `variant='secondary'` → `variant='primary'` on
  Add channel so it matches every other "Add X" CTA in the console.
- UsageLogsFilters: expand grid to 8 cols at 2xl, place LogType
  as HeroUI Select.
- ModelsActions: remove inherited order/width hacks now that the
  parent flex layout is gone.

Modals

- EditTokenModal / EditChannelModal / EditTagModal: migrate the
  hand-rolled <aside> drawer to the shared SideSheet (portals fix
  the backdrop-filter containment bug).
- AddUserModal / EditUserModal / UserSubscriptionsModal /
  EditRedemptionModal / AddEditSubscriptionModal /
  EditModelModal / EditVendorModal / EditPrefillGroupModal /
  PrefillGroupManagement / ModelDetailSideSheet: same SideSheet
  treatment, plus drop IconTile decorations, drop X / Save icons
  from footer buttons, replace native inputs/selects with HeroUI
  Input/Select/TextArea/InputGroup, swap manual badges for shared
  StatusChip, and use ModalHeading inside ModalHeader.

Page-level

- /console/models: split actionsArea (ModelsActions) and searchArea
  (ModelsFilters) into CardPro's dedicated slots — they used to
  fight for one flex row at medium widths and the buttons stacked
  vertically.
- /console/subscription: warning Alert lives next to "Create plan"
  in actionsArea (status='warning' + AlertTriangle icon, vertically
  centered via the new ct-compact-alert helper).
- /console/models page-level Alert and JSONEditor warnings adopt the
  same HeroUI Alert + ct-compact-alert pattern.

Column defs

- All column header titles inherit `whitespace-nowrap` from HeroTable
  so short CJK / slash-bearing titles like "已用/剩余" no longer
  wrap mid-character in narrow columns.
- Compact button styling shared across every operations cell.

Made-with: Cursor
- Setting/index.jsx: rewrite the top-level settings tab strip from
  hand-rolled <button> pills (no pointer cursor, black-pill selected
  state, missing radiogroup semantics) to a HeroUI ToggleButtonGroup
  + ToggleButton row. `!justify-start` keeps wrapped tabs left-
  aligned. Pane definitions split icon + label into separate fields
  so each ToggleButton renders both consistently at 12px.
- SettingGlobalModel / SettingsPaymentGatewayStripe /
  SettingsPaymentGatewayWaffoPancake: replace custom amber warning
  banners with HeroUI Alert (status='warning') + AlertTriangle +
  ct-compact-alert density tweaks, matching the /console/models
  notice and /console/subscription Stripe banner.
- GroupRatioSettings: migrate the "分组设置使用说明" sidebar from a
  hand-rolled <aside> drawer to the shared SideSheet so it inherits
  the portal-based escape from backdrop-filter containing blocks.
- SecureVerificationModal: wrap the modal title in ModalHeading so
  it picks up the proper text-base + font-medium heading style
  instead of falling through to body font-size.

Made-with: Cursor
@dingyi

dingyi commented Apr 30, 2026

Copy link
Copy Markdown
Owner Author

Direct merge instead.

@dingyi dingyi closed this Apr 30, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 82968a4898

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +100 to +101
key={String(o.value)}
id={String(o.value)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep empty Select key consistent with option ids

FormSelect maps empty values to SELECT_EMPTY_KEY via toSelectKey, but the rendered ListBox.Item ids are still raw String(o.value). When upgrade_group (or any optional select field) is '', selectedKey becomes __plan_select_empty__, which doesn't match any item id, so the control can't represent a real selected option (e.g. “不升级” is shown only as placeholder and reopens with no selected item/checkmark). Map item ids through the same key-normalization path to keep controlled selection state consistent.

Useful? React with 👍 / 👎.

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