sirenamc/stackline
Stackline, a team task board you run from the web or from Claude/ChatGPT. FastAPI + DataK3 + appid + Models + MCP.
A remix template · 0 remixes · Remix sirenamc/stackline
app/boards/doors/landing.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Stackline</title>
<meta name="description" content="A team task board you run from the web page or straight from Claude and ChatGPT.">
<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=Fraunces:opsz,wght,SOFT@9..144,500..700,50..100&family=Hanken+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/paper.css">
<style>
.masthead nav { display: flex; gap: 24px; align-items: center; font: 500 15px var(--sans); }
.masthead nav a { color: var(--ink-faded); text-decoration: none; }
.masthead nav a:hover { color: var(--ink); }
.masthead nav a.press, .masthead nav a.press:hover { color: var(--paper); }
.hero { display: grid; grid-template-columns: 1.05fr 1fr; gap: 40px; align-items: center; padding: 64px 0 44px; }
.hero h2 { margin: 0 0 24px; font: 600 clamp(46px, 6vw, 84px)/.98 var(--serif); font-variation-settings: "SOFT" 100, "opsz" 144; letter-spacing: -2px; }
.hero .deck { font-size: 19.5px; line-height: 1.55; color: var(--ink-faded); margin: 0 0 30px; max-width: 32rem; }
.cta { display: flex; gap: 18px; flex-wrap: wrap; align-items: center; }
.cta .under { font-weight: 600; text-decoration: underline; text-underline-offset: 6px; text-decoration-thickness: 2px; text-decoration-color: var(--blush); }
/* a fan of index cards, the board Claude just made */
.fan { position: relative; height: 470px; }
.ic { position: absolute; width: 300px; min-height: 176px; border-radius: 6px 10px 12px 12px; padding: 20px 20px 16px; color: #23211c; box-shadow: 0 22px 34px -22px rgba(60,40,10,.55); }
.ic .tab { position: absolute; top: -27px; height: 28px; padding: 5px 14px; border-radius: 9px 9px 0 0; background: inherit; font: 600 13px var(--sans); }
.ic h3 { margin: 6px 0 10px; font: 600 23px/1.15 var(--serif); font-variation-settings: "SOFT" 100; }
.ic .m { font-size: 13.5px; display: flex; gap: 6px; flex-wrap: wrap; }
.ic .m span { background: rgba(255,255,255,.55); border-radius: 99px; padding: 1px 10px; }
.ic.a { background: var(--sage); left: 0; top: 270px; transform: rotate(-6deg); z-index: 3; } .ic.a .tab { left: 14px; }
.ic.b { background: var(--butter); left: 120px; top: 150px; transform: rotate(-1deg); z-index: 2; } .ic.b .tab { left: 110px; }
.ic.c { background: var(--blush); left: 240px; top: 30px; transform: rotate(5deg); z-index: 1; } .ic.c .tab { right: 16px; }
.bubble { position: absolute; right: -6px; top: 330px; z-index: 4; width: 300px; background: var(--slip); border-radius: 18px 18px 4px 18px; padding: 14px 16px; box-shadow: 0 16px 34px -16px rgba(0,0,0,.4); font-size: 15px; }
.bubble small { display: block; color: var(--ink-faded); margin-top: 6px; font-size: 13px; }
.bubble code { font-size: 12.5px; }
.strip { display: grid; grid-template-columns: repeat(4, 1fr); gap: 24px; padding-top: 20px; border-top: 1.5px solid var(--ink); }
.strip h3 { margin: 0 0 5px; font: 600 19px/1.2 var(--serif); font-variation-settings: "SOFT" 100; }
.strip p { margin: 0; color: var(--ink-faded); font-size: 15px; }
.dot { display: inline-block; width: 12px; height: 12px; border-radius: 3px; margin-right: 9px; }
.try { display: grid; grid-template-columns: 1.1fr 1fr; gap: 30px; margin-top: 56px; }
.panelcard { background: var(--slip); border-radius: 6px 16px 16px 16px; padding: 24px 26px; box-shadow: 0 14px 30px -24px rgba(60,40,10,.6); position: relative; margin-top: 26px; }
.panelcard .tab { position: absolute; left: 0; top: -27px; height: 28px; padding: 5px 16px; border-radius: 10px 10px 0 0; font: 600 13px var(--sans); color: #23211c; }
.panelcard h3 { margin: 0 0 10px; font: 600 27px/1.1 var(--serif); font-variation-settings: "SOFT" 100; }
.logins { border-collapse: collapse; width: 100%; font-size: 15px; margin: 12px 0; }
.logins td { padding: 8px 6px; border-bottom: 1px solid var(--rule); vertical-align: top; }
.logins td:first-child { font-weight: 600; white-space: nowrap; }
.checks { margin: 0; padding-left: 20px; } .checks li { margin: 7px 0; }
.said { list-style: none; margin: 0; padding: 0; }
.said li { padding: 9px 0; border-bottom: 1px solid var(--rule); font-size: 15px; }
.said .q { font: 600 16px var(--serif); font-variation-settings: "SOFT" 100; }
.said .a { display: block; color: var(--ink-faded); font-size: 14px; }
.connect { margin-top: 16px; padding: 12px 14px; background: var(--paper); border-radius: 12px; font-size: 14.5px; }
.connect p { margin: 4px 0; }
@media (max-width: 900px) {
.hero, .try { grid-template-columns: 1fr; gap: 30px; padding-top: 28px; }
.strip { grid-template-columns: 1fr 1fr; }
.fan { height: 430px; transform: scale(.82); transform-origin: left top; width: 122%; }
.masthead nav .hide-sm { display: none; }
}
@media (max-width: 520px) { .strip { grid-template-columns: 1fr; } .fan { transform: scale(.58); height: 300px; width: 172%; } }
.hero { overflow-x: clip; }
</style>
</head>
<body>
<div class="sheet"><div class="paper">
<header class="masthead">
<a class="brand" href="/" aria-label="Stackline">
<svg width="40" height="40" viewBox="0 0 40 40" aria-hidden="true"><path d="M9 6h8l2 3h14a2 2 0 0 1 2 2v12H7V8a2 2 0 0 1 2-2z" fill="var(--blush)"/><path d="M6 13h12l2 3h13a2 2 0 0 1 2 2v12H4V15a2 2 0 0 1 2-2z" fill="var(--butter)"/><path d="M4 20h7l2 3h21a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V22a2 2 0 0 1 2-2z" fill="var(--sage)"/><path d="M8 29h14" stroke="var(--ink)" stroke-width="2.2" stroke-linecap="round"/></svg>
<h1 class="nameplate">Stackline</h1>
</a>
<span class="grow"></span>
<nav aria-label="Page">
<a class="hide-sm" href="#try">Try it</a>
<a class="hide-sm" href="#claude">Claude and ChatGPT</a>
<a class="hide-sm" href="https://git.dodil.io/sirenamc/stackline">Code</a>
<button type="button" class="switch" id="themeToggle" aria-pressed="false">Dark</button>
<a class="press" href="/.dodil/auth/login?return_to=/">Sign in</a>
</nav>
</header>
<main>
<section class="hero" aria-labelledby="h-hero">
<div>
<h2 id="h-hero">Tell Claude the goal. Get the board.</h2>
<p class="deck">A task board for small teams. One sentence gives you the cards, the dates and what waits on what. Then run it from the page or from the chat: "what's late?", "move card 12 to Done", "what is blocking the launch?"</p>
<div class="cta">
<a class="press" href="#try">Try the demo</a>
<a class="under" href="/.dodil/auth/login?return_to=/">Sign in or create an account</a>
</div>
</div>
<div class="fan" role="img" aria-label="Three index cards from a board Claude set up, with a question to Claude beside them">
<div class="ic a"><span class="tab">#12 Doing</span><h3>Write the home page copy</h3><div class="m"><span>2 days late</span><span>Ola</span></div></div>
<div class="ic b"><span class="tab">#13 To do</span><h3>Design the home page</h3><div class="m"><span>waits on #12</span><span>due Fri</span></div></div>
<div class="ic c"><span class="tab">#17 Launch</span><h3>Launch the site</h3><div class="m"><span>waits on #13, #15, #16</span><span>20 Nov</span></div></div>
<div class="bubble">"What is blocking the launch?"<small>Claude used <code>what_is_blocking</code>: #12 is two days late and holds up #13.</small></div>
</div>
</section>
<section class="strip" aria-label="What it does">
<div><h3><span class="dot" style="background:var(--sage)"></span>Owner, member, viewer</h3><p>The same rules on the page and in Claude. A viewer gets the same refusal in both.</p></div>
<div><h3><span class="dot" style="background:var(--butter)"></span>Every change on record</h3><p>Who moved which card, when, and what it said before.</p></div>
<div><h3><span class="dot" style="background:var(--blush)"></span>Late is visible</h3><p>Overdue cards are flagged on their own, blocker chains followed through.</p></div>
<div><h3><span class="dot" style="background:var(--sky)"></span>Yours in one remix</h3><p>A copy in your Dodil organization, with sign-in, in minutes.</p></div>
</section>
<section class="try" aria-label="Try it">
<div class="panelcard" id="try">
<span class="tab" style="background:var(--butter)">Try it in a minute</span>
<!--DEMO-->
</div>
<div class="panelcard" id="claude">
<span class="tab" style="background:var(--sky)">Claude and ChatGPT</span>
<h3>Run it from the chat</h3>
<ul class="said">
<li><span class="q">/plan_my_week</span><span class="a">Your late cards first, then this week's, and who to talk to about blockers.</span></li>
<li><span class="q">/standup Launch</span><span class="a">What moved, what is late and who owns it, what is blocked.</span></li>
<li><span class="q">Add "Send invoice" to Doing on Ops</span><span class="a">Card created, with its number, in the log.</span></li>
<li><span class="q">What is blocking the launch?</span><span class="a">The unfinished cards in its way, following the chain.</span></li>
</ul>
<div class="connect">
<p>Add a custom connector with this URL: <code id="mcpUrl">/mcp</code></p>
<p class="muted">In Claude, set the OAuth client ID to <code>stackline-claude</code> and there is no approval step.</p>
</div>
</div>
</section>
</main>
<footer class="footer">
<p>Open source under the MIT licence. Want your own copy? <code>dodil git remix sirenamc/stackline --to-org <your-org></code>, then follow the one step in the README.</p>
<p>Sign-in, data and AI run on Dodil: appid, DataK3, Ignite and Dodil Models.</p>
</footer>
</div></div>
<script>
document.getElementById("mcpUrl").textContent = location.origin + "/mcp";
const root = document.documentElement, btn = document.getElementById("themeToggle");
function setTheme(night) {
root.dataset.theme = night ? "night" : "day";
btn.textContent = night ? "Light" : "Dark";
btn.setAttribute("aria-pressed", String(night));
try { localStorage.setItem("stackline-theme", root.dataset.theme); } catch (e) {}
}
let saved = null; try { saved = localStorage.getItem("stackline-theme"); } catch (e) {}
setTheme(saved ? saved === "night" : matchMedia("(prefers-color-scheme: dark)").matches);
btn.onclick = () => setTheme(root.dataset.theme !== "night");
</script>
</body>
</html>