Repository navigation
Dashboard redesign in the msk-scripts.de style - #41
Merged
Merged
Conversation
The dashboard is served next to msk-scripts.de (hosted bots go through the shop's bot proxy) but still looked like the old dark-only MSK panel: DM Sans, Syne and Space Mono, Lucide icons and a palette close to GitHub dark. msk-shop v2.0.0 replaced all of that, so the panel now takes the same foundation. - Tokens from msk-shop DESIGN.md for a light and a dark theme: anthracite surfaces, MSK green from the logo ramp, status colours, a console surface that stays dark for the bot log, and syntax colours for the file editor. - The green is split into a fill (--primary) and a text and border role (--primary-text). With the built-in green both are the same value, but a customer accent can get a readable text variant without losing its fill. - Fluid type and layout tokens with clamp(), one focus outline, tap-target, and rules for prefers-reduced-motion, prefers-contrast and forced-colors. - Archivo and Source Sans 3 from @fontsource-variable, still bundled locally; code and ids use the system monospace. - Phosphor icons through one module that renders them aria-hidden unless they are labelled. Lucide is on the redesign's exclusion list. - Button, field, select, switch, table and notice primitives follow the shop's components: 44 px controls, 1.5 px borders that reach 3:1. tests/dashboard-contrast.test.js reads the tokens from index.css and recomputes every rendered pair in both themes, with a coverage guard so a new colour token cannot ship without a pair. tests/dashboard-a11y-prefs.test.js keeps the system preference rules from being dropped in a later restyle.
…downMenu The shop builds its dialogs and menus on the native <dialog> and popover elements, and the dashboard follows. showModal() brings the focus trap, Escape, the inert page behind and the top layer from the browser, and a popover closes on Escape and on a click outside, so less script has to be right and nothing injects a <style> element at runtime. - components/ui/dialog.jsx: Dialog (title and description wired through aria-labelledby and aria-describedby, backdrop click closes) and Drawer, the navigation below the sidebar breakpoint. Sheet and the unused dropdown-menu wrapper are gone, and with them @radix-ui/react-dialog and @radix-ui/react-dropdown-menu. - components/ui/popover.jsx: a panel on popover="auto" for the top bar menus, placed at its button and kept inside the window. - lib/layers.js: remembers the opener and hands focus back on close when it is lost or still inside the closed layer, but never takes it from where the person has moved it. Browsers mostly do this themselves; this covers closing from code and an opener React re-rendered away. - The close-ticket confirmation and the mobile navigation use them. The language picker is a native select now: a Radix list opens in a portal on <body>, below the modal drawer it also sits in. Select, Tooltip, ScrollArea, Switch, Tabs, Label, Slot and Separator stay on Radix. The CSP is unchanged.
msk-scripts.de offers light and dark and starts with the system setting, and its dashboards share one frame. The bot dashboard was dark only with a frame of its own; it now follows both. Theme: - First visit follows prefers-color-scheme, a switch in the top bar offers system, light and dark as toggle buttons with aria-pressed, and the choice is remembered per browser under tb.theme, like the language. - No flash of the wrong theme: src/theme-init.js puts the class on <html> before the first paint. The CSP has no nonce and forbids inline script, so it is a classic script file on the dashboard's own origin. A small Vite plugin emits it under a content hash (the server caches everything except index.html as immutable) and places it right after the <title>. - theme-core.js holds the rules for React; the pre-paint script repeats them in plain ES5. tests/dashboard-theme.test.js runs both against one table of cases, including blocked storage and unknown stored values. Shell (after msk-shop components/dashboard/AppShell): - Sticky top bar with the 4 px stripe, the product name, the page title below the sidebar breakpoint, the theme switch and an account menu on the native popover with role, language and sign out. - Sidebar with labelled groups (Support, Bot, Administration) from 1024 px, the native drawer below, and a bar with the most used areas at the bottom of a phone. Navigation entries are real links with aria-current, so a middle click opens a new tab; a plain click still routes in the app. - Skip link to the one <main id="main">, a content column capped at 130rem, fluid gutters, and a tab title that names the current page. Routes, the landing view and the permission gating of the navigation are unchanged. New strings are in all seven dashboard languages.
With tokens and shell in place, the views still used the old cards, uppercase table heads and dark-only colours. Each one now follows the dashboard components of msk-shop (PageHeader, SectionCard, StatusBadge, StatTile, Notice, DataTable, FormField), rebuilt in ui.jsx and the primitives. Behaviour, routes, API calls and permission checks stay as they were. - My tickets and Tickets: page head with the count, labelled filters, a table in its own labelled scroll region, the ticket id as a real link (a plain click opens it in the app as before, a middle click opens a new tab), pagination with named previous and next buttons. - Ticket detail: back link, status and lock as badges with word and sign, details as a definition list, rating as stars with a spoken value, the conversation as a focusable region, a labelled reply field with its character count tied to it. The close confirmation is the native dialog. - Statistics: tiles that gain columns with the room they have, handling time as a tile with its hint, team ranking in a section. - Bot control: process state as a badge, controls below the head, the console stays dark in both themes and maps ANSI colours onto console tokens that are checked against it. - Configuration: file buttons and the form/file switch as pressed toggles, collapsible sections with the remove buttons outside the summary, every field tied to its label, hint and error, Discord IDs stacked so they fit a narrow column, the editor with theme-aware syntax colours. - Permissions: permission choices as pressed toggles in a fieldset, the active switch with its label, state badges in the list, red only in the confirming step of a removal. - Dashboard settings: two sections side by side when there is room. Layouts switch on the width of the content column (container queries), not the window. tests/dashboard-design-rules.test.js keeps Tailwind palette colours, bracketed hex values, Lucide and the excluded fonts out of the components, because the contrast test only sees the tokens.
Server owners can brand the dashboard with any hex colour. Until now that colour replaced the green tokens one to one, with black or white text picked by YIQ brightness, in a dark-only panel. With a light and a dark theme, one colour cannot serve every role: a yellow is a fine button in both themes but unreadable as link text on white, a navy disappears as text on anthracite. accent.js derives the accent per theme and role: - Label on the fill by WCAG relative luminance, whichever of white and the shop's near-black label has more contrast. YIQ picked white on pure red, which reaches only 4.0:1. - If no label reaches 4.5:1 on the fill (mid greys), the fill moves away from the label until one does. The hover fill moves further away still. - The soft surface behind the selected item is a tint of the accent that gets paler until text and secondary text on it reach 4.5:1. - As text, sign, border and focus ring the accent has to reach 4.5:1 on the ground, the tile, a card and the soft surface. Where it does not, a darker variant is used in the light theme and a lighter one in the dark theme. Only HSL lightness changes, in 1 % steps, so hue and saturation stay and the variant stays as close to the chosen colour as AA allows. - A colour that already passes is used as it is; the MSK green does. settings.js writes both themes' values as --accent-light-* and --accent-dark-* custom properties through the CSSOM and sets data-accent; index.css maps them onto the tokens, so switching the theme needs no script. The settings page previews the accent in both themes side by side and says in words when a role uses a variant. The default shown there is now the shop's MSK green. tests/dashboard-accent.test.js checks every role in both themes for named colours and a grid over the RGB cube, that a passing colour is kept, that variants do not overshoot, the WCAG label choice, that the measured surfaces equal the tokens in index.css, and that every written property is read.
The Playwright run over every view at 375, 1440 and 3440 px measured the hit area of each control. Two stayed below 44 px: the brand link on a phone, where only its 32 px mark is visible beside the page title, and the text field inside the Discord ID tag inputs, which was 32 px tall inside a 44 px frame, so a tap on the frame's edge missed it. Both now reach 44 px. Also drops the Card primitive, which no view uses since the restyle, and the unused Translate icon.
The changelog lists the redesign, the light and dark theme and the accent fitting under [Unreleased], so they ship with the pending security fixes. The dashboard guide in English and German gets an Appearance section (theme choice, how the accent stays readable in both themes) and points to the new place of the language picker, which moved from the sidebar into the account menu.
web/dist is committed so a self-hoster needs no build step. This build carries the redesign: new fonts and icons, the light and dark theme with the hashed pre-paint script assets/theme-init-*.js, the native dialog and popover, and the accent fitting.
graphify update . (AST only, no LLM) picks up the new modules: the shell components, theme-core and theme-init, accent.js, lib/layers.js, the native dialog and popover, and the new dashboard tests.
tailwind-merge only knows Tailwind's own font sizes and takes any other text-* for a colour. cn() therefore dropped text-danger-foreground and text-primary-foreground as soon as a button's size added text-control or text-small, and every filled button showed the inherited text on its fill: near black on red in the light theme, about 2.4:1. The contrast test could not see it, it checks token pairs, not the merged classes. cn() now registers the type scale of index.css as font sizes, as the shop does since its own redesign. A test reads the --text-* tokens from index.css, so a new size cannot slip past the list.
…riant With a dark customer accent the 4 px stripe all but vanished in the dark theme, and a bright one does the same in the light theme. The stripe now takes the same variant as links, borders and the focus ring, which is checked against both surfaces. Filled buttons keep the colour as chosen.
Tabs, Tooltip, ScrollArea, Separator and Skeleton came with the old component kit and are imported nowhere since the redesign. Four Radix packages only hung on them and go with them; the bundle does not change, the install gets smaller.
The contrast test built a regular expression from the selector and escaped only dots and brackets, which CodeQL reports as incomplete sanitization. A plain line search needs no escaping at all and finds the same rules.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The web dashboard now looks like msk-scripts.de. Routes, API calls, permissions and the CSP are unchanged,
src/is not touched.Look
Light and dark
A first visit follows the system setting, the switch offers System, Light and Dark and is remembered per browser.
src/theme-init.jssets the class before the first paint. The CSP has no nonce, so a small Vite plugin emits it as a hashed file on the dashboard's own origin instead of an inline script.Customer accent
The saved accent now works in both themes. The label on a filled button is black or white by WCAG contrast instead of YIQ (YIQ put white on pure red at 4.0:1). Where the accent would be unreadable as text, links, borders, the focus ring and the stripe use a darker variant in the light theme and a lighter one in the dark theme, found by stepping only the lightness. The settings page previews both themes and says when a variant applies.
Native dialog and menu
The close-ticket confirmation and the mobile drawer use
<dialog>, the top bar menus the popover attribute, with focus return and Escape.@radix-ui/react-dialogand-dropdown-menuare gone, as are four Radix packages that only unused wrappers needed. Select, Switch, Label and Slot stay;style-src-attr 'unsafe-inline'is still needed for them, the ANSI console and the editor.Small behaviour changes
document.titlenames the page.Checks
npm test: 378 passing. New tests for contrast of every token pair in both themes (with a coverage guard), theme choice, dialog and popover focus, the accent fallbacks, design rules, and the class merge: tailwind-merge took our own font sizes for colours and dropped the button label colours, now the type scale is registered and checked againstindex.css. Every rule was counter-checked by breaking it.npm run buildinweb/,web/distrebuilt and committed.The JS bundle grows from 533 to 658 kB (196 kB gzip), Phosphor ships every weight per icon. Ships as 2.21.0 together with the security fixes already in
[Unreleased].