Compare commits
13
Commits
e1906611d9
...
dc927062a3
@@ -3,3 +3,4 @@ screenshots/
|
||||
.DS_Store
|
||||
tools/.cache/
|
||||
docs/plans/
|
||||
dev/plugins/
|
||||
|
||||
@@ -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.
|
||||
@@ -48,9 +52,22 @@ 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) 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. 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 +75,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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
// 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, purple).
|
||||
const looks = (page) => page.evaluate(() => {
|
||||
const d = document.documentElement.dataset;
|
||||
return `${d.mode ?? 'auto'} ${d.contrast ?? 'standard'} ${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 and color are three form rows
|
||||
const rowsShown = () => admin.$$eval('p:has([data-nm-default])', (rows) => rows.length === 3 && 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 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);
|
||||
const jdoe2 = await login('jdoe');
|
||||
await expectLook('choice follows the user to another browser', jdoe2, 'light standard blue');
|
||||
|
||||
// 4. Parts the user didn't change follow a new default
|
||||
await saveDefault(admin, () => admin.click('[data-nm-default] [data-nm-palette="green"]'));
|
||||
await jdoe2.reload({ waitUntil: 'networkidle' });
|
||||
await expectLook('unchanged parts follow the new default', jdoe2, 'light standard 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 green');
|
||||
await jdoe2.reload({ waitUntil: 'networkidle' });
|
||||
await expectLook('reset is kept after reload', jdoe2, 'dark standard 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 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 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 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>' }, { 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 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 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 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 purple');
|
||||
await Promise.all(['jdoe', 'msmith', 'pweber'].map(resetUser));
|
||||
|
||||
await browser.close();
|
||||
process.exit(failures ? 1 : 0);
|
||||
@@ -1,47 +1,83 @@
|
||||
// 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)
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const BASE = process.env.REDMINE_URL ?? 'http://127.0.0.1:3070';
|
||||
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 } });
|
||||
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'));
|
||||
|
||||
let failures = 0;
|
||||
for (const path of pages) {
|
||||
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'));
|
||||
|
||||
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('')}`);
|
||||
}
|
||||
await page.close();
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,58 @@
|
||||
module RedmineTonalmine
|
||||
# Validates appearance hashes ({mode, contrast, 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
|
||||
# Ids must match PALETTES in tonalmine/javascripts/theme.js (tools/palettes checks this).
|
||||
PALETTES = %w[purple blue green orange].freeze
|
||||
KEYS = %w[mode contrast 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.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 = 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
+237
-25
@@ -1,84 +1,296 @@
|
||||
/*
|
||||
* tonalmine: per-user appearance (mode + color palette).
|
||||
* tonalmine: per-user appearance (mode, contrast, 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 KEY = 'tonalmine';
|
||||
var CUSTOM_SEL = ':root[data-palette="custom"]';
|
||||
var DEFAULT_KEYS = ['mode', 'contrast', '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');
|
||||
// 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,
|
||||
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 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-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-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-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');
|
||||
// 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, 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', '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[2].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]);
|
||||
// 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[2].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);
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -100,7 +100,7 @@ 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 {
|
||||
@@ -178,18 +178,36 @@ 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 */
|
||||
.tabular select:has(+ .icon-only) { inline-size: calc(100% - 36px); max-inline-size: calc(480px - 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; }
|
||||
|
||||
@@ -341,20 +359,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 {
|
||||
@@ -438,7 +511,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) *****/
|
||||
@@ -501,12 +574,11 @@ 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;
|
||||
padding-inline: 8px;
|
||||
border: 1px solid var(--md-sys-color-outline);
|
||||
background: transparent;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
@@ -519,8 +591,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 +602,36 @@ 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: 32px;
|
||||
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: 40px; margin-block: 0; }
|
||||
.nm-appearance--default .nm-swatches { min-block-size: 40px; 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); }
|
||||
|
||||
|
||||
@@ -202,7 +202,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); }
|
||||
@@ -288,28 +298,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;
|
||||
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 {
|
||||
|
||||
@@ -46,13 +46,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,6 +89,26 @@
|
||||
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: 40px; 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 {
|
||||
@@ -96,7 +141,59 @@
|
||||
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 +229,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; }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; } }
|
||||
|
||||
@@ -149,7 +149,40 @@ a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: var(--md-sy
|
||||
#filters-table .filter .field { display: flex; align-items: center; gap: 8px; min-block-size: 40px; }
|
||||
#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; }
|
||||
/* "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.
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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('');
|
||||
}
|
||||
Reference in New Issue
Block a user