hilfe eingebaut
This commit is contained in:
1 parent
ae6c818848
commit
836826dba5
6 files changed
+532
-25
No files matched your search
@@ -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,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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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) ──
|
||||
|
||||
|
||||
Reference in new issue
Block a user