Improve UI of themed banner

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-21 17:09:30 +04:00
parent b603d04d8d
commit fab95413df
2 changed files with 168 additions and 62 deletions

View File

@@ -23,7 +23,6 @@ export const THEMED_STYLES = `
--_shadow: var(--probo-shadow, 0 4px 24px rgba(0, 0, 0, 0.12)); --_shadow: var(--probo-shadow, 0 4px 24px rgba(0, 0, 0, 0.12));
--_accent: var(--probo-accent, #1a1a1a); --_accent: var(--probo-accent, #1a1a1a);
--_accent-text: var(--probo-accent-text, #ffffff); --_accent-text: var(--probo-accent-text, #ffffff);
--_overlay: var(--probo-overlay, rgba(0, 0, 0, 0.4));
--_z-index: var(--probo-z-index, 2147483646); --_z-index: var(--probo-z-index, 2147483646);
--_btn-radius: var(--probo-btn-radius, 8px); --_btn-radius: var(--probo-btn-radius, 8px);
--_font-size: var(--probo-font-size, 14px); --_font-size: var(--probo-font-size, 14px);
@@ -40,26 +39,71 @@ export const THEMED_STYLES = `
box-sizing: border-box; box-sizing: border-box;
} }
.overlay { .floating {
position: fixed; position: fixed;
inset: 0;
background: var(--_overlay);
z-index: var(--_z-index); z-index: var(--_z-index);
padding: 24px;
display: flex; display: flex;
align-items: flex-end; pointer-events: none;
justify-content: center; }
padding: 16px;
.floating[data-position="bottom-left"] {
bottom: 0;
left: 0;
}
.floating[data-position="bottom-right"] {
bottom: 0;
right: 0;
}
.floating[data-position="bottom-center"] {
bottom: 0;
left: 50%;
transform: translateX(-50%);
}
.floating[data-position="top-left"] {
top: 0;
left: 0;
}
.floating[data-position="top-right"] {
top: 0;
right: 0;
}
.floating[data-position="top-center"] {
top: 0;
left: 50%;
transform: translateX(-50%);
} }
.card { .card {
background: var(--_bg); background: var(--_bg);
border-radius: var(--_radius); border-radius: var(--_radius);
box-shadow: var(--_shadow); box-shadow: var(--_shadow);
max-width: 520px;
width: 100%; width: 100%;
padding: 24px; padding: 24px;
max-height: 85vh; pointer-events: auto;
}
probo-banner .card {
max-width: 450px;
}
probo-preference-panel .card {
max-width: 520px;
max-height: 75vh;
display: flex;
flex-direction: column;
padding: 0;
}
probo-preference-panel probo-category-list {
overflow-y: auto; overflow-y: auto;
flex: 1;
min-height: 0;
} }
.title { .title {
@@ -87,10 +131,10 @@ export const THEMED_STYLES = `
.btn { .btn {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
padding: 10px 16px; padding: 8px 10px;
border-radius: var(--_btn-radius); border-radius: var(--_btn-radius);
border: 1px solid var(--_border); border: 1px solid var(--_border);
background: var(--_bg); background: color-mix(in srgb, var(--_text) 8%, var(--_bg));
color: var(--_text); color: var(--_text);
font-family: var(--_font); font-family: var(--_font);
font-size: var(--_font-size); font-size: var(--_font-size);
@@ -101,7 +145,14 @@ export const THEMED_STYLES = `
} }
.btn:hover { .btn:hover {
border-color: var(--_accent); opacity: 0.8;
}
.btn-link {
background: transparent;
border: none;
color: var(--_accent);
text-decoration: underline;
} }
.btn-primary { .btn-primary {
@@ -110,50 +161,53 @@ export const THEMED_STYLES = `
border-color: var(--_accent); border-color: var(--_accent);
} }
.btn-primary:hover {
opacity: 0.9;
}
.panel-header { .panel-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 16px; padding: 24px;
border-bottom: 1px solid var(--_border);
} }
.panel-back { .panel-close {
background: none; background: none;
border: none; border: none;
cursor: pointer; cursor: pointer;
padding: 4px; padding: 4px;
color: var(--_text); color: var(--_text-secondary);
font-family: var(--_font);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; justify-content: center;
} }
.panel-back:hover { .panel-close:hover {
color: var(--_accent); color: var(--_text);
} }
probo-category-list { probo-category-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; }
margin-bottom: 20px;
probo-preference-panel .buttons {
border-top: 1px solid var(--_border);
padding: 20px 24px 24px;
} }
probo-category { probo-category {
display: block; display: block;
border: 1px solid var(--_border); border-bottom: 1px solid var(--_border);
border-radius: 8px; padding: 12px 40px 12px 60px;
padding: 12px 16px; position: relative;
}
probo-category:last-child {
border-bottom: none;
} }
.category-header { .category-header {
display: flex; display: flex;
align-items: center; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
} }
@@ -181,9 +235,11 @@ export const THEMED_STYLES = `
.toggle { .toggle {
position: relative; position: relative;
width: 44px; display: inline-block;
height: 24px; width: 34px;
height: 18px;
flex-shrink: 0; flex-shrink: 0;
margin-top: 2px;
} }
.toggle input { .toggle input {
@@ -197,7 +253,7 @@ export const THEMED_STYLES = `
position: absolute; position: absolute;
inset: 0; inset: 0;
background: var(--_border); background: var(--_border);
border-radius: 12px; border-radius: 9px;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;
} }
@@ -207,8 +263,8 @@ export const THEMED_STYLES = `
position: absolute; position: absolute;
top: 2px; top: 2px;
left: 2px; left: 2px;
width: 20px; width: 14px;
height: 20px; height: 14px;
background: white; background: white;
border-radius: 50%; border-radius: 50%;
transition: transform 0.2s; transition: transform 0.2s;
@@ -220,7 +276,7 @@ export const THEMED_STYLES = `
} }
.toggle input:checked + .toggle-track::after { .toggle input:checked + .toggle-track::after {
transform: translateX(20px); transform: translateX(16px);
} }
.toggle input:disabled + .toggle-track { .toggle input:disabled + .toggle-track {
@@ -228,41 +284,71 @@ export const THEMED_STYLES = `
cursor: not-allowed; cursor: not-allowed;
} }
.cookie-toggle {
position: absolute;
left: 36px;
top: 14px;
background: none;
border: none;
cursor: pointer;
padding: 2px;
margin: 0;
color: var(--_text-secondary);
display: flex;
align-items: center;
justify-content: center;
}
.cookie-toggle:hover {
color: var(--_accent);
}
.cookie-toggle svg {
transition: transform 0.2s;
transform: rotate(-90deg);
}
.cookie-toggle.open svg {
transform: rotate(0deg);
}
probo-cookie-list { probo-cookie-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6px; margin-top: 10px;
margin-top: 8px; background: color-mix(in srgb, var(--_text) 4%, var(--_bg));
padding-top: 8px; border-radius: 8px;
border-top: 1px solid var(--_border); overflow: hidden;
} }
.cookie-item { .cookie-item {
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
align-items: baseline; gap: 2px;
padding: 10px 12px;
font-size: calc(var(--_font-size) - 2px); font-size: calc(var(--_font-size) - 2px);
gap: 8px; border-bottom: 1px solid var(--_border);
}
.cookie-item:last-child {
border-bottom: none;
} }
.cookie-name { .cookie-name {
font-weight: 500; font-weight: 500;
font-family: monospace; font-family: monospace;
flex-shrink: 0;
} }
.cookie-duration { .cookie-detail {
color: var(--_text-secondary); color: var(--_text-secondary);
flex-shrink: 0; }
.cookie-label {
font-weight: 500;
color: var(--_text);
} }
[hidden] { [hidden] {
display: none !important; display: none !important;
} }
@media (min-width: 640px) {
.overlay {
align-items: center;
}
}
`; `;

View File

@@ -17,7 +17,8 @@ import type { ProboCookieBannerRoot } from "../components/cookie-banner-root";
import type { BannerConfig } from "../client"; import type { BannerConfig } from "../client";
import { THEMED_STYLES } from "./styles"; import { THEMED_STYLES } from "./styles";
const BACK_ARROW = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>`; const CLOSE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
const CHEVRON_DOWN = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>`;
export class ProboThemedBanner extends HTMLElement { export class ProboThemedBanner extends HTMLElement {
private shadow: ShadowRoot; private shadow: ShadowRoot;
@@ -48,33 +49,35 @@ export class ProboThemedBanner extends HTMLElement {
<style>${THEMED_STYLES}</style> <style>${THEMED_STYLES}</style>
<probo-cookie-banner-root banner-id="${this.esc(bannerId)}" base-url="${this.esc(baseUrl)}"> <probo-cookie-banner-root banner-id="${this.esc(bannerId)}" base-url="${this.esc(baseUrl)}">
<probo-banner> <probo-banner>
<div class="overlay"> <div class="floating" data-position="${this.esc(position)}">
<div class="card" role="dialog" aria-label="Cookie consent"> <div class="card" role="dialog" aria-label="Cookie consent">
<p class="title">Cookie Preferences</p> <p class="title">Cookie Preferences</p>
<p class="description" data-description> <p class="description" data-description>
We use cookies to improve your experience and analyze site traffic. We use cookies to improve your experience and analyze site traffic.
</p> </p>
<div class="buttons"> <div class="buttons">
<probo-reject-button><button class="btn">Reject all</button></probo-reject-button>
<probo-customize-button><button class="btn">Customize</button></probo-customize-button>
<probo-accept-button><button class="btn btn-primary">Accept all</button></probo-accept-button> <probo-accept-button><button class="btn btn-primary">Accept all</button></probo-accept-button>
<probo-reject-button><button class="btn">Reject all</button></probo-reject-button>
<probo-customize-button><button class="btn btn-link">Customize</button></probo-customize-button>
</div> </div>
</div> </div>
</div> </div>
</probo-banner> </probo-banner>
<probo-preference-panel> <probo-preference-panel>
<div class="overlay"> <div class="floating" data-position="${this.esc(position)}">
<div class="card" role="dialog" aria-label="Cookie preferences"> <div class="card" role="dialog" aria-label="Cookie preferences">
<div class="panel-header"> <div class="panel-header">
<button class="panel-back" data-action="back">
${BACK_ARROW} Back
</button>
<p class="title" style="margin:0">Preferences</p> <p class="title" style="margin:0">Preferences</p>
<div style="width:60px"></div> <button class="panel-close" data-action="back" aria-label="Close">
${CLOSE_ICON}
</button>
</div> </div>
<probo-category-list> <probo-category-list>
<template> <template>
<button class="cookie-toggle" data-action="toggle-cookies">
${CHEVRON_DOWN}
</button>
<div class="category-header"> <div class="category-header">
<div class="category-info"> <div class="category-info">
<div class="category-name" data-slot="name"></div> <div class="category-name" data-slot="name"></div>
@@ -87,11 +90,12 @@ export class ProboThemedBanner extends HTMLElement {
</label> </label>
</probo-category-toggle> </probo-category-toggle>
</div> </div>
<probo-cookie-list> <probo-cookie-list hidden>
<template> <template>
<div class="cookie-item"> <div class="cookie-item">
<span class="cookie-name" data-slot="name"></span> <span class="cookie-name" data-slot="name"></span>
<span class="cookie-duration" data-slot="duration"></span> <span class="cookie-detail"><span class="cookie-label">Description:</span> <span data-slot="description"></span></span>
<span class="cookie-detail"><span class="cookie-label">Duration:</span> <span data-slot="duration"></span></span>
</div> </div>
</template> </template>
</probo-cookie-list> </probo-cookie-list>
@@ -120,6 +124,22 @@ export class ProboThemedBanner extends HTMLElement {
this.shadow.querySelector("[data-action=back]")?.addEventListener("click", () => { this.shadow.querySelector("[data-action=back]")?.addEventListener("click", () => {
root.setState("banner"); root.setState("banner");
}); });
this.shadow.addEventListener("click", (e: Event) => {
const btn = (e.target as Element).closest?.("[data-action=toggle-cookies]") as HTMLElement | null;
if (!btn) return;
const category = btn.closest("probo-category");
const cookieList = category?.querySelector("probo-cookie-list") as HTMLElement | null;
if (!cookieList) return;
const open = cookieList.hasAttribute("hidden");
if (open) {
cookieList.removeAttribute("hidden");
btn.classList.add("open");
} else {
cookieList.setAttribute("hidden", "");
btn.classList.remove("open");
}
});
} }
private updateDescription(config: BannerConfig): void { private updateDescription(config: BannerConfig): void {