fixes + theme
This commit is contained in:
1 parent
5af49655d6
commit
22f73ab3b9
25 files changed
+1004
-206
No files matched your search
@@ -78,7 +78,7 @@ document.addEventListener('keydown', async e => {
|
||||
e.preventDefault();
|
||||
if (localStorage.getItem('hyprcrd-confirm-delete') !== '0') {
|
||||
const { showDialog } = await import('./dialog.js');
|
||||
const ok = await showDialog({ title: 'delete cards', message: `Delete ${selectedIds.size} card${selectedIds.size > 1 ? 's' : ''}?`, danger: true, okLabel: 'delete' });
|
||||
const ok = await showDialog({ title: 'delete cards', message: `Delete ${selectedIds.size} card${selectedIds.size > 1 ? 's' : ''}?`, danger: true, okLabel: 'Delete' });
|
||||
if (!ok) return;
|
||||
}
|
||||
for (const id of selectedIds) await api(`api/cards/${id}`, { method: 'DELETE' });
|
||||
@@ -176,7 +176,7 @@ $('select-bucket')?.addEventListener('click', () => {
|
||||
$('select-color')?.addEventListener('click', async () => {
|
||||
if (!selectedIds.size) return;
|
||||
const { showDialog } = await import('./dialog.js');
|
||||
const color = await showDialog({ title: 'set color', input: true, placeholder: '#38bdf8', okLabel: 'apply' });
|
||||
const color = await showDialog({ title: 'set color', input: true, placeholder: '#38bdf8', okLabel: 'Apply' });
|
||||
if (!color || !/^#[0-9a-fA-F]{6}$/.test(color.trim())) return;
|
||||
for (const id of selectedIds) {
|
||||
await api(`api/cards/${id}`, { method: 'PATCH', body: { color: color.trim() } });
|
||||
@@ -188,7 +188,7 @@ $('select-color')?.addEventListener('click', async () => {
|
||||
$('select-tag')?.addEventListener('click', async () => {
|
||||
if (!selectedIds.size) return;
|
||||
const { showDialog } = await import('./dialog.js');
|
||||
const tag = await showDialog({ title: 'add tag', input: true, placeholder: '#tagname', okLabel: 'add' });
|
||||
const tag = await showDialog({ title: 'add tag', input: true, placeholder: '#tagname', okLabel: 'Add' });
|
||||
if (!tag) return;
|
||||
const cleanTag = tag.trim().replace(/^#/, '');
|
||||
if (!cleanTag) return;
|
||||
@@ -215,7 +215,7 @@ $('select-delete')?.addEventListener('click', async () => {
|
||||
if (!selectedIds.size) return;
|
||||
if (localStorage.getItem('hyprcrd-confirm-delete') !== '0') {
|
||||
const { showDialog } = await import('./dialog.js');
|
||||
const ok = await showDialog({ title: 'delete cards', message: `Delete ${selectedIds.size} card${selectedIds.size > 1 ? 's' : ''}?`, danger: true, okLabel: 'delete' });
|
||||
const ok = await showDialog({ title: 'delete cards', message: `Delete ${selectedIds.size} card${selectedIds.size > 1 ? 's' : ''}?`, danger: true, okLabel: 'Delete' });
|
||||
if (!ok) return;
|
||||
}
|
||||
for (const id of selectedIds) {
|
||||
@@ -418,7 +418,7 @@ export function renderCardHtml(c, inGroup = false, recent = []) {
|
||||
${loaded
|
||||
? (localStorage.getItem('hyprcrd-body-lines') === '0' ? '' : `<p class="text-xs ${hasCover ? 'text-t-text' : 'text-t-muted'} leading-relaxed flex-1" style="-webkit-line-clamp:var(--card-body-lines,5);display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden;">${esc(stripMd(c.body))}</p>`)
|
||||
: `<div class="flex-1 flex items-center justify-center py-2"><i class="ph ph-spinner card-spinner text-t-dim text-lg"></i></div>`}
|
||||
${tags.length && localStorage.getItem('hyprcrd-show-tags') !== '0' ? `<div class="flex flex-wrap gap-1 mt-1.5 card-layer">${tags.slice(0, 5).map(t => tagChipHtml(t)).join('')}${tags.length > 5 ? `<span class="tag-chip tag-chip-more">+${tags.length - 5}</span>` : ''}</div>` : ''}
|
||||
${tags.length && localStorage.getItem('hyprcrd-show-tags') !== '0' ? `<div class="card-tags flex flex-wrap gap-1 mt-1.5 card-layer">${tags.slice(0, 5).map(t => tagChipHtml(t)).join('')}${tags.length > 5 ? `<span class="tag-chip tag-chip-more">+${tags.length - 5}</span>` : ''}</div>` : ''}
|
||||
<div class="mt-auto pt-2 flex items-center gap-2 text-[10px] ${hasCover ? 'text-t-text' : 'text-t-dim'} font-pixel">
|
||||
${c.due_date ? `<span class="due-pill" style="background:${overdue ? 'var(--t-danger)' : cardColor};"><i class="ph ph-clock-countdown" style="font-size:9px;"></i>${c.due_date}</span>` : `<span class="flex items-center gap-1"><i class="ph ph-clock text-xs"></i>${(c.updated_at || c.created_at || '').substring(0, 10)}</span>`}
|
||||
${c.bucket_name ? `<span class="flex items-center gap-1"><i class="ph ph-folder text-xs"></i>${esc(c.bucket_name)}</span>` : ''}
|
||||
@@ -774,7 +774,7 @@ function renderGroupBar(cards) {
|
||||
bar.dataset.groupId = String(state.activeGroup);
|
||||
bar.innerHTML = `
|
||||
<button id="group-back" 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">
|
||||
<i class="ph ph-arrow-left text-sm"></i><span class="font-pixel text-[10px]">back</span>
|
||||
<i class="ph ph-arrow-left text-sm"></i><span class="font-pixel text-[10px]">Back</span>
|
||||
</button>
|
||||
<i class="ph ph-stack text-t-accent text-sm"></i>
|
||||
<span class="font-pixel text-[10px] text-t-accent">${esc(parent?.title || 'group')}</span>
|
||||
|
||||
@@ -25,16 +25,16 @@ async function showBucketCtx(x, y, bucket) {
|
||||
const hasCards = state.cards.some(c => c.bucket_id == bucket.id);
|
||||
|
||||
ctxMenu.innerHTML = `<div class="py-1">
|
||||
<button class="ctx-item" data-baction="rename"><i class="ph ph-pencil-simple text-sm"></i><span class="font-pixel text-[10px]">rename</span></button>
|
||||
<button class="ctx-item" data-baction="color"><i class="ph ph-palette text-sm"></i><span class="font-pixel text-[10px]">color</span><i class="ph ph-caret-right text-xs ml-auto"></i></button>
|
||||
<button class="ctx-item" data-baction="rename"><i class="ph ph-pencil-simple text-sm"></i><span class="font-pixel text-[10px]">Rename</span></button>
|
||||
<button class="ctx-item" data-baction="color"><i class="ph ph-palette text-sm"></i><span class="font-pixel text-[10px]">Color</span><i class="ph ph-caret-right text-xs ml-auto"></i></button>
|
||||
<div style="height:1px;background:var(--t-border);margin:2px 0"></div>
|
||||
<button class="ctx-item ${!canLeft ? 'opacity-30 pointer-events-none' : ''}" data-baction="move-left"><i class="ph ph-arrow-left text-sm"></i><span class="font-pixel text-[10px]">move left</span></button>
|
||||
<button class="ctx-item ${!canRight ? 'opacity-30 pointer-events-none' : ''}" data-baction="move-right"><i class="ph ph-arrow-right text-sm"></i><span class="font-pixel text-[10px]">move right</span></button>
|
||||
<button class="ctx-item ${!canLeft ? 'opacity-30 pointer-events-none' : ''}" data-baction="move-left"><i class="ph ph-arrow-left text-sm"></i><span class="font-pixel text-[10px]">Move left</span></button>
|
||||
<button class="ctx-item ${!canRight ? 'opacity-30 pointer-events-none' : ''}" data-baction="move-right"><i class="ph ph-arrow-right text-sm"></i><span class="font-pixel text-[10px]">Move right</span></button>
|
||||
<div style="height:1px;background:var(--t-border);margin:2px 0"></div>
|
||||
${hasCards
|
||||
? `<button class="ctx-item" data-baction="delete-keep"><i class="ph ph-trash text-sm"></i><span class="font-pixel text-[10px]">delete (keep cards)</span></button>
|
||||
<button class="ctx-item danger" data-baction="delete-all"><i class="ph ph-trash text-sm"></i><span class="font-pixel text-[10px]">delete + cards</span></button>`
|
||||
: `<button class="ctx-item danger" data-baction="delete-keep"><i class="ph ph-trash text-sm"></i><span class="font-pixel text-[10px]">delete</span></button>`
|
||||
? `<button class="ctx-item" data-baction="delete-keep"><i class="ph ph-trash text-sm"></i><span class="font-pixel text-[10px]">Delete (keep cards)</span></button>
|
||||
<button class="ctx-item danger" data-baction="delete-all"><i class="ph ph-trash text-sm"></i><span class="font-pixel text-[10px]">Delete + cards</span></button>`
|
||||
: `<button class="ctx-item danger" data-baction="delete-keep"><i class="ph ph-trash text-sm"></i><span class="font-pixel text-[10px]">Delete</span></button>`
|
||||
}
|
||||
</div>
|
||||
<div id="bctx-colors" class="hidden px-3 py-2 border-t border-t-border">
|
||||
@@ -76,7 +76,7 @@ async function showBucketCtx(x, y, bucket) {
|
||||
hideBucketCtx();
|
||||
const cards = state.cards.filter(c => c.bucket_id == bucket.id);
|
||||
if (cards.length > 0 || localStorage.getItem('hyprcrd-confirm-delete') !== '0') {
|
||||
const ok = await showDialog({ title: 'delete_bucket', message: cards.length ? `Delete "${bucket.name}"? ${cards.length} card(s) will be kept.` : `Delete "${bucket.name}"?`, danger: true, okLabel: 'delete' });
|
||||
const ok = await showDialog({ title: 'delete_bucket', message: cards.length ? `Delete "${bucket.name}"? ${cards.length} card(s) will be kept.` : `Delete "${bucket.name}"?`, danger: true, okLabel: 'Delete' });
|
||||
if (!ok) return;
|
||||
}
|
||||
if (state.activeBucket == bucket.id) state.activeBucket = 'all';
|
||||
@@ -86,7 +86,7 @@ async function showBucketCtx(x, y, bucket) {
|
||||
else if (a === 'delete-all') {
|
||||
hideBucketCtx();
|
||||
const cards = state.cards.filter(c => c.bucket_id == bucket.id);
|
||||
const ok = await showDialog({ title: 'delete_bucket', message: `Delete "${bucket.name}" and ${cards.length} card(s)?`, danger: true, okLabel: 'delete all' });
|
||||
const ok = await showDialog({ title: 'delete_bucket', message: `Delete "${bucket.name}" and ${cards.length} card(s)?`, danger: true, okLabel: 'Delete all' });
|
||||
if (!ok) return;
|
||||
for (const c of cards) await api(`api/cards/${c.id}`, { method: 'DELETE' });
|
||||
if (state.activeBucket == bucket.id) state.activeBucket = 'all';
|
||||
@@ -165,7 +165,7 @@ function renderBar() {
|
||||
const active = state.activeBucket == b.id;
|
||||
return `<button class="${tab(active)}" data-bucket="${b.id}" draggable="true" data-bucket-drag="${b.id}" style="border-top:3px solid ${c};${active ? `background:${c};color:var(--t-bg);border-color:${c}` : ''}"><i class="ph ph-folder text-sm"></i><span class="font-pixel text-[10px]">${esc(b.name)}</span></button>`;
|
||||
}).join('')}
|
||||
<button id="add-bucket-btn" class="bucket-add flex items-center gap-1 px-2 py-1 text-xs text-t-dim hover:text-t-accent transition-colors shrink-0"><i class="ph ph-plus text-sm"></i><span class="font-pixel text-[10px]">bucket</span></button>`;
|
||||
<button id="add-bucket-btn" class="bucket-add flex items-center gap-1 px-2 py-1 text-xs text-t-dim hover:text-t-accent transition-colors shrink-0"><i class="ph ph-plus text-sm"></i><span class="font-pixel text-[10px]">Bucket</span></button>`;
|
||||
|
||||
updateHeaderTint();
|
||||
|
||||
|
||||
@@ -37,28 +37,28 @@ export function showContextMenu(x, y, card) {
|
||||
const isChild = !!card.parent_id;
|
||||
|
||||
items.innerHTML = `
|
||||
<button class="ctx-item" data-action="open"><i class="ph ph-eye text-sm"></i><span class="font-pixel text-[10px]">open</span></button>
|
||||
<button class="ctx-item" data-action="edit"><i class="ph ph-pencil-simple text-sm"></i><span class="font-pixel text-[10px]">edit</span></button>
|
||||
<button class="ctx-item" data-action="duplicate"><i class="ph ph-copy text-sm"></i><span class="font-pixel text-[10px]">duplicate</span></button>
|
||||
${/^\d{4}-\d{2}-\d{2}/.test(card.title) ? `<button class="ctx-item" data-action="move-date"><i class="ph ph-calendar-dots text-sm"></i><span class="font-pixel text-[10px]">move to date</span></button>` : ''}
|
||||
<button class="ctx-item" data-action="due-date"><i class="ph ph-clock-countdown text-sm ${card.due_date ? 'text-t-accent' : ''}"></i><span class="font-pixel text-[10px]">${card.due_date ? 'due: ' + card.due_date : 'set due date'}</span></button>
|
||||
${card.due_date ? `<button class="ctx-item" data-action="due-done"><i class="ph ph-check-circle text-sm text-t-accent"></i><span class="font-pixel text-[10px]">mark done</span></button>` : ''}
|
||||
<button class="ctx-item" data-action="save-md"><i class="ph ph-file-md text-sm"></i><span class="font-pixel text-[10px]">save .md</span></button>
|
||||
<button class="ctx-item" data-action="push-relay"><i class="ph ph-broadcast text-sm"></i><span class="font-pixel text-[10px]">push to relay</span><i class="ph ph-caret-right text-xs ml-auto"></i></button>
|
||||
<button class="ctx-item" data-action="favorite"><i class="ph${isFavorite(card.id) ? '-fill' : ''} ph-star text-sm ${isFavorite(card.id) ? 'text-amber-400' : ''}"></i><span class="font-pixel text-[10px]">${isFavorite(card.id) ? 'unfavorite' : 'favorite'}</span></button>
|
||||
<button class="ctx-item" data-action="open"><i class="ph ph-eye text-sm"></i><span class="font-pixel text-[10px]">Open</span></button>
|
||||
<button class="ctx-item" data-action="edit"><i class="ph ph-pencil-simple text-sm"></i><span class="font-pixel text-[10px]">Edit</span></button>
|
||||
<button class="ctx-item" data-action="duplicate"><i class="ph ph-copy text-sm"></i><span class="font-pixel text-[10px]">Duplicate</span></button>
|
||||
${/^\d{4}-\d{2}-\d{2}/.test(card.title) ? `<button class="ctx-item" data-action="move-date"><i class="ph ph-calendar-dots text-sm"></i><span class="font-pixel text-[10px]">Move to date</span></button>` : ''}
|
||||
<button class="ctx-item" data-action="due-date"><i class="ph ph-clock-countdown text-sm ${card.due_date ? 'text-t-accent' : ''}"></i><span class="font-pixel text-[10px]">${card.due_date ? 'Due: ' + card.due_date : 'Set due date'}</span></button>
|
||||
${card.due_date ? `<button class="ctx-item" data-action="due-done"><i class="ph ph-check-circle text-sm text-t-accent"></i><span class="font-pixel text-[10px]">Mark done</span></button>` : ''}
|
||||
<button class="ctx-item" data-action="save-md"><i class="ph ph-file-md text-sm"></i><span class="font-pixel text-[10px]">Save .md</span></button>
|
||||
<button class="ctx-item" data-action="push-relay"><i class="ph ph-broadcast text-sm"></i><span class="font-pixel text-[10px]">Push to relay</span><i class="ph ph-caret-right text-xs ml-auto"></i></button>
|
||||
<button class="ctx-item" data-action="favorite"><i class="ph${isFavorite(card.id) ? '-fill' : ''} ph-star text-sm ${isFavorite(card.id) ? 'text-amber-400' : ''}"></i><span class="font-pixel text-[10px]">${isFavorite(card.id) ? 'Unfavorite' : 'Favorite'}</span></button>
|
||||
<div class="ctx-sep"></div>
|
||||
<button class="ctx-item" data-action="color"><i class="ph ph-palette text-sm"></i><span class="font-pixel text-[10px]">color</span><i class="ph ph-caret-right text-xs ml-auto"></i></button>
|
||||
<button class="ctx-item" data-action="bucket"><i class="ph ph-folder text-sm"></i><span class="font-pixel text-[10px]">bucket</span><i class="ph ph-caret-right text-xs ml-auto"></i></button>
|
||||
<button class="ctx-item" data-action="tags"><i class="ph ph-hash text-sm"></i><span class="font-pixel text-[10px]">tags</span><i class="ph ph-caret-right text-xs ml-auto"></i></button>
|
||||
<button class="ctx-item" data-action="color"><i class="ph ph-palette text-sm"></i><span class="font-pixel text-[10px]">Color</span><i class="ph ph-caret-right text-xs ml-auto"></i></button>
|
||||
<button class="ctx-item" data-action="bucket"><i class="ph ph-folder text-sm"></i><span class="font-pixel text-[10px]">Bucket</span><i class="ph ph-caret-right text-xs ml-auto"></i></button>
|
||||
<button class="ctx-item" data-action="tags"><i class="ph ph-hash text-sm"></i><span class="font-pixel text-[10px]">Tags</span><i class="ph ph-caret-right text-xs ml-auto"></i></button>
|
||||
${isParent ? `<div class="ctx-sep"></div>
|
||||
<button class="ctx-item" data-action="expand-group"><i class="ph ph-stack text-sm"></i><span class="font-pixel text-[10px]">open group</span></button>
|
||||
<button class="ctx-item" data-action="group-members"><i class="ph ph-list text-sm"></i><span class="font-pixel text-[10px]">members (${children.length + 1})</span><i class="ph ph-caret-right text-xs ml-auto"></i></button>
|
||||
<button class="ctx-item" data-action="dissolve"><i class="ph ph-arrow-line-up text-sm"></i><span class="font-pixel text-[10px]">dissolve group</span></button>` : ''}
|
||||
<button class="ctx-item" data-action="expand-group"><i class="ph ph-stack text-sm"></i><span class="font-pixel text-[10px]">Open group</span></button>
|
||||
<button class="ctx-item" data-action="group-members"><i class="ph ph-list text-sm"></i><span class="font-pixel text-[10px]">Members (${children.length + 1})</span><i class="ph ph-caret-right text-xs ml-auto"></i></button>
|
||||
<button class="ctx-item" data-action="dissolve"><i class="ph ph-arrow-line-up text-sm"></i><span class="font-pixel text-[10px]">Dissolve group</span></button>` : ''}
|
||||
${isChild ? `<div class="ctx-sep"></div>
|
||||
<button class="ctx-item" data-action="go-to-group"><i class="ph ph-stack text-sm"></i><span class="font-pixel text-[10px]">go to group</span></button>
|
||||
<button class="ctx-item" data-action="ungroup"><i class="ph ph-arrow-line-up text-sm"></i><span class="font-pixel text-[10px]">ungroup</span></button>` : ''}
|
||||
<button class="ctx-item" data-action="go-to-group"><i class="ph ph-stack text-sm"></i><span class="font-pixel text-[10px]">Go to group</span></button>
|
||||
<button class="ctx-item" data-action="ungroup"><i class="ph ph-arrow-line-up text-sm"></i><span class="font-pixel text-[10px]">Ungroup</span></button>` : ''}
|
||||
<div class="ctx-sep"></div>
|
||||
<button class="ctx-item danger" data-action="delete"><i class="ph ph-trash text-sm"></i><span class="font-pixel text-[10px]">delete</span></button>`;
|
||||
<button class="ctx-item danger" data-action="delete"><i class="ph ph-trash text-sm"></i><span class="font-pixel text-[10px]">Delete</span></button>`;
|
||||
|
||||
// Color sub-panel
|
||||
const grid = $('ctx-color-grid');
|
||||
@@ -77,7 +77,7 @@ export function showContextMenu(x, y, card) {
|
||||
members.classList.add('hidden');
|
||||
|
||||
// Bucket sub-panel
|
||||
buckets.innerHTML = `<button class="ctx-item ${!card.bucket_id ? 'text-t-accent' : ''}" data-ctx-bucket=""><i class="ph ph-x text-sm"></i><span class="font-pixel text-[10px]">no bucket</span></button>` +
|
||||
buckets.innerHTML = `<button class="ctx-item ${!card.bucket_id ? 'text-t-accent' : ''}" data-ctx-bucket=""><i class="ph ph-x text-sm"></i><span class="font-pixel text-[10px]">No bucket</span></button>` +
|
||||
state.buckets.map(b => `<button class="ctx-item ${card.bucket_id == b.id ? 'text-t-accent' : ''}" data-ctx-bucket="${b.id}"><i class="ph ph-folder text-sm"></i><span class="font-pixel text-[10px]">${esc(b.name)}</span></button>`).join('');
|
||||
buckets.classList.add('hidden');
|
||||
|
||||
@@ -97,7 +97,7 @@ export function showContextMenu(x, y, card) {
|
||||
<span class="font-pixel text-[10px]">#${esc(t)}</span>
|
||||
</button>`).join('') +
|
||||
(f && !allTags.includes(f) ? `<button class="ctx-item text-t-dim" data-ctx-tag-new="${f}">
|
||||
<i class="ph ph-plus text-sm"></i><span class="font-pixel text-[10px]">add #${esc(f)}</span>
|
||||
<i class="ph ph-plus text-sm"></i><span class="font-pixel text-[10px]">Add #${esc(f)}</span>
|
||||
</button>` : '');
|
||||
|
||||
tagList.querySelectorAll('[data-ctx-tag]').forEach(btn => btn.addEventListener('click', async e => {
|
||||
@@ -181,7 +181,7 @@ export function showContextMenu(x, y, card) {
|
||||
const hasDate = /^\d{4}-\d{2}-\d{2}/.test(card.title);
|
||||
if (hasDate) {
|
||||
const curDate = card.title.substring(0, 10);
|
||||
const newDate = await showDialog({ title: 'duplicate_to_date', input: 'date', defaultValue: curDate, okLabel: 'duplicate' });
|
||||
const newDate = await showDialog({ title: 'duplicate_to_date', input: 'date', defaultValue: curDate, okLabel: 'Duplicate' });
|
||||
if (!newDate) return;
|
||||
const newTitle = card.title.replace(/^\d{4}-\d{2}-\d{2}/, newDate.trim());
|
||||
await api('api/cards', { method: 'POST', body: { title: newTitle, body: card.body, color: card.color, bucket_id: card.bucket_id, position: -1 } });
|
||||
@@ -193,7 +193,7 @@ export function showContextMenu(x, y, card) {
|
||||
else if (a === 'move-date') {
|
||||
hideContextMenu();
|
||||
const curDate = card.title.substring(0, 10);
|
||||
const newDate = await showDialog({ title: 'move_to_date', input: 'date', defaultValue: curDate, okLabel: 'move' });
|
||||
const newDate = await showDialog({ title: 'move_to_date', input: 'date', defaultValue: curDate, okLabel: 'Move' });
|
||||
if (!newDate || newDate === curDate) return;
|
||||
const newTitle = card.title.replace(/^\d{4}-\d{2}-\d{2}/, newDate.trim());
|
||||
await api(`api/cards/${card.id}`, { method: 'PATCH', body: { title: newTitle } });
|
||||
@@ -209,7 +209,7 @@ export function showContextMenu(x, y, card) {
|
||||
}
|
||||
else if (a === 'due-date') {
|
||||
hideContextMenu();
|
||||
const newDate = await showDialog({ title: card.due_date ? 'change due date' : 'set due date', input: 'date', defaultValue: card.due_date || new Date().toISOString().substring(0, 10), okLabel: card.due_date ? 'update' : 'set' });
|
||||
const newDate = await showDialog({ title: card.due_date ? 'change due date' : 'set due date', input: 'date', defaultValue: card.due_date || new Date().toISOString().substring(0, 10), okLabel: card.due_date ? 'Update' : 'Set' });
|
||||
if (newDate === null) return;
|
||||
await api(`api/cards/${card.id}`, { method: 'PATCH', body: { due_date: newDate || null } });
|
||||
await window._reloadAll();
|
||||
@@ -226,7 +226,7 @@ export function showContextMenu(x, y, card) {
|
||||
else if (a === 'expand-group') { hideContextMenu(); state.activeGroup = card.id; window._renderBoard(); }
|
||||
else if (a === 'go-to-group') { hideContextMenu(); const p = state.cards.find(c => c.id == card.parent_id); if (p) { state.activeGroup = p.id; window._renderBoard(); } }
|
||||
else if (a === 'dissolve') {
|
||||
hideContextMenu(); const ok = await showDialog({ title: 'dissolve_group', message: `Dissolve group "${card.title}"?`, danger: true, okLabel: 'dissolve' });
|
||||
hideContextMenu(); const ok = await showDialog({ title: 'dissolve_group', message: `Dissolve group "${card.title}"?`, danger: true, okLabel: 'Dissolve' });
|
||||
if (ok) { const ch = getChildren(card.id); pushBatch('dissolve', ch.map(c => ({ cardId: c.id, prevData: { parent_id: card.id } }))); await Promise.all(ch.map(c => api(`api/cards/${c.id}`, { method: 'PATCH', body: { parent_id: null } }))); await window._reloadAll(); }
|
||||
}
|
||||
else if (a === 'ungroup') { hideContextMenu(); pushCardEdit('ungroup', card.id, { parent_id: card.parent_id }); await api(`api/cards/${card.id}`, { method: 'PATCH', body: { parent_id: null } }); await window._reloadAll(); }
|
||||
@@ -234,7 +234,7 @@ export function showContextMenu(x, y, card) {
|
||||
hideContextMenu();
|
||||
const needConfirm = localStorage.getItem('hyprcrd-confirm-delete') !== '0';
|
||||
if (needConfirm) {
|
||||
const ok = await showDialog({ title: 'delete_card', message: `Really delete "${card.title}"?`, danger: true, okLabel: 'delete' });
|
||||
const ok = await showDialog({ title: 'delete_card', message: `Really delete "${card.title}"?`, danger: true, okLabel: 'Delete' });
|
||||
if (!ok) return;
|
||||
}
|
||||
const wasSync = !!card.caldav_uid;
|
||||
|
||||
@@ -417,6 +417,7 @@ function renderSidebar(card) {
|
||||
|
||||
if (groupCards && groupCards.length > 1) {
|
||||
sidebar.classList.remove('hidden');
|
||||
sidebar.dataset.groupId = String(groupCards[0].id);
|
||||
$('detail-sidebar-count').textContent = groupCards.length;
|
||||
list.innerHTML = groupCards.map(gc => {
|
||||
const active = gc.id == card.id;
|
||||
@@ -438,6 +439,7 @@ function renderSidebar(card) {
|
||||
});
|
||||
} else {
|
||||
sidebar.classList.add('hidden');
|
||||
delete sidebar.dataset.groupId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,7 +453,7 @@ function updateDoneButton(card) {
|
||||
btn.classList.toggle('text-t-accent', isDone);
|
||||
btn.classList.toggle('border-t-accent', isDone);
|
||||
}
|
||||
if (label) label.textContent = isDone ? 'undone' : 'done';
|
||||
if (label) label.textContent = isDone ? 'Undone' : 'Done';
|
||||
}
|
||||
|
||||
function showContent(card) {
|
||||
@@ -892,7 +894,7 @@ function loadSplitCard(id) {
|
||||
<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>
|
||||
<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', restoreSplitList);
|
||||
@@ -1015,7 +1017,7 @@ $('detail-done')?.addEventListener('click', async () => {
|
||||
$('detail-delete')?.addEventListener('click', async () => {
|
||||
if (!state.viewingCard) return;
|
||||
if (localStorage.getItem('hyprcrd-confirm-delete') !== '0') {
|
||||
const ok = await showDialog({ title: 'delete_card', message: `Really delete "${state.viewingCard.title}"?`, danger: true, okLabel: 'delete' });
|
||||
const ok = await showDialog({ title: 'delete_card', message: `Really delete "${state.viewingCard.title}"?`, danger: true, okLabel: 'Delete' });
|
||||
if (!ok) return;
|
||||
}
|
||||
pushCardDelete('delete', snapshotCard(state.viewingCard));
|
||||
|
||||
@@ -110,7 +110,7 @@ function renderDatepicker() {
|
||||
});
|
||||
}
|
||||
|
||||
export function showDialog({ title, message = '', input = false, placeholder = '', defaultValue = '', danger = false, okLabel = 'ok', cancelLabel = 'cancel' }) {
|
||||
export function showDialog({ title, message = '', input = false, placeholder = '', defaultValue = '', danger = false, okLabel = 'Ok', cancelLabel = 'Cancel' }) {
|
||||
return new Promise(res => {
|
||||
resolve = res;
|
||||
titleEl.textContent = title;
|
||||
|
||||
@@ -116,7 +116,7 @@ const inpDueBtn = $('inp-due-date');
|
||||
|
||||
function setDueDate(date) {
|
||||
if (inpDueValue) inpDueValue.value = date || '';
|
||||
if (inpDueLabel) inpDueLabel.textContent = date || 'no due';
|
||||
if (inpDueLabel) inpDueLabel.textContent = date || 'No due';
|
||||
if (inpDueBtn) {
|
||||
inpDueBtn.classList.toggle('text-t-accent', !!date);
|
||||
inpDueBtn.classList.toggle('border-t-accent', !!date);
|
||||
@@ -131,7 +131,7 @@ inpDueBtn?.addEventListener('click', async () => {
|
||||
title: current ? 'change due date' : 'set due date',
|
||||
input: 'date',
|
||||
defaultValue: current || new Date().toISOString().substring(0, 10),
|
||||
okLabel: current ? 'update' : 'set',
|
||||
okLabel: current ? 'Update' : 'Set',
|
||||
});
|
||||
if (result === null) return;
|
||||
setDueDate(result || '');
|
||||
@@ -147,7 +147,7 @@ function updateDoneToggle() {
|
||||
inpDoneToggle.classList.toggle('border-t-accent', isDone);
|
||||
inpDoneToggle.classList.toggle('text-t-dim', !isDone);
|
||||
}
|
||||
if (inpDoneLabel) inpDoneLabel.textContent = isDone ? 'undone' : 'done';
|
||||
if (inpDoneLabel) inpDoneLabel.textContent = isDone ? 'Undone' : 'Done';
|
||||
}
|
||||
|
||||
inpDoneToggle?.addEventListener('click', () => {
|
||||
|
||||
@@ -156,7 +156,7 @@ export async function openGallery() {
|
||||
e.stopPropagation();
|
||||
const name = btn.dataset.delOrphan;
|
||||
if (localStorage.getItem('hyprcrd-confirm-delete') !== '0') {
|
||||
const ok = await showDialog({ title: 'delete_image', message: `Delete "${name}"?`, danger: true, okLabel: 'delete' });
|
||||
const ok = await showDialog({ title: 'delete_image', message: `Delete "${name}"?`, danger: true, okLabel: 'Delete' });
|
||||
if (!ok) return;
|
||||
}
|
||||
await fetch(`api/uploads/${name}`, { method: 'DELETE', headers: { 'X-CSRF-Token': getCsrf() } });
|
||||
@@ -179,13 +179,13 @@ function openOrphanLightbox(file) {
|
||||
<img src="${file.url}" style="max-width:min(92vw,1400px);max-height:85vh;object-fit:contain;border:1px solid var(--t-border);box-shadow:0 16px 60px rgba(0,0,0,0.55);">
|
||||
<div style="position:fixed;bottom:24px;left:50%;transform:translateX(-50%);display:flex;gap:8px;">
|
||||
<button class="orphan-lb-create flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent hover:text-t-bg transition-colors" style="border-radius:4px;">
|
||||
<i class="ph ph-plus text-sm"></i><span class="font-pixel text-[10px]">new card</span>
|
||||
<i class="ph ph-plus text-sm"></i><span class="font-pixel text-[10px]">New card</span>
|
||||
</button>
|
||||
<button class="orphan-lb-slide flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent hover:text-t-bg transition-colors" style="border-radius:4px;">
|
||||
<i class="ph ph-image-square text-sm"></i><span class="font-pixel text-[10px]">new slide</span>
|
||||
<i class="ph ph-image-square text-sm"></i><span class="font-pixel text-[10px]">New slide</span>
|
||||
</button>
|
||||
<button class="orphan-lb-delete flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-danger text-t-danger hover:bg-t-danger hover:text-t-bg transition-colors" style="border-radius:4px;">
|
||||
<i class="ph ph-trash text-sm"></i><span class="font-pixel text-[10px]">delete</span>
|
||||
<i class="ph ph-trash text-sm"></i><span class="font-pixel text-[10px]">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
<button style="position:fixed;top:12px;right:12px;color:rgba(255,255,255,0.6);font-size:20px;background:none;border:none;cursor:pointer;" class="orphan-lb-close">
|
||||
@@ -213,7 +213,7 @@ function openOrphanLightbox(file) {
|
||||
});
|
||||
|
||||
overlay.querySelector('.orphan-lb-delete').addEventListener('click', async () => {
|
||||
const ok = await showDialog({ title: 'delete_image', message: `Delete "${file.name}"?`, danger: true, okLabel: 'delete' });
|
||||
const ok = await showDialog({ title: 'delete_image', message: `Delete "${file.name}"?`, danger: true, okLabel: 'Delete' });
|
||||
if (!ok) return;
|
||||
await fetch(`api/uploads/${file.name}`, { method: 'DELETE', headers: { 'X-CSRF-Token': getCsrf() } });
|
||||
close();
|
||||
|
||||
+320
-4
@@ -10,7 +10,7 @@ const themeList = $('settings-theme-list');
|
||||
// Render theme list in settings
|
||||
function renderThemes() {
|
||||
if (!themeList) return;
|
||||
const current = localStorage.getItem('hyprcrd-theme') || 'slate';
|
||||
const current = (designPending?.theme) || localStorage.getItem('hyprcrd-theme') || 'slate';
|
||||
themeList.innerHTML = '';
|
||||
for (const [key, t] of Object.entries(window.themes)) {
|
||||
const btn = document.createElement('button');
|
||||
@@ -19,7 +19,16 @@ function renderThemes() {
|
||||
btn.innerHTML = `<span class="w-4 h-4 rounded-full shrink-0" style="background:${t.vars['--t-accent']}"></span>
|
||||
<span class="font-pixel text-[10px] flex-1">${t.label}</span>
|
||||
${active ? '<i class="ph ph-check text-t-accent text-sm"></i>' : ''}`;
|
||||
btn.onclick = () => { window.applyTheme(key); renderThemes(); window._renderBoard?.(); };
|
||||
btn.onclick = () => {
|
||||
window.applyTheme(key);
|
||||
if (designPending) {
|
||||
designPending.theme = key;
|
||||
window.saveDesign(designPending);
|
||||
flashSaved();
|
||||
}
|
||||
renderThemes();
|
||||
window._renderBoard?.();
|
||||
};
|
||||
themeList.appendChild(btn);
|
||||
}
|
||||
}
|
||||
@@ -36,10 +45,311 @@ overlay?.querySelectorAll('.settings-tab').forEach(tab => {
|
||||
if (panel === 'relay') { window._loadRelaySettings?.(); }
|
||||
if (panel === 'notifications') renderNotifSettings();
|
||||
if (panel === 'behavior') initBehaviorSettings();
|
||||
if (panel === 'theme') initDesignSettings();
|
||||
if (panel === 'plugins') window._openPluginSettings?.();
|
||||
if (panel !== 'theme') setPeekUI(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Design (fonts, geometry, borders, spacing, effects, presets) ──
|
||||
let designPending = null;
|
||||
|
||||
const SLIDER_FIELDS = [
|
||||
['setting-radius-card', 'radiusCard', 'val-radius-card'],
|
||||
['setting-radius-button', 'radiusButton', 'val-radius-button'],
|
||||
['setting-radius-input', 'radiusInput', 'val-radius-input'],
|
||||
['setting-radius-modal', 'radiusModal', 'val-radius-modal'],
|
||||
['setting-radius-tag', 'radiusTag', 'val-radius-tag'],
|
||||
['setting-radius-image', 'radiusImage', 'val-radius-image'],
|
||||
['setting-border-card', 'borderCard', 'val-border-card'],
|
||||
['setting-bar-height', 'barHeight', 'val-bar-height'],
|
||||
['setting-pad-card', 'padCard', 'val-pad-card'],
|
||||
['setting-gap-board', 'gapBoard', 'val-gap-board'],
|
||||
['setting-lift-card', 'liftCard', 'val-lift-card'],
|
||||
['setting-blur-overlay', 'blurOverlay', 'val-blur-overlay'],
|
||||
];
|
||||
const SELECT_FIELDS = [
|
||||
['setting-font-headline', 'fontHeadline'],
|
||||
['setting-font-content', 'fontContent'],
|
||||
['setting-font-ui', 'fontUi'],
|
||||
['setting-border-style', 'borderStyle'],
|
||||
['setting-shadow-card', 'shadowCard'],
|
||||
['setting-shadow-modal', 'shadowModal'],
|
||||
['setting-shadow-ui', 'shadowUi'],
|
||||
['setting-anim-speed', 'animSpeed'],
|
||||
];
|
||||
|
||||
function updatePreview() {
|
||||
const preview = $('theme-preview');
|
||||
if (!preview || !designPending) return;
|
||||
const d = designPending, fonts = window.themeFonts, shadows = window.designShadows;
|
||||
const set = (k, v) => preview.style.setProperty(k, v);
|
||||
set('--tp-font-headline', fonts[d.fontHeadline] || fonts.pixel);
|
||||
set('--tp-font-content', fonts[d.fontContent] || fonts.mono);
|
||||
set('--tp-font-ui', fonts[d.fontUi] || fonts.pixel);
|
||||
set('--tp-radius-card', d.radiusCard + 'px');
|
||||
set('--tp-radius-button', d.radiusButton + 'px');
|
||||
set('--tp-radius-input', d.radiusInput + 'px');
|
||||
set('--tp-radius-tag', d.radiusTag + 'px');
|
||||
set('--tp-border-card', d.borderCard + 'px');
|
||||
set('--tp-border-style', d.borderStyle);
|
||||
set('--tp-bar-height', d.barHeight + 'px');
|
||||
set('--tp-pad-card', d.padCard + 'px');
|
||||
set('--tp-shadow-card', shadows[d.shadowCard] || 'none');
|
||||
const scan = $('theme-preview-scan');
|
||||
if (scan) scan.style.display = String(d.scanlines) === '1' ? '' : 'none';
|
||||
}
|
||||
|
||||
function fillFontSelect(id, value) {
|
||||
const el = $(id);
|
||||
if (!el) return;
|
||||
el.innerHTML = '';
|
||||
for (const [key, label] of Object.entries(window.themeFontLabels)) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = key; opt.textContent = label;
|
||||
if (key === value) opt.selected = true;
|
||||
el.appendChild(opt);
|
||||
}
|
||||
}
|
||||
|
||||
function syncControlsFromPending() {
|
||||
if (!designPending) return;
|
||||
fillFontSelect('setting-font-headline', designPending.fontHeadline);
|
||||
fillFontSelect('setting-font-content', designPending.fontContent);
|
||||
fillFontSelect('setting-font-ui', designPending.fontUi);
|
||||
for (const [id, key] of SELECT_FIELDS) {
|
||||
if (id.startsWith('setting-font-')) continue;
|
||||
const el = $(id);
|
||||
if (el) el.value = String(designPending[key] ?? '');
|
||||
}
|
||||
for (const [id, key, valId] of SLIDER_FIELDS) {
|
||||
const v = String(designPending[key] ?? 0);
|
||||
const el = $(id);
|
||||
if (el) el.value = v;
|
||||
const valEl = $(valId);
|
||||
if (valEl) valEl.textContent = v;
|
||||
}
|
||||
const scanEl = $('setting-scanlines');
|
||||
if (scanEl) scanEl.classList.toggle('on', String(designPending.scanlines) === '1');
|
||||
}
|
||||
|
||||
function commitDesign() {
|
||||
updatePreview();
|
||||
window.applyDesign(designPending);
|
||||
window.saveDesign(designPending);
|
||||
flashSaved();
|
||||
}
|
||||
|
||||
let saveFlashTimer = null;
|
||||
function flashSaved() {
|
||||
const status = $('design-status');
|
||||
if (!status) return;
|
||||
status.textContent = 'saved';
|
||||
status.style.color = 'var(--t-accent)';
|
||||
clearTimeout(saveFlashTimer);
|
||||
saveFlashTimer = setTimeout(() => {
|
||||
status.textContent = 'auto-saving';
|
||||
status.style.color = 'var(--t-dim)';
|
||||
}, 700);
|
||||
}
|
||||
|
||||
let designWired = false;
|
||||
|
||||
function initDesignSettings() {
|
||||
designPending = window.loadDesign();
|
||||
syncControlsFromPending();
|
||||
updatePreview();
|
||||
refreshPresetSelect();
|
||||
|
||||
const status = $('design-status');
|
||||
if (status) { status.textContent = 'auto-saving'; status.style.color = 'var(--t-dim)'; }
|
||||
|
||||
if (designWired) return;
|
||||
designWired = true;
|
||||
|
||||
// Scanlines toggle
|
||||
const scanEl = $('setting-scanlines');
|
||||
const scanRow = scanEl?.closest('.toggle-row');
|
||||
if (scanRow) scanRow.onclick = () => {
|
||||
const nowOn = !scanEl.classList.contains('on');
|
||||
scanEl.classList.toggle('on', nowOn);
|
||||
designPending.scanlines = nowOn ? '1' : '0';
|
||||
commitDesign();
|
||||
};
|
||||
|
||||
// Selects
|
||||
for (const [id, key] of SELECT_FIELDS) {
|
||||
const el = $(id);
|
||||
if (!el) continue;
|
||||
el.addEventListener('change', () => {
|
||||
designPending[key] = el.value;
|
||||
commitDesign();
|
||||
});
|
||||
}
|
||||
|
||||
// Sliders
|
||||
for (const [id, key, valId] of SLIDER_FIELDS) {
|
||||
const el = $(id);
|
||||
if (!el) continue;
|
||||
el.addEventListener('input', () => {
|
||||
const num = Number(el.value);
|
||||
designPending[key] = num;
|
||||
const v = $(valId);
|
||||
if (v) v.textContent = num;
|
||||
commitDesign();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$('design-reset')?.addEventListener('click', () => {
|
||||
designPending = { ...window.designDefaults };
|
||||
syncControlsFromPending();
|
||||
commitDesign();
|
||||
renderThemes();
|
||||
});
|
||||
|
||||
// ── Peek toggle (hide background dim so the real board is visible behind the popup) ──
|
||||
function setPeekUI(peeking) {
|
||||
document.body.classList.toggle('settings-peeking', peeking);
|
||||
const sw = $('design-peek-toggle');
|
||||
if (sw) sw.classList.toggle('on', peeking);
|
||||
}
|
||||
const peekRow = $('design-peek-row');
|
||||
if (peekRow) peekRow.onclick = () => {
|
||||
setPeekUI(!document.body.classList.contains('settings-peeking'));
|
||||
};
|
||||
|
||||
// ── Presets ──
|
||||
|
||||
function loadUserPresets() {
|
||||
try { return JSON.parse(localStorage.getItem('hyprcrd-design-presets') || '{}'); }
|
||||
catch { return {}; }
|
||||
}
|
||||
function saveUserPresets(p) {
|
||||
localStorage.setItem('hyprcrd-design-presets', JSON.stringify(p));
|
||||
}
|
||||
|
||||
function refreshPresetSelect() {
|
||||
const sel = $('design-preset-select');
|
||||
if (!sel) return;
|
||||
const cur = sel.value;
|
||||
sel.innerHTML = '';
|
||||
const builtinGroup = document.createElement('optgroup');
|
||||
builtinGroup.label = 'built-in';
|
||||
for (const name of Object.keys(window.builtinPresets || {})) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = 'builtin:' + name; opt.textContent = name;
|
||||
builtinGroup.appendChild(opt);
|
||||
}
|
||||
sel.appendChild(builtinGroup);
|
||||
const user = loadUserPresets();
|
||||
const names = Object.keys(user);
|
||||
if (names.length) {
|
||||
const userGroup = document.createElement('optgroup');
|
||||
userGroup.label = 'custom';
|
||||
for (const name of names) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = 'user:' + name; opt.textContent = name;
|
||||
userGroup.appendChild(opt);
|
||||
}
|
||||
sel.appendChild(userGroup);
|
||||
}
|
||||
if (cur) sel.value = cur;
|
||||
}
|
||||
|
||||
function getSelectedPreset() {
|
||||
const sel = $('design-preset-select');
|
||||
if (!sel?.value) return null;
|
||||
const [kind, ...rest] = sel.value.split(':');
|
||||
const name = rest.join(':');
|
||||
if (kind === 'builtin') return window.builtinPresets?.[name] ? { ...window.builtinPresets[name] } : null;
|
||||
if (kind === 'user') return loadUserPresets()[name] ? { ...loadUserPresets()[name] } : null;
|
||||
return null;
|
||||
}
|
||||
|
||||
function loadSelectedPreset() {
|
||||
const cfg = getSelectedPreset();
|
||||
if (!cfg) return;
|
||||
designPending = { ...window.designDefaults, ...cfg };
|
||||
syncControlsFromPending();
|
||||
commitDesign();
|
||||
renderThemes();
|
||||
}
|
||||
$('design-preset-select')?.addEventListener('change', loadSelectedPreset);
|
||||
|
||||
$('design-preset-save')?.addEventListener('click', () => {
|
||||
const nameEl = $('design-preset-name');
|
||||
const name = (nameEl?.value || '').trim();
|
||||
if (!name) { nameEl?.focus(); return; }
|
||||
const presets = loadUserPresets();
|
||||
presets[name] = { ...designPending };
|
||||
saveUserPresets(presets);
|
||||
if (nameEl) nameEl.value = '';
|
||||
refreshPresetSelect();
|
||||
const sel = $('design-preset-select');
|
||||
if (sel) sel.value = 'user:' + name;
|
||||
const status = $('design-status');
|
||||
if (status) { status.textContent = `preset "${name}" saved`; status.style.color = 'var(--t-accent)'; }
|
||||
});
|
||||
|
||||
$('design-preset-delete')?.addEventListener('click', () => {
|
||||
const sel = $('design-preset-select');
|
||||
if (!sel?.value?.startsWith('user:')) return;
|
||||
const name = sel.value.slice('user:'.length);
|
||||
const presets = loadUserPresets();
|
||||
delete presets[name];
|
||||
saveUserPresets(presets);
|
||||
refreshPresetSelect();
|
||||
});
|
||||
|
||||
$('design-preset-export')?.addEventListener('click', async () => {
|
||||
const cfg = designPending || window.loadDesign();
|
||||
const json = JSON.stringify(cfg, null, 2);
|
||||
// Best-effort clipboard (silently fails on non-secure contexts)
|
||||
let copied = false;
|
||||
try {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(json);
|
||||
copied = true;
|
||||
}
|
||||
} catch {}
|
||||
// Always also trigger a file download — works regardless of clipboard support
|
||||
try {
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'hyprcrd-design.json';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
} catch (e) {
|
||||
console.error('design export failed', e);
|
||||
}
|
||||
const status = $('design-status');
|
||||
if (status) {
|
||||
status.textContent = copied ? 'downloaded + copied to clipboard' : 'downloaded hyprcrd-design.json';
|
||||
status.style.color = 'var(--t-accent)';
|
||||
}
|
||||
});
|
||||
|
||||
$('design-preset-import')?.addEventListener('click', async () => {
|
||||
const { showDialog } = await import('./dialog.js');
|
||||
const json = await showDialog({ title: 'Import design JSON', input: true, placeholder: '{"radiusCard":8,...}', okLabel: 'Import' });
|
||||
if (!json) return;
|
||||
try {
|
||||
const cfg = JSON.parse(json);
|
||||
designPending = { ...window.designDefaults, ...cfg };
|
||||
syncControlsFromPending();
|
||||
commitDesign();
|
||||
renderThemes();
|
||||
} catch {
|
||||
const status = $('design-status');
|
||||
if (status) { status.textContent = 'invalid JSON'; status.style.color = 'var(--t-danger)'; }
|
||||
}
|
||||
});
|
||||
|
||||
// Open/Close
|
||||
function updateStats() {
|
||||
const sc = $('stats-cards'); if (sc) sc.textContent = state.cards?.length || 0;
|
||||
@@ -77,12 +387,17 @@ async function loadVaultStatus() {
|
||||
|
||||
$('settings-btn')?.addEventListener('click', () => {
|
||||
renderThemes();
|
||||
initDesignSettings();
|
||||
updateStats();
|
||||
loadVaultStatus();
|
||||
overlay?.classList.add('open');
|
||||
});
|
||||
$('settings-close')?.addEventListener('click', () => overlay?.classList.remove('open'));
|
||||
overlay?.addEventListener('click', e => { if (e.target === overlay) overlay?.classList.remove('open'); });
|
||||
function closeSettings() {
|
||||
overlay?.classList.remove('open');
|
||||
setPeekUI(false);
|
||||
}
|
||||
$('settings-close')?.addEventListener('click', closeSettings);
|
||||
overlay?.addEventListener('click', e => { if (e.target === overlay) closeSettings(); });
|
||||
|
||||
// Password change
|
||||
$('settings-pw-save')?.addEventListener('click', async () => {
|
||||
@@ -123,6 +438,7 @@ function initBehaviorSettings() {
|
||||
'setting-card-width': { key: 'hyprcrd-card-width', def: '280' },
|
||||
'setting-body-lines': { key: 'hyprcrd-body-lines', def: '5' },
|
||||
'setting-click-action': { key: 'hyprcrd-click-action', def: 'preview' },
|
||||
'setting-esc-action': { key: 'hyprcrd-esc-action', def: 'close' },
|
||||
};
|
||||
const toggles = {
|
||||
'setting-current-include-synced': { key: 'hyprcrd-current-include-synced', def: '0' },
|
||||
|
||||
Reference in new issue
Block a user