<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Track WebUI{% endblock %}</title>
<meta name="description" content="Track - Task and TODO Management">
<link rel="icon" href="/static/favicon.ico" sizes="any">
<style>
:root {
--bg-primary: #0f0f1a;
--bg-secondary: #1a1a2e;
--bg-card: rgba(30, 30, 50, 0.8);
--bg-input: rgba(40, 40, 60, 0.6);
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--text-muted: #6b6b8a;
--accent: #7c3aed;
--accent-hover: #8b5cf6;
--accent-light: rgba(124, 58, 237, 0.2);
--success: #10b981;
--success-light: rgba(16, 185, 129, 0.2);
--warning: #f59e0b;
--danger: #ef4444;
--danger-light: rgba(239, 68, 68, 0.2);
--border-color: rgba(255, 255, 255, 0.1);
--border-radius: 12px;
--border-radius-sm: 8px;
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
--gradient-bg: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
--transition: all 0.2s ease;
}
[data-theme="light"] {
--bg-primary: #f8f9fa;
--bg-secondary: #ffffff;
--bg-card: rgba(255, 255, 255, 0.9);
--bg-input: rgba(255, 255, 255, 0.8);
--text-primary: #1a1a1a;
--text-secondary: #4a5568;
--text-muted: #718096;
--accent: #7c3aed;
--accent-hover: #6d28d9;
--accent-light: rgba(124, 58, 237, 0.15);
--accent-text: #ffffff;
--success: #059669;
--success-dark: #047857;
--success-light: rgba(5, 150, 105, 0.15);
--success-text: #ffffff;
--warning: #d97706;
--warning-dark: #b45309;
--warning-text: #ffffff;
--danger: #dc2626;
--danger-light: rgba(220, 38, 38, 0.1);
--border-color: rgba(0, 0, 0, 0.1);
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--gradient-bg: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
background: var(--gradient-bg);
background-attachment: fixed;
min-height: 100%;
overscroll-behavior: none;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--gradient-bg);
background-attachment: fixed;
color: var(--text-primary);
min-height: 100vh;
line-height: 1.6;
overscroll-behavior: none;
}
body.focus-mode {
height: 100vh;
overflow: hidden;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem 1rem;
display: flex;
flex-direction: column;
}
body.focus-mode .container {
height: 100%;
overflow: hidden;
}
.card {
background: var(--bg-card);
backdrop-filter: blur(10px);
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: var(--shadow);
transition: var(--transition);
}
.card:hover {
border-color: rgba(255, 255, 255, 0.15);
}
.card.scrollable-card {
display: flex;
flex-direction: column;
min-height: 300px;
height: 100%;
margin-bottom: 0;
}
.card.scrollable-card .card-header {
flex-shrink: 0;
}
.card.scrollable-card .card-content {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.card.scrollable-card .input-form-container {
flex-shrink: 0;
margin-bottom: 1rem;
}
.card.scrollable-card .list-container {
flex: 1;
overflow-y: auto;
min-height: 0;
scrollbar-width: none;
-ms-overflow-style: none;
}
.card.scrollable-card .list-container::-webkit-scrollbar {
display: none;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 0.5rem;
}
.card-title .icon {
font-size: 1.5rem;
}
.task-header {
position: relative;
text-align: center;
margin-bottom: 2rem;
}
.task-header h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, var(--accent) 0%, #a855f7 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.task-meta {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
color: var(--text-muted);
font-size: 0.875rem;
}
.task-meta .badge {
display: inline-block;
background: var(--accent-light);
color: var(--accent);
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
[data-theme="light"] .task-meta .badge {
background: var(--accent);
color: white;
font-weight: 600;
}
.task-meta .task-id {
color: var(--text-muted);
font-size: 0.875rem;
}
.task-alias {
color: var(--text-muted);
font-size: 0.9rem;
font-weight: 400;
font-style: italic;
}
.description {
color: var(--text-secondary);
white-space: pre-wrap;
}
.text-muted {
color: var(--text-muted);
font-style: italic;
}
.ticket-info {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.ticket-id {
background: var(--accent-light);
color: var(--accent);
padding: 0.5rem 1rem;
border-radius: var(--border-radius-sm);
font-weight: 600;
font-size: 1rem;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
}
[data-theme="light"] .ticket-id {
background: var(--accent);
color: white;
}
.ticket-url {
color: var(--accent);
text-decoration: none;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.25rem;
transition: var(--transition);
}
.ticket-url:hover {
color: var(--accent-hover);
text-decoration: underline;
}
.checkbox-container {
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
padding: 0 0.75rem;
cursor: pointer;
transition: var(--transition);
}
.checkbox-container:hover {
border-color: var(--accent);
background: rgba(255, 255, 255, 0.05);
}
[data-theme="light"] .checkbox-container:hover {
background: rgba(0, 0, 0, 0.05);
}
.checkbox-container input[type="checkbox"] {
margin-right: 0.5rem;
accent-color: var(--accent);
cursor: pointer;
width: 1.25em;
height: 1.25em;
}
.checkbox-container .icon {
font-size: 1.25rem;
}
.input-group {
display: flex;
gap: 0.5rem;
}
.form-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.75rem;
}
input[type="text"],
textarea {
flex: 1;
width: 100%;
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
padding: 0.75rem 1rem;
color: var(--text-primary);
font-size: 0.875rem;
font-family: inherit;
transition: var(--transition);
resize: vertical;
}
input[type="text"]:focus,
textarea:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-light);
}
input[type="text"]::placeholder,
textarea::placeholder {
color: var(--text-muted);
}
button {
background: var(--accent);
color: white;
border: none;
border-radius: var(--border-radius-sm);
padding: 0.75rem 1.25rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 0.5rem;
white-space: nowrap;
}
button:hover {
background: var(--accent-hover);
transform: translateY(-1px);
}
[data-theme="light"] button {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
}
[data-theme="light"] button:hover {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}
button:active {
transform: translateY(0);
}
button.btn-edit {
background: transparent;
color: var(--text-muted);
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
}
button.btn-edit:hover {
background: rgba(255, 255, 255, 0.05);
color: var(--text-primary);
}
[data-theme="light"] button.btn-edit:hover {
background: rgba(0, 0, 0, 0.05);
}
button.btn-primary {
background: var(--accent);
color: white;
}
button.btn-secondary {
background: transparent;
color: var(--text-secondary);
border: 1px solid var(--border-color);
}
button.btn-secondary:hover {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.2);
}
[data-theme="light"] button.btn-secondary:hover {
background: rgba(0, 0, 0, 0.05);
border-color: rgba(0, 0, 0, 0.2);
}
button.btn-success {
background: transparent;
color: var(--success);
padding: 0.5rem;
font-size: 1rem;
}
button.btn-success:hover {
background: var(--success-light);
}
button.btn-warning {
background: transparent;
color: var(--warning);
padding: 0.5rem;
font-size: 1rem;
}
button.btn-warning:hover {
background: rgba(245, 158, 11, 0.2);
}
button.btn-danger {
background: transparent;
color: var(--danger);
padding: 0.5rem;
}
button.btn-danger:hover {
background: var(--danger-light);
}
.todo-actions {
display: flex;
gap: 0.25rem;
align-items: center;
}
.btn-scrap-link {
background: transparent;
color: var(--text-muted);
padding: 0.25rem 0.5rem;
font-size: 1rem;
border: none;
cursor: pointer;
transition: var(--transition);
margin-left: 0.5rem;
}
.btn-scrap-link:hover {
background: rgba(124, 58, 237, 0.2);
color: var(--accent);
transform: scale(1.15);
}
.btn-scrap-link:active {
transform: scale(1.05);
}
.scrap-highlighted {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.3) 0%, rgba(168, 85, 247, 0.2) 100%) !important;
border-left-color: var(--accent-hover) !important;
box-shadow: 0 0 20px rgba(124, 58, 237, 0.4);
animation: scrap-pulse 1s ease-in-out 3;
}
[data-theme="light"] .scrap-highlighted {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.25) 0%, rgba(168, 85, 247, 0.15) 100%) !important;
box-shadow: 0 0 15px rgba(124, 58, 237, 0.35);
}
@keyframes scrap-pulse {
0%,
100% {
box-shadow: 0 0 20px rgba(124, 58, 237, 0.4);
}
50% {
box-shadow: 0 0 30px rgba(124, 58, 237, 0.6);
}
}
.two-pane-layout {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
.two-pane-layout.main-content {
flex: 1;
min-height: 0;
overflow: hidden;
}
@media (min-width: 1024px) {
.two-pane-layout {
grid-template-columns: 1fr 1fr;
}
}
.single-pane-layout {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
margin-top: 1.5rem;
}
.calendar-container {
width: 100%;
overflow: hidden;
border-radius: var(--border-radius-sm);
position: relative;
}
#calendar-wrapper {
filter: invert(0.9) grayscale(1) brightness(1.1) contrast(0.9);
background: #1a1a2e;
border-radius: var(--border-radius-sm);
overflow: hidden;
position: relative;
}
#calendar-wrapper::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(124, 58, 237, 0.08) 0%, rgba(168, 85, 247, 0.05) 100%);
pointer-events: none;
z-index: 1;
}
#calendar-wrapper iframe {
display: block;
margin-bottom: -10px;
position: relative;
z-index: 0;
}
[data-theme="light"] #calendar-wrapper {
filter: none;
background: #ffffff;
}
[data-theme="light"] #calendar-wrapper::before {
display: none;
}
[data-theme="light"] #calendar-wrapper iframe {
filter: none;
margin-bottom: 0;
}
.todo-list {
list-style: none;
}
.todo-item {
position: relative;
border-radius: var(--border-radius-sm);
transition: var(--transition);
margin-bottom: 0.5rem;
}
.todo-item.menu-open {
z-index: 10;
}
.todo-main {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem;
cursor: pointer;
border-radius: var(--border-radius-sm);
transition: var(--transition);
}
.todo-item.oldest-pending .todo-main {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.15) 0%, rgba(168, 85, 247, 0.1) 100%);
border: 2px solid var(--accent);
box-shadow: 0 0 20px rgba(124, 58, 237, 0.3);
animation: subtle-pulse 2s ease-in-out infinite;
}
[data-theme="light"] .todo-item.oldest-pending .todo-main {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.12) 0%, rgba(168, 85, 247, 0.08) 100%);
box-shadow: 0 0 15px rgba(124, 58, 237, 0.25);
}
@keyframes subtle-pulse {
0%,
100% {
box-shadow: 0 0 20px rgba(124, 58, 237, 0.3);
}
50% {
box-shadow: 0 0 30px rgba(124, 58, 237, 0.5);
}
}
.todo-main:hover {
background: rgba(255, 255, 255, 0.05);
}
.todo-item.oldest-pending .todo-main:hover {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.2) 0%, rgba(168, 85, 247, 0.15) 100%);
}
[data-theme="light"] .todo-main:hover {
background: rgba(0, 0, 0, 0.03);
}
[data-theme="light"] .todo-item.oldest-pending .todo-main:hover {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.18) 0%, rgba(168, 85, 247, 0.12) 100%);
}
.todo-menu-indicator {
margin-left: auto;
color: var(--text-muted);
font-size: 1.25rem;
font-weight: bold;
transition: var(--transition);
}
.todo-main:hover .todo-menu-indicator {
color: var(--text-secondary);
}
.todo-menu {
position: fixed;
background: #1a1a2e !important;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
box-shadow: var(--shadow);
min-width: 200px;
z-index: 9999;
overflow: hidden;
}
[data-theme="light"] .todo-menu {
background: #ffffff !important;
}
.menu-item {
width: 100%;
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem !important;
background: inherit;
border: none !important;
border-radius: 0;
text-align: left;
cursor: pointer;
transition: var(--transition);
font-size: 0.875rem;
}
.menu-item:hover {
background: rgba(255, 255, 255, 0.05);
}
[data-theme="light"] .menu-item:hover {
background: rgba(0, 0, 0, 0.05);
}
.menu-item.btn-success {
color: var(--success);
}
.menu-item.btn-success:hover {
background: var(--success-light);
}
.menu-item.btn-warning {
color: var(--warning);
}
.menu-item.btn-warning:hover {
background: rgba(245, 158, 11, 0.2);
}
.menu-item.btn-secondary {
color: var(--text-secondary);
}
.menu-item.btn-secondary:hover {
background: rgba(255, 255, 255, 0.05);
}
[data-theme="light"] .menu-item.btn-secondary:hover {
background: rgba(0, 0, 0, 0.05);
}
.menu-item.btn-danger {
color: var(--danger);
}
.menu-item.btn-danger:hover {
background: var(--danger-light);
}
.menu-icon {
font-size: 1rem;
width: 1.25rem;
text-align: center;
}
.menu-label {
flex: 1;
}
.todo-item.done {
opacity: 0.6;
}
.todo-item.done .todo-content {
text-decoration: line-through;
}
.todo-index {
background: var(--accent-light);
color: var(--accent);
font-size: 0.75rem;
font-weight: 600;
padding: 0.25rem 0.5rem;
border-radius: 4px;
min-width: 2rem;
text-align: center;
}
[data-theme="light"] .todo-index {
background: var(--accent);
color: white;
}
.todo-content {
flex: 1;
}
.todo-status {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.todo-status.pending {
background: #f59e0b;
color: #000;
font-weight: 500;
}
.todo-status.in-progress {
background: #3b82f6;
color: #fff;
font-weight: 600;
}
.todo-worktrees {
padding: 0 0.75rem 0.5rem 3.5rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.worktree-path {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.75rem;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.todo-status.done {
background: var(--success);
color: #000;
font-weight: 600;
}
.todo-status.cancelled {
background: var(--text-muted);
color: #000;
font-weight: 600;
}
[data-theme="light"] .todo-status.pending {
background: #f59e0b;
color: white;
}
[data-theme="light"] .todo-status.in-progress {
background: #3b82f6;
color: white;
}
[data-theme="light"] .todo-status.done {
background: var(--success);
color: white;
}
[data-theme="light"] .todo-status.cancelled {
background: var(--text-muted);
color: white;
}
.todo-item.cancelled {
opacity: 0.5;
}
.todo-item.cancelled .todo-content {
text-decoration: line-through;
}
.scrap-item {
padding: 1rem;
border-left: 3px solid var(--accent);
background: rgba(255, 255, 255, 0.03);
border-radius: 0 var(--border-radius-sm) var(--border-radius-sm) 0;
margin-bottom: 0.75rem;
}
[data-theme="light"] .scrap-item {
background: rgba(0, 0, 0, 0.03);
}
.scrap-time {
color: var(--text-secondary);
font-size: 0.8125rem;
font-weight: 500;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.scrap-time::before {
content: '🕐';
font-size: 0.875rem;
}
.scrap-content {
white-space: pre-wrap;
word-break: break-word;
}
.scrap-markdown {
line-height: 1.3;
}
.scrap-markdown h1,
.scrap-markdown h2,
.scrap-markdown h3,
.scrap-markdown h4,
.scrap-markdown h5,
.scrap-markdown h6 {
margin: 0.25rem 0 0 0;
font-weight: 600;
color: var(--text-primary);
}
.scrap-markdown h1 {
font-size: 1.5rem;
border-bottom: 2px solid var(--border-color);
padding-bottom: 0.25rem;
margin-bottom: 0.125rem;
}
.scrap-markdown h2 {
font-size: 1.25rem;
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.25rem;
margin-bottom: 0.125rem;
}
.scrap-markdown h3 {
font-size: 1.125rem;
}
.scrap-markdown h4 {
font-size: 1rem;
}
.scrap-markdown h5,
.scrap-markdown h6 {
font-size: 0.875rem;
}
.scrap-markdown p {
margin: 0;
}
.scrap-markdown ul,
.scrap-markdown ol {
margin: 0;
padding-left: 1.5rem;
margin-block-start: 0;
margin-block-end: 0;
}
.scrap-markdown li {
margin: 0;
line-height: 1.3;
}
.scrap-markdown li p {
margin: 0;
display: inline;
}
.scrap-markdown code {
background: var(--bg-input);
padding: 0.125rem 0.375rem;
border-radius: 3px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.875em;
color: var(--accent);
}
.scrap-markdown pre {
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
padding: 0.75rem;
overflow-x: auto;
margin: 0.25rem 0;
}
.scrap-markdown pre code {
background: none;
padding: 0;
color: var(--text-primary);
}
.scrap-markdown a {
color: #5eead4;
text-decoration: none;
transition: var(--transition);
}
.scrap-markdown a:hover {
color: #2dd4bf;
text-decoration: underline;
}
[data-theme="light"] .scrap-markdown a {
color: #0ea5e9;
}
[data-theme="light"] .scrap-markdown a:hover {
color: #0284c7;
}
.todo-content a {
color: #5eead4;
text-decoration: none;
transition: var(--transition);
}
.todo-content a:hover {
color: #2dd4bf;
text-decoration: underline;
}
[data-theme="light"] .todo-content a {
color: #0ea5e9;
}
[data-theme="light"] .todo-content a:hover {
color: #0284c7;
}
.scrap-markdown blockquote {
border-left: 3px solid var(--accent);
padding-left: 1rem;
margin: 0.25rem 0;
color: var(--text-secondary);
font-style: italic;
}
.scrap-markdown strong {
font-weight: 600;
color: var(--text-primary);
}
.scrap-markdown em {
font-style: italic;
}
.scrap-markdown hr {
border: none;
border-top: 1px solid var(--border-color);
margin: 0.25rem 0;
}
.scrap-markdown table {
border-collapse: collapse;
width: 100%;
margin: 0.25rem 0;
}
.scrap-markdown th,
.scrap-markdown td {
border: 1px solid var(--border-color);
padding: 0.5rem;
text-align: left;
}
.scrap-markdown th {
background: var(--bg-input);
font-weight: 600;
}
.scrap-markdown img {
max-width: 100%;
height: auto;
border-radius: var(--border-radius-sm);
margin: 0.25rem 0;
}
.repo-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0;
border-bottom: 1px solid var(--border-color);
}
.repo-item:last-child {
border-bottom: none;
}
.repo-path {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.875rem;
color: var(--text-secondary);
}
.link-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.link-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.5rem 0;
border-bottom: 1px solid var(--border-color);
}
.link-item:last-child {
border-bottom: none;
}
.link-url {
color: var(--accent);
text-decoration: none;
font-size: 0.875rem;
transition: var(--transition);
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.link-url:hover {
color: var(--accent-hover);
text-decoration: underline;
}
.link-url::before {
content: '🔗';
font-size: 0.875rem;
}
.empty-state {
text-align: center;
padding: 2rem;
color: var(--text-muted);
}
.empty-state .icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.htmx-request {
opacity: 0.7;
pointer-events: none;
}
.htmx-swapping {
opacity: 0;
transition: opacity 0.15s ease-out;
}
.htmx-settling {
opacity: 1;
transition: opacity 0.15s ease-in;
}
.no-task {
text-align: center;
padding: 4rem 2rem;
}
.no-task h2 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: var(--text-secondary);
}
.no-task p {
color: var(--text-muted);
}
.no-task code {
background: var(--bg-input);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
}
.connection-status {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
display: flex;
align-items: center;
gap: 0.375rem;
box-shadow: var(--shadow);
height: 2.5rem;
white-space: nowrap;
transition: var(--transition);
}
.connection-status.connected {
border-color: var(--success);
color: var(--success);
}
.connection-status.disconnected {
border-color: var(--danger);
color: var(--danger);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.connected .status-dot {
background: var(--success);
}
.disconnected .status-dot {
background: var(--danger);
animation: none;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.toolbar {
position: fixed;
top: 1rem;
right: 1rem;
display: flex;
gap: 0.5rem;
z-index: 1000;
}
.toolbar-btn {
background: var(--bg-card);
color: var(--text-secondary);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 0.375rem;
box-shadow: var(--shadow);
height: 2.5rem;
white-space: nowrap;
}
.toolbar-btn:hover {
background: var(--bg-input);
border-color: var(--accent);
transform: translateY(-1px);
}
.toolbar-btn:active {
transform: translateY(0);
}
[data-theme="light"] .toolbar-btn {
background: var(--bg-card);
}
[data-theme="light"] .toolbar-btn:hover {
background: var(--bg-input);
}
.toolbar-btn .icon {
font-size: 1.125rem;
}
.theme-toggle {
min-width: 2.5rem;
justify-content: center;
padding: 0.5rem;
}
.theme-icon {
font-size: 1.25rem;
transition: var(--transition);
}
@media (max-width: 640px) {
.container {
padding: 1rem;
}
.task-header h1 {
font-size: 1.5rem;
}
.input-group {
flex-direction: column;
}
button {
width: 100%;
justify-content: center;
}
.form-actions {
flex-direction: column;
}
.toolbar {
top: 0.5rem;
right: 0.5rem;
}
}
</style>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
</head>
<body>
<div class="toolbar">
{% block toolbar_buttons %}
<div id="connection-status" class="connection-status disconnected">
<span class="status-dot"></span>
<span class="status-text">Connecting...</span>
</div>
<button class="toolbar-btn theme-toggle" id="theme-toggle" aria-label="Toggle theme">
<span class="theme-icon" id="theme-icon">🌙</span>
</button>
{% endblock %}
</div>
<div class="container">
{% block content %}{% endblock %}
</div>
<script>
(function () {
const html = document.documentElement;
const themeToggle = document.getElementById('theme-toggle');
const themeIcon = document.getElementById('theme-icon');
const savedTheme = localStorage.getItem('theme') || 'dark';
function applyTheme(theme) {
if (theme === 'light') {
html.setAttribute('data-theme', 'light');
themeIcon.textContent = '☀️';
} else {
html.removeAttribute('data-theme');
themeIcon.textContent = '🌙';
}
localStorage.setItem('theme', theme);
}
applyTheme(savedTheme);
themeToggle.addEventListener('click', function () {
const currentTheme = html.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
applyTheme(newTheme);
});
})();
</script>
<script>
if (!window.sseInitialized) {
console.log('Initializing SSE connection...');
window.sseInitialized = true;
window.eventSource = null;
window.reconnectAttempts = 0;
const maxReconnectAttempts = 5;
window.connectSSE = function () {
if (window.eventSource) {
console.log('Closing existing SSE connection');
window.eventSource.close();
window.eventSource = null;
}
console.log('Creating new SSE connection');
window.eventSource = new EventSource('/api/sse');
window.eventSource.onopen = function () {
console.log('SSE connection opened');
window.reconnectAttempts = 0;
const status = document.getElementById('connection-status');
if (status) {
status.className = 'connection-status connected';
const textEl = status.querySelector('.status-text');
if (textEl) textEl.textContent = 'Live';
}
};
window.eventSource.addEventListener('update', function (e) {
const event = JSON.parse(e.data);
console.log('SSE event:', event);
switch (event.type) {
case 'header':
console.log('Updating task header');
window.location.reload();
break;
case 'description':
console.log('Updating description section');
htmx.ajax('GET', '/api/card/description', {
target: '#description-section',
swap: 'outerHTML'
});
break;
case 'ticket':
console.log('Updating ticket section');
htmx.ajax('GET', '/api/card/ticket', {
target: '#ticket-section',
swap: 'outerHTML'
});
break;
case 'links':
console.log('Updating links section');
htmx.ajax('GET', '/api/card/links', {
target: '#links-section',
swap: 'outerHTML'
});
break;
case 'todos':
console.log('Updating todos section');
htmx.ajax('GET', '/api/card/todos', {
target: '#todos-section',
swap: 'outerHTML'
}).then(() => {
if (document.body.classList.contains('focus-mode')) {
setTimeout(() => {
if (typeof scrollToOldestPendingTask === 'function') {
scrollToOldestPendingTask();
}
}, 100);
}
});
break;
case 'scraps':
console.log('Updating scraps section');
htmx.ajax('GET', '/api/card/scraps', {
target: '#scraps-section',
swap: 'outerHTML'
});
break;
case 'repos':
console.log('Updating repos section');
htmx.ajax('GET', '/partials/repos', {
target: '#repos-section',
swap: 'outerHTML'
});
break;
default:
console.warn('Unknown SSE event type:', event.type);
}
});
window.eventSource.onerror = function () {
console.log('SSE connection error');
const status = document.getElementById('connection-status');
if (status) {
status.className = 'connection-status disconnected';
const textEl = status.querySelector('.status-text');
if (textEl) textEl.textContent = 'Disconnected';
}
if (window.eventSource) {
window.eventSource.close();
window.eventSource = null;
}
if (window.reconnectAttempts < maxReconnectAttempts) {
window.reconnectAttempts++;
const delay = 2000 * window.reconnectAttempts;
console.log(`Reconnecting in ${delay}ms (attempt ${window.reconnectAttempts}/${maxReconnectAttempts})`);
setTimeout(window.connectSSE, delay);
}
};
};
window.connectSSE();
document.addEventListener('visibilitychange', function () {
if (!document.hidden && (!window.eventSource || window.eventSource.readyState === EventSource.CLOSED)) {
console.log('Page visible, reconnecting SSE');
window.reconnectAttempts = 0;
window.connectSSE();
}
});
setTimeout(() => {
const statusElements = document.querySelectorAll('.connection-status');
console.log(`Found ${statusElements.length} connection-status elements`);
if (statusElements.length > 1) {
console.warn('Removing duplicate connection-status elements');
statusElements.forEach(el => {
if (el.id !== 'connection-status') {
el.remove();
}
});
}
}, 100);
} else {
console.log('SSE already initialized, skipping');
}
</script>
<script>
window.toggleDescEdit = function () {
const display = document.getElementById('desc-display');
const form = document.getElementById('desc-edit-form');
if (form && form.style.display === 'none') {
if (display) display.style.display = 'none';
form.style.display = 'block';
const textarea = form.querySelector('textarea');
if (textarea) textarea.focus();
} else if (form) {
if (display) display.style.display = 'block';
form.style.display = 'none';
}
};
window.toggleTicketEdit = function () {
const display = document.getElementById('ticket-display');
const form = document.getElementById('ticket-edit-form');
if (form && form.style.display === 'none') {
if (display) display.style.display = 'none';
form.style.display = 'block';
const input = form.querySelector('input[name="ticket_id"]');
if (input) input.focus();
} else if (form) {
if (display) display.style.display = 'block';
form.style.display = 'none';
}
};
</script>
<script>
window.toggleTodoMenu = function (event, todoId) {
event.stopPropagation();
const currentItem = document.querySelector(`[data-todo-id="${todoId}"]`);
const currentMenu = document.getElementById(`todo-menu-${todoId}`);
document.querySelectorAll('.todo-item').forEach(item => {
if (item.getAttribute('data-todo-id') !== todoId) {
const menuId = `todo-menu-${item.getAttribute('data-todo-id')}`;
const menu = document.getElementById(menuId);
if (menu) {
menu.style.display = 'none';
}
item.classList.remove('menu-open');
}
});
if (currentMenu) {
const isOpen = currentMenu.style.display === 'block';
if (isOpen) {
currentMenu.style.display = 'none';
currentItem.classList.remove('menu-open');
} else {
if (currentMenu.parentElement !== document.body) {
document.body.appendChild(currentMenu);
}
currentMenu.style.left = `${event.clientX}px`;
currentMenu.style.top = `${event.clientY}px`;
currentMenu.style.display = 'block';
currentItem.classList.add('menu-open');
}
} else {
console.error('Menu not found:', `todo-menu-${todoId}`);
}
};
document.addEventListener('click', function (event) {
if (!event.target.closest('.todo-item') && !event.target.closest('.todo-menu')) {
document.querySelectorAll('.todo-menu').forEach(menu => {
menu.style.display = 'none';
});
document.querySelectorAll('.todo-item').forEach(item => {
item.classList.remove('menu-open');
});
}
});
document.body.addEventListener('htmx:beforeRequest', function () {
document.querySelectorAll('.todo-menu').forEach(menu => {
menu.style.display = 'none';
});
document.querySelectorAll('.todo-item').forEach(item => {
item.classList.remove('menu-open');
});
});
document.addEventListener('click', function (event) {
const link = event.target.closest('a');
if (link && (link.closest('.todo-content') || link.closest('.scrap-content'))) {
event.stopPropagation();
}
}, true); </script>
</body>
</html>