Refine static layouts and card primitives

This commit is contained in:
thePR0M3TH3AN committed 2025-10-11 13:45:22 -04:00
1 parent 03c7dc9653
commit b50d474151
16 files changed
+268 -408

No files matched your search

+1 -1
View File
@@ -1,3 +1,3 @@
:root{--color-page:#fff;--color-page-alt:#f9fafb;--color-surface:#0f172a;--color-surface-alt:#111827;--color-bg:var(--color-surface);--color-card:#1e293b;--color-card-private:#3b1d74;--color-card-critical:#3f0b13;--color-panel:#1f2937;--color-panel-hover:#334155;--color-border:#374151;--color-border-subtle:#d1d5db;--color-primary:#fe0032;--color-secondary:#ff93a5;--color-info:#60a5fa;--color-info-strong:#2563eb;--color-info-pressed:#1d4ed8;--color-muted-strong:#6b7280;--color-text:#f8fafc;--color-text-strong:#0f172a;--color-text-muted:var(--color-muted);--color-muted:#94a3b8;--color-muted-alt:#9ca3af;--color-subtle-text:#e2e8f0;--color-subtle:#e5e7eb;--color-critical:#7f1d1d;--color-critical-strong:#991b1b;--color-warning:#facc15;--color-warning-strong:#d97706;--color-warning-surface:rgba(146,64,14,.24);--color-neutral-700:#3f3f46;--color-neutral-600:#52525b;--color-white:#fff;--color-black:#000;--color-overlay:#1a2234;--color-overlay-black:rgba(0,0,0,.9);--color-overlay-muted:rgba(15,23,42,.55);--color-overlay-strong:rgba(15,23,42,.75);--color-overlay-darker:rgba(15,23,42,.88);--color-overlay-panel:rgba(31,41,55,.9);--color-overlay-panel-soft:rgba(31,41,55,.75);--color-overlay-panel-tint:rgba(31,41,55,.5);--color-overlay-panel-glass:rgba(30,41,59,.85);--color-overlay-veil:rgba(11,17,32,.96);--color-overlay-banner:rgba(51,65,85,.95);--color-overlay-panel-gradient-start:rgba(30,41,59,.92);--color-overlay-panel-gradient-end:rgba(15,23,42,.92);--color-surface-tint-subtle:rgba(148,163,184,.08);--color-border-translucent:rgba(148,163,184,.18);--color-border-translucent-strong:rgba(148,163,184,.25);--color-border-translucent-bright:rgba(148,163,184,.32);--color-border-translucent-stronger:rgba(148,163,184,.35);--color-border-translucent-strongest:rgba(148,163,184,.4);--color-border-translucent-glow:rgba(148,163,184,.55);--color-border-translucent-medium:rgba(148,163,184,.2);--color-border-neutral-soft:rgba(229,231,235,.35);--color-border-info-soft:rgba(96,165,250,.35);--color-border-sidebar-strong:rgba(55,65,81,.9);--color-ring-private:rgba(168,85,247,.55);--color-ring-private-hover:rgba(192,132,252,.65);--color-ring-critical:rgba(239,68,68,.4);--color-ring-critical-soft:rgba(239,68,68,.35);--color-ring-critical-hover:rgba(239,68,68,.45);--color-ring-critical-strong:rgba(239,68,68,.55);--color-ring-alert:rgba(220,38,38,.6);--color-gradient-visibility-start:rgba(129,140,248,.4);--color-gradient-visibility-end:rgba(192,132,252,.32);--color-white-soft:hsla(0,0%,100%,.1);--color-white-softer:hsla(0,0%,100%,.3);--color-white-softest:hsla(0,0%,100%,.4);--color-shadow-floating:rgba(8,11,30,.55);--color-shadow-elevated:rgba(8,11,30,.45);--color-shadow-panel:rgba(15,23,42,.35);--color-shadow-notification:rgba(15,23,42,.3);--color-shadow-sidebar:rgba(0,0,0,.45);--color-divider-strong:rgba(15,23,42,.35);--color-text-frosted-strong:rgba(226,232,240,.94);--color-text-frosted:rgba(226,232,240,.9);--color-text-frosted-soft:rgba(226,232,240,.82);--color-blog-transparent:transparent;--color-blog-cyan-500:#06b6d4;--color-blog-sky-500:#0bf;--color-blog-teal-600:#0e7490;--color-blog-green-800:#14532d;--color-blog-green-700:#15803d;--color-blog-teal-700:#164e63;--color-blog-gray-900:#171717;--color-blog-coffee-900:#1c1917;--color-blog-green-500:#22c55e;--color-blog-gray-800:#2b2b2b;--color-blog-gray-700:#3b3b3b;--color-blog-teal-accent:#3e8b8a;--color-blog-gray-650:#404040;--color-blog-brown-700:#44403c;--color-blog-purple-900:#581c87;--color-blog-gray-500:#666;--color-blog-gray-450:#737373;--color-blog-gray-440:#747474;--color-blog-brown-500:#78716c;--color-blog-rust-700:#7c2d12;--color-blog-purple-700:#7e22ce;--color-blog-purple-accent:#92379c;--color-blog-purple-muted:#96609c;--color-blog-gray-300:#999;--color-blog-purple-500:#a855f7;--color-blog-gray-275:#a9a9a9;--color-blog-red-600:#b91c1c;--color-blog-rust-600:#c2410c;--color-blog-error:#ca1717;--color-blog-gray-220:#cbcbcb;--color-blog-gray-210:#ccc;--color-blog-gray-200:#dedede;--color-blog-amber-500:#e4984d;--color-blog-gray-190:#e0e0e0;--color-blog-orange-600:#e35428;--color-blog-gray-180:#e9e9e9;--color-blog-gray-170:#ebebeb;--color-blog-red-500:#ef4444;--color-blog-amber-400:#f6ad55;--color-blog-neutral-100:#f5f5f5;--color-blog-neutral-120:#f6f6f6;--color-blog-orange-500:#f97316;--color-blog-neutral-50:#fdfdfd;--space-2xs:0.25rem;--space-xs:0.5rem;--space-sm:0.75rem;--space-md:1rem;--space-lg:1.5rem;--space-xl:2rem;--space-2xl:3rem;--radius-sm:0.25rem;--radius-md:0.5rem;--radius-lg:0.75rem;--radius-xl:1rem;--radius-full:9999px;--shadow-sm:0 0.0625rem 0.125rem 0 rgb(0 0 0/5%);--shadow-md:0 0.25rem 0.375rem -0.0625rem rgba(0,0,0,.1);--shadow-lg:0 0.625rem 0.9375rem -0.1875rem rgba(0,0,0,.1);--shadow-modal:0 1.25rem 2.5rem -0.375rem rgba(15,23,42,.55);--font-family-sans:"Inter","Segoe UI",system-ui,-apple-system,sans-serif;--font-family-mono:"Fira Code","SFMono-Regular",ui-monospace,monospace;--font-size-xs:0.75rem;--font-size-sm:0.875rem;--font-size-base:1rem;--font-size-lLine truncated
/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--font-family-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.focus-ring{outline:2px solid transparent;outline-offset:2px}.focus-ring:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:var(--color-info);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--color-surface);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1)}.popover{position:relative;display:inline-flex}.popover__panel{outline:2px solid transparent;outline-offset:2px}.popover__panel:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:var(--color-info);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--color-surface);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1)}.popover__panel{pointer-events:none;position:absolute;left:0;top:100%;z-index:70;margin-top:.5rem;min-width:12rem;transform-origin:top;--tw-scale-x:.95;--tw-scale-y:.95;border-radius:var(--radius-lg);border-width:1px;border-color:var(--color-border);--tw-border-opacity:0.4;background-color:var(--color-panel);padding:.75rem 1rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--color-text);opacity:0;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)Line truncated
/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--font-family-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.focus-ring{outline:2px solid transparent;outline-offset:2px}.focus-ring:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:var(--color-info);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--color-surface);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1)}.popover{position:relative;display:inline-flex}.popover__panel{outline:2px solid transparent;outline-offset:2px}.popover__panel:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:var(--color-info);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--color-surface);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1)}.popover__panel{pointer-events:none;position:absolute;left:0;top:100%;z-index:70;margin-top:.5rem;min-width:12rem;transform-origin:top;--tw-scale-x:.95;--tw-scale-y:.95;border-radius:var(--radius-lg);border-width:1px;border-color:var(--color-border);--tw-border-opacity:0.4;background-color:var(--color-panel);padding:.75rem 1rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--color-text);opacity:0;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)Line truncated
+1 -1
View File
@@ -369,7 +369,7 @@
/* Legacy compatibility shims */
.video-card {
@apply card overflow-visible;
@apply overflow-visible;
}
.video-card.video-card--owner-private {
+3 -2
View File
@@ -175,7 +175,8 @@
<!-- Error Container -->
<div
id="errorContainer"
class="bg-critical/10 mb-4 hidden rounded-md p-4 text-critical-strong"
class="card mb-4 hidden p-md text-sm text-critical-strong"
data-state="critical"
></div>
<!-- Status Container -->
@@ -192,7 +193,7 @@
<!-- Success Container -->
<div
id="successContainer"
class="bg-info/10 mb-4 hidden rounded-md p-4 text-info-strong"
class="card mb-4 hidden p-md text-sm text-info-strong"
></div>
</div>
+29 -82
View File
@@ -4063,65 +4063,41 @@ class Application {
*/
getUrlHealthPlaceholderMarkup(options = {}) {
const includeMargin = options?.includeMargin !== false;
const classes = [
"url-health-badge",
"text-xs",
"font-semibold",
"px-2",
"py-1",
"rounded",
"inline-flex",
"items-center",
"gap-1",
"bg-gray-800",
"text-gray-300",
];
const classes = ["badge", "url-health-badge", "text-muted"];
if (includeMargin) {
classes.splice(1, 0, "mt-3");
classes.push("mt-sm");
}
return `
<div
<span
class="${classes.join(" ")}"
data-url-health-state="checking"
data-variant="neutral"
aria-live="polite"
role="status"
>
Checking hosted URL…
</div>
</span>
`;
}
getTorrentHealthBadgeMarkup(options = {}) {
const includeMargin = options?.includeMargin !== false;
const classes = [
"torrent-health-badge",
"text-xs",
"font-semibold",
"px-2",
"py-1",
"rounded",
"inline-flex",
"items-center",
"gap-1",
"bg-gray-800",
"text-gray-300",
"transition-colors",
"duration-200",
];
const classes = ["badge", "torrent-health-badge"];
if (includeMargin) {
classes.unshift("mt-3");
classes.push("mt-sm");
}
return `
<div
<span
class="${classes.join(" ")}"
data-stream-health-state="checking"
data-variant="neutral"
aria-live="polite"
role="status"
>
⏳ Torrent
</div>
</span>
`;
}
@@ -4170,10 +4146,10 @@ class Application {
? "⚠️ CDN timed out"
: "Checking hosted URL…");
const hadMargin = badgeEl.classList.contains("mt-3");
const hadCompactMargin =
badgeEl.classList.contains("mt-sm") || badgeEl.classList.contains("mt-3");
badgeEl.dataset.urlHealthState = status;
const cardEl = badgeEl.closest(".video-card");
const cardEl = badgeEl.closest(".card[data-video-id]");
if (cardEl) {
cardEl.dataset.urlHealthState = status;
}
@@ -4181,53 +4157,24 @@ class Application {
badgeEl.setAttribute("role", status === "offline" ? "alert" : "status");
badgeEl.textContent = message;
const baseClasses = [
"url-health-badge",
"text-xs",
"font-semibold",
"px-2",
"py-1",
"rounded",
"transition-colors",
"duration-200",
];
if (hadMargin) {
baseClasses.unshift("mt-3");
const classes = ["badge", "url-health-badge"];
if (hadCompactMargin) {
classes.push("mt-sm");
}
badgeEl.className = baseClasses.join(" ");
badgeEl.className = classes.join(" ");
if (status === "healthy") {
badgeEl.classList.add(
"inline-flex",
"items-center",
"gap-1",
"bg-green-900",
"text-green-200"
);
} else if (status === "offline") {
badgeEl.classList.add(
"inline-flex",
"items-center",
"gap-1",
"bg-red-900",
"text-red-200"
);
} else if (status === "unknown" || status === "timeout") {
badgeEl.classList.add(
"inline-flex",
"items-center",
"gap-1",
"bg-amber-900",
"text-amber-200"
);
} else {
badgeEl.classList.add(
"inline-flex",
"items-center",
"gap-1",
"bg-gray-800",
"text-gray-300"
);
const variantMap = {
healthy: "info",
offline: "critical",
unknown: "neutral",
timeout: "neutral",
checking: "neutral",
};
const variant = variantMap[status];
if (variant) {
badgeEl.dataset.variant = variant;
} else if (badgeEl.dataset.variant) {
delete badgeEl.dataset.variant;
}
if (
+1 -1
View File
@@ -1,4 +1,4 @@
const DEFAULT_CARD_SELECTOR = ".video-card";
const DEFAULT_CARD_SELECTOR = ".card[data-video-id]";
function defaultIsCardVisible(entry) {
if (!entry) {
+15 -22
View File
@@ -433,55 +433,48 @@ function setBadge(card, state, details) {
return;
}
const hadMargin = badge.classList.contains("mt-3");
const hadCompactMargin =
badge.classList.contains("mt-sm") || badge.classList.contains("mt-3");
const baseClasses = [
"torrent-health-badge",
"text-xs",
"font-semibold",
"px-2",
"py-1",
"rounded",
"inline-flex",
"items-center",
"gap-1",
"transition-colors",
"duration-200",
];
if (hadMargin) {
baseClasses.unshift("mt-3");
const classes = ["badge", "torrent-health-badge"];
if (hadCompactMargin) {
classes.push("mt-sm");
}
badge.className = baseClasses.join(" ");
badge.className = classes.join(" ");
const map = {
healthy: {
icon: "🟢",
aria: "WebTorrent peers available",
classes: ["bg-green-900", "text-green-200"],
variant: "info",
role: "status",
},
unhealthy: {
icon: "🔴",
aria: "WebTorrent peers unavailable",
classes: ["bg-red-900", "text-red-200"],
variant: "critical",
role: "alert",
},
checking: {
icon: "⏳",
aria: "Checking WebTorrent peers",
classes: ["bg-gray-800", "text-gray-300"],
variant: "neutral",
role: "status",
},
unknown: {
icon: "⚪",
aria: "WebTorrent status unknown",
classes: ["bg-gray-800", "text-gray-300"],
variant: "neutral",
role: "status",
},
};
const entry = map[normalizedState] || map.unknown;
entry.classes.forEach((cls) => badge.classList.add(cls));
if (entry.variant) {
badge.dataset.variant = entry.variant;
} else if (badge.dataset.variant) {
delete badge.dataset.variant;
}
const peersText =
normalizedState === "healthy" && peersValue > 0 ? ` (${peersValue})` : "";
+118 -186
View File
@@ -210,7 +210,12 @@ export class VideoCard {
closeSettingsMenu() {
if (this.settingsDropdown) {
this.settingsDropdown.classList.add("hidden");
this.settingsDropdown.dataset.state = "closed";
this.settingsDropdown.setAttribute("aria-hidden", "true");
this.settingsDropdown.hidden = true;
}
if (this.settingsButton) {
this.settingsButton.setAttribute("aria-expanded", "false");
}
}
@@ -218,15 +223,7 @@ export class VideoCard {
const doc = this.document;
const root = this.createElement("div", {
classNames: [
"video-card",
"bg-card",
"rounded-lg",
"shadow-lg",
"hover:shadow-2xl",
"transition-all",
"duration-300"
]
classNames: ["video-card", "card"]
});
this.applyClassListFromString(root, this.highlightClass);
@@ -266,7 +263,9 @@ export class VideoCard {
ratio.appendChild(thumbnail);
anchor.appendChild(ratio);
const content = this.createElement("div", { classNames: ["p-4"] });
const content = this.createElement("div", {
classNames: ["p-md", "bv-stack", "bv-stack--tight"]
});
const title = this.createElement("h3", {
classNames: [
@@ -275,8 +274,7 @@ export class VideoCard {
"text-text",
"line-clamp-2",
"hover:text-info-strong",
"cursor-pointer",
"mb-3"
"cursor-pointer"
],
textContent: this.video.title
});
@@ -551,21 +549,22 @@ export class VideoCard {
if (this.capabilities.canEdit) {
const wrapper = this.createElement("div", {
classNames: ["relative", "inline-block", "ml-3", "overflow-visible"]
classNames: ["popover", "ml-2"]
});
const button = this.createElement("button", {
classNames: [
"focus-ring",
"inline-flex",
"items-center",
"p-2",
"btn-ghost",
"h-10",
"w-10",
"rounded-full",
"text-muted",
"hover:text-text",
"hover:bg-panel"
"p-0",
"text-muted"
],
attrs: {
type: "button"
type: "button",
"aria-haspopup": "true",
"aria-expanded": "false",
"aria-label": "Video settings"
}
});
button.dataset.settingsDropdown = String(this.index);
@@ -573,89 +572,77 @@ export class VideoCard {
const icon = doc.createElement("img");
icon.src = "assets/svg/video-settings-gear.svg";
icon.alt = "Settings";
icon.classList.add("w-5", "h-5");
icon.classList.add("h-5", "w-5");
button.appendChild(icon);
const dropdown = this.createElement("div", {
classNames: [
"hidden",
"absolute",
"right-0",
"bottom-full",
"mb-2",
"w-32",
"rounded-md",
"shadow-lg",
"bg-panel",
"ring-1",
"ring-black",
"ring-opacity-5",
"z-50"
]
classNames: ["popover__panel", "w-44", "p-0"],
attrs: {
hidden: "",
"data-state": "closed",
"aria-hidden": "true"
}
});
dropdown.id = `settingsDropdown-${this.index}`;
dropdown.setAttribute("role", "menu");
const list = this.createElement("div", {
classNames: ["py-1"]
classNames: ["menu"],
attrs: { role: "none" }
});
const editButton = this.createElement("button", {
classNames: [
"block",
"w-full",
"text-left",
"px-4",
"py-2",
"text-sm",
"text-text",
"hover:bg-panel-hover"
],
textContent: "Edit"
const addMenuItem = ({ text, variant = null, dataset = {} }) => {
const item = this.createElement("button", {
classNames: ["menu__item", "justify-start"],
textContent: text,
attrs: {
type: "button",
role: "menuitem"
}
});
if (variant) {
item.dataset.variant = variant;
}
Object.entries(dataset).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
item.dataset[key] = String(value);
});
list.appendChild(item);
return item;
};
const editButton = addMenuItem({
text: "Edit",
dataset: {
editIndex: String(this.index),
editEventId: this.video.id
}
});
editButton.dataset.editIndex = String(this.index);
editButton.dataset.editEventId = this.video.id;
list.appendChild(editButton);
this.editButton = editButton;
if (this.capabilities.canRevert) {
const revertButton = this.createElement("button", {
classNames: [
"block",
"w-full",
"text-left",
"px-4",
"py-2",
"text-sm",
"text-critical",
"hover:bg-critical/20",
"hover:text-text"
],
textContent: "Revert"
const revertButton = addMenuItem({
text: "Revert",
variant: "critical",
dataset: {
revertIndex: String(this.index),
revertEventId: this.video.id
}
});
revertButton.dataset.revertIndex = String(this.index);
revertButton.dataset.revertEventId = this.video.id;
list.appendChild(revertButton);
this.revertButton = revertButton;
}
if (this.capabilities.canDelete) {
const deleteButton = this.createElement("button", {
classNames: [
"block",
"w-full",
"text-left",
"px-4",
"py-2",
"text-sm",
"text-critical",
"hover:bg-critical/20",
"hover:text-text"
],
textContent: "Delete All"
const deleteButton = addMenuItem({
text: "Delete All",
variant: "critical",
dataset: {
deleteAllIndex: String(this.index),
deleteAllEventId: this.video.id
}
});
deleteButton.dataset.deleteAllIndex = String(this.index);
deleteButton.dataset.deleteAllEventId = this.video.id;
list.appendChild(deleteButton);
this.deleteButton = deleteButton;
}
@@ -680,17 +667,12 @@ export class VideoCard {
const button = this.createElement("button", {
classNames: [
"focus-ring",
"inline-flex",
"items-center",
"justify-center",
"w-10",
"btn-ghost",
"h-10",
"p-2",
"w-10",
"rounded-full",
"text-muted",
"hover:text-text",
"hover:bg-panel"
"p-0",
"text-muted"
],
attrs: {
type: "button",
@@ -882,17 +864,8 @@ export class VideoCard {
const pieces = [];
if (this.playbackUrl) {
const badge = this.createElement("div", {
classNames: [
"url-health-badge",
"text-xs",
"font-semibold",
"px-2",
"py-1",
"rounded",
"transition-colors",
"duration-200"
]
const badge = this.createElement("span", {
classNames: ["badge", "url-health-badge"]
});
badge.setAttribute("aria-live", "polite");
badge.setAttribute("role", "status");
@@ -902,17 +875,8 @@ export class VideoCard {
}
if (this.magnetSupported && this.magnetProvided) {
const badge = this.createElement("div", {
classNames: [
"torrent-health-badge",
"text-xs",
"font-semibold",
"px-2",
"py-1",
"rounded",
"transition-colors",
"duration-200"
]
const badge = this.createElement("span", {
classNames: ["badge", "torrent-health-badge"]
});
badge.setAttribute("aria-live", "polite");
badge.setAttribute("role", "status");
@@ -929,7 +893,7 @@ export class VideoCard {
}
const container = this.createElement("div", {
classNames: ["mt-3", "flex", "flex-wrap", "items-center", "gap-2"]
classNames: ["flex", "flex-wrap", "items-center", "gap-sm"]
});
pieces.forEach((el) => container.appendChild(el));
return container;
@@ -1058,35 +1022,20 @@ export class VideoCard {
? entry.message
: "Checking hosted URL…";
const hadMargin = badge.classList.contains("mt-3");
const baseClasses = [
"url-health-badge",
"text-xs",
"font-semibold",
"px-2",
"py-1",
"rounded",
"transition-colors",
"duration-200"
];
if (hadMargin) {
baseClasses.unshift("mt-3");
}
badge.className = baseClasses.join(" ");
badge.className = ["badge", "url-health-badge"].join(" ");
const common = ["inline-flex", "items-center", "gap-1"];
const addClasses = (classes) => {
classes.forEach((cls) => badge.classList.add(cls));
const variantMap = {
healthy: "info",
offline: "critical",
unknown: "neutral",
timeout: "neutral",
checking: "neutral"
};
if (status === "healthy") {
addClasses([...common, "bg-info/20", "text-info-strong"]);
} else if (status === "offline") {
addClasses([...common, "bg-critical/20", "text-critical"]);
} else if (status === "unknown" || status === "timeout") {
addClasses([...common, "bg-warning-surface", "text-warning-strong"]);
} else {
addClasses([...common, "bg-panel", "text-muted"]);
const variant = variantMap[status];
if (variant) {
badge.dataset.variant = variant;
} else if (badge.dataset.variant) {
delete badge.dataset.variant;
}
badge.dataset.urlHealthState = status;
@@ -1124,55 +1073,41 @@ export class VideoCard {
? "assertive"
: "polite";
const hadMargin = badge.classList.contains("mt-3");
const baseClasses = [
"torrent-health-badge",
"text-xs",
"font-semibold",
"px-2",
"py-1",
"rounded",
"transition-colors",
"duration-200"
];
if (hadMargin) {
baseClasses.unshift("mt-3");
}
badge.className = baseClasses.join(" ");
const common = ["inline-flex", "items-center", "gap-1"];
const addClasses = (classes) => {
classes.forEach((cls) => badge.classList.add(cls));
};
if (state === "healthy") {
addClasses([...common, "bg-info/20", "text-info-strong"]);
} else if (state === "unhealthy") {
addClasses([...common, "bg-critical/20", "text-critical"]);
} else {
addClasses([...common, "bg-panel", "text-muted"]);
}
badge.className = ["badge", "torrent-health-badge"].join(" ");
const map = {
healthy: {
icon: "🟢",
aria: "WebTorrent peers available"
aria: "WebTorrent peers available",
variant: "info",
role: "status"
},
unhealthy: {
icon: "🔴",
aria: "WebTorrent peers unavailable"
aria: "WebTorrent peers unavailable",
variant: "critical",
role: "alert"
},
checking: {
icon: "⏳",
aria: "Checking WebTorrent peers"
aria: "Checking WebTorrent peers",
variant: "neutral",
role: "status"
},
unknown: {
icon: "⚪",
aria: "WebTorrent status unknown"
aria: "WebTorrent status unknown",
variant: "neutral",
role: "status"
}
};
const descriptor = map[state] || map.unknown;
if (descriptor.variant) {
badge.dataset.variant = descriptor.variant;
} else if (badge.dataset.variant) {
delete badge.dataset.variant;
}
const peersText =
state === "healthy" && peersValue > 0 ? ` (${peersValue})` : "";
const iconPrefix = descriptor.icon ? `${descriptor.icon} ` : "";
@@ -1245,13 +1180,7 @@ export class VideoCard {
: safeCount.toLocaleString();
const container = this.createElement("div", {
classNames: [
"flex",
"items-center",
"text-xs",
"text-muted-strong",
"mt-3"
]
classNames: ["flex", "items-center", "text-xs", "text-muted-strong"]
});
container.dataset.discussionCount = this.video.id;
container.dataset.countState = "ready";
@@ -1507,14 +1436,17 @@ export class VideoCard {
this.settingsButton.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
const willOpen = this.settingsDropdown.classList.contains("hidden");
const willOpen = this.settingsDropdown.dataset.state !== "open";
if (this.onRequestCloseAllMenus) {
this.onRequestCloseAllMenus(this);
} else {
this.closeSettingsMenu();
}
if (willOpen) {
this.settingsDropdown.classList.remove("hidden");
this.settingsDropdown.hidden = false;
this.settingsDropdown.dataset.state = "open";
this.settingsDropdown.setAttribute("aria-hidden", "false");
this.settingsButton.setAttribute("aria-expanded", "true");
}
});
}
+10 -2
View File
@@ -54,8 +54,16 @@ export async function loadView(viewUrl) {
});
} catch (err) {
console.error("View loading error:", err);
document.getElementById("viewContainer").innerHTML =
"<p class='text-center text-red-500'>Failed to load content.</p>";
const fallbackMarkup = `
<div class="bv-stack">
<article class="card p-md" data-state="critical">
<p class="text-sm text-critical-strong text-center">
Failed to load content.
</p>
</article>
</div>
`;
document.getElementById("viewContainer").innerHTML = fallbackMarkup;
}
}
+10 -15
View File
@@ -1,20 +1,15 @@
<!-- views/about.html -->
<section class="mb-8">
<!--
This container uses a white background and dark text,
overriding your global “body { color: … }” rule
-->
<div class="rounded-lg bg-page p-6 text-text-strong shadow-md">
<h2 class="mb-4 text-2xl font-bold text-text-strong">About bitvid</h2>
<!--
The .markdown-body class ensures the Markdown styling
from your markdown.css can be applied here.
-->
<div id="markdown-container" class="markdown-body">
<p class="text-muted-strong">Loading content...</p>
<section class="bv-stack" aria-labelledby="aboutHeading">
<article class="card p-lg">
<div class="bv-stack bv-stack--tight">
<h2 id="aboutHeading" class="text-2xl font-semibold text-text-strong">
About bitvid
</h2>
<div id="markdown-container" class="markdown-body">
<p class="text-muted-strong">Loading content...</p>
</div>
</div>
</div>
</article>
</section>
<script>
+14 -11
View File
@@ -1,14 +1,17 @@
<!-- views/blog.html -->
<section class="mb-8">
<div class="rounded-lg bg-page p-6 text-text-strong shadow-md">
<h2 class="mb-4 text-2xl font-bold text-text-strong">Latest Blog Posts</h2>
<div class="relative overflow-hidden" style="height: 1600px">
<iframe
id="blog-iframe"
src="https://beta.bitvid.network/blog.html"
class="h-full w-full border-none"
></iframe>
<section class="bv-stack" aria-labelledby="blogHeading">
<article class="card p-lg">
<div class="bv-stack bv-stack--tight">
<h2 id="blogHeading" class="text-2xl font-semibold text-text-strong">
Latest Blog Posts
</h2>
<div class="relative isolate overflow-hidden rounded-lg bg-surface-alt" style="height: 1600px">
<iframe
id="blog-iframe"
src="https://beta.bitvid.network/blog.html"
class="h-full w-full border-none"
></iframe>
</div>
</div>
</div>
</article>
</section>
+13 -17
View File
@@ -1,22 +1,18 @@
<!-- views/community-guidelines.html -->
<section class="mb-8">
<!--
This card has a light background and shadow, similar to your other partials.
We use text-text-strong to override the global body text color.
-->
<div class="rounded-lg bg-page p-6 text-text-strong shadow-md">
<h2 class="mb-4 text-2xl font-bold text-text-strong">
Community Guidelines
</h2>
<!--
The .markdown-body class can use your markdown.css
so headings, paragraphs, etc. are styled nicely.
-->
<div id="markdown-container" class="markdown-body">
<p class="text-muted-strong">Loading content...</p>
<section class="bv-stack" aria-labelledby="communityGuidelinesHeading">
<article class="card p-lg">
<div class="bv-stack bv-stack--tight">
<h2
id="communityGuidelinesHeading"
class="text-2xl font-semibold text-text-strong"
>
Community Guidelines
</h2>
<div id="markdown-container" class="markdown-body">
<p class="text-muted-strong">Loading content...</p>
</div>
</div>
</div>
</article>
</section>
<script>
+13 -9
View File
@@ -1,14 +1,18 @@
<!-- views/getting-started.html -->
<section class="mb-8">
<!-- Light background, shadow, and dark text to override global site color -->
<div class="rounded-lg bg-page p-6 text-text-strong shadow-md">
<h2 class="mb-4 text-2xl font-bold text-text-strong">Getting Started</h2>
<!-- .markdown-body references your markdown.css to style headings, paragraphs, etc. -->
<div id="markdown-container" class="markdown-body">
<p class="text-muted-strong">Loading content...</p>
<section class="bv-stack" aria-labelledby="gettingStartedHeading">
<article class="card p-lg">
<div class="bv-stack bv-stack--tight">
<h2
id="gettingStartedHeading"
class="text-2xl font-semibold text-text-strong"
>
Getting Started
</h2>
<div id="markdown-container" class="markdown-body">
<p class="text-muted-strong">Loading content...</p>
</div>
</div>
</div>
</article>
</section>
<script>
+9 -19
View File
@@ -14,14 +14,14 @@
<button
type="button"
data-history-action="clear-cache"
class="focus-ring rounded-full bg-panel px-4 py-2 text-sm font-medium text-text hover:bg-panel-hover"
class="btn-ghost"
>
Clear local cache
</button>
<button
type="button"
data-history-action="republish"
class="focus-ring rounded-full bg-info px-4 py-2 text-sm font-medium text-text hover:bg-info-strong focus-visible:bg-info-pressed"
class="btn"
>
Republish now
</button>
@@ -30,7 +30,7 @@
<div
id="watchHistoryPrivacyBanner"
class="border-info/40 bg-info/10 hidden rounded-lg border p-4"
class="card hidden p-md"
role="status"
aria-live="polite"
>
@@ -42,17 +42,13 @@
thumbnails across devices?
</p>
<div class="flex flex-wrap gap-2">
<button
id="watchHistoryPrivacyToggle"
type="button"
class="focus-ring rounded-full bg-info px-4 py-2 text-sm font-medium text-text hover:bg-info-strong focus-visible:bg-info-pressed"
>
<button id="watchHistoryPrivacyToggle" type="button" class="btn">
Share metadata
</button>
<button
id="watchHistoryPrivacyDismiss"
type="button"
class="focus-ring hover:bg-info/10 rounded-full bg-transparent px-3 py-2 text-sm font-medium text-info"
class="btn-ghost"
>
Dismiss
</button>
@@ -60,16 +56,14 @@
</div>
</div>
<div
id="watchHistoryInfo"
class="bg-panel/60 rounded-lg p-4 text-sm text-text"
>
<div id="watchHistoryInfo" class="card p-md text-sm">
Thumbnails and titles are hydrated locally from your device caches.
</div>
<div
id="watchHistoryError"
class="border-critical/50 bg-critical/40 hidden rounded-lg border p-3 text-sm text-critical"
class="card hidden p-md text-sm text-critical"
data-state="critical"
role="alert"
></div>
</div>
@@ -100,11 +94,7 @@
Your watch history is empty. Watch some videos to populate this list.
</p>
<button
id="watchHistoryLoadMore"
type="button"
class="focus-ring hidden w-full rounded-full bg-panel px-4 py-2 text-sm font-medium text-text hover:bg-panel-hover"
>
<button id="watchHistoryLoadMore" type="button" class="btn-ghost hidden w-full">
Load more
</button>
+10 -15
View File
@@ -1,20 +1,15 @@
<!-- views/ipns.html -->
<section class="mb-8">
<!--
Added text-text-strong here to ensure we override
any global white text color that might be inherited.
-->
<div class="rounded-lg bg-page p-6 text-text-strong shadow-md">
<h2 class="mb-4 text-2xl font-bold text-text-strong">IPNS Gateways</h2>
<!--
"markdown-body" can be any class name; it’s just so markdown.css
can style headings, lists, etc.
-->
<div id="markdown-container" class="markdown-body">
<p class="text-muted-strong">Loading content...</p>
<section class="bv-stack" aria-labelledby="ipnsHeading">
<article class="card p-lg">
<div class="bv-stack bv-stack--tight">
<h2 id="ipnsHeading" class="text-2xl font-semibold text-text-strong">
IPNS Gateways
</h2>
<div id="markdown-container" class="markdown-body">
<p class="text-muted-strong">Loading content...</p>
</div>
</div>
</div>
</article>
</section>
<script>
+11 -16
View File
@@ -1,20 +1,15 @@
<!-- views/about.html -->
<section class="mb-8">
<!--
This container uses a white background and dark text,
overriding your global “body { color: … }” rule
-->
<div class="rounded-lg bg-page p-6 text-text-strong shadow-md">
<h2 class="mb-4 text-2xl font-bold text-text-strong">bitvid Links</h2>
<!--
The .markdown-body class ensures the Markdown styling
from your markdown.css can be applied here.
-->
<div id="markdown-container" class="markdown-body">
<p class="text-muted-strong">Loading content...</p>
<!-- views/links.html -->
<section class="bv-stack" aria-labelledby="linksHeading">
<article class="card p-lg">
<div class="bv-stack bv-stack--tight">
<h2 id="linksHeading" class="text-2xl font-semibold text-text-strong">
bitvid Links
</h2>
<div id="markdown-container" class="markdown-body">
<p class="text-muted-strong">Loading content...</p>
</div>
</div>
</div>
</article>
</section>
<script>
+10 -9
View File
@@ -1,14 +1,15 @@
<!-- views/roadmap.html -->
<section class="mb-8">
<!-- White card, dark text to override global site color -->
<div class="rounded-lg bg-page p-6 text-text-strong shadow-md">
<h2 class="mb-4 text-2xl font-bold text-text-strong">Roadmap</h2>
<!-- .markdown-body uses your markdown.css rules for headings, etc. -->
<div id="markdown-container" class="markdown-body">
<p class="text-muted-strong">Loading content...</p>
<section class="bv-stack" aria-labelledby="roadmapHeading">
<article class="card p-lg">
<div class="bv-stack bv-stack--tight">
<h2 id="roadmapHeading" class="text-2xl font-semibold text-text-strong">
Roadmap
</h2>
<div id="markdown-container" class="markdown-body">
<p class="text-muted-strong">Loading content...</p>
</div>
</div>
</div>
</article>
</section>
<script>