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 &lt;your-org&gt;</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>