diff options
| author | Paul Buetow <paul@buetow.org> | 2025-06-22 22:06:43 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-06-22 22:06:43 +0300 |
| commit | 3af674aebad9e3792fbf13b3cbda7b1691b1f4f3 (patch) | |
| tree | c70f6033d21628579d96044c89a060e9031dbf8b /extras/html/themes/generate_50_themes.py | |
| parent | 99078f90bf5222c618a60e536cb148850e4b89e2 (diff) | |
Add 50 new experimental HTML themes for Gemtexter
- Generated 50 unique themes with creative layouts and color schemes
- Each theme includes:
- Custom CSS with W3C validated styles
- Example HTML preview page
- Font files with proper licensing
- Theme configuration file
- License documentation
- Layout types include: centered, wide, magazine, card, brutalist,
terminal, book, hero, sidebar, and more creative designs
- All themes support both light and dark color schemes
- Fixed CSS validation issues for W3C compliance
- Created theme gallery page at extras/html/themes/index.html
- Added screenshot previews for all themes
- Utility scripts included:
- generate_50_themes.py - Main theme generator
- fix_css_validation.py - CSS validator/fixer
- create_theme_previews.py - Screenshot generator
- Theme gallery with filtering at index.html
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'extras/html/themes/generate_50_themes.py')
| -rwxr-xr-x | extras/html/themes/generate_50_themes.py | 1741 |
1 files changed, 1741 insertions, 0 deletions
diff --git a/extras/html/themes/generate_50_themes.py b/extras/html/themes/generate_50_themes.py new file mode 100755 index 0000000..6b166de --- /dev/null +++ b/extras/html/themes/generate_50_themes.py @@ -0,0 +1,1741 @@ +#!/usr/bin/env python3 + +import os +import random +import colorsys +from pathlib import Path +import shutil +import subprocess +from datetime import datetime +import concurrent.futures +from PIL import Image, ImageDraw, ImageFont +import json + +# Theme names components +ADJECTIVES = [ + "cosmic", "serene", "vibrant", "minimal", "bold", "elegant", "modern", "classic", + "dynamic", "subtle", "refined", "crisp", "warm", "cool", "fresh", "clean", + "sharp", "smooth", "bright", "deep", "light", "rich", "soft", "strong", + "pure", "clear", "radiant", "muted", "vivid", "gentle", "sleek", "cozy", + "ethereal", "mystic", "zen", "urban", "rustic", "electric", "pastel", "neon", + "aurora", "twilight", "frost", "ember", "jade", "sapphire", "ruby", "amber" +] + +NOUNS = [ + "wave", "sky", "forest", "ocean", "mountain", "valley", "desert", "river", + "lake", "field", "garden", "meadow", "storm", "breeze", "mist", "frost", + "flame", "spark", "glow", "shadow", "light", "dawn", "dusk", "night", + "day", "season", "horizon", "vista", "peak", "flow", "cascade", "canyon", + "oasis", "glacier", "nebula", "cosmos", "prism", "crystal", "ember", "aurora", + "echo", "whisper", "dream", "voyage", "odyssey", "zen", "pulse", "rhythm" +] + +# Font combinations with licensing info +FONT_COMBINATIONS = [ + # Sans-serif combinations + {"heading": ("Abril_Fatface", "OFL", "display"), "body": ("Lato", "OFL", "sans-serif"), "code": ("consola-mono", "OFL", "monospace")}, + {"heading": ("oxygen", "OFL", "sans-serif"), "body": ("Lato", "OFL", "sans-serif"), "code": ("hack", "MIT", "monospace")}, + {"heading": ("roboto-slab", "Apache", "serif"), "body": ("oxygen", "OFL", "sans-serif"), "code": ("intelone-mono", "OFL", "monospace")}, + {"heading": ("Merriweather", "OFL", "serif"), "body": ("oxygen", "OFL", "sans-serif"), "code": ("hack", "MIT", "monospace")}, + {"heading": ("higher-jump", "Free", "display"), "body": ("Lato", "OFL", "sans-serif"), "code": ("consola-mono", "OFL", "monospace")}, + {"heading": ("pixelon", "Free", "display"), "body": ("oxygen", "OFL", "sans-serif"), "code": ("hack", "MIT", "monospace")}, + {"heading": ("repetition-scrolling", "Free", "display"), "body": ("Merriweather", "OFL", "serif"), "code": ("intelone-mono", "OFL", "monospace")}, + {"heading": ("zai-aeg-mignon-typewriter-1924", "Free", "display"), "body": ("roboto-slab", "Apache", "serif"), "code": ("hack", "MIT", "monospace")}, + {"heading": ("khand", "Free", "handwriting"), "body": ("Lato", "OFL", "sans-serif"), "code": ("consola-mono", "OFL", "monospace")}, + {"heading": ("Abril_Fatface", "OFL", "display"), "body": ("Merriweather", "OFL", "serif"), "code": ("hack", "MIT", "monospace")}, + # More variations + {"heading": ("roboto-slab", "Apache", "serif"), "body": ("Lato", "OFL", "sans-serif"), "code": ("consola-mono", "OFL", "monospace")}, + {"heading": ("oxygen", "OFL", "sans-serif"), "body": ("Merriweather", "OFL", "serif"), "code": ("intelone-mono", "OFL", "monospace")}, + {"heading": ("Merriweather", "OFL", "serif"), "body": ("roboto-slab", "Apache", "serif"), "code": ("hack", "MIT", "monospace")}, + {"heading": ("higher-jump", "Free", "display"), "body": ("oxygen", "OFL", "sans-serif"), "code": ("intelone-mono", "OFL", "monospace")}, + {"heading": ("pixelon", "Free", "display"), "body": ("roboto-slab", "Apache", "serif"), "code": ("consola-mono", "OFL", "monospace")}, +] + +# Layout types +LAYOUTS = [ + "centered", "wide", "magazine", "card", "asymmetric", "minimal_grid", + "brutalist", "newspaper", "terminal", "book", "sidebar", "hero", + "masonry", "split", "overlap", "floating", "gradient", "geometric", + "swiss", "retro", "future", "organic", "technical", "artistic" +] + +def generate_color_palette(): + """Generate a harmonious color palette""" + palette_type = random.choice(["complementary", "triadic", "analogous", "monochromatic", "split_complementary"]) + + base_hue = random.random() + base_saturation = random.uniform(0.3, 0.9) + base_lightness = random.uniform(0.3, 0.7) + + # Determine if dark or light theme + is_dark = random.random() > 0.5 + + if is_dark: + bg_lightness = random.uniform(0.05, 0.15) + text_lightness = random.uniform(0.85, 0.95) + else: + bg_lightness = random.uniform(0.92, 0.98) + text_lightness = random.uniform(0.05, 0.15) + + # Generate colors based on palette type + if palette_type == "complementary": + primary_rgb = colorsys.hls_to_rgb(base_hue, base_lightness, base_saturation) + secondary_hue = (base_hue + 0.5) % 1.0 + secondary_rgb = colorsys.hls_to_rgb(secondary_hue, base_lightness, base_saturation * 0.8) + accent_rgb = colorsys.hls_to_rgb(base_hue, base_lightness * 0.8, base_saturation * 0.6) + elif palette_type == "triadic": + primary_rgb = colorsys.hls_to_rgb(base_hue, base_lightness, base_saturation) + secondary_hue = (base_hue + 0.333) % 1.0 + secondary_rgb = colorsys.hls_to_rgb(secondary_hue, base_lightness, base_saturation) + accent_hue = (base_hue + 0.667) % 1.0 + accent_rgb = colorsys.hls_to_rgb(accent_hue, base_lightness, base_saturation) + elif palette_type == "analogous": + primary_rgb = colorsys.hls_to_rgb(base_hue, base_lightness, base_saturation) + secondary_hue = (base_hue + 0.08) % 1.0 + secondary_rgb = colorsys.hls_to_rgb(secondary_hue, base_lightness, base_saturation * 0.9) + accent_hue = (base_hue - 0.08) % 1.0 + accent_rgb = colorsys.hls_to_rgb(accent_hue, base_lightness * 0.9, base_saturation) + elif palette_type == "monochromatic": + primary_rgb = colorsys.hls_to_rgb(base_hue, base_lightness, base_saturation) + secondary_rgb = colorsys.hls_to_rgb(base_hue, base_lightness * 0.7, base_saturation * 0.8) + accent_rgb = colorsys.hls_to_rgb(base_hue, base_lightness * 1.2, base_saturation * 0.6) + else: # split_complementary + primary_rgb = colorsys.hls_to_rgb(base_hue, base_lightness, base_saturation) + secondary_hue = (base_hue + 0.42) % 1.0 + secondary_rgb = colorsys.hls_to_rgb(secondary_hue, base_lightness, base_saturation * 0.8) + accent_hue = (base_hue + 0.58) % 1.0 + accent_rgb = colorsys.hls_to_rgb(accent_hue, base_lightness, base_saturation * 0.8) + + # Background and text colors + bg_rgb = colorsys.hls_to_rgb(base_hue, bg_lightness, 0.1) + text_rgb = colorsys.hls_to_rgb(0, text_lightness, 0) + + # Convert to hex + primary = '#{:02x}{:02x}{:02x}'.format(int(primary_rgb[0]*255), int(primary_rgb[1]*255), int(primary_rgb[2]*255)) + secondary = '#{:02x}{:02x}{:02x}'.format(int(secondary_rgb[0]*255), int(secondary_rgb[1]*255), int(secondary_rgb[2]*255)) + accent = '#{:02x}{:02x}{:02x}'.format(int(accent_rgb[0]*255), int(accent_rgb[1]*255), int(accent_rgb[2]*255)) + background = '#{:02x}{:02x}{:02x}'.format(int(bg_rgb[0]*255), int(bg_rgb[1]*255), int(bg_rgb[2]*255)) + text = '#{:02x}{:02x}{:02x}'.format(int(text_rgb[0]*255), int(text_rgb[1]*255), int(text_rgb[2]*255)) + + return { + "primary": primary, + "secondary": secondary, + "accent": accent, + "background": background, + "text": text, + "is_dark": is_dark, + "palette_type": palette_type + } + +def generate_layout_css(layout_type, colors, font_sizes): + """Generate CSS for different layout types""" + base_css = f"""/* Base styles */ +@font-face {{ + font-family: 'text'; + src: url("./text.ttf") format("truetype"); +}} + +@font-face {{ + font-family: 'heading'; + src: url("./heading.ttf") format("truetype"); +}} + +@font-face {{ + font-family: 'code'; + src: url("./code.ttf") format("truetype"); +}} + +@font-face {{ + font-family: 'handnotes'; + src: url("./handnotes.ttf") format("truetype"); +}} + +:root {{ + --color-primary: {colors['primary']}; + --color-secondary: {colors['secondary']}; + --color-accent: {colors['accent']}; + --color-bg: {colors['background']}; + --color-text: {colors['text']}; + --font-size-base: {font_sizes['base']}px; + --font-size-h1: {font_sizes['h1']}em; + --font-size-h2: {font_sizes['h2']}em; + --font-size-h3: {font_sizes['h3']}em; + --line-height: {font_sizes['line_height']}; +}} + +* {{ + box-sizing: border-box; +}} + +html {{ + font-size: var(--font-size-base); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +}} + +body {{ + font-family: text, sans-serif; + background-color: var(--color-bg); + color: var(--color-text); + line-height: var(--line-height); + margin: 0; + padding: 0; + word-wrap: break-word; +}} + +h1, h2, h3 {{ + font-family: heading, serif; + line-height: 1.2; + margin-top: 1em; + margin-bottom: 0.5em; +}} + +h1 {{ + font-size: var(--font-size-h1); + color: var(--color-primary); +}} + +h2 {{ + font-size: var(--font-size-h2); + color: var(--color-primary); +}} + +h3 {{ + font-size: var(--font-size-h3); + color: var(--color-secondary); +}} + +a {{ + font-family: code, monospace; + color: var(--color-secondary); + text-decoration: none; + transition: all 0.3s ease; +}} + +a:hover {{ + color: var(--color-accent); + text-decoration: underline; +}} + +.textlink {{ + font-family: text, sans-serif; +}} + +.quote {{ + font-family: handnotes, cursive; + border-left: 4px solid var(--color-accent); + padding: 1em 1.5em; + margin: 1.5em 0; + background-color: {colors['primary']}11; + font-style: italic; +}} + +pre {{ + font-family: code, monospace; + background-color: {colors['text']}0A; + border: 1px solid {colors['text']}22; + padding: 1em; + overflow-x: auto; + border-radius: 4px; + font-size: 0.9em; +}} + +code {{ + font-family: code, monospace; + background-color: {colors['text']}0A; + padding: 0.2em 0.4em; + border-radius: 3px; + font-size: 0.9em; +}} + +ul, ol {{ + padding-left: 2em; + margin: 1em 0; +}} + +li {{ + margin: 0.5em 0; +}} + +img {{ + max-width: 100%; + height: auto; +}} + +hr {{ + border: none; + border-top: 1px solid {colors['text']}33; + margin: 2em 0; +}} +""" + + # Layout-specific CSS + layout_css = "" + + if layout_type == "centered": + layout_css = """ +body { + max-width: 65ch; + margin: 0 auto; + padding: 2em 1em; +} + +.header { + text-align: center; + margin-bottom: 3em; + padding-bottom: 2em; + border-bottom: 2px solid var(--color-primary); +} +""" + elif layout_type == "wide": + layout_css = """ +body { + max-width: 90%; + margin: 0 auto; + padding: 3em 2em; +} + +.header { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: 4em; + padding-bottom: 2em; + border-bottom: 3px solid var(--color-primary); +} + +.content { + max-width: 75ch; + margin: 0 auto; +} +""" + elif layout_type == "magazine": + layout_css = """ +body { + max-width: 1400px; + margin: 0 auto; + padding: 2em; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 2em; +} + +.header { + grid-column: 1 / -1; + text-align: center; + margin-bottom: 2em; + padding: 3em 0; + background: linear-gradient(135deg, var(--color-primary)22, var(--color-secondary)22); +} + +h1, h2 { + grid-column: 1 / -1; +} + +p, ul, pre, .quote { + grid-column: span 1; +} + +p:first-of-type { + grid-column: 1 / 3; + font-size: 1.2em; + line-height: 1.6; +} +""" + elif layout_type == "card": + layout_css = f""" +body {{ + padding: 2em; + background: linear-gradient(135deg, {colors['primary']}11, {colors['secondary']}11); + min-height: 100vh; +}} + +.header {{ + background-color: var(--color-bg); + padding: 3em; + margin-bottom: 3em; + border-radius: 16px; + box-shadow: 0 8px 32px rgba(0,0,0,0.1); + text-align: center; +}} + +h1, h2, h3, p, ul, pre, .quote {{ + background-color: var(--color-bg); + padding: 2em; + margin: 1em 0; + border-radius: 12px; + box-shadow: 0 4px 16px rgba(0,0,0,0.05); +}} + +h1, h2, h3 {{ + padding: 1em 2em; +}} +""" + elif layout_type == "asymmetric": + layout_css = """ +body { + max-width: 1200px; + margin: 0 auto; + padding: 2em; +} + +.header { + margin-left: 20%; + margin-bottom: 4em; +} + +h1 { + margin-left: -5%; + font-size: calc(var(--font-size-h1) * 1.2); +} + +h2 { + margin-left: 10%; +} + +h3 { + margin-left: 25%; +} + +p:nth-child(odd) { + margin-left: 5%; + margin-right: 20%; +} + +p:nth-child(even) { + margin-left: 20%; + margin-right: 5%; +} + +.quote { + margin-left: 15%; + margin-right: 15%; + text-align: center; +} +""" + elif layout_type == "minimal_grid": + layout_css = """ +body { + display: grid; + grid-template-columns: repeat(12, 1fr); + gap: 1.5em; + padding: 3em 2em; + max-width: 1400px; + margin: 0 auto; +} + +.header { + grid-column: 2 / 12; + text-align: center; + margin-bottom: 3em; + padding-bottom: 2em; + border-bottom: 1px solid var(--color-text); +} + +h1 { + grid-column: 1 / 13; + text-align: center; +} + +h2 { + grid-column: 2 / 12; +} + +h3 { + grid-column: 3 / 11; +} + +p, ul, pre { + grid-column: 3 / 11; +} + +.quote { + grid-column: 2 / 12; + text-align: center; +} +""" + elif layout_type == "brutalist": + layout_css = f""" +body {{ + background-color: {colors['text']}; + color: {colors['background']}; + padding: 0; +}} + +.header {{ + background-color: var(--color-primary); + color: var(--color-bg); + padding: 4em 2em; + transform: skewY(-3deg); + margin-bottom: 4em; +}} + +.header * {{ + transform: skewY(3deg); +}} + +h1, h2, h3 {{ + background-color: var(--color-bg); + color: var(--color-text); + padding: 1em; + text-transform: uppercase; + letter-spacing: 0.2em; + margin: 0; +}} + +p, ul, pre, .quote {{ + background-color: var(--color-bg); + color: var(--color-text); + margin: 0; + padding: 2em; + border: 8px solid var(--color-primary); +}} + +a {{ + background-color: var(--color-secondary); + color: var(--color-bg); + padding: 0.3em 0.6em; + text-decoration: none; +}} + +a:hover {{ + background-color: var(--color-accent); +}} +""" + elif layout_type == "newspaper": + layout_css = """ +body { + max-width: 1200px; + margin: 0 auto; + padding: 2em; + columns: 3; + column-gap: 2em; + column-rule: 1px solid var(--color-text); +} + +.header { + column-span: all; + text-align: center; + border-top: 6px double var(--color-text); + border-bottom: 6px double var(--color-text); + padding: 2em 0; + margin-bottom: 3em; +} + +.header h1 { + font-size: 4em; + margin: 0; + letter-spacing: 0.1em; +} + +h1, h2 { + column-span: all; + text-align: center; + margin: 2em 0 1em 0; +} + +h3 { + break-after: avoid; +} + +p { + text-align: justify; + hyphens: auto; +} + +.quote { + column-span: all; + text-align: center; + font-size: 1.5em; + margin: 2em 0; +} +""" + elif layout_type == "terminal": + layout_css = f""" +@import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;700&display=swap'); + +body {{ + background-color: #000; + color: #0f0; + font-family: 'Source Code Pro', code, monospace; + padding: 1em; + line-height: 1.4; +}} + +.header {{ + border: 2px solid #0f0; + padding: 1em; + margin-bottom: 2em; + position: relative; +}} + +.header::before {{ + content: "[SYSTEM] "; + color: #ff0; +}} + +h1, h2, h3 {{ + color: #0f0; + font-family: 'Source Code Pro', code, monospace; +}} + +h1::before {{ + content: "### "; + color: #f0f; +}} + +h2::before {{ + content: "## "; + color: #0ff; +}} + +h3::before {{ + content: "# "; + color: #ff0; +}} + +p::before {{ + content: "> "; + color: #666; +}} + +a {{ + color: #0ff; + text-decoration: underline; +}} + +a:hover {{ + background-color: #0ff; + color: #000; +}} + +.quote {{ + border: 1px dashed #0f0; + background-color: #0f0111; + color: #0f0; +}} + +pre {{ + background-color: #111; + border: 1px solid #0f0; + color: #0f0; + overflow-x: scroll; +}} + +pre::before {{ + content: "$ cat output.log\\A"; + color: #666; +}} +""" + elif layout_type == "book": + layout_css = """ +body { + max-width: 40em; + margin: 4em auto; + padding: 2em; + line-height: 1.8; + text-align: justify; + hyphens: auto; +} + +.header { + text-align: center; + margin-bottom: 6em; + page-break-after: always; +} + +.header h1 { + font-size: 3em; + margin-bottom: 0.5em; +} + +h1 { + text-align: center; + margin: 3em 0 2em 0; + page-break-before: always; +} + +h2 { + margin-top: 2em; + text-align: left; +} + +p { + text-indent: 1.5em; + margin: 0; +} + +p:first-of-type { + text-indent: 0; +} + +p:first-of-type::first-letter { + font-size: 4em; + line-height: 1; + float: left; + margin: 0 0.1em 0 0; + font-family: heading, serif; + color: var(--color-primary); +} + +.quote { + margin: 2em 2em; + text-align: center; + font-style: italic; +} +""" + elif layout_type == "sidebar": + layout_css = """ +body { + display: grid; + grid-template-columns: 300px 1fr; + min-height: 100vh; + margin: 0; +} + +.header { + grid-column: 1; + position: sticky; + top: 0; + height: 100vh; + padding: 3em 2em; + background-color: var(--color-primary); + color: var(--color-bg); + overflow-y: auto; +} + +.header h1 { + color: var(--color-bg); +} + +.content { + grid-column: 2; + padding: 3em; + max-width: 65ch; +} + +h1, h2, h3, p, ul, pre, .quote { + grid-column: 2; +} +""" + elif layout_type == "hero": + layout_css = f""" +body {{ + margin: 0; + padding: 0; +}} + +.header {{ + min-height: 80vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: linear-gradient(135deg, {colors['primary']}CC, {colors['secondary']}CC), + linear-gradient(45deg, {colors['accent']}22, transparent); + color: var(--color-bg); + text-align: center; + position: relative; +}} + +.header h1 {{ + font-size: 5em; + margin: 0; + color: var(--color-bg); + text-shadow: 0 4px 8px rgba(0,0,0,0.3); +}} + +.header p {{ + font-size: 1.5em; + opacity: 0.9; +}} + +.content {{ + max-width: 65ch; + margin: 4em auto; + padding: 0 2em; +}} +""" + elif layout_type == "masonry": + layout_css = """ +body { + padding: 2em; + columns: 320px; + column-gap: 2em; +} + +.header { + column-span: all; + text-align: center; + margin-bottom: 3em; + padding: 2em; + background: var(--color-primary); + color: var(--color-bg); +} + +.header h1 { + color: var(--color-bg); +} + +h1, h2 { + column-span: all; + text-align: center; + margin: 2em 0; +} + +p, ul, pre, .quote, h3 { + break-inside: avoid; + margin-bottom: 2em; + padding: 1.5em; + background-color: var(--color-bg); + border: 1px solid var(--color-text)11; + border-radius: 8px; +} +""" + elif layout_type == "split": + layout_css = f""" +body {{ + display: grid; + grid-template-columns: 1fr 1fr; + min-height: 100vh; + margin: 0; +}} + +.header {{ + grid-column: 1; + background: linear-gradient(135deg, {colors['primary']}, {colors['secondary']}); + color: var(--color-bg); + padding: 4em; + position: sticky; + top: 0; + height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; +}} + +.header h1 {{ + color: var(--color-bg); + font-size: 3.5em; +}} + +.content {{ + grid-column: 2; + padding: 4em; + overflow-y: auto; +}} + +h1, h2, h3, p, ul, pre, .quote {{ + grid-column: 2; +}} +""" + elif layout_type == "overlap": + layout_css = f""" +body {{ + padding: 2em; + position: relative; + max-width: 1200px; + margin: 0 auto; +}} + +.header {{ + position: relative; + z-index: 10; + background-color: var(--color-bg); + padding: 3em; + margin-bottom: -2em; + box-shadow: 0 10px 40px rgba(0,0,0,0.1); + border: 2px solid var(--color-primary); +}} + +h1 {{ + font-size: 3.5em; + margin-top: 1em; + position: relative; + z-index: 5; +}} + +h2 {{ + margin-left: 2em; + position: relative; + z-index: 4; + background-color: var(--color-bg); + padding: 1em; + box-shadow: 0 4px 20px rgba(0,0,0,0.05); +}} + +h3 {{ + margin-left: 4em; + position: relative; + z-index: 3; +}} + +p, ul, pre, .quote {{ + position: relative; + background-color: var(--color-bg); + padding: 2em; + margin: 1em 0 1em 3em; + box-shadow: 0 4px 20px rgba(0,0,0,0.05); + border-left: 4px solid var(--color-accent); +}} + +p:nth-child(even), ul:nth-child(even) {{ + margin-left: 0; + margin-right: 3em; + border-left: none; + border-right: 4px solid var(--color-secondary); +}} +""" + elif layout_type == "floating": + layout_css = f""" +body {{ + padding: 4em 2em; + background: linear-gradient(45deg, {colors['primary']}11, {colors['secondary']}11); + min-height: 100vh; +}} + +.header {{ + text-align: center; + margin-bottom: 4em; + animation: float 6s ease-in-out infinite; +}} + +@keyframes float {{ + 0%, 100% {{ transform: translateY(0); }} + 50% {{ transform: translateY(-20px); }} +}} + +h1, h2, h3, p, ul, pre, .quote {{ + background-color: var(--color-bg); + padding: 2em; + margin: 2em auto; + max-width: 65ch; + border-radius: 16px; + box-shadow: 0 8px 32px rgba(0,0,0,0.1); + transition: transform 0.3s ease; +}} + +h1:hover, h2:hover, h3:hover, p:hover, ul:hover, pre:hover, .quote:hover {{ + transform: translateY(-4px); + box-shadow: 0 12px 48px rgba(0,0,0,0.15); +}} +""" + elif layout_type == "gradient": + layout_css = f""" +body {{ + margin: 0; + padding: 0; + background: linear-gradient(180deg, + {colors['primary']}22 0%, + {colors['secondary']}22 50%, + {colors['accent']}22 100%); + min-height: 100vh; +}} + +.header {{ + text-align: center; + padding: 6em 2em; + background: rgba(255,255,255,0.1); + backdrop-filter: blur(10px); + margin-bottom: 4em; +}} + +h1, h2, h3, p, ul, pre, .quote {{ + max-width: 65ch; + margin: 2em auto; + padding: 2em; + background: rgba(255,255,255,0.95); + backdrop-filter: blur(10px); + border-radius: 20px; + box-shadow: 0 8px 32px rgba(0,0,0,0.1); +}} +""" + elif layout_type == "geometric": + layout_css = f""" +body {{ + padding: 2em; + background-color: var(--color-bg); + background-image: + repeating-linear-gradient(45deg, + {colors['primary']}11 0, + {colors['primary']}11 10px, + transparent 10px, + transparent 20px), + repeating-linear-gradient(-45deg, + {colors['secondary']}11 0, + {colors['secondary']}11 10px, + transparent 10px, + transparent 20px); +}} + +.header {{ + background-color: var(--color-bg); + border: 4px solid var(--color-primary); + padding: 3em; + margin-bottom: 3em; + position: relative; + clip-path: polygon(0 0, 100% 0, 95% 100%, 5% 100%); +}} + +h1, h2, h3 {{ + position: relative; + padding-left: 2em; +}} + +h1::before, h2::before, h3::before {{ + content: ""; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 1em; + height: 1em; + background-color: var(--color-accent); + clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); +}} + +p, ul, pre, .quote {{ + background-color: var(--color-bg); + padding: 2em; + margin: 2em 0; + border-left: 4px solid var(--color-secondary); + position: relative; +}} + +p::after, ul::after, pre::after, .quote::after {{ + content: ""; + position: absolute; + right: -10px; + top: 50%; + transform: translateY(-50%); + width: 20px; + height: 20px; + background-color: var(--color-accent); + clip-path: polygon(0% 0%, 100% 50%, 0% 100%); +}} +""" + elif layout_type == "swiss": + layout_css = """ +body { + max-width: 1200px; + margin: 0 auto; + padding: 2em; + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 2em; +} + +.header { + grid-column: 1 / -1; + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 2em; + margin-bottom: 4em; + padding-bottom: 2em; + border-bottom: 4px solid var(--color-text); +} + +.header h1 { + grid-column: 1 / 4; + font-size: 4em; + margin: 0; + line-height: 0.9; +} + +.header p { + grid-column: 4 / -1; + align-self: end; +} + +h1 { + grid-column: 1 / -1; + font-size: 3em; + margin: 1em 0 0.5em 0; +} + +h2 { + grid-column: 1 / 5; + margin-top: 2em; +} + +h3 { + grid-column: 2 / 6; +} + +p, ul, pre { + grid-column: 2 / 6; +} + +.quote { + grid-column: 1 / -1; + font-size: 1.5em; + text-align: center; + margin: 2em 0; +} +""" + elif layout_type == "retro": + layout_css = f""" +@import url('https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap'); + +body {{ + background-color: #f4e8d0; + color: #2a2a2a; + padding: 2em; + position: relative; +}} + +body::before {{ + content: ""; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: + repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0,0,0,0.03) 2px, + rgba(0,0,0,0.03) 4px + ); + pointer-events: none; + z-index: 1; +}} + +.header {{ + background-color: {colors['primary']}; + color: #f4e8d0; + padding: 2em; + margin: -2em -2em 2em -2em; + text-align: center; + box-shadow: 0 4px 8px rgba(0,0,0,0.3); + position: relative; + z-index: 2; +}} + +.header h1 {{ + font-family: 'Courier Prime', monospace; + font-size: 3em; + margin: 0; + text-transform: uppercase; + letter-spacing: 0.1em; + color: #f4e8d0; +}} + +h1, h2, h3 {{ + font-family: 'Courier Prime', monospace; + text-transform: uppercase; + letter-spacing: 0.05em; + position: relative; + z-index: 2; +}} + +p, ul, pre, .quote {{ + background-color: rgba(255,255,255,0.8); + padding: 1.5em; + margin: 1em 0; + box-shadow: 2px 2px 4px rgba(0,0,0,0.1); + position: relative; + z-index: 2; +}} + +a {{ + color: {colors['secondary']}; + text-decoration: underline; + text-decoration-style: wavy; +}} +""" + elif layout_type == "future": + layout_css = f""" +body {{ + background-color: #000; + color: #fff; + padding: 0; + margin: 0; + position: relative; + overflow-x: hidden; +}} + +body::before {{ + content: ""; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + radial-gradient(circle at 20% 50%, {colors['primary']}44 0%, transparent 50%), + radial-gradient(circle at 80% 80%, {colors['secondary']}44 0%, transparent 50%), + radial-gradient(circle at 40% 20%, {colors['accent']}44 0%, transparent 50%); + z-index: -1; +}} + +.header {{ + padding: 6em 2em; + text-align: center; + position: relative; +}} + +.header h1 {{ + font-size: 5em; + margin: 0; + background: linear-gradient(45deg, {colors['primary']}, {colors['secondary']}, {colors['accent']}); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-transform: uppercase; + letter-spacing: 0.2em; +}} + +h1, h2, h3, p, ul, pre, .quote {{ + max-width: 65ch; + margin: 2em auto; + padding: 2em; + background: rgba(255,255,255,0.05); + backdrop-filter: blur(10px); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 20px; + box-shadow: 0 8px 32px rgba(0,0,0,0.2); +}} + +a {{ + color: {colors['accent']}; + text-decoration: none; + position: relative; +}} + +a::after {{ + content: ""; + position: absolute; + bottom: -2px; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, {colors['primary']}, {colors['secondary']}); + transform: scaleX(0); + transition: transform 0.3s ease; +}} + +a:hover::after {{ + transform: scaleX(1); +}} +""" + elif layout_type == "organic": + layout_css = f""" +body {{ + padding: 2em; + background-color: {colors['background']}; + background-image: + radial-gradient(circle at 10% 20%, {colors['primary']}11 0%, transparent 50%), + radial-gradient(circle at 80% 80%, {colors['secondary']}11 0%, transparent 50%); +}} + +.header {{ + text-align: center; + margin-bottom: 4em; + padding: 3em; + background: {colors['background']}; + border-radius: 50% 20% 30% 70% / 30% 60% 40% 70%; + box-shadow: 0 8px 32px rgba(0,0,0,0.1); +}} + +h1, h2, h3 {{ + position: relative; + padding-left: 3em; +}} + +h1::before, h2::before, h3::before {{ + content: "🌿"; + position: absolute; + left: 0; + font-size: 2em; + opacity: 0.3; +}} + +p, ul, pre, .quote {{ + background-color: {colors['background']}; + padding: 2em; + margin: 2em 0; + border-radius: 20px 5px 20px 5px; + box-shadow: 0 4px 16px rgba(0,0,0,0.05); + position: relative; + overflow: hidden; +}} + +p::before, ul::before, pre::before, .quote::before {{ + content: ""; + position: absolute; + top: -50%; + right: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle, {colors['accent']}11 0%, transparent 70%); + z-index: -1; +}} +""" + elif layout_type == "technical": + layout_css = f""" +body {{ + font-family: code, monospace; + background-color: #0a0a0a; + color: #e0e0e0; + padding: 2em; + background-image: + linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px); + background-size: 20px 20px; +}} + +.header {{ + border: 2px solid {colors['primary']}; + padding: 2em; + margin-bottom: 3em; + position: relative; + background: linear-gradient(135deg, transparent 10px, #0a0a0a 10px); +}} + +.header::before {{ + content: "//"; + position: absolute; + top: 10px; + left: 10px; + color: {colors['primary']}; + font-size: 2em; +}} + +h1, h2, h3 {{ + font-family: code, monospace; + text-transform: uppercase; + letter-spacing: 0.1em; + border-left: 4px solid {colors['accent']}; + padding-left: 1em; +}} + +h1::before {{ content: "001 "; color: {colors['primary']}; }} +h2::before {{ content: "010 "; color: {colors['secondary']}; }} +h3::before {{ content: "011 "; color: {colors['accent']}; }} + +p, ul, pre, .quote {{ + background-color: rgba(255,255,255,0.02); + border: 1px solid rgba(255,255,255,0.1); + padding: 1.5em; + margin: 1.5em 0; + position: relative; +}} + +p::before, ul::before, .quote::before {{ + content: ">"; + position: absolute; + left: -1em; + color: {colors['accent']}; +}} + +a {{ + color: {colors['accent']}; + text-decoration: none; + padding: 0.2em 0.4em; + background-color: rgba(255,255,255,0.05); + border: 1px solid {colors['accent']}33; + transition: all 0.3s ease; +}} + +a:hover {{ + background-color: {colors['accent']}22; + border-color: {colors['accent']}; +}} +""" + elif layout_type == "artistic": + layout_css = f""" +body {{ + padding: 4em 2em; + background: linear-gradient(45deg, {colors['background']}, {colors['background']}DD); + min-height: 100vh; + position: relative; +}} + +body::before, body::after {{ + content: ""; + position: fixed; + width: 300px; + height: 300px; + border-radius: 50%; + filter: blur(100px); + z-index: -1; +}} + +body::before {{ + top: -150px; + left: -150px; + background: {colors['primary']}44; +}} + +body::after {{ + bottom: -150px; + right: -150px; + background: {colors['secondary']}44; +}} + +.header {{ + text-align: center; + margin-bottom: 4em; + position: relative; +}} + +.header h1 {{ + font-size: 4em; + margin: 0; + transform: perspective(500px) rotateY(-15deg); + text-shadow: + 3px 3px 0 {colors['primary']}, + 6px 6px 0 {colors['secondary']}, + 9px 9px 20px rgba(0,0,0,0.2); +}} + +h2, h3 {{ + position: relative; + display: inline-block; +}} + +h2::after, h3::after {{ + content: ""; + position: absolute; + bottom: -5px; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, {colors['primary']}, {colors['secondary']}, {colors['accent']}); + border-radius: 2px; +}} + +p, ul, pre, .quote {{ + max-width: 65ch; + margin: 2em auto; + padding: 2em; + background: rgba(255,255,255,0.9); + box-shadow: + 0 8px 32px rgba(0,0,0,0.1), + inset 0 1px 0 rgba(255,255,255,0.5); + border-radius: 20px; + transform: rotate(-1deg); +}} + +p:nth-child(even), ul:nth-child(even), .quote:nth-child(even) {{ + transform: rotate(1deg); +}} + +a {{ + color: {colors['secondary']}; + text-decoration: none; + background-image: linear-gradient(90deg, {colors['primary']}, {colors['secondary']}); + background-size: 100% 2px; + background-position: 0 100%; + background-repeat: no-repeat; + transition: all 0.3s ease; +}} + +a:hover {{ + background-size: 100% 100%; + color: white; + padding: 0.2em 0.4em; + margin: -0.2em -0.4em; +}} +""" + + return base_css + "\n\n/* Layout: " + layout_type + " */\n" + layout_css + +def generate_font_sizes(): + """Generate harmonious font size combinations""" + base_sizes = [14, 15, 16, 17, 18] + base = random.choice(base_sizes) + + scale_ratios = [1.125, 1.2, 1.25, 1.333, 1.414, 1.5, 1.618] + scale = random.choice(scale_ratios) + + return { + 'base': base, + 'h1': round(scale ** 3, 2), + 'h2': round(scale ** 2, 2), + 'h3': round(scale, 2), + 'line_height': round(random.uniform(1.4, 1.8), 2) + } + +def create_theme(theme_name, fonts, colors, layout, font_sizes): + """Create a complete theme directory with all files""" + theme_dir = Path(f"/home/paul/git/gemtexter/extras/html/themes/{theme_name}") + theme_dir.mkdir(exist_ok=True) + + # Generate theme.conf + theme_conf = f"""declare -xr HTML_HEADER=./extras/html/header.html.part +declare -xr HTML_FOOTER=./extras/html/footer.html.part +declare -xr HTML_CSS_STYLE=$HTML_THEME_DIR/style.css +declare -xr HTML_WEBFONT_HEADING=./extras/html/fonts/{fonts['heading'][0]}/{fonts['heading'][0]}-Bold.ttf +declare -xr HTML_WEBFONT_TEXT=./extras/html/fonts/{fonts['body'][0]}/{fonts['body'][0]}-Regular.ttf +declare -xr HTML_WEBFONT_CODE=./extras/html/fonts/{fonts['code'][0]}/{fonts['code'][0]}-Regular.ttf +declare -xr HTML_WEBFONT_HANDNOTES=./extras/html/fonts/khand/khand.ttf +declare -xr SOURCE_HIGHLIGHT_CSS=./extras/html/source-highlight-styles/mono.css +""" + + with open(theme_dir / "theme.conf", "w") as f: + f.write(theme_conf) + + # Copy fonts and generate style.css + css_content = generate_layout_css(layout, colors, font_sizes) + + # Add font file references + font_mapping = { + 'heading': fonts['heading'][0], + 'text': fonts['body'][0], + 'code': fonts['code'][0], + 'handnotes': 'khand' + } + + # Copy font files + for font_type, font_name in font_mapping.items(): + if font_name == "Abril_Fatface": + src = f"/home/paul/git/gemtexter/extras/html/fonts/{font_name}/AbrilFatface-Regular.ttf" + dst = theme_dir / f"{font_type}.ttf" + elif font_name in ["Lato", "Merriweather", "oxygen", "roboto-slab"]: + weight = "Bold" if font_type == "heading" else "Regular" + src = f"/home/paul/git/gemtexter/extras/html/fonts/{font_name}/{font_name}-{weight}.ttf" + dst = theme_dir / f"{font_type}.ttf" + elif font_name == "consola-mono": + weight = "Bold" if font_type == "heading" else "Book" + src = f"/home/paul/git/gemtexter/extras/html/fonts/{font_name}/ConsolaMono-{weight}.ttf" + dst = theme_dir / f"{font_type}.ttf" + elif font_name == "hack": + src = f"/home/paul/git/gemtexter/extras/html/fonts/{font_name}/Hack-Regular.ttf" + dst = theme_dir / f"{font_type}.ttf" + elif font_name == "intelone-mono": + src = f"/home/paul/git/gemtexter/extras/html/fonts/{font_name}/intelone-mono-font-family-regular.ttf" + dst = theme_dir / f"{font_type}.ttf" + elif font_name == "higher-jump": + src = f"/home/paul/git/gemtexter/extras/html/fonts/{font_name}/Higher Jump.ttf" + dst = theme_dir / f"{font_type}.ttf" + elif font_name == "pixelon": + src = f"/home/paul/git/gemtexter/extras/html/fonts/{font_name}/Pixelon.ttf" + dst = theme_dir / f"{font_type}.ttf" + elif font_name == "repetition-scrolling": + src = f"/home/paul/git/gemtexter/extras/html/fonts/{font_name}/repet___.ttf" + dst = theme_dir / f"{font_type}.ttf" + elif font_name == "zai-aeg-mignon-typewriter-1924": + src = f"/home/paul/git/gemtexter/extras/html/fonts/{font_name}/zai_AEGMignonTypewriter1924.ttf" + dst = theme_dir / f"{font_type}.ttf" + else: + src = f"/home/paul/git/gemtexter/extras/html/fonts/{font_name}/{font_name}.ttf" + dst = theme_dir / f"{font_type}.ttf" + + try: + shutil.copy(src, dst) + except FileNotFoundError: + # Try alternative path + print(f"Warning: Could not find {src}") + + with open(theme_dir / "style.css", "w") as f: + f.write(css_content) + + # Generate example.html + example_html = f"""<!DOCTYPE html> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>{theme_name.replace('_', ' ').title()} - Gemtexter Theme</title> + <link rel="stylesheet" href="style.css" /> +</head> +<body> + <div class="header"> + <h1>{theme_name.replace('_', ' ').title()}</h1> + <p>A {layout.replace('_', ' ')} layout with {colors['palette_type'].replace('_', ' ')} colors</p> + </div> + + <div class="content"> + <h1>Welcome to {theme_name.replace('_', ' ').title()}</h1> + <p>This theme features a carefully crafted {layout.replace('_', ' ')} layout with a {'dark' if colors['is_dark'] else 'light'} {colors['palette_type'].replace('_', ' ')} color scheme. The typography combines {fonts['heading'][2]} fonts for headings with {fonts['body'][2]} for body text.</p> + + <h2>Typography Showcase</h2> + <p>The {fonts['body'][0].replace('_', ' ')} font family provides excellent readability for body text, while {fonts['heading'][0].replace('_', ' ')} adds character to headings. Code blocks use {fonts['code'][0].replace('_', ' ')} for clarity.</p> + + <h2>Color Palette</h2> + <p>Primary: <span style="color: {colors['primary']}; font-weight: bold;">{colors['primary']}</span> | + Secondary: <span style="color: {colors['secondary']}; font-weight: bold;">{colors['secondary']}</span> | + Accent: <span style="color: {colors['accent']}; font-weight: bold;">{colors['accent']}</span></p> + + <h3>Interactive Elements</h3> + <p>Links like <a href="#">this example</a> and longer <a href="#" class="textlink">text links that demonstrate the theme's navigation style</a> use the secondary color.</p> + + <div class="quote"> + "Design is not just what it looks like and feels like. Design is how it works." — Steve Jobs + </div> + + <h3>Code Examples</h3> + <p>Inline code like <code>theme.generate()</code> and larger blocks:</p> + <pre>// Theme configuration +const theme = {{ + name: "{theme_name}", + layout: "{layout}", + colors: {{ + primary: "{colors['primary']}", + secondary: "{colors['secondary']}", + accent: "{colors['accent']}" + }}, + fonts: {{ + heading: "{fonts['heading'][0]}", + body: "{fonts['body'][0]}", + code: "{fonts['code'][0]}" + }} +}};</pre> + + <h2>Content Structure</h2> + <ul> + <li>Clean, readable typography with {font_sizes['base']}px base font size</li> + <li>Heading scale ratio of {font_sizes['h1']}x for visual hierarchy</li> + <li>Line height of {font_sizes['line_height']} for comfortable reading</li> + <li>{layout.replace('_', ' ').title()} layout optimized for content flow</li> + <li>{'Dark' if colors['is_dark'] else 'Light'} theme with {colors['palette_type'].replace('_', ' ')} color harmony</li> + </ul> + + <h2>Font Licensing</h2> + <p>All fonts used in this theme are properly licensed:</p> + <ul> + <li>{fonts['heading'][0]}: {fonts['heading'][1]} License</li> + <li>{fonts['body'][0]}: {fonts['body'][1]} License</li> + <li>{fonts['code'][0]}: {fonts['code'][1]} License</li> + </ul> + + <h3>Final Thoughts</h3> + <p>Every element of this theme has been carefully designed to create a harmonious reading experience. The {layout.replace('_', ' ')} layout ensures content is presented in an engaging way, while the color scheme provides the perfect backdrop for your words to shine.</p> + + <p>Whether you're writing technical documentation, creative prose, or anything in between, this theme adapts to showcase your content beautifully.</p> + </div> +</body> +</html>""" + + with open(theme_dir / "example.html", "w") as f: + f.write(example_html) + + # Generate LICENSE file + license_content = f"""Theme: {theme_name} +Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} + +Layout: {layout} +Color Scheme: {colors['palette_type']} ({'Dark' if colors['is_dark'] else 'Light'}) + +Font Licenses: +============== +Heading Font: {fonts['heading'][0]} +License: {fonts['heading'][1]} +Category: {fonts['heading'][2]} + +Body Font: {fonts['body'][0]} +License: {fonts['body'][1]} +Category: {fonts['body'][2]} + +Code Font: {fonts['code'][0]} +License: {fonts['code'][1]} +Category: {fonts['code'][2]} + +All fonts are free for personal use. +""" + + with open(theme_dir / "LICENSE", "w") as f: + f.write(license_content) + + return theme_name + +def generate_screenshot(theme_name): + """Generate a screenshot preview of the theme""" + theme_dir = Path(f"/home/paul/git/gemtexter/extras/html/themes/{theme_name}") + + # Create a simple preview image using PIL + img = Image.new('RGB', (400, 300), color='white') + draw = ImageDraw.Draw(img) + + # Try to load theme colors from style.css + with open(theme_dir / "style.css", "r") as f: + css_content = f.read() + + # Extract colors from CSS + import re + bg_match = re.search(r'--color-bg:\s*([#\w]+);', css_content) + primary_match = re.search(r'--color-primary:\s*([#\w]+);', css_content) + + if bg_match: + try: + bg_color = bg_match.group(1) + img = Image.new('RGB', (400, 300), color=bg_color) + draw = ImageDraw.Draw(img) + except: + pass + + # Add theme name + try: + font = ImageFont.truetype("/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf", 24) + except: + font = None + + text_color = 'black' if bg_match and bg_match.group(1).startswith('#f') else 'white' + draw.text((20, 20), theme_name.replace('_', ' ').title(), fill=text_color, font=font) + + # Add some preview elements + draw.rectangle((20, 60, 380, 62), fill=text_color) + draw.text((20, 80), "Preview of theme layout", fill=text_color) + + # Save screenshot + img.save(theme_dir.parent / "screenshots" / f"{theme_name}.png") + +def main(): + """Generate 50 unique themes""" + print("Generating 50 random themes...") + + themes = [] + used_names = set() + + # Ensure screenshots directory exists + screenshots_dir = Path("/home/paul/git/gemtexter/extras/html/themes/screenshots") + screenshots_dir.mkdir(exist_ok=True) + + while len(themes) < 50: + # Generate unique theme name + theme_name = f"{random.choice(ADJECTIVES)}_{random.choice(NOUNS)}" + if theme_name in used_names: + continue + used_names.add(theme_name) + + # Random font combination + fonts = random.choice(FONT_COMBINATIONS) + + # Random color palette + colors = generate_color_palette() + + # Random layout + layout = random.choice(LAYOUTS) + + # Random font sizes + font_sizes = generate_font_sizes() + + # Create theme + try: + created_theme = create_theme(theme_name, fonts, colors, layout, font_sizes) + themes.append({ + 'name': created_theme, + 'layout': layout, + 'colors': colors, + 'fonts': fonts + }) + print(f"Created theme {len(themes)}/50: {created_theme}") + + # Generate screenshot + generate_screenshot(created_theme) + except Exception as e: + print(f"Error creating theme {theme_name}: {e}") + + # Save theme metadata + with open("/home/paul/git/gemtexter/extras/html/themes/themes_metadata.json", "w") as f: + json.dump(themes, f, indent=2) + + print(f"\nSuccessfully created {len(themes)} themes!") + return themes + +if __name__ == "__main__": + main()
\ No newline at end of file |
