Merge pull request #534 from PR0M3TH3AN/codex/extend-css-with-semantic-color-tokens

feat: add semantic color palette and lint guard
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-14 15:18:25 -04:00
commit 19bec873bc
14 files changed
+639 -196

No files matched your search

+3 -2
View File
@@ -357,7 +357,7 @@
</p>
<div
id="profileHistoryFeatureBanner"
class="card hidden border border-amber-500/40 bg-amber-500/10 px-4 py-3 text-sm text-amber-100"
class="card hidden border border-status-warning-border bg-status-warning-surface px-4 py-3 text-sm text-status-warning-on"
role="status"
aria-live="polite"
></div>
@@ -424,7 +424,8 @@
></div>
<div
id="profileHistorySessionWarning"
class="hidden inline-flex items-center gap-2 rounded-full border border-amber-500/50 bg-amber-500/10 px-3 py-1 text-xs font-medium text-amber-100"
class="hidden pill"
data-variant="warning"
role="status"
aria-live="polite"
>
+1 -1
View File
@@ -1049,7 +1049,7 @@
<div id="cloudflareUploadSection" class="hidden">
<div class="space-y-8">
<p
class="rounded-md border border-amber-500/40 bg-amber-500/10 p-3 text-sm text-amber-300"
class="rounded-md border border-status-warning-border bg-status-warning-surface p-3 text-sm text-status-warning-on"
>
This feature is experimental and may not be working correctly.
</p>
+75 -7
View File
@@ -637,19 +637,87 @@
}
.badge {
@apply inline-flex items-center gap-1 rounded-full bg-panel px-3 py-1 text-xs font-semibold uppercase tracking-wide text-muted-strong;
}
.badge[data-variant="critical"] {
@apply bg-critical bg-opacity-20 text-critical-strong;
@apply inline-flex items-center gap-1 rounded-full border border-status-neutral-border bg-status-neutral-surface px-3 py-1 text-2xs font-semibold uppercase tracking-extra-wide text-status-neutral-on;
}
.badge[data-variant="info"] {
@apply bg-info bg-opacity-20 text-info-strong;
@apply border-status-info-border bg-status-info-surface text-status-info-on;
}
.badge[data-variant="success"] {
@apply border-status-success-border bg-status-success-surface text-status-success-on;
}
.badge[data-variant="warning"] {
@apply border-status-warning-border bg-status-warning-surface text-status-warning-on;
}
.badge[data-variant="critical"] {
@apply border-status-danger-border bg-status-danger-surface text-status-danger-on;
}
.badge[data-variant="private"] {
@apply border-status-private-border bg-status-private-surface text-status-private-on;
}
.badge[data-variant="neutral"] {
@apply bg-border bg-opacity-40 text-muted-strong;
@apply border-status-neutral-border bg-status-neutral-surface text-status-neutral-on;
}
.pill {
@apply inline-flex items-center gap-2 rounded-full border border-status-neutral-border bg-status-neutral-surface px-3 py-1 text-xs font-medium text-status-neutral-on;
}
.pill[data-size="compact"] {
@apply px-2 py-0.5 text-2xs;
}
.pill[data-variant="info"] {
@apply border-status-info-border bg-status-info-surface text-status-info-on;
}
.pill[data-variant="success"] {
@apply border-status-success-border bg-status-success-surface text-status-success-on;
}
.pill[data-variant="warning"] {
@apply border-status-warning-border bg-status-warning-surface text-status-warning-on;
}
.pill[data-variant="critical"] {
@apply border-status-danger-border bg-status-danger-surface text-status-danger-on;
}
.pill[data-variant="private"] {
@apply border-status-private-border bg-status-private-surface text-status-private-on;
}
.pill[data-variant="neutral"] {
@apply border-status-neutral-border bg-status-neutral-surface text-status-neutral-on;
}
.overlay-scrim {
background-color: var(--surface-overlay);
}
.overlay-scrim[data-strength="muted"] {
background-color: var(--surface-overlay-muted);
}
.overlay-scrim[data-strength="strong"] {
background-color: var(--surface-overlay-strong);
}
.overlay-panel {
background-color: var(--surface-overlay-panel);
}
.overlay-panel[data-variant="soft"] {
background-color: var(--surface-overlay-panel-soft);
}
.overlay-panel[data-variant="glass"] {
background-color: var(--surface-overlay-panel-glass);
}
.menu {
+100
View File
@@ -101,6 +101,88 @@
--color-text-frosted: rgb(226 232 240 / 0.9);
--color-text-frosted-soft: rgb(226 232 240 / 0.82);
/* Semantic palette */
--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: rgb(11 17 32 / 0.92);
--surface-muted: rgb(30 41 59 / 0.82);
--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(--color-overlay-banner);
--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);
--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);
--text-primary: var(--color-text);
--text-inverse: var(--color-text-strong);
--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: rgb(96 165 250 / 0.4);
--status-info-on: rgb(219 234 254);
--status-success: rgb(34 197 94);
--status-success-strong: rgb(22 163 74);
--status-success-surface: rgb(22 163 74 / 0.28);
--status-success-border: rgb(34 197 94 / 0.4);
--status-success-on: rgb(220 252 231);
--status-warning: var(--color-warning);
--status-warning-strong: var(--color-warning-strong);
--status-warning-surface: var(--color-warning-surface);
--status-warning-border: rgb(217 119 6 / 0.45);
--status-warning-on: rgb(254 249 195);
--status-danger: var(--color-critical);
--status-danger-strong: var(--color-critical-strong);
--status-danger-surface: rgb(127 29 29 / 0.32);
--status-danger-border: rgb(239 68 68 / 0.45);
--status-danger-on: rgb(254 202 202);
--status-private: rgb(168 85 247);
--status-private-strong: rgb(147 51 234);
--status-private-surface: rgb(91 33 182 / 0.32);
--status-private-border: rgb(168 85 247 / 0.45);
--status-private-on: rgb(233 213 255);
--status-neutral: var(--color-muted);
--status-neutral-strong: var(--color-muted-strong);
--status-neutral-surface: rgb(148 163 184 / 0.25);
--status-neutral-border: rgb(148 163 184 / 0.35);
--status-neutral-on: rgb(226 232 240);
/* Layout & stacking */
--scrollbar-width: 0.5rem;
--scrollbar-track: var(--color-white-soft);
@@ -530,6 +612,24 @@
--shadow-sidebar-panel: 0 2.25rem 4.5rem -1.85rem rgb(15 23 42 / 0.28);
--shadow-sidebar-focus-ring: 0 0 0 0.125rem rgb(59 130 246 / 0.35),
0 0 0 0.25rem rgb(59 130 246 / 0.28);
--surface-sunken: rgb(229 231 235 / 0.85);
--surface-muted: rgb(226 232 240 / 0.85);
--text-primary: var(--color-text);
--text-inverse: var(--color-white);
--text-muted: var(--color-text-muted);
--text-muted-strong: var(--color-muted-strong);
--text-subtle: var(--color-subtle-text);
--status-info-border: rgb(59 130 246 / 0.35);
--status-success-surface: rgb(34 197 94 / 0.18);
--status-success-border: rgb(16 185 129 / 0.32);
--status-warning-border: rgb(217 119 6 / 0.35);
--status-danger-surface: rgb(220 38 38 / 0.18);
--status-danger-border: rgb(220 38 38 / 0.32);
--status-private-surface: rgb(147 51 234 / 0.22);
--status-private-border: rgb(147 51 234 / 0.32);
--status-neutral-surface: rgb(100 116 139 / 0.18);
--status-neutral-border: rgb(100 116 139 / 0.3);
}
:root[data-theme="dark"] {
+16
View File
@@ -85,6 +85,22 @@ The blog embed now consumes dedicated typography and spacing tokens alongside Ta
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.
## 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:
- **Text:** `text-primary`, `text-inverse`, `text-muted`, `text-muted-strong`, and `text-subtle` map to `--text-*` tokens. Use them for copy, helper text, and frosted overlays instead of `text-gray-*`.
- **Surfaces:** `bg-surface`, `bg-surface-alt`, `bg-surface-raised`, `bg-surface-muted`, and `bg-overlay-*` wrap cards, modals, and scrims. Pair them with `border-surface`, `border-overlay`, or `border-overlay-strong` when rendering shells.
- **Statuses:** `status-*` tokens expose matching text, background, and border slots. Tailwind utilities include `text-status-success`, `bg-status-warning-surface`, `border-status-danger-border`, and chip helpers (`.pill`, `.badge`) that accept `data-variant="info" | "success" | "warning" | "critical" | "private" | "neutral"`.
- **Overlays:** `.overlay-scrim` and `.overlay-panel` helpers consume the overlay tier tokens for modals, sheets, and popovers. Adjust the strength via `data-strength`/`data-variant` rather than applying translucent grays.
### Migration guidance
- Swap raw Tailwind palette utilities (for example, `text-gray-400`, `bg-amber-500/10`, `border-red-700/70`) for the matching semantic utilities above. The new lint (`npm run lint:tailwind-colors`) fails when legacy color classes appear outside deprecated directories.
- Replace ad-hoc chips (`inline-flex ... border-red-700/70 bg-red-900/40`) with `.pill` or `.badge` plus a `data-variant`. These helpers automatically pull the correct status text/border/fill tokens.
- 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.
+10 -9
View File
@@ -10,16 +10,17 @@ import { makeR2Client, multipartUpload } from "./storage/r2-s3.js";
const STORAGE_KEY = "bitvid:quickR2Settings";
const STATUS_CLASSNAMES = [
"text-gray-400",
"text-green-400",
"text-red-400",
"text-yellow-400",
"text-status-neutral",
"text-status-info",
"text-status-success",
"text-status-danger",
"text-status-warning",
];
const STATUS_TONES = {
info: "text-gray-400",
success: "text-green-400",
error: "text-red-400",
warning: "text-yellow-400",
info: "text-status-info",
success: "text-status-success",
error: "text-status-danger",
warning: "text-status-warning",
};
function sanitizeDomain(value) {
@@ -71,7 +72,7 @@ function setStatus(el, message, tone = "info") {
return;
}
STATUS_CLASSNAMES.forEach((cls) => el.classList.remove(cls));
const className = STATUS_TONES[tone] || STATUS_TONES.info;
const className = STATUS_TONES[tone] || "text-status-neutral";
if (className) {
el.classList.add(className);
}
+123 -123
View File
@@ -385,7 +385,7 @@ export class RevertModal {
if (!this.revisions.length) {
this.list.innerHTML =
'<p class="text-xs text-gray-500">No revisions found.</p>';
'<p class="text-xs text-subtle">No revisions found.</p>';
return;
}
@@ -417,36 +417,36 @@ export class RevertModal {
"focus:outline-none",
"focus:ring-2",
"focus:ring-offset-2",
"focus:ring-offset-gray-900",
"focus:ring-offset-surface",
];
if (isSelected) {
classes.push(
"border-blue-500",
"bg-blue-500/20",
"text-blue-100",
"focus:ring-blue-500"
"border-status-info-border",
"bg-status-info-surface",
"text-status-info-on",
"focus:ring-status-info"
);
} else if (isCurrent) {
classes.push(
"border-green-500/60",
"bg-green-500/10",
"text-green-100",
"focus:ring-green-500/80"
"border-status-success-border",
"bg-status-success-surface",
"text-status-success-on",
"focus:ring-status-success"
);
} else if (isDeleted) {
classes.push(
"border-red-800/70",
"bg-red-900/30",
"text-red-200/90",
"hover:bg-red-900/40"
"border-status-danger-border",
"bg-status-danger-surface",
"text-status-danger-on",
"hover:border-status-danger-border"
);
} else {
classes.push(
"border-gray-800",
"bg-gray-800/60",
"hover:bg-gray-700/70",
"text-gray-200"
"border-overlay",
"bg-overlay-panel-soft",
"hover:bg-overlay-panel",
"text-primary"
);
}
@@ -478,8 +478,8 @@ export class RevertModal {
}
const meta = metaParts.join(" • ");
const metaClass = entry.deleted
? "text-red-200/80"
: "text-gray-400";
? "text-status-danger-on"
: "text-muted";
const escape = (value) =>
this.escapeHTML ? this.escapeHTML(String(value ?? "")) : String(value ?? "");
@@ -488,10 +488,10 @@ export class RevertModal {
<div class="flex items-start justify-between gap-3">
<div class="space-y-1">
<p class="font-semibold">${escape(entry.title || "Untitled")}</p>
<p class="text-xs text-gray-300">${escape(relative)} • ${escape(absolute)}</p>
<p class="text-xs text-subtle">${escape(relative)} • ${escape(absolute)}</p>
${meta ? `<p class="text-xs ${metaClass}">${escape(meta)}</p>` : ""}
</div>
<div class="text-xs uppercase tracking-wide text-gray-400">
<div class="text-xs uppercase tracking-wide text-muted">
${escape(versionLabel)}
</div>
</div>
@@ -588,7 +588,7 @@ export class RevertModal {
: displayMagnet;
const label = escape(labelRaw);
const caption = isPrivate
? '<span class="block text-xs text-purple-200/90 mt-1">Magnet stays visible only to you — private notes keep the raw string local.</span>'
? '<span class="block mt-1 text-xs text-status-private-on">Magnet stays visible only to you — private notes keep the raw string local.</span>'
: "";
magnetHtml = `<div class="break-all">${label}${caption}</div>`;
}
@@ -599,33 +599,33 @@ export class RevertModal {
const conflictingAudienceFlags = nsfwFlag && forKidsFlag;
if (version.deleted) {
chips.push(
'<span class="inline-flex items-center rounded-full border border-red-700/70 bg-red-900/40 px-2 py-0.5 text-xs text-red-200/90">Marked deleted</span>'
'<span class="pill" data-size="compact" data-variant="critical">Marked deleted</span>'
);
}
if (isPrivate) {
chips.push(
'<span class="inline-flex items-center rounded-full border border-purple-600/60 bg-purple-900/40 px-2 py-0.5 text-xs text-purple-200/90">Private</span>'
'<span class="pill" data-size="compact" data-variant="private">Private</span>'
);
}
if (conflictingAudienceFlags) {
chips.push(
'<span class="inline-flex items-center rounded-full border border-amber-700/70 bg-amber-900/40 px-2 py-0.5 text-xs text-amber-200/90">NSFW + For kids conflict</span>'
'<span class="pill" data-size="compact" data-variant="warning">NSFW + For kids conflict</span>'
);
} else {
if (nsfwFlag) {
chips.push(
'<span class="inline-flex items-center rounded-full border border-red-700/70 bg-red-900/40 px-2 py-0.5 text-xs text-red-200/90">Marked NSFW</span>'
'<span class="pill" data-size="compact" data-variant="critical">Marked NSFW</span>'
);
}
if (forKidsFlag) {
chips.push(
'<span class="inline-flex items-center rounded-full border border-emerald-700/70 bg-emerald-900/40 px-2 py-0.5 text-xs text-emerald-200/90">For kids</span>'
'<span class="pill" data-size="compact" data-variant="success">For kids</span>'
);
}
}
if (version.version !== undefined) {
chips.push(
`<span class="inline-flex items-center rounded-full border border-gray-700 bg-gray-800/80 px-2 py-0.5 text-xs text-gray-200">Schema v${escape(
`<span class="pill" data-size="compact" data-variant="neutral">Schema v${escape(
String(version.version)
)}</span>`
);
@@ -637,10 +637,10 @@ export class RevertModal {
unspecifiedLabel = "Not specified",
} = {}) => {
if (value === true) {
return `<span class="text-gray-200">${escape(yesLabel)}</span>`;
return `<span class="text-primary">${escape(yesLabel)}</span>`;
}
if (value === false) {
return `<span class="text-gray-200">${escape(noLabel)}</span>`;
return `<span class="text-primary">${escape(noLabel)}</span>`;
}
return this.renderPlaceholder(unspecifiedLabel);
};
@@ -657,12 +657,12 @@ export class RevertModal {
});
const audienceConflictNotice = conflictingAudienceFlags
? '<p class="text-xs text-amber-300">Conflicting flags detected — this revision is marked both NSFW and for kids. Review before reverting.</p>'
? '<p class="text-xs text-status-warning-on">Conflicting flags detected — this revision is marked both NSFW and for kids. Review before reverting.</p>'
: "";
const descriptionHtml = description
? `<p class="whitespace-pre-wrap text-gray-200">${escape(description)}</p>`
: '<p class="text-gray-500">No description provided.</p>';
? `<p class="whitespace-pre-wrap text-primary">${escape(description)}</p>`
: '<p class="text-subtle">No description provided.</p>';
const rootId =
typeof version.videoRootId === "string" ? version.videoRootId : "";
@@ -698,17 +698,17 @@ export class RevertModal {
: "";
const parts = [];
if (absoluteLabel) {
parts.push(`<span class="text-gray-200">${escape(absoluteLabel)}</span>`);
parts.push(`<span class="text-primary">${escape(absoluteLabel)}</span>`);
}
if (relativeLabel) {
parts.push(`<span class="text-gray-400">(${escape(relativeLabel)})</span>`);
parts.push(`<span class="text-muted">(${escape(relativeLabel)})</span>`);
}
if (parts.length) {
return parts.join(" ");
}
}
if (raw) {
return `<span class="text-gray-200">${escape(raw)}</span>`;
return `<span class="text-primary">${escape(raw)}</span>`;
}
return this.renderPlaceholder();
};
@@ -717,12 +717,12 @@ export class RevertModal {
if (Number.isFinite(seconds)) {
const display = this.formatDurationSeconds(seconds);
const suffix = seconds === 1 ? "second" : "seconds";
return `<span class="text-gray-200">${escape(display)}</span> <span class="text-gray-500">(${escape(
return `<span class="text-primary">${escape(display)}</span> <span class="text-subtle">(${escape(
`${seconds} ${suffix}`
)})</span>`;
}
if (raw) {
return `<span class="text-gray-200">${escape(raw)}</span>`;
return `<span class="text-primary">${escape(raw)}</span>`;
}
return this.renderPlaceholder();
};
@@ -740,21 +740,21 @@ export class RevertModal {
suffix = "video";
}
}
const badge = `<code class="rounded bg-gray-800/80 px-1.5 py-0.5 text-gray-100">kind ${escape(
const badge = `<code class="rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary">kind ${escape(
label
)}</code>`;
kindHtml = suffix
? `${badge} <span class="text-gray-400">(${escape(suffix)})</span>`
? `${badge} <span class="text-muted">(${escape(suffix)})</span>`
: badge;
}
const summaryHtml = nip71Metadata.summary
? `<p class="whitespace-pre-wrap text-gray-200">${escape(
? `<p class="whitespace-pre-wrap text-primary">${escape(
nip71Metadata.summary
)}</p>`
: this.renderPlaceholder("Not provided");
const contentWarningHtml = nip71Metadata.contentWarning
? `<span class="inline-flex items-center rounded-full border border-amber-700/70 bg-amber-900/40 px-2 py-0.5 text-xs text-amber-200/90">${escape(
? `<span class="pill" data-size="compact" data-variant="warning">${escape(
nip71Metadata.contentWarning
)}</span>`
: this.renderPlaceholder("Not provided");
@@ -767,14 +767,14 @@ export class RevertModal {
nip71Metadata.durationRaw
);
const altHtml = nip71Metadata.alt
? `<p class="whitespace-pre-wrap text-gray-200">${escape(nip71Metadata.alt)}</p>`
? `<p class="whitespace-pre-wrap text-primary">${escape(nip71Metadata.alt)}</p>`
: this.renderPlaceholder("Not provided");
const definition = (label, valueHtml, { span = 1 } = {}) => {
const colSpan = span > 1 ? "sm:col-span-2" : "";
return `
<div class="${colSpan}">
<dt class="font-semibold text-gray-200">${escape(label)}</dt>
<dt class="font-semibold text-primary">${escape(label)}</dt>
<dd class="mt-1">${valueHtml}</dd>
</div>
`;
@@ -797,7 +797,7 @@ export class RevertModal {
this.details.innerHTML = `
<div class="space-y-6">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start">
<div class="overflow-hidden rounded-md border border-gray-800 bg-black/40 w-full max-w-sm">
<div class="overflow-hidden rounded-md border border-overlay bg-overlay-strong w-full max-w-sm">
<img
src="${thumbnailSrc}"
alt="${escape(thumbnailAlt)}"
@@ -807,23 +807,23 @@ export class RevertModal {
</div>
<div class="flex-1 space-y-3">
<div class="space-y-1">
<h3 class="text-lg font-semibold text-white">${escape(
<h3 class="text-lg font-semibold text-primary">${escape(
version.title || "Untitled"
)}</h3>
<p class="text-xs text-gray-400">${timestampHtml}</p>
<p class="text-xs text-muted">${timestampHtml}</p>
</div>
${
chips.length
? `<div class="flex flex-wrap gap-2">${chips.join("")}</div>`
: ""
}
<dl class="grid gap-3 sm:grid-cols-2 text-xs text-gray-300">
<dl class="grid gap-3 sm:grid-cols-2 text-xs text-subtle">
<div>
<dt class="font-semibold text-gray-200">Hosted URL</dt>
<dt class="font-semibold text-primary">Hosted URL</dt>
<dd class="mt-1">${urlHtml}</dd>
</div>
<div>
<dt class="font-semibold text-gray-200">Magnet</dt>
<dt class="font-semibold text-primary">Magnet</dt>
<dd class="mt-1">${magnetHtml}</dd>
</div>
</dl>
@@ -831,65 +831,65 @@ export class RevertModal {
</div>
<section class="space-y-2">
<h4 class="text-sm font-semibold text-gray-200">Description</h4>
<h4 class="text-sm font-semibold text-primary">Description</h4>
${descriptionHtml}
</section>
<section class="space-y-2">
<h4 class="text-sm font-semibold text-gray-200">Audience flags</h4>
<dl class="grid gap-3 sm:grid-cols-2 text-xs text-gray-300">
<h4 class="text-sm font-semibold text-primary">Audience flags</h4>
<dl class="grid gap-3 sm:grid-cols-2 text-xs text-subtle">
${audienceRows}
</dl>
${audienceConflictNotice}
</section>
<section class="space-y-2">
<h4 class="text-sm font-semibold text-gray-200">NIP-71 event metadata</h4>
<dl class="grid gap-3 sm:grid-cols-2 text-xs text-gray-300">
<h4 class="text-sm font-semibold text-primary">NIP-71 event metadata</h4>
<dl class="grid gap-3 sm:grid-cols-2 text-xs text-subtle">
${eventMetadataRows}
</dl>
</section>
<section class="space-y-2">
<h4 class="text-sm font-semibold text-gray-200">Note pointers</h4>
<dl class="grid gap-3 sm:grid-cols-2 text-xs text-gray-300">
<h4 class="text-sm font-semibold text-primary">Note pointers</h4>
<dl class="grid gap-3 sm:grid-cols-2 text-xs text-subtle">
<div>
<dt class="font-semibold text-gray-200">Mode</dt>
<dt class="font-semibold text-primary">Mode</dt>
<dd class="mt-1">${escape(version.mode || "live")}</dd>
</div>
<div>
<dt class="font-semibold text-gray-200">d tag</dt>
<dt class="font-semibold text-primary">d tag</dt>
<dd class="mt-1">
${
dTagValue
? `<code class="rounded bg-gray-800/80 px-1.5 py-0.5">${escape(
? `<code class="rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary">${escape(
dTagValue
)}</code>`
: '<span class="text-gray-500">Not provided</span>'
: '<span class="text-subtle">Not provided</span>'
}
</dd>
</div>
<div>
<dt class="font-semibold text-gray-200">videoRootId</dt>
<dt class="font-semibold text-primary">videoRootId</dt>
<dd class="mt-1">
${
rootDisplay
? `<code class="break-all rounded bg-gray-800/80 px-1.5 py-0.5" title="${escape(
? `<code class="break-all rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary" title="${escape(
rootId
)}">${rootDisplay}</code>`
: '<span class="text-gray-500">Not provided</span>'
: '<span class="text-subtle">Not provided</span>'
}
</dd>
</div>
<div>
<dt class="font-semibold text-gray-200">Event ID</dt>
<dt class="font-semibold text-primary">Event ID</dt>
<dd class="mt-1">
${
eventDisplay
? `<code class="break-all rounded bg-gray-800/80 px-1.5 py-0.5" title="${escape(
? `<code class="break-all rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary" title="${escape(
version.id || ""
)}">${eventDisplay}</code>`
: '<span class="text-gray-500">Unknown</span>'
: '<span class="text-subtle">Unknown</span>'
}
</dd>
</div>
@@ -897,30 +897,30 @@ export class RevertModal {
</section>
<section class="space-y-4">
<h4 class="text-sm font-semibold text-gray-200">NIP-71 media metadata</h4>
<h4 class="text-sm font-semibold text-primary">NIP-71 media metadata</h4>
<div class="space-y-4">
<div class="space-y-2">
<h5 class="text-xs font-semibold uppercase tracking-wide text-gray-400">Media variants (imeta)</h5>
<h5 class="text-xs font-semibold uppercase tracking-wide text-muted">Media variants (imeta)</h5>
${this.renderImetaVariants(nip71Metadata.imeta)}
</div>
<div class="space-y-2">
<h5 class="text-xs font-semibold uppercase tracking-wide text-gray-400">Caption tracks</h5>
<h5 class="text-xs font-semibold uppercase tracking-wide text-muted">Caption tracks</h5>
${this.renderTextTracks(nip71Metadata.textTracks)}
</div>
<div class="space-y-2">
<h5 class="text-xs font-semibold uppercase tracking-wide text-gray-400">Chapters</h5>
<h5 class="text-xs font-semibold uppercase tracking-wide text-muted">Chapters</h5>
${this.renderSegments(nip71Metadata.segments)}
</div>
<div class="space-y-2">
<h5 class="text-xs font-semibold uppercase tracking-wide text-gray-400">Hashtags</h5>
<h5 class="text-xs font-semibold uppercase tracking-wide text-muted">Hashtags</h5>
${this.renderHashtags(nip71Metadata.hashtags)}
</div>
<div class="space-y-2">
<h5 class="text-xs font-semibold uppercase tracking-wide text-gray-400">Participants</h5>
<h5 class="text-xs font-semibold uppercase tracking-wide text-muted">Participants</h5>
${this.renderParticipants(nip71Metadata.participants)}
</div>
<div class="space-y-2">
<h5 class="text-xs font-semibold uppercase tracking-wide text-gray-400">References</h5>
<h5 class="text-xs font-semibold uppercase tracking-wide text-muted">References</h5>
${this.renderReferences(nip71Metadata.references)}
</div>
</div>
@@ -1123,12 +1123,12 @@ export class RevertModal {
renderPlaceholder(text = "Not provided") {
const escape = this.getEscapeFn();
return `<span class="text-gray-500">${escape(text)}</span>`;
return `<span class="text-subtle">${escape(text)}</span>`;
}
renderListEmpty(text) {
const escape = this.getEscapeFn();
return `<p class="text-xs text-gray-500">${escape(text)}</p>`;
return `<p class="text-xs text-subtle">${escape(text)}</p>`;
}
buildLinkMarkup(value, { breakAll = true } = {}) {
@@ -1150,10 +1150,10 @@ export class RevertModal {
const breakClass = breakAll ? " break-all" : " break-words";
if (/^https?:/i.test(raw)) {
return `<a href="${safeValue}" target="_blank" rel="noopener noreferrer" class="text-blue-400 hover:text-blue-300${breakClass}">${safeDisplay}</a>`;
return `<a href="${safeValue}" target="_blank" rel="noopener noreferrer" class="text-info hover:text-info-strong${breakClass}">${safeDisplay}</a>`;
}
return `<code class="rounded bg-gray-800/70 px-1.5 py-0.5 text-[0.75rem] text-gray-200${breakClass}">${safeDisplay}</code>`;
return `<code class="rounded bg-overlay-panel-soft px-1.5 py-0.5 text-[0.75rem] text-primary${breakClass}">${safeDisplay}</code>`;
}
formatDurationSeconds(seconds) {
@@ -1365,19 +1365,19 @@ export class RevertModal {
const badges = [];
if (variant.autoGenerated) {
badges.push(
'<span class="inline-flex items-center rounded-full border border-amber-600/70 bg-amber-900/30 px-2 py-0.5 text-4xs uppercase tracking-wide text-amber-200/90">Auto-generated</span>'
'<span class="badge" data-variant="warning">Auto-generated</span>'
);
}
const variantNumber = `Variant ${index + 1}`;
const mimeHtml = variant.m
? `<code class="rounded bg-gray-800/80 px-1.5 py-0.5 text-gray-100">${escape(
? `<code class="rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary">${escape(
variant.m
)}</code>`
: this.renderPlaceholder();
const dimHtml = variant.dim
? `<code class="rounded bg-gray-800/80 px-1.5 py-0.5 text-gray-100">${escape(
? `<code class="rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary">${escape(
variant.dim
)}</code>`
: this.renderPlaceholder();
@@ -1385,14 +1385,14 @@ export class RevertModal {
? this.buildLinkMarkup(variant.url)
: this.renderPlaceholder("No URL");
const hashHtml = variant.x
? `<code class="break-all rounded bg-gray-800/80 px-1.5 py-0.5 text-gray-100">${escape(
? `<code class="break-all rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary">${escape(
variant.x
)}</code>`
: this.renderPlaceholder("Not provided");
const renderNestedList = (label, values, emptyLabel) => {
if (!Array.isArray(values) || values.length === 0) {
return `<div><p class="text-2xs uppercase tracking-wide text-gray-500">${escape(
return `<div><p class="text-2xs uppercase tracking-wide text-subtle">${escape(
label
)}</p>${this.renderListEmpty(emptyLabel)}</div>`;
}
@@ -1403,17 +1403,17 @@ export class RevertModal {
.map((markup) => `<li>${markup}</li>`);
if (!items.length) {
return `<div><p class="text-2xs uppercase tracking-wide text-gray-500">${escape(
return `<div><p class="text-2xs uppercase tracking-wide text-subtle">${escape(
label
)}</p>${this.renderListEmpty(emptyLabel)}</div>`;
}
return `
<div>
<p class="text-2xs uppercase tracking-wide text-gray-500">${escape(
<p class="text-2xs uppercase tracking-wide text-subtle">${escape(
label
)}</p>
<ul class="mt-1 space-y-1 text-xs text-gray-200">${items.join("")}</ul>
<ul class="mt-1 space-y-1 text-xs text-primary">${items.join("")}</ul>
</div>
`;
};
@@ -1425,30 +1425,30 @@ export class RevertModal {
].join("");
return `
<li class="rounded-md border border-gray-800/70 bg-black/30 p-3 space-y-3">
<div class="flex flex-wrap items-center justify-between gap-2 text-xs text-gray-400">
<span class="font-semibold text-gray-200">${escape(variantNumber)}</span>
<li class="rounded-md border border-overlay bg-overlay-muted p-3 space-y-3">
<div class="flex flex-wrap items-center justify-between gap-2 text-xs text-muted">
<span class="font-semibold text-primary">${escape(variantNumber)}</span>
${badges.length ? `<div class="flex flex-wrap gap-2">${badges.join("")}</div>` : ""}
</div>
<dl class="grid gap-3 sm:grid-cols-2 text-xs text-gray-300">
<dl class="grid gap-3 sm:grid-cols-2 text-xs text-subtle">
<div>
<dt class="font-semibold text-gray-200">MIME type</dt>
<dt class="font-semibold text-primary">MIME type</dt>
<dd class="mt-1">${mimeHtml}</dd>
</div>
<div>
<dt class="font-semibold text-gray-200">Dimensions</dt>
<dt class="font-semibold text-primary">Dimensions</dt>
<dd class="mt-1">${dimHtml}</dd>
</div>
<div class="sm:col-span-2">
<dt class="font-semibold text-gray-200">URL</dt>
<dt class="font-semibold text-primary">URL</dt>
<dd class="mt-1">${urlHtml}</dd>
</div>
<div class="sm:col-span-2">
<dt class="font-semibold text-gray-200">Content hash</dt>
<dt class="font-semibold text-primary">Content hash</dt>
<dd class="mt-1">${hashHtml}</dd>
</div>
</dl>
<div class="grid gap-3 sm:grid-cols-3 text-xs text-gray-300">
<div class="grid gap-3 sm:grid-cols-3 text-xs text-subtle">
${nestedSections}
</div>
</li>
@@ -1471,33 +1471,33 @@ export class RevertModal {
? this.buildLinkMarkup(track.url)
: this.renderPlaceholder("No URL");
const typeHtml = track.type
? `<code class="rounded bg-gray-800/80 px-1.5 py-0.5 text-gray-100">${escape(
? `<code class="rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary">${escape(
track.type
)}</code>`
: this.renderPlaceholder();
const languageHtml = track.language
? `<span class="inline-flex items-center rounded-full border border-gray-700 bg-gray-800/70 px-2 py-0.5 text-2xs uppercase tracking-wide text-gray-200">${escape(
? `<span class="pill" data-size="compact" data-variant="neutral">${escape(
track.language
)}</span>`
: this.renderPlaceholder();
return `
<li class="rounded-md border border-gray-800/70 bg-black/30 p-3 space-y-3">
<div class="flex items-center justify-between text-xs text-gray-400">
<span class="font-semibold text-gray-200">${escape(headerLabel)}</span>
<li class="rounded-md border border-overlay bg-overlay-muted p-3 space-y-3">
<div class="flex items-center justify-between text-xs text-muted">
<span class="font-semibold text-primary">${escape(headerLabel)}</span>
${track.language ? languageHtml : ""}
</div>
<dl class="grid gap-3 sm:grid-cols-2 text-xs text-gray-300">
<dl class="grid gap-3 sm:grid-cols-2 text-xs text-subtle">
<div class="sm:col-span-2">
<dt class="font-semibold text-gray-200">URL</dt>
<dt class="font-semibold text-primary">URL</dt>
<dd class="mt-1">${urlHtml}</dd>
</div>
<div>
<dt class="font-semibold text-gray-200">Type</dt>
<dt class="font-semibold text-primary">Type</dt>
<dd class="mt-1">${typeHtml}</dd>
</div>
<div>
<dt class="font-semibold text-gray-200">Language</dt>
<dt class="font-semibold text-primary">Language</dt>
<dd class="mt-1">${track.language ? languageHtml : this.renderPlaceholder()}</dd>
</div>
</dl>
@@ -1518,12 +1518,12 @@ export class RevertModal {
if (Number.isFinite(seconds)) {
const display = this.formatDurationSeconds(seconds);
const suffix = seconds === 1 ? "second" : "seconds";
return `<span class="text-gray-200">${escape(display)}</span> <span class="text-gray-500">(${escape(
return `<span class="text-primary">${escape(display)}</span> <span class="text-subtle">(${escape(
`${seconds} ${suffix}`
)})</span>`;
}
if (raw) {
return `<span class="text-gray-200">${escape(raw)}</span>`;
return `<span class="text-primary">${escape(raw)}</span>`;
}
return this.renderPlaceholder();
};
@@ -1534,30 +1534,30 @@ export class RevertModal {
const startHtml = formatTime(segment.startSeconds, segment.startRaw);
const endHtml = formatTime(segment.endSeconds, segment.endRaw);
const titleHtml = segment.title
? `<p class="whitespace-pre-wrap text-gray-200">${escape(segment.title)}</p>`
? `<p class="whitespace-pre-wrap text-primary">${escape(segment.title)}</p>`
: this.renderPlaceholder("No title");
const thumbnailHtml = segment.thumbnail
? this.buildLinkMarkup(segment.thumbnail)
: this.renderPlaceholder("No thumbnail");
return `
<li class="rounded-md border border-gray-800/70 bg-black/30 p-3 space-y-3">
<div class="text-xs font-semibold text-gray-200">${escape(headerLabel)}</div>
<dl class="grid gap-3 sm:grid-cols-2 text-xs text-gray-300">
<li class="rounded-md border border-overlay bg-overlay-muted p-3 space-y-3">
<div class="text-xs font-semibold text-primary">${escape(headerLabel)}</div>
<dl class="grid gap-3 sm:grid-cols-2 text-xs text-subtle">
<div>
<dt class="font-semibold text-gray-200">Start</dt>
<dt class="font-semibold text-primary">Start</dt>
<dd class="mt-1">${startHtml}</dd>
</div>
<div>
<dt class="font-semibold text-gray-200">End</dt>
<dt class="font-semibold text-primary">End</dt>
<dd class="mt-1">${endHtml}</dd>
</div>
<div class="sm:col-span-2">
<dt class="font-semibold text-gray-200">Title</dt>
<dt class="font-semibold text-primary">Title</dt>
<dd class="mt-1">${titleHtml}</dd>
</div>
<div class="sm:col-span-2">
<dt class="font-semibold text-gray-200">Thumbnail</dt>
<dt class="font-semibold text-primary">Thumbnail</dt>
<dd class="mt-1">${thumbnailHtml}</dd>
</div>
</dl>
@@ -1576,7 +1576,7 @@ export class RevertModal {
const chips = hashtags.map((tag) => {
const label = tag.startsWith("#") ? tag : `#${tag}`;
return `<span class="inline-flex items-center rounded-full border border-gray-700 bg-gray-800/70 px-2 py-0.5 text-xs text-gray-200">${escape(
return `<span class="pill" data-size="compact" data-variant="neutral">${escape(
label
)}</span>`;
});
@@ -1600,7 +1600,7 @@ export class RevertModal {
: participant.pubkey
: "";
const pubkeyHtml = participant.pubkey
? `<code class="break-all rounded bg-gray-800/80 px-1.5 py-0.5 text-gray-100" title="${escape(
? `<code class="break-all rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary" title="${escape(
participant.pubkey
)}">${escape(displayPubkey)}</code>`
: this.renderPlaceholder("No pubkey");
@@ -1609,15 +1609,15 @@ export class RevertModal {
: this.renderPlaceholder("No relay specified");
return `
<li class="rounded-md border border-gray-800/70 bg-black/30 p-3 space-y-2">
<div class="text-xs font-semibold text-gray-200">${escape(label)}</div>
<dl class="grid gap-3 sm:grid-cols-2 text-xs text-gray-300">
<li class="rounded-md border border-overlay bg-overlay-muted p-3 space-y-2">
<div class="text-xs font-semibold text-primary">${escape(label)}</div>
<dl class="grid gap-3 sm:grid-cols-2 text-xs text-subtle">
<div class="sm:col-span-2">
<dt class="font-semibold text-gray-200">Pubkey</dt>
<dt class="font-semibold text-primary">Pubkey</dt>
<dd class="mt-1">${pubkeyHtml}</dd>
</div>
<div class="sm:col-span-2">
<dt class="font-semibold text-gray-200">Relay</dt>
<dt class="font-semibold text-primary">Relay</dt>
<dd class="mt-1">${relayHtml}</dd>
</div>
</dl>
@@ -1641,7 +1641,7 @@ export class RevertModal {
return this.renderListEmpty("No external references.");
}
return `<ul class="space-y-1 text-xs text-gray-200">${items.join("")}</ul>`;
return `<ul class="space-y-1 text-xs text-primary">${items.join("")}</ul>`;
}
extractDTagValue(tags) {
+1 -1
View File
@@ -322,7 +322,7 @@ export class VideoCard {
if (this.showUnsupportedTorrentBadge) {
const warning = this.createElement("p", {
classNames: ["mt-3", "text-xs", "text-amber-300"],
classNames: ["mt-3", "text-xs", "text-status-warning-on"],
attrs: { title: this.assets.unsupportedBtihMessage || "" },
textContent:
"WebTorrent fallback unavailable (magnet missing btih info hash)"
+22 -16
View File
@@ -1275,7 +1275,7 @@ export class ProfileModalController {
if (!entriesToRender.length) {
listEl.setAttribute("data-profile-switcher-empty", "true");
const helper = document.createElement("p");
helper.className = "text-sm text-gray-400";
helper.className = "text-sm text-muted";
helper.textContent = "No other profiles saved yet.";
helper.setAttribute("role", "note");
listEl.appendChild(helper);
@@ -1309,7 +1309,7 @@ export class ProfileModalController {
const avatarSpan = document.createElement("span");
avatarSpan.className =
"flex h-12 w-12 flex-shrink-0 items-center justify-center overflow-hidden rounded-full border-2 border-slate-500/50 bg-slate-900";
"flex h-12 w-12 flex-shrink-0 items-center justify-center overflow-hidden rounded-full border-2 border-overlay-strong bg-overlay-panel-soft";
const avatarImg = document.createElement("img");
avatarImg.className = "h-full w-full object-cover";
avatarImg.src = meta.picture || FALLBACK_PROFILE_AVATAR;
@@ -1327,23 +1327,23 @@ export class ProfileModalController {
const label = document.createElement("span");
label.className =
"text-3xs font-semibold uppercase tracking-extra-wide text-blue-200";
"text-3xs font-semibold uppercase tracking-extra-wide text-status-info";
label.textContent =
entry.authType === "nsec" ? "Direct key" : "Saved profile";
const action = document.createElement("span");
action.className = "text-xs font-medium text-gray-400";
action.className = "text-xs font-medium text-muted";
action.setAttribute("aria-hidden", "true");
action.textContent = isSelected ? "Selected" : "Switch";
topLine.append(label, action);
const nameSpan = document.createElement("span");
nameSpan.className = "truncate text-sm font-semibold text-white";
nameSpan.className = "truncate text-sm font-semibold text-primary";
nameSpan.textContent = cardDisplayName;
const npubSpan = document.createElement("span");
npubSpan.className = "break-all font-mono text-xs text-gray-400";
npubSpan.className = "break-all font-mono text-xs text-muted";
npubSpan.textContent = meta.npub
? truncate(meta.npub, 48)
: "npub unavailable";
@@ -1507,7 +1507,7 @@ export class ProfileModalController {
if (!relays.length) {
const emptyState = document.createElement("li");
emptyState.className =
"card border border-dashed border-gray-700 p-4 text-center text-sm text-gray-400";
"card border border-dashed border-surface-strong p-4 text-center text-sm text-muted";
emptyState.textContent = "No relays configured.";
this.relayList.appendChild(emptyState);
return;
@@ -1522,11 +1522,11 @@ export class ProfileModalController {
info.className = "flex-1 min-w-0";
const urlEl = document.createElement("p");
urlEl.className = "text-sm font-medium text-gray-100 break-all";
urlEl.className = "text-sm font-medium text-primary break-all";
urlEl.textContent = entry.url;
const statusEl = document.createElement("p");
statusEl.className = "mt-1 text-xs text-gray-400";
statusEl.className = "mt-1 text-xs text-muted";
let modeLabel = "Read & write";
if (entry.mode === "read") {
modeLabel = "Read only";
@@ -1867,7 +1867,7 @@ export class ProfileModalController {
info.className = "min-w-0 flex-1";
const title = document.createElement("p");
title.className = "break-all text-sm font-medium text-gray-100";
title.className = "break-all text-sm font-medium text-primary";
title.textContent = label;
info.appendChild(title);
@@ -2167,13 +2167,19 @@ export class ProfileModalController {
const element = this.walletStatusText;
const variants = {
success: "text-green-400",
error: "text-red-400",
info: "text-gray-400",
success: "text-status-success",
error: "text-status-danger",
info: "text-status-info",
neutral: "text-status-neutral",
};
element.classList.remove("text-gray-400", "text-green-400", "text-red-400");
const variantClass = variants[variant] || variants.info;
element.classList.remove(
"text-status-info",
"text-status-success",
"text-status-danger",
"text-status-neutral",
);
const variantClass = variants[variant] || variants.neutral;
element.classList.add(variantClass);
element.textContent = message || "";
}
@@ -2644,7 +2650,7 @@ export class ProfileModalController {
"card flex flex-col gap-3 p-4 sm:flex-row sm:items-center sm:justify-between";
const label = document.createElement("p");
label.className = "break-all text-sm font-medium text-gray-100";
label.className = "break-all text-sm font-medium text-primary";
label.textContent = npub;
item.appendChild(label);
+1 -1
View File
@@ -297,7 +297,7 @@ export class VideoListView {
}
this.lastRenderedVideoSignature = EMPTY_VIDEO_LIST_SIGNATURE;
this.container.innerHTML = `
<p class="flex justify-center items-center h-full w-full text-center text-gray-500">
<p class="flex justify-center items-center h-full w-full text-center text-subtle">
No public videos available yet. Be the first to upload one!
</p>`;
return displayVideos;
+1 -1
View File
@@ -118,7 +118,7 @@ export const viewInitRegistry = {
const container = document.getElementById("subscriptionsVideoList");
if (container) {
container.innerHTML =
"<p class='text-gray-500'>Please log in to see your subscriptions.</p>";
"<p class='text-muted'>Please log in to see your subscriptions.</p>";
}
return;
}
+2 -1
View File
@@ -9,11 +9,12 @@
"build:beacon": "npm run build:beacon:bundle && npm run lint:inline-styles",
"ci:beacon": "npm run build:beacon",
"format": "prettier --cache --write css/tailwind.source.css css/tokens.css \"docs/**/*.html\" tailwind.config.cjs README.md",
"lint": "npm run lint:css && npm run lint:hex && npm run lint:inline-styles && npm run lint:tokens",
"lint": "npm run lint:css && npm run lint:hex && npm run lint:inline-styles && npm run lint:tokens && npm run lint:tailwind-colors",
"lint:css": "stylelint css/tailwind.source.css css/tokens.css",
"lint:hex": "node scripts/check-hex.js",
"lint:inline-styles": "node scripts/check-inline-styles.mjs",
"lint:tokens": "node scripts/check-design-tokens.mjs",
"lint:tailwind-colors": "node scripts/check-tailwind-colors.mjs",
"check:css": "npm run build:css && git diff --exit-code -- css/tailwind.generated.css",
"test:visual": "npm run build:css && npx playwright test",
"test:visual:update": "npm run build:css && cross-env UPDATE_VISUAL_BASELINES=1 npx playwright test",
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env node
import { readdir, readFile } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, "..");
const TARGET_DIRECTORIES = ["components", "js", "views"];
const IGNORED_DIRECTORIES = new Set(["ed-legacy", "legacy", "__snapshots__"]);
const LEGACY_ALLOWLIST = new Set([
"components/ed-/revert-video-modal.html",
"components/ed-/profile-modal.html",
]);
const COLOR_PATTERN =
/\b(?:text|bg|border|divide|ring|stroke|fill|outline|shadow|accent|placeholder)-(?:slate|gray|zinc|stone|neutral|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)(?:-[\w/]+)?\b/gim;
function toPosix(relPath) {
return relPath.split(path.sep).join("/");
}
async function collectFiles() {
const results = new Set();
for (const directory of TARGET_DIRECTORIES) {
const absolute = path.resolve(repoRoot, directory);
await walkDirectory(absolute, results);
}
return [...results];
}
async function walkDirectory(dir, results) {
let entries = [];
try {
entries = await readdir(dir, { withFileTypes: true });
} catch (error) {
return;
}
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (IGNORED_DIRECTORIES.has(entry.name)) {
continue;
}
await walkDirectory(fullPath, results);
continue;
}
if (!entry.isFile()) {
continue;
}
results.add(fullPath);
}
}
function buildLineIndex(content) {
const lines = [0];
let index = 0;
while ((index = content.indexOf("\n", index)) !== -1) {
lines.push(index + 1);
index += 1;
}
return lines;
}
function getLineNumber(indexes, matchIndex) {
let low = 0;
let high = indexes.length;
while (low < high) {
const mid = Math.floor((low + high) / 2);
if (indexes[mid] <= matchIndex) {
low = mid + 1;
} else {
high = mid;
}
}
return low;
}
async function main() {
const files = await collectFiles();
const violations = [];
for (const filePath of files) {
const rel = toPosix(path.relative(repoRoot, filePath));
if (LEGACY_ALLOWLIST.has(rel)) {
continue;
}
let content;
try {
content = await readFile(filePath, "utf8");
} catch (error) {
continue;
}
const lineIndex = buildLineIndex(content);
let match;
COLOR_PATTERN.lastIndex = 0;
while ((match = COLOR_PATTERN.exec(content))) {
const line = getLineNumber(lineIndex, match.index);
violations.push({
file: rel,
line,
value: match[0],
});
}
}
if (violations.length > 0) {
console.error("Tailwind color lint failed: raw palette classes detected.\n");
for (const violation of violations) {
console.error(`${violation.file}:${violation.line} → ${violation.value}`);
}
console.error(
"\nReplace raw tailwind colors with semantic utilities (text-muted, bg-status-warning-surface, etc.)."
);
process.exit(1);
}
process.exit(0);
}
main().catch((error) => {
console.error("Failed to check Tailwind color usage.");
console.error(error);
process.exit(1);
});
+151 -34
View File
@@ -16,20 +16,31 @@ module.exports = {
colors: {
page: "var(--color-page)",
"page-alt": "var(--color-page-alt)",
surface: "var(--color-surface)",
"surface-alt": "var(--color-surface-alt)",
surface: {
DEFAULT: "var(--surface-base)",
alt: "var(--surface-alt)",
raised: "var(--surface-raised)",
"raised-private": "var(--surface-raised-private)",
"raised-critical": "var(--surface-raised-critical)",
panel: "var(--surface-panel)",
"panel-hover": "var(--surface-panel-hover)",
sunken: "var(--surface-sunken)",
muted: "var(--surface-muted)",
inverse: "var(--surface-inverse)",
},
overlay: {
DEFAULT: "var(--color-overlay)",
muted: "var(--color-overlay-muted)",
strong: "var(--color-overlay-strong)",
darker: "var(--color-overlay-darker)",
veil: "var(--color-overlay-veil)",
banner: "var(--color-overlay-banner)",
panel: "var(--color-overlay-panel)",
"panel-soft": "var(--color-overlay-panel-soft)",
"panel-tint": "var(--color-overlay-panel-tint)",
"panel-glass": "var(--color-overlay-panel-glass)",
black: "var(--color-overlay-black)"
DEFAULT: "var(--surface-overlay)",
muted: "var(--surface-overlay-muted)",
strong: "var(--surface-overlay-strong)",
darker: "var(--surface-overlay-darker)",
veil: "var(--surface-veil)",
banner: "var(--surface-overlay-banner)",
panel: "var(--surface-overlay-panel)",
"panel-soft": "var(--surface-overlay-panel-soft)",
"panel-tint": "var(--surface-overlay-panel-tint)",
"panel-glass": "var(--surface-overlay-panel-glass)",
"panel-glass-soft": "var(--surface-overlay-panel-glass-soft)",
black: "var(--surface-overlay-black)"
},
shadow: {
intense: "var(--color-shadow-intense)"
@@ -37,35 +48,39 @@ module.exports = {
primary: "var(--color-primary)",
secondary: "var(--color-secondary)",
panel: {
DEFAULT: "var(--color-panel)",
hover: "var(--color-panel-hover)"
DEFAULT: "var(--surface-panel)",
hover: "var(--surface-panel-hover)"
},
card: {
DEFAULT: "var(--color-card)",
private: "var(--color-card-private)",
critical: "var(--color-card-critical)"
DEFAULT: "var(--surface-raised)",
private: "var(--surface-raised-private)",
critical: "var(--surface-raised-critical)"
},
border: {
DEFAULT: "var(--color-border)",
subtle: "var(--color-border-subtle)",
translucent: "var(--color-border-translucent)",
DEFAULT: "var(--border-default)",
subtle: "var(--border-subtle)",
strong: "var(--border-strong)",
translucent: "var(--border-overlay)",
"translucent-strong": "var(--color-border-translucent-strong)",
"translucent-bright": "var(--color-border-translucent-bright)",
"translucent-medium": "var(--color-border-translucent-medium)",
"translucent-stronger": "var(--color-border-translucent-stronger)",
"translucent-strongest": "var(--color-border-translucent-strongest)",
"translucent-glow": "var(--color-border-translucent-glow)",
"info-soft": "var(--color-border-info-soft)",
"neutral-soft": "var(--color-border-neutral-soft)",
"sidebar-strong": "var(--color-border-sidebar-strong)"
"translucent-glow": "var(--border-overlay-glow)",
"info-soft": "var(--border-info-soft)",
"neutral-soft": "var(--border-neutral-soft)",
"sidebar-strong": "var(--border-sidebar-strong)"
},
text: {
DEFAULT: "var(--color-text)",
strong: "var(--color-text-strong)",
muted: "var(--color-muted)",
frosted: "var(--color-text-frosted)",
"frosted-strong": "var(--color-text-frosted-strong)",
"frosted-soft": "var(--color-text-frosted-soft)"
DEFAULT: "var(--text-primary)",
strong: "var(--color-white)",
inverse: "var(--text-inverse)",
muted: "var(--text-muted)",
"muted-strong": "var(--text-muted-strong)",
subtle: "var(--text-subtle)",
frosted: "var(--text-frosted-primary)",
"frosted-strong": "var(--text-frosted-strong)",
"frosted-soft": "var(--text-frosted-soft)"
},
muted: {
DEFAULT: "var(--color-muted)",
@@ -82,9 +97,53 @@ module.exports = {
strong: "var(--color-critical-strong)"
},
warning: {
DEFAULT: "var(--color-warning)",
strong: "var(--color-warning-strong)",
surface: "var(--color-warning-surface)"
DEFAULT: "var(--status-warning)",
strong: "var(--status-warning-strong)",
surface: "var(--status-warning-surface)"
},
status: {
info: {
DEFAULT: "var(--status-info)",
strong: "var(--status-info-strong)",
surface: "var(--status-info-surface)",
border: "var(--status-info-border)",
on: "var(--status-info-on)"
},
success: {
DEFAULT: "var(--status-success)",
strong: "var(--status-success-strong)",
surface: "var(--status-success-surface)",
border: "var(--status-success-border)",
on: "var(--status-success-on)"
},
warning: {
DEFAULT: "var(--status-warning)",
strong: "var(--status-warning-strong)",
surface: "var(--status-warning-surface)",
border: "var(--status-warning-border)",
on: "var(--status-warning-on)"
},
danger: {
DEFAULT: "var(--status-danger)",
strong: "var(--status-danger-strong)",
surface: "var(--status-danger-surface)",
border: "var(--status-danger-border)",
on: "var(--status-danger-on)"
},
private: {
DEFAULT: "var(--status-private)",
strong: "var(--status-private-strong)",
surface: "var(--status-private-surface)",
border: "var(--status-private-border)",
on: "var(--status-private-on)"
},
neutral: {
DEFAULT: "var(--status-neutral)",
strong: "var(--status-neutral-strong)",
surface: "var(--status-neutral-surface)",
border: "var(--status-neutral-border)",
on: "var(--status-neutral-on)"
}
},
neutral: {
600: "var(--color-neutral-600)",
@@ -139,6 +198,64 @@ module.exports = {
"neutral-50": "var(--color-blog-neutral-50)"
}
},
textColor: {
primary: "var(--text-primary)",
inverse: "var(--text-inverse)",
muted: "var(--text-muted)",
"muted-strong": "var(--text-muted-strong)",
subtle: "var(--text-subtle)",
"status-info": "var(--status-info)",
"status-info-on": "var(--status-info-on)",
"status-success": "var(--status-success)",
"status-success-on": "var(--status-success-on)",
"status-warning": "var(--status-warning)",
"status-warning-on": "var(--status-warning-on)",
"status-danger": "var(--status-danger)",
"status-danger-on": "var(--status-danger-on)",
"status-private": "var(--status-private)",
"status-private-on": "var(--status-private-on)",
"status-neutral": "var(--status-neutral)",
"status-neutral-on": "var(--status-neutral-on)"
},
backgroundColor: {
surface: "var(--surface-base)",
"surface-alt": "var(--surface-alt)",
"surface-raised": "var(--surface-raised)",
"surface-muted": "var(--surface-muted)",
"overlay-strong": "var(--surface-overlay-strong)",
"overlay-muted": "var(--surface-overlay-muted)",
"overlay-banner": "var(--surface-overlay-banner)",
"overlay-panel": "var(--surface-overlay-panel)",
"overlay-panel-soft": "var(--surface-overlay-panel-soft)",
"overlay-panel-glass": "var(--surface-overlay-panel-glass)",
"status-info-surface": "var(--status-info-surface)",
"status-success-surface": "var(--status-success-surface)",
"status-warning-surface": "var(--status-warning-surface)",
"status-danger-surface": "var(--status-danger-surface)",
"status-private-surface": "var(--status-private-surface)",
"status-neutral-surface": "var(--status-neutral-surface)"
},
borderColor: {
surface: "var(--border-default)",
"surface-strong": "var(--border-strong)",
"overlay": "var(--border-overlay)",
"overlay-strong": "var(--border-overlay-strong)",
"status-info-border": "var(--status-info-border)",
"status-success-border": "var(--status-success-border)",
"status-warning-border": "var(--status-warning-border)",
"status-danger-border": "var(--status-danger-border)",
"status-private-border": "var(--status-private-border)",
"status-neutral-border": "var(--status-neutral-border)"
},
ringColor: {
"status-danger": "var(--status-danger)",
"status-warning": "var(--status-warning)",
"status-info": "var(--status-info)",
"status-success": "var(--status-success)"
},
ringOffsetColor: {
surface: "var(--surface-base)"
},
borderRadius: {
sm: "var(--radius-sm)",
md: "var(--radius-md)",