# 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. ### 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.