hilfe eingebaut

This commit is contained in:
th committed 2026-04-07 23:57:30 +02:00
1 parent ae6c818848
commit 836826dba5
6 files changed
+532 -25

No files matched your search

+6 -4
View File
@@ -256,7 +256,6 @@ body.detail-zen-open { overflow: hidden; }
#detail.zen #split-right { display: none !important; }
#detail.zen .card-bar { display: none; }
#detail.zen #detail-scroll {
max-width: 680px;
margin: 0 auto;
padding: 3rem 2rem;
overflow: visible !important;
@@ -279,6 +278,8 @@ body.detail-zen-open { overflow: hidden; }
#detail.zen #detail-controls:hover { opacity: 1; }
#detail.zen #split-right-btn { display: none; }
/* Zen resize handles */
/* Detail close morph */
.detail-close-frame {
z-index: 130;
@@ -449,9 +450,10 @@ body.detail-image-fs-open { overflow: hidden; }
.card-link.broken { color: var(--t-danger); border-color: var(--t-danger); opacity: 0.6; }
/* Autocomplete */
#link-ac .ac-item, #tag-ac .ac-item { padding: 6px 12px; cursor: pointer; transition: background 0.1s; }
#link-ac .ac-item:hover, #link-ac .ac-item.active, #tag-ac .ac-item:hover, #tag-ac .ac-item.active { background: var(--t-raised); }
#link-ac .ac-item .ac-title, #tag-ac .ac-item .ac-title { font-size: 12px; color: var(--t-bright); }
#link-ac .ac-item, #tag-ac .ac-item, #snippet-ac .ac-item { padding: 6px 12px; cursor: pointer; transition: background 0.1s; }
#link-ac .ac-item:hover, #link-ac .ac-item.active, #tag-ac .ac-item:hover, #tag-ac .ac-item.active, #snippet-ac .ac-item:hover, #snippet-ac .ac-item.active { background: var(--t-raised); }
#link-ac .ac-item .ac-title, #tag-ac .ac-item .ac-title, #snippet-ac .ac-item .ac-title { font-size: 12px; color: var(--t-bright); }
#snippet-ac .ac-item { display: flex; align-items: center; gap: 4px; }
#link-ac .ac-item .ac-bucket { font-size: 9px; color: var(--t-dim); margin-left: 6px; }
/* Prose (markdown) */
+1 -1
View File
@@ -1,2 +1,2 @@
/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-pan-x:initial;--tw-pan-y:initial;--tw-pinch-zoom:initial;--tw-scroll-snap-strictness:proximity;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-x-reverse:0;--tw-border-style:solid;--tw-divide-y-reverse:0;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-contain-size:initial;--tw-contain-layout:initial;--tw-contain-paint:initial;--tw-contain-style:initial;--tw-text-shadow-color:initial;--tw-text-shadow-alpha:100%}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-serif:ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;--font-mono:"JetBrains Mono", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-orange-50:oklch(98% .016 73.684);--color-orange-100:oklch(95.4% .038 75.164);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-300:oklch(83.7% .128 66.29);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-orange-800:oklch(47% .157 37.304);--color-orange-900:oklch(40.8% .123 38.172);--color-orange-950:oklch(26.6% .079 36.259);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-amber-950:oklch(27.9% .077 45.635);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-700:oklch(55.4% .135 66.442);--color-yellow-800:oklch(47.6% .114 61.907);--color-yellow-900:oklch(42.1% .095 57.708);--color-yellow-950:oklch(28.6% .066 53.813);--color-lime-50:oklch(98.6% .031 120.757);--color-lime-100:oklch(96.7% .067 122.328);--color-lime-200:oklch(93.8% .127 124.321);--color-lime-300:oklch(89.7% .196 126.665);--color-lime-400:oklch(84.1% .238 128.85);--color-lime-500:oklch(76.8% .233 130.85);--color-lime-600:oklch(64.8% .2 131.684);--color-lime-700:oklch(53.2% .157 131.589);--color-lime-800:oklch(45.3% .124 130.933);--color-lime-900:oklch(40.5% .101 131.063);--color-lime-950:oklch(27.4% .072 132.109);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8Line truncated
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:"JetBrains Mono", monospace;--color-amber-400:oklch(82.8% .189 84.429);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-3xl:48rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-bold:700;--tracking-wider:.05em;--leading-relaxed:1.625;--radius-sm:.25rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--font-pixel:"Silkscreen", cursive;--color-t-bg:var(--t-bg);--color-t-surface:var(--t-surface);--color-t-raised:var(--t-raised);--color-t-border:var(--t-border);--color-t-dim:var(--t-dim);--color-t-muted:var(--t-muted);--color-t-text:var(--t-text);--color-t-bright:var(--t-bright);--color-t-accent:var(--t-accent);--color-t-accent2:var(--t-accent2);--color-t-danger:var(--t-danger)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-Line truncated
+162 -5
View File
@@ -610,9 +610,165 @@ if (str_starts_with($route, '/api/')) {
</div>
</div>
<div id="footer-count" class="font-pixel text-[10px] text-t-dim whitespace-nowrap absolute left-1/2 -translate-x-1/2"></div>
<div class="font-pixel text-[10px] text-t-dim whitespace-nowrap">© <?= date('Y') ?> hyprcrd</div>
<div class="flex items-center gap-2">
<button id="help-btn" class="flex items-center gap-1.5 px-2.5 py-1 text-xs border border-t-border text-t-dim hover:border-t-accent hover:text-t-accent transition-colors"><i class="ph ph-question text-sm"></i><span class="font-pixel text-[10px]">help</span></button>
<span class="font-pixel text-[10px] text-t-dim whitespace-nowrap">© <?= date('Y') ?> hyprcrd</span>
</div>
</footer>
<!-- Help -->
<div class="overlay fixed inset-0 z-[95] flex justify-center items-start pt-12 pb-12 px-8" style="background:var(--t-overlay)" id="help">
<div class="bg-t-surface border border-t-border shadow-2xl w-full max-w-3xl max-h-full overflow-y-auto" style="border-radius:4px;">
<div class="flex items-center gap-3 px-6 py-4 border-b border-t-border sticky top-0 bg-t-surface z-[1]">
<i class="ph ph-question text-t-accent text-base"></i>
<h2 class="font-pixel text-sm text-t-accent">help</h2>
<div class="flex-1"></div>
<button id="help-close" class="flex items-center gap-1.5 px-2.5 py-1 text-xs border border-t-border text-t-muted hover:border-t-danger hover:text-t-danger transition-colors"><i class="ph ph-x text-sm"></i><span class="font-pixel text-[10px]">close</span></button>
</div>
<div class="p-6 text-sm text-t-text space-y-6" id="help-content">
<section>
<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><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><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: <span class="text-t-dim font-pixel">all</span> (everything), <span class="text-t-dim font-pixel">current</span> (recently touched), <span class="text-t-dim font-pixel">favorites</span> (starred), <span class="text-t-dim font-pixel">journal</span> (date-titled cards), <span class="text-t-dim font-pixel">unsorted</span> (no bucket). Drag a card onto a bucket tab to move it. Drag to the + button to create a new bucket from a card.</p>
<p><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 "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 "dissolve group" to ungroup all at once.</p>
<p><span class="text-t-bright font-pixel">tags</span> are written as <span class="text-t-dim font-pixel">#tagname</span> in the card body and extracted automatically. They support hierarchy: <span class="text-t-dim font-pixel">#project/frontend</span>. Click a tag on any card to filter the board — Ctrl+click to combine multiple tags (AND). Tags can also be assigned via right-click > tags without editing the card body. Each segment of a hierarchical tag is independently clickable.</p>
<p><span class="text-t-bright font-pixel">links</span> connect cards. Write <span class="text-t-dim font-pixel">[[Card Title]]</span> in the body to create a link. Linked cards are clickable in preview and show up as backlinks on the target card. The split view shows related cards based on shared tags, links, and content overlap.</p>
</div>
</section>
<section>
<h3 class="font-pixel text-xs text-t-accent mb-3 flex items-center gap-2"><i class="ph ph-keyboard text-sm"></i>shortcuts</h3>
<table class="w-full text-xs">
<tr><td class="py-1 text-t-dim w-40 font-pixel">Ctrl+K</td><td>open / close search</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">Ctrl+Z</td><td>undo last action</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">F1</td><td>open / close help</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">Escape</td><td>close any overlay</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-squares-four text-sm"></i>board</h3>
<table class="w-full text-xs">
<tr><td class="py-1 text-t-dim w-40 font-pixel">click card</td><td>open preview</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">double-click</td><td>open editor</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">right-click</td><td>context menu (color, bucket, tags, ...)</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">Ctrl+click</td><td>multi-select cards</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">drag card</td><td>reorder, move to bucket, or group</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">click tag</td><td>filter by tag</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">Ctrl+click tag</td><td>add/remove tag filter (AND)</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">group badge</td><td>expand group</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-eye text-sm"></i>preview</h3>
<table class="w-full text-xs">
<tr><td class="py-1 text-t-dim w-40 font-pixel">double-click image</td><td>fullscreen (scroll to zoom, drag to pan)</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">Arrow Left/Right</td><td>prev/next image in gallery</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">click [[link]]</td><td>open linked card</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">click checkbox</td><td>toggle task completion</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">zen mode</td><td>full-screen reading (drag edges to resize)</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">split view</td><td>show related / tagged cards side-by-side</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-pencil-simple text-sm"></i>editor</h3>
<table class="w-full text-xs">
<tr><td class="py-1 text-t-dim w-40 font-pixel">Ctrl+S</td><td>save</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">Ctrl+Shift+S</td><td>save and close</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">Alt+Arrow Up/Down</td><td>move line</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">Shift+Alt+Arrow</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 image</td><td>upload and insert</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">drag divider</td><td>resize editor / preview split</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-lightning text-sm"></i>autocomplete</h3>
<table class="w-full text-xs">
<tr><td class="py-1 text-t-dim w-40 font-pixel">[[</td><td>link to card (search by title)</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">#</td><td>tag autocomplete (existing tags)</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">?</td><td>snippet menu (see below)</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">Arrow Up/Down</td><td>navigate suggestions</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">Enter / Tab</td><td>accept suggestion</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-magic-wand text-sm"></i>snippets (type ? + keyword + space)</h3>
<table class="w-full text-xs">
<tr><td class="py-1 text-t-dim w-40 font-pixel">?dat</td><td>current date (2026-04-07)</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">?time</td><td>current time (22:53)</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">?now</td><td>date + time</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">?week</td><td>calendar week (KW15)</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">?todo</td><td>checkbox - [ ]</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">?hr</td><td>horizontal rule ---</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">?h1 ?h2 ?h3</td><td>headings</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">?table</td><td>markdown table template</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">?code</td><td>fenced code block</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-calculator text-sm"></i>inline calculator</h3>
<table class="w-full text-xs">
<tr><td class="py-1 text-t-dim w-40 font-pixel">?3+5 Space</td><td>replaces with 8</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">34+56? Space</td><td>becomes 34+56=90</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">operators</td><td>+ - * / () % ^</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-hash text-sm"></i>tags</h3>
<table class="w-full text-xs">
<tr><td class="py-1 text-t-dim w-40 font-pixel">#tagname</td><td>write tags in card body</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">#parent/child</td><td>hierarchical tags</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">right-click > tags</td><td>assign tags via context menu</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">click tag segment</td><td>each path level is clickable</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-magnifying-glass text-sm"></i>search</h3>
<table class="w-full text-xs">
<tr><td class="py-1 text-t-dim w-40 font-pixel">text</td><td>search titles + body</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">#tag</td><td>filter by tag</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">#tag word</td><td>tag filter + text search</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">#tag1 #tag2</td><td>multiple tags (AND)</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">bucket filter</td><td>dropdown to restrict scope</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-article text-sm"></i>markdown</h3>
<table class="w-full text-xs">
<tr><td class="py-1 text-t-dim w-40 font-pixel">[[title]]</td><td>card link</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">```gallery</td><td>image gallery grid (click to fullscreen)</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">```tree</td><td>tree-style list</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">```bullets</td><td>bullet-style list</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">- [ ] / - [x]</td><td>interactive checkboxes</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">external links</td><td>open in new tab automatically</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-download-simple text-sm"></i>import / export</h3>
<table class="w-full text-xs">
<tr><td class="py-1 text-t-dim w-40 font-pixel">export</td><td>JSON + media ZIP (re-importable)</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">vault</td><td>Obsidian / Logseq markdown vault</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">right-click > save .md</td><td>single card as markdown + images</td></tr>
<tr><td class="py-1 text-t-dim font-pixel">import</td><td>restore from exported ZIP</td></tr>
</table>
</section>
</div>
</div>
</div>
<!-- Dialog -->
<div class="overlay fixed inset-0 z-[90] flex justify-center items-center px-4" style="background:var(--t-overlay)" id="dialog">
<div class="bg-t-surface border border-t-border w-full max-w-sm">
@@ -642,7 +798,6 @@ if (str_starts_with($route, '/api/')) {
<i class="ph ph-magnifying-glass text-t-accent text-base"></i>
<input type="text" id="search-input" placeholder="search cards..." class="flex-1 bg-transparent text-t-bright text-sm placeholder:text-t-dim font-mono" autocomplete="off" spellcheck="false">
<select id="search-bucket-filter" class="bg-t-bg border border-t-border text-t-muted text-[10px] font-pixel px-2 py-1 rounded-sm focus:border-t-accent focus:outline-none cursor-pointer"><option value="">all buckets</option></select>
<kbd class="text-[9px] text-t-dim border border-t-border rounded px-1.5 py-0.5 font-pixel">esc</kbd>
<button id="search-close" class="flex items-center gap-1.5 px-2.5 py-1 text-xs border border-t-border text-t-muted hover:border-t-danger hover:text-t-danger transition-colors">
<i class="ph ph-x text-sm"></i><span class="font-pixel text-[10px]">close</span>
</button>
@@ -751,6 +906,7 @@ if (str_starts_with($route, '/api/')) {
<div id="link-ac" class="fixed z-[80] bg-t-surface border border-t-border shadow-xl max-h-48 overflow-y-auto hidden" style="min-width:220px;max-width:360px;"></div>
<div id="tag-ac" class="fixed z-[80] bg-t-surface border border-t-border shadow-xl max-h-48 overflow-y-auto hidden" style="min-width:180px;max-width:300px;"></div>
<div id="snippet-ac" class="fixed z-[80] bg-t-surface border border-t-border shadow-xl max-h-56 overflow-y-auto hidden" style="min-width:240px;max-width:360px;"></div>
<!-- Gallery -->
<div class="overlay fixed inset-0 z-[72]" style="background:var(--t-overlay)" id="gallery">
@@ -836,13 +992,14 @@ if (str_starts_with($route, '/api/')) {
<button id="toggle-lines" class="toolbar-btn flex items-center gap-1.5 px-2" title="line numbers"><i class="ph ph-hash text-sm"></i></button>
<button id="toggle-preview" class="toolbar-btn flex items-center gap-1.5 px-2"><i class="ph ph-eye text-sm"></i><span class="font-pixel text-[9px]">preview</span></button>
</div>
<div class="flex flex-1 min-h-0">
<div class="flex flex-1 min-h-0 overflow-hidden">
<div id="editor-gutter" class="hidden bg-t-bg text-t-dim text-right font-mono text-sm select-none overflow-hidden shrink-0 border-r border-t-border" style="width:3.5em; padding:1rem 0.25rem 1rem 0.5rem; line-height:1.625;"></div>
<div class="flex-1 relative min-h-full bg-t-bg">
<div id="editor-pane" class="relative min-h-full bg-t-bg" style="flex:1 1 0%;min-width:0;">
<div id="editor-line-hl" class="absolute pointer-events-none left-0 right-0" style="background:var(--t-border);opacity:0.18;display:none;z-index:1;"></div>
<textarea id="editor-body" placeholder="> write markdown..." class="absolute inset-0 w-full h-full text-t-text text-sm font-mono placeholder:text-t-dim border-none overflow-y-auto" style="padding:1rem; line-height:1.625; resize:none; z-index:2; background:transparent;"></textarea>
</div>
<div id="editor-preview" class="flex-1 border-l border-t-border bg-t-bg p-4 overflow-y-auto hidden"><div id="preview-content" class="prose text-sm text-t-text"></div></div>
<div id="editor-divider" class="hidden shrink-0 border-l border-t-border" style="width:1px;cursor:col-resize;position:relative;padding:0 5px;margin:0 -5px;z-index:3;"><div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:4px;height:48px;border-radius:2px;background:var(--t-muted);opacity:0.08;transition:opacity 0.2s;"></div></div>
<div id="editor-preview" class="bg-t-bg p-4 overflow-y-auto hidden" style="flex:1 1 0%;min-width:0;"><div id="preview-content" class="prose text-sm text-t-text"></div></div>
</div>
</div>
</div>
+12
View File
@@ -54,6 +54,18 @@ async function loadCards() {
renderBoard();
}
// ── Help ──
const helpOverlay = document.getElementById('help');
function openHelp() { helpOverlay?.classList.add('open'); }
function closeHelp() { helpOverlay?.classList.remove('open'); }
document.getElementById('help-btn')?.addEventListener('click', openHelp);
document.getElementById('help-close')?.addEventListener('click', closeHelp);
helpOverlay?.addEventListener('click', e => { if (e.target === helpOverlay) closeHelp(); });
document.addEventListener('keydown', e => {
if (e.key === 'F1') { e.preventDefault(); helpOverlay?.classList.contains('open') ? closeHelp() : openHelp(); }
});
// ── Export / Import ──
document.getElementById('export-btn')?.addEventListener('click', () => { window.location.href = 'api/export'; });
+137 -12
View File
@@ -371,6 +371,7 @@ export function closeDetail(options = {}) {
closeImageFullscreen();
detail.classList.remove('open', 'zen');
removeZenHandles();
document.documentElement.classList.remove('detail-zen-open');
document.body.classList.remove('detail-zen-open');
$('split-right')?.classList.add('hidden');
@@ -614,6 +615,84 @@ document.addEventListener('keydown', e => {
// Tree view toggle
// Extract selection to new card
const ZEN_WIDTH_KEY = 'hyprcrd-zen-width';
let zenWidth = parseInt(localStorage.getItem(ZEN_WIDTH_KEY)) || 680;
let zenHandles = [];
function applyZenWidth() {
if (!scroll) return;
if (detail.classList.contains('zen')) {
scroll.style.maxWidth = zenWidth + 'px';
// Position handles at content edges
const margin = Math.max(0, (window.innerWidth - zenWidth) / 2);
zenHandles.forEach(h => {
h.el.style.left = h.side === 'left' ? (margin - 12) + 'px' : '';
h.el.style.right = h.side === 'right' ? (margin - 12) + 'px' : '';
});
} else {
scroll.style.maxWidth = '';
}
}
function createZenHandles() {
removeZenHandles();
['left', 'right'].forEach(side => {
const el = document.createElement('div');
Object.assign(el.style, {
position: 'fixed', top: '0', bottom: '0', width: '24px',
cursor: 'col-resize', zIndex: '200',
});
const bar = document.createElement('div');
Object.assign(bar.style, {
position: 'absolute', top: '50%', transform: 'translateY(-50%)',
width: '4px', height: '48px', borderRadius: '2px',
background: 'var(--t-muted)', opacity: '0.08', transition: 'opacity 0.2s',
});
bar.style[side === 'left' ? 'right' : 'left'] = '8px';
el.appendChild(bar);
el.addEventListener('mouseenter', () => { bar.style.opacity = '0.5'; });
el.addEventListener('mouseleave', () => { if (!el._dragging) bar.style.opacity = '0.08'; });
let startX, startW;
el.addEventListener('mousedown', e => {
e.preventDefault();
startX = e.clientX;
startW = zenWidth;
el._dragging = true;
bar.style.opacity = '0.7';
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
const onMove = ev => {
const dx = ev.clientX - startX;
zenWidth = Math.max(400, Math.min(window.innerWidth - 80, startW + (side === 'left' ? -dx : dx) * 2));
applyZenWidth();
};
const onUp = () => {
el._dragging = false;
bar.style.opacity = '0.08';
document.body.style.cursor = '';
document.body.style.userSelect = '';
localStorage.setItem(ZEN_WIDTH_KEY, String(zenWidth));
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
document.body.appendChild(el);
zenHandles.push({ el, side });
});
applyZenWidth();
}
function removeZenHandles() {
zenHandles.forEach(h => h.el.remove());
zenHandles = [];
}
$('detail-zen')?.addEventListener('click', () => {
detail.classList.toggle('zen');
const isZen = detail.classList.contains('zen');
@@ -621,8 +700,12 @@ $('detail-zen')?.addEventListener('click', () => {
document.body.classList.toggle('detail-zen-open', isZen);
const icon = $('detail-zen')?.querySelector('i');
if (icon) icon.className = isZen ? 'ph ph-corners-in text-sm' : 'ph ph-corners-out text-sm';
if (isZen) createZenHandles();
else { removeZenHandles(); scroll.style.maxWidth = ''; }
});
window.addEventListener('resize', () => { if (detail.classList.contains('zen')) applyZenWidth(); });
// ── Split pane (right) ──
const splitPane = $('split-right');
@@ -719,18 +802,39 @@ function renderSplitGroups(groups) {
function searchAllCards(query) {
const q = query.toLowerCase().trim();
return state.cards
.filter(c => c.id != state.viewingCard?.id)
const parts = q.split(/\s+/).filter(Boolean);
const tagFilters = parts.filter(p => p.startsWith('#') && p.length > 1).map(p => p.substring(1));
const words = parts.filter(p => !p.startsWith('#'));
const textQ = words.join(' ');
let pool = state.cards.filter(c => c.id != state.viewingCard?.id);
if (tagFilters.length) {
pool = pool.filter(c => {
const ct = parseTags(c);
return tagFilters.every(tf => ct.some(t => t === tf || t.startsWith(tf + '/') || t.includes(tf)));
});
}
if (!words.length) return pool.slice(0, 15);
return pool
.map(c => {
const t = (c.title || '').toLowerCase();
const b = stripMd(c.body || '').toLowerCase();
let score = 0;
if (t.includes(q)) score += 50;
if (t.startsWith(q)) score += 20;
if (b.includes(q)) score += 10;
// fuzzy on title
const fm = fuzzyMatch(q, t);
if (fm !== null) score += 5 + fm;
const tags = (c.tags || '').toLowerCase();
let score = tagFilters.length ? 30 : 0;
if (t.includes(textQ)) score += 50;
if (t.startsWith(textQ)) score += 20;
if (b.includes(textQ)) score += 10;
if (tags.includes(textQ)) score += 40;
for (const w of words) {
if (t.includes(w)) score += 15;
if (b.includes(w)) score += 5;
if (tags.includes(w)) score += 20;
}
if (!score) {
const fm = fuzzyMatch(textQ, t);
if (fm !== null) score += 5 + fm;
}
return score > 0 ? { card: c, score } : null;
})
.filter(Boolean)
@@ -740,6 +844,18 @@ function searchAllCards(query) {
}
let splitCardId = null;
let splitLastQuery = '';
function restoreSplitList() {
if (splitLastQuery) {
splitSearch.value = splitLastQuery;
const results = searchAllCards(splitLastQuery);
renderSplitPicks(results, `${results.length} result${results.length !== 1 ? 's' : ''}`);
} else {
openSplitPane();
}
splitContent.innerHTML = '';
}
function loadSplitCard(id) {
const card = state.cards.find(c => c.id == id);
@@ -747,13 +863,13 @@ function loadSplitCard(id) {
splitCardId = id;
splitPicks.innerHTML = `<div class="flex items-center border-b border-t-border">
<button id="split-back" class="flex items-center gap-1.5 flex-1 px-3 py-2 text-xs text-t-muted hover:text-t-accent hover:bg-t-raised transition-colors">
<i class="ph ph-arrow-left text-sm"></i><span class="font-pixel text-[10px]">related cards</span>
<i class="ph ph-arrow-left text-sm"></i><span class="font-pixel text-[10px]">${splitLastQuery ? 'results' : 'related cards'}</span>
</button>
<button id="split-promote" class="flex items-center gap-1.5 px-3 py-2 text-xs text-t-muted hover:text-t-accent hover:bg-t-raised transition-colors" title="open as main card">
<i class="ph ph-arrow-line-left text-sm"></i><span class="font-pixel text-[10px]">focus</span>
</button>
</div>`;
splitPicks.querySelector('#split-back').addEventListener('click', openSplitPane);
splitPicks.querySelector('#split-back').addEventListener('click', restoreSplitList);
splitPicks.querySelector('#split-promote').addEventListener('click', () => promoteSplitCard(id));
splitSearch.value = '';
splitContent.innerHTML = `
@@ -779,6 +895,7 @@ function openSplitPane() {
if (!splitPane) return;
splitPane.classList.remove('hidden');
splitSearch.value = '';
splitLastQuery = '';
splitContent.innerHTML = '';
const tagged = findTaggedCards(state.viewingCard);
const related = findRelatedCards(state.viewingCard);
@@ -800,9 +917,17 @@ $('split-close')?.addEventListener('click', () => splitPane.classList.add('hidde
splitSearch?.addEventListener('input', () => {
const q = splitSearch.value.trim();
splitLastQuery = q;
splitContent.innerHTML = '';
if (!q) {
const tagged = findTaggedCards(state.viewingCard);
const related = findRelatedCards(state.viewingCard);
renderSplitPicks(related, 'related');
const taggedIds = new Set(tagged.map(c => c.id));
const relatedOnly = related.filter(c => !taggedIds.has(c.id));
renderSplitGroups([
{ label: 'tags', cards: tagged },
{ label: 'related', cards: relatedOnly },
]);
return;
}
const results = searchAllCards(q);
+214 -3
View File
@@ -139,7 +139,9 @@ export function openModal(card = null) {
selectColor(card ? card.color : '#38bdf8');
state.previewVisible = true;
preview.classList.remove('hidden');
editorDivider?.classList.remove('hidden');
$('toggle-preview').classList.add('text-t-accent');
applyEditorSplit();
updatePreview();
modal.classList.add('open');
if (linesVisible) updateGutter();
@@ -296,6 +298,7 @@ export function closeModal({ animate = true } = {}) {
else clearMorphGhost();
hideLinkAc();
hideTagAc();
hideSnippetAc();
modal.classList.remove('open');
state.editingId = null;
state.returnToGalleryAfterEdit = false;
@@ -388,13 +391,68 @@ $('md-toolbar')?.addEventListener('click', e => {
cmds[btn.dataset.md]?.();
});
const editorDivider = $('editor-divider');
const EDITOR_SPLIT_KEY = 'hyprcrd-editor-split';
let editorSplit = parseFloat(localStorage.getItem(EDITOR_SPLIT_KEY)) || 50;
const editorPane = $('editor-pane');
function applyEditorSplit() {
if (!state.previewVisible) return;
if (editorPane) editorPane.style.flex = `0 0 ${editorSplit}%`;
preview.style.flex = `0 0 ${100 - editorSplit}%`;
}
$('toggle-preview')?.addEventListener('click', () => {
state.previewVisible = !state.previewVisible;
preview.classList.toggle('hidden', !state.previewVisible);
editorDivider?.classList.toggle('hidden', !state.previewVisible);
$('toggle-preview').classList.toggle('text-t-accent', state.previewVisible);
if (state.previewVisible) {
applyEditorSplit();
} else {
if (editorPane) editorPane.style.flex = '';
preview.style.flex = '';
}
updatePreview();
});
if (editorDivider) {
const bar = editorDivider.firstElementChild;
editorDivider.addEventListener('mouseenter', () => { if (bar) bar.style.opacity = '0.5'; });
editorDivider.addEventListener('mouseleave', () => { if (bar && !editorDivider._dragging) bar.style.opacity = '0.12'; });
editorDivider.addEventListener('mousedown', e => {
e.preventDefault();
editorDivider._dragging = true;
if (bar) bar.style.opacity = '0.7';
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
const container = editorDivider.parentElement;
const startX = e.clientX;
const startSplit = editorSplit;
const onMove = ev => {
const rect = container.getBoundingClientRect();
const dx = ev.clientX - startX;
const pct = startSplit + (dx / rect.width) * 100;
editorSplit = Math.max(25, Math.min(75, pct));
applyEditorSplit();
};
const onUp = () => {
editorDivider._dragging = false;
if (bar) bar.style.opacity = '0.12';
document.body.style.cursor = '';
document.body.style.userSelect = '';
localStorage.setItem(EDITOR_SPLIT_KEY, String(editorSplit));
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
}
$('add-btn')?.addEventListener('click', () => openModal());
$('modal-cancel')?.addEventListener('click', closeModal);
$('modal-save')?.addEventListener('click', saveOnly);
@@ -455,7 +513,147 @@ function completeLinkAc(idx) {
hideLinkAc(); updatePreview(); body.focus();
}
body.addEventListener('input', () => {
const SNIPPET_HINTS = [
{ key: 'dat', label: 'date', desc: '2026-04-07' },
{ key: 'time', label: 'time', desc: '22:53' },
{ key: 'now', label: 'date+time', desc: '2026-04-07 22:53' },
{ key: 'week', label: 'calendar week', desc: 'KW15' },
{ key: 'todo', label: 'checkbox', desc: '- [ ]' },
{ key: 'hr', label: 'divider', desc: '---' },
{ key: 'h1', label: 'heading 1', desc: '# ' },
{ key: 'h2', label: 'heading 2', desc: '## ' },
{ key: 'h3', label: 'heading 3', desc: '### ' },
{ key: 'table', label: 'table', desc: '| | |' },
{ key: 'code', label: 'code block', desc: '```' },
];
const snippetAc = $('snippet-ac');
let snippetAcVisible = false, snippetAcItems = [], snippetAcActive = 0;
function getSnippetQuery() {
const before = body.value.substring(0, body.selectionStart);
const m = before.match(/\?([a-z0-9]*)$/);
if (!m) return null;
// Don't show for math expressions (starts with digit)
if (m[1] && /^\d/.test(m[1])) return null;
return { start: before.length - m[0].length, query: m[1] };
}
function showSnippetAc(query) {
const q = query.toLowerCase();
snippetAcItems = SNIPPET_HINTS.filter(s => !q || s.key.includes(q) || s.label.includes(q));
if (!snippetAcItems.length) { hideSnippetAc(); return; }
snippetAcActive = 0;
snippetAc.innerHTML = snippetAcItems.map((s, i) => `
<div class="ac-item ${i === 0 ? 'active' : ''}" data-idx="${i}">
<span class="ac-title font-pixel" style="min-width:40px">?${esc(s.key)}</span>
<span style="font-size:9px;color:var(--t-dim);margin-left:8px">${esc(s.label)}</span>
<span style="font-size:9px;color:var(--t-border);margin-left:auto">${esc(s.desc)}</span>
</div>`).join('');
snippetAc.querySelectorAll('.ac-item').forEach(el => {
el.addEventListener('mousedown', e => { e.preventDefault(); completeSnippetAc(parseInt(el.dataset.idx)); });
});
const rect = body.getBoundingClientRect();
const lines = body.value.substring(0, body.selectionStart).split('\n');
snippetAc.style.top = Math.min(rect.top + (lines.length - 1) * 22 - body.scrollTop + 26, innerHeight - 200) + 'px';
snippetAc.style.left = Math.min(rect.left + 40, innerWidth - 340) + 'px';
snippetAc.classList.remove('hidden');
snippetAcVisible = true;
}
function hideSnippetAc() { snippetAc.classList.add('hidden'); snippetAcVisible = false; }
function completeSnippetAc(idx) {
const item = snippetAcItems[idx];
if (!item) return;
const info = getSnippetQuery();
if (!info) { hideSnippetAc(); return; }
const snippet = getSnippet(item.key);
if (snippet === null) { hideSnippetAc(); return; }
const before = body.value.substring(0, info.start);
const after = body.value.substring(body.selectionStart);
const cursorInside = snippet.includes('\n') ? snippet.indexOf('\n') + 1 : snippet.length;
body.value = before + snippet + after;
body.selectionStart = body.selectionEnd = info.start + cursorInside;
hideSnippetAc(); updatePreview(); body.focus();
}
document.addEventListener('click', e => {
if (snippetAcVisible && !snippetAc.contains(e.target) && e.target !== body) hideSnippetAc();
});
function getSnippet(key) {
const d = new Date();
const pad = n => String(n).padStart(2, '0');
const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const time = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
// ISO week number
const jan4 = new Date(d.getFullYear(), 0, 4);
const week = Math.ceil(((d - jan4) / 86400000 + jan4.getDay() + 1) / 7);
const snippets = {
dat: date,
time: time,
now: `${date} ${time}`,
week: `KW${week}`,
todo: '- [ ] ',
hr: '---',
h1: '# ',
h2: '## ',
h3: '### ',
table: '| col1 | col2 | col3 |\n| --- | --- | --- |\n| | | |',
code: '```\n\n```',
};
return snippets[key] ?? null;
}
body.addEventListener('input', e => {
// Inline snippets & calc: ?keyword SPACE or ?expr SPACE
if (e.inputType === 'insertText' && e.data === ' ') {
const pos = body.selectionStart;
const before = body.value.substring(0, pos - 1);
// Text snippets: ?dat, ?time, ?now, ?week, ?todo, ?hr, ?h1-3, ?table, ?code
const sm = before.match(/\?([a-z][a-z0-9]*)$/);
if (sm) {
const snippet = getSnippet(sm[1]);
if (snippet !== null) {
const start = pos - 1 - sm[0].length;
const cursorInside = snippet.includes('\n') ? snippet.indexOf('\n') + 1 : snippet.length;
body.value = body.value.substring(0, start) + snippet + body.value.substring(pos);
body.selectionStart = body.selectionEnd = start + cursorInside;
updatePreview();
return;
}
}
// Math calc: ?3+2 → replaces with result; 34+56? → 34+56=90
const cm = before.match(/\?([0-9][0-9+\-*\/().,%^ ]*[0-9)%])$/);
const cm2 = !cm ? before.match(/([0-9][0-9+\-*\/().,%^ ]*[0-9)%])\?$/) : null;
const calcMatch = cm || cm2;
if (calcMatch) {
try {
const expr = calcMatch[1].replace(/%/g, '/100*').replace(/\^/g, '**');
const result = Function('"use strict"; return (' + expr + ')')();
if (typeof result === 'number' && isFinite(result)) {
const rounded = Math.round(result * 1e10) / 1e10;
const start = pos - 1 - calcMatch[0].length;
if (cm) {
// ?expr → result
body.value = body.value.substring(0, start) + rounded + body.value.substring(pos);
body.selectionStart = body.selectionEnd = start + String(rounded).length;
} else {
// expr? → expr=result
const replacement = calcMatch[1] + '=' + rounded;
body.value = body.value.substring(0, start) + replacement + body.value.substring(pos);
body.selectionStart = body.selectionEnd = start + replacement.length;
}
}
} catch {}
}
}
const snippetInfo = getSnippetQuery();
snippetInfo ? showSnippetAc(snippetInfo.query) : hideSnippetAc();
const linkInfo = getLinkQuery();
linkInfo ? showLinkAc(linkInfo.query) : hideLinkAc();
const tagInfo = getTagQuery();
@@ -511,9 +709,11 @@ function hideTagAc() { tagAc.classList.add('hidden'); tagAcVisible = false; }
document.addEventListener('click', e => {
if (tagAcVisible && !tagAc.contains(e.target) && e.target !== body) hideTagAc();
if (snippetAcVisible && !snippetAc.contains(e.target) && e.target !== body) hideSnippetAc();
});
document.addEventListener('keydown', e => {
if (tagAcVisible && e.key === 'Escape') { hideTagAc(); }
if (tagAcVisible && e.key === 'Escape') hideTagAc();
if (snippetAcVisible && e.key === 'Escape') hideSnippetAc();
});
function completeTagAc(idx) {
@@ -596,6 +796,17 @@ body.addEventListener('keydown', e => {
});
return;
}
if (snippetAcVisible) {
if (e.key === 'ArrowDown') { e.preventDefault(); snippetAcActive = Math.min(snippetAcActive + 1, snippetAcItems.length - 1); }
else if (e.key === 'ArrowUp') { e.preventDefault(); snippetAcActive = Math.max(snippetAcActive - 1, 0); }
else if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); completeSnippetAc(snippetAcActive); return; }
else if (e.key === 'Escape') { e.preventDefault(); hideSnippetAc(); return; }
snippetAc.querySelectorAll('.ac-item').forEach((el, i) => {
el.classList.toggle('active', i === snippetAcActive);
if (i === snippetAcActive) el.scrollIntoView({ block: 'nearest' });
});
return;
}
// Alt+Arrow: move line
if (e.altKey && !e.shiftKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
e.preventDefault();
@@ -611,7 +822,7 @@ body.addEventListener('keydown', e => {
if (e.key === 'Tab') { e.preventDefault(); insertMd(' '); }
});
body.addEventListener('blur', () => { setTimeout(hideLinkAc, 150); setTimeout(hideTagAc, 150); });
body.addEventListener('blur', () => { setTimeout(hideLinkAc, 150); setTimeout(hideTagAc, 150); setTimeout(hideSnippetAc, 150); });
// ── File upload (paste + drag-drop) ──