help: add relay, context menu, find/replace, assets, super-buckets docs; fix flaky tests
Updates help screen with all undocumented features. Fixes F1 blocked by input suppression filter (allow function keys). Stabilizes card-join tests with poll loop instead of fixed sleep. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
bbfe968ff1
commit
22a43fdef7
4 files changed
+66
-16
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
define('APP_NAME', 'hyprcrd');
|
||||
define('APP_VERSION', '1.0.1');
|
||||
define('APP_VERSION', '1.0.2');
|
||||
|
||||
// Database path — writable directory, outside webroot
|
||||
define('DB_PATH', __DIR__ . '/data/hyprcrd.db');
|
||||
|
||||
+46
-4
@@ -1358,7 +1358,7 @@ if (str_starts_with($route, '/api/')) {
|
||||
<h3 class="font-pixel text-xs text-t-accent mb-3 flex items-center gap-2"><i class="ph ph-info text-sm"></i>concepts</h3>
|
||||
<div class="text-xs leading-relaxed space-y-3 text-t-muted">
|
||||
<p><i class="ph ph-cards text-t-accent text-sm"></i> <span class="text-t-bright font-pixel">cards</span> are the basic unit — each card has a title, a markdown body, a color, and optional tags. Cards appear as tiles on the board. Click to preview, double-click to edit, right-click for actions. Cards can contain images, checklists, links to other cards, and any markdown content.</p>
|
||||
<p><i class="ph ph-folder text-t-accent text-sm"></i> <span class="text-t-bright font-pixel">buckets</span> organize cards into categories (like folders or columns). Each bucket has a name and color. The bucket bar at the top lets you filter the board. Special built-in views: <i class="ph ph-squares-four text-xs"></i> <span class="text-t-dim font-pixel">all</span>, <i class="ph ph-lightning text-xs"></i> <span class="text-t-dim font-pixel">current</span>, <i class="ph ph-star text-xs"></i> <span class="text-t-dim font-pixel">favorites</span>, <i class="ph ph-notebook text-xs"></i> <span class="text-t-dim font-pixel">journal</span>, <i class="ph ph-tray text-xs"></i> <span class="text-t-dim font-pixel">unsorted</span>. Drag a card onto a bucket tab to move it. Drag to <i class="ph ph-plus text-xs"></i> to create a new bucket.</p>
|
||||
<p><i class="ph ph-folder text-t-accent text-sm"></i> <span class="text-t-bright font-pixel">buckets</span> organize cards into categories (like folders or columns). Each bucket has a name and color. The bucket bar at the top lets you filter the board. Special built-in views: <i class="ph ph-squares-four text-xs"></i> <span class="text-t-dim font-pixel">all</span>, <i class="ph ph-lightning text-xs"></i> <span class="text-t-dim font-pixel">current</span>, <i class="ph ph-star text-xs"></i> <span class="text-t-dim font-pixel">favorites</span>, <i class="ph ph-notebook text-xs"></i> <span class="text-t-dim font-pixel">journal</span>, <i class="ph ph-tray text-xs"></i> <span class="text-t-dim font-pixel">unsorted</span>. Drag a card onto a bucket tab to move it. Drag to <i class="ph ph-plus text-xs"></i> to create a new bucket. Buckets can be nested one level deep — drag a bucket onto another to create a <span class="text-t-bright font-pixel">super-bucket</span> with a dropdown for its children.</p>
|
||||
<p><i class="ph ph-stack text-t-accent text-sm"></i> <span class="text-t-bright font-pixel">groups</span> let you nest cards. Drag a card to the center of another card to group them. The parent card shows a badge with the member count. Click the badge or use <i class="ph ph-stack text-xs"></i> "open group" in the context menu to see all members. Inside a group you can reorder, drag cards out to ungroup, or drag the first card to promote a new parent. Use <i class="ph ph-arrow-line-up text-xs"></i> "dissolve group" to ungroup all at once.</p>
|
||||
<p><i class="ph ph-hash text-t-accent text-sm"></i> <span class="text-t-bright font-pixel">tags</span> are written as <span class="text-t-accent font-pixel">#tagname</span> in the card body and extracted automatically. They support hierarchy: <span class="text-t-accent font-pixel">#project/frontend</span>. Click a tag to filter — Ctrl+click to combine multiple tags (AND). Assign via right-click > <i class="ph ph-hash text-xs"></i> tags. Each segment of a hierarchical tag is independently clickable.</p>
|
||||
<p><i class="ph ph-link text-t-accent text-sm"></i> <span class="text-t-bright font-pixel">links</span> connect cards. Write <span class="text-t-accent font-pixel">[[Card Title]]</span> in the body to create a link. Linked cards are clickable in preview and show up as <i class="ph ph-arrow-bend-left-up text-xs"></i> backlinks on the target card. The <i class="ph ph-split-horizontal text-xs"></i> split view shows related cards based on shared tags, links, and content overlap.</p>
|
||||
@@ -1376,12 +1376,30 @@ if (str_starts_with($route, '/api/')) {
|
||||
<table class="w-full text-xs">
|
||||
<tr><td class="py-1 text-t-dim w-40 font-pixel">click card</td><td><i class="ph ph-eye text-xs"></i> open preview (or side panel in split mode)</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">double-click</td><td><i class="ph ph-pencil-simple text-xs"></i> open editor</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">right-click</td><td><i class="ph ph-list text-xs"></i> context menu (color, bucket, tags, ...)</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">right-click</td><td><i class="ph ph-list text-xs"></i> context menu (color, bucket, tags, copy text, mark done, join, ...)</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">Ctrl+click</td><td><i class="ph ph-check-square text-xs"></i> multi-select cards</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">drag card</td><td><i class="ph ph-arrows-out-cardinal text-xs"></i> reorder, move to bucket, or group</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-arrow-up text-xs"></i><i class="ph ph-arrow-down text-xs"></i><i class="ph ph-arrow-left text-xs"></i><i class="ph ph-arrow-right text-xs"></i></td><td>navigate cards with arrow keys, Enter to open</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">click tag</td><td><i class="ph ph-funnel text-xs"></i> filter by tag</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">Ctrl+click tag</td><td><i class="ph ph-plus-circle text-xs"></i> add/remove tag filter (AND)</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">group badge</td><td><i class="ph ph-stack text-xs"></i> expand group</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">Ctrl+N</td><td><i class="ph ph-lightning text-xs"></i> quick capture — create card in active bucket</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">1–9</td><td><i class="ph ph-folder text-xs"></i> switch to bucket by number</td></tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="font-pixel text-xs text-t-accent mb-3 flex items-center gap-2"><i class="ph ph-list text-sm"></i>context menu</h3>
|
||||
<table class="w-full text-xs">
|
||||
<tr><td class="py-1 text-t-dim w-40 font-pixel"><i class="ph ph-palette text-xs"></i> color</td><td>change card color</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-folder text-xs"></i> move</td><td>move to a different bucket</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-hash text-xs"></i> tags</td><td>assign tags via picker</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-copy text-xs"></i> copy text</td><td>copy card body as plain text (strips markdown)</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-check-circle text-xs"></i> mark done</td><td>toggle #done tag on any card</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-unite text-xs"></i> join cards</td><td>merge multi-selected cards into one</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-copy text-xs"></i> duplicate</td><td>duplicate card</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-file-md text-xs"></i> save .md</td><td>download as markdown + images</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-trash text-xs"></i> delete</td><td>delete card (with undo)</td></tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
@@ -1402,11 +1420,14 @@ if (str_starts_with($route, '/api/')) {
|
||||
<table class="w-full text-xs">
|
||||
<tr><td class="py-1 text-t-dim w-40 font-pixel">Ctrl+S</td><td><i class="ph ph-floppy-disk text-xs"></i> save</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">Ctrl+Shift+S</td><td><i class="ph ph-floppy-disk text-xs"></i> save and close</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">Ctrl+H</td><td><i class="ph ph-magnifying-glass text-xs"></i> find + replace in card</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">Ctrl+F</td><td><i class="ph ph-magnifying-glass text-xs"></i> find in card</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">Alt+<i class="ph ph-arrow-up text-xs"></i>/<i class="ph ph-arrow-down text-xs"></i></td><td>move line up/down</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">Shift+Alt+<i class="ph ph-arrow-up text-xs"></i>/<i class="ph ph-arrow-down text-xs"></i></td><td>duplicate line</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">Tab</td><td>indent (4 spaces)</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">paste / drag</td><td><i class="ph ph-upload-simple text-xs"></i> upload and insert image</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">paste / drag</td><td><i class="ph ph-upload-simple text-xs"></i> upload image, audio, or PDF</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-scissors text-xs"></i> extract</td><td>selection to new card</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-file-pdf text-xs"></i> PDF</td><td>upload + link a PDF from toolbar</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">drag divider</td><td>resize editor / preview split</td></tr>
|
||||
</table>
|
||||
</section>
|
||||
@@ -1464,6 +1485,9 @@ if (str_starts_with($route, '/api/')) {
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><span class="text-t-accent">#tag</span> word</td><td>tag filter + text search combined</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><span class="text-t-accent">#tag1 #tag2</span></td><td><i class="ph ph-intersect text-xs"></i> multiple tags (AND)</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">bucket filter</td><td><i class="ph ph-folder text-xs"></i> dropdown to restrict scope</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-swap text-xs"></i> Replace</td><td>toggle replace mode — bulk find + replace across all matching cards</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">Ctrl+Shift+H</td><td><i class="ph ph-swap text-xs"></i> open search directly in replace mode</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">drag result</td><td><i class="ph ph-stack text-xs"></i> drag onto another card to group</td></tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
@@ -1506,12 +1530,30 @@ if (str_starts_with($route, '/api/')) {
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="font-pixel text-xs text-t-accent mb-3 flex items-center gap-2"><i class="ph ph-layout text-sm"></i>views</h3>
|
||||
<h3 class="font-pixel text-xs text-t-accent mb-3 flex items-center gap-2"><i class="ph ph-layout text-sm"></i>views & toolbar</h3>
|
||||
<table class="w-full text-xs">
|
||||
<tr><td class="py-1 text-t-dim w-40 font-pixel"><i class="ph ph-split-horizontal text-xs"></i> split</td><td>side-by-side board + preview panel</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-list text-xs"></i> superview</td><td>compact multi-column title list with hover cards</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-check-circle text-xs"></i> hide done</td><td>toggle cards tagged #done on/off</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-calendar-x text-xs"></i> hide termine</td><td>toggle appointment cards on/off</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-hash text-xs"></i> hide tagged</td><td>toggle tagged cards on/off</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-sort-ascending text-xs"></i> sort</td><td>manual / newest / oldest / alpha / tag</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-images text-xs"></i> assets</td><td>gallery of all images, audio, and PDFs across cards — with orphan detection</td></tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="font-pixel text-xs text-t-accent mb-3 flex items-center gap-2"><i class="ph ph-broadcast text-sm"></i>relay</h3>
|
||||
<div class="text-xs leading-relaxed space-y-3 text-t-muted mb-3">
|
||||
<p>The <span class="text-t-bright font-pixel">relay</span> is an optional self-hosted server that connects multiple hyprcrd instances. Configure the relay URL and your client secret in <i class="ph ph-gear text-xs"></i> Settings > Relay. The relay has its own admin UI for user management.</p>
|
||||
</div>
|
||||
<table class="w-full text-xs">
|
||||
<tr><td class="py-1 text-t-dim w-40 font-pixel"><i class="ph ph-folders text-xs"></i> collab-buckets</td><td>shared buckets between tenants — exclusive card locking, unread indicators, E2E encrypted sync</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-chat-circle-dots text-xs"></i> chat</td><td>encrypted chat between tenants — footer flyout, delivery + read receipts</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-paper-plane-tilt text-xs"></i> compose</td><td>send messages to other tenants via the relay inbox</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-tray text-xs"></i> inbox</td><td>read received messages — synced badge shows delivery status</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-lock text-xs"></i> card locking</td><td>editing a collab card locks it for all others — takeover on first keystroke</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-key text-xs"></i> client secret</td><td>per-tenant E2E key — set in relay account settings, shared across devices</td></tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -114,8 +114,8 @@ document.addEventListener('keydown', (e) => {
|
||||
if (!matched) continue;
|
||||
|
||||
// Suppress non-modifier shortcuts when focus is in an editable field
|
||||
// (except Escape which should always close overlays)
|
||||
if (inEditable && !s.ctrl && !s.alt && !s.match && e.key !== 'Escape') continue;
|
||||
// (allow Escape and function keys like F1 through)
|
||||
if (inEditable && !s.ctrl && !s.alt && !s.match && e.key !== 'Escape' && !e.key.startsWith('F')) continue;
|
||||
|
||||
// Guard
|
||||
if (s.guard && !s.guard()) continue;
|
||||
|
||||
@@ -20,16 +20,24 @@ function selectIds(ids, { bulk = false } = {}) {
|
||||
for (const id of ids) state.selectedIds.add(String(id));
|
||||
}
|
||||
|
||||
async function clickJoin(rightClickTargetEl) {
|
||||
async function clickJoin(rightClickTargetEl, winnerId) {
|
||||
rightClick(rightClickTargetEl);
|
||||
await sleep(150);
|
||||
const joinBtn = $$('#ctx-items [data-action="join"]')[0];
|
||||
if (!joinBtn) return false;
|
||||
click(joinBtn);
|
||||
// joinSelectedCards does: fetch full bodies → PATCH winner → N × DELETE
|
||||
// losers → _reloadAll. That's 3+ serial API calls and a full board
|
||||
// re-render, so 400ms was sometimes too tight on slower runs.
|
||||
await sleep(800);
|
||||
// losers → _reloadAll. Poll for the winner to have merged body.
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await sleep(100);
|
||||
if (winnerId) {
|
||||
const c = await getCard(winnerId);
|
||||
if (c?.body?.includes('## ')) return true;
|
||||
} else {
|
||||
await sleep(600);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -109,7 +117,7 @@ describe('Card: Join — sequential selection (first wins)', () => {
|
||||
if (!firstEl) return;
|
||||
selectIds([first.id, second.id, third.id], { bulk: false });
|
||||
|
||||
const ok = await clickJoin(firstEl);
|
||||
const ok = await clickJoin(firstEl, first.id);
|
||||
if (!ok) return;
|
||||
|
||||
// Winner survives
|
||||
@@ -132,7 +140,7 @@ describe('Card: Join — sequential selection (first wins)', () => {
|
||||
if (!aEl) return;
|
||||
selectIds([a.id, b.id, c.id], { bulk: false });
|
||||
|
||||
const ok = await clickJoin(aEl);
|
||||
const ok = await clickJoin(aEl, a.id);
|
||||
if (!ok) return;
|
||||
|
||||
const survivor = await getCard(a.id);
|
||||
@@ -159,7 +167,7 @@ describe('Card: Join — sequential selection (first wins)', () => {
|
||||
if (!aEl) return;
|
||||
selectIds([a.id, b.id], { bulk: false });
|
||||
|
||||
const ok = await clickJoin(aEl);
|
||||
const ok = await clickJoin(aEl, a.id);
|
||||
if (!ok) return;
|
||||
|
||||
expect(state.selectedIds.size).toBe(0);
|
||||
@@ -187,7 +195,7 @@ describe('Card: Join — bulk selection (longest title wins)', () => {
|
||||
// join code uses longest-title strategy.
|
||||
selectIds([short.id, longer.id, medium.id], { bulk: true });
|
||||
|
||||
const ok = await clickJoin(shortEl);
|
||||
const ok = await clickJoin(shortEl, longer.id);
|
||||
if (!ok) return;
|
||||
|
||||
// The longest-titled card should be the survivor
|
||||
@@ -220,7 +228,7 @@ describe('Card: Join — undo', () => {
|
||||
if (!winnerEl) return;
|
||||
selectIds([winner.id, loser.id], { bulk: false });
|
||||
|
||||
const ok = await clickJoin(winnerEl);
|
||||
const ok = await clickJoin(winnerEl, winner.id);
|
||||
if (!ok) return;
|
||||
|
||||
// Sanity: loser is gone, winner has merged body
|
||||
|
||||
Reference in new issue
Block a user