Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 115 additions & 21 deletions assets/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -189,41 +189,135 @@ h1 span {
}

#custom-color-picker {
display: none;
}

.color-picker-wrap {
position: relative;
display: inline-flex;
align-items: center;
}

.color-picker-trigger {
background: conic-gradient(red, yellow, lime, aqua, blue, magenta, red);
cursor: pointer;
width: 28px;
height: 28px;
border-radius: 50%;
padding: 0;
border: none;
overflow: hidden;
appearance: none;
-webkit-appearance: none;
cursor: pointer;
background: conic-gradient(red, yellow, lime, aqua, blue, magenta, red);
transition: transform 0.15s;
border: 2px solid transparent;
transition: transform 0.15s, border-color 0.15s;
flex-shrink: 0;
}

#custom-color-picker::-webkit-color-swatch-wrapper {
padding: 0;
.color-picker-trigger:hover {
transform: scale(1.15);
}

#custom-color-picker::-webkit-color-swatch {
border: none;
border-radius: 50%;
background: transparent;
.color-picker-trigger.active {
border-color: #fff;
}

.color-picker-panel {
display: none;
position: absolute;
top: calc(100% + 12px);
left: 50%;
transform: translateX(-50%);
width: 240px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px;
z-index: 100;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
flex-direction: column;
gap: 12px;
}

#custom-color-picker::-moz-color-swatch {
.color-picker-panel.open {
display: flex;
}

.cp-preview-row {
display: flex;
align-items: center;
gap: 10px;
}

.cp-swatch {
width: 32px;
height: 32px;
border-radius: 6px;
border: 1px solid var(--border);
flex-shrink: 0;
background: #58a6ff;
}

.cp-hex-input {
flex: 1;
background: var(--surface);
border: 1px solid var(--border);
color: var(--fg);
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
padding: 6px 10px;
border-radius: 6px;
outline: none;
transition: border-color 0.2s;
}

.cp-hex-input:focus {
border-color: var(--blue);
}

.cp-sliders {
display: flex;
flex-direction: column;
gap: 8px;
}

.cp-label {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--fg3);
}

.cp-range {
width: 100%;
height: 6px;
border-radius: 3px;
outline: none;
border: none;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
}

.cp-range::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: transparent;
background: #fff;
border: 2px solid var(--border);
cursor: pointer;
box-shadow: 0 1px 4px rgba(0,0,0,0.4);
}

#custom-color-picker:hover {
transform: scale(1.15);
.cp-range::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 50%;
background: #fff;
border: 2px solid var(--border);
cursor: pointer;
}

#custom-color-picker.active {
border-color: #fff;
.cp-hue {
background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);
}

.theme-dot[data-theme="blue"] {
Expand Down Expand Up @@ -726,7 +820,7 @@ h1 span {
.platform-spacer {
width: 100%;
height: 77px;
background: #0d1117;
background: transparent;
flex-shrink: 0;
position: relative;
z-index: 15;
Expand Down
63 changes: 63 additions & 0 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,3 +286,66 @@ applyTheme(DEFAULT_THEME);
applyTemplate('grid');
applyPlatform('mobile');
startBlobAnimation();

// Custom color picker initialization
(function initCustomPicker() {
const wrap = document.getElementById('custom-picker-wrap');
const trigger = document.getElementById('custom-picker-trigger');
const panel = document.getElementById('custom-picker-panel');
const swatch = document.getElementById('cp-swatch');
const hexInput = document.getElementById('cp-hex');
const hueSlider = document.getElementById('cp-hue');
const satSlider = document.getElementById('cp-sat');
const litSlider = document.getElementById('cp-lit');

if (!wrap) return;

function hslToHex(h, s, l) {
s /= 100; l /= 100;
const a = s * Math.min(l, 1 - l);
const f = n => {
const k = (n + h / 30) % 12;
return l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
};
return '#' + [f(0), f(8), f(4)].map(x =>
Math.round(x * 255).toString(16).padStart(2, '0')
).join('');
}

function updateFromSliders() {
const h = +hueSlider.value, s = +satSlider.value, l = +litSlider.value;
const hex = hslToHex(h, s, l);
swatch.style.background = hex;
hexInput.value = hex;
trigger.style.background = hex;
trigger.classList.add('active');
satSlider.style.background = `linear-gradient(to right, hsl(${h},0%,${l}%), hsl(${h},100%,${l}%))`;
litSlider.style.background = `linear-gradient(to right, hsl(${h},${s}%,10%), hsl(${h},${s}%,50%), hsl(${h},${s}%,90%))`;
applyCustomTheme(hex);
}
Comment on lines +315 to +325
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Initialization force-switches the app into custom theme mode.

Line 350 calls updateFromSliders(), and Line 324 then calls applyCustomTheme(hex). This overrides the default/preset theme state on load.

🐛 Proposed fix
-    function updateFromSliders() {
+    function updateFromSliders({ apply = true } = {}) {
         const h = +hueSlider.value, s = +satSlider.value, l = +litSlider.value;
         const hex = hslToHex(h, s, l);
         swatch.style.background = hex;
         hexInput.value = hex;
         trigger.style.background = hex;
-        trigger.classList.add('active');
         satSlider.style.background = `linear-gradient(to right, hsl(${h},0%,${l}%), hsl(${h},100%,${l}%))`;
         litSlider.style.background = `linear-gradient(to right, hsl(${h},${s}%,10%), hsl(${h},${s}%,50%), hsl(${h},${s}%,90%))`;
-        applyCustomTheme(hex);
+        if (apply) {
+            trigger.classList.add('active');
+            applyCustomTheme(hex);
+        }
     }
@@
-    updateFromSliders();
+    updateFromSliders({ apply: false });

Also applies to: 350-350

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/js/app.js` around lines 315 - 325, updateFromSliders currently always
calls applyCustomTheme(hex), which forces the app into custom-theme mode during
initialization; change updateFromSliders (and its callers) to accept an optional
flag (e.g., applyTheme = true) or check an initializing boolean so it only calls
applyCustomTheme(hex) on explicit user interactions, not on initial load (ensure
the initial call from the initializer passes applyTheme = false or sets
initializing = true so applyCustomTheme is skipped); update references to
updateFromSliders and any initialization invocation accordingly so the hex
update/slider visuals still happen without switching the theme until the user
makes a change.


trigger.addEventListener('click', (e) => {
e.stopPropagation();
panel.classList.toggle('open');
});

document.addEventListener('click', (e) => {
if (!wrap.contains(e.target)) panel.classList.remove('open');
});

hueSlider.addEventListener('input', updateFromSliders);
satSlider.addEventListener('input', updateFromSliders);
litSlider.addEventListener('input', updateFromSliders);

hexInput.addEventListener('input', (e) => {
const val = e.target.value;
if (/^#[0-9a-fA-F]{6}$/.test(val)) {
swatch.style.background = val;
trigger.style.background = val;
trigger.classList.add('active');
applyCustomTheme(val);
}
Comment on lines +340 to +347
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hex input path does not sync slider positions.

When a valid hex is typed, Line 346 updates theme but leaves H/S/L sliders unchanged, so the next slider move can jump to an unrelated color.

🔧 Suggested approach
+    function hexToHsl(hex) {
+        const { r, g, b } = hexToRgb(hex);
+        const rn = r / 255, gn = g / 255, bn = b / 255;
+        const max = Math.max(rn, gn, bn), min = Math.min(rn, gn, bn);
+        const d = max - min;
+        let h = 0;
+        const l = (max + min) / 2;
+        const s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
+        if (d !== 0) {
+            if (max === rn) h = 60 * (((gn - bn) / d) % 6);
+            else if (max === gn) h = 60 * ((bn - rn) / d + 2);
+            else h = 60 * ((rn - gn) / d + 4);
+        }
+        if (h < 0) h += 360;
+        return { h: Math.round(h), s: Math.round(s * 100), l: Math.round(l * 100) };
+    }
@@
     hexInput.addEventListener('input', (e) => {
         const val = e.target.value;
         if (/^#[0-9a-fA-F]{6}$/.test(val)) {
+            const { h, s, l } = hexToHsl(val);
+            hueSlider.value = h;
+            satSlider.value = s;
+            litSlider.value = l;
             swatch.style.background = val;
             trigger.style.background = val;
             trigger.classList.add('active');
             applyCustomTheme(val);
+            satSlider.style.background = `linear-gradient(to right, hsl(${h},0%,${l}%), hsl(${h},100%,${l}%))`;
+            litSlider.style.background = `linear-gradient(to right, hsl(${h},${s}%,10%), hsl(${h},${s}%,50%), hsl(${h},${s}%,90%))`;
         }
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/js/app.js` around lines 340 - 347, The hex input handler updates the
theme but doesn't sync the H/S/L controls; after the valid-hex branch in the
hexInput input listener (the block that calls applyCustomTheme(val)), convert
the hex color to HSL and set the hue/saturation/lightness slider element values
(e.g. hueSlider.value, satSlider.value, lightSlider.value) and update their
UI/state (either by calling the existing slider update handler or dispatching an
'input' event) so slider positions reflect the typed color; if no hexToHsl
utility exists, add one and use it before setting the sliders.

});

updateFromSliders();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not force-apply custom theme during picker startup

Calling updateFromSliders() during initialization immediately invokes applyCustomTheme(hex), which overrides currentTheme right after applyTheme(DEFAULT_THEME) runs and clears the preset active-dot state before any user interaction. This changes default behavior from preset blue to custom on every load and makes theme selection state inconsistent.

Useful? React with 👍 / 👎.

})();
19 changes: 18 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,24 @@ <h1>GitHub Social <span>Preview</span></h1>
<div class="theme-dot" data-theme="orange"></div>
<div class="theme-dot" data-theme="red"></div>
<div class="theme-dot" data-theme="cyan"></div>
<input type="color" id="custom-color-picker" value="#58a6ff" aria-label="Pick custom theme color">
<div class="color-picker-wrap" id="custom-picker-wrap">
<div class="theme-dot color-picker-trigger" id="custom-picker-trigger" title="Custom color"></div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove non-themed trigger from theme-dot click targets

This new trigger reuses the .theme-dot class but has no data-theme, so bindEvents() includes it in elements.themeDots and calls applyTheme(undefined) when it is clicked. That path dereferences THEMES[undefined] and throws, and it also sets currentTheme to undefined before crashing, which can break later generatePreview() calls after a user opens the custom picker.

Useful? React with 👍 / 👎.

<input type="color" id="custom-color-picker" value="#58a6ff" aria-label="Pick custom theme color">
<div class="color-picker-panel" id="custom-picker-panel">
<div class="cp-preview-row">
<div class="cp-swatch" id="cp-swatch"></div>
<input class="cp-hex-input" id="cp-hex" type="text" maxlength="7" value="#58a6ff" spellcheck="false">
</div>
<div class="cp-sliders">
<label class="cp-label">Hue</label>
<input class="cp-range cp-hue" id="cp-hue" type="range" min="0" max="360" value="213">
<label class="cp-label">Saturation</label>
<input class="cp-range cp-sat" id="cp-sat" type="range" min="0" max="100" value="100">
<label class="cp-label">Lightness</label>
<input class="cp-range cp-lit" id="cp-lit" type="range" min="10" max="90" value="61">
Comment on lines +38 to +51
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify semantic trigger + control associations for the custom picker.
rg -n 'id="custom-picker-trigger"|aria-controls="custom-picker-panel"|aria-expanded=|for="cp-(hue|sat|lit)"|id="cp-(hue|sat|lit)"|id="cp-hex"' index.html

Repository: readme-SVG/github-social-preview-generator

Length of output: 653


🏁 Script executed:

# Search for JavaScript handling of the custom-picker-trigger
rg -A 10 "custom-picker-trigger" --type js

Repository: readme-SVG/github-social-preview-generator

Length of output: 750


🏁 Script executed:

# Check for CSS that hides the native color input
rg -n "custom-color-picker|color-picker-panel" --type css -A 3

Repository: readme-SVG/github-social-preview-generator

Length of output: 590


🏁 Script executed:

# Check if there's any keyboard event handling or ARIA updates in JS
rg -n "addEventListener|keydown|keypress|keyup|aria-expanded" --type js | head -30

Repository: readme-SVG/github-social-preview-generator

Length of output: 1183


🏁 Script executed:

# Get the full trigger click handler to see if there's keyboard support or focus management
rg -A 5 "trigger.addEventListener\('click'" assets/js/app.js

Repository: readme-SVG/github-social-preview-generator

Length of output: 258


🏁 Script executed:

# Check if there's any focus management or keyboard event listeners elsewhere
rg -B 2 -A 5 "custom-picker-trigger" assets/js/app.js | head -50

Repository: readme-SVG/github-social-preview-generator

Length of output: 543


🏁 Script executed:

# Verify the label structure in HTML (check if labels are inside or outside the inputs)
sed -n '45,51p' index.html

Repository: readme-SVG/github-social-preview-generator

Length of output: 627


Custom picker trigger is not keyboard-accessible.

The trigger on line 38 uses a <div> with only a click listener, blocking keyboard access entirely. With the native color input hidden (CSS display: none), there is no keyboard fallback. Additionally, labels for the sliders (lines 46, 48, 50) lack for attributes, preventing proper semantic association.

♿ Proposed markup fix
-            <div class="theme-dot color-picker-trigger" id="custom-picker-trigger" title="Custom color"></div>
+            <button
+                type="button"
+                class="theme-dot color-picker-trigger"
+                id="custom-picker-trigger"
+                title="Custom color"
+                aria-label="Custom color picker"
+                aria-controls="custom-picker-panel"
+                aria-expanded="false"
+            ></button>
@@
-                    <input class="cp-hex-input" id="cp-hex" type="text" maxlength="7" value="#58a6ff" spellcheck="false">
+                    <input class="cp-hex-input" id="cp-hex" type="text" maxlength="7" value="#58a6ff" spellcheck="false" aria-label="Hex color value">
@@
-                    <label class="cp-label">Hue</label>
+                    <label class="cp-label" for="cp-hue">Hue</label>
@@
-                    <label class="cp-label">Saturation</label>
+                    <label class="cp-label" for="cp-sat">Saturation</label>
@@
-                    <label class="cp-label">Lightness</label>
+                    <label class="cp-label" for="cp-lit">Lightness</label>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="theme-dot color-picker-trigger" id="custom-picker-trigger" title="Custom color"></div>
<input type="color" id="custom-color-picker" value="#58a6ff" aria-label="Pick custom theme color">
<div class="color-picker-panel" id="custom-picker-panel">
<div class="cp-preview-row">
<div class="cp-swatch" id="cp-swatch"></div>
<input class="cp-hex-input" id="cp-hex" type="text" maxlength="7" value="#58a6ff" spellcheck="false">
</div>
<div class="cp-sliders">
<label class="cp-label">Hue</label>
<input class="cp-range cp-hue" id="cp-hue" type="range" min="0" max="360" value="213">
<label class="cp-label">Saturation</label>
<input class="cp-range cp-sat" id="cp-sat" type="range" min="0" max="100" value="100">
<label class="cp-label">Lightness</label>
<input class="cp-range cp-lit" id="cp-lit" type="range" min="10" max="90" value="61">
<button
type="button"
class="theme-dot color-picker-trigger"
id="custom-picker-trigger"
title="Custom color"
aria-label="Custom color picker"
aria-controls="custom-picker-panel"
aria-expanded="false"
></button>
<input type="color" id="custom-color-picker" value="#58a6ff" aria-label="Pick custom theme color">
<div class="color-picker-panel" id="custom-picker-panel">
<div class="cp-preview-row">
<div class="cp-swatch" id="cp-swatch"></div>
<input class="cp-hex-input" id="cp-hex" type="text" maxlength="7" value="#58a6ff" spellcheck="false" aria-label="Hex color value">
</div>
<div class="cp-sliders">
<label class="cp-label" for="cp-hue">Hue</label>
<input class="cp-range cp-hue" id="cp-hue" type="range" min="0" max="360" value="213">
<label class="cp-label" for="cp-sat">Saturation</label>
<input class="cp-range cp-sat" id="cp-sat" type="range" min="0" max="100" value="100">
<label class="cp-label" for="cp-lit">Lightness</label>
<input class="cp-range cp-lit" id="cp-lit" type="range" min="10" max="90" value="61">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.html` around lines 38 - 51, The custom color trigger currently uses a
non-focusable <div> (id="custom-picker-trigger") and the native color input
(id="custom-color-picker") is hidden, blocking keyboard access; also the slider
labels (for cp-hue, cp-sat, cp-lit and cp-hex) lack proper associations. Replace
the trigger <div> with a semantic <button id="custom-picker-trigger"
aria-haspopup="dialog" aria-expanded="false"
aria-controls="custom-picker-panel"> (or add tabindex="0", role="button" and
keyboard handlers if you must keep the element) so it is keyboard-focusable and
responds to Enter/Space; keep the native <input id="custom-color-picker"
type="color"> in the DOM but make it visually-hidden in an
accessibility-preserving way (avoid display:none; use offscreen CSS that
preserves focus) so keyboard users can open the picker; and update each <label>
to include for="cp-hue", for="cp-sat", for="cp-lit" (and associate cp-hex with a
label or aria-label) so cp-hue, cp-sat, cp-lit and cp-hex are semantically
linked to their controls.

</div>
</div>
</div>
</div>

<div class="template-row">
Expand Down