Compare commits

...
24 Commits
Author SHA1 Message Date
surtic86andClaude Opus 5.5 75209874ec fix: + button after a select lines up with Assign to me
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 10:48:12 +02:00
surtic86andClaude Opus 5.5 901ed1c098 fix: e2e size helper keeps the user's other appearance choices
It sent the size alone, which replaced the admin's saved mode, contrast
and color.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 10:18:45 +02:00
surtic86andClaude Opus 5.5 f08d2c3f6c fix: center text vertically in Compact and Large
The size modes scale rem line heights to fractions (1.25rem = 22.857px in
Large), and pixel rounding moved text up to 1.2px off center. Type scale
line heights and the header's 3rem now round to whole px (no change in
Normal). Segmented buttons, Reset to default and the project select drop
core's vertical padding, which made the line overflow and sink.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 10:18:45 +02:00
surtic86andClaude Opus 5.5 f5119bba50 fix: mobile search container grows with the search field in Large
Core sets the container to 64px with a 54px line-height, so the 56px
Large search pill pushed against the avatar row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:32:28 +02:00
surtic86andClaude Opus 5.5 833393a686 docs: size modes (T10)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:24:04 +02:00
surtic86andClaude Opus 5.5 232958cd13 test: SIZE option for screenshots and form alignment; scale filter rows (T9)
Query filter rows kept a fixed 40px height, so in Compact their labels
sat 3px below the operator select.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:24:04 +02:00
surtic86andClaude Opus 5.5 5738abe3ef test: check-sizes measures text and control heights per size mode (T8)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:24:04 +02:00
surtic86andClaude Opus 5.5 77ff80e60b test: size in the appearance end-to-end check (T7)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:24:04 +02:00
surtic86andClaude Opus 5.5 94a9bd066e feat: validate and store the size choice in redmine_tonalmine (T6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:17:23 +02:00
surtic86andClaude Opus 5.5 7ef21c739a feat: size picker in the appearance menu and admin default (T5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:17:23 +02:00
surtic86andClaude Opus 5.5 9a04f94118 feat: density token and root font size for size modes (T1-T4)
Compact, Normal and Large scale the text (13/14/16px) and the control
heights (32/40/48px) via data-size on <html>. Normal is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:16:40 +02:00
surtic86 dc927062a3 stff 2026-10-15 08:49:11 +02:00
surtic86andClaude Opus 5.5 70640da6f2 fix: lighter placeholders and regular-weight sidebar links
Placeholders use the outline color, so an empty field no longer looks
filled.

Sidebar link lists (queries, versions, wiki index, admin menu) use
regular body text; only the section titles stay bold. Each item gets
8px vertical padding and a 2px gap, so wrapped names stay apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:24:08 +02:00
surtic86andClaude Opus 5.5 44084eaffa fix: contrast of progress bars, selections and selected query
At medium and high contrast, secondary-container turns dark. The empty
part of progress bars, highlighted code lines and a targeted roadmap
header now use surface-container-highest, so the fill and the text stay
readable. The selected project in the project jump is light at these
levels too; its check mark marks it.

The selected sidebar query gets the pill style even with its "clear"
icon next to it. The icon sits inside the pill and takes the pill's
text color.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 09:43:14 +02:00
Andreas Reinhold / reiniandClaude Opus 5.5 db58c6e86f fix: align form field widths across the app
From 900px up, selects and text fields fill their column up to 480px,
numbers and dates are 140px, and long text stays full width, so every
form shares one right edge. Below 900px every field runs full width.
Overrides core's id-pinned widths (% Done, mobile dates, settings
textareas). check-form-alignment.mjs now checks widths at 1440, 1024
and 390px across all settings tabs and admin forms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 14:12:44 +02:00
Andreas Reinhold / reiniandClaude Opus 5.5 20b7269df2 feat: redmine_tonalmine plugin for default and per-account appearance
Admins set the default mode, contrast and color on Settings > Display,
shown as three labelled form rows below the Theme select. Logged-in
users' choices are saved on their account and override only the parts
they changed; anonymous visitors get the default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 14:12:44 +02:00
Andreas Reinhold / reiniandClaude Opus 5.5 77040e24a8 fix: calendar month bar and mobile agenda layout
The month and year selects sit between previous/next icon buttons on one
row, with Apply / Clear / Save after them, on all widths. On mobile the
calendar becomes an agenda: date column beside the issues, days of
neighbouring months hidden, empty days collapsed to one line, lighter
week headers and a highlighted today row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 13:06:00 +02:00
Andreas Reinhold / reiniandClaude Opus 5.5 d687084cf0 fix: full-width mobile query filters and tonal "Add filter" button
On mobile the filter operator and value fields span the row instead of
fixed desktop widths, and empty value rows are hidden. "Add filter"
becomes an M3 tonal button on all widths so it stands out from the
outlined filter fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:53:02 +02:00
Andreas Reinhold / reiniandClaude Opus 5.5 906153014b fix: structure calendar/Gantt hover cards into title and field grid
- Regroup Redmine's "<strong>Label</strong>: value<br>" tooltip lines on
  first hover into a title block and a label/value grid (theme.js).
- Style the issue link as a kicker, the subject as title-small, and the
  labels as a muted column next to aligned values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:44:19 +02:00
Andreas Reinhold / reiniandClaude Opus 5.5 6c271ffffe feat: contrast levels and custom color picker in appearance menu
Generate medium/high contrast schemes, add a custom swatch that builds a scheme
in the browser via lazy-loaded scheme.js, allow Theme Builder exports as palettes.
Drop the teal palette and spread the swatches across the menu width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:40:34 +02:00
Andreas Reinhold / reiniandClaude Opus 5.5 b4eba7ae63 chore: mount local Redmine plugins in dev container
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:40:34 +02:00
Andreas Reinhold / reiniandClaude Opus 5.5 a79c9404d5 fix: style calendar/Gantt hover cards and plain tooltips for Material 3
- Theme div.tooltip span.tip as an M3 rich tooltip (surface-container,
  elevation, rounded corners, body-medium text).
- Raise .ui-tooltip specificity so jQuery UI's .ui-widget-content rule
  no longer overrides the inverse-surface colors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:35:04 +02:00
Andreas Reinhold / reiniandClaude Opus 5.5 0e9ce26a1a fix: mobile drawer width, group dividers and sidebar form layout
- Drawer uses the M3 width (up to 360px) so the appearance picker fits;
  segmented buttons get equal widths and swatches no longer shrink.
- Dividers separate the drawer groups, so section headings no longer
  read as nav items.
- Sidebar forms in the drawer (e.g. activity filters) line up with the
  nav labels, fields stack under their label, checkbox rows align.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:28:25 +02:00
Andreas Reinhold / reiniandClaude Opus 5.5 b6654f31e7 fix: mobile navigation drawer overlays content instead of pushing it
Core's responsive.css shifts #wrapper and #header aside when the flyout
opens. Make it an M3 modal drawer: fixed over the page with a scrim that
closes it on tap via core's existing #main click handler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:22:13 +02:00
26 changed files with 1332 additions and 193 deletions
+1
View File
@@ -3,3 +3,4 @@ screenshots/
.DS_Store
tools/.cache/
docs/plans/
dev/plugins/
+35 -6
View File
@@ -8,6 +8,10 @@ Material Design 3 theme for Redmine 7.0.
2. Restart Redmine (theme assets are compiled at boot).
3. Select **Tonalmine** in Administration → Settings → Display → Theme.
Optional: the `redmine_tonalmine` plugin adds a default appearance for administrators and saves each
user's choice on their account. Copy the `redmine_tonalmine/` folder into `<redmine>/plugins/` and
restart Redmine (no migrations).
## Development
Requires Docker.
@@ -32,6 +36,8 @@ node screenshots.mjs # all pages, light/dark × desktop/mobile → sc
node screenshots.mjs issue # only pages whose name contains "issue"
node check-form-alignment.mjs # fails if a form label is not level with its field
node check-font-sizes.mjs # fails if any text is off the type scale (e.g. core 0.9em leftovers)
node check-sizes.mjs # fails if text, fields, buttons or rows are off in a size mode (needs the plugin)
SIZE=large node screenshots.mjs # screenshots or check-form-alignment.mjs in a size mode (compact or large; needs the plugin)
```
### Icons
@@ -48,9 +54,25 @@ the map, so run it after every Redmine update.
### Colors
Each user picks a mode (Auto / Light / Dark) and a color (Purple, Blue, Teal, Green, Orange) in the
**Appearance** section of the profile menu (top right; in the mobile menu at the bottom). The choice is
stored per browser (`localStorage`); Auto follows the operating system.
Each user picks a mode (Auto / Light / Dark), a contrast level (Standard / Medium / High), a size
(Compact / Normal / Large) and a color (Purple, Blue, Green, Orange) in the
**Appearance** section of the profile menu (top right; in the mobile menu at the bottom). Auto follows
the operating system. The size scales text (13 / 14 / 16px) and the height of fields, buttons and
table rows (32 / 40 / 48px), also on phones, where Compact makes touch targets smaller than 48px.
Without the plugin, the choice is stored per browser (`localStorage`).
With the `redmine_tonalmine` plugin:
- Administrators set the default appearance in Administration → Settings → Display, below the Theme
select. It shows while Tonalmine is selected; when switching from another theme, save once first.
- Logged-in users' choices are saved on their account (`pref[:tonalmine]`) and follow them to every
browser. Only the parts a user changed override the default; **Reset to default** clears them.
A choice already stored in the browser is moved to the account on the first visit.
- Anonymous visitors get the default; their own choice is still stored per browser.
`node e2e/check-appearance.mjs` checks this end to end (it empties the default when done). The last (rainbow) swatch
lets users pick any color: `javascripts/scheme.js` is loaded on demand, generates the scheme in the
browser, and the generated CSS is stored with the choice, so later page loads don't need it.
The color roles are generated from one seed color per palette with `@material/material-color-utilities`
(scheme TonalSpot) into `tonalmine/stylesheets/palettes.css`:
@@ -58,12 +80,19 @@ The color roles are generated from one seed color per palette with `@material/ma
```sh
cd tools/palettes
npm install # once
npm run build # writes palettes.css
npm run build # writes palettes.css and javascripts/scheme.js
```
To add or change a palette, edit `PALETTES` in `tools/palettes/build_palettes.mjs` and in
`tonalmine/javascripts/theme.js` (the build fails if they differ), then rebuild. The only other
hard-coded colors are the select chevrons `images/arrow-drop-down*.svg` (one per mode).
`tonalmine/javascripts/theme.js` (the build fails if they differ), then rebuild.
To use a scheme from [Material Theme Builder](https://material-foundation.github.io/material-theme-builder/)
as-is: export it as Web (CSS), copy its `light.css` and `dark.css` into `tools/palettes/themes/<name>/`,
and also its `light-mc.css`, `light-hc.css`, `dark-mc.css` and `dark-hc.css` contrast files. Then add
`['<name>', '<seed>', 'themes/<name>']` to `PALETTES`. The roles are copied from the export; the seed
only tints the success/warning colors.
The only other hard-coded colors are the select chevrons `images/arrow-drop-down*.svg` (one per mode).
## License
+2
View File
@@ -7,6 +7,8 @@ services:
- redmine-sqlite:/usr/src/redmine/sqlite
- redmine-files:/usr/src/redmine/files
- ../tonalmine:/usr/src/redmine/themes/tonalmine
- ./plugins:/usr/src/redmine/plugins
- ../redmine_tonalmine:/usr/src/redmine/plugins/redmine_tonalmine
- .:/seed:ro
volumes:
+171
View File
@@ -0,0 +1,171 @@
// Checks the redmine_tonalmine plugin: the admin default appearance (Display settings tab), users'
// choices saved on their account, Reset to default, the one-time upload of a localStorage choice,
// and the endpoint's validation. Needs the plugin mounted (dev/docker-compose.yml) and the seed users.
// Usage: node check-appearance.mjs (exits 1 on any failure; leaves the admin default empty)
import { chromium } from 'playwright';
const BASE = process.env.REDMINE_URL ?? 'http://127.0.0.1:3070';
const browser = await chromium.launch();
let failures = 0;
function check(name, ok, detail = '') {
if (!ok) failures++;
console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${ok ? '' : `\n ${detail}`}`);
}
async function login(user, password = 'password123', setup) {
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await context.newPage();
await page.goto(`${BASE}/login`);
if (setup) await page.evaluate(setup);
await page.fill('#username', user);
await page.fill('#password', password);
await page.click('#login-submit');
await page.waitForURL((url) => !url.pathname.startsWith('/login'));
await page.waitForLoadState('networkidle');
return page;
}
// The <html> attributes theme.js sets; absent means the built-in default (auto, standard, normal, purple).
const looks = (page) => page.evaluate(() => {
const d = document.documentElement.dataset;
return `${d.mode ?? 'auto'} ${d.contrast ?? 'standard'} ${d.size ?? 'normal'} ${d.palette ?? 'purple'}`;
});
async function expectLook(name, page, want) {
const got = await looks(page);
check(name, got === want, `expected "${want}", got "${got}"`);
}
const put = (page, body, withToken = true) => page.evaluate(async ([body, withToken]) => {
const headers = { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' };
if (withToken) headers['X-CSRF-Token'] = document.querySelector('meta[name="csrf-token"]').content;
const response = await fetch('/tonalmine/appearance', { method: 'PUT', headers, body: JSON.stringify(body) });
return response.status;
}, [body, withToken]);
// Clicks a picker control in the profile menu and waits until the account has it.
async function pick(page, selector) {
const saved = page.waitForResponse((r) => r.url().includes('/tonalmine/appearance'));
await page.evaluate((s) => document.querySelector(`#account ${s}`).click(), selector);
return (await saved).status();
}
// Picks a custom color the way the color dialog does (input while dragging, change at the end).
const pickColor = (page, scope, seed) => page.evaluate(([scope, seed]) => {
const input = document.querySelector(`${scope} [data-nm-custom]`);
input.value = seed;
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}, [scope, seed]);
const customCss = (page) => page.evaluate(() => document.getElementById('nm-custom-palette')?.textContent.length ?? 0);
async function saveDefault(page, change) {
await page.goto(`${BASE}/settings?tab=display`);
await change();
await Promise.all([page.waitForURL(/tab=display/), page.click('#tab-content-display form [type=submit]')]);
await page.waitForLoadState('networkidle');
}
const resetUser = async (user) => {
const page = await login(user);
await put(page, {});
await page.context().close();
};
await Promise.all(['jdoe', 'msmith', 'pweber'].map(resetUser));
// 1. Admin default on the Display settings tab
const admin = await login('admin', 'admin');
await admin.goto(`${BASE}/settings?tab=display`);
// Mode, contrast, size and color are four form rows
const rowsShown = () => admin.$$eval('p:has([data-nm-default])', (rows) => rows.length === 4 && rows.map((r) => !!r.offsetParent));
check('default picker shown while Tonalmine is selected', (await rowsShown())?.every(Boolean));
await admin.selectOption('#settings_ui_theme', '');
check('default picker hidden and not posted for another theme', (await rowsShown())?.every((shown) => !shown) &&
(await admin.$$eval('[data-nm-default-input]', (inputs) => inputs.every((i) => i.disabled))));
await saveDefault(admin, async () => {
await admin.click('[data-nm-default] [data-nm-palette="blue"]');
await admin.click('[data-nm-default] [data-nm-mode="dark"]');
});
check('default picker shows the saved default', await admin.evaluate(() =>
document.querySelector('[data-nm-default] [data-nm-palette="blue"]').getAttribute('aria-pressed') === 'true' &&
document.querySelector('[data-nm-default] [data-nm-mode="dark"]').getAttribute('aria-pressed') === 'true'));
// 2. A user without a choice gets the default
const jdoe = await login('jdoe');
await expectLook('user without a choice gets the default', jdoe, 'dark standard normal blue');
// 3. The user's choice is saved on the account and only overrides that part
check('choosing Light is saved (204)', (await pick(jdoe, '[data-nm-mode="light"]')) === 204);
check('choosing Compact is saved (204)', (await pick(jdoe, '[data-nm-size="compact"]')) === 204);
const jdoe2 = await login('jdoe');
await expectLook('choice follows the user to another browser', jdoe2, 'light standard compact blue');
// 4. Parts the user didn't change follow a new default; the user's Compact beats the default Large
await saveDefault(admin, async () => {
await admin.click('[data-nm-default] [data-nm-palette="green"]');
await admin.click('[data-nm-default] [data-nm-size="large"]');
});
await jdoe2.reload({ waitUntil: 'networkidle' });
await expectLook('unchanged parts follow the new default', jdoe2, 'light standard compact green');
// 5. Reset to default
check('Reset to default is shown after a change', await jdoe2.isVisible('#account [data-nm-reset]') ||
await jdoe2.evaluate(() => !document.querySelector('#account [data-nm-reset]').hidden));
check('reset is saved (204)', (await pick(jdoe2, '[data-nm-reset]')) === 204);
await expectLook('reset returns to the default', jdoe2, 'dark standard large green');
await jdoe2.reload({ waitUntil: 'networkidle' });
await expectLook('reset is kept after reload', jdoe2, 'dark standard large green');
check('Reset to default is hidden without a choice', await jdoe2.evaluate(() => document.querySelector('#account [data-nm-reset]').hidden));
// 5b. A custom color, with its generated CSS, is saved on the account
const savedColor = jdoe2.waitForResponse((r) => r.url().includes('/tonalmine/appearance'));
await pickColor(jdoe2, '#account', '#c0392b');
check('custom color is saved (204)', (await savedColor).status() === 204);
const jdoe3 = await login('jdoe');
await expectLook('custom color follows the user', jdoe3, 'dark standard large custom');
check('custom color CSS comes from the account', (await customCss(jdoe3)) > 1000);
await pick(jdoe3, '[data-nm-reset]');
// 6. A localStorage choice from before the plugin moves to the account once
const msmith = await login('msmith', 'password123', () => localStorage.setItem('tonalmine', '{"contrast":"high"}'));
await expectLook('localStorage choice is applied after login', msmith, 'dark high large green');
check('localStorage is cleared for logged-in users', await msmith.evaluate(() => localStorage.getItem('tonalmine') === null));
const msmith2 = await login('msmith');
await expectLook('uploaded choice is on the account', msmith2, 'dark high large green');
// 7. Validation; the request without a CSRF token goes last, since Redmine then ends the session
const pweber = await login('pweber');
check('valid choice is saved (204)', (await put(pweber, { contrast: 'medium' })) === 204);
for (const body of [{ mode: 'neon' }, { palette: 'custom' }, { palette: 'custom', seed: '#123456', css: '</style>' }, { size: 'huge' }, { foo: 1 }, { mode: 5 }]) {
const status = await put(pweber, body);
check(`invalid ${JSON.stringify(body)} is refused (422)`, status === 422, `got ${status}`);
}
const tokenless = await put(pweber, { contrast: 'high' }, false);
check('request without CSRF token is refused (422)', tokenless === 422, `got ${tokenless}`);
const pweber2 = await login('pweber');
await expectLook('refused requests left the choice unchanged', pweber2, 'dark medium large green');
// 8. Anonymous visitors get the default
const anonymous = await (await browser.newContext()).newPage();
await anonymous.goto(`${BASE}/login`);
await expectLook('anonymous visitor gets the default', anonymous, 'dark standard large green');
// 8b. A custom default color reaches everyone
await saveDefault(admin, async () => {
await pickColor(admin, '[data-nm-default]', '#1e88e5');
await admin.waitForFunction(() => document.querySelector('[data-nm-default-input="css"]').value.length > 1000);
});
await anonymous.reload();
await expectLook('custom default reaches anonymous visitors', anonymous, 'dark standard large custom');
check('custom default CSS is rendered', (await customCss(anonymous)) > 1000);
// 9. Clean up: empty default, so other checks and screenshots see the built-in look
await saveDefault(admin, () => admin.$$eval('[data-nm-default-input]', (inputs) => inputs.forEach((i) => { i.value = ''; })));
await anonymous.reload();
await expectLook('empty default restores the built-in look', anonymous, 'auto standard normal purple');
await Promise.all(['jdoe', 'msmith', 'pweber'].map(resetUser));
await browser.close();
process.exit(failures ? 1 : 0);
+58 -17
View File
@@ -1,47 +1,88 @@
// Checks that each form label (.tabular forms, query filters) lines up with the field next to it.
// Checks .tabular forms and query filters at desktop, tablet landscape and phone width:
// - each form label lines up with the field next to it (not on phones, where labels stack)
// - field widths follow the three tracks (components.css): standard fields fill the column up
// to 480px, compact fields (numbers, dates) are 140px, long-text fields fill the column;
// on phones every field fills its row
// Usage: node check-form-alignment.mjs (exits 1 if any row is off by more than 2px)
// SIZE=compact|large node check-form-alignment.mjs (in that size mode; needs the plugin)
import { chromium } from 'playwright';
import { setSize } from './size.mjs';
const BASE = process.env.REDMINE_URL ?? 'http://127.0.0.1:3070';
const SIZE = process.env.SIZE;
const settingsTabs = ['general', 'display', 'authentication', 'api', 'projects', 'users', 'issues', 'timelog', 'attachments', 'mail_handler', 'repositories'];
const pages = [
'/projects?display_type=list', '/projects/apollo/issues', '/projects/apollo/time_entries', '/projects/apollo/issues/new', '/issues/13/edit', '/issues/bulk_edit?ids[]=13&ids[]=17',
'/projects/apollo/time_entries/new', '/settings', '/my/account', '/users/5/edit',
'/projects/apollo/settings', '/custom_fields/1/edit', '/projects/apollo/wiki/Wiki/edit',
'/projects/apollo/time_entries/new', '/my/account', '/my/password', '/users/5/edit',
'/projects/apollo/settings', '/projects/apollo/settings/issues', '/custom_fields/1/edit', '/projects/apollo/wiki/Wiki/edit',
'/projects/apollo/wiki/Wiki/rename', '/projects/apollo/versions/new', '/projects/apollo/news/new', '/projects/apollo/documents/new',
'/projects/apollo/queries/new', '/projects/new', '/roles/new', '/trackers/new', '/groups/new',
...settingsTabs.map((tab) => `/settings?tab=${tab}`),
];
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
let failures = 0;
for (const width of [1440, 1024, 390]) {
const page = await browser.newPage({ viewport: { width, height: 900 } });
await page.goto(`${BASE}/login`);
await page.fill('#username', 'admin');
await page.fill('#password', 'admin');
await page.click('#login-submit');
await page.waitForURL((url) => !url.pathname.startsWith('/login'));
if (SIZE) await setSize(page, SIZE);
let failures = 0;
for (const path of pages) {
await page.goto(`${BASE}${path}`, { waitUntil: 'networkidle' });
const rows = await page.evaluate(() => [
const rows = await page.evaluate((stacked) => {
const visible = (el) => el && el.offsetParent;
const out = [];
if (!stacked) {
// Query filters: label text vs. operator select
...[...document.querySelectorAll('#filters-table .filter')].flatMap((row) => {
for (const row of document.querySelectorAll('#filters-table .filter')) {
const label = row.querySelector('.field label'), control = row.querySelector('.operator select');
if (!label || !control || !control.offsetParent) return [];
if (!label || !visible(control)) continue;
const l = label.getBoundingClientRect(), c = control.getBoundingClientRect();
return [{ label: `filter ${label.textContent.trim().slice(0, 22)}`, offset: Math.round(l.top + l.height / 2 - (c.top + c.height / 2)) }];
}),
...[...document.querySelectorAll('.tabular p')].flatMap((row) => {
out.push({ label: `filter ${label.textContent.trim().slice(0, 22)}`, offset: Math.round(l.top + l.height / 2 - (c.top + c.height / 2)) });
}
// Labels vs. fields (and the default appearance groups on the Display tab)
for (const row of document.querySelectorAll('.tabular p')) {
const label = row.querySelector(':scope > label:not(.floating):not(.block):not(.inline):not(.no-css)');
const control = row.querySelector(':scope > input:not([type=hidden]), :scope > select, :scope > span > input:not([type=hidden]), :scope > span > select');
if (!label || !control || !label.offsetParent || !control.offsetParent) return [];
const control = row.querySelector(':scope > input:not([type=hidden]), :scope > select, :scope > span > input:not([type=hidden]), :scope > span > select, :scope > .nm-appearance > [role="group"]');
if (!visible(label) || !visible(control)) continue;
const style = getComputedStyle(label);
const labelBox = label.getBoundingClientRect();
const controlBox = control.getBoundingClientRect();
const labelMiddle = labelBox.top + parseFloat(style.paddingTop) + parseFloat(style.lineHeight) / 2;
return [{ label: label.textContent.trim().slice(0, 30), offset: Math.round(labelMiddle - (controlBox.top + controlBox.height / 2)) }];
}),
]);
out.push({ label: label.textContent.trim().slice(0, 30), offset: Math.round(labelMiddle - (controlBox.top + controlBox.height / 2)) });
}
}
// Field widths
const textLike = 'textarea, select:not([multiple]), input:is([type="text"], [type="password"], [type="email"], [type="url"], [type="search"], [type="tel"], [type="number"], [type="date"], :not([type]))';
const compact = 'input:is([type="number"], [type="date"], .date, [size="1"], [size="2"], [size="3"], [size="4"], [size="5"], [size="6"])';
const full = 'textarea, input:is(.string_cf, .link_cf, #issue_subject, #document_title, #content_comments, #time_entry_comments)';
for (const field of document.querySelectorAll(`.tabular p > :is(${textLike}), .tabular p > span > :is(${textLike})`)) {
if (!visible(field)) continue;
// The project select shares its row with the floated "Private" checkbox
if (field.id === 'issue_project_id') continue;
const row = field.closest('p');
const style = getComputedStyle(row);
const column = row.clientWidth - parseFloat(style.paddingInlineStart) - parseFloat(style.paddingInlineEnd);
const icon = field.nextElementSibling?.matches('.icon-only') ? 36 : 0;
const expected = stacked || field.matches(full) ? column - icon
: field.matches(compact) ? 140
: Math.min(480, column) - icon;
const actual = field.getBoundingClientRect().width;
const name = row.querySelector(':scope > label')?.textContent.trim().slice(0, 30) || field.id || field.name;
out.push({ label: `width ${name}`, offset: Math.round(actual - expected) });
}
return out;
}, width < 900);
const bad = rows.filter((r) => Math.abs(r.offset) > 2);
failures += bad.length;
console.log(`${bad.length ? 'FAIL' : 'ok '} ${path} (${rows.length} rows)${bad.map((r) => `\n ${r.label}: ${r.offset}px`).join('')}`);
console.log(`${bad.length ? 'FAIL' : 'ok '} ${width} ${path} (${rows.length} checks)${bad.map((r) => `\n ${r.label}: ${r.offset}px`).join('')}`);
}
if (SIZE) await setSize(page);
await page.close();
}
await browser.close();
+51
View File
@@ -0,0 +1,51 @@
// Checks the size modes (Compact / Normal / Large): body text and the heights of a table row, a
// text field, the submit button and (on phones) a mobile menu link. Sets the size on the admin
// account, so it needs the redmine_tonalmine plugin (dev/docker-compose.yml); resets it when done.
// Usage: node check-sizes.mjs (exits 1 on any failure)
import { chromium } from 'playwright';
import { setSize } from './size.mjs';
const BASE = process.env.REDMINE_URL ?? 'http://127.0.0.1:3070';
// size → [body text, fields / buttons / phone table rows, mobile menu links]
const SIZES = { compact: ['13px', 32, 40], normal: ['14px', 40, 48], large: ['16px', 48, 56] };
const browser = await chromium.launch();
let failures = 0;
function check(name, got, want) {
const ok = got === want;
if (!ok) failures++;
console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${ok ? '' : `\n expected ${want}, got ${got}`}`);
}
const height = (page, selector) => page.$eval(selector, (el) => Math.round(el.getBoundingClientRect().height));
for (const [width, mobile] of [[1440, false], [390, true]]) {
const page = await browser.newPage({ viewport: { width, height: 900 } });
await page.goto(`${BASE}/login`);
await page.fill('#username', 'admin');
await page.fill('#password', 'admin');
await page.click('#login-submit');
await page.waitForURL((url) => !url.pathname.startsWith('/login'));
for (const [size, [text, control, menuLink]] of Object.entries(SIZES)) {
await setSize(page, size);
const at = `${size} ${width}`;
await page.goto(`${BASE}/projects/apollo/issues`, { waitUntil: 'networkidle' });
check(`${at} body text`, await page.$eval('body', (el) => getComputedStyle(el).fontSize), text);
// On desktop the cells' 4px padding comes on top of their block-size
check(`${at} issue row`, await height(page, 'table.list.issues tbody tr:first-child'), control + (mobile ? 0 : 8));
if (mobile) {
await page.click('.mobile-toggle-button');
await page.waitForSelector('.flyout-menu .js-project-menu a.overview', { state: 'visible' });
check(`${at} mobile menu link`, await height(page, '.flyout-menu .js-project-menu a.overview'), menuLink);
}
await page.goto(`${BASE}/projects/apollo/issues/new`, { waitUntil: 'networkidle' });
check(`${at} text field`, await height(page, '#issue_subject'), control);
check(`${at} submit button`, await height(page, 'input[name="commit"]'), control);
}
await setSize(page);
await page.close();
}
await browser.close();
process.exit(failures ? 1 : 0);
+8 -1
View File
@@ -1,12 +1,15 @@
// Captures the page checklist in light/dark × desktop/mobile into ../screenshots/.
// Usage: node screenshots.mjs [filter] e.g. `node screenshots.mjs issue` for pages whose name contains "issue".
// SIZE=compact|large node screenshots.mjs (in that size mode, files end in -compact/-large; needs the plugin)
// Needs the dev server with seed data (see README).
import { chromium } from 'playwright';
import { mkdir } from 'node:fs/promises';
import { setSize } from './size.mjs';
const BASE = process.env.REDMINE_URL ?? 'http://127.0.0.1:3070';
const OUT = new URL('../screenshots/', import.meta.url).pathname;
const filter = process.argv[2] ?? '';
const SIZE = process.env.SIZE;
const pages = [
['my-page', '/my/page'],
@@ -111,9 +114,11 @@ for (const scheme of ['light', 'dark']) {
for (const [vpName, vp] of Object.entries(viewports)) {
const { width, height, ...rest } = vp;
const context = await browser.newContext({ viewport: { width, height }, colorScheme: scheme, ...rest });
// Logged out, the size comes from localStorage; logged in, from the account (set below)
if (SIZE) await context.addInitScript((size) => localStorage.setItem('tonalmine', JSON.stringify({ size })), SIZE);
const page = await context.newPage();
const shot = (name, fullPage = true) =>
page.screenshot({ path: `${OUT}${name}-${scheme}-${vpName}.png`, fullPage });
page.screenshot({ path: `${OUT}${name}-${scheme}-${vpName}${SIZE ? `-${SIZE}` : ''}.png`, fullPage });
for (const [name, path] of publicPages.filter(([n]) => n.includes(filter))) {
await page.goto(`${BASE}${path}`, { waitUntil: 'networkidle' });
@@ -124,6 +129,7 @@ for (const scheme of ['light', 'dark']) {
await page.fill('#password', 'admin');
await page.click('#login-submit');
await page.waitForURL((url) => !url.pathname.startsWith('/login'));
if (SIZE) await setSize(page, SIZE);
for (const [name, path] of pages.filter(([n]) => n.includes(filter))) {
await page.goto(`${BASE}${path}`, { waitUntil: 'networkidle' });
@@ -135,6 +141,7 @@ for (const scheme of ['light', 'dark']) {
await act(page);
await shot(name, !opts.viewportOnly);
}
if (SIZE) await setSize(page);
await context.close();
}
}
+16
View File
@@ -0,0 +1,16 @@
// Saves a size mode on the logged-in user's account (needs the redmine_tonalmine plugin). The user's
// other choices (mode, contrast, color) are kept. No size removes the size choice again.
// The page must be loaded after the last change, since the saved choice is read from it.
export async function setSize(page, size) {
const status = await page.evaluate(async (size) => {
// The plugin hands the saved choice to theme.js in <head>: Tonalmine.configure({..., user: {...}})
const script = [...document.scripts].find((s) => s.text.includes('Tonalmine.configure('));
const prefs = JSON.parse(script.text.match(/Tonalmine\.configure\((.*)\);/s)[1]).user;
if (size) prefs.size = size;
else delete prefs.size;
const headers = { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content };
return (await fetch('/tonalmine/appearance', { method: 'PUT', headers, body: JSON.stringify(prefs) })).status;
}, size);
if (status !== 204) throw new Error(`could not save size ${size ?? 'default'} (HTTP ${status})`);
}
@@ -0,0 +1,17 @@
# Saves the current user's appearance (sent by theme.js). The body replaces the whole saved
# choice; an empty object resets the user to the default.
class TonalmineAppearanceController < ApplicationController
before_action :require_login
def update
prefs = params.to_unsafe_h.except('controller', 'action', 'tonalmine_appearance')
errors = RedmineTonalmine::Appearance.errors(prefs)
if errors.any?
render json: {errors: errors}, status: :unprocessable_content
else
User.current.pref[:tonalmine] = RedmineTonalmine::Appearance.clean(prefs)
User.current.pref.save
head :no_content
end
end
end
+3
View File
@@ -0,0 +1,3 @@
# No format segment: Redmine treats .json requests as API calls, which skip the session and the
# CSRF check. theme.js sends JSON to this plain path with the X-CSRF-Token header instead.
put 'tonalmine/appearance', to: 'tonalmine_appearance#update', as: 'tonalmine_appearance', format: false
+14
View File
@@ -0,0 +1,14 @@
# Companion plugin for the Tonalmine theme: an administrator default appearance (set in
# Administration > Settings > Display, below the theme select) and each user's appearance saved
# on their account. The theme works without it; then the choice is stored per browser.
Redmine::Plugin.register :redmine_tonalmine do
name 'Tonalmine'
author 'Andreas Reinhold'
description 'Default appearance and per-user appearance for the Tonalmine theme'
version '0.1.0'
requires_redmine version_or_higher: '7.0.0'
# No :partial: the default is edited on the Display settings tab (see theme.js).
settings default: {}
end
RedmineTonalmine::Hooks # loads and registers the hook listener
@@ -0,0 +1,61 @@
module RedmineTonalmine
# Validates appearance hashes ({mode, contrast, size, palette, seed, css}) coming from users and from
# the Display settings form. Any part may be missing: it then falls back to the default.
module Appearance
MODES = %w[auto light dark].freeze
CONTRASTS = %w[standard medium high].freeze
SIZES = %w[compact normal large].freeze
# Ids must match PALETTES in tonalmine/javascripts/theme.js (tools/palettes checks this).
PALETTES = %w[purple blue green orange].freeze
KEYS = %w[mode contrast size palette seed css].freeze
MAX_CSS = 32.kilobytes
module_function
# Returns a list of problems; empty when the hash is valid.
def errors(hash)
prefs = normalize(hash)
errors = hash.to_h.filter_map do |key, value|
if !KEYS.include?(key.to_s) then "unknown key #{key}"
elsif !value.nil? && !value.is_a?(String) then "invalid #{key}"
end
end
errors << 'invalid mode' if prefs['mode'] && !MODES.include?(prefs['mode'])
errors << 'invalid contrast' if prefs['contrast'] && !CONTRASTS.include?(prefs['contrast'])
errors << 'invalid size' if prefs['size'] && !SIZES.include?(prefs['size'])
errors.concat(custom_errors(prefs))
errors
end
# Returns only the valid parts, with string keys.
def clean(hash)
prefs = normalize(hash)
prefs.delete('mode') unless MODES.include?(prefs['mode'])
prefs.delete('contrast') unless CONTRASTS.include?(prefs['contrast'])
prefs.delete('size') unless SIZES.include?(prefs['size'])
prefs = prefs.except('palette', 'seed', 'css') if custom_errors(prefs).any?
prefs.compact
end
# String keys, known keys only, blank values dropped (the settings form posts "" for unset parts).
def normalize(hash)
hash.to_h.transform_keys(&:to_s).slice(*KEYS).select { |_, value| value.is_a?(String) && value.present? }
end
# palette, seed and css belong together: a custom palette needs its seed and generated CSS.
# A built-in palette may keep the last custom color, so the custom swatch can switch back.
def custom_errors(prefs)
palette, seed, css = prefs.values_at('palette', 'seed', 'css')
return [] if palette.nil? && seed.nil? && css.nil?
required = palette == 'custom'
errors = []
errors << 'invalid palette' unless required || PALETTES.include?(palette)
errors << 'invalid seed' unless (seed.nil? && !required) || seed.to_s.match?(/\A#\h{6}\z/)
# The CSS is rendered into a <script> as JSON and applied via style.textContent; refusing
# "<" keeps it from ever closing either element.
errors << 'invalid css' unless (css.nil? && !required) || (css && !css.include?('<') && css.bytesize <= MAX_CSS)
errors
end
end
end
@@ -0,0 +1,19 @@
module RedmineTonalmine
# Hands the default and the current user's appearance to theme.js. Rendered in <head> right
# after the theme's scripts, so the colors are applied before the page is painted.
class Hooks < Redmine::Hook::ViewListener
def view_layouts_base_html_head(context = {})
defaults = Appearance.clean(Setting.plugin_redmine_tonalmine)
config = {defaults: defaults, user: nil, url: nil}
if User.current.logged?
user = Appearance.clean(User.current.pref[:tonalmine])
# The user's own palette replaces the default one: skip the default's generated CSS.
# Admins keep it: the Display settings tab posts the whole default back.
defaults.delete('css') if user['palette'] && !User.current.admin?
config.update(user: user, url: tonalmine_appearance_path)
end
# to_json escapes <, > and &, so values can't end the script element.
javascript_tag "window.Tonalmine && Tonalmine.configure(#{config.to_json});"
end
end
end
File diff suppressed because one or more lines are too long
+249 -25
View File
@@ -1,84 +1,308 @@
/*
* tonalmine: per-user appearance (mode + color palette).
* tonalmine: per-user appearance (mode, contrast, size, color palette).
*
* Redmine loads this file in <head>, so the saved choice is applied before the page renders
* (no flash of the default colors). The picker is added to the profile menu and the mobile
* flyout. The choice is stored per browser in localStorage.
* flyout. The choice is stored per browser in localStorage. With the redmine_tonalmine plugin,
* the plugin calls Tonalmine.configure() right after this file: it adds the administrator's
* default, and logged-in users' choices are saved on their account instead.
*/
(function () {
// Ids must match PALETTES in tools/palettes/build_palettes.mjs (the build checks this).
// Ids must match PALETTES in tools/palettes/build_palettes.mjs and
// redmine_tonalmine/lib/redmine_tonalmine/appearance.rb (the palettes build checks both).
var PALETTES = [
['purple', 'Purple', '#6750A4'],
['blue', 'Blue', '#0061A4'],
['teal', 'Teal', '#006A6A'],
['green', 'Green', '#386A20'],
['orange', 'Orange', '#8B5000']
];
var MODES = [['auto', 'Auto'], ['light', 'Light'], ['dark', 'Dark']];
var CONTRASTS = [['standard', 'Standard'], ['medium', 'Medium'], ['high', 'High']];
var SIZES = [['compact', 'Compact'], ['normal', 'Normal'], ['large', 'Large']];
var KEY = 'tonalmine';
var CUSTOM_SEL = ':root[data-palette="custom"]';
var DEFAULT_KEYS = ['mode', 'contrast', 'size', 'palette', 'seed', 'css'];
var root = document.documentElement;
function load() {
try { return JSON.parse(localStorage.getItem(KEY)) || {}; } catch (e) { return {}; }
}
function save(prefs) {
try { localStorage.setItem(KEY, JSON.stringify(prefs)); } catch (e) { /* private mode: applies to this page only */ }
}
function apply(prefs) {
if (prefs.palette && prefs.palette !== PALETTES[0][0]) root.setAttribute('data-palette', prefs.palette);
else root.removeAttribute('data-palette');
if (prefs.mode === 'light' || prefs.mode === 'dark') root.setAttribute('data-mode', prefs.mode);
else root.removeAttribute('data-mode');
if (prefs.contrast === 'medium' || prefs.contrast === 'high') root.setAttribute('data-contrast', prefs.contrast);
else root.removeAttribute('data-contrast');
if (prefs.size === 'compact' || prefs.size === 'large') root.setAttribute('data-size', prefs.size);
else root.removeAttribute('data-size');
// The custom palette's rules are generated once (see withScheme) and kept with the prefs.
if (prefs.palette === 'custom' && prefs.css) {
var style = document.getElementById('nm-custom-palette');
if (!style) {
style = document.createElement('style');
style.id = 'nm-custom-palette';
document.head.appendChild(style);
}
style.textContent = prefs.css;
}
}
var prefs = load();
apply(prefs);
// The scheme generator (scheme.js, ~90 KB) is only loaded when someone picks a custom color.
// Redmine serves theme assets under digested names only, so its URL comes from the CSS
// variable --nm-scheme-js (Redmine rewrites url() in CSS to the digested path).
var schemeScript;
function withScheme(done) {
if (window.tonalmineScheme) return done(window.tonalmineScheme);
if (!schemeScript) {
var url = getComputedStyle(root).getPropertyValue('--nm-scheme-js').match(/url\(["']?([^"')]+)/)[1];
schemeScript = document.createElement('script');
schemeScript.src = url;
document.head.appendChild(schemeScript);
}
schemeScript.addEventListener('load', function () { done(window.tonalmineScheme); });
}
// own: the parts this user changed. defaults: the administrator's default (plugin only).
// The page shows the defaults overridden by own, part by part.
var own = load();
var defaults = {};
var url = null; // set when the user's choice is saved on their account (plugin, logged in)
var configured = false;
var draft = null; // the default being edited on the Display settings tab
// palette, seed and css form one part: a custom palette brings its own generated CSS.
function merge(base, top) {
var src = top.palette ? top : base;
return { mode: top.mode || base.mode, contrast: top.contrast || base.contrast, size: top.size || base.size,
palette: src.palette, seed: src.seed, css: src.css };
}
function effective() { return merge(defaults, own); }
function isEmpty(prefs) { return !Object.keys(prefs).length; }
function save() {
if (!url) {
try { localStorage.setItem(KEY, JSON.stringify(own)); } catch (e) { /* private mode: applies to this page only */ }
return;
}
var token = document.querySelector('meta[name="csrf-token"]');
fetch(url, {
method: 'PUT',
credentials: 'same-origin',
keepalive: true, // finish even if the user navigates away right after picking
// X-Requested-With: an expired session answers 401 instead of redirecting to the login page.
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-Token': token ? token.content : '' },
body: JSON.stringify(own)
}).then(function (response) {
if (response.status !== 204) throw new Error('HTTP ' + response.status);
}).catch(function (error) { console.warn('tonalmine: appearance not saved', error); });
}
// Called by the redmine_tonalmine plugin from <head>, right after this file.
window.Tonalmine = {
configure: function (config) {
defaults = config.defaults || {};
if (config.user) {
url = config.url;
var local = own;
own = config.user;
// A choice made in this browser before the plugin was installed moves to the account once.
if (isEmpty(own) && !isEmpty(local)) {
own = local;
save();
}
// Logged-in choices live on the account only, so the next person on this browser doesn't inherit them.
try { localStorage.removeItem(KEY); } catch (e) { /* nothing stored */ }
}
configured = true;
apply(effective());
}
};
apply(effective());
function button(attr, value, label, pressed, extra) {
return '<button type="button" ' + attr + '="' + value + '" aria-pressed="' + pressed + '"' + (extra || '') + '>' + label + '</button>';
}
function pickerHtml() {
// isDefault: the administrator's picker on the Display settings tab (no title, no reset).
function pickerHtml(prefs, isDefault) {
var mode = prefs.mode || 'auto';
var contrast = prefs.contrast || 'standard';
var size = prefs.size || 'normal';
var palette = prefs.palette || PALETTES[0][0];
return '<div class="nm-appearance">' +
'<div class="nm-appearance__title">Appearance</div>' +
return '<div class="nm-appearance' + (isDefault ? ' nm-appearance--default" data-nm-default' : '"') + '>' +
(isDefault ? '' : '<div class="nm-appearance__title">Appearance</div>') +
'<div class="nm-segmented" role="group" aria-label="Mode">' +
MODES.map(function (m) { return button('data-nm-mode', m[0], m[1], m[0] === mode); }).join('') +
'</div>' +
'<div class="nm-appearance__title">Contrast</div>' +
'<div class="nm-segmented" role="group" aria-label="Contrast">' +
CONTRASTS.map(function (c) { return button('data-nm-contrast', c[0], c[1], c[0] === contrast); }).join('') +
'</div>' +
'<div class="nm-appearance__title">Size</div>' +
'<div class="nm-segmented" role="group" aria-label="Size">' +
SIZES.map(function (s) { return button('data-nm-size', s[0], s[1], s[0] === size); }).join('') +
'</div>' +
'<div class="nm-swatches" role="group" aria-label="Color">' +
PALETTES.map(function (p) {
return button('data-nm-palette', p[0], '', p[0] === palette,
' class="nm-swatch" title="' + p[1] + '" aria-label="' + p[1] + '" style="--nm-swatch: ' + p[2] + '"');
}).join('') +
'</div></div>';
'<label class="nm-swatch nm-swatch--custom no-css" data-nm-palette="custom" aria-pressed="' + (palette === 'custom') + '"' +
(prefs.seed ? ' style="--nm-swatch: ' + prefs.seed + '"' : '') + ' title="Custom color">' +
'<input type="color" data-nm-custom value="' + (prefs.seed || PALETTES[0][2].toLowerCase()) + '" aria-label="Custom color"></label>' +
'</div>' +
(isDefault ? '' : '<button type="button" class="nm-appearance__reset" data-nm-reset' + (isEmpty(own) ? ' hidden' : '') + '>Reset to default</button>') +
'</div>';
}
function sync() {
document.querySelectorAll('[data-nm-mode]').forEach(function (b) {
document.querySelectorAll('.nm-appearance').forEach(function (picker) {
var prefs = picker.hasAttribute('data-nm-default') ? draft : effective();
picker.querySelectorAll('[data-nm-mode]').forEach(function (b) {
b.setAttribute('aria-pressed', String(b.getAttribute('data-nm-mode') === (prefs.mode || 'auto')));
});
document.querySelectorAll('[data-nm-palette]').forEach(function (b) {
picker.querySelectorAll('[data-nm-contrast]').forEach(function (b) {
b.setAttribute('aria-pressed', String(b.getAttribute('data-nm-contrast') === (prefs.contrast || 'standard')));
});
picker.querySelectorAll('[data-nm-size]').forEach(function (b) {
b.setAttribute('aria-pressed', String(b.getAttribute('data-nm-size') === (prefs.size || 'normal')));
});
picker.querySelectorAll('[data-nm-palette]').forEach(function (b) {
b.setAttribute('aria-pressed', String(b.getAttribute('data-nm-palette') === (prefs.palette || PALETTES[0][0])));
});
if (prefs.seed) {
picker.querySelectorAll('[data-nm-custom]').forEach(function (input) {
input.value = prefs.seed;
input.parentNode.style.setProperty('--nm-swatch', prefs.seed);
});
}
var reset = picker.querySelector('[data-nm-reset]');
if (reset) reset.hidden = isEmpty(own);
});
document.querySelectorAll('[data-nm-default-input]').forEach(function (input) {
input.value = draft[input.getAttribute('data-nm-default-input')] || '';
});
}
document.addEventListener('click', function (event) {
var target = event.target.closest('[data-nm-mode], [data-nm-palette]');
if (!target) return;
if (target.hasAttribute('data-nm-mode')) prefs.mode = target.getAttribute('data-nm-mode');
else prefs.palette = target.getAttribute('data-nm-palette');
save(prefs);
apply(prefs);
// persist: save the user's choice (the default is saved with the settings form instead).
function changed(isDefault, persist) {
if (isDefault) apply(draft); // preview
else {
if (persist) save();
apply(effective());
}
sync();
}
document.addEventListener('click', function (event) {
var target = event.target.closest('[data-nm-mode], [data-nm-contrast], [data-nm-size], [data-nm-palette], [data-nm-reset]');
if (!target) return;
var isDefault = !!target.closest('[data-nm-default]');
var prefs = isDefault ? draft : own;
if (target.hasAttribute('data-nm-reset')) own = {};
else if (target.hasAttribute('data-nm-mode')) prefs.mode = target.getAttribute('data-nm-mode');
else if (target.hasAttribute('data-nm-contrast')) prefs.contrast = target.getAttribute('data-nm-contrast');
else if (target.hasAttribute('data-nm-size')) prefs.size = target.getAttribute('data-nm-size');
// The custom swatch opens the color dialog; it only selects the palette once a color was picked.
else if (target.getAttribute('data-nm-palette') !== 'custom' || prefs.css) prefs.palette = target.getAttribute('data-nm-palette');
changed(isDefault, true);
});
document.addEventListener('input', function (event) {
if (!event.target.matches('[data-nm-custom]')) return;
var isDefault = !!event.target.closest('[data-nm-default]');
var seed = event.target.value;
withScheme(function (scheme) {
var prefs = isDefault ? draft : own;
prefs.palette = 'custom';
prefs.seed = seed;
prefs.css = scheme.paletteCss(CUSTOM_SEL, seed);
// While the user drags, only localStorage keeps up; the account gets the final color on change.
changed(isDefault, !url);
});
});
document.addEventListener('change', function (event) {
if (!url || !event.target.matches('[data-nm-custom]') || event.target.closest('[data-nm-default]')) return;
withScheme(save); // queued behind the input event's pending update
});
// Display settings tab (admins, plugin installed): edit the default appearance below the theme
// select. The hidden inputs are saved by the settings form as Setting.plugin_redmine_tonalmine.
function defaultPicker(select) {
draft = merge({}, defaults);
// One labelled form row per group (mode, contrast, size, color), each in its own default picker
// wrapper so sync() and the click handlers treat it like the whole picker.
var picker = document.createElement('div');
picker.innerHTML = pickerHtml(draft, true);
var groups = picker.querySelectorAll('[role="group"]');
var rows = ['Default appearance', 'Contrast', 'Size', 'Color'].map(function (label, i) {
var row = document.createElement('p');
row.innerHTML = '<label>' + label + '</label><div class="nm-appearance nm-appearance--default" data-nm-default></div>';
row.lastChild.appendChild(groups[i]);
return row;
});
rows[3].insertAdjacentHTML('beforeend', DEFAULT_KEYS.map(function (key) {
return '<input type="hidden" name="settings[plugin_redmine_tonalmine][' + key + ']" data-nm-default-input="' + key + '">';
}).join(''));
select.closest('p').after(rows[0], rows[1], rows[2], rows[3]);
// Only shown, and only posted, while Tonalmine is selected; otherwise the saved default stays.
function toggle() {
var on = select.value === 'tonalmine';
rows.forEach(function (row) { row.style.display = on ? '' : 'none'; });
rows[3].querySelectorAll('[data-nm-default-input]').forEach(function (input) { input.disabled = !on; });
}
select.addEventListener('change', toggle);
toggle();
sync();
}
// Calendar/Gantt hover cards: Redmine renders "<a>Tracker #id</a>: subject<br><br>" followed by
// "<strong>Label</strong>: value<br>" lines. Regroup them on first hover into a title and a
// label/value grid (nodes are moved, not re-parsed, so links and avatars stay intact).
function structureTip(tip) {
var lines = [[]];
Array.prototype.slice.call(tip.childNodes).forEach(function (node) {
if (node.nodeName === 'BR') lines.push([]);
else if (node.nodeType !== 3 || node.nodeValue.trim()) lines[lines.length - 1].push(node);
});
var title = document.createElement('span');
var fields = document.createElement('span');
title.className = 'nm-tip__title';
fields.className = 'nm-tip__fields';
lines.filter(function (line) { return line.length; }).forEach(function (line, i) {
var head = line[0];
var value = document.createElement('span');
line.slice(1).forEach(function (node) { value.appendChild(node); });
if (value.firstChild && value.firstChild.nodeType === 3) {
value.firstChild.nodeValue = value.firstChild.nodeValue.replace(/^\s*:\s*/, '');
}
var target = i > 0 && head.nodeName === 'STRONG' ? fields : title;
target.appendChild(head);
target.appendChild(value);
});
tip.replaceChildren(title, fields);
tip.classList.add('nm-tip');
}
document.addEventListener('mouseover', function (event) {
var tip = event.target.closest && event.target.closest('div.tooltip');
tip = tip && tip.querySelector(':scope > span.tip:not(.nm-tip)');
if (tip) structureTip(tip);
});
document.addEventListener('DOMContentLoaded', function () {
var menu = document.querySelector('#account .dropdown-content .dropdown-divider');
if (menu) menu.insertAdjacentHTML('afterend', pickerHtml() + '<div class="dropdown-divider"></div>');
if (menu) menu.insertAdjacentHTML('afterend', pickerHtml(effective()) + '<div class="dropdown-divider"></div>');
var flyout = document.querySelector('.js-flyout-menu');
if (flyout) flyout.insertAdjacentHTML('beforeend', pickerHtml());
if (flyout) flyout.insertAdjacentHTML('beforeend', pickerHtml(effective()));
var select = document.getElementById('settings_ui_theme');
if (select && configured) defaultPicker(select);
});
})();
+136 -31
View File
@@ -6,7 +6,7 @@
/***** Buttons *****/
input[type="submit"], input[type="button"], input[type="reset"], button[type="submit"] {
block-size: 40px;
block-size: calc(40px + var(--nm-density));
padding-block: 0;
padding-inline: 24px;
margin-block: 4px;
@@ -51,8 +51,8 @@ input[type="submit"]:disabled, input[type="button"]:disabled, button:disabled {
/***** Text fields (outlined) *****/
input[type="text"], input[type="password"], input[type="email"], input[type="url"], input[type="number"],
input[type="date"], input[type="search"], input[type="tel"], input:not([type]), select, textarea {
min-block-size: 40px;
padding-block: 8px;
min-block-size: calc(40px + var(--nm-density));
padding-block: calc(8px + min(var(--nm-density), 0px) / 2);
padding-inline: 12px;
border: 1px solid var(--md-sys-color-outline);
border-radius: var(--md-sys-shape-corner-extra-small);
@@ -100,11 +100,11 @@ input.autocomplete {
:where(:root[data-mode="dark"]) input.autocomplete { background-image: url(../images/search-dark.svg); }
@media (prefers-color-scheme: dark) { :where(:root:not([data-mode="light"])) input.autocomplete { background-image: url(../images/search-dark.svg); } }
select[multiple] { background-image: none; padding-inline-end: 12px; }
input::placeholder, textarea::placeholder { color: var(--md-sys-color-on-surface-variant); opacity: 1; }
input::placeholder, textarea::placeholder { color: var(--md-sys-color-outline); opacity: 1; } /* lighter than typed text, so an empty field does not look filled */
input[type="checkbox"], input[type="radio"] { accent-color: var(--md-sys-color-primary); }
input[type="file"] { color: var(--md-sys-color-on-surface-variant); }
input[type="file"]::file-selector-button {
block-size: 40px;
block-size: calc(40px + var(--nm-density));
margin-inline-end: 12px;
padding-inline: 24px;
border: 0;
@@ -165,10 +165,10 @@ fieldset.collapsible > legend { display: flex; align-items: center; gap: 4px; }
.tabular label { color: var(--md-sys-color-on-surface-variant); font-weight: 500; }
.tabular p { padding-block: 4px; }
/* Center the 24px label line on the 40px fields next to it */
.tabular label:not(.floating):not(.block):not(.inline):not(.no-css) { padding-block-start: 8px; }
.tabular label:not(.floating):not(.block):not(.inline):not(.no-css) { padding-block-start: calc(8px + var(--nm-density) / 2); }
/* Checkboxes and radios placed directly in a row align with that label line too */
/* Checkboxes and radios next to such a label align with its text line */
.tabular p:has(> label:not(.floating):not(.block):not(.inline):not(.no-css)) > input:is([type="checkbox"], [type="radio"]) { margin-block: 13px 0; }
.tabular p:has(> label:not(.floating):not(.block):not(.inline):not(.no-css)) > input:is([type="checkbox"], [type="radio"]) { margin-block: calc(13px + var(--nm-density) / 2) 0; }
/* Rows without a field at the label's height start at the top: checkbox lists (watchers)
and the issue description, whose editor stays hidden until "Edit" is clicked */
.tabular p:is(:has(label.floating), :has(> #issue_description_and_toolbar[style*="none"])) > label:not(.floating):not(.block):not(.inline):not(.no-css) { padding-block-start: 0; }
@@ -178,18 +178,37 @@ blockquote { border-inline-start: 4px solid var(--md-sys-color-outline-variant);
/* Core gives wide form fields 90%, 95% or 99% and caps .tabular fields at 95%. They all
run to the content edge, so the right edges in a form line up */
.tabular :is(input, select) { max-inline-size: 100%; }
/* Core pins these to 90% by id */
#user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select { inline-size: 100%; }
.tabular textarea, textarea.wiki-edit, textarea.text_cf, input.string_cf, input.link_cf,
textarea#custom_field_possible_values, textarea#custom_field_default_value,
input#issue_subject, input#document_title, input#content_comments, input#time_entry_comments,
.tabular .jstTabs, .tabular .wiki-preview, .jstBlock .jstTabs, .jstBlock .wiki-preview { inline-size: 100%; box-sizing: border-box; }
/* Desktop and tablet landscape: three field widths, so the right edges in a form line up.
Standard: selects and text fields fill the column up to 480px (wider than core's widest,
size=60). Compact: numbers and dates, 140px fits a date with its picker. Full: long text.
Below 900px core's responsive.css runs every field full width. */
@media screen and (min-width: 900px) {
.tabular :is(select, input:is([type="text"], [type="password"], [type="email"], [type="url"], [type="search"], [type="tel"], :not([type]))) {
inline-size: 100%;
max-inline-size: 480px;
box-sizing: border-box;
}
.tabular :is(input[type="number"], input[type="date"], input.date, input:is([size="1"], [size="2"], [size="3"], [size="4"], [size="5"], [size="6"])) { inline-size: 140px; }
/* The icon button after a select (new category, new version) stays on its line. The select
keeps the 480px cap, so the button sits after the right edge like "Assign to me" */
.tabular select:has(+ .icon-only) { inline-size: calc(100% - 36px); }
}
html .tabular textarea, html textarea.wiki-edit, html textarea.text_cf, html input.string_cf, html input.link_cf,
html textarea#custom_field_possible_values, html textarea#custom_field_default_value,
html input#issue_subject, html input#document_title, html input#content_comments, html input#time_entry_comments,
html .tabular .jstTabs, html .tabular .wiki-preview, html .jstBlock .jstTabs, html .jstBlock .wiki-preview,
html .tabular.settings :is(textarea, .jstTabs, .wiki-preview) { inline-size: 100%; max-inline-size: none; box-sizing: border-box; }
/* Core pins % Done to 95px by id; it follows the standard width like the other selects */
html select#issue_done_ratio { inline-size: 100%; }
/* An invalid field keeps the error colour while focused */
.tabular label.error + :is(input, select, textarea):focus {
border-color: var(--md-sys-color-error);
outline-color: var(--md-sys-color-error);
}
/* Rows without a field start at the top, like a row with only links (two-factor auth) */
.tabular p:not(:has(input:not([type=hidden]), select, textarea)) > label:not(.floating):not(.block):not(.inline):not(.no-css) { padding-block-start: 0; }
.tabular p:not(:has(input:not([type=hidden]), select, textarea, .nm-segmented)) > label:not(.floating):not(.block):not(.inline):not(.no-css) { padding-block-start: 0; }
/* Cancel next to a submit button */
input[type="submit"] + a { margin-inline-start: 8px; }
@@ -233,7 +252,7 @@ div.flash.error svg.icon-svg, #errorExplanation svg.icon-svg,
div.flash.notice svg.icon-svg, div.flash.warning svg.icon-svg, .conflict svg.icon-svg { stroke: currentColor; }
/***** Secondary tabs (settings, history, admin) *****/
#content .tabs { block-size: 48px; margin-block-end: 16px; }
#content .tabs { block-size: calc(48px + var(--nm-density)); margin-block-end: 16px; }
#content .tabs ul { padding-inline-start: 0; border-block-end: 1px solid var(--md-sys-color-outline-variant); }
#content .tabs ul li { margin-inline-end: 0; margin-block-end: 0; }
#content .tabs ul li a, #content .tabs ul li a:hover {
@@ -262,7 +281,7 @@ div.flash.notice svg.icon-svg, div.flash.warning svg.icon-svg, .conflict svg.ico
justify-content: end;
gap: 4px;
inline-size: auto;
block-size: 48px;
block-size: calc(48px + var(--nm-density));
padding-inline-start: 8px;
box-sizing: border-box;
background: var(--md-sys-color-surface);
@@ -274,8 +293,8 @@ div.flash.notice svg.icon-svg, div.flash.warning svg.icon-svg, .conflict svg.ico
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: 40px;
block-size: 40px;
inline-size: calc(40px + var(--nm-density));
block-size: calc(40px + var(--nm-density));
padding: 0;
border: 0;
border-radius: var(--md-sys-shape-corner-full);
@@ -317,8 +336,8 @@ span.pagination { margin-inline-start: 0; color: var(--md-sys-color-on-surface-v
margin-inline-end: 4px;
border: 0;
border-radius: var(--md-sys-shape-corner-full);
line-height: 40px;
min-inline-size: 40px;
line-height: calc(40px + var(--nm-density));
min-inline-size: calc(40px + var(--nm-density));
}
.pagination ul.pages li a, .pagination ul.pages li span {
display: block;
@@ -341,20 +360,75 @@ table.progress { border-radius: var(--md-sys-shape-corner-full); overflow: hidde
table.progress td { block-size: 4px; padding: 0; }
.version-overview table.progress td { block-size: 8px; }
table.progress td.closed { background: var(--md-sys-color-primary); }
table.progress td.done { background: color-mix(in srgb, var(--md-sys-color-primary) 55%, var(--md-sys-color-secondary-container)); }
table.progress td.todo { background: var(--md-sys-color-secondary-container); }
table.progress td.done { background: color-mix(in srgb, var(--md-sys-color-primary) 55%, var(--md-sys-color-surface-container-highest)); }
/* Not secondary-container: at medium/high contrast it is dark and the empty track looks filled */
table.progress td.todo { background: var(--md-sys-color-surface-container-highest); }
/***** Tooltips (plain) *****/
.ui-tooltip {
/* jQuery UI also adds .ui-widget.ui-widget-content, which would win over a bare .ui-tooltip. */
.ui-tooltip.ui-widget.ui-widget-content {
background: var(--md-sys-color-inverse-surface);
color: var(--md-sys-color-inverse-on-surface);
border: 0;
border-radius: var(--md-sys-shape-corner-extra-small);
box-shadow: none;
font: var(--md-sys-typescale-body-small);
letter-spacing: var(--md-sys-typescale-body-small-tracking);
padding-block: 4px;
padding-inline: 8px;
}
/***** Tooltips (rich): calendar and Gantt issue hover cards *****/
div.tooltip:hover span.tip {
inline-size: 300px;
background-color: var(--md-sys-color-surface-container);
color: var(--md-sys-color-on-surface-variant);
border: 0;
border-radius: var(--md-sys-shape-corner-medium);
box-shadow: var(--md-sys-elevation-level2);
padding-block: 12px;
padding-inline: 16px;
font: var(--md-sys-typescale-body-medium);
letter-spacing: var(--md-sys-typescale-body-medium-tracking);
}
div.tooltip span.tip strong { color: var(--md-sys-color-on-surface); font-weight: 500; }
div.tooltip span.tip .avatar { vertical-align: middle; }
/* Structured by theme.js (structureTip): title block, then a label/value grid. */
div.tooltip span.nm-tip .nm-tip__title {
display: flex;
flex-direction: column;
gap: 2px;
margin-block-end: 12px;
}
div.tooltip span.nm-tip .nm-tip__title > a {
font: var(--md-sys-typescale-label-medium);
letter-spacing: var(--md-sys-typescale-label-medium-tracking);
text-decoration: none;
}
div.tooltip span.nm-tip .nm-tip__title > span {
color: var(--md-sys-color-on-surface);
font: var(--md-sys-typescale-title-small);
letter-spacing: var(--md-sys-typescale-title-small-tracking);
}
div.tooltip span.nm-tip .nm-tip__fields {
display: grid;
grid-template-columns: auto 1fr;
gap: 4px 16px;
align-items: center;
}
div.tooltip span.nm-tip .nm-tip__fields > strong {
color: var(--md-sys-color-on-surface-variant);
font: var(--md-sys-typescale-label-medium);
letter-spacing: var(--md-sys-typescale-label-medium-tracking);
}
div.tooltip span.nm-tip .nm-tip__fields > span {
color: var(--md-sys-color-on-surface);
display: flex;
align-items: center;
gap: 6px;
min-inline-size: 0;
}
/***** jQuery UI: dialogs, date picker, menus *****/
.ui-widget, .ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: var(--md-ref-typeface-plain); }
.ui-widget.ui-widget-content, .ui-widget-content {
@@ -378,8 +452,8 @@ table.progress td.todo { background: var(--md-sys-color-secondary-container); }
.ui-dialog .ui-dialog-title { font: var(--md-sys-typescale-headline-small); }
.ui-dialog .ui-dialog-content { padding-inline: 16px; }
.ui-dialog .ui-dialog-titlebar-close {
inline-size: 40px;
block-size: 40px;
inline-size: calc(40px + var(--nm-density));
block-size: calc(40px + var(--nm-density));
margin-block-start: -20px;
border: 0;
border-radius: var(--md-sys-shape-corner-full);
@@ -438,7 +512,7 @@ a.icon:hover .icon-svg, a.icon-only:hover .icon-svg, span.icon-actions:hover .ic
.icon-warning svg.icon-svg { color: var(--md-sys-color-warning); }
div.flash svg.icon-svg, #errorExplanation svg.icon-svg, .conflict svg.icon-svg,
.top-menu__links svg.icon-svg, #account .dropdown-trigger .icon-svg,
#main-menu svg.icon-svg, #sidebar a.selected svg.icon-svg, .drdn-items svg.icon-svg { color: inherit; }
#main-menu svg.icon-svg, #sidebar :is(a.selected, .icon-clear-query) svg.icon-svg, .drdn-items svg.icon-svg { color: inherit; }
html .context-menu-selection svg.icon-svg { color: var(--md-sys-color-on-secondary-container) !important; }
/***** Wiki toolbar (jstoolbar.css loads after the theme) *****/
@@ -447,8 +521,8 @@ html .context-menu-selection svg.icon-svg { color: var(--md-sys-color-on-seconda
html .jstElements { block-size: auto; padding-block-end: 4px; }
html .jstElements button {
position: relative;
inline-size: 32px;
block-size: 32px;
inline-size: calc(32px + var(--nm-density));
block-size: calc(32px + var(--nm-density));
margin-inline-end: 0;
padding: 0;
border: 0;
@@ -501,12 +575,12 @@ html .table-generator td.selected-cell, html .table-generator td:hover { backgro
letter-spacing: var(--md-sys-typescale-label-medium-tracking);
}
/* M3 segmented button */
.nm-segmented { display: flex; margin-block-end: 12px; }
.nm-segmented { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; margin-block-end: 12px; } /* equal segments */
.nm-segmented button {
flex: 1;
border-radius: 0;
block-size: 32px;
padding-inline: 12px;
block-size: calc(32px + var(--nm-density));
padding-block: 0; /* the box centers the line; core's 4px made it overflow (and sink) in Compact */
padding-inline: 8px;
border: 1px solid var(--md-sys-color-outline);
background: transparent;
color: var(--md-sys-color-on-surface);
@@ -519,8 +593,9 @@ html .table-generator td.selected-cell, html .table-generator td:hover { backgro
.nm-segmented button:hover { background-image: var(--nm-state-hover); }
.nm-segmented button[aria-pressed="true"] { background-color: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container); }
/* Color swatches: the selected one gets a ring */
.nm-swatches { display: flex; gap: 8px; }
.nm-swatches { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px; }
.nm-swatch {
flex-shrink: 0;
inline-size: 28px;
block-size: 28px;
padding: 0;
@@ -529,7 +604,37 @@ html .table-generator td.selected-cell, html .table-generator td:hover { backgro
background: var(--nm-swatch);
cursor: pointer;
}
/* Custom color: a native color input stretched invisibly over a rainbow (or the picked color) swatch */
/* label.: beats core label.no-css { inline-size: auto } (the class keeps .tabular label styles off) */
label.nm-swatch--custom { position: relative; inline-size: 28px; background: var(--nm-swatch, conic-gradient(#e53935, #fdd835, #43a047, #1e88e5, #8e24aa, #e53935)); }
.nm-swatch--custom input { position: absolute; inset: 0; inline-size: 100%; block-size: 100%; padding: 0; border: 0; opacity: 0; cursor: pointer; }
.nm-swatch--custom:focus-within { outline: 2px solid var(--md-sys-color-primary); outline-offset: 2px; }
/* theme.js lazy-loads the scheme generator from here; Redmine rewrites url() to the digested path */
:root { --nm-scheme-js: url(./scheme.js); }
.nm-swatch[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--md-sys-color-surface-container), 0 0 0 4px var(--md-sys-color-on-surface); }
/* Reset to default: M3 text button, shown once the user changed something */
.nm-appearance__reset {
margin-block-start: 12px;
block-size: calc(32px + var(--nm-density));
padding-block: 0;
padding-inline: 12px;
border: 0;
border-radius: var(--md-sys-shape-corner-full);
background: transparent;
color: var(--md-sys-color-primary);
font: var(--md-sys-typescale-label-large);
cursor: pointer;
}
.nm-appearance__reset:hover { background-image: var(--nm-state-hover); }
/* Default appearance on Administration > Settings > Display (redmine_tonalmine plugin) */
.nm-appearance--default { max-inline-size: 280px; padding: 0; }
/* Each group is its own form row: 40px tall like the fields (M3 segmented button height), so
the label lines up; the row padding spaces them */
.nm-appearance--default .nm-segmented { margin: 0; }
.nm-appearance--default .nm-segmented button { block-size: calc(40px + var(--nm-density)); margin-block: 0; }
.nm-appearance--default .nm-swatches { min-block-size: calc(40px + var(--nm-density)); align-items: center; }
/* The custom swatch is a <label class="no-css">; also undo .tabular.settings label margins and mobile full width */
html .tabular label.nm-swatch--custom { display: block; margin: 0; inline-size: 28px; }
html .flyout-menu .nm-appearance { padding-inline: 16px; }
html .flyout-menu .nm-appearance__title { color: var(--md-sys-color-on-surface-variant); }
+38 -21
View File
@@ -56,7 +56,7 @@ body.has-main-menu #header { padding-block-end: 48px; }
#header h1 {
font: var(--md-sys-typescale-title-large);
min-block-size: 3rem;
min-block-size: round(3rem, 1px); /* whole px, so the header controls do not sit between pixels */
}
#header h1 .breadcrumbs {
font: var(--md-sys-typescale-label-medium);
@@ -65,10 +65,10 @@ body.has-main-menu #header { padding-block-end: 48px; }
}
#header h1 .breadcrumbs a { color: var(--md-sys-color-on-surface-variant); }
#quick-search { min-block-size: 3rem; color: var(--md-sys-color-on-surface-variant); }
#quick-search { min-block-size: round(3rem, 1px); color: var(--md-sys-color-on-surface-variant); }
#quick-search #q {
inline-size: 220px;
block-size: 40px;
block-size: calc(40px + var(--nm-density));
padding-inline: 16px;
border: 0;
border-radius: var(--md-sys-shape-corner-full);
@@ -81,14 +81,15 @@ body.has-main-menu #header { padding-block-end: 48px; }
#project-jump.drdn { margin-inline-start: 8px; }
#project-jump .drdn-trigger {
block-size: 40px;
block-size: calc(40px + var(--nm-density));
padding-block: 0; /* core's 1.5px / 3px pushed the text down */
padding-inline: 16px 36px;
border: 1px solid var(--md-sys-color-outline-variant);
border-radius: var(--md-sys-shape-corner-full);
background-color: transparent;
color: var(--md-sys-color-on-surface);
font: var(--md-sys-typescale-label-large);
line-height: 38px;
line-height: calc(38px + var(--nm-density));
}
#project-jump .drdn-trigger:hover { background-image: var(--nm-state-hover); }
@@ -99,7 +100,7 @@ body.has-main-menu #header { padding-block-end: 48px; }
padding-block: 0;
padding-inline: 16px;
}
#main-menu ul { min-block-size: 48px; }
#main-menu ul { min-block-size: calc(48px + var(--nm-density)); }
#main-menu li a {
position: relative;
color: var(--md-sys-color-on-surface-variant);
@@ -136,8 +137,8 @@ body.has-main-menu #header { padding-block-end: 48px; }
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: 40px;
block-size: 40px;
inline-size: calc(40px + var(--nm-density));
block-size: calc(40px + var(--nm-density));
padding: 0;
margin-block: 0;
border: 0;
@@ -158,7 +159,7 @@ body.has-main-menu #header { padding-block-end: 48px; }
align-items: center;
gap: 4px;
inline-size: auto;
block-size: 48px;
block-size: calc(48px + var(--nm-density));
inset-inline-end: 0;
padding-inline: 8px;
border: 0;
@@ -202,7 +203,17 @@ html .dropdown-content .dropdown-items a:hover,
}
#main-menu .menu-children li a:hover::after { display: none; }
.drdn-items > .selected { background-color: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container); }
/* At medium/high contrast secondary-container turns into a dark block; the check mark already marks the item */
:root:is([data-contrast="medium"], [data-contrast="high"]) .drdn-items > .selected { background-color: var(--md-sys-color-surface-container-highest); color: var(--md-sys-color-on-surface); }
#project-jump .drdn-items > a:hover { background-color: transparent; color: var(--md-sys-color-on-surface) !important; }
/* Section headers (Bookmarks, Recently used, All Projects): small primary labels, a divider between sections */
#project-jump .drdn-items > strong {
padding-block: 12px 4px;
color: var(--md-sys-color-primary) !important; /* core: gray !important */
font: var(--md-sys-typescale-label-medium);
letter-spacing: var(--md-sys-typescale-label-medium-tracking);
}
#project-jump .drdn-items > strong:not(:first-child) { margin-block-start: 8px; border-block-start: 1px solid var(--md-sys-color-outline-variant); }
div + .drdn-items, .dropdown-divider, #account .dropdown-content .logout { border-color: var(--md-sys-color-outline-variant); }
#account .dropdown-content .user-info { color: var(--md-sys-color-on-surface-variant); font: var(--md-sys-typescale-body-medium); }
#account .dropdown-content .user-info .user-name { color: var(--md-sys-color-on-surface); font: var(--md-sys-typescale-title-small); }
@@ -280,7 +291,7 @@ div + .drdn-items, .dropdown-divider, #account .dropdown-content .logout { borde
display: flex;
align-items: center;
gap: 12px;
min-block-size: 32px;
min-block-size: calc(32px + var(--nm-density));
}
#sidebar :is(li, form > p) > label:has(input[type="checkbox"]) { flex: 1; }
#sidebar input[type="checkbox"] { inline-size: 18px; block-size: 18px; margin: 0; flex-shrink: 0; }
@@ -288,28 +299,34 @@ div + .drdn-items, .dropdown-divider, #account .dropdown-content .logout { borde
#sidebar ul + ul, #sidebar ul + p { margin-block-start: 12px; }
/* Navigation-drawer style items for plain link lists in the sidebar (queries, sort order,
versions, wiki index, …): a link that is alone in its list item */
#sidebar li > a:only-child, #admin-menu li a {
versions, wiki index, …): a link that is alone in its list item, or a saved query */
#sidebar li > a:is(:only-child, .query), #admin-menu li a {
display: flex;
align-items: center;
min-block-size: 36px;
min-block-size: calc(36px + var(--nm-density));
padding-block: 8px; /* long names wrap: keep the items apart */
padding-inline: 12px;
box-sizing: border-box;
border-radius: var(--md-sys-shape-corner-full);
color: var(--md-sys-color-on-surface-variant);
font: var(--md-sys-typescale-label-large);
letter-spacing: var(--md-sys-typescale-label-large-tracking);
font: var(--md-sys-typescale-body-medium); /* regular weight: only the section titles are bold */
letter-spacing: var(--md-sys-typescale-body-medium-tracking);
transition: var(--nm-transition);
}
#sidebar li > a:only-child:hover, #admin-menu li a:hover { background-image: var(--nm-state-hover); text-decoration: none; }
#sidebar li > a.selected:only-child, #admin-menu li a.selected {
#sidebar li > a:is(:only-child, .query):hover, #admin-menu li a:hover { background-image: var(--nm-state-hover); text-decoration: none; }
#sidebar li > a.selected:is(:only-child, .query), #admin-menu li a.selected {
margin-inline-start: 0; /* core: -6px, inset marker bar */
padding-block: 0;
padding-block: 8px;
box-shadow: none;
line-height: normal;
background-color: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
}
#admin-menu li { padding-block-end: 2px; }
#admin-menu li, #sidebar li:has(> a:is(:only-child, .query)) { padding-block-end: 2px; }
/* The selected query also holds its "clear" icon: it sits at the end, inside the pill */
#sidebar li:has(> .icon-clear-query) { display: grid; align-items: center; }
#sidebar li:has(> .icon-clear-query) > * { grid-area: 1 / 1; }
#sidebar li:has(> .icon-clear-query) > a.query { padding-inline-end: 40px; }
#sidebar li > .icon-clear-query { justify-self: end; margin-inline-end: 12px; color: var(--md-sys-color-on-secondary-container); }
/***** Footer *****/
#footer {
@@ -329,4 +346,4 @@ div + .drdn-items, .dropdown-divider, #account .dropdown-content .logout { borde
border-radius: var(--md-sys-shape-corner-extra-large);
}
#login-form label { font: var(--md-sys-typescale-label-large); color: var(--md-sys-color-on-surface-variant); }
#login-form input { block-size: 40px; }
#login-form input { block-size: calc(40px + var(--nm-density)); }
+123 -9
View File
@@ -12,7 +12,8 @@
border: 0;
color: var(--md-sys-color-on-surface);
font: var(--md-sys-typescale-title-large);
line-height: 50px;
line-height: calc(50px + var(--nm-density));
padding-block: 1.5px 3px; /* core's, which layout.css removes: keeps the title level with the ▾ icon */
}
html #project-jump.drdn { padding-inline-start: 8px; }
/* Header icons: Material Symbols masks instead of core's scaled "^" and "≡" text glyphs */
@@ -29,8 +30,8 @@
mask: url(../images/arrow-drop-down.svg) center / 24px no-repeat;
}
html #header a.mobile-toggle-button {
inline-size: 48px;
block-size: 48px;
inline-size: calc(48px + var(--nm-density));
block-size: calc(48px + var(--nm-density));
margin-block-start: 8px;
margin-inline-end: 8px;
border-radius: var(--md-sys-shape-corner-full);
@@ -46,13 +47,38 @@
mask: url(../images/menu.svg) center / 24px no-repeat;
}
/* Flyout: navigation drawer */
/* Flyout: M3 modal navigation drawer. It slides over the page behind a scrim instead of
core's push, which shoves page and header off screen. Tapping the scrim hits #main,
which core's responsive.js already binds to close the flyout. */
html.flyout-is-active :is(#wrapper, #header) { inset-inline-end: 0; }
html .flyout-menu {
--nm-drawer-width: min(360px, calc(100vw - 56px)); /* M3 modal drawer; core's 250px squeezes the appearance picker */
position: fixed;
inset-block: 0;
inset-inline-end: calc(-1 * var(--nm-drawer-width));
inline-size: var(--nm-drawer-width);
z-index: 10001; /* over core's fixed header (9999) and the scrim */
block-size: auto;
overflow-y: auto;
overscroll-behavior: contain;
visibility: hidden;
transition: inset-inline-end 250ms cubic-bezier(0.2, 0, 0, 1), visibility 250ms;
border-start-start-radius: var(--md-sys-shape-corner-large);
border-end-start-radius: var(--md-sys-shape-corner-large);
box-shadow: var(--md-sys-elevation-level1);
background-color: var(--md-sys-color-surface-container-low);
color: var(--md-sys-color-on-surface);
padding-inline: 12px;
box-sizing: border-box;
}
html.flyout-is-active .flyout-menu { inset-inline-end: 0; visibility: visible; }
html.flyout-is-active #main::after {
content: '';
position: fixed;
inset: 0;
z-index: 10000;
background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent);
}
html .flyout-menu h3 {
block-size: auto;
margin-block: 16px 4px;
@@ -64,17 +90,37 @@
letter-spacing: var(--md-sys-typescale-title-small-tracking);
text-transform: none;
}
/* M3 drawer dividers between groups; headings alone look like one more nav item */
html .flyout-menu > :is(h3, .flyout-menu__sidebar:has(#sidebar-wrapper > *), .nm-appearance) {
display: block; /* the sidebar group is a span */
margin-block-start: 8px;
padding-block-start: 8px;
border-block-start: 1px solid var(--md-sys-color-outline-variant);
}
html .flyout-menu > h3 { padding-block-start: 16px; }
/* Sidebar forms (e.g. activity filters) line up with the nav labels; a field stacks under
its label, which is a bare text node, so grid is the only way to put it on its own row */
html .flyout-menu__sidebar :is(form > p, li:has(> input[type="checkbox"])) { padding-inline: 16px; }
html .flyout-menu__sidebar p:has(> :is(select, input:not([type="submit"], [type="checkbox"], [type="radio"]))) {
display: grid;
gap: 4px;
color: var(--md-sys-color-on-surface-variant);
font: var(--md-sys-typescale-label-large);
}
html .flyout-menu__sidebar p > :is(select, input:not([type="submit"], [type="checkbox"], [type="radio"])) { inline-size: 100%; box-sizing: border-box; }
html .flyout-menu__sidebar li:has(> input[type="checkbox"]) { display: flex; align-items: center; gap: 12px; min-block-size: calc(40px + var(--nm-density)); border: 0; }
html .flyout-menu .flyout-menu__sidebar li:has(> input[type="checkbox"]) a { block-size: auto; padding: 0; line-height: inherit; }
html .flyout-menu h4 { color: var(--md-sys-color-on-surface); }
html .flyout-menu a { color: var(--md-sys-color-on-surface-variant); }
html .flyout-menu ul li a, html .flyout-menu ul li:first-child a {
block-size: 48px;
block-size: calc(48px + var(--nm-density));
line-height: 48px;
margin-inline-start: 0;
padding-inline: 16px;
border: 0;
border-radius: var(--md-sys-shape-corner-full);
font: var(--md-sys-typescale-label-large);
line-height: 48px;
line-height: calc(48px + var(--nm-density));
}
html .flyout-menu ul li a:hover { background-image: var(--nm-state-hover); }
html .flyout-menu ul li a.selected {
@@ -84,7 +130,7 @@
html .flyout-menu #watchers ul li, html .flyout-menu ul.queries li, html .flyout-menu__avatar { border-block-start: 0; }
html .flyout-menu .icon svg, html .flyout-menu .icon-only svg { color: inherit; stroke: currentColor; }
html .flyout-menu__search input[type='text'] {
block-size: 48px;
block-size: calc(48px + var(--nm-density));
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--md-sys-color-surface-container-highest);
color: var(--md-sys-color-on-surface);
@@ -92,11 +138,64 @@
}
/* Search pill, avatar row and nav labels share one inline start: the icon and the
avatar sit where the nav labels begin (drawer padding 12px + item padding 16px) */
html .flyout-menu .flyout-menu__search { padding-inline: 0; }
/* Core's 64px / 54px container grows with the search pill */
html .flyout-menu .flyout-menu__search { padding-inline: 0; block-size: calc(64px + var(--nm-density)); line-height: calc(54px + var(--nm-density)); }
html .search-magnifier--flyout { inset-inline-start: 28px; }
html .flyout-menu .flyout-menu__avatar { padding-inline-start: 16px; }
html .cal .week-number { background-color: var(--md-sys-color-surface-container); border-color: var(--md-sys-color-outline-variant); }
/* Calendar month bar: the month select fills the row, Apply / Clear / Save wrap below */
html body.controller-calendars p.buttons select#month { flex: 1 1 0; min-inline-size: 0; }
html body.controller-calendars #query_form_with_buttons::after { content: ''; order: 2; flex-basis: 100%; margin-block: -4px; } /* line break */
html body.controller-calendars p.buttons a { order: 3; }
/* Calendar as an agenda: weekday column on the left, issues on the right. Days of the
neighbouring months are dropped; empty days shrink to one line. */
html .cal .week-number {
padding-block: 8px;
padding-inline: 16px;
border: 0;
border-block-start: 1px solid var(--md-sys-color-outline-variant);
background-color: var(--md-sys-color-surface-container);
color: var(--md-sys-color-on-surface-variant);
font: var(--md-sys-typescale-label-medium);
letter-spacing: var(--md-sys-typescale-label-medium-tracking);
}
html .cal .week-number:first-child { border-block-start: 0; }
html .cal .calbody.other-month { display: none; }
html .cal .calbody {
display: grid;
grid-template-columns: 40px minmax(0, 1fr);
column-gap: 8px;
align-items: start;
min-block-size: 0;
padding-block: 8px;
padding-inline: 8px;
border: 0;
border-block-start: 1px solid var(--md-sys-color-outline-variant);
}
/* ponytail: the date spans 50 implicit rows (empty ones collapse to 0); a day with more
issues lets the extra ones fall under the date */
html .cal .calbody p.day-num {
grid-row: 1 / span 50;
display: flex;
flex-direction: column;
align-items: center;
margin: 0;
text-align: center;
}
html .cal .calbody .day-value { inline-size: 32px; block-size: 32px; font: var(--md-sys-typescale-title-medium); }
html .cal .calbody .abbr-day { color: var(--md-sys-color-on-surface-variant); font: var(--md-sys-typescale-label-small); }
html .cal .calbody div.issue, html .cal .calbody > .icon { grid-column: 2; margin-block: 0 4px; }
/* An empty day is one short line: date and weekday side by side, greyed out */
html .cal .calbody:not(:has(div.issue, > .icon)) { padding-block: 4px; }
html .cal .calbody:not(:has(div.issue, > .icon)) p.day-num { flex-direction: row; gap: 8px; color: var(--md-sys-color-on-surface-variant); }
html .cal .calbody:not(:has(div.issue, > .icon)) .day-value { flex: none; inline-size: 40px; block-size: 24px; font: var(--md-sys-typescale-label-large); }
/* Today: tinted row with a primary stripe on its leading edge */
html .cal .calbody.today {
border-inline-start: 4px solid var(--md-sys-color-primary);
padding-inline-start: 4px;
background-color: color-mix(in srgb, var(--md-sys-color-primary) 8%, var(--md-sys-color-surface));
}
/* Page actions become M3 outlined buttons, like Apply / Clear. The forum topic wraps its
actions in a quote-reply controller, so core's #content > .contextual misses them. */
html #content > [data-controller="quote-reply"] > .contextual {
@@ -132,8 +231,23 @@
/* Form fields run full width; a select keeps room for the icon button after it */
html form .attributes select { inline-size: 100%; }
html form .attributes select:has(+ .icon-only) { inline-size: calc(100% - 36px); }
/* Core leaves the tracker at 90%; only the project row shares its line (with "Private") */
html #all_attributes #issue_tracker_id { inline-size: 100%; }
/* Core leaves dates at their content width; every field runs full width here */
html .tabular input:is(.date, [type="date"]) { inline-size: 100%; max-inline-size: 100%; }
html .tabular input[type="radio"] { inline-size: auto; }
/* Query filters: operator and values run full width under the checkbox label instead of
the desktop's fixed 180px / 260px columns. A flex-basis of 0 overrides those widths, so
"between" fields split the row and a multi-select toggle keeps its spot after the field. */
html #filters-table .filter { row-gap: 8px; margin-block-end: 8px; }
html #filters-table .filter :is(.operator, .values) { flex: 1 1 100%; }
html #filters-table .filter .values:not(:has(> span:not([style*="none"]))) { display: none; } /* e.g. status "open" */
html #filters-table .filter .operator select { inline-size: 100%; }
html #filters-table .filter .values > span { display: flex; flex: 1 1 0; align-items: center; gap: 8px; }
html #filters-table .filter .values .value:not([size="3"]) { flex: 1 1 0; min-inline-size: 0; }
html .add-filter { justify-content: stretch; margin-block-start: 16px; } /* full-width button */
/* Wiki toolbar: buttons wrap instead of being cut off */
html #content .jstTabs.tabs { clear: both; block-size: auto; overflow: visible; } /* below the floated field label */
html #content .jstTabs.tabs ul { position: static; display: flex; flex-wrap: wrap; inline-size: auto; min-inline-size: 0; }
+34 -2
View File
@@ -122,7 +122,7 @@ div.wiki *:not(pre) > code, div.wiki > code {
.syntaxhl :is(.nv, .vc, .vg, .vi, .vm, .nl, .ni) { color: var(--md-sys-color-warning); font-weight: normal; }
.syntaxhl :is(.err, .gd, .gr, .ne, .gt) { color: var(--md-sys-color-error); background-color: transparent; }
.syntaxhl :is(.o) { color: var(--md-sys-color-on-surface); }
.syntaxhl .hll { background-color: var(--md-sys-color-secondary-container); }
.syntaxhl .hll { background-color: var(--md-sys-color-surface-container-highest); } /* stays a surface tone at every contrast, so token colors stay readable */
/***** Activity *****/
/* One card per day (the <dl>), entries are M3 list rows. Each entry is a <dt> + <dd> pair:
@@ -342,7 +342,7 @@ div#roadmap .version-article {
border: 1px solid var(--md-sys-color-outline-variant);
border-radius: var(--md-sys-shape-corner-medium);
}
div#roadmap header:target { background-color: var(--md-sys-color-secondary-container); }
div#roadmap header:target { background-color: var(--md-sys-color-surface-container-highest); }
div#version-summary { background-color: transparent; }
/* Overview and issue table share one left edge (core indents the overview by 1.5rem) */
@@ -374,6 +374,38 @@ ul.cal {
.cal .calbody.other-month, .cal .calbody.nwday:not(.other-month) { background-color: var(--md-sys-color-surface-container-low); }
.cal .calbody.today .day-value { color: var(--md-sys-color-on-primary); }
/* Month bar: ‹ [month] [year] › then Apply / Clear / Save. The arrows and the selects live
in separate containers, so those dissolve into one flex row. */
body.controller-calendars #query_form_with_buttons { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-block-end: 16px; } /* p.buttons had it */
body.controller-calendars #query_form_content { flex: 1 1 100%; }
body.controller-calendars :is(#query_form_with_buttons > .pagination, #query_form_with_buttons > .pagination ul, #query_form_with_buttons > p.buttons) { display: contents; }
body.controller-calendars p.buttons label { position: absolute; clip-path: inset(50%); inline-size: 1px; block-size: 1px; overflow: hidden; }
body.controller-calendars .pagination li { margin: 0; }
body.controller-calendars .pagination li.next { order: 1; }
body.controller-calendars p.buttons a { order: 2; }
/* Previous / next month: icon buttons; the month name stays as the accessible text */
body.controller-calendars .pagination li a {
display: block;
inline-size: 40px;
block-size: 40px;
padding: 0;
border: 0;
border-radius: var(--md-sys-shape-corner-full);
font-size: 0;
color: var(--md-sys-color-on-surface-variant);
}
body.controller-calendars .pagination li a:hover { background-image: var(--nm-state-hover); }
body.controller-calendars .pagination li a::before {
content: '';
display: block;
block-size: 100%;
background-color: currentColor;
mask: url(../images/chevron-left-idnt.svg) center / 24px no-repeat;
}
body.controller-calendars .pagination li.next a::before,
html[dir="rtl"] body.controller-calendars .pagination li.previous a::before { transform: scaleX(-1); }
html[dir="rtl"] body.controller-calendars .pagination li.next a::before { transform: none; }
/***** Gantt (gantt.css loads after the theme, hence the `html` prefix) *****/
/*
* Geometry is set inline by Redmine: rows every 20px, each with a 16px text line whose top is
+26 -6
View File
@@ -1,8 +1,9 @@
/*
* Generated by tools/palettes/build_palettes.mjs; do not edit by hand.
* M3 color roles (TonalSpot, contrast 0) per palette, light and dark.
* M3 color roles (TonalSpot) per palette, light and dark, at contrast 0, 0.5 and 1.
* <html data-palette="..."> picks the palette, <html data-mode="light|dark"> forces a mode;
* without data-mode the OS setting (prefers-color-scheme) decides.
* <html data-contrast="medium|high"> picks the contrast level; without it, standard.
*/
:root { color-scheme: light; }
@@ -13,23 +14,42 @@
:root { --md-sys-color-primary: #65558f; --md-sys-color-on-primary: #ffffff; --md-sys-color-primary-container: #e9ddff; --md-sys-color-on-primary-container: #4d3d75; --md-sys-color-inverse-primary: #cfbdfe; --md-sys-color-secondary: #625b71; --md-sys-color-on-secondary: #ffffff; --md-sys-color-secondary-container: #e8def8; --md-sys-color-on-secondary-container: #4a4458; --md-sys-color-tertiary: #7e5260; --md-sys-color-on-tertiary: #ffffff; --md-sys-color-tertiary-container: #ffd9e3; --md-sys-color-on-tertiary-container: #633b48; --md-sys-color-error: #ba1a1a; --md-sys-color-on-error: #ffffff; --md-sys-color-error-container: #ffdad6; --md-sys-color-on-error-container: #93000a; --md-sys-color-background: #fdf7ff; --md-sys-color-on-background: #1d1b20; --md-sys-color-surface: #fdf7ff; --md-sys-color-surface-dim: #ded8e0; --md-sys-color-surface-bright: #fdf7ff; --md-sys-color-surface-container-lowest: #ffffff; --md-sys-color-surface-container-low: #f8f2fa; --md-sys-color-surface-container: #f2ecf4; --md-sys-color-surface-container-high: #ece6ee; --md-sys-color-surface-container-highest: #e6e0e9; --md-sys-color-on-surface: #1d1b20; --md-sys-color-surface-variant: #e7e0eb; --md-sys-color-on-surface-variant: #49454e; --md-sys-color-inverse-surface: #322f35; --md-sys-color-inverse-on-surface: #f5eff7; --md-sys-color-outline: #7a757f; --md-sys-color-outline-variant: #cac4cf; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #65558f; --md-sys-color-success: #0a6d36; --md-sys-color-on-success: #ffffff; --md-sys-color-success-container: #9ef6b0; --md-sys-color-on-success-container: #00210b; --md-sys-color-warning: #885200; --md-sys-color-on-warning: #ffffff; --md-sys-color-warning-container: #ffddbb; --md-sys-color-on-warning-container: #2b1700; }
:root[data-mode="dark"] { --md-sys-color-primary: #cfbdfe; --md-sys-color-on-primary: #36275d; --md-sys-color-primary-container: #4d3d75; --md-sys-color-on-primary-container: #e9ddff; --md-sys-color-inverse-primary: #65558f; --md-sys-color-secondary: #cbc2db; --md-sys-color-on-secondary: #332d41; --md-sys-color-secondary-container: #4a4458; --md-sys-color-on-secondary-container: #e8def8; --md-sys-color-tertiary: #efb8c8; --md-sys-color-on-tertiary: #4a2532; --md-sys-color-tertiary-container: #633b48; --md-sys-color-on-tertiary-container: #ffd9e3; --md-sys-color-error: #ffb4ab; --md-sys-color-on-error: #690005; --md-sys-color-error-container: #93000a; --md-sys-color-on-error-container: #ffdad6; --md-sys-color-background: #141218; --md-sys-color-on-background: #e6e0e9; --md-sys-color-surface: #141218; --md-sys-color-surface-dim: #141218; --md-sys-color-surface-bright: #3b383e; --md-sys-color-surface-container-lowest: #0f0d13; --md-sys-color-surface-container-low: #1d1b20; --md-sys-color-surface-container: #211f24; --md-sys-color-surface-container-high: #2b292f; --md-sys-color-surface-container-highest: #36343a; --md-sys-color-on-surface: #e6e0e9; --md-sys-color-surface-variant: #49454e; --md-sys-color-on-surface-variant: #cac4cf; --md-sys-color-inverse-surface: #e6e0e9; --md-sys-color-inverse-on-surface: #322f35; --md-sys-color-outline: #948f99; --md-sys-color-outline-variant: #49454e; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #cfbdfe; --md-sys-color-success: #82d996; --md-sys-color-on-success: #003918; --md-sys-color-success-container: #005226; --md-sys-color-on-success-container: #9ef6b0; --md-sys-color-warning: #ffb869; --md-sys-color-on-warning: #482900; --md-sys-color-warning-container: #673d00; --md-sys-color-on-warning-container: #ffddbb; }
@media (prefers-color-scheme: dark) { :root:not([data-mode="light"]) { --md-sys-color-primary: #cfbdfe; --md-sys-color-on-primary: #36275d; --md-sys-color-primary-container: #4d3d75; --md-sys-color-on-primary-container: #e9ddff; --md-sys-color-inverse-primary: #65558f; --md-sys-color-secondary: #cbc2db; --md-sys-color-on-secondary: #332d41; --md-sys-color-secondary-container: #4a4458; --md-sys-color-on-secondary-container: #e8def8; --md-sys-color-tertiary: #efb8c8; --md-sys-color-on-tertiary: #4a2532; --md-sys-color-tertiary-container: #633b48; --md-sys-color-on-tertiary-container: #ffd9e3; --md-sys-color-error: #ffb4ab; --md-sys-color-on-error: #690005; --md-sys-color-error-container: #93000a; --md-sys-color-on-error-container: #ffdad6; --md-sys-color-background: #141218; --md-sys-color-on-background: #e6e0e9; --md-sys-color-surface: #141218; --md-sys-color-surface-dim: #141218; --md-sys-color-surface-bright: #3b383e; --md-sys-color-surface-container-lowest: #0f0d13; --md-sys-color-surface-container-low: #1d1b20; --md-sys-color-surface-container: #211f24; --md-sys-color-surface-container-high: #2b292f; --md-sys-color-surface-container-highest: #36343a; --md-sys-color-on-surface: #e6e0e9; --md-sys-color-surface-variant: #49454e; --md-sys-color-on-surface-variant: #cac4cf; --md-sys-color-inverse-surface: #e6e0e9; --md-sys-color-inverse-on-surface: #322f35; --md-sys-color-outline: #948f99; --md-sys-color-outline-variant: #49454e; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #cfbdfe; --md-sys-color-success: #82d996; --md-sys-color-on-success: #003918; --md-sys-color-success-container: #005226; --md-sys-color-on-success-container: #9ef6b0; --md-sys-color-warning: #ffb869; --md-sys-color-on-warning: #482900; --md-sys-color-warning-container: #673d00; --md-sys-color-on-warning-container: #ffddbb; } }
:root[data-contrast="medium"] { --md-sys-color-primary: #3c2d63; --md-sys-color-on-primary: #ffffff; --md-sys-color-primary-container: #74649f; --md-sys-color-on-primary-container: #ffffff; --md-sys-color-inverse-primary: #cfbdfe; --md-sys-color-secondary: #393347; --md-sys-color-on-secondary: #ffffff; --md-sys-color-secondary-container: #716a80; --md-sys-color-on-secondary-container: #ffffff; --md-sys-color-tertiary: #502b38; --md-sys-color-on-tertiary: #ffffff; --md-sys-color-tertiary-container: #8e606f; --md-sys-color-on-tertiary-container: #ffffff; --md-sys-color-error: #740006; --md-sys-color-on-error: #ffffff; --md-sys-color-error-container: #cf2c27; --md-sys-color-on-error-container: #ffffff; --md-sys-color-background: #fdf7ff; --md-sys-color-on-background: #1d1b20; --md-sys-color-surface: #fdf7ff; --md-sys-color-surface-dim: #cac5cc; --md-sys-color-surface-bright: #fdf7ff; --md-sys-color-surface-container-lowest: #ffffff; --md-sys-color-surface-container-low: #f8f2fa; --md-sys-color-surface-container: #ece6ee; --md-sys-color-surface-container-high: #e1dbe3; --md-sys-color-surface-container-highest: #d5d0d8; --md-sys-color-on-surface: #121016; --md-sys-color-surface-variant: #e7e0eb; --md-sys-color-on-surface-variant: #38353d; --md-sys-color-inverse-surface: #322f35; --md-sys-color-inverse-on-surface: #f5eff7; --md-sys-color-outline: #54515a; --md-sys-color-outline-variant: #6f6b75; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #65558f; --md-sys-color-success: #0a6d36; --md-sys-color-on-success: #ffffff; --md-sys-color-success-container: #9ef6b0; --md-sys-color-on-success-container: #00210b; --md-sys-color-warning: #885200; --md-sys-color-on-warning: #ffffff; --md-sys-color-warning-container: #ffddbb; --md-sys-color-on-warning-container: #2b1700; }
:root[data-contrast="medium"][data-mode="dark"] { --md-sys-color-primary: #e3d6ff; --md-sys-color-on-primary: #2b1b52; --md-sys-color-primary-container: #9887c5; --md-sys-color-on-primary-container: #000000; --md-sys-color-inverse-primary: #4e3f77; --md-sys-color-secondary: #e2d8f2; --md-sys-color-on-secondary: #282336; --md-sys-color-secondary-container: #958da4; --md-sys-color-on-secondary-container: #000000; --md-sys-color-tertiary: #ffd0dd; --md-sys-color-on-tertiary: #3d1a27; --md-sys-color-tertiary-container: #b58392; --md-sys-color-on-tertiary-container: #000000; --md-sys-color-error: #ffd2cc; --md-sys-color-on-error: #540003; --md-sys-color-error-container: #ff5449; --md-sys-color-on-error-container: #000000; --md-sys-color-background: #141218; --md-sys-color-on-background: #e6e0e9; --md-sys-color-surface: #141218; --md-sys-color-surface-dim: #141218; --md-sys-color-surface-bright: #46434a; --md-sys-color-surface-container-lowest: #08070b; --md-sys-color-surface-container-low: #1f1d22; --md-sys-color-surface-container: #29272d; --md-sys-color-surface-container-high: #343138; --md-sys-color-surface-container-highest: #3f3c43; --md-sys-color-on-surface: #ffffff; --md-sys-color-surface-variant: #49454e; --md-sys-color-on-surface-variant: #e0dae5; --md-sys-color-inverse-surface: #e6e0e9; --md-sys-color-inverse-on-surface: #2b292f; --md-sys-color-outline: #b5b0bb; --md-sys-color-outline-variant: #938e99; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #cfbdfe; --md-sys-color-success: #82d996; --md-sys-color-on-success: #003918; --md-sys-color-success-container: #005226; --md-sys-color-on-success-container: #9ef6b0; --md-sys-color-warning: #ffb869; --md-sys-color-on-warning: #482900; --md-sys-color-warning-container: #673d00; --md-sys-color-on-warning-container: #ffddbb; }
@media (prefers-color-scheme: dark) { :root[data-contrast="medium"]:not([data-mode="light"]) { --md-sys-color-primary: #e3d6ff; --md-sys-color-on-primary: #2b1b52; --md-sys-color-primary-container: #9887c5; --md-sys-color-on-primary-container: #000000; --md-sys-color-inverse-primary: #4e3f77; --md-sys-color-secondary: #e2d8f2; --md-sys-color-on-secondary: #282336; --md-sys-color-secondary-container: #958da4; --md-sys-color-on-secondary-container: #000000; --md-sys-color-tertiary: #ffd0dd; --md-sys-color-on-tertiary: #3d1a27; --md-sys-color-tertiary-container: #b58392; --md-sys-color-on-tertiary-container: #000000; --md-sys-color-error: #ffd2cc; --md-sys-color-on-error: #540003; --md-sys-color-error-container: #ff5449; --md-sys-color-on-error-container: #000000; --md-sys-color-background: #141218; --md-sys-color-on-background: #e6e0e9; --md-sys-color-surface: #141218; --md-sys-color-surface-dim: #141218; --md-sys-color-surface-bright: #46434a; --md-sys-color-surface-container-lowest: #08070b; --md-sys-color-surface-container-low: #1f1d22; --md-sys-color-surface-container: #29272d; --md-sys-color-surface-container-high: #343138; --md-sys-color-surface-container-highest: #3f3c43; --md-sys-color-on-surface: #ffffff; --md-sys-color-surface-variant: #49454e; --md-sys-color-on-surface-variant: #e0dae5; --md-sys-color-inverse-surface: #e6e0e9; --md-sys-color-inverse-on-surface: #2b292f; --md-sys-color-outline: #b5b0bb; --md-sys-color-outline-variant: #938e99; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #cfbdfe; --md-sys-color-success: #82d996; --md-sys-color-on-success: #003918; --md-sys-color-success-container: #005226; --md-sys-color-on-success-container: #9ef6b0; --md-sys-color-warning: #ffb869; --md-sys-color-on-warning: #482900; --md-sys-color-warning-container: #673d00; --md-sys-color-on-warning-container: #ffddbb; } }
:root[data-contrast="high"] { --md-sys-color-primary: #312259; --md-sys-color-on-primary: #ffffff; --md-sys-color-primary-container: #4f4078; --md-sys-color-on-primary-container: #ffffff; --md-sys-color-inverse-primary: #cfbdfe; --md-sys-color-secondary: #2f293c; --md-sys-color-on-secondary: #ffffff; --md-sys-color-secondary-container: #4c465b; --md-sys-color-on-secondary-container: #ffffff; --md-sys-color-tertiary: #45212e; --md-sys-color-on-tertiary: #ffffff; --md-sys-color-tertiary-container: #663d4b; --md-sys-color-on-tertiary-container: #ffffff; --md-sys-color-error: #600004; --md-sys-color-on-error: #ffffff; --md-sys-color-error-container: #98000a; --md-sys-color-on-error-container: #ffffff; --md-sys-color-background: #fdf7ff; --md-sys-color-on-background: #1d1b20; --md-sys-color-surface: #fdf7ff; --md-sys-color-surface-dim: #bcb7bf; --md-sys-color-surface-bright: #fdf7ff; --md-sys-color-surface-container-lowest: #ffffff; --md-sys-color-surface-container-low: #f5eff7; --md-sys-color-surface-container: #e6e0e9; --md-sys-color-surface-container-high: #d8d2da; --md-sys-color-surface-container-highest: #cac5cc; --md-sys-color-on-surface: #000000; --md-sys-color-surface-variant: #e7e0eb; --md-sys-color-on-surface-variant: #000000; --md-sys-color-inverse-surface: #322f35; --md-sys-color-inverse-on-surface: #ffffff; --md-sys-color-outline: #2e2b33; --md-sys-color-outline-variant: #4b4851; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #65558f; --md-sys-color-success: #0a6d36; --md-sys-color-on-success: #ffffff; --md-sys-color-success-container: #9ef6b0; --md-sys-color-on-success-container: #00210b; --md-sys-color-warning: #885200; --md-sys-color-on-warning: #ffffff; --md-sys-color-warning-container: #ffddbb; --md-sys-color-on-warning-container: #2b1700; }
:root[data-contrast="high"][data-mode="dark"] { --md-sys-color-primary: #f5edff; --md-sys-color-on-primary: #000000; --md-sys-color-primary-container: #cbb9fa; --md-sys-color-on-primary-container: #0f0033; --md-sys-color-inverse-primary: #4e3f77; --md-sys-color-secondary: #f5edff; --md-sys-color-on-secondary: #000000; --md-sys-color-secondary-container: #c8bfd8; --md-sys-color-on-secondary-container: #0d081a; --md-sys-color-tertiary: #ffebef; --md-sys-color-on-tertiary: #000000; --md-sys-color-tertiary-container: #ebb4c4; --md-sys-color-on-tertiary-container: #1d020d; --md-sys-color-error: #ffece9; --md-sys-color-on-error: #000000; --md-sys-color-error-container: #ffaea4; --md-sys-color-on-error-container: #220001; --md-sys-color-background: #141218; --md-sys-color-on-background: #e6e0e9; --md-sys-color-surface: #141218; --md-sys-color-surface-dim: #141218; --md-sys-color-surface-bright: #524f55; --md-sys-color-surface-container-lowest: #000000; --md-sys-color-surface-container-low: #211f24; --md-sys-color-surface-container: #322f35; --md-sys-color-surface-container-high: #3d3a41; --md-sys-color-surface-container-highest: #48464c; --md-sys-color-on-surface: #ffffff; --md-sys-color-surface-variant: #49454e; --md-sys-color-on-surface-variant: #ffffff; --md-sys-color-inverse-surface: #e6e0e9; --md-sys-color-inverse-on-surface: #000000; --md-sys-color-outline: #f4eef9; --md-sys-color-outline-variant: #c6c0cb; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #cfbdfe; --md-sys-color-success: #82d996; --md-sys-color-on-success: #003918; --md-sys-color-success-container: #005226; --md-sys-color-on-success-container: #9ef6b0; --md-sys-color-warning: #ffb869; --md-sys-color-on-warning: #482900; --md-sys-color-warning-container: #673d00; --md-sys-color-on-warning-container: #ffddbb; }
@media (prefers-color-scheme: dark) { :root[data-contrast="high"]:not([data-mode="light"]) { --md-sys-color-primary: #f5edff; --md-sys-color-on-primary: #000000; --md-sys-color-primary-container: #cbb9fa; --md-sys-color-on-primary-container: #0f0033; --md-sys-color-inverse-primary: #4e3f77; --md-sys-color-secondary: #f5edff; --md-sys-color-on-secondary: #000000; --md-sys-color-secondary-container: #c8bfd8; --md-sys-color-on-secondary-container: #0d081a; --md-sys-color-tertiary: #ffebef; --md-sys-color-on-tertiary: #000000; --md-sys-color-tertiary-container: #ebb4c4; --md-sys-color-on-tertiary-container: #1d020d; --md-sys-color-error: #ffece9; --md-sys-color-on-error: #000000; --md-sys-color-error-container: #ffaea4; --md-sys-color-on-error-container: #220001; --md-sys-color-background: #141218; --md-sys-color-on-background: #e6e0e9; --md-sys-color-surface: #141218; --md-sys-color-surface-dim: #141218; --md-sys-color-surface-bright: #524f55; --md-sys-color-surface-container-lowest: #000000; --md-sys-color-surface-container-low: #211f24; --md-sys-color-surface-container: #322f35; --md-sys-color-surface-container-high: #3d3a41; --md-sys-color-surface-container-highest: #48464c; --md-sys-color-on-surface: #ffffff; --md-sys-color-surface-variant: #49454e; --md-sys-color-on-surface-variant: #ffffff; --md-sys-color-inverse-surface: #e6e0e9; --md-sys-color-inverse-on-surface: #000000; --md-sys-color-outline: #f4eef9; --md-sys-color-outline-variant: #c6c0cb; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #cfbdfe; --md-sys-color-success: #82d996; --md-sys-color-on-success: #003918; --md-sys-color-success-container: #005226; --md-sys-color-on-success-container: #9ef6b0; --md-sys-color-warning: #ffb869; --md-sys-color-on-warning: #482900; --md-sys-color-warning-container: #673d00; --md-sys-color-on-warning-container: #ffddbb; } }
/* blue (seed #0061A4) */
:root[data-palette="blue"] { --md-sys-color-primary: #36618e; --md-sys-color-on-primary: #ffffff; --md-sys-color-primary-container: #d1e4ff; --md-sys-color-on-primary-container: #1a4975; --md-sys-color-inverse-primary: #a0cafd; --md-sys-color-secondary: #535f70; --md-sys-color-on-secondary: #ffffff; --md-sys-color-secondary-container: #d7e3f8; --md-sys-color-on-secondary-container: #3b4858; --md-sys-color-tertiary: #6b5778; --md-sys-color-on-tertiary: #ffffff; --md-sys-color-tertiary-container: #f3daff; --md-sys-color-on-tertiary-container: #523f5f; --md-sys-color-error: #ba1a1a; --md-sys-color-on-error: #ffffff; --md-sys-color-error-container: #ffdad6; --md-sys-color-on-error-container: #93000a; --md-sys-color-background: #f8f9ff; --md-sys-color-on-background: #191c20; --md-sys-color-surface: #f8f9ff; --md-sys-color-surface-dim: #d8dae0; --md-sys-color-surface-bright: #f8f9ff; --md-sys-color-surface-container-lowest: #ffffff; --md-sys-color-surface-container-low: #f2f3fa; --md-sys-color-surface-container: #eceef4; --md-sys-color-surface-container-high: #e6e8ee; --md-sys-color-surface-container-highest: #e1e2e8; --md-sys-color-on-surface: #191c20; --md-sys-color-surface-variant: #dfe2eb; --md-sys-color-on-surface-variant: #43474e; --md-sys-color-inverse-surface: #2e3135; --md-sys-color-inverse-on-surface: #eff0f7; --md-sys-color-outline: #73777f; --md-sys-color-outline-variant: #c3c6cf; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #36618e; --md-sys-color-success: #0a6d36; --md-sys-color-on-success: #ffffff; --md-sys-color-success-container: #9ef6b0; --md-sys-color-on-success-container: #00210b; --md-sys-color-warning: #6f5d00; --md-sys-color-on-warning: #ffffff; --md-sys-color-warning-container: #ffe16a; --md-sys-color-on-warning-container: #221b00; }
:root[data-palette="blue"][data-mode="dark"] { --md-sys-color-primary: #a0cafd; --md-sys-color-on-primary: #003258; --md-sys-color-primary-container: #1a4975; --md-sys-color-on-primary-container: #d1e4ff; --md-sys-color-inverse-primary: #36618e; --md-sys-color-secondary: #bbc7db; --md-sys-color-on-secondary: #253140; --md-sys-color-secondary-container: #3b4858; --md-sys-color-on-secondary-container: #d7e3f8; --md-sys-color-tertiary: #d7bee4; --md-sys-color-on-tertiary: #3b2948; --md-sys-color-tertiary-container: #523f5f; --md-sys-color-on-tertiary-container: #f3daff; --md-sys-color-error: #ffb4ab; --md-sys-color-on-error: #690005; --md-sys-color-error-container: #93000a; --md-sys-color-on-error-container: #ffdad6; --md-sys-color-background: #111418; --md-sys-color-on-background: #e1e2e8; --md-sys-color-surface: #111418; --md-sys-color-surface-dim: #111418; --md-sys-color-surface-bright: #36393e; --md-sys-color-surface-container-lowest: #0b0e13; --md-sys-color-surface-container-low: #191c20; --md-sys-color-surface-container: #1d2024; --md-sys-color-surface-container-high: #272a2f; --md-sys-color-surface-container-highest: #32353a; --md-sys-color-on-surface: #e1e2e8; --md-sys-color-surface-variant: #43474e; --md-sys-color-on-surface-variant: #c3c6cf; --md-sys-color-inverse-surface: #e1e2e8; --md-sys-color-inverse-on-surface: #2e3135; --md-sys-color-outline: #8d9199; --md-sys-color-outline-variant: #43474e; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #a0cafd; --md-sys-color-success: #82d996; --md-sys-color-on-success: #003918; --md-sys-color-success-container: #005226; --md-sys-color-on-success-container: #9ef6b0; --md-sys-color-warning: #e2c54a; --md-sys-color-on-warning: #3a3000; --md-sys-color-warning-container: #544600; --md-sys-color-on-warning-container: #ffe16a; }
@media (prefers-color-scheme: dark) { :root[data-palette="blue"]:not([data-mode="light"]) { --md-sys-color-primary: #a0cafd; --md-sys-color-on-primary: #003258; --md-sys-color-primary-container: #1a4975; --md-sys-color-on-primary-container: #d1e4ff; --md-sys-color-inverse-primary: #36618e; --md-sys-color-secondary: #bbc7db; --md-sys-color-on-secondary: #253140; --md-sys-color-secondary-container: #3b4858; --md-sys-color-on-secondary-container: #d7e3f8; --md-sys-color-tertiary: #d7bee4; --md-sys-color-on-tertiary: #3b2948; --md-sys-color-tertiary-container: #523f5f; --md-sys-color-on-tertiary-container: #f3daff; --md-sys-color-error: #ffb4ab; --md-sys-color-on-error: #690005; --md-sys-color-error-container: #93000a; --md-sys-color-on-error-container: #ffdad6; --md-sys-color-background: #111418; --md-sys-color-on-background: #e1e2e8; --md-sys-color-surface: #111418; --md-sys-color-surface-dim: #111418; --md-sys-color-surface-bright: #36393e; --md-sys-color-surface-container-lowest: #0b0e13; --md-sys-color-surface-container-low: #191c20; --md-sys-color-surface-container: #1d2024; --md-sys-color-surface-container-high: #272a2f; --md-sys-color-surface-container-highest: #32353a; --md-sys-color-on-surface: #e1e2e8; --md-sys-color-surface-variant: #43474e; --md-sys-color-on-surface-variant: #c3c6cf; --md-sys-color-inverse-surface: #e1e2e8; --md-sys-color-inverse-on-surface: #2e3135; --md-sys-color-outline: #8d9199; --md-sys-color-outline-variant: #43474e; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #a0cafd; --md-sys-color-success: #82d996; --md-sys-color-on-success: #003918; --md-sys-color-success-container: #005226; --md-sys-color-on-success-container: #9ef6b0; --md-sys-color-warning: #e2c54a; --md-sys-color-on-warning: #3a3000; --md-sys-color-warning-container: #544600; --md-sys-color-on-warning-container: #ffe16a; } }
/* teal (seed #006A6A) */
:root[data-palette="teal"] { --md-sys-color-primary: #006a6a; --md-sys-color-on-primary: #ffffff; --md-sys-color-primary-container: #9cf1f0; --md-sys-color-on-primary-container: #004f4f; --md-sys-color-inverse-primary: #80d5d4; --md-sys-color-secondary: #4a6363; --md-sys-color-on-secondary: #ffffff; --md-sys-color-secondary-container: #cce8e7; --md-sys-color-on-secondary-container: #324b4b; --md-sys-color-tertiary: #4b607c; --md-sys-color-on-tertiary: #ffffff; --md-sys-color-tertiary-container: #d3e4ff; --md-sys-color-on-tertiary-container: #334863; --md-sys-color-error: #ba1a1a; --md-sys-color-on-error: #ffffff; --md-sys-color-error-container: #ffdad6; --md-sys-color-on-error-container: #93000a; --md-sys-color-background: #f4fbfa; --md-sys-color-on-background: #161d1d; --md-sys-color-surface: #f4fbfa; --md-sys-color-surface-dim: #d5dbda; --md-sys-color-surface-bright: #f4fbfa; --md-sys-color-surface-container-lowest: #ffffff; --md-sys-color-surface-container-low: #eff5f4; --md-sys-color-surface-container: #e9efee; --md-sys-color-surface-container-high: #e3e9e9; --md-sys-color-surface-container-highest: #dde4e3; --md-sys-color-on-surface: #161d1d; --md-sys-color-surface-variant: #dae5e4; --md-sys-color-on-surface-variant: #3f4948; --md-sys-color-inverse-surface: #2b3231; --md-sys-color-inverse-on-surface: #ecf2f1; --md-sys-color-outline: #6f7979; --md-sys-color-outline-variant: #bec9c8; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #006a6a; --md-sys-color-success: #0a6d36; --md-sys-color-on-success: #ffffff; --md-sys-color-success-container: #9ef6b0; --md-sys-color-on-success-container: #00210b; --md-sys-color-warning: #6f5d00; --md-sys-color-on-warning: #ffffff; --md-sys-color-warning-container: #ffe16a; --md-sys-color-on-warning-container: #221b00; }
:root[data-palette="teal"][data-mode="dark"] { --md-sys-color-primary: #80d5d4; --md-sys-color-on-primary: #003737; --md-sys-color-primary-container: #004f4f; --md-sys-color-on-primary-container: #9cf1f0; --md-sys-color-inverse-primary: #006a6a; --md-sys-color-secondary: #b0cccb; --md-sys-color-on-secondary: #1b3534; --md-sys-color-secondary-container: #324b4b; --md-sys-color-on-secondary-container: #cce8e7; --md-sys-color-tertiary: #b3c8e8; --md-sys-color-on-tertiary: #1c314b; --md-sys-color-tertiary-container: #334863; --md-sys-color-on-tertiary-container: #d3e4ff; --md-sys-color-error: #ffb4ab; --md-sys-color-on-error: #690005; --md-sys-color-error-container: #93000a; --md-sys-color-on-error-container: #ffdad6; --md-sys-color-background: #0e1514; --md-sys-color-on-background: #dde4e3; --md-sys-color-surface: #0e1514; --md-sys-color-surface-dim: #0e1514; --md-sys-color-surface-bright: #343a3a; --md-sys-color-surface-container-lowest: #090f0f; --md-sys-color-surface-container-low: #161d1d; --md-sys-color-surface-container: #1a2121; --md-sys-color-surface-container-high: #252b2b; --md-sys-color-surface-container-highest: #2f3636; --md-sys-color-on-surface: #dde4e3; --md-sys-color-surface-variant: #3f4948; --md-sys-color-on-surface-variant: #bec9c8; --md-sys-color-inverse-surface: #dde4e3; --md-sys-color-inverse-on-surface: #2b3231; --md-sys-color-outline: #889392; --md-sys-color-outline-variant: #3f4948; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #80d5d4; --md-sys-color-success: #82d996; --md-sys-color-on-success: #003918; --md-sys-color-success-container: #005226; --md-sys-color-on-success-container: #9ef6b0; --md-sys-color-warning: #e2c54a; --md-sys-color-on-warning: #3a3000; --md-sys-color-warning-container: #544600; --md-sys-color-on-warning-container: #ffe16a; }
@media (prefers-color-scheme: dark) { :root[data-palette="teal"]:not([data-mode="light"]) { --md-sys-color-primary: #80d5d4; --md-sys-color-on-primary: #003737; --md-sys-color-primary-container: #004f4f; --md-sys-color-on-primary-container: #9cf1f0; --md-sys-color-inverse-primary: #006a6a; --md-sys-color-secondary: #b0cccb; --md-sys-color-on-secondary: #1b3534; --md-sys-color-secondary-container: #324b4b; --md-sys-color-on-secondary-container: #cce8e7; --md-sys-color-tertiary: #b3c8e8; --md-sys-color-on-tertiary: #1c314b; --md-sys-color-tertiary-container: #334863; --md-sys-color-on-tertiary-container: #d3e4ff; --md-sys-color-error: #ffb4ab; --md-sys-color-on-error: #690005; --md-sys-color-error-container: #93000a; --md-sys-color-on-error-container: #ffdad6; --md-sys-color-background: #0e1514; --md-sys-color-on-background: #dde4e3; --md-sys-color-surface: #0e1514; --md-sys-color-surface-dim: #0e1514; --md-sys-color-surface-bright: #343a3a; --md-sys-color-surface-container-lowest: #090f0f; --md-sys-color-surface-container-low: #161d1d; --md-sys-color-surface-container: #1a2121; --md-sys-color-surface-container-high: #252b2b; --md-sys-color-surface-container-highest: #2f3636; --md-sys-color-on-surface: #dde4e3; --md-sys-color-surface-variant: #3f4948; --md-sys-color-on-surface-variant: #bec9c8; --md-sys-color-inverse-surface: #dde4e3; --md-sys-color-inverse-on-surface: #2b3231; --md-sys-color-outline: #889392; --md-sys-color-outline-variant: #3f4948; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #80d5d4; --md-sys-color-success: #82d996; --md-sys-color-on-success: #003918; --md-sys-color-success-container: #005226; --md-sys-color-on-success-container: #9ef6b0; --md-sys-color-warning: #e2c54a; --md-sys-color-on-warning: #3a3000; --md-sys-color-warning-container: #544600; --md-sys-color-on-warning-container: #ffe16a; } }
:root[data-palette="blue"][data-contrast="medium"] { --md-sys-color-primary: #003862; --md-sys-color-on-primary: #ffffff; --md-sys-color-primary-container: #46709e; --md-sys-color-on-primary-container: #ffffff; --md-sys-color-inverse-primary: #a0cafd; --md-sys-color-secondary: #2b3746; --md-sys-color-on-secondary: #ffffff; --md-sys-color-secondary-container: #626e7f; --md-sys-color-on-secondary-container: #ffffff; --md-sys-color-tertiary: #412f4e; --md-sys-color-on-tertiary: #ffffff; --md-sys-color-tertiary-container: #7a6587; --md-sys-color-on-tertiary-container: #ffffff; --md-sys-color-error: #740006; --md-sys-color-on-error: #ffffff; --md-sys-color-error-container: #cf2c27; --md-sys-color-on-error-container: #ffffff; --md-sys-color-background: #f8f9ff; --md-sys-color-on-background: #191c20; --md-sys-color-surface: #f8f9ff; --md-sys-color-surface-dim: #c4c6cc; --md-sys-color-surface-bright: #f8f9ff; --md-sys-color-surface-container-lowest: #ffffff; --md-sys-color-surface-container-low: #f2f3fa; --md-sys-color-surface-container: #e6e8ee; --md-sys-color-surface-container-high: #dbdde3; --md-sys-color-surface-container-highest: #d0d1d7; --md-sys-color-on-surface: #0e1116; --md-sys-color-surface-variant: #dfe2eb; --md-sys-color-on-surface-variant: #32363d; --md-sys-color-inverse-surface: #2e3135; --md-sys-color-inverse-on-surface: #eff0f7; --md-sys-color-outline: #4e535a; --md-sys-color-outline-variant: #696d75; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #36618e; --md-sys-color-success: #0a6d36; --md-sys-color-on-success: #ffffff; --md-sys-color-success-container: #9ef6b0; --md-sys-color-on-success-container: #00210b; --md-sys-color-warning: #6f5d00; --md-sys-color-on-warning: #ffffff; --md-sys-color-warning-container: #ffe16a; --md-sys-color-on-warning-container: #221b00; }
:root[data-palette="blue"][data-contrast="medium"][data-mode="dark"] { --md-sys-color-primary: #c7deff; --md-sys-color-on-primary: #002747; --md-sys-color-primary-container: #6b94c4; --md-sys-color-on-primary-container: #000000; --md-sys-color-inverse-primary: #1b4a76; --md-sys-color-secondary: #d0ddf1; --md-sys-color-on-secondary: #1a2635; --md-sys-color-secondary-container: #8592a4; --md-sys-color-on-secondary-container: #000000; --md-sys-color-tertiary: #edd3fb; --md-sys-color-on-tertiary: #301e3c; --md-sys-color-tertiary-container: #9f88ad; --md-sys-color-on-tertiary-container: #000000; --md-sys-color-error: #ffd2cc; --md-sys-color-on-error: #540003; --md-sys-color-error-container: #ff5449; --md-sys-color-on-error-container: #000000; --md-sys-color-background: #111418; --md-sys-color-on-background: #e1e2e8; --md-sys-color-surface: #111418; --md-sys-color-surface-dim: #111418; --md-sys-color-surface-bright: #42454a; --md-sys-color-surface-container-lowest: #05080b; --md-sys-color-surface-container-low: #1b1e22; --md-sys-color-surface-container: #25282d; --md-sys-color-surface-container-high: #303337; --md-sys-color-surface-container-highest: #3b3e43; --md-sys-color-on-surface: #ffffff; --md-sys-color-surface-variant: #43474e; --md-sys-color-on-surface-variant: #d9dce5; --md-sys-color-inverse-surface: #e1e2e8; --md-sys-color-inverse-on-surface: #272a2f; --md-sys-color-outline: #aeb2ba; --md-sys-color-outline-variant: #8c9098; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #a0cafd; --md-sys-color-success: #82d996; --md-sys-color-on-success: #003918; --md-sys-color-success-container: #005226; --md-sys-color-on-success-container: #9ef6b0; --md-sys-color-warning: #e2c54a; --md-sys-color-on-warning: #3a3000; --md-sys-color-warning-container: #544600; --md-sys-color-on-warning-container: #ffe16a; }
@media (prefers-color-scheme: dark) { :root[data-palette="blue"][data-contrast="medium"]:not([data-mode="light"]) { --md-sys-color-primary: #c7deff; --md-sys-color-on-primary: #002747; --md-sys-color-primary-container: #6b94c4; --md-sys-color-on-primary-container: #000000; --md-sys-color-inverse-primary: #1b4a76; --md-sys-color-secondary: #d0ddf1; --md-sys-color-on-secondary: #1a2635; --md-sys-color-secondary-container: #8592a4; --md-sys-color-on-secondary-container: #000000; --md-sys-color-tertiary: #edd3fb; --md-sys-color-on-tertiary: #301e3c; --md-sys-color-tertiary-container: #9f88ad; --md-sys-color-on-tertiary-container: #000000; --md-sys-color-error: #ffd2cc; --md-sys-color-on-error: #540003; --md-sys-color-error-container: #ff5449; --md-sys-color-on-error-container: #000000; --md-sys-color-background: #111418; --md-sys-color-on-background: #e1e2e8; --md-sys-color-surface: #111418; --md-sys-color-surface-dim: #111418; --md-sys-color-surface-bright: #42454a; --md-sys-color-surface-container-lowest: #05080b; --md-sys-color-surface-container-low: #1b1e22; --md-sys-color-surface-container: #25282d; --md-sys-color-surface-container-high: #303337; --md-sys-color-surface-container-highest: #3b3e43; --md-sys-color-on-surface: #ffffff; --md-sys-color-surface-variant: #43474e; --md-sys-color-on-surface-variant: #d9dce5; --md-sys-color-inverse-surface: #e1e2e8; --md-sys-color-inverse-on-surface: #272a2f; --md-sys-color-outline: #aeb2ba; --md-sys-color-outline-variant: #8c9098; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #a0cafd; --md-sys-color-success: #82d996; --md-sys-color-on-success: #003918; --md-sys-color-success-container: #005226; --md-sys-color-on-success-container: #9ef6b0; --md-sys-color-warning: #e2c54a; --md-sys-color-on-warning: #3a3000; --md-sys-color-warning-container: #544600; --md-sys-color-on-warning-container: #ffe16a; } }
:root[data-palette="blue"][data-contrast="high"] { --md-sys-color-primary: #002e51; --md-sys-color-on-primary: #ffffff; --md-sys-color-primary-container: #1d4b77; --md-sys-color-on-primary-container: #ffffff; --md-sys-color-inverse-primary: #a0cafd; --md-sys-color-secondary: #212d3c; --md-sys-color-on-secondary: #ffffff; --md-sys-color-secondary-container: #3e4a5a; --md-sys-color-on-secondary-container: #ffffff; --md-sys-color-tertiary: #362543; --md-sys-color-on-tertiary: #ffffff; --md-sys-color-tertiary-container: #554262; --md-sys-color-on-tertiary-container: #ffffff; --md-sys-color-error: #600004; --md-sys-color-on-error: #ffffff; --md-sys-color-error-container: #98000a; --md-sys-color-on-error-container: #ffffff; --md-sys-color-background: #f8f9ff; --md-sys-color-on-background: #191c20; --md-sys-color-surface: #f8f9ff; --md-sys-color-surface-dim: #b7b9be; --md-sys-color-surface-bright: #f8f9ff; --md-sys-color-surface-container-lowest: #ffffff; --md-sys-color-surface-container-low: #eff0f7; --md-sys-color-surface-container: #e1e2e8; --md-sys-color-surface-container-high: #d3d4da; --md-sys-color-surface-container-highest: #c4c6cc; --md-sys-color-on-surface: #000000; --md-sys-color-surface-variant: #dfe2eb; --md-sys-color-on-surface-variant: #000000; --md-sys-color-inverse-surface: #2e3135; --md-sys-color-inverse-on-surface: #ffffff; --md-sys-color-outline: #282c33; --md-sys-color-outline-variant: #454950; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #36618e; --md-sys-color-success: #0a6d36; --md-sys-color-on-success: #ffffff; --md-sys-color-success-container: #9ef6b0; --md-sys-color-on-success-container: #00210b; --md-sys-color-warning: #6f5d00; --md-sys-color-on-warning: #ffffff; --md-sys-color-warning-container: #ffe16a; --md-sys-color-on-warning-container: #221b00; }
:root[data-palette="blue"][data-contrast="high"][data-mode="dark"] { --md-sys-color-primary: #e8f0ff; --md-sys-color-on-primary: #000000; --md-sys-color-primary-container: #9dc6f9; --md-sys-color-on-primary-container: #000c1b; --md-sys-color-inverse-primary: #1b4a76; --md-sys-color-secondary: #e8f0ff; --md-sys-color-on-secondary: #000000; --md-sys-color-secondary-container: #b7c4d7; --md-sys-color-on-secondary-container: #010c1a; --md-sys-color-tertiary: #fbebff; --md-sys-color-on-tertiary: #000000; --md-sys-color-tertiary-container: #d3bae0; --md-sys-color-on-tertiary-container: #140420; --md-sys-color-error: #ffece9; --md-sys-color-on-error: #000000; --md-sys-color-error-container: #ffaea4; --md-sys-color-on-error-container: #220001; --md-sys-color-background: #111418; --md-sys-color-on-background: #e1e2e8; --md-sys-color-surface: #111418; --md-sys-color-surface-dim: #111418; --md-sys-color-surface-bright: #4d5055; --md-sys-color-surface-container-lowest: #000000; --md-sys-color-surface-container-low: #1d2024; --md-sys-color-surface-container: #2e3135; --md-sys-color-surface-container-high: #393c40; --md-sys-color-surface-container-highest: #44474c; --md-sys-color-on-surface: #ffffff; --md-sys-color-surface-variant: #43474e; --md-sys-color-on-surface-variant: #ffffff; --md-sys-color-inverse-surface: #e1e2e8; --md-sys-color-inverse-on-surface: #000000; --md-sys-color-outline: #ecf0f9; --md-sys-color-outline-variant: #bfc3cb; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #a0cafd; --md-sys-color-success: #82d996; --md-sys-color-on-success: #003918; --md-sys-color-success-container: #005226; --md-sys-color-on-success-container: #9ef6b0; --md-sys-color-warning: #e2c54a; --md-sys-color-on-warning: #3a3000; --md-sys-color-warning-container: #544600; --md-sys-color-on-warning-container: #ffe16a; }
@media (prefers-color-scheme: dark) { :root[data-palette="blue"][data-contrast="high"]:not([data-mode="light"]) { --md-sys-color-primary: #e8f0ff; --md-sys-color-on-primary: #000000; --md-sys-color-primary-container: #9dc6f9; --md-sys-color-on-primary-container: #000c1b; --md-sys-color-inverse-primary: #1b4a76; --md-sys-color-secondary: #e8f0ff; --md-sys-color-on-secondary: #000000; --md-sys-color-secondary-container: #b7c4d7; --md-sys-color-on-secondary-container: #010c1a; --md-sys-color-tertiary: #fbebff; --md-sys-color-on-tertiary: #000000; --md-sys-color-tertiary-container: #d3bae0; --md-sys-color-on-tertiary-container: #140420; --md-sys-color-error: #ffece9; --md-sys-color-on-error: #000000; --md-sys-color-error-container: #ffaea4; --md-sys-color-on-error-container: #220001; --md-sys-color-background: #111418; --md-sys-color-on-background: #e1e2e8; --md-sys-color-surface: #111418; --md-sys-color-surface-dim: #111418; --md-sys-color-surface-bright: #4d5055; --md-sys-color-surface-container-lowest: #000000; --md-sys-color-surface-container-low: #1d2024; --md-sys-color-surface-container: #2e3135; --md-sys-color-surface-container-high: #393c40; --md-sys-color-surface-container-highest: #44474c; --md-sys-color-on-surface: #ffffff; --md-sys-color-surface-variant: #43474e; --md-sys-color-on-surface-variant: #ffffff; --md-sys-color-inverse-surface: #e1e2e8; --md-sys-color-inverse-on-surface: #000000; --md-sys-color-outline: #ecf0f9; --md-sys-color-outline-variant: #bfc3cb; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #a0cafd; --md-sys-color-success: #82d996; --md-sys-color-on-success: #003918; --md-sys-color-success-container: #005226; --md-sys-color-on-success-container: #9ef6b0; --md-sys-color-warning: #e2c54a; --md-sys-color-on-warning: #3a3000; --md-sys-color-warning-container: #544600; --md-sys-color-on-warning-container: #ffe16a; } }
/* green (seed #386A20) */
:root[data-palette="green"] { --md-sys-color-primary: #446732; --md-sys-color-on-primary: #ffffff; --md-sys-color-primary-container: #c5efab; --md-sys-color-on-primary-container: #2d4f1c; --md-sys-color-inverse-primary: #a9d291; --md-sys-color-secondary: #55624c; --md-sys-color-on-secondary: #ffffff; --md-sys-color-secondary-container: #d8e7cb; --md-sys-color-on-secondary-container: #3e4a35; --md-sys-color-tertiary: #386666; --md-sys-color-on-tertiary: #ffffff; --md-sys-color-tertiary-container: #bbebec; --md-sys-color-on-tertiary-container: #1e4e4f; --md-sys-color-error: #ba1a1a; --md-sys-color-on-error: #ffffff; --md-sys-color-error-container: #ffdad6; --md-sys-color-on-error-container: #93000a; --md-sys-color-background: #f8faf0; --md-sys-color-on-background: #191d16; --md-sys-color-surface: #f8faf0; --md-sys-color-surface-dim: #d9dbd1; --md-sys-color-surface-bright: #f8faf0; --md-sys-color-surface-container-lowest: #ffffff; --md-sys-color-surface-container-low: #f2f5ea; --md-sys-color-surface-container: #edefe5; --md-sys-color-surface-container-high: #e7e9df; --md-sys-color-surface-container-highest: #e1e4d9; --md-sys-color-on-surface: #191d16; --md-sys-color-surface-variant: #dfe4d7; --md-sys-color-on-surface-variant: #43483e; --md-sys-color-inverse-surface: #2e312b; --md-sys-color-inverse-on-surface: #f0f2e7; --md-sys-color-outline: #74796d; --md-sys-color-outline-variant: #c3c8bb; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #446732; --md-sys-color-success: #386a1f; --md-sys-color-on-success: #ffffff; --md-sys-color-success-container: #b8f397; --md-sys-color-on-success-container: #072100; --md-sys-color-warning: #6f5d00; --md-sys-color-on-warning: #ffffff; --md-sys-color-warning-container: #ffe16a; --md-sys-color-on-warning-container: #221b00; }
:root[data-palette="green"][data-mode="dark"] { --md-sys-color-primary: #a9d291; --md-sys-color-on-primary: #173807; --md-sys-color-primary-container: #2d4f1c; --md-sys-color-on-primary-container: #c5efab; --md-sys-color-inverse-primary: #446732; --md-sys-color-secondary: #bdcbb0; --md-sys-color-on-secondary: #283420; --md-sys-color-secondary-container: #3e4a35; --md-sys-color-on-secondary-container: #d8e7cb; --md-sys-color-tertiary: #a0cfd0; --md-sys-color-on-tertiary: #003738; --md-sys-color-tertiary-container: #1e4e4f; --md-sys-color-on-tertiary-container: #bbebec; --md-sys-color-error: #ffb4ab; --md-sys-color-on-error: #690005; --md-sys-color-error-container: #93000a; --md-sys-color-on-error-container: #ffdad6; --md-sys-color-background: #11140f; --md-sys-color-on-background: #e1e4d9; --md-sys-color-surface: #11140f; --md-sys-color-surface-dim: #11140f; --md-sys-color-surface-bright: #373a33; --md-sys-color-surface-container-lowest: #0c0f0a; --md-sys-color-surface-container-low: #191d16; --md-sys-color-surface-container: #1d211a; --md-sys-color-surface-container-high: #282b24; --md-sys-color-surface-container-highest: #32362f; --md-sys-color-on-surface: #e1e4d9; --md-sys-color-surface-variant: #43483e; --md-sys-color-on-surface-variant: #c3c8bb; --md-sys-color-inverse-surface: #e1e4d9; --md-sys-color-inverse-on-surface: #2e312b; --md-sys-color-outline: #8d9286; --md-sys-color-outline-variant: #43483e; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #a9d291; --md-sys-color-success: #9cd67e; --md-sys-color-on-success: #113800; --md-sys-color-success-container: #205107; --md-sys-color-on-success-container: #b8f397; --md-sys-color-warning: #e2c54a; --md-sys-color-on-warning: #3a3000; --md-sys-color-warning-container: #544600; --md-sys-color-on-warning-container: #ffe16a; }
@media (prefers-color-scheme: dark) { :root[data-palette="green"]:not([data-mode="light"]) { --md-sys-color-primary: #a9d291; --md-sys-color-on-primary: #173807; --md-sys-color-primary-container: #2d4f1c; --md-sys-color-on-primary-container: #c5efab; --md-sys-color-inverse-primary: #446732; --md-sys-color-secondary: #bdcbb0; --md-sys-color-on-secondary: #283420; --md-sys-color-secondary-container: #3e4a35; --md-sys-color-on-secondary-container: #d8e7cb; --md-sys-color-tertiary: #a0cfd0; --md-sys-color-on-tertiary: #003738; --md-sys-color-tertiary-container: #1e4e4f; --md-sys-color-on-tertiary-container: #bbebec; --md-sys-color-error: #ffb4ab; --md-sys-color-on-error: #690005; --md-sys-color-error-container: #93000a; --md-sys-color-on-error-container: #ffdad6; --md-sys-color-background: #11140f; --md-sys-color-on-background: #e1e4d9; --md-sys-color-surface: #11140f; --md-sys-color-surface-dim: #11140f; --md-sys-color-surface-bright: #373a33; --md-sys-color-surface-container-lowest: #0c0f0a; --md-sys-color-surface-container-low: #191d16; --md-sys-color-surface-container: #1d211a; --md-sys-color-surface-container-high: #282b24; --md-sys-color-surface-container-highest: #32362f; --md-sys-color-on-surface: #e1e4d9; --md-sys-color-surface-variant: #43483e; --md-sys-color-on-surface-variant: #c3c8bb; --md-sys-color-inverse-surface: #e1e4d9; --md-sys-color-inverse-on-surface: #2e312b; --md-sys-color-outline: #8d9286; --md-sys-color-outline-variant: #43483e; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #a9d291; --md-sys-color-success: #9cd67e; --md-sys-color-on-success: #113800; --md-sys-color-success-container: #205107; --md-sys-color-on-success-container: #b8f397; --md-sys-color-warning: #e2c54a; --md-sys-color-on-warning: #3a3000; --md-sys-color-warning-container: #544600; --md-sys-color-on-warning-container: #ffe16a; } }
:root[data-palette="green"][data-contrast="medium"] { --md-sys-color-primary: #1d3e0d; --md-sys-color-on-primary: #ffffff; --md-sys-color-primary-container: #52773f; --md-sys-color-on-primary-container: #ffffff; --md-sys-color-inverse-primary: #a9d291; --md-sys-color-secondary: #2d3a26; --md-sys-color-on-secondary: #ffffff; --md-sys-color-secondary-container: #64715a; --md-sys-color-on-secondary-container: #ffffff; --md-sys-color-tertiary: #073d3e; --md-sys-color-on-tertiary: #ffffff; --md-sys-color-tertiary-container: #477575; --md-sys-color-on-tertiary-container: #ffffff; --md-sys-color-error: #740006; --md-sys-color-on-error: #ffffff; --md-sys-color-error-container: #cf2c27; --md-sys-color-on-error-container: #ffffff; --md-sys-color-background: #f8faf0; --md-sys-color-on-background: #191d16; --md-sys-color-surface: #f8faf0; --md-sys-color-surface-dim: #c5c8be; --md-sys-color-surface-bright: #f8faf0; --md-sys-color-surface-container-lowest: #ffffff; --md-sys-color-surface-container-low: #f2f5ea; --md-sys-color-surface-container: #e7e9df; --md-sys-color-surface-container-high: #dbded4; --md-sys-color-surface-container-highest: #d0d3c9; --md-sys-color-on-surface: #0f120c; --md-sys-color-surface-variant: #dfe4d7; --md-sys-color-on-surface-variant: #33382e; --md-sys-color-inverse-surface: #2e312b; --md-sys-color-inverse-on-surface: #f0f2e7; --md-sys-color-outline: #4f544a; --md-sys-color-outline-variant: #6a6f64; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #446732; --md-sys-color-success: #386a1f; --md-sys-color-on-success: #ffffff; --md-sys-color-success-container: #b8f397; --md-sys-color-on-success-container: #072100; --md-sys-color-warning: #6f5d00; --md-sys-color-on-warning: #ffffff; --md-sys-color-warning-container: #ffe16a; --md-sys-color-on-warning-container: #221b00; }
:root[data-palette="green"][data-contrast="medium"][data-mode="dark"] { --md-sys-color-primary: #bfe8a5; --md-sys-color-on-primary: #0b2c00; --md-sys-color-primary-container: #759b60; --md-sys-color-on-primary-container: #000000; --md-sys-color-inverse-primary: #2e501e; --md-sys-color-secondary: #d2e1c5; --md-sys-color-on-secondary: #1d2916; --md-sys-color-secondary-container: #87957c; --md-sys-color-on-secondary-container: #000000; --md-sys-color-tertiary: #b5e5e6; --md-sys-color-on-tertiary: #002b2c; --md-sys-color-tertiary-container: #6b9999; --md-sys-color-on-tertiary-container: #000000; --md-sys-color-error: #ffd2cc; --md-sys-color-on-error: #540003; --md-sys-color-error-container: #ff5449; --md-sys-color-on-error-container: #000000; --md-sys-color-background: #11140f; --md-sys-color-on-background: #e1e4d9; --md-sys-color-surface: #11140f; --md-sys-color-surface-dim: #11140f; --md-sys-color-surface-bright: #42463e; --md-sys-color-surface-container-lowest: #060804; --md-sys-color-surface-container-low: #1b1f18; --md-sys-color-surface-container: #252922; --md-sys-color-surface-container-high: #30342d; --md-sys-color-surface-container-highest: #3b3f38; --md-sys-color-on-surface: #ffffff; --md-sys-color-surface-variant: #43483e; --md-sys-color-on-surface-variant: #d9ded1; --md-sys-color-inverse-surface: #e1e4d9; --md-sys-color-inverse-on-surface: #282b24; --md-sys-color-outline: #afb4a7; --md-sys-color-outline-variant: #8d9286; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #a9d291; --md-sys-color-success: #9cd67e; --md-sys-color-on-success: #113800; --md-sys-color-success-container: #205107; --md-sys-color-on-success-container: #b8f397; --md-sys-color-warning: #e2c54a; --md-sys-color-on-warning: #3a3000; --md-sys-color-warning-container: #544600; --md-sys-color-on-warning-container: #ffe16a; }
@media (prefers-color-scheme: dark) { :root[data-palette="green"][data-contrast="medium"]:not([data-mode="light"]) { --md-sys-color-primary: #bfe8a5; --md-sys-color-on-primary: #0b2c00; --md-sys-color-primary-container: #759b60; --md-sys-color-on-primary-container: #000000; --md-sys-color-inverse-primary: #2e501e; --md-sys-color-secondary: #d2e1c5; --md-sys-color-on-secondary: #1d2916; --md-sys-color-secondary-container: #87957c; --md-sys-color-on-secondary-container: #000000; --md-sys-color-tertiary: #b5e5e6; --md-sys-color-on-tertiary: #002b2c; --md-sys-color-tertiary-container: #6b9999; --md-sys-color-on-tertiary-container: #000000; --md-sys-color-error: #ffd2cc; --md-sys-color-on-error: #540003; --md-sys-color-error-container: #ff5449; --md-sys-color-on-error-container: #000000; --md-sys-color-background: #11140f; --md-sys-color-on-background: #e1e4d9; --md-sys-color-surface: #11140f; --md-sys-color-surface-dim: #11140f; --md-sys-color-surface-bright: #42463e; --md-sys-color-surface-container-lowest: #060804; --md-sys-color-surface-container-low: #1b1f18; --md-sys-color-surface-container: #252922; --md-sys-color-surface-container-high: #30342d; --md-sys-color-surface-container-highest: #3b3f38; --md-sys-color-on-surface: #ffffff; --md-sys-color-surface-variant: #43483e; --md-sys-color-on-surface-variant: #d9ded1; --md-sys-color-inverse-surface: #e1e4d9; --md-sys-color-inverse-on-surface: #282b24; --md-sys-color-outline: #afb4a7; --md-sys-color-outline-variant: #8d9286; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #a9d291; --md-sys-color-success: #9cd67e; --md-sys-color-on-success: #113800; --md-sys-color-success-container: #205107; --md-sys-color-on-success-container: #b8f397; --md-sys-color-warning: #e2c54a; --md-sys-color-on-warning: #3a3000; --md-sys-color-warning-container: #544600; --md-sys-color-on-warning-container: #ffe16a; } }
:root[data-palette="green"][data-contrast="high"] { --md-sys-color-primary: #123304; --md-sys-color-on-primary: #ffffff; --md-sys-color-primary-container: #2f511f; --md-sys-color-on-primary-container: #ffffff; --md-sys-color-inverse-primary: #a9d291; --md-sys-color-secondary: #232f1c; --md-sys-color-on-secondary: #ffffff; --md-sys-color-secondary-container: #404d38; --md-sys-color-on-secondary-container: #ffffff; --md-sys-color-tertiary: #003233; --md-sys-color-on-tertiary: #ffffff; --md-sys-color-tertiary-container: #215051; --md-sys-color-on-tertiary-container: #ffffff; --md-sys-color-error: #600004; --md-sys-color-on-error: #ffffff; --md-sys-color-error-container: #98000a; --md-sys-color-on-error-container: #ffffff; --md-sys-color-background: #f8faf0; --md-sys-color-on-background: #191d16; --md-sys-color-surface: #f8faf0; --md-sys-color-surface-dim: #b7bab0; --md-sys-color-surface-bright: #f8faf0; --md-sys-color-surface-container-lowest: #ffffff; --md-sys-color-surface-container-low: #f0f2e7; --md-sys-color-surface-container: #e1e4d9; --md-sys-color-surface-container-high: #d3d5cb; --md-sys-color-surface-container-highest: #c5c8be; --md-sys-color-on-surface: #000000; --md-sys-color-surface-variant: #dfe4d7; --md-sys-color-on-surface-variant: #000000; --md-sys-color-inverse-surface: #2e312b; --md-sys-color-inverse-on-surface: #ffffff; --md-sys-color-outline: #292e25; --md-sys-color-outline-variant: #464b41; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #446732; --md-sys-color-success: #386a1f; --md-sys-color-on-success: #ffffff; --md-sys-color-success-container: #b8f397; --md-sys-color-on-success-container: #072100; --md-sys-color-warning: #6f5d00; --md-sys-color-on-warning: #ffffff; --md-sys-color-warning-container: #ffe16a; --md-sys-color-on-warning-container: #221b00; }
:root[data-palette="green"][data-contrast="high"][data-mode="dark"] { --md-sys-color-primary: #d2fcb8; --md-sys-color-on-primary: #000000; --md-sys-color-primary-container: #a6ce8e; --md-sys-color-on-primary-container: #020e00; --md-sys-color-inverse-primary: #2e501e; --md-sys-color-secondary: #e6f5d8; --md-sys-color-on-secondary: #000000; --md-sys-color-secondary-container: #b9c7ac; --md-sys-color-on-secondary-container: #040e02; --md-sys-color-tertiary: #c9f9f9; --md-sys-color-on-tertiary: #000000; --md-sys-color-tertiary-container: #9ccbcc; --md-sys-color-on-tertiary-container: #000e0e; --md-sys-color-error: #ffece9; --md-sys-color-on-error: #000000; --md-sys-color-error-container: #ffaea4; --md-sys-color-on-error-container: #220001; --md-sys-color-background: #11140f; --md-sys-color-on-background: #e1e4d9; --md-sys-color-surface: #11140f; --md-sys-color-surface-dim: #11140f; --md-sys-color-surface-bright: #4e514a; --md-sys-color-surface-container-lowest: #000000; --md-sys-color-surface-container-low: #1d211a; --md-sys-color-surface-container: #2e312b; --md-sys-color-surface-container-high: #393c35; --md-sys-color-surface-container-highest: #444840; --md-sys-color-on-surface: #ffffff; --md-sys-color-surface-variant: #43483e; --md-sys-color-on-surface-variant: #ffffff; --md-sys-color-inverse-surface: #e1e4d9; --md-sys-color-inverse-on-surface: #000000; --md-sys-color-outline: #edf2e4; --md-sys-color-outline-variant: #bfc4b7; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #a9d291; --md-sys-color-success: #9cd67e; --md-sys-color-on-success: #113800; --md-sys-color-success-container: #205107; --md-sys-color-on-success-container: #b8f397; --md-sys-color-warning: #e2c54a; --md-sys-color-on-warning: #3a3000; --md-sys-color-warning-container: #544600; --md-sys-color-on-warning-container: #ffe16a; }
@media (prefers-color-scheme: dark) { :root[data-palette="green"][data-contrast="high"]:not([data-mode="light"]) { --md-sys-color-primary: #d2fcb8; --md-sys-color-on-primary: #000000; --md-sys-color-primary-container: #a6ce8e; --md-sys-color-on-primary-container: #020e00; --md-sys-color-inverse-primary: #2e501e; --md-sys-color-secondary: #e6f5d8; --md-sys-color-on-secondary: #000000; --md-sys-color-secondary-container: #b9c7ac; --md-sys-color-on-secondary-container: #040e02; --md-sys-color-tertiary: #c9f9f9; --md-sys-color-on-tertiary: #000000; --md-sys-color-tertiary-container: #9ccbcc; --md-sys-color-on-tertiary-container: #000e0e; --md-sys-color-error: #ffece9; --md-sys-color-on-error: #000000; --md-sys-color-error-container: #ffaea4; --md-sys-color-on-error-container: #220001; --md-sys-color-background: #11140f; --md-sys-color-on-background: #e1e4d9; --md-sys-color-surface: #11140f; --md-sys-color-surface-dim: #11140f; --md-sys-color-surface-bright: #4e514a; --md-sys-color-surface-container-lowest: #000000; --md-sys-color-surface-container-low: #1d211a; --md-sys-color-surface-container: #2e312b; --md-sys-color-surface-container-high: #393c35; --md-sys-color-surface-container-highest: #444840; --md-sys-color-on-surface: #ffffff; --md-sys-color-surface-variant: #43483e; --md-sys-color-on-surface-variant: #ffffff; --md-sys-color-inverse-surface: #e1e4d9; --md-sys-color-inverse-on-surface: #000000; --md-sys-color-outline: #edf2e4; --md-sys-color-outline-variant: #bfc4b7; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #a9d291; --md-sys-color-success: #9cd67e; --md-sys-color-on-success: #113800; --md-sys-color-success-container: #205107; --md-sys-color-on-success-container: #b8f397; --md-sys-color-warning: #e2c54a; --md-sys-color-on-warning: #3a3000; --md-sys-color-warning-container: #544600; --md-sys-color-on-warning-container: #ffe16a; } }
/* orange (seed #8B5000) */
:root[data-palette="orange"] { --md-sys-color-primary: #865319; --md-sys-color-on-primary: #ffffff; --md-sys-color-primary-container: #ffdcbe; --md-sys-color-on-primary-container: #6a3c01; --md-sys-color-inverse-primary: #fdb975; --md-sys-color-secondary: #735a42; --md-sys-color-on-secondary: #ffffff; --md-sys-color-secondary-container: #ffdcbe; --md-sys-color-on-secondary-container: #59422d; --md-sys-color-tertiary: #586339; --md-sys-color-on-tertiary: #ffffff; --md-sys-color-tertiary-container: #dce8b4; --md-sys-color-on-tertiary-container: #414b24; --md-sys-color-error: #ba1a1a; --md-sys-color-on-error: #ffffff; --md-sys-color-error-container: #ffdad6; --md-sys-color-on-error-container: #93000a; --md-sys-color-background: #fff8f5; --md-sys-color-on-background: #211a14; --md-sys-color-surface: #fff8f5; --md-sys-color-surface-dim: #e6d7cd; --md-sys-color-surface-bright: #fff8f5; --md-sys-color-surface-container-lowest: #ffffff; --md-sys-color-surface-container-low: #fff1e7; --md-sys-color-surface-container: #faebe0; --md-sys-color-surface-container-high: #f4e5db; --md-sys-color-surface-container-highest: #efe0d5; --md-sys-color-on-surface: #211a14; --md-sys-color-surface-variant: #f2dfd1; --md-sys-color-on-surface-variant: #51453a; --md-sys-color-inverse-surface: #372f28; --md-sys-color-inverse-on-surface: #fdeee3; --md-sys-color-outline: #837468; --md-sys-color-outline-variant: #d5c3b5; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #865319; --md-sys-color-success: #516600; --md-sys-color-on-success: #ffffff; --md-sys-color-success-container: #d3ee80; --md-sys-color-on-success-container: #161e00; --md-sys-color-warning: #835500; --md-sys-color-on-warning: #ffffff; --md-sys-color-warning-container: #ffddb4; --md-sys-color-on-warning-container: #291800; }
:root[data-palette="orange"][data-mode="dark"] { --md-sys-color-primary: #fdb975; --md-sys-color-on-primary: #4a2800; --md-sys-color-primary-container: #6a3c01; --md-sys-color-on-primary-container: #ffdcbe; --md-sys-color-inverse-primary: #865319; --md-sys-color-secondary: #e1c1a4; --md-sys-color-on-secondary: #402c18; --md-sys-color-secondary-container: #59422d; --md-sys-color-on-secondary-container: #ffdcbe; --md-sys-color-tertiary: #c0cc9a; --md-sys-color-on-tertiary: #2b3410; --md-sys-color-tertiary-container: #414b24; --md-sys-color-on-tertiary-container: #dce8b4; --md-sys-color-error: #ffb4ab; --md-sys-color-on-error: #690005; --md-sys-color-error-container: #93000a; --md-sys-color-on-error-container: #ffdad6; --md-sys-color-background: #19120c; --md-sys-color-on-background: #efe0d5; --md-sys-color-surface: #19120c; --md-sys-color-surface-dim: #19120c; --md-sys-color-surface-bright: #403830; --md-sys-color-surface-container-lowest: #130d07; --md-sys-color-surface-container-low: #211a14; --md-sys-color-surface-container: #261e18; --md-sys-color-surface-container-high: #302822; --md-sys-color-surface-container-highest: #3c332c; --md-sys-color-on-surface: #efe0d5; --md-sys-color-surface-variant: #51453a; --md-sys-color-on-surface-variant: #d5c3b5; --md-sys-color-inverse-surface: #efe0d5; --md-sys-color-inverse-on-surface: #372f28; --md-sys-color-outline: #9e8e81; --md-sys-color-outline-variant: #51453a; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #fdb975; --md-sys-color-success: #b7d167; --md-sys-color-on-success: #293500; --md-sys-color-success-container: #3c4d00; --md-sys-color-on-success-container: #d3ee80; --md-sys-color-warning: #ffb955; --md-sys-color-on-warning: #452b00; --md-sys-color-warning-container: #633f00; --md-sys-color-on-warning-container: #ffddb4; }
@media (prefers-color-scheme: dark) { :root[data-palette="orange"]:not([data-mode="light"]) { --md-sys-color-primary: #fdb975; --md-sys-color-on-primary: #4a2800; --md-sys-color-primary-container: #6a3c01; --md-sys-color-on-primary-container: #ffdcbe; --md-sys-color-inverse-primary: #865319; --md-sys-color-secondary: #e1c1a4; --md-sys-color-on-secondary: #402c18; --md-sys-color-secondary-container: #59422d; --md-sys-color-on-secondary-container: #ffdcbe; --md-sys-color-tertiary: #c0cc9a; --md-sys-color-on-tertiary: #2b3410; --md-sys-color-tertiary-container: #414b24; --md-sys-color-on-tertiary-container: #dce8b4; --md-sys-color-error: #ffb4ab; --md-sys-color-on-error: #690005; --md-sys-color-error-container: #93000a; --md-sys-color-on-error-container: #ffdad6; --md-sys-color-background: #19120c; --md-sys-color-on-background: #efe0d5; --md-sys-color-surface: #19120c; --md-sys-color-surface-dim: #19120c; --md-sys-color-surface-bright: #403830; --md-sys-color-surface-container-lowest: #130d07; --md-sys-color-surface-container-low: #211a14; --md-sys-color-surface-container: #261e18; --md-sys-color-surface-container-high: #302822; --md-sys-color-surface-container-highest: #3c332c; --md-sys-color-on-surface: #efe0d5; --md-sys-color-surface-variant: #51453a; --md-sys-color-on-surface-variant: #d5c3b5; --md-sys-color-inverse-surface: #efe0d5; --md-sys-color-inverse-on-surface: #372f28; --md-sys-color-outline: #9e8e81; --md-sys-color-outline-variant: #51453a; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #fdb975; --md-sys-color-success: #b7d167; --md-sys-color-on-success: #293500; --md-sys-color-success-container: #3c4d00; --md-sys-color-on-success-container: #d3ee80; --md-sys-color-warning: #ffb955; --md-sys-color-on-warning: #452b00; --md-sys-color-warning-container: #633f00; --md-sys-color-on-warning-container: #ffddb4; } }
:root[data-palette="orange"][data-contrast="medium"] { --md-sys-color-primary: #522d00; --md-sys-color-on-primary: #ffffff; --md-sys-color-primary-container: #976126; --md-sys-color-on-primary-container: #ffffff; --md-sys-color-inverse-primary: #fdb975; --md-sys-color-secondary: #47321d; --md-sys-color-on-secondary: #ffffff; --md-sys-color-secondary-container: #826850; --md-sys-color-on-secondary-container: #ffffff; --md-sys-color-tertiary: #313a15; --md-sys-color-on-tertiary: #ffffff; --md-sys-color-tertiary-container: #677247; --md-sys-color-on-tertiary-container: #ffffff; --md-sys-color-error: #740006; --md-sys-color-on-error: #ffffff; --md-sys-color-error-container: #cf2c27; --md-sys-color-on-error-container: #ffffff; --md-sys-color-background: #fff8f5; --md-sys-color-on-background: #211a14; --md-sys-color-surface: #fff8f5; --md-sys-color-surface-dim: #d2c4ba; --md-sys-color-surface-bright: #fff8f5; --md-sys-color-surface-container-lowest: #ffffff; --md-sys-color-surface-container-low: #fff1e7; --md-sys-color-surface-container: #f4e5db; --md-sys-color-surface-container-high: #e9dad0; --md-sys-color-surface-container-highest: #ddcfc5; --md-sys-color-on-surface: #16100a; --md-sys-color-surface-variant: #f2dfd1; --md-sys-color-on-surface-variant: #3f342a; --md-sys-color-inverse-surface: #372f28; --md-sys-color-inverse-on-surface: #fdeee3; --md-sys-color-outline: #5d5045; --md-sys-color-outline-variant: #796a5f; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #865319; --md-sys-color-success: #516600; --md-sys-color-on-success: #ffffff; --md-sys-color-success-container: #d3ee80; --md-sys-color-on-success-container: #161e00; --md-sys-color-warning: #835500; --md-sys-color-on-warning: #ffffff; --md-sys-color-warning-container: #ffddb4; --md-sys-color-on-warning-container: #291800; }
:root[data-palette="orange"][data-contrast="medium"][data-mode="dark"] { --md-sys-color-primary: #ffd5af; --md-sys-color-on-primary: #3b1f00; --md-sys-color-primary-container: #c08446; --md-sys-color-on-primary-container: #000000; --md-sys-color-inverse-primary: #6b3d02; --md-sys-color-secondary: #f8d6b9; --md-sys-color-on-secondary: #34220e; --md-sys-color-secondary-container: #a88b71; --md-sys-color-on-secondary-container: #000000; --md-sys-color-tertiary: #d6e2ae; --md-sys-color-on-tertiary: #202906; --md-sys-color-tertiary-container: #8b9668; --md-sys-color-on-tertiary-container: #000000; --md-sys-color-error: #ffd2cc; --md-sys-color-on-error: #540003; --md-sys-color-error-container: #ff5449; --md-sys-color-on-error-container: #000000; --md-sys-color-background: #19120c; --md-sys-color-on-background: #efe0d5; --md-sys-color-surface: #19120c; --md-sys-color-surface-dim: #19120c; --md-sys-color-surface-bright: #4c433b; --md-sys-color-surface-container-lowest: #0c0603; --md-sys-color-surface-container-low: #241c16; --md-sys-color-surface-container: #2e2620; --md-sys-color-surface-container-high: #39312a; --md-sys-color-surface-container-highest: #453c34; --md-sys-color-on-surface: #ffffff; --md-sys-color-surface-variant: #51453a; --md-sys-color-on-surface-variant: #ecd9cb; --md-sys-color-inverse-surface: #efe0d5; --md-sys-color-inverse-on-surface: #312922; --md-sys-color-outline: #c0afa1; --md-sys-color-outline-variant: #9d8d81; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #fdb975; --md-sys-color-success: #b7d167; --md-sys-color-on-success: #293500; --md-sys-color-success-container: #3c4d00; --md-sys-color-on-success-container: #d3ee80; --md-sys-color-warning: #ffb955; --md-sys-color-on-warning: #452b00; --md-sys-color-warning-container: #633f00; --md-sys-color-on-warning-container: #ffddb4; }
@media (prefers-color-scheme: dark) { :root[data-palette="orange"][data-contrast="medium"]:not([data-mode="light"]) { --md-sys-color-primary: #ffd5af; --md-sys-color-on-primary: #3b1f00; --md-sys-color-primary-container: #c08446; --md-sys-color-on-primary-container: #000000; --md-sys-color-inverse-primary: #6b3d02; --md-sys-color-secondary: #f8d6b9; --md-sys-color-on-secondary: #34220e; --md-sys-color-secondary-container: #a88b71; --md-sys-color-on-secondary-container: #000000; --md-sys-color-tertiary: #d6e2ae; --md-sys-color-on-tertiary: #202906; --md-sys-color-tertiary-container: #8b9668; --md-sys-color-on-tertiary-container: #000000; --md-sys-color-error: #ffd2cc; --md-sys-color-on-error: #540003; --md-sys-color-error-container: #ff5449; --md-sys-color-on-error-container: #000000; --md-sys-color-background: #19120c; --md-sys-color-on-background: #efe0d5; --md-sys-color-surface: #19120c; --md-sys-color-surface-dim: #19120c; --md-sys-color-surface-bright: #4c433b; --md-sys-color-surface-container-lowest: #0c0603; --md-sys-color-surface-container-low: #241c16; --md-sys-color-surface-container: #2e2620; --md-sys-color-surface-container-high: #39312a; --md-sys-color-surface-container-highest: #453c34; --md-sys-color-on-surface: #ffffff; --md-sys-color-surface-variant: #51453a; --md-sys-color-on-surface-variant: #ecd9cb; --md-sys-color-inverse-surface: #efe0d5; --md-sys-color-inverse-on-surface: #312922; --md-sys-color-outline: #c0afa1; --md-sys-color-outline-variant: #9d8d81; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #fdb975; --md-sys-color-success: #b7d167; --md-sys-color-on-success: #293500; --md-sys-color-success-container: #3c4d00; --md-sys-color-on-success-container: #d3ee80; --md-sys-color-warning: #ffb955; --md-sys-color-on-warning: #452b00; --md-sys-color-warning-container: #633f00; --md-sys-color-on-warning-container: #ffddb4; } }
:root[data-palette="orange"][data-contrast="high"] { --md-sys-color-primary: #442400; --md-sys-color-on-primary: #ffffff; --md-sys-color-primary-container: #6c3e03; --md-sys-color-on-primary-container: #ffffff; --md-sys-color-inverse-primary: #fdb975; --md-sys-color-secondary: #3c2814; --md-sys-color-on-secondary: #ffffff; --md-sys-color-secondary-container: #5c452f; --md-sys-color-on-secondary-container: #ffffff; --md-sys-color-tertiary: #272f0c; --md-sys-color-on-tertiary: #ffffff; --md-sys-color-tertiary-container: #434d26; --md-sys-color-on-tertiary-container: #ffffff; --md-sys-color-error: #600004; --md-sys-color-on-error: #ffffff; --md-sys-color-error-container: #98000a; --md-sys-color-on-error-container: #ffffff; --md-sys-color-background: #fff8f5; --md-sys-color-on-background: #211a14; --md-sys-color-surface: #fff8f5; --md-sys-color-surface-dim: #c4b6ac; --md-sys-color-surface-bright: #fff8f5; --md-sys-color-surface-container-lowest: #ffffff; --md-sys-color-surface-container-low: #fdeee3; --md-sys-color-surface-container: #efe0d5; --md-sys-color-surface-container-high: #e0d2c7; --md-sys-color-surface-container-highest: #d2c4ba; --md-sys-color-on-surface: #000000; --md-sys-color-surface-variant: #f2dfd1; --md-sys-color-on-surface-variant: #000000; --md-sys-color-inverse-surface: #372f28; --md-sys-color-inverse-on-surface: #ffffff; --md-sys-color-outline: #352a21; --md-sys-color-outline-variant: #53473c; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #865319; --md-sys-color-success: #516600; --md-sys-color-on-success: #ffffff; --md-sys-color-success-container: #d3ee80; --md-sys-color-on-success-container: #161e00; --md-sys-color-warning: #835500; --md-sys-color-on-warning: #ffffff; --md-sys-color-warning-container: #ffddb4; --md-sys-color-on-warning-container: #291800; }
:root[data-palette="orange"][data-contrast="high"][data-mode="dark"] { --md-sys-color-primary: #ffeddf; --md-sys-color-on-primary: #000000; --md-sys-color-primary-container: #f9b572; --md-sys-color-on-primary-container: #160800; --md-sys-color-inverse-primary: #6b3d02; --md-sys-color-secondary: #ffeddf; --md-sys-color-on-secondary: #000000; --md-sys-color-secondary-container: #ddbda0; --md-sys-color-on-secondary-container: #160800; --md-sys-color-tertiary: #eaf6c0; --md-sys-color-on-tertiary: #000000; --md-sys-color-tertiary-container: #bcc896; --md-sys-color-on-tertiary-container: #080d00; --md-sys-color-error: #ffece9; --md-sys-color-on-error: #000000; --md-sys-color-error-container: #ffaea4; --md-sys-color-on-error-container: #220001; --md-sys-color-background: #19120c; --md-sys-color-on-background: #efe0d5; --md-sys-color-surface: #19120c; --md-sys-color-surface-dim: #19120c; --md-sys-color-surface-bright: #584e46; --md-sys-color-surface-container-lowest: #000000; --md-sys-color-surface-container-low: #261e18; --md-sys-color-surface-container: #372f28; --md-sys-color-surface-container-high: #433a32; --md-sys-color-surface-container-highest: #4e453d; --md-sys-color-on-surface: #ffffff; --md-sys-color-surface-variant: #51453a; --md-sys-color-on-surface-variant: #ffffff; --md-sys-color-inverse-surface: #efe0d5; --md-sys-color-inverse-on-surface: #000000; --md-sys-color-outline: #ffeddf; --md-sys-color-outline-variant: #d1bfb2; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #fdb975; --md-sys-color-success: #b7d167; --md-sys-color-on-success: #293500; --md-sys-color-success-container: #3c4d00; --md-sys-color-on-success-container: #d3ee80; --md-sys-color-warning: #ffb955; --md-sys-color-on-warning: #452b00; --md-sys-color-warning-container: #633f00; --md-sys-color-on-warning-container: #ffddb4; }
@media (prefers-color-scheme: dark) { :root[data-palette="orange"][data-contrast="high"]:not([data-mode="light"]) { --md-sys-color-primary: #ffeddf; --md-sys-color-on-primary: #000000; --md-sys-color-primary-container: #f9b572; --md-sys-color-on-primary-container: #160800; --md-sys-color-inverse-primary: #6b3d02; --md-sys-color-secondary: #ffeddf; --md-sys-color-on-secondary: #000000; --md-sys-color-secondary-container: #ddbda0; --md-sys-color-on-secondary-container: #160800; --md-sys-color-tertiary: #eaf6c0; --md-sys-color-on-tertiary: #000000; --md-sys-color-tertiary-container: #bcc896; --md-sys-color-on-tertiary-container: #080d00; --md-sys-color-error: #ffece9; --md-sys-color-on-error: #000000; --md-sys-color-error-container: #ffaea4; --md-sys-color-on-error-container: #220001; --md-sys-color-background: #19120c; --md-sys-color-on-background: #efe0d5; --md-sys-color-surface: #19120c; --md-sys-color-surface-dim: #19120c; --md-sys-color-surface-bright: #584e46; --md-sys-color-surface-container-lowest: #000000; --md-sys-color-surface-container-low: #261e18; --md-sys-color-surface-container: #372f28; --md-sys-color-surface-container-high: #433a32; --md-sys-color-surface-container-highest: #4e453d; --md-sys-color-on-surface: #ffffff; --md-sys-color-surface-variant: #51453a; --md-sys-color-on-surface-variant: #ffffff; --md-sys-color-inverse-surface: #efe0d5; --md-sys-color-inverse-on-surface: #000000; --md-sys-color-outline: #ffeddf; --md-sys-color-outline-variant: #d1bfb2; --md-sys-color-shadow: #000000; --md-sys-color-scrim: #000000; --md-sys-color-surface-tint: #fdb975; --md-sys-color-success: #b7d167; --md-sys-color-on-success: #293500; --md-sys-color-success-container: #3c4d00; --md-sys-color-on-success-container: #d3ee80; --md-sys-color-warning: #ffb955; --md-sys-color-on-warning: #452b00; --md-sys-color-warning-container: #633f00; --md-sys-color-on-warning-container: #ffddb4; } }
+38 -5
View File
@@ -29,13 +29,13 @@ table.list th, .table-list-header {
color: var(--md-sys-color-on-surface-variant);
font: var(--md-sys-typescale-title-small);
letter-spacing: var(--md-sys-typescale-title-small-tracking);
padding-block: 10px;
padding-block: calc(10px + var(--nm-density) / 2);
padding-inline: 8px;
border-block-end: 1px solid var(--md-sys-color-outline-variant);
}
table.list th a, table.list th a:link, table.list th a:visited { color: var(--md-sys-color-on-surface-variant); }
table.list td {
block-size: 40px;
block-size: calc(40px + var(--nm-density));
padding-block: 4px;
padding-inline: 8px;
border-block-start: 1px solid var(--md-sys-color-outline-variant);
@@ -146,10 +146,43 @@ a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: var(--md-sy
40px high and centers its content; the row stays top-aligned so an expanded multi-select grows
downwards without dragging the label to its middle. */
#filters-table .filter { align-items: flex-start; margin-block: 4px 0; }
#filters-table .filter .field { display: flex; align-items: center; gap: 8px; min-block-size: 40px; }
#filters-table .filter .field { display: flex; align-items: center; gap: 8px; min-block-size: calc(40px + var(--nm-density)); }
#filters-table .filter .field input[type="checkbox"] { margin: 0; }
#filters-table .filter .values { display: flex; align-items: center; min-block-size: 40px; flex-wrap: wrap; gap: 4px 8px; }
.add-filter { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
#filters-table .filter .values { display: flex; align-items: center; min-block-size: calc(40px + var(--nm-density)); flex-wrap: wrap; gap: 4px 8px; }
/* "Add filter" is an M3 tonal button, not one more outlined field. A select has no
placeholder, so the label lies over it in the same grid cell and lets taps through.
A zero width with a 100% minimum keeps the longest option from widening the button. */
.add-filter { display: grid; justify-content: end; margin-block-start: 4px; }
.add-filter > * { grid-area: 1 / 1; }
.add-filter select {
inline-size: 0;
min-inline-size: 100%;
block-size: 40px;
border: 0;
border-radius: var(--md-sys-shape-corner-full);
background: var(--md-sys-color-secondary-container); /* drops the dropdown arrow too */
cursor: pointer;
}
.add-filter select:hover { background-image: var(--nm-state-hover); }
.add-filter label {
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding-inline: 16px 24px;
pointer-events: none;
color: var(--md-sys-color-on-secondary-container);
font: var(--md-sys-typescale-label-large);
letter-spacing: var(--md-sys-typescale-label-large-tracking);
}
.add-filter label::before {
content: '';
inline-size: 18px;
block-size: 18px;
background-color: currentColor;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M440-440H200v-80h240v-240h80v240h240v80H520v240h-80v-240Z'/%3E%3C/svg%3E") center / 18px no-repeat;
}
/* Core fixes the operator at 120px, too narrow for "has never been" with M3 field padding */
#filters-table .filter .operator select { inline-size: 180px; }
/* All value fields share one width so the filter rows form a clean second column.
+24 -16
View File
@@ -7,22 +7,23 @@
/* Typeface */
--md-ref-typeface-plain: Roboto, system-ui, sans-serif;
/* Type scale, as `font` shorthand values: weight size/line-height family */
--md-sys-typescale-display-large: 400 3.5625rem/4rem var(--md-ref-typeface-plain);
--md-sys-typescale-display-medium: 400 2.8125rem/3.25rem var(--md-ref-typeface-plain);
--md-sys-typescale-display-small: 400 2.25rem/2.75rem var(--md-ref-typeface-plain);
--md-sys-typescale-headline-large: 400 2rem/2.5rem var(--md-ref-typeface-plain);
--md-sys-typescale-headline-medium: 400 1.75rem/2.25rem var(--md-ref-typeface-plain);
--md-sys-typescale-headline-small: 400 1.5rem/2rem var(--md-ref-typeface-plain);
--md-sys-typescale-title-large: 400 1.375rem/1.75rem var(--md-ref-typeface-plain);
--md-sys-typescale-title-medium: 500 1rem/1.5rem var(--md-ref-typeface-plain);
--md-sys-typescale-title-small: 500 0.875rem/1.25rem var(--md-ref-typeface-plain);
--md-sys-typescale-body-large: 400 1rem/1.5rem var(--md-ref-typeface-plain);
--md-sys-typescale-body-medium: 400 0.875rem/1.25rem var(--md-ref-typeface-plain);
--md-sys-typescale-body-small: 400 0.75rem/1rem var(--md-ref-typeface-plain);
--md-sys-typescale-label-large: 500 0.875rem/1.25rem var(--md-ref-typeface-plain);
--md-sys-typescale-label-medium: 500 0.75rem/1rem var(--md-ref-typeface-plain);
--md-sys-typescale-label-small: 500 0.6875rem/1rem var(--md-ref-typeface-plain);
/* Type scale, as `font` shorthand values: weight size/line-height family. Line heights are rounded to
whole px: in the size modes 1.25rem is e.g. 22.857px, and the rounding moved text off center */
--md-sys-typescale-display-large: 400 3.5625rem/round(4rem, 1px) var(--md-ref-typeface-plain);
--md-sys-typescale-display-medium: 400 2.8125rem/round(3.25rem, 1px) var(--md-ref-typeface-plain);
--md-sys-typescale-display-small: 400 2.25rem/round(2.75rem, 1px) var(--md-ref-typeface-plain);
--md-sys-typescale-headline-large: 400 2rem/round(2.5rem, 1px) var(--md-ref-typeface-plain);
--md-sys-typescale-headline-medium: 400 1.75rem/round(2.25rem, 1px) var(--md-ref-typeface-plain);
--md-sys-typescale-headline-small: 400 1.5rem/round(2rem, 1px) var(--md-ref-typeface-plain);
--md-sys-typescale-title-large: 400 1.375rem/round(1.75rem, 1px) var(--md-ref-typeface-plain);
--md-sys-typescale-title-medium: 500 1rem/round(1.5rem, 1px) var(--md-ref-typeface-plain);
--md-sys-typescale-title-small: 500 0.875rem/round(1.25rem, 1px) var(--md-ref-typeface-plain);
--md-sys-typescale-body-large: 400 1rem/round(1.5rem, 1px) var(--md-ref-typeface-plain);
--md-sys-typescale-body-medium: 400 0.875rem/round(1.25rem, 1px) var(--md-ref-typeface-plain);
--md-sys-typescale-body-small: 400 0.75rem/round(1rem, 1px) var(--md-ref-typeface-plain);
--md-sys-typescale-label-large: 500 0.875rem/round(1.25rem, 1px) var(--md-ref-typeface-plain);
--md-sys-typescale-label-medium: 500 0.75rem/round(1rem, 1px) var(--md-ref-typeface-plain);
--md-sys-typescale-label-small: 500 0.6875rem/round(1rem, 1px) var(--md-ref-typeface-plain);
/* Tracking (letter-spacing) per type role */
--md-sys-typescale-display-large-tracking: -0.25px;
@@ -71,4 +72,11 @@
--md-sys-elevation-level3: 0 1px 3px rgb(0 0 0 / 30%), 0 4px 8px 3px rgb(0 0 0 / 15%);
--md-sys-elevation-level4: 0 2px 3px rgb(0 0 0 / 30%), 0 6px 10px 4px rgb(0 0 0 / 15%);
--md-sys-elevation-level5: 0 4px 4px rgb(0 0 0 / 30%), 0 8px 12px 6px rgb(0 0 0 / 15%);
/* Size mode (theme.js sets data-size): added to control heights, e.g. calc(40px + var(--nm-density)) */
--nm-density: 0px;
}
/* Text is in rem, so the root size scales it; percent keeps the browser's font setting */
:root[data-size="compact"] { font-size: 92.857%; --nm-density: -8px; } /* 13px body, M3 density -2 */
:root[data-size="large"] { font-size: 114.286%; --nm-density: 8px; } /* 16px body */
+1 -1
View File
@@ -25,7 +25,7 @@ p.breadcrumb, p.subtitle, p.footnote, div.issue p.author, div.issue div.subject
#issue_tree .issues-stat, #relations .issues-stat, .wiki-update-info, p.other-formats, em.info,
div.attachments span.author, table.files tr.file td.digest, tr.entry td.size,
table.plugins span.description, table.plugins span.url, #document-list .document-item .updated_on,
.cal .calbody, div.tooltip:hover span.tip, div.issue.details .reaction, div.message .reaction, div.news .reaction {
.cal .calbody, div.issue.details .reaction, div.message .reaction, div.news .reaction {
font-size: 0.75rem;
}
div.issue span.private, div.journal span.private { font-size: 0.6875rem; }
+25 -33
View File
@@ -1,64 +1,52 @@
// Generates tonalmine/stylesheets/palettes.css: one M3 color scheme per seed color,
// in light and dark. Run: cd tools/palettes && npm install && npm run build
// The same build bundles scheme.mjs into tonalmine/javascripts/scheme.js for the custom color picker
// (bundled with esbuild because material-color-utilities 0.4.0 has extensionless ESM imports).
import { writeFileSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { Hct, SchemeTonalSpot, MaterialDynamicColors as C, hexFromArgb, argbFromHex, customColor } from '@material/material-color-utilities';
import { ROLES, kebab, paletteCss } from './scheme.mjs';
// The first palette is the default. Ids must match PALETTES in tonalmine/javascripts/theme.js.
// The first palette is the default. Ids must match PALETTES in tonalmine/javascripts/theme.js
// and redmine_tonalmine/lib/redmine_tonalmine/appearance.rb.
// An optional third entry is a Material Theme Builder export folder (light.css, dark.css and the
// -mc/-hc contrast variants):
// its roles are copied as-is instead of generated, and the seed only drives success/warning.
const PALETTES = [
['purple', '#6750A4'],
['blue', '#0061A4'],
['teal', '#006A6A'],
['green', '#386A20'],
['orange', '#8B5000'],
];
const CUSTOM = [['success', '#386A20'], ['warning', '#7C5800']];
const ROLES = ['primary', 'onPrimary', 'primaryContainer', 'onPrimaryContainer', 'inversePrimary',
'secondary', 'onSecondary', 'secondaryContainer', 'onSecondaryContainer',
'tertiary', 'onTertiary', 'tertiaryContainer', 'onTertiaryContainer',
'error', 'onError', 'errorContainer', 'onErrorContainer',
'background', 'onBackground', 'surface', 'surfaceDim', 'surfaceBright',
'surfaceContainerLowest', 'surfaceContainerLow', 'surfaceContainer', 'surfaceContainerHigh', 'surfaceContainerHighest',
'onSurface', 'surfaceVariant', 'onSurfaceVariant', 'inverseSurface', 'inverseOnSurface',
'outline', 'outlineVariant', 'shadow', 'scrim', 'surfaceTint'];
const kebab = (s) => s.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
const hex = (argb) => hexFromArgb(argb);
function roles(seedHex, dark) {
const seed = argbFromHex(seedHex);
const scheme = new SchemeTonalSpot(Hct.fromInt(seed), dark, 0);
const lines = ROLES.map((r) => `--md-sys-color-${kebab(r)}: ${hex(C[r].getArgb(scheme))};`);
for (const [name, value] of CUSTOM) {
const c = customColor(seed, { name, value: argbFromHex(value), blend: true })[dark ? 'dark' : 'light'];
lines.push(`--md-sys-color-${name}: ${hex(c.color)};`, `--md-sys-color-on-${name}: ${hex(c.onColor)};`,
`--md-sys-color-${name}-container: ${hex(c.colorContainer)};`, `--md-sys-color-on-${name}-container: ${hex(c.onColorContainer)};`);
}
return lines.join(' ');
// Reads the roles from a Theme Builder export; fails if one is missing.
function exported(dir, dark, contrast) {
const file = `${dir}/${dark ? 'dark' : 'light'}${{ medium: '-mc', high: '-hc' }[contrast] || ''}.css`;
const vars = Object.fromEntries([...readFileSync(file, 'utf8').matchAll(/--md-sys-color-([a-z-]+):\s*([^;]+);/g)].map((m) => [m[1], m[2].trim()]));
return ROLES.map((r) => {
if (!vars[kebab(r)]) { console.error(`${file} is missing --md-sys-color-${kebab(r)}`); process.exit(1); }
return `--md-sys-color-${kebab(r)}: ${vars[kebab(r)]};`;
});
}
// Specificity: the default palette uses bare :root, the others add [data-palette], and every dark
// rule adds one more attribute, so "palette + mode" always beats "palette" and the default.
let css = `/*
* Generated by tools/palettes/build_palettes.mjs; do not edit by hand.
* M3 color roles (TonalSpot, contrast 0) per palette, light and dark.
* M3 color roles (TonalSpot) per palette, light and dark, at contrast 0, 0.5 and 1.
* <html data-palette="..."> picks the palette, <html data-mode="light|dark"> forces a mode;
* without data-mode the OS setting (prefers-color-scheme) decides.
* <html data-contrast="medium|high"> picks the contrast level; without it, standard.
*/
:root { color-scheme: light; }
:root[data-mode="dark"] { color-scheme: dark; }
@media (prefers-color-scheme: dark) { :root:not([data-mode="light"]) { color-scheme: dark; } }
`;
PALETTES.forEach(([id, seed], i) => {
PALETTES.forEach(([id, seed, dir], i) => {
const sel = i === 0 ? ':root' : `:root[data-palette="${id}"]`;
css += `
/* ${id} (seed ${seed}) */
${sel} { ${roles(seed, false)} }
${sel}[data-mode="dark"] { ${roles(seed, true)} }
@media (prefers-color-scheme: dark) { ${sel}:not([data-mode="light"]) { ${roles(seed, true)} } }
`;
/* ${id} (${dir ? `export ${dir}, ` : ''}seed ${seed}) */
` + paletteCss(sel, seed, dir && ((dark, contrast) => exported(dir, dark, contrast)));
});
// Paths are relative to tools/palettes (npm runs scripts there); the bundle itself lives in .build/.
@@ -66,8 +54,12 @@ const theme = resolve(process.cwd(), '../../tonalmine');
const out = `${theme}/stylesheets/palettes.css`;
writeFileSync(out, css);
// Keep theme.js in sync: every palette id must appear in its PALETTES list.
// Keep theme.js and the plugin's validation in sync: every palette id must appear in their PALETTES lists.
const js = readFileSync(`${theme}/javascripts/theme.js`, 'utf8');
const missing = PALETTES.map(([id]) => id).filter((id) => !js.includes(`'${id}'`));
if (missing.length) { console.error(`theme.js is missing palettes: ${missing.join(', ')}`); process.exit(1); }
const rb = readFileSync(resolve(theme, '../redmine_tonalmine/lib/redmine_tonalmine/appearance.rb'), 'utf8');
const rbIds = rb.match(/PALETTES = %w\[([^\]]*)\]/)[1].split(/\s+/);
const rbMissing = PALETTES.map(([id]) => id).filter((id) => !rbIds.includes(id));
if (rbMissing.length) { console.error(`appearance.rb is missing palettes: ${rbMissing.join(', ')}`); process.exit(1); }
console.log(`Wrote ${PALETTES.length} palettes to ${out}`);
+1 -1
View File
@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"scripts": {
"build": "esbuild build_palettes.mjs --bundle --platform=node --format=esm --outfile=.build/palettes.mjs --log-level=warning && node .build/palettes.mjs"
"build": "esbuild scheme.mjs --bundle --minify --format=iife --global-name=tonalmineScheme --outfile=../../tonalmine/javascripts/scheme.js --log-level=warning && esbuild build_palettes.mjs --bundle --platform=node --format=esm --outfile=.build/palettes.mjs --log-level=warning && node .build/palettes.mjs"
},
"devDependencies": {
"@material/material-color-utilities": "0.4.0",
+47
View File
@@ -0,0 +1,47 @@
// M3 color roles for one palette, shared by build_palettes.mjs (palettes.css) and the browser
// (bundled as tonalmine/javascripts/scheme.js, global tonalmineScheme, for the custom color picker).
import { Hct, SchemeTonalSpot, MaterialDynamicColors as C, hexFromArgb, argbFromHex, customColor } from '@material/material-color-utilities';
const CUSTOM = [['success', '#386A20'], ['warning', '#7C5800']];
export const ROLES = ['primary', 'onPrimary', 'primaryContainer', 'onPrimaryContainer', 'inversePrimary',
'secondary', 'onSecondary', 'secondaryContainer', 'onSecondaryContainer',
'tertiary', 'onTertiary', 'tertiaryContainer', 'onTertiaryContainer',
'error', 'onError', 'errorContainer', 'onErrorContainer',
'background', 'onBackground', 'surface', 'surfaceDim', 'surfaceBright',
'surfaceContainerLowest', 'surfaceContainerLow', 'surfaceContainer', 'surfaceContainerHigh', 'surfaceContainerHighest',
'onSurface', 'surfaceVariant', 'onSurfaceVariant', 'inverseSurface', 'inverseOnSurface',
'outline', 'outlineVariant', 'shadow', 'scrim', 'surfaceTint'];
export const kebab = (s) => s.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
const hex = (argb) => hexFromArgb(argb);
// Contrast levels as Material Theme Builder has them; '' is standard (no data-contrast attribute).
export const CONTRASTS = [['', 0], ['medium', 0.5], ['high', 1]];
// TonalSpot roles plus success/warning; `own` replaces the generated ROLES lines.
// ponytail: success/warning use customColor, which has no contrast level (same tones at every level).
function roles(seedHex, dark, level, own) {
const seed = argbFromHex(seedHex);
const scheme = new SchemeTonalSpot(Hct.fromInt(seed), dark, level);
const lines = own || ROLES.map((r) => `--md-sys-color-${kebab(r)}: ${hex(C[r].getArgb(scheme))};`);
for (const [name, value] of CUSTOM) {
const c = customColor(seed, { name, value: argbFromHex(value), blend: true })[dark ? 'dark' : 'light'];
lines.push(`--md-sys-color-${name}: ${hex(c.color)};`, `--md-sys-color-on-${name}: ${hex(c.onColor)};`,
`--md-sys-color-${name}-container: ${hex(c.colorContainer)};`, `--md-sys-color-on-${name}-container: ${hex(c.onColorContainer)};`);
}
return lines.join(' ');
}
// Light, forced dark and OS dark rules for one palette selector, per contrast level.
// `own(dark, contrast)` may return role lines to use instead of generated ones.
// Each contrast rule adds an attribute, so it beats the same palette's standard rules.
export function paletteCss(sel, seedHex, own) {
return CONTRASTS.map(([id, level]) => {
const s = id ? `${sel}[data-contrast="${id}"]` : sel;
const r = (dark) => roles(seedHex, dark, level, own && own(dark, id));
return `${s} { ${r(false)} }
${s}[data-mode="dark"] { ${r(true)} }
@media (prefers-color-scheme: dark) { ${s}:not([data-mode="light"]) { ${r(true)} } }
`;
}).join('');
}