Frontend Design System
Semantic tokens, color palette, dark mode strategy, unified components (InfoBox, TabBar, PickerPopover, CodeEditor), tab system conventions, and dark-mode lessons learned.
SDNUChronoSync Frontend Design System (Design System / DESIGN.md)
Section titled “SDNUChronoSync Frontend Design System (Design System / DESIGN.md)”This document is the single source of truth for the frontend visual, interaction, and theme design. It records: the semantic token system, dark mode strategy, color palette, typography, radii/shadows, shared component classes, unified components (
InfoBox/TabBar/PickerPopover/CodeEditor), conventions for the two kinds of tabs and their smooth switching, a full component inventory, and every pitfall we have hit.Rule: before adding or changing any color, font size, radius, shadow, dark-mode behavior, or interactive component, read this document first and stay consistent with the existing system.
Companion docs: Full changelog; Architecture; Deployment guide; Development guide.
Table of Contents
Section titled “Table of Contents”- Purpose and Conventions
- Theme Strategy Overview
- Semantic Token System (CSS Variables)
- Color System (Tailwind Palette)
- Typography
- Radius / Shadows / Spacing
- Shared Component Classes (addComponents)
- Unified Component 1: InfoBox
- Unified Component 2: TabBar
- Unified Component 3: PickerPopover (Date/Time Picker)
- Unified Component 4: CodeEditor
- Tab System Conventions (Two Tab Types + Smooth Switching)
- Avatar and Team Cover Palette
- Backend Integration (availability / Avatar Upload)
- Component Inventory (by Page/Feature)
- Dark Mode Lessons Learned
- Verification Methodology (Browser Audit / Build)
- Commit Conventions and Workflow
1. Purpose and Conventions
Section titled “1. Purpose and Conventions”1.1 Why This Document Exists
Section titled “1.1 Why This Document Exists”This project’s dark mode, token system, and unified components were converged through many iterations and a lot of pitfalls. Problems we have hit:
- Every component hardcoded its own colors → light/dark inconsistencies and repeated oversights;
:global(.dark)does not compile inside Vue scoped styles → dark mode silently never applied and the cause was untraceable;- Active tab was the same color as its container → “cannot tell which tab is active” in dark mode;
- Primary button hover turned near-white → white text became invisible;
- Nested modals closed on a single click;
- A repo-wide fix script misfired → deleted correct dark variants.
This document freezes those lessons so that future changes can be “right the first time”, and so components stay searchable and reusable.
1.2 Core Conventions (Mandatory)
Section titled “1.2 Core Conventions (Mandatory)”- Use tokens whenever possible (CSS variables); add
dark:variants only for local tweaks; never hardcode hex colors per component. - Always use
InfoBoxfor callouts; always useTabBarfor tabs/segments, or follow its active-state baseline. - Dark-mode active-state baseline: container
dark:bg-neutral-800, activedark:bg-neutral-700, textdark:text-neutral-50. - Primary button hover uses
hover:bg-primary-500— neverhover:bg-primary-50, never the invalid opacity/300. - Dark overrides in scoped styles must live in a non-scoped
<style>block —:global(html.dark)inside<style scoped>does not compile. - Use tokens for z-index (
--layer-*), not magic numbers likez-[9999]. - After changes:
grep -rnE "\]\s+dark:"must be empty →vue-tsc→eslint→astro build→ headless-browser per-page audit.
2. Theme Strategy Overview
Section titled “2. Theme Strategy Overview”2.1 Dark Mode Switch
Section titled “2.1 Dark Mode Switch”tailwind.config.mjs:darkMode: 'class'. Adding thedarkclass to<html>enters dark mode.- Store:
frontend/src/stores/theme.tsis the single read/write point. localStorage key:app-theme, valuesdark/light. - Theme toggle button (
Navigation.vue): to avoid SSR/client hydration mismatch (the Vue warn caused by differentdpaths on the sun/moon icons), render both the sun and moon icons simultaneously and toggle visibility with the CSS classesdark:block/dark:hiddeninstead ofv-if="isDark". This keeps the SSR and client DOM identical.
<SunIcon class="h-5 w-5 hidden dark:block" aria-hidden="true" /><MoonIcon class="h-5 w-5 dark:hidden" aria-hidden="true" />2.2 No “Invert Filter” Dark Mode
Section titled “2.2 No “Invert Filter” Dark Mode”There was historically a filter: invert() hack — it has been deleted. It inverted images, brand colors, and the
logo along with everything else, and could not be controlled precisely.
2.3 Two Parallel Mechanisms
Section titled “2.3 Two Parallel Mechanisms”| Mechanism | Use case | Notes |
|---|---|---|
| ① Semantic tokens (CSS variables) | Page-level large surfaces/borders/text/accent | Defined in BaseLayout.astro: :root light, html.dark dark; components consume var(--...) and switch automatically |
② Tailwind dark: variants | Small color patches inside a single component | Append dark:bg-…, dark:text-… etc. to class names |
All six showcase mock components (
*Showcase.vue,PerspectiveSchedule.vue) have been migrated to consume tokens (mechanism ①).
3. Semantic Token System (CSS Variables)
Section titled “3. Semantic Token System (CSS Variables)”Defined in the global <style is:global> of frontend/src/layouts/BaseLayout.astro.
3.1 Layer (z-index) Tokens
Section titled “3.1 Layer (z-index) Tokens”| Variable | Value | Purpose |
|---|---|---|
--layer-dropdown | 1200 | Dropdown menus |
--layer-popover | 1250 | Popovers/pickers |
--layer-modal | 1400 | Modals |
--layer-toast | 1500 | Toast notifications (topmost) |
Usage: style="z-index: var(--layer-modal)". Never hardcode magic numbers like z-[9999] (they fight with
the toast/modal layers).
3.2 Semantic Color Tokens
Section titled “3.2 Semantic Color Tokens”| Variable | Light | Dark (html.dark) | Purpose |
|---|---|---|---|
--bg-page | #f8fafc | #0f172a | Page background |
--bg-card | #ffffff | #1e293b | Card/panel surface |
--bg-muted | #f1f5f9 | #334155 | Secondary surface (input background) |
--bg-subtle | #e2e8f0 | #475569 | Weaker surface (divider/hover background) |
--border | #e2e8f0 | #334155 | Regular borders |
--border-strong | #cbd5e1 | #475569 | Emphasized borders |
--text | #0f172a | #f8fafc | Primary text |
--text-2 | #475569 | #cbd5e1 | Secondary text |
--text-3 | #64748b | #94a3b8 | Weak text/placeholder |
--text-4 | #94a3b8 | #64748b | Weakest text |
--accent | #0ea5e9 | #38bdf8 | Accent (primary color) |
--accent-strong | #0284c7 | #0ea5e9 | Accent (pressed/solid) |
--accent-soft | #e0f2fe | #0c4a6e | Accent light background |
Consumption example:
.card { background: var(--bg-card); color: var(--text); border-color: var(--border);}4. Color System (Tailwind Palette)
Section titled “4. Color System (Tailwind Palette)”frontend/tailwind.config.mjs → theme.extend.colors.
4.1 primary (Primary Accent = sky)
Section titled “4.1 primary (Primary Accent = sky)”The only accent color on the site. There was historically a
secondary(fuchsia) palette — deleted;skyhas also been folded intoprimary.
| Step | Value |
|---|---|
| 50 | #f0f9ff |
| 100 | #e0f2fe |
| 200 | #bae6fd |
| 300 | #7dd3fc |
| 400 | #38bdf8 |
| 500 | #0ea5e9 |
| 600 | #0284c7 |
| 700 | #0369a1 |
| 800 | #075985 |
| 900 | #0c4a6e |
⚠️
primaryonly has 50–900, no 950. Anyprimary-950class is invalid and silently dropped by Tailwind (dark mode will not apply). For dark light-backgrounds useprimary-900, e.g.dark:bg-primary-900/30.
4.2 neutral (Neutral = slate)
Section titled “4.2 neutral (Neutral = slate)”The site uniformly uses
neutralinstead of scatteredgray/slate. Do not introducegray-*/slate-*.
| Step | Value |
|---|---|
| 50 | #f8fafc |
| 100 | #f1f5f9 |
| 200 | #e2e8f0 |
| 300 | #cbd5e1 |
| 400 | #94a3b8 |
| 500 | #64748b |
| 600 | #475569 |
| 700 | #334155 |
| 800 | #1e293b |
| 900 | #0f172a |
| 950 | #020617 |
neutral-800(#1e293b) is the standard dark for cards/panels;neutral-700(#334155) is the standard for active states;neutral-900(#0f172a) is the standard for pages/inset tracks.
4.3 Semantic Auxiliary Colors
Section titled “4.3 Semantic Auxiliary Colors”Uses Tailwind’s default blue/emerald/amber/red (all include 950). Unified dark convention:
| Semantics | Light | Dark |
|---|---|---|
| Info | bg-blue-50 text-blue-800 border-blue-200 | dark:bg-blue-900/30 dark:text-blue-200 dark:border-blue-800 |
| Success | bg-green-50 text-green-800 | dark:bg-green-900/25 dark:text-green-200 |
| Warning | bg-amber-50 text-amber-800 | dark:bg-amber-900/25 dark:text-amber-200 |
| Danger | bg-red-50 text-red-800 | dark:bg-red-900/25 dark:text-red-200 |
| Accent (purple) | bg-purple-50 text-purple-800 | dark:bg-purple-900/30 dark:text-purple-200 |
| Accent (orange) | bg-orange-50 text-orange-800 | dark:bg-orange-900/25 dark:text-orange-200 |
5. Typography
Section titled “5. Typography”tailwind.config.mjs → theme.extend.fontFamily.
| Family | Font stack | Purpose |
|---|---|---|
sans | System sans-serif stack (-apple-system, PingFang SC, Microsoft YaHei, etc.) | Body/UI |
display | Serif: STSong, Songti SC, Noto Serif SC, SimSun, etc. | Display/headings |
BaseLayout.astro sets font-family: -apple-system, ... globally on html for consistent Chinese rendering.
6. Radius / Shadows / Spacing
Section titled “6. Radius / Shadows / Spacing”6.1 Radius Conventions
Section titled “6.1 Radius Conventions”| Purpose | Value |
|---|---|
| Small buttons / badges | rounded-md / rounded-lg |
| Cards / panels | rounded-xl / rounded-2xl |
| Large panels / modals | rounded-[28px] (e.g. ShareScheduleView) |
| Pills (multi-select/segments) | rounded-full |
6.2 Shadow Conventions
Section titled “6.2 Shadow Conventions”- Card shadows are uniformly
shadow-sm/shadow-xl(modals). - In dark mode, most card shadows are turned off with
dark:shadow-none(shadows are barely visible on dark backgrounds and look dirty). - Primary button glow
shadow-[0_16px_36px_-18px_rgba(2,132,199,0.95)]is light-mode only; dark mode usesdark:shadow-none.
6.3 Spacing
Section titled “6.3 Spacing”- Page padding
p-4 md:p-6; modal inner paddingpx-4 pb-4 pt-5 sm:p-6. - Tab/segment container padding
p-1.
7. Shared Component Classes (addComponents)
Section titled “7. Shared Component Classes (addComponents)”Defined in tailwind.config.mjs → plugins → addComponents. All have dark: variants built in (unless noted).
| Class | Purpose | Dark |
|---|---|---|
.input-base | Standard input | ✅ |
.select-base | Standard select | ✅ |
.input-search | Search input with left icon | ✅ |
.dropdown-button / .dropdown-menu / .dropdown-item / .dropdown-search / .dropdown-search-input / .dropdown-check | Headless dropdown | ✅ |
.picker-input-base / .picker-trigger-button | Picker input/trigger button | ✅ |
.btn-secondary / .btn-primary | Secondary/primary buttons | ✅ |
.warning-surface / .warning-surface-soft | Amber callout (legacy class; dark covered by .dark .warning-surface override) | ✅ |
.btn-clear-selection | Clear-selection small button | ⚠️ still uses slate-*, not yet migrated to neutral |
.scrollbar-custom | Custom scrollbar | — |
Prefer reusing these classes in new code instead of hand-writing a whole set of
@apply. ⚠️ Custom classes inaddComponentshave no automaticdark:variants — you must add.dark .xxx { … }overrides manually.
8. Unified Component 1: InfoBox
Section titled “8. Unified Component 1: InfoBox”File: frontend/src/components/InfoBox.vue.
8.1 Background
Section titled “8.1 Background”There used to be many scattered semantic callouts (“Important”, “Note”, “Dangerous operation”, “Temporary class booking”, “Logged into the academic system”, “Code injection”, etc.), each component hardcoding its own colors → light/dark inconsistencies and repeated oversights. Now converged into a single component with three variants:
| variant | Semantics | Light | Dark |
|---|---|---|---|
info | Explanation/note | bg-primary-50 text-primary-900 border-primary-200 | dark:bg-primary-900/25 dark:text-primary-100 dark:border-primary-800/70 |
warning | Caution | bg-amber-50 text-amber-900 border-amber-200 | dark:bg-amber-900/25 dark:text-amber-100 dark:border-amber-800/70 |
danger | Dangerous operation | bg-red-50 text-red-900 border-red-200 | dark:bg-red-900/25 dark:text-red-100 dark:border-red-800/70 |
8.2 Usage
Section titled “8.2 Usage”<InfoBox variant="warning" title="注意事项"> <ul class="list-disc list-inside"> <li>转让后您将不再是团队创建者</li> <li>此操作不可撤销</li> </ul></InfoBox>8.3 Migrated Locations
Section titled “8.3 Migrated Locations”TransferTeamModal (caution), DissolveTeamModal (dangerous operation), TemporaryTeamDrawer (temporary class
booking notice), ScheduleImporter (important notice). Any new callout of the same kind must use InfoBox — do
not write hardcoded color blocks again.
9. Unified Component 2: TabBar
Section titled “9. Unified Component 2: TabBar”File: frontend/src/components/TabBar.vue.
9.1 Background
Section titled “9.1 Background”Tab bars in system settings, team management, etc. were each written separately with inconsistent active states
across light/dark (at one point the active tab and its container were both dark:bg-neutral-800, making it
“impossible to see which tab is active” in dark mode). Now converged into a single component.
9.2 Props and Usage
Section titled “9.2 Props and Usage”tabs: { id, name|label }[],modelValue(active id),stretch?: boolean(buttons split evenly to fill the width).- Two-way binding via
v-model.
<TabBar :tabs="tabs" v-model="activeTab" /><TabBar stretch :tabs="[{id:'swap',name:'对调工作日'},{id:'holiday',name:'设置假期'}]" v-model="mode" />9.3 Sliding Indicator (Smooth Switching)
Section titled “9.3 Sliding Indicator (Smooth Switching)”Following Tabs.vue from ~/AinOfficialWiki, implements a sliding indicator pill:
- Container is
relative, with an absolutely positioned pill<span>(bg-white shadow dark:bg-neutral-700) laid under the buttons. - Use
getBoundingClientRectto measure the active button’sleft/widthand write them intotransform: translate3d(...)andwidth. - CSS
transition-[width,transform,opacity] duration-200 ease-outmakes it glide smoothly to the active tab. - Re-sync on
onMounted/resize/modelValue/tabschanges. - Buttons only keep the text color (active
dark:text-neutral-50); the pill handles the background.
9.4 Migrated Locations
Section titled “9.4 Migrated Locations”admin/SystemSettings.vue (site/storage/email/code injection), TeamEditorModal.vue (team info/member
management/visibility/actions), ScheduleAdjuster.vue (swap workdays/set holidays, stretch). Any new tabs must
use TabBar.
10. Unified Component 3: PickerPopover (Date/Time Picker)
Section titled “10. Unified Component 3: PickerPopover (Date/Time Picker)”File: frontend/src/components/PickerPopover.vue.
- Trigger button uses the shared classes
.picker-input-base/.picker-trigger-button(addComponentshas dark built in). - The panel
panelClassis a class string built in a computed property; it used to be the only light-mode leak (bg-whitewithout dark) → now fixed withdark:border-neutral-700 dark:bg-neutral-800 dark:shadow-none dark:ring-neutral-700. - Calendar cells/time/minutes/clear/today/confirm buttons all have dark variants.
- ⚠️ When concatenating classes in a JS string, do not forget the dark variants (
.vuemodule checks cannot see styles — verify against the build output).
11. Unified Component 4: CodeEditor
Section titled “11. Unified Component 4: CodeEditor”File: frontend/src/components/CodeEditor.vue (used by system settings “code injection”).
- The scoped classes (
.code-editor-container/.line-numbers/.code-textarea) hardcode light backgrounds and have no automatic dark variants. - Fix: after the
<style scoped>block, add a non-scoped<style>withhtml.dark .code-editor-container { … }overrides covering background, line numbers, text, error states, and scrollbar. - ⚠️ This is the positive fix for the “
:global(html.dark)does not compile in scoped styles” pitfall — see §16 item 9.
12. Tab System Conventions (Two Tab Types + Smooth Switching)
Section titled “12. Tab System Conventions (Two Tab Types + Smooth Switching)”This project has two kinds of tabs/switchers with different conventions — do not confuse them:
12.1 Scenario 1: Page Option Switching (TabBar / ProfilePage)
Section titled “12.1 Scenario 1: Page Option Switching (TabBar / ProfilePage)”For “switching options on the same page”: personal center, system settings, team management.
- Component:
TabBar.vue(with sliding indicator). - Container
bg-neutral-100 dark:bg-neutral-800; the active state is handled by the sliding pill background, textdark:text-neutral-50. - No focus glow ring allowed (
focus:ring-2 focus:ring-primary-500causes “glows forever after clicking until you click elsewhere”) — keep onlyfocus:outline-none. Baseline:ProfilePage.vue(no glow). - Dark active-state baseline: container
dark:bg-neutral-800, active pilldark:bg-neutral-700, textdark:text-neutral-50.
12.2 Scenario 2: Selector Switching (Select All/Clear, Multi-Select, Smart Scheduling Switcher)
Section titled “12.2 Scenario 2: Selector Switching (Select All/Clear, Multi-Select, Smart Scheduling Switcher)”For “select all/multi-select/clear” or mode switching (week/day, week/date, permission select, etc.), scattered
across forms (EmptyClassroomQuery, BatchTeamEventModal, TeamScheduleTaskModal, AllTeamsViewPage,
SystemSettings storage switcher).
- Container
bg-neutral-100 dark:bg-neutral-900(one step darker than the card’sneutral-800, forming an inset track — otherwise it is indistinguishable from the card). - Active:
bg-white dark:bg-neutral-700+dark:text-neutral-50(text must be light, otherwise black text on a dark surface is unreadable). - Inactive:
text-neutral-600 dark:text-neutral-300, hoverdark:hover:bg-neutral-800. - Smooth:
transition-all/transition-colorsare already in place; color changes are smooth.
12.3 Key Lessons (Repeatedly Hit)
Section titled “12.3 Key Lessons (Repeatedly Hit)”- Active text must have
dark:text-neutral-50— a script once deleted it, leaving black text on a dark surface unreadable. - Active background must not equal the container color (both
dark:bg-neutral-800makes the active state invisible). - The container background must differ from the card it sits in (same
dark:bg-neutral-800makes the track invisible) → selector tracks usedark:bg-neutral-900. - Page tabs must not have a focus glow.
13. Avatar and Team Cover Palette
Section titled “13. Avatar and Team Cover Palette”- The 17 DiceBear-style
avatarPresetsgroups are converged to a singleprimary/neutralanchor tone (no more rainbow colors). - Preset avatar upload:
avatarPresetSvgToFilerasterizes the generated SVG to PNG before uploading. Reason: the backendALLOWED_IMAGE_EXTENSIONSonly acceptsjpg/jpeg/png/gif/webp, not SVG; and serving SVG statically is an XSS vector. - Remote avatars (e.g.
gastigado.cnies.org) without CORS headers block browserfetch;<img>display is unaffected.
14. Backend Integration (availability / Avatar Upload)
Section titled “14. Backend Integration (availability / Avatar Upload)”14.1 availability Week Parameter
Section titled “14.1 availability Week Parameter”- The frontend
getWeekNumberreturns a calendar week (max 53 per year); the backend availability routes originally limitedweek ≤ 30→ returned 422. - Fix: both the team and temporary availability routes raised the
weekcaple=30 → le=53(reference_dateis the real basis for week resolution;weekis only a fallback).
14.2 Avatar Upload
Section titled “14.2 Avatar Upload”- Backend
ALLOWED_IMAGE_EXTENSIONS = {jpg,jpeg,png,gif,webp}, no SVG. - Frontend preset avatars are rasterized from SVG to PNG before upload, bypassing the backend validation and avoiding SVG XSS.
15. Component Inventory (by Page/Feature)
Section titled “15. Component Inventory (by Page/Feature)”The complete inventory for looking up “which component does this UI use, and is dark mode covered”. All are dark-adapted unless noted.
15.1 Top-Level Layout
Section titled “15.1 Top-Level Layout”| Component | Purpose | Notes |
|---|---|---|
BaseLayout.astro | Global token variables, fonts, body background | Dark-mode foundation |
Navigation.vue | Desktop sidebar | Theme toggle button (dual-rendered icons, dark:block/hidden) |
MobileDrawer.vue | Mobile drawer | |
MobileBottomTabBar.vue | Mobile bottom bar | |
Footer.vue | Footer |
15.2 Schedule
Section titled “15.2 Schedule”| Component | Purpose |
|---|---|
MySchedulePage.vue | My schedule main page (week/day/calendar/list views, schedule selection, share management, stats cards) |
ScheduleGanttWeekView.vue / ScheduleDayListView.vue / ScheduleCalendar.vue | Week/day/calendar views (ScheduleCalendar’s .week-view/.time-column use non-scoped <style> dark overrides) |
ScheduleEditor.vue / EventModal.vue / EventDetailModal.vue / StackedEventsModal.vue / TeamEventDetailModal.vue | Editor/event modals |
ScheduleAdjuster.vue | Schedule adjustment (swap workdays/set holidays, TabBar stretch) |
ScheduleImporter.vue / ImportScheduleModal.vue / JwxtConnectionModeSelector.vue / ImportOptionsModal.vue | Schedule import (WebVPN/academic-system selection, step hints) |
ScheduleList.vue | Schedule list |
ExportOptionsModal.vue / ShareOptionsModal.vue / ShareScheduleView.vue / TeamAvailabilityShareModal.vue | Export/share |
PickerPopover.vue | Date/time picker |
EmptyClassroomQuery.vue / CsvImportModal.vue / PersonalizationModal.vue | Empty classroom query/CSV import/personalization |
15.3 Team
Section titled “15.3 Team”| Component | Purpose |
|---|---|
MyTeamsPage.vue / TeamList.vue / AllTeamsViewPage.vue | My teams/team list/team view |
TeamViewPage.vue | Team view (week/month/heatmap, filter bar) |
TeamEditorModal.vue | Team management (team info/members/visibility/team actions; TabBar; batch-add schedule/smart scheduling/transfer/dissolve buttons) |
DissolveTeamModal.vue / TransferTeamModal.vue / LeaveTeamModal.vue | Dissolve/transfer/leave confirmation modals |
TeamHeatmapDrawer.vue / TeamAvailabilityGrid.vue / TeamSlotDetailDrawer.vue | Heatmap/common free time |
TeamScheduleTaskModal.vue / BatchTeamEventModal.vue / BatchOperationsLog.vue / TemporaryTeamDrawer.vue | Smart scheduling/batch add/batch operation log/temporary booking |
CreateTeam.vue / JoinTeam.vue / AvatarPresetDrawer.vue / TutorialEntry.vue | Create/join team, avatar presets |
UserScheduleViewer.vue / TeamMemberSchedulePanel.vue / TeamMemberStrip.vue / ActionConfirmModal.vue / FilterSidebar.vue | Member schedule/member strip/confirm/filter |
15.4 Admin Backend
Section titled “15.4 Admin Backend”| Component | Purpose |
|---|---|
admin/UserManagementPage.vue | User management (table/dropdown/ban) |
admin/AdminTeamManagement.vue | Team management (edit/delete/members) |
admin/SystemSettings.vue | System settings (site/storage/email/code injection; TabBar + CodeEditor) |
admin/UserEditModal.vue / admin/BanUserModal.vue / admin/UserScheduleModal.vue / admin/ConfirmDeleteModal.vue / admin/BatchRestoreConfirmToast.vue | Sub-modals/confirmations/batch restore |
15.5 Auth / Landing / Navigation
Section titled “15.5 Auth / Landing / Navigation”| Component | Purpose |
|---|---|
LoginForm.vue / RegisterForm.vue / ForgetPasswordForm.vue / ForceBindEmailModal.vue / FirstStartAdminModal.vue | Login/register/forgot password/email binding/initial admin |
LandingNavbar.vue / CTASection.vue / FeatureSection.vue / 6 *Showcase.vue / PerspectiveSchedule.vue | Landing page |
ChangelogModal.vue | Changelog (dark:prose-invert) |
InAppBrowserPrompt.vue | In-app browser prompt |
15.6 Unified Components and Reuse Rules
Section titled “15.6 Unified Components and Reuse Rules”- Callouts →
InfoBox.vue - Tabs/segments →
TabBar.vue; selector switchers follow the “containerdark:bg-neutral-900+ activedark:bg-neutral-700” baseline - Inputs/selects/pickers →
.input-base/.select-base/.dropdown-*/.picker-*(addComponents, dark built in) - Semantic color blocks → the unified
bg-*-50 dark:bg-*-900/N + text-*-800 dark:text-*-200convention - Primary button hover →
bg-primary-600 hover:bg-primary-500(nothover:bg-primary-50, not the invalid opacity/300)
16. Dark Mode Lessons Learned
Section titled “16. Dark Mode Lessons Learned”Each of these once caused “dark mode not applying”, “build failure”, or “interaction bug” — keep them in mind.
primary-950does not exist.dark:bg-primary-950/30is silently a no-op → the surface stays light in dark mode. Always useprimary-900.:global(.dark)does not compile into Vue scoped styles. The correct approaches: Tailwinddark:variants or CSS variables.- Appending
dark:outside a:classbinding = build error. Check:grep -rnE "\]\s+dark:" components/must be empty (the]insidetransition-[...]arbitrary values is a false positive). addComponentscustom classes have no automatic dark variants — you must add.dark .xxx { … }overrides manually (e.g..warning-surface).hover:bg-white/hover:bg-neutral-50brightens in dark mode — adddark:hover:bg-neutral-800.- WSL2 file watching is unreliable; the dev server goes stale. Verify modules with
curl, delete caches and restart clean, and trustastro buildas the source of truth. - Use tokens for z-index, not magic numbers.
- Headless-browser audit: record a stray light color when luminance
0.299r+0.587g+0.114b > 215and area>40×40. :global(html.dark)is not compiled inside scoped styles → use a non-scoped<style>withhtml.dark .xxx(ScheduleCalendar/CodeEditor/PersonalizationModal).- Active tab = container color = invisible → active uses
dark:bg-neutral-700. - Primary button
hover:bg-primary-50makes white text disappear; invalid opacity/300breaks the dark hover → usehover:bg-primary-500. - Nested modals close on a single click: the sub-modal
Teleport to="body"lives outside the parent panel’s DOM, so clicking the sub-modal triggers the parent’s outside-click. Fix: add the sub-modal’s open state to the parentDialog’s:staticand to the early-return guard inhandleClose. - Multiple dev servers fight over the port: a stale process holds 4322 → the browser gets stale modules. Run
ss -tlnp | grep :4322to confirm a single instance. - Unified semantic color-block convention:
bg-red-50 dark:bg-red-900/25,bg-blue-50 dark:bg-blue-900/30etc. +text-*-800 dark:text-*-200. hover:dark:is a non-standard ordering and gets misjudged: functionally equivalent todark:hover:, but regexes matchingdark:bg-*will false-match. Always writedark:hover:.- A repo-wide “add dark per line” script misfires: when a line already has another
dark:variant, do not append a default color again; and do not treathover:dark:bg-*as a conflicting background. - JS strings concatenating classes must also carry dark (e.g. PickerPopover’s
panelClass);.vuemodule checks cannot see styles — verify against the build output.
17. Verification Methodology (Browser Audit / Build)
Section titled “17. Verification Methodology (Browser Audit / Build)”17.1 Required Checklist
Section titled “17.1 Required Checklist”After any dark-mode/interaction change, run in order:
cd frontendgrep -rnE "\]\s+dark:" src/components/ # 必须为空bun run type-check # vue-tsc 0 错误bun run lint # 0 错误(存量 warning 可忽略)bun run build # astro build 14 页成功17.2 Headless-Browser Per-Page Audit
Section titled “17.2 Headless-Browser Per-Page Audit”Open every page with Playwright/puppeteer, inject access_token + app-theme=dark, and iterate over all elements:
const lum = (c) => { const m = c.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); return m ? 0.299*m[1]+0.587*m[2]+0.114*m[3] : -1; };// 对每个元素:opacity>0.05 && width>40 && height>40 && lum(bgColor)>215 → 漏网浅色Fix each occurrence in order of area.
17.3 Test Accounts
Section titled “17.3 Test Accounts”- Backend uses PostgreSQL (
DATABASE_URL=postgresql+psycopg://chronosync:...@localhost:5432/chronosync), not SQLite. Users inserted into SQLite are invalid for the running backend. - Admin account:
admin/975280hc(test only). - JWT is valid for 15 minutes; if the test interval is long, re-login to refresh the token.
17.4 Dev Server Troubleshooting
Section titled “17.4 Dev Server Troubleshooting”- Confirm only one astro dev process holds 4322:
ss -tlnp | grep :4322. - Is the module current:
curl http://localhost:4322/src/components/<File>.vue. - Is the style in the build: after
astro build, grep the CSS/JS indist/(<style>is injected separately, not in the.vuemodule).
18. Commit Conventions and Workflow
Section titled “18. Commit Conventions and Workflow”18.1 Conventional Commits
Section titled “18.1 Conventional Commits”Group commits by feature/module, format type(scope): subject:
| type | Purpose |
|---|---|
feat | New feature / dark-mode adaptation |
fix | Bug fix |
docs | Documentation |
refactor | Refactor (no behavior change) |
chore | Miscellaneous |
Example commits grouped by module:
feat(dark-mode): establish design token system and unified UI componentsfeat(dark-mode): adapt schedule and classroom viewsfeat(dark-mode): adapt team, heatmap and temporary-scheduling viewsfeat(dark-mode): adapt admin user, team and system pagesfeat(dark-mode): adapt auth, landing, navigation and showcase viewsfeat(avatar): converge preset accents to primary palette and rasterize to PNGfix(api): accept calendar-week in availability queries18.2 Commit Discipline
Section titled “18.2 Commit Discipline”- Only commit your own changes. If you see things in git that are not yours (e.g.
_apkwork/*,server-sync/in.gitignore, or someone else’s uncommitted files), leave them alone; stage precisely withgit add <explicit files>, nevergit add .. - One commit per module; the body explains “why changed, constraints, trade-offs”.
- Before committing, make sure
type-check+buildpass.
18.3 Modification Workflow
Section titled “18.3 Modification Workflow”- First read this
DESIGN.md+tailwind.config.mjs+BaseLayout.astro. - Use tokens when possible; use
dark:for local tweaks; useInfoBoxfor callouts; useTabBarfor tabs. - After changes, run the §17.1 required checklist + headless-browser per-page audit.
- Commit per module with Conventional Commits;
git addonly your own files.
19. Dark Mode Implementation Details (Correct vs Wrong)
Section titled “19. Dark Mode Implementation Details (Correct vs Wrong)”19.1 Cards/Panels
Section titled “19.1 Cards/Panels”<!-- ✅ 正确:卡片在深色用 neutral-800,阴影关掉 --><div class="rounded-xl bg-white shadow-sm dark:bg-neutral-800 dark:shadow-none">
<!-- ❌ 错误:深色没补背景 → 白卡片 --><div class="rounded-xl bg-white shadow-sm">19.2 Primary Button (White Text)
Section titled “19.2 Primary Button (White Text)”<!-- ✅ 正确:hover 到 primary-500(仍够深,白字可见) --><button class="bg-primary-600 text-white hover:bg-primary-500">
<!-- ❌ 错误:hover 到 primary-50(近白)→ 白字看不见 --><button class="bg-primary-600 text-white hover:bg-primary-50">
<!-- ❌ 错误:非法透明度 /300(仅支持 /0–/100)→ 深色 hover 失效 --><button class="bg-primary-600 text-white dark:hover:bg-primary-900/300">19.3 Page Tabs (TabBar)
Section titled “19.3 Page Tabs (TabBar)”<!-- ✅ TabBar:激活由滑动胶囊负责,文字浅色,无 focus 光晕 --><TabBar :tabs="tabs" v-model="activeTab" />19.4 Selector Switchers (Select All/Clear, etc.)
Section titled “19.4 Selector Switchers (Select All/Clear, etc.)”<!-- ✅ 容器凹陷轨道 neutral-900(区别于卡片 800),激活 neutral-700 + 浅文字 --><div class="rounded-2xl bg-neutral-100 p-1 dark:bg-neutral-900"> <button :class="selected ? 'bg-white text-neutral-900 shadow dark:bg-neutral-700 dark:text-neutral-50' : 'text-neutral-600 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-800'">全选</button>
<!-- ❌ 错误:激活=容器同色(都 800)→ 看不出激活;激活文字没 dark:text → 黑字不可读 -->19.5 Semantic Color Blocks
Section titled “19.5 Semantic Color Blocks”<!-- ✅ 信息:bg-blue-50 dark:bg-blue-900/30 + text-blue-800 dark:text-blue-200 --><div class="rounded-md bg-blue-50 p-4 text-blue-800 dark:bg-blue-900/30 dark:text-blue-200">
<!-- ❌ 错误:只写浅色 --><div class="rounded-md bg-blue-50 p-4 text-blue-800">19.6 Inputs
Section titled “19.6 Inputs”<!-- ✅ 用共享类 .input-base(addComponents 内置 dark) --><input class="input-base" />
<!-- ❌ 错误:手写 bg-white 忘 dark --><input class="bg-white border-neutral-200" />20. Dark Mode Implementation Playbook (Step by Step)
Section titled “20. Dark Mode Implementation Playbook (Step by Step)”The complete steps for making a page/component dark:
- Read the conventions: first check
DESIGN.md§4 palette, §7 shared classes, §12 tab conventions. - Find light colors:
grep -nE "bg-white|bg-neutral-50|text-neutral-900|border-neutral-200" <File>.vue. - Split into categories:
- Page-level large surfaces/cards → tokens (
var(--bg-card)) ordark:bg-neutral-800/900; - Semantic color blocks → per the §4.3 convention add
dark:bg-*-900/N + dark:text-*-200; - Buttons/interactions → the correct patterns in §19.
- Page-level large surfaces/cards → tokens (
- Colors hardcoded in scoped styles → add a separate non-scoped
<style>withhtml.dark .xxx. - Verify:
grep -rnE "\]\s+dark:"is empty;vue-tscandastro buildpass;- headless-browser per-page audit (§17.2) confirms 0 leftover light colors, visible active states, and normal hovers.
- Commit: per-module Conventional Commit
feat(dark-mode): ...;git addonly this module’s files.
21. FAQ
Section titled “21. FAQ”Q: A block is still white in dark mode even though the code has dark:?
A: Most likely a stale dev server (§16 items 6 and 13). Verify the module with curl, confirm a single instance,
hard-refresh; trust the astro build output.
Q: Cannot see the active tab?
A: The active background equals the container color (both dark:bg-neutral-800). Change the active state to
dark:bg-neutral-700 (§16 item 10).
Q: There is a glowing border around the tab after clicking it?
A: That is focus:ring-2 focus:ring-primary-500. Remove it, keep only focus:outline-none (§12.1).
Q: The selected text is black and hard to read?
A: The active state is missing dark:text-neutral-50 (§16 items 10/15).
Q: :global(html.dark) in scoped styles does not work?
A: Vue scoped styles do not compile it. Use a non-scoped <style> block (§16 item 9).
Q: Nested modal (dissolve) closes on a single click?
A: The sub-modal teleports to body and triggers the parent modal’s outside-click. Add the sub-modal’s open state to
the parent’s :static and to the handleClose guard (§16 item 12).
Q: Is hover:dark:bg-* correct?
A: Functionally equivalent to dark:hover:bg-*, but it is non-standard and gets misjudged by tooling — always
write dark:hover: (§16 item 15).
Q: Backend availability returns 422? A: The frontend sends calendar weeks (max 53); the backend originally limited it to 30. Now relaxed to 53 (§14.1).
Q: Preset avatar upload returns 400? A: SVG is not accepted by the backend. The frontend rasterizes it to PNG before upload (§13).
22. Dark Mode Implementation Notes per Module (Deep Retrospective)
Section titled “22. Dark Mode Implementation Notes per Module (Deep Retrospective)”22.1 Schedule Module
Section titled “22.1 Schedule Module”ScheduleCalendar.vue’s.week-view/.time-column/.time-column-headerare scoped classes with hardcoded white backgrounds — dark overrides were added with a non-scoped<style>usinghtml.dark(§16 item 9). The scrollbar track/thumb are dark too.MySchedulePage.vue’s print/export views (inlinebackground:#ffffff) are intentionally white (for printing) — do not touch.ScheduleAdjuster.vue’s “swap workdays/set holidays” has been migrated toTabBar stretch(two tabs split evenly).EmptyClassroomQuery.vue’s week/day/period selector: containerdark:bg-neutral-900inset track + activedark:bg-neutral-700.
22.2 Team Module
Section titled “22.2 Team Module”TeamEditorModal.vue’s batch-add schedule/smart scheduling/transfer/dissolve buttons:bg-white dark:bg-neutral-800+border-*-300 dark:border-*-800+text-*-700 dark:text-*-300+hover:bg-*-50 dark:hover:bg-*-900/30.- The root cause of
DissolveTeamModalclosing on one click was the parentTeamEditorModal’shandleCloseguard missingshowDissolveModal; fixed: the:staticlist and thehandleCloseearly return both includeshowDissolveModal(§16 item 12). - Team member badges: creator
bg-blue-100 dark:bg-blue-900/40, adminbg-purple-100 dark:bg-purple-900/40.
22.3 Admin Backend
Section titled “22.3 Admin Backend”- Table row-action dropdown
z-[260]; rowfocus-within:z-[220]keeps the row with an open menu above adjacent rows (z-index convention). - User list action cell
sticky right-0 z-[90]; the dropdown menu lives inside thez-[260]container. SystemSettings.vue’s code injection usesCodeEditor.vue(non-scoped dark overrides).- All three admin pages are fully dark: user-management (142 dark occurrences), team-management (139), system-settings (102).
22.4 Auth / Landing
Section titled “22.4 Auth / Landing”- The theme toggle icon uses dual rendering +
dark:block/hiddento eliminate the hydration mismatch (§2.1). - The changelog
ChangelogModalusesdark:prose-invertso the markdown body lightens in dark mode. - The six showcase mocks consume tokens (
var(--bg-card)etc.) and switch automatically.
22.5 Avatars
Section titled “22.5 Avatars”- The 17 preset accents converge to primary/neutral.
- Upload path: SVG→PNG rasterization (§13).