fixes + theme

This commit is contained in:
th committed 2026-04-11 13:01:27 +02:00
1 parent 5af49655d6
commit 22f73ab3b9
25 files changed
+1004 -206

No files matched your search

+3 -1
View File
@@ -1,3 +1,5 @@
{ {
"disabled": [] "disabled": [
"logseq-import"
]
} }
+184 -7
View File
@@ -1,5 +1,181 @@
body { font-family: 'JetBrains Mono', monospace; } body { font-family: var(--t-font-content, 'JetBrains Mono', monospace); }
.font-pixel { font-family: var(--t-font-pixel, 'Silkscreen', cursive) !important; } .font-pixel { font-family: var(--t-font-ui, var(--t-font-pixel, 'Silkscreen', cursive)) !important; }
.brand { font-family: 'Silkscreen', cursive !important; }
body.no-scanlines::after { display: none !important; }
#dialog-msg { white-space: pre-line; word-break: break-word; }
/* ── User-customizable design tokens ── */
:root {
--ui-radius-card: 0px;
--ui-radius-button: 0px;
--ui-radius-input: 0px;
--ui-radius-modal: 4px;
--ui-radius-tag: 2px;
--ui-radius-image: 4px;
--ui-border-card: 1px;
--ui-border-style: solid;
--ui-bar-height: 3px;
--ui-pad-card: 16px;
--ui-gap-board: 0px;
--ui-shadow-card: none;
--ui-shadow-modal: 0 25px 50px -12px rgba(0,0,0,0.35);
--ui-shadow-header: 0 4px 20px -4px rgba(0,0,0,0.3);
--ui-shadow-footer: 0 -4px 20px -4px rgba(0,0,0,0.3);
--ui-lift-card: 0px;
--ui-blur-overlay: 0px;
}
#main-header { box-shadow: var(--ui-shadow-header); }
#main-footer { box-shadow: var(--ui-shadow-footer); }
/* Cards: geometry & shadow driven by design tokens */
.board-card {
border-radius: var(--ui-radius-card);
border-width: var(--ui-border-card);
border-style: var(--ui-border-style);
box-shadow: var(--ui-shadow-card);
transition: border-color 0.12s, transform 0.15s, box-shadow 0.2s;
overflow: hidden; /* clip card-bar (and other inner elements) to the rounded corners */
}
.board-card:hover { transform: translateY(calc(0px - var(--ui-lift-card))); }
.board-card > .p-4 { padding: var(--ui-pad-card); }
.board-card .card-tags { flex-shrink: 0; min-height: 18px; }
#board { gap: var(--ui-gap-board); }
/* Bordered action buttons */
button[class*="border-t-border"]:not(.toolbar-btn):not(.settings-tab):not(.bucket-tab):not(.ctx-item):not(.tp-btn),
button[class*="border-t-accent"]:not(.toolbar-btn):not(.settings-tab):not(.bucket-tab):not(.ctx-item):not(.tp-btn),
button[class*="border-t-danger"]:not(.toolbar-btn):not(.settings-tab):not(.bucket-tab):not(.ctx-item):not(.tp-btn),
a[class*="border-t-border"],
a[class*="border-t-accent"] {
border-radius: var(--ui-radius-button);
}
/* Inputs / selects / textareas */
input:not([type="checkbox"]):not([type="radio"]):not([type="range"])[class*="border-t-border"],
input:not([type="checkbox"]):not([type="radio"]):not([type="range"])[class*="border-t-accent"],
textarea[class*="border-t-border"],
textarea[class*="border-t-accent"],
select[class*="border-t-border"],
select[class*="border-t-accent"] {
border-radius: var(--ui-radius-input);
}
/* Tag chips */
.tag-chip { border-radius: var(--ui-radius-tag); }
/* Prose images & card cover */
.prose img { border-radius: var(--ui-radius-image); }
.board-card.card-has-cover { border-radius: var(--ui-radius-card); }
/* Modal containers (the inner box of each overlay) */
#modal > div,
#detail > div,
#settings-overlay > div,
#help > div,
#search > div,
#tasks > div,
#calendar > div,
#gallery > div,
#dialog > div {
border-radius: var(--ui-radius-modal);
box-shadow: var(--ui-shadow-modal);
}
/* Overlay backdrop blur (applies to anything that's already an .overlay) */
.overlay { backdrop-filter: blur(var(--ui-blur-overlay)); -webkit-backdrop-filter: blur(var(--ui-blur-overlay)); }
/* Animation speed (toggled via body class from applyDesign) */
body.anim-off *, body.anim-off *::before, body.anim-off *::after {
transition: none !important;
animation: none !important;
}
body.anim-fast *, body.anim-fast *::before, body.anim-fast *::after {
transition-duration: 50ms !important;
animation-duration: 100ms !important;
}
body.anim-slow *, body.anim-slow *::before, body.anim-slow *::after {
transition-duration: 400ms !important;
animation-duration: 600ms !important;
}
/* ── Theme preview (settings) ── */
.theme-preview {
position: relative; padding: 14px; overflow: hidden;
background: var(--t-bg); border: 1px solid var(--t-border); border-radius: 3px;
}
.theme-preview .tp-scan { position: absolute; inset: 0; pointer-events: none; background: repeating-linear-gradient(0deg, transparent, transparent 2px, var(--t-scanline, rgba(0,0,0,0.04)) 2px, var(--t-scanline, rgba(0,0,0,0.04)) 4px); }
.theme-preview .tp-row { display: flex; gap: 12px; align-items: stretch; position: relative; z-index: 1; }
.theme-preview .tp-card {
flex: 1; min-width: 0;
background: var(--t-surface);
border: var(--tp-border-card, 1px) var(--tp-border-style, solid) var(--t-border);
border-radius: var(--tp-radius-card, 0px);
box-shadow: var(--tp-shadow-card, none);
overflow: hidden;
}
.theme-preview .tp-card-bar { height: var(--tp-bar-height, 3px); background: var(--t-accent); }
.theme-preview .tp-card-body { padding: var(--tp-pad-card, 14px); }
.theme-preview .tp-headline { font-family: var(--tp-font-headline); color: var(--t-bright); font-size: 16px; font-weight: 700; margin-bottom: 4px; }
.theme-preview .tp-content { font-family: var(--tp-font-content); color: var(--t-text); font-size: 11px; line-height: 1.55; margin-bottom: 8px; }
.theme-preview .tp-meta { display: flex; gap: 6px; align-items: center; }
.theme-preview .tp-tag {
font-family: var(--tp-font-ui); font-size: 9px;
color: var(--t-accent); background: color-mix(in srgb, var(--t-accent) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--t-accent) 25%, transparent);
border-radius: var(--tp-radius-tag, 2px); padding: 1px 6px;
}
.theme-preview .tp-ui { font-family: var(--tp-font-ui); font-size: 9px; color: var(--t-dim); text-transform: uppercase; letter-spacing: 0.05em; }
.theme-preview .tp-side { display: flex; flex-direction: column; gap: 6px; min-width: 96px; }
.theme-preview .tp-btn {
font-family: var(--tp-font-ui); font-size: 10px;
color: var(--t-accent); border: 1px solid var(--t-accent);
padding: 4px 10px; border-radius: var(--tp-radius-button, 0px);
background: transparent; cursor: pointer;
}
.theme-preview .tp-input {
font-family: var(--tp-font-ui); font-size: 10px;
color: var(--t-text); background: var(--t-bg);
border: 1px solid var(--t-border);
padding: 4px 8px; border-radius: var(--tp-radius-input, 0px);
outline: none; width: 100%;
}
/* Range slider styling for design panel */
.design-slider {
flex: 1; height: 4px;
-webkit-appearance: none; appearance: none;
background: var(--t-border); border-radius: 2px; outline: none; cursor: pointer;
}
.design-slider::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 12px; height: 12px; border-radius: 50%;
background: var(--t-accent); cursor: pointer; border: none;
}
.design-slider::-moz-range-thumb {
width: 12px; height: 12px; border-radius: 50%;
background: var(--t-accent); cursor: pointer; border: none;
}
.design-row { display: flex; align-items: center; gap: 10px; padding: 2px 0; }
.design-row > label { flex: 0 0 88px; font-size: 11px; color: var(--t-muted); }
.design-row > select { width: 110px; }
.design-row > .design-val { font-family: var(--t-font-pixel); font-size: 9px; color: var(--t-dim); width: 36px; text-align: right; flex-shrink: 0; }
.design-section-label { font-family: var(--t-font-pixel); font-size: 9px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--t-dim); margin: 12px 0 4px; }
.design-section-label:first-child { margin-top: 0; }
#design-advanced > summary { list-style: none; }
#design-advanced > summary::-webkit-details-marker { display: none; }
#design-advanced > summary:hover { color: var(--t-bright); }
#design-advanced .design-advanced-caret { transition: transform 0.15s; display: inline-block; }
#design-advanced[open] .design-advanced-caret { transform: rotate(90deg); }
/* Settings peek: hide only the dim background so the user can see the real UI behind, while the settings popup stays open and interactive */
body.settings-peeking #settings-overlay {
background: transparent !important;
pointer-events: none !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
body.settings-peeking #settings-overlay > div { pointer-events: auto; }
/* Toolbar toggles */ /* Toolbar toggles */
.toolbar-toggle { .toolbar-toggle {
@@ -118,7 +294,8 @@ body::after {
.card-spinner { animation: spin 0.8s linear infinite; } .card-spinner { animation: spin 0.8s linear infinite; }
/* Cards */ /* Cards */
.card-bar { image-rendering: pixelated; height: 3px; } .card-bar { image-rendering: pixelated; height: var(--ui-bar-height, 3px); }
.board-card > .card-bar { border-top-left-radius: var(--ui-radius-card); border-top-right-radius: var(--ui-radius-card); }
.board-card { min-height: var(--card-min-h, 180px); } .board-card { min-height: var(--card-min-h, 180px); }
.card-layer { position: relative; z-index: 1; } .card-layer { position: relative; z-index: 1; }
.card-has-cover { .card-has-cover {
@@ -163,8 +340,8 @@ body::after {
.card-group::after { top: 8px; opacity: 0.3; left: 8px; right: 8px; } .card-group::after { top: 8px; opacity: 0.3; left: 8px; right: 8px; }
.card-group-badge { .card-group-badge {
position: absolute; position: absolute;
top: -6px; top: 6px;
right: -6px; right: 6px;
min-width: 20px; min-width: 20px;
height: 20px; height: 20px;
padding: 0 6px; padding: 0 6px;
@@ -212,7 +389,7 @@ body::after {
box-shadow: 0 4px 16px rgba(0,0,0,0.4); box-shadow: 0 4px 16px rgba(0,0,0,0.4);
z-index: 10; z-index: 10;
} }
.card-glow:hover { box-shadow: 0 0 16px -3px var(--glow-color, rgba(56, 189, 248, 0.15)); } .card-glow:hover { box-shadow: var(--ui-shadow-card), 0 0 16px -3px var(--glow-color, rgba(56, 189, 248, 0.15)); }
.card-selected { outline: 2px solid var(--t-accent) !important; outline-offset: -2px; } .card-selected { outline: 2px solid var(--t-accent) !important; outline-offset: -2px; }
.card-select-check { .card-select-check {
position: absolute; top: 6px; left: 6px; width: 18px; height: 18px; position: absolute; top: 6px; left: 6px; width: 18px; height: 18px;
@@ -541,7 +718,7 @@ body.detail-image-fs-open { overflow: hidden; }
/* Prose (markdown) */ /* Prose (markdown) */
.prose { line-height: 1.75; } .prose { line-height: 1.75; }
.prose h1, .prose h2, .prose h3, .prose h4 { color: var(--t-bright); font-family: var(--t-font-pixel, 'Silkscreen', cursive); margin: 1.5em 0 0.6em; font-weight: 700; } .prose h1, .prose h2, .prose h3, .prose h4 { color: var(--t-bright); font-family: var(--t-font-headline, var(--t-font-pixel, 'Silkscreen', cursive)); margin: 1.5em 0 0.6em; font-weight: 700; }
.prose h1 { font-size: 1.8em; padding-bottom: 0.3em; border-bottom: 1px solid var(--t-border); } .prose h1 { font-size: 1.8em; padding-bottom: 0.3em; border-bottom: 1px solid var(--t-border); }
.prose h2 { font-size: 1.4em; } .prose h2 { font-size: 1.4em; }
.prose h3 { font-size: 1.15em; } .prose h3 { font-size: 1.15em; }
+289 -74
View File
@@ -1020,8 +1020,10 @@ if (str_starts_with($route, '/api/')) {
phosphor: { label:'Phosphor', vars:{ '--t-bg':'#0a0f0a','--t-surface':'#0f1a0f','--t-raised':'#142214','--t-border':'#1a3a1a','--t-dim':'#2d5a2d','--t-muted':'#4a8a4a','--t-text':'#7bc67b','--t-bright':'#b8f0b8','--t-accent':'#4ade80','--t-accent2':'#22d3ee','--t-danger':'#f87171','--t-scanline':'rgba(0,0,0,0.1)','--t-overlay':'rgba(5,10,5,0.85)','--t-font-pixel':'"Silkscreen", cursive','--t-cover-gradient':'linear-gradient(180deg, rgba(10,15,10,0.22) 0%, rgba(10,15,10,0.7) 62%, rgba(10,15,10,0.86) 100%)' }}, phosphor: { label:'Phosphor', vars:{ '--t-bg':'#0a0f0a','--t-surface':'#0f1a0f','--t-raised':'#142214','--t-border':'#1a3a1a','--t-dim':'#2d5a2d','--t-muted':'#4a8a4a','--t-text':'#7bc67b','--t-bright':'#b8f0b8','--t-accent':'#4ade80','--t-accent2':'#22d3ee','--t-danger':'#f87171','--t-scanline':'rgba(0,0,0,0.1)','--t-overlay':'rgba(5,10,5,0.85)','--t-font-pixel':'"Silkscreen", cursive','--t-cover-gradient':'linear-gradient(180deg, rgba(10,15,10,0.22) 0%, rgba(10,15,10,0.7) 62%, rgba(10,15,10,0.86) 100%)' }},
midnight: { label:'Midnight', vars:{ '--t-bg':'#0f172a','--t-surface':'#162033','--t-raised':'#1c2940','--t-border':'#263650','--t-dim':'#475882','--t-muted':'#7088b0','--t-text':'#a8bdd8','--t-bright':'#e2e8f0','--t-accent':'#a78bfa','--t-accent2':'#f472b6','--t-danger':'#fb7185','--t-scanline':'rgba(0,0,0,0.06)','--t-overlay':'rgba(10,15,30,0.8)','--t-font-pixel':'"Silkscreen", cursive','--t-cover-gradient':'linear-gradient(180deg, rgba(15,23,42,0.22) 0%, rgba(15,23,42,0.7) 62%, rgba(15,23,42,0.86) 100%)' }}, midnight: { label:'Midnight', vars:{ '--t-bg':'#0f172a','--t-surface':'#162033','--t-raised':'#1c2940','--t-border':'#263650','--t-dim':'#475882','--t-muted':'#7088b0','--t-text':'#a8bdd8','--t-bright':'#e2e8f0','--t-accent':'#a78bfa','--t-accent2':'#f472b6','--t-danger':'#fb7185','--t-scanline':'rgba(0,0,0,0.06)','--t-overlay':'rgba(10,15,30,0.8)','--t-font-pixel':'"Silkscreen", cursive','--t-cover-gradient':'linear-gradient(180deg, rgba(15,23,42,0.22) 0%, rgba(15,23,42,0.7) 62%, rgba(15,23,42,0.86) 100%)' }},
amber: { label:'Amber', vars:{ '--t-bg':'#1a1408','--t-surface':'#231c0e','--t-raised':'#2c2414','--t-border':'#3d3520','--t-dim':'#6b5c38','--t-muted':'#a08c5a','--t-text':'#d4c088','--t-bright':'#fbe8b0','--t-accent':'#fbbf24','--t-accent2':'#fb923c','--t-danger':'#ef4444','--t-scanline':'rgba(0,0,0,0.08)','--t-overlay':'rgba(20,16,5,0.8)','--t-font-pixel':'"Silkscreen", cursive','--t-cover-gradient':'linear-gradient(180deg, rgba(26,20,8,0.22) 0%, rgba(26,20,8,0.7) 62%, rgba(26,20,8,0.86) 100%)' }}, amber: { label:'Amber', vars:{ '--t-bg':'#1a1408','--t-surface':'#231c0e','--t-raised':'#2c2414','--t-border':'#3d3520','--t-dim':'#6b5c38','--t-muted':'#a08c5a','--t-text':'#d4c088','--t-bright':'#fbe8b0','--t-accent':'#fbbf24','--t-accent2':'#fb923c','--t-danger':'#ef4444','--t-scanline':'rgba(0,0,0,0.08)','--t-overlay':'rgba(20,16,5,0.8)','--t-font-pixel':'"Silkscreen", cursive','--t-cover-gradient':'linear-gradient(180deg, rgba(26,20,8,0.22) 0%, rgba(26,20,8,0.7) 62%, rgba(26,20,8,0.86) 100%)' }},
void: { label:'Void', vars:{ '--t-bg':'#000000','--t-surface':'#0a0a0a','--t-raised':'#141414','--t-border':'#222222','--t-dim':'#444444','--t-muted':'#666666','--t-text':'#a0a0a0','--t-bright':'#e0e0e0','--t-accent':'#ffffff','--t-accent2':'#888888','--t-danger':'#ff4444','--t-scanline':'rgba(0,0,0,0)','--t-overlay':'rgba(0,0,0,0.85)','--t-font-pixel':'"Silkscreen", cursive','--t-cover-gradient':'linear-gradient(180deg, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.75) 62%, rgba(0,0,0,0.9) 100%)' }}, void: { label:'Void', vars:{ '--t-bg':'#000000','--t-surface':'#0a0a0a','--t-raised':'#141414','--t-border':'#222222','--t-dim':'#444444','--t-muted':'#666666','--t-text':'#a0a0a0','--t-bright':'#e0e0e0','--t-accent':'#ffffff','--t-accent2':'#888888','--t-danger':'#ff4444','--t-scanline':'rgba(255,255,255,0.04)','--t-overlay':'rgba(0,0,0,0.85)','--t-font-pixel':'"Silkscreen", cursive','--t-cover-gradient':'linear-gradient(180deg, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.75) 62%, rgba(0,0,0,0.9) 100%)' }},
paper: { label:'Paper', vars:{ '--t-bg':'#f4f4f5','--t-surface':'#ffffff','--t-raised':'#e8e8eb','--t-border':'#d4d4d8','--t-dim':'#a1a1aa','--t-muted':'#71717a','--t-text':'#3f3f46','--t-bright':'#18181b','--t-accent':'#2563eb','--t-accent2':'#7c3aed','--t-danger':'#dc2626','--t-scanline':'rgba(0,0,0,0)','--t-overlay':'rgba(244,244,245,0.8)','--t-font-pixel':'"JetBrains Mono", monospace','--t-cover-gradient':'linear-gradient(180deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.75) 40%, rgba(255,255,255,0.3) 100%)' }}, paper: { label:'Bright Mind', vars:{ '--t-bg':'#f4f4f5','--t-surface':'#ffffff','--t-raised':'#e8e8eb','--t-border':'#d4d4d8','--t-dim':'#a1a1aa','--t-muted':'#71717a','--t-text':'#3f3f46','--t-bright':'#18181b','--t-accent':'#2563eb','--t-accent2':'#7c3aed','--t-danger':'#dc2626','--t-scanline':'rgba(0,0,0,0.05)','--t-overlay':'rgba(244,244,245,0.8)','--t-font-pixel':'"JetBrains Mono", monospace','--t-cover-gradient':'linear-gradient(180deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.75) 40%, rgba(255,255,255,0.3) 100%)' }},
sand: { label:'Sand', vars:{ '--t-bg':'#d6c5a3','--t-surface':'#e2d5b8','--t-raised':'#ede2c5','--t-border':'#a89572','--t-dim':'#8a7654','--t-muted':'#5d4f36','--t-text':'#3a3120','--t-bright':'#1a1408','--t-accent':'#b91c1c','--t-accent2':'#15803d','--t-danger':'#7c2d12','--t-scanline':'rgba(60,40,20,0.06)','--t-overlay':'rgba(60,50,30,0.75)','--t-font-pixel':'"Silkscreen", cursive','--t-cover-gradient':'linear-gradient(180deg, rgba(214,196,160,0.22) 0%, rgba(214,196,160,0.7) 62%, rgba(214,196,160,0.86) 100%)' }},
stone: { label:'Stone', vars:{ '--t-bg':'#94a3b8','--t-surface':'#a8b6c8','--t-raised':'#bcc7d5','--t-border':'#64748b','--t-dim':'#475569','--t-muted':'#334155','--t-text':'#1e293b','--t-bright':'#0f172a','--t-accent':'#1d4ed8','--t-accent2':'#7e22ce','--t-danger':'#991b1b','--t-scanline':'rgba(15,23,42,0.06)','--t-overlay':'rgba(50,60,80,0.75)','--t-font-pixel':'"Silkscreen", cursive','--t-cover-gradient':'linear-gradient(180deg, rgba(148,163,184,0.22) 0%, rgba(148,163,184,0.7) 62%, rgba(148,163,184,0.86) 100%)' }},
}; };
window.applyTheme = function(name) { window.applyTheme = function(name) {
const t = themes[name] || themes.slate; const t = themes[name] || themes.slate;
@@ -1033,38 +1035,153 @@ if (str_starts_with($route, '/api/')) {
}); });
} }
applyTheme(localStorage.getItem('hyprcrd-theme') || 'slate'); applyTheme(localStorage.getItem('hyprcrd-theme') || 'slate');
window.themeFonts = {
pixel: '"Silkscreen", cursive',
mono: '"JetBrains Mono", monospace',
sans: 'system-ui, -apple-system, sans-serif',
serif: 'Georgia, "Times New Roman", serif',
};
window.themeFontLabels = { pixel: 'Pixel (Silkscreen)', mono: 'Mono (JetBrains)', sans: 'Sans', serif: 'Serif' };
window.designShadows = {
none: '0 0 0 0 transparent',
soft: '0 2px 8px rgba(0,0,0,0.18)',
medium: '0 6px 20px -4px rgba(0,0,0,0.35)',
heavy: '0 12px 40px -8px rgba(0,0,0,0.55)',
};
// Header drops down, footer drops up — same intensity scale.
window.designUiShadows = {
header: {
none: '0 0 0 0 transparent',
soft: '0 2px 8px -2px rgba(0,0,0,0.18)',
medium: '0 4px 20px -4px rgba(0,0,0,0.3)',
heavy: '0 8px 30px -4px rgba(0,0,0,0.5)',
},
footer: {
none: '0 0 0 0 transparent',
soft: '0 -2px 8px -2px rgba(0,0,0,0.18)',
medium: '0 -4px 20px -4px rgba(0,0,0,0.3)',
heavy: '0 -8px 30px -4px rgba(0,0,0,0.5)',
},
};
window.designDefaults = {
// color theme
theme: 'slate',
// typography
scanlines: '1',
fontHeadline: 'pixel', fontContent: 'mono', fontUi: 'pixel',
// geometry
radiusCard: 0, radiusButton: 0, radiusInput: 0, radiusModal: 0, radiusTag: 0, radiusImage: 0,
// borders
borderCard: 1, borderStyle: 'solid', barHeight: 3,
// spacing
padCard: 10, gapBoard: 17,
// effects
shadowCard: 'none', shadowModal: 'medium', shadowUi: 'medium', liftCard: 1, blurOverlay: 0, animSpeed: 'fast',
};
window.builtinPresets = {
Terminal: { ...window.designDefaults },
Modern: { theme: 'paper',
scanlines: '0',
fontHeadline: 'mono', fontContent: 'mono', fontUi: 'mono',
radiusCard: 10, radiusButton: 24, radiusInput: 5, radiusModal: 15, radiusTag: 3, radiusImage: 8,
borderCard: 1, borderStyle: 'solid', barHeight: 3,
padCard: 10, gapBoard: 17,
shadowCard: 'none', shadowModal: 'none', shadowUi: 'none',
liftCard: 2, blurOverlay: 5, animSpeed: 'normal' },
};
window.applyDesign = function(d) {
const root = document.documentElement.style;
// color theme
if (d.theme && window.themes?.[d.theme]) window.applyTheme(d.theme);
// fonts
root.setProperty('--t-font-headline', window.themeFonts[d.fontHeadline] || window.themeFonts.pixel);
root.setProperty('--t-font-content', window.themeFonts[d.fontContent] || window.themeFonts.mono);
root.setProperty('--t-font-ui', window.themeFonts[d.fontUi] || window.themeFonts.pixel);
// geometry
root.setProperty('--ui-radius-card', d.radiusCard + 'px');
root.setProperty('--ui-radius-button', d.radiusButton + 'px');
root.setProperty('--ui-radius-input', d.radiusInput + 'px');
root.setProperty('--ui-radius-modal', d.radiusModal + 'px');
root.setProperty('--ui-radius-tag', d.radiusTag + 'px');
root.setProperty('--ui-radius-image', d.radiusImage + 'px');
// borders
root.setProperty('--ui-border-card', d.borderCard + 'px');
root.setProperty('--ui-border-style', d.borderStyle);
root.setProperty('--ui-bar-height', d.barHeight + 'px');
// spacing
root.setProperty('--ui-pad-card', d.padCard + 'px');
root.setProperty('--ui-gap-board', d.gapBoard + 'px');
// effects
root.setProperty('--ui-shadow-card', window.designShadows[d.shadowCard] || 'none');
root.setProperty('--ui-shadow-modal', window.designShadows[d.shadowModal] || window.designShadows.medium);
root.setProperty('--ui-shadow-header', window.designUiShadows.header[d.shadowUi] || window.designUiShadows.header.medium);
root.setProperty('--ui-shadow-footer', window.designUiShadows.footer[d.shadowUi] || window.designUiShadows.footer.medium);
root.setProperty('--ui-lift-card', d.liftCard + 'px');
root.setProperty('--ui-blur-overlay', d.blurOverlay + 'px');
// body classes
document.body.classList.toggle('no-scanlines', String(d.scanlines) !== '1');
document.body.classList.remove('anim-off', 'anim-fast', 'anim-slow');
if (d.animSpeed === 'off') document.body.classList.add('anim-off');
if (d.animSpeed === 'fast') document.body.classList.add('anim-fast');
if (d.animSpeed === 'slow') document.body.classList.add('anim-slow');
};
window.loadDesign = function() {
let saved = {};
try { saved = JSON.parse(localStorage.getItem('hyprcrd-design') || '{}'); } catch {}
// Migrate legacy individual keys (only if no JSON blob exists yet)
if (!Object.keys(saved).length) {
const legacy = {};
const sl = localStorage.getItem('hyprcrd-scanlines');
if (sl !== null) legacy.scanlines = sl;
const fh = localStorage.getItem('hyprcrd-font-headline'); if (fh) legacy.fontHeadline = fh;
const fc = localStorage.getItem('hyprcrd-font-content'); if (fc) legacy.fontContent = fc;
const fu = localStorage.getItem('hyprcrd-font-ui'); if (fu) legacy.fontUi = fu;
saved = legacy;
}
// Always pull the legacy theme key as a fallback so existing users keep their selected theme
if (!saved.theme) {
const t = localStorage.getItem('hyprcrd-theme');
if (t) saved.theme = t;
}
return { ...window.designDefaults, ...saved };
};
window.saveDesign = function(d) { localStorage.setItem('hyprcrd-design', JSON.stringify(d)); };
// Apply once body exists
const _applySaved = () => window.applyDesign(window.loadDesign());
if (document.body) _applySaved(); else document.addEventListener('DOMContentLoaded', _applySaved);
</script> </script>
</head> </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 --> <!-- 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 h-screen transition-colors duration-200 flex flex-col overflow-hidden"> <body class="bg-t-bg text-t-text h-screen transition-colors duration-200 flex flex-col 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;"> <header id="main-header" class="shrink-0 z-40 bg-t-bg border-b border-t-border" style="overflow:hidden;">
<div class="flex items-center justify-between px-6 py-3 relative z-[1]"> <div class="flex items-center justify-between px-6 py-3 relative z-[1]">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<i class="ph ph-terminal-window text-t-accent text-xl"></i> <i class="ph ph-terminal-window text-t-accent text-xl"></i>
<h1 class="font-pixel text-t-accent text-lg tracking-wider">hyprcrd</h1> <h1 class="brand font-pixel text-t-accent text-lg tracking-wider">hyprcrd</h1>
<span class="text-[10px] text-t-dim font-pixel ml-1">v<?= APP_VERSION ?></span> <span class="text-[10px] text-t-dim font-pixel ml-1">v<?= APP_VERSION ?></span>
<button id="search-btn" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors ml-3"> <button id="search-btn" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors ml-3">
<i class="ph ph-magnifying-glass text-sm"></i><span class="font-pixel text-[10px]">search</span> <i class="ph ph-magnifying-glass text-sm"></i><span class="font-pixel text-[10px]">Search</span>
<kbd class="text-[9px] text-t-dim border border-t-border rounded px-1 py-0.5 ml-1 font-pixel">ctrl+k</kbd> <kbd class="text-[9px] text-t-dim border border-t-border rounded px-1 py-0.5 ml-1 font-pixel">ctrl+k</kbd>
</button> </button>
<button id="header-sync" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors" title="sync now"> <button id="header-sync" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors" title="sync now">
<i class="ph ph-arrows-clockwise text-sm" id="header-sync-icon"></i><span class="font-pixel text-[10px]">sync</span> <i class="ph ph-arrows-clockwise text-sm" id="header-sync-icon"></i><span class="font-pixel text-[10px]">Sync</span>
</button> </button>
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button id="calendar-btn" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors"> <button id="calendar-btn" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors">
<i class="ph ph-calendar-dots text-sm"></i><span class="font-pixel text-[10px]">calendar</span> <i class="ph ph-calendar-dots text-sm"></i><span class="font-pixel text-[10px]">Calendar</span>
</button> </button>
<button id="gallery-btn" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors"> <button id="gallery-btn" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors">
<i class="ph ph-image-square text-sm"></i><span class="font-pixel text-[10px]">images</span> <i class="ph ph-image-square text-sm"></i><span class="font-pixel text-[10px]">Images</span>
</button> </button>
<button id="tasks-btn" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent2 hover:text-t-accent2 transition-colors"> <button id="tasks-btn" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent2 hover:text-t-accent2 transition-colors">
<i class="ph ph-check-square-offset text-sm"></i><span class="font-pixel text-[10px]">tasks</span> <i class="ph ph-check-square-offset text-sm"></i><span class="font-pixel text-[10px]">Tasks</span>
<span id="tasks-open-count" class="font-pixel text-[9px] px-1.5 py-0.5 border border-t-border rounded text-t-dim">0</span> <span id="tasks-open-count" class="font-pixel text-[9px] px-1.5 py-0.5 border border-t-border rounded text-t-dim">0</span>
</button> </button>
<button id="add-btn" class="flex items-center gap-2 px-3 py-1.5 text-xs bg-t-accent text-t-bg border border-t-accent hover:brightness-110 transition-all"> <button id="add-btn" class="flex items-center gap-2 px-3 py-1.5 text-xs bg-t-accent text-t-bg border border-t-accent hover:brightness-110 transition-all">
<i class="ph ph-plus-square text-sm"></i><span class="font-pixel text-[11px]">new_card</span> <i class="ph ph-plus-square text-sm"></i><span class="font-pixel text-[11px]">New_card</span>
</button> </button>
</div> </div>
</div> </div>
@@ -1122,39 +1239,39 @@ if (str_starts_with($route, '/api/')) {
</div> </div>
</div> </div>
<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"> <footer id="main-footer" class="shrink-0 z-40 border-t border-t-border bg-t-bg px-6 py-2 flex items-center justify-between gap-4">
<div class="flex items-center gap-2 flex-wrap"> <div class="flex items-center gap-2 flex-wrap">
<input type="file" id="import-file" accept=".zip,application/zip" class="hidden"> <input type="file" id="import-file" accept=".zip,application/zip" class="hidden">
<button id="logout-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-danger hover:text-t-danger transition-colors"> <button id="logout-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-danger hover:text-t-danger transition-colors">
<i class="ph ph-sign-out text-sm"></i><span class="font-pixel text-[10px]">logout</span> <i class="ph ph-sign-out text-sm"></i><span class="font-pixel text-[10px]">Logout</span>
</button> </button>
<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"> <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">
<i class="ph ph-upload-simple text-sm"></i><span class="font-pixel text-[10px]">import</span> <i class="ph ph-upload-simple text-sm"></i><span class="font-pixel text-[10px]">Import</span>
</button> </button>
<button id="export-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"> <button id="export-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">
<i class="ph ph-download-simple text-sm"></i><span class="font-pixel text-[10px]">export</span> <i class="ph ph-download-simple text-sm"></i><span class="font-pixel text-[10px]">Export</span>
</button> </button>
<button id="export-md-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"> <button id="export-md-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">
<i class="ph ph-vault text-sm"></i><span class="font-pixel text-[10px]">export vault</span> <i class="ph ph-vault text-sm"></i><span class="font-pixel text-[10px]">Export vault</span>
</button> </button>
<button id="settings-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-accent2 hover:text-t-accent2 transition-colors"> <button id="settings-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-accent2 hover:text-t-accent2 transition-colors">
<i class="ph ph-gear text-sm"></i><span class="font-pixel text-[10px]">settings</span> <i class="ph ph-gear text-sm"></i><span class="font-pixel text-[10px]">Settings</span>
</button> </button>
</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 id="footer-count" class="font-pixel text-[10px] text-t-dim whitespace-nowrap absolute left-1/2 -translate-x-1/2"></div>
<div class="flex items-center gap-2"> <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> <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>
</div> </div>
</footer> </footer>
<!-- Help --> <!-- 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="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="bg-t-surface border border-t-border shadow-2xl w-full max-w-3xl max-h-full overflow-y-auto">
<div class="flex items-center gap-3 px-6 py-4 border-b border-t-border sticky top-0 bg-t-surface z-[1]"> <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> <i class="ph ph-question text-t-accent text-base"></i>
<h2 class="font-pixel text-sm text-t-accent">help</h2> <h2 class="font-pixel text-sm text-t-accent">help</h2>
<div class="flex-1"></div> <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> <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>
<div class="p-6 text-sm text-t-text space-y-6" id="help-content"> <div class="p-6 text-sm text-t-text space-y-6" id="help-content">
@@ -1166,7 +1283,7 @@ if (str_starts_with($route, '/api/')) {
<p><i class="ph ph-stack text-t-accent text-sm"></i> <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 <i class="ph ph-stack text-xs"></i> "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 <i class="ph ph-arrow-line-up text-xs"></i> "dissolve group" to ungroup all at once.</p> <p><i class="ph ph-stack text-t-accent text-sm"></i> <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 <i class="ph ph-stack text-xs"></i> "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 <i class="ph ph-arrow-line-up text-xs"></i> "dissolve group" to ungroup all at once.</p>
<p><i class="ph ph-hash text-t-accent text-sm"></i> <span class="text-t-bright font-pixel">tags</span> are written as <span class="text-t-accent font-pixel">#tagname</span> in the card body and extracted automatically. They support hierarchy: <span class="text-t-accent font-pixel">#project/frontend</span>. Click a tag to filter — Ctrl+click to combine multiple tags (AND). Assign via right-click > <i class="ph ph-hash text-xs"></i> tags. Each segment of a hierarchical tag is independently clickable.</p> <p><i class="ph ph-hash text-t-accent text-sm"></i> <span class="text-t-bright font-pixel">tags</span> are written as <span class="text-t-accent font-pixel">#tagname</span> in the card body and extracted automatically. They support hierarchy: <span class="text-t-accent font-pixel">#project/frontend</span>. Click a tag to filter — Ctrl+click to combine multiple tags (AND). Assign via right-click > <i class="ph ph-hash text-xs"></i> tags. Each segment of a hierarchical tag is independently clickable.</p>
<p><i class="ph ph-link text-t-accent text-sm"></i> <span class="text-t-bright font-pixel">links</span> connect cards. Write <span class="text-t-accent font-pixel">[[Card Title]]</span> in the body to create a link. Linked cards are clickable in preview and show up as <i class="ph ph-arrow-bend-left-up text-xs"></i> backlinks on the target card. The <i class="ph ph-split-horizontal text-xs"></i> split view shows related cards based on shared tags, links, and content overlap.</p> <p><i class="ph ph-link text-t-accent text-sm"></i> <span class="text-t-bright font-pixel">links</span> connect cards. Write <span class="text-t-accent font-pixel">[[Card Title]]</span> in the body to create a link. Linked cards are clickable in preview and show up as <i class="ph ph-arrow-bend-left-up text-xs"></i> backlinks on the target card. The <i class="ph ph-split-horizontal text-xs"></i> split view shows related cards based on shared tags, links, and content overlap.</p>
<p><i class="ph ph-calendar-dots text-t-accent text-sm"></i> <span class="text-t-bright font-pixel">calendar</span> shows cards on their dates. Journal cards (<span class="text-t-dim font-pixel">YYYY-MM-DD</span>) and appointments (<span class="text-t-dim font-pixel">YYYY-MM-DD — Subject</span>) appear on their date. <i class="ph ph-cake text-xs" style="color:#fb7185"></i> Birthdays (<span class="text-t-accent font-pixel">#geburtstag</span>) repeat yearly. Sync with ownCloud/Nextcloud via <i class="ph ph-cloud-arrow-up text-xs"></i> CalDAV.</p> <p><i class="ph ph-calendar-dots text-t-accent text-sm"></i> <span class="text-t-bright font-pixel">Calendar</span> shows cards on their dates. Journal cards (<span class="text-t-dim font-pixel">YYYY-MM-DD</span>) and appointments (<span class="text-t-dim font-pixel">YYYY-MM-DD — Subject</span>) appear on their date. <i class="ph ph-cake text-xs" style="color:#fb7185"></i> Birthdays (<span class="text-t-accent font-pixel">#geburtstag</span>) repeat yearly. Sync with ownCloud/Nextcloud via <i class="ph ph-cloud-arrow-up text-xs"></i> CalDAV.</p>
</div> </div>
</section> </section>
@@ -1344,29 +1461,29 @@ if (str_starts_with($route, '/api/')) {
<!-- Settings --> <!-- Settings -->
<div class="overlay fixed inset-0 z-[88] flex justify-center items-center px-6" style="background:var(--t-overlay)" id="settings-overlay"> <div class="overlay fixed inset-0 z-[88] flex justify-center items-center px-6" style="background:var(--t-overlay)" id="settings-overlay">
<div class="bg-t-surface border border-t-border shadow-2xl w-full max-w-2xl flex" style="border-radius:4px;height:90vh;"> <div class="bg-t-surface border border-t-border shadow-2xl w-full max-w-2xl flex" style="height:90vh;">
<div class="w-44 shrink-0 border-r border-t-border py-4 flex flex-col gap-1" id="settings-nav"> <div class="w-44 shrink-0 border-r border-t-border py-4 flex flex-col gap-1" id="settings-nav">
<button class="settings-tab active" data-tab="branding"><i class="ph ph-terminal-window text-sm"></i><span class="font-pixel text-[10px]">hyprcrd</span></button> <button class="settings-tab active" data-tab="branding"><i class="ph ph-terminal-window text-sm"></i><span class="brand font-pixel text-[10px]">hyprcrd</span></button>
<button class="settings-tab" data-tab="account"><i class="ph ph-user text-sm"></i><span class="font-pixel text-[10px]">account</span></button> <button class="settings-tab" data-tab="account"><i class="ph ph-user text-sm"></i><span class="font-pixel text-[10px]">Account</span></button>
<button class="settings-tab" data-tab="theme"><i class="ph ph-palette text-sm"></i><span class="font-pixel text-[10px]">themes</span></button> <button class="settings-tab" data-tab="theme"><i class="ph ph-palette text-sm"></i><span class="font-pixel text-[10px]">Themes</span></button>
<button class="settings-tab" data-tab="caldav"><i class="ph ph-calendar-dots text-sm"></i><span class="font-pixel text-[10px]">calendar</span></button> <button class="settings-tab" data-tab="caldav"><i class="ph ph-calendar-dots text-sm"></i><span class="font-pixel text-[10px]">Calendar</span></button>
<button class="settings-tab" data-tab="relay"><i class="ph ph-broadcast text-sm"></i><span class="font-pixel text-[10px]">relay</span></button> <button class="settings-tab" data-tab="relay"><i class="ph ph-broadcast text-sm"></i><span class="font-pixel text-[10px]">Relay</span></button>
<button class="settings-tab" data-tab="behavior"><i class="ph ph-sliders text-sm"></i><span class="font-pixel text-[10px]">behavior</span></button> <button class="settings-tab" data-tab="behavior"><i class="ph ph-sliders text-sm"></i><span class="font-pixel text-[10px]">Behavior</span></button>
<button class="settings-tab" data-tab="notifications"><i class="ph ph-bell text-sm"></i><span class="font-pixel text-[10px]">notifications</span></button> <button class="settings-tab" data-tab="notifications"><i class="ph ph-bell text-sm"></i><span class="font-pixel text-[10px]">Notifications</span></button>
<button class="settings-tab" data-tab="plugins"><i class="ph ph-puzzle-piece text-sm"></i><span class="font-pixel text-[10px]">plugins</span></button> <button class="settings-tab" data-tab="plugins"><i class="ph ph-puzzle-piece text-sm"></i><span class="font-pixel text-[10px]">Plugins</span></button>
</div> </div>
<div class="flex-1 flex flex-col min-h-0"> <div class="flex-1 flex flex-col min-h-0">
<div class="flex items-center gap-3 px-5 py-3 border-b border-t-border shrink-0"> <div class="flex items-center gap-3 px-5 py-3 border-b border-t-border shrink-0">
<i class="ph ph-gear text-t-accent text-base"></i> <i class="ph ph-gear text-t-accent text-base"></i>
<h2 id="settings-title" class="font-pixel text-xs text-t-accent">settings</h2> <h2 id="settings-title" class="font-pixel text-xs text-t-accent">settings</h2>
<div class="flex-1"></div> <div class="flex-1"></div>
<button id="settings-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> <button id="settings-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>
<div class="flex-1 overflow-y-auto p-5"> <div class="flex-1 overflow-y-auto p-5 relative">
<div id="settings-panel-branding"> <div id="settings-panel-branding">
<div class="flex flex-col items-center justify-center py-12 text-center"> <div class="flex flex-col items-center justify-center py-12 text-center">
<i class="ph ph-terminal-window text-t-accent text-5xl mb-4"></i> <i class="ph ph-terminal-window text-t-accent text-5xl mb-4"></i>
<h1 class="font-pixel text-2xl text-t-accent tracking-wider mb-2">hyprcrd</h1> <h1 class="brand font-pixel text-2xl text-t-accent tracking-wider mb-2">hyprcrd</h1>
<p class="font-pixel text-[10px] text-t-dim mb-6">v<?= APP_VERSION ?></p> <p class="font-pixel text-[10px] text-t-dim mb-6">v<?= APP_VERSION ?></p>
<p class="text-xs text-t-muted leading-relaxed max-w-sm">Self-hosted card manager with markdown, tags, calendar, CalDAV sync, encrypted WebDAV notes, and Obsidian-compatible export.</p> <p class="text-xs text-t-muted leading-relaxed max-w-sm">Self-hosted card manager with markdown, tags, calendar, CalDAV sync, encrypted WebDAV notes, and Obsidian-compatible export.</p>
<div class="flex items-center gap-4 mt-8 text-t-dim"> <div class="flex items-center gap-4 mt-8 text-t-dim">
@@ -1379,9 +1496,100 @@ if (str_starts_with($route, '/api/')) {
<p class="text-[10px] text-t-dim mt-8">Press <kbd class="px-1 py-0.5 border border-t-border rounded text-[9px] font-pixel">F1</kbd> for help</p> <p class="text-[10px] text-t-dim mt-8">Press <kbd class="px-1 py-0.5 border border-t-border rounded text-[9px] font-pixel">F1</kbd> for help</p>
</div> </div>
</div> </div>
<div id="settings-panel-theme" class="hidden"> <div id="settings-panel-theme" class="hidden absolute inset-0 p-5 flex flex-col">
<div class="font-pixel text-[9px] uppercase tracking-wider text-t-dim mb-3">color theme</div> <div class="shrink-0 pb-3 border-b border-t-border">
<div id="settings-theme-list" class="grid grid-cols-2 gap-2"></div> <div class="flex items-center justify-between mb-2">
<div class="font-pixel text-[9px] uppercase tracking-wider text-t-dim">preview</div>
<div class="flex items-center gap-2 cursor-pointer select-none" id="design-peek-row">
<span class="font-pixel text-[10px] text-t-muted">Peek Board</span>
<div class="toggle-switch" id="design-peek-toggle"></div>
</div>
</div>
<div id="theme-preview" class="theme-preview">
<div class="tp-scan" id="theme-preview-scan"></div>
<div class="tp-row">
<div class="tp-card">
<div class="tp-card-bar"></div>
<div class="tp-card-body">
<div class="tp-headline">Headline</div>
<div class="tp-content">Sample content text rendered in the chosen content font. 0123456789</div>
<div class="tp-meta">
<span class="tp-tag">tag</span>
<span class="tp-ui">ui_label</span>
</div>
</div>
</div>
<div class="tp-side">
<button class="tp-btn" type="button">Button</button>
<input class="tp-input" type="text" placeholder="input">
</div>
</div>
</div>
</div>
<div class="flex-1 overflow-y-auto pt-4 space-y-5" style="margin-right:-1.25rem;padding-right:1.25rem;">
<div>
<div class="font-pixel text-[9px] uppercase tracking-wider text-t-dim mb-3">color theme</div>
<div id="settings-theme-list" class="grid grid-cols-2 gap-2"></div>
</div>
<div class="border-t border-t-border pt-4">
<div class="design-section-label">presets</div>
<div class="design-row">
<select id="design-preset-select" class="bg-t-bg border border-t-border text-t-bright text-xs font-pixel px-2 py-1 rounded-sm focus:border-t-accent focus:outline-none cursor-pointer flex-1"></select>
<button id="design-preset-delete" class="flex items-center gap-1 px-2 py-1 text-[10px] border border-t-border text-t-muted hover:border-t-danger hover:text-t-danger transition-colors" title="Delete user preset"><i class="ph ph-trash text-xs"></i></button>
</div>
<div class="design-row">
<input id="design-preset-name" type="text" placeholder="new preset name..." class="bg-t-bg border border-t-border text-t-bright text-xs font-mono px-2 py-1 flex-1 focus:border-t-accent focus:outline-none">
<button id="design-preset-save" class="flex items-center gap-1 px-2 py-1 text-[10px] border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-xs"></i><span class="font-pixel">Save</span></button>
</div>
<div class="design-row">
<button id="design-preset-export" class="flex items-center gap-1 px-2 py-1 text-[10px] border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors flex-1 justify-center"><i class="ph ph-export text-xs"></i><span class="font-pixel">Export JSON</span></button>
<button id="design-preset-import" class="flex items-center gap-1 px-2 py-1 text-[10px] border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors flex-1 justify-center"><i class="ph ph-upload-simple text-xs"></i><span class="font-pixel">Import JSON</span></button>
</div>
</div>
<details id="design-advanced" class="border-t border-t-border pt-4">
<summary class="cursor-pointer select-none flex items-center gap-2 font-pixel text-[9px] uppercase tracking-wider text-t-dim">
<i class="ph ph-caret-right text-xs design-advanced-caret"></i>
<span>advanced</span>
</summary>
<div class="mt-2">
<div class="design-section-label">typography</div>
<div class="toggle-row"><span class="toggle-label">Scan lines</span><div class="toggle-switch" id="setting-scanlines"></div></div>
<div class="design-row"><label>Headline</label><select id="setting-font-headline" class="bg-t-bg border border-t-border text-t-bright text-xs font-pixel px-2 py-1 rounded-sm focus:border-t-accent focus:outline-none cursor-pointer"></select></div>
<div class="design-row"><label>Content</label><select id="setting-font-content" class="bg-t-bg border border-t-border text-t-bright text-xs font-pixel px-2 py-1 rounded-sm focus:border-t-accent focus:outline-none cursor-pointer"></select></div>
<div class="design-row"><label>UI</label><select id="setting-font-ui" class="bg-t-bg border border-t-border text-t-bright text-xs font-pixel px-2 py-1 rounded-sm focus:border-t-accent focus:outline-none cursor-pointer"></select></div>
<div class="design-section-label">corners</div>
<div class="design-row"><label>Card</label><input type="range" id="setting-radius-card" min="0" max="24" step="1" class="design-slider"><span class="design-val" id="val-radius-card">0</span></div>
<div class="design-row"><label>Button</label><input type="range" id="setting-radius-button" min="0" max="24" step="1" class="design-slider"><span class="design-val" id="val-radius-button">0</span></div>
<div class="design-row"><label>Input</label><input type="range" id="setting-radius-input" min="0" max="24" step="1" class="design-slider"><span class="design-val" id="val-radius-input">0</span></div>
<div class="design-row"><label>Modal</label><input type="range" id="setting-radius-modal" min="0" max="24" step="1" class="design-slider"><span class="design-val" id="val-radius-modal">0</span></div>
<div class="design-row"><label>Tag</label><input type="range" id="setting-radius-tag" min="0" max="16" step="1" class="design-slider"><span class="design-val" id="val-radius-tag">0</span></div>
<div class="design-row"><label>Image</label><input type="range" id="setting-radius-image" min="0" max="24" step="1" class="design-slider"><span class="design-val" id="val-radius-image">0</span></div>
<div class="design-section-label">borders</div>
<div class="design-row"><label>Card width</label><input type="range" id="setting-border-card" min="0" max="4" step="1" class="design-slider"><span class="design-val" id="val-border-card">1</span></div>
<div class="design-row"><label>Style</label><select id="setting-border-style" class="bg-t-bg border border-t-border text-t-bright text-xs font-pixel px-2 py-1 rounded-sm focus:border-t-accent focus:outline-none cursor-pointer"><option value="solid">solid</option><option value="dashed">dashed</option><option value="dotted">dotted</option><option value="double">double</option></select></div>
<div class="design-row"><label>Bar height</label><input type="range" id="setting-bar-height" min="0" max="10" step="1" class="design-slider"><span class="design-val" id="val-bar-height">3</span></div>
<div class="design-section-label">spacing</div>
<div class="design-row"><label>Card padding</label><input type="range" id="setting-pad-card" min="4" max="32" step="1" class="design-slider"><span class="design-val" id="val-pad-card">16</span></div>
<div class="design-row"><label>Board gap</label><input type="range" id="setting-gap-board" min="0" max="24" step="1" class="design-slider"><span class="design-val" id="val-gap-board">0</span></div>
<div class="design-section-label">effects</div>
<div class="design-row"><label>Card shadow</label><select id="setting-shadow-card" class="bg-t-bg border border-t-border text-t-bright text-xs font-pixel px-2 py-1 rounded-sm focus:border-t-accent focus:outline-none cursor-pointer"><option value="none">none</option><option value="soft">soft</option><option value="medium">medium</option><option value="heavy">heavy</option></select></div>
<div class="design-row"><label>Modal shadow</label><select id="setting-shadow-modal" class="bg-t-bg border border-t-border text-t-bright text-xs font-pixel px-2 py-1 rounded-sm focus:border-t-accent focus:outline-none cursor-pointer"><option value="none">none</option><option value="soft">soft</option><option value="medium">medium</option><option value="heavy">heavy</option></select></div>
<div class="design-row"><label>UI shadow</label><select id="setting-shadow-ui" class="bg-t-bg border border-t-border text-t-bright text-xs font-pixel px-2 py-1 rounded-sm focus:border-t-accent focus:outline-none cursor-pointer" title="Header & footer shadow"><option value="none">none</option><option value="soft">soft</option><option value="medium">medium</option><option value="heavy">heavy</option></select></div>
<div class="design-row"><label>Card lift</label><input type="range" id="setting-lift-card" min="0" max="8" step="1" class="design-slider"><span class="design-val" id="val-lift-card">0</span></div>
<div class="design-row"><label>Overlay blur</label><input type="range" id="setting-blur-overlay" min="0" max="16" step="1" class="design-slider"><span class="design-val" id="val-blur-overlay">0</span></div>
<div class="design-row"><label>Animation</label><select id="setting-anim-speed" class="bg-t-bg border border-t-border text-t-bright text-xs font-pixel px-2 py-1 rounded-sm focus:border-t-accent focus:outline-none cursor-pointer"><option value="off">off</option><option value="fast">fast</option><option value="normal">normal</option><option value="slow">slow</option></select></div>
<div class="flex items-center gap-2 mt-4">
<button id="design-reset" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-danger hover:text-t-danger transition-colors"><i class="ph ph-arrow-counter-clockwise text-sm"></i><span class="font-pixel text-[10px]">Reset to defaults</span></button>
<span id="design-status" class="font-pixel text-[9px] text-t-dim ml-2">auto-saving</span>
</div>
</div>
</details>
</div>
</div> </div>
<div id="settings-panel-caldav" class="hidden space-y-3"> <div id="settings-panel-caldav" class="hidden space-y-3">
<div class="font-pixel text-[9px] uppercase tracking-wider text-t-dim mb-2"><i class="ph ph-calendar-dots text-xs"></i> CalDAV sync</div> <div class="font-pixel text-[9px] uppercase tracking-wider text-t-dim mb-2"><i class="ph ph-calendar-dots text-xs"></i> CalDAV sync</div>
@@ -1391,7 +1599,7 @@ if (str_starts_with($route, '/api/')) {
<div class="flex-1"><label class="font-pixel text-[9px] text-t-dim uppercase block mb-1">password</label><input id="caldav-pass" type="text" name="caldav_pass_x" placeholder="app password" autocomplete="off" data-1p-ignore data-lpignore="true" data-form-type="other" style="-webkit-text-security:disc;" class="w-full bg-t-bg border border-t-border text-t-bright px-3 py-2 text-xs focus:border-t-accent focus:outline-none font-mono"></div> <div class="flex-1"><label class="font-pixel text-[9px] text-t-dim uppercase block mb-1">password</label><input id="caldav-pass" type="text" name="caldav_pass_x" placeholder="app password" autocomplete="off" data-1p-ignore data-lpignore="true" data-form-type="other" style="-webkit-text-security:disc;" class="w-full bg-t-bg border border-t-border text-t-bright px-3 py-2 text-xs focus:border-t-accent focus:outline-none font-mono"></div>
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button id="caldav-test" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors"><i class="ph ph-plugs-connected text-sm"></i><span class="font-pixel text-[10px]">test</span></button> <button id="caldav-test" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors"><i class="ph ph-plugs-connected text-sm"></i><span class="font-pixel text-[10px]">Test</span></button>
</div> </div>
<div id="caldav-test-status" class="hidden mt-1 px-3 py-2 text-[10px] font-mono break-words border border-t-border bg-t-bg"></div> <div id="caldav-test-status" class="hidden mt-1 px-3 py-2 text-[10px] font-mono break-words border border-t-border bg-t-bg"></div>
<div id="caldav-calendars" class="hidden border-t border-t-border pt-2"> <div id="caldav-calendars" class="hidden border-t border-t-border pt-2">
@@ -1399,8 +1607,8 @@ if (str_starts_with($route, '/api/')) {
<div id="caldav-cal-list" class="space-y-2"></div> <div id="caldav-cal-list" class="space-y-2"></div>
</div> </div>
<div class="flex flex-wrap gap-2 pt-2 border-t border-t-border"> <div class="flex flex-wrap gap-2 pt-2 border-t border-t-border">
<button id="caldav-save" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">save</span></button> <button id="caldav-save" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">Save</span></button>
<button id="caldav-sync-now" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors"><i class="ph ph-arrows-clockwise text-sm"></i><span class="font-pixel text-[10px]">sync now</span></button> <button id="caldav-sync-now" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors"><i class="ph ph-arrows-clockwise text-sm"></i><span class="font-pixel text-[10px]">Sync now</span></button>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span id="caldav-sync-status" class="font-pixel text-[9px] text-t-dim"></span> <span id="caldav-sync-status" class="font-pixel text-[9px] text-t-dim"></span>
@@ -1414,8 +1622,8 @@ if (str_starts_with($route, '/api/')) {
<div><label class="font-pixel text-[9px] text-t-dim uppercase block mb-1">username</label><input id="relay-username" type="text" placeholder="your relay username" autocomplete="off" class="w-full bg-t-bg border border-t-border text-t-bright px-3 py-2 text-xs focus:border-t-accent focus:outline-none placeholder:text-t-dim font-mono"></div> <div><label class="font-pixel text-[9px] text-t-dim uppercase block mb-1">username</label><input id="relay-username" type="text" placeholder="your relay username" autocomplete="off" class="w-full bg-t-bg border border-t-border text-t-bright px-3 py-2 text-xs focus:border-t-accent focus:outline-none placeholder:text-t-dim font-mono"></div>
<div><label class="font-pixel text-[9px] text-t-dim uppercase block mb-1">shared secret</label><input id="relay-key" type="text" placeholder="shared secret" autocomplete="off" style="-webkit-text-security:disc;" class="w-full bg-t-bg border border-t-border text-t-bright px-3 py-2 text-xs focus:border-t-accent focus:outline-none font-mono"></div> <div><label class="font-pixel text-[9px] text-t-dim uppercase block mb-1">shared secret</label><input id="relay-key" type="text" placeholder="shared secret" autocomplete="off" style="-webkit-text-security:disc;" class="w-full bg-t-bg border border-t-border text-t-bright px-3 py-2 text-xs focus:border-t-accent focus:outline-none font-mono"></div>
<div class="flex gap-2"> <div class="flex gap-2">
<button id="relay-save" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">save</span></button> <button id="relay-save" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">Save</span></button>
<button id="relay-test" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors"><i class="ph ph-plugs-connected text-sm"></i><span class="font-pixel text-[10px]">test</span></button> <button id="relay-test" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors"><i class="ph ph-plugs-connected text-sm"></i><span class="font-pixel text-[10px]">Test</span></button>
</div> </div>
<div id="relay-status" class="hidden mt-1 px-3 py-2 text-[10px] font-mono break-words border border-t-border bg-t-bg"></div> <div id="relay-status" class="hidden mt-1 px-3 py-2 text-[10px] font-mono break-words border border-t-border bg-t-bg"></div>
</div> </div>
@@ -1481,6 +1689,13 @@ if (str_starts_with($route, '/api/')) {
<option value="edit">editor</option> <option value="edit">editor</option>
</select> </select>
</div> </div>
<div class="flex items-center gap-3">
<label class="text-xs text-t-muted flex-1">ESC in detail view</label>
<select id="setting-esc-action" class="bg-t-bg border border-t-border text-t-bright text-xs font-pixel px-2 py-1 rounded-sm focus:border-t-accent focus:outline-none cursor-pointer">
<option value="close">close card</option>
<option value="exitZen">exit zen first</option>
</select>
</div>
<div class="toggle-row" data-toggle="setting-confirm-delete"><span class="toggle-label">Confirm before delete</span><div class="toggle-switch" id="setting-confirm-delete"></div></div> <div class="toggle-row" data-toggle="setting-confirm-delete"><span class="toggle-label">Confirm before delete</span><div class="toggle-switch" id="setting-confirm-delete"></div></div>
<div class="toggle-row" data-toggle="setting-auto-save"><span class="toggle-label">Auto-save on close (editor)</span><div class="toggle-switch" id="setting-auto-save"></div></div> <div class="toggle-row" data-toggle="setting-auto-save"><span class="toggle-label">Auto-save on close (editor)</span><div class="toggle-switch" id="setting-auto-save"></div></div>
</div> </div>
@@ -1522,7 +1737,7 @@ if (str_starts_with($route, '/api/')) {
<div class="border-t border-t-border pt-3 mt-2"> <div class="border-t border-t-border pt-3 mt-2">
<div class="font-pixel text-[9px] uppercase tracking-wider text-t-dim mb-2">notification stream</div> <div class="font-pixel text-[9px] uppercase tracking-wider text-t-dim mb-2">notification stream</div>
<div class="flex items-center gap-2 mb-2"> <div class="flex items-center gap-2 mb-2">
<button id="notif-clear-dismissed" 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-counter-clockwise text-sm"></i><span class="font-pixel text-[10px]">restore dismissed</span></button> <button id="notif-clear-dismissed" 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-counter-clockwise text-sm"></i><span class="font-pixel text-[10px]">Restore dismissed</span></button>
</div> </div>
<div id="notif-stream" class="max-h-64 overflow-y-auto space-y-1"></div> <div id="notif-stream" class="max-h-64 overflow-y-auto space-y-1"></div>
</div> </div>
@@ -1535,7 +1750,7 @@ if (str_starts_with($route, '/api/')) {
<div id="plugins-reload-notice" class="hidden mt-3 flex items-center gap-2 px-3 py-2 border border-t-accent bg-t-accent/10 text-xs text-t-accent"> <div id="plugins-reload-notice" class="hidden mt-3 flex items-center gap-2 px-3 py-2 border border-t-accent bg-t-accent/10 text-xs text-t-accent">
<i class="ph ph-warning text-sm"></i> <i class="ph ph-warning text-sm"></i>
<span class="flex-1 font-pixel text-[10px]">reload required to apply changes</span> <span class="flex-1 font-pixel text-[10px]">reload required to apply changes</span>
<button id="plugins-reload-btn" class="font-pixel text-[10px] underline hover:text-t-bright">reload now</button> <button id="plugins-reload-btn" class="font-pixel text-[10px] underline hover:text-t-bright">Reload now</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1545,7 +1760,7 @@ if (str_starts_with($route, '/api/')) {
<p class="text-[10px] text-t-dim leading-relaxed mb-2">Shared encryption key for Notes and CalDAV credentials. Must be identical on all connected hyprcrd instances.</p> <p class="text-[10px] text-t-dim leading-relaxed mb-2">Shared encryption key for Notes and CalDAV credentials. Must be identical on all connected hyprcrd instances.</p>
<div class="flex gap-2"> <div class="flex gap-2">
<input id="settings-vault-key" type="text" placeholder="enter vault key" autocomplete="off" style="-webkit-text-security:disc;" class="flex-1 bg-t-bg border border-t-border text-t-bright px-3 py-2 text-xs focus:border-t-accent focus:outline-none font-mono"> <input id="settings-vault-key" type="text" placeholder="enter vault key" autocomplete="off" style="-webkit-text-security:disc;" class="flex-1 bg-t-bg border border-t-border text-t-bright px-3 py-2 text-xs focus:border-t-accent focus:outline-none font-mono">
<button id="settings-vault-save" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">save</span></button> <button id="settings-vault-save" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">Save</span></button>
</div> </div>
<span id="settings-vault-status" class="font-pixel text-[9px] text-t-dim mt-1 block"></span> <span id="settings-vault-status" class="font-pixel text-[9px] text-t-dim mt-1 block"></span>
</div> </div>
@@ -1564,7 +1779,7 @@ if (str_starts_with($route, '/api/')) {
<input id="settings-pw-confirm" type="password" autocomplete="new-password" class="w-full bg-t-bg border border-t-border text-t-bright px-3 py-2 text-xs focus:border-t-accent focus:outline-none font-mono"> <input id="settings-pw-confirm" type="password" autocomplete="new-password" class="w-full bg-t-bg border border-t-border text-t-bright px-3 py-2 text-xs focus:border-t-accent focus:outline-none font-mono">
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button id="settings-pw-save" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">change password</span></button> <button id="settings-pw-save" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">Change password</span></button>
<span id="settings-pw-status" class="font-pixel text-[9px] text-t-dim"></span> <span id="settings-pw-status" class="font-pixel text-[9px] text-t-dim"></span>
</div> </div>
</div> </div>
@@ -1582,7 +1797,7 @@ if (str_starts_with($route, '/api/')) {
<h2 id="dialog-title" class="font-pixel text-xs text-t-accent"></h2> <h2 id="dialog-title" class="font-pixel text-xs text-t-accent"></h2>
<div class="flex-1"></div> <div class="flex-1"></div>
<button id="dialog-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"> <button id="dialog-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> <i class="ph ph-x text-sm"></i><span class="font-pixel text-[10px]">Close</span>
</button> </button>
</div> </div>
<div class="p-4 space-y-3"> <div class="p-4 space-y-3">
@@ -1591,8 +1806,8 @@ if (str_starts_with($route, '/api/')) {
<div id="dialog-datepicker" class="hidden"></div> <div id="dialog-datepicker" class="hidden"></div>
</div> </div>
<div class="flex justify-end gap-2 px-4 py-3 border-t border-t-border"> <div class="flex justify-end gap-2 px-4 py-3 border-t border-t-border">
<button id="dialog-cancel" class="font-pixel text-[10px] px-3 py-1.5 text-xs border border-t-border text-t-muted hover:text-t-bright transition-colors">cancel</button> <button id="dialog-cancel" class="font-pixel text-[10px] px-3 py-1.5 text-xs border border-t-border text-t-muted hover:text-t-bright transition-colors">Cancel</button>
<button id="dialog-ok" class="font-pixel text-[10px] px-3 py-1.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors">ok</button> <button id="dialog-ok" class="font-pixel text-[10px] px-3 py-1.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors">Ok</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1605,7 +1820,7 @@ if (str_starts_with($route, '/api/')) {
<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"> <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> <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>
<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"> <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> <i class="ph ph-x text-sm"></i><span class="font-pixel text-[10px]">Close</span>
</button> </button>
</div> </div>
<div class="flex flex-1 min-h-0"> <div class="flex flex-1 min-h-0">
@@ -1645,7 +1860,7 @@ if (str_starts_with($route, '/api/')) {
<option value="none">unsorted</option> <option value="none">unsorted</option>
</select> </select>
<button id="tasks-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"> <button id="tasks-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> <i class="ph ph-x text-sm"></i><span class="font-pixel text-[10px]">Close</span>
</button> </button>
</div> </div>
<div id="tasks-summary" class="px-4 py-2 border-b border-t-border text-[10px] text-t-dim font-pixel"></div> <div id="tasks-summary" class="px-4 py-2 border-b border-t-border text-[10px] text-t-dim font-pixel"></div>
@@ -1670,23 +1885,23 @@ if (str_starts_with($route, '/api/')) {
<div class="card-bar w-full shrink-0" id="detail-color"></div> <div class="card-bar w-full shrink-0" id="detail-color"></div>
<div id="detail-controls" class="flex items-center gap-2 px-4 py-2 border-b border-t-border shrink-0"> <div id="detail-controls" class="flex items-center gap-2 px-4 py-2 border-b border-t-border shrink-0">
<button id="detail-edit" class="flex items-center gap-1.5 px-2.5 py-1 text-xs border border-t-border text-t-muted hover:border-t-accent2 hover:text-t-accent2 transition-colors"> <button id="detail-edit" class="flex items-center gap-1.5 px-2.5 py-1 text-xs border border-t-border text-t-muted hover:border-t-accent2 hover:text-t-accent2 transition-colors">
<i class="ph ph-pencil-simple text-sm"></i><span class="font-pixel text-[10px]">edit</span> <i class="ph ph-pencil-simple text-sm"></i><span class="font-pixel text-[10px]">Edit</span>
</button> </button>
<button id="detail-delete" 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"> <button id="detail-delete" 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-trash text-sm"></i><span class="font-pixel text-[10px]">del</span> <i class="ph ph-trash text-sm"></i><span class="font-pixel text-[10px]">Del</span>
</button> </button>
<button id="detail-done" 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" title="mark done"> <button id="detail-done" 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" title="mark done">
<i class="ph ph-check-circle text-sm"></i><span class="font-pixel text-[10px]" id="detail-done-label">done</span> <i class="ph ph-check-circle text-sm"></i><span class="font-pixel text-[10px]" id="detail-done-label">Done</span>
</button> </button>
<button id="detail-zen" 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" title="zen mode"> <button id="detail-zen" 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" title="zen mode">
<i class="ph ph-corners-out text-sm"></i> <i class="ph ph-corners-out text-sm"></i>
</button> </button>
<div class="flex-1"></div> <div class="flex-1"></div>
<button id="split-right-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" title="split view"> <button id="split-right-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" title="split view">
<i class="ph ph-split-horizontal text-sm"></i><span class="font-pixel text-[10px]">split</span> <i class="ph ph-split-horizontal text-sm"></i><span class="font-pixel text-[10px]">Split</span>
</button> </button>
<button id="detail-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"> <button id="detail-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> <i class="ph ph-x text-sm"></i><span class="font-pixel text-[10px]">Close</span>
</button> </button>
</div> </div>
<div id="detail-scroll" class="p-6 pt-5 flex-1 overflow-y-auto"> <div id="detail-scroll" class="p-6 pt-5 flex-1 overflow-y-auto">
@@ -1726,12 +1941,12 @@ if (str_starts_with($route, '/api/')) {
<button id="cal-prev" class="text-t-muted hover:text-t-accent transition-colors"><i class="ph ph-caret-left text-sm"></i></button> <button id="cal-prev" class="text-t-muted hover:text-t-accent transition-colors"><i class="ph ph-caret-left text-sm"></i></button>
<h2 id="cal-title" class="font-pixel text-xs text-t-accent min-w-[120px] text-center"></h2> <h2 id="cal-title" class="font-pixel text-xs text-t-accent min-w-[120px] text-center"></h2>
<button id="cal-next" class="text-t-muted hover:text-t-accent transition-colors"><i class="ph ph-caret-right text-sm"></i></button> <button id="cal-next" class="text-t-muted hover:text-t-accent transition-colors"><i class="ph ph-caret-right text-sm"></i></button>
<button id="cal-today" class="font-pixel text-[10px] px-2 py-0.5 border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors">today</button> <button id="cal-today" class="font-pixel text-[10px] px-2 py-0.5 border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors">Today</button>
<button id="cal-sync" class="font-pixel text-[10px] px-2 py-0.5 border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors flex items-center gap-1"><i class="ph ph-cloud-arrow-up text-xs"></i>sync</button> <button id="cal-sync" class="font-pixel text-[10px] px-2 py-0.5 border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors flex items-center gap-1"><i class="ph ph-cloud-arrow-up text-xs"></i>Sync</button>
<button id="cal-caldav-settings" class="text-t-dim hover:text-t-accent transition-colors"><i class="ph ph-gear text-sm"></i></button> <button id="cal-caldav-settings" class="text-t-dim hover:text-t-accent transition-colors"><i class="ph ph-gear text-sm"></i></button>
<div class="flex-1"></div> <div class="flex-1"></div>
<button id="cal-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"> <button id="cal-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> <i class="ph ph-x text-sm"></i><span class="font-pixel text-[10px]">Close</span>
</button> </button>
</div> </div>
<div id="cal-grid" class="flex-1 flex flex-col overflow-hidden"></div> <div id="cal-grid" class="flex-1 flex flex-col overflow-hidden"></div>
@@ -1748,11 +1963,11 @@ if (str_starts_with($route, '/api/')) {
<span id="gallery-count" class="font-pixel text-[9px] text-t-dim"></span> <span id="gallery-count" class="font-pixel text-[9px] text-t-dim"></span>
<div class="flex-1"></div> <div class="flex-1"></div>
<button id="gallery-upload" 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"> <button id="gallery-upload" 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-upload-simple text-sm"></i><span class="font-pixel text-[10px]">upload</span> <i class="ph ph-upload-simple text-sm"></i><span class="font-pixel text-[10px]">Upload</span>
</button> </button>
<input type="file" id="gallery-upload-file" accept="image/*,audio/*" multiple class="hidden"> <input type="file" id="gallery-upload-file" accept="image/*,audio/*" multiple class="hidden">
<button id="gallery-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"> <button id="gallery-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> <i class="ph ph-x text-sm"></i><span class="font-pixel text-[10px]">Close</span>
</button> </button>
</div> </div>
<div id="gallery-grid" class="flex-1 overflow-y-auto p-4 grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-3 content-start select-none"></div> <div id="gallery-grid" class="flex-1 overflow-y-auto p-4 grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-3 content-start select-none"></div>
@@ -1763,28 +1978,28 @@ if (str_starts_with($route, '/api/')) {
<div id="select-bar" class="fixed bottom-16 left-1/2 -translate-x-1/2 z-[45] bg-t-surface border border-t-border shadow-2xl px-4 py-2 flex items-center gap-3 hidden" style="border-radius:6px;"> <div id="select-bar" class="fixed bottom-16 left-1/2 -translate-x-1/2 z-[45] bg-t-surface border border-t-border shadow-2xl px-4 py-2 flex items-center gap-3 hidden" style="border-radius:6px;">
<span id="select-count" class="font-pixel text-[10px] text-t-accent"></span> <span id="select-count" class="font-pixel text-[10px] text-t-accent"></span>
<button id="select-group" 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"> <button id="select-group" 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-stack text-sm"></i><span class="font-pixel text-[10px]">group</span> <i class="ph ph-stack text-sm"></i><span class="font-pixel text-[10px]">Group</span>
</button> </button>
<button id="select-bucket" 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"> <button id="select-bucket" 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-folder text-sm"></i><span class="font-pixel text-[10px]">bucket</span> <i class="ph ph-folder text-sm"></i><span class="font-pixel text-[10px]">Bucket</span>
</button> </button>
<button id="select-color" 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"> <button id="select-color" 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-palette text-sm"></i><span class="font-pixel text-[10px]">color</span> <i class="ph ph-palette text-sm"></i><span class="font-pixel text-[10px]">Color</span>
</button> </button>
<button id="select-tag" 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"> <button id="select-tag" 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-hash text-sm"></i><span class="font-pixel text-[10px]">tag</span> <i class="ph ph-hash text-sm"></i><span class="font-pixel text-[10px]">Tag</span>
</button> </button>
<button id="select-unbucket" 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"> <button id="select-unbucket" 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-folder-minus text-sm"></i><span class="font-pixel text-[10px]">unsort</span> <i class="ph ph-folder-minus text-sm"></i><span class="font-pixel text-[10px]">Unsort</span>
</button> </button>
<button id="select-export" 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"> <button id="select-export" 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-download-simple text-sm"></i><span class="font-pixel text-[10px]">export</span> <i class="ph ph-download-simple text-sm"></i><span class="font-pixel text-[10px]">Export</span>
</button> </button>
<button id="select-delete" 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"> <button id="select-delete" 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-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> </button>
<button id="select-clear" 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"> <button id="select-clear" 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]">clear</span> <i class="ph ph-x text-sm"></i><span class="font-pixel text-[10px]">Clear</span>
</button> </button>
</div> </div>
@@ -1810,19 +2025,19 @@ if (str_starts_with($route, '/api/')) {
<div class="flex items-center justify-between px-4 py-2.5 border-b border-t-border shrink-0"> <div class="flex items-center justify-between px-4 py-2.5 border-b border-t-border shrink-0">
<div class="flex items-center gap-3"><i class="ph ph-file-plus text-t-accent text-sm"></i><h2 id="modal-title" class="font-pixel text-xs text-t-accent">new_card</h2></div> <div class="flex items-center gap-3"><i class="ph ph-file-plus text-t-accent text-sm"></i><h2 id="modal-title" class="font-pixel text-xs text-t-accent">new_card</h2></div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button id="modal-cancel" 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> <button id="modal-cancel" 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>
<button id="modal-save" 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-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">save</span></button> <button id="modal-save" 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-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">Save</span></button>
<button id="modal-save-view" 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-eye text-sm"></i><span class="font-pixel text-[10px]">save+view</span></button> <button id="modal-save-view" 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-eye text-sm"></i><span class="font-pixel text-[10px]">Save+view</span></button>
<button id="modal-save-close" class="flex items-center gap-1.5 px-2.5 py-1 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">save+close</span></button> <button id="modal-save-close" class="flex items-center gap-1.5 px-2.5 py-1 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">Save+close</span></button>
</div> </div>
</div> </div>
<div class="flex items-center gap-3 px-4 py-2.5 border-b border-t-border shrink-0"> <div class="flex items-center gap-3 px-4 py-2.5 border-b border-t-border shrink-0">
<input type="text" id="inp-title" placeholder="> title..." class="flex-1 bg-transparent border-none text-t-bright text-base font-bold focus:outline-none placeholder:text-t-dim font-pixel"> <input type="text" id="inp-title" placeholder="> title..." class="flex-1 bg-transparent border-none text-t-bright text-base font-bold focus:outline-none placeholder:text-t-dim font-pixel">
<button id="inp-due-date" class="flex items-center gap-1 px-2 py-1 text-[10px] font-pixel border border-t-border text-t-dim hover:border-t-accent hover:text-t-accent transition-colors rounded-sm shrink-0" title="set due date"> <button id="inp-due-date" class="flex items-center gap-1 px-2 py-1 text-[10px] font-pixel border border-t-border text-t-dim hover:border-t-accent hover:text-t-accent transition-colors rounded-sm shrink-0" title="set due date">
<i class="ph ph-clock-countdown text-xs"></i><span id="inp-due-label">no due</span> <i class="ph ph-clock-countdown text-xs"></i><span id="inp-due-label">No due</span>
</button> </button>
<button id="inp-done-toggle" class="flex items-center gap-1 px-2 py-1 text-[10px] font-pixel border border-t-border text-t-dim hover:border-t-accent hover:text-t-accent transition-colors rounded-sm shrink-0" title="mark done"> <button id="inp-done-toggle" class="flex items-center gap-1 px-2 py-1 text-[10px] font-pixel border border-t-border text-t-dim hover:border-t-accent hover:text-t-accent transition-colors rounded-sm shrink-0" title="mark done">
<i class="ph ph-check-circle text-xs"></i><span id="inp-done-label">done</span> <i class="ph ph-check-circle text-xs"></i><span id="inp-done-label">Done</span>
</button> </button>
<input type="hidden" id="inp-due-value" value=""> <input type="hidden" id="inp-due-value" value="">
<select id="inp-bucket" 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="">no bucket</option></select> <select id="inp-bucket" 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="">no bucket</option></select>
@@ -1852,7 +2067,7 @@ if (str_starts_with($route, '/api/')) {
<?= PluginManager::instance()->slot('md-toolbar') ?> <?= PluginManager::instance()->slot('md-toolbar') ?>
<div class="flex-1"></div> <div class="flex-1"></div>
<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-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> <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>
<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 shrink-0 border-r border-t-border" style="width:3.5em; padding:1rem 0.25rem 1rem 0.5rem; overflow-y:scroll; scrollbar-width:none; -ms-overflow-style:none;"></div> <div id="editor-gutter" class="hidden bg-t-bg text-t-dim text-right font-mono text-sm select-none shrink-0 border-r border-t-border" style="width:3.5em; padding:1rem 0.25rem 1rem 0.5rem; overflow-y:scroll; scrollbar-width:none; -ms-overflow-style:none;"></div>
+29 -12
View File
@@ -126,10 +126,10 @@ importFile?.addEventListener('change', async e => {
// Check if it's a selective export (merge) or ask for full replace // Check if it's a selective export (merge) or ask for full replace
const form = new FormData(); form.append('file', file); const form = new FormData(); form.append('file', file);
const mode = await showDialog({ title: 'import_data', message: 'How do you want to import?', okLabel: 'merge (add to existing)', cancelLabel: 'replace (wipe all)' }); const mode = await showDialog({ title: 'import_data', message: 'How do you want to import?', okLabel: 'Merge (add to existing)', cancelLabel: 'Replace (wipe all)' });
// mode: true = merge, null = replace // mode: true = merge, null = replace
if (mode === null) { if (mode === null) {
const sure = await showDialog({ title: 'replace_all', message: 'All existing data will be replaced!', danger: true, okLabel: 'replace' }); const sure = await showDialog({ title: 'replace_all', message: 'All existing data will be replaced!', danger: true, okLabel: 'Replace' });
if (!sure) { e.target.value = ''; return; } if (!sure) { e.target.value = ''; return; }
} }
try { try {
@@ -145,7 +145,7 @@ importFile?.addEventListener('change', async e => {
// ── Auth ── // ── Auth ──
document.getElementById('logout-btn')?.addEventListener('click', async () => { document.getElementById('logout-btn')?.addEventListener('click', async () => {
const ok = await showDialog({ title: 'logout', message: 'Log out?', okLabel: 'logout', danger: true }); const ok = await showDialog({ title: 'logout', message: 'Log out?', okLabel: 'Logout', danger: true });
if (!ok) return; if (!ok) return;
await fetch('api/auth/logout', { method: 'POST' }); await fetch('api/auth/logout', { method: 'POST' });
location.reload(); location.reload();
@@ -159,7 +159,7 @@ async function checkAuth() {
if (data.setup) { if (data.setup) {
let pw = null; let pw = null;
while (!pw || pw.length < 4) { while (!pw || pw.length < 4) {
pw = await showDialog({ title: 'setup', message: 'Welcome to hyprcrd. Set a password:', input: 'password', placeholder: '> password (min. 4 chars)...', okLabel: 'set', cancelLabel: '' }); pw = await showDialog({ title: 'setup', message: 'Welcome to hyprcrd. Set a password:', input: 'password', placeholder: '> password (min. 4 chars)...', okLabel: 'Set', cancelLabel: '' });
if (pw && pw.length < 4) pw = null; if (pw && pw.length < 4) pw = null;
} }
const r = await fetch('api/auth/setup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: pw }) }); const r = await fetch('api/auth/setup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: pw }) });
@@ -169,7 +169,7 @@ async function checkAuth() {
} else if (!data.logged_in) { } else if (!data.logged_in) {
let ok = false; let ok = false;
while (!ok) { while (!ok) {
const pw = await showDialog({ title: 'login', message: '', input: 'password', placeholder: '> password...', okLabel: 'login', cancelLabel: '' }); const pw = await showDialog({ title: 'login', message: '', input: 'password', placeholder: '> password...', okLabel: 'Login', cancelLabel: '' });
if (!pw) continue; if (!pw) continue;
const r = await fetch('api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: pw }) }); const r = await fetch('api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: pw }) });
const d = await r.json(); const d = await r.json();
@@ -204,7 +204,15 @@ document.addEventListener('keydown', e => {
if (e.key === 'Escape') { if (e.key === 'Escape') {
const qc = document.getElementById('quick-capture'); const qc = document.getElementById('quick-capture');
if (!qc.classList.contains('hidden')) { qc.classList.add('hidden'); document.getElementById('quick-capture-input').value = ''; return; } if (!qc.classList.contains('hidden')) { qc.classList.add('hidden'); document.getElementById('quick-capture-input').value = ''; return; }
if (detail.classList.contains('open')) closeDetail(); if (detail.classList.contains('open')) {
// If the user chose "exit zen first", ESC leaves zen mode before closing the card.
const escAction = localStorage.getItem('hyprcrd-esc-action') || 'close';
if (escAction === 'exitZen' && detail.classList.contains('zen')) {
document.getElementById('detail-zen')?.click();
return;
}
closeDetail();
}
else if (modal.classList.contains('open')) closeModal(); else if (modal.classList.contains('open')) closeModal();
else if (search.classList.contains('open')) closeSearch(); else if (search.classList.contains('open')) closeSearch();
else if (tasks?.classList.contains('open')) closeTasks(); else if (tasks?.classList.contains('open')) closeTasks();
@@ -236,14 +244,23 @@ document.getElementById('quick-capture-input')?.addEventListener('keydown', asyn
return; return;
} }
if (e.key !== 'Enter') return; if (e.key !== 'Enter') return;
const title = e.target.value.trim(); const raw = e.target.value.trim();
if (!title) return; if (!raw) return;
e.preventDefault(); e.preventDefault();
// Split: first line = title, rest = body // Split on first "|" — title before, body after.
const lines = title.split(/\n/); // Also accept newlines as a fallback (first line = title, rest = body).
const cardTitle = lines[0]; let cardTitle, body;
let body = lines.slice(1).join('\n').trim(); const pipeIdx = raw.indexOf('|');
if (pipeIdx >= 0) {
cardTitle = raw.slice(0, pipeIdx).trim();
body = raw.slice(pipeIdx + 1).trim();
} else {
const lines = raw.split(/\n/);
cardTitle = lines[0].trim();
body = lines.slice(1).join('\n').trim();
}
if (!cardTitle) return;
if (!body.includes('#inbox')) body = (body ? body + '\n\n' : '') + '#inbox'; if (!body.includes('#inbox')) body = (body ? body + '\n\n' : '') + '#inbox';
await api('api/cards', { method: 'POST', body: { title: cardTitle, body, color: '#38bdf8', position: -1 } }); await api('api/cards', { method: 'POST', body: { title: cardTitle, body, color: '#38bdf8', position: -1 } });
+2 -2
View File
@@ -217,8 +217,8 @@
message, message,
input: false, input: false,
danger: opts.danger ?? true, danger: opts.danger ?? true,
okLabel: opts.okLabel || 'ok', okLabel: opts.okLabel || 'Ok',
cancelLabel: opts.cancelLabel || 'cancel', cancelLabel: opts.cancelLabel || 'Cancel',
}); });
}, },
}; };
+6 -6
View File
@@ -78,7 +78,7 @@ document.addEventListener('keydown', async e => {
e.preventDefault(); e.preventDefault();
if (localStorage.getItem('hyprcrd-confirm-delete') !== '0') { if (localStorage.getItem('hyprcrd-confirm-delete') !== '0') {
const { showDialog } = await import('./dialog.js'); 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; if (!ok) return;
} }
for (const id of selectedIds) await api(`api/cards/${id}`, { method: 'DELETE' }); 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 () => { $('select-color')?.addEventListener('click', async () => {
if (!selectedIds.size) return; if (!selectedIds.size) return;
const { showDialog } = await import('./dialog.js'); 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; if (!color || !/^#[0-9a-fA-F]{6}$/.test(color.trim())) return;
for (const id of selectedIds) { for (const id of selectedIds) {
await api(`api/cards/${id}`, { method: 'PATCH', body: { color: color.trim() } }); 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 () => { $('select-tag')?.addEventListener('click', async () => {
if (!selectedIds.size) return; if (!selectedIds.size) return;
const { showDialog } = await import('./dialog.js'); 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; if (!tag) return;
const cleanTag = tag.trim().replace(/^#/, ''); const cleanTag = tag.trim().replace(/^#/, '');
if (!cleanTag) return; if (!cleanTag) return;
@@ -215,7 +215,7 @@ $('select-delete')?.addEventListener('click', async () => {
if (!selectedIds.size) return; if (!selectedIds.size) return;
if (localStorage.getItem('hyprcrd-confirm-delete') !== '0') { if (localStorage.getItem('hyprcrd-confirm-delete') !== '0') {
const { showDialog } = await import('./dialog.js'); 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; if (!ok) return;
} }
for (const id of selectedIds) { for (const id of selectedIds) {
@@ -418,7 +418,7 @@ export function renderCardHtml(c, inGroup = false, recent = []) {
${loaded ${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>`) ? (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>`} : `<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"> <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.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>` : ''} ${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.dataset.groupId = String(state.activeGroup);
bar.innerHTML = ` 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"> <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> </button>
<i class="ph ph-stack text-t-accent text-sm"></i> <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> <span class="font-pixel text-[10px] text-t-accent">${esc(parent?.title || 'group')}</span>
+10 -10
View File
@@ -25,16 +25,16 @@ async function showBucketCtx(x, y, bucket) {
const hasCards = state.cards.some(c => c.bucket_id == bucket.id); const hasCards = state.cards.some(c => c.bucket_id == bucket.id);
ctxMenu.innerHTML = `<div class="py-1"> 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="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="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> <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 ${!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 ${!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> <div style="height:1px;background:var(--t-border);margin:2px 0"></div>
${hasCards ${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" 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-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 danger" data-baction="delete-keep"><i class="ph ph-trash text-sm"></i><span class="font-pixel text-[10px]">Delete</span></button>`
} }
</div> </div>
<div id="bctx-colors" class="hidden px-3 py-2 border-t border-t-border"> <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(); hideBucketCtx();
const cards = state.cards.filter(c => c.bucket_id == bucket.id); const cards = state.cards.filter(c => c.bucket_id == bucket.id);
if (cards.length > 0 || localStorage.getItem('hyprcrd-confirm-delete') !== '0') { 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 (!ok) return;
} }
if (state.activeBucket == bucket.id) state.activeBucket = 'all'; if (state.activeBucket == bucket.id) state.activeBucket = 'all';
@@ -86,7 +86,7 @@ async function showBucketCtx(x, y, bucket) {
else if (a === 'delete-all') { else if (a === 'delete-all') {
hideBucketCtx(); hideBucketCtx();
const cards = state.cards.filter(c => c.bucket_id == bucket.id); 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; if (!ok) return;
for (const c of cards) await api(`api/cards/${c.id}`, { method: 'DELETE' }); for (const c of cards) await api(`api/cards/${c.id}`, { method: 'DELETE' });
if (state.activeBucket == bucket.id) state.activeBucket = 'all'; if (state.activeBucket == bucket.id) state.activeBucket = 'all';
@@ -165,7 +165,7 @@ function renderBar() {
const active = state.activeBucket == b.id; 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>`; 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('')} }).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(); updateHeaderTint();
+25 -25
View File
@@ -37,28 +37,28 @@ export function showContextMenu(x, y, card) {
const isChild = !!card.parent_id; const isChild = !!card.parent_id;
items.innerHTML = ` 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="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="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> <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>` : ''} ${/^\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> <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>` : ''} ${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="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="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="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> <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="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="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="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> ${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="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="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="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> ${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="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="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> <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 // Color sub-panel
const grid = $('ctx-color-grid'); const grid = $('ctx-color-grid');
@@ -77,7 +77,7 @@ export function showContextMenu(x, y, card) {
members.classList.add('hidden'); members.classList.add('hidden');
// Bucket sub-panel // 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(''); 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'); buckets.classList.add('hidden');
@@ -97,7 +97,7 @@ export function showContextMenu(x, y, card) {
<span class="font-pixel text-[10px]">#${esc(t)}</span> <span class="font-pixel text-[10px]">#${esc(t)}</span>
</button>`).join('') + </button>`).join('') +
(f && !allTags.includes(f) ? `<button class="ctx-item text-t-dim" data-ctx-tag-new="${f}"> (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>` : ''); </button>` : '');
tagList.querySelectorAll('[data-ctx-tag]').forEach(btn => btn.addEventListener('click', async e => { 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); const hasDate = /^\d{4}-\d{2}-\d{2}/.test(card.title);
if (hasDate) { if (hasDate) {
const curDate = card.title.substring(0, 10); 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; if (!newDate) return;
const newTitle = card.title.replace(/^\d{4}-\d{2}-\d{2}/, newDate.trim()); 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 } }); 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') { else if (a === 'move-date') {
hideContextMenu(); hideContextMenu();
const curDate = card.title.substring(0, 10); 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; if (!newDate || newDate === curDate) return;
const newTitle = card.title.replace(/^\d{4}-\d{2}-\d{2}/, newDate.trim()); const newTitle = card.title.replace(/^\d{4}-\d{2}-\d{2}/, newDate.trim());
await api(`api/cards/${card.id}`, { method: 'PATCH', body: { title: newTitle } }); 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') { else if (a === 'due-date') {
hideContextMenu(); 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; if (newDate === null) return;
await api(`api/cards/${card.id}`, { method: 'PATCH', body: { due_date: newDate || null } }); await api(`api/cards/${card.id}`, { method: 'PATCH', body: { due_date: newDate || null } });
await window._reloadAll(); 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 === '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 === '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') { 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(); } 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(); } 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(); hideContextMenu();
const needConfirm = localStorage.getItem('hyprcrd-confirm-delete') !== '0'; const needConfirm = localStorage.getItem('hyprcrd-confirm-delete') !== '0';
if (needConfirm) { 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; if (!ok) return;
} }
const wasSync = !!card.caldav_uid; const wasSync = !!card.caldav_uid;
+5 -3
View File
@@ -417,6 +417,7 @@ function renderSidebar(card) {
if (groupCards && groupCards.length > 1) { if (groupCards && groupCards.length > 1) {
sidebar.classList.remove('hidden'); sidebar.classList.remove('hidden');
sidebar.dataset.groupId = String(groupCards[0].id);
$('detail-sidebar-count').textContent = groupCards.length; $('detail-sidebar-count').textContent = groupCards.length;
list.innerHTML = groupCards.map(gc => { list.innerHTML = groupCards.map(gc => {
const active = gc.id == card.id; const active = gc.id == card.id;
@@ -438,6 +439,7 @@ function renderSidebar(card) {
}); });
} else { } else {
sidebar.classList.add('hidden'); 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('text-t-accent', isDone);
btn.classList.toggle('border-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) { 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> <i class="ph ph-arrow-left text-sm"></i><span class="font-pixel text-[10px]">${splitLastQuery ? 'results' : 'related cards'}</span>
</button> </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"> <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> </button>
</div>`; </div>`;
splitPicks.querySelector('#split-back').addEventListener('click', restoreSplitList); splitPicks.querySelector('#split-back').addEventListener('click', restoreSplitList);
@@ -1015,7 +1017,7 @@ $('detail-done')?.addEventListener('click', async () => {
$('detail-delete')?.addEventListener('click', async () => { $('detail-delete')?.addEventListener('click', async () => {
if (!state.viewingCard) return; if (!state.viewingCard) return;
if (localStorage.getItem('hyprcrd-confirm-delete') !== '0') { 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; if (!ok) return;
} }
pushCardDelete('delete', snapshotCard(state.viewingCard)); pushCardDelete('delete', snapshotCard(state.viewingCard));
+1 -1
View File
@@ -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 => { return new Promise(res => {
resolve = res; resolve = res;
titleEl.textContent = title; titleEl.textContent = title;
+3 -3
View File
@@ -116,7 +116,7 @@ const inpDueBtn = $('inp-due-date');
function setDueDate(date) { function setDueDate(date) {
if (inpDueValue) inpDueValue.value = date || ''; if (inpDueValue) inpDueValue.value = date || '';
if (inpDueLabel) inpDueLabel.textContent = date || 'no due'; if (inpDueLabel) inpDueLabel.textContent = date || 'No due';
if (inpDueBtn) { if (inpDueBtn) {
inpDueBtn.classList.toggle('text-t-accent', !!date); inpDueBtn.classList.toggle('text-t-accent', !!date);
inpDueBtn.classList.toggle('border-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', title: current ? 'change due date' : 'set due date',
input: 'date', input: 'date',
defaultValue: current || new Date().toISOString().substring(0, 10), defaultValue: current || new Date().toISOString().substring(0, 10),
okLabel: current ? 'update' : 'set', okLabel: current ? 'Update' : 'Set',
}); });
if (result === null) return; if (result === null) return;
setDueDate(result || ''); setDueDate(result || '');
@@ -147,7 +147,7 @@ function updateDoneToggle() {
inpDoneToggle.classList.toggle('border-t-accent', isDone); inpDoneToggle.classList.toggle('border-t-accent', isDone);
inpDoneToggle.classList.toggle('text-t-dim', !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', () => { inpDoneToggle?.addEventListener('click', () => {
+5 -5
View File
@@ -156,7 +156,7 @@ export async function openGallery() {
e.stopPropagation(); e.stopPropagation();
const name = btn.dataset.delOrphan; const name = btn.dataset.delOrphan;
if (localStorage.getItem('hyprcrd-confirm-delete') !== '0') { 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; if (!ok) return;
} }
await fetch(`api/uploads/${name}`, { method: 'DELETE', headers: { 'X-CSRF-Token': getCsrf() } }); 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);"> <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;"> <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;"> <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>
<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;"> <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>
<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;"> <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> </button>
</div> </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"> <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 () => { 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; if (!ok) return;
await fetch(`api/uploads/${file.name}`, { method: 'DELETE', headers: { 'X-CSRF-Token': getCsrf() } }); await fetch(`api/uploads/${file.name}`, { method: 'DELETE', headers: { 'X-CSRF-Token': getCsrf() } });
close(); close();
+320 -4
View File
@@ -10,7 +10,7 @@ const themeList = $('settings-theme-list');
// Render theme list in settings // Render theme list in settings
function renderThemes() { function renderThemes() {
if (!themeList) return; if (!themeList) return;
const current = localStorage.getItem('hyprcrd-theme') || 'slate'; const current = (designPending?.theme) || localStorage.getItem('hyprcrd-theme') || 'slate';
themeList.innerHTML = ''; themeList.innerHTML = '';
for (const [key, t] of Object.entries(window.themes)) { for (const [key, t] of Object.entries(window.themes)) {
const btn = document.createElement('button'); 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> 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> <span class="font-pixel text-[10px] flex-1">${t.label}</span>
${active ? '<i class="ph ph-check text-t-accent text-sm"></i>' : ''}`; ${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); themeList.appendChild(btn);
} }
} }
@@ -36,10 +45,311 @@ overlay?.querySelectorAll('.settings-tab').forEach(tab => {
if (panel === 'relay') { window._loadRelaySettings?.(); } if (panel === 'relay') { window._loadRelaySettings?.(); }
if (panel === 'notifications') renderNotifSettings(); if (panel === 'notifications') renderNotifSettings();
if (panel === 'behavior') initBehaviorSettings(); if (panel === 'behavior') initBehaviorSettings();
if (panel === 'theme') initDesignSettings();
if (panel === 'plugins') window._openPluginSettings?.(); 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 // Open/Close
function updateStats() { function updateStats() {
const sc = $('stats-cards'); if (sc) sc.textContent = state.cards?.length || 0; const sc = $('stats-cards'); if (sc) sc.textContent = state.cards?.length || 0;
@@ -77,12 +387,17 @@ async function loadVaultStatus() {
$('settings-btn')?.addEventListener('click', () => { $('settings-btn')?.addEventListener('click', () => {
renderThemes(); renderThemes();
initDesignSettings();
updateStats(); updateStats();
loadVaultStatus(); loadVaultStatus();
overlay?.classList.add('open'); overlay?.classList.add('open');
}); });
$('settings-close')?.addEventListener('click', () => overlay?.classList.remove('open')); function closeSettings() {
overlay?.addEventListener('click', e => { if (e.target === overlay) overlay?.classList.remove('open'); }); overlay?.classList.remove('open');
setPeekUI(false);
}
$('settings-close')?.addEventListener('click', closeSettings);
overlay?.addEventListener('click', e => { if (e.target === overlay) closeSettings(); });
// Password change // Password change
$('settings-pw-save')?.addEventListener('click', async () => { $('settings-pw-save')?.addEventListener('click', async () => {
@@ -123,6 +438,7 @@ function initBehaviorSettings() {
'setting-card-width': { key: 'hyprcrd-card-width', def: '280' }, 'setting-card-width': { key: 'hyprcrd-card-width', def: '280' },
'setting-body-lines': { key: 'hyprcrd-body-lines', def: '5' }, 'setting-body-lines': { key: 'hyprcrd-body-lines', def: '5' },
'setting-click-action': { key: 'hyprcrd-click-action', def: 'preview' }, 'setting-click-action': { key: 'hyprcrd-click-action', def: 'preview' },
'setting-esc-action': { key: 'hyprcrd-esc-action', def: 'close' },
}; };
const toggles = { const toggles = {
'setting-current-include-synced': { key: 'hyprcrd-current-include-synced', def: '0' }, 'setting-current-include-synced': { key: 'hyprcrd-current-include-synced', def: '0' },
+8 -2
View File
@@ -132,7 +132,8 @@ PluginManager::instance()->onApiRoute(function (string $route, string $method) {
} }
$envelope = chatEncrypt($body, $r['key']); $envelope = chatEncrypt($body, $r['key']);
$ch = curl_init(rtrim($r['url'], '/') . '/api/chat'); $relayChatUrl = rtrim($r['url'], '/') . '/api/chat';
$ch = curl_init($relayChatUrl);
curl_setopt_array($ch, [ curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true, CURLOPT_POST => true,
@@ -152,7 +153,12 @@ PluginManager::instance()->onApiRoute(function (string $route, string $method) {
$relayErr = ''; $relayErr = '';
$decoded = json_decode($response, true); $decoded = json_decode($response, true);
if (is_array($decoded) && !empty($decoded['error'])) $relayErr = ' — ' . $decoded['error']; if (is_array($decoded) && !empty($decoded['error'])) $relayErr = ' — ' . $decoded['error'];
chatJson(['ok' => false, 'error' => "relay returned HTTP $status$relayErr"]); chatJson([
'ok' => false,
'error' => "relay returned HTTP $status$relayErr",
'url' => $relayChatUrl,
'response_body' => is_string($response) ? substr($response, 0, 500) : null,
]);
} }
$resData = json_decode($response, true); $resData = json_decode($response, true);
+3 -1
View File
@@ -9,7 +9,8 @@
max-height: calc(100vh - 100px); max-height: calc(100vh - 100px);
background: var(--t-surface); background: var(--t-surface);
border: 1px solid var(--t-border); border: 1px solid var(--t-border);
box-shadow: 0 8px 32px rgba(0,0,0,0.5); border-radius: var(--ui-radius-modal);
box-shadow: var(--ui-shadow-modal);
z-index: 100; z-index: 100;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -277,6 +278,7 @@
padding: 4px 10px; padding: 4px 10px;
font-size: 12px; font-size: 12px;
border: 1px solid var(--t-border); border: 1px solid var(--t-border);
border-radius: var(--ui-radius-button);
color: var(--t-muted); color: var(--t-muted);
background: transparent; background: transparent;
cursor: pointer; cursor: pointer;
+1 -1
View File
@@ -15,7 +15,7 @@
<div id="chat-empty" class="hidden chat-empty"> <div id="chat-empty" class="hidden chat-empty">
<i class="ph ph-chats text-2xl text-t-dim mb-2"></i> <i class="ph ph-chats text-2xl text-t-dim mb-2"></i>
<p class="font-pixel text-[10px] text-t-dim">no conversations yet</p> <p class="font-pixel text-[10px] text-t-dim">no conversations yet</p>
<button id="chat-empty-start" class="mt-3 px-2 py-1 text-[10px] font-pixel border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors">start chat</button> <button id="chat-empty-start" class="mt-3 px-2 py-1 text-[10px] font-pixel border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors">Start chat</button>
</div> </div>
</div> </div>
+19 -2
View File
@@ -235,14 +235,31 @@ async function sendMessage(e) {
body: { peer: currentPeer, body }, body: { peer: currentPeer, body },
}); });
if (!res.ok) { if (!res.ok) {
alert('Send failed: ' + (res.error || 'unknown')); const detail = [
res.error || 'unknown',
res.url ? `URL: ${res.url}` : null,
res.response_body ? `Body: ${res.response_body}` : null,
].filter(Boolean).join('\n');
await hyprcrd.dialog.show({
title: 'send failed',
message: detail,
danger: true,
okLabel: 'Ok',
cancelLabel: '',
});
return; return;
} }
input.value = ''; input.value = '';
await loadThread(currentPeer); await loadThread(currentPeer);
await loadConversations(); await loadConversations();
} catch (err) { } catch (err) {
alert('Send error: ' + err.message); await hyprcrd.dialog.show({
title: 'send error',
message: String(err?.message || err),
danger: true,
okLabel: 'Ok',
cancelLabel: '',
});
} finally { } finally {
sendBtn.disabled = false; sendBtn.disabled = false;
input.disabled = false; input.disabled = false;
+10 -10
View File
@@ -10,8 +10,8 @@
<button id="email-refresh" class="toolbar-btn" title="Refresh"><i class="ph ph-arrows-clockwise text-sm"></i></button> <button id="email-refresh" class="toolbar-btn" title="Refresh"><i class="ph ph-arrows-clockwise text-sm"></i></button>
<span id="email-status" class="text-[10px] text-t-dim font-pixel"></span> <span id="email-status" class="text-[10px] text-t-dim font-pixel"></span>
<div class="flex-1"></div> <div class="flex-1"></div>
<button id="email-show-settings" class="flex items-center gap-1.5 px-2 py-1 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors" title="Manage accounts in settings"><i class="ph ph-gear text-sm"></i><span class="font-pixel text-[10px]">accounts</span></button> <button id="email-show-settings" class="flex items-center gap-1.5 px-2 py-1 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors" title="Manage accounts in settings"><i class="ph ph-gear text-sm"></i><span class="font-pixel text-[10px]">Accounts</span></button>
<button id="email-close" class="flex items-center gap-1.5 px-2 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> <button id="email-close" class="flex items-center gap-1.5 px-2 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>
<!-- ── Welcome view (shown when no accounts) ── --> <!-- ── Welcome view (shown when no accounts) ── -->
@@ -27,7 +27,7 @@
</p> </p>
<button id="email-welcome-configure" class="inline-flex items-center gap-2 px-4 py-2 text-xs bg-t-accent text-t-bg border border-t-accent hover:brightness-110 transition-all"> <button id="email-welcome-configure" class="inline-flex items-center gap-2 px-4 py-2 text-xs bg-t-accent text-t-bg border border-t-accent hover:brightness-110 transition-all">
<i class="ph ph-gear text-sm"></i> <i class="ph ph-gear text-sm"></i>
<span class="font-pixel text-[11px]">configure accounts</span> <span class="font-pixel text-[11px]">Configure accounts</span>
</button> </button>
<p class="text-[10px] text-t-dim mt-4 font-pixel">settings → mail</p> <p class="text-[10px] text-t-dim mt-4 font-pixel">settings → mail</p>
</div> </div>
@@ -55,13 +55,13 @@
<span id="email-selection-count" class="font-pixel text-[10px] text-t-accent">0 selected</span> <span id="email-selection-count" class="font-pixel text-[10px] text-t-accent">0 selected</span>
<div class="flex-1"></div> <div class="flex-1"></div>
<button id="email-selection-mark-spam" class="flex items-center gap-1 px-2 py-0.5 text-xs border border-t-danger text-t-danger hover:bg-t-danger/10 transition-colors" title="Mark sender as spam (future mail from same sender will be flagged)"> <button id="email-selection-mark-spam" class="flex items-center gap-1 px-2 py-0.5 text-xs border border-t-danger text-t-danger hover:bg-t-danger/10 transition-colors" title="Mark sender as spam (future mail from same sender will be flagged)">
<i class="ph ph-warning-octagon text-sm"></i><span class="font-pixel text-[9px]">spam</span> <i class="ph ph-warning-octagon text-sm"></i><span class="font-pixel text-[9px]">Spam</span>
</button> </button>
<button id="email-selection-mark-ham" class="flex items-center gap-1 px-2 py-0.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors" title="Mark sender as not spam (allowlist)"> <button id="email-selection-mark-ham" class="flex items-center gap-1 px-2 py-0.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors" title="Mark sender as not spam (allowlist)">
<i class="ph ph-check-circle text-sm"></i><span class="font-pixel text-[9px]">not spam</span> <i class="ph ph-check-circle text-sm"></i><span class="font-pixel text-[9px]">Not spam</span>
</button> </button>
<button id="email-selection-delete" class="flex items-center gap-1 px-2 py-0.5 text-xs border border-t-danger text-t-danger hover:bg-t-danger/10 transition-colors" title="Delete selected on server"> <button id="email-selection-delete" class="flex items-center gap-1 px-2 py-0.5 text-xs border border-t-danger text-t-danger hover:bg-t-danger/10 transition-colors" title="Delete selected on server">
<i class="ph ph-trash text-sm"></i><span class="font-pixel text-[9px]">delete</span> <i class="ph ph-trash text-sm"></i><span class="font-pixel text-[9px]">Delete</span>
</button> </button>
<button id="email-selection-clear" class="flex items-center gap-1 px-2 py-0.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors" title="Clear selection"> <button id="email-selection-clear" class="flex items-center gap-1 px-2 py-0.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors" title="Clear selection">
<i class="ph ph-x text-sm"></i> <i class="ph ph-x text-sm"></i>
@@ -90,10 +90,10 @@
<span><span class="text-t-muted">date:</span> <span id="email-msg-date" class="text-t-text"></span></span> <span><span class="text-t-muted">date:</span> <span id="email-msg-date" class="text-t-text"></span></span>
</div> </div>
<div class="mt-3 flex items-center gap-2 flex-wrap"> <div class="mt-3 flex items-center gap-2 flex-wrap">
<button id="email-extract-btn" class="flex items-center gap-1.5 px-3 py-1 text-xs bg-t-accent text-t-bg border border-t-accent hover:brightness-110 transition-all"><i class="ph ph-scissors text-sm"></i><span class="font-pixel text-[10px]">extract as card</span></button> <button id="email-extract-btn" class="flex items-center gap-1.5 px-3 py-1 text-xs bg-t-accent text-t-bg border border-t-accent hover:brightness-110 transition-all"><i class="ph ph-scissors text-sm"></i><span class="font-pixel text-[10px]">Extract as card</span></button>
<button id="email-view-html" class="hidden items-center gap-1.5 px-3 py-1 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors" title="Show HTML rendered version (external images blocked by default)"><i class="ph ph-code text-sm"></i><span class="font-pixel text-[10px]">view as html</span></button> <button id="email-view-html" class="hidden items-center gap-1.5 px-3 py-1 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors" title="Show HTML rendered version (external images blocked by default)"><i class="ph ph-code text-sm"></i><span class="font-pixel text-[10px]">View as html</span></button>
<button id="email-view-text" class="hidden items-center gap-1.5 px-3 py-1 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors" title="Switch back to plain text"><i class="ph ph-text-aa text-sm"></i><span class="font-pixel text-[10px]">view as text</span></button> <button id="email-view-text" class="hidden items-center gap-1.5 px-3 py-1 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors" title="Switch back to plain text"><i class="ph ph-text-aa text-sm"></i><span class="font-pixel text-[10px]">View as text</span></button>
<button id="email-load-images" class="hidden items-center gap-1.5 px-3 py-1 text-xs border border-t-accent2 text-t-accent2 hover:bg-t-accent2/10 transition-colors" title="Load external images (privacy: tracking pixels may be loaded)"><i class="ph ph-image text-sm"></i><span class="font-pixel text-[10px]">load images (<span id="email-blocked-count">0</span>)</span></button> <button id="email-load-images" class="hidden items-center gap-1.5 px-3 py-1 text-xs border border-t-accent2 text-t-accent2 hover:bg-t-accent2/10 transition-colors" title="Load external images (privacy: tracking pixels may be loaded)"><i class="ph ph-image text-sm"></i><span class="font-pixel text-[10px]">Load images (<span id="email-blocked-count">0</span>)</span></button>
<span id="email-extract-status" class="font-pixel text-[9px] text-t-dim"></span> <span id="email-extract-status" class="font-pixel text-[9px] text-t-dim"></span>
</div> </div>
</div> </div>
+7 -7
View File
@@ -165,9 +165,9 @@ function buildSettingsPanel(panelEl) {
<input id="email-acc-password" type="password" autocomplete="off" class="w-full bg-t-bg border border-t-border text-t-bright px-2 py-1.5 text-xs focus:border-t-accent focus:outline-none font-mono"> <input id="email-acc-password" type="password" autocomplete="off" class="w-full bg-t-bg border border-t-border text-t-bright px-2 py-1.5 text-xs focus:border-t-accent focus:outline-none font-mono">
</label> </label>
<div class="flex items-center gap-2 pt-1"> <div class="flex items-center gap-2 pt-1">
<button type="submit" id="email-acc-save" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">save</span></button> <button type="submit" id="email-acc-save" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">Save</span></button>
<button type="button" id="email-acc-test" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors"><i class="ph ph-plug text-sm"></i><span class="font-pixel text-[10px]">test</span></button> <button type="button" id="email-acc-test" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors"><i class="ph ph-plug text-sm"></i><span class="font-pixel text-[10px]">Test</span></button>
<button type="button" id="email-acc-cancel" class="flex items-center gap-1.5 px-3 py-1.5 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]">cancel</span></button> <button type="button" id="email-acc-cancel" class="flex items-center gap-1.5 px-3 py-1.5 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]">Cancel</span></button>
</div> </div>
<div id="email-acc-status" class="hidden mt-1 px-3 py-2 text-[10px] font-mono break-words border border-t-border bg-t-bg"></div> <div id="email-acc-status" class="hidden mt-1 px-3 py-2 text-[10px] font-mono break-words border border-t-border bg-t-bg"></div>
</form> </form>
@@ -297,7 +297,7 @@ async function deleteAccount(id) {
if (!acc) return; if (!acc) return;
const ok = await hyprcrd.dialog.confirm( const ok = await hyprcrd.dialog.confirm(
`Delete email account "${acc.name}"? Stored credentials will be removed but no mail is deleted from the server.`, `Delete email account "${acc.name}"? Stored credentials will be removed but no mail is deleted from the server.`,
{ title: 'delete account', okLabel: 'delete', cancelLabel: 'cancel', danger: true } { title: 'delete account', okLabel: 'Delete', cancelLabel: 'Cancel', danger: true }
); );
if (!ok) return; if (!ok) return;
await hyprcrd.api(`api/email/accounts/${id}`, { method: 'DELETE' }); await hyprcrd.api(`api/email/accounts/${id}`, { method: 'DELETE' });
@@ -573,7 +573,7 @@ async function markSelectedAs(action) {
const verb = action === 'spam' ? 'as spam' : 'as not spam'; const verb = action === 'spam' ? 'as spam' : 'as not spam';
const ok = await hyprcrd.dialog.confirm( const ok = await hyprcrd.dialog.confirm(
`Mark ${n} sender${n === 1 ? '' : 's'} ${verb}? This adds ${action === 'spam' ? 'block' : 'allow'} rules that affect all future messages from the same senders.`, `Mark ${n} sender${n === 1 ? '' : 's'} ${verb}? This adds ${action === 'spam' ? 'block' : 'allow'} rules that affect all future messages from the same senders.`,
{ title: `mark ${verb}`, okLabel: 'mark', cancelLabel: 'cancel', danger: action === 'spam' } { title: `mark ${verb}`, okLabel: 'Mark', cancelLabel: 'Cancel', danger: action === 'spam' }
); );
if (!ok) return; if (!ok) return;
@@ -609,7 +609,7 @@ async function deleteSelected({ skipConfirm = false } = {}) {
if (!skipConfirm) { if (!skipConfirm) {
const ok = await hyprcrd.dialog.confirm( const ok = await hyprcrd.dialog.confirm(
`Permanently delete ${n} message${n === 1 ? '' : 's'} from the server? This cannot be undone.`, `Permanently delete ${n} message${n === 1 ? '' : 's'} from the server? This cannot be undone.`,
{ title: 'delete mail', okLabel: 'delete', cancelLabel: 'cancel', danger: true } { title: 'delete mail', okLabel: 'Delete', cancelLabel: 'Cancel', danger: true }
); );
if (!ok) return; if (!ok) return;
} }
@@ -861,7 +861,7 @@ function injectHeaderButton() {
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.id = 'email-btn'; btn.id = 'email-btn';
btn.className = 'flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors'; btn.className = 'flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors';
btn.innerHTML = '<i class="ph ph-envelope text-sm"></i><span class="font-pixel text-[10px]">mail</span>'; btn.innerHTML = '<i class="ph ph-envelope text-sm"></i><span class="font-pixel text-[10px]">Mail</span>';
btn.addEventListener('click', openEmail); btn.addEventListener('click', openEmail);
const addBtn = document.getElementById('add-btn'); const addBtn = document.getElementById('add-btn');
if (addBtn) header.insertBefore(btn, addBtn); if (addBtn) header.insertBefore(btn, addBtn);
+15 -15
View File
@@ -9,8 +9,8 @@
<select id="gitea-account-select" class="bg-t-bg border border-t-border text-t-bright text-xs font-pixel px-2 py-1 rounded-sm focus:border-t-accent focus:outline-none cursor-pointer min-w-32"></select> <select id="gitea-account-select" class="bg-t-bg border border-t-border text-t-bright text-xs font-pixel px-2 py-1 rounded-sm focus:border-t-accent focus:outline-none cursor-pointer min-w-32"></select>
<span id="gitea-status" class="text-[10px] text-t-dim font-pixel"></span> <span id="gitea-status" class="text-[10px] text-t-dim font-pixel"></span>
<div class="flex-1"></div> <div class="flex-1"></div>
<button id="gitea-show-settings" class="flex items-center gap-1.5 px-2 py-1 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors" title="Manage accounts in settings"><i class="ph ph-gear text-sm"></i><span class="font-pixel text-[10px]">accounts</span></button> <button id="gitea-show-settings" class="flex items-center gap-1.5 px-2 py-1 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors" title="Manage accounts in settings"><i class="ph ph-gear text-sm"></i><span class="font-pixel text-[10px]">Accounts</span></button>
<button id="gitea-close" class="flex items-center gap-1.5 px-2 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> <button id="gitea-close" class="flex items-center gap-1.5 px-2 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>
<!-- ── Welcome view (shown when no accounts) ── --> <!-- ── Welcome view (shown when no accounts) ── -->
@@ -26,7 +26,7 @@
</p> </p>
<button id="gitea-welcome-configure" class="inline-flex items-center gap-2 px-4 py-2 text-xs bg-t-accent text-t-bg border border-t-accent hover:brightness-110 transition-all"> <button id="gitea-welcome-configure" class="inline-flex items-center gap-2 px-4 py-2 text-xs bg-t-accent text-t-bg border border-t-accent hover:brightness-110 transition-all">
<i class="ph ph-gear text-sm"></i> <i class="ph ph-gear text-sm"></i>
<span class="font-pixel text-[11px]">configure accounts</span> <span class="font-pixel text-[11px]">Configure accounts</span>
</button> </button>
<p class="text-[10px] text-t-dim mt-4 font-pixel">settings → gitea</p> <p class="text-[10px] text-t-dim mt-4 font-pixel">settings → gitea</p>
</div> </div>
@@ -126,8 +126,8 @@
<p class="text-[9px] text-t-dim mt-1 italic">project: not supported by gitea api on issue create — set it manually on gitea after creation</p> <p class="text-[9px] text-t-dim mt-1 italic">project: not supported by gitea api on issue create — set it manually on gitea after creation</p>
</div> </div>
<div class="mt-4 flex items-center gap-2"> <div class="mt-4 flex items-center gap-2">
<button id="gitea-create-submit" class="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-t-accent text-t-bg border border-t-accent hover:brightness-110 transition-all"><i class="ph ph-paper-plane-tilt text-sm"></i><span class="font-pixel text-[10px]">create issue</span></button> <button id="gitea-create-submit" class="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-t-accent text-t-bg border border-t-accent hover:brightness-110 transition-all"><i class="ph ph-paper-plane-tilt text-sm"></i><span class="font-pixel text-[10px]">Create issue</span></button>
<button id="gitea-create-cancel-2" class="flex items-center gap-1.5 px-3 py-1.5 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]">cancel</span></button> <button id="gitea-create-cancel-2" class="flex items-center gap-1.5 px-3 py-1.5 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]">Cancel</span></button>
</div> </div>
<div id="gitea-create-status" class="hidden mt-2 px-3 py-2 text-[10px] font-mono break-words border border-t-border bg-t-bg"></div> <div id="gitea-create-status" class="hidden mt-2 px-3 py-2 text-[10px] font-mono break-words border border-t-border bg-t-bg"></div>
</div> </div>
@@ -164,8 +164,8 @@
<div id="gitea-labels-warning" class="hidden mb-2 px-2 py-1 text-[10px] font-mono break-words border border-amber-500 text-amber-400 bg-t-bg"></div> <div id="gitea-labels-warning" class="hidden mb-2 px-2 py-1 text-[10px] font-mono break-words border border-amber-500 text-amber-400 bg-t-bg"></div>
<div id="gitea-labels-checkboxes" class="space-y-1 max-h-48 overflow-y-auto"></div> <div id="gitea-labels-checkboxes" class="space-y-1 max-h-48 overflow-y-auto"></div>
<div class="flex items-center gap-2 mt-2 pt-2 border-t border-t-border"> <div class="flex items-center gap-2 mt-2 pt-2 border-t border-t-border">
<button id="gitea-labels-save" class="flex items-center gap-1.5 px-2 py-0.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">save</span></button> <button id="gitea-labels-save" class="flex items-center gap-1.5 px-2 py-0.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">Save</span></button>
<button id="gitea-labels-cancel" class="flex items-center gap-1.5 px-2 py-0.5 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]">cancel</span></button> <button id="gitea-labels-cancel" class="flex items-center gap-1.5 px-2 py-0.5 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]">Cancel</span></button>
</div> </div>
</div> </div>
<!-- Metadata row: assignees + milestone + due date with inline editor --> <!-- Metadata row: assignees + milestone + due date with inline editor -->
@@ -198,14 +198,14 @@
</div> </div>
</div> </div>
<div class="flex items-center gap-2 mt-3 pt-2 border-t border-t-border"> <div class="flex items-center gap-2 mt-3 pt-2 border-t border-t-border">
<button id="gitea-meta-save" class="flex items-center gap-1.5 px-2 py-0.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">save</span></button> <button id="gitea-meta-save" class="flex items-center gap-1.5 px-2 py-0.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">Save</span></button>
<button id="gitea-meta-cancel" class="flex items-center gap-1.5 px-2 py-0.5 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]">cancel</span></button> <button id="gitea-meta-cancel" class="flex items-center gap-1.5 px-2 py-0.5 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]">Cancel</span></button>
</div> </div>
</div> </div>
<div class="mt-3 flex items-center gap-2 flex-wrap"> <div class="mt-3 flex items-center gap-2 flex-wrap">
<button id="gitea-extract-btn" class="flex items-center gap-1.5 px-3 py-1 text-xs bg-t-accent text-t-bg border border-t-accent hover:brightness-110 transition-all"><i class="ph ph-scissors text-sm"></i><span class="font-pixel text-[10px]">extract as card</span></button> <button id="gitea-extract-btn" class="flex items-center gap-1.5 px-3 py-1 text-xs bg-t-accent text-t-bg border border-t-accent hover:brightness-110 transition-all"><i class="ph ph-scissors text-sm"></i><span class="font-pixel text-[10px]">Extract as card</span></button>
<button id="gitea-state-toggle" class="flex items-center gap-1.5 px-3 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-check-circle text-sm"></i><span class="font-pixel text-[10px]">close</span></button> <button id="gitea-state-toggle" class="flex items-center gap-1.5 px-3 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-check-circle text-sm"></i><span class="font-pixel text-[10px]">Close</span></button>
<a id="gitea-open-link" href="#" target="_blank" rel="noopener" class="inline-flex items-center gap-1.5 px-3 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-square-out text-sm"></i><span class="font-pixel text-[10px]">open on gitea</span></a> <a id="gitea-open-link" href="#" target="_blank" rel="noopener" class="inline-flex items-center gap-1.5 px-3 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-square-out text-sm"></i><span class="font-pixel text-[10px]">Open on gitea</span></a>
</div> </div>
<div id="gitea-extract-status" class="hidden mt-2 px-3 py-2 text-[10px] font-mono break-words border border-t-border bg-t-bg"></div> <div id="gitea-extract-status" class="hidden mt-2 px-3 py-2 text-[10px] font-mono break-words border border-t-border bg-t-bg"></div>
</div> </div>
@@ -218,8 +218,8 @@
<div id="gitea-body-edit" class="hidden"> <div id="gitea-body-edit" class="hidden">
<textarea id="gitea-body-input" class="w-full bg-t-bg border border-t-accent text-t-text px-3 py-2 text-xs focus:outline-none font-mono leading-relaxed" rows="10"></textarea> <textarea id="gitea-body-input" class="w-full bg-t-bg border border-t-accent text-t-text px-3 py-2 text-xs focus:outline-none font-mono leading-relaxed" rows="10"></textarea>
<div class="flex items-center gap-2 mt-2"> <div class="flex items-center gap-2 mt-2">
<button id="gitea-body-save" class="flex items-center gap-1.5 px-3 py-1 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">save</span></button> <button id="gitea-body-save" class="flex items-center gap-1.5 px-3 py-1 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">Save</span></button>
<button id="gitea-body-cancel" class="flex items-center gap-1.5 px-3 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]">cancel</span></button> <button id="gitea-body-cancel" class="flex items-center gap-1.5 px-3 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]">Cancel</span></button>
</div> </div>
</div> </div>
<div id="gitea-detail-comments-section" class="mt-6 hidden"> <div id="gitea-detail-comments-section" class="mt-6 hidden">
@@ -231,7 +231,7 @@
<div class="font-pixel text-[10px] uppercase tracking-wider text-t-dim mb-2"><i class="ph ph-chat-text text-xs"></i> add comment</div> <div class="font-pixel text-[10px] uppercase tracking-wider text-t-dim mb-2"><i class="ph ph-chat-text text-xs"></i> add comment</div>
<textarea id="gitea-comment-input" rows="3" placeholder="markdown supported..." class="w-full bg-t-bg border border-t-border text-t-text px-3 py-2 text-xs focus:border-t-accent focus:outline-none font-mono leading-relaxed"></textarea> <textarea id="gitea-comment-input" rows="3" placeholder="markdown supported..." class="w-full bg-t-bg border border-t-border text-t-text px-3 py-2 text-xs focus:border-t-accent focus:outline-none font-mono leading-relaxed"></textarea>
<div class="flex items-center gap-2 mt-2"> <div class="flex items-center gap-2 mt-2">
<button id="gitea-comment-post" class="flex items-center gap-1.5 px-3 py-1 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-paper-plane-tilt text-sm"></i><span class="font-pixel text-[10px]">post</span></button> <button id="gitea-comment-post" class="flex items-center gap-1.5 px-3 py-1 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-paper-plane-tilt text-sm"></i><span class="font-pixel text-[10px]">Post</span></button>
<span id="gitea-comment-status" class="font-pixel text-[9px] text-t-dim"></span> <span id="gitea-comment-status" class="font-pixel text-[9px] text-t-dim"></span>
</div> </div>
</div> </div>
+7 -7
View File
@@ -114,9 +114,9 @@ function buildSettingsPanel(panelEl) {
<input id="gitea-acc-token" type="password" autocomplete="off" class="w-full bg-t-bg border border-t-border text-t-bright px-2 py-1.5 text-xs focus:border-t-accent focus:outline-none font-mono"> <input id="gitea-acc-token" type="password" autocomplete="off" class="w-full bg-t-bg border border-t-border text-t-bright px-2 py-1.5 text-xs focus:border-t-accent focus:outline-none font-mono">
</label> </label>
<div class="flex items-center gap-2 pt-1"> <div class="flex items-center gap-2 pt-1">
<button type="submit" id="gitea-acc-save" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">save</span></button> <button type="submit" id="gitea-acc-save" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">Save</span></button>
<button type="button" id="gitea-acc-test" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors"><i class="ph ph-plug text-sm"></i><span class="font-pixel text-[10px]">test</span></button> <button type="button" id="gitea-acc-test" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors"><i class="ph ph-plug text-sm"></i><span class="font-pixel text-[10px]">Test</span></button>
<button type="button" id="gitea-acc-cancel" class="flex items-center gap-1.5 px-3 py-1.5 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]">cancel</span></button> <button type="button" id="gitea-acc-cancel" class="flex items-center gap-1.5 px-3 py-1.5 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]">Cancel</span></button>
</div> </div>
<div id="gitea-acc-status" class="hidden mt-1 px-3 py-2 text-[10px] font-mono break-words border border-t-border bg-t-bg"></div> <div id="gitea-acc-status" class="hidden mt-1 px-3 py-2 text-[10px] font-mono break-words border border-t-border bg-t-bg"></div>
</form> </form>
@@ -224,7 +224,7 @@ async function deleteAccount(id) {
if (!acc) return; if (!acc) return;
const ok = await hyprcrd.dialog.confirm( const ok = await hyprcrd.dialog.confirm(
`Delete gitea account "${acc.name}"? Stored token will be removed but no issues are deleted on the server.`, `Delete gitea account "${acc.name}"? Stored token will be removed but no issues are deleted on the server.`,
{ title: 'delete account', okLabel: 'delete', cancelLabel: 'cancel', danger: true } { title: 'delete account', okLabel: 'Delete', cancelLabel: 'Cancel', danger: true }
); );
if (!ok) return; if (!ok) return;
await hyprcrd.api(`api/gitea/accounts/${id}`, { method: 'DELETE' }); await hyprcrd.api(`api/gitea/accounts/${id}`, { method: 'DELETE' });
@@ -1169,9 +1169,9 @@ function renderStateToggleButton() {
const btn = $('gitea-state-toggle'); const btn = $('gitea-state-toggle');
if (!btn || !currentIssue) return; if (!btn || !currentIssue) return;
if (currentIssue.state === 'open') { if (currentIssue.state === 'open') {
btn.innerHTML = '<i class="ph ph-check-circle text-sm"></i><span class="font-pixel text-[10px]">close</span>'; btn.innerHTML = '<i class="ph ph-check-circle text-sm"></i><span class="font-pixel text-[10px]">Close</span>';
} else { } else {
btn.innerHTML = '<i class="ph ph-arrow-counter-clockwise text-sm"></i><span class="font-pixel text-[10px]">reopen</span>'; btn.innerHTML = '<i class="ph ph-arrow-counter-clockwise text-sm"></i><span class="font-pixel text-[10px]">Reopen</span>';
} }
} }
@@ -1447,7 +1447,7 @@ function injectHeaderButton() {
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.id = 'gitea-btn'; btn.id = 'gitea-btn';
btn.className = 'flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors'; btn.className = 'flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors';
btn.innerHTML = '<i class="ph ph-git-pull-request text-sm"></i><span class="font-pixel text-[10px]">gitea</span>'; btn.innerHTML = '<i class="ph ph-git-pull-request text-sm"></i><span class="font-pixel text-[10px]">Gitea</span>';
btn.addEventListener('click', openGitea); btn.addEventListener('click', openGitea);
const addBtn = document.getElementById('add-btn'); const addBtn = document.getElementById('add-btn');
if (addBtn) header.insertBefore(btn, addBtn); if (addBtn) header.insertBefore(btn, addBtn);
@@ -26,7 +26,7 @@ function injectFooterButton() {
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.id = 'logseq-import-btn'; btn.id = 'logseq-import-btn';
btn.className = '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'; btn.className = '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';
btn.innerHTML = '<i class="ph ph-graph text-sm"></i><span class="font-pixel text-[10px]">logseq import</span>'; btn.innerHTML = '<i class="ph ph-graph text-sm"></i><span class="font-pixel text-[10px]">Logseq import</span>';
group.insertBefore(btn, settingsBtn || null); group.insertBefore(btn, settingsBtn || null);
btn.addEventListener('click', () => fileInput.click()); btn.addEventListener('click', () => fileInput.click());
@@ -47,14 +47,14 @@ function injectFooterButton() {
await hyprcrd.dialog.show({ await hyprcrd.dialog.show({
title: 'logseq import', title: 'logseq import',
message: `${data.imported} cards, ${data.assets} images imported`, message: `${data.imported} cards, ${data.assets} images imported`,
okLabel: 'ok', okLabel: 'Ok',
}); });
} catch (err) { } catch (err) {
await hyprcrd.dialog.show({ await hyprcrd.dialog.show({
title: 'logseq import failed', title: 'logseq import failed',
message: String(err?.message || err) || 'unknown error', message: String(err?.message || err) || 'unknown error',
danger: true, danger: true,
okLabel: 'ok', okLabel: 'Ok',
}); });
} }
e.target.value = ''; e.target.value = '';
+19
View File
@@ -164,6 +164,25 @@
border-color: var(--t-accent); border-color: var(--t-accent);
} }
/* ── Detail-sidebar button (icon-only, sits next to the "group" label) ── */
#detail-sidebar .slideshow-present-btn-sidebar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
margin-left: 4px;
color: var(--t-muted);
border: 1px solid var(--t-border);
background: transparent;
cursor: pointer;
transition: all 0.15s;
}
#detail-sidebar .slideshow-present-btn-sidebar:hover {
color: var(--t-accent);
border-color: var(--t-accent);
}
/* ── Hotspot editor ── */ /* ── Hotspot editor ── */
#slideshow-editor.hidden { display: none; } #slideshow-editor.hidden { display: none; }
#slideshow-editor[disabled], #slideshow-editor [disabled] { opacity: 0.4; pointer-events: none; } #slideshow-editor[disabled], #slideshow-editor [disabled] { opacity: 0.4; pointer-events: none; }
+5 -5
View File
@@ -60,11 +60,11 @@
<span id="slideshow-gp-count" class="font-pixel text-[9px] text-t-dim"></span> <span id="slideshow-gp-count" class="font-pixel text-[9px] text-t-dim"></span>
<div class="flex-1"></div> <div class="flex-1"></div>
<button id="slideshow-gp-upload" 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"> <button id="slideshow-gp-upload" 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-upload-simple text-sm"></i><span class="font-pixel text-[10px]">upload</span> <i class="ph ph-upload-simple text-sm"></i><span class="font-pixel text-[10px]">Upload</span>
</button> </button>
<input type="file" id="slideshow-gp-file" accept="image/*" multiple class="hidden"> <input type="file" id="slideshow-gp-file" accept="image/*" multiple class="hidden">
<button id="slideshow-gp-cancel" 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"> <button id="slideshow-gp-cancel" 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]">cancel</span> <i class="ph ph-x text-sm"></i><span class="font-pixel text-[10px]">Cancel</span>
</button> </button>
</div> </div>
<div id="slideshow-gp-grid" class="flex-1 min-h-0 p-4 grid gap-3 content-start select-none" style="overflow:auto;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));"></div> <div id="slideshow-gp-grid" class="flex-1 min-h-0 p-4 grid gap-3 content-start select-none" style="overflow:auto;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));"></div>
@@ -79,9 +79,9 @@
<span class="font-pixel text-[10px] text-t-accent">hotspot target</span> <span class="font-pixel text-[10px] text-t-accent">hotspot target</span>
</div> </div>
<div class="flex flex-col gap-1 p-3 border-b border-t-border shrink-0"> <div class="flex flex-col gap-1 p-3 border-b border-t-border shrink-0">
<button data-pick-target="slide:next" class="slideshow-pick-row"><i class="ph ph-caret-right text-sm"></i><span>next slide</span></button> <button data-pick-target="slide:next" class="slideshow-pick-row"><i class="ph ph-caret-right text-sm"></i><span>Next slide</span></button>
<button data-pick-target="slide:prev" class="slideshow-pick-row"><i class="ph ph-caret-left text-sm"></i><span>previous slide</span></button> <button data-pick-target="slide:prev" class="slideshow-pick-row"><i class="ph ph-caret-left text-sm"></i><span>Previous slide</span></button>
<button data-pick-target="slide:first" class="slideshow-pick-row"><i class="ph ph-skip-back text-sm"></i><span>first slide</span></button> <button data-pick-target="slide:first" class="slideshow-pick-row"><i class="ph ph-skip-back text-sm"></i><span>First slide</span></button>
</div> </div>
<div class="px-3 py-2 shrink-0 border-b border-t-border"> <div class="px-3 py-2 shrink-0 border-b border-t-border">
<input id="slideshow-pick-search" type="text" placeholder="search cards…" class="w-full bg-t-bg border border-t-border text-t-text text-xs" style="padding:4px 8px;border-radius:3px;"> <input id="slideshow-pick-search" type="text" placeholder="search cards…" class="w-full bg-t-bg border border-t-border text-t-text text-xs" style="padding:4px 8px;border-radius:3px;">
+25
View File
@@ -306,6 +306,31 @@ if (groupBar) {
injectGroupBarButton(); injectGroupBarButton();
} }
// ── Detail-view sidebar button (shown when browsing a group card) ──
function injectDetailSidebarButton() {
const sidebar = $('detail-sidebar');
if (!sidebar) return;
if (sidebar.querySelector('.slideshow-present-btn')) return;
// Sidebar header is the first direct child div
const header = sidebar.firstElementChild;
if (!header) return;
const btn = document.createElement('button');
btn.className = 'slideshow-present-btn slideshow-present-btn-sidebar';
btn.type = 'button';
btn.title = 'present this group';
btn.innerHTML = '<i class="ph ph-projector-screen text-sm"></i>';
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const pid = sidebar.dataset.groupId;
if (pid) await startPresentationForGroup(pid);
});
header.appendChild(btn);
}
if ($('detail-sidebar')) injectDetailSidebarButton();
else document.addEventListener('DOMContentLoaded', injectDetailSidebarButton, { once: true });
// ── Hotspot editor ─────────────────────────────────────────────── // ── Hotspot editor ───────────────────────────────────────────────
const ed = { const ed = {