fixes + theme
This commit is contained in:
1 parent
5af49655d6
commit
22f73ab3b9
25 files changed
+1002
-204
No files matched your search
+3
-1
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"disabled": []
|
"disabled": [
|
||||||
|
"logseq-import"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
+184
-7
@@ -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; }
|
||||||
|
|||||||
+287
-72
@@ -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,10 +1496,101 @@ 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="shrink-0 pb-3 border-b border-t-border">
|
||||||
|
<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 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 id="settings-theme-list" class="grid grid-cols-2 gap-2"></div>
|
||||||
</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 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>
|
||||||
<div><label class="font-pixel text-[9px] text-t-dim uppercase block mb-1">server url</label><input id="caldav-url" type="text" name="caldav_url_x" placeholder="https://cloud.example.com/remote.php/dav/calendars/user/" autocomplete="off" data-1p-ignore data-lpignore="true" data-form-type="other" 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">server url</label><input id="caldav-url" type="text" name="caldav_url_x" placeholder="https://cloud.example.com/remote.php/dav/calendars/user/" autocomplete="off" data-1p-ignore data-lpignore="true" data-form-type="other" 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>
|
||||||
@@ -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
@@ -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 } });
|
||||||
|
|||||||
@@ -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',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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
@@ -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' },
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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,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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 = '';
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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;">
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in new issue
Block a user