Files
thandClaude Opus 4.7 48b5907f4c v2.0.2 — English tag aliases, English-only website, undo-body flaky fix
- tags: accept #birthday and #appointment as English synonyms for
  #geburtstag and #termin. Normalised to the German canonical form at
  extractTags() write-time so every downstream check (parseTags /
  calendar logic / board filters) keeps working without per-call-site
  aliasing. Editor body-regex extended so the "new appointment"
  heuristic and auto-sync trigger on either variant.
- website: replace #termin/#geburtstag with #appointment/#birthday in
  tag chips, calendar feature list, and calendar legend.
- tests: fix flaky "Undo: Editor Save → Ctrl+Z reverts body change"
  that raced doSave(). The test polled the server for the patched body
  and fired Ctrl+Z as soon as it landed, but the undo shortcut is
  guarded by !guards.isOpen('modal'), and closeModal() only runs after
  the post-patch _reloadAll — on fast servers the key was swallowed.
  Poll now also waits for the modal `.open` class to drop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 10:04:08 +02:00

815 lines
42 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>hyprcrd — stop renting your memory</title>
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<meta name="description" content="Self-hosted card manager. Markdown, tags, wiki-links, calendar, encrypted relay. Find everything.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Silkscreen&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/style.css">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #06080f;
--s1: #0d1117;
--s2: #161b22;
--bdr: #21262d;
--tx: #8b949e;
--hi: #e6edf3;
--ac: #38bdf8;
--ac2: #a78bfa;
--rd: #fb7185;
--gn: #34d399;
--yl: #fbbf24;
--pk: #f472b6;
--pixel: 'Silkscreen', monospace;
--mono: 'JetBrains Mono', monospace;
}
html { scroll-behavior: smooth; }
body { font-family: var(--mono); background: var(--bg); color: var(--tx); }
::selection { background: rgba(56,189,248,0.25); }
/* ── Top bar ── */
header {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
background: rgba(6,8,15,0.88); backdrop-filter: blur(16px);
border-bottom: 1px solid var(--bdr); padding: 0 48px;
}
header .inner {
display: flex; align-items: center; height: 48px; gap: 20px;
}
header .brand { font-family: var(--pixel); font-size: 12px; color: var(--ac); letter-spacing: 2px; }
header nav { margin-left: auto; display: flex; gap: 20px; align-items: center; }
header nav a { font-size: 11px; color: var(--tx); font-family: var(--pixel); text-decoration: none; }
header nav a:hover { color: var(--hi); }
.gh-btn {
font-family: var(--pixel); font-size: 10px; color: var(--bg);
background: var(--ac); padding: 5px 16px; border-radius: 4px;
}
.gh-btn:hover { background: #7dd3fc; }
/* ── Sections ── */
.section {
min-height: 100vh; padding: 80px 48px;
display: flex; flex-direction: column; justify-content: center;
border-bottom: 1px solid var(--bdr);
position: relative; overflow: hidden;
}
/* ── Section label — big watermark word ── */
.section-label {
font-family: var(--pixel); font-size: clamp(60px, 12vw, 140px);
color: var(--bdr); letter-spacing: 4px; text-transform: uppercase;
line-height: 1; margin-bottom: 48px; user-select: none;
opacity: 0.7;
}
.section-label .accent { color: rgba(56,189,248,0.15); }
/* ── Hero ── */
.hero { padding-top: 100px; }
.hero-brand {
display: flex; align-items: center; gap: clamp(16px, 3vw, 32px);
margin-bottom: 48px;
}
.hero-brand i {
font-size: clamp(60px, 12vw, 140px); color: var(--ac);
line-height: 1; display: block;
}
.hero-brand span {
font-family: var(--pixel); font-size: clamp(60px, 12vw, 140px);
color: var(--ac); letter-spacing: 4px; line-height: 1;
}
.hero-brand .typed { display: inline; }
.hero-brand .cursor {
display: inline-block; width: 0.55em; height: 1em;
background: var(--ac); vertical-align: text-bottom;
margin-left: 2px;
animation: cursor-blink 530ms steps(1) infinite;
}
.cursor-done { animation: cursor-blink 530ms steps(1) 3 forwards !important; }
@keyframes cursor-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
.hero-title {
font-size: clamp(48px, 8vw, 100px); font-weight: 700;
color: var(--hi); line-height: 1.05; margin-bottom: 32px; letter-spacing: -3px;
}
.hero-title .accent { color: var(--ac); }
.hero-title .dim { color: var(--bdr); }
.hero-sub { font-size: 16px; line-height: 1.8; max-width: 560px; margin-bottom: 48px; }
.hero-terminal {
background: var(--s1); border: 1px solid var(--bdr); border-radius: 8px;
padding: 20px 28px; font-size: 13px; line-height: 2.2; display: inline-block;
}
.hero-terminal .p { color: var(--gn); }
.hero-terminal .c { color: var(--hi); }
.hero-terminal .d { opacity: 0.3; }
/* ── Feature layout ── */
.feat {
display: grid; grid-template-columns: 1fr 1fr; gap: 80px;
width: 100%; align-items: center;
}
.feat.reverse .feat-text { order: 2; }
.feat.reverse .feat-visual { order: 1; }
.feat-text h3 {
font-size: 28px; color: var(--hi); font-weight: 700;
line-height: 1.3; margin-bottom: 20px; letter-spacing: -0.5px;
}
.feat-text p { font-size: 14px; line-height: 1.9; margin-bottom: 16px; }
.feat-text ul { list-style: none; margin-top: 16px; }
.feat-text li {
font-size: 13px; padding: 6px 0; display: flex; align-items: flex-start; gap: 10px; line-height: 1.6;
}
.feat-text li::before {
content: ''; width: 6px; height: 6px; border-radius: 50%;
background: var(--ac); flex-shrink: 0; margin-top: 8px;
}
.feat-visual {
background: var(--s1); border: 1px solid var(--bdr); border-radius: 10px; overflow: hidden;
}
/* ── Visual panels ── */
.vis-code {
padding: 28px 32px; font-size: 13px; line-height: 2; white-space: pre-wrap;
}
.vis-code .kw { color: var(--ac); }
.vis-code .str { color: var(--gn); }
.vis-code .tag { color: var(--yl); }
.vis-code .link { color: var(--ac2); }
.vis-code .cm { opacity: 0.3; }
.vis-code .hi { color: var(--hi); }
.vis-tags { padding: 32px; display: flex; flex-wrap: wrap; gap: 10px; align-content: flex-start; }
.vtag {
font-family: var(--pixel); font-size: 11px; padding: 8px 18px;
border-radius: 6px; border: 1px solid;
}
.vis-keys { padding: 32px; display: flex; flex-wrap: wrap; gap: 10px; }
.vkey {
font-family: var(--pixel); font-size: 12px; padding: 10px 16px;
background: var(--bg); border: 1px solid var(--bdr); border-radius: 6px;
color: var(--hi); min-width: 40px; text-align: center;
box-shadow: 0 3px 0 var(--bdr);
}
.vis-cards { padding: 24px; display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.vcard {
background: var(--bg); border: 1px solid var(--bdr); border-radius: 6px;
padding: 16px; border-top: 3px solid;
}
.vcard-t { font-family: var(--pixel); font-size: 10px; color: var(--hi); margin-bottom: 8px; }
.vis-gitea { padding: 28px 32px; }
.vis-gitea .ghdr {
font-family: var(--pixel); font-size: 10px; color: var(--tx);
margin-bottom: 16px; display: flex; align-items: center; gap: 8px;
}
.vis-gitea .issue-row {
display: flex; align-items: center; gap: 12px;
padding: 12px 0; border-bottom: 1px solid var(--bdr); font-size: 12px;
}
.vis-gitea .issue-row:last-child { border-bottom: none; }
.vis-gitea .issue-state { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.vis-gitea .issue-title { color: var(--hi); flex: 1; }
.vis-gitea .issue-labels { display: flex; gap: 4px; }
.vis-gitea .issue-label {
font-family: var(--pixel); font-size: 8px; padding: 2px 8px; border-radius: 3px;
}
.vis-gitea .issue-num { font-family: var(--pixel); font-size: 10px; color: var(--tx); }
.vis-relay { padding: 28px 32px; font-size: 12px; line-height: 2.2; }
.vis-relay .node {
display: inline-block; padding: 4px 14px; border-radius: 4px;
font-family: var(--pixel); font-size: 10px; border: 1px solid; margin: 2px;
}
.vis-relay .arrow { color: var(--bdr); margin: 0 6px; }
/* ── Screenshot ── */
.hero-screenshot {
margin-top: 56px; border-radius: 10px; overflow: hidden;
border: 1px solid var(--bdr);
box-shadow: 0 24px 80px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.03);
}
.hero-screenshot img { display: block; width: 100%; height: auto; }
/* ── Email visual ── */
.vis-inbox { padding: 24px 32px; }
.vis-inbox .inbox-row {
display: flex; align-items: center; gap: 12px;
padding: 10px 0; border-bottom: 1px solid var(--bdr); font-size: 12px;
}
.vis-inbox .inbox-row:last-child { border-bottom: none; }
.vis-inbox .inbox-from { width: 120px; color: var(--hi); flex-shrink: 0; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vis-inbox .inbox-subject { flex: 1; color: var(--tx); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vis-inbox .inbox-badge {
font-family: var(--pixel); font-size: 8px; padding: 2px 8px;
border-radius: 3px; flex-shrink: 0;
}
.vis-inbox .inbox-row.spam { opacity: 0.4; }
.vis-inbox .inbox-row.spam .inbox-from { text-decoration: line-through; }
/* ── CTA ── */
.cta-section {
min-height: 60vh; display: flex; align-items: center; justify-content: center;
text-align: center; padding: 80px 48px;
}
.cta-section h2 {
font-family: var(--pixel); font-size: clamp(24px, 4vw, 48px);
color: var(--hi); margin-bottom: 16px; letter-spacing: -1px;
}
.cta-section p { font-size: 14px; margin-bottom: 40px; max-width: 440px; margin-left: auto; margin-right: auto; }
.cta-links { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; }
.cta-btn {
font-family: var(--pixel); font-size: 11px; padding: 12px 28px;
border-radius: 6px; transition: all 0.2s; text-decoration: none;
}
.cta-btn.primary { background: var(--ac); color: var(--bg); }
.cta-btn.primary:hover { background: #7dd3fc; }
.cta-btn.outline { border: 1px solid var(--bdr); color: var(--tx); }
.cta-btn.outline:hover { border-color: var(--ac); color: var(--ac); }
footer {
padding: 24px 48px; border-top: 1px solid var(--bdr);
font-family: var(--pixel); font-size: 9px; color: var(--tx); opacity: 0.3;
text-align: center;
}
@media (max-width: 900px) {
.section { padding: 60px 24px; }
.feat { grid-template-columns: 1fr; gap: 40px; }
.feat.reverse .feat-text, .feat.reverse .feat-visual { order: unset; }
.section-label { font-size: clamp(36px, 10vw, 80px); }
header { padding: 0 20px; }
header nav a:not(.gh-btn) { display: none; }
footer { padding: 24px; }
.cta-section { padding: 60px 24px; }
}
</style>
</head>
<body>
<header>
<div class="inner">
<span class="brand">hyprcrd</span>
<nav>
<a href="#search">search</a>
<a href="#tags">tags</a>
<a href="#editor">editor</a>
<a href="#gitea">gitea</a>
<a href="#relay">relay</a>
<a href="#email">email</a>
<a href="#deploy">deploy</a>
<a href="https://git.uesome.de/tanztee/hyprcrd" class="gh-btn">source</a>
</nav>
</div>
</header>
<main>
<section class="section hero">
<div class="hero-brand">
<i class="ph ph-terminal-window"></i>
<span><span class="typed" id="hero-typed"></span><span class="cursor" id="hero-cursor"></span></span>
</div>
<h1 class="hero-title">
stop losing<br>
<span class="accent">information</span><span class="dim">.</span>
</h1>
<p class="hero-sub">
Cards. Tags. Wiki-links. Markdown. Calendar. Encrypted sync between devices.
One SQLite file. Zero cloud. Built for finding things, not filing them.
</p>
<div class="hero-terminal">
<span class="p">$</span> <span class="c">git clone https://git.uesome.de/tanztee/hyprcrd</span><br>
<span class="p">$</span> <span class="c">cd hyprcrd && docker compose up -d</span><br>
<span class="d"># open localhost:8088 — set password — done</span>
</div>
<div class="hero-screenshot">
<img src="screenshot.png" alt="hyprcrd board view with cards, tags, and buckets">
</div>
</section>
<section class="section" id="search">
<div class="section-label">SEARCH</div>
<div class="feat">
<div class="feat-text">
<h3>Type to find. Instantly.</h3>
<p>Full-text search hits titles, card bodies, and tags at the same time. Combine <code>#tag</code> filters with free text — <code>#project deploy script</code> finds exactly what you think it does.</p>
<ul>
<li>Search + replace across every card in the database</li>
<li><code>#tag1 #tag2</code> for AND filtering</li>
<li>Scope to a specific bucket</li>
<li>Drag search results onto each other to group cards</li>
<li>Preview panel shows rendered markdown</li>
</ul>
</div>
<div class="feat-visual">
<div class="vis-code"><span class="cm">&gt;</span> <span class="hi">#project deploy</span>
<span class="cm"> 3 results in 2ms</span>
<span class="kw">API redesign</span>
<span class="cm">...the <mark style="background:rgba(56,189,248,0.15);color:var(--ac)">deploy</mark> script handles
staging and prod environments...</span>
<span class="kw">Infra checklist</span>
<span class="cm">...<mark style="background:rgba(56,189,248,0.15);color:var(--ac)">deploy</mark> to k8s cluster
after running integration tests...</span>
<span class="kw">Sprint retro</span>
<span class="cm">...discussed <mark style="background:rgba(56,189,248,0.15);color:var(--ac)">deploy</mark>
frequency and rollback strategy...</span></div>
</div>
</div>
</section>
<section class="section" id="tags">
<div class="section-label">TAGS</div>
<div class="feat reverse">
<div class="feat-text">
<h3>Write a hashtag.<br>It becomes structure.</h3>
<p>No separate tag dialog. No filing step. Write <code>#project</code> in the card body and it extracts automatically into a clickable, filterable tag.</p>
<ul>
<li>Hierarchical: <code>#project/frontend/auth</code> — each segment is independently clickable</li>
<li>Click to filter, Ctrl+click to combine (AND logic)</li>
<li>Tag quick-pick with <kbd>T</kbd> — searchable overlay of all existing tags</li>
<li>Assign via right-click context menu</li>
<li>Super-buckets: nest buckets into collapsible groups</li>
</ul>
</div>
<div class="feat-visual">
<div class="vis-tags">
<span class="vtag" style="background:rgba(56,189,248,0.06);color:var(--ac);border-color:rgba(56,189,248,0.2);">#project</span>
<span class="vtag" style="background:rgba(167,139,250,0.06);color:var(--ac2);border-color:rgba(167,139,250,0.2);">#project/api</span>
<span class="vtag" style="background:rgba(167,139,250,0.06);color:var(--ac2);border-color:rgba(167,139,250,0.2);">#project/frontend</span>
<span class="vtag" style="background:rgba(167,139,250,0.06);color:var(--ac2);border-color:rgba(167,139,250,0.2);">#project/frontend/auth</span>
<span class="vtag" style="background:rgba(251,113,133,0.06);color:var(--rd);border-color:rgba(251,113,133,0.2);">#bug</span>
<span class="vtag" style="background:rgba(251,113,133,0.06);color:var(--rd);border-color:rgba(251,113,133,0.2);">#urgent</span>
<span class="vtag" style="background:rgba(52,211,153,0.06);color:var(--gn);border-color:rgba(52,211,153,0.2);">#ops</span>
<span class="vtag" style="background:rgba(52,211,153,0.06);color:var(--gn);border-color:rgba(52,211,153,0.2);">#done</span>
<span class="vtag" style="background:rgba(251,191,36,0.06);color:var(--yl);border-color:rgba(251,191,36,0.2);">#meeting</span>
<span class="vtag" style="background:rgba(251,191,36,0.06);color:var(--yl);border-color:rgba(251,191,36,0.2);">#appointment</span>
<span class="vtag" style="background:rgba(56,189,248,0.06);color:var(--ac);border-color:rgba(56,189,248,0.2);">#research</span>
<span class="vtag" style="background:rgba(244,114,182,0.06);color:var(--pk);border-color:rgba(244,114,182,0.2);">#idea</span>
<span class="vtag" style="background:rgba(244,114,182,0.06);color:var(--pk);border-color:rgba(244,114,182,0.2);">#birthday</span>
<span class="vtag" style="background:rgba(56,189,248,0.06);color:var(--ac);border-color:rgba(56,189,248,0.2);">#personal</span>
</div>
</div>
</div>
</section>
<section class="section" id="editor">
<div class="section-label">EDITOR</div>
<div class="feat">
<div class="feat-text">
<h3>Markdown that does more.</h3>
<p>Every card is a markdown document with live preview. Enriched with tools that make it a knowledge system, not just a text file.</p>
<ul>
<li>Wiki-links: <code>[[Card Title]]</code> — auto-complete, backlinks, split view</li>
<li>Snippets: <code>?dat</code> <code>?table</code> <code>?code</code> <code>?todo</code></li>
<li>Inline calculator: <code>?3+5</code> space &rarr; 8</li>
<li>Line operations: Alt+Up/Down to move, Shift+Alt to duplicate</li>
<li>Upload images, audio, PDFs — paste or drag</li>
<li>Asset picker: thumbnail grid with search</li>
<li>Extract selection to new card with auto-link</li>
<li>Find &amp; replace within the card (Ctrl+H)</li>
</ul>
</div>
<div class="feat-visual">
<div class="vis-code"><span class="hi">## Sprint planning Q3</span>
<span class="cm">Goals for next quarter:</span>
- [x] Finish API redesign
- [x] Migrate auth to <span class="link">[[Architecture RFC]]</span>
- [ ] Load testing
- [ ] Deploy to prod
<span class="kw">**Deadline:**</span> 2026-07-01
<span class="kw">**Owner:**</span> @alice
See also: <span class="link">[[Infra checklist]]</span>
<span class="tag">#project/api</span> <span class="tag">#meeting</span></div>
</div>
</div>
</section>
<section class="section" id="calendar">
<div class="section-label">CALENDAR</div>
<div class="feat">
<div class="feat-text">
<h3>Cards become events.<br>Events stay synced.</h3>
<p>A month-grid calendar built into hyprcrd. Journal entries, appointments, and birthdays — all just cards with the right title or tag. Bidirectional CalDAV sync with ownCloud and Nextcloud.</p>
<ul>
<li>Journal entries: title = <code>YYYY-MM-DD</code>, one per day, no tag needed</li>
<li>Appointments: <code>#appointment</code> tag + structured template (time, end, location, attendees)</li>
<li>Birthdays: <code>#birthday</code> tag, recurring annually</li>
<li>Click any day to create a journal entry, appointment, or birthday</li>
<li>CalDAV sync: bidirectional push/pull with ownCloud, Nextcloud, or any CalDAV server</li>
<li>Per-calendar color + icon configuration</li>
<li>Upcoming-event notifications with configurable lookahead</li>
</ul>
</div>
<div class="feat-visual">
<div style="font-family:var(--mono);font-size:11px;color:var(--tx);line-height:2;">
<div style="text-align:center;margin-bottom:12px;"><span style="color:var(--ac);font-family:var(--pixel);font-size:10px;">APRIL 2026</span></div>
<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:4px;text-align:center;">
<span style="color:var(--hi);font-family:var(--pixel);font-size:9px;">Mo</span>
<span style="color:var(--hi);font-family:var(--pixel);font-size:9px;">Tu</span>
<span style="color:var(--hi);font-family:var(--pixel);font-size:9px;">We</span>
<span style="color:var(--hi);font-family:var(--pixel);font-size:9px;">Th</span>
<span style="color:var(--hi);font-family:var(--pixel);font-size:9px;">Fr</span>
<span style="color:var(--hi);font-family:var(--pixel);font-size:9px;">Sa</span>
<span style="color:var(--hi);font-family:var(--pixel);font-size:9px;">Su</span>
<span style="opacity:0.3">30</span><span style="opacity:0.3">31</span><span>1</span><span>2</span><span>3</span><span>4</span><span>5</span>
<span>6</span><span>7</span><span>8</span><span>9</span><span>10</span><span>11</span><span>12</span>
<span>13</span><span>14</span><span style="color:var(--ac);border:1px solid var(--ac);border-radius:3px;">15</span><span>16</span><span style="color:var(--yl);">17</span><span>18</span><span>19</span>
<span>20</span><span>21</span><span>22</span><span style="color:var(--pk);">23</span><span>24</span><span>25</span><span>26</span>
<span>27</span><span>28</span><span>29</span><span>30</span><span style="opacity:0.3">1</span><span style="opacity:0.3">2</span><span style="opacity:0.3">3</span>
</div>
<div style="margin-top:12px;font-size:10px;display:flex;gap:12px;justify-content:center;">
<span><span style="color:var(--ac);">&bull;</span> journal</span>
<span><span style="color:var(--yl);">&bull;</span> appointment</span>
<span><span style="color:var(--pk);">&bull;</span> birthday</span>
</div>
</div>
</div>
</div>
</section>
<section class="section" id="gitea">
<div class="section-label"><span class="accent">GITEA</span></div>
<div class="feat reverse">
<div class="feat-text">
<h3>Cards become tickets.<br>Tickets stay linked.</h3>
<p>Connect any number of Gitea instances. Browse issues, create tickets directly from cards, and keep your project management in sync without leaving hyprcrd.</p>
<ul>
<li>Multi-account: connect unlimited Gitea servers with token auth</li>
<li>Default repo per account — pre-fills create form, stays editable</li>
<li>Right-click any card &rarr; <em>Make ticket</em>: title + body pre-fill the issue form</li>
<li>Full issue browser: filter by state, scope (assigned/created/mentioned)</li>
<li>Label picker, milestone selector, assignee multi-select</li>
<li>Repo autocomplete with fuzzy search across all repos</li>
<li>Inline issue detail: view body, labels, assignees, edit title, toggle state</li>
<li>Pagination for large repos</li>
</ul>
</div>
<div class="feat-visual">
<div class="vis-gitea">
<div class="ghdr">
<span style="color:var(--ac);">git.example.com</span>
<span style="opacity:0.3">/</span>
<span style="color:var(--hi);">team/hyprcrd</span>
<span style="margin-left:auto;padding:2px 10px;border-radius:3px;background:rgba(52,211,153,0.08);color:var(--gn);border:1px solid rgba(52,211,153,0.15);font-family:var(--pixel);">open</span>
</div>
<div class="issue-row">
<span class="issue-state" style="background:var(--gn);"></span>
<span class="issue-title">API redesign: migrate to v3 endpoints</span>
<span class="issue-labels">
<span class="issue-label" style="background:rgba(56,189,248,0.1);color:var(--ac);">enhancement</span>
<span class="issue-label" style="background:rgba(167,139,250,0.1);color:var(--ac2);">api</span>
</span>
<span class="issue-num">#142</span>
</div>
<div class="issue-row">
<span class="issue-state" style="background:var(--gn);"></span>
<span class="issue-title">Auth timeout on mobile devices</span>
<span class="issue-labels">
<span class="issue-label" style="background:rgba(251,113,133,0.1);color:var(--rd);">bug</span>
<span class="issue-label" style="background:rgba(251,191,36,0.1);color:var(--yl);">priority</span>
</span>
<span class="issue-num">#139</span>
</div>
<div class="issue-row">
<span class="issue-state" style="background:var(--gn);"></span>
<span class="issue-title">Add CalDAV recurring event support</span>
<span class="issue-labels">
<span class="issue-label" style="background:rgba(52,211,153,0.1);color:var(--gn);">feature</span>
</span>
<span class="issue-num">#137</span>
</div>
<div class="issue-row">
<span class="issue-state" style="background:var(--ac2);"></span>
<span class="issue-title">Relay: per-pair key exchange for strict E2E</span>
<span class="issue-labels">
<span class="issue-label" style="background:rgba(244,114,182,0.1);color:var(--pk);">security</span>
<span class="issue-label" style="background:rgba(167,139,250,0.1);color:var(--ac2);">relay</span>
</span>
<span class="issue-num">#131</span>
</div>
<div class="issue-row">
<span class="issue-state" style="background:var(--tx);opacity:0.3;"></span>
<span class="issue-title" style="opacity:0.4;">Export: include plugin settings in backup</span>
<span class="issue-labels">
<span class="issue-label" style="background:rgba(56,189,248,0.05);color:var(--tx);opacity:0.5;">enhancement</span>
</span>
<span class="issue-num" style="opacity:0.4;">#128</span>
</div>
</div>
</div>
</div>
</section>
<section class="section" id="relay">
<div class="section-label">RELAY</div>
<div class="feat">
<div class="feat-text">
<h3>Sync between devices.<br>Share with people.</h3>
<p>A self-hosted message relay that connects multiple hyprcrd instances. Per-user encryption. Collaborative buckets. Group chat with persistent history. Shopping lists on your phone.</p>
<ul>
<li>Multi-tenant: each user gets their own login + encryption secret</li>
<li>Cards sync between devices — push from one, poll from another</li>
<li>Collaborative buckets: shared editing with exclusive card locking</li>
<li>Group chat: named rooms, member management, persistent history that backfills on new devices</li>
<li>1:1 chat with delivery + read receipts, disposable or persistent threads</li>
<li>Shopping lists: check off items on your phone, changes sync back to hyprcrd</li>
<li>Dedicated inbox view with inbox, shared buckets, and sent messages</li>
<li>Admin dashboard: user management, last-seen tracking</li>
<li>AES-256-CBC at every hop, bcrypt passwords, master key at rest</li>
</ul>
</div>
<div class="feat-visual">
<div class="vis-relay">
<div style="margin-bottom:20px;">
<span class="node" style="border-color:var(--ac);color:var(--ac);">alice</span>
<span class="arrow">&rarr;</span>
<span style="font-family:var(--pixel);font-size:10px;color:var(--tx);">encrypts with own secret</span>
<span class="arrow">&rarr;</span>
<span class="node" style="border-color:var(--yl);color:var(--yl);">relay</span>
</div>
<div style="margin-bottom:20px;padding-left:24px;border-left:2px solid var(--bdr);">
<span style="font-size:11px;color:var(--tx);">
1. authenticate sender<br>
2. decrypt with sender's secret<br>
3. re-encrypt with inter-tenant key<br>
4. store (persistent or ephemeral)<br>
5. on recipient's poll: decrypt<br>
6. re-encrypt with recipient's secret<br>
7. deliver
</span>
</div>
<div>
<span class="node" style="border-color:var(--yl);color:var(--yl);">relay</span>
<span class="arrow">&rarr;</span>
<span style="font-family:var(--pixel);font-size:10px;color:var(--tx);">re-encrypted for recipient</span>
<span class="arrow">&rarr;</span>
<span class="node" style="border-color:var(--gn);color:var(--gn);">bob</span>
</div>
</div>
</div>
</div>
</section>
<section class="section" id="bookmarks">
<div class="section-label">BOOKMARKS</div>
<div class="feat">
<div class="feat-text">
<h3>Column-based bookmarks.<br>Stored as cards.</h3>
<p>A bookmark list is a card. Organise links in named columns, drag to reorder, paste a URL and the title + favicon are fetched automatically. Link to other cards for cross-referencing.</p>
<ul>
<li>Tag any card with <code>#bookmarks</code> to turn it into a bookmark list</li>
<li>Named columns: create, rename, delete, drag items between them</li>
<li>Paste a URL from clipboard: title + favicon fetched server-side and stored as data: URI</li>
<li>Full-width column-width slider per list</li>
<li>Card links: <code>card:123</code> opens the linked card or switches to another bookmark list</li>
<li>Multiple lists with different names, colors, and bucket assignments</li>
<li>Board tiles show card color + link icon, no body preview</li>
</ul>
</div>
<div class="feat-visual">
<div style="display:flex;gap:12px;font-family:var(--mono);font-size:11px;">
<div style="flex:1;border:1px solid var(--bdr);border-radius:6px;padding:10px;">
<div style="font-family:var(--pixel);font-size:9px;color:var(--ac);margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid var(--bdr);">Dev</div>
<div style="display:flex;flex-direction:column;gap:4px;">
<div style="display:flex;align-items:center;gap:6px;padding:6px 8px;background:var(--s2);border:1px solid var(--bdr);border-radius:4px;"><span style="color:var(--ac);">&#9679;</span><span style="color:var(--hi);">GitHub</span></div>
<div style="display:flex;align-items:center;gap:6px;padding:6px 8px;background:var(--s2);border:1px solid var(--bdr);border-radius:4px;"><span style="color:var(--ac);">&#9679;</span><span style="color:var(--hi);">MDN</span></div>
</div>
</div>
<div style="flex:1;border:1px solid var(--bdr);border-radius:6px;padding:10px;">
<div style="font-family:var(--pixel);font-size:9px;color:var(--ac);margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid var(--bdr);">News</div>
<div style="display:flex;flex-direction:column;gap:4px;">
<div style="display:flex;align-items:center;gap:6px;padding:6px 8px;background:var(--s2);border:1px solid var(--bdr);border-radius:4px;"><span style="color:var(--yl);">&#9679;</span><span style="color:var(--hi);">HN</span></div>
<div style="display:flex;align-items:center;gap:6px;padding:6px 8px;background:var(--s2);border:1px solid var(--bdr);border-radius:4px;"><span style="color:var(--yl);">&#9679;</span><span style="color:var(--hi);">Lobsters</span></div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="section" id="shopping">
<div class="section-label">SHOPPING</div>
<div class="feat">
<div class="feat-text">
<h3>Write the list on desktop.<br>Check it off on your phone.</h3>
<p>Tag a card with <code>#einkauf</code> or <code>#shopping</code>. Activate it via the Shopping button. The card's checkbox items sync to the relay and appear as a phone-friendly checklist. Tap to check off, changes sync back.</p>
<ul>
<li>Multiple shopping lists: activate any combination, all show on the phone</li>
<li>Drag-and-drop ordering in the picker, order syncs to the phone</li>
<li>Markdown headings (<code># Produce</code>) group items into sections</li>
<li>Phone only toggles checkboxes — text editing stays in hyprcrd</li>
<li>4-second poll on phone, instant push from hyprcrd on every edit</li>
<li>Works inside the relay UI: same login, same session, no separate app</li>
</ul>
</div>
<div class="feat-visual">
<div style="max-width:240px;margin:0 auto;font-family:var(--mono);font-size:13px;">
<div style="font-family:var(--pixel);font-size:10px;color:var(--ac);text-align:center;margin-bottom:12px;">Shopping</div>
<div style="font-weight:600;font-size:12px;color:var(--hi);margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid var(--bdr);">Baumarkt</div>
<div style="display:flex;flex-direction:column;gap:6px;">
<div style="padding:10px 12px;background:var(--s2);border:1px solid var(--bdr);border-radius:8px;color:var(--hi);">Bretter</div>
<div style="padding:10px 12px;background:var(--s2);border:1px solid var(--bdr);border-radius:8px;color:var(--hi);">Kleber</div>
<div style="padding:10px 12px;background:var(--s2);border:1px solid var(--bdr);border-radius:8px;color:var(--hi);opacity:0.35;text-decoration:line-through;">Schrauben</div>
</div>
</div>
</div>
</div>
</section>
<section class="section" id="email">
<div class="section-label">EMAIL</div>
<div class="feat">
<div class="feat-text">
<h3>Built-in mail client.<br>Spam dies on arrival.</h3>
<p>A focused IMAP mail client built into hyprcrd. Not a replacement for your full email workflow — a triage tool. See what's real, nuke what's not, move on.</p>
<ul>
<li>Multi-account IMAP with encrypted credential storage</li>
<li>Automatic spam scoring — sender reputation, header analysis, pattern matching</li>
<li>One-click spam/ham training per sender — builds your personal allowlist/blocklist</li>
<li>Bulk select all flagged spam and delete in one action</li>
<li>Suspicious messages marked with warning badge and score breakdown</li>
<li>HTML rendering with external images blocked by default (tracking pixel protection)</li>
<li>Plain text fallback — always available, always safe</li>
<li>Pagination for large mailboxes, keyboard-friendly navigation</li>
</ul>
</div>
<div class="feat-visual">
<div class="vis-inbox">
<div class="inbox-row">
<span class="inbox-from">@alice</span>
<span class="inbox-subject">Re: Sprint planning — updated timeline attached</span>
<span class="inbox-badge" style="background:rgba(52,211,153,0.1);color:var(--gn);">inbox</span>
</div>
<div class="inbox-row">
<span class="inbox-from">GitHub</span>
<span class="inbox-subject">[hyprcrd] PR #142 merged: API v3 migration</span>
<span class="inbox-badge" style="background:rgba(56,189,248,0.1);color:var(--ac);">notification</span>
</div>
<div class="inbox-row">
<span class="inbox-from">@carol</span>
<span class="inbox-subject">Relay docs draft — please review before Friday</span>
<span class="inbox-badge" style="background:rgba(52,211,153,0.1);color:var(--gn);">inbox</span>
</div>
<div class="inbox-row spam">
<span class="inbox-from">prince.ng@mail.ru</span>
<span class="inbox-subject">URGENT: $4.5M inheritance awaiting your claim</span>
<span class="inbox-badge" style="background:rgba(251,113,133,0.1);color:var(--rd);">spam 94%</span>
</div>
<div class="inbox-row spam">
<span class="inbox-from">deals@sz-wholesale.cn</span>
<span class="inbox-subject">Limited offer!!! Best prices on electronics!!!</span>
<span class="inbox-badge" style="background:rgba(251,113,133,0.1);color:var(--rd);">spam 87%</span>
</div>
<div class="inbox-row">
<span class="inbox-from">Sentry</span>
<span class="inbox-subject">New issue: TypeError in /api/relay/push</span>
<span class="inbox-badge" style="background:rgba(251,191,36,0.1);color:var(--yl);">? suspicious</span>
</div>
<div class="inbox-row spam">
<span class="inbox-from">crypto-gains@proton.me</span>
<span class="inbox-subject">Make 500% returns with this one trick</span>
<span class="inbox-badge" style="background:rgba(251,113,133,0.1);color:var(--rd);">spam 98%</span>
</div>
</div>
</div>
</div>
</section>
<section class="section" id="keys">
<div class="section-label">KEYBOARD</div>
<div class="feat reverse">
<div class="feat-text">
<h3>40+ shortcuts.<br>Zero mouse required.</h3>
<p>Every action has a key binding. Arrow keys navigate the board. Number keys switch buckets. Quick capture creates cards without leaving the keyboard.</p>
<ul>
<li>Press <kbd>?</kbd> for the full shortcut reference</li>
<li>Shortcuts auto-suppress in input fields</li>
<li>Context-aware: editor shortcuts only fire in the editor</li>
<li>Styled tooltips and inline help across the UI</li>
</ul>
</div>
<div class="feat-visual">
<div class="vis-keys">
<span class="vkey">?</span>
<span class="vkey">N</span>
<span class="vkey">J</span>
<span class="vkey">T</span>
<span class="vkey">C</span>
<span class="vkey">W</span>
<span class="vkey">M</span>
<span class="vkey">/</span>
<span class="vkey">Esc</span>
<span class="vkey">F1</span>
<span class="vkey">Ctrl K</span>
<span class="vkey">Ctrl N</span>
<span class="vkey">Ctrl H</span>
<span class="vkey">Ctrl S</span>
<span class="vkey">Ctrl Z</span>
<span class="vkey">&uarr;</span>
<span class="vkey">&darr;</span>
<span class="vkey">&larr;</span>
<span class="vkey">&rarr;</span>
<span class="vkey">Enter</span>
<span class="vkey">Tab</span>
<span class="vkey">1</span>
<span class="vkey">2</span>
<span class="vkey">3</span>
<span class="vkey">...</span>
<span class="vkey">9</span>
</div>
</div>
</div>
</section>
<section class="section" id="deploy">
<div class="section-label">DEPLOY</div>
<div class="feat">
<div class="feat-text">
<h3>Docker. PHP host. Desktop app.</h3>
<p>Three deployment paths, same app. The Electron build bundles a static PHP binary — no PHP installation needed. Data lives outside the app bundle and survives updates.</p>
<ul>
<li>One SQLite file — back up by copying <code>data/hyprcrd.db</code></li>
<li>Export as JSON+media ZIP or Obsidian/Logseq markdown vault</li>
<li>Plugin system: drop a folder, reload, feature appears</li>
<li>Bundled: chat, email, Gitea, bookmarks, shopping lists, canvas, TIM (time calc), terminal, slideshows, Logseq import, watch</li>
<li>Fully themed: design tokens, presets, build your own</li>
<li>MIT licensed</li>
</ul>
</div>
<div class="feat-visual">
<div class="vis-cards">
<div class="vcard" style="border-color:var(--ac);">
<div class="vcard-t">Docker</div>
<div style="font-size:11px;color:var(--gn);line-height:1.8;">docker compose up -d<br><span style="opacity:0.3">&rarr; localhost:8088</span></div>
</div>
<div class="vcard" style="border-color:var(--ac2);">
<div class="vcard-t">PHP host</div>
<div style="font-size:11px;color:var(--tx);line-height:1.8;">Upload to webspace<br>Set docroot &rarr; public/</div>
</div>
<div class="vcard" style="border-color:var(--gn);">
<div class="vcard-t">Electron</div>
<div style="font-size:11px;color:var(--gn);line-height:1.8;">npm run dist<br><span style="opacity:0.3">&rarr; AppImage / DMG / NSIS</span></div>
</div>
<div class="vcard" style="border-color:var(--yl);">
<div class="vcard-t">Data</div>
<div style="font-size:11px;color:var(--tx);line-height:1.8;">data/hyprcrd.db<br>public/uploads/</div>
</div>
</div>
</div>
</div>
</section>
<section class="cta-section">
<div>
<h2>stop renting your memory.</h2>
<p>Set a password. Start writing. Find everything.</p>
<div class="cta-links">
<a href="https://git.uesome.de/tanztee/hyprcrd" class="cta-btn primary">get the source</a>
<a href="https://git.uesome.de/tanztee/hyprcrd/src/branch/master/README.md" class="cta-btn outline">readme</a>
<a href="https://git.uesome.de/tanztee/hyprcrd/src/branch/master/relay/README.md" class="cta-btn outline">relay docs</a>
<a href="https://git.uesome.de/tanztee/hyprcrd/src/branch/master/electron/README.md" class="cta-btn outline">electron</a>
</div>
</div>
</section>
</main>
<footer>hyprcrd &middot; MIT license &middot; v2.0.0</footer>
<script>
(function() {
const text = 'hyprcrd';
const el = document.getElementById('hero-typed');
const cursor = document.getElementById('hero-cursor');
if (!el) return;
let i = 0;
function type() {
if (i < text.length) {
el.textContent += text[i++];
setTimeout(type, 90 + Math.random() * 80);
} else {
setTimeout(() => cursor.classList.add('cursor-done'), 1500);
}
}
setTimeout(type, 400);
})();
</script>
</body>
</html>