232 lines
15 KiB
Markdown
232 lines
15 KiB
Markdown
# AI Character Admin Design System
|
||
|
||
## 0. Research Log
|
||
|
||
- PRD baseline: adopted `10.1~10.9` as the visual contract because Task 1.1 is PRD-driven setup, not a new visual exploration.
|
||
- ui-ux-pro-max: ran `.opencode/skills/ui-ux-pro-max/scripts/search.py` with the PRD `10.9` design-system query. Adopted dense operational dashboard, subtle motion, status colors, focus visibility, reduced-motion, and no emoji icons. Excluded generated green palette, white on primary, dark mode support, Fira remote fonts, oversized landing typography, glass/continuous animation, and GSAP page transitions because they conflict with PRD `10.1~10.3`.
|
||
- ui-ux-pro-max UX: ran `.opencode/skills/ui-ux-pro-max/scripts/search.py "animation accessibility z-index loading" --domain ux -n 12`. Adopted loading feedback for waits over 300ms, semantic z-index scale, reduced motion, 150-300ms micro-interactions, and no decorative infinite animation. Deferred lazy-loaded media, loading buttons, skeletons, and route-level loading because Task 1.1 has no async page, media, form submit, or router surface yet.
|
||
- Existing UI: only the Phase 0 root shell exists, so this document defines the minimal system before new UI consumes it.
|
||
|
||
## 1. Atmosphere & Identity
|
||
|
||
A bright Korean operations console: dense, calm, and explicit. The signature is cyan as an operational signal, used for primary action, links, focus, and information states while surfaces stay quiet and readable.
|
||
|
||
## 2. Color
|
||
|
||
### Primitive Tokens
|
||
|
||
| Role | Token | Light | Usage |
|
||
|---|---|---:|---|
|
||
| Brand 50 | `--color-brand-50` | `#F0FBFF` | Faint emphasis background |
|
||
| Brand 100 | `--color-brand-100` | `#D9F6FF` | Selected row, accent surface |
|
||
| Brand 200 | `--color-brand-200` | `#B5EEFF` | Emphasis border |
|
||
| Brand 300 | `--color-brand-300` | `#7CE2FF` | Decorative low emphasis |
|
||
| Brand 400 | `--color-brand-400` | `#36D1FF` | Secondary accent |
|
||
| Brand 500 | `--color-brand-500` | `#00BDF7` | Fixed main color and primary background |
|
||
| Brand 600 | `--color-brand-600` | `#00A9DE` | Primary hover |
|
||
| Brand 700 | `--color-brand-700` | `#009DCE` | Primary active |
|
||
| Brand 800 | `--color-brand-800` | `#007EA8` | Link, focus ring, info |
|
||
| Brand 900 | `--color-brand-900` | `#086789` | Link hover |
|
||
| Brand 950 | `--color-brand-950` | `#063747` | Deep brand accent |
|
||
|
||
### Semantic Tokens
|
||
|
||
| Role | Token | Light | Usage |
|
||
|---|---|---:|---|
|
||
| Background | `--background` | `#F6FBFD` | Page background |
|
||
| Foreground | `--foreground` | `#102A33` | Main text |
|
||
| Card / Popover | `--card`, `--popover` | `#FFFFFF` | Surfaces and overlays |
|
||
| Muted | `--muted` | `#E9F4F7` | Muted surface |
|
||
| Muted foreground | `--muted-foreground` | `#425F69` | Secondary text |
|
||
| Secondary | `--secondary` | `#E1F5FA` | Secondary controls |
|
||
| Secondary foreground | `--secondary-foreground` | `#123E4B` | Text on secondary |
|
||
| Accent | `--accent` | `#D9F6FF` | Hover and selected surface |
|
||
| Accent foreground | `--accent-foreground` | `#0C566F` | Text on accent |
|
||
| Border | `--border` | `#D5E8EE` | Decorative separators |
|
||
| Input | `--input` | `#577581` | Required control boundary |
|
||
| Primary | `--primary` | `#00BDF7` | Main CTA |
|
||
| Primary foreground | `--primary-foreground` | `#062B36` | Text/icons on primary |
|
||
| Ring / Link | `--ring`, `--link` | `#007EA8` / `#086789` | Focus and link |
|
||
| Info | `--info` | `#086789` | Small informational labels |
|
||
| Success | `--success` | `#167347` | Open/success state |
|
||
| Success surface | `--success-surface` | `#EAF8F0` | Success Badge surface |
|
||
| Warning | `--warning` | `#9A5B00` | Scheduled/warning state |
|
||
| Warning surface | `--warning-surface` | `#FFF7E6` | Warning Badge surface |
|
||
| Destructive | `--destructive` | `#B42318` | Error/destructive state |
|
||
| Inactive | `--inactive` | `#52636A` | Inactive state |
|
||
| Inactive surface | `--inactive-surface` | `#EEF3F5` | Inactive Badge surface |
|
||
|
||
### Rules
|
||
|
||
- Feature components use semantic or component tokens, not raw hex.
|
||
- `#00BDF7` never uses white foreground; `--primary-foreground` is `#062B36`.
|
||
- Light theme only in this release. No `.dark`, theme provider, theme toggle, or system dark integration.
|
||
|
||
## 3. Typography
|
||
|
||
| Level | Size | Weight | Line Height | Usage |
|
||
|---|---:|---:|---:|---|
|
||
| Page title | `24px` | 700 | 1.3 | Main page title |
|
||
| Section title | `20px` | 650 | 1.4 | Section headers |
|
||
| Body | `14px` | 400 | 1.5 | Dense admin text and tables |
|
||
| Small | `13px` | 400 | 1.45 | Secondary metadata |
|
||
| Caption | `12px` | 600 | 1.4 | Labels and badges |
|
||
| Mobile input | `16px` | 400 | 1.5 | iOS-safe input text |
|
||
|
||
Primary font stack: `Pretendard`, `Noto Sans KR`, `Apple SD Gothic Neo`, `system-ui`, `sans-serif`.
|
||
|
||
## 4. Spacing & Layout
|
||
|
||
- Base spacing is an 8px grid, with 4px available only for tight icon-label gaps.
|
||
- Control target minimum is 44px.
|
||
- Initial shell remains simple: the document scrolls until Task 1.4 introduces the admin shell.
|
||
- Future desktop shell dimensions follow PRD: 240px sidebar and 56px header.
|
||
|
||
## 5. Components
|
||
|
||
### StatusBadge
|
||
|
||
- Structure: inline status container with a decorative dot and visible Korean text label.
|
||
- Variants: `OPEN`, `SCHEDULED`, `INACTIVE`.
|
||
- Spacing: `--space-1`, `--space-2`.
|
||
- States: static display only in Task 1.1.
|
||
- Accessibility: `aria-label="상태: {label}"`; color never carries status alone.
|
||
- Motion: none.
|
||
|
||
### IconOnlyAction
|
||
|
||
- Structure: button with icon slot, accessible name, and tooltip description.
|
||
- Variants: single icon-only control primitive.
|
||
- Spacing: 44px minimum target, centered icon.
|
||
- States: hover, active, focus-visible, disabled.
|
||
- Accessibility: `aria-label`, tooltip element, 3:1 control boundary and focus ring tokens. Tooltip text equal to the label is not wired as `aria-describedby` to avoid duplicate name/description.
|
||
- Motion: 150ms transform/color transition, disabled under reduced motion.
|
||
|
||
### PageState
|
||
|
||
- Structure: tokenized card surface for loading, empty, error, and content pass-through states.
|
||
- Variants: loading and empty use `role="status"`; error uses `role="alert"` and optional retry button.
|
||
- Accessibility: state meaning is visible Korean copy plus semantic role; retry is a native button.
|
||
- Motion: none.
|
||
|
||
### SearchToolbar
|
||
|
||
- Structure: controlled search input with optional filter slot inside a bordered card surface.
|
||
- Variants: generic query only; domain endpoint query names stay outside the primitive.
|
||
- Accessibility: search input has a visible label and mobile-safe input sizing.
|
||
- Motion: none.
|
||
|
||
### ResourcePagination
|
||
|
||
- Structure: `PageData` summary, page-size select, and native previous/next buttons.
|
||
- Variants: disabled previous/next derive from page bounds and `hasNext`.
|
||
- Accessibility: native controls expose Korean names and preserve keyboard behavior.
|
||
- Motion: none beyond control state color.
|
||
|
||
### ResponsiveResourceList
|
||
|
||
- Structure: desktop and mobile rendering slots inside one region; the primitive owns breakpoint visibility only.
|
||
- Variants: none; domain columns, DTOs, and action unions stay in consuming screens.
|
||
- Accessibility: region is named by the caller.
|
||
- Motion: none.
|
||
|
||
### ConfirmDeactivateDialog
|
||
|
||
- Structure: modal confirmation dialog with target name, impact copy, cancel, and destructive confirm action.
|
||
- Variants: confirmation only; never a switch replacement.
|
||
- Accessibility: `alertdialog`, visible title, native buttons.
|
||
- Motion: none.
|
||
|
||
### UnsavedChangesGuard
|
||
|
||
- Structure: render-prop guard for route-leave triggers that opens a confirmation dialog only while dirty.
|
||
- Variants: dirty blocks, safe/saved state lets the route callback run immediately.
|
||
- Accessibility: cancel keeps the user in context and returns focus to the trigger.
|
||
- Motion: none.
|
||
|
||
### FileField
|
||
|
||
- Structure: controlled `File | null` field with visible label, native file input, accept guidance, selected filename, and clear button.
|
||
- Variants: domain-neutral only; allowed extensions, MIME, and max bytes are injected by caller policy.
|
||
- Accessibility: label targets the input; description, accept guidance, and error are connected with `aria-describedby`; clear is a native button.
|
||
- Motion: none.
|
||
|
||
### TagChipInput
|
||
|
||
- Structure: controlled comma-separated string boundary that renders committed values as visible wrapping chips and preserves an inline draft input. A visible label, help text, and error text accompany the field.
|
||
- Controlled value: the caller owns one comma-separated string. Enter or a comma commits the non-empty draft into that string; each chip's native remove button removes only its value. Duplicate prevention and maximum-count limits are caller policy, not this pattern's policy.
|
||
- States: default, hover, focus-visible, disabled, invalid, and empty-with-draft. The input boundary uses `--input`; focus-visible uses `--ring`; invalid text and boundary use `--destructive`; chip and helper surfaces use `--muted`, `--accent`, `--accent-foreground`, `--foreground`, and `--muted-foreground` only.
|
||
- Responsive: chips wrap within the field at 375px, 768px, and 1280px without clipping or horizontal overflow. The inline draft remains usable after wrapped chips, Korean and CJK text may wrap naturally, and every input or remove target remains at least 44px.
|
||
- Accessibility: label is always visible and associated with the inline input; help and error text are connected with `aria-describedby`. Native remove buttons expose the chip value in their accessible name, and focus-visible remains visible for the input and every remove button.
|
||
- Motion: none.
|
||
|
||
### SelectionCard
|
||
|
||
- Structure: a native radio or checkbox remains in the DOM inside a full-card label, making the entire restrained card surface the hit target. The same grammar applies to purchase choices, preview, point, adult, comments, full-detail, and release mode.
|
||
- States: default uses `--card`, `--border`, and `--foreground`; hover uses `--accent` and `--accent-foreground`; checked uses `--accent`, `--accent-foreground`, and `--border`; focus-visible uses `--ring`; disabled uses `--muted` and `--muted-foreground`; error uses `--destructive` with the visible error message. Control boundaries use `--input` and selected native-control accents use `--accent` with `--accent-foreground` only.
|
||
- Responsive: cards wrap in a single column or available-width grid at 375px, 768px, and 1280px. Long Korean and CJK labels wrap without clipping, overflow, or shrinking the native control below its 44px target.
|
||
- Accessibility: the native radio or checkbox keeps its normal keyboard and focus behavior and is never replaced with a custom control. The whole label activates the control, visible labels and error text state the choice and problem, and focus-visible is clear on the active card and native control.
|
||
- Motion: none; hover and checked feedback are tonal and border state changes only.
|
||
|
||
### ImageCropDialog
|
||
|
||
- Structure: modal crop surface with preview, output size, directional move buttons, zoom range, reset, cancel, and apply.
|
||
- Variants: caller injects `aspect`, `maxWidth`, and `noUpscale`; domain profile names and GIF exceptions stay outside the primitive.
|
||
- Accessibility: dialog has visible title, keyboard preview controls, range input, and button alternatives. No pointer-only requirement in Phase 1.6.
|
||
- Motion: transform-only preview adjustment. No crop dependency is added; Canvas is used only when generating the final `File`.
|
||
|
||
### UploadProgress
|
||
|
||
- Structure: status label, optional filename, progressbar, and optional cancel/retry buttons.
|
||
- Variants: display-only upload state; no upload client, request adapter, or domain form ownership.
|
||
- Accessibility: progressbar exposes `aria-valuenow`; cancel/retry are native buttons.
|
||
- Motion: none.
|
||
|
||
### SuccessNotification
|
||
|
||
- Structure: one shell-level `role="status"` message after successful create, update, or deactivate navigation.
|
||
- Variants: success only.
|
||
- Tokens: `--success-surface`, `--success`, `--border`, `--radius-lg`, `--space-3`, `--space-4`.
|
||
- Accessibility: `aria-label="작업 성공"`; no timer or dismiss control in P3-T3.
|
||
- Motion: none.
|
||
|
||
### AdminAudioPlayer
|
||
|
||
- Reference: [Plyr audio](https://plyr.io/#audio) is the primary surface reference; [Media Chrome audio](https://www.media-chrome.org/docs/en/audio-player) supplies the explicit current/duration and playback-rate anatomy. Runtime extraction on 2026-08-03 found 44–52px single-row bars, icon-only media controls, a flexible progress range, compact time/rate values, and no persistent descriptive labels.
|
||
- Structure: native audio element wrapped by one compact control bar ordered play/pause → flexible seek → current/duration → playback rate → volume icon/range, followed by the existing generic error and manual retry block. No image, poster, video viewport, waveform, or settings menu is rendered.
|
||
- Surface: `card`, `border`, `input`, `primary`, `primary-foreground`, and existing radius/spacing tokens only. The standard bar is approximately one 44px control high with 4–12px token spacing; remaining inline space goes to seek before volume.
|
||
- Variants: shared signed-URL player only; it never downloads, autoplays, auto-refetches, or infers signed URL expiry.
|
||
- Responsive: 375px, 768px, and 1280px standard viewports keep a single visual bar. Only constrained containers at 200% zoom may wrap secondary controls; clipping and horizontal overflow are forbidden.
|
||
- Accessibility: player region is named by title, keyboard Space/Enter toggles play, seek/volume use range inputs, speed uses native select, and icon-only controls retain accessible names. Visible `볼륨` and `재생 속도` labels are omitted to match the compact reference while labels remain available to assistive technology.
|
||
- Motion: none.
|
||
|
||
### AudioPlaybackProvider
|
||
|
||
- Structure: shared context coordinates active audio by player id so only one player continues at a time.
|
||
- Variants: stores only player ids in React state; signed URLs are not logged, persisted, or passed into the provider.
|
||
- Accessibility: no direct rendered surface.
|
||
- Motion: none.
|
||
|
||
### CommentThread
|
||
|
||
- Structure: two-level comment management surface with root comment form, root rows, direct reply expansion, reply form, and edit/delete row actions.
|
||
- Variants: audio target includes `languageCode`; community target omits it. Replies never expose nested reply controls.
|
||
- Accessibility: forms use visible labels, rows are named articles, reply lists are named regions, and mutation actions are native buttons.
|
||
- Motion: none beyond existing control state color.
|
||
|
||
## 6. Motion & Interaction
|
||
|
||
- Motion is limited to 150ms micro-interactions for real control state changes.
|
||
- Only `transform`, `opacity`, and color changes are used for Task 1.1 primitives.
|
||
- `prefers-reduced-motion: reduce` disables non-essential transitions and animations.
|
||
|
||
## 7. Depth & Surface
|
||
|
||
Strategy: mixed but restrained. Dense admin surfaces primarily use borders and tonal shifts; shadows are reserved for overlays in later phases.
|
||
|
||
## 8. Accessibility Constraints & Accepted Debt
|
||
|
||
- Target WCAG 2.2 AA: body text 4.5:1, control boundary/focus indicator 3:1, visible focus on every interactive element.
|
||
- Korean text must keep system fallbacks and inputs must stay at least 16px on mobile.
|
||
- Accepted debt: no component showcase route in Task 1.1 because the requested scope is token/base/component setup only; RTL tests exercise the primitive states.
|