Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/(tools)/cron-explainer/CronExplainerClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export function CronExplainerClient() {
</div>

{!parsed.ok && expression.trim() !== "" && (
<p role="alert" className="rounded-xl border border-[var(--rose)]/30 bg-[var(--rose)]/5 p-3 text-sm text-[var(--rose)]">
<p role="alert" className="rounded-xl border border-tint-rose-30 bg-tint-rose-5 p-3 text-sm text-[var(--rose)]">
{parsed.error}
</p>
)}
Expand Down
2 changes: 1 addition & 1 deletion app/(tools)/csv-formatter/components/IssueList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function IssueList({ issues }: { issues: CsvIssue[] }) {
return (
<p
role="status"
className="flex items-center gap-2 rounded-xl border border-[var(--green)]/30 bg-[var(--green)]/5 p-3 text-sm text-[var(--green)]"
className="flex items-center gap-2 rounded-xl border border-tint-green-30 bg-tint-green-5 p-3 text-sm text-[var(--green)]"
>
<CheckCircle2 className="h-4 w-4 shrink-0" aria-hidden="true" />
No problems found — every row matches the header.
Expand Down
2 changes: 1 addition & 1 deletion app/(tools)/markdown-preview/MarkdownPreviewClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export function MarkdownPreviewClient() {
<button type="button" className="btn" onClick={downloadHtml} disabled={!markdown}>Download HTML</button>
<button type="button" className="btn" onClick={download} disabled={!markdown}>Download .md</button>
</div>
{importError && <p role="alert" className="rounded-md border border-[var(--rose)]/30 bg-[var(--rose)]/5 px-3 py-2 text-[13px] text-[var(--rose)]">{importError}</p>}
{importError && <p role="alert" className="rounded-md border border-tint-rose-30 bg-tint-rose-5 px-3 py-2 text-[13px] text-[var(--rose)]">{importError}</p>}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
<div className="card overflow-hidden"><div className="px-3 py-2 border-b border-[var(--hairline)] bg-[var(--bg-raise)] font-mono text-[10.5px] tracking-[0.22em] uppercase text-[var(--fg-dim)]">Markdown</div><textarea ref={textareaRef} aria-label="Markdown input" value={markdown} onChange={(e) => setMarkdown(e.target.value)} spellCheck={false} className="editor-input p-4" style={{ minHeight: 520 }} /></div>
<div className="card overflow-hidden" style={{ background: darkPreview ? "#17191f" : "#ffffff", color: darkPreview ? "#f4f4f5" : "#17191f" }}><div className="px-3 py-2 border-b border-black/10 font-mono text-[10.5px] tracking-[0.22em] uppercase opacity-60">Preview</div><article className="markdown-preview p-5 min-h-[520px] overflow-auto" dangerouslySetInnerHTML={{ __html: html }} /></div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export function NumberBaseConverterClient() {
<p
id="nb-error"
role="alert"
className="rounded-xl border border-[var(--rose)]/30 bg-[var(--rose)]/5 p-3 text-sm text-[var(--rose)]"
className="rounded-xl border border-tint-rose-30 bg-tint-rose-5 p-3 text-sm text-[var(--rose)]"
>
{error}
</p>
Expand Down Expand Up @@ -267,7 +267,7 @@ export function NumberBaseConverterClient() {
</select>
</div>
{twos && "error" in twos ? (
<p role="alert" className="rounded-xl border border-[var(--rose)]/30 bg-[var(--rose)]/5 p-3 text-sm text-[var(--rose)]">
<p role="alert" className="rounded-xl border border-tint-rose-30 bg-tint-rose-5 p-3 text-sm text-[var(--rose)]">
{twos.error}
</p>
) : (
Expand Down
2 changes: 1 addition & 1 deletion app/(tools)/regex-tester/RegexTesterClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export function RegexTesterClient() {
</div>

{!compiled.ok && (
<p role="alert" className="rounded-xl border border-[var(--rose)]/30 bg-[var(--rose)]/5 p-3 text-sm text-[var(--rose)]">
<p role="alert" className="rounded-xl border border-tint-rose-30 bg-tint-rose-5 p-3 text-sm text-[var(--rose)]">
{compiled.error}
</p>
)}
Expand Down
4 changes: 2 additions & 2 deletions app/(tools)/timestamp-converter/TimestampConverterClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function TimestampConverterClient() {
</div>
</div>
{!epochResult.ok ? (
<p role="alert" className="rounded-xl border border-[var(--rose)]/30 bg-[var(--rose)]/5 p-3 text-sm text-[var(--rose)]">{epochResult.error}</p>
<p role="alert" className="rounded-xl border border-tint-rose-30 bg-tint-rose-5 p-3 text-sm text-[var(--rose)]">{epochResult.error}</p>
) : (
<div className="space-y-3">
<p className="text-xs text-[var(--fg-muted)]">Interpreted as {UNIT_OPTIONS.find((option) => option.value === epochResult.unitUsed)?.label}</p>
Expand All @@ -115,7 +115,7 @@ export function TimestampConverterClient() {
<label className="block text-sm font-medium text-[var(--fg)]" htmlFor="date-input">Date or ISO 8601</label>
<input id="date-input" value={dateInput} onChange={(e) => setDateInput(e.target.value)} spellCheck={false} placeholder="2024-02-29T00:00:00Z" className="w-full rounded-xl border border-[var(--hairline)] bg-[var(--bg)] px-3 py-3 font-mono text-sm text-[var(--fg)] outline-none focus:border-[var(--green)]" />
{!dateResult.ok ? (
<p role="alert" className="rounded-xl border border-[var(--rose)]/30 bg-[var(--rose)]/5 p-3 text-sm text-[var(--rose)]">{dateResult.error}</p>
<p role="alert" className="rounded-xl border border-tint-rose-30 bg-tint-rose-5 p-3 text-sm text-[var(--rose)]">{dateResult.error}</p>
) : (
<div className="space-y-3">
<Field label="Unix seconds" value={String(dateResult.seconds)} copyValue={String(dateResult.seconds)} />
Expand Down
4 changes: 2 additions & 2 deletions app/(tools)/uuid-generator/UuidGeneratorClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function compareUuidBytes(a: Uint8Array, b: Uint8Array): number {
function InspectionResult({ result }: { result: UuidInspection }) {
if (!result.ok) {
return (
<p role="alert" className="rounded-xl border border-[var(--rose)]/30 bg-[var(--rose)]/5 p-3 text-sm text-[var(--rose)]">
<p role="alert" className="rounded-xl border border-tint-rose-30 bg-tint-rose-5 p-3 text-sm text-[var(--rose)]">
Enter a valid UUID in hyphenated or compact format.
</p>
);
Expand Down Expand Up @@ -190,7 +190,7 @@ export function UuidGeneratorClient() {
</div>

{generationError && (
<p role="alert" className="rounded-xl border border-[var(--rose)]/30 bg-[var(--rose)]/5 p-3 text-sm text-[var(--rose)]">
<p role="alert" className="rounded-xl border border-tint-rose-30 bg-tint-rose-5 p-3 text-sm text-[var(--rose)]">
{generationError}
</p>
)}
Expand Down
4 changes: 2 additions & 2 deletions app/guides/convert-unix-timestamp-to-date/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export default function ConvertUnixTimestampToDateGuidePage() {
className="relative p-6 rounded-lg border border-[var(--hairline)] bg-[var(--bg-raise)] scroll-mt-20"
>
<div className="flex items-center gap-3 mb-3">
<span className="flex items-center justify-center w-7 h-7 rounded-full bg-[var(--green)]/10 border border-[var(--green)]/30 font-mono text-xs font-bold text-[var(--green)]">
<span className="flex items-center justify-center w-7 h-7 rounded-full bg-tint-green-10 border border-tint-green-30 font-mono text-xs font-bold text-[var(--green)]">
{idx + 1}
</span>
<h3 className="font-display text-lg font-semibold text-[var(--fg)]">
Expand Down Expand Up @@ -328,7 +328,7 @@ dt = epoch + timedelta(seconds=-86400)
</section>

{/* Section: Year 2038 */}
<section className="p-6 rounded-lg border border-[var(--amber)]/40 bg-[var(--bg-raise)]">
<section className="p-6 rounded-lg border border-tint-amber-40 bg-[var(--bg-raise)]">
<div className="flex items-center gap-2 font-mono text-xs font-semibold uppercase tracking-wider text-[var(--amber)] mb-3">
<AlertTriangle className="w-4 h-4" />
Year 2038 limit
Expand Down
2 changes: 1 addition & 1 deletion app/guides/create-a-professional-invoice/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export default function CreateProfessionalInvoiceGuidePage() {
className="relative p-6 rounded-lg border border-[var(--hairline)] bg-[var(--bg-raise)] scroll-mt-20"
>
<div className="flex items-center gap-3 mb-3">
<span className="flex items-center justify-center w-7 h-7 rounded-full bg-[var(--green)]/10 border border-[var(--green)]/30 font-mono text-xs font-bold text-[var(--green)]">
<span className="flex items-center justify-center w-7 h-7 rounded-full bg-tint-green-10 border border-tint-green-30 font-mono text-xs font-bold text-[var(--green)]">
{idx + 1}
</span>
<h3 className="font-display text-lg font-semibold text-[var(--fg)]">
Expand Down
4 changes: 2 additions & 2 deletions app/guides/decode-jwt/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export default function DecodeJwtGuidePage() {
className="relative p-6 rounded-lg border border-[var(--hairline)] bg-[var(--bg-raise)] scroll-mt-20"
>
<div className="flex items-center gap-3 mb-3">
<span className="flex items-center justify-center w-7 h-7 rounded-full bg-[var(--violet)]/10 border border-[var(--violet)]/30 font-mono text-xs font-bold text-[var(--violet)]">
<span className="flex items-center justify-center w-7 h-7 rounded-full bg-tint-violet-10 border border-tint-violet-30 font-mono text-xs font-bold text-[var(--violet)]">
{idx + 1}
</span>
<h3 className="font-display text-lg font-semibold text-[var(--fg)]">
Expand Down Expand Up @@ -355,7 +355,7 @@ console.log(decode(payloadPart));`}
</div>
</section>

<section className="p-6 rounded-lg border border-[var(--amber)]/40 bg-[var(--bg-raise)]">
<section className="p-6 rounded-lg border border-tint-amber-40 bg-[var(--bg-raise)]">
<div className="flex items-center gap-2 font-mono text-xs font-semibold uppercase tracking-wider text-[var(--amber)] mb-3">
<ShieldAlert className="w-4 h-4" /> Decoding is not verification
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/guides/format-and-validate-json/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export default function FormatAndValidateJsonGuidePage() {
className="relative p-6 rounded-lg border border-[var(--hairline)] bg-[var(--bg-raise)] scroll-mt-20"
>
<div className="flex items-center gap-3 mb-3">
<span className="flex items-center justify-center w-7 h-7 rounded-full bg-[var(--cyan)]/10 border border-[var(--cyan)]/30 font-mono text-xs font-bold text-[var(--cyan)]">
<span className="flex items-center justify-center w-7 h-7 rounded-full bg-tint-cyan-10 border border-tint-cyan-30 font-mono text-xs font-bold text-[var(--cyan)]">
{idx + 1}
</span>
<h3 className="font-display text-lg font-semibold text-[var(--fg)]">
Expand Down
2 changes: 1 addition & 1 deletion app/guides/wcag-color-contrast-ratio/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ const contrast = (a, b) => {
className="relative p-6 rounded-lg border border-[var(--hairline)] bg-[var(--bg-raise)] scroll-mt-20"
>
<div className="flex items-center gap-3 mb-3">
<span className="flex items-center justify-center w-7 h-7 rounded-full bg-[var(--fuchsia)]/10 border border-[var(--fuchsia)]/30 font-mono text-xs font-bold text-[var(--fuchsia)]">
<span className="flex items-center justify-center w-7 h-7 rounded-full bg-tint-fuchsia-10 border border-tint-fuchsia-30 font-mono text-xs font-bold text-[var(--fuchsia)]">
{idx + 1}
</span>
<h3 className="font-display text-lg font-semibold text-[var(--fg)]">
Expand Down
84 changes: 84 additions & 0 deletions lib/__tests__/tint-classes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, expect, it } from "vitest";
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, relative } from "node:path";
import postcss from "postcss";
import tailwindcss from "tailwindcss";
import config from "../../tailwind.config";

// Tailwind 3 cannot put an opacity on a var() colour. `border-[var(--rose)]/30` builds no CSS at all, so the box
// silently falls back to the default grey border with no tint. The tinted boxes use `border-tint-rose-30` and
// `bg-tint-rose-5` instead (see tailwind.config.ts).

const ROOT = join(__dirname, "..", "..");
const SOURCE_DIRS = ["app", "components", "lib"];
const SOURCE_FILE = /\.(tsx?|jsx?|mdx|css)$/;

function sourceFiles(dir: string): string[] {
return readdirSync(dir).flatMap((name) => {
const path = join(dir, name);
if (statSync(path).isDirectory()) return name === "__tests__" || name === "node_modules" ? [] : sourceFiles(path);
return SOURCE_FILE.test(name) ? [path] : [];
});
}

const files = SOURCE_DIRS.flatMap((dir) => sourceFiles(join(ROOT, dir)));
const text = (file: string) => readFileSync(file, "utf8");

// A utility whose value is a var() colour followed by an opacity: `-[var(--x)]/30`, `-[var(--x)]/[0.3]`.
const VAR_OPACITY = /-\[var\(--[\w-]+\)\]\/(\d|\[)/;
const TINT_CLASS = /\b(?:bg|border|text|ring|outline|divide|fill|stroke|shadow|from|via|to)-tint-[a-z]+-\d+\b/g;

describe("tinted boxes", () => {
it("finds the source files it checks", () => {
expect(files.length).toBeGreaterThan(50);
});

it("uses no [var(--x)]/NN opacity class, which Tailwind 3 never builds", () => {
const offenders = files.flatMap((file) =>
text(file)
.split("\n")
.flatMap((line, i) => (VAR_OPACITY.test(line) ? [`${relative(ROOT, file)}:${i + 1}`] : [])),
);
expect(offenders).toEqual([]);
});

it("catches the class it bans", () => {
for (const bad of ["border-[var(--rose)]/30", "bg-[var(--cyan)]/5", "text-[var(--fg-muted)]/[0.4]"]) {
expect(VAR_OPACITY.test(`className="p-3 ${bad}"`)).toBe(true);
}
for (const fine of ["border-[var(--rose)]", "bg-tint-rose-5", "border-tint-green-30", "w-1/2", "bg-accent-rose/30"]) {
expect(VAR_OPACITY.test(`className="p-3 ${fine}"`)).toBe(false);
}
});

describe("the tint classes", () => {
const used = [...new Set(files.flatMap((file) => text(file).match(TINT_CLASS) ?? []))].sort();

async function build(classes: string[]): Promise<string> {
const result = await postcss([
tailwindcss({ ...config, content: [{ raw: classes.join(" "), extension: "html" }] }),
]).process("@tailwind utilities;", { from: undefined });
return result.css;
}

it("are used by the boxes that were missing their tint", () => {
expect(used.length).toBeGreaterThanOrEqual(10);
expect(used).toContain("border-tint-rose-30");
expect(used).toContain("bg-tint-rose-5");
});

it("are all built by Tailwind, each as a mix of the accent with transparent", async () => {
const css = await build(used);
for (const name of used) {
const [, accent, step] = /tint-([a-z]+)-(\d+)$/.exec(name) as RegExpExecArray;
const property = name.startsWith("bg-") ? "background-color" : "border-color";
expect(css, name).toContain(`.${name} {`);
expect(css, name).toContain(`${property}: color-mix(in srgb, var(--${accent}) ${step}%, transparent)`);
}
});

it("is not built for a percentage nobody added", async () => {
expect(await build(["border-tint-rose-35"])).not.toContain("tint-rose-35");
});
});
});
13 changes: 13 additions & 0 deletions tailwind.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import type { Config } from "tailwindcss";

// Tailwind 3 cannot put an opacity on a var() colour, so `border-[var(--rose)]/30` builds no CSS at all and the
// box falls back to the default grey border with no tint. `border-tint-rose-30` and `bg-tint-rose-5` mix the
// accent with transparent instead (the accent itself follows the theme). For another percentage, add it to TINT_STEPS.
const TINT_ACCENTS = ["cyan", "amber", "rose", "violet", "sky", "lime", "green", "fuchsia"] as const;
const TINT_STEPS = [5, 10, 30, 40] as const;
const tint = Object.fromEntries(
TINT_ACCENTS.map((accent) => [
accent,
Object.fromEntries(TINT_STEPS.map((step) => [step, `color-mix(in srgb, var(--${accent}) ${step}%, transparent)`])),
]),
);

const config: Config = {
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
Expand All @@ -24,6 +36,7 @@ const config: Config = {
100: "#DDE0EA",
50: "#E8E6E1",
},
tint,
accent: {
cyan: "#00E5C7",
amber: "#FFB547",
Expand Down
Loading