commit 06cd1db99c9f29a4ca07dce2f273c037c849e9f7 Author: surtic86 Date: Thu Oct 1 06:05:29 2026 +0200 Initial commit: Tonalmine, a Material Design 3 theme for Redmine 7 Co-Authored-By: Claude Opus 5.5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..37ee66a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +screenshots/ +.DS_Store +tools/.cache/ +docs/plans/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6d708a9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +MIT License + +Copyright (c) 2026 nonameweb + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Bundled third-party assets keep their own licenses: +- tonalmine/fonts/ — Roboto, SIL Open Font License 1.1 (see OFL.txt there) +- tonalmine/images/icons.svg — Material Symbols, Apache License 2.0 diff --git a/README.md b/README.md new file mode 100644 index 0000000..90eb610 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# Tonalmine + +Material Design 3 theme for Redmine 7.0. + +## Install + +1. Copy the `tonalmine/` folder into `/themes/`. +2. Restart Redmine (theme assets are compiled at boot). +3. Select **Tonalmine** in Administration → Settings → Display → Theme. + +## Development + +Requires Docker. + +```sh +cd dev +docker compose up -d # Redmine 7.0.1 on http://localhost:3070 +docker compose exec redmine bin/rails runner /seed/seed.rb # once: demo data, selects the theme +docker compose restart redmine # after every CSS change +``` + +Login: `admin` / `admin`. + +The theme folder is bind-mounted into the container, so edits are picked up on restart. + +### Screenshots + +```sh +cd e2e +npm install && npx playwright install chromium # once +node screenshots.mjs # all pages, light/dark × desktop/mobile → screenshots/ +node screenshots.mjs issue # only pages whose name contains "issue" +node check-form-alignment.mjs # fails if a form label is not level with its field +node check-font-sizes.mjs # fails if any text is off the type scale (e.g. core 0.9em leftovers) +``` + +### Icons + +`tonalmine/images/icons.svg` replaces Redmine's icon sprite with Material Symbols Outlined. +It is generated from `tools/icon_map.yml`: + +```sh +ruby tools/build_icons.rb +``` + +The script fails when Redmine's `config/icon_source.yml` (7.0-stable) has icon names that are missing from +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. + +The color roles are generated from one seed color per palette with `@material/material-color-utilities` +(scheme TonalSpot) into `tonalmine/stylesheets/palettes.css`: + +```sh +cd tools/palettes +npm install # once +npm run build # writes palettes.css +``` + +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). + +## License + +MIT, see `LICENSE`. Roboto (OFL 1.1) and Material Symbols (Apache 2.0) keep their own licenses. diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml new file mode 100644 index 0000000..711b692 --- /dev/null +++ b/dev/docker-compose.yml @@ -0,0 +1,14 @@ +services: + redmine: + image: redmine:7.0.1 + ports: + - "3070:3000" + volumes: + - redmine-sqlite:/usr/src/redmine/sqlite + - redmine-files:/usr/src/redmine/files + - ../tonalmine:/usr/src/redmine/themes/tonalmine + - .:/seed:ro + +volumes: + redmine-sqlite: + redmine-files: diff --git a/dev/seed.rb b/dev/seed.rb new file mode 100644 index 0000000..d111a8d --- /dev/null +++ b/dev/seed.rb @@ -0,0 +1,201 @@ +# Demo data for visual theme testing. +# Run: docker compose exec redmine bin/rails runner /seed/seed.rb +# Idempotent: exits if the demo project already exists. Dates are relative to today +# so Gantt, calendar and roadmap always show current data. + +if Project.exists?(identifier: 'apollo') + puts 'Demo data already present.' + load File.join(__dir__, 'seed_extras.rb') + exit +end + +srand(42) +today = Date.today + +# One transaction: a failure leaves no partial data. +ActiveRecord::Base.transaction do + Redmine::DefaultData::Loader.load('en') if Redmine::DefaultData::Loader.no_data? + + Setting.ui_theme = 'tonalmine' + Setting.text_formatting = 'common_mark' + Setting.notified_events = [] # demo data must not send mail + + admin = User.find_by(login: 'admin') + admin.update!(must_change_passwd: false) + User.current = admin + + people = [ + %w[jdoe Jane Doe], %w[msmith Max Smith], %w[lnguyen Linh Nguyen], %w[pweber Paula Weber] + ].map do |login, first, last| + User.create!(login: login, firstname: first, lastname: last, mail: "#{login}@example.com", + password: 'password123', password_confirmation: 'password123', language: 'en') + end + team = Group.create!(lastname: 'Developers') + team.users << people[0..2] + + manager = Role.find_by!(name: 'Manager') + developer = Role.find_by!(name: 'Developer') + reporter = Role.find_by!(name: 'Reporter') + + customer_field = IssueCustomField.create!(name: 'Customer', field_format: 'list', possible_values: %w[Acme Globex Initech], + is_for_all: true, is_filter: true, tracker_ids: Tracker.ids) + review_field = IssueCustomField.create!(name: 'Review date', field_format: 'date', is_for_all: true, + is_filter: true, tracker_ids: Tracker.ids) + + modules = Redmine::AccessControl.available_project_modules.map(&:to_s) + make_project = lambda do |name, identifier, parent, description| + Project.create!(name: name, identifier: identifier, description: description, is_public: true, + parent: parent, tracker_ids: Tracker.ids, enabled_module_names: modules) + end + + apollo = make_project.('Apollo', 'apollo', nil, "Customer portal relaunch.\n\n**Goal:** ship v2.0 this quarter.") + web = make_project.('Apollo Web', 'apollo-web', apollo, 'Frontend of the customer portal.') + api = make_project.('Apollo API', 'apollo-api', apollo, 'REST backend.') + infra = make_project.('Infrastructure', 'infra', nil, 'Servers, CI and monitoring.') + projects = [apollo, web, api, infra] + + projects.each do |p| + Member.create!(project: p, principal: admin, role_ids: [manager.id]) + Member.create!(project: p, principal: people[0], role_ids: [manager.id]) + Member.create!(project: p, principal: team, role_ids: [developer.id]) + Member.create!(project: p, principal: people[3], role_ids: [reporter.id]) + %w[Frontend Backend Docs].each { |c| IssueCategory.create!(project: p, name: c) } + end + + v1 = Version.create!(project: apollo, name: '1.0', effective_date: today - 60, sharing: 'descendants') + v2 = Version.create!(project: apollo, name: '2.0', effective_date: today + 14, sharing: 'descendants', + description: 'Portal relaunch') + v3 = Version.create!(project: apollo, name: '2.1', effective_date: today + 60, sharing: 'descendants') + vi = Version.create!(project: infra, name: 'Q4 hardening', effective_date: today + 30) + + titles = [ + 'Login page shows wrong error message', 'Add CSV export to reports', 'Upgrade database driver', + 'Dashboard loads slowly with many widgets', 'Password reset mail not localized', 'Add dark mode', + 'Refactor session handling', 'Document REST authentication', 'Search ignores umlauts', + 'Broken layout on small screens', 'Add audit log', 'Rate limit API endpoints', 'Improve onboarding tour', + 'Fix timezone in due dates', 'Add SSO via OIDC', 'Remove legacy XML endpoints', 'Paginate project list', + 'Cache avatar images', 'Nightly backup verification', 'Migrate CI to new runners' + ] + open_statuses = IssueStatus.sorted.reject(&:is_closed?) + closed_status = IssueStatus.sorted.find(&:is_closed?) + priorities = IssuePriority.active.to_a + assignees = people + [admin, nil] + + issues = Array.new(60) do |i| + project = projects[i % projects.size] + version = project == infra ? vi : [v1, v2, v2, v3, nil][i % 5] + status = version == v1 || i % 9 == 7 ? closed_status : open_statuses[i % open_statuses.size] + start = today + rand(-40..20) + suffix = i >= titles.size ? " (#{i / titles.size + 1})" : '' + issue = Issue.new(project: project, tracker: project.trackers.to_a[i % project.trackers.size], + author: people[i % people.size], subject: titles[i % titles.size] + suffix, + description: "Steps to reproduce:\n\n1. Open the page\n2. Click **Save**\n3. See the error\n\n```ruby\nputs 'hello'\n```", + priority: priorities[i % priorities.size], assigned_to: assignees[i % assignees.size], + category: project.issue_categories.to_a[i % 3], fixed_version: version, + start_date: start, due_date: start + rand(3..30), estimated_hours: [2, 4, 8, 16, nil][i % 5], + done_ratio: status.is_closed? ? 100 : [0, 10, 30, 50, 80][i % 5], is_private: i == 5) + issue.status = status + issue.custom_field_values = { customer_field.id => %w[Acme Globex Initech][i % 3], review_field.id => (today + i).to_s } + issue.save! + issue + end + + v1.update!(status: 'closed') # closed versions reject new issues, so close it afterwards + + # Subtasks and relations (same project: cross-project relations are off by default) + showcase = issues[12] # open apollo issue: gets subtasks, attachments and watchers + [issues[24], issues[28]].each { |child| child.reload.update!(parent_issue_id: showcase.id) } + IssueRelation.create!(issue_from: showcase, issue_to: issues[32], relation_type: 'relates') + IssueRelation.create!(issue_from: issues[9], issue_to: issues[13], relation_type: 'blocks') + IssueRelation.create!(issue_from: issues[14], issue_to: issues[18], relation_type: 'precedes') + + # Journals: notes and attribute changes + issues.first(15).each_with_index do |issue, i| + issue.reload.init_journal(people[i % 3], "Looked into this. #{i.even? ? 'Needs a decision from **product**.' : 'Fix is in review.'}") + issue.done_ratio = [issue.done_ratio.to_i + 10, 100].min unless issue.closed? + issue.save! + end + showcase.watcher_users << people[1..2] + + # Attachments + # 160x90 solid-color RGB PNG, built by hand so the seed needs no image file + png_chunk = ->(type, data) { [data.bytesize].pack('N') + type + data + [Zlib.crc32(type + data)].pack('N') } + png = "\x89PNG\r\n\x1a\n".b + png_chunk.('IHDR', [160, 90, 8, 2, 0, 0, 0].pack('NNC5')) + + png_chunk.('IDAT', Zlib::Deflate.deflate(([0] + [103, 80, 164] * 160).pack('C*') * 90)) + png_chunk.('IEND', '') + [['screenshot.png', png, 'image/png'], ['notes.txt', "Some notes\n", 'text/plain']].each do |name, data, type| + Attachment.create!(file: StringIO.new(data), author: admin, filename: name, content_type: type, container: showcase) + end + + # Wiki + wiki = apollo.reload.wiki + start_text = <<~MD + # Apollo wiki + + {{toc}} + + ## Overview + + The customer portal lets clients see **tickets**, *invoices* and `usage`. + + > Keep this page short. Details go into child pages. + + ## Team + + | Name | Role | Since | + |------|------|-------| + | Jane Doe | Product owner | 2024 | + | Max Smith | Backend | 2025 | + | Linh Nguyen | Frontend | 2026 | + + ## Setup + + ```ruby + class Portal + def initialize(user) + @user = user # current user + end + end + ``` + + - [ ] Install dependencies + - [x] Configure database + + See [[Architecture]] and issue #13. + MD + page = WikiPage.new(wiki: wiki, title: wiki.start_page) + page.content = WikiContent.new(page: page, text: start_text, author: admin) + page.save! + child = WikiPage.new(wiki: wiki, title: 'Architecture', parent: page) + child.content = WikiContent.new(page: child, text: "# Architecture\n\nThree tiers: web, API, database.", author: people[1]) + child.save! + + # News, forum, documents + news = News.create!(project: apollo, title: 'Version 2.0 beta available', summary: 'Try the new portal', + description: 'The beta is live on staging. Please report issues.', author: people[0]) + Comment.create!(commented: news, author: people[1], content: 'Great work!') + + board = Board.create!(project: apollo, name: 'General', description: 'General discussion') + topic = Message.create!(board: board, subject: 'Release planning', content: 'When do we freeze 2.0?', author: people[0]) + Message.create!(board: board, parent: topic, subject: 'RE: Release planning', content: 'Next Friday.', author: people[1]) + + Document.create!(project: apollo, title: 'Requirements', description: 'Signed-off requirements.', + category: DocumentCategory.first) + + # Time entries over the last 30 days + activities = TimeEntryActivity.active.to_a + issues.first(20).each_with_index do |issue, i| + user = people[i % 3] + TimeEntry.create!(project: issue.project, issue: issue, user: user, author: user, hours: [0.5, 1, 2, 3.5][i % 4], + spent_on: today - (i % 30), activity: activities[i % activities.size], comments: 'Work on issue') + end + + # Saved public query: grouped, with totals + IssueQuery.create!(name: 'Open by assignee', user: admin, visibility: Query::VISIBILITY_PUBLIC, + group_by: 'assigned_to', totalable_names: %w[estimated_hours spent_hours], + column_names: %i[tracker status priority subject assigned_to due_date done_ratio]) +end + +puts "Seeded #{Project.count} projects, #{Issue.count} issues, #{User.count} users." + +load File.join(__dir__, 'spread_activity.rb') +load File.join(__dir__, 'seed_extras.rb') diff --git a/dev/seed_extras.rb b/dev/seed_extras.rb new file mode 100644 index 0000000..228284b --- /dev/null +++ b/dev/seed_extras.rb @@ -0,0 +1,67 @@ +# Extra demo data for pages the base seed does not cover: repository, wiki diff, description diff. +# Loaded by seed.rb. Each block has its own guard, so rerunning the seed adds only what is missing. + +require 'fileutils' +require 'open3' +require 'tmpdir' + +apollo = Project.find_by!(identifier: 'apollo') +admin = User.find_by!(login: 'admin') +User.current = admin + +# Git repository: built in a work tree, then cloned bare into the files volume (persists restarts). +unless apollo.repository + bare = '/usr/src/redmine/files/demo-repo.git' + work = Dir.mktmpdir + git = lambda do |*args| + out, status = Open3.capture2e('git', '-C', work, *args) + raise "git #{args.join(' ')} failed: #{out}" unless status.success? + end + env = { 'user.name' => 'Jane Doe', 'user.email' => 'jdoe@example.com' }.flat_map { |k, v| ['-c', "#{k}=#{v}"] } + + git.('init', '-q', '-b', 'main') + File.write(File.join(work, 'README.md'), "# Apollo\n\nCustomer portal.\n") + FileUtils.mkdir_p(File.join(work, 'lib')) + File.write(File.join(work, 'lib/portal.rb'), "class Portal\n def initialize(user)\n @user = user\n end\nend\n") + git.(*env, 'add', '.') + git.(*env, 'commit', '-q', '-m', 'Initial import') + File.write(File.join(work, 'lib/portal.rb'), + "class Portal\n def initialize(user, locale = :en)\n @user = user\n @locale = locale\n end\n\n def greeting\n \"Hello, \#{@user}\"\n end\nend\n") + git.(*env, 'commit', '-q', '-am', 'Add locale and greeting (refs #13)') + File.write(File.join(work, 'README.md'), "# Apollo\n\nCustomer portal.\n\n## Setup\n\nRun `bin/setup`.\n") + git.(*env, 'commit', '-q', '-am', 'Document setup') + + FileUtils.rm_rf(bare) + out, status = Open3.capture2e('git', 'clone', '-q', '--bare', work, bare) + raise "git clone failed: #{out}" unless status.success? + FileUtils.rm_rf(work) + FileUtils.chown_R('redmine', 'redmine', bare) if Process.uid.zero? # the app runs as redmine + + apollo.enabled_module_names = apollo.enabled_module_names | ['repository'] + repo = Repository::Git.create!(project: apollo, url: bare, identifier: 'portal', is_default: true) + repo.fetch_changesets +end + +# Second wiki version, so history and diff have something to compare +start = apollo.wiki.find_page(apollo.wiki.start_page) +if start && start.content.version == 1 + start.content.text = start.content.text.sub('Keep this page short.', 'Keep this page short and current.') + + "\n## Release\n\nVersion 2.0 ships at the end of the sprint.\n" + start.content.author = User.find_by!(login: 'jdoe') + start.content.comments = 'Add release section' + start.content.save! +end + +# Description change on the showcase issue, so the journal has a "diff" link +showcase = Issue.find(13) # the issue the screenshot checklist shows +unless showcase.journals.joins(:details).exists?(journal_details: { prop_key: 'description' }) + showcase.init_journal(User.find_by!(login: 'msmith')) + showcase.description = "#{showcase.description}\n\nAlso happens in the API search endpoint." + showcase.save! +end + +# Second forum, so the forum index lists boards instead of redirecting to the only one +Board.create!(project: apollo, name: 'Support', description: 'Questions from customers') if apollo.boards.count < 2 + +puts "Extras: #{Repository.count} repositories, wiki v#{start&.content&.reload&.version}, " \ + "#{JournalDetail.where(prop_key: 'description').count} description diffs, #{apollo.boards.count} forums." diff --git a/dev/spread_activity.rb b/dev/spread_activity.rb new file mode 100644 index 0000000..7aec387 --- /dev/null +++ b/dev/spread_activity.rb @@ -0,0 +1,60 @@ +# Spreads demo record timestamps over the last two weeks (working hours), so the activity page, +# "updated" columns and history show different days and times. Deterministic (based on ids), +# safe to run again. Called by seed.rb; can also run alone: +# docker compose exec redmine bin/rails runner /seed/spread_activity.rb + +now = Time.current +today = Date.current +DAYS = 14 + +# Day `days_ago` at a working-hour time derived from `n` +at = lambda do |days_ago, n| + (today - days_ago).in_time_zone.change(hour: 8 + (n * 7) % 10, min: (n * 13) % 60) +end +not_future = ->(t, n) { t < now ? t : now - (n % 90 + 5).minutes } + +ActiveRecord::Base.transaction do + Issue.order(:id).each_with_index do |issue, i| + created = at.(DAYS - 1 - (i % DAYS), i) + last = created + issue.journals.order(:id).each do |journal| + t = not_future.(created + (1 + journal.id % 3).days + (journal.id * 17 % 300).minutes, journal.id) + journal.update_columns(created_on: t, updated_on: t) + last = [last, t].max + end + closed = issue.closed? ? [last, created + 1.day].max : nil + issue.update_columns(created_on: created, updated_on: last, closed_on: closed && not_future.(closed, issue.id)) + end + + TimeEntry.find_each do |entry| + t = not_future.(entry.spent_on.in_time_zone.change(hour: 17, min: entry.id * 7 % 60), entry.id) + entry.update_columns(created_on: t, updated_on: t) + end + + Attachment.find_each { |a| a.update_columns(created_on: not_future.(a.container.try(:created_on).to_time + 2.hours, a.id)) if a.container } + + WikiContentVersion.order(:id).each_with_index do |version, i| + t = at.(9 - i * 2, version.id) + version.update_columns(updated_on: t) + version.page.content.update_columns(updated_on: t) if version.version == version.page.content.version + end + WikiPage.find_each { |page| page.update_columns(created_on: page.content.versions.minimum(:updated_on) || page.created_on) } + + News.find_each do |news| + news.update_columns(created_on: at.(6, news.id)) + news.comments.each_with_index { |c, i| c.update_columns(created_on: at.(5 - i, c.id), updated_on: at.(5 - i, c.id)) } + end + + Message.where(parent_id: nil).find_each do |topic| + topic.update_columns(created_on: at.(4, topic.id), updated_on: at.(4, topic.id)) + topic.children.each_with_index do |reply, i| + t = not_future.(at.(3 - i, reply.id), reply.id) + reply.update_columns(created_on: t, updated_on: t) + topic.update_columns(updated_on: t) + end + end + + Document.find_each { |d| d.update_columns(created_on: at.(8, d.id)) } +end + +puts "Spread activity over the last #{DAYS} days." diff --git a/e2e/check-core-leaks.mjs b/e2e/check-core-leaks.mjs new file mode 100644 index 0000000..9f38e89 --- /dev/null +++ b/e2e/check-core-leaks.mjs @@ -0,0 +1,94 @@ +// Reports visible elements that still carry core Redmine shapes the theme should have replaced: +// 1-3px corners (core uses 3px, M3 uses 4/8/12/16/28px or full), core's grey box shadow +// (rgba of --oc-gray-9, theme shadows never use it), square boxes outlined on all four sides +// (core's card style, e.g. the old .mypage-box), and dashed or dotted borders. +// Checks desktop and mobile, because core's responsive.css brings its own shapes. Exits 1 on any hit. +// Usage: node check-core-leaks.mjs [--verbose] +import { chromium } from 'playwright'; + +const BASE = process.env.REDMINE_URL ?? 'http://127.0.0.1:3070'; +const pages = [ + '/my/page', '/projects', '/projects?display_type=list', '/projects/apollo', '/projects/apollo/issues', '/issues/13', + '/projects/apollo/issues/new', '/issues/13/edit', '/issues/bulk_edit?ids[]=13&ids[]=17', '/projects/apollo/issues/gantt', + '/projects/apollo/issues/calendar', '/projects/apollo/roadmap', '/versions/2', '/projects/apollo/activity', + '/projects/apollo/wiki', '/projects/apollo/wiki/Wiki/edit', '/news/1', '/boards/1/topics/1', '/projects/apollo/documents', + '/projects/apollo/files', '/projects/apollo/time_entries', '/projects/apollo/time_entries/report?criteria[]=user&columns=month', + '/search?q=login', '/my/account', '/admin', '/settings', '/users', '/roles/permissions', + '/workflows/edit?role_id=3&tracker_id=1', '/workflows/permissions?role_id=3&tracker_id=1', + '/projects/apollo/repository', '/projects/apollo/repository/portal/revisions', '/projects/apollo/repository/portal/entry/lib/portal.rb', + '/projects/apollo/repository/portal/annotate/lib/portal.rb', '/projects/apollo/repository/portal/revisions/main/diff', + '/projects/apollo/wiki/Wiki/history', '/projects/apollo/wiki/Wiki/diff?version=2&version_from=1', '/projects/apollo/settings', + '/projects/apollo/settings/members', '/projects/apollo/settings/versions', '/projects/apollo/settings/repositories', '/trackers', + '/issue_statuses', '/custom_fields', '/custom_fields/1/edit', '/enumerations', '/admin/info', '/admin/plugins', + '/projects/apollo/boards', +]; +// Elements whose core shape is intended. Each entry needs a reason. +const ALLOW = [ + 'div.gantt_hdr', // Gantt chart header cells form the chart grid +]; +const viewports = { + desktop: { viewport: { width: 1440, height: 900 } }, + mobile: { viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true }, +}; + +const browser = await chromium.launch(); +const offenders = new Map(); // "viewport selector reason" -> pages +for (const [vpName, opts] of Object.entries(viewports)) { + const page = await browser.newPage(opts); + 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 found = await page.evaluate((allow) => { + const describe = (el) => { + const parts = []; + for (let e = el; e && e !== document.body && parts.length < 3; e = e.parentElement) { + parts.unshift(e.id ? `#${e.id}` : e.tagName.toLowerCase() + [...e.classList].slice(0, 2).map((c) => `.${c}`).join('')); + if (e.id) break; + } + return parts.join(' > '); + }; + const opaque = (color) => !/^rgba\(.*,\s*0\)$/.test(color) && color !== 'transparent'; + const sides = ['Top', 'Right', 'Bottom', 'Left']; + const out = []; + for (const el of document.querySelectorAll('body *:not(script):not(style):not(svg *)')) { + const r = el.getBoundingClientRect(); + if (!r.width || !r.height || allow.some((s) => el.matches(s))) continue; + const cs = getComputedStyle(el); + if (cs.visibility === 'hidden' || cs.display === 'none') continue; + const borders = sides.filter((s) => parseFloat(cs[`border${s}Width`]) > 0 && cs[`border${s}Style`] !== 'none' + && opaque(cs[`border${s}Color`])); + const radii = ['TopLeft', 'TopRight', 'BottomRight', 'BottomLeft'].map((c) => parseFloat(cs[`border${c}Radius`])); + if (radii.some((x) => x >= 1 && x <= 3) && (borders.length || opaque(cs.backgroundColor))) { + out.push([describe(el), `${Math.max(...radii)}px corner`]); + } + if (cs.boxShadow.includes('rgba(33, 37, 41')) out.push([describe(el), 'core shadow']); + if (borders.length === 4 && radii.every((x) => x === 0) && r.width >= 100 && r.height >= 32 + && !['TABLE', 'TD', 'TH', 'TR'].includes(el.tagName)) { + out.push([describe(el), 'square outlined box']); + } + const dashed = borders.find((s) => ['dashed', 'dotted'].includes(cs[`border${s}Style`])); + if (dashed) out.push([describe(el), `${cs[`border${dashed}Style`]} border`]); + } + return out; + }, ALLOW); + for (const [sel, reason] of found) { + const key = `${vpName.padEnd(7)} ${sel} (${reason})`; + if (!offenders.has(key)) offenders.set(key, new Set()); + offenders.get(key).add(path); + } + } + await page.close(); +} +await browser.close(); + +const rows = [...offenders].sort(([a], [b]) => a.localeCompare(b)); +for (const [key, where] of rows) { + console.log(`${key} ${process.argv.includes('--verbose') ? [...where].join(', ') : `${where.size} page${where.size > 1 ? 's' : ''}`}`); +} +console.log(rows.length ? `\n${rows.length} core shapes left` : 'No core shapes left.'); +process.exit(rows.length ? 1 : 0); diff --git a/e2e/check-font-sizes.mjs b/e2e/check-font-sizes.mjs new file mode 100644 index 0000000..2504d0a --- /dev/null +++ b/e2e/check-font-sizes.mjs @@ -0,0 +1,67 @@ +// Reports visible text whose computed font size is off the theme's type scale. +// Core Redmine uses relative sizes (0.8em, 90%, …) that compound when nested; the theme should +// leave only M3 scale sizes. Exits 1 if anything is off-scale. +// Usage: node check-font-sizes.mjs [--verbose] +import { chromium } from 'playwright'; + +const BASE = process.env.REDMINE_URL ?? 'http://127.0.0.1:3070'; +// M3 type scale in px (label-small 11 … display-large 57) +const SCALE = [11, 12, 14, 16, 22, 24, 28, 32, 36, 45, 57]; +const pages = [ + '/my/page', '/projects', '/projects/apollo', '/projects/apollo/issues', '/issues/13', '/projects/apollo/issues/new', + '/projects/apollo/roadmap', '/versions/2', '/projects/apollo/activity', '/projects/apollo/wiki', '/news/1', + '/boards/1/topics/1', '/projects/apollo/documents', '/projects/apollo/files', '/projects/apollo/time_entries', + '/projects/apollo/time_entries/report?criteria[]=user&columns=month', '/search?q=login', '/my/account', + '/admin', '/settings', '/users', '/roles/permissions', '/projects/apollo/issues/calendar', + '/projects/apollo/repository', '/projects/apollo/repository/portal/revisions', '/projects/apollo/repository/portal/entry/lib/portal.rb', + '/projects/apollo/repository/portal/annotate/lib/portal.rb', '/projects/apollo/repository/portal/revisions/main/diff', + '/projects/apollo/wiki/Wiki/history', '/projects/apollo/wiki/Wiki/diff?version=2&version_from=1', '/projects/apollo/settings', + '/projects/apollo/settings/members', '/projects/apollo/settings/versions', '/projects/apollo/settings/repositories', '/trackers', + '/issue_statuses', '/custom_fields', '/custom_fields/1/edit', '/enumerations', '/admin/info', '/admin/plugins', + '/workflows/permissions?role_id=3&tracker_id=1', '/projects/apollo/boards', +]; + +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')); + +const offenders = new Map(); // "selector @ size" -> pages +for (const path of pages) { + await page.goto(`${BASE}${path}`, { waitUntil: 'networkidle' }); + const found = await page.evaluate((scale) => { + const out = []; + const describe = (el) => { + const parts = []; + for (let e = el; e && e !== document.body && parts.length < 3; e = e.parentElement) { + parts.unshift(e.id ? `#${e.id}` : e.tagName.toLowerCase() + [...e.classList].slice(0, 2).map((c) => `.${c}`).join('')); + if (e.id) break; + } + return parts.join(' > '); + }; + for (const el of document.querySelectorAll('body *:not(script):not(style):not(svg *)')) { + if (el.closest('table.gantt-table, .avatar')) continue; // Gantt is zoomed; avatar initials are graphics + const hasText = [...el.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim()); + if (!hasText || !el.offsetParent) continue; + const size = parseFloat(getComputedStyle(el).fontSize); + if (!scale.some((s) => Math.abs(s - size) < 0.3)) out.push([describe(el), Math.round(size * 10) / 10]); + } + return out; + }, SCALE); + for (const [sel, size] of found) { + const key = `${sel} @ ${size}px`; + if (!offenders.has(key)) offenders.set(key, new Set()); + offenders.get(key).add(path); + } +} +await browser.close(); + +const rows = [...offenders].sort((a, b) => parseFloat(a[0].split('@ ')[1]) - parseFloat(b[0].split('@ ')[1])); +for (const [key, where] of rows) { + console.log(`${key} (${where.size} page${where.size > 1 ? 's' : ''}${process.argv.includes('--verbose') ? ': ' + [...where].join(', ') : ''})`); +} +console.log(rows.length ? `\n${rows.length} off-scale text styles` : 'All text on the type scale.'); +process.exit(rows.length ? 1 : 0); diff --git a/e2e/check-form-alignment.mjs b/e2e/check-form-alignment.mjs new file mode 100644 index 0000000..9b4f6cf --- /dev/null +++ b/e2e/check-form-alignment.mjs @@ -0,0 +1,48 @@ +// Checks that each form label (.tabular forms, query filters) lines up with the field next to it. +// 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 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', +]; + +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) { + await page.goto(`${BASE}${path}`, { waitUntil: 'networkidle' }); + const rows = await page.evaluate(() => [ + // Query filters: label text vs. operator select + ...[...document.querySelectorAll('#filters-table .filter')].flatMap((row) => { + const label = row.querySelector('.field label'), control = row.querySelector('.operator select'); + if (!label || !control || !control.offsetParent) return []; + 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) => { + 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 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)) }]; + }), + ]); + 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('')}`); +} + +await browser.close(); +process.exit(failures ? 1 : 0); diff --git a/e2e/fixtures/issues.csv b/e2e/fixtures/issues.csv new file mode 100644 index 0000000..65b04ad --- /dev/null +++ b/e2e/fixtures/issues.csv @@ -0,0 +1,3 @@ +Subject,Tracker,Status,Priority,Assignee,Due date +Import check,Bug,New,Normal,,2026-12-01 +Second row,Feature,New,High,, diff --git a/e2e/package-lock.json b/e2e/package-lock.json new file mode 100644 index 0000000..5bd3a08 --- /dev/null +++ b/e2e/package-lock.json @@ -0,0 +1,40 @@ +{ + "name": "tonalmine-e2e", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tonalmine-e2e", + "dependencies": { + "playwright": "^1" + } + }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + } + } +} diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 0000000..011329e --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,11 @@ +{ + "name": "tonalmine-e2e", + "private": true, + "type": "module", + "scripts": { + "screenshots": "node screenshots.mjs" + }, + "dependencies": { + "playwright": "^1" + } +} diff --git a/e2e/screenshots.mjs b/e2e/screenshots.mjs new file mode 100644 index 0000000..50ce7c9 --- /dev/null +++ b/e2e/screenshots.mjs @@ -0,0 +1,143 @@ +// Captures the page checklist in light/dark × desktop/mobile into ../screenshots/. +// Usage: node screenshots.mjs [filter] e.g. `node screenshots.mjs issue` for pages whose name contains "issue". +// Needs the dev server with seed data (see README). +import { chromium } from 'playwright'; +import { mkdir } from 'node:fs/promises'; + +const BASE = process.env.REDMINE_URL ?? 'http://127.0.0.1:3070'; +const OUT = new URL('../screenshots/', import.meta.url).pathname; +const filter = process.argv[2] ?? ''; + +const pages = [ + ['my-page', '/my/page'], + ['projects-board', '/projects?display_type=board'], + ['projects-list', '/projects?display_type=list'], + ['project-overview', '/projects/apollo'], + ['issues-grouped', '/projects/apollo/issues?set_filter=1&f[]=status_id&op[status_id]=*&group_by=status&t[]=estimated_hours&t[]=spent_hours'], + ['issue-show', '/issues/13'], + ['issue-new', '/projects/apollo/issues/new'], + ['issue-edit', '/issues/13/edit'], + ['issue-bulk-edit', '/issues/bulk_edit?ids[]=13&ids[]=17'], + ['gantt', '/projects/apollo/issues/gantt'], + ['calendar', '/projects/apollo/issues/calendar'], + ['roadmap', '/projects/apollo/roadmap'], + ['version', '/versions/2'], + ['activity', '/projects/apollo/activity'], + ['wiki', '/projects/apollo/wiki'], + ['wiki-edit', '/projects/apollo/wiki/Wiki/edit'], + ['news', '/news/1'], + ['forum-topic', '/boards/1/topics/1'], + ['documents', '/projects/apollo/documents'], + ['files', '/projects/apollo/files'], + ['time-entries', '/projects/apollo/time_entries'], + ['time-report', '/projects/apollo/time_entries/report?criteria[]=user&columns=month'], + ['search', '/search?q=login'], + ['my-account', '/my/account'], + ['admin', '/admin'], + ['settings', '/settings'], + ['users', '/users'], + ['permissions', '/roles/permissions'], + ['workflow', '/workflows/edit?role_id=3&tracker_id=1'], + ['not-found', '/issues/99999'], + ['repository', '/projects/apollo/repository'], + ['repository-revisions', '/projects/apollo/repository/portal/revisions'], + ['repository-entry', '/projects/apollo/repository/portal/entry/lib/portal.rb'], + ['repository-annotate', '/projects/apollo/repository/portal/annotate/lib/portal.rb'], + ['repository-diff', '/projects/apollo/repository/portal/revisions/main/diff'], + ['wiki-history', '/projects/apollo/wiki/Wiki/history'], + ['wiki-diff', '/projects/apollo/wiki/Wiki/diff?version=2&version_from=1'], + ['project-settings', '/projects/apollo/settings'], + ['project-settings-members', '/projects/apollo/settings/members'], + ['project-settings-versions', '/projects/apollo/settings/versions'], + ['project-settings-repositories', '/projects/apollo/settings/repositories'], + ['trackers', '/trackers'], + ['issue-statuses', '/issue_statuses'], + ['custom-fields', '/custom_fields'], + ['custom-field-edit', '/custom_fields/1/edit'], + ['enumerations', '/enumerations'], + ['admin-info', '/admin/info'], + ['admin-plugins', '/admin/plugins'], + ['workflow-fields', '/workflows/permissions?role_id=3&tracker_id=1'], + ['forums', '/projects/apollo/boards'], +]; + +// Pages only reachable while logged out. +const publicPages = [ + ['login', '/login'], + ['register', '/account/register'], + ['lost-password', '/account/lost_password'], +]; + +// Pages that need an interaction before the capture. +const interactions = [ + ['issue-new-error', '/projects/apollo/issues/new', async (p) => { + await p.click('#issue-form input[name=commit]'); + await p.waitForLoadState('networkidle'); + }], + ['issues-context-menu', '/projects/apollo/issues', async (p) => { + await p.click('table.list.issues tr.issue td.status', { button: 'right' }); + await p.waitForSelector('#context-menu ul'); + }, { desktopOnly: true }], + ['journal-diff', '/issues/13', async (p) => { + // The journal ID differs per database, so follow the first description diff link. + await Promise.all([p.waitForURL(/\/journals\/\d+\/diff/), p.click('#history a[href*="/diff"]')]); + await p.waitForLoadState('networkidle'); + }], + ['import-mapping', '/issues/imports/new?project_id=apollo', async (p) => { + await p.setInputFiles('#file', new URL('fixtures/issues.csv', import.meta.url).pathname); + await Promise.all([p.waitForURL(/\/imports\/[^/]+\/settings/), p.click('input[type=submit]')]); + await Promise.all([p.waitForURL(/\/imports\/[^/]+\/mapping/), p.click('input[type=submit]')]); + await p.waitForLoadState('networkidle'); + }], + ['project-jump', '/projects/apollo-api/issues', async (p) => { + await p.click('#project-jump .drdn-trigger'); + await p.waitForSelector('#project-jump .drdn-items'); + }, { desktopOnly: true }], + ['mobile-flyout', '/projects/apollo/issues', async (p) => { + await p.click('.mobile-toggle-button'); + await p.waitForTimeout(300); + }, { mobileOnly: true, viewportOnly: true }], +]; + +const viewports = { + desktop: { width: 1440, height: 900 }, + mobile: { width: 390, height: 844, isMobile: true, hasTouch: true }, +}; + +await mkdir(OUT, { recursive: true }); +const browser = await chromium.launch(); + +for (const scheme of ['light', 'dark']) { + for (const [vpName, vp] of Object.entries(viewports)) { + const { width, height, ...rest } = vp; + const context = await browser.newContext({ viewport: { width, height }, colorScheme: scheme, ...rest }); + const page = await context.newPage(); + const shot = (name, fullPage = true) => + page.screenshot({ path: `${OUT}${name}-${scheme}-${vpName}.png`, fullPage }); + + for (const [name, path] of publicPages.filter(([n]) => n.includes(filter))) { + await page.goto(`${BASE}${path}`, { waitUntil: 'networkidle' }); + await shot(name); + } + 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 [name, path] of pages.filter(([n]) => n.includes(filter))) { + await page.goto(`${BASE}${path}`, { waitUntil: 'networkidle' }); + await shot(name); + } + for (const [name, path, act, opts = {}] of interactions.filter(([n]) => n.includes(filter))) { + if ((opts.desktopOnly && vpName !== 'desktop') || (opts.mobileOnly && vpName !== 'mobile')) continue; + await page.goto(`${BASE}${path}`, { waitUntil: 'networkidle' }); + await act(page); + await shot(name, !opts.viewportOnly); + } + await context.close(); + } +} + +await browser.close(); +console.log(`Screenshots written to ${OUT}`); diff --git a/tonalmine/favicon/favicon.png b/tonalmine/favicon/favicon.png new file mode 100644 index 0000000..45aae68 Binary files /dev/null and b/tonalmine/favicon/favicon.png differ diff --git a/tonalmine/fonts/OFL.txt b/tonalmine/fonts/OFL.txt new file mode 100644 index 0000000..e96f4bc --- /dev/null +++ b/tonalmine/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic) Roboto-Italic[wdth,wght].ttf: Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/tonalmine/fonts/roboto-latin-ext-wght-normal.woff2 b/tonalmine/fonts/roboto-latin-ext-wght-normal.woff2 new file mode 100644 index 0000000..1ee3769 Binary files /dev/null and b/tonalmine/fonts/roboto-latin-ext-wght-normal.woff2 differ diff --git a/tonalmine/fonts/roboto-latin-wght-normal.woff2 b/tonalmine/fonts/roboto-latin-wght-normal.woff2 new file mode 100644 index 0000000..9b0f141 Binary files /dev/null and b/tonalmine/fonts/roboto-latin-wght-normal.woff2 differ diff --git a/tonalmine/images/LICENSE-material-symbols.txt b/tonalmine/images/LICENSE-material-symbols.txt new file mode 100644 index 0000000..387bd31 --- /dev/null +++ b/tonalmine/images/LICENSE-material-symbols.txt @@ -0,0 +1,204 @@ +This license applies to images/icons.svg (Material Symbols by Google, https://github.com/google/material-design-icons). + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/tonalmine/images/arrow-drop-down-dark.svg b/tonalmine/images/arrow-drop-down-dark.svg new file mode 100644 index 0000000..66aa560 --- /dev/null +++ b/tonalmine/images/arrow-drop-down-dark.svg @@ -0,0 +1 @@ + diff --git a/tonalmine/images/arrow-drop-down.svg b/tonalmine/images/arrow-drop-down.svg new file mode 100644 index 0000000..cd7d53f --- /dev/null +++ b/tonalmine/images/arrow-drop-down.svg @@ -0,0 +1 @@ + diff --git a/tonalmine/images/chevron-left-idnt-dark.svg b/tonalmine/images/chevron-left-idnt-dark.svg new file mode 100644 index 0000000..dc76b66 --- /dev/null +++ b/tonalmine/images/chevron-left-idnt-dark.svg @@ -0,0 +1 @@ + diff --git a/tonalmine/images/chevron-left-idnt.svg b/tonalmine/images/chevron-left-idnt.svg new file mode 100644 index 0000000..3614902 --- /dev/null +++ b/tonalmine/images/chevron-left-idnt.svg @@ -0,0 +1 @@ + diff --git a/tonalmine/images/chevron-right-idnt-dark.svg b/tonalmine/images/chevron-right-idnt-dark.svg new file mode 100644 index 0000000..4ca8225 --- /dev/null +++ b/tonalmine/images/chevron-right-idnt-dark.svg @@ -0,0 +1 @@ + diff --git a/tonalmine/images/chevron-right-idnt.svg b/tonalmine/images/chevron-right-idnt.svg new file mode 100644 index 0000000..85bbb77 --- /dev/null +++ b/tonalmine/images/chevron-right-idnt.svg @@ -0,0 +1 @@ + diff --git a/tonalmine/images/icons.svg b/tonalmine/images/icons.svg new file mode 100644 index 0000000..1426665 --- /dev/null +++ b/tonalmine/images/icons.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tonalmine/images/menu.svg b/tonalmine/images/menu.svg new file mode 100644 index 0000000..d1a4e97 --- /dev/null +++ b/tonalmine/images/menu.svg @@ -0,0 +1 @@ + diff --git a/tonalmine/images/search-dark.svg b/tonalmine/images/search-dark.svg new file mode 100644 index 0000000..0dbaf43 --- /dev/null +++ b/tonalmine/images/search-dark.svg @@ -0,0 +1 @@ + diff --git a/tonalmine/images/search.svg b/tonalmine/images/search.svg new file mode 100644 index 0000000..8fc941b --- /dev/null +++ b/tonalmine/images/search.svg @@ -0,0 +1 @@ + diff --git a/tonalmine/javascripts/theme.js b/tonalmine/javascripts/theme.js new file mode 100644 index 0000000..ba80bc1 --- /dev/null +++ b/tonalmine/javascripts/theme.js @@ -0,0 +1,84 @@ +/* + * tonalmine: per-user appearance (mode + color palette). + * + * Redmine loads this file in , 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. + */ +(function () { + // Ids must match PALETTES in tools/palettes/build_palettes.mjs (the build checks this). + 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 KEY = 'tonalmine'; + 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'); + } + + var prefs = load(); + apply(prefs); + + function button(attr, value, label, pressed, extra) { + return ''; + } + + function pickerHtml() { + var mode = prefs.mode || 'auto'; + var palette = prefs.palette || PALETTES[0][0]; + return '
' + + '
Appearance
' + + '
' + + MODES.map(function (m) { return button('data-nm-mode', m[0], m[1], m[0] === mode); }).join('') + + '
' + + '
' + + 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('') + + '
'; + } + + function sync() { + document.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) { + b.setAttribute('aria-pressed', String(b.getAttribute('data-nm-palette') === (prefs.palette || PALETTES[0][0]))); + }); + } + + 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); + sync(); + }); + + document.addEventListener('DOMContentLoaded', function () { + var menu = document.querySelector('#account .dropdown-content .dropdown-divider'); + if (menu) menu.insertAdjacentHTML('afterend', pickerHtml() + ''); + var flyout = document.querySelector('.js-flyout-menu'); + if (flyout) flyout.insertAdjacentHTML('beforeend', pickerHtml()); + }); +})(); diff --git a/tonalmine/stylesheets/application.css b/tonalmine/stylesheets/application.css new file mode 100644 index 0000000..fb448bb --- /dev/null +++ b/tonalmine/stylesheets/application.css @@ -0,0 +1,12 @@ +/* tonalmine: Material Design 3 theme for Redmine 7.0 */ +@import url(/application.css); +@import url(./tokens.css); +@import url(./palettes.css); +@import url(./bridge.css); +@import url(./base.css); +@import url(./layout.css); +@import url(./components.css); +@import url(./tables.css); +@import url(./pages.css); +@import url(./typography.css); +@import url(./mobile.css); diff --git a/tonalmine/stylesheets/base.css b/tonalmine/stylesheets/base.css new file mode 100644 index 0000000..2c47ee6 --- /dev/null +++ b/tonalmine/stylesheets/base.css @@ -0,0 +1,47 @@ +@font-face { + font-family: Roboto; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url(../fonts/roboto-latin-ext-wght-normal.woff2) format("woff2"); + unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF; +} + +@font-face { + font-family: Roboto; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url(../fonts/roboto-latin-wght-normal.woff2) format("woff2"); + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; +} + +body { + background-color: var(--md-sys-color-surface); + color: var(--md-sys-color-on-surface); + font: var(--md-sys-typescale-body-medium); + letter-spacing: var(--md-sys-typescale-body-medium-tracking); +} + +::selection { + background-color: var(--md-sys-color-primary-container); + color: var(--md-sys-color-on-primary-container); +} + +:focus-visible { + outline: 2px solid var(--md-sys-color-primary); + outline-offset: 2px; +} + +a, a:link, a:visited { color: var(--md-sys-color-primary); } +a:hover, a:active { color: var(--md-sys-color-primary); text-decoration: underline; } + +h1, h2, h3, h4, h5, h6, +#content h1, #content h2, #content h3, #content h4, #content h5, #content h6 { + color: var(--md-sys-color-on-surface); + font-family: var(--md-ref-typeface-plain); +} + +h2, #content h2 { font: var(--md-sys-typescale-headline-small); } +h3, #content h3 { font: var(--md-sys-typescale-title-large); } +h4, #content h4 { font: var(--md-sys-typescale-title-small); letter-spacing: var(--md-sys-typescale-title-small-tracking); } diff --git a/tonalmine/stylesheets/bridge.css b/tonalmine/stylesheets/bridge.css new file mode 100644 index 0000000..55544cb --- /dev/null +++ b/tonalmine/stylesheets/bridge.css @@ -0,0 +1,139 @@ +/* + * Maps Redmine 7 core variables (open-color --oc-*, --fonts-main, --color-*) to M3 roles. + * Core CSS uses these ~330 times, so this file recolors most of Redmine, and because + * the roles switch per palette and mode (palettes.css), all of Redmine follows. + * + * Ramp per color family (index 0-9): 0-2 light containers, 3-5 mid tones for + * borders, 6-9 the strong role color used for text and icons. + * --oc-white is used both as background and as text on strong colors; both work with + * surface-container-lowest because strong colors are light in dark mode. + */ + +:root { + --fonts-main: var(--md-ref-typeface-plain); + --color-current-marker: var(--md-sys-color-primary); + + --oc-white: var(--md-sys-color-surface-container-lowest); + --oc-black: var(--md-sys-color-inverse-surface); + + --oc-gray-0: var(--md-sys-color-surface-container-low); + --oc-gray-1: var(--md-sys-color-surface-container); + --oc-gray-2: var(--md-sys-color-surface-container-high); + --oc-gray-3: var(--md-sys-color-surface-container-highest); + --oc-gray-4: var(--md-sys-color-outline-variant); + --oc-gray-5: var(--md-sys-color-outline); + --oc-gray-6: var(--md-sys-color-on-surface-variant); + --oc-gray-7: var(--md-sys-color-on-surface-variant); + --oc-gray-8: var(--md-sys-color-on-surface); + --oc-gray-9: var(--md-sys-color-on-surface); + + --oc-blue-0: color-mix(in srgb, var(--md-sys-color-primary-container) 35%, var(--md-sys-color-surface)); + --oc-blue-1: color-mix(in srgb, var(--md-sys-color-primary-container) 65%, var(--md-sys-color-surface)); + --oc-blue-2: var(--md-sys-color-primary-container); + --oc-blue-3: color-mix(in srgb, var(--md-sys-color-primary) 35%, var(--md-sys-color-primary-container)); + --oc-blue-4: color-mix(in srgb, var(--md-sys-color-primary) 60%, var(--md-sys-color-primary-container)); + --oc-blue-5: color-mix(in srgb, var(--md-sys-color-primary) 80%, var(--md-sys-color-primary-container)); + --oc-blue-6: var(--md-sys-color-primary); + --oc-blue-7: var(--md-sys-color-primary); + --oc-blue-8: var(--md-sys-color-primary); + --oc-blue-9: var(--md-sys-color-primary); + + --oc-indigo-0: color-mix(in srgb, var(--md-sys-color-primary-container) 35%, var(--md-sys-color-surface)); + --oc-indigo-1: color-mix(in srgb, var(--md-sys-color-primary-container) 65%, var(--md-sys-color-surface)); + --oc-indigo-2: var(--md-sys-color-primary-container); + --oc-indigo-3: color-mix(in srgb, var(--md-sys-color-primary) 35%, var(--md-sys-color-primary-container)); + --oc-indigo-4: color-mix(in srgb, var(--md-sys-color-primary) 60%, var(--md-sys-color-primary-container)); + --oc-indigo-5: color-mix(in srgb, var(--md-sys-color-primary) 80%, var(--md-sys-color-primary-container)); + --oc-indigo-6: var(--md-sys-color-primary); + --oc-indigo-7: var(--md-sys-color-primary); + --oc-indigo-8: var(--md-sys-color-primary); + --oc-indigo-9: var(--md-sys-color-primary); + + --oc-cyan-0: color-mix(in srgb, var(--md-sys-color-primary-container) 35%, var(--md-sys-color-surface)); + --oc-cyan-1: color-mix(in srgb, var(--md-sys-color-primary-container) 65%, var(--md-sys-color-surface)); + --oc-cyan-2: var(--md-sys-color-primary-container); + --oc-cyan-3: color-mix(in srgb, var(--md-sys-color-primary) 35%, var(--md-sys-color-primary-container)); + --oc-cyan-4: color-mix(in srgb, var(--md-sys-color-primary) 60%, var(--md-sys-color-primary-container)); + --oc-cyan-5: color-mix(in srgb, var(--md-sys-color-primary) 80%, var(--md-sys-color-primary-container)); + --oc-cyan-6: var(--md-sys-color-primary); + --oc-cyan-7: var(--md-sys-color-primary); + --oc-cyan-8: var(--md-sys-color-primary); + --oc-cyan-9: var(--md-sys-color-primary); + + --oc-violet-0: color-mix(in srgb, var(--md-sys-color-primary-container) 35%, var(--md-sys-color-surface)); + --oc-violet-1: color-mix(in srgb, var(--md-sys-color-primary-container) 65%, var(--md-sys-color-surface)); + --oc-violet-2: var(--md-sys-color-primary-container); + --oc-violet-3: color-mix(in srgb, var(--md-sys-color-primary) 35%, var(--md-sys-color-primary-container)); + --oc-violet-4: color-mix(in srgb, var(--md-sys-color-primary) 60%, var(--md-sys-color-primary-container)); + --oc-violet-5: color-mix(in srgb, var(--md-sys-color-primary) 80%, var(--md-sys-color-primary-container)); + --oc-violet-6: var(--md-sys-color-primary); + --oc-violet-7: var(--md-sys-color-primary); + --oc-violet-8: var(--md-sys-color-primary); + --oc-violet-9: var(--md-sys-color-primary); + + --oc-grape-0: color-mix(in srgb, var(--md-sys-color-primary-container) 35%, var(--md-sys-color-surface)); + --oc-grape-1: color-mix(in srgb, var(--md-sys-color-primary-container) 65%, var(--md-sys-color-surface)); + --oc-grape-2: var(--md-sys-color-primary-container); + --oc-grape-3: color-mix(in srgb, var(--md-sys-color-primary) 35%, var(--md-sys-color-primary-container)); + --oc-grape-4: color-mix(in srgb, var(--md-sys-color-primary) 60%, var(--md-sys-color-primary-container)); + --oc-grape-5: color-mix(in srgb, var(--md-sys-color-primary) 80%, var(--md-sys-color-primary-container)); + --oc-grape-6: var(--md-sys-color-primary); + --oc-grape-7: var(--md-sys-color-primary); + --oc-grape-8: var(--md-sys-color-primary); + --oc-grape-9: var(--md-sys-color-primary); + + --oc-pink-0: color-mix(in srgb, var(--md-sys-color-tertiary-container) 35%, var(--md-sys-color-surface)); + --oc-pink-1: color-mix(in srgb, var(--md-sys-color-tertiary-container) 65%, var(--md-sys-color-surface)); + --oc-pink-2: var(--md-sys-color-tertiary-container); + --oc-pink-3: color-mix(in srgb, var(--md-sys-color-tertiary) 35%, var(--md-sys-color-tertiary-container)); + --oc-pink-4: color-mix(in srgb, var(--md-sys-color-tertiary) 60%, var(--md-sys-color-tertiary-container)); + --oc-pink-5: color-mix(in srgb, var(--md-sys-color-tertiary) 80%, var(--md-sys-color-tertiary-container)); + --oc-pink-6: var(--md-sys-color-tertiary); + --oc-pink-7: var(--md-sys-color-tertiary); + --oc-pink-8: var(--md-sys-color-tertiary); + --oc-pink-9: var(--md-sys-color-tertiary); + + --oc-red-0: color-mix(in srgb, var(--md-sys-color-error-container) 35%, var(--md-sys-color-surface)); + --oc-red-1: color-mix(in srgb, var(--md-sys-color-error-container) 65%, var(--md-sys-color-surface)); + --oc-red-2: var(--md-sys-color-error-container); + --oc-red-3: color-mix(in srgb, var(--md-sys-color-error) 35%, var(--md-sys-color-error-container)); + --oc-red-4: color-mix(in srgb, var(--md-sys-color-error) 60%, var(--md-sys-color-error-container)); + --oc-red-5: color-mix(in srgb, var(--md-sys-color-error) 80%, var(--md-sys-color-error-container)); + --oc-red-6: var(--md-sys-color-error); + --oc-red-7: var(--md-sys-color-error); + --oc-red-8: var(--md-sys-color-error); + --oc-red-9: var(--md-sys-color-error); + + --oc-green-0: color-mix(in srgb, var(--md-sys-color-success-container) 35%, var(--md-sys-color-surface)); + --oc-green-1: color-mix(in srgb, var(--md-sys-color-success-container) 65%, var(--md-sys-color-surface)); + --oc-green-2: var(--md-sys-color-success-container); + --oc-green-3: color-mix(in srgb, var(--md-sys-color-success) 35%, var(--md-sys-color-success-container)); + --oc-green-4: color-mix(in srgb, var(--md-sys-color-success) 60%, var(--md-sys-color-success-container)); + --oc-green-5: color-mix(in srgb, var(--md-sys-color-success) 80%, var(--md-sys-color-success-container)); + --oc-green-6: var(--md-sys-color-success); + --oc-green-7: var(--md-sys-color-success); + --oc-green-8: var(--md-sys-color-success); + --oc-green-9: var(--md-sys-color-success); + + --oc-yellow-0: color-mix(in srgb, var(--md-sys-color-warning-container) 35%, var(--md-sys-color-surface)); + --oc-yellow-1: color-mix(in srgb, var(--md-sys-color-warning-container) 65%, var(--md-sys-color-surface)); + --oc-yellow-2: var(--md-sys-color-warning-container); + --oc-yellow-3: color-mix(in srgb, var(--md-sys-color-warning) 35%, var(--md-sys-color-warning-container)); + --oc-yellow-4: color-mix(in srgb, var(--md-sys-color-warning) 60%, var(--md-sys-color-warning-container)); + --oc-yellow-5: color-mix(in srgb, var(--md-sys-color-warning) 80%, var(--md-sys-color-warning-container)); + --oc-yellow-6: var(--md-sys-color-warning); + --oc-yellow-7: var(--md-sys-color-warning); + --oc-yellow-8: var(--md-sys-color-warning); + --oc-yellow-9: var(--md-sys-color-warning); + + --oc-orange-0: color-mix(in srgb, var(--md-sys-color-warning-container) 35%, var(--md-sys-color-surface)); + --oc-orange-1: color-mix(in srgb, var(--md-sys-color-warning-container) 65%, var(--md-sys-color-surface)); + --oc-orange-2: var(--md-sys-color-warning-container); + --oc-orange-3: color-mix(in srgb, var(--md-sys-color-warning) 35%, var(--md-sys-color-warning-container)); + --oc-orange-4: color-mix(in srgb, var(--md-sys-color-warning) 60%, var(--md-sys-color-warning-container)); + --oc-orange-5: color-mix(in srgb, var(--md-sys-color-warning) 80%, var(--md-sys-color-warning-container)); + --oc-orange-6: var(--md-sys-color-warning); + --oc-orange-7: var(--md-sys-color-warning); + --oc-orange-8: var(--md-sys-color-warning); + --oc-orange-9: var(--md-sys-color-warning); +} diff --git a/tonalmine/stylesheets/components.css b/tonalmine/stylesheets/components.css new file mode 100644 index 0000000..d92318f --- /dev/null +++ b/tonalmine/stylesheets/components.css @@ -0,0 +1,557 @@ +/* + * Components: buttons, text fields, cards/boxes, banners, tabs, badges, + * pagination, progress, tooltips, dialogs, date picker, autocomplete. + * Uses --nm-state-* from layout.css. + */ + +/***** Buttons *****/ +input[type="submit"], input[type="button"], input[type="reset"], button[type="submit"] { + block-size: 40px; + padding-block: 0; + padding-inline: 24px; + margin-block: 4px; + border: 1px solid var(--md-sys-color-outline); + border-radius: var(--md-sys-shape-corner-full); + background-color: transparent; + color: var(--md-sys-color-primary); + font: var(--md-sys-typescale-label-large); + letter-spacing: var(--md-sys-typescale-label-large-tracking); + cursor: pointer; + transition: var(--nm-transition), box-shadow var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard); +} + +/* Filled: the main action of a form */ +input[type="submit"], button[type="submit"] { + border-color: transparent; + background-color: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); +} + +/* Tonal: "Create and continue" and similar secondary submits */ +input[type="submit"][name="continue"], input[type="submit"][name="follow"] { + background-color: var(--md-sys-color-secondary-container); + color: var(--md-sys-color-on-secondary-container); +} + +input[type="submit"]:hover, input[type="button"]:hover, input[type="reset"]:hover, button[type="submit"]:hover { + background-image: var(--nm-state-hover); +} +input[type="submit"]:hover, button[type="submit"]:hover { background-color: var(--md-sys-color-primary); box-shadow: var(--md-sys-elevation-level1); } +input[type="submit"][name="continue"]:hover, input[type="submit"][name="follow"]:hover { background-color: var(--md-sys-color-secondary-container); } +input[type="submit"]:active, input[type="button"]:active, button[type="submit"]:active { background-image: var(--nm-state-pressed); box-shadow: none; } + +input[type="submit"]:disabled, input[type="button"]:disabled, button:disabled { + background-color: color-mix(in srgb, var(--md-sys-color-on-surface) var(--md-sys-state-disabled-container-opacity), transparent); + border-color: transparent; + color: color-mix(in srgb, var(--md-sys-color-on-surface) var(--md-sys-state-disabled-content-opacity), transparent); + box-shadow: none; + cursor: default; +} + +/***** Text fields (outlined) *****/ +input[type="text"], input[type="password"], input[type="email"], input[type="url"], input[type="number"], +input[type="date"], input[type="search"], input[type="tel"], input:not([type]), select, textarea { + min-block-size: 40px; + padding-block: 8px; + padding-inline: 12px; + border: 1px solid var(--md-sys-color-outline); + border-radius: var(--md-sys-shape-corner-extra-small); + background-color: transparent; + color: var(--md-sys-color-on-surface); + font: var(--md-sys-typescale-body-medium); + letter-spacing: var(--md-sys-typescale-body-medium-tracking); + transition: border-color var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard); +} +input[type="text"]:hover, input[type="password"]:hover, input[type="email"]:hover, input[type="url"]:hover, +input[type="number"]:hover, input[type="date"]:hover, input[type="search"]:hover, select:hover, textarea:hover { + border-color: var(--md-sys-color-on-surface); +} +input[type="text"]:focus, input[type="text"]:active, +input[type="password"]:focus, input[type="password"]:active, +input[type="email"]:focus, input[type="url"]:focus, input[type="search"]:focus, +input[type="date"]:focus, input[type="date"]:active, +input[type="number"]:focus, input[type="number"]:active, +select:focus, select:active, +textarea:focus, textarea:active { + border-color: var(--md-sys-color-primary); + outline: 1px solid var(--md-sys-color-primary); /* 1px border + 1px outline = M3's 2px focused outline without layout shift */ + outline-offset: -2px; +} +select { + background-color: transparent; + background-image: url(../images/arrow-drop-down.svg); + background-position: calc(100% - 6px) 50%; + background-size: 24px; + padding-inline-end: 32px; +} +[dir="rtl"] select { background-position: 6px 50%; } +/* The chevron is an image, so it cannot use color roles: one file per mode */ +:root[data-mode="dark"] select { background-image: url(../images/arrow-drop-down-dark.svg); } +@media (prefers-color-scheme: dark) { :root:not([data-mode="light"]) select { background-image: url(../images/arrow-drop-down-dark.svg); } } +/* Autocomplete fields (parent task, watchers): M3 leading search icon, same per-mode files */ +input.autocomplete { + background: transparent url(../images/search.svg) no-repeat 12px 50% / 20px; + padding-inline-start: 40px !important; /* core sets 20px with !important */ +} +[dir="rtl"] input.autocomplete { background-position: calc(100% - 12px) 50%; } +/* :where() keeps the mode swap at the base rule's specificity, so core's own cases still win: + the jump box search (own icon, no background) and the ajax-loading spinner */ +: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[type="checkbox"], input[type="radio"] { accent-color: var(--md-sys-color-primary); } +input[type="file"] { color: var(--md-sys-color-on-surface-variant); } +input[type="file"]::file-selector-button { + block-size: 40px; + margin-inline-end: 12px; + padding-inline: 24px; + border: 0; + border-radius: var(--md-sys-shape-corner-full); + background-color: var(--md-sys-color-secondary-container); + color: var(--md-sys-color-on-secondary-container); + font: var(--md-sys-typescale-label-large); + cursor: pointer; +} +input:disabled, select:disabled, textarea:disabled { + border-color: color-mix(in srgb, var(--md-sys-color-on-surface) var(--md-sys-state-disabled-container-opacity), transparent); + color: color-mix(in srgb, var(--md-sys-color-on-surface) var(--md-sys-state-disabled-content-opacity), transparent); +} + +/***** Cards: .box, tabular forms, fieldsets *****/ +.box { + padding: 16px; + margin-block-end: 16px; + background-color: var(--md-sys-color-surface-container-low); + color: var(--md-sys-color-on-surface); + border: 0; + border-radius: var(--md-sys-shape-corner-medium); + line-height: 1.5; +} +/* M3 has no legend notched into a border. A fieldset on its own is a filled card with the + legend as headline inside. A fieldset inside a card or form, and a collapsible one, is a + section: a full-width divider with the legend as subheader below it (M3 divider guidance). */ +fieldset { + padding: 16px; + margin-block-end: 16px; + border: 0; + border-radius: var(--md-sys-shape-corner-medium); + background-color: var(--md-sys-color-surface-container-low); +} +:is(.box, fieldset) fieldset, fieldset.collapsible:not(.box) { + padding: 8px 0 0; + margin-block: 8px; + border: 0; + border-block-start: 1px solid var(--md-sys-color-outline-variant); + border-radius: 0; + background-color: transparent; +} +/* A section that opens its card needs no divider: the card edge already separates it */ +:is(.box, fieldset) > fieldset:first-child { margin-block-start: 0; padding-block-start: 0; border-block-start: 0; } +/* Floated, the legend leaves the border and flows as the first line of the fieldset */ +fieldset > legend { + float: inline-start; + inline-size: 100%; + padding: 0 0 8px; + color: var(--md-sys-color-on-surface-variant); + font: var(--md-sys-typescale-title-small); + letter-spacing: var(--md-sys-typescale-title-small-tracking); +} +fieldset > legend ~ * { clear: inline-start; } +fieldset.collapsed > legend { padding-block-end: 0; } +/* The chevron stays with a label that wraps */ +fieldset.collapsible > legend { display: flex; align-items: center; gap: 4px; } +.tabular label { color: var(--md-sys-color-on-surface-variant); font-weight: 500; } +.tabular p { padding-block: 4px; } +/* Center the 24px label line on the 40px fields next to it */ +.tabular label:not(.floating):not(.block):not(.inline):not(.no-css) { padding-block-start: 8px; } +/* Checkboxes and radios placed directly in a row align with that label line too */ +/* Checkboxes and radios next to such a label align with its text line */ +.tabular p:has(> label:not(.floating):not(.block):not(.inline):not(.no-css)) > input:is([type="checkbox"], [type="radio"]) { margin-block: 13px 0; } +/* Rows without a field at the label's height start at the top: checkbox lists (watchers) + and the issue description, whose editor stays hidden until "Edit" is clicked */ +.tabular p:is(:has(label.floating), :has(> #issue_description_and_toolbar[style*="none"])) > label:not(.floating):not(.block):not(.inline):not(.no-css) { padding-block-start: 0; } +hr { background: var(--md-sys-color-outline-variant); } +blockquote { border-inline-start: 4px solid var(--md-sys-color-outline-variant); color: var(--md-sys-color-on-surface-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%; } +#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; } +/* 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; } +/* Cancel next to a submit button */ +input[type="submit"] + a { margin-inline-start: 8px; } + +/* Checkbox lists in forms (custom field values, notified projects) */ +.check_box_group { + padding-block: 4px; + padding-inline: 8px; + border-color: var(--md-sys-color-outline); + border-radius: var(--md-sys-shape-corner-extra-small); + background-color: transparent; +} + +/***** Banners: flash messages, errors, warnings, empty states *****/ +#errorExplanation, div.flash, .nodata, .warning, .conflict { + border: 0; + border-radius: var(--md-sys-shape-corner-medium); + padding-block: 14px; + padding-inline: 48px 16px; + font: var(--md-sys-typescale-body-large); + letter-spacing: var(--md-sys-typescale-body-large-tracking); +} +div.flash svg.icon-svg, #errorExplanation svg.icon-svg, .conflict svg.icon-svg { margin-inline: -32px 8px; } +div.flash.error, #errorExplanation { + background-color: var(--md-sys-color-error-container); + color: var(--md-sys-color-on-error-container); +} +div.flash.notice { + background-color: var(--md-sys-color-success-container); + color: var(--md-sys-color-on-success-container); +} +div.flash.warning, .conflict { + background-color: var(--md-sys-color-warning-container); + color: var(--md-sys-color-on-warning-container); +} +.nodata, .warning { + padding-inline: 16px; + background-color: var(--md-sys-color-surface-container); + color: var(--md-sys-color-on-surface-variant); +} +div.flash.error svg.icon-svg, #errorExplanation svg.icon-svg, +div.flash.notice svg.icon-svg, div.flash.warning svg.icon-svg, .conflict svg.icon-svg { stroke: currentColor; } + +/***** Secondary tabs (settings, history, admin) *****/ +#content .tabs { block-size: 48px; margin-block-end: 16px; } +#content .tabs ul { padding-inline-start: 0; border-block-end: 1px solid var(--md-sys-color-outline-variant); } +#content .tabs ul li { margin-inline-end: 0; margin-block-end: 0; } +#content .tabs ul li a, #content .tabs ul li a:hover { + position: relative; + padding-block: 14px; + padding-inline: 16px; + border: 0; + border-radius: 0; + background-color: transparent; + box-shadow: none; + color: var(--md-sys-color-on-surface-variant); + font: var(--md-sys-typescale-title-small); + letter-spacing: var(--md-sys-typescale-title-small-tracking); +} +#content .tabs ul li a:hover { background-image: var(--nm-state-hover); color: var(--md-sys-color-on-surface); } +#content .tabs ul li a.selected, #content .tabs ul li a.selected:hover { + background-color: transparent; + border: 0; + box-shadow: inset 0 -2px 0 var(--md-sys-color-primary); + color: var(--md-sys-color-on-surface); +} +/* Scroll arrows for tabs that overflow: standard icon buttons at the end of the tab row */ +#content .tabs div.tabs-buttons { + display: flex; + align-items: center; + justify-content: end; + gap: 4px; + inline-size: auto; + block-size: 48px; + padding-inline-start: 8px; + box-sizing: border-box; + background: var(--md-sys-color-surface); + border-block-end: 1px solid var(--md-sys-color-outline-variant); +} +#content .tabs button.tab-left, #content .tabs button.tab-right, +#main-menu button.tab-left, #main-menu button.tab-right { + position: static; + display: inline-flex; + align-items: center; + justify-content: center; + inline-size: 40px; + block-size: 40px; + padding: 0; + border: 0; + border-radius: var(--md-sys-shape-corner-full); + background-color: transparent; + color: var(--md-sys-color-on-surface-variant); +} +:is(#content .tabs, #main-menu) button:is(.tab-left, .tab-right) svg.icon-svg { stroke: currentColor; } +:is(#content .tabs, #main-menu) button:is(.tab-left, .tab-right):hover { background-image: var(--nm-state-hover); } +:is(#content .tabs, #main-menu) button:is(.tab-left, .tab-right).disabled { + background: transparent; + color: color-mix(in srgb, var(--md-sys-color-on-surface) var(--md-sys-state-disabled-content-opacity), transparent); +} + +/***** Badges *****/ +.badge { + inset-block-end: 2px; + padding-block: 2px; + padding-inline: 6px; + border-radius: var(--md-sys-shape-corner-small); + font: var(--md-sys-typescale-label-small); + letter-spacing: var(--md-sys-typescale-label-small-tracking); +} +.badge-private { background: var(--md-sys-color-error); border-color: var(--md-sys-color-error); color: var(--md-sys-color-on-error); } +.badge-count, .badge-issues-count { + border-radius: var(--md-sys-shape-corner-full); + background: var(--md-sys-color-secondary-container); + color: var(--md-sys-color-on-secondary-container); +} +.badge-status-open { border-color: var(--md-sys-color-primary); color: var(--md-sys-color-primary); } +.badge-status-closed { border-color: var(--md-sys-color-success); color: var(--md-sys-color-success); } +.badge-status-locked { border-color: var(--md-sys-color-outline); color: var(--md-sys-color-on-surface-variant); } + +/***** Pagination *****/ +span.pagination { margin-inline-start: 0; color: var(--md-sys-color-on-surface-variant); } +.pagination ul.pages { margin-inline: 0 8px; } +.pagination ul.pages li, +.pagination ul.pages li:first-child, +.pagination ul.pages li:last-child { + margin-inline-end: 4px; + border: 0; + border-radius: var(--md-sys-shape-corner-full); + line-height: 40px; + min-inline-size: 40px; +} +.pagination ul.pages li a, .pagination ul.pages li span { + display: block; + padding-block: 0; + padding-inline: 12px; + border-radius: var(--md-sys-shape-corner-full); + color: var(--md-sys-color-on-surface-variant); +} +.pagination ul.pages li.page:hover { background-color: transparent; background-image: var(--nm-state-hover); } +.pagination ul.pages li.page a:hover { color: var(--md-sys-color-on-surface); text-decoration: none; } +.pagination ul.pages li.current { + background-color: var(--md-sys-color-secondary-container); + color: var(--md-sys-color-on-secondary-container); + font-weight: 500; +} +.pagination ul.pages li.current span { color: inherit; } + +/***** Linear progress *****/ +table.progress { border-radius: var(--md-sys-shape-corner-full); overflow: hidden; } +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); } + +/***** Tooltips (plain) *****/ +.ui-tooltip { + background: var(--md-sys-color-inverse-surface); + color: var(--md-sys-color-inverse-on-surface); + border-radius: var(--md-sys-shape-corner-extra-small); + font: var(--md-sys-typescale-body-small); + letter-spacing: var(--md-sys-typescale-body-small-tracking); + padding-block: 4px; + padding-inline: 8px; +} + +/***** 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 { + background: var(--md-sys-color-surface-container-high); + border-color: var(--md-sys-color-outline-variant); + color: var(--md-sys-color-on-surface); +} +.ui-widget-content a { color: var(--md-sys-color-primary); } +.ui-dialog.ui-widget.ui-widget-content { + padding: 8px; + border: 0; + border-radius: var(--md-sys-shape-corner-extra-large); + box-shadow: var(--md-sys-elevation-level3); +} +.ui-widget-header { + background: transparent; + border: 0; + color: var(--md-sys-color-on-surface); +} +.ui-dialog .ui-dialog-titlebar { padding-block: 16px 8px; padding-inline: 16px; } +.ui-dialog .ui-dialog-title { font: var(--md-sys-typescale-headline-small); } +.ui-dialog .ui-dialog-content { padding-inline: 16px; } +.ui-dialog .ui-dialog-titlebar-close { + inline-size: 40px; + block-size: 40px; + margin-block-start: -20px; + border: 0; + border-radius: var(--md-sys-shape-corner-full); + background: transparent; +} +.ui-widget-overlay { background: var(--md-sys-color-scrim); opacity: 32%; } + +.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { + background: transparent; + border-color: transparent; + color: var(--md-sys-color-on-surface); + border-radius: var(--md-sys-shape-corner-full); +} +.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus { + background: color-mix(in srgb, var(--md-sys-color-on-surface) var(--md-sys-state-hover-state-layer-opacity), transparent); + border-color: transparent; + color: var(--md-sys-color-on-surface); +} +.ui-state-highlight, .ui-widget-content .ui-state-highlight { + background: transparent; + border: 1px solid var(--md-sys-color-primary); + color: var(--md-sys-color-primary); +} +.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { + background: var(--md-sys-color-primary); + border-color: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); +} +.ui-datepicker { padding: 12px; border-radius: var(--md-sys-shape-corner-large); box-shadow: var(--md-sys-elevation-level3); } +.ui-datepicker th { color: var(--md-sys-color-on-surface-variant); font: var(--md-sys-typescale-body-small); } +.ui-datepicker td a, .ui-datepicker td span { text-align: center; padding: 6px; } + +.ui-autocomplete, .ui-menu, .tribute-container ul { + background: var(--md-sys-color-surface-container); + border: 0; + border-radius: var(--md-sys-shape-corner-extra-small); + box-shadow: var(--md-sys-elevation-level2); + color: var(--md-sys-color-on-surface); + padding-block: 8px; +} +.ui-menu .ui-menu-item-wrapper.ui-state-active, .tribute-container li.highlight { + margin: 0; + background: var(--md-sys-color-secondary-container); + border-color: transparent; + color: var(--md-sys-color-on-secondary-container); +} + +/***** Icons *****/ +/* The theme sprite (images/icons.svg) draws Material Symbols with fill="currentColor", + so icon color comes from `color`. Core's `stroke` rules still style plugin icons. */ +svg.icon-svg { color: var(--md-sys-color-on-surface-variant); } +a.icon .icon-svg, a.icon-only .icon-svg, span.icon-actions .icon-svg, a .icon-svg { color: var(--md-sys-color-primary); } +a.icon:hover .icon-svg, a.icon-only:hover .icon-svg, span.icon-actions:hover .icon-svg { color: var(--md-sys-color-on-surface); } +.icon-ok svg.icon-svg { color: var(--md-sys-color-success); } +.icon-error svg.icon-svg { color: var(--md-sys-color-error); } +.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; } +html .context-menu-selection svg.icon-svg { color: var(--md-sys-color-on-secondary-container) !important; } + +/***** Wiki toolbar (jstoolbar.css loads after the theme) *****/ +/* Core draws the buttons as fixed-color background images, unreadable in dark mode. + The same SVGs are used as masks here, filled with currentColor. */ +html .jstElements { block-size: auto; padding-block-end: 4px; } +html .jstElements button { + position: relative; + inline-size: 32px; + block-size: 32px; + margin-inline-end: 0; + padding: 0; + border: 0; + border-radius: var(--md-sys-shape-corner-full); + background-color: transparent; + background-image: none; + color: var(--md-sys-color-on-surface-variant); + opacity: 1; + vertical-align: middle; +} +html .jstElements button:hover { background-color: color-mix(in srgb, currentColor var(--md-sys-state-hover-state-layer-opacity), transparent); color: var(--md-sys-color-on-surface); } +html .jstElements button::after { + content: ""; + position: absolute; + inset: 7px; + background-color: currentColor; + mask: var(--nm-jst-icon) center / contain no-repeat; +} +html .jstElements button.jstb_pre::after { display: none; } +html .jstb_pre::before { color: currentColor; font: var(--md-sys-typescale-label-small); } +html .jstSpacer { margin-inline-end: 8px; } +html .jstb_strong { --nm-jst-icon: url(/jstoolbar/bold.svg); } +html .jstb_em { --nm-jst-icon: url(/jstoolbar/italic.svg); } +html .jstb_ins { --nm-jst-icon: url(/jstoolbar/underline.svg); } +html .jstb_del { --nm-jst-icon: url(/jstoolbar/strikethrough.svg); } +html .jstb_code { --nm-jst-icon: url(/jstoolbar/letter-c.svg); } +html .jstb_h1 { --nm-jst-icon: url(/jstoolbar/h1.svg); } +html .jstb_h2 { --nm-jst-icon: url(/jstoolbar/h2.svg); } +html .jstb_h3 { --nm-jst-icon: url(/jstoolbar/h3.svg); } +html .jstb_ul { --nm-jst-icon: url(/jstoolbar/list.svg); } +html .jstb_ol { --nm-jst-icon: url(/jstoolbar/list-numbers.svg); } +html .jstb_tl { --nm-jst-icon: url(/jstoolbar/list-check.svg); } +html .jstb_bq { --nm-jst-icon: url(/jstoolbar/indent-increase.svg); } +html .jstb_unbq { --nm-jst-icon: url(/jstoolbar/indent-decrease.svg); } +html .jstb_precode { --nm-jst-icon: url(/jstoolbar/code.svg); } +html .jstb_link { --nm-jst-icon: url(/jstoolbar/wiki_link.svg); } +html .jstb_img { --nm-jst-icon: url(/jstoolbar/image.svg); } +html .jstb_table { --nm-jst-icon: url(/jstoolbar/table.svg); } +html .jstb_help { --nm-jst-icon: url(/jstoolbar/help.svg); } +html .wiki-preview { background-color: transparent; color: var(--md-sys-color-on-surface); border-color: var(--md-sys-color-outline-variant); border-radius: var(--md-sys-shape-corner-extra-small); } +html .table-generator td { background-color: var(--md-sys-color-surface-container-high); border-color: var(--md-sys-color-outline); } +html .table-generator td.selected-cell, html .table-generator td:hover { background-color: var(--md-sys-color-primary-container); } + +/***** Appearance picker (added by javascripts/theme.js to the profile menu and mobile flyout) *****/ +.nm-appearance { padding-block: 8px 12px; padding-inline: 12px; } +.nm-appearance__title { + margin-block-end: 8px; + color: var(--md-sys-color-on-surface-variant); + font: var(--md-sys-typescale-label-medium); + letter-spacing: var(--md-sys-typescale-label-medium-tracking); +} +/* M3 segmented button */ +.nm-segmented { display: flex; margin-block-end: 12px; } +.nm-segmented button { + flex: 1; + border-radius: 0; + block-size: 32px; + padding-inline: 12px; + border: 1px solid var(--md-sys-color-outline); + background: transparent; + color: var(--md-sys-color-on-surface); + font: var(--md-sys-typescale-label-large); + cursor: pointer; +} +.nm-segmented button + button { margin-inline-start: -1px; } +.nm-segmented button:first-child { border-start-start-radius: var(--md-sys-shape-corner-full); border-end-start-radius: var(--md-sys-shape-corner-full); } +.nm-segmented button:last-child { border-start-end-radius: var(--md-sys-shape-corner-full); border-end-end-radius: var(--md-sys-shape-corner-full); } +.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-swatch { + inline-size: 28px; + block-size: 28px; + padding: 0; + border: 0; + border-radius: var(--md-sys-shape-corner-full); + background: var(--nm-swatch); + cursor: pointer; +} +.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); } +html .flyout-menu .nm-appearance { padding-inline: 16px; } +html .flyout-menu .nm-appearance__title { color: var(--md-sys-color-on-surface-variant); } + +/***** Loading indicator: an M3 snackbar-style chip on inverse surface *****/ +#ajax-indicator { + inline-size: auto; + padding-block: 12px; + padding-inline: 16px; + border: 0; + border-radius: var(--md-sys-shape-corner-extra-small); + background-color: var(--md-sys-color-inverse-surface); + color: var(--md-sys-color-inverse-on-surface); + box-shadow: var(--md-sys-elevation-level3); + font: var(--md-sys-typescale-body-medium); + opacity: 1; +} + +/***** Code block copy button and dragged table rows *****/ +div.pre-wrapper a.copy-pre-content-link { + padding: 4px; + border: 0; + border-radius: var(--md-sys-shape-corner-full); + background-color: var(--md-sys-color-surface-container-high); + color: var(--md-sys-color-on-surface-variant); +} +tr.ui-sortable-helper { border-color: var(--md-sys-color-outline-variant); box-shadow: var(--md-sys-elevation-level2); } diff --git a/tonalmine/stylesheets/layout.css b/tonalmine/stylesheets/layout.css new file mode 100644 index 0000000..6bcaac0 --- /dev/null +++ b/tonalmine/stylesheets/layout.css @@ -0,0 +1,332 @@ +/* + * Page layout: top app bar (top menu + header), primary tabs (project menu), + * sidebar card, content, footer, menus. + * + * State layers: M3 draws hover/focus/press as a translucent layer of the content + * color. Here that is a background-image of currentColor mixed at the state opacity, + * so it works on top of any background color. + */ + +:root { + --nm-state-hover: linear-gradient(color-mix(in srgb, currentColor var(--md-sys-state-hover-state-layer-opacity), transparent) 0 0); + --nm-state-pressed: linear-gradient(color-mix(in srgb, currentColor var(--md-sys-state-pressed-state-layer-opacity), transparent) 0 0); + --nm-transition: background-color var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard), + color var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard); +} + +#wrapper { background: var(--md-sys-color-surface); } + +/***** Top app bar *****/ +nav.top-menu { + background: var(--md-sys-color-surface-container); + color: var(--md-sys-color-on-surface-variant); + font: var(--md-sys-typescale-label-large); + letter-spacing: var(--md-sys-typescale-label-large-tracking); + padding-block: 4px; + padding-inline: 12px; +} + +.top-menu__links a, .top-menu__links a:link, .top-menu__links a:visited, #account .dropdown-trigger { + color: var(--md-sys-color-on-surface-variant); + border-radius: var(--md-sys-shape-corner-full); + padding-block: 6px; + padding-inline: 12px; + transition: var(--nm-transition); +} + +.top-menu__links a:hover, #account .dropdown-trigger:hover { + color: var(--md-sys-color-on-surface); + background-color: transparent; + background-image: var(--nm-state-hover); +} + +.top-menu__links svg.icon-svg, #account .dropdown-trigger .icon-svg, +.top-menu__links a:hover svg.icon-svg, #account .dropdown-trigger:hover .icon-svg { + stroke: currentColor; +} + +#header { + background: var(--md-sys-color-surface-container); + color: var(--md-sys-color-on-surface); + padding-block: 4px 0; + padding-inline: 24px; +} +body.has-main-menu #header { padding-block-end: 48px; } +#header a, #header a:hover { color: var(--md-sys-color-on-surface); } + +#header h1 { + font: var(--md-sys-typescale-title-large); + min-block-size: 3rem; +} +#header h1 .breadcrumbs { + font: var(--md-sys-typescale-label-medium); + letter-spacing: var(--md-sys-typescale-label-medium-tracking); + color: var(--md-sys-color-on-surface-variant); +} +#header h1 .breadcrumbs a { color: var(--md-sys-color-on-surface-variant); } + +#quick-search { min-block-size: 3rem; color: var(--md-sys-color-on-surface-variant); } +#quick-search #q { + inline-size: 220px; + block-size: 40px; + padding-inline: 16px; + border: 0; + border-radius: var(--md-sys-shape-corner-full); + background-color: var(--md-sys-color-surface-container-highest); + color: var(--md-sys-color-on-surface); + font: var(--md-sys-typescale-body-large); +} +#quick-search #q:focus { outline: 2px solid var(--md-sys-color-primary); outline-offset: 0; } +#quick-search form label { font: var(--md-sys-typescale-label-large); } + +#project-jump.drdn { margin-inline-start: 8px; } +#project-jump .drdn-trigger { + block-size: 40px; + padding-inline: 16px 36px; + border: 1px solid var(--md-sys-color-outline-variant); + border-radius: var(--md-sys-shape-corner-full); + background-color: transparent; + color: var(--md-sys-color-on-surface); + font: var(--md-sys-typescale-label-large); + line-height: 38px; +} +#project-jump .drdn-trigger:hover { background-image: var(--nm-state-hover); } + +/***** Primary tabs (project menu) *****/ +#main-menu { + background: var(--md-sys-color-surface-container); + border-block-end: 1px solid var(--md-sys-color-outline-variant); + padding-block: 0; + padding-inline: 16px; +} +#main-menu ul { min-block-size: 48px; } +#main-menu li a { + position: relative; + color: var(--md-sys-color-on-surface-variant); + font: var(--md-sys-typescale-title-small); + letter-spacing: var(--md-sys-typescale-title-small-tracking); + padding-block: 14px; + padding-inline: 16px; + line-height: 20px; + transition: var(--nm-transition); +} +#main-menu li a:hover { + background-color: transparent; + background-image: var(--nm-state-hover); + color: var(--md-sys-color-on-surface); +} +#main-menu li a.selected, #main-menu li a.selected:hover { + background-color: transparent; + color: var(--md-sys-color-primary); + font-weight: 500; + box-shadow: none; +} +#main-menu li a.selected::after { + content: ""; + position: absolute; + inset-inline: 16px; + inset-block-end: 0; + block-size: 3px; + border-radius: 3px 3px 0 0; + background: var(--md-sys-color-primary); +} + +/* "+" new object: tonal icon button */ +#main-menu li a.new-object { + display: inline-flex; + align-items: center; + justify-content: center; + inline-size: 40px; + block-size: 40px; + padding: 0; + margin-block: 0; + border: 0; + border-radius: var(--md-sys-shape-corner-medium); + background-color: var(--md-sys-color-secondary-container); + color: var(--md-sys-color-on-secondary-container); +} +#main-menu li a.new-object:hover { + background-color: var(--md-sys-color-secondary-container); + background-image: var(--nm-state-hover); + color: var(--md-sys-color-on-secondary-container); +} + +/* Scroll arrows when the menu overflows: icon buttons on the bar's colour, so menu items + slide under them instead of showing through (button styles in components.css) */ +#main-menu div.tabs-buttons { + display: flex; + align-items: center; + gap: 4px; + inline-size: auto; + block-size: 48px; + inset-inline-end: 0; + padding-inline: 8px; + border: 0; + background-color: var(--md-sys-color-surface-container); +} + +/***** Menus: project menu children, dropdowns, profile menu *****/ +#main-menu .menu-children, +html .drdn-content, +html .dropdown-content { + background-color: var(--md-sys-color-surface-container); + border: 0; + border-radius: var(--md-sys-shape-corner-extra-small); + box-shadow: var(--md-sys-elevation-level2); + color: var(--md-sys-color-on-surface); + padding-block: 8px; + overflow: hidden; +} +#main-menu .menu-children li a, +html .dropdown-content li a, +html .dropdown-content .dropdown-items a, +.drdn-items > * { + background-color: transparent; + border: 0; + border-radius: 0; + color: var(--md-sys-color-on-surface); + font: var(--md-sys-typescale-label-large); + letter-spacing: var(--md-sys-typescale-label-large-tracking); + padding-block: 10px; + padding-inline: 12px; +} +#main-menu .menu-children li a:hover, +html .dropdown-content li a:hover, +html .dropdown-content .dropdown-items a:hover, +.drdn-items > a:hover, +.drdn-items > a:focus { + background-color: transparent; + background-image: var(--nm-state-hover); + color: var(--md-sys-color-on-surface); + text-decoration: none; +} +#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); } +#project-jump .drdn-items > a:hover { background-color: transparent; color: var(--md-sys-color-on-surface) !important; } +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); } + +/***** Content and sidebar *****/ +#main { background: var(--md-sys-color-surface); } + +#content { + background-color: var(--md-sys-color-surface); + padding-block: 24px 16px; + padding-inline: 24px; +} + +#sidebar { + background: var(--md-sys-color-surface-container-low); + border: 0; + border-radius: var(--md-sys-shape-corner-large); + margin-block: 16px; + margin-inline-end: 16px; + padding-block: 16px; + padding-inline: 16px; + box-sizing: border-box; + align-self: flex-start; +} +/* Collapsed: only the switch button remains, in a small rounded surface */ +#main.collapsedsidebar #sidebar { inline-size: auto; margin-block: 16px; margin-inline-end: 16px; padding: 4px; } +#main.collapsedsidebar #sidebar-switch-panel { float: none; margin: 0; } +#sidebar h3 { + color: var(--md-sys-color-on-surface-variant); + font: var(--md-sys-typescale-title-small); + letter-spacing: var(--md-sys-typescale-title-small-tracking); + margin-block: 16px 8px; +} +#sidebar hr { background: var(--md-sys-color-outline-variant); margin-block: 12px; } + +/* Collapse button: 32px icon button in the top corner, level with the first heading */ +#sidebar-switch-panel { float: inline-end; inline-size: auto; margin-block: -6px 0; margin-inline: 8px -6px; padding: 0; } +#sidebar-switch-button { + display: flex; + align-items: center; + justify-content: center; + inline-size: 32px; + block-size: 32px; + padding: 0; + border-radius: var(--md-sys-shape-corner-full); + color: var(--md-sys-color-on-surface-variant); +} +#sidebar-switch-button svg { stroke: var(--md-sys-color-on-surface-variant); color: inherit; } +#sidebar-switch-button:hover { background-color: transparent; background-image: var(--nm-state-hover); } + +/* Sidebar forms (activity, roadmap, …): label above a full-width field */ +#sidebar form > p:has(> input:not([type="submit"], [type="hidden"], [type="checkbox"]), > select) { + display: flex; + flex-direction: column; + gap: 4px; + margin-block: 0 12px; + color: var(--md-sys-color-on-surface-variant); + font: var(--md-sys-typescale-label-medium); + letter-spacing: var(--md-sys-typescale-label-medium-tracking); +} +#sidebar form > p > :is(input:not([type="submit"], [type="checkbox"]), select) { inline-size: 100%; max-inline-size: none; margin: 0; } +/* First heading sits level with the collapse button, also after hidden form inputs */ +#sidebar-wrapper > form:first-child > h3:first-of-type, +#sidebar-wrapper > .contextual:first-child + h3 { margin-block-start: 0; } +#sidebar-wrapper > .contextual:first-child { margin-block: 0; } +/* Watchers opening the sidebar: "Add" sits next to the collapse button, heading level with it */ +#sidebar-wrapper > #watchers:first-child > .contextual { margin-block: 0; margin-inline-end: 0; } +#sidebar-wrapper > #watchers:first-child > .contextual + h3 { margin-block-start: 0; } +#sidebar form > p:empty { display: none; } + +/* Checkbox lists: even 32px rows, text-colored labels */ +#sidebar li:has(input[type="checkbox"]), +#sidebar :is(li, form > p) > label:has(input[type="checkbox"]), +#sidebar form > p:has(> input[type="checkbox"]) { + display: flex; + align-items: center; + gap: 12px; + min-block-size: 32px; +} +#sidebar :is(li, form > p) > label:has(input[type="checkbox"]) { flex: 1; } +#sidebar input[type="checkbox"] { inline-size: 18px; block-size: 18px; margin: 0; flex-shrink: 0; } +#sidebar li label, #sidebar li label a, #sidebar form > p > label { color: var(--md-sys-color-on-surface); font: var(--md-sys-typescale-body-medium); } +#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 { + display: flex; + align-items: center; + min-block-size: 36px; + padding-inline: 12px; + 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); + 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 { + margin-inline-start: 0; /* core: -6px, inset marker bar */ + padding-block: 0; + 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; } + +/***** Footer *****/ +#footer { + background: var(--md-sys-color-surface); + border: 0; + color: var(--md-sys-color-on-surface-variant); + font: var(--md-sys-typescale-label-small); + letter-spacing: var(--md-sys-typescale-label-small-tracking); + padding: 16px; +} + +/***** Login *****/ +#login-form { + inline-size: 400px; + padding: 24px; + background-color: var(--md-sys-color-surface-container-low); + border-radius: var(--md-sys-shape-corner-extra-large); +} +#login-form label { font: var(--md-sys-typescale-label-large); color: var(--md-sys-color-on-surface-variant); } +#login-form input { block-size: 40px; } diff --git a/tonalmine/stylesheets/mobile.css b/tonalmine/stylesheets/mobile.css new file mode 100644 index 0000000..dd56f2c --- /dev/null +++ b/tonalmine/stylesheets/mobile.css @@ -0,0 +1,156 @@ +/* + * Mobile overrides at core's responsive.css breakpoint. The core file stays in place + * (a theme file named responsive.css would replace it); it loads after the theme, + * hence the `html` prefix on conflicting selectors. + */ + +@media screen and (max-width: 899px) { + /* Top app bar */ + html #header { background-color: var(--md-sys-color-surface-container); color: var(--md-sys-color-on-surface); } + html #header a.mobile-toggle-button { color: var(--md-sys-color-on-surface-variant); border-inline-start: 0; } + html #project-jump .drdn-trigger { + border: 0; + color: var(--md-sys-color-on-surface); + font: var(--md-sys-typescale-title-large); + line-height: 50px; + } + html #project-jump.drdn { padding-inline-start: 8px; } + /* Header icons: Material Symbols masks instead of core's scaled "^" and "≡" text glyphs */ + html #project-jump .drdn-trigger:before { + content: ''; + inset-block: 0; + margin-block: auto; + inline-size: 24px; + block-size: 24px; + padding: 0; + transform: none; + opacity: 1; + background-color: var(--md-sys-color-on-surface-variant); + mask: url(../images/arrow-drop-down.svg) center / 24px no-repeat; + } + html #header a.mobile-toggle-button { + inline-size: 48px; + block-size: 48px; + margin-block-start: 8px; + margin-inline-end: 8px; + border-radius: var(--md-sys-shape-corner-full); + line-height: normal; + } + html #header a.mobile-toggle-button:hover { background-image: var(--nm-state-hover); } + html .mobile-toggle-button:after { + content: ''; + inline-size: 24px; + block-size: 24px; + margin: 12px; + background-color: currentColor; + mask: url(../images/menu.svg) center / 24px no-repeat; + } + + /* Flyout: navigation drawer */ + html .flyout-menu { + 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-menu h3 { + block-size: auto; + margin-block: 16px 4px; + padding-inline: 16px; + border: 0; + background-color: transparent; + color: var(--md-sys-color-on-surface-variant); + font: var(--md-sys-typescale-title-small); + letter-spacing: var(--md-sys-typescale-title-small-tracking); + text-transform: none; + } + html .flyout-menu h4 { color: var(--md-sys-color-on-surface); } + html .flyout-menu a { color: var(--md-sys-color-on-surface-variant); } + html .flyout-menu ul li a, html .flyout-menu ul li:first-child a { + block-size: 48px; + line-height: 48px; + margin-inline-start: 0; + padding-inline: 16px; + border: 0; + border-radius: var(--md-sys-shape-corner-full); + font: var(--md-sys-typescale-label-large); + line-height: 48px; + } + html .flyout-menu ul li a:hover { background-image: var(--nm-state-hover); } + html .flyout-menu ul li a.selected { + background-color: var(--md-sys-color-secondary-container); + color: var(--md-sys-color-on-secondary-container); + } + html .flyout-menu #watchers ul li, html .flyout-menu ul.queries li, html .flyout-menu__avatar { border-block-start: 0; } + html .flyout-menu .icon svg, html .flyout-menu .icon-only svg { color: inherit; stroke: currentColor; } + html .flyout-menu__search input[type='text'] { + block-size: 48px; + border-radius: var(--md-sys-shape-corner-full); + background-color: var(--md-sys-color-surface-container-highest); + color: var(--md-sys-color-on-surface); + padding-inline-start: 52px; + } + /* Search pill, avatar row and nav labels share one inline start: the icon and the + avatar sit where the nav labels begin (drawer padding 12px + item padding 16px) */ + html .flyout-menu .flyout-menu__search { padding-inline: 0; } + 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); } + /* 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 { + inline-size: 100%; + margin-block-end: 8px; + padding-inline-start: 0; + white-space: normal; + } + html :is(#content > .contextual > a, #content > .contextual .drdn, #content > [data-controller="quote-reply"] > .contextual > a, p.buttons a) { + display: inline-flex; + align-items: center; + block-size: 40px; + margin-block: 4px; + margin-inline: 0 4px; + padding-block: 0; + padding-inline: 16px; + border: 1px solid var(--md-sys-color-outline); + border-radius: var(--md-sys-shape-corner-full); + box-sizing: border-box; + font: var(--md-sys-typescale-label-large); + letter-spacing: var(--md-sys-typescale-label-large-tracking); + } + + /* A scrolling table takes its natural width instead of squeezing text columns to a + word per line; it scrolls inside its card */ + html .autoscroll > table.list { inline-size: max-content; min-inline-size: 100%; } + /* Tables core does not wrap scroll themselves, so they no longer push the whole content + area sideways; thead and tbody share one anonymous table, so columns stay aligned. + ponytail: a table narrower than the screen would leave a gap inside its border, so small + summary tables are excluded by class; add others here if one shows a gap */ + html #content table.list:not(.autoscroll > table, .issue-report) { display: block; overflow-x: auto; max-inline-size: 100%; } + + /* 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); } + html .tabular input[type="radio"] { inline-size: auto; } + + /* 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; } + html #content .jstTabs.tabs li.tab-elements { flex: 1 1 auto; min-inline-size: 0; block-size: auto; } + html #content .jstTabs .jstElements { display: flex; flex-wrap: wrap; white-space: normal; } + html #content .jstTabs.tabs li.tab-elements::before { display: none; } /* core's full-height strut for vertical centring */ + + /* Subtasks and related issues: the progress bar fits its cell, the subject leaves room + for both icon buttons */ + html :is(#issue_tree, #relations) td.done_ratio table.progress { inline-size: 100%; } + html :is(#issue_tree, #relations):not(.with-related-issues-table-headers) .issue:has(.buttons a) > td.subject { padding-inline-end: 72px; } + + html div#version-summary { margin-block-start: 16px; } + + /* Core gives the current, previous and next page a third of the row each */ + html .pagination ul.pages li:is(.current, .previous, .next) { inline-size: auto; overflow: visible; } + + /* The desktop sidebar card has no place on mobile; its content moves into the flyout */ + #sidebar { margin: 0; border-radius: 0; } +} diff --git a/tonalmine/stylesheets/pages.css b/tonalmine/stylesheets/pages.css new file mode 100644 index 0000000..5037c9a --- /dev/null +++ b/tonalmine/stylesheets/pages.css @@ -0,0 +1,493 @@ +/* + * Page-specific styles: issue view, journals, wiki content and code, + * activity, roadmap, calendar, Gantt. + */ + +/***** Issue view *****/ +/* .details: calendar entries are div.issue too */ +div.issue.details { + background: var(--md-sys-color-surface-container-low); + border-radius: var(--md-sys-shape-corner-medium); + padding: 24px; + margin-block-end: 16px; +} +div.issue div.subject h3 { font: var(--md-sys-typescale-headline-small); } +div.issue p.author { color: var(--md-sys-color-on-surface-variant); } +div.issue .attributes .attribute .label { color: var(--md-sys-color-on-surface-variant); font-weight: 500; } +div.issue hr { background: var(--md-sys-color-outline-variant); } +div.issue.overdue .due-date .value { color: var(--md-sys-color-error); } +div.issue table.list { background: var(--md-sys-color-surface); } + +/* Sticky issue header while scrolling: an M3 top app bar in its scrolled state + (surface-container, no shadow). Core hard-codes a white background. */ +div#sticky-issue-header { + padding-block: 12px; + padding-inline: 24px; + border-block-end: 1px solid var(--md-sys-color-outline-variant); + background-color: var(--md-sys-color-surface-container); + box-shadow: none; + color: var(--md-sys-color-on-surface-variant); + font: var(--md-sys-typescale-title-medium); +} +div#sticky-issue-header .subject { color: var(--md-sys-color-on-surface); font-weight: 500; } + +/* Progress bars inside list rows must not take the row padding and height */ +table.list table.progress td, table.list table.progress td:first-child { + block-size: 4px; + padding: 0; + border: 0; +} + +/***** Journals: each change is an outlined card, its header a band on top *****/ +.journals .journal { + margin-block-end: 12px; + background-color: var(--md-sys-color-surface-container-low); + border: 1px solid var(--md-sys-color-outline-variant); + border-radius: var(--md-sys-shape-corner-medium); + overflow: hidden; +} +.journals h4.journal-header { + margin: 0; + background-color: var(--md-sys-color-surface-container-high); + border-block-end: 1px solid var(--md-sys-color-outline-variant); + border-radius: 0; + padding-block: 8px; + padding-inline: 16px; + font: var(--md-sys-typescale-title-small); +} +.journals h4.journal-header .update-info { color: var(--md-sys-color-on-surface-variant); font: var(--md-sys-typescale-body-small); } +.journals div:target h4.journal-header { background-color: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container); } +.journals div.journal:has(div:target), .journals div.journal:target { border-color: var(--md-sys-color-primary); } +.journals .journal-content { margin-block-end: 0; padding-block: 4px 12px; padding-inline: 16px; } +.journals .journal-content :is(.journal-details, .wiki):last-child, .journals .journal-content .wiki > :last-child { margin-block-end: 0; } +.journals .private-notes { border-inline-start: 4px solid var(--md-sys-color-error); } +.journals .journal-details, ul.revision-info { color: var(--md-sys-color-on-surface-variant); } + +/***** Reading pages: the main content sits in one filled card, like the issue details *****/ +#content > div.wiki.wiki-page, +#content > div.news, +.controller-documents.action-show #content > div.wiki, +#content > [data-controller="quote-reply"] > div.message { + margin-block-end: 16px; + padding: 24px; + border-radius: var(--md-sys-shape-corner-medium); + background-color: var(--md-sys-color-surface-container-low); +} +/* The card's padding is the gap: no paragraph margin on top of it (news and messages + open with a floated reaction button) */ +:is(#content > div.wiki.wiki-page, #content > div.news, .controller-documents.action-show #content > div.wiki, + #content > [data-controller="quote-reply"] > div.message) { + & > :first-child, & > .reaction + * { margin-block-start: 0; } + & > :last-child, & > .wiki:last-child > :last-child { margin-block-end: 0; } +} +/* The wiki page's actions sit above its card, not on its edge */ +#content > div.wiki.wiki-page { clear: both; } + +/***** Wiki content *****/ +div.wiki table { padding: 0; border-collapse: separate; border-spacing: 0; border: 1px solid var(--md-sys-color-outline-variant); border-radius: var(--md-sys-shape-corner-medium); overflow: hidden; } +div.wiki td, div.wiki th { border: 0; border-block-start: 1px solid var(--md-sys-color-outline-variant); padding-block: 8px; padding-inline: 12px; } +div.wiki tr:first-child td, div.wiki tr:first-child th { border-block-start: 0; } +div.wiki th { background: var(--md-sys-color-surface-container); color: var(--md-sys-color-on-surface-variant); font: var(--md-sys-typescale-title-small); } +div.wiki a.new { color: var(--md-sys-color-error); } +/* Wiki text mostly sits in a card, so the TOC steps one tone up from it */ +div.wiki ul.toc { + background-color: var(--md-sys-color-surface-container); + border: 0; + border-radius: var(--md-sys-shape-corner-medium); + padding: 12px 16px; +} +div.wiki ul.toc li.heading { color: var(--md-sys-color-on-surface-variant); font: var(--md-sys-typescale-title-small); } + +div.wiki pre { + margin-inline: 0; + padding: 16px; + background-color: var(--md-sys-color-surface-container); + border: 0; + border-radius: var(--md-sys-shape-corner-medium); +} +div.wiki pre .syntaxhl, .syntaxhl { background: transparent; } +div.wiki *:not(pre) > code, div.wiki > code { + background: var(--md-sys-color-surface-container-high); + border-radius: var(--md-sys-shape-corner-extra-small); + padding-block: 0.1em; + padding-inline: 0.3em; +} + +/* Syntax highlighting: core Rouge colors are fixed hex values, regrouped here onto roles */ +.syntaxhl :is(.c, .ch, .cm, .cpf, .c1, .cs, .cp, .go, .w) { color: var(--md-sys-color-on-surface-variant); font-weight: normal; font-style: italic; } +.syntaxhl :is(.k, .kc, .kd, .kn, .kp, .kr, .ow) { color: var(--md-sys-color-primary); font-weight: 500; } +.syntaxhl :is(.nf, .fm, .na, .nt, .nb, .bp, .nd, .gh, .gu) { color: var(--md-sys-color-primary); font-weight: normal; } +.syntaxhl :is(.kt, .nc, .nn, .no, .m, .mb, .mf, .mh, .mi, .mo, .il) { color: var(--md-sys-color-tertiary); font-weight: normal; } +.syntaxhl :is(.s, .sa, .sb, .sc, .dl, .sd, .s2, .se, .sh, .si, .sx, .sr, .s1, .ss, .gi) { color: var(--md-sys-color-success); background-color: transparent; } +.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); } + +/***** Activity *****/ +/* One card per day (the
), entries are M3 list rows. Each entry is a
+
pair: + dt is a grid (type icon | headline | time, project below), dd holds the clamped + description and the author. Follow-up events on the same item hang off a thread rail. */ +div#activity h3 { + position: sticky; + inset-block-start: 0; + z-index: 1; + margin-block: 24px 8px; + padding-block: 8px; + padding-inline: 0; + background-color: var(--md-sys-color-surface); + color: var(--md-sys-color-on-surface); + font: var(--md-sys-typescale-title-medium); +} +div#activity h3:first-child { margin-block-start: 0; } +div#activity dl { + margin-inline-start: 0; + background-color: var(--md-sys-color-surface-container-low); + border: 1px solid var(--md-sys-color-outline-variant); + border-radius: var(--md-sys-shape-corner-medium); + overflow: hidden; +} + +div#activity dt, div#activity dl dt:first-child { + display: grid; + grid-template-columns: 40px minmax(0, 1fr) auto; + grid-template-areas: "icon title time" "icon project time"; + column-gap: 16px; + align-items: center; + inline-size: auto; + margin: 0; + padding-block: 12px 0; + padding-inline: 16px; + background: none; + border: 0; + border-block-start: 1px solid var(--md-sys-color-outline-variant); + border-radius: 0; +} +div#activity dl dt:first-child { border-block-start: 0; } + +/* Leading element: type icon in a tonal circle, author avatar as a badge on it */ +div#activity dt > svg.icon-svg { + grid-area: icon; + box-sizing: content-box; + padding: 11px; + margin: 0; + border-radius: var(--md-sys-shape-corner-full); + background-color: var(--md-sys-color-secondary-container); + color: var(--md-sys-color-on-secondary-container); +} +div#activity dt.issue > svg.icon-svg { background-color: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container); } +div#activity dt.issue-closed > svg.icon-svg { background-color: var(--md-sys-color-success-container); color: var(--md-sys-color-on-success-container); } +div#activity dt:is(.wiki-page, .document, .news, .message, .attachment) > svg.icon-svg { background-color: var(--md-sys-color-tertiary-container); color: var(--md-sys-color-on-tertiary-container); } +div#activity dt > .avatar { + grid-area: icon; + align-self: end; + justify-self: end; + inline-size: 18px; + block-size: 18px; + margin: 0 -6px -4px 0; + font-size: 7px; + line-height: 18px; + box-shadow: 0 0 0 2px var(--md-sys-color-surface-container-low); +} + +div#activity dt > a { grid-area: title; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; font: var(--md-sys-typescale-title-small); } +div#activity dt .time { grid-area: time; align-self: start; margin: 0; color: var(--md-sys-color-on-surface-variant); font: var(--md-sys-typescale-label-medium); } +div#activity dt.me .time { border-block-end: 0; } +div#activity span.project { grid-area: project; color: var(--md-sys-color-on-surface-variant); font: var(--md-sys-typescale-body-small); } +div#activity span.project::after { content: none; } + +div#activity dd { + margin: 0; + padding-block: 2px 12px; + padding-inline: 72px 16px; /* 16 padding + 40 icon + 16 gap: under the headline */ + background: none; + border: 0; + border-radius: 0; + font: var(--md-sys-typescale-body-medium); +} +div#activity dd span.description { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + margin-block: 0 2px; /* a top margin would collapse through dd */ + color: var(--md-sys-color-on-surface); + font-style: normal; +} +div#activity dd span.description:empty { display: none; } +div#activity dd .author { font: var(--md-sys-typescale-label-medium); } +/* No project line (entry from the current project): the icon still spans two rows, so pull the details up */ +div#activity dt:not(:has(> .project)) + dd { margin-block-start: -20px; } + +/* Thread: follow-up events hang off a rail starting under the first event's icon */ +div#activity dt.grouped, div#activity dd.grouped { + margin-inline-start: 36px; + box-shadow: inset 2px 0 0 var(--md-sys-color-outline-variant); +} +div#activity dt.grouped { + grid-template-columns: 32px minmax(0, 1fr) auto; + padding-inline-start: 18px; + border-block-start: 0; +} +div#activity dt.grouped > svg.icon-svg { padding: 7px; } +div#activity dd.grouped { padding-inline-start: 66px; } /* 18 + 32 + 16 */ + +/***** My page: each block is a card like .box; drop zones show only while dragging *****/ +.mypage-box { + padding: 16px; + background-color: var(--md-sys-color-surface-container-low); + color: var(--md-sys-color-on-surface); + border: 0; + border-radius: var(--md-sys-shape-corner-medium); + box-shadow: none; +} +.mypage-box .autoscroll { margin-block-end: 0; } +.block-receiver { border-color: transparent; border-radius: var(--md-sys-shape-corner-medium); } +.dragging .block-receiver { border-color: var(--md-sys-color-outline-variant); } + +/***** Search results *****/ +/* The type checkboxes' legend is only a "check all" icon: it leads their row */ +#search-form fieldset.box > legend { inline-size: auto; padding: 0; margin-inline-end: 8px; } +#search-form fieldset.box > legend ~ * { clear: none; } +/* A checkbox stays with its label when the row wraps */ +#search-form label { display: inline-block; white-space: nowrap; } +/* Results start at the content edge; the title wraps under its project, not beside it */ +#search-results { margin-inline-start: 0; } +#search-results dt { display: block; } +div#activity dt .time, #search-results dt .time { color: var(--md-sys-color-on-surface-variant); } +#search-results dd span.description { color: var(--md-sys-color-on-surface-variant); } + +/***** Projects board: each root project is a filled card *****/ +#projects-index ul.projects li.root { + padding: 16px; + border: 0; + border-radius: var(--md-sys-shape-corner-medium); + background-color: var(--md-sys-color-surface-container-low); + box-shadow: none; +} +#projects-index li.root ul.projects { border-inline-start: 1px solid var(--md-sys-color-outline-variant); } + +/***** Documents: one filled card per category, documents are list rows inside *****/ +#document-list .document-group { + margin-block-end: 16px; + padding-block: 8px; + padding-inline: 16px; + border-radius: var(--md-sys-shape-corner-medium); + background-color: var(--md-sys-color-surface-container-low); +} +#document-list .document-group h3 { + margin: 0; + padding-block: 8px; + padding-inline: 0; + background-color: transparent; + color: var(--md-sys-color-on-surface); + font: var(--md-sys-typescale-title-medium); +} +#document-list .document-item { padding-block: 12px; } +#document-list .document-item + .document-item { border-block-start: 1px solid var(--md-sys-color-outline-variant); } +#document-list .document-group h3 + .document-item { padding-block-start: 4px; } + +/***** News list: each article is a filled card *****/ +#news-list article.news-article { + margin-block-end: 12px; + padding: 16px; + border-radius: var(--md-sys-shape-corner-medium); + background-color: var(--md-sys-color-surface-container-low); +} +/* Hanging indent: a wrapped title and the comment count line up past the avatar */ +#news-list article.news-article header h3 { + margin-block: 0 4px; + padding-inline-start: 32px; + text-indent: -32px; + font: var(--md-sys-typescale-title-medium); +} +#news-list article.news-article header h3 .avatar { margin-inline-end: 4px; vertical-align: middle; text-indent: 0; } /* + the space after it = 8px */ +/* Author and text line up with the title, past the 24px avatar and its gap */ +#news-list article.news-article header p.author, +#news-list article.news-article .wiki { margin-inline-start: 32px; } +#news-list article.news-article header p.author { margin-block: 0 8px; color: var(--md-sys-color-on-surface-variant); } +#news-list article.news-article .wiki > :last-child { margin-block-end: 0; } + +/***** Workflow: allowed transitions use the selected state *****/ +table.transitions td.enabled { background-color: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container); } +table.fields_permissions td.readonly { background-color: var(--md-sys-color-surface-container-highest); } +table.fields_permissions td.required { background-color: var(--md-sys-color-error-container); color: var(--md-sys-color-on-error-container); } + +/***** Attachments, diffs, imports, headings *****/ +div.thumbnail { + overflow: hidden; + border: 1px solid var(--md-sys-color-outline-variant); + border-radius: var(--md-sys-shape-corner-small); + background: transparent; +} +div.thumbnail img { margin: 0; } +pre.text-diff { + padding: 16px; + border-color: var(--md-sys-color-outline-variant); + border-radius: var(--md-sys-shape-corner-medium); +} +h2 .avatar, h3 .avatar { margin-inline-end: 8px; } +/* "Replies (2)": the icon heading is a flex row, which drops the space before the count */ +h3.comments .icon-label { margin-inline-end: 0.25em; } +#wiki_page_parent_id { min-inline-size: 240px; } +select#columns, select#criterias { margin-inline: 8px 16px; } +input#months { inline-size: 72px; } +/* Tabs start below the floated page actions, so their divider spans the page */ +#content > .tabs { clear: both; } + +/***** Roadmap *****/ +div#roadmap .version-article { + padding: 16px; + margin-block-end: 16px; + 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#version-summary { background-color: transparent; } + +/* Overview and issue table share one left edge (core indents the overview by 1.5rem) */ +div#roadmap article.version-article .version-overview, +div#roadmap article.version-article .wiki, +div#roadmap article.version-article form { padding-inline-start: 0; } +div#roadmap .related-issues td.assigned_to { padding-inline: 12px 4px; } +div#roadmap .related-issues caption { padding-block-end: 8px; color: var(--md-sys-color-on-surface-variant); font: var(--md-sys-typescale-title-small); } + +/* Progress bar spans the width of the issue table below it; the percentage sits at its end */ +.version-overview table.progress { inline-size: calc(100% - 56px); margin-block: 6px; } +.version-overview p.percent { font: var(--md-sys-typescale-label-large); line-height: 20px; text-align: end; } +.version-overview p.progress-info { margin-block: 4px 0; color: var(--md-sys-color-on-surface-variant); font: var(--md-sys-typescale-body-small); } +/* Version page: the issue table is 70% wide next to the summary */ +body.controller-versions.action-show .version-overview table.progress { inline-size: calc(70% - 56px); } +body.controller-versions.action-show .version-overview p.percent { inline-size: 70%; } + +/***** Calendar *****/ +ul.cal { + overflow: hidden; + border-color: var(--md-sys-color-outline-variant); + border-radius: var(--md-sys-shape-corner-medium); + box-shadow: none; +} +.cal .calbody { box-shadow: none; } +.cal .calhead, .cal .week-number { background-color: var(--md-sys-color-surface-container); color: var(--md-sys-color-on-surface-variant); } +.cal .calbody { border-color: var(--md-sys-color-outline-variant); } +.cal .calbody.this-month { background-color: var(--md-sys-color-surface); } +.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); } + +/***** 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 + * `header height + 8 + 20n` in the subjects column and `7 + 20n` inside #gantt_draw_area. + * Bars are centered on that 16px line; row bands run from 2px above to 2px below it. + */ +:root { + --nm-gantt-band: color-mix(in srgb, var(--md-sys-color-on-surface) 4%, transparent); + --nm-gantt-rows: repeating-linear-gradient(to bottom, var(--nm-gantt-band) 0 20px, transparent 20px 40px); + --nm-gantt-track: color-mix(in srgb, var(--md-sys-color-primary) 25%, var(--md-sys-color-surface-container-highest)); + --nm-gantt-subjects-offset: 2px; /* (header height + 6) mod 20; the header has one 18px row per zoom level */ +} +:root:has(#zoom[value="1"]) { --nm-gantt-subjects-offset: 4px; } +:root:has(#zoom[value="3"]) { --nm-gantt-subjects-offset: 0px; } +:root:has(#zoom[value="4"]) { --nm-gantt-subjects-offset: 18px; } + +/* Redmine's geometry is fixed at 20px rows; `zoom` scales rows, bars, text and positions together. + The relation/progress SVG is drawn by JS from on-screen (already zoomed) positions, so it must + not be zoomed a second time. */ +:root { --nm-gantt-zoom: 1.3; } +html table.gantt-table { zoom: var(--nm-gantt-zoom); } +html #gantt_draw_area svg { zoom: calc(1 / var(--nm-gantt-zoom)); overflow: visible; } + +html #gantt_draw_area { background: var(--nm-gantt-rows) 0 5px; } +html .gantt_subjects_container { background: var(--nm-gantt-rows) 0 var(--nm-gantt-subjects-offset); } + +/* Header: months / weeks / days */ +html .gantt_hdr { border-color: var(--md-sys-color-outline-variant); color: var(--md-sys-color-on-surface-variant); font: var(--md-sys-typescale-label-medium); line-height: 18px; } +html .gantt_hdr[style*="background"] { background: var(--md-sys-color-surface-container) !important; } +html .gantt_hdr.nwday { background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 5%, transparent); color: var(--md-sys-color-on-surface-variant); } +html .gantt_hdr a { color: var(--md-sys-color-on-surface-variant); } + +/* Subjects */ +html .gantt_subjects, html .gantt_selected_column_content.gantt_hdr { font: var(--md-sys-typescale-body-small); letter-spacing: var(--md-sys-typescale-body-small-tracking); } +html .gantt_subjects div { line-height: 16px; } +html .gantt_subjects div.issue-subject:hover { background-color: color-mix(in srgb, var(--md-sys-color-on-surface) var(--md-sys-state-hover-state-layer-opacity), transparent); } +html .gantt_subjects div.project-name, html .gantt_subjects div.version-name { font-weight: 500; } + +/* Issue bars: 10px pills centered on the text line (core: 8px at the top of it) */ +html .task_todo, html .task_done, html .task_late { + block-size: 10px; + margin-block-start: 3px; + box-sizing: border-box; + border-radius: var(--md-sys-shape-corner-full); + background-image: none; +} +html .task_todo { background-color: var(--nm-gantt-track); border-color: var(--nm-gantt-track); } +html .task_done { background-color: var(--md-sys-color-primary); border-color: var(--md-sys-color-primary); } +html .task_late { background-color: var(--md-sys-color-error); border-color: var(--md-sys-color-error); } + +/* Parent issues: thin bar with triangles at both ends */ +html .task_todo.parent, html .task_done.parent, html .task_late.parent { block-size: 4px; margin-block-start: 6px; border-radius: 0; } +html .task_todo.parent { background-color: var(--md-sys-color-on-surface-variant); border-color: var(--md-sys-color-on-surface-variant); } +html .task.parent.marker.starting, html .task.parent.marker.ending { + background: var(--md-sys-color-on-surface-variant); + clip-path: polygon(0 7px, 8px 7px, 4px 14px); +} + +/* Project and version milestones: 2px line with a diamond marker */ +html .project.task_todo, html .project.task_done, html .project.task_late, +html .version.task_todo, html .version.task_done, html .version.task_late { + block-size: 2px; + margin-block-start: 7px; + border: 0; + border-radius: 0; +} +html :is(.project, .version):is(.task_todo, .task_done, .task_late) { background-image: none; } +html :is(.project, .version).task_todo { background-color: var(--md-sys-color-outline); } +html :is(.project, .version).task_done { background-color: var(--md-sys-color-primary); } +html :is(.project, .version).task_late { background-color: var(--md-sys-color-error); } +html .version.marker, html .project.marker { + block-size: 10px; + margin-block-start: 3px; + background: var(--md-sys-color-tertiary); + clip-path: polygon(4px 0, 8px 5px, 4px 10px, 0 5px); +} +html .project.marker { background: var(--md-sys-color-primary); } + +/* Labels next to bars ("New 80%") */ +html .task { color: var(--md-sys-color-on-surface-variant); font: var(--md-sys-typescale-label-small); line-height: 16px; } +html .task.label.project, html .task.label.version { color: var(--md-sys-color-on-surface); font-weight: 500; } +html .version-behind-schedule a, html .issue-behind-schedule a { color: var(--md-sys-color-warning); } +html .version-overdue a, html .issue-overdue a, html .project-overdue a { color: var(--md-sys-color-error); } + +/* Today line (inline style); the progress line takes its color from it */ +html #today_line { border-inline-start: 2px solid var(--md-sys-color-error) !important; } + +/* Relation arrows are drawn with fixed colors per relation type */ +html #gantt_draw_area path[stroke="#228be6"] { stroke: var(--md-sys-color-primary); } +html #gantt_draw_area path[fill="#228be6"] { fill: var(--md-sys-color-primary); } +html #gantt_draw_area path[stroke="#fa5252"] { stroke: var(--md-sys-color-error); } +html #gantt_draw_area path[fill="#fa5252"] { fill: var(--md-sys-color-error); } + +/***** Issue context menu (context_menu.css loads after the theme) *****/ +html #context-menu { font: var(--md-sys-typescale-label-large); } +html #context-menu ul { + background: var(--md-sys-color-surface-container); + border: 0; + border-radius: var(--md-sys-shape-corner-extra-small); + box-shadow: var(--md-sys-elevation-level2); + padding-block: 8px; +} +html #context-menu li { border: 0; border-radius: 0; } +html #context-menu li:hover { border: 0; border-radius: 0; background-color: color-mix(in srgb, var(--md-sys-color-on-surface) var(--md-sys-state-hover-state-layer-opacity), transparent); } +html #context-menu a { color: var(--md-sys-color-on-surface); padding-block: 8px; } +html #context-menu a:hover { color: var(--md-sys-color-on-surface); text-decoration: none; } +html #context-menu a.disabled, html #context-menu a.disabled:hover { color: color-mix(in srgb, var(--md-sys-color-on-surface) var(--md-sys-state-disabled-content-opacity), transparent); } +html #context-menu a.disabled svg.icon-svg { color: inherit; } + +.cal .calbody div.issue { + margin-block: 4px; + padding-block: 4px; + padding-inline: 8px; + background-color: var(--md-sys-color-surface-container); + border-radius: var(--md-sys-shape-corner-small); + font: var(--md-sys-typescale-body-small); +} diff --git a/tonalmine/stylesheets/palettes.css b/tonalmine/stylesheets/palettes.css new file mode 100644 index 0000000..5b6114d --- /dev/null +++ b/tonalmine/stylesheets/palettes.css @@ -0,0 +1,35 @@ +/* + * Generated by tools/palettes/build_palettes.mjs; do not edit by hand. + * M3 color roles (TonalSpot, contrast 0) per palette, light and dark. + * picks the palette, forces a mode; + * without data-mode the OS setting (prefers-color-scheme) decides. + */ + +:root { color-scheme: light; } +:root[data-mode="dark"] { color-scheme: dark; } +@media (prefers-color-scheme: dark) { :root:not([data-mode="light"]) { color-scheme: dark; } } + +/* purple (seed #6750A4) */ +: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; } } + +/* 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; } } + +/* 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; } } + +/* 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; } } diff --git a/tonalmine/stylesheets/tables.css b/tonalmine/stylesheets/tables.css new file mode 100644 index 0000000..4fc0455 --- /dev/null +++ b/tonalmine/stylesheets/tables.css @@ -0,0 +1,159 @@ +/* + * Data tables (M3 has no table spec): surface rows, outline-variant dividers, + * title-small headers, hover state layer, secondary-container selection. + * Compact density: ~40px rows. + */ + +table.list, .table-list { + font: var(--md-sys-typescale-body-medium); + letter-spacing: var(--md-sys-typescale-body-medium-tracking); + font-variant-numeric: tabular-nums; + border: 1px solid var(--md-sys-color-outline-variant); + border-radius: var(--md-sys-shape-corner-medium); + border-collapse: separate; + border-spacing: 0; + margin-block-end: 8px; +} +/* A table that scrolls sideways: the wrapper is the outlined card, so the card edge and + corners stay visible while the table scrolls inside it */ +.autoscroll:has(> table.list) { + clear: both; /* scrolling makes it narrow beside a float instead of going below it */ + padding: 0; + margin-block-end: 8px; + border: 1px solid var(--md-sys-color-outline-variant); + border-radius: var(--md-sys-shape-corner-medium); +} +.autoscroll > table.list { margin: 0; border: 0; border-radius: 0; } +table.list th, .table-list-header { + background-color: var(--md-sys-color-surface-container); + color: var(--md-sys-color-on-surface-variant); + font: var(--md-sys-typescale-title-small); + letter-spacing: var(--md-sys-typescale-title-small-tracking); + padding-block: 10px; + padding-inline: 8px; + border-block-end: 1px solid var(--md-sys-color-outline-variant); +} +table.list th a, table.list th a:link, table.list th a:visited { color: var(--md-sys-color-on-surface-variant); } +table.list td { + block-size: 40px; + padding-block: 4px; + padding-inline: 8px; + border-block-start: 1px solid var(--md-sys-color-outline-variant); + background-color: transparent; +} +table.list tbody tr:first-child td, table.list tbody tr.group + tr td { border-block-start: 0; } + +/* No striping: M3 separates rows with dividers */ +table.list:not(.odd-even) tbody tr:nth-child(odd), table.list:not(.odd-even) tbody tr:nth-child(even), +.odd, .even, .box table.list:not(.odd-even) tbody tr:nth-child(even), .box .even { + background-color: transparent; +} +table.list:not(.odd-even) tbody tr:nth-child(odd):hover, table.list:not(.odd-even) tbody tr:nth-child(even):hover, +.odd:hover, .even:hover { + background-color: color-mix(in srgb, var(--md-sys-color-on-surface) var(--md-sys-state-hover-state-layer-opacity), transparent); +} + +/* Status names in the workflow header are td cells: same band as the th row above */ +table.workflows thead td { + background-color: var(--md-sys-color-surface-container); + color: var(--md-sys-color-on-surface-variant); + font: var(--md-sys-typescale-title-small); + letter-spacing: var(--md-sys-typescale-title-small-tracking); + border-block-end: 1px solid var(--md-sys-color-outline-variant); +} +/* Role names in permission group rows sit on the group name's line */ +table.permissions tr.group td { vertical-align: middle; } +/* Field permissions: the "»" copy link stays next to its select */ +table.fields_permissions td { white-space: nowrap; } +/* An empty table: no divider under the header, the table border closes it */ +table.list:not(:has(tbody tr)) th { border-block-end: 0; } +/* Child projects: chevron in the theme's colour, one file per mode */ +tr.idnt :is(td.subject, td.name) { background-image: url(../images/chevron-right-idnt.svg); } +[dir="rtl"] tr.idnt :is(td.subject, td.name) { background-image: url(../images/chevron-left-idnt.svg); } +:root[data-mode="dark"] tr.idnt :is(td.subject, td.name) { background-image: url(../images/chevron-right-idnt-dark.svg); } +:root[data-mode="dark"][dir="rtl"] tr.idnt :is(td.subject, td.name) { background-image: url(../images/chevron-left-idnt-dark.svg); } +@media (prefers-color-scheme: dark) { + :root:not([data-mode="light"]) tr.idnt :is(td.subject, td.name) { background-image: url(../images/chevron-right-idnt-dark.svg); } + :root:not([data-mode="light"])[dir="rtl"] tr.idnt :is(td.subject, td.name) { background-image: url(../images/chevron-left-idnt-dark.svg); } +} + +/* Repository file, annotate and diff views: outlined like table.list, line numbers + separated by one divider instead of a grid (scm.css loads after the theme, hence html) */ +html table.filecontent { + inline-size: 100%; + overflow: hidden; + border: 1px solid var(--md-sys-color-outline-variant); + border-collapse: separate; + border-spacing: 0; + border-radius: var(--md-sys-shape-corner-medium); + background-color: var(--md-sys-color-surface-container-lowest); +} +html table.filecontent th, html table.filecontent th.line-num { + border: 0; + border-inline-end: 1px solid var(--md-sys-color-outline-variant); + background-color: var(--md-sys-color-surface-container); +} +html table.filecontent th.filename { + padding-block: 10px; + padding-inline: 12px; + border-inline-end: 0; + border-block-end: 1px solid var(--md-sys-color-outline-variant); + color: var(--md-sys-color-on-surface-variant); + font: var(--md-sys-typescale-title-small); +} +/* CSV import preview */ +.sample-data { + margin: 0; + overflow: hidden; + border: 1px solid var(--md-sys-color-outline-variant); + border-collapse: separate; + border-spacing: 0; + border-radius: var(--md-sys-shape-corner-medium); + background-color: transparent; +} +.sample-data td { padding-block: 8px; padding-inline: 12px; border: 0; border-block-start: 1px solid var(--md-sys-color-outline-variant); } +.sample-data tr:first-child td { border-block-start: 0; background-color: var(--md-sys-color-surface-container); } + +tr.group td { + padding-block: 12px 8px; + padding-inline: 8px; + border-block-end: 1px solid var(--md-sys-color-outline-variant); + background-color: var(--md-sys-color-surface-container-low); + color: var(--md-sys-color-on-surface); +} +tr.group span.name { font-weight: 500; } +tr.group span.totals, tr.group a.toggle-all { color: var(--md-sys-color-on-surface-variant); } +tr.group span.totals .value { color: var(--md-sys-color-on-surface); font-weight: 500; } +table.list tbody tr.group:hover { background-color: transparent; } + +/* Selected rows (context menu); core uses !important */ +html .context-menu-selection, html .context-menu-selection:hover { + background-color: var(--md-sys-color-secondary-container) !important; + color: var(--md-sys-color-on-secondary-container) !important; +} +html .context-menu-selection a, html .context-menu-selection a:hover { color: var(--md-sys-color-on-secondary-container) !important; } +html .context-menu-selection svg.icon-svg { stroke: var(--md-sys-color-on-secondary-container) !important; } + +table.list tr.overdue:not(.context-menu-selection) td.due_date { color: var(--md-sys-color-error); } +a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: var(--md-sys-color-on-surface-variant); } + +.query-totals { color: var(--md-sys-color-on-surface-variant); } +.query-totals .value { color: var(--md-sys-color-on-surface); font-weight: 500; } +#query_form_content { font-size: inherit; } +#query_form_with_buttons > p.contextual { font: var(--md-sys-typescale-label-large); } + +/* Query filters: checkbox + label sit level with the 40px operator/value fields. The field cell is + 40px high and centers its content; the row stays top-aligned so an expanded multi-select grows + downwards without dragging the label to its middle. */ +#filters-table .filter { align-items: flex-start; margin-block: 4px 0; } +#filters-table .filter .field { display: flex; align-items: center; gap: 8px; min-block-size: 40px; } +#filters-table .filter .field 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; } +/* 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. + "between" filters show two fields (both spans visible): each takes half. */ +#filters-table .filter .values select.value, +#filters-table .filter .values input.value:not([size="3"]) { inline-size: 260px; } +#filters-table .filter .values:has(> span:first-child + span:not([style*="none"])) input.value { inline-size: 126px; padding-inline: 8px; } diff --git a/tonalmine/stylesheets/tokens.css b/tonalmine/stylesheets/tokens.css new file mode 100644 index 0000000..cdd27e4 --- /dev/null +++ b/tonalmine/stylesheets/tokens.css @@ -0,0 +1,74 @@ +/* + * Material Design 3 system tokens (type, shape, state, motion, elevation). + * Color roles live in palettes.css, generated by tools/palettes/build_palettes.mjs. + */ + +:root { + /* Typeface */ + --md-ref-typeface-plain: Roboto, system-ui, sans-serif; + + /* Type scale, as `font` shorthand values: weight size/line-height family */ + --md-sys-typescale-display-large: 400 3.5625rem/4rem var(--md-ref-typeface-plain); + --md-sys-typescale-display-medium: 400 2.8125rem/3.25rem var(--md-ref-typeface-plain); + --md-sys-typescale-display-small: 400 2.25rem/2.75rem var(--md-ref-typeface-plain); + --md-sys-typescale-headline-large: 400 2rem/2.5rem var(--md-ref-typeface-plain); + --md-sys-typescale-headline-medium: 400 1.75rem/2.25rem var(--md-ref-typeface-plain); + --md-sys-typescale-headline-small: 400 1.5rem/2rem var(--md-ref-typeface-plain); + --md-sys-typescale-title-large: 400 1.375rem/1.75rem var(--md-ref-typeface-plain); + --md-sys-typescale-title-medium: 500 1rem/1.5rem var(--md-ref-typeface-plain); + --md-sys-typescale-title-small: 500 0.875rem/1.25rem var(--md-ref-typeface-plain); + --md-sys-typescale-body-large: 400 1rem/1.5rem var(--md-ref-typeface-plain); + --md-sys-typescale-body-medium: 400 0.875rem/1.25rem var(--md-ref-typeface-plain); + --md-sys-typescale-body-small: 400 0.75rem/1rem var(--md-ref-typeface-plain); + --md-sys-typescale-label-large: 500 0.875rem/1.25rem var(--md-ref-typeface-plain); + --md-sys-typescale-label-medium: 500 0.75rem/1rem var(--md-ref-typeface-plain); + --md-sys-typescale-label-small: 500 0.6875rem/1rem var(--md-ref-typeface-plain); + + /* Tracking (letter-spacing) per type role */ + --md-sys-typescale-display-large-tracking: -0.25px; + --md-sys-typescale-title-medium-tracking: 0.15px; + --md-sys-typescale-title-small-tracking: 0.1px; + --md-sys-typescale-body-large-tracking: 0.5px; + --md-sys-typescale-body-medium-tracking: 0.25px; + --md-sys-typescale-body-small-tracking: 0.4px; + --md-sys-typescale-label-large-tracking: 0.1px; + --md-sys-typescale-label-medium-tracking: 0.5px; + --md-sys-typescale-label-small-tracking: 0.5px; + + /* Shape */ + --md-sys-shape-corner-none: 0; + --md-sys-shape-corner-extra-small: 4px; + --md-sys-shape-corner-small: 8px; + --md-sys-shape-corner-medium: 12px; + --md-sys-shape-corner-large: 16px; + --md-sys-shape-corner-extra-large: 28px; + --md-sys-shape-corner-full: 9999px; + + /* State layer opacities */ + --md-sys-state-hover-state-layer-opacity: 8%; + --md-sys-state-focus-state-layer-opacity: 10%; + --md-sys-state-pressed-state-layer-opacity: 10%; + --md-sys-state-dragged-state-layer-opacity: 16%; + --md-sys-state-disabled-content-opacity: 38%; + --md-sys-state-disabled-container-opacity: 12%; + + /* Motion */ + --md-sys-motion-duration-short2: 100ms; + --md-sys-motion-duration-short4: 200ms; + --md-sys-motion-duration-medium2: 300ms; + --md-sys-motion-duration-medium4: 400ms; + --md-sys-motion-duration-long2: 500ms; + --md-sys-motion-easing-standard: cubic-bezier(0.2, 0, 0, 1); + --md-sys-motion-easing-standard-decelerate: cubic-bezier(0, 0, 0, 1); + --md-sys-motion-easing-standard-accelerate: cubic-bezier(0.3, 0, 1, 1); + --md-sys-motion-easing-emphasized-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1); + --md-sys-motion-easing-emphasized-accelerate: cubic-bezier(0.3, 0, 0.8, 0.15); + + /* Elevation (shadow part; M3 prefers surface-container roles for depth) */ + --md-sys-elevation-level0: none; + --md-sys-elevation-level1: 0 1px 2px rgb(0 0 0 / 30%), 0 1px 3px 1px rgb(0 0 0 / 15%); + --md-sys-elevation-level2: 0 1px 2px rgb(0 0 0 / 30%), 0 2px 6px 2px rgb(0 0 0 / 15%); + --md-sys-elevation-level3: 0 1px 3px rgb(0 0 0 / 30%), 0 4px 8px 3px rgb(0 0 0 / 15%); + --md-sys-elevation-level4: 0 2px 3px rgb(0 0 0 / 30%), 0 6px 10px 4px rgb(0 0 0 / 15%); + --md-sys-elevation-level5: 0 4px 4px rgb(0 0 0 / 30%), 0 8px 12px 6px rgb(0 0 0 / 15%); +} diff --git a/tonalmine/stylesheets/typography.css b/tonalmine/stylesheets/typography.css new file mode 100644 index 0000000..f14f99c --- /dev/null +++ b/tonalmine/stylesheets/typography.css @@ -0,0 +1,40 @@ +/* + * One type scale for the whole app. Core Redmine sets ~60 relative sizes (0.8125rem, 0.9em, 93%, …) + * that compound when nested, e.g. the sidebar at 13px and its "Add" link at 11.7px. + * Here they are mapped onto the M3 scale: 14px (body-medium) for anything readable, + * 12px (body-small) for secondary meta text, headings on the heading scale. + * e2e/check-font-sizes.mjs reports anything that falls off the scale again. + */ + +/* 14px: content, lists, actions, sidebars, forms */ +#sidebar, .contextual, .contextual input, .contextual select, .buttons, #query_form_content, +#query_form_with_buttons > p.contextual, .query-totals, span.search_for_watchers, span.add_attachment, +table.issues td.block_column, tr.message td.last_message, table.boards td.last-message, +.tabular.settings p, #errorExplanation ul, .conflict-details, div.wiki ul.toc a, +#document-list .document-item .wiki.description, table.permissions td.role, #workflow_form table select, +tr.group span.totals, tr.group a.toggle-all, ul.properties, #sidebar h3 + .contextual, #search-results dd { + font-size: 0.875rem; +} +/* Parts of a number (hours "8:00") keep the number's size */ +.hours-dec, table.time-entries td.hours .hours-dec, table#time-report .hours-dec, .total-hours span.hours-int { + font-size: inherit; +} + +/* 12px: secondary meta text (who/when, stats, breadcrumbs, footers of blocks) */ +p.breadcrumb, p.subtitle, p.footnote, div.issue p.author, div.issue div.subject p, +#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 { + font-size: 0.75rem; +} +div.issue span.private, div.journal span.private { font-size: 0.6875rem; } + +/* Headings */ +.wiki h1, #content .wiki h1 { font: var(--md-sys-typescale-headline-small); } +.wiki h2, #content .wiki h2 { font: var(--md-sys-typescale-title-large); } +.wiki h3, #content .wiki h3, div#roadmap .wiki h1, div#roadmap .wiki h2 { font: var(--md-sys-typescale-title-medium); letter-spacing: var(--md-sys-typescale-title-medium-tracking); } +.wiki h4, #content .wiki h4 { font: var(--md-sys-typescale-title-small); letter-spacing: var(--md-sys-typescale-title-small-tracking); } +#document-list .document-item h4.title { font: var(--md-sys-typescale-title-medium); } +.total-hours { font: var(--md-sys-typescale-title-medium); } +.cal .calbody p.day-num { font: var(--md-sys-typescale-label-large); } diff --git a/tools/build_icons.rb b/tools/build_icons.rb new file mode 100644 index 0000000..8708ae3 --- /dev/null +++ b/tools/build_icons.rb @@ -0,0 +1,64 @@ +# Builds tonalmine/images/icons.svg from tools/icon_map.yml. +# Usage (from repo root): ruby tools/build_icons.rb +# Ruby stdlib only. Downloads are cached in tools/.cache/. +require 'yaml' +require 'net/http' +require 'uri' +require 'fileutils' + +CORE_ICONS = 'https://raw.githubusercontent.com/redmine/redmine/7.0-stable/config/icon_source.yml' +SYMBOLS = 'https://raw.githubusercontent.com/google/material-design-icons/master/symbols/web' +ROOT = File.expand_path('..', __dir__) +CACHE = File.join(ROOT, 'tools/.cache') +OUT = File.join(ROOT, 'tonalmine/images/icons.svg') + +def fetch(url) + res = Net::HTTP.get_response(URI(url)) + res.is_a?(Net::HTTPSuccess) ? res.body : nil +end + +def fail_with(message, items) + warn "#{message}:\n #{items.join("\n ")}" + exit 1 +end + +# 1. Map must cover exactly the core icon names. +core_yml = fetch(CORE_ICONS) or fail_with('Could not fetch', [CORE_ICONS]) +core = YAML.safe_load(core_yml).map { |e| e['name'] }.uniq +map = YAML.load_file(File.join(ROOT, 'tools/icon_map.yml')) +names = map.map { |e| e['name'] } +dupes = names.select { |n| names.count(n) > 1 }.uniq +fail_with('Duplicate names in icon_map.yml', dupes) if dupes.any? +missing = core - names +extra = names - core +fail_with('Missing from icon_map.yml', missing) if missing.any? +fail_with('Not in Redmine icon_source.yml (extra)', extra) if extra.any? + +# 2. Download (or read cached) SVGs. +FileUtils.mkdir_p(CACHE) +not_found = [] +symbols = map.sort_by { |e| e['name'] }.map do |e| + file = "#{e['symbol']}#{e['fill'] ? '_fill1' : ''}_24px.svg" + cached = File.join(CACHE, file) + unless File.exist?(cached) + svg = fetch("#{SYMBOLS}/#{e['symbol']}/materialsymbolsoutlined/#{file}") + next not_found << "#{e['name']} -> #{file}" unless svg + File.write(cached, svg) + end + paths = File.read(cached).scan(/]*\bd="([^"]+)"/).flatten + fail_with('No found in', [cached]) if paths.empty? + body = paths.map { |d| %() }.join + %(#{body}) +end +fail_with('Material Symbol not found', not_found) if not_found.any? + +# 3. Write the sprite. +File.write(OUT, <<~SVG) + + + #{symbols.join("\n")} + +SVG +puts "Wrote #{symbols.size} icons to #{OUT.sub("#{ROOT}/", '')}" diff --git a/tools/icon_map.yml b/tools/icon_map.yml new file mode 100644 index 0000000..54247ac --- /dev/null +++ b/tools/icon_map.yml @@ -0,0 +1,242 @@ +# Redmine 7.0 icon name -> Material Symbols (Outlined, weight 400) name. +# Order follows Redmine's config/icon_source.yml. `fill: true` uses the filled variant. +# Rebuild the sprite with: ruby tools/build_icons.rb +- name: edit + symbol: edit +- name: add + symbol: add_circle +- name: copy + symbol: content_copy +- name: del + symbol: delete +- name: save + symbol: save +- name: download + symbol: download +- name: attachment + symbol: attach_file +- name: time-add + symbol: more_time +- name: time + symbol: schedule +- name: fav + symbol: star +- name: copy-link + symbol: link # note: core uses a clipboard; "link" reads better for "Copy link" +- name: 3-bullets + symbol: more_horiz +- name: history + symbol: history +- name: folder + symbol: folder +- name: folder-open + symbol: folder_open +- name: lock + symbol: lock +- name: unlock + symbol: lock_open +- name: link-break + symbol: link_off +- name: zoom-in + symbol: zoom_in +- name: zoom-out + symbol: zoom_out +- name: settings + symbol: settings +- name: news + symbol: newspaper +- name: user + symbol: person +- name: group + symbol: group +- name: bookmarked + symbol: bookmark +- name: bookmark-delete + symbol: bookmark_remove +- name: bookmark-add + symbol: bookmark_add +- name: import + symbol: database_upload +- name: summary + symbol: bolt # note: mirrors core's Tabler "bolt" +- name: angle-down + symbol: expand_more +- name: angle-right + symbol: chevron_right +- name: angle-up + symbol: expand_less +- name: angle-left + symbol: chevron_left +- name: email + symbol: mail +- name: email-disabled + symbol: mail_off +- name: stats + symbol: bar_chart +- name: reorder + symbol: reorder # note: Tabler "menu-order" has up/down arrows; Material is plain lines +- name: close + symbol: close # note: plain X, no square frame +- name: checked + symbol: check +- name: reload + symbol: refresh +- name: link + symbol: link +- name: plugins + symbol: extension +- name: roles + symbol: admin_panel_settings # note: Tabler "shield-cog" +- name: list + symbol: list +- name: workflows + symbol: account_tree # note: Tabler "jump-rope" has no Material equivalent +- name: server-authentication + symbol: dns +- name: table-multiple + symbol: refresh # note: mirrors core, which also draws "refresh" here +- name: projects + symbol: inventory_2 # note: Tabler "packages" +- name: project + symbol: inventory_2 # note: Tabler "packages" +- name: package + symbol: package_2 +- name: custom-fields + symbol: dynamic_form # note: Tabler "input-check" +- name: help + symbol: info +- name: changeset + symbol: code +- name: clear-query + symbol: filter_alt_off # note: by meaning; core uses a square X +- name: warning + symbol: warning +- name: comments + symbol: forum +- name: comment + symbol: comment +- name: message + symbol: chat_bubble +- name: reply + symbol: reply # note: by meaning; core uses Tabler "messages" +- name: arrow-right + symbol: arrow_forward +- name: wiki-page + symbol: article # note: Tabler "notebook" +- name: bullet-end + symbol: arrow_circle_left +- name: bullet-go + symbol: arrow_circle_right +- name: bullet-go-end + symbol: diamond # note: Tabler "diamonds" +- name: move + symbol: move_item # note: Tabler "arrow-forward-up" +- name: cancel + symbol: undo +- name: document + symbol: description +- name: issue + symbol: sticky_note_2 # note: Tabler "note" +- name: issue-closed + symbol: check_box +- name: issue-edit + symbol: edit_square +- name: issue-note + symbol: add_comment +- name: file + symbol: draft +- name: text-plain + symbol: description +- name: text-x-c + symbol: code_blocks # note: generic code file +- name: text-x-csharp + symbol: code_blocks # note: generic code file +- name: text-x-java + symbol: code_blocks # note: generic code file +- name: text-x-php + symbol: php +- name: text-x-ruby + symbol: code_blocks # note: generic code file +- name: text-xml + symbol: code # note: generic markup +- name: text-css + symbol: css +- name: text-html + symbol: html +- name: application-javascript + symbol: javascript +- name: application-pdf + symbol: picture_as_pdf +- name: application-zip + symbol: folder_zip +- name: application-gzip + symbol: folder_zip +- name: chevrons-right + symbol: keyboard_double_arrow_right +- name: chevrons-left + symbol: keyboard_double_arrow_left +- name: key + symbol: key +- name: search + symbol: search +- name: toggle-plus + symbol: add_box +- name: toggle-minus + symbol: indeterminate_check_box +- name: circle-minus + symbol: do_not_disturb_on +- name: circle-dot-filled + symbol: radio_button_checked + fill: true +- name: watch + symbol: visibility +- name: unwatch + symbol: visibility_off +- name: copy-pre-content + symbol: content_paste +- name: thumb-up + symbol: thumb_up +- name: thumb-up-filled + symbol: thumb_up + fill: true +- name: alert-circle + symbol: error +- name: bulb + symbol: lightbulb +- name: message-report + symbol: feedback +- name: quote-filled + symbol: format_quote + fill: true +- name: loader + symbol: progress_activity +- name: hourglass + symbol: hourglass +- name: webhook + symbol: webhook +- name: apps + symbol: apps +- name: shield-check + symbol: verified_user +- name: arrow-narrow-left + symbol: arrow_left_alt +- name: arrow-narrow-right + symbol: arrow_right_alt +- name: photo + symbol: image +- name: movie + symbol: movie +- name: file-music + symbol: audio_file +- name: file-type-docx + symbol: docs # note: generic document with lines +- name: file-type-xls + symbol: table # note: generic spreadsheet +- name: file-type-ppt + symbol: slideshow # note: generic presentation +- name: maximize + symbol: fullscreen +- name: link-add + symbol: add_link +- name: unknown + symbol: help diff --git a/tools/palettes/.gitignore b/tools/palettes/.gitignore new file mode 100644 index 0000000..30bcfa4 --- /dev/null +++ b/tools/palettes/.gitignore @@ -0,0 +1 @@ +.build/ diff --git a/tools/palettes/build_palettes.mjs b/tools/palettes/build_palettes.mjs new file mode 100644 index 0000000..fa5f8a5 --- /dev/null +++ b/tools/palettes/build_palettes.mjs @@ -0,0 +1,73 @@ +// 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 +// (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'; + +// The first palette is the default. Ids must match PALETTES in tonalmine/javascripts/theme.js. +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(' '); +} + +// 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. + * picks the palette, forces a mode; + * without data-mode the OS setting (prefers-color-scheme) decides. + */ + +: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) => { + 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)} } } +`; +}); + +// Paths are relative to tools/palettes (npm runs scripts there); the bundle itself lives in .build/. +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. +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); } +console.log(`Wrote ${PALETTES.length} palettes to ${out}`); diff --git a/tools/palettes/package-lock.json b/tools/palettes/package-lock.json new file mode 100644 index 0000000..2114943 --- /dev/null +++ b/tools/palettes/package-lock.json @@ -0,0 +1,505 @@ +{ + "name": "tonalmine-palettes", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tonalmine-palettes", + "devDependencies": { + "@material/material-color-utilities": "0.4.0", + "esbuild": "^0.25.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@material/material-color-utilities": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@material/material-color-utilities/-/material-color-utilities-0.4.0.tgz", + "integrity": "sha512-dlq6VExJReb8dhjj3a/yTigr3ncNwoFmL5Iy2ENtbDX03EmNeOEdZ+vsaGrj7RTuO+mB7L58II4LCsl4NpM8uw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + } + } +} diff --git a/tools/palettes/package.json b/tools/palettes/package.json new file mode 100644 index 0000000..a153746 --- /dev/null +++ b/tools/palettes/package.json @@ -0,0 +1,12 @@ +{ + "name": "tonalmine-palettes", + "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" + }, + "devDependencies": { + "@material/material-color-utilities": "0.4.0", + "esbuild": "^0.25.0" + } +}