mirror of
https://github.com/HKUDS/CLI-Anything.git
synced 2026-08-28 23:27:04 +08:00
6f372d36f8
Fast scrolling on iPhone could still kill the tab after the GIF/video and content-visibility work. Two remaining causes only real iOS exhibits: - iOS Safari fires a resize when its toolbar collapses mid-scroll, and the handler rebuilt both masonry grids (teardown + re-append of 101 cards) during the fling. Masonry columns depend only on width, so the handler now bails when the width is unchanged. - Phones laid out all 79 harness cards (~28k px of live DOM) up front — content-visibility skips paint but the initial masonry measurement still forces layout of every card, and a fling still rasterizes the whole run. Single-column layouts now render the deck in batches of 20 that grow via a sentinel IntersectionObserver (1600px margin) as the user approaches; the mobile deck placeholder shrinks to match. Multi-column desktop masonry still renders the full set so shortest-column balancing is exact. jumpToTool materializes remaining batches before scrolling to a target card; search/filter/sort reset to the first batch. Chromium renderer RSS (390x844@3x fling scenario): load 297MB -> 171MB, peak 807MB -> 380MB vs the original page. Full cross-engine suite passes (WebKit + Chromium, mobile + desktop): batch growth while scrolling, reset-on-search, jumpToTool into late batches, footer reachable at true bottom, video rings, flip lifecycle, no overflow, zero page errors.
2897 lines
182 KiB
HTML
2897 lines
182 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en" data-theme="dark">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta name="robots" content="index,follow,max-snippet:-1,max-image-preview:large,max-video-preview:-1">
|
||
<title>CLI-Anything Hub - Agent-Friendly CLI Registry</title>
|
||
<meta name="description" content="CLI-Anything Hub is an agent-friendly registry and package manager for CLI tools that let AI agents discover, install, and operate GUI applications, developer tools, creative software, web APIs, and public SaaS platforms.">
|
||
<meta name="author" content="HKUDS and Yuhao Yang">
|
||
<meta name="copyright" content="Copyright: HKUDS. Lead author: Yuhao Yang (yuhao.page).">
|
||
<link rel="canonical" href="https://clianything.cc/">
|
||
<link rel="alternate" type="text/markdown" href="https://clianything.cc/index.md" title="CLI-Anything Hub Markdown">
|
||
<link rel="alternate" type="text/plain" href="https://clianything.cc/llms.txt" title="CLI-Anything LLM Context">
|
||
<link rel="alternate" type="text/plain" href="https://clianything.cc/llms-full.txt" title="CLI-Anything Full LLM Context">
|
||
<meta property="og:type" content="website">
|
||
<meta property="og:site_name" content="CLI-Anything Hub">
|
||
<meta property="og:title" content="CLI-Anything Hub - Agent-Friendly CLI Registry">
|
||
<meta property="og:description" content="Discover, install, and launch agent-friendly CLI tools for GUI applications, developer tools, creative software, web APIs, and public SaaS platforms.">
|
||
<meta property="og:url" content="https://clianything.cc/">
|
||
<meta name="twitter:card" content="summary">
|
||
<meta name="twitter:title" content="CLI-Anything Hub - Agent-Friendly CLI Registry">
|
||
<meta name="twitter:description" content="Agent-friendly CLI registry and package manager for discovering, installing, and launching command-line tools.">
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,400;0,500;0,600;0,700;0,800;1,500&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;1,6..72,300;1,6..72,400&display=swap" rel="stylesheet">
|
||
<style>
|
||
/* ===================== Tokens (cool/neutral/tech palette system) =====================
|
||
The anchor tokens (--bg, --surface, --ink/2/3, --line, --accent, --accent2) are set
|
||
per [data-palette][data-theme] by applyPalette() in JS from the PALETTES object.
|
||
Everything below derives glass / chip / shadow / accent-ink from those anchors so the
|
||
glass optics survive every palette × theme combination. Steel-dark values are inlined
|
||
as a no-JS fallback. */
|
||
:root {
|
||
color-scheme: dark;
|
||
/* --- anchor tokens (Steel · dark fallback; overridden by JS) --- */
|
||
--bg: #0d1117;
|
||
--surface: #151b24;
|
||
--ink: #e8edf4;
|
||
--ink-2: #9fb0c4;
|
||
--ink-3: #647488;
|
||
--line: rgba(255,255,255,0.08);
|
||
--accent: #5b9bd6;
|
||
--accent2: #7db4e6;
|
||
--accent-rgb: 91,155,214;
|
||
--surface-rgb: 21,27,36;
|
||
--accent-band: 209; /* palette accent hue; per-category tints fan around it */
|
||
|
||
--ok: #54d196;
|
||
--radius: 22px;
|
||
--radius-sm: 13px;
|
||
|
||
/* derived (dark) */
|
||
--accent-ink: var(--accent2);
|
||
--glass-bg: linear-gradient(158deg, rgba(255,255,255,0.075), rgba(255,255,255,0.024) 46%, rgba(255,255,255,0.010));
|
||
--glass-bg-fallback: rgba(var(--surface-rgb), 0.86);
|
||
--glass-border: var(--line);
|
||
--glass-edge: rgba(255,255,255,0.14);
|
||
--glass-shadow: 0 24px 60px -22px rgba(0,0,0,0.62), 0 2px 8px -2px rgba(0,0,0,0.48);
|
||
--glass-shadow-hi: 0 38px 90px -28px rgba(0,0,0,0.70), 0 4px 14px -4px rgba(0,0,0,0.52);
|
||
--chip-bg: rgba(255,255,255,0.055);
|
||
--cmd-bg: rgba(0,0,0,0.30);
|
||
--cmd-line: rgba(255,255,255,0.08);
|
||
|
||
--hue-default: 212;
|
||
--tint-s: 52%;
|
||
--tint-l: 62%;
|
||
--ease: cubic-bezier(.22,.61,.36,1);
|
||
--spring: cubic-bezier(.34,1.32,.5,1);
|
||
--flip-ease: cubic-bezier(.46,.9,.4,1.05);
|
||
--palette-transition: background .45s var(--ease), color .45s var(--ease), border-color .45s var(--ease);
|
||
}
|
||
|
||
[data-theme="light"] {
|
||
color-scheme: light;
|
||
/* Steel · light fallback (overridden by JS) */
|
||
--bg: #f4f6f9;
|
||
--surface: #ffffff;
|
||
--ink: #14181f;
|
||
--ink-2: #4a5568;
|
||
--ink-3: #8595a8;
|
||
--line: rgba(20,40,80,0.10);
|
||
--accent: #2f6fb0;
|
||
--accent2: #1f5793;
|
||
--accent-rgb: 47,111,176;
|
||
--surface-rgb: 255,255,255;
|
||
|
||
--accent-ink: var(--accent2);
|
||
--glass-bg: linear-gradient(158deg, rgba(255,255,255,0.92), rgba(255,255,255,0.62) 48%, rgba(255,255,255,0.42));
|
||
--glass-bg-fallback: rgba(var(--surface-rgb), 0.92);
|
||
--glass-border: var(--line);
|
||
--glass-edge: rgba(255,255,255,0.96);
|
||
--glass-shadow: 0 22px 50px -24px rgba(30,44,70,0.22), 0 2px 6px -2px rgba(30,44,70,0.12);
|
||
--glass-shadow-hi: 0 34px 80px -28px rgba(30,44,70,0.28), 0 4px 12px -4px rgba(30,44,70,0.16);
|
||
--chip-bg: rgba(20,40,80,0.045);
|
||
--cmd-bg: rgba(20,40,80,0.05);
|
||
--cmd-line: rgba(20,40,80,0.08);
|
||
--tint-s: 46%;
|
||
--tint-l: 44%;
|
||
}
|
||
/* every surface eases between palettes (reduced-motion neutralizes below) */
|
||
body, .nav.scrolled, .card-face, .mcard, .stat, .empower-card,
|
||
.typing-shell, .deck-tab, .filter-btn, .search, .sort-select, .chip-label,
|
||
.scope-banner, .palette-swatch { transition: var(--palette-transition); }
|
||
|
||
/* ===================== Reset / base ===================== */
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
html { scroll-behavior: smooth; scroll-padding-top: 84px; -webkit-text-size-adjust: 100%; }
|
||
body {
|
||
font-family: 'Hanken Grotesk', sans-serif;
|
||
background: var(--bg);
|
||
color: var(--ink);
|
||
line-height: 1.6;
|
||
font-size: 16px;
|
||
letter-spacing: -0.004em;
|
||
-webkit-font-smoothing: antialiased;
|
||
-moz-osx-font-smoothing: grayscale;
|
||
min-height: 100vh;
|
||
overflow-x: hidden;
|
||
}
|
||
::selection { background: rgba(var(--accent-rgb), 0.30); color: var(--ink); }
|
||
a { color: inherit; }
|
||
button { font-family: inherit; }
|
||
h1,h2,h3 { letter-spacing: -0.022em; line-height: 1.1; font-weight: 700; }
|
||
code, .mono { font-family: 'JetBrains Mono', monospace; }
|
||
|
||
/* ===================== Ambient backdrop ===================== */
|
||
.ambient { position: fixed; inset: 0; z-index: -2; overflow: hidden; pointer-events: none; }
|
||
/* No filter:blur() here — a 100px blur over three ~700px fixed layers is one of the most
|
||
expensive things a page can ask a mobile GPU to hold. The radial gradients below fade
|
||
to transparent well before their edge, which reads identically to the blurred version. */
|
||
.ambient i { position: absolute; border-radius: 50%; opacity: 0.7; }
|
||
.ambient .a { width: 60vw; max-width: 760px; aspect-ratio: 1; top: -18vw; right: -10vw;
|
||
background: radial-gradient(circle at 38% 38%, hsl(calc(var(--accent-band) + 28) var(--tint-s) 58% / 0.26), hsl(calc(var(--accent-band) + 28) var(--tint-s) 58% / 0.10) 40%, transparent 68%); }
|
||
.ambient .b { width: 54vw; max-width: 680px; aspect-ratio: 1; bottom: -16vw; left: -12vw;
|
||
background: radial-gradient(circle at 60% 42%, hsl(var(--accent-band) calc(var(--tint-s) + 12%) 56% / 0.24), hsl(var(--accent-band) calc(var(--tint-s) + 12%) 56% / 0.09) 38%, transparent 66%); }
|
||
.ambient .c { width: 46vw; max-width: 560px; aspect-ratio: 1; top: 42%; left: 50%;
|
||
background: radial-gradient(circle, hsl(calc(var(--accent-band) - 22) var(--tint-s) 54% / 0.14), hsl(calc(var(--accent-band) - 22) var(--tint-s) 54% / 0.06) 40%, transparent 70%); }
|
||
[data-theme="light"] .ambient i { opacity: 0.55; }
|
||
.grain { position: fixed; inset: 0; z-index: -1; pointer-events: none; opacity: 0.04; mix-blend-mode: overlay;
|
||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); }
|
||
/* The full-viewport blend layer forces the whole page through an offscreen compositing
|
||
group; on phone GPUs that costs more than the texture is worth. */
|
||
@media (hover: none), (pointer: coarse) { .grain { display: none; } }
|
||
|
||
/* ===================== Glass primitive ===================== */
|
||
.glass {
|
||
position: relative;
|
||
background: var(--glass-bg-fallback);
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--glass-shadow);
|
||
}
|
||
/* Real backdrop blur is desktop-only. Each blurred surface is its own composited layer
|
||
that re-reads the backdrop, and this page has dozens at once — enough to blow past
|
||
iOS Safari's per-tab GPU budget (the page reloads/white-flashes on iPhone). Touch
|
||
devices get the tuned --glass-bg-fallback, which was designed to stand alone. */
|
||
@media (hover: hover) and (pointer: fine) {
|
||
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
||
.glass { background: var(--glass-bg);
|
||
-webkit-backdrop-filter: blur(22px) saturate(1.5); backdrop-filter: blur(22px) saturate(1.5); }
|
||
}
|
||
}
|
||
.glass::before { content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
|
||
box-shadow: inset 0 1px 0 var(--glass-edge), inset 0 0 0 1px rgba(255,255,255,0.02); }
|
||
|
||
/* ===================== Layout ===================== */
|
||
.wrap { max-width: 1180px; margin: 0 auto; padding: 0 22px; }
|
||
|
||
/* ===================== Nav ===================== */
|
||
.nav {
|
||
position: sticky; top: 0; z-index: 40;
|
||
border-bottom: 1px solid transparent;
|
||
transition: border-color .3s, background .3s, backdrop-filter .3s;
|
||
}
|
||
.nav.scrolled {
|
||
border-color: var(--line);
|
||
background: color-mix(in oklab, var(--bg) 78%, transparent);
|
||
-webkit-backdrop-filter: blur(16px) saturate(1.4); backdrop-filter: blur(16px) saturate(1.4);
|
||
}
|
||
.nav-in { max-width: 1180px; margin: 0 auto; padding: 0 22px; display: flex; align-items: center; justify-content: space-between; gap: 14px; height: 64px; }
|
||
.nav-brand { display: inline-flex; align-items: center; gap: 9px; text-decoration: none; font-weight: 800; font-size: 1.08rem; letter-spacing: -0.02em; }
|
||
.nav-brand-badge { width: 30px; height: 30px; display: grid; place-items: center; border-radius: 9px; flex-shrink: 0;
|
||
border: 1px solid var(--glass-border); background: var(--chip-bg); box-shadow: var(--glass-shadow); }
|
||
.nav-brand-badge svg { width: 64%; height: 64%; }
|
||
.nav-links { display: flex; align-items: center; gap: 4px; }
|
||
.nav-link { padding: 7px 11px; border-radius: 10px; text-decoration: none; color: var(--ink-2); font-size: 0.84rem; font-weight: 500;
|
||
transition: color .18s, background .18s; display: inline-flex; align-items: center; gap: 6px; }
|
||
.nav-link:hover { color: var(--ink); background: var(--chip-bg); }
|
||
.nav-link svg { width: 15px; height: 15px; flex-shrink: 0; }
|
||
.nav-link-stars { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; font-weight: 600; color: var(--accent-ink);
|
||
background: var(--chip-bg); border: 1px solid var(--line); border-radius: 7px; padding: 1px 7px; font-variant-numeric: tabular-nums; }
|
||
.theme-toggle { display: inline-grid; place-items: center; width: 36px; height: 36px; border-radius: 11px;
|
||
border: 1px solid var(--line); background: var(--chip-bg); color: var(--ink-2); cursor: pointer; transition: color .18s, border-color .18s; }
|
||
.theme-toggle:hover { color: var(--ink); border-color: var(--ink-3); }
|
||
.theme-toggle svg { width: 17px; height: 17px; }
|
||
.theme-toggle .icon-moon { display: none; }
|
||
[data-theme="light"] .theme-toggle .icon-moon { display: block; }
|
||
[data-theme="light"] .theme-toggle .icon-sun { display: none; }
|
||
|
||
/* Mobile nav drawer */
|
||
.nav-menu { display: flex; align-items: center; gap: 4px; }
|
||
.nav-toggle { display: none; position: relative; z-index: 70; place-items: center; width: 38px; height: 38px;
|
||
border-radius: 11px; border: 1px solid var(--line); background: var(--chip-bg); color: var(--ink); cursor: pointer;
|
||
transition: color .18s, border-color .18s; }
|
||
.nav-toggle:hover { border-color: var(--ink-3); }
|
||
.nav-toggle svg { width: 19px; height: 19px; }
|
||
.nav-toggle .icon-x { display: none; }
|
||
.nav-toggle.open .icon-bars { display: none; }
|
||
.nav-toggle.open .icon-x { display: block; }
|
||
.nav-scrim { position: fixed; inset: 0; z-index: 55; background: rgba(2,5,10,0.5);
|
||
-webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); opacity: 0; visibility: hidden;
|
||
transition: opacity .3s var(--ease), visibility .3s; }
|
||
.nav-scrim.open { opacity: 1; visibility: visible; }
|
||
.nav-menu.open { transform: none; }
|
||
@media (max-width: 860px) {
|
||
.nav-toggle { display: inline-grid; }
|
||
.nav-menu { position: fixed; top: 0; right: 0; height: 100dvh; width: min(82vw, 320px);
|
||
flex-direction: column; align-items: stretch; gap: 6px; padding: 80px 16px 28px;
|
||
background: var(--glass-bg-fallback); border-left: 1px solid var(--line);
|
||
box-shadow: -24px 0 64px -28px rgba(0,0,0,0.6); transform: translateX(106%);
|
||
transition: transform .34s var(--ease); overflow-y: auto; z-index: 60; }
|
||
.nav-menu .nav-link { padding: 12px 14px; font-size: 0.98rem; border-radius: 11px; color: var(--ink-2); }
|
||
.nav-menu .nav-link.active { color: var(--ink); background: var(--chip-bg); }
|
||
.nav-menu .nav-link svg { width: 19px; height: 19px; }
|
||
.nav-menu .nav-link-stars { margin-left: auto; }
|
||
}
|
||
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
||
@media (max-width: 860px) {
|
||
.nav-menu { background: var(--glass-bg); -webkit-backdrop-filter: blur(22px) saturate(1.4); backdrop-filter: blur(22px) saturate(1.4); }
|
||
}
|
||
}
|
||
|
||
/* ===================== Palette switcher ===================== */
|
||
/* Palette switcher hidden from the header (default palette = Steel still applies). */
|
||
.palette-switch { display: none; align-items: center; gap: 5px; padding: 4px; border-radius: 13px;
|
||
border: 1px solid var(--line); background: var(--chip-bg); }
|
||
.palette-switch-label { font-size: 0.62rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;
|
||
color: var(--ink-3); padding: 0 4px 0 6px; }
|
||
.palette-swatch { width: 24px; height: 24px; border-radius: 8px; cursor: pointer; padding: 0; position: relative;
|
||
border: 1px solid rgba(255,255,255,0.18); overflow: hidden;
|
||
background: linear-gradient(135deg, var(--sw-accent) 0 52%, var(--sw-grey) 52% 100%); }
|
||
[data-theme="light"] .palette-swatch { border-color: rgba(20,40,80,0.18); }
|
||
.palette-swatch:hover { transform: translateY(-1px); }
|
||
.palette-swatch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||
.palette-swatch[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent); }
|
||
@media (max-width: 860px) { .palette-switch-label { display: none; } }
|
||
@media (max-width: 520px) { .palette-switch { display: none; } }
|
||
/* compact popover variant for small screens */
|
||
.palette-popwrap { position: relative; display: none; }
|
||
.palette-pop-btn { display: inline-grid; place-items: center; width: 36px; height: 36px; border-radius: 11px;
|
||
border: 1px solid var(--line); background: var(--chip-bg); color: var(--ink-2); cursor: pointer; }
|
||
.palette-pop-btn:hover { color: var(--ink); border-color: var(--ink-3); }
|
||
.palette-pop-btn svg { width: 17px; height: 17px; }
|
||
.palette-pop-panel { position: absolute; right: 0; top: calc(100% + 8px); display: none; flex-direction: column; gap: 6px;
|
||
padding: 10px; border-radius: 13px; border: 1px solid var(--glass-border); background: var(--glass-bg-fallback);
|
||
box-shadow: var(--glass-shadow-hi); z-index: 50; min-width: 150px; }
|
||
.palette-popwrap.open .palette-pop-panel { display: flex; }
|
||
.palette-pop-panel .pp-row { display: flex; align-items: center; gap: 9px; padding: 6px 8px; border-radius: 9px;
|
||
border: 1px solid transparent; background: transparent; color: var(--ink-2); cursor: pointer; font-size: 0.84rem; font-weight: 600; font-family: inherit; }
|
||
.palette-pop-panel .pp-row:hover { background: var(--chip-bg); color: var(--ink); }
|
||
.palette-pop-panel .pp-row[aria-pressed="true"] { color: var(--ink); border-color: var(--accent); }
|
||
.palette-pop-panel .pp-dot { width: 18px; height: 18px; border-radius: 6px; flex-shrink: 0;
|
||
background: linear-gradient(135deg, var(--sw-accent) 0 52%, var(--sw-grey) 52% 100%); }
|
||
|
||
/* ===================== Reveal motion ===================== */
|
||
.reveal { opacity: 0; transform: translateY(22px); transition: opacity .7s var(--ease), transform .7s var(--ease); }
|
||
.reveal.in { opacity: 1; transform: none; }
|
||
[style*="--d"] { transition-delay: var(--d); }
|
||
|
||
/* ===================== Hero ===================== */
|
||
.hero { max-width: 1180px; margin: 0 auto; padding: 70px 22px 24px; text-align: center; }
|
||
.hero-title { display: flex; align-items: center; justify-content: center; gap: clamp(14px, 2.4vw, 22px); }
|
||
/* Badge scales with the serif title so it reads as a peer of the wordmark (≈ title
|
||
font-size), not a small chip. The inner glyph tracks the tile size. */
|
||
.hero-title-badge { width: clamp(2.6rem, 7.4vw, 5rem); height: clamp(2.6rem, 7.4vw, 5rem);
|
||
display: grid; place-items: center; border-radius: clamp(15px, 1.8vw, 22px); flex-shrink: 0;
|
||
border: 1px solid var(--glass-border); background: var(--chip-bg); box-shadow: var(--glass-shadow); }
|
||
.hero-title-badge svg { width: 64%; height: 64%; }
|
||
/* Premium Serif Display title (V1 "Accent on Anything"): Newsreader serif,
|
||
"Anything" in serif italic carrying the solid palette accent, while "CLI-" and
|
||
"Hub" stay ink. No gradient. The accent uses the palette's accent2 in light and
|
||
accent in dark (legibility-tuned), so the word recolors with the palette switcher. */
|
||
.hero h1 { font-family: 'Newsreader', serif; font-weight: 400; color: var(--ink);
|
||
font-size: clamp(2.7rem, 7.6vw, 5.1rem); letter-spacing: -0.018em; line-height: 1.0; }
|
||
.hero-word { display: inline-block; }
|
||
.hero-word--cli, .hero-word--hub { color: var(--ink); }
|
||
.hero-word--anything { font-style: italic; font-weight: 300; letter-spacing: 0; color: var(--accent2); }
|
||
[data-theme="dark"] .hero-word--anything { color: var(--accent); }
|
||
.hero-tagline { max-width: 60ch; margin: 20px auto 0; color: var(--ink-2); font-size: clamp(1rem, 2.2vw, 1.18rem); }
|
||
.hero-tagline-line--any { color: var(--ink); font-weight: 600; }
|
||
|
||
.stats { display: flex; justify-content: center; gap: 12px; flex-wrap: wrap; margin-top: 30px; }
|
||
.stat { padding: 13px 24px; border-radius: 15px; text-align: center; min-width: 130px;
|
||
background: var(--glass-bg-fallback); border: 1px solid var(--glass-border); box-shadow: var(--glass-shadow); position: relative; }
|
||
@media (hover: hover) and (pointer: fine) {
|
||
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
||
.stat { background: var(--glass-bg); -webkit-backdrop-filter: blur(18px) saturate(1.4); backdrop-filter: blur(18px) saturate(1.4); }
|
||
}
|
||
}
|
||
/* Stat figures use the SAME two-token accent rule as the italic "Anything" word in the
|
||
title (accent2 in light, accent in dark), so they recolor with palette + theme, and
|
||
are enlarged to read as large figures like the remote reference. */
|
||
.stat-num { font-size: clamp(2.1rem, 4.4vw, 2.7rem); font-weight: 800; letter-spacing: -0.03em;
|
||
font-variant-numeric: tabular-nums; line-height: 1.05; color: var(--accent2); }
|
||
[data-theme="dark"] .stat-num { color: var(--accent); }
|
||
.stat-label { font-size: 0.72rem; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.1em; font-weight: 600; margin-top: 2px; }
|
||
|
||
/* ===================== Empower cards ===================== */
|
||
/* align-items: stretch makes the two side-by-side cards equal height; each card is a
|
||
column flexbox so its inner content distributes evenly down to the matched height. */
|
||
.empower-row { display: grid; gap: 18px; grid-template-columns: 1fr 1fr; margin-top: 38px; text-align: left; align-items: stretch; }
|
||
/* grid items default to min-width:auto, so the no-wrap install command was forcing the
|
||
card — and the whole page — wider than a phone screen (627px on a 390px viewport) */
|
||
.empower-row > .empower-card { min-width: 0; }
|
||
@media (max-width: 820px) { .empower-row { grid-template-columns: 1fr; } }
|
||
.empower-card { padding: 24px 24px 22px; border-radius: var(--radius); border: 1px solid var(--glass-border);
|
||
background: var(--glass-bg-fallback); box-shadow: var(--glass-shadow); position: relative;
|
||
display: flex; flex-direction: column; }
|
||
@media (hover: hover) and (pointer: fine) {
|
||
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
||
.empower-card { background: var(--glass-bg); -webkit-backdrop-filter: blur(20px) saturate(1.4); backdrop-filter: blur(20px) saturate(1.4); }
|
||
}
|
||
}
|
||
.empower-card::before { content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
|
||
box-shadow: inset 0 1px 0 var(--glass-edge); }
|
||
.empower-card h3 { font-size: 1.16rem; font-weight: 700; }
|
||
.empower-card h3 span { color: var(--accent2); }
|
||
[data-theme="dark"] .empower-card h3 span { color: var(--accent); }
|
||
.empower-available { font-size: 0.84rem; color: var(--ink-3); margin-top: 8px; }
|
||
.empower-available a { color: var(--accent-ink); text-decoration: none; font-weight: 600; }
|
||
.empower-available a:hover { text-decoration: underline; }
|
||
.install-row { display: flex; align-items: center; gap: 0; margin-top: 14px; border-radius: 13px; overflow: hidden;
|
||
border: 1px solid var(--cmd-line); background: var(--cmd-bg); }
|
||
.install-row--wrap { flex-wrap: wrap; }
|
||
.install-row .chip-label { font-family: 'JetBrains Mono', monospace; font-size: 0.68rem; font-weight: 600; text-transform: lowercase;
|
||
color: var(--ink-3); padding: 0 12px; align-self: stretch; display: inline-flex; align-items: center; border-right: 1px solid var(--cmd-line); white-space: nowrap; }
|
||
.install-row code { font-family: 'JetBrains Mono', monospace; font-size: 0.82rem; padding: 12px 14px; color: var(--ink);
|
||
flex: 1; min-width: 0; overflow-x: auto; white-space: nowrap; }
|
||
.copy-btn { border: 0; border-left: 1px solid var(--cmd-line); background: var(--chip-bg); color: var(--ink-2);
|
||
padding: 0 16px; align-self: stretch; cursor: pointer; font-size: 0.78rem; font-weight: 600;
|
||
transition: color .18s, background .18s; white-space: nowrap; }
|
||
.copy-btn:hover { color: var(--ink); background: color-mix(in oklab, var(--accent) 18%, transparent); }
|
||
.empower-note { font-size: 0.84rem; color: var(--ink-2); margin-top: 14px; }
|
||
.empower-note strong { color: var(--ink); }
|
||
.empower-prompt { font-size: 0.85rem; color: var(--ink-2); margin-top: 10px; padding: 11px 13px; border-radius: 11px;
|
||
border: 1px solid var(--line); background: var(--chip-bg); }
|
||
.empower-prompt strong { color: var(--ink); }
|
||
.empower-prompt em { color: var(--accent-ink); font-style: italic; }
|
||
|
||
/* Toolkit grid: compressed rows (tighter padding, smaller gaps) so the "Empower yourself"
|
||
card fills its share of the matched equal-height row cleanly without looking sparse. */
|
||
.cmd-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 14px; flex: 1 1 auto; align-content: stretch; }
|
||
@media (max-width: 420px) { .cmd-grid { grid-template-columns: 1fr; } }
|
||
.cmd-item { display: flex; flex-direction: column; justify-content: center; gap: 0; padding: 7px 11px; border-radius: 9px;
|
||
border: 1px solid var(--line); background: var(--chip-bg); }
|
||
.cmd-name { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; color: var(--accent-ink); font-weight: 500; line-height: 1.35; }
|
||
.cmd-desc { font-size: 0.72rem; color: var(--ink-3); line-height: 1.3; }
|
||
|
||
/* ===================== Hero actions ===================== */
|
||
.hero-actions { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-top: 34px; }
|
||
.btn-primary, .btn-secondary { display: inline-flex; align-items: center; gap: 8px; padding: 12px 20px; border-radius: 13px;
|
||
font-weight: 600; font-size: 0.9rem; text-decoration: none; cursor: pointer; transition: transform .15s, border-color .18s, background .18s; }
|
||
.btn-primary { color: #fff; border: 1px solid transparent;
|
||
background: linear-gradient(120deg, var(--accent2), var(--accent)); box-shadow: 0 12px 30px -14px rgba(var(--accent-rgb), 0.7); }
|
||
.btn-primary:hover { transform: translateY(-2px); }
|
||
.btn-secondary { color: var(--ink); border: 1px solid var(--glass-border); background: var(--chip-bg); }
|
||
.btn-secondary:hover { transform: translateY(-2px); border-color: var(--ink-3); }
|
||
.btn-secondary svg { width: 15px; height: 15px; }
|
||
|
||
.hero-hint { margin-top: 18px; font-size: 0.86rem; color: var(--ink-3); }
|
||
.hero-hint a { color: var(--accent-ink); font-weight: 600; text-decoration: none; }
|
||
.hero-hint a:hover { text-decoration: underline; }
|
||
.hero-cta { max-width: 760px; margin: 26px auto 0; font-size: 0.9rem; color: var(--ink-2); text-align: center; }
|
||
.hero-cta .nowrap { white-space: nowrap; }
|
||
.hero-cta em { color: var(--accent-ink); font-style: normal; font-weight: 700; }
|
||
.hero-cta a { color: var(--accent-ink); font-weight: 600; text-decoration: none; }
|
||
.hero-cta a:hover { text-decoration: underline; }
|
||
|
||
/* ===================== Marketplace prelude / typing shell =====================
|
||
Wide-but-short, matching the remote design's compact proportions: the shell
|
||
spans the column (no narrow max-width), the caption + Copy sit on one short
|
||
header row, and the command line is a single slim band — re-themed to glass. */
|
||
.marketplace-prelude { max-width: 1180px; margin: 36px auto 0; padding: 0 22px; }
|
||
.typing-shell { width: 100%; max-width: 720px; margin: 0 auto; border-radius: var(--radius-sm); overflow: hidden;
|
||
padding: 13px 15px 14px; border: 1px solid var(--glass-border); background: var(--glass-bg-fallback);
|
||
box-shadow: var(--glass-shadow); position: relative; }
|
||
@media (hover: hover) and (pointer: fine) {
|
||
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
||
.typing-shell { background: var(--glass-bg); -webkit-backdrop-filter: blur(20px) saturate(1.4); backdrop-filter: blur(20px) saturate(1.4); }
|
||
}
|
||
}
|
||
.typing-shell-head { display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
||
margin-bottom: 9px; }
|
||
.typing-shell-label { display: inline-flex; align-items: center; gap: 7px; font-size: 0.7rem; color: var(--ink-3);
|
||
letter-spacing: 0.05em; text-transform: uppercase; font-weight: 600; font-style: italic; }
|
||
.typing-shell-label::before { content: ''; width: 7px; height: 7px; border-radius: 999px; background: var(--ok);
|
||
box-shadow: 0 0 12px color-mix(in oklab, var(--ok) 60%, transparent); flex-shrink: 0; }
|
||
.typing-shell-copy { border: 1px solid var(--cmd-line); border-left: 1px solid var(--cmd-line); border-radius: 9px;
|
||
padding: 5px 13px; flex-shrink: 0; }
|
||
.typing-shell-body { display: flex; align-items: center; gap: 9px; min-height: 0; padding: 9px 12px;
|
||
border-radius: 9px; border: 1px solid var(--cmd-line); background: var(--cmd-bg); }
|
||
.typing-shell-prompt { color: var(--ok); font-family: 'JetBrains Mono', monospace; font-weight: 600; font-size: 0.84rem; flex-shrink: 0; }
|
||
.typing-shell-code { flex: 1; min-width: 0; font-family: 'JetBrains Mono', monospace; font-size: 0.84rem; color: var(--ink);
|
||
display: inline-flex; align-items: center; white-space: nowrap; overflow: hidden; }
|
||
.typing-shell-cursor { display: inline-block; width: 8px; height: 1.05em; background: var(--accent); margin-left: 2px; animation: blink 1.05s steps(1) infinite; }
|
||
@keyframes blink { 0%,50%{opacity:1} 50.01%,100%{opacity:0} }
|
||
|
||
/* ===================== Deck nav ===================== */
|
||
.deck-nav { max-width: 1180px; margin: 56px auto 18px; padding: 0 22px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||
.deck-tab { display: inline-flex; align-items: center; gap: 9px; padding: 10px 16px; border-radius: 999px; cursor: pointer;
|
||
border: 1px solid var(--line); background: var(--chip-bg); color: var(--ink-2); font-size: 0.9rem; font-weight: 600;
|
||
transition: color .18s, border-color .18s, background .18s; }
|
||
.deck-tab:hover { color: var(--ink); }
|
||
.deck-tab.active { color: var(--bg); background: var(--ink); border-color: var(--ink); }
|
||
[data-theme="light"] .deck-tab.active { color: #fff; }
|
||
.deck-count { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; font-weight: 600; opacity: 0.7; font-variant-numeric: tabular-nums; }
|
||
.deck-separator { flex: 1; }
|
||
.deck-arrow-btn { display: inline-flex; align-items: center; gap: 7px; padding: 9px 14px; border-radius: 11px; cursor: pointer;
|
||
border: 1px solid var(--glass-border); background: var(--chip-bg); color: var(--ink-2); font-size: 0.84rem; font-weight: 600;
|
||
transition: color .18s, border-color .18s; }
|
||
.deck-arrow-btn:hover { color: var(--ink); border-color: var(--ink-3); }
|
||
.deck-arrow-btn svg { width: 16px; height: 16px; }
|
||
.deck-arrow-btn--back { flex-direction: row-reverse; }
|
||
@media (max-width: 560px) { .deck-arrow-btn { display: none; } .deck-separator { display: none; } }
|
||
|
||
/* ===================== Deck stage ===================== */
|
||
/* ── Render-skip for offscreen page regions ──
|
||
content-visibility:auto lets the engine skip layout+paint for sections far
|
||
from the viewport, so a fast fling doesn't rasterize ~30k px of cards at
|
||
once (the iOS Safari tab-kill scenario). JS measurement inside a skipped
|
||
subtree (masonry offsetHeight) still works: layout APIs force layout, only
|
||
paint stays skipped. contain-intrinsic-size uses the `auto` keyword so the
|
||
placeholder snaps to the real size after first render; the px values are
|
||
only the pre-render estimate. Engines without support just ignore this. */
|
||
/* Each rule sets a plain-length fallback first: Safari 15.4–16 supports
|
||
content-visibility but not the `auto` keyword here, and would otherwise
|
||
drop the whole declaration and give skipped sections no height. */
|
||
.demos-teaser, .marketplace-prelude, .matrices, .deck-stage, .footer { content-visibility: auto; }
|
||
.demos-teaser { contain-intrinsic-size: 1000px; contain-intrinsic-size: auto 1000px; }
|
||
.marketplace-prelude { contain-intrinsic-size: 220px; contain-intrinsic-size: auto 220px; }
|
||
.matrices { contain-intrinsic-size: 900px; contain-intrinsic-size: auto 900px; }
|
||
.deck-stage { contain-intrinsic-size: 9500px; contain-intrinsic-size: auto 9500px; }
|
||
@media (max-width: 640px) { .deck-stage { contain-intrinsic-size: 7200px; contain-intrinsic-size: auto 7200px; } }
|
||
.footer { contain-intrinsic-size: 320px; contain-intrinsic-size: auto 320px; }
|
||
|
||
.deck-stage { max-width: 1180px; margin: 0 auto; padding: 0 22px; }
|
||
.deck-viewport { overflow: hidden; touch-action: pan-y; cursor: grab; }
|
||
.deck-viewport.dragging { cursor: grabbing; }
|
||
/* will-change only while the deck actually moves (deck-sliding is armed on every drag or
|
||
animated switch) — a permanent will-change would pin the full-height two-panel track
|
||
as a composited layer, a large standing GPU allocation on phones. */
|
||
.deck-track { display: flex; transition: transform .5s var(--spring); }
|
||
html.deck-sliding .deck-track { will-change: transform; }
|
||
.deck-track.is-dragging { transition: none; }
|
||
/* Each panel is exactly one viewport wide; the track slides by one full panel width
|
||
between decks. (Replaces the old over-wide "peek" carousel that mis-measured its
|
||
width and clipped the active panel on the right while bleeding the other on the left.) */
|
||
.deck-panel { flex: 0 0 100%; min-width: 0; position: relative; }
|
||
.deck-sentinel { height: 1px; }
|
||
/* Perf — Safari especially. While the deck moves we collapse every effect that forces a
|
||
per-frame re-rasterization of the large card subtree, so each panel caches as a flat
|
||
layer the GPU can translate cheaply. All effects return the moment motion stops.
|
||
• per-card glass backdrop-blur (cheap on Chrome, costly on Safari)
|
||
• the ~150 3D flip contexts (perspective + preserve-3d + backface-visibility) — the
|
||
real Safari cliff: Safari cannot flatten them and re-renders each context per frame.
|
||
Flattening to transform-style:flat keeps the correct face visible via
|
||
backface-visibility, with no flip glitch on settle.
|
||
• the sticky-nav backdrop-blur and the blend-mode grain overlay. */
|
||
html.deck-sliding .card-face,
|
||
html.deck-sliding .nav.scrolled { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; }
|
||
html.deck-sliding .card { perspective: none !important; }
|
||
html.deck-sliding .card-inner { transform-style: flat !important; }
|
||
html.deck-sliding .grain { visibility: hidden; }
|
||
/* Pager dots + swipe hint — signal the deck is a swipeable two-panel carousel. */
|
||
.deck-dots { display: inline-flex; align-items: center; gap: 7px; margin-left: 2px; }
|
||
.deck-dot { width: 8px; height: 8px; padding: 0; border: 0; border-radius: 999px; cursor: pointer;
|
||
background: var(--ink-3); opacity: 0.4; transition: width .25s var(--ease), opacity .2s, background .2s; }
|
||
.deck-dot:hover { opacity: 0.7; }
|
||
.deck-dot.active { width: 22px; opacity: 1; background: var(--accent); }
|
||
.deck-hint { display: inline-flex; align-items: center; gap: 6px; font-size: 0.74rem; color: var(--ink-3); white-space: nowrap; }
|
||
.deck-hint svg { width: 16px; height: 16px; opacity: 0.8; }
|
||
@media (max-width: 600px) { .deck-hint span { display: none; } }
|
||
|
||
/* ===================== Controls (search + sort) ===================== */
|
||
.controls { display: flex; gap: 12px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
|
||
.search { flex: 1; min-width: 220px; padding: 13px 16px; border-radius: 13px; border: 1px solid var(--glass-border);
|
||
background: var(--chip-bg); color: var(--ink); font-size: 0.92rem; font-family: inherit; transition: border-color .18s, background .18s; }
|
||
.search::placeholder { color: var(--ink-3); }
|
||
.search:focus { outline: none; border-color: var(--accent); background: color-mix(in oklab, var(--accent) 8%, var(--chip-bg)); }
|
||
.sort-select { padding: 12px 14px; border-radius: 13px; border: 1px solid var(--glass-border); background: var(--chip-bg);
|
||
color: var(--ink); font-size: 0.86rem; font-family: inherit; font-weight: 500; cursor: pointer; transition: border-color .18s; }
|
||
.sort-select:focus { outline: none; border-color: var(--accent); }
|
||
.sort-select option { background: var(--bg-2); color: var(--ink); }
|
||
|
||
.public-panel-note { font-size: 0.82rem; color: var(--ink-3); margin-bottom: 14px; padding: 12px 15px; border-radius: 12px;
|
||
border: 1px solid var(--line); background: var(--chip-bg); max-width: 80ch; }
|
||
|
||
/* ===================== Filter chips ===================== */
|
||
.filter-row { display: flex; gap: 7px; flex-wrap: wrap; margin-bottom: 22px; }
|
||
.filter-btn { padding: 7px 14px; border-radius: 999px; border: 1px solid var(--line); background: var(--chip-bg);
|
||
color: var(--ink-2); font-size: 0.8rem; font-weight: 600; cursor: pointer; transition: color .15s, border-color .15s, background .15s; }
|
||
.filter-btn:hover { color: var(--ink); }
|
||
.filter-btn.active { color: var(--bg); background: var(--ink); border-color: var(--ink); }
|
||
[data-theme="light"] .filter-btn.active { color: #fff; }
|
||
|
||
/* ===================== Card grid + 3D flip cards ===================== */
|
||
/* Masonry: cards interlock into balanced columns so uneven heights fill the vertical
|
||
gaps — an abundant, staggered "琳琅满目" wall of CLIs rather than a ragged row grid.
|
||
Engine-independent JS masonry (layoutMasonry): a flex row of .grid-col wrappers,
|
||
each a vertical flex column; cards are distributed into the SHORTEST column as we go.
|
||
(CSS multicol balancing was buggy — Safari left empty tops in columns 2–3.)
|
||
max-width caps usable width because the deck-panel is intentionally over-wide for
|
||
the swipe carousel; columns land at ~3 across desktop / 2 tablet / 1 phone. */
|
||
/* Cap to the visible viewport (not the over-wide carousel panel) so the grid's own width
|
||
tracks the real screen — this lets masonryColumnCount pick 3/2/1 columns responsively. */
|
||
.grid { display: flex; align-items: flex-start; gap: 18px; max-width: min(1044px, calc(100vw - 108px)); padding-bottom: 24px; }
|
||
/* phones: the panel is exactly viewport-wide now, so let cards use the full column
|
||
instead of the desktop's defensive 100vw-108px cap (which left ~60px dead space) */
|
||
@media (max-width: 640px) { .grid { max-width: 100%; } }
|
||
.grid-col { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 18px; }
|
||
/* the .empty / error state is a direct child of .grid (not wrapped in a column) */
|
||
.grid > .empty { flex: 1 1 100%; }
|
||
|
||
/* Cards are FLAT by default. Safari cannot flatten a perspective + preserve-3d +
|
||
backface-visibility context and re-renders each one per frame — with ~150 of them this
|
||
is the biggest static cost on the page (the deck-slide suspender below already proved
|
||
the flat state renders the correct face via backface-visibility with no glitch).
|
||
The real 3D context is enabled per-card only while it is hovered or mid-flip (.is-3d,
|
||
set by setCardFlipped for the duration of the flip transition). */
|
||
.card { position: relative; }
|
||
.card-inner { position: absolute; inset: 0; width: 100%; height: 100%;
|
||
transform-style: flat; transition: transform .72s var(--flip-ease); }
|
||
.card.flipped .card-inner { transform: rotateY(180deg); }
|
||
.card.is-3d { perspective: 1600px; }
|
||
.card.is-3d .card-inner { transform-style: preserve-3d; }
|
||
/* subtle hover tilt hint (does not auto-flip) */
|
||
@media (hover: hover) {
|
||
.card:hover { perspective: 1600px; }
|
||
.card:hover .card-inner { transform-style: preserve-3d; }
|
||
.card:not(.flipped):hover .card-inner { transform: rotateY(-5deg) rotateX(2.5deg); }
|
||
}
|
||
.card-face {
|
||
position: absolute; inset: 0; display: flex; flex-direction: column;
|
||
-webkit-backface-visibility: hidden; backface-visibility: hidden;
|
||
border-radius: var(--radius); border: 1px solid var(--glass-border);
|
||
background:
|
||
radial-gradient(120% 90% at 0% 0%, hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.16), transparent 56%),
|
||
var(--glass-bg-fallback);
|
||
box-shadow: var(--glass-shadow); overflow: hidden;
|
||
}
|
||
/* ~110 live card faces at once — the single biggest backdrop-filter population on the
|
||
page. Desktop pointers only (see the .glass note above). */
|
||
@media (hover: hover) and (pointer: fine) {
|
||
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
||
.card-face { background:
|
||
radial-gradient(120% 90% at 0% 0%, hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.18), transparent 56%),
|
||
var(--glass-bg);
|
||
-webkit-backdrop-filter: blur(18px) saturate(1.4); backdrop-filter: blur(18px) saturate(1.4); }
|
||
}
|
||
}
|
||
.card-face::before { content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
|
||
box-shadow: inset 0 1px 0 var(--glass-edge); z-index: 3; }
|
||
.card-front { padding: 18px 19px; }
|
||
.card-back { transform: rotateY(180deg); padding: 16px 17px; }
|
||
|
||
/* content-sized: .card-sizer is in normal flow and its height follows the FRONT face
|
||
only (the back is taken out of flow), so the card fits its compact front content
|
||
with no empty bottom gap. .card-inner overlays absolutely on top for the real 3D
|
||
flip; when flipped, the back fills that same height and scrolls internally. */
|
||
.card-sizer { position: relative; width: 100%; }
|
||
.card-sizer .card-face { position: relative; box-shadow: none; border: 1px solid transparent; background: none; visibility: hidden;
|
||
-webkit-backdrop-filter: none; backdrop-filter: none; }
|
||
.card-sizer .card-back { position: absolute; inset: 0; transform: none; }
|
||
|
||
/* FRONT face content */
|
||
.card-head { display: flex; align-items: flex-start; gap: 10px; }
|
||
.card-title { font-size: 1.04rem; font-weight: 700; flex: 1; min-width: 0; line-height: 1.25; }
|
||
.card-title a { text-decoration: none; color: var(--ink); display: inline-flex; align-items: center; gap: 5px; }
|
||
.card-title a:hover { color: var(--accent-ink); }
|
||
.card-title svg { width: 12px; height: 12px; flex-shrink: 0; opacity: 0.65; }
|
||
.card-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 5px; flex-shrink: 0; }
|
||
.category-tag { font-size: 0.62rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; padding: 3px 8px; border-radius: 7px;
|
||
color: hsl(var(--ch, var(--hue-default)) 80% 76%); background: hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.16);
|
||
border: 1px solid hsl(var(--ch, var(--hue-default)) 70% 64% / 0.32); white-space: nowrap; }
|
||
[data-theme="light"] .category-tag { color: hsl(var(--ch, var(--hue-default)) 72% 38%); }
|
||
.card-version { font-family: 'JetBrains Mono', monospace; font-size: 0.66rem; color: var(--ink-3); font-weight: 500; }
|
||
.public-badge { font-size: 0.6rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; padding: 3px 7px; border-radius: 6px;
|
||
border: 1px solid var(--line); background: var(--chip-bg); color: var(--ink-2); white-space: nowrap; }
|
||
.public-badge--npm { color: #d98a4a; border-color: #d98a4a55; }
|
||
.public-badge--brew { color: #c79cff; border-color: #c79cff55; }
|
||
.public-badge--bundled { color: var(--ink-3); }
|
||
.card-desc { color: var(--ink-2); font-size: 0.86rem; margin-top: 11px;
|
||
display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
||
.card-date { font-size: 0.72rem; color: var(--ink-3); margin-top: 10px; }
|
||
.card-requires { font-size: 0.76rem; color: var(--ink-2); margin-top: 8px; }
|
||
.card-requires strong { color: var(--ink); font-weight: 700; }
|
||
|
||
/* install block (used on both faces where applicable) */
|
||
.card-install { display: flex; align-items: stretch; gap: 0; margin-top: 8px; border-radius: 11px; overflow: hidden;
|
||
border: 1px solid var(--cmd-line); background: var(--cmd-bg); }
|
||
.card-install code { font-family: 'JetBrains Mono', monospace; font-size: 0.74rem; color: var(--ink); padding: 9px 11px;
|
||
flex: 1; min-width: 0; overflow: hidden; display: flex; flex-direction: column; gap: 3px; }
|
||
/* Each command renders on exactly ONE line; long lines truncate with an ellipsis.
|
||
The full, untruncated text is preserved in the Copy payload (display only). */
|
||
.card-install .install-command { color: var(--accent-ink); display: block; min-width: 0; line-height: 1.5;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.card-install--note code { color: var(--ink-3); display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
/* two-step install: a short plain-language label above the clean command box */
|
||
.install-steps { margin-top: 14px; }
|
||
.install-steps-label { display: flex; align-items: center; gap: 7px; font-size: 0.7rem; font-weight: 600; color: var(--ink-3);
|
||
letter-spacing: 0.01em; margin-bottom: 5px; }
|
||
.install-steps-label .isl-num { display: inline-grid; place-items: center; width: 15px; height: 15px; border-radius: 5px; flex-shrink: 0;
|
||
font-family: 'JetBrains Mono', monospace; font-size: 0.6rem; font-weight: 700; color: var(--accent-ink);
|
||
background: hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.16); border: 1px solid hsl(var(--ch, var(--hue-default)) var(--tint-s) 60% / 0.3); }
|
||
.install-steps-label .isl-sep { color: var(--ink-3); opacity: 0.55; }
|
||
.card-copy-btn { border: 0; border-left: 1px solid var(--cmd-line); background: var(--chip-bg); color: var(--ink-2);
|
||
padding: 0 13px; cursor: pointer; font-size: 0.72rem; font-weight: 600; white-space: nowrap; transition: color .18s, background .18s; align-self: stretch; }
|
||
.card-copy-btn:hover { color: var(--ink); background: color-mix(in oklab, var(--accent) 18%, transparent); }
|
||
|
||
.card-public-notes { margin-top: 10px; display: flex; flex-direction: column; gap: 8px; }
|
||
.card-npx { font-size: 0.76rem; color: var(--ink-2); }
|
||
.card-npx--direct strong { color: var(--ink); font-weight: 700; display: block; margin-bottom: 2px; }
|
||
.card-npx--direct span { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--accent-ink); word-break: break-all; }
|
||
.card-npx--comment { padding-left: 11px; border-left: 2px solid var(--line); color: var(--ink-3); }
|
||
|
||
.card-footer { margin-top: auto; padding-top: 14px; display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
|
||
.card-links { display: flex; align-items: center; gap: 4px; font-size: 0.78rem; color: var(--ink-3); }
|
||
.card-links a { color: var(--accent-ink); text-decoration: none; font-weight: 600; }
|
||
.card-links a:hover { text-decoration: underline; }
|
||
.card-contributor { font-size: 0.76rem; color: var(--ink-3); display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||
.card-contributor a { color: var(--ink-2); text-decoration: none; font-weight: 500; }
|
||
.card-contributor a:hover { color: var(--ink); text-decoration: underline; }
|
||
.community-badge { font-size: 0.58rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; padding: 2px 6px; border-radius: 5px;
|
||
color: var(--ok); border: 1px solid color-mix(in oklab, var(--ok) 45%, transparent); background: color-mix(in oklab, var(--ok) 14%, transparent); }
|
||
|
||
/* flip affordance — a clear "Details" / "Back" pill (NOT a refresh glyph) */
|
||
.card-front-foot { margin-top: 14px; padding-top: 13px; display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||
.flip-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: 10px; cursor: pointer;
|
||
border: 1px solid var(--glass-border); background: var(--chip-bg); color: var(--ink-2); font-size: 0.76rem; font-weight: 600;
|
||
font-family: inherit; transition: color .18s, border-color .18s, background .18s, transform .15s; flex-shrink: 0; }
|
||
.flip-btn:hover { color: var(--ink); border-color: hsl(var(--ch, var(--hue-default)) 70% 64% / 0.6);
|
||
background: hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.14); transform: translateY(-1px); }
|
||
.flip-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||
.flip-btn svg { width: 14px; height: 14px; flex-shrink: 0; }
|
||
.flip-btn--back svg { transform: scaleX(-1); }
|
||
.flip-hint { font-size: 0.7rem; color: var(--ink-3); font-weight: 600; }
|
||
|
||
/* ===================== Software icons (Simple Icons) ===================== */
|
||
/* Unified rounded tile; the <img> is the single-color Simple Icons SVG tinted to the
|
||
palette ink; the .icon-mono fallback is a tasteful single-letter monogram. */
|
||
.sw-icon { display: inline-grid; place-items: center; width: 26px; height: 26px; border-radius: 8px; flex-shrink: 0;
|
||
background: hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.14);
|
||
border: 1px solid hsl(var(--ch, var(--hue-default)) var(--tint-s) 60% / 0.28); overflow: hidden; }
|
||
.sw-icon img { width: 15px; height: 15px; object-fit: contain; display: block;
|
||
/* Simple Icons are single-color SVGs; flatten them to a cohesive monochrome glyph that
|
||
stays legible in both themes (near-white on dark, near-ink on light). */
|
||
filter: var(--icon-tint); }
|
||
/* Favicon-sourced logos (apps Simple Icons lacks) keep their real colour — no monochrome
|
||
flatten — and sit a touch larger since they're already a rounded glyph. */
|
||
.sw-icon img.raw { filter: none; width: 18px; height: 18px; border-radius: 4px; }
|
||
.sw-icon .icon-mono { font-family: 'Hanken Grotesk', sans-serif; font-size: 0.8rem; font-weight: 700; line-height: 1;
|
||
color: hsl(var(--ch, var(--hue-default)) 70% 70%); text-transform: uppercase; }
|
||
[data-theme="light"] .sw-icon .icon-mono { color: hsl(var(--ch, var(--hue-default)) calc(var(--tint-s) + 14%) 40%); }
|
||
/* dark theme: glyph → light ink; light theme: glyph → dark ink. brightness(0) flattens any
|
||
source color to black, then invert/opacity lifts it to the right tone for the theme. */
|
||
:root { --icon-tint: brightness(0) invert(1) opacity(0.86); }
|
||
[data-theme="light"] { --icon-tint: brightness(0) invert(0.18) opacity(0.92); }
|
||
/* a row of stacked member icons on matrix cards */
|
||
.icon-stack { display: flex; margin-top: 14px; align-items: center; }
|
||
.icon-stack .sw-icon { margin-left: -8px; box-shadow: 0 2px 6px rgba(0,0,0,0.25); background:
|
||
radial-gradient(120% 120% at 30% 25%, hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.2), var(--surface)); }
|
||
.icon-stack .sw-icon:first-child { margin-left: 0; }
|
||
.icon-stack .sw-icon.miss { opacity: 0.6; }
|
||
.icon-stack .more { font-size: 0.72rem; color: var(--ink-3); margin-left: 9px; }
|
||
/* small icon beside a card / matrix title */
|
||
.title-icon { width: 22px; height: 22px; border-radius: 7px; }
|
||
.title-icon img { width: 13px; height: 13px; }
|
||
.title-icon .icon-mono { font-size: 0.66rem; }
|
||
.card-title-row { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
|
||
|
||
/* back face */
|
||
.card-back-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
|
||
.card-back-title { font-size: 0.7rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-3); }
|
||
.card-back-scroll { flex: 1; min-height: 0; overflow-y: auto; padding-right: 4px; }
|
||
.card-back-scroll::-webkit-scrollbar { width: 7px; }
|
||
.card-back-scroll::-webkit-scrollbar-track { background: transparent; }
|
||
.card-back-scroll::-webkit-scrollbar-thumb { background: var(--line); border-radius: 6px; }
|
||
.card-back-scroll::-webkit-scrollbar-thumb:hover { background: var(--ink-3); }
|
||
.card-back-scroll { scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
|
||
.card-back-field { margin-top: 12px; }
|
||
.card-back-field:first-child { margin-top: 0; }
|
||
.card-back-label { font-size: 0.64rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3); margin-bottom: 5px; }
|
||
|
||
/* a face hidden from a11y/tab when not active */
|
||
.card:not(.flipped) .card-back :is(a,button,input,select,[tabindex]),
|
||
.card.flipped .card-front :is(a,button,input,select,[tabindex]) { /* aria-hidden handled via attr; this is style fallback */ }
|
||
|
||
.empty { grid-column: 1 / -1; text-align: center; padding: 56px 20px; color: var(--ink-3); }
|
||
.empty a { color: var(--accent); }
|
||
.skeleton { border-radius: var(--radius); min-height: 300px; background: var(--chip-bg); animation: shimmerSk 1.6s var(--ease) infinite; }
|
||
@keyframes shimmerSk { 0%,100%{opacity:.5} 50%{opacity:1} }
|
||
|
||
/* skill popover (public cards) */
|
||
.skill-popover { position: relative; display: inline-block; }
|
||
.card-link-btn { border: 0; background: transparent; color: var(--accent-ink); font-size: 0.78rem; font-weight: 600;
|
||
font-family: inherit; cursor: pointer; padding: 0; }
|
||
.card-link-btn:hover { text-decoration: underline; }
|
||
.skill-popover-panel { position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%) translateY(6px);
|
||
min-width: 240px; max-width: 320px; padding: 12px; border-radius: 12px; border: 1px solid var(--glass-border);
|
||
background: var(--glass-bg-fallback); box-shadow: var(--glass-shadow-hi); z-index: 20;
|
||
opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s; display: flex; flex-direction: column; gap: 8px; }
|
||
@media (hover: hover) and (pointer: fine) {
|
||
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
||
.skill-popover-panel { background: var(--glass-bg); -webkit-backdrop-filter: blur(20px) saturate(1.5); backdrop-filter: blur(20px) saturate(1.5); }
|
||
}
|
||
}
|
||
.skill-popover.is-open .skill-popover-panel { opacity: 1; pointer-events: auto; transform: translateX(-50%); }
|
||
.skill-popover-label { font-size: 0.62rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3); }
|
||
.skill-popover-panel code { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: var(--accent-ink); word-break: break-all;
|
||
padding: 8px 10px; border-radius: 8px; background: var(--cmd-bg); border: 1px solid var(--cmd-line); display: block; }
|
||
.skill-popover-copy { align-self: flex-start; border: 1px solid var(--cmd-line); border-radius: 8px; padding: 6px 12px; }
|
||
/* front-face Skill control: compact icon+label, sized like the flip pill (not oversized) */
|
||
.card-skill-front { display: inline-flex; align-items: center; gap: 6px; padding: 6px 11px; border-radius: 10px; cursor: pointer;
|
||
border: 1px solid var(--glass-border); background: var(--chip-bg); color: var(--accent-ink); font-size: 0.76rem; font-weight: 600;
|
||
font-family: inherit; text-decoration: none; transition: color .18s, border-color .18s, background .18s, transform .15s; flex-shrink: 0; }
|
||
.card-skill-front:hover { color: var(--ink); border-color: hsl(var(--ch, var(--hue-default)) 70% 64% / 0.6);
|
||
background: hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.14); transform: translateY(-1px); text-decoration: none; }
|
||
.card-skill-front:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||
.card-skill-front svg { width: 14px; height: 14px; flex-shrink: 0; }
|
||
|
||
/* ===================== Matrices band ===================== */
|
||
.matrices { max-width: 1180px; margin: 0 auto; padding: 64px 22px 8px; }
|
||
.matrices-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin: 0 0 24px; flex-wrap: wrap; }
|
||
.matrices-head .klabel { font-size: 0.72rem; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: var(--accent-ink); }
|
||
.matrices-head h2 { font-size: clamp(1.6rem, 4vw, 2.2rem); margin-top: 8px; }
|
||
.matrices-head p { color: var(--ink-2); max-width: 54ch; margin-top: 8px; font-size: 0.96rem; }
|
||
.matrices-head .mat-stat { font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--ink-3); white-space: nowrap; }
|
||
|
||
.shelf { display: grid; gap: 18px; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); grid-auto-rows: 1fr; align-items: stretch; }
|
||
@media (min-width: 980px) { .shelf { grid-template-columns: repeat(3, 1fr); } }
|
||
@media (max-width: 760px) {
|
||
.shelf { display: flex; grid-auto-flow: column; overflow-x: auto; scroll-snap-type: x mandatory;
|
||
margin: 0 -22px; padding: 0 22px 12px; gap: 16px; }
|
||
.shelf > .mcard { flex: 0 0 84%; scroll-snap-align: center; }
|
||
}
|
||
/* matrix flip card — reuses .card / .card-inner / .card-face machinery, taller min-height */
|
||
/* flat-by-default like .card above; 3D only on hover / mid-flip */
|
||
.mcard { min-height: 372px; position: relative; }
|
||
.mcard .card-inner { position: absolute; inset: 0; width: 100%; height: 100%;
|
||
transform-style: flat; transition: transform .72s var(--flip-ease); }
|
||
.mcard.flipped .card-inner { transform: rotateY(180deg); }
|
||
.mcard.is-3d { perspective: 1600px; }
|
||
.mcard.is-3d .card-inner { transform-style: preserve-3d; }
|
||
@media (hover: hover) {
|
||
.mcard:hover { perspective: 1600px; }
|
||
.mcard:hover .card-inner { transform-style: preserve-3d; }
|
||
.mcard:not(.flipped):hover .card-inner { transform: rotateY(-4deg) rotateX(2deg); }
|
||
}
|
||
.mcard .card-sizer { position: relative; width: 100%; min-height: inherit; }
|
||
.mcard .card-sizer .card-face { position: relative; box-shadow: none; border: 1px solid transparent; background: none; visibility: hidden;
|
||
-webkit-backdrop-filter: none; backdrop-filter: none; }
|
||
.mcard .card-sizer .card-back { position: absolute; inset: 0; transform: none; }
|
||
|
||
.mcard-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||
.mcard-glyph { width: 42px; height: 42px; border-radius: 13px; display: grid; place-items: center;
|
||
background: hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.18); border: 1px solid hsl(var(--ch, var(--hue-default)) var(--tint-s) 60% / 0.34); color: hsl(var(--ch, var(--hue-default)) var(--tint-s) 74%); }
|
||
[data-theme="light"] .mcard-glyph { color: hsl(var(--ch, var(--hue-default)) calc(var(--tint-s) + 14%) 38%); }
|
||
.mcard-glyph svg { width: 21px; height: 21px; }
|
||
.mcard h3 { font-size: 1.2rem; margin-top: 16px; }
|
||
.mcard .mdesc { color: var(--ink-2); font-size: 0.88rem; margin-top: 8px;
|
||
display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
||
.mcard .meta-row { display: flex; align-items: center; gap: 14px; margin-top: 14px; font-size: 0.8rem; color: var(--ink-3); }
|
||
.mcard .meta-row b { color: var(--ink); font-weight: 700; }
|
||
.avatars { display: flex; margin-top: 14px; align-items: center; }
|
||
.avatars .av { width: 30px; height: 30px; border-radius: 9px; margin-left: -7px; display: grid; place-items: center;
|
||
font-size: 0.6rem; font-weight: 700; background: var(--chip-bg); border: 1px solid var(--glass-border);
|
||
box-shadow: 0 2px 6px rgba(0,0,0,0.25); text-transform: uppercase; color: var(--ink-2); }
|
||
.avatars .av:first-child { margin-left: 0; }
|
||
.avatars .av.miss { opacity: 0.55; }
|
||
.avatars .more { font-size: 0.72rem; color: var(--ink-3); margin-left: 9px; }
|
||
|
||
/* matrix front: primary command + flip foot */
|
||
.mcard-front-foot { margin-top: auto; padding-top: 13px; display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||
|
||
/* matrix back: scrolling detail */
|
||
.mback-scroll { flex: 1; min-height: 0; overflow-y: auto; padding-right: 5px; scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
|
||
.mback-scroll::-webkit-scrollbar { width: 7px; }
|
||
.mback-scroll::-webkit-scrollbar-thumb { background: var(--line); border-radius: 6px; }
|
||
.msub { font-size: 0.62rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3); margin: 16px 0 8px; }
|
||
.msub:first-child { margin-top: 2px; }
|
||
.mcmd { display: flex; align-items: center; gap: 8px; background: var(--cmd-bg); border: 1px solid var(--cmd-line);
|
||
border-radius: 10px; padding: 8px 10px; margin-bottom: 7px; }
|
||
/* matrix commands stay on a single line; truncate with an ellipsis if they overflow
|
||
(Copy still copies the full command). */
|
||
.mcmd code { font-family: 'JetBrains Mono', monospace; font-size: 0.74rem; color: var(--ink); flex: 1; min-width: 0;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.5; }
|
||
.mcmd code .pfx { color: var(--ink-3); }
|
||
.mcmd .card-copy-btn { border: 0; border-left: 1px solid var(--cmd-line); }
|
||
|
||
/* capability accordions */
|
||
.cap { border: 1px solid var(--line); border-radius: 11px; margin-bottom: 7px; overflow: hidden; background: var(--chip-bg); }
|
||
.cap > summary { list-style: none; cursor: pointer; padding: 10px 12px; display: flex; align-items: center; gap: 9px; }
|
||
.cap > summary::-webkit-details-marker { display: none; }
|
||
.cap > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 10px; }
|
||
.cap .cap-id { font-family: 'JetBrains Mono', monospace; font-size: 0.66rem; color: hsl(var(--ch, var(--hue-default)) var(--tint-s) 74%); flex-shrink: 0;
|
||
background: hsl(var(--ch, var(--hue-default)) 50% 50% / 0.14); padding: 3px 6px; border-radius: 6px; }
|
||
[data-theme="light"] .cap .cap-id { color: hsl(var(--ch, var(--hue-default)) calc(var(--tint-s) + 14%) 38%); }
|
||
.cap .cap-title { font-weight: 600; font-size: 0.82rem; flex: 1; min-width: 0; }
|
||
.cap .chev { width: 14px; height: 14px; color: var(--ink-3); transition: transform .3s var(--ease); flex-shrink: 0; }
|
||
.cap[open] .chev { transform: rotate(90deg); }
|
||
.cap-detail { padding: 0 12px 12px; }
|
||
.cap-intent { color: var(--ink-2); font-size: 0.8rem; margin-bottom: 10px; }
|
||
.io { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 10px; }
|
||
.io span { font-family: 'JetBrains Mono', monospace; font-size: 0.64rem; padding: 3px 7px; border-radius: 6px; background: var(--cmd-bg); color: var(--ink-2); border: 1px solid var(--cmd-line); }
|
||
.io span.out { color: hsl(var(--ch, var(--hue-default)) var(--tint-s) 70%); }
|
||
[data-theme="light"] .io span.out { color: hsl(var(--ch, var(--hue-default)) calc(var(--tint-s) + 12%) 40%); }
|
||
.prov { display: flex; gap: 9px; padding: 8px 0; border-top: 1px dashed var(--line); }
|
||
.prov:first-of-type { border-top: 0; }
|
||
.prov .pk { font-size: 0.58rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; padding: 3px 6px; border-radius: 6px;
|
||
white-space: nowrap; height: fit-content; border: 1px solid; flex-shrink: 0; margin-top: 2px; }
|
||
.prov .pbody b { font-weight: 600; font-size: 0.8rem; }
|
||
.prov .pbody .pmeta { font-size: 0.7rem; color: var(--ink-3); margin-top: 1px; }
|
||
.prov .pbody .pnotes { font-size: 0.74rem; color: var(--ink-2); margin-top: 3px; }
|
||
/* provider-kind badges (cool/neutral) */
|
||
.pk-agentnative { color: #54d196; border-color: #54d19655; background: #54d19614; }
|
||
.pk-publiccli { color: var(--accent); border-color: rgba(var(--accent-rgb),.4); background: rgba(var(--accent-rgb),.10); }
|
||
.pk-harnesscli { color: #9aa6ff; border-color: #9aa6ff55; background: #9aa6ff14; }
|
||
.pk-agentskill { color: #5ad1d1; border-color: #5ad1d155; background: #5ad1d114; }
|
||
.pk-bundledscript{ color: #79c0e8; border-color: #79c0e855; background: #79c0e814; }
|
||
.pk-api { color: #5ad1d1; border-color: #5ad1d155; background: #5ad1d114; }
|
||
.pk-native, .pk-python, .pk-websearch, .pk-fallback { color: var(--ink-2); border-color: var(--line); background: var(--chip-bg); }
|
||
[data-theme="light"] .pk-agentnative{color:#0f8a52} [data-theme="light"] .pk-harnesscli{color:#4350c0}
|
||
[data-theme="light"] .pk-agentskill{color:#127d7d} [data-theme="light"] .pk-bundledscript{color:#1f6fa0} [data-theme="light"] .pk-api{color:#127d7d}
|
||
|
||
.member { display: flex; flex-wrap: wrap; gap: 7px; }
|
||
.member button { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border-radius: 9px; cursor: pointer;
|
||
border: 1px solid var(--glass-border); background: var(--chip-bg); color: var(--ink); font-size: 0.78rem; font-weight: 500;
|
||
font-family: inherit; transition: border-color .18s, background .18s, transform .18s; }
|
||
.member button:hover { border-color: hsl(var(--ch, var(--hue-default)) var(--tint-s) 60% / 0.6); transform: translateY(-1px); }
|
||
.member button .badge-mini { font-size: 0.56rem; padding: 1px 5px; border-radius: 5px; background: var(--cmd-bg); color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.04em; }
|
||
.member button.miss { opacity: 0.6; cursor: default; }
|
||
.mback-cta { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; }
|
||
.mbtn { display: inline-flex; align-items: center; gap: 7px; padding: 9px 13px; border-radius: 10px; font-weight: 600; font-size: 0.8rem;
|
||
text-decoration: none; cursor: pointer; border: 1px solid var(--glass-border); background: var(--chip-bg); color: var(--ink); font-family: inherit;
|
||
transition: border-color .18s, background .18s, transform .15s; }
|
||
.mbtn:hover { transform: translateY(-1px); border-color: var(--ink-3); }
|
||
.mbtn.primary { background: hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.2); border-color: hsl(var(--ch, var(--hue-default)) var(--tint-s) 60% / 0.5); }
|
||
.mbtn.primary:hover { background: hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.3); }
|
||
.mbtn svg { width: 14px; height: 14px; }
|
||
|
||
/* scope banner above the decks */
|
||
.scope-banner { display: none; align-items: center; gap: 12px; flex-wrap: wrap; padding: 12px 16px; margin: 0 22px 16px;
|
||
max-width: 1180px; margin-left: auto; margin-right: auto;
|
||
border-radius: 14px; border: 1px solid hsl(var(--ch, var(--hue-default)) var(--tint-s) 60% / 0.4); background: hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.12); }
|
||
.scope-banner.on { display: flex; }
|
||
.scope-banner .lbl { font-size: 0.86rem; }
|
||
.scope-banner b { font-weight: 700; }
|
||
.scope-banner .clear { margin-left: auto; border: 0; background: transparent; color: var(--ink-2); cursor: pointer; font-weight: 600;
|
||
font-size: 0.82rem; display: inline-flex; gap: 6px; align-items: center; padding: 5px 9px; border-radius: 9px; font-family: inherit; }
|
||
.scope-banner .clear:hover { background: var(--chip-bg); color: var(--ink); }
|
||
|
||
/* membership chip on catalog cards */
|
||
.in-matrix { display: inline-flex; align-items: center; gap: 5px; font-size: 0.68rem; font-weight: 600; padding: 4px 8px; border-radius: 8px;
|
||
border: 1px solid var(--line); background: var(--chip-bg); color: var(--ink-2); cursor: pointer; font-family: inherit; transition: color .15s, border-color .15s; }
|
||
.in-matrix:hover { color: var(--ink); border-color: var(--accent); }
|
||
.in-matrix .mid { font-family: 'JetBrains Mono', monospace; color: var(--accent-ink); }
|
||
.card.flash .card-face { animation: cardFlash 1.5s var(--ease); }
|
||
@keyframes cardFlash { 0%,12% { box-shadow: 0 0 0 2px var(--accent), var(--glass-shadow-hi); } 100% { box-shadow: var(--glass-shadow); } }
|
||
|
||
/* ===================== Footer ===================== */
|
||
.footer { border-top: 1px solid var(--line); margin-top: 56px; padding: 34px 22px 50px; max-width: 1180px; margin-left: auto; margin-right: auto;
|
||
display: flex; flex-wrap: wrap; gap: 18px 32px; align-items: center; justify-content: space-between; }
|
||
.footer-left, .footer-right { font-size: 0.84rem; color: var(--ink-3); }
|
||
.footer a { color: var(--accent-ink); text-decoration: none; font-weight: 600; }
|
||
.footer a:hover { text-decoration: underline; }
|
||
.footer-stats { width: 100%; margin-top: 6px; }
|
||
.analytics-report { display: inline-flex; align-items: center; gap: 14px; flex-wrap: wrap; padding: 10px 16px; border-radius: 13px;
|
||
border: 1px solid var(--line); background: var(--chip-bg); }
|
||
.analytics-kicker { font-size: 0.64rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent-ink); }
|
||
.analytics-row { display: inline-flex; align-items: baseline; gap: 8px; }
|
||
.analytics-title { font-size: 0.74rem; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600; }
|
||
.analytics-total { font-size: 1.02rem; font-weight: 800; font-variant-numeric: tabular-nums; }
|
||
.analytics-pair { font-size: 0.78rem; color: var(--ink-3); font-variant-numeric: tabular-nums; }
|
||
.analytics-pair .human-value { color: var(--ink-2); }
|
||
.analytics-pair .agent-value { color: var(--accent-ink); }
|
||
.analytics-divider { width: 1px; height: 18px; background: var(--line); }
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
* { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; }
|
||
.reveal { opacity: 1; transform: none; transition: none; }
|
||
.card-inner { transition: opacity .2s ease !important; transform: none !important; }
|
||
/* Reduced-motion flip becomes an instant crossfade */
|
||
.card .card-front { opacity: 1; transition: opacity .2s ease; }
|
||
.card .card-back { opacity: 0; transition: opacity .2s ease; }
|
||
.card.flipped .card-front { opacity: 0; }
|
||
.card.flipped .card-back { opacity: 1; }
|
||
.card-face { -webkit-backface-visibility: visible; backface-visibility: visible; }
|
||
.card-back { transform: none; }
|
||
.card:hover .card-inner { transform: none !important; }
|
||
/* matrix flip cards: crossfade too */
|
||
.mcard .card-inner { transition: opacity .2s ease !important; transform: none !important; }
|
||
.mcard .card-front { opacity: 1; transition: opacity .2s ease; }
|
||
.mcard .card-back { opacity: 0; transition: opacity .2s ease; }
|
||
.mcard.flipped .card-front { opacity: 0; }
|
||
.mcard.flipped .card-back { opacity: 1; }
|
||
.mcard:hover .card-inner { transform: none !important; }
|
||
}
|
||
|
||
/* ===================== Real-World Demos — homepage teaser (full gallery on demos.html) ===================== */
|
||
.demos-teaser { max-width: 1180px; margin: 56px auto 0; padding: 0 22px; }
|
||
.dt-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 22px; flex-wrap: wrap; }
|
||
.dt-head .kl { font-size: 0.72rem; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: var(--accent-ink); }
|
||
.dt-head h2 { font-size: clamp(1.6rem, 4vw, 2.2rem); margin-top: 8px; }
|
||
.dt-head p { color: var(--ink-2); max-width: 56ch; margin-top: 8px; font-size: 0.96rem; }
|
||
.dt-seeall { display: inline-flex; align-items: center; gap: 7px; font-weight: 700; font-size: 0.88rem; color: var(--accent-ink); text-decoration: none;
|
||
white-space: nowrap; padding: 9px 14px; border-radius: 11px; border: 1px solid var(--glass-border); background: var(--chip-bg); transition: border-color .18s, transform .18s; }
|
||
.dt-seeall:hover { border-color: var(--accent); transform: translateY(-1px); }
|
||
.dt-seeall svg { width: 16px; height: 16px; }
|
||
.demos-strip { display: grid; grid-template-columns: repeat(3, 1fr) auto; gap: 18px; align-items: stretch; }
|
||
@media (max-width: 900px) {
|
||
.demos-strip { grid-auto-flow: column; grid-template-columns: none; grid-auto-columns: 80%; overflow-x: auto;
|
||
scroll-snap-type: x mandatory; margin: 0 -22px; padding: 0 22px 12px; }
|
||
.demos-strip > * { scroll-snap-align: start; }
|
||
}
|
||
.dposter { display: flex; flex-direction: column; padding: 14px; border-radius: var(--radius); text-decoration: none; color: inherit;
|
||
background: var(--glass-bg-fallback); border: 1px solid var(--glass-border); box-shadow: var(--glass-shadow); position: relative; transition: transform .3s var(--ease), box-shadow .3s var(--ease); }
|
||
@media (hover: hover) and (pointer: fine) {
|
||
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
||
.dposter { background: var(--glass-bg); -webkit-backdrop-filter: blur(22px) saturate(1.5); backdrop-filter: blur(22px) saturate(1.5); } }
|
||
}
|
||
.dposter:hover { transform: translateY(-4px); box-shadow: var(--glass-shadow-hi); }
|
||
.dt-media { position: relative; border-radius: 13px; overflow: hidden; border: 1px solid var(--line); background: #05070b; }
|
||
[data-theme="light"] .dt-media { background: #e9edf3; }
|
||
.dt-chrome { display: flex; align-items: center; gap: 6px; padding: 7px 11px; background: var(--cmd-bg); border-bottom: 1px solid var(--cmd-line); }
|
||
.dt-chrome .dts { display: inline-flex; gap: 4px; }
|
||
.dt-chrome .dts i { width: 8px; height: 8px; border-radius: 50%; }
|
||
.dt-chrome .dts i:nth-child(1){ background: hsl(8 60% 60%); } .dt-chrome .dts i:nth-child(2){ background: hsl(45 70% 58%); } .dt-chrome .dts i:nth-child(3){ background: hsl(140 45% 55%); }
|
||
.dt-chrome .u { font-family: 'JetBrains Mono', monospace; font-size: 0.62rem; color: var(--ink-3); flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.dt-chrome .lv { display: inline-flex; align-items: center; gap: 4px; font-size: 0.56rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent-ink); }
|
||
.dt-chrome .lv i { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 7px var(--accent); animation: dtpulse 1.6s infinite; }
|
||
@keyframes dtpulse { 0%,100%{opacity:1} 50%{opacity:.35} }
|
||
.dt-media img, .dt-media video { display: block; width: 100%; height: 170px; object-fit: cover; }
|
||
.dposter .dt-cat { display: inline-flex; align-items: center; gap: 6px; font-size: 0.64rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; margin-top: 13px;
|
||
color: hsl(var(--ch) var(--tint-s) 74%); }
|
||
[data-theme="light"] .dposter .dt-cat { color: hsl(var(--ch) calc(var(--tint-s) + 12%) 36%); }
|
||
.dposter .dt-cat svg { width: 12px; height: 12px; }
|
||
.dposter h3 { font-family: 'Newsreader', serif; font-weight: 400; font-size: 1.3rem; margin-top: 6px; letter-spacing: -0.01em; }
|
||
.dposter h3 .ap { font-style: italic; color: var(--accent-ink); }
|
||
.dposter .dt-blurb { color: var(--ink-2); font-size: 0.83rem; margin-top: 7px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||
.dposter .dt-by { margin-top: auto; padding-top: 12px; font-size: 0.74rem; color: var(--ink-3); display: flex; align-items: center; gap: 7px; }
|
||
.dposter .dt-by b { color: var(--ink-2); font-weight: 600; }
|
||
.dt-av { width: 22px; height: 22px; border-radius: 7px; display: grid; place-items: center; font-size: 0.55rem; font-weight: 700; text-transform: uppercase;
|
||
background: hsl(var(--ch) var(--tint-s) 52% / 0.2); color: hsl(var(--ch) var(--tint-s) 74%); border: 1px solid hsl(var(--ch) var(--tint-s) 55% / 0.35); }
|
||
[data-theme="light"] .dt-av { color: hsl(var(--ch) calc(var(--tint-s) + 12%) 36%); }
|
||
.dposter-end { display: grid; place-content: center; gap: 8px; text-align: center; min-width: 184px; padding: 22px; text-decoration: none; color: inherit;
|
||
border-radius: var(--radius); border: 1px dashed hsl(var(--accent-band) var(--tint-s) 55% / 0.5); background: hsl(var(--accent-band) var(--tint-s) 52% / 0.07); transition: background .25s, transform .25s; }
|
||
.dposter-end:hover { background: hsl(var(--accent-band) var(--tint-s) 52% / 0.13); transform: translateY(-2px); }
|
||
.dposter-end .ico { width: 52px; height: 52px; border-radius: 50%; display: grid; place-items: center; margin: 0 auto; color: var(--accent-ink);
|
||
border: 1px solid hsl(var(--accent-band) var(--tint-s) 55% / 0.5); background: hsl(var(--accent-band) var(--tint-s) 52% / 0.14); }
|
||
.dposter-end .ico svg { width: 22px; height: 22px; }
|
||
.dposter-end .go { font-weight: 700; color: var(--ink); font-size: 0.95rem; }
|
||
.dposter-end .tx { color: var(--ink-2); font-size: 0.82rem; max-width: 16ch; margin: 0 auto; }
|
||
@media (max-width: 900px) { .dposter-end { min-width: 70%; } }
|
||
@media (prefers-reduced-motion: reduce) { .dt-chrome .lv i { animation: none; } }
|
||
</style>
|
||
<script>document.documentElement.classList.add('js');</script>
|
||
</head>
|
||
<body>
|
||
<div class="ambient" aria-hidden="true"><i class="a"></i><i class="b"></i><i class="c"></i></div>
|
||
<div class="grain" aria-hidden="true"></div>
|
||
|
||
<!-- Nav -->
|
||
<nav class="nav" id="nav">
|
||
<div class="nav-in">
|
||
<a class="nav-brand" href="#"><span class="nav-brand-badge" aria-hidden="true"><svg viewBox="0 0 48 48" fill="none"><path d="M8 16.5 18.5 24 8 31.5" stroke="currentColor" stroke-width="3.9" stroke-linecap="square" stroke-linejoin="miter" style="color: var(--accent);"/><path d="M24 29.2h16" stroke="currentColor" stroke-width="3.9" stroke-linecap="square" style="color: var(--ink);"/></svg></span>CLI-Hub</a>
|
||
<div class="nav-links">
|
||
<div class="nav-menu" id="navMenu">
|
||
<a class="nav-link" href="#demos">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="14" rx="2"/><path d="m10 9 4 2.5L10 14z" fill="currentColor" stroke="none"/></svg>
|
||
Demos
|
||
</a>
|
||
<a class="nav-link" href="#matrices">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.6"/><rect x="13.5" y="13.5" width="7" height="7" rx="1.6"/></svg>
|
||
Matrices
|
||
</a>
|
||
<a class="nav-link" href="docs.html">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4.8A1.8 1.8 0 0 1 5.8 3H12v17H5.8A1.8 1.8 0 0 0 4 21.8z"/><path d="M20 4.8A1.8 1.8 0 0 0 18.2 3H12v17h6.2A1.8 1.8 0 0 1 20 21.8z"/></svg>
|
||
Docs
|
||
</a>
|
||
<a class="nav-link" href="https://reeceyang.sgp1.cdn.digitaloceanspaces.com/SKILL.md" target="_blank">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5.5 18.5 9.8-9.8"/><path d="m13.7 6.9 3.4 3.4"/><path d="M18 4v2.4"/><path d="M16.8 5.2h2.4"/><path d="M7.5 4.8v2"/><path d="M6.5 5.8h2"/><path d="M18.2 15.8v2"/><path d="M17.2 16.8h2"/></svg>
|
||
SKILL
|
||
</a>
|
||
<a class="nav-link" href="https://github.com/HKUDS/CLI-Anything" target="_blank">
|
||
<svg viewBox="0 0 98 96" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6C29.304 70.25 17.9 66.013 17.9 47.02c0-5.52 1.94-10.046 5.127-13.58-.486-1.302-2.264-6.437.486-13.34 0 0 4.206-1.302 13.59 5.216 3.963-1.14 8.17-1.628 12.34-1.628 4.17 0 8.376.568 12.34 1.628 9.384-6.518 13.59-5.216 13.59-5.216 2.75 6.903.972 12.038.486 13.34 3.268 3.534 5.127 8.06 5.127 13.58 0 19.074-11.485 23.15-22.328 24.29 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"/></svg>
|
||
GitHub
|
||
<span class="nav-link-stars" data-github-stars aria-label="GitHub star count">--</span>
|
||
</a>
|
||
<a class="nav-link" href="https://arxiv.org/abs/2606.03854" target="_blank" rel="noopener noreferrer">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 3.5h8.2L19 7.3v12A1.7 1.7 0 0 1 17.3 21H7a2 2 0 0 1-2-2V5.5a2 2 0 0 1 2-2z"/><path d="M15 3.5v4h4"/><path d="M8.5 11.2h7"/><path d="M8.5 14.5h7"/><path d="M8.5 17.8h4.5"/><path d="M10 3.5v5l1.7-1.1L13.4 8.5v-5"/></svg>
|
||
Tech Report
|
||
</a>
|
||
<a class="nav-link" href="https://github.com/HKUDS/CLI-Anything/blob/main/CONTRIBUTING.md" target="_blank">
|
||
Contribute
|
||
</a>
|
||
</div>
|
||
<div class="palette-switch" id="palette-switch" role="group" aria-label="Color palette">
|
||
<span class="palette-switch-label">Palette</span>
|
||
</div>
|
||
<div class="palette-popwrap" id="palette-popwrap">
|
||
<button class="palette-pop-btn" id="palette-pop-btn" aria-haspopup="true" aria-expanded="false" aria-label="Choose color palette">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125 0-.926.746-1.688 1.688-1.688H16c3.314 0 6-2.686 6-6 0-4.97-4.5-8.375-10-8.375Z"/></svg>
|
||
</button>
|
||
<div class="palette-pop-panel" id="palette-pop-panel" role="group" aria-label="Color palette"></div>
|
||
</div>
|
||
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle theme">
|
||
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
||
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||
</button>
|
||
<button class="nav-toggle" id="navToggle" aria-label="Menu" aria-expanded="false" aria-controls="navMenu">
|
||
<svg class="icon-bars" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||
<svg class="icon-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="nav-scrim" id="navScrim"></div>
|
||
</nav>
|
||
|
||
<!-- Hero -->
|
||
<div class="hero">
|
||
<div class="hero-title">
|
||
<span class="hero-title-badge" aria-hidden="true">
|
||
<svg viewBox="0 0 48 48" fill="none">
|
||
<path d="M8 16.5 18.5 24 8 31.5" stroke="currentColor" stroke-width="3.9" stroke-linecap="square" stroke-linejoin="miter" style="color: var(--accent);" />
|
||
<path d="M24 29.2h16" stroke="currentColor" stroke-width="3.9" stroke-linecap="square" style="color: var(--ink);" />
|
||
<path d="M26 12h14" stroke="url(#heroBadgeBeam)" stroke-width="2.6" stroke-linecap="square" opacity="0.78" />
|
||
<path d="M7 10.5h9" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" style="color: var(--ink-3);" />
|
||
<path d="M7 37.5h11" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" style="color: rgba(96,165,250,0.3);" />
|
||
<defs>
|
||
<linearGradient id="heroBadgeBeam" x1="26" y1="12" x2="40" y2="12" gradientUnits="userSpaceOnUse">
|
||
<stop stop-color="#60A5FA" stop-opacity="0" />
|
||
<stop offset="0.52" stop-color="#93C5FD" />
|
||
<stop offset="1" stop-color="#60A5FA" stop-opacity="0" />
|
||
</linearGradient>
|
||
</defs>
|
||
</svg>
|
||
</span>
|
||
<h1>
|
||
<span class="hero-word hero-word--cli">CLI-</span><span class="hero-word hero-word--anything">Anything</span> <span class="hero-word hero-word--hub">Hub</span>
|
||
</h1>
|
||
</div>
|
||
<p class="hero-tagline"><span class="hero-tagline-line hero-tagline-line--any">Any software. Any codebase. Any Web API.</span> <span class="hero-tagline-line hero-tagline-line--rest">Generate an agent-native CLI and let AI agents operate it — install with a single command.</span></p>
|
||
<div class="stats">
|
||
<div class="stat">
|
||
<div class="stat-num" id="cli-count">—</div>
|
||
<div class="stat-label">CLIs Available</div>
|
||
</div>
|
||
<div class="stat">
|
||
<div class="stat-num" id="category-count">—</div>
|
||
<div class="stat-label">Categories</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="empower-row">
|
||
<div class="empower-card">
|
||
<h3>Empower your agents — <span>install in one command</span></h3>
|
||
<p class="empower-available">Also Available on: <a href="https://clawhub.ai/yuh-yang/cli-anything-hub" target="_blank">ClawHub</a>, <a href="https://www.skillhub.club/web/skills/itsyuhao-cli-anything-hub" target="_blank">SkillHub</a>, <a href="https://skillhub.cn/skills/cli-hub-meta-skill" target="_blank">SkillHub.cn</a></p>
|
||
<div class="install-row install-row--wrap">
|
||
<span class="chip-label">npx skills</span>
|
||
<code>npx skills add HKUDS/CLI-Anything --skill cli-hub-meta-skill -g -y</code>
|
||
<button class="copy-btn" onclick="copyCmd(this, 'npx skills add HKUDS/CLI-Anything --skill cli-hub-meta-skill -g -y')">Copy</button>
|
||
</div>
|
||
<p class="empower-note"><strong>Works with:</strong> OpenClaw, Nanobot, Claude Code, Codex, Antigravity, and other SKILL-compatible agents.</p>
|
||
<div class="empower-prompt">
|
||
<strong>Then prompt:</strong> <em>"Find appropriate CLI software in CLI-Hub and complete the task: ..."</em>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="empower-card empower-self">
|
||
<h3>Empower yourself — <span>your CLI toolkit</span></h3>
|
||
<div class="install-row install-row--wrap">
|
||
<span class="chip-label">pip</span>
|
||
<code>pip install cli-anything-hub</code>
|
||
<button class="copy-btn" onclick="copyCmd(this, 'pip install cli-anything-hub')">Copy</button>
|
||
</div>
|
||
<div class="cmd-grid">
|
||
<div class="cmd-item"><span class="cmd-name">cli-hub list</span><span class="cmd-desc">Browse the registry</span></div>
|
||
<div class="cmd-item"><span class="cmd-name">cli-hub search</span><span class="cmd-desc">Search by keyword</span></div>
|
||
<div class="cmd-item"><span class="cmd-name">cli-hub install</span><span class="cmd-desc">Install from CLI-Hub</span></div>
|
||
<div class="cmd-item"><span class="cmd-name">cli-hub info</span><span class="cmd-desc">Inspect one CLI</span></div>
|
||
<div class="cmd-item"><span class="cmd-name">cli-hub update</span><span class="cmd-desc">Update an install</span></div>
|
||
<div class="cmd-item"><span class="cmd-name">cli-hub uninstall</span><span class="cmd-desc">Remove an install</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="hero-actions">
|
||
<a class="btn-primary" href="https://reeceyang.sgp1.cdn.digitaloceanspaces.com/SKILL.md" target="_blank">Agent SKILL (SKILL.md)</a>
|
||
<a class="btn-secondary" href="docs.html">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4.8A1.8 1.8 0 0 1 5.8 3H12v17H5.8A1.8 1.8 0 0 0 4 21.8z"/><path d="M20 4.8A1.8 1.8 0 0 0 18.2 3H12v17h6.2A1.8 1.8 0 0 1 20 21.8z"/></svg>
|
||
Documentation
|
||
</a>
|
||
<a class="btn-secondary" href="https://github.com/HKUDS/CLI-Anything" target="_blank">
|
||
<svg viewBox="0 0 98 96" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6C29.304 70.25 17.9 66.013 17.9 47.02c0-5.52 1.94-10.046 5.127-13.58-.486-1.302-2.264-6.437.486-13.34 0 0 4.206-1.302 13.59 5.216 3.963-1.14 8.17-1.628 12.34-1.628 4.17 0 8.376.568 12.34 1.628 9.384-6.518 13.59-5.216 13.59-5.216 2.75 6.903.972 12.038.486 13.34 3.268 3.534 5.127 8.06 5.127 13.58 0 19.074-11.485 23.15-22.328 24.29 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"/></svg>
|
||
Star on GitHub
|
||
</a>
|
||
<a class="btn-secondary" href="https://github.com/HKUDS/CLI-Anything/blob/main/CONTRIBUTING.md" target="_blank">
|
||
Contributing Guide
|
||
</a>
|
||
<a class="btn-secondary" href="https://github.com/HKUDS/CLI-Anything/blob/main/.github/PULL_REQUEST_TEMPLATE.md" target="_blank">
|
||
PR Template
|
||
</a>
|
||
</div>
|
||
|
||
<p class="hero-hint">Feed <a href="https://reeceyang.sgp1.cdn.digitaloceanspaces.com/SKILL.md" target="_blank">SKILL.md</a> to your AI agent for autonomous CLI discovery & installation</p>
|
||
|
||
<p class="hero-cta">We welcome contributions for <em>any</em> application — desktop apps, dev tools, cloud services, SaaS APIs, creative suites, and beyond. If it has a GUI or an API, it deserves an agent-native CLI. Read the <a href="https://github.com/HKUDS/CLI-Anything/blob/main/CONTRIBUTING.md" target="_blank">Contributing Guide</a>, use the <a href="https://github.com/HKUDS/CLI-Anything/blob/main/.github/PULL_REQUEST_TEMPLATE.md" target="_blank">PR Template</a>, and bring it <span class="nowrap">to the hub!</span></p>
|
||
|
||
</div>
|
||
|
||
<!-- Real-World Demos — homepage teaser (full Editorial gallery lives on demos.html) -->
|
||
<section class="demos-teaser" id="demos">
|
||
<div class="dt-head">
|
||
<div>
|
||
<div class="kl">See it work</div>
|
||
<h2>Real-world demos</h2>
|
||
<p>Agents driving generated CLIs to finished artifacts — CAD models, 3D scenes, diagrams, gameplay, captioned video, and live GIS. No GUI needed.</p>
|
||
</div>
|
||
<a class="dt-seeall" href="demos.html">See all demos
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
||
</a>
|
||
</div>
|
||
<div class="demos-strip">
|
||
|
||
<a class="dposter" href="demos.html#freecad" style="--ch:210">
|
||
<div class="dt-media">
|
||
<div class="dt-chrome"><span class="dts"><i></i><i></i><i></i></span><span class="u">cli-anything-freecad · preview live</span><span class="lv"><i></i>live</span></div>
|
||
<video class="lazy-vid" muted loop playsinline preload="none" poster="assets/demos/freecad-curiosity-preview-trajectory-poster.jpg" src="assets/demos/freecad-curiosity-preview-trajectory.mp4" aria-label="FreeCAD Curiosity rover demo"></video>
|
||
</div>
|
||
<span class="dt-cat"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 3 7v10l9 5 9-5V7z"/><path d="M3 7l9 5 9-5"/><path d="M12 12v10"/></svg>3D / CAD</span>
|
||
<h3><span class="ap">FreeCAD</span> — Curiosity Rover</h3>
|
||
<p class="dt-blurb">An agent assembles a Curiosity-inspired rover while publishing real preview bundles and recording a replayable trajectory.</p>
|
||
<div class="dt-by"><span class="dt-av">CA</span><span><b>Core team</b> · CLI-Anything</span></div>
|
||
</a>
|
||
|
||
<a class="dposter" href="demos.html#blender" style="--ch:28">
|
||
<div class="dt-media">
|
||
<div class="dt-chrome"><span class="dts"><i></i><i></i><i></i></span><span class="u">cli-anything-blender · preview live</span><span class="lv"><i></i>live</span></div>
|
||
<video class="lazy-vid" muted loop playsinline preload="none" poster="assets/demos/blender-orbital-relay-drone-preview-trajectory-poster.jpg" src="assets/demos/blender-orbital-relay-drone-preview-trajectory.mp4" aria-label="Blender orbital relay drone demo"></video>
|
||
</div>
|
||
<span class="dt-cat"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 3 7v10l9 5 9-5V7z"/><path d="M3 7l9 5 9-5"/><path d="M12 12v10"/></svg>3D · Hard-surface</span>
|
||
<h3><span class="ap">Blender</span> — Orbital Relay Drone</h3>
|
||
<p class="dt-blurb">A hard-surface drone grows under a real preview loop, each stage pushing render-backed bundles tied to the trajectory.</p>
|
||
<div class="dt-by"><span class="dt-av">CA</span><span><b>Core team</b> · CLI-Anything</span></div>
|
||
</a>
|
||
|
||
<a class="dposter" href="demos.html#drawio" style="--ch:250">
|
||
<div class="dt-media">
|
||
<div class="dt-chrome"><span class="dts"><i></i><i></i><i></i></span><span class="u">cli-anything-drawio · build</span><span class="lv"><i></i>live</span></div>
|
||
<video class="lazy-vid" muted loop playsinline preload="none" poster="assets/demos/drawio-demo-poster.jpg" src="assets/demos/drawio-demo.mp4" aria-label="Draw.io HTTPS handshake diagram demo"></video>
|
||
</div>
|
||
<span class="dt-cat"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="5" rx="1"/><rect x="14" y="16" width="7" height="5" rx="1"/><path d="M6.5 8v4a2 2 0 0 0 2 2h9"/></svg>Diagramming</span>
|
||
<h3><span class="ap">Draw.io</span> — HTTPS Handshake</h3>
|
||
<p class="dt-blurb">A full HTTPS lifecycle diagram built from scratch — TCP handshake, TLS negotiation, and termination — entirely via CLI.</p>
|
||
<div class="dt-by"><span class="dt-av">ZH</span><span>Contributed by <b>@zhangxilong-43</b></span></div>
|
||
</a>
|
||
|
||
<a class="dposter-end" href="demos.html">
|
||
<span class="ico" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg></span>
|
||
<span class="go">See all demos</span>
|
||
<span class="tx">Explore the full gallery</span>
|
||
</a>
|
||
|
||
</div>
|
||
</section>
|
||
|
||
<div class="marketplace-prelude">
|
||
<div class="typing-shell" data-typing-text="pip install cli-anything-hub">
|
||
<div class="typing-shell-head">
|
||
<span class="typing-shell-label">install first, then browse the marketplace</span>
|
||
<button class="copy-btn typing-shell-copy" onclick="copyCmd(this, 'pip install cli-anything-hub')">Copy</button>
|
||
</div>
|
||
<div class="typing-shell-body" aria-label="pip install cli-anything-hub">
|
||
<span class="typing-shell-prompt">$</span>
|
||
<code class="typing-shell-code"><span class="typing-shell-text" data-typing-target></span><span class="typing-shell-cursor" aria-hidden="true"></span></code>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Matrices -->
|
||
<section class="matrices" id="matrices">
|
||
<div class="matrices-head">
|
||
<div>
|
||
<div class="klabel">The collections</div>
|
||
<h2>Matrices</h2>
|
||
<p>Each matrix is a capability bundle — a curated set of intents mapped to provider CLIs from the catalog below. Flip a card for its capabilities, then scope the catalog to just its tools.</p>
|
||
</div>
|
||
<div class="mat-stat" id="mat-stat"></div>
|
||
</div>
|
||
<div class="shelf" id="shelf">
|
||
<div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Catalog scope banner (set by matrix "show only these tools") -->
|
||
<div class="scope-banner" id="scopeBanner" role="status">
|
||
<span class="lbl">Scoped to <b id="scopeName">a matrix</b> — showing <b id="scopeCount">0</b> tools.</span>
|
||
<button class="clear" id="scopeClear" type="button">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" width="14" height="14"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
Show all tools
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Deck nav -->
|
||
<div class="deck-nav">
|
||
<button class="deck-tab active" data-deck="harness" onclick="switchDeck('harness')">
|
||
CLI-Anything CLIs
|
||
<span class="deck-count" id="deck-count-harness">—</span>
|
||
</button>
|
||
<button class="deck-tab deck-tab--public" data-deck="public" onclick="switchDeck('public')">
|
||
Public CLIs
|
||
<span class="deck-count" id="deck-count-public">—</span>
|
||
</button>
|
||
<div class="deck-dots" role="tablist" aria-label="Switch deck">
|
||
<button class="deck-dot active" data-deck="harness" onclick="switchDeck('harness')" aria-label="Show CLI-Anything CLIs" title="CLI-Anything CLIs"></button>
|
||
<button class="deck-dot" data-deck="public" onclick="switchDeck('public')" aria-label="Show Public CLIs" title="Public CLIs"></button>
|
||
</div>
|
||
<span class="deck-separator"></span>
|
||
<span class="deck-hint" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M8 9 4 12l4 3"/><path d="M16 9l4 3-4 3"/><path d="M4 12h16"/></svg>
|
||
<span>drag, scroll, or tap to switch</span>
|
||
</span>
|
||
<button class="deck-arrow-btn" id="deck-toggle" onclick="toggleDeck()">
|
||
<span>Public CLIs</span>
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Deck viewport -->
|
||
<div class="deck-stage">
|
||
<div class="deck-viewport">
|
||
<div class="deck-track" id="deck-track" data-active="harness">
|
||
|
||
<!-- Harness deck -->
|
||
<div class="deck-panel deck-panel--harness" data-peek-label="CLI-Anything">
|
||
<div class="controls">
|
||
<input class="search" type="text" placeholder="Search CLIs..." id="search-harness">
|
||
<select class="sort-select" id="sort-harness">
|
||
<option value="updated">Last updated</option>
|
||
<option value="name">Name</option>
|
||
<option value="category">Category</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-row" id="filters-harness"></div>
|
||
<div class="grid" id="grid-harness">
|
||
<div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div>
|
||
</div>
|
||
<div class="deck-sentinel" id="sentinel-harness" data-deck="harness" hidden aria-hidden="true"></div>
|
||
</div>
|
||
|
||
<!-- Public CLI deck -->
|
||
<div class="deck-panel deck-panel--public" data-peek-label="Public CLIs">
|
||
<div class="controls">
|
||
<input class="search" type="text" placeholder="Search public CLIs..." id="search-public">
|
||
<select class="sort-select" id="sort-public">
|
||
<option value="updated">Last updated</option>
|
||
<option value="name">Name</option>
|
||
<option value="category">Category</option>
|
||
</select>
|
||
</div>
|
||
<div class="public-panel-note">
|
||
Public CLIs remain the property of their respective software owners and maintainers. All rights stay with those owners; CLI-Hub only helps you discover and install them.
|
||
</div>
|
||
<div class="filter-row" id="filters-public"></div>
|
||
<div class="grid" id="grid-public"></div>
|
||
<div class="deck-sentinel" id="sentinel-public" data-deck="public" hidden aria-hidden="true"></div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<footer class="footer">
|
||
<div class="footer-left">
|
||
Want to add or update a CLI? Read the <a href="https://github.com/HKUDS/CLI-Anything/blob/main/CONTRIBUTING.md" target="_blank">Contributing Guide</a> and use the <a href="https://github.com/HKUDS/CLI-Anything/blob/main/.github/PULL_REQUEST_TEMPLATE.md" target="_blank">PR Template</a>
|
||
</div>
|
||
<div class="footer-right">
|
||
Powered by <a href="https://github.com/HKUDS/CLI-Anything" target="_blank">CLI-Anything</a>
|
||
</div>
|
||
<div class="footer-stats">
|
||
<section class="analytics-report" aria-label="Traffic snapshot">
|
||
<span class="analytics-kicker">Traffic</span>
|
||
<span class="analytics-row">
|
||
<span class="analytics-title">Web</span>
|
||
<strong id="stat-web-unique" class="analytics-total">...</strong>
|
||
</span>
|
||
<span class="analytics-divider" aria-hidden="true"></span>
|
||
<span class="analytics-row">
|
||
<span class="analytics-title">CLI</span>
|
||
<strong id="stat-cli-total" class="analytics-total">...</strong>
|
||
<span class="analytics-pair"><strong id="stat-cli-human" class="human-value">...</strong>/<strong id="stat-cli-agent" class="agent-value">...</strong></span>
|
||
</span>
|
||
</section>
|
||
</div>
|
||
</footer>
|
||
|
||
<script>
|
||
// Forward declaration so palette init (which runs before the matrix block) can read it.
|
||
var STATE = {
|
||
byName: new Map(), // cli name -> catalog entry (with ._deck)
|
||
matrices: [],
|
||
membership: new Map(), // cli name -> [matrix, ...]
|
||
scope: null, // matrix name the catalog is scoped to
|
||
};
|
||
var MATRIX_URLS = [
|
||
'../../matrix_registry.json',
|
||
'https://raw.githubusercontent.com/HKUDS/CLI-Anything/main/matrix_registry.json'
|
||
];
|
||
|
||
// ── Palette system (tech · humanistic · aesthetic) ──
|
||
// Anchor tokens per palette × theme; glass/chip/shadow derive in CSS from these.
|
||
// accentBand = the palette's ACCENT hue; per-category tints spread ±~25° around it
|
||
// (catHue), so each palette keeps its own colour family (warm palettes → warm tints).
|
||
const PALETTES = {
|
||
steel: {
|
||
label: 'Steel', accentBand: 209,
|
||
sw: { accent: '#2f6fb0', grey: '#8595a8' },
|
||
light: { bg:'#f4f6f9', surface:'#ffffff', ink:'#14181f', ink2:'#4a5568', ink3:'#8595a8', line:'rgba(20,40,80,.10)', accent:'#2f6fb0', accent2:'#1f5793', accentRgb:'47,111,176', surfaceRgb:'255,255,255' },
|
||
dark: { bg:'#0d1117', surface:'#151b24', ink:'#e8edf4', ink2:'#9fb0c4', ink3:'#647488', line:'rgba(255,255,255,.08)', accent:'#5b9bd6', accent2:'#7db4e6', accentRgb:'91,155,214', surfaceRgb:'21,27,36' },
|
||
},
|
||
steelsky: {
|
||
label: 'Steel Sky', accentBand: 203,
|
||
sw: { accent: '#1c8ee0', grey: '#8595a8' },
|
||
light: { bg:'#f4f6f9', surface:'#ffffff', ink:'#14181f', ink2:'#4a5568', ink3:'#8595a8', line:'rgba(20,40,80,.10)', accent:'#1c8ee0', accent2:'#1573bd', accentRgb:'28,142,224', surfaceRgb:'255,255,255' },
|
||
dark: { bg:'#0d1117', surface:'#151b24', ink:'#e8edf4', ink2:'#9fb0c4', ink3:'#647488', line:'rgba(255,255,255,.08)', accent:'#46b2f4', accent2:'#6cc6f8', accentRgb:'70,178,244', surfaceRgb:'21,27,36' },
|
||
},
|
||
sky: {
|
||
label: 'Sky', accentBand: 203,
|
||
sw: { accent: '#1c8ee0', grey: '#8299ac' },
|
||
light: { bg:'#f4f9fe', surface:'#ffffff', ink:'#0f1f2d', ink2:'#465a6b', ink3:'#8299ac', line:'rgba(20,70,115,.10)', accent:'#1c8ee0', accent2:'#1573bd', accentRgb:'28,142,224', surfaceRgb:'255,255,255' },
|
||
dark: { bg:'#0a1320', surface:'#121e2d', ink:'#e9f3fb', ink2:'#9bb4c9', ink3:'#5f7689', line:'rgba(255,255,255,.08)', accent:'#46b2f4', accent2:'#6cc6f8', accentRgb:'70,178,244', surfaceRgb:'18,30,45' },
|
||
},
|
||
sage: {
|
||
label: 'Sage', accentBand: 160,
|
||
sw: { accent: '#2c9b78', grey: '#7f968a' },
|
||
light: { bg:'#f3f8f4', surface:'#ffffff', ink:'#13211b', ink2:'#475a51', ink3:'#7f968a', line:'rgba(20,65,48,.10)', accent:'#2c9b78', accent2:'#1d8062', accentRgb:'44,155,120', surfaceRgb:'255,255,255' },
|
||
dark: { bg:'#0b1410', surface:'#121d18', ink:'#e7f2eb', ink2:'#9ab4a7', ink3:'#5e7568', line:'rgba(255,255,255,.07)', accent:'#48c29c', accent2:'#67d0b1', accentRgb:'72,194,156', surfaceRgb:'18,29,24' },
|
||
},
|
||
sand: {
|
||
label: 'Sand', accentBand: 175,
|
||
sw: { accent: '#1d7a71', grey: '#948979' },
|
||
light: { bg:'#f8f5ef', surface:'#fffdf8', ink:'#221d15', ink2:'#585044', ink3:'#948979', line:'rgba(75,58,30,.12)', accent:'#1d7a71', accent2:'#145d57', accentRgb:'29,122,113', surfaceRgb:'255,253,248' },
|
||
dark: { bg:'#13110b', surface:'#1b1811', ink:'#f1ece1', ink2:'#b2a797', ink3:'#796e5e', line:'rgba(255,255,255,.07)', accent:'#3eb2a4', accent2:'#5cc3b6', accentRgb:'62,178,164', surfaceRgb:'27,24,17' },
|
||
},
|
||
blush: {
|
||
label: 'Blush', accentBand: 3,
|
||
sw: { accent: '#c25e5a', grey: '#9b8a87' },
|
||
light: { bg:'#faf6f4', surface:'#fffefd', ink:'#271d1c', ink2:'#5e504e', ink3:'#9b8a87', line:'rgba(90,50,45,.11)', accent:'#c25e5a', accent2:'#a4453f', accentRgb:'194,94,90', surfaceRgb:'255,254,253' },
|
||
dark: { bg:'#140f0e', surface:'#1c1615', ink:'#f3eae8', ink2:'#b6a4a1', ink3:'#7d6b68', line:'rgba(255,255,255,.07)', accent:'#df8480', accent2:'#e89f9b', accentRgb:'223,132,128', surfaceRgb:'28,22,21' },
|
||
},
|
||
mist: {
|
||
label: 'Mist', accentBand: 222,
|
||
sw: { accent: '#3b6ef0', grey: '#8893a6' },
|
||
light: { bg:'#f6f8fb', surface:'#ffffff', ink:'#171b22', ink2:'#4e586a', ink3:'#8893a6', line:'rgba(25,35,60,.09)', accent:'#3b6ef0', accent2:'#2f57cf', accentRgb:'59,110,240', surfaceRgb:'255,255,255' },
|
||
dark: { bg:'#0a0d14', surface:'#12161f', ink:'#eef1f7', ink2:'#9aa6bd', ink3:'#606b82', line:'rgba(255,255,255,.07)', accent:'#5b8bff', accent2:'#789fff', accentRgb:'91,139,255', surfaceRgb:'18,22,31' },
|
||
},
|
||
};
|
||
const PALETTE_ORDER = ['steel', 'steelsky', 'sky', 'sage', 'sand', 'blush', 'mist'];
|
||
const DEFAULT_PALETTE = 'steelsky';
|
||
|
||
function getPalette() {
|
||
const stored = localStorage.getItem('palette');
|
||
return (stored && PALETTES[stored]) ? stored : DEFAULT_PALETTE;
|
||
}
|
||
function applyPalette(name) {
|
||
const p = PALETTES[name] || PALETTES[DEFAULT_PALETTE];
|
||
const theme = document.documentElement.getAttribute('data-theme') || 'dark';
|
||
const t = (theme === 'light') ? p.light : p.dark;
|
||
const root = document.documentElement.style;
|
||
root.setProperty('--bg', t.bg);
|
||
root.setProperty('--surface', t.surface);
|
||
root.setProperty('--ink', t.ink);
|
||
root.setProperty('--ink-2', t.ink2);
|
||
root.setProperty('--ink-3', t.ink3);
|
||
root.setProperty('--line', t.line);
|
||
root.setProperty('--accent', t.accent);
|
||
root.setProperty('--accent2', t.accent2);
|
||
root.setProperty('--accent-rgb', t.accentRgb);
|
||
root.setProperty('--surface-rgb', t.surfaceRgb);
|
||
root.setProperty('--accent-band', String(p.accentBand));
|
||
root.setProperty('--hue-default', String(p.accentBand));
|
||
document.documentElement.setAttribute('data-palette', name);
|
||
syncPaletteUI(name);
|
||
if (typeof rerenderTints === 'function') rerenderTints();
|
||
}
|
||
function setPalette(name) {
|
||
if (!PALETTES[name]) return;
|
||
applyPalette(name);
|
||
localStorage.setItem('palette', name);
|
||
}
|
||
function syncPaletteUI(active) {
|
||
document.querySelectorAll('.palette-swatch, .pp-row').forEach(b => {
|
||
b.setAttribute('aria-pressed', String(b.dataset.palette === active));
|
||
});
|
||
}
|
||
function buildPaletteSwitcher() {
|
||
const row = document.getElementById('palette-switch');
|
||
const panel = document.getElementById('palette-pop-panel');
|
||
if (!row || !panel) return;
|
||
PALETTE_ORDER.forEach(key => {
|
||
const p = PALETTES[key];
|
||
const sw = document.createElement('button');
|
||
sw.type = 'button';
|
||
sw.className = 'palette-swatch';
|
||
sw.dataset.palette = key;
|
||
sw.style.setProperty('--sw-accent', p.sw.accent);
|
||
sw.style.setProperty('--sw-grey', p.sw.grey);
|
||
sw.setAttribute('aria-label', p.label + ' palette');
|
||
sw.title = p.label;
|
||
sw.addEventListener('click', () => setPalette(key));
|
||
row.appendChild(sw);
|
||
|
||
const pr = document.createElement('button');
|
||
pr.type = 'button';
|
||
pr.className = 'pp-row';
|
||
pr.dataset.palette = key;
|
||
pr.setAttribute('aria-label', p.label + ' palette');
|
||
const dot = document.createElement('span');
|
||
dot.className = 'pp-dot';
|
||
dot.style.setProperty('--sw-accent', p.sw.accent);
|
||
dot.style.setProperty('--sw-grey', p.sw.grey);
|
||
pr.appendChild(dot);
|
||
pr.appendChild(document.createTextNode(p.label));
|
||
pr.addEventListener('click', () => { setPalette(key); closePalettePop(); });
|
||
panel.appendChild(pr);
|
||
});
|
||
// popover toggle (small screens)
|
||
const popwrap = document.getElementById('palette-popwrap');
|
||
const popBtn = document.getElementById('palette-pop-btn');
|
||
if (popBtn && popwrap) {
|
||
popBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const open = popwrap.classList.toggle('open');
|
||
popBtn.setAttribute('aria-expanded', String(open));
|
||
});
|
||
document.addEventListener('click', (e) => { if (!popwrap.contains(e.target)) closePalettePop(); });
|
||
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closePalettePop(); });
|
||
}
|
||
syncPaletteUI(getPalette());
|
||
}
|
||
function closePalettePop() {
|
||
const popwrap = document.getElementById('palette-popwrap');
|
||
const popBtn = document.getElementById('palette-pop-btn');
|
||
if (popwrap) popwrap.classList.remove('open');
|
||
if (popBtn) popBtn.setAttribute('aria-expanded', 'false');
|
||
}
|
||
|
||
// ── Theme: system preference + localStorage override ──
|
||
function getPreferredTheme() {
|
||
const stored = localStorage.getItem('theme');
|
||
if (stored) return stored;
|
||
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||
}
|
||
function applyTheme(theme) {
|
||
document.documentElement.setAttribute('data-theme', theme);
|
||
applyPalette(getPalette()); // re-resolve anchor tokens for the new theme
|
||
}
|
||
function toggleTheme() {
|
||
const current = document.documentElement.getAttribute('data-theme') || 'dark';
|
||
const next = current === 'dark' ? 'light' : 'dark';
|
||
applyTheme(next);
|
||
localStorage.setItem('theme', next);
|
||
}
|
||
applyTheme(getPreferredTheme());
|
||
buildPaletteSwitcher();
|
||
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (e) => {
|
||
if (!localStorage.getItem('theme')) applyTheme(e.matches ? 'light' : 'dark');
|
||
});
|
||
|
||
// ── Nav scrolled state ──
|
||
(function () {
|
||
const nav = document.getElementById('nav');
|
||
const onScroll = () => nav.classList.toggle('scrolled', window.scrollY > 8);
|
||
window.addEventListener('scroll', onScroll, { passive: true }); onScroll();
|
||
})();
|
||
|
||
// ── Mobile nav drawer ──
|
||
(function () {
|
||
const toggle = document.getElementById('navToggle');
|
||
const menu = document.getElementById('navMenu');
|
||
const scrim = document.getElementById('navScrim');
|
||
if (!toggle || !menu) return;
|
||
function setOpen(open) {
|
||
menu.classList.toggle('open', open);
|
||
toggle.classList.toggle('open', open);
|
||
if (scrim) scrim.classList.toggle('open', open);
|
||
toggle.setAttribute('aria-expanded', String(open));
|
||
document.body.style.overflow = open ? 'hidden' : '';
|
||
}
|
||
toggle.addEventListener('click', () => setOpen(!menu.classList.contains('open')));
|
||
if (scrim) scrim.addEventListener('click', () => setOpen(false));
|
||
menu.addEventListener('click', (e) => { if (e.target.closest('a')) setOpen(false); });
|
||
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') setOpen(false); });
|
||
window.addEventListener('resize', () => { if (window.innerWidth > 860) setOpen(false); });
|
||
})();
|
||
|
||
// ── Demo videos: play near the viewport, tear down far from it ──
|
||
// Demos ship as H.264 <video> (hardware-decoded, a few MB) instead of animated GIF
|
||
// (hundreds of MB of decoded frames — the iOS Safari tab-kill culprit). preload="none"
|
||
// keeps them free until scrolled near. Two rings: within 200px play/pause; beyond
|
||
// 1200px drop the src entirely so the media pipeline is fully released (poster stays).
|
||
(function () {
|
||
const vids = document.querySelectorAll('video.lazy-vid');
|
||
if (!vids.length) return;
|
||
const start = (v) => {
|
||
if (!v.getAttribute('src') && v.dataset.src) { v.setAttribute('src', v.dataset.src); v.load(); }
|
||
v.muted = true; const p = v.play(); if (p) p.catch(() => {});
|
||
};
|
||
if (!('IntersectionObserver' in window)) { vids.forEach(start); return; }
|
||
const near = new IntersectionObserver((entries) => {
|
||
entries.forEach((en) => {
|
||
if (en.isIntersecting) start(en.target);
|
||
else if (!en.target.paused) en.target.pause();
|
||
});
|
||
}, { rootMargin: '200px 0px' });
|
||
const far = new IntersectionObserver((entries) => {
|
||
entries.forEach((en) => {
|
||
const v = en.target;
|
||
if (!en.isIntersecting && v.getAttribute('src')) {
|
||
v.dataset.src = v.getAttribute('src');
|
||
if (!v.paused) v.pause();
|
||
v.removeAttribute('src'); v.load();
|
||
}
|
||
});
|
||
}, { rootMargin: '1200px 0px' });
|
||
vids.forEach(v => { near.observe(v); far.observe(v); });
|
||
})();
|
||
|
||
const REPO = 'https://github.com/HKUDS/CLI-Anything';
|
||
const GITHUB_REPO_API = 'https://api.github.com/repos/HKUDS/CLI-Anything';
|
||
const GITHUB_STARS_BADGE_API = 'https://img.shields.io/github/stars/HKUDS/CLI-Anything.json';
|
||
const REPO_SKILLS_SOURCE = 'HKUDS/CLI-Anything';
|
||
const REGISTRY_URLS = [
|
||
'../../registry.json',
|
||
'https://raw.githubusercontent.com/HKUDS/CLI-Anything/main/registry.json'
|
||
];
|
||
const PUBLIC_REGISTRY_URLS = [
|
||
'../../public_registry.json',
|
||
'https://raw.githubusercontent.com/HKUDS/CLI-Anything/main/public_registry.json'
|
||
];
|
||
const DATES_URLS = [
|
||
'https://hkuds.github.io/CLI-Anything/registry-dates.json',
|
||
'../../registry-dates.json'
|
||
];
|
||
const EXTERNAL_LINK_SVG = '<svg viewBox="0 0 16 16" fill="currentColor"><path d="M3.75 2h3.5a.75.75 0 0 1 0 1.5H4.56l6.72 6.72a.75.75 0 1 1-1.06 1.06L3.5 4.56v2.69a.75.75 0 0 1-1.5 0v-3.5A1.75 1.75 0 0 1 3.75 2Zm7.5 0h1A1.75 1.75 0 0 1 14 3.75v8.5A1.75 1.75 0 0 1 12.25 14h-8.5A1.75 1.75 0 0 1 2 12.25v-1a.75.75 0 0 1 1.5 0v1c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-8.5a.25.25 0 0 0-.25-.25h-1a.75.75 0 0 1 0-1.5Z"/></svg>';
|
||
// Skill glyph — identical to the header nav SKILL link icon, for a unified design language.
|
||
const SKILL_ICON_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5.5 18.5 9.8-9.8"/><path d="m13.7 6.9 3.4 3.4"/><path d="M18 4v2.4"/><path d="M16.8 5.2h2.4"/><path d="M7.5 4.8v2"/><path d="M6.5 5.8h2"/><path d="M18.2 15.8v2"/><path d="M17.2 16.8h2"/></svg>';
|
||
|
||
// ── Flip affordance glyphs ──
|
||
// FRONT → "Details" (chevron-right circle), BACK → "Back" (arrow-left). These deliberately
|
||
// avoid the old circular two-arrow icon, which read as "refresh/reload" rather than "flip".
|
||
const DETAILS_GLYPH = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="m10.5 8.5 3.5 3.5-3.5 3.5"/></svg>';
|
||
const BACK_GLYPH = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5"/><path d="m12 5-7 7 7 7"/></svg>';
|
||
|
||
// ===================== Software icon system (Simple Icons) =====================
|
||
// ICON_REGISTRY maps a CLI/software *name* (lowercased) → a Simple Icons slug.
|
||
// HOW TO ADD AN ICON:
|
||
// 1. Find the slug at https://simpleicons.org (e.g. "Krita" → "krita").
|
||
// 2. Add a lowercased-name → slug entry below (the catalog `name`, not display_name).
|
||
// 3. That's it — iconFor() renders https://cdn.simpleicons.org/<slug> and tints it.
|
||
// Resolver order in iconFor(): (1) entry.icon field if present (future-proofing),
|
||
// (2) ICON_REGISTRY[name], (3) a single-letter monogram fallback. A wrong/missing slug
|
||
// that 404s also degrades to the monogram via <img onerror>.
|
||
const ICON_REGISTRY = {
|
||
gimp: 'gimp', blender: 'blender', obs: 'obsstudio', 'obs-studio': 'obsstudio', obsstudio: 'obsstudio',
|
||
audacity: 'audacity', krita: 'krita', inkscape: 'inkscape', shotcut: 'shotcut', ffmpeg: 'ffmpeg',
|
||
imagemagick: 'imagemagick', darktable: 'darktable', kdenlive: 'kdenlive', davinci: 'davinciresolve',
|
||
'davinci-resolve': 'davinciresolve', handbrake: 'handbrake', vlc: 'vlcmediaplayer', mpv: 'mpv',
|
||
godot: 'godotengine', 'godot-engine': 'godotengine', godotengine: 'godotengine', unity: 'unity',
|
||
unrealengine: 'unrealengine', 'unreal-engine': 'unrealengine', aseprite: 'aseprite', musescore: 'musescore',
|
||
lmms: 'lmms', ardour: 'ardour', reaper: 'reaper',
|
||
github: 'github', gitlab: 'gitlab', git: 'git', slack: 'slack', discord: 'discord', telegram: 'telegram',
|
||
notion: 'notion', obsidian: 'obsidian', zoom: 'zoom', calibre: 'calibre', libreoffice: 'libreoffice',
|
||
figma: 'figma', miro: 'miro', trello: 'trello', asana: 'asana', jira: 'jira', linear: 'linear',
|
||
airtable: 'airtable', todoist: 'todoist', evernote: 'evernote', spotify: 'spotify', youtube: 'youtube',
|
||
vimeo: 'vimeo', twitch: 'twitch', canva: 'canva', adobe: 'adobe', photoshop: 'adobephotoshop',
|
||
premiere: 'adobepremierepro', illustrator: 'adobeillustrator', aftereffects: 'adobeaftereffects',
|
||
docker: 'docker', kubernetes: 'kubernetes', postman: 'postman', insomnia: 'insomnia', vscode: 'visualstudiocode',
|
||
'vs-code': 'visualstudiocode', vim: 'vim', neovim: 'neovim', emacs: 'gnuemacs', sublime: 'sublimetext',
|
||
jupyter: 'jupyter', anaconda: 'anaconda', python: 'python', nodejs: 'nodedotjs', npm: 'npm',
|
||
ghostscript: 'ghostscript', pandoc: 'pandoc', latex: 'latex', tableau: 'tableau', grafana: 'grafana',
|
||
wordpress: 'wordpress', shopify: 'shopify', stripe: 'stripe', mailchimp: 'mailchimp', hubspot: 'hubspot',
|
||
salesforce: 'salesforce', googledrive: 'googledrive', dropbox: 'dropbox', onedrive: 'microsoftonedrive',
|
||
gmail: 'gmail', googlecalendar: 'googlecalendar', googlesheets: 'googlesheets', googledocs: 'googledocs',
|
||
excalidraw: 'excalidraw', drawio: 'diagramsdotnet', openai: 'openai', anthropic: 'anthropic',
|
||
huggingface: 'huggingface', replicate: 'replicate', elevenlabs: 'elevenlabs',
|
||
// Chinese / APAC brands Simple Icons carries (others resolve via the favicon chain below)
|
||
wechat: 'wechat', weixin: 'wechat', qq: 'tencentqq', tencentqq: 'tencentqq',
|
||
bilibili: 'bilibili', weibo: 'sinaweibo', sinaweibo: 'sinaweibo', zhihu: 'zhihu',
|
||
baidu: 'baidu', alipay: 'alipay', dingtalk: 'dingtalk', tiktok: 'tiktok', douyin: 'tiktok',
|
||
xiaohongshu: 'xiaohongshu', taobao: 'taobao', tmall: 'tmall', gitee: 'gitee',
|
||
alibabacloud: 'alibabacloud', huawei: 'huawei', xiaomi: 'xiaomi',
|
||
};
|
||
|
||
// Synonyms & non-English names → a canonical key (used for BOTH the Simple Icons slug and
|
||
// the favicon-domain lookup). Powers 近义查询 (synonyms); paired with the containment scan
|
||
// in resolveIconKey() for 模糊查询 (fuzzy). Add an alias to widen what a CLI name can match.
|
||
const ICON_ALIASES = {
|
||
feishu: 'lark', lark: 'lark', larksuite: 'lark', '飞书': 'lark',
|
||
wecom: 'wecom', wework: 'wecom', wechatwork: 'wecom', weixinwork: 'wecom', '企业微信': 'wecom',
|
||
minimax: 'minimax', minimaxi: 'minimax',
|
||
wechat: 'wechat', weixin: 'wechat', '微信': 'wechat',
|
||
dingtalk: 'dingtalk', dingding: 'dingtalk', '钉钉': 'dingtalk',
|
||
alipay: 'alipay', '支付宝': 'alipay',
|
||
bilibili: 'bilibili', bili: 'bilibili', '哔哩哔哩': 'bilibili',
|
||
weibo: 'weibo', '微博': 'weibo', sinaweibo: 'weibo',
|
||
zhihu: 'zhihu', '知乎': 'zhihu', baidu: 'baidu', '百度': 'baidu',
|
||
taobao: 'taobao', '淘宝': 'taobao', tmall: 'tmall', '天猫': 'tmall',
|
||
douyin: 'douyin', '抖音': 'douyin', tiktok: 'tiktok',
|
||
kuaishou: 'kuaishou', '快手': 'kuaishou', meituan: 'meituan', '美团': 'meituan',
|
||
amap: 'amap', gaode: 'amap', '高德': 'amap', netease: 'netease', '网易': 'netease',
|
||
neteasemusic: 'neteasemusic', qqmusic: 'qqmusic', yuque: 'yuque', '语雀': 'yuque',
|
||
xiaohongshu: 'xiaohongshu', rednote: 'xiaohongshu', '小红书': 'xiaohongshu',
|
||
qq: 'qq', tencent: 'tencent',
|
||
};
|
||
|
||
// Curated real-logo favicon domains (authoritative — beats a generic homepage such as a
|
||
// github.com repo URL). Apps Simple Icons lacks still get a real colour logo from here.
|
||
const ICON_DOMAINS = {
|
||
lark: 'larksuite.com', wecom: 'work.weixin.qq.com', minimax: 'minimaxi.com',
|
||
wechat: 'weixin.qq.com', dingtalk: 'dingtalk.com', alipay: 'alipay.com',
|
||
bilibili: 'bilibili.com', weibo: 'weibo.com', zhihu: 'zhihu.com', baidu: 'baidu.com',
|
||
taobao: 'taobao.com', tmall: 'tmall.com', douyin: 'douyin.com', tiktok: 'tiktok.com',
|
||
kuaishou: 'kuaishou.com', meituan: 'meituan.com', amap: 'amap.com', netease: '163.com',
|
||
neteasemusic: 'music.163.com', qqmusic: 'y.qq.com', yuque: 'yuque.com',
|
||
xiaohongshu: 'xiaohongshu.com', qq: 'qq.com', tencent: 'tencent.com',
|
||
};
|
||
// Generic code/registry hosts we never use as a favicon source (they'd brand every app the
|
||
// same). When an entry's homepage is one of these, we skip straight to the monogram.
|
||
const ICON_GENERIC_HOSTS = new Set(['github.com', 'gitlab.com', 'bitbucket.org', 'npmjs.com', 'pypi.org', 'gitee.com', 'sourceforge.net', 'readthedocs.io', 'readthedocs.org', 'codeberg.org']);
|
||
|
||
function iconNormName(s) {
|
||
return String(s || '').toLowerCase().trim()
|
||
.replace(/[\s_]*cli$/, '').replace(/^cli[\s_-]*/, '').replace(/[\s_\-./]+/g, '');
|
||
}
|
||
function iconDomainFromUrl(u) { try { return new URL(u).hostname.replace(/^www\./, ''); } catch (_) { return ''; } }
|
||
// Multi-source <img> fallback: walk data-fb (pipe-separated URLs) before degrading to a monogram.
|
||
window.iconFallback = function (img) {
|
||
const fbs = (img.getAttribute('data-fb') || '').split('|').filter(Boolean);
|
||
if (fbs.length) { const next = fbs.shift(); img.setAttribute('data-fb', fbs.join('|')); if (next.indexOf('simpleicons') === -1) img.classList.add('raw'); img.src = next; return; }
|
||
const span = document.createElement('span'); span.className = 'icon-mono';
|
||
span.textContent = img.getAttribute('data-letter') || '?';
|
||
img.replaceWith(span);
|
||
};
|
||
// Cross-reference resolver: alias (synonym) → exact key → containment (fuzzy) scan.
|
||
function resolveIconKey(name) {
|
||
const n = iconNormName(name);
|
||
if (!n) return '';
|
||
if (ICON_ALIASES[n]) return ICON_ALIASES[n];
|
||
if (ICON_REGISTRY[n] || ICON_DOMAINS[n]) return n;
|
||
const keys = Object.keys(ICON_ALIASES).concat(Object.keys(ICON_REGISTRY), Object.keys(ICON_DOMAINS));
|
||
for (const k of keys) { if (k.length >= 3 && (n.includes(k) || k.includes(n))) return ICON_ALIASES[k] || k; }
|
||
return n;
|
||
}
|
||
|
||
// Tasteful single-letter monogram fallback (one clean letter, palette-tinted tile).
|
||
function iconMono(label) {
|
||
const ch = (String(label || '?').trim()[0] || '?').toUpperCase();
|
||
return `<span class="icon-mono">${esc(ch)}</span>`;
|
||
}
|
||
// Resolve a CLI/catalog entry (or bare name string) to a tinted Simple Icons <img>, or
|
||
// the monogram fallback. extraClass lets callers reuse the tile at different sizes.
|
||
function iconFor(cli, extraClass) {
|
||
const obj = (cli && typeof cli === 'object') ? cli : null;
|
||
const label = obj ? (obj.display_name || obj.name) : String(cli || '');
|
||
const rawName = String(obj ? obj.name : cli || '');
|
||
const cls = 'sw-icon' + (extraClass ? ' ' + extraClass : '');
|
||
const letter = (String(label || '?').trim()[0] || '?').toUpperCase();
|
||
const key = resolveIconKey(rawName);
|
||
|
||
// (1) explicit slug on the entry, then (2) Simple Icons by resolved key or raw name.
|
||
let slug = obj && typeof obj.icon === 'string' && obj.icon.trim() ? obj.icon.trim().toLowerCase() : '';
|
||
if (!slug) slug = ICON_REGISTRY[key] || ICON_REGISTRY[iconNormName(rawName)] || '';
|
||
// (3) real-logo favicon domain: curated first, else the entry's own homepage (skip code hosts).
|
||
let domain = ICON_DOMAINS[key] || '';
|
||
if (!domain && obj) { const hp = iconDomainFromUrl(obj.homepage || obj.source_url || ''); if (hp && !ICON_GENERIC_HOSTS.has(hp)) domain = hp; }
|
||
|
||
// Ordered source chain → monogram. Simple Icons (clean monochrome) first; then live
|
||
// favicons widen coverage to apps it lacks (esp. Chinese: Lark, WeCom, MiniMax, DingTalk…).
|
||
const srcs = [];
|
||
if (slug) srcs.push(`https://cdn.simpleicons.org/${esc(slug)}`);
|
||
if (domain) {
|
||
srcs.push(`https://icons.duckduckgo.com/ip3/${esc(domain)}.ico`);
|
||
srcs.push(`https://www.google.com/s2/favicons?domain=${esc(domain)}&sz=64`);
|
||
}
|
||
if (!srcs.length) return `<span class="${cls}" title="${escAttr(label)}">${iconMono(label)}</span>`;
|
||
const first = srcs.shift();
|
||
const rawCls = first.indexOf('simpleicons') === -1 ? ' class="raw"' : '';
|
||
return `<span class="${cls}" title="${escAttr(label)}"><img${rawCls} loading="lazy" decoding="async" ` +
|
||
`src="${first}" data-fb="${escAttr(srcs.join('|'))}" data-letter="${escAttr(letter)}" ` +
|
||
`alt="${escAttr(label)} icon" onerror="iconFallback(this)"></span>`;
|
||
}
|
||
|
||
// Per-category tints follow the ACTIVE palette's accent hue (catHue centres on the
|
||
// palette accent band and spreads each category a small signed offset around it), so the
|
||
// whole page reads as one colour family that recolours with the palette: warm palettes
|
||
// (Sand, Blush) get warm tints, cool palettes (Steel, Sky, Mist) stay cool, Sage stays
|
||
// green. Saturation/lightness are fixed in CSS (legible in both themes); we only vary hue.
|
||
const TINT_SPREAD = 25; // categories fan ±~25° around the palette accent hue
|
||
// ordered offsets in [-1, 1]; scaled by TINT_SPREAD so the spread tracks the band
|
||
const CATEGORY_OFFSET = {
|
||
video: -0.30, knowledge: 0, '3d': 0.58, game: -0.74, image: 1.0,
|
||
ai: 0.30, audio: -0.52, dev: 0.15, design: 0.86, productivity: -0.15,
|
||
creative: 0.72, web: 0, data: -0.38, science: 0.44, business: 0.22, other: 0
|
||
};
|
||
// hues wrap on the 0–360 circle (Blush's ~3° accent fans into both reds and oranges)
|
||
function wrapHue(h) { return ((h % 360) + 360) % 360; }
|
||
function catHue(cat) {
|
||
const band = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--accent-band')) || 209;
|
||
const key = String(cat || 'other');
|
||
let norm;
|
||
if (Object.prototype.hasOwnProperty.call(CATEGORY_OFFSET, key)) {
|
||
norm = CATEGORY_OFFSET[key];
|
||
} else {
|
||
// deterministic offset in [-1, 1] for any unlisted category
|
||
let h = 0; for (let i = 0; i < key.length; i++) h = (h * 31 + key.charCodeAt(i)) % 101;
|
||
norm = (h - 50) / 50;
|
||
}
|
||
return wrapHue(Math.round(band + norm * TINT_SPREAD));
|
||
}
|
||
|
||
// Recompute per-category --ch in place whenever the palette (accent band) changes,
|
||
// so tints re-bias toward the new accent without losing card/scroll/flip state.
|
||
function rerenderTints() {
|
||
document.querySelectorAll('[data-cat]').forEach(el => {
|
||
el.style.setProperty('--ch', String(catHue(el.dataset.cat)));
|
||
});
|
||
// scope banner follows the scoped matrix's hue
|
||
const sb = document.getElementById('scopeBanner');
|
||
if (sb && STATE.scope) {
|
||
const m = STATE.matrices.find(x => x.name === STATE.scope);
|
||
if (m) sb.style.setProperty('--ch', String(catHue(m.category)));
|
||
}
|
||
}
|
||
|
||
let harnessClis = [];
|
||
let publicClis = [];
|
||
let activeDeck = 'harness';
|
||
let deckSuspendTimer = null;
|
||
// Collapse heavy effects (glass blur, the 3D flip contexts, sticky-nav blur, grain) for the
|
||
// duration of ANY deck movement — drag or animated switch — so panels translate as flat,
|
||
// GPU-cached layers. This is what makes the slide smooth on Safari. Auto-clears 560ms after
|
||
// the last movement (covers the .5s settle transition); each move resets the timer.
|
||
function armDeckSuspend() {
|
||
document.documentElement.classList.add('deck-sliding');
|
||
clearTimeout(deckSuspendTimer);
|
||
deckSuspendTimer = setTimeout(() => document.documentElement.classList.remove('deck-sliding'), 560);
|
||
}
|
||
let wheelSwitchLockUntil = 0;
|
||
|
||
const deckGesture = { pointerId: null, startX: 0, deltaX: 0, dragging: false };
|
||
const deckState = {
|
||
harness: { filter: 'all', sort: 'updated', query: '' },
|
||
public: { filter: 'all', sort: 'updated', query: '' },
|
||
};
|
||
|
||
async function fetchJson(urls) {
|
||
for (const url of urls) {
|
||
try { const resp = await fetch(url); if (resp.ok) return await resp.json(); }
|
||
catch (_) { continue; }
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function formatCompactCount(value) {
|
||
return new Intl.NumberFormat('en', {
|
||
notation: 'compact', maximumFractionDigits: value >= 10000 ? 0 : 1,
|
||
}).format(value);
|
||
}
|
||
function formatBadgeCount(value) {
|
||
if (typeof value !== 'string') return null;
|
||
const label = value.trim();
|
||
return label ? label.toUpperCase() : null;
|
||
}
|
||
function renderGitHubStars(label, title) {
|
||
document.querySelectorAll('[data-github-stars]').forEach(el => {
|
||
el.textContent = label; el.setAttribute('aria-label', title); el.title = title;
|
||
});
|
||
}
|
||
async function loadGitHubStars() {
|
||
try {
|
||
try {
|
||
const badgeResp = await fetch(GITHUB_STARS_BADGE_API);
|
||
if (badgeResp.ok) {
|
||
const badge = await badgeResp.json();
|
||
const badgeLabel = formatBadgeCount(badge.message || badge.value);
|
||
if (badgeLabel) { renderGitHubStars(badgeLabel, `${badgeLabel} GitHub stars`); return; }
|
||
}
|
||
} catch (_) {}
|
||
const resp = await fetch(GITHUB_REPO_API, { headers: { 'Accept': 'application/vnd.github+json' } });
|
||
if (!resp.ok) return;
|
||
const repo = await resp.json();
|
||
const stars = repo.stargazers_count;
|
||
if (typeof stars !== 'number') return;
|
||
renderGitHubStars(formatCompactCount(stars), `${stars.toLocaleString('en-US')} GitHub stars`);
|
||
} catch (_) {}
|
||
}
|
||
|
||
async function loadRegistries() {
|
||
try {
|
||
const [harnessData, publicData, dates, matrixData] = await Promise.all([
|
||
fetchJson(REGISTRY_URLS), fetchJson(PUBLIC_REGISTRY_URLS), fetchJson(DATES_URLS), fetchJson(MATRIX_URLS),
|
||
]);
|
||
if (!harnessData) throw new Error('Registry load failed');
|
||
|
||
harnessClis = harnessData.clis;
|
||
if (dates) harnessClis.forEach(c => c.last_modified = dates[c.name]);
|
||
if (publicData && publicData.clis) {
|
||
publicClis = publicData.clis;
|
||
if (dates) publicClis.forEach(c => c.last_modified = dates[c.name]);
|
||
}
|
||
|
||
const totalClis = harnessClis.length + publicClis.length;
|
||
const allCategories = [...new Set([...harnessClis, ...publicClis].map(c => c.category))];
|
||
document.getElementById('deck-count-harness').textContent = harnessClis.length;
|
||
document.getElementById('deck-count-public').textContent = publicClis.length;
|
||
|
||
const harnessCats = [...new Set(harnessClis.map(c => c.category))];
|
||
buildFilters('harness', harnessCats);
|
||
const publicCats = [...new Set(publicClis.map(c => c.category))];
|
||
buildFilters('public', publicCats);
|
||
|
||
// Build name -> entry map for the unification join (harness wins on name clash)
|
||
STATE.byName.clear();
|
||
publicClis.forEach(c => { c._deck = 'public'; STATE.byName.set(c.name, c); });
|
||
harnessClis.forEach(c => { c._deck = 'harness'; STATE.byName.set(c.name, c); });
|
||
|
||
// Ingest matrices BEFORE the first deck render so membership chips land in the one
|
||
// and only render. (The old flow rendered both decks, then re-rendered BOTH again
|
||
// when the matrix registry arrived — doubling the heaviest work on the page, right
|
||
// under the hero counter animation.)
|
||
ingestMatrices(matrixData);
|
||
|
||
renderDeck('harness');
|
||
|
||
// Start the hero counters only after the visible deck has rendered AND painted
|
||
// (double rAF) — started before, their animation frames were starved by the big
|
||
// render and the count visibly stuttered.
|
||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||
animateCount(document.getElementById('cli-count'), totalClis, 980);
|
||
animateCount(document.getElementById('category-count'), allCategories.length, 860);
|
||
}));
|
||
|
||
// The public deck sits on the offscreen carousel panel and the matrix shelf is far
|
||
// below the fold — render them after the counters finish rather than competing with
|
||
// the animation for the main thread. switchDeck() renders on demand if the user
|
||
// swipes over first.
|
||
setTimeout(renderDeferredSections, 1100);
|
||
} catch (e) {
|
||
document.getElementById('grid-harness').innerHTML =
|
||
'<div class="empty">Failed to load registry. <a href="' + REPO + '/blob/main/registry.json" style="color:var(--accent)">View source</a></div>';
|
||
}
|
||
}
|
||
|
||
// ── Matrices (the unification): build membership from the already-fetched registry ──
|
||
function ingestMatrices(data) {
|
||
if (!data || !Array.isArray(data.matrices) || !data.matrices.length) {
|
||
STATE.matrices = [];
|
||
return;
|
||
}
|
||
STATE.matrices = data.matrices;
|
||
STATE.membership.clear();
|
||
STATE.matrices.forEach(m => {
|
||
(m.clis || []).forEach(n => {
|
||
if (!STATE.membership.has(n)) STATE.membership.set(n, []);
|
||
STATE.membership.get(n).push(m);
|
||
});
|
||
});
|
||
const capCount = STATE.matrices.reduce((s, m) => s + ((m.capabilities && m.capabilities.length) || 0), 0);
|
||
const ms = document.getElementById('mat-stat');
|
||
if (ms) ms.textContent = STATE.matrices.length + ' matrices · ' + capCount + ' capabilities';
|
||
}
|
||
|
||
// Deferred second wave: the offscreen public deck + the below-the-fold matrix shelf.
|
||
let deferredSectionsDone = false;
|
||
function renderDeferredSections() {
|
||
if (deferredSectionsDone) return;
|
||
deferredSectionsDone = true;
|
||
renderDeck('public');
|
||
const shelf = document.getElementById('shelf');
|
||
if (!STATE.matrices.length) {
|
||
if (shelf) shelf.innerHTML = '<div class="empty">Could not load the matrix registry right now — the catalog below is unaffected. Try refreshing, or browse on <a href="' + REPO + '" target="_blank" rel="noopener">GitHub</a>.</div>';
|
||
const ms = document.getElementById('mat-stat'); if (ms) ms.textContent = '';
|
||
} else {
|
||
renderShelf();
|
||
}
|
||
syncFlipA11y();
|
||
}
|
||
|
||
const CATEGORY_LABELS = { 'ai': 'AI', '3d': '3D' };
|
||
function catLabel(cat) {
|
||
return CATEGORY_LABELS[cat] || cat.charAt(0).toUpperCase() + cat.slice(1);
|
||
}
|
||
|
||
function buildFilters(deck, categories) {
|
||
const container = document.getElementById('filters-' + deck);
|
||
container.innerHTML = '';
|
||
const allBtn = document.createElement('button');
|
||
allBtn.className = 'filter-btn active';
|
||
allBtn.textContent = 'All';
|
||
allBtn.dataset.cat = 'all';
|
||
allBtn.dataset.deck = deck;
|
||
allBtn.onclick = () => setFilter(deck, 'all');
|
||
container.appendChild(allBtn);
|
||
|
||
categories.sort().forEach(cat => {
|
||
const btn = document.createElement('button');
|
||
btn.className = 'filter-btn';
|
||
btn.textContent = catLabel(cat);
|
||
btn.dataset.cat = cat;
|
||
btn.dataset.deck = deck;
|
||
btn.onclick = () => setFilter(deck, cat);
|
||
container.appendChild(btn);
|
||
});
|
||
}
|
||
|
||
function setFilter(deck, cat) {
|
||
deckState[deck].filter = cat;
|
||
document.querySelectorAll(`#filters-${deck} .filter-btn`).forEach(b => {
|
||
b.classList.toggle('active', b.dataset.cat === cat);
|
||
});
|
||
renderDeck(deck);
|
||
}
|
||
|
||
function switchDeck(deck) {
|
||
// the public deck's first render is deferred; materialize it before sliding to it
|
||
if (deck === 'public') renderDeferredSections();
|
||
activeDeck = deck;
|
||
const track = document.getElementById('deck-track');
|
||
track.dataset.active = deck;
|
||
armDeckSuspend();
|
||
updateDeckPosition();
|
||
document.querySelectorAll('.deck-tab').forEach(t => {
|
||
t.classList.toggle('active', t.dataset.deck === deck);
|
||
});
|
||
document.querySelectorAll('.deck-dot').forEach(d => {
|
||
d.classList.toggle('active', d.dataset.deck === deck);
|
||
});
|
||
const toggle = document.getElementById('deck-toggle');
|
||
if (deck === 'harness') {
|
||
toggle.querySelector('span').textContent = 'Public CLIs';
|
||
toggle.querySelector('svg').innerHTML = '<polyline points="9 18 15 12 9 6"/>';
|
||
toggle.classList.remove('deck-arrow-btn--back');
|
||
} else {
|
||
toggle.querySelector('span').textContent = 'CLI-Anything CLIs';
|
||
toggle.querySelector('svg').innerHTML = '<polyline points="15 18 9 12 15 6"/>';
|
||
toggle.classList.add('deck-arrow-btn--back');
|
||
}
|
||
}
|
||
function toggleDeck() { switchDeck(activeDeck === 'harness' ? 'public' : 'harness'); }
|
||
|
||
function getDeckMetrics() {
|
||
const viewport = document.querySelector('.deck-viewport');
|
||
const track = document.getElementById('deck-track');
|
||
if (!viewport || !track) return null;
|
||
// Each panel is exactly one viewport wide, so the slide distance is the viewport width.
|
||
const panelWidth = viewport.clientWidth;
|
||
return { viewport, track, panelWidth, maxShift: panelWidth };
|
||
}
|
||
function setDeckTransform(offset = 0) {
|
||
const metrics = getDeckMetrics();
|
||
if (!metrics) return;
|
||
const { track, maxShift } = metrics;
|
||
const baseShift = activeDeck === 'public' ? -maxShift : 0;
|
||
const clampedShift = Math.min(0, Math.max(-maxShift, baseShift + offset));
|
||
track.style.transform = `translateX(${clampedShift}px)`;
|
||
}
|
||
function updateDeckPosition() { setDeckTransform(); }
|
||
|
||
function finishDeckDrag() {
|
||
const viewport = document.querySelector('.deck-viewport');
|
||
const track = document.getElementById('deck-track');
|
||
if (viewport) viewport.classList.remove('dragging');
|
||
if (track) track.classList.remove('is-dragging');
|
||
deckGesture.pointerId = null; deckGesture.startX = 0; deckGesture.deltaX = 0; deckGesture.dragging = false;
|
||
}
|
||
function initDeckGestures() {
|
||
const viewport = document.querySelector('.deck-viewport');
|
||
const track = document.getElementById('deck-track');
|
||
if (!viewport || !track) return;
|
||
const isInteractiveTarget = (target) => !!target.closest('a, button, input, select, textarea, label');
|
||
|
||
viewport.addEventListener('pointerdown', (e) => {
|
||
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
||
if (isInteractiveTarget(e.target)) return;
|
||
deckGesture.pointerId = e.pointerId; deckGesture.startX = e.clientX; deckGesture.deltaX = 0; deckGesture.dragging = true;
|
||
viewport.classList.add('dragging'); track.classList.add('is-dragging');
|
||
if (viewport.setPointerCapture) viewport.setPointerCapture(e.pointerId);
|
||
});
|
||
viewport.addEventListener('pointermove', (e) => {
|
||
if (!deckGesture.dragging || e.pointerId !== deckGesture.pointerId) return;
|
||
deckGesture.deltaX = e.clientX - deckGesture.startX;
|
||
armDeckSuspend();
|
||
setDeckTransform(deckGesture.deltaX);
|
||
});
|
||
const endDrag = (e) => {
|
||
if (!deckGesture.dragging || e.pointerId !== deckGesture.pointerId) return;
|
||
const deltaX = deckGesture.deltaX;
|
||
finishDeckDrag();
|
||
if (activeDeck === 'harness' && deltaX < -60) { switchDeck('public'); return; }
|
||
if (activeDeck === 'public' && deltaX > 60) { switchDeck('harness'); return; }
|
||
updateDeckPosition();
|
||
};
|
||
viewport.addEventListener('pointerup', endDrag);
|
||
viewport.addEventListener('pointercancel', endDrag);
|
||
viewport.addEventListener('lostpointercapture', () => {
|
||
if (!deckGesture.dragging) return;
|
||
finishDeckDrag(); updateDeckPosition();
|
||
});
|
||
viewport.addEventListener('wheel', (e) => {
|
||
const now = Date.now();
|
||
if (now < wheelSwitchLockUntil) return;
|
||
if (Math.abs(e.deltaX) < 24 || Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;
|
||
e.preventDefault();
|
||
if (e.deltaX > 0 && activeDeck !== 'public') { switchDeck('public'); wheelSwitchLockUntil = now + 450; }
|
||
else if (e.deltaX < 0 && activeDeck !== 'harness') { switchDeck('harness'); wheelSwitchLockUntil = now + 450; }
|
||
}, { passive: false });
|
||
}
|
||
|
||
function renderDeck(deck) {
|
||
let clis = deck === 'harness' ? harnessClis : publicClis;
|
||
const state = deckState[deck];
|
||
const query = state.query.toLowerCase();
|
||
|
||
// matrix scoping: restrict to the scoped matrix's member CLIs
|
||
if (STATE.scope) {
|
||
const m = STATE.matrices.find(x => x.name === STATE.scope);
|
||
if (m) { const set = new Set(m.clis || []); clis = clis.filter(c => set.has(c.name)); }
|
||
}
|
||
|
||
const filtered = clis.filter(c => {
|
||
const matchFilter = state.filter === 'all' || c.category === state.filter;
|
||
const matchSearch = !query ||
|
||
c.display_name.toLowerCase().includes(query) ||
|
||
c.description.toLowerCase().includes(query) ||
|
||
c.name.toLowerCase().includes(query);
|
||
return matchFilter && matchSearch;
|
||
});
|
||
|
||
filtered.sort((a, b) => {
|
||
if (state.sort === 'updated') return (b.last_modified || '').localeCompare(a.last_modified || '');
|
||
if (state.sort === 'name') return a.display_name.localeCompare(b.display_name);
|
||
if (state.sort === 'category') return a.category.localeCompare(b.category) || a.display_name.localeCompare(b.display_name);
|
||
return 0;
|
||
});
|
||
|
||
const grid = document.getElementById('grid-' + deck);
|
||
if (filtered.length === 0) {
|
||
grid.innerHTML = '<div class="empty">No CLIs match your search.</div>';
|
||
state.fullList = []; state.shownCount = 0;
|
||
updateDeckSentinel(deck);
|
||
return;
|
||
}
|
||
// Phones (single-column) render in growing batches: a full 79-card render
|
||
// means ~28k px of live DOM, and laying out + rasterizing it during a fast
|
||
// fling is what killed the tab on iPhones. Desktop masonry keeps the full
|
||
// set so its shortest-column balancing stays exact.
|
||
state.fullList = filtered;
|
||
state.shownCount = Math.min(deckBatchLimit(grid), filtered.length);
|
||
const shown = filtered.slice(0, state.shownCount);
|
||
if (deck === 'harness') grid.innerHTML = shown.map(renderHarnessCard).join('');
|
||
else grid.innerHTML = shown.map(renderNpmCard).join('');
|
||
layoutMasonry(grid);
|
||
updateDeckSentinel(deck);
|
||
}
|
||
|
||
// ── Progressive deck batches (phones) ──
|
||
const DECK_BATCH_SIZE = 20;
|
||
function deckBatchLimit(grid) {
|
||
// Batch only in single-column layout and only when IO can grow the list.
|
||
return (deckSentinelIO && masonryColumnCount(grid) === 1) ? DECK_BATCH_SIZE : Infinity;
|
||
}
|
||
function appendDeckBatch(deck) {
|
||
const state = deckState[deck];
|
||
if (!state.fullList || state.shownCount >= state.fullList.length) return;
|
||
const grid = document.getElementById('grid-' + deck);
|
||
const next = state.fullList.slice(state.shownCount, state.shownCount + DECK_BATCH_SIZE);
|
||
state.shownCount += next.length;
|
||
grid.insertAdjacentHTML('beforeend', next.map(deck === 'harness' ? renderHarnessCard : renderNpmCard).join(''));
|
||
layoutMasonry(grid);
|
||
updateDeckSentinel(deck);
|
||
}
|
||
function updateDeckSentinel(deck) {
|
||
const s = document.getElementById('sentinel-' + deck);
|
||
if (!s || !deckSentinelIO) return;
|
||
const state = deckState[deck];
|
||
const more = !!(state.fullList && state.shownCount < state.fullList.length);
|
||
s.hidden = !more;
|
||
// Re-observe so IO re-evaluates even when the sentinel never left the
|
||
// margin band (otherwise a still-intersecting sentinel fires no new event).
|
||
deckSentinelIO.unobserve(s);
|
||
if (more) deckSentinelIO.observe(s);
|
||
}
|
||
const deckSentinelIO = ('IntersectionObserver' in window)
|
||
? new IntersectionObserver((entries) => {
|
||
entries.forEach((en) => { if (en.isIntersecting) appendDeckBatch(en.target.dataset.deck); });
|
||
}, { rootMargin: '1600px 0px' })
|
||
: null;
|
||
|
||
// ── Engine-independent JS masonry: shortest-column distribution ──
|
||
// Rebuilds the grid's .grid-col wrappers from the current set of .card elements and
|
||
// drops each next card into the currently shortest column. Idempotent / re-runnable.
|
||
function masonryColumnCount(grid) {
|
||
// Measure the grid's OWN usable width (capped by max-width), not the over-wide
|
||
// swipe-carousel panel it lives in. clientWidth already reflects the max-width cap.
|
||
const w = grid.clientWidth || grid.getBoundingClientRect().width || 0;
|
||
if (w >= 900) return 3;
|
||
if (w >= 600) return 2;
|
||
return 1;
|
||
}
|
||
function layoutMasonry(grid) {
|
||
if (!grid) return;
|
||
// Collect the current card set (whether loose or already inside columns).
|
||
const cards = Array.from(grid.querySelectorAll(':scope > .card, :scope > .grid-col > .card'));
|
||
if (!cards.length) return; // empty/error states render a bare .empty div — leave it
|
||
const cols = masonryColumnCount(grid);
|
||
|
||
// Rebuild column wrappers from scratch so the layout is fully re-runnable. The
|
||
// wrappers must be live in the DOM so offsetHeight reflects real laid-out height as
|
||
// we go, which is what makes the shortest-column placement correct.
|
||
grid.innerHTML = '';
|
||
const wrappers = [];
|
||
for (let i = 0; i < cols; i++) {
|
||
const col = document.createElement('div');
|
||
col.className = 'grid-col';
|
||
grid.appendChild(col);
|
||
wrappers.push(col);
|
||
}
|
||
// Place each card into the currently shortest column, preserving overall card order.
|
||
cards.forEach(card => {
|
||
let target = wrappers[0];
|
||
let min = wrappers[0].offsetHeight;
|
||
for (let i = 1; i < wrappers.length; i++) {
|
||
const h = wrappers[i].offsetHeight;
|
||
if (h < min) { min = h; target = wrappers[i]; }
|
||
}
|
||
target.appendChild(card);
|
||
});
|
||
}
|
||
function relayoutAllGrids() {
|
||
layoutMasonry(document.getElementById('grid-harness'));
|
||
layoutMasonry(document.getElementById('grid-public'));
|
||
}
|
||
|
||
function repoSkillId(skillPath) {
|
||
if (!skillPath || skillPath.startsWith('http')) return '';
|
||
const match = skillPath.match(/^skills\/([^/]+)\/SKILL\.md$/);
|
||
return match ? match[1] : '';
|
||
}
|
||
function isWebUrl(value) { return /^https?:\/\//i.test(value || ''); }
|
||
function isRepoSkillPath(value) { return !/\s/.test(value || '') && /(^\.{0,2}\/|\/|\.md$|\.txt$)/i.test(value || ''); }
|
||
function normalizeNpxSkillsCmd(cmd) {
|
||
if (!cmd) return '';
|
||
const trimmed = cmd.trim();
|
||
if (!trimmed.startsWith('npx skills add ')) return '';
|
||
return trimmed.replace(/\s+-g\b/g, '').replace(/\s+-y\b/g, '').trim() + ' -g -y';
|
||
}
|
||
function harnessSkillCmd(c) {
|
||
const skillId = repoSkillId(c.skill_md);
|
||
return skillId ? `npx skills add ${REPO_SKILLS_SOURCE} --skill ${skillId} -g -y` : '';
|
||
}
|
||
function publicSkillCmd(c) { return normalizeNpxSkillsCmd(c.skill_md); }
|
||
|
||
function renderSkillAction(value) {
|
||
if (!value) return '';
|
||
if (isWebUrl(value)) return `<a href="${esc(value)}" target="_blank">Skill</a>`;
|
||
if (isRepoSkillPath(value)) return `<a href="${REPO}/blob/main/${esc(value)}" target="_blank">Skill</a>`;
|
||
return `
|
||
<span class="skill-popover">
|
||
<button class="card-link-btn skill-trigger" type="button" aria-expanded="false" data-skill-value="${escAttr(value)}">Skill</button>
|
||
<span class="skill-popover-panel">
|
||
<span class="skill-popover-label">Skill install</span>
|
||
<code>${esc(value)}</code>
|
||
<button class="card-copy-btn skill-popover-copy" type="button" data-copy-value="${escAttr(value)}">Copy</button>
|
||
</span>
|
||
</span>`;
|
||
}
|
||
|
||
// FRONT-face Skill control: small icon+label consistent with the card visual language.
|
||
// Same three cases as renderSkillAction (web URL → link, repo skill path → link,
|
||
// npx popover → trigger + popover), with the header SKILL icon prepended.
|
||
function renderSkillFront(value) {
|
||
if (!value) return '';
|
||
if (isWebUrl(value)) return `<a class="card-skill-front" href="${esc(value)}" target="_blank">${SKILL_ICON_SVG}<span>Skill</span></a>`;
|
||
if (isRepoSkillPath(value)) return `<a class="card-skill-front" href="${REPO}/blob/main/${esc(value)}" target="_blank">${SKILL_ICON_SVG}<span>Skill</span></a>`;
|
||
return `
|
||
<span class="skill-popover">
|
||
<button class="card-skill-front skill-trigger" type="button" aria-expanded="false" data-skill-value="${escAttr(value)}">${SKILL_ICON_SVG}<span>Skill</span></button>
|
||
<span class="skill-popover-panel">
|
||
<span class="skill-popover-label">Skill install</span>
|
||
<code>${esc(value)}</code>
|
||
<button class="card-copy-btn skill-popover-copy" type="button" data-copy-value="${escAttr(value)}">Copy</button>
|
||
</span>
|
||
</span>`;
|
||
}
|
||
|
||
// Clean install box: just the command lines (NO inline "# step …" comments) + Copy-all.
|
||
// `label` is a short plain-language hint rendered ABOVE the box; `numbered` adds tiny
|
||
// 1·2 step markers in that label when there are two commands.
|
||
function renderInstallStack(steps, label, numbered) {
|
||
const cmds = steps.map(s => s.cmd);
|
||
const display = cmds.map(cmd => `<span class="install-command">${esc(cmd)}</span>`).join('');
|
||
const copyText = cmds.join('\n');
|
||
let head = '';
|
||
if (label) {
|
||
head = `<div class="install-steps-label">${esc(label)}</div>`;
|
||
} else if (numbered && cmds.length === 2) {
|
||
head = `<div class="install-steps-label"><span class="isl-num">1</span>Install the CLI<span class="isl-sep">·</span><span class="isl-num">2</span>add the skill</div>`;
|
||
}
|
||
return `<div class="install-steps">
|
||
${head}
|
||
<div class="card-install">
|
||
<code>${display}</code>
|
||
<button class="card-copy-btn" onclick='copyCmd(this, ${JSON.stringify(copyText)})'>Copy</button>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
// ── 3D flip card shell ──
|
||
// front = important info (title, desc, category, version, primary install copy)
|
||
// back = full install command, requires, entry_point/direct cmd, source/skill links, contributors
|
||
function flipCardShell(c, hue, frontHTML, backHTML) {
|
||
const titleText = esc(c.display_name || c.name);
|
||
return `
|
||
<div class="card" id="tool-${cssId(c.name)}" style="--ch:${hue}" data-cat="${escAttr(c.category || 'other')}">
|
||
<div class="card-sizer" aria-hidden="true">
|
||
<div class="card-face card-front">${frontHTML}</div>
|
||
<div class="card-face card-back">${backHTML}</div>
|
||
</div>
|
||
<div class="card-inner">
|
||
<div class="card-face card-front">${frontHTML}</div>
|
||
<div class="card-face card-back" aria-hidden="true">${backHTML}</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function flipAffordance() {
|
||
return `<button class="flip-btn flip-toggle" type="button" aria-label="Show details, requirements and links" aria-pressed="false"><span>Details</span>${DETAILS_GLYPH}</button>`;
|
||
}
|
||
function flipBackBtn() {
|
||
return `<button class="flip-btn flip-btn--back flip-toggle" type="button" aria-label="Back to overview" aria-pressed="true">${BACK_GLYPH}<span>Back</span></button>`;
|
||
}
|
||
|
||
// ── Matrix card rendering (FLIP metaphor, lifted from Gallery D, re-themed) ──
|
||
const MATRIX_GLYPH = {
|
||
video: '<path d="m22 8-6 4 6 4V8z"/><rect x="2" y="6" width="14" height="12" rx="2"/>',
|
||
knowledge: '<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>',
|
||
'3d': '<path d="M12 2 2 7l10 5 10-5-10-5z"/><path d="m2 17 10 5 10-5M2 12l10 5 10-5"/>',
|
||
game: '<rect x="2" y="6" width="20" height="12" rx="4"/><path d="M6 12h4M8 10v4"/><circle cx="16" cy="11" r="1"/><circle cx="18.5" cy="13.5" r="1"/>',
|
||
image: '<rect x="3" y="3" width="18" height="18" rx="3"/><circle cx="9" cy="9" r="2"/><path d="m21 15-5-5L5 21"/>'
|
||
};
|
||
function matrixGlyph(cat) { return MATRIX_GLYPH[cat] || MATRIX_GLYPH.knowledge; }
|
||
const PK_LABEL = {
|
||
'agent-native': 'Agent-native', 'public-cli': 'Public CLI', 'harness-cli': 'Harness CLI',
|
||
'agent-skill': 'Agent skill', 'bundled-script': 'Bundled script', 'api': 'API',
|
||
'native': 'Native', 'python': 'Python', 'web-search': 'Web search'
|
||
};
|
||
const PK_KNOWN = { 'agentnative':1,'publiccli':1,'harnesscli':1,'agentskill':1,'bundledscript':1,'api':1,'native':1,'python':1,'websearch':1 };
|
||
function pkClass(k) {
|
||
const c = String(k || '').replace(/-/g, '');
|
||
return PK_KNOWN[c] ? ('pk-' + c) : 'pk-fallback';
|
||
}
|
||
function initials(s) {
|
||
const parts = String(s).replace(/[^A-Za-z0-9 ]/g, ' ').trim().split(/\s+/);
|
||
if (parts.length === 1) return parts[0].slice(0, 2);
|
||
return (parts[0][0] || '') + (parts[1][0] || '');
|
||
}
|
||
function firstSentence(s) {
|
||
if (!s) return '';
|
||
const i = s.indexOf('. ');
|
||
return (i > 0 && i < 90) ? s.slice(0, i + 1) : (s.length > 96 ? s.slice(0, 95) + '…' : s);
|
||
}
|
||
function matrixCmdRow(cmd) {
|
||
return `<div class="mcmd"><code><span class="pfx">$ </span>${esc(cmd)}</code>` +
|
||
`<button class="card-copy-btn" type="button" onclick='copyCmd(this, ${JSON.stringify(cmd)})'>Copy</button></div>`;
|
||
}
|
||
|
||
function renderShelf() {
|
||
const shelf = document.getElementById('shelf');
|
||
if (!shelf) return;
|
||
if (!STATE.matrices.length) { shelf.innerHTML = '<div class="empty">No matrices found in the registry.</div>'; return; }
|
||
shelf.innerHTML = STATE.matrices.map(matrixFlipCard).join('');
|
||
}
|
||
|
||
function matrixFlipCard(m) {
|
||
const hue = catHue(m.category);
|
||
const members = m.clis || [];
|
||
const caps = m.capabilities || [];
|
||
const install = 'cli-hub matrix install ' + m.name;
|
||
const preflight = 'cli-hub matrix preflight ' + m.name;
|
||
const glyph = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" stroke-linecap="round">${matrixGlyph(m.category)}</svg>`;
|
||
|
||
// FRONT — cover: display_name, id chip, description, counts, member software icons, primary install copy
|
||
const memberIcons = members.slice(0, 6).map(n => {
|
||
const t = STATE.byName.get(n);
|
||
return iconFor(t || n, t ? '' : 'miss');
|
||
}).join('');
|
||
const moreN = members.length - 6;
|
||
const front = `
|
||
<div class="mcard-top">
|
||
<span class="mcard-glyph">${glyph}</span>
|
||
</div>
|
||
<h3>${esc(m.display_name || m.name)}</h3>
|
||
<p class="mdesc">${esc(m.description || '')}</p>
|
||
<div class="meta-row"><span><b>${caps.length}</b> capabilities</span><span><b>${members.length}</b> CLIs</span></div>
|
||
<div class="icon-stack">${memberIcons}${moreN > 0 ? `<span class="more">+${moreN}</span>` : ''}</div>
|
||
${matrixCmdRow(install)}
|
||
<div class="mcard-front-foot">
|
||
<span class="flip-hint">Flip: ${caps.length} capabilities, providers & tools</span>
|
||
${flipAffordance()}
|
||
</div>`;
|
||
|
||
// BACK — full capability list, both commands, member chips, scope action (internally scrolling)
|
||
const capsHtml = caps.map(c => {
|
||
const io = []
|
||
.concat((c.inputs || []).map(x => `<span>${esc(x)}</span>`))
|
||
.concat((c.outputs || []).map(x => `<span class="out">${esc(x)} →</span>`))
|
||
.join('');
|
||
const provs = (c.providers || []).map(p => {
|
||
const meta = [];
|
||
if (p.cost_tier) meta.push(esc(p.cost_tier));
|
||
if (p.quality_tier) meta.push(esc(p.quality_tier) + ' quality');
|
||
if (p.offline) meta.push('offline');
|
||
if (p.requires) meta.push('needs ' + esc(p.requires));
|
||
return `<div class="prov">` +
|
||
`<span class="pk ${pkClass(p.kind)}">${esc(PK_LABEL[p.kind] || p.kind || 'other')}</span>` +
|
||
`<div class="pbody"><b>${esc(p.name || '')}</b>` +
|
||
(meta.length ? `<div class="pmeta">${meta.join(' · ')}</div>` : '') +
|
||
(p.notes ? `<div class="pnotes">${esc(p.notes)}</div>` : '') +
|
||
`</div></div>`;
|
||
}).join('');
|
||
return `<details class="cap">` +
|
||
`<summary><span class="cap-id">${esc(c.id || '')}</span><span class="cap-title">${esc(firstSentence(c.intent))}</span>` +
|
||
`<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="m9 6 6 6-6 6"/></svg></summary>` +
|
||
`<div class="cap-detail">` +
|
||
`<p class="cap-intent">${esc(c.intent || '')}</p>` +
|
||
(io ? `<div class="io">${io}</div>` : '') +
|
||
provs +
|
||
`</div></details>`;
|
||
}).join('');
|
||
|
||
const memberChips = members.map(n => {
|
||
const t = STATE.byName.get(n);
|
||
if (!t) return `<button class="miss" type="button" disabled>${esc(n)}<span class="badge-mini">unlisted</span></button>`;
|
||
const bk = t._deck === 'public' ? 'public' : 'harness';
|
||
return `<button type="button" data-goto="${escAttr(t.name)}">${esc(t.display_name || t.name)}<span class="badge-mini">${bk}</span></button>`;
|
||
}).join('');
|
||
|
||
const skillLink = m.skill_md
|
||
? `<a class="mbtn" href="${m.skill_md.startsWith('http') ? esc(m.skill_md) : `${REPO}/blob/main/${esc(m.skill_md)}`}" target="_blank" rel="noopener"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>SKILL.md</a>`
|
||
: '';
|
||
|
||
const back = `
|
||
<div class="card-back-head">
|
||
<span class="card-back-title">${esc(m.display_name || m.name)} · ${esc(m.matrix_id || '')}</span>
|
||
${flipBackBtn()}
|
||
</div>
|
||
<div class="mback-scroll">
|
||
<div class="msub">Install & preflight</div>
|
||
${matrixCmdRow(install)}
|
||
${matrixCmdRow(preflight)}
|
||
<div class="msub">${caps.length} capabilities · intent → providers</div>
|
||
${capsHtml || '<p style="color:var(--ink-3);font-size:.82rem">No capabilities listed.</p>'}
|
||
<div class="msub">${members.length} member CLIs <span style="font-weight:400;text-transform:none;letter-spacing:0;color:var(--ink-3)">— tap to jump into the catalog</span></div>
|
||
<div class="member">${memberChips}</div>
|
||
<div class="mback-cta">
|
||
<button class="mbtn primary" type="button" data-scope="${escAttr(m.name)}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M7 12h10M11 18h2"/></svg>Show only these ${members.length} tools</button>
|
||
${skillLink}
|
||
</div>
|
||
</div>`;
|
||
|
||
return `
|
||
<div class="mcard" style="--ch:${hue}" data-cat="${escAttr(m.category || 'other')}" data-matrix="${escAttr(m.name)}">
|
||
<div class="card-sizer" aria-hidden="true">
|
||
<div class="card-face card-front">${front}</div>
|
||
<div class="card-face card-back">${back}</div>
|
||
</div>
|
||
<div class="card-inner">
|
||
<div class="card-face card-front">${front}</div>
|
||
<div class="card-face card-back" aria-hidden="true">${back}</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
// open a matrix card (flip it + scroll into view) — used by catalog membership chips
|
||
function openMatrix(name) {
|
||
const card = document.querySelector('.mcard[data-matrix="' + cssEsc(name) + '"]');
|
||
if (!card) return;
|
||
document.getElementById('matrices').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
requestAnimationFrame(() => setCardFlipped(card, true));
|
||
}
|
||
function cssEsc(s) { return String(s).replace(/["\\]/g, '\\$&'); }
|
||
function cssId(s) { return String(s).replace(/[^A-Za-z0-9_-]/g, '_'); }
|
||
|
||
// ── Catalog scoping + jump-to-tool (the bidirectional stitching) ──
|
||
function jumpToTool(name) {
|
||
// clear any scope/filter so the tool is guaranteed visible, switch to its deck
|
||
const t = STATE.byName.get(name);
|
||
clearScope(true);
|
||
const deck = (t && t._deck === 'public') ? 'public' : 'harness';
|
||
deckState.harness.filter = 'all'; deckState.public.filter = 'all';
|
||
deckState[deck].query = '';
|
||
const si = document.getElementById('search-' + deck); if (si) si.value = '';
|
||
setFilter(deck, 'all');
|
||
switchDeck(deck);
|
||
// phones render the deck in batches — materialize until the target exists
|
||
let guard = 0;
|
||
while (!document.getElementById('tool-' + cssId(name)) &&
|
||
deckState[deck].fullList && deckState[deck].shownCount < deckState[deck].fullList.length &&
|
||
guard++ < 50) {
|
||
appendDeckBatch(deck);
|
||
}
|
||
requestAnimationFrame(() => {
|
||
const el = document.getElementById('tool-' + cssId(name));
|
||
if (el) {
|
||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
const card = el.querySelector('.card-face') ? el : el;
|
||
el.classList.remove('flash'); void el.offsetWidth; el.classList.add('flash');
|
||
}
|
||
});
|
||
}
|
||
|
||
function applyScope(matName) {
|
||
const m = STATE.matrices.find(x => x.name === matName);
|
||
if (!m) return;
|
||
STATE.scope = matName;
|
||
// reset both decks' filters/search so every scoped member is visible
|
||
deckState.harness.filter = 'all'; deckState.harness.query = '';
|
||
deckState.public.filter = 'all'; deckState.public.query = '';
|
||
const sh = document.getElementById('search-harness'); if (sh) sh.value = '';
|
||
const sp = document.getElementById('search-public'); if (sp) sp.value = '';
|
||
setFilter('harness', 'all'); setFilter('public', 'all');
|
||
updateScopeBanner();
|
||
renderDeck('harness'); renderDeck('public');
|
||
syncFlipA11y();
|
||
// surface whichever deck holds more of the matrix's members
|
||
const set = new Set(m.clis || []);
|
||
const inHarness = harnessClis.filter(c => set.has(c.name)).length;
|
||
const inPublic = publicClis.filter(c => set.has(c.name)).length;
|
||
switchDeck(inPublic > inHarness ? 'public' : 'harness');
|
||
const nav = document.querySelector('.deck-nav');
|
||
if (nav) nav.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
}
|
||
function clearScope(silent) {
|
||
STATE.scope = null;
|
||
updateScopeBanner();
|
||
if (!silent) { renderDeck('harness'); renderDeck('public'); syncFlipA11y(); }
|
||
}
|
||
function updateScopeBanner() {
|
||
const b = document.getElementById('scopeBanner');
|
||
if (!b) return;
|
||
if (STATE.scope) {
|
||
const m = STATE.matrices.find(x => x.name === STATE.scope);
|
||
if (!m) { b.classList.remove('on'); return; }
|
||
b.style.setProperty('--ch', String(catHue(m.category)));
|
||
const members = (m.clis || []).filter(n => STATE.byName.has(n)).length;
|
||
document.getElementById('scopeName').textContent = (m.display_name || m.name) + ' (' + (m.matrix_id || '') + ')';
|
||
document.getElementById('scopeCount').textContent = members;
|
||
b.classList.add('on');
|
||
} else {
|
||
b.classList.remove('on');
|
||
}
|
||
}
|
||
// delegated clicks for member chips / scope / membership chips
|
||
document.addEventListener('click', (e) => {
|
||
const goto = e.target.closest('[data-goto]');
|
||
if (goto && !goto.disabled) { jumpToTool(goto.getAttribute('data-goto')); return; }
|
||
const scopeBtn = e.target.closest('[data-scope]');
|
||
if (scopeBtn) { applyScope(scopeBtn.getAttribute('data-scope')); return; }
|
||
const inM = e.target.closest('[data-openmatrix]');
|
||
if (inM) { openMatrix(inM.getAttribute('data-openmatrix')); return; }
|
||
});
|
||
(function () {
|
||
const sc = document.getElementById('scopeClear');
|
||
if (sc) sc.addEventListener('click', () => clearScope(false));
|
||
})();
|
||
|
||
// membership chip ("In S1 · S4") — opens the matrix flip card; the core unification proof
|
||
function membershipChip(name) {
|
||
const mems = STATE.membership.get(name) || [];
|
||
if (!mems.length) return '';
|
||
const ids = mems.map(m => esc(m.matrix_id || '')).filter(Boolean).join(' · ');
|
||
const title = mems.length > 1
|
||
? ('In ' + mems.length + ' matrices — open ' + (mems[0].display_name || mems[0].name))
|
||
: ('In ' + (mems[0].display_name || mems[0].name) + ' — open it');
|
||
return `<button class="in-matrix" type="button" data-openmatrix="${escAttr(mems[0].name)}" title="${escAttr(title)}">In <span class="mid">${ids}</span></button>`;
|
||
}
|
||
|
||
function renderHarnessCard(c) {
|
||
const hue = catHue(c.category);
|
||
const memChip = membershipChip(c.name);
|
||
const requiresHtml = c.requires
|
||
? `<div class="card-requires"><strong>Requires</strong> ${esc(c.requires)}</div>` : '';
|
||
const skillFront = renderSkillFront(c.skill_md);
|
||
const sourceLink = c.source_url
|
||
? `<a href="${esc(c.source_url)}" target="_blank">Source</a>`
|
||
: `<a href="${REPO}/tree/main/${c.name}/agent-harness" target="_blank">Source</a>`;
|
||
const titleHtml = c.homepage
|
||
? `<a href="${esc(c.homepage)}" target="_blank">${esc(c.display_name)}${EXTERNAL_LINK_SVG}</a>`
|
||
: esc(c.display_name);
|
||
const titleRow = `<span class="card-title"><span class="card-title-row">${iconFor(c, 'title-icon')}<span>${titleHtml}</span></span></span>`;
|
||
const backTitleRow = `<span class="card-back-title">${esc(c.display_name || c.name)}</span>`;
|
||
const contributors = c.contributors || (c.contributor ? [{name: c.contributor, url: c.contributor_url}] : []);
|
||
const isTeam = contributors.length === 1 && contributors[0].name === 'CLI-Anything-Team';
|
||
const contributorHtml = contributors.length
|
||
? `<div class="card-contributor">
|
||
${contributors.map(ct => `<a href="${esc(ct.url)}" target="_blank">${esc(ct.name)}</a>`).join(', ')}${isTeam ? '' : '<span class="community-badge">Community</span>'}
|
||
</div>`
|
||
: '';
|
||
const versionHtml = c.version
|
||
? `<div class="card-back-field"><div class="card-back-label">Version</div><div class="card-requires">${esc(c.version)}</div></div>` : '';
|
||
const updatedHtml = c.last_modified
|
||
? `<div class="card-back-field"><div class="card-back-label">Updated</div><div class="card-requires">${esc(c.last_modified)}</div></div>` : '';
|
||
const runHtml = c.entry_point
|
||
? `<div class="card-back-field"><div class="card-back-label">Run directly</div><div class="card-install card-install--note"><code>${esc(c.entry_point)}</code></div></div>` : '';
|
||
|
||
// Two-step install (clean box: command lines only, NO inline comments).
|
||
const installSteps = [{ cmd: `cli-hub install ${c.name}` }];
|
||
const skillCmd = harnessSkillCmd(c);
|
||
if (skillCmd) installSteps.push({ cmd: skillCmd });
|
||
// FRONT: full two-line install with a short plain-language label above the box.
|
||
const frontInstall = installSteps.length === 2
|
||
? renderInstallStack(installSteps, null, true)
|
||
: renderInstallStack(installSteps, 'Install the CLI');
|
||
|
||
// FRONT (page 1): title · description · install box · contributor(s) · Details
|
||
const front = `
|
||
<div class="card-head">
|
||
${titleRow}
|
||
</div>
|
||
<div class="card-desc">${esc(c.description)}</div>
|
||
${memChip ? `<div class="tool-foot" style="display:flex;margin-top:11px">${memChip}</div>` : ''}
|
||
${frontInstall}
|
||
${(contributorHtml || skillFront) ? `<div style="margin-top:12px;display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap">${contributorHtml || '<span></span>'}${skillFront}</div>` : ''}
|
||
<div class="card-front-foot">
|
||
<span class="flip-hint">Requirements, version & source</span>
|
||
${flipAffordance()}
|
||
</div>`;
|
||
|
||
// BACK (page 2): requirements · version · run directly · source · secondary fields
|
||
const back = `
|
||
<div class="card-back-head">
|
||
${backTitleRow}
|
||
${flipBackBtn()}
|
||
</div>
|
||
<div class="card-back-scroll">
|
||
${requiresHtml ? `<div class="card-back-field">${requiresHtml}</div>` : ''}
|
||
${versionHtml}
|
||
${runHtml}
|
||
${updatedHtml}
|
||
<div class="card-back-field">
|
||
<div class="card-back-label">Links</div>
|
||
<div class="card-links">${sourceLink}</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
return flipCardShell(c, hue, front, back);
|
||
}
|
||
|
||
function formatPublicManager(c) {
|
||
const manager = (c.package_manager || c.install_strategy || 'public').toLowerCase();
|
||
const labelMap = { npm: 'npm', brew: 'brew', bundled: 'bundled' };
|
||
const label = labelMap[manager] || manager;
|
||
return `<span class="public-badge public-badge--${esc(manager)}">${esc(label)}</span>`;
|
||
}
|
||
function publicDirectCmd(c) { return c.direct_cmd || c.entry_point || c.npx_cmd || ''; }
|
||
|
||
function renderNpmCard(c) {
|
||
const hue = catHue(c.category);
|
||
const memChip = membershipChip(c.name);
|
||
const isBundled = (c.install_strategy || c.package_manager) === 'bundled';
|
||
const requiresHtml = c.requires
|
||
? `<div class="card-requires"><strong>Requires</strong> ${esc(c.requires)}</div>` : '';
|
||
const titleHtml = c.homepage
|
||
? `<a href="${esc(c.homepage)}" target="_blank">${esc(c.display_name)}${EXTERNAL_LINK_SVG}</a>`
|
||
: esc(c.display_name);
|
||
const titleRow = `<span class="card-title"><span class="card-title-row">${iconFor(c, 'title-icon')}<span>${titleHtml}</span></span></span>`;
|
||
const backTitleRow = `<span class="card-back-title">${esc(c.display_name || c.name)}</span>`;
|
||
const skillFront = renderSkillFront(c.skill_md);
|
||
const sourceLink = c.source_url ? `<a href="${esc(c.source_url)}" target="_blank">Source</a>` : '';
|
||
const contributors = c.contributors || [];
|
||
const contributorHtml = contributors.length
|
||
? `<div class="card-contributor">
|
||
${contributors.map(ct => `<a href="${esc(ct.url)}" target="_blank">${esc(ct.name)}</a>`).join(', ')}
|
||
</div>`
|
||
: '';
|
||
const directCmd = c.install_cmd || publicDirectCmd(c);
|
||
const directCmdHtml = directCmd && !isBundled
|
||
? `<div class="card-npx card-npx--direct"><strong>Or run directly:</strong><span>${esc(directCmd)}</span></div>` : '';
|
||
const installNotesHtml = c.install_notes
|
||
? `<div class="card-npx card-npx--comment">${esc(c.install_notes)}</div>` : '';
|
||
const publicNotesHtml = (directCmdHtml || installNotesHtml)
|
||
? `<div class="card-public-notes">${directCmdHtml}${installNotesHtml}</div>` : '';
|
||
const updatedHtml = c.last_modified
|
||
? `<div class="card-back-field"><div class="card-back-label">Updated</div><div class="card-requires">${esc(c.last_modified)}</div></div>` : '';
|
||
|
||
// Clean install box — command lines only (no inline comments).
|
||
const installSteps = isBundled ? [] : [{ cmd: `cli-hub install ${c.name}` }];
|
||
const skillCmd = publicSkillCmd(c);
|
||
if (skillCmd) installSteps.push({ cmd: skillCmd });
|
||
|
||
let frontInstall;
|
||
if (isBundled) {
|
||
frontInstall = `<div class="install-steps"><div class="install-steps-label">Bundled with the upstream app — no install needed</div>` +
|
||
`<div class="card-install card-install--note"><code>Bundled with the upstream app</code></div></div>`;
|
||
} else if (installSteps.length === 2) {
|
||
frontInstall = renderInstallStack(installSteps, null, true);
|
||
} else {
|
||
frontInstall = renderInstallStack(installSteps, 'Install the CLI');
|
||
}
|
||
|
||
const linksHtml = [sourceLink].filter(Boolean).join(' · ');
|
||
|
||
// FRONT (page 1): title (+ manager badge) · description · install box · contributor(s) · Details
|
||
const front = `
|
||
<div class="card-head">
|
||
${titleRow}
|
||
<div class="card-meta">
|
||
${formatPublicManager(c)}
|
||
<span class="category-tag">${catLabel(c.category)}</span>
|
||
</div>
|
||
</div>
|
||
<div class="card-desc">${esc(c.description)}</div>
|
||
${memChip ? `<div class="tool-foot" style="display:flex;margin-top:11px">${memChip}</div>` : ''}
|
||
${frontInstall}
|
||
${(contributorHtml || skillFront) ? `<div style="margin-top:12px;display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap">${contributorHtml || '<span></span>'}${skillFront}</div>` : ''}
|
||
<div class="card-front-foot">
|
||
<span class="flip-hint">Requirements, run command & source</span>
|
||
${flipAffordance()}
|
||
</div>`;
|
||
|
||
// BACK (page 2): requirements · run directly / notes · updated · source
|
||
const back = `
|
||
<div class="card-back-head">
|
||
${backTitleRow}
|
||
${flipBackBtn()}
|
||
</div>
|
||
<div class="card-back-scroll">
|
||
${requiresHtml ? `<div class="card-back-field">${requiresHtml}</div>` : ''}
|
||
${publicNotesHtml ? `<div class="card-back-field"><div class="card-back-label">Run & notes</div>${publicNotesHtml}</div>` : ''}
|
||
${updatedHtml}
|
||
<div class="card-back-field">
|
||
<div class="card-back-label">Links</div>
|
||
<div class="card-links">${linksHtml}</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
return flipCardShell(c, hue, front, back);
|
||
}
|
||
|
||
function copyCmd(btn, cmd) {
|
||
navigator.clipboard.writeText(cmd).then(() => {
|
||
btn.textContent = 'Copied!';
|
||
setTimeout(() => btn.textContent = 'Copy', 1500);
|
||
});
|
||
}
|
||
|
||
function animateCount(el, target, duration = 900) {
|
||
if (!el) return;
|
||
const finalValue = Number(target) || 0;
|
||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { el.textContent = finalValue; return; }
|
||
const start = performance.now();
|
||
let shown = null;
|
||
function step(now) {
|
||
const progress = Math.min((now - start) / duration, 1);
|
||
const eased = 1 - Math.pow(1 - progress, 3);
|
||
const value = progress < 1 ? Math.round(finalValue * eased) : finalValue;
|
||
// write only when the displayed integer changes — most frames it doesn't
|
||
if (value !== shown) { shown = value; el.textContent = value; }
|
||
if (progress < 1) window.requestAnimationFrame(step);
|
||
}
|
||
window.requestAnimationFrame(step);
|
||
}
|
||
|
||
function initHeroTypingBar() {
|
||
const shell = document.querySelector('[data-typing-text]');
|
||
if (!shell) return;
|
||
const target = shell.querySelector('[data-typing-target]');
|
||
const fullText = shell.dataset.typingText || '';
|
||
if (!target || !fullText) return;
|
||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { target.textContent = fullText; return; }
|
||
let index = 0, deleting = false;
|
||
function tick() {
|
||
target.textContent = fullText.slice(0, index);
|
||
let delay = deleting ? 44 : 86;
|
||
if (!deleting && index < fullText.length) { index += 1; }
|
||
else if (!deleting && index === fullText.length) { deleting = true; delay = 1650; }
|
||
else if (deleting && index > 0) { index -= 1; }
|
||
else { deleting = false; delay = 480; }
|
||
window.setTimeout(tick, delay);
|
||
}
|
||
window.setTimeout(tick, 420);
|
||
}
|
||
|
||
function esc(s) {
|
||
if (!s) return '';
|
||
const d = document.createElement('div');
|
||
d.textContent = s;
|
||
return d.innerHTML;
|
||
}
|
||
function escAttr(s) {
|
||
if (!s) return '';
|
||
return String(s).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||
}
|
||
|
||
// ── Flip interaction (delegated) ──
|
||
function setCardFlipped(card, flipped) {
|
||
// Cards are flat by default (see the .card CSS notes); grant this card its real 3D
|
||
// context just for the flip transition, then drop back to the cheap flat state.
|
||
card.classList.add('is-3d');
|
||
clearTimeout(card._flip3dTimer);
|
||
card._flip3dTimer = setTimeout(() => card.classList.remove('is-3d'), 780);
|
||
card.classList.toggle('flipped', flipped);
|
||
const inner = card.querySelector('.card-inner');
|
||
if (!inner) return;
|
||
const front = inner.querySelector('.card-front');
|
||
const back = inner.querySelector('.card-back');
|
||
if (front) front.toggleAttribute('aria-hidden', flipped);
|
||
if (back) back.toggleAttribute('aria-hidden', !flipped);
|
||
// toggle tabbability of the hidden face
|
||
const hidden = flipped ? front : back;
|
||
const shown = flipped ? back : front;
|
||
if (hidden) hidden.querySelectorAll('a,button,input,select,summary,[tabindex]').forEach(el => el.setAttribute('tabindex', '-1'));
|
||
if (shown) shown.querySelectorAll('a,button,input,select,summary,[tabindex]').forEach(el => el.removeAttribute('tabindex'));
|
||
// update flip-toggle aria-pressed on shown face
|
||
const tgl = (shown || inner).querySelector('.flip-toggle');
|
||
if (tgl) tgl.setAttribute('aria-pressed', String(flipped));
|
||
}
|
||
// initialise hidden-face tabbability after each render (CLI cards + matrix cards)
|
||
function syncFlipA11y(root) {
|
||
(root || document).querySelectorAll('.card, .mcard').forEach(card => {
|
||
const inner = card.querySelector('.card-inner');
|
||
if (!inner) return;
|
||
const back = inner.querySelector('.card-back');
|
||
if (back && !card.classList.contains('flipped')) {
|
||
back.setAttribute('aria-hidden', 'true');
|
||
back.querySelectorAll('a,button,input,select,summary,[tabindex]').forEach(el => el.setAttribute('tabindex', '-1'));
|
||
}
|
||
});
|
||
}
|
||
|
||
document.addEventListener('click', (e) => {
|
||
const tgl = e.target.closest('.flip-toggle');
|
||
if (tgl) {
|
||
const card = tgl.closest('.card, .mcard');
|
||
if (card) { setCardFlipped(card, !card.classList.contains('flipped')); }
|
||
return;
|
||
}
|
||
});
|
||
// Enter/Space when the flip affordance is focused (native button handles this,
|
||
// but ensure Space doesn't scroll and both keys flip reliably)
|
||
document.addEventListener('keydown', (e) => {
|
||
if ((e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') && document.activeElement && document.activeElement.classList && document.activeElement.classList.contains('flip-toggle')) {
|
||
e.preventDefault();
|
||
const card = document.activeElement.closest('.card, .mcard');
|
||
if (card) setCardFlipped(card, !card.classList.contains('flipped'));
|
||
}
|
||
});
|
||
|
||
// ── Skill popover (public cards) ──
|
||
function toggleSkillPopover(trigger) {
|
||
const popover = trigger.closest('.skill-popover');
|
||
if (!popover) return;
|
||
const shouldOpen = !popover.classList.contains('is-open');
|
||
document.querySelectorAll('.skill-popover.is-open').forEach((node) => {
|
||
node.classList.remove('is-open');
|
||
const btn = node.querySelector('.skill-trigger');
|
||
if (btn) btn.setAttribute('aria-expanded', 'false');
|
||
});
|
||
if (!shouldOpen) return;
|
||
popover.classList.add('is-open');
|
||
trigger.setAttribute('aria-expanded', 'true');
|
||
}
|
||
|
||
document.addEventListener('click', (e) => {
|
||
const copyBtn = e.target.closest('.skill-popover-copy');
|
||
if (copyBtn) { e.preventDefault(); e.stopPropagation(); copyCmd(copyBtn, copyBtn.dataset.copyValue); return; }
|
||
const trigger = e.target.closest('.skill-trigger');
|
||
if (trigger) { e.preventDefault(); e.stopPropagation(); toggleSkillPopover(trigger); return; }
|
||
document.querySelectorAll('.skill-popover.is-open').forEach((node) => {
|
||
node.classList.remove('is-open');
|
||
const btn = node.querySelector('.skill-trigger');
|
||
if (btn) btn.setAttribute('aria-expanded', 'false');
|
||
});
|
||
});
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key !== 'Escape') return;
|
||
document.querySelectorAll('.skill-popover.is-open').forEach((node) => {
|
||
node.classList.remove('is-open');
|
||
const btn = node.querySelector('.skill-trigger');
|
||
if (btn) btn.setAttribute('aria-expanded', 'false');
|
||
});
|
||
});
|
||
|
||
// Sync flip a11y whenever a grid is (re)rendered
|
||
(function () {
|
||
const obsTargets = ['grid-harness', 'grid-public', 'shelf'].map(id => document.getElementById(id)).filter(Boolean);
|
||
if ('MutationObserver' in window) {
|
||
const mo = new MutationObserver(() => syncFlipA11y());
|
||
obsTargets.forEach(t => mo.observe(t, { childList: true }));
|
||
}
|
||
})();
|
||
|
||
// Wire up search and sort for each deck
|
||
document.getElementById('search-harness').addEventListener('input', (e) => {
|
||
deckState.harness.query = e.target.value; renderDeck('harness');
|
||
});
|
||
document.getElementById('sort-harness').addEventListener('change', (e) => {
|
||
deckState.harness.sort = e.target.value; renderDeck('harness');
|
||
});
|
||
document.getElementById('search-public').addEventListener('input', (e) => {
|
||
deckState.public.query = e.target.value; renderDeck('public');
|
||
});
|
||
document.getElementById('sort-public').addEventListener('change', (e) => {
|
||
deckState.public.sort = e.target.value; renderDeck('public');
|
||
});
|
||
|
||
let masonryResizeTimer = null;
|
||
let masonryLastWidth = window.innerWidth;
|
||
window.addEventListener('resize', () => {
|
||
updateDeckPosition();
|
||
// iOS Safari fires resize when its toolbar collapses during scroll — a
|
||
// height-only change. Rebuilding both masonry grids mid-fling is a huge
|
||
// main-thread stall for a layout that cannot have changed (columns depend
|
||
// only on width), so bail unless the width actually moved.
|
||
if (window.innerWidth === masonryLastWidth) return;
|
||
masonryLastWidth = window.innerWidth;
|
||
// debounce the (re)layout: recompute column count + redistribute cards
|
||
clearTimeout(masonryResizeTimer);
|
||
masonryResizeTimer = setTimeout(relayoutAllGrids, 150);
|
||
});
|
||
|
||
initDeckGestures();
|
||
initHeroTypingBar();
|
||
loadRegistries().then(() => { updateDeckPosition(); syncFlipA11y(); });
|
||
|
||
// ── Live analytics via Cloudflare Worker proxy ──
|
||
(function () {
|
||
const ANALYTICS_PROXY_URL = 'https://analytics.clianything.cc/';
|
||
const ZERO_STATS = { web_unique_visitors: 0, cli_hub_total: 0, cli_hub_human: 0, cli_hub_agent: 0 };
|
||
|
||
function applyVisitorStats(stats) {
|
||
const formatK = (val) => {
|
||
const n = Number(val) || 0;
|
||
if (n < 1000) return String(n);
|
||
return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'K';
|
||
};
|
||
const setText = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = formatK(val); };
|
||
setText('stat-web-unique', stats.web_unique_visitors);
|
||
setText('stat-cli-total', stats.cli_hub_total);
|
||
setText('stat-cli-human', stats.cli_hub_human);
|
||
setText('stat-cli-agent', stats.cli_hub_agent);
|
||
}
|
||
|
||
function normalizeStats(stats) {
|
||
const cliHubHuman = Number(stats && stats.cli_hub_human) || 0;
|
||
const cliHubAgent = Number(stats && stats.cli_hub_agent) || 0;
|
||
return {
|
||
web_unique_visitors: Number(stats && (stats.web_unique_visitors ?? stats.total)) || 0,
|
||
cli_hub_total: Number(stats && stats.cli_hub_total) || cliHubHuman + cliHubAgent,
|
||
cli_hub_human: cliHubHuman,
|
||
cli_hub_agent: cliHubAgent,
|
||
};
|
||
}
|
||
async function fetchWorkerStats() {
|
||
const response = await fetch(ANALYTICS_PROXY_URL + 'stats');
|
||
if (!response.ok) throw new Error('analytics stats http ' + response.status);
|
||
const data = await response.json();
|
||
return normalizeStats(data.stats || data);
|
||
}
|
||
function hasAnyStats(s) { return Object.keys(ZERO_STATS).some((key) => (s[key] || 0) > 0); }
|
||
async function refreshVisitorStats() {
|
||
const stats = await fetchWorkerStats();
|
||
if (!hasAnyStats(stats)) return false;
|
||
applyVisitorStats(stats);
|
||
return true;
|
||
}
|
||
async function loadVisitorStats() { await refreshVisitorStats().catch(() => false); }
|
||
|
||
loadVisitorStats();
|
||
if (typeof loadGitHubStars === 'function') loadGitHubStars();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|