Created
July 14, 2026 18:01
-
-
Save greggman/62124f8802f7d7849642253ea4a8ac32 to your computer and use it in GitHub Desktop.
Browser Color Theme Generator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| :root { | |
| --font-ui: 'Outfit', sans-serif; | |
| --font-mockup: 'Inter', sans-serif; | |
| --font-code: 'Fira Code', monospace; | |
| --transition-fast: 0.15s cubic-bezier(0.4, 0, 0.2, 1); | |
| --transition-normal: 0.25s cubic-bezier(0.4, 0, 0.2, 1); | |
| --transition-slow: 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
| --sidebar-width: 360px; | |
| --border-radius-sm: 8px; | |
| --border-radius-md: 12px; | |
| --border-radius-lg: 16px; | |
| } | |
| .dark-app { | |
| --color-app-bg: #0D0F13; | |
| --color-sidebar-bg: #14171E; | |
| --color-surface: #1B1E26; | |
| --color-surface-elevated: #242A35; | |
| --color-text-primary: #F8F9FA; | |
| --color-text-secondary: #94A3B8; | |
| --color-border: #262C38; | |
| --color-dot: rgba(255, 255, 255, 0.04); | |
| --glass-bg: rgba(27, 30, 38, 0.65); | |
| --glass-border: rgba(255, 255, 255, 0.04); | |
| --color-mock-static-bg: #282C34; | |
| --color-mock-static-border: #3A404B; | |
| --color-mock-static-text: #ABB2BF; | |
| } | |
| .light-app { | |
| --color-app-bg: #F4F6F9; | |
| --color-sidebar-bg: #FFFFFF; | |
| --color-surface: #FAFBFD; | |
| --color-surface-elevated: #FFFFFF; | |
| --color-text-primary: #1E293B; | |
| --color-text-secondary: #64748B; | |
| --color-border: #E2E8F0; | |
| --color-dot: rgba(0, 0, 0, 0.04); | |
| --glass-bg: rgba(255, 255, 255, 0.7); | |
| --glass-border: rgba(0, 0, 0, 0.04); | |
| --color-mock-static-bg: #FFFFFF; | |
| --color-mock-static-border: #DFE1E5; | |
| --color-mock-static-text: #5F6368; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: var(--font-ui); | |
| background-color: var(--color-app-bg); | |
| color: var(--color-text-primary); | |
| width: 100vw; | |
| height: 100vh; | |
| overflow: hidden; | |
| display: flex; | |
| transition: background-color var(--transition-slow), color var(--transition-slow); | |
| } | |
| #app { | |
| display: flex; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| /* Sidebar Styling */ | |
| .sidebar { | |
| width: var(--sidebar-width); | |
| height: 100%; | |
| background-color: var(--color-sidebar-bg); | |
| border-right: 1px solid var(--color-border); | |
| display: flex; | |
| flex-direction: column; | |
| z-index: 10; | |
| flex-shrink: 0; | |
| box-shadow: 4px 0 24px rgba(0, 0, 0, 0.2); | |
| transition: background-color var(--transition-slow), border-color var(--transition-slow); | |
| } | |
| .app-header { | |
| padding: 20px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| border-bottom: 1px solid var(--color-border); | |
| } | |
| .logo-group { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .logo-group h1 { | |
| font-size: 1.2rem; | |
| font-weight: 700; | |
| letter-spacing: -0.5px; | |
| background: linear-gradient(135deg, var(--color-accent, #6366f1), #FF007F); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| transition: all var(--transition-normal); | |
| } | |
| .version-tag { | |
| font-size: 0.65rem; | |
| font-weight: 600; | |
| background-color: var(--color-surface); | |
| color: var(--color-text-secondary); | |
| padding: 4px 8px; | |
| border-radius: 12px; | |
| border: 1px solid var(--color-border); | |
| } | |
| .control-scroll { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| } | |
| .control-scroll::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .control-scroll::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| .control-scroll::-webkit-scrollbar-thumb { | |
| background: var(--color-border); | |
| border-radius: 3px; | |
| } | |
| .section { | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| border: 1px solid var(--glass-border); | |
| border-radius: var(--border-radius-md); | |
| padding: 16px; | |
| transition: all var(--transition-normal); | |
| } | |
| .section h2 { | |
| font-size: 0.8rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| color: var(--color-text-secondary); | |
| margin-bottom: 12px; | |
| } | |
| .section-desc { | |
| font-size: 0.75rem; | |
| color: var(--color-text-secondary); | |
| margin-top: 8px; | |
| line-height: 1.4; | |
| } | |
| .preset-grid { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 8px; | |
| margin-bottom: 16px; | |
| } | |
| .preset-btn { | |
| height: 34px; | |
| border-radius: var(--border-radius-sm); | |
| border: 2px solid transparent; | |
| cursor: pointer; | |
| transition: transform var(--transition-fast), border-color var(--transition-fast); | |
| box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); | |
| } | |
| .preset-btn:hover { | |
| transform: translateY(-2px) scale(1.05); | |
| } | |
| .preset-btn.active { | |
| border-color: var(--color-text-primary); | |
| box-shadow: 0 0 0 2px var(--color-sidebar-bg), 0 0 0 4px var(--color-accent); | |
| transform: scale(1); | |
| } | |
| .custom-color-row { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding-top: 12px; | |
| border-top: 1px solid var(--color-border); | |
| } | |
| .custom-color-row label { | |
| font-size: 0.85rem; | |
| font-weight: 500; | |
| } | |
| .color-picker-wrapper { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| background: var(--color-surface-elevated); | |
| padding: 4px 10px; | |
| border-radius: var(--border-radius-sm); | |
| border: 1px solid var(--color-border); | |
| } | |
| .color-picker-wrapper input[type="color"] { | |
| -webkit-appearance: none; | |
| border: none; | |
| width: 24px; | |
| height: 24px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| background: none; | |
| } | |
| .color-picker-wrapper input[type="color"]::-webkit-color-swatch-wrapper { | |
| padding: 0; | |
| } | |
| .color-picker-wrapper input[type="color"]::-webkit-color-swatch { | |
| border: none; | |
| border-radius: 6px; | |
| } | |
| .color-picker-wrapper span { | |
| font-size: 0.8rem; | |
| font-family: var(--font-code); | |
| font-weight: 500; | |
| color: var(--color-text-primary); | |
| } | |
| .segmented-control { | |
| display: flex; | |
| background-color: var(--color-surface-elevated); | |
| border-radius: var(--border-radius-sm); | |
| padding: 3px; | |
| border: 1px solid var(--color-border); | |
| } | |
| .seg-btn { | |
| flex: 1; | |
| background: none; | |
| border: none; | |
| padding: 6px 4px; | |
| font-family: var(--font-ui); | |
| font-size: 0.75rem; | |
| font-weight: 500; | |
| color: var(--color-text-secondary); | |
| border-radius: 6px; | |
| cursor: pointer; | |
| transition: all var(--transition-fast); | |
| } | |
| .seg-btn:hover { | |
| color: var(--color-text-primary); | |
| } | |
| .seg-btn.active { | |
| background-color: var(--color-surface); | |
| color: var(--color-text-primary); | |
| box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); | |
| font-weight: 600; | |
| } | |
| .visualizer-content { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 16px; | |
| } | |
| #colorWheelCanvas { | |
| background: none; | |
| flex-shrink: 0; | |
| } | |
| .readout-card { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .contrast-badge { | |
| background-color: var(--color-accent, #6366f1); | |
| color: #FFFFFF; | |
| padding: 6px 12px; | |
| border-radius: 20px; | |
| font-size: 0.7rem; | |
| font-weight: 600; | |
| text-align: center; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | |
| transition: background-color var(--transition-fast), color var(--transition-fast); | |
| } | |
| .contrast-sub { | |
| font-size: 0.7rem; | |
| color: var(--color-text-secondary); | |
| line-height: 1.4; | |
| } | |
| .export-desc { | |
| font-size: 0.75rem; | |
| color: var(--color-text-secondary); | |
| line-height: 1.4; | |
| margin-bottom: 12px; | |
| } | |
| .btn-primary { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| background: linear-gradient(135deg, var(--color-accent, #6366f1), #FF007F); | |
| color: #FFFFFF; | |
| border: none; | |
| width: 100%; | |
| padding: 12px; | |
| border-radius: var(--border-radius-sm); | |
| font-family: var(--font-ui); | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: transform var(--transition-fast), box-shadow var(--transition-fast); | |
| box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25); | |
| } | |
| .btn-primary:active { | |
| transform: translateY(0); | |
| } | |
| .guide-box { | |
| margin-top: 16px; | |
| background-color: var(--color-surface); | |
| border-radius: var(--border-radius-sm); | |
| padding: 12px; | |
| border: 1px solid var(--color-border); | |
| } | |
| .guide-box h4 { | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| color: var(--color-text-primary); | |
| margin-bottom: 6px; | |
| } | |
| .guide-box ol { | |
| font-size: 0.7rem; | |
| color: var(--color-text-secondary); | |
| padding-left: 16px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| } | |
| .guide-box li strong { | |
| color: var(--color-text-primary); | |
| } | |
| /* Main Preview Area Styling */ | |
| .preview-area { | |
| flex: 1; | |
| height: 100%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background-color: var(--color-app-bg); | |
| background-image: radial-gradient(var(--color-dot) 1px, transparent 1px); | |
| background-size: 24px 24px; | |
| padding: 40px; | |
| transition: background-color var(--transition-slow); | |
| } | |
| .preview-wrapper { | |
| width: 100%; | |
| max-width: 960px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .mockup-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .mockup-header h3 { | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| color: var(--color-text-secondary); | |
| } | |
| .mockup-controls { | |
| display: flex; | |
| gap: 6px; | |
| } | |
| .mockup-dot { | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| background-color: var(--color-surface); | |
| border: 1px solid var(--color-border); | |
| } | |
| /* Chrome Mockup Drawing (Strict Native Mappings) */ | |
| .chrome-mockup { | |
| width: 100%; | |
| border-radius: var(--border-radius-lg); | |
| overflow: hidden; | |
| box-shadow: 0 30px 80px rgba(0, 0, 0, 0.4); | |
| font-family: var(--font-mockup); | |
| display: flex; | |
| flex-direction: column; | |
| transition: box-shadow var(--transition-normal); | |
| } | |
| .chrome-frame { | |
| display: flex; | |
| flex-direction: column; | |
| padding-top: 8px; | |
| transition: background-color var(--transition-normal); | |
| } | |
| .chrome-tabs-row { | |
| display: flex; | |
| align-items: flex-end; | |
| padding-left: 12px; | |
| height: 36px; | |
| gap: 2px; | |
| } | |
| .chrome-tab { | |
| padding: 6px 14px; | |
| border-top-left-radius: 8px; | |
| border-top-right-radius: 8px; | |
| font-size: 0.72rem; | |
| font-weight: 500; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| height: 28px; | |
| cursor: default; | |
| position: relative; | |
| transition: background-color var(--transition-normal), color var(--transition-normal); | |
| } | |
| .chrome-tab.active { | |
| z-index: 2; | |
| box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); | |
| } | |
| .chrome-tab.inactive { | |
| z-index: 1; | |
| opacity: 0.9; | |
| } | |
| .icon-tab { | |
| opacity: 0.8; | |
| flex-shrink: 0; | |
| } | |
| .icon-close { | |
| opacity: 0.5; | |
| margin-left: 4px; | |
| flex-shrink: 0; | |
| } | |
| .chrome-newtab-plus { | |
| width: 24px; | |
| height: 24px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| margin-left: 6px; | |
| margin-bottom: 2px; | |
| opacity: 0.7; | |
| cursor: pointer; | |
| transition: background-color var(--transition-fast), color var(--transition-normal); | |
| } | |
| .chrome-newtab-plus:hover { | |
| background-color: rgba(255, 255, 255, 0.08); | |
| } | |
| .chrome-toolbar { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 8px 14px; | |
| z-index: 3; | |
| position: relative; | |
| transition: background-color var(--transition-normal); | |
| box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12); | |
| } | |
| .chrome-nav-buttons { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .chrome-nav-buttons svg { | |
| opacity: 0.85; | |
| transition: color var(--transition-normal); | |
| } | |
| .chrome-nav-buttons svg.disabled { | |
| opacity: 0.3; | |
| } | |
| .chrome-omnibox { | |
| flex: 1; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 6px 14px; | |
| border-radius: 30px; | |
| font-size: 0.75rem; | |
| transition: background-color var(--transition-normal), border-color var(--transition-normal), color var(--transition-normal); | |
| } | |
| .chrome-omnibox span { | |
| flex: 1; | |
| letter-spacing: 0.1px; | |
| } | |
| .star-icon { | |
| opacity: 0.7; | |
| } | |
| .chrome-menu-buttons { | |
| opacity: 0.85; | |
| transition: color var(--transition-normal); | |
| } | |
| .chrome-bookmark-bar { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| padding: 6px 16px 8px 16px; | |
| font-size: 0.7rem; | |
| font-weight: 500; | |
| transition: background-color var(--transition-normal), color var(--transition-normal); | |
| border-bottom: 1px solid rgba(0, 0, 0, 0.05); | |
| } | |
| .dark-app .chrome-bookmark-bar { | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.05); | |
| } | |
| .chrome-bookmark { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| opacity: 0.9; | |
| transition: color var(--transition-normal); | |
| } | |
| /* New Tab Page Area (Strict Mapping) */ | |
| .chrome-ntp-area { | |
| flex: 1; | |
| min-height: 420px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 32px; | |
| padding: 40px; | |
| transition: background-color var(--transition-normal); | |
| } | |
| .mock-foogle-logo { | |
| font-size: 4rem; | |
| font-weight: 700; | |
| letter-spacing: -2px; | |
| color: var(--color-mock-static-text); | |
| opacity: 0.4; | |
| user-select: none; | |
| } | |
| .mock-search-bar { | |
| width: 100%; | |
| max-width: 560px; | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| padding: 12px 22px; | |
| border-radius: 40px; | |
| font-size: 0.82rem; | |
| background-color: var(--color-mock-static-bg); | |
| border: 1px solid var(--color-mock-static-border); | |
| color: var(--color-mock-static-text); | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); | |
| opacity: 0.7; | |
| user-select: none; | |
| } | |
| .mock-search-bar svg { | |
| color: var(--color-mock-static-text); | |
| opacity: 0.6; | |
| } | |
| .mock-shortcuts { | |
| display: flex; | |
| gap: 32px; | |
| } | |
| .shortcut-item { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 8px; | |
| opacity: 0.65; | |
| } | |
| .shortcut-circle { | |
| width: 48px; | |
| height: 48px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| background-color: var(--color-mock-static-bg); | |
| border: 1px solid var(--color-mock-static-border); | |
| color: var(--color-mock-static-text); | |
| box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); | |
| } | |
| .shortcut-text { | |
| font-size: 0.68rem; | |
| font-weight: 500; | |
| color: var(--color-mock-static-text); | |
| } | |
| .mock-ntp-readout { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 6px; | |
| margin-top: 12px; | |
| padding: 12px 24px; | |
| border-radius: var(--border-radius-sm); | |
| background-color: rgba(0, 0, 0, 0.03); | |
| border: 1px dashed rgba(0, 0, 0, 0.08); | |
| transition: background-color var(--transition-normal), border-color var(--transition-normal); | |
| } | |
| .dark-app .mock-ntp-readout { | |
| background-color: rgba(255, 255, 255, 0.02); | |
| border: 1px dashed rgba(255, 255, 255, 0.08); | |
| } | |
| .mock-ntp-text { | |
| font-size: 0.8rem; | |
| font-weight: 500; | |
| transition: color var(--transition-normal); | |
| } | |
| .mock-ntp-link { | |
| font-size: 0.8rem; | |
| font-weight: 600; | |
| text-decoration: none; | |
| transition: color var(--transition-normal); | |
| } | |
| .mock-ntp-link:hover { | |
| text-decoration: underline; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>ThemeGen - Browser Theme Generator & Previewer</title> | |
| <link rel="stylesheet" href="index.css"> | |
| </head> | |
| <body class="dark-app"> | |
| <div id="app"> | |
| <!-- Sidebar Control Panel --> | |
| <aside class="sidebar"> | |
| <header class="app-header"> | |
| <div class="logo-group"> | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="32" height="32" class="header-logo"> | |
| <circle cx="64" cy="64" r="56" fill="url(#headerGrad)" /> | |
| <defs> | |
| <linearGradient id="headerGrad" x1="0%" y1="0%" x2="100%" y2="100%"> | |
| <stop offset="0%" stop-color="#FF007F" /> | |
| <stop offset="100%" stop-color="#00A896" /> | |
| </linearGradient> | |
| </defs> | |
| <circle cx="64" cy="64" r="28" fill="#FFFFFF" /> | |
| </svg> | |
| <h1>ThemeGen</h1> | |
| </div> | |
| <div class="version-tag">Native Themer</div> | |
| </header> | |
| <div class="control-scroll"> | |
| <section class="section preset-section"> | |
| <h2>Designer Presets</h2> | |
| <div class="preset-grid" id="presetGrid"> | |
| <!-- Populated via JS --> | |
| </div> | |
| <div class="custom-color-row"> | |
| <label for="baseColorInput">Custom Base Color</label> | |
| <div class="color-picker-wrapper"> | |
| <input type="color" id="baseColorInput" value="#3D5A80"> | |
| <span id="baseColorHex">#3D5A80</span> | |
| </div> | |
| </div> | |
| </section> | |
| <section class="section harmony-section"> | |
| <h2>Color Theory Harmony</h2> | |
| <div class="segmented-control" id="harmonyControl"> | |
| <button data-value="monochromatic" class="seg-btn">Mono</button> | |
| <button data-value="analogous" class="seg-btn">Analogous</button> | |
| <button data-value="complementary" class="seg-btn active">Complementary</button> | |
| <button data-value="triadic" class="seg-btn">Triad</button> | |
| </div> | |
| <p class="section-desc" id="harmonyDesc">Computes pure opposite hues for dynamic and balanced secondary | |
| contrasts.</p> | |
| </section> | |
| <section class="section mode-section"> | |
| <h2>UI Mode Target</h2> | |
| <div class="segmented-control" id="modeControl"> | |
| <button data-value="light" class="seg-btn">Premium Light</button> | |
| <button data-value="dark" class="seg-btn active">Midnight Dark</button> | |
| </div> | |
| </section> | |
| <section class="section visualizer-section"> | |
| <h2>Harmony Analysis</h2> | |
| <div class="visualizer-content"> | |
| <canvas id="colorWheelCanvas" width="160" height="160"></canvas> | |
| <div class="readout-card"> | |
| <div class="contrast-badge" id="contrastBadge">WCAG AAA Pass</div> | |
| <p class="contrast-sub">Relative luminance calculated to guarantee perfect off-white/charcoal text | |
| readability on your toolbar and frame.</p> | |
| </div> | |
| </div> | |
| </section> | |
| <section class="section export-section"> | |
| <h2>Save Theme</h2> | |
| <p class="export-desc">Exports <strong>manifest.json</strong> file | |
| ready to be loaded unpacked into your browser.</p> | |
| <button class="btn-primary" id="btnSave"> | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" | |
| stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path> | |
| <polyline points="17 21 17 13 7 13 7 21"></polyline> | |
| <polyline points="7 3 7 8 15 8"></polyline> | |
| </svg> | |
| Save manifest.json | |
| </button> | |
| <div class="guide-box"> | |
| <h4>How to Install Unpacked Theme:</h4> | |
| <ol> | |
| <li>Put <strong>manifest.json</strong> in a new empty folder.</li> | |
| <li>Go to <strong>chrome://extensions</strong>, enable Developer Mode.</li> | |
| <li>Click <strong>Load Unpacked</strong> and select that folder.</li> | |
| </ol> | |
| </div> | |
| </section> | |
| </div> | |
| </aside> | |
| <!-- Main Live Preview Area --> | |
| <main class="preview-area"> | |
| <div class="preview-wrapper"> | |
| <div class="mockup-header"> | |
| <h3>Live Native Chrome Theme Preview</h3> | |
| <div class="mockup-controls"> | |
| <span class="mockup-dot"></span> | |
| <span class="mockup-dot"></span> | |
| <span class="mockup-dot"></span> | |
| </div> | |
| </div> | |
| <!-- Chrome Browser Mockup (Strict Native Mappings) --> | |
| <div class="chrome-mockup"> | |
| <div class="chrome-frame"> | |
| <div class="chrome-tabs-row"> | |
| <!-- Active Tab (Inherits Toolbar background!) --> | |
| <div class="chrome-tab active"> | |
| <svg class="icon-tab" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="12" height="12" | |
| fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path> | |
| </svg> | |
| <span>New Tab (Active)</span> | |
| <svg class="icon-close" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="10" height="10" | |
| fill="none" stroke="currentColor" stroke-width="2.5"> | |
| <line x1="18" y1="6" x2="6" y2="18"></line> | |
| <line x1="6" y1="6" x2="18" y2="18"></line> | |
| </svg> | |
| </div> | |
| <!-- Inactive Tab (Inherits Frame background!) --> | |
| <div class="chrome-tab inactive"> | |
| <svg class="icon-tab" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="12" height="12" | |
| fill="none" stroke="currentColor" stroke-width="2"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| </svg> | |
| <span>Foogle (Inactive)</span> | |
| </div> | |
| <div class="chrome-tab inactive"> | |
| <svg class="icon-tab" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="12" height="12" | |
| fill="none" stroke="currentColor" stroke-width="2"> | |
| <path | |
| d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"> | |
| </path> | |
| </svg> | |
| <span>GitHub (Inactive)</span> | |
| </div> | |
| <div class="chrome-newtab-plus"> | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="12" height="12" fill="none" | |
| stroke="currentColor" stroke-width="2.5"> | |
| <line x1="12" y1="5" x2="12" y2="19"></line> | |
| <line x1="5" y1="12" x2="19" y2="12"></line> | |
| </svg> | |
| </div> | |
| </div> | |
| <!-- Toolbar Area (Sets toolbar color!) --> | |
| <div class="chrome-toolbar"> | |
| <div class="chrome-nav-buttons"> | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" | |
| stroke="currentColor" stroke-width="2"> | |
| <polyline points="15 18 9 12 15 6"></polyline> | |
| </svg> | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" | |
| stroke="currentColor" stroke-width="2" class="disabled"> | |
| <polyline points="9 18 15 12 9 6"></polyline> | |
| </svg> | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" | |
| stroke="currentColor" stroke-width="2"> | |
| <path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67"></path> | |
| </svg> | |
| </div> | |
| <div class="chrome-omnibox"> | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="12" height="12" fill="none" | |
| stroke="currentColor" stroke-width="2"> | |
| <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> | |
| <path d="M7 11V7a5 5 0 0 1 10 0v4"></path> | |
| </svg> | |
| <span>chrome://newtab</span> | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="12" height="12" fill="none" | |
| stroke="currentColor" stroke-width="2" class="star-icon"> | |
| <polygon | |
| points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"> | |
| </polygon> | |
| </svg> | |
| </div> | |
| <div class="chrome-menu-buttons"> | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" | |
| stroke="currentColor" stroke-width="2"> | |
| <circle cx="12" cy="12" r="1"></circle> | |
| <circle cx="12" cy="5" r="1"></circle> | |
| <circle cx="12" cy="19" r="1"></circle> | |
| </svg> | |
| </div> | |
| </div> | |
| <!-- Bookmark Bar (Inherits Toolbar background!) --> | |
| <div class="chrome-bookmark-bar"> | |
| <div class="chrome-bookmark"> | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="12" height="12" fill="none" | |
| stroke="currentColor" stroke-width="2"> | |
| <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path> | |
| </svg> | |
| <span>Work Folder</span> | |
| </div> | |
| <div class="chrome-bookmark"> | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="12" height="12" fill="none" | |
| stroke="currentColor" stroke-width="2"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| </svg> | |
| <span>YouTube</span> | |
| </div> | |
| <div class="chrome-bookmark"> | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="12" height="12" fill="none" | |
| stroke="currentColor" stroke-width="2"> | |
| <polygon | |
| points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"> | |
| </polygon> | |
| </svg> | |
| <span>Favorites</span> | |
| </div> | |
| </div> | |
| <!-- New Tab Page Area (Inherits ntp_background, ntp_text, ntp_link!) --> | |
| <div class="chrome-ntp-area"> | |
| <!-- Neutral static Foogle logo representing the actual un-themable Chrome NTP Foogle logo --> | |
| <div class="mock-foogle-logo">Foogle</div> | |
| <!-- Static neutral search bar to avoid misleading the user --> | |
| <div class="mock-search-bar"> | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" | |
| stroke="currentColor" stroke-width="2"> | |
| <circle cx="11" cy="11" r="8"></circle> | |
| <line x1="21" y1="21" x2="16.65" y2="16.65"></line> | |
| </svg> | |
| <span>Search Foogle or type a URL</span> | |
| </div> | |
| <div class="mock-shortcuts"> | |
| <div class="shortcut-item"> | |
| <div class="shortcut-circle">G</div> | |
| <span class="shortcut-text">Gmail</span> | |
| </div> | |
| <div class="shortcut-item"> | |
| <div class="shortcut-circle">YT</div> | |
| <span class="shortcut-text">YouTube</span> | |
| </div> | |
| <div class="shortcut-item"> | |
| <div class="shortcut-circle">GH</div> | |
| <span class="shortcut-text">GitHub</span> | |
| </div> | |
| <div class="shortcut-item"> | |
| <div class="shortcut-circle">CAL</div> | |
| <span class="shortcut-text">Calendar</span> | |
| </div> | |
| </div> | |
| <!-- Sample text & link to visualize ntp_text and ntp_link --> | |
| <div class="mock-ntp-readout"> | |
| <p class="mock-ntp-text">Sample Native NTP Text</p> | |
| <a href="#" class="mock-ntp-link">Sample Native NTP Link</a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <script src="index.js"></script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * ThemeGen - Unified Chrome Theme Generator & Previewer System | |
| * Implements DEEP and HIGHLY VISIBLE Graphic Design Harmonies. | |
| * Shifting the Hue of the Frame, NTP Background, and Accents dynamically | |
| * based on Monochromatic, Analogous, Complementary, and Triadic calculations. | |
| */ | |
| const ColorUtils = { | |
| hexToRgb(hex) { | |
| hex = hex.replace(/^#/, ''); | |
| if (hex.length === 3) { | |
| hex = hex.split('').map(c => c + c).join(''); | |
| } | |
| const num = parseInt(hex, 16); | |
| return [(num >> 16) & 255, (num >> 8) & 255, num & 255]; | |
| }, | |
| rgbToHex(r, g, b) { | |
| return "#" + [r, g, b].map(x => { | |
| const hex = x.toString(16); | |
| return hex.length === 1 ? '0' + hex : hex; | |
| }).join(''); | |
| }, | |
| rgbToHsl(r, g, b) { | |
| r /= 255; g /= 255; b /= 255; | |
| const max = Math.max(r, g, b), min = Math.min(r, g, b); | |
| let h, s, l = (max + min) / 2; | |
| if (max === min) { | |
| h = s = 0; | |
| } else { | |
| const d = max - min; | |
| s = l > 0.5 ? d / (2 - max - min) : d / (max + min); | |
| switch (max) { | |
| case r: h = (g - b) / d + (g < b ? 6 : 0); break; | |
| case g: h = (b - r) / d + 2; break; | |
| case b: h = (r - g) / d + 4; break; | |
| } | |
| h /= 6; | |
| } | |
| return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)]; | |
| }, | |
| hslToRgb(h, s, l) { | |
| h /= 360; s /= 100; l /= 100; | |
| let r, g, b; | |
| if (s === 0) { | |
| r = g = b = l; | |
| } else { | |
| const hue2rgb = (p, q, t) => { | |
| if (t < 0) t += 1; | |
| if (t > 1) t -= 1; | |
| if (t < 1 / 6) return p + (q - p) * 6 * t; | |
| if (t < 1 / 2) return q; | |
| if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; | |
| return p; | |
| }; | |
| const q = l < 0.5 ? l * (1 + s) : l + s - l * s; | |
| const p = 2 * l - q; | |
| r = hue2rgb(p, q, h + 1 / 3); | |
| g = hue2rgb(p, q, h); | |
| b = hue2rgb(p, q, h - 1 / 3); | |
| } | |
| return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; | |
| }, | |
| getLuminance(r, g, b) { | |
| const a = [r, g, b].map(v => { | |
| v /= 255; | |
| return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); | |
| }); | |
| return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2]; | |
| }, | |
| getContrastColor(r, g, b, baseHue) { | |
| const l = this.getLuminance(r, g, b); | |
| if (l > 0.38) { | |
| const [tr, tg, tb] = this.hslToRgb(baseHue, 30, 8); | |
| return [tr, tg, tb]; | |
| } else { | |
| const [tr, tg, tb] = this.hslToRgb(baseHue, 12, 97); | |
| return [tr, tg, tb]; | |
| } | |
| }, | |
| getSecondaryTextColor(r, g, b, baseHue) { | |
| const l = this.getLuminance(r, g, b); | |
| if (l > 0.38) { | |
| const [tr, tg, tb] = this.hslToRgb(baseHue, 20, 36); | |
| return [tr, tg, tb]; | |
| } else { | |
| const [tr, tg, tb] = this.hslToRgb(baseHue, 15, 78); | |
| return [tr, tg, tb]; | |
| } | |
| } | |
| }; | |
| const PaletteGenerator = { | |
| generate(baseHex, harmonyMode = 'complementary', themeMode = 'dark') { | |
| const baseRgb = ColorUtils.hexToRgb(baseHex); | |
| const [h, s, l] = ColorUtils.rgbToHsl(...baseRgb); | |
| const primaryHue = h; | |
| let frameHue, ntpBgHue, ntpLinkHue, secondaryHue, tertiaryHue; | |
| // Compute Hues with HIGHLY VISIBLE and meaningful graphic design purposes | |
| switch (harmonyMode) { | |
| case 'monochromatic': | |
| frameHue = primaryHue; | |
| ntpBgHue = primaryHue; | |
| ntpLinkHue = primaryHue; | |
| secondaryHue = primaryHue; | |
| tertiaryHue = primaryHue; | |
| break; | |
| case 'analogous': | |
| frameHue = (primaryHue + 30) % 360; | |
| ntpBgHue = (primaryHue - 30 + 360) % 360; | |
| ntpLinkHue = (primaryHue + 30) % 360; | |
| secondaryHue = (primaryHue + 30) % 360; | |
| tertiaryHue = (primaryHue - 30 + 360) % 360; | |
| break; | |
| case 'complementary': | |
| frameHue = (primaryHue + 180) % 360; // Frame adopts the COMPLEMENT! | |
| ntpBgHue = primaryHue; | |
| ntpLinkHue = (primaryHue + 180) % 360; // Links pop in Complement | |
| secondaryHue = (primaryHue + 180) % 360; | |
| tertiaryHue = primaryHue; | |
| break; | |
| case 'triadic': | |
| frameHue = (primaryHue + 120) % 360; // First Triad hue for Frame | |
| ntpBgHue = (primaryHue + 240) % 360; // Second Triad hue for NTP | |
| ntpLinkHue = (primaryHue + 120) % 360; | |
| secondaryHue = (primaryHue + 120) % 360; | |
| tertiaryHue = (primaryHue + 240) % 360; | |
| break; | |
| default: | |
| frameHue = primaryHue; | |
| ntpBgHue = primaryHue; | |
| ntpLinkHue = (primaryHue + 180) % 360; | |
| secondaryHue = (primaryHue + 180) % 360; | |
| tertiaryHue = primaryHue; | |
| } | |
| const safeSat = Math.max(s, 35); // Ensure there is enough saturation to actually see the hues! | |
| let colors = {}; | |
| let frameRgb, frameInactiveRgb, bgRgb, buttonBgRgb; | |
| if (themeMode === 'dark') { | |
| // Dark Mode: Highly colorful Frame and deep NTP background | |
| frameRgb = ColorUtils.hslToRgb(frameHue, Math.min(safeSat, 55), Math.max(Math.min(l - 12, 16), 11)); | |
| frameInactiveRgb = ColorUtils.hslToRgb(frameHue, Math.min(safeSat * 0.7, 40), Math.max(Math.min(l - 16, 12), 8)); | |
| bgRgb = ColorUtils.hslToRgb(ntpBgHue, Math.min(safeSat * 0.4, 25), 8); | |
| buttonBgRgb = ColorUtils.hslToRgb(frameHue, Math.min(safeSat, 45), 24); | |
| } else { | |
| // Light Mode: Frame adopts a gorgeous pastel/muted variant of the harmony hue | |
| frameRgb = ColorUtils.hslToRgb(frameHue, Math.min(safeSat * 0.5, 35), Math.min(Math.max(l + 12, 85), 90)); | |
| frameInactiveRgb = ColorUtils.hslToRgb(frameHue, Math.min(safeSat * 0.3, 25), Math.min(Math.max(l + 16, 90), 93)); | |
| bgRgb = ColorUtils.hslToRgb(ntpBgHue, Math.min(safeSat * 0.15, 12), 97); | |
| buttonBgRgb = ColorUtils.hslToRgb(frameHue, Math.min(safeSat * 0.4, 30), 85); | |
| } | |
| const textPrimary = ColorUtils.getContrastColor(...baseRgb, primaryHue); | |
| const textSecondary = ColorUtils.getSecondaryTextColor(...baseRgb, primaryHue); | |
| const accentRgb = ColorUtils.hslToRgb(ntpLinkHue, Math.max(safeSat, 75), themeMode === 'dark' ? 65 : 45); | |
| // If Monochromatic, make Frame a deeper/lighter shade of the EXACT toolbar hue | |
| if (harmonyMode === 'monochromatic') { | |
| if (themeMode === 'dark') { | |
| frameRgb = ColorUtils.hslToRgb(primaryHue, safeSat * 0.9, Math.max(l - 10, 8)); | |
| frameInactiveRgb = ColorUtils.hslToRgb(primaryHue, safeSat * 0.7, Math.max(l - 14, 6)); | |
| } else { | |
| frameRgb = ColorUtils.hslToRgb(primaryHue, safeSat * 0.4, Math.min(l + 12, 88)); | |
| frameInactiveRgb = ColorUtils.hslToRgb(primaryHue, safeSat * 0.2, Math.min(l + 16, 92)); | |
| } | |
| } | |
| colors = { | |
| primary: baseHex, | |
| secondary: ColorUtils.rgbToHex(accentRgb[0], accentRgb[1], accentRgb[2]), | |
| tertiary: ColorUtils.rgbToHex(...ColorUtils.hslToRgb(tertiaryHue, Math.max(safeSat, 70), 55)), | |
| background: ColorUtils.rgbToHex(...bgRgb), | |
| surface: baseHex, | |
| surfaceElevated: ColorUtils.rgbToHex(...buttonBgRgb), | |
| textPrimary: ColorUtils.rgbToHex(...textPrimary), | |
| textSecondary: ColorUtils.rgbToHex(...textSecondary), | |
| borderColor: ColorUtils.rgbToHex(...ColorUtils.hslToRgb(primaryHue, safeSat * 0.3, themeMode === 'dark' ? 22 : 84)), | |
| chrome: { | |
| frame: frameRgb, | |
| frame_inactive: frameInactiveRgb, | |
| frame_incognito: [30, 32, 38], | |
| frame_incognito_inactive: [22, 24, 28], | |
| toolbar: baseRgb, // EXACTLY the user's selected base color! | |
| tab_text: textPrimary, | |
| tab_background_text: ColorUtils.getSecondaryTextColor(...frameRgb, primaryHue), | |
| bookmark_text: textPrimary, | |
| ntp_background: bgRgb, | |
| ntp_text: ColorUtils.getContrastColor(...bgRgb, primaryHue), | |
| ntp_link: accentRgb, | |
| button_background: buttonBgRgb | |
| } | |
| }; | |
| return { | |
| baseHex: baseHex.toUpperCase(), | |
| harmonyMode, | |
| themeMode, | |
| primaryHue, | |
| secondaryHue, | |
| tertiaryHue, | |
| colors | |
| }; | |
| }, | |
| generateNativeThemeJson(palette) { | |
| const c = palette.colors.chrome; | |
| const manifest = { | |
| manifest_version: 3, | |
| name: `ThemeGen - ${palette.baseHex} ${palette.themeMode.charAt(0).toUpperCase() + palette.themeMode.slice(1)} Theme`, | |
| version: "1.0.0", | |
| description: `Premium graphic-design principled Chrome theme generated dynamically from ${palette.baseHex} using a ${palette.harmonyMode} harmony.`, | |
| theme: { | |
| colors: { | |
| frame: c.frame, | |
| frame_inactive: c.frame_inactive, | |
| frame_incognito: c.frame_incognito, | |
| frame_incognito_inactive: c.frame_incognito_inactive, | |
| toolbar: c.toolbar, | |
| tab_text: c.tab_text, | |
| tab_background_text: c.tab_background_text, | |
| bookmark_text: c.bookmark_text, | |
| ntp_background: c.ntp_background, | |
| ntp_text: c.ntp_text, | |
| ntp_link: c.ntp_link, | |
| button_background: c.button_background | |
| }, | |
| tints: { | |
| buttons: [palette.secondaryHue / 360, 0.3, palette.themeMode === 'dark' ? 0.6 : 0.4] | |
| }, | |
| properties: { | |
| ntp_background_alignment: "center", | |
| ntp_background_repeat: "no-repeat" | |
| } | |
| } | |
| }; | |
| return JSON.stringify(manifest, null, 2); | |
| } | |
| }; | |
| function drawColorWheel(canvas, primaryHue, secondaryHue, tertiaryHue, harmonyMode, themeMode) { | |
| const ctx = canvas.getContext('2d'); | |
| const width = canvas.width; | |
| const height = canvas.height; | |
| const cx = width / 2; | |
| const cy = height / 2; | |
| const radius = Math.min(cx, cy) - 10; | |
| ctx.clearRect(0, 0, width, height); | |
| for (let angle = 0; angle < 360; angle++) { | |
| const startAngle = (angle - 0.5) * Math.PI / 180; | |
| const endAngle = (angle + 1.5) * Math.PI / 180; | |
| ctx.beginPath(); | |
| ctx.moveTo(cx, cy); | |
| ctx.arc(cx, cy, radius, startAngle, endAngle); | |
| ctx.closePath(); | |
| ctx.fillStyle = `hsl(${angle}, 85%, 55%)`; | |
| ctx.fill(); | |
| } | |
| ctx.beginPath(); | |
| ctx.arc(cx, cy, radius * 0.68, 0, 2 * Math.PI); | |
| ctx.fillStyle = themeMode === 'dark' ? '#14171E' : '#FFFFFF'; | |
| ctx.fill(); | |
| const drawDot = (hue, isPrimary = false) => { | |
| const rad = hue * Math.PI / 180; | |
| const rDist = radius * 0.84; | |
| const px = cx + rDist * Math.cos(rad); | |
| const py = cy + rDist * Math.sin(rad); | |
| ctx.beginPath(); | |
| ctx.moveTo(cx, cy); | |
| ctx.lineTo(px, py); | |
| ctx.strokeStyle = isPrimary ? (themeMode === 'dark' ? 'rgba(255, 255, 255, 0.4)' : 'rgba(0, 0, 0, 0.25)') : (themeMode === 'dark' ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.1)'); | |
| ctx.lineWidth = isPrimary ? 2 : 1; | |
| ctx.setLineDash([4, 4]); | |
| ctx.stroke(); | |
| ctx.setLineDash([]); | |
| ctx.beginPath(); | |
| ctx.arc(px, py, isPrimary ? 8 : 6, 0, 2 * Math.PI); | |
| ctx.fillStyle = `hsl(${hue}, 90%, 65%)`; | |
| ctx.strokeStyle = '#FFFFFF'; | |
| ctx.lineWidth = 2; | |
| ctx.shadowBlur = 8; | |
| ctx.shadowColor = `hsl(${hue}, 90%, 65%)`; | |
| ctx.fill(); | |
| ctx.stroke(); | |
| ctx.shadowBlur = 0; | |
| }; | |
| ctx.beginPath(); | |
| const radP = primaryHue * Math.PI / 180; | |
| const radS = secondaryHue * Math.PI / 180; | |
| const rDist = radius * 0.84; | |
| ctx.moveTo(cx + rDist * Math.cos(radP), cy + rDist * Math.sin(radP)); | |
| if (harmonyMode === 'complementary') { | |
| ctx.lineTo(cx + rDist * Math.cos(radS), cy + rDist * Math.sin(radS)); | |
| } else if (harmonyMode === 'triadic') { | |
| const radT = tertiaryHue * Math.PI / 180; | |
| ctx.lineTo(cx + rDist * Math.cos(radS), cy + rDist * Math.sin(radS)); | |
| ctx.lineTo(cx + rDist * Math.cos(radT), cy + rDist * Math.sin(radT)); | |
| ctx.closePath(); | |
| } else if (harmonyMode === 'analogous') { | |
| ctx.lineTo(cx + rDist * Math.cos(radS), cy + rDist * Math.sin(radS)); | |
| const radT = tertiaryHue * Math.PI / 180; | |
| ctx.moveTo(cx + rDist * Math.cos(radP), cy + rDist * Math.sin(radP)); | |
| ctx.lineTo(cx + rDist * Math.cos(radT), cy + rDist * Math.sin(radT)); | |
| } | |
| ctx.strokeStyle = themeMode === 'dark' ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.15)'; | |
| ctx.lineWidth = 1.5; | |
| ctx.stroke(); | |
| if (harmonyMode === 'triadic') { | |
| drawDot(secondaryHue); | |
| drawDot(tertiaryHue); | |
| } else if (harmonyMode === 'analogous') { | |
| drawDot(secondaryHue); | |
| drawDot(tertiaryHue); | |
| } else if (harmonyMode === 'complementary') { | |
| drawDot(secondaryHue); | |
| } | |
| drawDot(primaryHue, true); | |
| } | |
| const presets = [ | |
| { name: 'Sapphire Slate', hex: '#3D5A80' }, | |
| { name: 'Aurora Teal', hex: '#00A896' }, | |
| { name: 'Terracotta Sunset', hex: '#E07A5F' }, | |
| { name: 'Neon Cyberpunk', hex: '#FF007F' }, | |
| { name: 'Forest Matcha', hex: '#81B622' }, | |
| { name: 'Amethyst Royal', hex: '#7209B7' }, | |
| { name: 'Amber Gold', hex: '#F4A261' }, | |
| { name: 'Nordic Snow', hex: '#8E9AAF' } | |
| ]; | |
| let currentConfig = { | |
| baseHex: '#3D5A80', | |
| harmonyMode: 'complementary', | |
| themeMode: 'dark' | |
| }; | |
| const presetGrid = document.getElementById('presetGrid'); | |
| const baseColorInput = document.getElementById('baseColorInput'); | |
| const baseColorHex = document.getElementById('baseColorHex'); | |
| const harmonyControl = document.getElementById('harmonyControl'); | |
| const modeControl = document.getElementById('modeControl'); | |
| const harmonyDesc = document.getElementById('harmonyDesc'); | |
| const canvas = document.getElementById('colorWheelCanvas'); | |
| const contrastBadge = document.getElementById('contrastBadge'); | |
| const btnSave = document.getElementById('btnSave'); | |
| function init() { | |
| presets.forEach(p => { | |
| const btn = document.createElement('button'); | |
| btn.className = 'preset-btn'; | |
| btn.style.backgroundColor = p.hex; | |
| btn.title = p.name; | |
| btn.setAttribute('data-hex', p.hex); | |
| btn.addEventListener('click', () => { | |
| updateConfig({ baseHex: p.hex }); | |
| }); | |
| presetGrid.appendChild(btn); | |
| }); | |
| baseColorInput.addEventListener('input', (e) => { | |
| const hex = e.target.value.toUpperCase(); | |
| baseColorHex.textContent = hex; | |
| updateConfig({ baseHex: hex }); | |
| }); | |
| setupSegmentedControl('harmonyControl', 'harmonyMode', (val) => { | |
| const descs = { | |
| monochromatic: "Varies saturation and lightness across your selected hue for a minimal, unified aesthetic.", | |
| analogous: "Shifts your Window Frame and NTP background to adjacent (± 30°) hues for a natural, harmonious palette.", | |
| complementary: "Shifts your Window Frame and Accents into pure opposite hues (180°) for a high-contrast dynamic palette.", | |
| triadic: "Forms an equilateral triangle (120° and 240°), splitting your Toolbar, Frame, and NTP into three beautifully balanced hues." | |
| }; | |
| harmonyDesc.textContent = descs[val] || ""; | |
| }); | |
| setupSegmentedControl('modeControl', 'themeMode'); | |
| btnSave.addEventListener('click', () => { | |
| const palette = PaletteGenerator.generate( | |
| currentConfig.baseHex, | |
| currentConfig.harmonyMode, | |
| currentConfig.themeMode | |
| ); | |
| const jsonStr = PaletteGenerator.generateNativeThemeJson(palette); | |
| const blob = new Blob([jsonStr], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'manifest.json'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }); | |
| applyConfig(); | |
| } | |
| function setupSegmentedControl(elementId, configKey, onUpdate) { | |
| const container = document.getElementById(elementId); | |
| const btns = container.querySelectorAll('.seg-btn'); | |
| btns.forEach(b => { | |
| b.addEventListener('click', () => { | |
| btns.forEach(x => x.classList.remove('active')); | |
| b.classList.add('active'); | |
| const val = b.getAttribute('data-value'); | |
| updateConfig({ [configKey]: val }); | |
| if (onUpdate) onUpdate(val); | |
| }); | |
| }); | |
| btns.forEach(b => { | |
| if (b.getAttribute('data-value') === currentConfig[configKey]) { | |
| b.classList.add('active'); | |
| } else { | |
| b.classList.remove('active'); | |
| } | |
| }); | |
| } | |
| function updateConfig(newPartial) { | |
| currentConfig = { ...currentConfig, ...newPartial }; | |
| applyConfig(); | |
| } | |
| function applyConfig() { | |
| baseColorInput.value = currentConfig.baseHex; | |
| baseColorHex.textContent = currentConfig.baseHex.toUpperCase(); | |
| const harmBtns = harmonyControl.querySelectorAll('.seg-btn'); | |
| harmBtns.forEach(b => { | |
| b.classList.toggle('active', b.getAttribute('data-value') === currentConfig.harmonyMode); | |
| }); | |
| const modeBtns = modeControl.querySelectorAll('.seg-btn'); | |
| modeBtns.forEach(b => { | |
| b.classList.toggle('active', b.getAttribute('data-value') === currentConfig.themeMode); | |
| }); | |
| // Highlight active preset | |
| const presetBtns = presetGrid.querySelectorAll('.preset-btn'); | |
| presetBtns.forEach(b => { | |
| b.classList.toggle('active', b.getAttribute('data-hex').toUpperCase() === currentConfig.baseHex.toUpperCase()); | |
| }); | |
| // 1. Generate Palette | |
| const palette = PaletteGenerator.generate( | |
| currentConfig.baseHex, | |
| currentConfig.harmonyMode, | |
| currentConfig.themeMode | |
| ); | |
| // 2. Set Application CSS Variables | |
| document.body.className = currentConfig.themeMode === 'dark' ? 'dark-app' : 'light-app'; | |
| document.body.style.setProperty('--color-accent', palette.colors.primary); | |
| // 3. Update Contrast Readout (Based on EXACT Toolbar Surface) | |
| const toolbarRgbTuple = palette.colors.chrome.toolbar; | |
| const badgeText = ColorUtils.getContrastColor(...toolbarRgbTuple, palette.primaryHue); | |
| contrastBadge.style.backgroundColor = palette.colors.primary; | |
| contrastBadge.style.color = ColorUtils.rgbToHex(...badgeText); | |
| const l = ColorUtils.getLuminance(...toolbarRgbTuple); | |
| if (l > 0.38) { | |
| contrastBadge.textContent = "WCAG AAA Reading Pass"; | |
| } else { | |
| contrastBadge.textContent = "WCAG AAA Immersive Pass"; | |
| } | |
| // 4. Map Palette EXACTLY and ONLY to Native Chrome Mockup Elements | |
| const c = palette.colors.chrome; | |
| const frameRgb = `rgb(${c.frame.join(',')})`; | |
| const toolbarRgb = `rgb(${c.toolbar.join(',')})`; | |
| const tabTextRgb = `rgb(${c.tab_text.join(',')})`; | |
| const tabBgTextRgb = `rgb(${c.tab_background_text.join(',')})`; | |
| const bookmarkTextRgb = `rgb(${c.bookmark_text.join(',')})`; | |
| const ntpBgRgb = `rgb(${c.ntp_background.join(',')})`; | |
| const ntpTextRgb = `rgb(${c.ntp_text.join(',')})`; | |
| const ntpLinkRgb = `rgb(${c.ntp_link.join(',')})`; | |
| const mockFrame = document.querySelector('.chrome-frame'); | |
| const mockToolbar = document.querySelector('.chrome-toolbar'); | |
| const mockBookmarkBar = document.querySelector('.chrome-bookmark-bar'); | |
| const mockNtpArea = document.querySelector('.chrome-ntp-area'); | |
| const mockActiveTab = document.querySelector('.chrome-tab.active'); | |
| const mockInactiveTabs = document.querySelectorAll('.chrome-tab.inactive'); | |
| const mockOmnibox = document.querySelector('.chrome-omnibox'); | |
| const mockPlusIcon = document.querySelector('.chrome-newtab-plus'); | |
| const mockNtpText = document.querySelector('.mock-ntp-text'); | |
| const mockNtpLink = document.querySelector('.mock-ntp-link'); | |
| // Frame color paints the title bar and background of inactive tabs | |
| mockFrame.style.backgroundColor = frameRgb; | |
| // Toolbar color paints the main toolbar, the active tab background, and the bookmark bar | |
| mockToolbar.style.backgroundColor = toolbarRgb; | |
| mockBookmarkBar.style.backgroundColor = toolbarRgb; | |
| mockActiveTab.style.backgroundColor = toolbarRgb; | |
| // ntp_background paints the New Tab Page viewport | |
| mockNtpArea.style.backgroundColor = ntpBgRgb; | |
| // Set tab texts | |
| mockActiveTab.style.color = tabTextRgb; | |
| mockInactiveTabs.forEach(t => { | |
| t.style.backgroundColor = frameRgb; | |
| t.style.color = tabBgTextRgb; | |
| }); | |
| // Set Bookmark text | |
| mockBookmarkBar.style.color = bookmarkTextRgb; | |
| // Omnibox adopts a perfectly calculated relative tint | |
| mockOmnibox.style.backgroundColor = currentConfig.themeMode === 'dark' ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.05)'; | |
| mockOmnibox.style.color = tabTextRgb; | |
| mockOmnibox.style.border = 'none'; | |
| mockPlusIcon.style.color = tabTextRgb; | |
| // Map ntp_text and ntp_link to their respective static readouts | |
| mockNtpText.style.color = ntpTextRgb; | |
| mockNtpLink.style.color = ntpLinkRgb; | |
| // Toolbar Iconsadopt exact tab_text and bookmark_text mappings | |
| document.querySelectorAll('.chrome-nav-buttons svg, .chrome-menu-buttons svg, .icon-close').forEach(svg => { | |
| svg.style.color = tabTextRgb; | |
| }); | |
| document.querySelectorAll('.chrome-bookmark svg').forEach(svg => { | |
| svg.style.color = bookmarkTextRgb; | |
| }); | |
| // 5. Draw Canvas Color Wheel | |
| drawColorWheel( | |
| canvas, | |
| palette.primaryHue, | |
| palette.secondaryHue, | |
| palette.tertiaryHue, | |
| currentConfig.harmonyMode, | |
| currentConfig.themeMode | |
| ); | |
| } | |
| init(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| {"name":"Browser Color Theme Generator ","settings":{},"filenames":["index.html","index.css","index.js"]} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment