This commit is contained in:
th committed 2026-04-08 14:16:07 +02:00
1 parent e0fca064ae
commit 60c9d56c05
6 files changed
+160 -7

No files matched your search

+1
View File
@@ -7,3 +7,4 @@
*.swp
*.swo
.DS_Store
*.zip
Executable
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
# Build hyprcrd distribution ZIP
set -e
cd "$(dirname "$0")"
VERSION=$(grep "APP_VERSION" config.php | grep -oP "'[^']+'" | tail -1 | tr -d "'" || echo "dev")
DIST="hyprcrd-${VERSION}.zip"
# Rebuild Tailwind if CLI exists
if [ -x ./tailwindcss ]; then
echo "Building Tailwind CSS..."
./tailwindcss --input public/css/tailwind.css --output public/css/tailwind.out.css --content "public/index.php,public/js/**/*.js" --minify
fi
echo "Creating ${DIST}..."
zip -r "$DIST" \
config.php \
Dockerfile \
docker-entrypoint.sh \
README.md \
.htaccess \
.gitignore \
src/ \
public/ \
-x "public/uploads/*" \
-x "public/uploads/.gitkeep" \
-x "public/css/tailwind.css"
# Add empty dirs with gitkeep
mkdir -p /tmp/hyprcrd-dist/data
mkdir -p /tmp/hyprcrd-dist/public/uploads
touch /tmp/hyprcrd-dist/data/.gitkeep
touch /tmp/hyprcrd-dist/public/uploads/.gitkeep
cd /tmp/hyprcrd-dist
zip -r "$OLDPWD/$DIST" data/.gitkeep public/uploads/.gitkeep
cd "$OLDPWD"
rm -rf /tmp/hyprcrd-dist
echo "Done: ${DIST} ($(du -h "$DIST" | cut -f1))"
+2
View File
@@ -61,6 +61,8 @@ body::after {
.superview-item:hover .superview-title { color: var(--t-bright); }
.superview-icon { font-size: 10px; flex-shrink: 0; }
.board-card.preview-active { border-color: var(--t-accent) !important; box-shadow: 0 0 0 1px var(--t-accent), 0 0 12px -2px color-mix(in srgb, var(--t-accent) 30%, transparent); }
/* Cards */
.card-bar { image-rendering: pixelated; height: 3px; }
.board-card { min-height: 180px; }
+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-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-lg:32rem;--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 ((-webkLine 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-lg:32rem;--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 ((-webkLine truncated
+26 -4
View File
@@ -577,9 +577,9 @@ if (str_starts_with($route, '/api/')) {
</script>
</head>
<!-- tailwind safelist: border-t-bright border-transparent scale-125 text-t-accent line-clamp-2 line-clamp-5 border-t-danger text-t-danger pr-16 text-amber-400 -->
<body class="bg-t-bg text-t-text min-h-screen transition-colors duration-200 flex flex-col">
<body class="bg-t-bg text-t-text h-screen transition-colors duration-200 flex flex-col overflow-hidden">
<header id="main-header" class="sticky top-0 z-40 bg-t-bg border-b border-t-border shadow-[0_4px_20px_-4px_rgba(0,0,0,0.3)]" style="overflow:hidden;">
<header id="main-header" class="shrink-0 z-40 bg-t-bg border-b border-t-border shadow-[0_4px_20px_-4px_rgba(0,0,0,0.3)]" style="overflow:hidden;">
<div class="flex items-center justify-between px-6 py-3 relative z-[1]">
<div class="flex items-center gap-3">
<i class="ph ph-terminal-window text-t-accent text-xl"></i>
@@ -608,6 +608,9 @@ if (str_starts_with($route, '/api/')) {
</div>
<div id="bucket-row" class="flex items-center px-6 py-1.5 relative">
<div class="flex items-center gap-1 overflow-x-auto flex-1" id="bucket-bar"></div>
<button id="board-split-toggle" class="flex items-center gap-1.5 px-2 py-1 text-[10px] font-pixel text-t-dim hover:text-t-accent transition-colors shrink-0 ml-2" title="split preview">
<i class="ph ph-split-horizontal text-xs"></i>
</button>
<button id="superview-toggle" class="flex items-center gap-1.5 px-2 py-1 text-[10px] font-pixel text-t-dim hover:text-t-accent transition-colors shrink-0 ml-2" title="compact list view">
<i class="ph ph-list text-xs"></i>
</button>
@@ -621,9 +624,28 @@ if (str_starts_with($route, '/api/')) {
</div>
<div id="group-bar" class="hidden flex items-center gap-2 px-6 pb-2 relative z-[1]"></div>
</header>
<div id="board" class="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-4 px-6 pt-5 pb-24"></div>
<div id="board-wrapper" class="flex flex-1 min-h-0 overflow-hidden">
<div id="board" class="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-4 px-6 pt-5 pb-6 flex-1 overflow-y-auto content-start"></div>
<div id="board-preview" class="hidden border-l border-t-border bg-t-surface flex flex-col" style="width:40%;min-width:300px;max-width:500px;">
<div id="board-preview-header" class="flex items-center gap-2 px-4 py-2.5 border-b border-t-border shrink-0">
<div class="card-bar w-8 rounded-sm" id="board-preview-color"></div>
<h3 id="board-preview-title" class="font-pixel text-xs text-t-bright flex-1 truncate"></h3>
<button id="board-preview-edit" class="text-t-muted hover:text-t-accent transition-colors"><i class="ph ph-pencil-simple text-sm"></i></button>
<button id="board-preview-close" class="text-t-muted hover:text-t-danger transition-colors"><i class="ph ph-x text-sm"></i></button>
</div>
<div id="board-preview-empty" class="flex-1 flex flex-col items-center justify-center text-t-dim">
<i class="ph ph-article text-3xl mb-2"></i>
<p class="font-pixel text-[10px]">click a card to preview</p>
</div>
<div id="board-preview-content" class="hidden flex-1 flex flex-col min-h-0">
<div id="board-preview-body" class="flex-1 overflow-y-auto p-5 prose text-sm text-t-text"></div>
<div id="board-preview-tags" class="hidden flex flex-wrap gap-1.5 px-5 pb-3"></div>
<div id="board-preview-meta" class="px-5 py-2 border-t border-t-border text-[10px] text-t-dim font-pixel shrink-0"></div>
</div>
</div>
</div>
<footer class="fixed inset-x-0 bottom-0 z-40 border-t border-t-border bg-t-bg shadow-[0_-4px_20px_-4px_rgba(0,0,0,0.3)] px-6 py-2 flex items-center justify-between gap-4">
<footer class="shrink-0 z-40 border-t border-t-border bg-t-bg shadow-[0_-4px_20px_-4px_rgba(0,0,0,0.3)] px-6 py-2 flex items-center justify-between gap-4">
<div class="flex items-center gap-2 flex-wrap">
<input type="file" id="import-file" accept=".zip,application/zip" class="hidden">
<button id="import-btn" class="flex items-center gap-1.5 px-2.5 py-1 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors">
+89 -2
View File
@@ -5,8 +5,17 @@ import { state, sortedCards, parseTags, tagChipHtml, getChildren, getRecent, get
import { stripMd, getTodos, firstImage, leadingImage } from './markdown.js';
import { pushReorder, pushCardEdit, pushBatch, snapshotCard } from './undo.js';
import { getCalConfig } from './caldav.js';
import { renderMd, addCodeCopyButtons } from './markdown.js';
const board = $('board');
const boardPreview = $('board-preview');
const boardPreviewBody = $('board-preview-body');
const boardPreviewTitle = $('board-preview-title');
const boardPreviewColor = $('board-preview-color');
const boardPreviewTags = $('board-preview-tags');
const boardPreviewMeta = $('board-preview-meta');
let boardSplitOpen = localStorage.getItem('hyprcrd-board-split') === '1';
let boardPreviewCardId = null;
let activeDragEl = null;
let dragGhost = null;
@@ -91,6 +100,80 @@ export function updateSortUI() {
if (icon) icon.className = `ph ${SORT_ICONS[state.sortMode]} text-xs`;
}
// ── Board split preview ──
function showBoardPreview(cardId) {
const card = state.cards.find(c => c.id == cardId);
if (!card || !boardSplitOpen) return;
boardPreviewCardId = cardId;
$('board-preview-empty')?.classList.add('hidden');
$('board-preview-content')?.classList.remove('hidden');
if (boardPreviewColor) boardPreviewColor.style.background = card.color;
if (boardPreviewTitle) boardPreviewTitle.textContent = card.title;
if (boardPreviewBody) {
boardPreviewBody.innerHTML = renderMd(card.body);
addCodeCopyButtons(boardPreviewBody);
}
const tags = parseTags(card);
if (boardPreviewTags) {
if (tags.length) {
boardPreviewTags.classList.remove('hidden');
boardPreviewTags.innerHTML = tags.map(t => tagChipHtml(t)).join('');
} else {
boardPreviewTags.classList.add('hidden');
}
}
if (boardPreviewMeta) {
boardPreviewMeta.textContent = '> ' + (card.updated_at || card.created_at || '').substring(0, 10) + (card.bucket_name ? ' | ' + card.bucket_name : '');
}
// Highlight active card
board.querySelectorAll('.board-card').forEach(el => el.classList.toggle('preview-active', el.dataset.id == cardId));
}
function closeBoardPreview() {
boardPreview?.classList.add('hidden');
boardSplitOpen = false;
localStorage.setItem('hyprcrd-board-split', '0');
const btn = $('board-split-toggle');
if (btn) btn.classList.remove('text-t-accent');
board.querySelectorAll('.board-card').forEach(el => el.classList.remove('preview-active'));
boardPreviewCardId = null;
resetBoardPreview();
}
function resetBoardPreview() {
$('board-preview-empty')?.classList.remove('hidden');
$('board-preview-content')?.classList.add('hidden');
if (boardPreviewTitle) boardPreviewTitle.textContent = '';
if (boardPreviewBody) boardPreviewBody.innerHTML = '';
}
function toggleBoardSplit() {
boardSplitOpen = !boardSplitOpen;
localStorage.setItem('hyprcrd-board-split', boardSplitOpen ? '1' : '0');
boardPreview?.classList.toggle('hidden', !boardSplitOpen);
const btn = $('board-split-toggle');
if (btn) btn.classList.toggle('text-t-accent', boardSplitOpen);
if (!boardSplitOpen) {
board.querySelectorAll('.board-card').forEach(el => el.classList.remove('preview-active'));
boardPreviewCardId = null;
}
}
$('board-split-toggle')?.addEventListener('click', toggleBoardSplit);
$('board-preview-close')?.addEventListener('click', closeBoardPreview);
$('board-preview-edit')?.addEventListener('click', () => {
const card = state.cards.find(c => c.id == boardPreviewCardId);
if (card) window._openModal(card);
});
// Init split state
if (boardSplitOpen) {
boardPreview?.classList.remove('hidden');
const btn = $('board-split-toggle');
if (btn) btn.classList.add('text-t-accent');
}
$('superview-toggle')?.addEventListener('click', () => {
state.superView = !state.superView;
localStorage.setItem('hyprcrd-superview', state.superView ? '1' : '0');
@@ -276,7 +359,7 @@ export function renderBoard() {
}
board.querySelectorAll('.superview-item[data-id]').forEach(el => {
el.addEventListener('click', () => { clearHover(); window._openDetail(el.dataset.id); });
el.addEventListener('click', () => { clearHover(); if (boardSplitOpen) showBoardPreview(el.dataset.id); else window._openDetail(el.dataset.id); });
el.addEventListener('dblclick', () => { clearHover(); const card = state.cards.find(c => c.id == el.dataset.id); if (card) window._openModal(card); });
el.addEventListener('contextmenu', e => { e.preventDefault(); clearHover(); const card = state.cards.find(c => c.id == el.dataset.id); if (card) window._showContextMenu(e.clientX, e.clientY, card); });
@@ -485,7 +568,11 @@ function bindCardEvents() {
el.addEventListener('click', (e) => {
if (e.ctrlKey || e.metaKey) { e.preventDefault(); toggleSelect(id); return; }
if (clickTimer) return;
clickTimer = setTimeout(() => { clickTimer = null; window._openDetail(id); }, 200);
clickTimer = setTimeout(() => {
clickTimer = null;
if (boardSplitOpen) { showBoardPreview(id); }
else { window._openDetail(id); }
}, 200);
});
el.addEventListener('dblclick', e => {
e.preventDefault();