todo fixes
This commit is contained in:
1 parent
fe93a0f80e
commit
940d9bbde4
8 files changed
+135
-47
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
define('APP_NAME', 'hyprcrd');
|
define('APP_NAME', 'hyprcrd');
|
||||||
define('APP_VERSION', '0.1.1');
|
define('APP_VERSION', '0.1.2');
|
||||||
|
|
||||||
// Database path — writable directory, outside webroot
|
// Database path — writable directory, outside webroot
|
||||||
define('DB_PATH', __DIR__ . '/data/hyprcrd.db');
|
define('DB_PATH', __DIR__ . '/data/hyprcrd.db');
|
||||||
|
|||||||
@@ -2,6 +2,18 @@
|
|||||||
# Build hyprcrd distribution ZIP with version bump
|
# Build hyprcrd distribution ZIP with version bump
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
|
||||||
|
echo "Usage: ./dist.sh [--patch|--minor|--major]"
|
||||||
|
echo ""
|
||||||
|
echo " --patch bump patch version (default) 0.1.1 → 0.1.2"
|
||||||
|
echo " --minor bump minor version 0.1.2 → 0.2.0"
|
||||||
|
echo " --major bump major version 0.2.0 → 1.0.0"
|
||||||
|
echo ""
|
||||||
|
echo "Builds Tailwind CSS, bumps version in config.php,"
|
||||||
|
echo "and creates hyprcrd-{version}.zip with all source files."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
# Read current version
|
# Read current version
|
||||||
|
|||||||
+26
-38
@@ -560,46 +560,34 @@ body.detail-image-fs-open { overflow: hidden; }
|
|||||||
.prose.tree .bullet-list li::marker { color: var(--t-accent); font-size: inherit; }
|
.prose.tree .bullet-list li::marker { color: var(--t-accent); font-size: inherit; }
|
||||||
.prose blockquote { border-left: 3px solid var(--t-accent); padding-left: 1em; margin: 0.8em 0; color: var(--t-muted); }
|
.prose blockquote { border-left: 3px solid var(--t-accent); padding-left: 1em; margin: 0.8em 0; color: var(--t-muted); }
|
||||||
.prose hr { border: none; border-top: 1px solid var(--t-border); margin: 1.2em 0; }
|
.prose hr { border: none; border-top: 1px solid var(--t-border); margin: 1.2em 0; }
|
||||||
/* Todo checkboxes */
|
/* Todo component */
|
||||||
.prose input[type="checkbox"] {
|
.todo-item {
|
||||||
appearance: none;
|
list-style: none !important;
|
||||||
width: 30px;
|
margin: 0 !important;
|
||||||
height: 30px;
|
margin-left: -1.5em !important;
|
||||||
border: 1px solid var(--t-dim);
|
display: flex !important;
|
||||||
border-radius: 4px;
|
gap: 10px;
|
||||||
background: var(--t-bg);
|
padding: 4px 0;
|
||||||
cursor: pointer;
|
border-bottom: 1px solid color-mix(in srgb, var(--t-border) 40%, transparent);
|
||||||
vertical-align: middle;
|
|
||||||
margin-right: 8px;
|
|
||||||
position: relative;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
.prose input[type="checkbox"]:checked {
|
.todo-item:last-child { border-bottom: none; }
|
||||||
background: var(--t-accent);
|
.todo-item.todo-done .todo-text { color: var(--t-dim); text-decoration: line-through; }
|
||||||
border-color: var(--t-accent);
|
.todo-text { flex: 1; min-width: 0; line-height: 20px; }
|
||||||
|
.todo-cb {
|
||||||
|
width: 20px; height: 20px; min-width: 20px;
|
||||||
|
border: 2px solid var(--t-border); border-radius: 3px;
|
||||||
|
background: transparent; cursor: pointer;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
padding: 0; font-size: 0;
|
||||||
|
transition: border-color 0.12s, background 0.12s;
|
||||||
}
|
}
|
||||||
.prose input[type="checkbox"]::after {
|
.todo-cb:hover { border-color: var(--t-accent); }
|
||||||
content: '';
|
.todo-cb.checked { background: var(--t-accent); border-color: var(--t-accent); }
|
||||||
position: absolute;
|
.todo-cb i { font-size: 14px; transition: opacity 0.12s; }
|
||||||
left: 9px;
|
.todo-cb:not(.checked) i { opacity: 0; color: var(--t-accent); }
|
||||||
top: 4px;
|
.todo-cb:not(.checked):hover i { opacity: 0.5; }
|
||||||
width: 8px;
|
.todo-cb.checked i { opacity: 1; color: var(--t-bg); }
|
||||||
height: 14px;
|
.prose input[type="checkbox"] { display: none; }
|
||||||
border: solid transparent;
|
|
||||||
border-width: 0 3px 3px 0;
|
|
||||||
transform: rotate(45deg);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.12s ease, border-color 0.12s ease;
|
|
||||||
}
|
|
||||||
.prose input[type="checkbox"]:checked::after {
|
|
||||||
border-color: var(--t-bg);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.prose input[type="checkbox"]:not(:checked):hover::after {
|
|
||||||
border-color: var(--t-accent);
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
.prose li:has(input[type="checkbox"]) { list-style: none; margin-left: -1.5em; }
|
|
||||||
|
|
||||||
.prose audio {
|
.prose audio {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
+2
-2
@@ -1146,11 +1146,11 @@ if (str_starts_with($route, '/api/')) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 min-h-0 overflow-hidden">
|
<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 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 id="editor-pane" class="relative min-h-full bg-t-bg" style="flex:1 1 0%;min-width:0;">
|
<div id="editor-pane" class="relative min-h-full bg-t-bg overflow-hidden" 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>
|
<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>
|
<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>
|
||||||
<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-divider" class="hidden shrink-0" style="width:11px;cursor:col-resize;position:relative;z-index:3;margin:0 -5px;"><div style="position:absolute;left:5px;top:0;bottom:0;width:1px;background:var(--t-border);"></div><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 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>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { state, sortedCards, parseTags, tagChipHtml, getChildren, getRecent, get
|
|||||||
import { stripMd, getTodos, firstImage, leadingImage } from './markdown.js';
|
import { stripMd, getTodos, firstImage, leadingImage } from './markdown.js';
|
||||||
import { pushReorder, pushCardEdit, pushBatch, snapshotCard } from './undo.js';
|
import { pushReorder, pushCardEdit, pushBatch, snapshotCard } from './undo.js';
|
||||||
import { getCalConfig } from './caldav.js';
|
import { getCalConfig } from './caldav.js';
|
||||||
import { renderMd, addCodeCopyButtons } from './markdown.js';
|
import { renderMd, addCodeCopyButtons, formatTodoItems } from './markdown.js';
|
||||||
|
import { bindCheckboxes } from './detail.js';
|
||||||
|
|
||||||
const board = $('board');
|
const board = $('board');
|
||||||
const boardPreview = $('board-preview');
|
const boardPreview = $('board-preview');
|
||||||
@@ -113,6 +114,8 @@ function showBoardPreview(cardId) {
|
|||||||
if (boardPreviewBody) {
|
if (boardPreviewBody) {
|
||||||
boardPreviewBody.innerHTML = renderMd(card.body);
|
boardPreviewBody.innerHTML = renderMd(card.body);
|
||||||
addCodeCopyButtons(boardPreviewBody);
|
addCodeCopyButtons(boardPreviewBody);
|
||||||
|
formatTodoItems(boardPreviewBody);
|
||||||
|
bindCheckboxes(boardPreviewBody, card);
|
||||||
}
|
}
|
||||||
const tags = parseTags(card);
|
const tags = parseTags(card);
|
||||||
if (boardPreviewTags) {
|
if (boardPreviewTags) {
|
||||||
@@ -283,7 +286,7 @@ export function renderBoard() {
|
|||||||
cards = cards.filter(c => favs.includes(String(c.id)));
|
cards = cards.filter(c => favs.includes(String(c.id)));
|
||||||
} else if (state.activeBucket === 'journal') {
|
} else if (state.activeBucket === 'journal') {
|
||||||
const today = new Date().toISOString().substring(0, 10);
|
const today = new Date().toISOString().substring(0, 10);
|
||||||
cards = cards.filter(c => /^\d{4}-\d{2}-\d{2}/.test(c.title) && c.title.substring(0, 10) <= today);
|
cards = cards.filter(c => /^\d{4}-\d{2}-\d{2}/.test(c.title) && c.title.substring(0, 10) <= today && !(c.tags || '').split(',').includes('geburtstag'));
|
||||||
cards.sort((a, b) => b.title.localeCompare(a.title));
|
cards.sort((a, b) => b.title.localeCompare(a.title));
|
||||||
} else if (state.activeBucket === 'none') {
|
} else if (state.activeBucket === 'none') {
|
||||||
cards = cards.filter(c => !c.bucket_id && !/^\d{4}-\d{2}-\d{2}/.test(c.title));
|
cards = cards.filter(c => !c.bucket_id && !/^\d{4}-\d{2}-\d{2}/.test(c.title));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { $, esc, api, fuzzyMatch } from './util.js';
|
import { $, esc, api, fuzzyMatch } from './util.js';
|
||||||
import { state, parseTags, tagChipHtml, getChildren, getGroupCards, getRecent, markRecent } from './state.js';
|
import { state, parseTags, tagChipHtml, getChildren, getGroupCards, getRecent, markRecent } from './state.js';
|
||||||
import { renderMd, stripMd, firstImage, addCodeCopyButtons } from './markdown.js';
|
import { renderMd, stripMd, firstImage, addCodeCopyButtons, formatTodoItems } from './markdown.js';
|
||||||
import { pushCardDelete, pushCardEdit, snapshotCard } from './undo.js';
|
import { pushCardDelete, pushCardEdit, snapshotCard } from './undo.js';
|
||||||
import { showDialog } from './dialog.js';
|
import { showDialog } from './dialog.js';
|
||||||
|
|
||||||
@@ -449,11 +449,16 @@ function showContent(card) {
|
|||||||
titleEl.textContent = card.title;
|
titleEl.textContent = card.title;
|
||||||
bodyEl.innerHTML = renderMd(card.body);
|
bodyEl.innerHTML = renderMd(card.body);
|
||||||
addCodeCopyButtons(bodyEl);
|
addCodeCopyButtons(bodyEl);
|
||||||
|
formatTodoItems(bodyEl);
|
||||||
applyReadingResizeHandles(bodyEl);
|
applyReadingResizeHandles(bodyEl);
|
||||||
bindDetailImageFullscreen(bodyEl);
|
bindDetailImageFullscreen(bodyEl);
|
||||||
|
|
||||||
// Interactive checkboxes
|
// Interactive checkboxes
|
||||||
bodyEl.querySelectorAll('input[type="checkbox"]').forEach((cb, idx) => {
|
bindCheckboxes(bodyEl, card);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bindCheckboxes(container, card) {
|
||||||
|
container.querySelectorAll('input[type="checkbox"]').forEach((cb, idx) => {
|
||||||
cb.disabled = false;
|
cb.disabled = false;
|
||||||
cb.addEventListener('change', async () => {
|
cb.addEventListener('change', async () => {
|
||||||
let n = 0;
|
let n = 0;
|
||||||
@@ -886,7 +891,9 @@ function loadSplitCard(id) {
|
|||||||
splitContent.querySelectorAll('.card-link[data-card-id]').forEach(link => {
|
splitContent.querySelectorAll('.card-link[data-card-id]').forEach(link => {
|
||||||
link.addEventListener('click', () => loadSplitCard(link.dataset.cardId));
|
link.addEventListener('click', () => loadSplitCard(link.dataset.cardId));
|
||||||
});
|
});
|
||||||
|
bindCheckboxes(splitContent, card);
|
||||||
addCodeCopyButtons(splitContent);
|
addCodeCopyButtons(splitContent);
|
||||||
|
formatTodoItems(splitContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
function promoteSplitCard(id) {
|
function promoteSplitCard(id) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { $, api, escRe, getCsrf } from './util.js';
|
import { $, api, escRe, getCsrf } from './util.js';
|
||||||
import { state, getChildren, CARD_COLORS } from './state.js';
|
import { state, getChildren, CARD_COLORS } from './state.js';
|
||||||
import { renderMd, addCodeCopyButtons } from './markdown.js';
|
import { renderMd, addCodeCopyButtons, formatTodoItems } from './markdown.js';
|
||||||
import { pushCardEdit, snapshotCard } from './undo.js';
|
import { pushCardEdit, snapshotCard } from './undo.js';
|
||||||
import { fuzzyMatch } from './util.js';
|
import { fuzzyMatch } from './util.js';
|
||||||
import { esc } from './util.js';
|
import { esc } from './util.js';
|
||||||
@@ -313,6 +313,16 @@ function updatePreview() {
|
|||||||
const title = inpTitle.value.trim();
|
const title = inpTitle.value.trim();
|
||||||
previewContent.innerHTML = (title ? `<h1>${esc(title)}</h1>` : '') + renderMd(body.value);
|
previewContent.innerHTML = (title ? `<h1>${esc(title)}</h1>` : '') + renderMd(body.value);
|
||||||
addCodeCopyButtons(previewContent);
|
addCodeCopyButtons(previewContent);
|
||||||
|
formatTodoItems(previewContent);
|
||||||
|
// Interactive checkboxes in editor preview — toggle in textarea
|
||||||
|
previewContent.querySelectorAll('input[type="checkbox"]').forEach((cb, idx) => {
|
||||||
|
cb.disabled = false;
|
||||||
|
cb.addEventListener('change', () => {
|
||||||
|
let n = 0;
|
||||||
|
body.value = body.value.replace(/- \[([ x])\]/g, (m, s) => n++ === idx ? `- [${s === 'x' ? ' ' : 'x'}]` : m);
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doSave(close, { animate = true } = {}) {
|
async function doSave(close, { animate = true } = {}) {
|
||||||
@@ -381,7 +391,32 @@ $('md-toolbar')?.addEventListener('click', e => {
|
|||||||
code: () => insertMd('`', '`', 'code'), codeblock: () => insertMd('```\n', '\n```', 'code'),
|
code: () => insertMd('`', '`', 'code'), codeblock: () => insertMd('```\n', '\n```', 'code'),
|
||||||
ul: () => insertMd('- ', '', 'item'), ol: () => insertMd('1. ', '', 'item'),
|
ul: () => insertMd('- ', '', 'item'), ol: () => insertMd('1. ', '', 'item'),
|
||||||
tree: () => insertMd('```tree\n- ', '\n - child\n', 'parent'),
|
tree: () => insertMd('```tree\n- ', '\n - child\n', 'parent'),
|
||||||
quote: () => insertMd('> ', '', 'quote'), todo: () => insertMd('- [ ] ', '', 'task'),
|
quote: () => insertMd('> ', '', 'quote'),
|
||||||
|
todo: () => {
|
||||||
|
const s = body.selectionStart, e = body.selectionEnd;
|
||||||
|
const sel = body.value.substring(s, e);
|
||||||
|
if (sel && sel.includes('\n') || (sel && /^[\s]*[-*+]\s/m.test(sel))) {
|
||||||
|
// Multi-line or bullet list: convert each line
|
||||||
|
const lines = sel.split('\n');
|
||||||
|
const hasTodos = lines.some(l => /^\s*- \[[ x]\]/.test(l));
|
||||||
|
const converted = lines.map(l => {
|
||||||
|
const trimmed = l.trim();
|
||||||
|
if (!trimmed) return l;
|
||||||
|
if (hasTodos) {
|
||||||
|
// Toggle back: remove checkbox
|
||||||
|
return l.replace(/^(\s*)- \[[ x]\] /, '$1- ');
|
||||||
|
}
|
||||||
|
// Add checkbox: strip existing bullet if present
|
||||||
|
return l.replace(/^(\s*)(?:[-*+]|\d+\.)\s/, '$1').replace(/^(\s*)(.+)$/, '$1- [ ] $2');
|
||||||
|
}).join('\n');
|
||||||
|
body.setRangeText(converted, s, e, 'select');
|
||||||
|
body.selectionStart = s;
|
||||||
|
body.selectionEnd = s + converted.length;
|
||||||
|
updatePreview();
|
||||||
|
} else {
|
||||||
|
insertMd('- [ ] ', '', 'task');
|
||||||
|
}
|
||||||
|
},
|
||||||
link: () => insertMd('[', '](url)', 'text'),
|
link: () => insertMd('[', '](url)', 'text'),
|
||||||
cardlink: () => { insertMd('[[', ']]', ''); showLinkAc(''); },
|
cardlink: () => { insertMd('[[', ']]', ''); showLinkAc(''); },
|
||||||
extract: async () => {
|
extract: async () => {
|
||||||
@@ -828,6 +863,33 @@ body.addEventListener('keydown', e => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.key === 'Tab') { e.preventDefault(); insertMd(' '); }
|
if (e.key === 'Tab') { e.preventDefault(); insertMd(' '); }
|
||||||
|
// Enter after todo/bullet/numbered list: continue the prefix
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
const pos = body.selectionStart;
|
||||||
|
const before = body.value.substring(0, pos);
|
||||||
|
const lineStart = before.lastIndexOf('\n') + 1;
|
||||||
|
const line = before.substring(lineStart);
|
||||||
|
const m = line.match(/^(\s*)(- \[[ x]\] |[-*+] |\d+\. )/);
|
||||||
|
if (m) {
|
||||||
|
e.preventDefault();
|
||||||
|
const indent = m[1];
|
||||||
|
let prefix = m[2];
|
||||||
|
// If current line is empty (just prefix), clear it instead
|
||||||
|
if (line.trim() === prefix.trim()) {
|
||||||
|
body.setRangeText('', lineStart, pos, 'end');
|
||||||
|
body.selectionStart = body.selectionEnd = lineStart;
|
||||||
|
updatePreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Increment number for ordered lists
|
||||||
|
const numMatch = prefix.match(/^(\d+)\. $/);
|
||||||
|
if (numMatch) prefix = (parseInt(numMatch[1]) + 1) + '. ';
|
||||||
|
body.setRangeText('\n' + indent + prefix, pos, pos, 'end');
|
||||||
|
updatePreview();
|
||||||
|
if (linesVisible) updateGutter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
body.addEventListener('blur', () => { setTimeout(hideLinkAc, 150); setTimeout(hideTagAc, 150); setTimeout(hideSnippetAc, 150); });
|
body.addEventListener('blur', () => { setTimeout(hideLinkAc, 150); setTimeout(hideTagAc, 150); setTimeout(hideSnippetAc, 150); });
|
||||||
|
|||||||
@@ -98,6 +98,22 @@ export function addCodeCopyButtons(container) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatTodoItems(container) {
|
||||||
|
if (!container) return;
|
||||||
|
container.querySelectorAll('li').forEach(li => {
|
||||||
|
const cb = li.querySelector('input[type="checkbox"]');
|
||||||
|
if (!cb) return;
|
||||||
|
if (li.classList.contains('todo-item')) return;
|
||||||
|
li.classList.add('todo-item');
|
||||||
|
if (cb.checked) li.classList.add('todo-done');
|
||||||
|
// Wrap remaining text nodes in a span
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.className = 'todo-text';
|
||||||
|
while (cb.nextSibling) span.appendChild(cb.nextSibling);
|
||||||
|
li.appendChild(span);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function stripMd(src) {
|
export function stripMd(src) {
|
||||||
return (src || '')
|
return (src || '')
|
||||||
.replace(/<img[^>]*>/gi, '')
|
.replace(/<img[^>]*>/gi, '')
|
||||||
|
|||||||
Reference in new issue
Block a user