style(dm): redesign dashboard with purple-gold theme

Redesign DM Dashboard (/dm/, /ru/dm/) with purple-gold theme (#534AB7 + #c8a84b):

Design System:
- Update dm-theme.css with 49 purple-gold tokens (transitions, shadows, hover states)
- Create DmTabs.astro with ARIA, keyboard nav, URL hash sync, localStorage persistence
- Create DmSidebar.astro for desktop navigation
- Redesign DmHeader with gold accent
- Update DmButton, DmCard, DmInput primitives
- Expand i18n to 47 strings

Layout:
- Redesign DmLayout with 3-column dashboard grid (sidebar|main|context)
- Update /dm/index.astro and /ru/dm/index.astro with named slots

Feature Components:
- DiceRoller: empty state, ARIA live, color-coded history, storage key fallback
- InitiativeTracker: HP editing, active highlighting, storage key fallback
- Open5eReference: skeletons, debounce, error retry, gold tabs
- NotesPanel: cross-tab sync, copy-to-clipboard, auto-save
- AuthPanel: VK/Yandex brand colors, pill buttons

Tests:
- Add 50 Playwright E2E tests (smoke, dice, initiative, reference, notes, data-migration)

Fixes:
- Document Open5e client.ts URL bugfix
- Add backward-compatible storage key fallback for dice/initiative
- Preserve legacy sessionStorage data on load
This commit is contained in:
emil
2026-05-15 17:49:32 +03:00
parent 11da171bfb
commit 1c38df7e74
113 changed files with 5651 additions and 596 deletions
@@ -0,0 +1,24 @@
# Color Contrast Report
## Method
Evaluated all visible text elements on mobile viewport (375x812) at /dm/. Checked computed color and background-color for contrast ratio against WCAG 2.1 AA thresholds (4.5:1 for normal text, 3:1 for large/bold text ≥18px or 14px bold).
- **PASS** | Ratio: 16.95:1 | rgb(244, 244, 245) on rgb(22, 18, 14) | font-size: 16px
- **PASS** | Ratio: 4.79:1 | rgb(255, 255, 255) on rgb(76, 117, 163) | font-size: 14px
- **FAIL** | Ratio: 3.58:1 | rgb(255, 255, 255) on rgb(252, 63, 29) | font-size: 14px
- **PASS** | Ratio: 6.93:1 | rgb(255, 255, 255) on rgb(83, 74, 183) | font-size: 14px
- **PASS** | Ratio: 6.47:1 | rgb(161, 161, 170) on rgb(34, 30, 24) | font-size: 14px
- **FAIL** | Ratio: 1.12:1 | rgb(161, 161, 170) on rgb(200, 168, 75) | font-size: 14px
- **PASS** | Ratio: 6.30:1 | rgb(244, 244, 245) on rgb(83, 74, 183) | font-size: 16px
- **PASS** | Ratio: 15.08:1 | rgb(244, 244, 245) on rgb(34, 30, 24) | font-size: 16px
- **PASS** | Ratio: 7.61:1 | rgb(200, 168, 75) on rgb(30, 25, 18) | font-size: 14px
- **PASS** | Ratio: 16.38:1 | rgb(244, 244, 245) on rgb(26, 22, 16) | font-size: 16px
- **PASS** | Ratio: 6.93:1 | rgb(255, 255, 255) on rgb(83, 74, 183) | font-size: 16px
- **PASS** | Ratio: 15.88:1 | rgb(244, 244, 245) on rgb(30, 25, 18) | font-size: 16px
- **PASS** | Ratio: 15.08:1 | rgb(244, 244, 245) on rgb(34, 30, 24) | font-size: 14px
- **FAIL** | Ratio: 2.09:1 | rgb(244, 244, 245) on rgb(200, 168, 75) | font-size: 16px
## Summary
- Total unique color combinations checked: 14
- Failures: 3
- Status: 3 contrast issues detected
Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

@@ -0,0 +1,190 @@
# Keyboard Navigation Report
Tested on desktop viewport (1440x900) by pressing Tab repeatedly and capturing focus state.
## Focus 1: `A` " ← Назад к Randify " (/)
- Position: 826,18 | Size: 142x28px | Visible: true
- Outline: `1px none rgb(17, 17, 17)` (offset: 1px)
- Ring width: ``
- Box shadow: `rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgb(22, 18, 14) 0px 0px 0px 2px, rgb(83, 74, 183) 0px 0px 0px 4px, rgba(0, 0, 0, 0) 0px 0px 0px 0px`
- **Focus visible:** YES
## Focus 2: `A` "VKВойти через VK" (/api/auth/login/vk)
- Position: 977,14 | Size: 158x36px | Visible: true
- Outline: `1px none rgb(46, 46, 46)` (offset: 1px)
- Ring width: ``
- Box shadow: `rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(22, 18, 14, 0.875) 0px 0px 0px 1.75194px, rgba(83, 74, 183, 0.875) 0px 0px 0px 3.50387px, oklab(0 0 0 / 0.2) 0px 4px 6px -1px, oklab(0 0 0 / 0.2) 0px 2px 4px -2px`
- **Focus visible:** YES
## Focus 3: `A` "YВойти через Яндекс" (/api/auth/login/yandex)
- Position: 1142,14 | Size: 178x36px | Visible: true
- Outline: `1px none rgb(60, 60, 60)` (offset: 1px)
- Ring width: ``
- Box shadow: `rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(22, 18, 14, 0.816) 0px 0px 0px 1.62858px, rgba(83, 74, 183, 0.816) 0px 0px 0px 3.25715px, oklab(0 0 0 / 0.2) 0px 4px 6px -1px, oklab(0 0 0 / 0.2) 0px 2px 4px -2px`
- **Focus visible:** YES
## Focus 4: `A` " 🎲 Кубики " (#dice)
- Position: 105,113 | Size: 260x40px | Visible: true
- Outline: `1px auto rgb(34, 34, 35)` (offset: 1px)
- Ring width: ``
- Box shadow: `none`
- **Focus visible:** YES
## Focus 5: `A` " ⚔️ Инициатива " (#initiative)
- Position: 105,157 | Size: 260x40px | Visible: true
- Outline: `1px auto rgb(34, 34, 35)` (offset: 1px)
- Ring width: ``
- Box shadow: `none`
- **Focus visible:** YES
## Focus 6: `A` " 📖 Справочник " (#reference)
- Position: 105,201 | Size: 260x40px | Visible: true
- Outline: `1px auto rgb(34, 34, 35)` (offset: 1px)
- Ring width: ``
- Box shadow: `none`
- **Focus visible:** YES
## Focus 7: `A` " 📝 Заметки " (#notes)
- Position: 105,245 | Size: 260x40px | Visible: true
- Outline: `1px auto rgb(34, 34, 35)` (offset: 1px)
- Ring width: ``
- Box shadow: `none`
- **Focus visible:** YES
## Focus 8: `A` " Монстры " (#reference)
- Position: 105,333 | Size: 260x36px | Visible: true
- Outline: `1px auto rgb(43, 43, 45)` (offset: 1px)
- Ring width: ``
- Box shadow: `none`
- **Focus visible:** YES
## Focus 9: `A` " Заклинания " (#reference)
- Position: 105,373 | Size: 260x36px | Visible: true
- Outline: `1px auto rgb(43, 43, 45)` (offset: 1px)
- Ring width: ``
- Box shadow: `none`
- **Focus visible:** YES
## Focus 10: `BUTTON` " d4 "
- Position: 402,198 | Size: 76x38px | Visible: true
- Outline: `1px none rgb(25, 23, 19)` (offset: 0px)
- Ring width: ``
- Box shadow: `rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(22, 18, 14, 0.953) 0px 0px 0px 1.9058px, rgba(83, 74, 183, 0.953) 0px 0px 0px 3.81159px, rgba(0, 0, 0, 0) 0px 0px 0px 0px`
- **Focus visible:** YES
## Focus 11: `BUTTON` " d6 "
- Position: 486,198 | Size: 76x38px | Visible: true
- Outline: `1px none rgb(50, 44, 27)` (offset: 0px)
- Ring width: ``
- Box shadow: `rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(22, 18, 14, 0.816) 0px 0px 0px 1.62858px, rgba(83, 74, 183, 0.816) 0px 0px 0px 3.25715px, rgba(0, 0, 0, 0) 0px 0px 0px 0px`
- **Focus visible:** YES
## Focus 12: `BUTTON` " d8 "
- Position: 570,198 | Size: 76x38px | Visible: true
- Outline: `1px none rgb(39, 35, 23)` (offset: 0px)
- Ring width: ``
- Box shadow: `rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(22, 18, 14, 0.875) 0px 0px 0px 1.75194px, rgba(83, 74, 183, 0.875) 0px 0px 0px 3.50387px, rgba(0, 0, 0, 0) 0px 0px 0px 0px`
- **Focus visible:** YES
## Focus 13: `BUTTON` " d10 "
- Position: 654,198 | Size: 76x38px | Visible: true
- Outline: `1px none rgb(39, 35, 23)` (offset: 0px)
- Ring width: ``
- Box shadow: `rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(22, 18, 14, 0.875) 0px 0px 0px 1.75194px, rgba(83, 74, 183, 0.875) 0px 0px 0px 3.50387px, rgba(0, 0, 0, 0) 0px 0px 0px 0px`
- **Focus visible:** YES
## Focus 14: `BUTTON` " d12 "
- Position: 739,198 | Size: 76x38px | Visible: true
- Outline: `1px none rgb(50, 44, 27)` (offset: 0px)
- Ring width: ``
- Box shadow: `rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(22, 18, 14, 0.816) 0px 0px 0px 1.62944px, rgba(83, 74, 183, 0.816) 0px 0px 0px 3.25887px, rgba(0, 0, 0, 0) 0px 0px 0px 0px`
- **Focus visible:** YES
## Focus 15: `BUTTON` " d20 "
- Position: 823,198 | Size: 76x38px | Visible: true
- Outline: `1px none rgb(39, 35, 23)` (offset: 0px)
- Ring width: ``
- Box shadow: `rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(22, 18, 14, 0.875) 0px 0px 0px 1.75194px, rgba(83, 74, 183, 0.875) 0px 0px 0px 3.50387px, rgba(0, 0, 0, 0) 0px 0px 0px 0px`
- **Focus visible:** YES
## Focus 16: `BUTTON` " d100 "
- Position: 907,198 | Size: 76x38px | Visible: true
- Outline: `1px none rgb(39, 35, 23)` (offset: 0px)
- Ring width: ``
- Box shadow: `rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(22, 18, 14, 0.875) 0px 0px 0px 1.75256px, rgba(83, 74, 183, 0.875) 0px 0px 0px 3.50513px, rgba(0, 0, 0, 0) 0px 0px 0px 0px`
- **Focus visible:** YES
## Focus 17: `INPUT` ""
- Position: 402,280 | Size: 459x42px | Visible: true
- Outline: `1px none rgb(44, 44, 44)` (offset: 0px)
- Ring width: ``
- Box shadow: `rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(22, 18, 14, 0.875) 0px 0px 0px 1.75194px, rgba(200, 168, 75, 0.875) 0px 0px 0px 3.50387px, rgba(0, 0, 0, 0) 0px 0px 0px 0px`
- **Focus visible:** YES
## Focus 18: `BUTTON` "Бросить"
- Position: 868,278 | Size: 115x44px | Visible: true
- Outline: `1px none rgb(81, 81, 81)` (offset: 0px)
- Ring width: ``
- Box shadow: `rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(22, 18, 14, 0.73) 0px 0px 0px 1.45914px, rgba(83, 74, 183, 0.73) 0px 0px 0px 2.91829px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.1) 0px 4px 6px -4px`
- **Focus visible:** YES
## Focus 19: `BUTTON` "Очистить"
- Position: 883,342 | Size: 100x34px | Visible: true
- Outline: `1px none rgb(44, 44, 44)` (offset: 0px)
- Ring width: ``
- Box shadow: `rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(22, 18, 14, 0.875) 0px 0px 0px 1.75131px, rgba(83, 74, 183, 0.875) 0px 0px 0px 3.50261px, rgba(0, 0, 0, 0) 0px 0px 0px 0px`
- **Focus visible:** YES
## Focus 20: `BUTTON` "
d20
"
- Position: 790,630 | Size: 79x34px | Visible: true
- Outline: `1px none rgb(58, 58, 59)` (offset: 0px)
- Ring width: ``
- Box shadow: `rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(22, 18, 14, 0.816) 0px 0px 0px 1.62858px, rgba(83, 74, 183, 0.816) 0px 0px 0px 3.25715px, rgba(0, 0, 0, 0) 0px 0px 0px 0px`
- **Focus visible:** YES
## Focus 21: `BUTTON` "+ Добавить"
- Position: 881,612 | Size: 79x52px | Visible: true
- Outline: `1px none rgb(46, 46, 46)` (offset: 0px)
- Ring width: ``
- Box shadow: `rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(22, 18, 14, 0.875) 0px 0px 0px 1.75194px, rgba(83, 74, 183, 0.875) 0px 0px 0px 3.50387px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.1) 0px 4px 6px -4px`
- **Focus visible:** YES
## Focus 22: `BUTTON` "
Монстры
"
- Position: 1042,150 | Size: 129x32px | Visible: true
- Outline: `1px none rgb(17, 17, 16)` (offset: 0px)
- Ring width: ``
- Box shadow: `rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgb(22, 18, 14) 0px 0px 0px 2px, rgb(83, 74, 183) 0px 0px 0px 4px, rgba(0, 0, 0, 0) 0px 0px 0px 0px`
- **Focus visible:** YES
## Focus 23: `BUTTON` "
Заклинания
"
- Position: 1171,150 | Size: 129x32px | Visible: true
- Outline: `1px none rgb(16, 16, 16)` (offset: 0px)
- Ring width: ``
- Box shadow: `rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgb(22, 18, 14) 0px 0px 0px 2px, rgb(83, 74, 183) 0px 0px 0px 4px, rgba(0, 0, 0, 0) 0px 0px 0px 0px`
- **Focus visible:** YES
## Focus 24: `SELECT` " Все "
- Position: 1125,220 | Size: 154x41px | Visible: true
- Outline: `1px none rgb(58, 58, 59)` (offset: 0px)
- Ring width: ``
- Box shadow: `rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(22, 18, 14, 0.816) 0px 0px 0px 1.62858px, rgba(200, 168, 75, 0.816) 0px 0px 0px 3.25715px, rgba(0, 0, 0, 0) 0px 0px 0px 0px`
- **Focus visible:** YES
## Focus 25: `TEXTAREA` ""
- Position: 1063,442 | Size: 216x256px | Visible: true
- Outline: `1px auto rgb(16, 16, 16)` (offset: 0px)
- Ring width: ``
- Box shadow: `rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgb(200, 168, 75) 0px 0px 0px 1px, rgba(0, 0, 0, 0) 0px 0px 0px 0px`
- **Focus visible:** YES
## Summary
- Total unique focusable elements reached: 25
- Elements with visible focus indicator: 25
- Elements without visible focus indicator: 0
- Focus screenshots saved: 25
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

@@ -0,0 +1,47 @@
# Mobile UX Observations
## Viewport Tested
375x812 (iPhone X) and 390x844 (iPhone 12/13/14)
## Tab Navigation
- Mobile tabs are sticky below the header (`sticky top-[64px]`) and use `overflow-x-auto`
- All 4 tabs (Кубики, Инициатива, Справочник, Заметки) are reachable via horizontal swipe
- Tab pill buttons have `min-h-[44px] min-w-[80px]` which satisfies touch target intent, though quick-dice buttons fall slightly short (38px height)
## Thumb Reachability
- **Top tabs**: On 375px width, tabs span full width and sit near top of screen. The rightmost tab (Заметки) may be harder to reach with right-thumb one-handed use.
- **Suggestion**: Consider moving primary navigation to bottom sheet or bottom tab bar for mobile. This would place controls within natural thumb reach zone (lower 2/3 of screen).
## Content Density
- Dice roller quick-buttons (d4–d100) are horizontally scrollable on mobile
- Initiative tracker list items are tall enough for comfortable tapping
- Open5e reference search + filter stack vertically; cards use 1-column grid on mobile which is appropriate
- Notes textarea fills available width; comfortable for typing
## Touch Target Gaps
- 9 elements measured below 44px on mobile (see `touch-targets.md`):
- Quick-dice buttons (d4–d100) are 66x38px — width is good, height is 6px short
- Clear history button (Очистить) is 100x34px — needs height increase
- One input field is 163x42px — 2px short on height
- **Recommendation**: Increase `py-2` to `py-2.5` or `min-h-[44px]` on quick-dice buttons and ghost buttons
## Swipe Gestures / Bottom Sheet
- Current tab switching requires precise tap on top tabs
- **Bottom sheet opportunity**: A swipe-up bottom sheet for dice roll history or initiative details could reduce top-screen interaction
- **Swipe-between-tabs**: Horizontal swipe on content area to switch tabs would feel native on mobile
## Scroll Behavior
- No horizontal overflow detected on mobile (body scrollWidth 360px within 375px viewport)
- Vertical scrolling is smooth; sticky tabs remain accessible while scrolling
## Visual Hierarchy
- Purple accent (#534AB7) on dark background is highly visible
- Gold headings (#c8a84b) stand out well against dark surfaces
- Cards have adequate padding and border contrast on mobile
## Keyboard UX
- No on-screen keyboard overlap issues detected in screenshots
- Input fields have visible focus rings (gold) on mobile
## Overall Assessment
Mobile layout is functional and readable. Primary UX improvement would be bottom-navigation or swipe gestures to reduce thumb travel to top tabs.
@@ -0,0 +1,13 @@
# Overflow / Layout Break Report
## desktop-1440 (1440x900)
- Body scrollWidth: 1425px | window.innerWidth: 1440px
- No overflow elements detected.
## tablet (768x1024)
- Body scrollWidth: 753px | window.innerWidth: 768px
- No overflow elements detected.
## mobile-375 (375x812)
- Body scrollWidth: 360px | window.innerWidth: 375px
- No overflow elements detected.
@@ -0,0 +1,87 @@
# Visual QA Summary Report
## Test Scope
DM Dashboard (`/dm/`) responsive layout, color contrast, keyboard navigation, and mobile UX.
## Viewports Tested
| Viewport | Dimensions | Purpose |
|----------|------------|---------|
| Desktop 1440 | 1440x900 | Standard laptop |
| Desktop 1920 | 1920x1080 | Full HD monitor |
| Tablet | 768x1024 | iPad portrait |
| Mobile 375 | 375x812 | iPhone X |
| Mobile 390 | 390x844 | iPhone 12/13/14 |
## Tabs Captured
- dice (Кубики)
- initiative (Инициатива)
- reference (Справочник)
- notes (Заметки)
Total screenshots: 20 (5 viewports × 4 tabs)
## Screenshots
All saved to `.sisyphus/evidence/visual-qa/`:
- `desktop-1440-{tab}.png`
- `desktop-1920-{tab}.png`
- `tablet-{tab}.png`
- `mobile-375-{tab}.png`
- `mobile-390-{tab}.png`
## Layout Integrity
- **Overflow check**: PASS — No horizontal overflow detected across desktop, tablet, or mobile viewports.
- **Content visibility**: PASS — All tabs render completely within viewport bounds.
- **Responsive grid**: PASS — DmLayout handles column collapse correctly; sidebar hidden on mobile/tablet.
## Color Contrast (WCAG 2.1 AA)
- **14 unique color combinations evaluated**
- **11 PASS, 3 FAIL**
### Failures
1. `rgb(255,255,255) on rgb(252,63,29)` — 3.58:1 (14px normal). Likely an orange/red button or alert. Needs darker background or larger text.
2. `rgb(161,161,170) on rgb(200,168,75)` — 1.12:1 (14px normal). Gray text on gold background — extremely low contrast.
3. `rgb(244,244,245) on rgb(200,168,75)` — 2.09:1 (16px normal). Light text on gold background — fails 4.5:1 threshold.
See `contrast-report.md` for full details.
## Touch Targets (Mobile)
- **9 elements below 44px minimum**
- Primary culprits: quick-dice buttons (38px height), clear-history button (34px height), one input (42px height)
- See `touch-targets.md` for full list.
## Keyboard Navigation
- **25 unique focusable elements reached**
- **25/25 have visible focus indicators**
- Focus rings use `focus-visible:ring-2 ring-[var(--accent)]` (purple) with offset
- Screenshots of focused elements saved as `focus-{n}-{tag}.png`
- See `keyboard-nav-report.md` for full details.
## Mobile UX
- Layout is readable and functional on 375–390px widths
- Sticky tabs work well but sit high on screen (thumb reach concern)
- Recommendations documented in `mobile-ux-notes.md`:
- Increase quick-dice button height to 44px
- Consider bottom navigation or swipe-between-tabs for mobile
- Evaluate bottom sheet for dice history / initiative details
## Artifacts
| File | Description |
|------|-------------|
| `desktop-1440-{tab}.png` | Desktop screenshots (1440x900) |
| `desktop-1920-{tab}.png` | Desktop screenshots (1920x1080) |
| `tablet-{tab}.png` | Tablet screenshots (768x1024) |
| `mobile-375-{tab}.png` | Mobile screenshots (375x812) |
| `mobile-390-{tab}.png` | Mobile screenshots (390x844) |
| `focus-{n}-{tag}.png` | Keyboard focus screenshots |
| `contrast-report.md` | WCAG color contrast analysis |
| `overflow-report.md` | Horizontal overflow detection |
| `touch-targets.md` | Mobile touch target audit |
| `keyboard-nav-report.md` | Keyboard navigation audit |
| `mobile-ux-notes.md` | Mobile UX research notes |
## Verdict
- **Layout**: PASS (no breaks)
- **Keyboard**: PASS (all focusable elements have visible indicators)
- **Contrast**: PARTIAL (3 failures to address)
- **Touch targets**: PARTIAL (9 undersized elements)
- **Mobile UX**: RESEARCH COMPLETE (recommendations documented, no changes implemented per scope)
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

@@ -0,0 +1,15 @@
# Touch Target Size Report (Mobile)
Viewport: 375x812. WCAG 2.1 recommends minimum 44x44px touch targets.
Found 9 elements smaller than 44px:
- `BUTTON` " d4 " — 66x38px (class: quick-dice-btn rounded-full border border-[var(--gold)] bg-[var(--bg-secondary)] px-2 py-2 text-sm font-semibold text-[var(--gold)] transition-all duration-[var(--transition-base)] hover:bg-[var(--gold)]/10 hover:shadow-lg hover:shadow-[var(--gold)]/20 active:scale-[0.97] active:bg-[var(--accent)] active:text-white active:border-[var(--accent)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-primary)])
- `BUTTON` " d6 " — 66x38px (class: quick-dice-btn rounded-full border border-[var(--gold)] bg-[var(--bg-secondary)] px-2 py-2 text-sm font-semibold text-[var(--gold)] transition-all duration-[var(--transition-base)] hover:bg-[var(--gold)]/10 hover:shadow-lg hover:shadow-[var(--gold)]/20 active:scale-[0.97] active:bg-[var(--accent)] active:text-white active:border-[var(--accent)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-primary)])
- `BUTTON` " d8 " — 66x38px (class: quick-dice-btn rounded-full border border-[var(--gold)] bg-[var(--bg-secondary)] px-2 py-2 text-sm font-semibold text-[var(--gold)] transition-all duration-[var(--transition-base)] hover:bg-[var(--gold)]/10 hover:shadow-lg hover:shadow-[var(--gold)]/20 active:scale-[0.97] active:bg-[var(--accent)] active:text-white active:border-[var(--accent)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-primary)])
- `BUTTON` " d10 " — 66x38px (class: quick-dice-btn rounded-full border border-[var(--gold)] bg-[var(--bg-secondary)] px-2 py-2 text-sm font-semibold text-[var(--gold)] transition-all duration-[var(--transition-base)] hover:bg-[var(--gold)]/10 hover:shadow-lg hover:shadow-[var(--gold)]/20 active:scale-[0.97] active:bg-[var(--accent)] active:text-white active:border-[var(--accent)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-primary)])
- `BUTTON` " d12 " — 66x38px (class: quick-dice-btn rounded-full border border-[var(--gold)] bg-[var(--bg-secondary)] px-2 py-2 text-sm font-semibold text-[var(--gold)] transition-all duration-[var(--transition-base)] hover:bg-[var(--gold)]/10 hover:shadow-lg hover:shadow-[var(--gold)]/20 active:scale-[0.97] active:bg-[var(--accent)] active:text-white active:border-[var(--accent)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-primary)])
- `BUTTON` " d20 " — 66x38px (class: quick-dice-btn rounded-full border border-[var(--gold)] bg-[var(--bg-secondary)] px-2 py-2 text-sm font-semibold text-[var(--gold)] transition-all duration-[var(--transition-base)] hover:bg-[var(--gold)]/10 hover:shadow-lg hover:shadow-[var(--gold)]/20 active:scale-[0.97] active:bg-[var(--accent)] active:text-white active:border-[var(--accent)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-primary)])
- `BUTTON` " d100 " — 66x38px (class: quick-dice-btn rounded-full border border-[var(--gold)] bg-[var(--bg-secondary)] px-2 py-2 text-sm font-semibold text-[var(--gold)] transition-all duration-[var(--transition-base)] hover:bg-[var(--gold)]/10 hover:shadow-lg hover:shadow-[var(--gold)]/20 active:scale-[0.97] active:bg-[var(--accent)] active:text-white active:border-[var(--accent)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-primary)])
- `INPUT` "" — 163x42px (class: w-full bg-[var(--bg-input)] border border-[var(--border-color)] rounded-lg px-3 py-2 text-[var(--text-primary)] text-base placeholder:text-[var(--text-secondary)] hover:border-[var(--text-secondary)] focus:outline-none focus:border-[var(--gold)] focus:ring-1 focus:ring-[var(--gold)] focus-visible:ring-2 focus-visible:ring-[var(--gold)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-primary)] transition-all duration-[var(--transition-base)] flex-1)
- `BUTTON` "Очистить" — 100x34px (class: inline-flex items-center justify-center font-semibold focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-primary)] transition-all duration-[var(--transition-base)] cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed active:scale-[0.97] bg-[var(--bg-card)] text-[var(--text-primary)] border border-[var(--border-color)] hover:border-[var(--gold)] hover:text-[var(--gold)] px-4 py-1.5 text-sm rounded-lg clear-history-btn)