Files
inbuxa-webmail/web/src/styles/__tests__/app-rail.test.ts
T
jcoffey-dev 5fe929c6a4
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 3m4s
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
App rail, pill counts, tinted folder icons and raised controls
On a wide screen the switcher moves from the foot of the folder pane to a
rail down the left edge: Mail (with the inbox's unread badge), Calendar,
Contacts and Files, then Settings and the folder-list toggle at the
bottom. The top bar loses the menu button and the Settings gear there,
both now on the rail. Phones are unchanged: no rail, the tab bar, the
menu button for the drawer and the gear.

- Folder counts are pills: filled with the accent when there is unread
  mail, neutral for totals (Drafts, Scheduled).
- Role folders get their own icon tint (Inbox, Drafts, Sent, Archive,
  Junk, Trash, Scheduled); a color the reader picked still wins.
- Buttons, Compose and the current selection have a top-lit, raised look
  with a pressed state; Compose and the selected rail item are filled
  with the accent.
- Collapsing the sidebar keeps Mail's icon strip; Calendar, Contacts,
  Files, Settings and Admin have no icon-only form, so their sidebar
  hides instead of being crushed.
- The collapsed sidebar no longer draws a cut-off "FOLDERS": the rule
  hiding section headings lost to a later one of the same weight (this
  is on main today too).

Contrast: the accent behind small text is darkened in light themes and
lightened a touch in dark ones. Measured in a browser across all 12
palettes x 6 accents x both modes: text on pills, the rail badge and
Compose at least 4.83:1, tinted icons at least 3.14:1 on the sidebar.

New strings: 2 ("Show folder list", "Hide folder list"), in all ten
catalogs (1699 -> 1701).
2026-10-05 12:01:27 -07:00

50 lines
2.4 KiB
TypeScript

import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
// Read off disk: `?raw` comes back empty for a stylesheet under the test runner.
const css = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "../app.css"), "utf8");
/*
* The app rail, the collapsed sidebar and the raised surfaces are all CSS, and
* jsdom does no layout, so these pin the rules themselves. Contrast for the
* pill accent was measured in a browser across every palette, accent and
* mode; the shape of that rule is pinned here.
*/
const rule = (selector: string) => {
const i = css.indexOf(selector + " {");
expect(i, `no rule for ${selector}`).toBeGreaterThan(-1);
return css.slice(i, css.indexOf("}", i));
};
describe("app layout CSS", () => {
it("gives the rail its own column ahead of the sidebar, collapsed or not", () => {
expect(rule(".app-body.has-rail")).toMatch(/grid-template-columns:\s*56px var\(--sidebar-w\)/);
expect(rule(".app-body.has-rail.collapsed")).toMatch(/grid-template-columns:\s*56px var\(--sidebar-w-collapsed\)/);
});
it("hides a sidebar with no icon-only form instead of crushing it", () => {
expect(rule(".app-body.has-rail.sidebar-hidden")).toMatch(/grid-template-columns:\s*56px 0 /);
expect(rule(".app-body.sidebar-hidden > .sidebar")).toMatch(/visibility:\s*hidden/);
});
it("hides section headings when collapsed, over the sidebar's own display rule", () => {
// `.sidebar .nav-section { display: flex }` comes later at the same weight;
// the collapsed rule has to outweigh it or "FOLDERS" is drawn cut off.
expect(css).toMatch(/\.app-body\.collapsed \.sidebar \.nav-section[^{]*\{\s*display:\s*none/);
});
it("darkens the pill accent in light themes and lightens it in dark ones", () => {
expect(css).toMatch(/:root \{ --pill-accent: color-mix\(in srgb, var\(--accent\) \d+%, black\); \}/);
expect(css).toMatch(/:root\[data-theme="dark"\] \{ --pill-accent: color-mix\(in srgb, var\(--accent\) \d+%, white\); \}/);
});
it("lets a color the reader picked win over the role tint", () => {
for (const role of ["inbox", "drafts", "sent", "archive", "junk", "trash", "scheduled"]) {
expect(css).toContain(`.folder-icon[data-role="${role}"]:not([style*="--folder-color"]) svg`);
}
});
});