Remove local blog view in favor of configurable external link

This commit is contained in:
thePR0M3TH3AN committed 2025-10-18 15:28:33 -04:00
1 parent 3993b4c824
commit 8c568af46a
20 files changed
+210 -4797

No files matched your search

+1 -1
View File
@@ -126,7 +126,7 @@ hairline borders or browser reset quirks) add an explicit allowlist entry in
`scripts/check-design-tokens.mjs` so future contributors understand why the
constant exists.
The script now crawls every markup-emitting module under `js/blog`, `js/ui`,
The script now crawls every markup-emitting module under `js/ui`,
`js/channelProfile.js`, and `torrent/ui`, so UI tweaks anywhere in the repo
benefit from the guard without manual configuration.
-73
View File
@@ -1,73 +0,0 @@
<!DOCTYPE html>
<html lang="en" class="blog-html">
<head>
<!-- Configure with your npub/nprofile -->
<meta
name="author"
value="npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe"
/>
<!-- Personalize your relays list -->
<meta
name="relays"
value="wss://nos.lol, wss://relay.damus.io, wss://relay.nostr.band, wss://nostr.wine"
/>
<!-- Show top notes with images, use a even value, zero to disable -->
<meta name="top-notes" value="2" />
<!-- Show short notes as carousel (value 'carousel') or in the main feed (value 'main'), empty to disable -->
<meta name="short-notes" value="carousel" />
<!-- Show short notes with X minimum characters for the 'short-notes' setting, zero to disable -->
<meta name="short-notes-min-chars" value="800" />
<!-- Size in characters of the short notes preview in the feed, zero to display full content -->
<meta name="short-notes-summary-max-chars" value="400" />
<!-- Enable navigation by topics (tags), empty to disable -->
<meta name="topics" value="" />
<!-- Enable comments, 'yes' to enable -->
<meta name="comments" value="yes" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Explore bitvid release notes, roadmap updates, and community highlights across the decentralized video network."
/>
<meta name="robots" content="index,follow" />
<title>bitvid Blog</title>
<link rel="stylesheet" href="./css/tailwind.generated.css" />
<script type="module" crossorigin>
import { bootstrapBlog } from "./js/blog/index.js";
const startBlog = () => {
bootstrapBlog();
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", startBlog, { once: true });
} else {
startBlog();
}
</script>
</head>
<body class="blog-body" data-ds="new">
<div
id="app"
class="blog-app-root mx-auto my-8 w-full max-w-none px-4 md:w-blog-fluid md:max-w-blog-shell md:px-0"
data-ds="new"
></div>
<script>
window.wnjParams = {
position: "bottom",
accent: "neutral",
startHidden: true,
compactMode: true,
};
</script>
<div id="uiOverlay" data-overlay-root aria-hidden="true"></div>
</body>
</html>
+7 -1
View File
@@ -168,7 +168,13 @@
/>
<span class="sidebar-text">Roadmap</span>
</a>
<a href="#view=blog" class="sidebar-nav-link sidebar-nav-link--sub" title="Blog">
<a
href="#"
data-blog-link
class="sidebar-nav-link sidebar-nav-link--sub"
title="Blog"
rel="noopener noreferrer"
>
<img
src="assets/svg/blog-icon.svg"
alt=""
+10
View File
@@ -44,6 +44,16 @@ export const ADMIN_SUPER_NPUB =
*/
export const BITVID_WEBSITE_URL = "https://bitvid.network/";
/**
* External destination for the blog menu link.
*
* Sidebar navigation routes visitors to this URL when they select "Blog".
* Update the value to point at the long-form publishing platform you
* maintain for release notes or community updates.
*/
export const BLOG_URL =
"https://habla.news/p/npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
/**
* Default image included in automated moderation DMs.
*
+28
View File
@@ -8,6 +8,7 @@ import {
PLATFORM_FEE_PERCENT,
PLATFORM_LUD16_OVERRIDE,
THEME_ACCENT_OVERRIDES,
BLOG_URL,
} from "./instance-config.js";
function assertSuperAdminConfigured(value) {
@@ -111,9 +112,35 @@ function resolveConfig(overrides = {}) {
overrides.PLATFORM_LUD16_OVERRIDE ?? PLATFORM_LUD16_OVERRIDE,
themeAccentOverrides:
overrides.THEME_ACCENT_OVERRIDES ?? THEME_ACCENT_OVERRIDES,
blogUrl: overrides.BLOG_URL ?? BLOG_URL,
};
}
function assertBlogUrl(url) {
if (typeof url !== "string") {
throw new Error("BLOG_URL must be a string.");
}
const trimmed = url.trim();
if (!trimmed) {
throw new Error("BLOG_URL cannot be empty. Provide a fully qualified URL.");
}
try {
const parsed = new URL(trimmed);
if (!parsed.protocol || !/^https?:$/.test(parsed.protocol)) {
throw new Error("BLOG_URL must use http or https.");
}
} catch (error) {
throw new Error(
"BLOG_URL must be a valid http(s) URL defined in config/instance-config.js.",
{ cause: error },
);
}
return trimmed;
}
export function validateInstanceConfig(overrides) {
const config = resolveConfig(overrides);
assertSuperAdminConfigured(config.adminSuperNpub);
@@ -131,4 +158,5 @@ export function validateInstanceConfig(overrides) {
}
assertThemeAccentOverrides(config.themeAccentOverrides);
assertBlogUrl(config.blogUrl);
}
+2 -2
View File
@@ -1,2 +1,2 @@
:root{--surface-base:var(--color-surface);--surface-alt:var(--color-surface-alt);--surface-raised:var(--color-card);--surface-raised-private:var(--color-card-private);--surface-raised-critical:var(--color-card-critical);--surface-panel:var(--color-panel);--surface-panel-hover:var(--color-panel-hover);--surface-sunken:var(--color-surface-sunken);--surface-muted:var(--color-surface-muted);--surface-veil:var(--color-overlay-veil);--surface-inverse:var(--color-page);--surface-overlay:var(--color-overlay);--surface-overlay-muted:var(--color-overlay-muted);--surface-overlay-strong:var(--color-overlay-strong);--surface-overlay-darker:var(--color-overlay-darker);--surface-overlay-black:var(--color-overlay-black);--surface-overlay-banner:var(--surface-overlay-banner-bottom);--surface-overlay-banner-bottom:var(--color-overlay-banner-bottom);--surface-overlay-banner-transparent:var(--color-overlay-banner-transparent);--surface-overlay-banner-gradient:linear-gradient(to top,var(--surface-overlay-banner-bottom) 0%,var(--surface-overlay-banner-transparent) 60%,var(--surface-overlay-banner-transparent) 100%);--surface-overlay-panel:var(--color-overlay-panel);--surface-overlay-panel-soft:var(--color-overlay-panel-soft);--surface-overlay-panel-tint:var(--color-overlay-panel-tint);--surface-overlay-panel-glass:var(--color-overlay-panel-glass);--surface-overlay-panel-glass-soft:var(--color-overlay-panel-glass-soft);--surface-modal-backdrop:var(--color-modal-backdrop);--surface-modal:var(--color-modal-surface);--blur-modal-backdrop:18px;--blur-modal-alert:14px;--blur-thumbnail-blur:18px;--modal-nav-backdrop-blur:6px;--border-default:var(--color-border);--border-subtle:var(--color-border-subtle);--border-strong:var(--color-border-translucent-stronger);--border-overlay:var(--color-border-translucent);--border-overlay-strong:var(--color-border-translucent-strongest);--border-overlay-glow:var(--color-border-translucent-glow);--border-neutral-soft:var(--color-border-neutral-soft);--border-info-soft:var(--color-border-info-soft);--border-sidebar-strong:var(--color-border-sidebar-strong);--border-width-hairline:1px;--border-width-fine:0.5px;--text-primary:var(--color-text);--text-inverse:var(--color-text-inverse);--text-muted:var(--color-text-muted);--text-muted-strong:var(--color-muted-strong);--text-subtle:var(--color-subtle-text);--text-frosted-primary:var(--color-text-frosted);--text-frosted-strong:var(--color-text-frosted-strong);--text-frosted-soft:var(--color-text-frosted-soft);--status-info:var(--color-info);--status-info-strong:var(--color-info-strong);--status-info-surface:var(--color-info-glass);--status-info-border:var(--color-info-border);--status-info-on:var(--color-info-on);--status-success:var(--color-success);--status-success-strong:var(--color-success-strong);--status-success-surface:var(--color-success-surface);--status-success-border:var(--color-success-border);--status-success-on:var(--color-success-on);--status-warning:var(--color-warning);--status-warning-strong:var(--color-warning-strong);--status-warning-surface:var(--color-warning-surface);--status-warning-border:var(--color-warning-border);--status-warning-on:var(--color-warning-on);--status-danger:var(--color-critical);--status-danger-strong:var(--color-critical-strong);--status-danger-surface:var(--color-danger-surface);--status-danger-border:var(--color-danger-border);--status-danger-on:var(--color-danger-on);--status-private:var(--color-private);--status-private-strong:var(--color-private-strong);--status-private-surface:var(--color-private-surface);--status-private-border:var(--color-private-border);--status-private-on:var(--color-private-on);--status-neutral:var(--color-muted);--status-neutral-strong:var(--color-muted-strong);--status-neutral-surface:var(--color-neutral-surface);--status-neutral-border:var(--color-neutral-border);--status-neutral-on:var(--color-neutral-on);--scrollbar-width:0.5rem;--scrollbar-track:var(--color-white-soft);--scrollbar-thumb:var(--color-white-softer);--scrollbar-thumb-hover:var(--color-white-softest);--z-overlay-nav:60;--z-overlay-floating:70;--z-overlay-popover:140;--z-overlay-toast:180;--z-sidebar-overlay:10;--z-sidebar:20;--z-modal-root:100;--z-modal-overlay:110;--z-modal-content:120;--z-modal-player:130;--overlay-backdrop-color:var(--color-overlay-backdrop);--overlay-backdrop-blur:12px;--sidebar-width-expanded:16rem;--sidebar-width-collapsed:4rem;--sidebar-width:var(--sidebar-width-expanded);--sidebar-control-size:2.75rem;--sidebar-content-gap:1rem;--space-sidebar-shell-inline:var(--space-lg);--space-sidebar-shell-block-start:var(--space-lg);--space-sidebar-shell-block-end:1.75rem;--space-sidebar-scroll-gap:1.75rem;--space-sidebar-panel-gap:1.35rem;--space-sidebar-panel-padding-block:1.35rem;--space-sidebar-panel-padding-inline:1.1rem;--space-sidebar-footer-gap:0.85rem;--space-sidebar-trigger-padding-block:0.78rem;--space-sidebar-popover-offset:0.85rem;--space-sidebar-popover-padding-block:0.85rem;--space-sidebar-pLine 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{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}html{font-size:100%}@media (min-width:1024px){html{font-size:75%}}*,:after,:before{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) transparent}::-webkit-scrollbar{width:var(--scrollbar-width);height:var(--scrollbar-width)}::-webkit-scrollbar-track{background:var(--scrollbar-track)}::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);border-radius:var(--radius-sm)}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover)}body{font-family:var(--font-family-sans);background-color:var(--color-page);color:var(--color-text-strong);line-height:1.5;margin:0;padding:0;overflow-x:hidden}#uiOverlay{position:fixed;inset:0;pointer-events:none;z-index:var(--z-overlay-popover)}#uiOverlay>*{pointer-events:auto}#editNip71TagsFieldset>legend,#nip71TagsFieldset>legend{margin-left:0;padding:0}#app{background-color:var(--color-page);color:inherit}body.modal-open,html.modal-open{overflow:hidden;height:100%}body.modal-open #app{overflow:hidden}body.modal-open #sidebar{pointer-events:none;opacity:0;visibility:hidden;z-index:var(--z-sidebar-overlay)}.ng-cloak,.ng-hide:not(.ng-hide-animate),.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}ng\:form{display:block}.\!container{width:100%!important}.container{width:100%}@media (min-width:25rem){.\!container{max-width:25rem!important}.container{max-width:25rem}}@media (min-width:30rem){.\!container{max-width:30rem!important}.container{max-width:30rem}}@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}}#playerModal.ds-header{display:flex}.ds-header{display:flex;align-items:center;justify-content:flex-start;padding-top:var(--space-sm);padding-bottom:var(--space-sm)}.ds-metric-probe{position:absolute;top:0;left:0;visibility:hidden;pointer-events:none;contain:layout size style;inline-size:0;block-size:var(--ds-metric-probe-length,0);overflow:hidden}.ds-header__logo{height:var(--space-2xl-plus);width:auto;max-width:none}.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-offsLine truncated
:root{--surface-base:var(--color-surface);--surface-alt:var(--color-surface-alt);--surface-raised:var(--color-card);--surface-raised-private:var(--color-card-private);--surface-raised-critical:var(--color-card-critical);--surface-panel:var(--color-panel);--surface-panel-hover:var(--color-panel-hover);--surface-sunken:var(--color-surface-sunken);--surface-muted:var(--color-surface-muted);--surface-veil:var(--color-overlay-veil);--surface-inverse:var(--color-page);--surface-overlay:var(--color-overlay);--surface-overlay-muted:var(--color-overlay-muted);--surface-overlay-strong:var(--color-overlay-strong);--surface-overlay-darker:var(--color-overlay-darker);--surface-overlay-black:var(--color-overlay-black);--surface-overlay-banner:var(--surface-overlay-banner-bottom);--surface-overlay-banner-bottom:var(--color-overlay-banner-bottom);--surface-overlay-banner-transparent:var(--color-overlay-banner-transparent);--surface-overlay-banner-gradient:linear-gradient(to top,var(--surface-overlay-banner-bottom) 0%,var(--surface-overlay-banner-transparent) 60%,var(--surface-overlay-banner-transparent) 100%);--surface-overlay-panel:var(--color-overlay-panel);--surface-overlay-panel-soft:var(--color-overlay-panel-soft);--surface-overlay-panel-tint:var(--color-overlay-panel-tint);--surface-overlay-panel-glass:var(--color-overlay-panel-glass);--surface-overlay-panel-glass-soft:var(--color-overlay-panel-glass-soft);--surface-modal-backdrop:var(--color-modal-backdrop);--surface-modal:var(--color-modal-surface);--blur-modal-backdrop:18px;--blur-modal-alert:14px;--blur-thumbnail-blur:18px;--modal-nav-backdrop-blur:6px;--border-default:var(--color-border);--border-subtle:var(--color-border-subtle);--border-strong:var(--color-border-translucent-stronger);--border-overlay:var(--color-border-translucent);--border-overlay-strong:var(--color-border-translucent-strongest);--border-overlay-glow:var(--color-border-translucent-glow);--border-neutral-soft:var(--color-border-neutral-soft);--border-info-soft:var(--color-border-info-soft);--border-sidebar-strong:var(--color-border-sidebar-strong);--border-width-hairline:1px;--border-width-fine:0.5px;--text-primary:var(--color-text);--text-inverse:var(--color-text-inverse);--text-muted:var(--color-text-muted);--text-muted-strong:var(--color-muted-strong);--text-subtle:var(--color-subtle-text);--text-frosted-primary:var(--color-text-frosted);--text-frosted-strong:var(--color-text-frosted-strong);--text-frosted-soft:var(--color-text-frosted-soft);--status-info:var(--color-info);--status-info-strong:var(--color-info-strong);--status-info-surface:var(--color-info-glass);--status-info-border:var(--color-info-border);--status-info-on:var(--color-info-on);--status-success:var(--color-success);--status-success-strong:var(--color-success-strong);--status-success-surface:var(--color-success-surface);--status-success-border:var(--color-success-border);--status-success-on:var(--color-success-on);--status-warning:var(--color-warning);--status-warning-strong:var(--color-warning-strong);--status-warning-surface:var(--color-warning-surface);--status-warning-border:var(--color-warning-border);--status-warning-on:var(--color-warning-on);--status-danger:var(--color-critical);--status-danger-strong:var(--color-critical-strong);--status-danger-surface:var(--color-danger-surface);--status-danger-border:var(--color-danger-border);--status-danger-on:var(--color-danger-on);--status-private:var(--color-private);--status-private-strong:var(--color-private-strong);--status-private-surface:var(--color-private-surface);--status-private-border:var(--color-private-border);--status-private-on:var(--color-private-on);--status-neutral:var(--color-muted);--status-neutral-strong:var(--color-muted-strong);--status-neutral-surface:var(--color-neutral-surface);--status-neutral-border:var(--color-neutral-border);--status-neutral-on:var(--color-neutral-on);--scrollbar-width:0.5rem;--scrollbar-track:var(--color-white-soft);--scrollbar-thumb:var(--color-white-softer);--scrollbar-thumb-hover:var(--color-white-softest);--z-overlay-nav:60;--z-overlay-floating:70;--z-overlay-popover:140;--z-overlay-toast:180;--z-sidebar-overlay:10;--z-sidebar:20;--z-modal-root:100;--z-modal-overlay:110;--z-modal-content:120;--z-modal-player:130;--overlay-backdrop-color:var(--color-overlay-backdrop);--overlay-backdrop-blur:12px;--sidebar-width-expanded:16rem;--sidebar-width-collapsed:4rem;--sidebar-width:var(--sidebar-width-expanded);--sidebar-control-size:2.75rem;--sidebar-content-gap:1rem;--space-sidebar-shell-inline:var(--space-lg);--space-sidebar-shell-block-start:var(--space-lg);--space-sidebar-shell-block-end:1.75rem;--space-sidebar-scroll-gap:1.75rem;--space-sidebar-panel-gap:1.35rem;--space-sidebar-panel-padding-block:1.35rem;--space-sidebar-panel-padding-inline:1.1rem;--space-sidebar-footer-gap:0.85rem;--space-sidebar-trigger-padding-block:0.78rem;--space-sidebar-popover-offset:0.85rem;--space-sidebar-popover-padding-block:0.85rem;--space-sidebar-pLine 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{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}html{font-size:100%}@media (min-width:1024px){html{font-size:75%}}*,:after,:before{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) transparent}::-webkit-scrollbar{width:var(--scrollbar-width);height:var(--scrollbar-width)}::-webkit-scrollbar-track{background:var(--scrollbar-track)}::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);border-radius:var(--radius-sm)}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover)}body{font-family:var(--font-family-sans);background-color:var(--color-page);color:var(--color-text-strong);line-height:1.5;margin:0;padding:0;overflow-x:hidden}#uiOverlay{position:fixed;inset:0;pointer-events:none;z-index:var(--z-overlay-popover)}#uiOverlay>*{pointer-events:auto}#editNip71TagsFieldset>legend,#nip71TagsFieldset>legend{margin-left:0;padding:0}#app{background-color:var(--color-page);color:inherit}body.modal-open,html.modal-open{overflow:hidden;height:100%}body.modal-open #app{overflow:hidden}body.modal-open #sidebar{pointer-events:none;opacity:0;visibility:hidden;z-index:var(--z-sidebar-overlay)}.ng-cloak,.ng-hide:not(.ng-hide-animate),.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}ng\:form{display:block}.\!container{width:100%!important}.container{width:100%}@media (min-width:25rem){.\!container{max-width:25rem!important}.container{max-width:25rem}}@media (min-width:30rem){.\!container{max-width:30rem!important}.container{max-width:30rem}}@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}}#playerModal.ds-header{display:flex}.ds-header{display:flex;align-items:center;justify-content:flex-start;padding-top:var(--space-sm);padding-bottom:var(--space-sm)}.ds-metric-probe{position:absolute;top:0;left:0;visibility:hidden;pointer-events:none;contain:layout size style;inline-size:0;block-size:var(--ds-metric-probe-length,0);overflow:hidden}.ds-header__logo{height:var(--space-2xl-plus);width:auto;max-width:none}.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-offsLine truncated
+145 -1110
View File
File diff suppressed because it is too large. Load diff
-101
View File
@@ -153,9 +153,6 @@
--revert-modal-sidebar-min: 240px;
--revert-modal-sidebar-width: 280px;
--popover-inline-safe-max: calc(100vw - var(--space-xl));
--blog-layout-fluid-width: 80%;
--blog-layout-max-width: 47.5rem;
--blog-embed-height: auto;
/* Progress primitives */
--progress-height-sm: 0.25rem;
@@ -166,52 +163,6 @@
--progress-transition-duration: var(--motion-duration-fast);
--progress-transition-ease: var(--motion-ease-out);
/* Blog layout */
--blog-font-size-root: 1.25rem;
--blog-font-size-display: 3rem;
--blog-font-size-title: 1.1rem;
--blog-font-size-body: 0.9rem;
--blog-font-size-meta: 0.8rem;
--blog-font-size-h1-mobile: 1.6rem;
--blog-space-compact: 0.3rem;
--blog-space-snug: 0.4rem;
--blog-space-stack-xxl: 4rem;
--blog-space-inline-lg: 2.5rem;
--blog-space-media-stack-start: 2.5rem;
--blog-space-media-stack-end: 3.5rem;
--blog-space-heading-stack: 1.4rem;
--blog-space-heading-gap: 0.6rem;
--blog-avatar-size: 2.5rem;
--blog-avatar-size-compact: 1.5rem;
--blog-line-height-body: 1.3rem;
--blog-line-height-tight: 0.9;
--ztr-font: var(--font-family-sans);
--ztr-font-size: var(--font-size-base);
--ztr-text-color: var(--color-blog-gray-800);
--ztr-link-color: var(--color-blog-purple-700);
--ztr-icon-color: var(--color-blog-gray-700);
--ztr-background-color: var(--color-blog-background);
--ztr-login-button-color: var(--color-blog-gray-800);
/* Blog carousel tokens */
--blog-carousel-gap: var(--space-lg);
--blog-carousel-slide-radius: var(--radius-xl);
--blog-carousel-slide-min-height: 18rem;
--blog-carousel-slide-background: var(--color-blog-neutral-50);
--blog-carousel-slide-foreground: var(--color-blog-gray-900);
--blog-carousel-slide-scale-inactive: 0.96;
--blog-carousel-progress-height: var(--progress-height-sm);
--blog-carousel-progress-radius: var(--progress-radius);
--blog-carousel-progress-track: var(--color-border-translucent);
--blog-carousel-progress-active: var(--color-blog-purple-500);
--blog-loader-border-width: 6px;
--blog-loader-dimension-lg: 92px;
--blog-loader-dimension-md: 64px;
--blog-theme-toggle-shadow-offset: 6px;
--blog-theme-toggle-shadow: inset calc(var(--blog-theme-toggle-shadow-offset) * -1)
calc(var(--blog-theme-toggle-shadow-offset) * -1)
var(--border-width-hairline) var(--border-width-hairline) var(--color-card);
/* Spacing scale */
--space-4xs: 0.0625rem;
--space-3xs: 0.125rem;
@@ -257,8 +208,6 @@
--status-log-min-height: 5rem;
--layout-player-max-width: 68.75rem;
--layout-docs-max-width: 92.5rem;
--layout-blog-max-width: 46.875rem;
--blog-top-note-image-height: 12.5rem;
--icon-size-sm: var(--font-size-lg);
--icon-size-md: var(--space-md-plus);
--icon-size-lg: var(--space-xl-plus);
@@ -363,56 +312,6 @@
--motion-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--modal-motion-duration: var(--motion-duration-base);
}
:root[data-theme="light"],
:root[data-theme="dark"],
:root:not([data-theme]) {
/* Blog palette (theme-agnostic color values) */
--color-blog-transparent: rgb(0 0 0 / 0);
--color-blog-cyan-500: rgb(6 182 212);
--color-blog-sky-500: rgb(0 187 255);
--color-blog-teal-600: rgb(14 116 144);
--color-blog-green-800: rgb(20 83 45);
--color-blog-green-700: rgb(21 128 61);
--color-blog-teal-700: rgb(22 78 99);
--color-blog-gray-900: rgb(23 23 23);
--color-blog-coffee-900: rgb(28 25 23);
--color-blog-green-500: rgb(34 197 94);
--color-blog-gray-800: rgb(43 43 43);
--color-blog-gray-700: rgb(59 59 59);
--color-blog-teal-accent: rgb(62 139 138);
--color-blog-gray-650: rgb(64 64 64);
--color-blog-brown-700: rgb(68 64 60);
--color-blog-purple-900: rgb(88 28 135);
--color-blog-gray-500: rgb(102 102 102);
--color-blog-gray-450: rgb(115 115 115);
--color-blog-gray-440: rgb(116 116 116);
--color-blog-brown-500: rgb(120 113 108);
--color-blog-rust-700: rgb(124 45 18);
--color-blog-purple-700: rgb(126 34 206);
--color-blog-purple-accent: rgb(146 55 156);
--color-blog-purple-muted: rgb(150 96 156);
--color-blog-gray-300: rgb(153 153 153);
--color-blog-purple-500: rgb(168 85 247);
--color-blog-gray-275: rgb(169 169 169);
--color-blog-red-600: rgb(185 28 28);
--color-blog-rust-600: rgb(194 65 12);
--color-blog-error: rgb(202 23 23);
--color-blog-gray-220: rgb(203 203 203);
--color-blog-gray-210: rgb(204 204 204);
--color-blog-gray-200: rgb(222 222 222);
--color-blog-amber-500: rgb(228 152 77);
--color-blog-gray-190: rgb(224 224 224);
--color-blog-orange-600: rgb(227 84 40);
--color-blog-gray-180: rgb(233 233 233);
--color-blog-gray-170: rgb(235 235 235);
--color-blog-red-500: rgb(239 68 68);
--color-blog-amber-400: rgb(246 173 85);
--color-blog-neutral-100: rgb(245 245 245);
--color-blog-neutral-120: rgb(246 246 246);
--color-blog-orange-500: rgb(249 115 22);
--color-blog-neutral-50: rgb(253 253 253);
--color-blog-background: rgb(0 0 0 / 0.03);
}
:root[data-theme="light"] {
/* Light theme palette */
--color-page: rgb(247 248 252);
+2 -50
View File
@@ -57,7 +57,7 @@ Run `npm run lint:tailwind-brackets` after touching markup or prose that embeds
sources and fails when Tailwind's bracket syntax introduces hard-coded measurements (`text-[11px]`, `max-w-[18rem]`, etc.).
- Prefer the semantic utilities shipped alongside the tokens (`max-w-popover-safe`, `max-h-modal-shell`, `text-3xs`).
- When you truly need an arbitrary value, drive it with a design token by referencing the CSS custom property (`w-[var(--blog-layout-fluid-width)]`, `min-h-[var(--form-textarea-min-height)]`).
- When you truly need an arbitrary value, drive it with a design token by referencing the CSS custom property (`w-[var(--layout-player-max-width)]`, `min-h-[var(--form-textarea-min-height)]`).
- Update the allowlist sparingly—every entry should correspond to a documented token so future adjustments cascade automatically.
The lint shares the same underlying script as `npm run lint:tokens`, so both commands stay in lockstep with the design-system
@@ -82,20 +82,6 @@ Tokens are sourced from `css/tokens.css` and consumed as CSS custom properties.
Limit overrides to variables defined in `css/tokens.css` and document any long-lived changes in this file so the design tokens remain the source of truth. Feature-flagged experiments should wrap overrides in a `data-flag-<name>` selector or toggle them via controller logic so the defaults remain stable when the flag is off.
### Blog layout tokens
The blog embed now consumes dedicated typography and spacing tokens alongside Tailwind utilities. `css/tokens.css` exposes the following helpers:
- `--blog-font-size-root`, `--blog-font-size-display`, `--blog-font-size-title`, `--blog-font-size-body`, `--blog-font-size-meta`, and `--blog-font-size-h1-mobile` scale copy across the blog shell, hero headings, and metadata.
- `--blog-line-height-body` and `--blog-line-height-tight` set the rhythm for long-form text and compact headers.
- `--blog-layout-fluid-width` and `--blog-layout-max-width` define the responsive container width; pair them with Tailwind utilities (`mx-auto`, `md:w-[var(--blog-layout-fluid-width)]`, `md:max-w-[var(--blog-layout-max-width)]`) when mounting the blog root.
- `--blog-space-compact`, `--blog-space-snug`, `--blog-space-heading-stack`, `--blog-space-heading-gap`, `--blog-space-inline-lg`, `--blog-space-media-stack-start`, `--blog-space-media-stack-end`, and `--blog-space-stack-xxl` standardise the bespoke gaps used throughout article summaries, media blocks, and footer treatments.
- `--blog-avatar-size` and `--blog-avatar-size-compact` size topic avatars responsively.
Templates should reference these tokens (directly or via Tailwind arbitrary values) instead of hard-coded measurements so the embed stays in sync with future blog refreshes.
The blog palette now resolves from the base `:root` scope, making color and layout tokens theme-agnostic. Neither the light nor dark theme overrides these variables, so any future blog-specific deltas should be called out explicitly in this guide if they return.
## Semantic Palette
All interactive surfaces now draw from a shared semantic color map exposed through `css/tokens.css` and Tailwind utility aliases. The palette is grouped into text, surface, overlay, and status slots so controllers can swap state styles without reaching for raw Tailwind hues:
@@ -112,43 +98,9 @@ All interactive surfaces now draw from a shared semantic color map exposed throu
- Use `text-info`/`hover:text-info-strong` for links that previously relied on `text-blue-400` so anchors adapt in both light and dark modes.
- When rendering tinted descriptions or placeholders, prefer `text-muted`, `text-muted-strong`, or `text-subtle` over semi-transparent whites. Token overrides keep the same classes usable across the default and light themes.
### Blog carousel markup
The short-notes carousel ships as a semantic wrapper around the `blog-carousel` primitive defined in `css/tailwind.source.css`. The runtime no longer injects inline styles or Splide classes—layout is driven entirely by data attributes that map onto tokenised CSS rules.
**Structure**
```html
<section class="short-notes blog-carousel" data-carousel data-per-page="3" data-index="0">
<div class="blog-carousel__viewport" data-carousel-viewport>
<ul class="blog-carousel__track" data-carousel-track>
<li class="blog-carousel__slide" data-carousel-slide data-index="0" data-active="true">
...
</li>
<!-- additional slides -->
</ul>
<button type="button" data-carousel-prev data-carousel-control="prev">‹</button>
<button type="button" data-carousel-next data-carousel-control="next">›</button>
</div>
<div class="blog-carousel__progress blog-progress" data-carousel-progress>
<progress class="progress progress--blog" data-progress-meter aria-label="Carousel progress"></progress>
</div>
</section>
```
**State contract**
- `data-per-page` controls how many columns render inside the viewport. CSS exposes variants for 1–4 slides per page; the custom property `--blog-carousel-per-page` updates automatically.
- `data-index` represents the active *page* (zero-indexed). The track translates by `100%` for each page, so the attribute should advance in whole-number increments rather than slide indices.
- Slides expose `data-index`, `data-active`, `data-inert`, and `data-state`. The carousel controller toggles `data-state="active"`, `"idle"`, and `"leaving"` to drive scale/opacity transitions without touching inline styles.
- The root `section` advertises `data-state="active" | "idle"` plus `data-transition="auto" | "instant"` when short-circuiting animations (for example, during first render or programmatic jumps).
- Progress wrappers set `data-state="idle" | "active" | "paused" | "complete"` while the inner `<progress>` leverages the shared `progress` primitive (`data-progress-meter`, `data-variant="blog"`) so theme tokens continue to drive track/fill colours.
Controllers should rely on these data hooks when orchestrating autoplay, pausing on pointer/visibility changes, or syncing the segmented progress indicator. Avoid reintroducing inline `style` mutations—tests enforce the contract via the inline-style guard.
### Layout & spacing extensions
The core spacing scale gained intermediate stops for tighter micro-layouts and larger hero treatments. `css/tokens.css` now exports `--space-4xs`, `--space-3xs`, `--space-xs-snug`, `--space-md-plus`, `--space-xl-compact`, `--space-xl-plus`, and `--space-2xl-plus`; Tailwind surfaces matching utilities such as `p-4xs`, `p-3xs`, `px-xs-snug`, `py-md-plus`, `gap-xl-compact`, `px-xl-plus`, and `gap-2xl-plus`. Pair them with the new layout primitives—`--menu-min-width` (`min-w-menu`), `--modal-max-width` (`max-w-modal`), `--layout-player-max-width` (`max-w-player`), and `--layout-docs-max-width` (`max-w-docs`)—to anchor consistent breakpoints across popovers, modals, and documentation shells without reintroducing raw `rem`/`px` literals. Blog-specific chrome reuses the same additions: the theme toggle knob now references `--radius-toggle-thumb`, and focus affordances draw from `--outline-thick-width` instead of hard-coded pixel outlines.
The core spacing scale gained intermediate stops for tighter micro-layouts and larger hero treatments. `css/tokens.css` now exports `--space-4xs`, `--space-3xs`, `--space-xs-snug`, `--space-md-plus`, `--space-xl-compact`, `--space-xl-plus`, and `--space-2xl-plus`; Tailwind surfaces matching utilities such as `p-4xs`, `p-3xs`, `px-xs-snug`, `py-md-plus`, `gap-xl-compact`, `px-xl-plus`, and `gap-2xl-plus`. Pair them with the new layout primitives—`--menu-min-width` (`min-w-menu`), `--modal-max-width` (`max-w-modal`), `--layout-player-max-width` (`max-w-player`), and `--layout-docs-max-width` (`max-w-docs`)—to anchor consistent breakpoints across popovers, modals, and documentation shells without reintroducing raw `rem`/`px` literals.
### Previewing themes
-232
View File
@@ -1,232 +0,0 @@
import { userLogger } from "../utils/logger.js";
function toArray(value) {
if (!value) {
return [];
}
return Array.isArray(value) ? value : Array.from(value);
}
const SLIDE_STATE_IDLE = 'idle';
const SLIDE_STATE_ACTIVE = 'active';
const SLIDE_STATE_LEAVING = 'leaving';
const SLIDE_TRANSITION_TIMEOUT = 420;
function getRange(pageIndex, perPage) {
const start = pageIndex * perPage;
return { start, end: start + perPage };
}
function clearTimer(state, slide) {
const handle = state.timers.get(slide);
if (typeof handle === 'number') {
clearTimeout(handle);
}
state.timers.delete(slide);
}
function scheduleSlideIdle(state, slide) {
clearTimer(state, slide);
const handle = setTimeout(() => {
state.timers.delete(slide);
if (slide.dataset.active !== 'true') {
slide.dataset.state = SLIDE_STATE_IDLE;
}
}, SLIDE_TRANSITION_TIMEOUT);
state.timers.set(slide, handle);
}
function applySlideState(state, slide, { isActive, wasActive, immediate }) {
if (isActive) {
clearTimer(state, slide);
slide.dataset.active = 'true';
slide.dataset.state = SLIDE_STATE_ACTIVE;
slide.dataset.inert = 'false';
slide.setAttribute('aria-hidden', 'false');
return;
}
slide.dataset.active = 'false';
slide.dataset.inert = 'true';
slide.setAttribute('aria-hidden', 'true');
if (!immediate && wasActive) {
slide.dataset.state = SLIDE_STATE_LEAVING;
scheduleSlideIdle(state, slide);
return;
}
clearTimer(state, slide);
slide.dataset.state = SLIDE_STATE_IDLE;
}
function updateSlides(state, { previousIndex, immediate = false } = {}) {
const currentRange = getRange(state.index, state.perPage);
const previousRange =
typeof previousIndex === 'number' ? getRange(previousIndex, state.perPage) : currentRange;
state.slides.forEach((slide, slideIndex) => {
const isActive = slideIndex >= currentRange.start && slideIndex < currentRange.end;
const wasActive = slideIndex >= previousRange.start && slideIndex < previousRange.end;
applySlideState(state, slide, { isActive, wasActive, immediate });
});
}
function syncTransitionMode(container, track, mode) {
if (!container || !track) {
return;
}
if (mode) {
container.dataset.transition = mode;
track.dataset.transition = mode;
} else {
delete container.dataset.transition;
delete track.dataset.transition;
}
}
export function createCarousel(container, options = {}) {
const track = options.track || container?.querySelector('[data-carousel-track]');
const slides = options.slides || toArray(track?.children);
if (!container || !track || slides.length === 0) {
return null;
}
const perPage = Math.max(1, Math.min(options.perPage || 1, slides.length));
const pageCount = Math.max(1, Math.ceil(slides.length / perPage));
const state = {
container,
track,
slides,
perPage,
pageCount,
loop: Boolean(options.loop),
autoPlayInterval: Math.max(0, options.autoPlayInterval || 0),
index: 0,
progress: null,
cleanup: [],
timers: new Map(),
setProgress(progressInstance) {
this.progress = progressInstance || null;
},
};
container.dataset.count = String(pageCount);
container.dataset.perPage = String(perPage);
container.dataset.index = '0';
container.dataset.state = pageCount > 1 ? 'active' : 'idle';
syncTransitionMode(container, track, 'auto');
slides.forEach((slide, slideIndex) => {
slide.dataset.index = String(slideIndex);
slide.dataset.active = 'false';
slide.dataset.state = SLIDE_STATE_IDLE;
slide.dataset.inert = 'true';
slide.setAttribute('aria-hidden', 'true');
});
function handlePrev(event) {
event?.preventDefault?.();
advanceSlide(state, -1);
}
function handleNext(event) {
event?.preventDefault?.();
advanceSlide(state, 1);
}
if (options.controls?.prev) {
options.controls.prev.addEventListener('click', handlePrev);
state.cleanup.push(() => options.controls.prev.removeEventListener('click', handlePrev));
}
if (options.controls?.next) {
options.controls.next.addEventListener('click', handleNext);
state.cleanup.push(() => options.controls.next.removeEventListener('click', handleNext));
}
state.goTo = function goTo(targetPage, { restartProgress = true, instant = false } = {}) {
const maxIndex = state.pageCount - 1;
let nextIndex = targetPage;
if (nextIndex < 0) {
nextIndex = state.loop ? maxIndex : 0;
} else if (nextIndex > maxIndex) {
nextIndex = state.loop ? 0 : maxIndex;
}
const previousIndex = state.index;
state.index = nextIndex;
container.dataset.index = String(nextIndex);
syncTransitionMode(container, track, instant ? 'instant' : 'auto');
updateSlides(state, { previousIndex, immediate: instant });
if (instant) {
syncTransitionMode(container, track, 'auto');
}
if (restartProgress && state.autoPlayInterval > 0 && state.progress) {
state.progress.reset();
state.progress.start(state.autoPlayInterval);
}
};
state.goTo(0, { restartProgress: false, instant: true });
return state;
}
export function advanceSlide(state, step = 1, options = {}) {
if (!state || typeof state.goTo !== 'function') {
return;
}
state.goTo(state.index + step, options);
}
export function destroyCarousel(state) {
if (!state) {
return;
}
state.cleanup?.forEach((dispose) => {
try {
dispose();
} catch (error) {
userLogger.warn('Failed to dispose carousel listener', error);
}
});
state.cleanup = [];
for (const handle of state.timers.values()) {
clearTimeout(handle);
}
state.timers.clear();
if (state.progress && typeof state.progress.destroy === 'function') {
state.progress.destroy();
}
if (state.container) {
delete state.container.dataset.index;
delete state.container.dataset.count;
delete state.container.dataset.perPage;
delete state.container.dataset.state;
delete state.container.dataset.transition;
}
if (state.track) {
delete state.track.dataset.transition;
}
state.slides?.forEach((slide) => {
delete slide.dataset.active;
delete slide.dataset.state;
delete slide.dataset.inert;
slide.setAttribute('aria-hidden', 'false');
});
}
-1123
View File
File diff suppressed because it is too large. Load diff
-126
View File
@@ -1,126 +0,0 @@
export function mountProgressBar(
container,
{ initialDuration = 0, autoStart = false, onComplete } = {},
) {
if (!container) {
return null;
}
const wrapper = document.createElement('div');
wrapper.className = 'blog-carousel__progress blog-progress mt-2';
wrapper.dataset.state = 'idle';
wrapper.setAttribute('data-carousel-progress', 'true');
const meter = document.createElement('progress');
meter.className = 'progress progress--blog';
meter.max = 100;
meter.value = 0;
meter.dataset.progress = '0';
meter.dataset.state = 'idle';
meter.setAttribute('data-progress-meter', 'true');
meter.setAttribute('data-variant', 'blog');
meter.setAttribute('aria-label', 'Carousel progress');
meter.setAttribute('aria-valuetext', 'Carousel progress 0% complete');
wrapper.appendChild(meter);
container.appendChild(wrapper);
let duration = Math.max(0, initialDuration);
let rafId = null;
let startTimestamp = null;
let completeHandler = typeof onComplete === 'function' ? onComplete : null;
function cancelAnimation() {
if (rafId !== null) {
cancelAnimationFrame(rafId);
rafId = null;
}
}
function setState(state) {
wrapper.dataset.state = state;
meter.dataset.state = state;
}
function updateMeter(ratio) {
const clamped = Math.max(0, Math.min(1, ratio));
const percent = Math.round(clamped * 100);
meter.value = percent;
meter.dataset.progress = String(percent);
meter.setAttribute('aria-valuetext', `Carousel progress ${percent}% complete`);
return clamped;
}
function stop() {
cancelAnimation();
startTimestamp = null;
setState('paused');
}
function reset() {
stop();
updateMeter(0);
setState('idle');
}
function step(timestamp) {
if (startTimestamp === null) {
startTimestamp = timestamp;
}
const elapsed = timestamp - startTimestamp;
const ratio = duration > 0 ? Math.min(1, elapsed / duration) : 1;
const progress = updateMeter(ratio);
if (progress >= 1) {
cancelAnimation();
startTimestamp = null;
setState('complete');
if (completeHandler) {
completeHandler();
}
return;
}
setState('active');
rafId = requestAnimationFrame(step);
}
function start(nextDuration = duration) {
duration = Math.max(0, nextDuration);
reset();
if (duration === 0) {
updateMeter(1);
setState('complete');
if (completeHandler) {
completeHandler();
}
return;
}
setState('active');
rafId = requestAnimationFrame(step);
}
function destroy() {
cancelAnimation();
wrapper.remove();
}
function setOnComplete(handler) {
completeHandler = typeof handler === 'function' ? handler : null;
}
if (autoStart && duration > 0) {
start(duration);
} else {
reset();
}
return {
element: wrapper,
meter,
start,
stop,
reset,
destroy,
setOnComplete,
};
}
File diff suppressed because it is too large. Load diff
+2
View File
@@ -6,6 +6,7 @@ import {
ADMIN_SUPER_NPUB,
ADMIN_DM_IMAGE_URL,
BITVID_WEBSITE_URL,
BLOG_URL,
MAX_WALLET_DEFAULT_ZAP,
PLATFORM_FEE_PERCENT,
PLATFORM_LUD16_OVERRIDE,
@@ -43,6 +44,7 @@ export const ADMIN_LIST_MODE = "nostr";
export { ADMIN_SUPER_NPUB };
export { ADMIN_DM_IMAGE_URL };
export { BITVID_WEBSITE_URL };
export { BLOG_URL };
export { MAX_WALLET_DEFAULT_ZAP };
export { PLATFORM_FEE_PERCENT };
export { PLATFORM_LUD16_OVERRIDE };
+10
View File
@@ -19,6 +19,7 @@ import {
openStaticModal,
closeStaticModal,
} from "./ui/components/staticModalAccessibility.js";
import { BLOG_URL } from "./config.js";
validateInstanceConfig();
@@ -209,6 +210,15 @@ async function bootstrapInterface() {
await loadSidebar("components/sidebar.html", "sidebarContainer");
devLogger.log("Sidebar loaded.");
const blogLink = document.querySelector("[data-blog-link]");
if (blogLink instanceof HTMLAnchorElement) {
blogLink.href = BLOG_URL;
blogLink.target = "_blank";
blogLink.rel = "noopener noreferrer";
} else {
userLogger.warn("Sidebar blog link not found; skipping external link binding.");
}
const sidebar = document.getElementById("sidebar");
const collapseToggle = document.getElementById("sidebarCollapseToggle");
if (sidebar && !sidebar.hasAttribute("data-footer-state")) {
+1 -1
View File
@@ -9,7 +9,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, "..");
const TARGET_DIRECTORIES = ["js/blog", "js/ui", "torrent/ui"];
const TARGET_DIRECTORIES = ["js/ui", "torrent/ui"];
const TARGET_FILES = ["js/channelProfile.js", "css/tailwind.source.css"];
const IGNORED_DIRECTORIES = new Set(["dist", "vendor"]);
const IGNORED_EXTENSIONS = new Set([".min.js", ".map"]);
+2 -53
View File
@@ -3,7 +3,6 @@ const typography = require("@tailwindcss/typography");
module.exports = {
content: [
"./index.html",
"./blog.html",
"./components/**/*.html",
"./views/**/*.html",
"./js/**/*.js",
@@ -158,53 +157,7 @@ module.exports = {
700: "var(--color-neutral-700)"
},
white: "var(--color-white)",
black: "var(--color-black)",
blog: {
transparent: "var(--color-blog-transparent)",
"cyan-500": "var(--color-blog-cyan-500)",
"sky-500": "var(--color-blog-sky-500)",
"teal-600": "var(--color-blog-teal-600)",
"green-800": "var(--color-blog-green-800)",
"green-700": "var(--color-blog-green-700)",
"teal-700": "var(--color-blog-teal-700)",
"gray-900": "var(--color-blog-gray-900)",
"coffee-900": "var(--color-blog-coffee-900)",
"green-500": "var(--color-blog-green-500)",
"gray-800": "var(--color-blog-gray-800)",
"gray-700": "var(--color-blog-gray-700)",
"teal-accent": "var(--color-blog-teal-accent)",
"gray-650": "var(--color-blog-gray-650)",
"brown-700": "var(--color-blog-brown-700)",
"purple-900": "var(--color-blog-purple-900)",
"gray-500": "var(--color-blog-gray-500)",
"gray-450": "var(--color-blog-gray-450)",
"gray-440": "var(--color-blog-gray-440)",
"brown-500": "var(--color-blog-brown-500)",
"rust-700": "var(--color-blog-rust-700)",
"purple-700": "var(--color-blog-purple-700)",
"purple-accent": "var(--color-blog-purple-accent)",
"purple-muted": "var(--color-blog-purple-muted)",
"gray-300": "var(--color-blog-gray-300)",
"purple-500": "var(--color-blog-purple-500)",
"gray-275": "var(--color-blog-gray-275)",
"red-600": "var(--color-blog-red-600)",
"rust-600": "var(--color-blog-rust-600)",
error: "var(--color-blog-error)",
"gray-220": "var(--color-blog-gray-220)",
"gray-210": "var(--color-blog-gray-210)",
"gray-200": "var(--color-blog-gray-200)",
"amber-500": "var(--color-blog-amber-500)",
"gray-190": "var(--color-blog-gray-190)",
"orange-600": "var(--color-blog-orange-600)",
"gray-180": "var(--color-blog-gray-180)",
"gray-170": "var(--color-blog-gray-170)",
"red-500": "var(--color-blog-red-500)",
"amber-400": "var(--color-blog-amber-400)",
"neutral-100": "var(--color-blog-neutral-100)",
"neutral-120": "var(--color-blog-neutral-120)",
"orange-500": "var(--color-blog-orange-500)",
"neutral-50": "var(--color-blog-neutral-50)"
}
black: "var(--color-black)"
},
textColor: {
primary: "var(--text-primary)",
@@ -351,15 +304,11 @@ module.exports = {
menu: "var(--menu-min-width)",
"grid-card": "var(--grid-card-min-width)"
},
width: {
"blog-fluid": "var(--blog-layout-fluid-width)"
},
maxWidth: {
modal: "var(--modal-max-width)",
player: "var(--layout-player-max-width)",
docs: "var(--layout-docs-max-width)",
"popover-safe": "var(--popover-inline-safe-max)",
"blog-shell": "var(--blog-layout-max-width)"
"popover-safe": "var(--popover-inline-safe-max)"
},
translate: {
toast: "var(--toast-translate)"
-297
View File
@@ -1,297 +0,0 @@
import assert from 'node:assert/strict';
import { beforeEach, afterEach, describe, test } from 'node:test';
import { advanceSlide, createCarousel, destroyCarousel } from '../js/blog/carousel.js';
import { mountProgressBar } from '../js/blog/progressBar.js';
import { createBlogDomEnvironment, createSpy } from './test-helpers/blog-dom.mjs';
function assertHasClasses(element, classes, messagePrefix = 'element') {
classes.forEach((className) => {
assert.ok(
element.classList.contains(className),
`${messagePrefix} should include the “${className}” class`,
);
});
}
function assertNoInlineStyleAttributes(elements, messagePrefix = 'element') {
elements.forEach((element, index) => {
assert.ok(element, `${messagePrefix} at index ${index} should exist`);
assert.strictEqual(
element.hasAttribute('style'),
false,
`${messagePrefix} at index ${index} should not set inline style attributes`,
);
});
}
const CAROUSEL_HTML = `
<section class="blog-carousel" data-carousel>
<div data-carousel-viewport>
<ul data-carousel-track>
<li data-carousel-slide>Slide 1</li>
<li data-carousel-slide>Slide 2</li>
<li data-carousel-slide>Slide 3</li>
</ul>
</div>
<button type="button" data-carousel-prev>Prev</button>
<button type="button" data-carousel-next>Next</button>
</section>
`;
const PROGRESS_HTML = '<div id="progress-root"></div>';
describe('blog carousel', () => {
let env;
let container;
let track;
let slides;
let prevButton;
let nextButton;
beforeEach(() => {
env = createBlogDomEnvironment(CAROUSEL_HTML);
const doc = env.document;
container = doc.querySelector('[data-carousel]');
track = container.querySelector('[data-carousel-track]');
slides = Array.from(track.children);
prevButton = container.querySelector('[data-carousel-prev]');
nextButton = container.querySelector('[data-carousel-next]');
env.inlineStyleGuard.guard(container);
env.inlineStyleGuard.guard(track);
env.inlineStyleGuard.guard(prevButton);
env.inlineStyleGuard.guard(nextButton);
slides.forEach((slide) => env.inlineStyleGuard.guard(slide));
});
afterEach(() => {
env.cleanup();
});
test('initializes slides with data attributes and accessibility state', () => {
const state = createCarousel(container, {
perPage: 1,
loop: true,
controls: { prev: prevButton, next: nextButton },
});
assert.ok(state, 'state should be created when container markup is valid');
assert.strictEqual(state.perPage, 1);
assert.strictEqual(state.slides.length, 3);
assert.strictEqual(state.pageCount, 3);
assert.strictEqual(container.dataset.index, '0');
assert.strictEqual(container.dataset.perPage, '1');
assert.strictEqual(container.dataset.count, '3');
assert.strictEqual(container.dataset.state, 'active');
assert.strictEqual(track.dataset.transition, 'auto');
assertNoInlineStyleAttributes([container, track, ...slides], 'carousel element');
slides.forEach((slide, index) => {
assert.strictEqual(slide.dataset.index, String(index));
assert.strictEqual(slide.dataset.active, index === 0 ? 'true' : 'false');
assert.strictEqual(slide.dataset.inert, index === 0 ? 'false' : 'true');
assert.strictEqual(slide.getAttribute('aria-hidden'), index === 0 ? 'false' : 'true');
});
state.goTo(2);
assert.strictEqual(state.index, 2);
assert.strictEqual(container.dataset.index, '2');
assert.strictEqual(slides[2].dataset.active, 'true');
assert.strictEqual(slides[0].dataset.active, 'false');
assert.strictEqual(slides[2].getAttribute('aria-hidden'), 'false');
assert.strictEqual(slides[0].getAttribute('aria-hidden'), 'true');
destroyCarousel(state);
});
test('control buttons advance slides and respect looping', () => {
const state = createCarousel(container, {
perPage: 1,
loop: true,
controls: { prev: prevButton, next: nextButton },
});
nextButton.dispatchEvent(new env.window.MouseEvent('click', { bubbles: true }));
assert.strictEqual(state.index, 1);
nextButton.dispatchEvent(new env.window.MouseEvent('click', { bubbles: true }));
assert.strictEqual(state.index, 2);
nextButton.dispatchEvent(new env.window.MouseEvent('click', { bubbles: true }));
assert.strictEqual(state.index, 0, 'looping should wrap forward navigation to the first slide');
prevButton.dispatchEvent(new env.window.MouseEvent('click', { bubbles: true }));
assert.strictEqual(state.index, 2, 'looping should wrap backward navigation to the last slide');
destroyCarousel(state);
});
test('auto play progress resets when navigation occurs', () => {
const state = createCarousel(container, {
perPage: 1,
controls: { prev: prevButton, next: nextButton },
});
const events = [];
const progress = {
reset: () => events.push('reset'),
start: (duration) => events.push(['start', duration]),
destroy: () => events.push('destroy'),
};
state.setProgress(progress);
state.autoPlayInterval = 4200;
state.goTo(1);
assert.deepStrictEqual(events, ['reset', ['start', 4200]]);
events.length = 0;
state.goTo(2, { restartProgress: false });
assert.deepStrictEqual(events, [], 'progress should not restart when restartProgress is false');
destroyCarousel(state);
});
test('destroyCarousel tears down listeners and progress state', () => {
const state = createCarousel(container, {
perPage: 1,
controls: { prev: prevButton, next: nextButton },
});
const progressEvents = [];
const progress = {
reset: () => progressEvents.push('reset'),
start: () => progressEvents.push('start'),
destroy: () => progressEvents.push('destroy'),
};
state.setProgress(progress);
state.autoPlayInterval = 2000;
state.goTo(1);
destroyCarousel(state);
assert.ok(progressEvents.includes('destroy'), 'progress.destroy should be called during teardown');
assert.ok(!container.classList.contains('is-initialized'));
assert.ok(!container.classList.contains('is-active'));
assert.strictEqual(state.cleanup.length, 0);
assertNoInlineStyleAttributes([container, track, ...slides], 'carousel element');
const previousIndex = state.index;
nextButton.dispatchEvent(new env.window.MouseEvent('click', { bubbles: true }));
assert.strictEqual(state.index, previousIndex, 'clicks after destroy should not change the index');
assert.strictEqual(container.dataset.index, undefined);
});
test('advanceSlide delegates to goTo with relative offsets', () => {
const state = createCarousel(container, { perPage: 1 });
const goToSpy = createSpy();
state.goTo = goToSpy;
state.index = 2;
advanceSlide(state, -1, { restartProgress: false });
assert.strictEqual(goToSpy.callCount, 1);
assert.deepStrictEqual(goToSpy.calls[0], [1, { restartProgress: false }]);
});
});
describe('progress bar', () => {
let env;
let container;
beforeEach(() => {
env = createBlogDomEnvironment(PROGRESS_HTML);
container = env.document.getElementById('progress-root');
env.inlineStyleGuard.guard(container);
});
afterEach(() => {
env.cleanup();
});
test('mountProgressBar renders semantic progress meter without style injection', () => {
const progress = mountProgressBar(container, { initialDuration: 500, autoStart: false });
assert.ok(progress, 'progress instance should be returned');
const wrapper = container.querySelector('.blog-carousel__progress');
assert.ok(wrapper, 'wrapper element should be appended');
assertHasClasses(wrapper, ['blog-carousel__progress', 'blog-progress', 'mt-2'], 'progress wrapper');
assert.strictEqual(wrapper.dataset.carouselProgress, 'true');
assert.strictEqual(wrapper.getAttribute('data-carousel-progress'), 'true');
const meter = wrapper.querySelector('progress.progress--blog');
assert.ok(meter, 'progress meter should be created');
assertHasClasses(meter, ['progress', 'progress--blog'], 'progress meter');
assert.strictEqual(meter.max, 100, 'progress meter should expose a 0-100 range');
assert.strictEqual(meter.value, 0, 'progress meter should start at zero');
assert.strictEqual(meter.dataset.state, 'idle');
assert.strictEqual(wrapper.dataset.state, 'idle');
assert.strictEqual(meter.dataset.progress, '0');
assert.strictEqual(
meter.getAttribute('aria-valuetext'),
'Carousel progress 0% complete',
'progress meter should emit accessible status text',
);
assert.strictEqual(meter.getAttribute('data-progress-meter'), 'true');
assert.strictEqual(meter.dataset.progressMeter, 'true');
assert.strictEqual(meter.getAttribute('data-variant'), 'blog');
progress.reset();
assertNoInlineStyleAttributes([container, wrapper, meter], 'progress element');
});
test('autoStart runs animation and triggers completion callback', () => {
const onComplete = createSpy();
const progress = mountProgressBar(container, { initialDuration: 600, autoStart: true, onComplete });
const wrapper = progress.element;
const meter = wrapper.querySelector('progress.progress--blog');
assert.ok(meter, 'progress meter should exist when autoStart is enabled');
assertNoInlineStyleAttributes([container, wrapper, meter], 'progress element');
env.raf.step(0);
env.raf.step(300);
assert.strictEqual(onComplete.callCount, 0);
assert.ok(meter.value > 0 && meter.value < 100, 'meter should report intermediate progress');
assert.strictEqual(meter.dataset.state, 'active');
env.raf.step(300);
assert.strictEqual(onComplete.callCount, 1);
assert.strictEqual(meter.value, 100);
assert.strictEqual(meter.dataset.state, 'complete');
assert.strictEqual(meter.dataset.progress, '100');
});
test('manual controls support stop, restart, and cleanup', () => {
const onComplete = createSpy();
const progress = mountProgressBar(container, { initialDuration: 800, onComplete });
const meter = progress.element.querySelector('progress.progress--blog');
progress.start(800);
env.raf.step(0);
env.raf.step(400);
assert.strictEqual(meter.value, 50, 'halfway through should yield a 50% reading');
assert.strictEqual(meter.dataset.state, 'active');
progress.stop();
assert.strictEqual(meter.dataset.state, 'paused');
const pausedValue = meter.value;
env.raf.step(600);
assert.strictEqual(meter.value, pausedValue, 'progress should not advance after stop');
assert.strictEqual(onComplete.callCount, 0, 'stop should cancel pending completion');
progress.setOnComplete(onComplete);
progress.start(0);
assert.strictEqual(onComplete.callCount, 1, 'zero duration should fire completion immediately');
assert.strictEqual(meter.value, 100);
assert.strictEqual(meter.dataset.state, 'complete');
progress.destroy();
assert.strictEqual(container.children.length, 0, 'destroy should remove wrapper from DOM');
assert.strictEqual(
container.querySelector('[data-carousel-progress]'),
null,
'destroy should remove declarative progress markup',
);
assertNoInlineStyleAttributes([container], 'progress container');
});
});
-205
View File
@@ -1,205 +0,0 @@
import { JSDOM } from 'jsdom';
const DOM_GLOBALS = [
'window',
'document',
'HTMLElement',
'HTMLStyleElement',
'Element',
'Node',
'Event',
'MouseEvent',
'CustomEvent',
'KeyboardEvent',
'MutationObserver',
'getComputedStyle',
];
export function createSpy(implementation = () => {}) {
function spy(...args) {
spy.calls.push(args);
spy.callCount += 1;
return implementation(...args);
}
spy.calls = [];
spy.callCount = 0;
return spy;
}
export function createAnimationFrameStub() {
let now = 0;
let handle = 0;
const queue = new Map();
function requestAnimationFrame(callback) {
handle += 1;
const id = handle;
queue.set(id, callback);
return id;
}
function cancelAnimationFrame(id) {
queue.delete(id);
}
function step(elapsed = 0) {
now += elapsed;
const callbacks = Array.from(queue.entries());
queue.clear();
for (const [, callback] of callbacks) {
callback(now);
}
}
return {
requestAnimationFrame,
cancelAnimationFrame,
step,
get now() {
return now;
},
get pending() {
return queue.size;
},
};
}
export function installInlineStyleGuard(document) {
const originalCreateElement = document.createElement.bind(document);
const ElementConstructor = document.defaultView?.Element;
const guardedElements = new WeakSet();
const attributePatches = [];
if (ElementConstructor?.prototype) {
const prototype = ElementConstructor.prototype;
if (typeof prototype.setAttribute === 'function') {
const original = prototype.setAttribute;
prototype.setAttribute = function inlineStyleSafeSetAttribute(name, value) {
if (typeof name === 'string' && name.toLowerCase() === 'style') {
throw new Error(
'Inline style attributes are disabled in blog widget tests. Use CSS classes or data attributes instead.',
);
}
return original.call(this, name, value);
};
attributePatches.push(() => {
prototype.setAttribute = original;
});
}
if (typeof prototype.setAttributeNS === 'function') {
const original = prototype.setAttributeNS;
prototype.setAttributeNS = function inlineStyleSafeSetAttributeNS(namespace, name, value) {
if (typeof name === 'string' && name.toLowerCase() === 'style') {
throw new Error(
'Inline style attributes are disabled in blog widget tests. Use CSS classes or data attributes instead.',
);
}
return original.call(this, namespace, name, value);
};
attributePatches.push(() => {
prototype.setAttributeNS = original;
});
}
}
function guard(element) {
if (!element || typeof element !== 'object') {
return element;
}
if (guardedElements.has(element)) {
return element;
}
Object.defineProperty(element, 'style', {
get() {
throw new Error(
'Inline style access is disabled in blog widget tests. Use CSS classes or data attributes instead.',
);
},
set() {
throw new Error(
'Inline style assignment is disabled in blog widget tests. Use CSS classes or data attributes instead.',
);
},
configurable: true,
});
guardedElements.add(element);
if (element.children?.length) {
Array.from(element.children).forEach((child) => guard(child));
}
return element;
}
document.createElement = function createElementGuarded(...args) {
const element = originalCreateElement(...args);
guard(element);
return element;
};
return {
guard,
restore() {
document.createElement = originalCreateElement;
attributePatches.forEach((restorePatch) => {
try {
restorePatch();
} catch (error) {
// ignore failures to restore in tests
}
});
},
};
}
export function createBlogDomEnvironment(html) {
const dom = new JSDOM(html, { url: 'https://example.com' });
const { window } = dom;
const previousGlobals = new Map();
for (const key of DOM_GLOBALS) {
previousGlobals.set(key, globalThis[key]);
const nextValue = window[key];
if (typeof nextValue !== 'undefined') {
globalThis[key] = nextValue;
} else {
delete globalThis[key];
}
}
const inlineStyleGuard = installInlineStyleGuard(window.document);
const raf = createAnimationFrameStub();
previousGlobals.set('requestAnimationFrame', globalThis.requestAnimationFrame);
previousGlobals.set('cancelAnimationFrame', globalThis.cancelAnimationFrame);
globalThis.requestAnimationFrame = raf.requestAnimationFrame;
globalThis.cancelAnimationFrame = raf.cancelAnimationFrame;
window.requestAnimationFrame = raf.requestAnimationFrame;
window.cancelAnimationFrame = raf.cancelAnimationFrame;
return {
window,
document: window.document,
dom,
raf,
inlineStyleGuard,
cleanup() {
inlineStyleGuard.restore();
dom.window.close();
for (const [key, value] of previousGlobals) {
if (typeof value === 'undefined') {
delete globalThis[key];
} else {
globalThis[key] = value;
}
}
},
};
}
-129
View File
@@ -1,129 +0,0 @@
<!-- views/blog.html -->
<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="blog-embed-shell relative isolate overflow-hidden rounded-lg bg-surface-alt">
<iframe
id="blog-iframe"
src="https://beta.bitvid.network/blog.html"
class="w-full border-none"
></iframe>
<script type="module">
const iframe = document.querySelector('#blog-iframe');
if (iframe) {
const abortController = new AbortController();
const { signal } = abortController;
const targetOrigin = (() => {
try {
return new URL(iframe.src, window.location.href).origin;
} catch (error) {
return window.location.origin;
}
})();
const applyHeight = (height) => {
const numeric = Math.max(0, Math.round(Number(height)));
if (!Number.isFinite(numeric)) {
return;
}
iframe.style.height = `${numeric}px`;
iframe.style.removeProperty('min-height');
iframe.style.removeProperty('max-height');
};
let receivedFirstHeight = false;
let retryIntervalId = null;
const stopRequestLoop = () => {
if (retryIntervalId === null) {
return;
}
window.clearInterval(retryIntervalId);
retryIntervalId = null;
};
const startRequestLoop = () => {
if (retryIntervalId !== null) {
return;
}
retryIntervalId = window.setInterval(() => {
if (receivedFirstHeight || signal.aborted) {
stopRequestLoop();
return;
}
requestHeight();
}, 250);
};
const handleMessage = (event) => {
if (event.source !== iframe.contentWindow) {
return;
}
if (event.origin !== targetOrigin) {
return;
}
if (event.data?.type === 'bitvid-blog-resize') {
applyHeight(event.data.height);
receivedFirstHeight = true;
stopRequestLoop();
}
};
window.addEventListener('message', handleMessage, { signal });
const requestHeight = () => {
if (!iframe.contentWindow) {
return;
}
iframe.contentWindow.postMessage(
{ type: 'bitvid-blog-request-height' },
targetOrigin,
);
};
let hasStarted = false;
const runInitialRequests = () => {
if (hasStarted) {
return;
}
hasStarted = true;
requestHeight();
startRequestLoop();
if (signal.aborted) {
stopRequestLoop();
} else {
signal.addEventListener('abort', stopRequestLoop, { once: true });
}
};
iframe.addEventListener('load', () => {
receivedFirstHeight = false;
stopRequestLoop();
hasStarted = false;
runInitialRequests();
}, { signal });
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', runInitialRequests, {
once: true,
signal,
});
} else {
runInitialRequests();
}
window.addEventListener('pagehide', () => {
abortController.abort();
stopRequestLoop();
}, { once: true });
}
</script>
</div>
</div>
</article>
</section>