Initial commit: Tonalmine, a Material Design 3 theme for Redmine 7

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
surtic86
2026-10-01 06:05:29 +02:00
co-authored by Claude Opus 5.5
commit 06cd1db99c
47 changed files with 4302 additions and 0 deletions
+64
View File
@@ -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(/<path\b[^>]*\bd="([^"]+)"/).flatten
fail_with('No <path> found in', [cached]) if paths.empty?
body = paths.map { |d| %(<path d="#{d}" fill="currentColor" stroke="none"/>) }.join
%(<symbol id="icon--#{e['name']}" viewBox="0 -960 960 960">#{body}</symbol>)
end
fail_with('Material Symbol not found', not_found) if not_found.any?
# 3. Write the sprite.
File.write(OUT, <<~SVG)
<svg xmlns="http://www.w3.org/2000/svg">
<!-- Generated by tools/build_icons.rb from tools/icon_map.yml. Do not edit by hand.
Icons: Material Symbols Outlined by Google (github.com/google/material-design-icons),
Apache License 2.0, see LICENSE-material-symbols.txt. -->
#{symbols.join("\n")}
</svg>
SVG
puts "Wrote #{symbols.size} icons to #{OUT.sub("#{ROOT}/", '')}"