Skip to content

Dashboard redesign in the msk-scripts.de style - #41

Merged
Musiker15 merged 15 commits into
mainfrom
feat/dashboard-redesign
Oct 28, 2026
Merged

Musiker15 merged 15 commits into
mainfrom
feat/dashboard-redesign

Conversation

@Musiker15

Copy link
Copy Markdown
Member

The web dashboard now looks like msk-scripts.de. Routes, API calls, permissions and the CSP are unchanged, src/ is not touched.

Look

  • The shop's anthracite palette with MSK green, Archivo and Source Sans 3 (self-hosted through fontsource), Phosphor instead of Lucide.
  • The shop's dashboard frame: top bar with a 4 px stripe, theme switch and account menu (language and sign out moved there), a sidebar with groups from 1024 px, a drawer below that and a bar at the bottom on phones.
  • Every view restyled: ticket lists, ticket detail, stats, bot control, configuration, permissions, settings.
  • Fluid from 320 px to ultrawide, no horizontal page scroll.

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.js sets 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-dialog and -dropdown-menu are 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

  • The ticket id in the lists is a real link, so middle click opens a ticket in a new tab.
  • Navigation entries are links, document.title names the page.
  • "Remove" in permissions is outlined in the first step, red only in the confirmation.
  • The default accent is the shop's green.

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 against index.css. Every rule was counter-checked by breaking it.
  • npm run build in web/, web/dist rebuilt and committed.
  • 198 screenshots against a mock server with the real CSP: 11 views at 375, 1440 and 3440 px, light and dark, default, bright and dark accent. No overflow, no target under 44 px, no CSP violation; keyboard checks for the dialog, popovers, drawer and skip link pass.

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

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.
Comment thread tests/dashboard-contrast.test.js Fixed
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.
@Musiker15
Musiker15 merged commit afceb0f into main Oct 28, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants