From 06cd1db99c9f29a4ca07dce2f273c037c849e9f7 Mon Sep 17 00:00:00 2001 From: surtic86 Date: Thu, 1 Oct 2026 06:05:29 +0200 Subject: [PATCH] Initial commit: Tonalmine, a Material Design 3 theme for Redmine 7 Co-Authored-By: Claude Opus 5.5 --- .gitignore | 5 + LICENSE | 25 + README.md | 70 +++ dev/docker-compose.yml | 14 + dev/seed.rb | 201 +++++++ dev/seed_extras.rb | 67 +++ dev/spread_activity.rb | 60 ++ e2e/check-core-leaks.mjs | 94 +++ e2e/check-font-sizes.mjs | 67 +++ e2e/check-form-alignment.mjs | 48 ++ e2e/fixtures/issues.csv | 3 + e2e/package-lock.json | 40 ++ e2e/package.json | 11 + e2e/screenshots.mjs | 143 +++++ tonalmine/favicon/favicon.png | Bin 0 -> 2456 bytes tonalmine/fonts/OFL.txt | 93 +++ .../fonts/roboto-latin-ext-wght-normal.woff2 | Bin 0 -> 29392 bytes .../fonts/roboto-latin-wght-normal.woff2 | Bin 0 -> 43136 bytes tonalmine/images/LICENSE-material-symbols.txt | 204 +++++++ tonalmine/images/arrow-drop-down-dark.svg | 1 + tonalmine/images/arrow-drop-down.svg | 1 + tonalmine/images/chevron-left-idnt-dark.svg | 1 + tonalmine/images/chevron-left-idnt.svg | 1 + tonalmine/images/chevron-right-idnt-dark.svg | 1 + tonalmine/images/chevron-right-idnt.svg | 1 + tonalmine/images/icons.svg | 123 ++++ tonalmine/images/menu.svg | 1 + tonalmine/images/search-dark.svg | 1 + tonalmine/images/search.svg | 1 + tonalmine/javascripts/theme.js | 84 +++ tonalmine/stylesheets/application.css | 12 + tonalmine/stylesheets/base.css | 47 ++ tonalmine/stylesheets/bridge.css | 139 +++++ tonalmine/stylesheets/components.css | 557 ++++++++++++++++++ tonalmine/stylesheets/layout.css | 332 +++++++++++ tonalmine/stylesheets/mobile.css | 156 +++++ tonalmine/stylesheets/pages.css | 493 ++++++++++++++++ tonalmine/stylesheets/palettes.css | 35 ++ tonalmine/stylesheets/tables.css | 159 +++++ tonalmine/stylesheets/tokens.css | 74 +++ tonalmine/stylesheets/typography.css | 40 ++ tools/build_icons.rb | 64 ++ tools/icon_map.yml | 242 ++++++++ tools/palettes/.gitignore | 1 + tools/palettes/build_palettes.mjs | 73 +++ tools/palettes/package-lock.json | 505 ++++++++++++++++ tools/palettes/package.json | 12 + 47 files changed, 4302 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 dev/docker-compose.yml create mode 100644 dev/seed.rb create mode 100644 dev/seed_extras.rb create mode 100644 dev/spread_activity.rb create mode 100644 e2e/check-core-leaks.mjs create mode 100644 e2e/check-font-sizes.mjs create mode 100644 e2e/check-form-alignment.mjs create mode 100644 e2e/fixtures/issues.csv create mode 100644 e2e/package-lock.json create mode 100644 e2e/package.json create mode 100644 e2e/screenshots.mjs create mode 100644 tonalmine/favicon/favicon.png create mode 100644 tonalmine/fonts/OFL.txt create mode 100644 tonalmine/fonts/roboto-latin-ext-wght-normal.woff2 create mode 100644 tonalmine/fonts/roboto-latin-wght-normal.woff2 create mode 100644 tonalmine/images/LICENSE-material-symbols.txt create mode 100644 tonalmine/images/arrow-drop-down-dark.svg create mode 100644 tonalmine/images/arrow-drop-down.svg create mode 100644 tonalmine/images/chevron-left-idnt-dark.svg create mode 100644 tonalmine/images/chevron-left-idnt.svg create mode 100644 tonalmine/images/chevron-right-idnt-dark.svg create mode 100644 tonalmine/images/chevron-right-idnt.svg create mode 100644 tonalmine/images/icons.svg create mode 100644 tonalmine/images/menu.svg create mode 100644 tonalmine/images/search-dark.svg create mode 100644 tonalmine/images/search.svg create mode 100644 tonalmine/javascripts/theme.js create mode 100644 tonalmine/stylesheets/application.css create mode 100644 tonalmine/stylesheets/base.css create mode 100644 tonalmine/stylesheets/bridge.css create mode 100644 tonalmine/stylesheets/components.css create mode 100644 tonalmine/stylesheets/layout.css create mode 100644 tonalmine/stylesheets/mobile.css create mode 100644 tonalmine/stylesheets/pages.css create mode 100644 tonalmine/stylesheets/palettes.css create mode 100644 tonalmine/stylesheets/tables.css create mode 100644 tonalmine/stylesheets/tokens.css create mode 100644 tonalmine/stylesheets/typography.css create mode 100644 tools/build_icons.rb create mode 100644 tools/icon_map.yml create mode 100644 tools/palettes/.gitignore create mode 100644 tools/palettes/build_palettes.mjs create mode 100644 tools/palettes/package-lock.json create mode 100644 tools/palettes/package.json 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 0000000000000000000000000000000000000000..45aae68d49559664d46691e2cb8b9ae028de5952 GIT binary patch literal 2456 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≪HK)Jkk15%m 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 bvI<7D559Yq23nvUqHV>OR3D*u761SMWPP$Q literal 0 HcmV?d00001 diff --git a/tonalmine/fonts/roboto-latin-wght-normal.woff2 b/tonalmine/fonts/roboto-latin-wght-normal.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..9b0f14165e44ad80256062baf0e55c163dfbe96b GIT binary patch literal 43136 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

7eh|( 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 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" + } +}