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
33 changes: 27 additions & 6 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Developer Tools Directory - TMHSDigital</title>
<meta name="description" content="Centralized directory of TMHSDigital developer tools, Cursor IDE plugins, and MCP servers." />
<link rel="canonical" href="https://tmhsdigital.github.io/Developer-Tools-Directory/" />
<meta property="og:title" content="Developer Tools Directory - TMHSDigital" />
<meta property="og:description" content="Centralized directory of TMHSDigital developer tools, Cursor IDE plugins, and MCP servers." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://tmhsdigital.github.io/Developer-Tools-Directory/" />
<meta property="og:image" content="assets/logo.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Developer Tools Directory - TMHSDigital" />
<meta name="twitter:description" content="Centralized directory of TMHSDigital developer tools, Cursor IDE plugins, and MCP servers." />
<meta name="twitter:image" content="assets/logo.png" />
<link rel="icon" href="assets/logo.png" />
<link rel="preload" as="font" type="font/woff2" crossorigin href="fonts/inter-regular.woff2" />
<link rel="preload" as="font" type="font/woff2" crossorigin href="fonts/inter-bold.woff2" />
Expand All @@ -21,6 +27,9 @@

*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}

/* Shared design tokens mirror site-template/tokens.css (the canonical
source). tests/test_design_tokens.py enforces the mirror. The themeable
--accent / --accent-light / --bg / --nav-bg are catalog-specific. */
:root{
--accent:#7c3aed;--accent-light:#a78bfa;--accent-glow:rgba(124,58,237,0.15);
--bg:#0d1117;--bg2:#161b22;--bg3:#1c2128;--bg-hover:#22272e;
Expand All @@ -30,27 +39,30 @@
--font-sans:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;
--font-mono:'JetBrains Mono','Fira Code',Consolas,monospace;
--radius:8px;--radius-lg:12px;
--hero-h1:2.5rem;--stat-size:1.75rem;--link-hover:#c4b5fd;
}

@media(prefers-color-scheme:light){
html:not([data-theme="dark"]){
--bg:#f6f8fa;--bg2:#ffffff;--bg3:#f0f2f5;--bg-hover:#e8ebef;
--border:#d0d7de;--text:#1f2328;--text-dim:#656d76;--text-muted:#8b949e;
--border:#d0d7de;--text:#1f2328;--text-dim:#656d76;--text-muted:#5b6470;
--nav-bg:rgba(255,255,255,0.88);--accent-glow:rgba(124,58,237,0.08);
}
}
[data-theme="light"]{
--bg:#f6f8fa;--bg2:#ffffff;--bg3:#f0f2f5;--bg-hover:#e8ebef;
--border:#d0d7de;--text:#1f2328;--text-dim:#656d76;--text-muted:#8b949e;
--border:#d0d7de;--text:#1f2328;--text-dim:#656d76;--text-muted:#5b6470;
--nav-bg:rgba(255,255,255,0.88);--accent-glow:rgba(124,58,237,0.08);
}

html{scroll-behavior:smooth}
body{font-family:var(--font-sans);background:var(--bg);color:var(--text);line-height:1.6;min-height:100vh}
a{color:var(--accent-light);text-decoration:none;transition:color .2s}
a:hover{color:#c4b5fd}
a:hover{color:var(--link-hover)}
/* Light mode: hovered links use the darker accent for WCAG AA contrast. */
[data-theme="light"] a{color:var(--accent)}
@media(prefers-color-scheme:light){html:not([data-theme="dark"]) a{color:var(--accent)}}
[data-theme="light"] a:hover{color:var(--accent)}
@media(prefers-color-scheme:light){html:not([data-theme="dark"]) a{color:var(--accent)}html:not([data-theme="dark"]) a:hover{color:var(--accent)}}

/* NAV */
.nav{position:sticky;top:0;z-index:100;background:var(--nav-bg);backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:0 1.5rem}
Expand All @@ -70,12 +82,12 @@
.hero{text-align:center;padding:4rem 1.5rem 3rem;background:linear-gradient(180deg,var(--bg) 0%,var(--bg2) 70%,var(--bg) 100%);position:relative}
.hero-inner{max-width:800px;margin:0 auto}
.hero-logo{width:80px;height:80px;border-radius:50%;object-fit:cover;margin-bottom:1.25rem;box-shadow:0 4px 24px rgba(0,0,0,.3);border:2px solid var(--border)}
.hero h1{font-size:2.5rem;font-weight:700;margin-bottom:1rem;background:linear-gradient(135deg,var(--text),var(--accent-light));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.hero h1{font-size:var(--hero-h1);font-weight:700;margin-bottom:1rem;background:linear-gradient(135deg,var(--text),var(--accent-light));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.hero p{font-size:1.125rem;color:var(--text-dim);margin-bottom:2rem;max-width:600px;margin-left:auto;margin-right:auto}

.stats-bar{display:flex;justify-content:center;gap:2rem;flex-wrap:wrap;margin-bottom:1rem}
.stat-item{text-align:center}
.stat-value{font-size:1.75rem;font-weight:700;color:var(--accent-light);font-family:var(--font-mono)}
.stat-value{font-size:var(--stat-size);font-weight:700;color:var(--accent-light);font-family:var(--font-mono)}
.stat-label{font-size:.75rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em}

.search-bar{max-width:480px;margin:0 auto 1.5rem;padding:0 1.5rem;position:relative}
Expand Down Expand Up @@ -147,6 +159,8 @@
details.cat-group>.cat-body{overflow:hidden}
.cat-body-anim{transition:max-height .25s ease,opacity .2s ease;overflow:hidden}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}
.skip-link{position:absolute;left:.5rem;top:-3rem;z-index:300;background:var(--accent);color:#fff;padding:.5rem 1rem;border-radius:6px;font-size:.875rem;font-weight:600;transition:top .15s}
.skip-link:focus{top:.5rem;color:#fff}

/* TOAST */
.toast{position:fixed;bottom:1.5rem;left:50%;transform:translateX(-50%) translateY(100%);background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:.5rem 1.25rem;border-radius:8px;font-size:.8125rem;font-weight:500;opacity:0;transition:transform .3s,opacity .3s;z-index:200;pointer-events:none}
Expand Down Expand Up @@ -198,6 +212,8 @@
</head>
<body>

<a href="#main" class="skip-link">Skip to content</a>

<nav class="nav">
<div class="nav-inner">
<a href="#" class="nav-brand">
Expand All @@ -221,6 +237,8 @@
</div>
</nav>

<main id="main">

<section class="hero">
<div class="hero-inner">
<img class="hero-logo" src="assets/logo.png" alt="Developer Tools Directory logo" />
Expand Down Expand Up @@ -317,6 +335,8 @@ <h3>Scaffold Generator</h3>

</div>

</main>

<footer>
<div class="footer-inner">
<div class="footer-links">
Expand Down Expand Up @@ -388,6 +408,7 @@ <h3>Scaffold Generator</h3>
function updateIcon(s){
setIconChildren(iconSvgs[s]||iconSvgs.auto);
btn.title='Theme: '+s;
btn.setAttribute('aria-label','Theme: '+s+' (click to change)');
}
btn.addEventListener('click',function(){var c=getState();apply(states[(states.indexOf(c)+1)%states.length])});
updateIcon(getState());
Expand Down
21 changes: 9 additions & 12 deletions site-template/template.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,15 @@

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* Shared design tokens, single source of truth (see tokens.css). */
{% include 'tokens.css' %}

/* Per-tool themeable values; the rest come from tokens.css above. */
:root {
--accent: {{ site.accent | default('#7c3aed') }};
--accent-light: {{ site.accentLight | default('#a78bfa') }};
--bg: {{ site.heroGradientFrom | default('#0d1117') }};
--bg2: #161b22;
--bg3: #1c2128;
--border: #30363d;
--text: #e6edf3;
--text-dim: #8b949e;
--nav-bg: rgba(13,17,23,0.85);
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
--hero-from: {{ site.heroGradientFrom | default('#0d1117') }};
--hero-to: {{ site.heroGradientTo | default('#161b22') }};
}
Expand All @@ -55,7 +52,7 @@
--hero-from: #f0f2f5; --hero-to: #f6f8fa;
}
html:not([data-theme="dark"]) a { color: var(--accent); }
html:not([data-theme="dark"]) a:hover { color: var(--accent-light); }
html:not([data-theme="dark"]) a:hover { color: var(--accent); }
html:not([data-theme="dark"]) .hero h1 { background: linear-gradient(135deg, var(--text), var(--accent)); -webkit-background-clip: text; background-clip: text; }
html:not([data-theme="dark"]) .data-table tr:hover td { background: rgba(0,0,0,0.03); }
html:not([data-theme="dark"]) .search-input { background: var(--bg3); }
Expand All @@ -69,7 +66,7 @@
--hero-from: #f0f2f5; --hero-to: #f6f8fa;
}
[data-theme="light"] a { color: var(--accent); }
[data-theme="light"] a:hover { color: var(--accent-light); }
[data-theme="light"] a:hover { color: var(--accent); }
[data-theme="light"] .hero h1 { background: linear-gradient(135deg, var(--text), var(--accent)); -webkit-background-clip: text; background-clip: text; }
[data-theme="light"] .data-table tr:hover td { background: rgba(0,0,0,0.03); }
[data-theme="light"] .search-input { background: var(--bg3); }
Expand All @@ -78,7 +75,7 @@
html { scroll-behavior: smooth; }
body { font-family: var(--font-sans); background: var(--bg); color: var(--text); line-height: 1.6; min-height: 100vh; }
a { color: var(--accent-light); text-decoration: none; transition: color 0.2s; }
a:hover { color: #fff; }
a:hover { color: var(--link-hover); }

/* NAV */
.nav { position: sticky; top: 0; z-index: 100; background: var(--nav-bg); backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); padding: 0 1.5rem; }
Expand All @@ -97,11 +94,11 @@
.hero { text-align: center; padding: 4rem 1.5rem 4.5rem; background: linear-gradient(180deg, var(--hero-from) 0%, var(--hero-to) 70%, var(--bg) 100%); position: relative; }
.hero-inner { max-width: 720px; margin: 0 auto; }
.hero-logo { width: 80px; height: 80px; border-radius: 50%; object-fit: cover; margin-bottom: 1.25rem; box-shadow: 0 4px 24px rgba(0,0,0,0.3); border: 2px solid var(--border); }
.hero h1 { font-size: 2.75rem; font-weight: 700; margin-bottom: 1rem; background: linear-gradient(135deg, var(--text), var(--accent-light)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.hero h1 { font-size: var(--hero-h1); font-weight: 700; margin-bottom: 1rem; background: linear-gradient(135deg, var(--text), var(--accent-light)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.hero p { font-size: 1.125rem; color: var(--text-dim); margin-bottom: 2rem; max-width: 600px; margin-left: auto; margin-right: auto; }
.stats { display: flex; justify-content: center; gap: 2.5rem; flex-wrap: wrap; margin-bottom: 2rem; }
.stat { text-align: center; }
.stat-val { font-size: 2rem; font-weight: 700; color: var(--accent-light); font-family: var(--font-mono); }
.stat-val { font-size: var(--stat-size); font-weight: 700; color: var(--accent-light); font-family: var(--font-mono); }
.stat-lbl { font-size: 0.75rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.08em; margin-top: 0.25rem; }
.hero-badges { display: flex; justify-content: center; gap: 0.75rem; flex-wrap: wrap; margin-top: 1.5rem; }
.badge { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1rem; background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; color: var(--text); font-size: 0.875rem; font-weight: 500; transition: border-color 0.2s, background 0.2s; }
Expand Down
35 changes: 35 additions & 0 deletions site-template/tokens.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* tokens.css - canonical shared design tokens for the TMHSDigital Developer
Tools Directory presentation surfaces.

Consumed by:
- the catalog site (docs/index.html), which mirrors these declarations in its
inline :root; tests/test_design_tokens.py enforces the mirror;
- the tool-site template (site-template/template.html.j2), which embeds this
file directly with a Jinja include of tokens.css.

Only the tokens that are shared and fixed across both surfaces live here. The
per-tool themeable values - accent, accent-light, the hero gradient, and the
page background - are NOT here; each surface sets those itself (the template
reads them from the tool's site.json). Edit a shared token here and mirror it
into docs/index.html; the parity test fails otherwise.

Container widths are intentionally NOT shared: the catalog is a card grid
(1200px) and a tool page is a reading column (1040px). */
:root {
--bg2: #161b22;
--bg3: #1c2128;
--bg-hover: #22272e;
--border: #30363d;
--text: #e6edf3;
--text-dim: #8b949e;
--text-muted: #6e7681;
--green: #3fb950;
--blue: #58a6ff;
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
--radius: 8px;
--radius-lg: 12px;
--hero-h1: 2.5rem;
--stat-size: 1.75rem;
--link-hover: #c4b5fd;
}
57 changes: 57 additions & 0 deletions tests/test_design_tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Enforce that the shared design tokens stay reconciled across the two
presentation surfaces (finding C2).

site-template/tokens.css is the single source of truth for the shared,
non-themeable design tokens. The tool-site template embeds it with a Jinja
include; the catalog (docs/index.html) is a static, build-less file and so
mirrors the same declarations in its inline :root. This test parses tokens.css
and asserts both surfaces agree, so the drift the audit found (different type
scale, hover colors, and missing variables) cannot return silently.
"""

import re
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent
TOKENS = REPO_ROOT / "site-template" / "tokens.css"
TEMPLATE = REPO_ROOT / "site-template" / "template.html.j2"
CATALOG = REPO_ROOT / "docs" / "index.html"


def _token_pairs() -> dict[str, str]:
text = TOKENS.read_text(encoding="utf-8")
root = re.search(r":root\s*\{(.*?)\}", text, re.DOTALL)
assert root, "tokens.css has no :root block"
return {
name: value.strip()
for name, value in re.findall(r"(--[\w-]+)\s*:\s*([^;]+);", root.group(1))
}


def _strip_ws(s: str) -> str:
return re.sub(r"\s+", "", s)


def test_tokens_file_has_expected_shared_tokens():
pairs = _token_pairs()
for expected in ("--text-muted", "--radius", "--hero-h1", "--stat-size", "--link-hover"):
assert expected in pairs, f"tokens.css is missing {expected}"


def test_template_includes_tokens():
assert "{% include 'tokens.css' %}" in TEMPLATE.read_text(encoding="utf-8"), (
"template.html.j2 must embed the shared tokens via a Jinja include"
)


def test_catalog_mirrors_tokens():
catalog = _strip_ws(CATALOG.read_text(encoding="utf-8"))
missing = []
for name, value in _token_pairs().items():
decl = _strip_ws(f"{name}:{value};")
if decl not in catalog:
missing.append(f"{name}: {value}")
assert not missing, (
"docs/index.html :root is out of sync with tokens.css for: "
+ ", ".join(missing)
)
Loading