You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
docuseal/vendor/bundle/ruby/4.0.0/gems/pagy-43.4.4/javascripts/wand.js

1173 lines
41 KiB

document.head.insertAdjacentHTML("afterbegin", `
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>`);
const fontIsolation = (async () => {
const PAGY_WAND_FONT_CACHE_KEY = "pagy-wand-processed-fonts";
const cachedDataJson = sessionStorage.getItem(PAGY_WAND_FONT_CACHE_KEY);
if (cachedDataJson) {
try {
const cachedData = JSON.parse(cachedDataJson);
if (typeof cachedData.fontFaceRules === "string" && typeof cachedData.processedComponentCss === "string") {
if (cachedData.fontFaceRules) {
const styleEl = document.createElement("style");
styleEl.id = "pagy-wand-font-css";
styleEl.textContent = cachedData.fontFaceRules;
if (document.head) {
document.head.appendChild(styleEl);
} else {
document.addEventListener("DOMContentLoaded", () => document.head.appendChild(styleEl), { once: true });
}
}
return { processedComponentCss: cachedData.processedComponentCss };
} else {
console.warn("Pagy Wand: Invalid cached font data structure found. Refetching.");
sessionStorage.removeItem(PAGY_WAND_FONT_CACHE_KEY);
}
} catch (e) {
console.error("Pagy Wand: Failed to parse cached font data. Refetching.", e);
sessionStorage.removeItem(PAGY_WAND_FONT_CACHE_KEY);
}
}
const icons = "check_circle,content_copy,error,help,tune,visibility,visibility_off";
const fontDefinitions = [
{
url: "https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&family=Ubuntu+Sans+Mono:ital,wght@0,400..700;1,400..700&display=swap",
fontMappings: [
{ original: "Nunito Sans", custom: "PagyWand-Sans" },
{ original: "Ubuntu Sans Mono", custom: "PagyWand-Mono" }
]
},
{
url: "https://fonts.googleapis.com/css2?family=Pattaya&text=PagyWand&display=swap",
fontMappings: [
{ original: "Pattaya", custom: "PagyWand-Logo" }
]
},
{
url: `https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,GRAD@20..48,300,-50..200&icon_names=${icons}&display=block`,
fontMappings: [
{ original: "Material Symbols Rounded", custom: "PagyWand-Symbols" }
],
classMappings: [
{ original: "material-symbols-rounded", custom: "pagy-wand-symbol" }
]
}
];
async function processGoogleFontCSS(fontCssUrl, fontMappings = [], classMappings = []) {
const result = { fontFaceRules: "", componentCss: "" };
try {
const response = await fetch(fontCssUrl);
if (!response.ok)
throw new Error(`Font CSS fetch failed (${response.status})`);
let cssText = await response.text();
fontMappings.forEach((mapping) => {
const regex = new RegExp(`(font-family:\\s*['"]?)${mapping.original}(['"]?;?)`, "g");
cssText = cssText.replace(regex, `$1${mapping.custom}$2`);
});
classMappings.forEach((mapping) => {
const regex = new RegExp(`\\.(${mapping.original})(?=[\\s\\.{,:])`, "g");
cssText = cssText.replace(regex, `.${mapping.custom}`);
});
const fontFaceRegex = /(?:\/\*[\s\S]*?\*\/[\s\n]*)?@font-face\s*\{[^}]*}/g;
const fontFaceMatches = cssText.match(fontFaceRegex);
result.fontFaceRules = fontFaceMatches ? fontFaceMatches.join(`
`) : "";
result.componentCss = cssText.replace(fontFaceRegex, "").trim();
} catch (error) {
console.error("Failed to process Google Font CSS:", fontCssUrl, error);
throw error;
}
return result;
}
const processedResults = await Promise.all(fontDefinitions.map((def) => processGoogleFontCSS(def.url, def.fontMappings, def.classMappings)));
const allFontFaceRules = processedResults.map((r) => r.fontFaceRules).join(`
`);
const allComponentCss = processedResults.map((r) => r.componentCss).join(`
`);
if (allFontFaceRules) {
const styleEl = document.createElement("style");
styleEl.id = "pagy-wand-font-css";
styleEl.textContent = allFontFaceRules;
if (document.head) {
document.head.appendChild(styleEl);
} else {
document.addEventListener("DOMContentLoaded", () => document.head.appendChild(styleEl), { once: true });
}
}
try {
const dataToCache = {
fontFaceRules: allFontFaceRules,
processedComponentCss: allComponentCss
};
sessionStorage.setItem(PAGY_WAND_FONT_CACHE_KEY, JSON.stringify(dataToCache));
} catch (e) {
console.error("Pagy Wand: Failed to save font data to sessionStorage.", e);
}
return { processedComponentCss: allComponentCss };
})();
document.addEventListener("DOMContentLoaded", async () => {
const PRESET = "pagy-wand-preset";
const OVERRIDE = "pagy-wand-override";
const POSITION = "pagy-wand-position";
const CONTROLS_CHK = "pagy-wand-controls-chk";
const HELP_CHK = "pagy-wand-help-chk";
const LIVE_CHK = "pagy-wand-live-chk";
const normalize = (str) => str.trim().replace(/\s+/g, " ");
function getSessionItem(key) {
return sessionStorage.getItem(key);
}
function setSessionItem(key, value) {
if (typeof value === "string") {
sessionStorage.setItem(key, value);
} else {
sessionStorage.setItem(key, JSON.stringify(value));
}
}
function removeSessionItem(key) {
sessionStorage.removeItem(key);
}
function getSessionBoolean(key, defaultValue) {
const value = sessionStorage.getItem(key);
return value !== null ? JSON.parse(value) : defaultValue;
}
function getSessionObject(key) {
const value = sessionStorage.getItem(key);
return value ? JSON.parse(value) : null;
}
function hslaToRgba(hsla) {
const h = parseFloat(hsla.hue) / 360;
const s = parseFloat(hsla.saturation) / 100;
const l = parseFloat(hsla.lightness) / 100;
const a = parseFloat(hsla.alpha);
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);
}
const toHex = (x) => Math.round(x * 255).toString(16).padStart(2, "0");
return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(a)}`;
}
function rgbaToHsla(rgba) {
const r = parseInt(rgba.slice(1, 3), 16) / 255;
const g = parseInt(rgba.slice(3, 5), 16) / 255;
const b = parseInt(rgba.slice(5, 7), 16) / 255;
const a = parseInt(rgba.slice(7, 9), 16) / 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h = 0, 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 {
hue: (h * 360).toFixed(2),
saturation: (s * 100).toFixed(2),
lightness: (l * 100).toFixed(2),
alpha: a.toFixed(3)
};
}
const host = document.createElement("div");
host.id = "pagy-wand-host";
document.body.appendChild(host);
const s = parseFloat(document.getElementById("pagy-wand").getAttribute("data-scale"));
const sliderHeight = s;
const thumbDiameter = s * 1.2;
const baseColor = "#484848";
const lightGray = "rgba(220,220,220,.6)";
const wandColor = "#81ffff";
const wandTint = "rgba(190,220,220,.6)";
const remSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
const style = document.getElementById("pagy-wand-default");
document.head.appendChild(style);
const shadow = host.attachShadow({ mode: "closed" });
const { processedComponentCss } = await fontIsolation;
shadow.innerHTML = `
<!--suppress CssInvalidPropertyValue -->
<style>
${processedComponentCss}
#panel select, #panel select option, #panel input[type="range"] {
font-family: 'PagyWand-Sans', sans-serif;
cursor: pointer;
font-weight: 600;
}
#panel select, textarea {
font-size: ${s * 0.8}rem;
border-radius: ${s}rem;
padding-left: ${s * 0.25}rem !important;
border: none;
box-shadow: inset 0 0 ${s * 0.2}rem ${wandColor};
}
#panel {
accent-color: ${baseColor};
font-family: 'PagyWand-Sans', sans-serif;
line-height: 1.2;
border-radius: ${s * 1.25}rem;
width: ${s * 22}rem;
box-sizing: border-box;
box-shadow: ${s * 0.75}rem
${s * 0.75}rem
${s * 1.5625}rem
${s * 0.0625}rem
rgba(0,0,0,.4);
position: fixed;
z-index: 10000;
overflow: hidden;
}
#panel.initial {
top: 0;
left: 0;
transform: translate(-50%, 0) scale(0.1) rotate(0deg);
}
#panel.centered {
transition: transform 2.5s ease-in-out;
transform: translate(calc(50vw - 50%), calc(50vh - 50%)) scale(1) rotate(2160deg);
}
#panel pre, #panel code, #panel kbd, #panel samp {
font-family: 'PagyWand-Mono', monospace;
}
#top-bar {
background: linear-gradient(to bottom,
#1a1a1a 0%,
#2b2b2b 5%,
#404040 10%,
#525252 20%,
#404040 40%,
#2b2b2b 60%,
#1a1a1a 80%,
#0d0d0d 92%,
#000000 100%
);
padding: ${s * 0.25}rem
${s * 0.75}rem
${s * 0.25}rem
${s * 0.875}rem;
cursor: move;
user-select: none;
color: white;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: inset 0 0 ${s * 0.2}rem ${wandColor};
}
#top-bar label {
display: flex;
align-items: center;
}
#top-bar label input[type="checkbox"] {
display: none;
}
#title {
color: ${wandColor};
font-family: 'PagyWand-Logo', sans-serif;
font-size: ${s * 1.4}rem;
text-shadow: ${s * 0.0625}rem
${s * 0.0625}rem
0 rgba(0,0,0,1);
margin-right: ${s * 0.125}rem;
}
.switch-icon {
font-size: ${s * 1.5}rem;
font-weight: 300;
cursor: pointer;
}
.selected-icon {
color: ${wandColor};
}
.content{
overflow-y: auto;
font-size: ${s * 0.8}rem;
color: black;
backdrop-filter: blur(${s * 0.875}rem);
padding: ${s * 0.8}rem 0;
border-top: ${s * 0.15}rem solid ${wandColor};
border-bottom: ${s * 0.15}rem solid ${wandColor};
height: ${s * 33}rem;
background: linear-gradient(to bottom,
${wandTint} 0%,
${lightGray} 5%,
${lightGray} 95%,
${wandTint} 100%
);
}
.controls {
border-top: ${s * 0.0625}rem solid #ccc;
border-bottom: ${s * 0.0625}rem solid #aaa;
display: grid;
grid-template-columns: 1fr 3fr;
grid-column-gap: ${s * 0.625}rem;
padding: ${s * 0.25}rem ${s}rem;
}
.controls-brightness {
padding-top: 0;
border-top: none;
grid-template-rows: ${s * 1.5}rem;
}
.controls-color {
grid-template-rows: repeat(5, ${s * 1.3}rem);
}
.controls-layout {
grid-template-rows: repeat(4, ${s * 1.3}rem);
}
.controls-typography {
grid-template-rows: repeat(3, ${s * 1.3}rem);
}
.controls-override {
grid-template-rows: ${s * 12.9}rem;
margin-bottom: ${s * 0.125}rem;
border-bottom: none;
}
.controls label {
font-weight: 600;
grid-column: 1;
white-space: nowrap;
justify-self: end;
align-self: center;
user-select: none;
cursor: default;
}
.controls input {
margin: 0;
align-self: center;
}
#brightness {
margin: 0 0 ${s * 0.2}rem 0;
color: white;
background-color: black;
opacity: .6;
}
#hue, #saturation, #lightness, #alpha {
-webkit-appearance: none;
appearance: none;
width: 1fr;
border-radius: ${s * 0.5}rem;
outline: none;
height: ${sliderHeight}rem;
box-shadow: inset 0 0 ${s * 0.1}rem rgba(0,0,0,0.5);
box-sizing: border-box;
}
#hue::-webkit-slider-thumb,
#saturation::-webkit-slider-thumb,
#lightness::-webkit-slider-thumb,
#alpha::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
box-sizing: border-box;
width: ${thumbDiameter}rem;
height: ${thumbDiameter}rem;
background: transparent;
border: ${s * 0.15}rem solid ${baseColor};
border-radius: 50%;
cursor: pointer;
}
#hue::-moz-range-thumb,
#saturation::-moz-range-thumb,
#lightness::-moz-range-thumb,
#alpha::-moz-range-thumb {
box-sizing: border-box;
width: ${thumbDiameter}rem;
height: ${thumbDiameter}rem;
background: transparent;
border: ${s * 0.15}rem solid ${baseColor};
border-radius: 50%;
cursor: pointer;
}
#alpha {
background-size: auto, ${s * 0.5}rem ${s * 0.5}rem; /* gradient, grid */
background-repeat: no-repeat, repeat; /* gradient, grid *//
}
#hex8-container {
display: flex;
}
#hex8, #color-sample {
border: none;
border-radius: ${s * 0.5}rem;
height: ${sliderHeight}rem;
}
#hex8 {
box-sizing: border-box;
width: ${s * 5}rem;
text-align: center;
font-family: "PagyWand-Mono", monospace;
font-weight: 500;
line-height: 1.1;
color: white;
background-color: black;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
opacity: .6;
}
#color-sample {
width: 100%;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
align-self: center;
box-shadow: inset 0 0 ${s * 0.1}rem rgba(0,0,0,0.5);
background-size: auto, ${s * 0.5}rem ${s * 0.5}rem; /* gradient, grid */
background-repeat: no-repeat, repeat; /* gradient, grid *//
}
label[for="override"] {
align-self: start !important;
margin-top: ${s * 0.35}rem;
}
#override-container {
display: inline;
position: relative;
}
#override {
white-space: pre-wrap; /* Preserve line breaks, wrap only when necessary */
word-break: normal; /* Prevent breaking words */
overflow-wrap: normal; /* Standard line breaking */
font-family: "PagyWand-Mono", monospace;
padding: ${s * 0.2}rem !important;
border-radius: ${s * 0.6}rem;
line-height: 1.1;
height: 100%;
width: 100%;
resize: none;
font-weight: 500;
box-sizing: border-box;
position: relative;
margin-top: ${s * 0.25}rem;
color: white;
background-color: black;
opacity: .6;
}
#copy-icon {
font-weight: 300;
color: ${lightGray};
position: absolute;
top: ${s * 0.8}rem;
right: ${s}rem;
cursor: pointer;
}
.copy-feedback {
position: absolute;
top: ${s}rem;
right: ${s * 2.7}rem;
padding: ${s * 0.1}rem ${s * 0.3}rem;
border-radius: ${s * 0.2}rem;
font-weight: 700;
white-space: nowrap;
align-self: center;
color: white;
opacity: 0;
visibility: hidden;
transition: opacity 0.4s ease-in-out, visibility 0.2s ease-in-out;
pointer-events: none; /* Prevent interfering with clicks */
}
.copy-feedback.visible {
opacity: 1;
visibility: visible;
}
.copy-feedback.success {
background-color: limegreen;
}
.copy-feedback.failure {
background-color: red;
}
#help {
padding-left: ${s}rem;
padding-right: ${s}rem;
}
.help-icon, .help-copy-icon {
font-size: ${s}rem;
vertical-align: -15.625%;
border-radius: 15%;
color: white;
background-color: ${baseColor};
margin-top: ${s * 0.2}rem;
padding: ${s * 0.125}rem
${s * 0.0625}rem
${s * 0.0625}rem
${s * 0.0625}rem;
font-weight: 300;
}
.selected-help-icon {
color: ${wandColor};
}
.help-copy-icon {
color: ${baseColor};
background-color: white;
}
.help-copy-icon.success {
color: limegreen;
}
.help-copy-icon.failure {
color: red;
}
#help {
display: none;
line-height: ${s}rem;
scrollbar-width: none;
}
#help::-webkit-scrollbar {
display: none;
}
#help > h4:first-child {
margin-top: 0;
}
#help h4 {
text-align: right;
padding: ${s * 0.25}rem ${s * 0.625}rem;
border-top-right-radius: ${s * 2.5}rem;
border-bottom-right-radius: 2.5rem;
color: white;
background: linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,.5));
margin-top: ${s * 0.8}rem;
margin-bottom: ${s * 0.4}rem;
}
#help p {
margin-top: ${s * 0.4}rem;
margin-bottom: ${s * 0.3}rem;
}
#help dl {
margin: 0;
}
#help dt {
font-weight: 700;
margin: ${s * 0.4}rem 0 ${s * 0.1}rem 0;
}
#help dd {
margin-bottom: ${s * 0.15}rem;
margin-left: ${s}rem;
}
#help .button-desc {
margin-left: ${s * 0.4}rem;
}
#help code {
font-family: "PagyWand-Mono", monospace;
font-weight: 400;
display: inline-block;
line-height: ${s * 0.8}rem;
border-radius: ${s * 0.625}rem;
background-color: white;
padding: ${s * 0.0625}rem ${s * 0.3125}rem;
}
</style>
<div id="panel">
<div id="top-bar">
<span id="title">PagyWand</span>
<label for="preset-menu">
<select id="preset-menu">
<option value="" disabled>Presets...</option>
</select>
</label>
<label for="controls-chk">
<input id="controls-chk" type="checkbox">
<span class="pagy-wand-symbol switch-icon" id="controls-icon">tune</span>
</label>
<label for="help-chk">
<input id="help-chk" type="checkbox">
<span class="pagy-wand-symbol switch-icon" id="help-icon">help</span>
</label>
<label for="live-chk">
<input id="live-chk" type="checkbox" checked>
<span class="pagy-wand-symbol switch-icon" id="live-icon">visibility</span>
</label>
</div>
<div id="content">
<div id="controls" class="content">
<div class="controls controls-brightness light-bknd">
<label for="brightness">Brightness</label>
<select id="brightness">
<option value="1">Light</option>
<option value="-1">Dark</option>
</select>
</div>
<div class="controls controls-color dark-bknd">
<label for="hue">Hue</label>
<input type="range" id="hue" min="0" max="360" step="0.01">
<label for="saturation">Saturation</label>
<input type="range" id="saturation" min="0" max="100" step="0.01">
<label for="lightness">Lightness</label>
<input type="range" id="lightness" min="0" max="100" step="0.01">
<label for="alpha">Alpha</label>
<input type="range" id="alpha" min="0" max="1" step="0.001">
<label for="hex8">Hex8</label>
<div id="hex8-container">
<input type="text" id="hex8" spellcheck="false">
<span id="color-sample"></span>
</div>
</div>
<div class="controls controls-layout light-bknd">
<label for="spacing">Spacing</label>
<input type="range" id="spacing" min="0" max="1.5" step="0.0625">
<label for="padding">Padding</label>
<input type="range" id="padding" min="0" max="1.5" step="0.0625">
<label for="rounding">Rounding</label>
<input type="range" id="rounding" min="0" max="3" step="0.0625">
<label for="borderWidth">Borders</label>
<input type="range" id="borderWidth" min="0" max="0.25" step="0.03125">
</div>
<div class="controls controls-typography dark-bknd">
<label for="fontSize">Font Size</label>
<input type="range" id="fontSize" min="0.75" max="2" step="0.0625">
<label for="fontWeight">Font Weight</label>
<input type="range" id="fontWeight" min="100" max="1000" step="50">
<label for="lineHeight">Line Height</label>
<input type="range" id="lineHeight" min="1.25" max="2.5" step="0.0625">
</div>
<div class="controls controls-override light-bknd">
<label for="override">CSS Override</label>
<div id="override-container">
<textarea id="override" readonly></textarea>
<span id="copy-feedback" class="copy-feedback"></span>
<span id="copy-icon" class="pagy-wand-symbol">content_copy</span>
</div>
</div>
</div>
<div id="help" class="content">
<h4>Install</h4>
<dl>
<dt>Add this line in your HTML head</dt>
<dd>
<p><code><%== Pagy.dev_tools %></code></p>
<p>You can pass the optional <code>wand_scale</code> argument to change the size of the Wand.
<br>Default: <code>wand_scale: 1</code></p>
</dd>
</dl>
<h4>Wand</h4>
<dl>
<dt>Top Bar</dt>
<dd>Drag the Wand.</dd>
<dt>Top Bar Indicator Buttons</dt>
<dd>
<ul style="list-style-type: none; padding-left: 0; margin: 0;">
<li><span class="pagy-wand-symbol help-icon">tune</span> <span class="pagy-wand-symbol help-icon selected-help-icon">tune</span><span class="button-desc">Toggle the Controls Section</span></li>
<li><span class="pagy-wand-symbol help-icon">help</span> <span class="pagy-wand-symbol help-icon selected-help-icon">help</span><span class="button-desc">Toggle the Help Section</span></li>
<li><span class="pagy-wand-symbol help-icon">visibility_off</span> <span class="pagy-wand-symbol help-icon selected-help-icon">visibility</span><span class="button-desc">Toggle the Live Preview</span></li>
</ul>
</dd>
<dt>Presets</dt>
<dd>Pick a starting point to try and further customize.</dd>
<dt>Close Icon</dt>
<dd>There is no dynamic close button by design, so you won't forget to remove it in production.</dd>
</dl>
<h4>Controls</h4>
<dl>
<dt>Brightness</dt>
<dd>Toggle between Light and Dark theming calculation. Adjust the lightness after toggling.</dd>
<dt>Hue, Saturation, Lightness, Alpha</dt>
<dd>Generate any color. Notice that the automatic calculations work better within certain ranges/combinations.</dd>
<dt>Hex8</dt>
<dd>8-digit hex-color code: useful to quickly copy/paste and match a color from your app.</dd>
<dt>Spacing, Padding, Rounding, Borders</dt>
<dd>Control the layout and overall look.</dd>
<dt>Font Size, Font Weight, Line Height</dt>
<dd>Control the typography of the page links. Notice that the <code>font-family</code> is inherited from your app.</dd>
<dt>Interactions</dt>
<dd>The combination of Padding, Font Size, Line Height, controls the internal proportions of the page links.</dd>
<dt>CSS Override</dt>
<dd>
The current set of <code>.pagy</code> rules.
<ul style="list-style-type: none; padding-left: 0; margin: 0;">
<li><span class="pagy-wand-symbol help-copy-icon">content_copy</span> <span class="button-desc">Copy the CSS Override</span></li>
<li><span class="pagy-wand-symbol help-copy-icon success">check_circle</span> <span class="button-desc">Copied! Feedback</li>
<li><span class="pagy-wand-symbol help-copy-icon failure">error</span> <span class="button-desc">Failed! Feedback</li>
</ul>
</dd>
</dl>
<h4>Customizing</h4>
<p>• You can change Pagy's styling quite radically, by just setting a few CSS Custom Properties:
the <code><i>pagy.css</i></code> or <code><i>pagy-tailwind.css</i></code> calculates all the other metrics.</p>
<p>• Pick a Presets as a starting point, customize it with the controls,
and copy/paste the CSS Override in your Stylesheet.</p>
<p>• Add further customization to the <code>.pagy</code> CSS Override, or override the calculated properties
for full control over the final style.</p>
<p><b>Important</b>: Do not link the Pagy CSS file. Copy its customized content in your CSS,
to avoid unwanted cosmetic changes that could happen on update.</p>
</div>
</div>
</div>
`;
const styleTagOverride = document.createElement("style");
styleTagOverride.id = "pagy-wand-override";
document.head.appendChild(styleTagOverride);
const panel = shadow.getElementById("panel");
const topBar = shadow.getElementById("top-bar");
const presetMenu = shadow.getElementById("preset-menu");
const controlsChk = shadow.getElementById("controls-chk");
controlsChk.checked = getSessionBoolean(CONTROLS_CHK, false);
const controlsIcon = shadow.getElementById("controls-icon");
const controlsDiv = shadow.getElementById("controls");
const helpChk = shadow.getElementById("help-chk");
helpChk.checked = getSessionBoolean(HELP_CHK, false);
const helpIcon = shadow.getElementById("help-icon");
const helpDiv = shadow.getElementById("help");
const liveChk = shadow.getElementById("live-chk");
liveChk.checked = getSessionBoolean(LIVE_CHK, true);
const liveIcon = shadow.getElementById("live-icon");
const liveStyle = document.getElementById("pagy-wand-default");
const copyIcon = shadow.getElementById("copy-icon");
const overrideArea = shadow.getElementById("override");
let controls = {
brightness: { name: "--B", unit: "" },
hue: { name: "--H", unit: "" },
saturation: { name: "--S", unit: "" },
lightness: { name: "--L", unit: "" },
alpha: { name: "--A", unit: "" },
hex8: { name: "", unit: "" },
spacing: { name: "--spacing", unit: "rem" },
padding: { name: "--padding", unit: "rem" },
rounding: { name: "--rounding", unit: "rem" },
borderWidth: { name: "--border-width", unit: "rem" },
fontSize: { name: "--font-size", unit: "rem" },
fontWeight: { name: "--font-weight", unit: "" },
lineHeight: { name: "--line-height", unit: "" }
};
function updateColorRamps() {
const b = controls.brightness.input.value;
let darker, lighter;
if (b === "-1") {
darker = "black";
lighter = "%23404040";
} else {
darker = "%23B0B0B0";
lighter = "white";
}
const gridUrl = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10'%3E%3Crect width='5' height='5' fill='${darker}'/%3E%3Crect x='5' width='5' height='5' fill='${lighter}'/%3E%3Crect y='5' width='5' height='5' fill='${lighter}'/%3E%3Crect x='5' y='5' width='5' height='5' fill='${darker}'/%3E%3C/svg%3E")`;
const h = controls.hue.input.value;
const s = controls.saturation.input.value;
const l = controls.lightness.input.value;
const a = controls.alpha.input.value;
const sliderWidth = controls.alpha.input.getBoundingClientRect().width;
const thumbRadius = remSize * thumbDiameter / 2;
const startSlider = thumbRadius + "px";
const endSlider = sliderWidth - thumbRadius + "px";
controls.hue.input.style.background = `linear-gradient(to right, hsl(0 100 50) ${startSlider}, hsl(60 100 50), hsl(120 100 50), hsl(180 100 50), hsl(240 100 50), hsl(300 100 50), hsl(360 100 50) ${endSlider})`;
controls.saturation.input.style.background = `linear-gradient(to right, hsl(${h} 0 ${l}) ${startSlider}, hsl(${h} 100 ${l}) ${endSlider})`;
controls.lightness.input.style.background = `linear-gradient(to right, hsl(${h} ${s} 0) ${startSlider}, hsl(${h} ${s} 50), hsl(${h} ${s} 100) ${endSlider})`;
controls.alpha.input.style.backgroundImage = `linear-gradient(to right, hsla(${h} ${s} ${l} / 0) ${startSlider}, hsla(${h} ${s} ${l} / 1) ${endSlider}), ${gridUrl}`;
controls.hex8.input.value = hslaToRgba({ hue: h, saturation: s, lightness: l, alpha: a });
const sample = shadow.getElementById("color-sample");
sample.style.backgroundImage = `linear-gradient(to right, hsla(${h} ${s} ${l} / ${a}), hsla(${h} ${s} ${l} / ${a})), ${gridUrl}`;
}
const finalize = () => {
updateOverride();
presetMenu.value = "";
};
for (const [id, c] of Object.entries(controls)) {
c.input = shadow.getElementById(id);
if (id === "hex8") {
c.input.addEventListener("change", () => {
const hsla = rgbaToHsla(controls.hex8.input.value);
controls.hue.input.value = hsla.hue;
controls.saturation.input.value = hsla.saturation;
controls.lightness.input.value = hsla.lightness;
controls.alpha.input.value = hsla.alpha;
finalize();
});
} else {
c.input.addEventListener("input", () => {
finalize();
});
}
}
const presets = {
Default: `
.pagy {
--B: 1;
--H: 0;
--S: 0;
--L: 50;
--A: 1;
--spacing: 0.125rem;
--padding: 0.75rem;
--rounding: 1.75rem;
--border-width: 0rem;
--font-size: 0.875rem;
--font-weight: 600;
--line-height: 1.75;
}
`,
Dark: `
.pagy {
--B: -1;
--H: 0;
--S: 0;
--L: 60;
--A: 1;
--spacing: 0.125rem;
--padding: 0.75rem;
--rounding: 1.75rem;
--border-width: 0rem;
--font-size: 0.875rem;
--font-weight: 600;
--line-height: 1.75;
}
`,
MidnighExpress: `
.pagy {
--B: -1;
--H: 231;
--S: 28;
--L: 60;
--A: 1;
--spacing: 0.1875rem;
--padding: 1rem;
--rounding: 0.375rem;
--border-width: 0rem;
--font-size: 1rem;
--font-weight: 450;
--line-height: 1.25;
}
`,
Pilloween: `
.pagy {
--B: -1;
--H: 20;
--S: 80;
--L: 50;
--A: 1;
--spacing: 0.375rem;
--padding: 0.75rem;
--rounding: 1.125rem;
--border-width: 0.0625rem;
--font-size: 0.875rem;
--font-weight: 600;
--line-height: 1.5;
}
`,
Peppermint: `
.pagy {
--B: 1;
--H: 78;
--S: 70;
--L: 38;
--A: 1;
--spacing: 0.1875rem;
--padding: 0.625rem;
--rounding: 0.75rem;
--border-width: 0rem;
--font-size: 0.875rem;
--font-weight: 550;
--line-height: 1.75;
}
`,
CocoaBeans: `
.pagy {
--B: 1;
--H: 27;
--S: 63;
--L: 17;
--A: 1;
--spacing: 0.0625rem;
--padding: 0.5rem;
--rounding: 1.125rem;
--border-width: 0rem;
--font-size: 0.875rem;
--font-weight: 600;
--line-height: 2.5;
}
`,
PurpleStripe: `
.pagy {
--B: 1;
--H: 255;
--S: 63;
--L: 43;
--A: 1;
--spacing: 0rem;
--padding: 0.875rem;
--rounding: 0rem;
--border-width: 0rem;
--font-size: 0.875rem;
--font-weight: 300;
--line-height: 1.5;
}
`,
GhostInThought: `
.pagy {
--B: 1;
--H: 174;
--S: 40;
--L: 70;
--A: 1;
--spacing: 0.125rem;
--padding: 0.75rem;
--rounding: 1.125rem;
--border-width: 0rem;
--font-size: 0.875rem;
--font-weight: 450;
--line-height: 1.75;
}
`,
VintageScent: `
.pagy {
--B: 1;
--H: 51;
--S: 27;
--L: 64;
--A: 1;
--spacing: 0.1875rem;
--padding: 0.75rem;
--rounding: 0.75rem;
--border-width: 0.0625rem;
--font-size: 0.875rem;
--font-weight: 300;
--line-height: 1.75;
}
`
};
for (const presetName in presets) {
const option = document.createElement("option");
option.value = presetName;
option.textContent = presetName;
presetMenu.appendChild(option);
}
presetMenu.value = "";
presetMenu.addEventListener("change", (e) => {
const name = e.target.value;
setSessionItem(PRESET, name);
applyCSS(presets[name]);
});
function applyCSS(css) {
css.match(/--[^:]+:\s*[^;]+/g)?.forEach((match) => {
let [cssVarName, value] = match.split(":");
cssVarName = cssVarName.trim();
value = value.trim().replace(/[a-zA-Z%]+$/, "");
for (const c of Object.values(controls)) {
if (c.name === cssVarName) {
c.input.value = value;
break;
}
}
});
updateOverride();
}
const initialOverride = getSessionItem(OVERRIDE);
const presetName = getSessionItem(PRESET) ?? "Default";
const presetCSS = normalize(presets[presetName]);
if (initialOverride && initialOverride !== presetCSS) {
applyCSS(initialOverride);
} else {
presetMenu.value = presetName;
applyCSS(presetCSS);
}
function updateOverride() {
let override = `.pagy {
`;
Object.values(controls).forEach((c) => {
if (c.name !== "") {
override += ` ${c.name}: ${c.input.value}${c.unit};
`;
}
});
override += "}";
overrideArea.value = override;
styleTagOverride.textContent = liveChk.checked ? override : "";
setSessionItem(OVERRIDE, normalize(override));
updateColorRamps();
}
function getSessionPosition() {
return getSessionObject(POSITION);
}
function setSessionPosition(left, top) {
setSessionItem(POSITION, { left: parseFloat(String(left)), top: parseFloat(String(top)) });
}
function keepTopBarInView() {
const width = window.visualViewport ? window.visualViewport.width : document.documentElement.clientWidth;
const height = window.visualViewport ? window.visualViewport.height : document.documentElement.clientHeight;
const rect = topBar.getBoundingClientRect();
let newLeft = panel.offsetLeft;
let newTop = panel.offsetTop;
if (rect.left < 0) {
newLeft = panel.offsetLeft - rect.left;
} else if (rect.right > width) {
newLeft = panel.offsetLeft - (rect.right - width);
}
if (rect.top < 0) {
newTop = panel.offsetTop - rect.top;
} else if (rect.bottom > height) {
newTop = panel.offsetTop - (rect.bottom - height);
}
panel.style.left = `${newLeft}px`;
panel.style.top = `${newTop}px`;
setSessionPosition(newLeft, newTop);
}
let resizeTimeout;
window.addEventListener("resize", () => {
if (resizeTimeout)
clearTimeout(resizeTimeout);
resizeTimeout = window.setTimeout(keepTopBarInView, 250);
});
const position = getSessionPosition();
const wandPositioned = new CustomEvent("wand-positioned");
if (position && !isNaN(position.left) && !isNaN(position.top)) {
panel.style.left = `${position.left}px`;
panel.style.top = `${position.top}px`;
document.dispatchEvent(wandPositioned);
} else {
panel.classList.add("initial");
requestAnimationFrame(() => {
panel.classList.add("centered");
});
panel.addEventListener("transitionend", (e) => {
if (e.propertyName === "transform") {
panel.style.transition = "none";
const rect = panel.getBoundingClientRect();
panel.style.top = rect.top + "px";
panel.style.left = rect.left + "px";
setSessionPosition(rect.left, rect.top);
panel.classList.remove("initial");
panel.classList.remove("centered");
document.dispatchEvent(wandPositioned);
}
}, { once: true });
}
let offsetX = 0;
let offsetY = 0;
let dragging = false;
topBar.addEventListener("mousedown", (e) => {
if (e.target.closest("#preset-menu, label"))
return;
dragging = true;
offsetX = e.clientX - panel.offsetLeft;
offsetY = e.clientY - panel.offsetTop;
topBar.style.cursor = "grab";
});
document.addEventListener("mousemove", (e) => {
if (!dragging)
return;
e.preventDefault();
const newLeft = e.clientX - offsetX;
const newTop = e.clientY - offsetY;
panel.style.left = `${newLeft}px`;
panel.style.top = `${newTop}px`;
});
document.addEventListener("mouseup", (e) => {
if (!dragging)
return;
dragging = false;
topBar.style.cursor = "move";
const finalLeft = e.clientX - offsetX;
const finalTop = e.clientY - offsetY;
setSessionPosition(finalLeft, finalTop);
});
function controlsSwitcher() {
if (controlsChk.checked) {
controlsIcon.classList.add("selected-icon");
controlsDiv.style.display = "grid";
helpChk.checked = false;
helpSwitcher();
updateColorRamps();
} else {
controlsIcon.classList.remove("selected-icon");
controlsDiv.style.display = "none";
}
setSessionItem(CONTROLS_CHK, controlsChk.checked);
}
controlsSwitcher();
controlsChk.addEventListener("change", controlsSwitcher);
function helpSwitcher() {
if (helpChk.checked) {
helpIcon.classList.add("selected-icon");
helpDiv.style.display = "block";
controlsChk.checked = false;
controlsSwitcher();
} else {
helpIcon.classList.remove("selected-icon");
helpDiv.style.display = "none";
}
setSessionItem(HELP_CHK, helpChk.checked);
}
helpSwitcher();
helpChk.addEventListener("change", helpSwitcher);
function liveSwitcher() {
if (liveChk.checked) {
liveIcon.classList.add("selected-icon");
liveIcon.textContent = "visibility";
liveStyle.disabled = false;
} else {
liveIcon.classList.remove("selected-icon");
liveIcon.textContent = "visibility_off";
liveStyle.disabled = true;
}
updateOverride();
setSessionItem(LIVE_CHK, liveChk.checked);
}
liveSwitcher();
liveChk.addEventListener("change", liveSwitcher);
async function copyToClipboard() {
const feedback = shadow.getElementById("copy-feedback");
const originalIcon = "content_copy";
feedback.classList.remove("visible");
try {
await navigator.clipboard.writeText(overrideArea.value);
copyIcon.textContent = "check_circle";
copyIcon.style.color = "limegreen";
feedback.textContent = "Copied!";
feedback.classList.add("visible", "success");
setTimeout(() => {
copyIcon.textContent = originalIcon;
copyIcon.style.color = lightGray;
feedback.classList.remove("visible", "success");
}, 3000);
} catch (err) {
console.error('Failed to copy! (navigator.clipboard requires "localhost" or HTTPS) - ', err);
copyIcon.textContent = "error";
copyIcon.style.color = "red";
feedback.textContent = "Failed!";
feedback.classList.add("visible", "failure");
setTimeout(() => {
copyIcon.textContent = originalIcon;
copyIcon.style.color = lightGray;
feedback.classList.remove("visible", "failure");
}, 5000);
}
}
copyIcon.addEventListener("click", copyToClipboard);
const transitionStyleTag = document.createElement("style");
transitionStyleTag.id = "pagy-wand-transition";
transitionStyleTag.textContent = `
.pagy {
transition: background-color 0.3s ease
}
.pagy a, .pagy label {
transition: background-color 0.3s ease,
color 0.3s ease,
border-color 0.3s ease,
padding 0.3s ease,
margin-left 0.3s ease,
margin-right 0.3s ease,
font-size 0.3s ease,
font-weight 0.3s ease,
line-height 0.3s ease,
border-radius 0.3s ease
}
`;
document.head.appendChild(transitionStyleTag);
});