taskunity 2026.3__tar.gz → 2026.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {taskunity-2026.3/src/taskunity.egg-info → taskunity-2026.4}/PKG-INFO +1 -1
  2. {taskunity-2026.3 → taskunity-2026.4}/pyproject.toml +1 -1
  3. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/app.py +6 -0
  4. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/static/app.css +176 -16
  5. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/templates/base.html +51 -0
  6. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/templates/partials/main.html +12 -34
  7. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/templates/partials/milestone_banner.html +1 -1
  8. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/templates/partials/milestone_panel.html +3 -1
  9. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/templates/partials/milestones.html +27 -20
  10. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/templates/partials/task_panel.html +2 -1
  11. {taskunity-2026.3 → taskunity-2026.4/src/taskunity.egg-info}/PKG-INFO +1 -1
  12. {taskunity-2026.3 → taskunity-2026.4}/MANIFEST.in +0 -0
  13. {taskunity-2026.3 → taskunity-2026.4}/README.md +0 -0
  14. {taskunity-2026.3 → taskunity-2026.4}/setup.cfg +0 -0
  15. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/__init__.py +0 -0
  16. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/cli.py +0 -0
  17. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/models.py +0 -0
  18. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/render.py +0 -0
  19. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/static/chart.umd.min.js +0 -0
  20. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/static/chartjs-adapter-date-fns.bundle.min.js +0 -0
  21. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/static/htmx.min.js +0 -0
  22. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/task_store.py +0 -0
  23. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/templates/index.html +0 -0
  24. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/templates/partials/board.html +0 -0
  25. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/templates/partials/calendar.html +0 -0
  26. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/templates/partials/projects.html +0 -0
  27. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/templates/partials/task_list.html +0 -0
  28. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity/templates/partials/timeline.html +0 -0
  29. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity.egg-info/SOURCES.txt +0 -0
  30. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity.egg-info/dependency_links.txt +0 -0
  31. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity.egg-info/entry_points.txt +0 -0
  32. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity.egg-info/requires.txt +0 -0
  33. {taskunity-2026.3 → taskunity-2026.4}/src/taskunity.egg-info/top_level.txt +0 -0
  34. {taskunity-2026.3 → taskunity-2026.4}/tests/test_git_workspace_scope.py +0 -0
  35. {taskunity-2026.3 → taskunity-2026.4}/tests/test_render_jsonantt.py +0 -0
  36. {taskunity-2026.3 → taskunity-2026.4}/tests/test_workspace_config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taskunity
3
- Version: 2026.3
3
+ Version: 2026.4
4
4
  Summary: A local, file-backed productivity app for program/task tracking.
5
5
  Author: Taskunity Contributors
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "taskunity"
7
- version = "2026.3"
7
+ version = "2026.4"
8
8
  description = "A local, file-backed productivity app for program/task tracking."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -320,6 +320,11 @@ def create_app(workspace: str | Path = ".") -> FastAPI:
320
320
  all_tasks = load_all_tasks(workspace)
321
321
  milestones = load_all_milestones(workspace)
322
322
  tasks_by_id = {t.id: t for t in all_tasks}
323
+ panel_task_id = (selected_task.id if selected_task else (request.query_params.get("panel_task") or "").strip())
324
+ if selected_task is None and panel_task_id:
325
+ selected_task = tasks_by_id.get(panel_task_id)
326
+ if selected_task is None:
327
+ panel_task_id = ""
323
328
  milestone_rollups = {m.id: milestone_rollup(m, tasks_by_id) for m in milestones}
324
329
 
325
330
  selected_milestone = None
@@ -443,6 +448,7 @@ def create_app(workspace: str | Path = ".") -> FastAPI:
443
448
  "calendar_next_query": build_query(projects, date_from, date_to, q, "calendar", sort, milestone, show_closed, stale_days, next_month, next_year),
444
449
  "calendar_year_prev_query": build_query(projects, date_from, date_to, q, "calendar", sort, milestone, show_closed, stale_days, year_prev_month, year_prev_year),
445
450
  "calendar_year_next_query": build_query(projects, date_from, date_to, q, "calendar", sort, milestone, show_closed, stale_days, year_next_month, year_next_year),
451
+ "panel_task": panel_task_id,
446
452
  "show_closed": show_closed,
447
453
  "hidden_closed_count": hidden_closed_count,
448
454
  "toggle_closed_query": build_query(projects, date_from, date_to, q, view, sort, milestone, not show_closed, stale_days, focus_month, focus_year),
@@ -1,5 +1,7 @@
1
1
  :root {
2
2
  color-scheme: light;
3
+ --topbar-height: 4.4rem;
4
+ --controls-height: 5.7rem;
3
5
  --bg: #eef1f7;
4
6
  --bg-soft: #f7f9fc;
5
7
  --card: #ffffff;
@@ -78,6 +80,7 @@ body {
78
80
  color: var(--text);
79
81
  -webkit-font-smoothing: antialiased;
80
82
  text-rendering: optimizeLegibility;
83
+ padding-top: var(--topbar-height);
81
84
  }
82
85
  html[data-theme="dark"] body {
83
86
  background: var(--bg);
@@ -96,8 +99,10 @@ html[data-theme="dark"] body {
96
99
  background: rgba(255, 255, 255, .82);
97
100
  backdrop-filter: saturate(160%) blur(10px);
98
101
  border-bottom: 1px solid var(--line);
99
- position: sticky;
102
+ position: fixed;
100
103
  top: 0;
104
+ left: 0;
105
+ right: 0;
101
106
  z-index: 10;
102
107
  }
103
108
  html[data-theme="dark"] .topbar {
@@ -188,6 +193,7 @@ h1, h2, h3, p { margin-top: 0; }
188
193
  .theme-option.active { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
189
194
  .settings-anchor { position: relative; }
190
195
  .new-task-form { display: flex; gap: .5rem; }
196
+ .new-task-form .new-task-btn { white-space: nowrap; }
191
197
  input, select, textarea, button {
192
198
  font: inherit;
193
199
  border: 1px solid var(--line);
@@ -220,7 +226,17 @@ button:focus-visible { outline: none; box-shadow: var(--ring); }
220
226
  padding: 1.1rem 1.4rem;
221
227
  align-items: start;
222
228
  }
223
- .content { min-width: 0; }
229
+ .layout.panel-collapsed {
230
+ grid-template-columns: minmax(0, 1fr);
231
+ }
232
+ .layout.panel-collapsed .pane-resizer,
233
+ .layout.panel-collapsed .side-panel {
234
+ display: none;
235
+ }
236
+ .content {
237
+ min-width: 0;
238
+ padding-top: calc(var(--controls-height) + .35rem);
239
+ }
224
240
  .pane-resizer {
225
241
  align-self: stretch;
226
242
  width: 10px;
@@ -370,6 +386,49 @@ button:focus-visible { outline: none; box-shadow: var(--ring); }
370
386
  margin-top: .55rem;
371
387
  }
372
388
  .mini-progress div { height: 100%; background: var(--working); }
389
+ .content-toolbar {
390
+ position: fixed !important;
391
+ top: var(--topbar-height);
392
+ left: 1.4rem;
393
+ right: calc(var(--side-pane-width, 430px) + 10px + .55rem + 1.4rem);
394
+ z-index: 8;
395
+ background: var(--bg);
396
+ border-bottom: 1px solid var(--line-soft);
397
+ padding: 0 0 .28rem;
398
+ min-height: var(--controls-height);
399
+ }
400
+ .layout.panel-collapsed .content-toolbar {
401
+ right: 1.4rem;
402
+ }
403
+ .content-toolbar::before {
404
+ content: "";
405
+ position: absolute;
406
+ left: 0;
407
+ right: 0;
408
+ top: -8px;
409
+ height: 8px;
410
+ background: var(--bg);
411
+ pointer-events: none;
412
+ }
413
+ .content-toolbar .filter-bar,
414
+ .content-toolbar .view-bar {
415
+ flex-wrap: nowrap;
416
+ overflow-x: auto;
417
+ overflow-y: hidden;
418
+ scrollbar-width: thin;
419
+ -webkit-overflow-scrolling: touch;
420
+ }
421
+ .content-toolbar .filter-bar { margin-bottom: .55rem; }
422
+ .content-toolbar .view-bar { margin-bottom: .25rem; }
423
+ .content-toolbar .pills,
424
+ .content-toolbar .view-switch,
425
+ .content-toolbar .new-task-form,
426
+ .content-toolbar .git-chip,
427
+ .content-toolbar .btn-link,
428
+ .content-toolbar .filter-menu {
429
+ flex: 0 0 auto;
430
+ }
431
+ .content-toolbar .filter-bar .spacer { flex: 1 0 1.2rem; }
373
432
  .filter-bar {
374
433
  display: flex;
375
434
  flex-wrap: wrap;
@@ -895,13 +954,21 @@ button:focus-visible { outline: none; box-shadow: var(--ring); }
895
954
  .dep-id { margin-left: auto; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .68rem; color: var(--muted); }
896
955
  .dep-empty { padding: .5rem .55rem; font-size: .82rem; }
897
956
  .side-panel {
898
- position: sticky;
957
+ position: fixed;
899
958
  top: 5rem;
900
- max-height: calc(100vh - 6rem);
959
+ right: 1.4rem;
960
+ height: calc(100vh - 6rem);
961
+ width: var(--side-pane-width, 430px);
901
962
  overflow: hidden;
902
963
  box-shadow: var(--shadow-md);
903
964
  }
904
- .panel-scroll { overflow-y: auto; max-height: calc(100vh - 6rem); padding: 0 1rem 1rem; font-size: .93rem; }
965
+ .panel-scroll {
966
+ overflow-y: auto;
967
+ height: 100%;
968
+ overscroll-behavior: contain;
969
+ padding: 0 1rem 1rem;
970
+ font-size: .93rem;
971
+ }
905
972
  .empty-panel { padding: 1.25rem 1rem; color: var(--muted); }
906
973
  .empty-panel h2 { margin: 0 0 .35rem; font-size: 1.05rem; font-weight: 650; color: var(--text); }
907
974
  .empty-panel p { margin: 0; font-size: .88rem; }
@@ -920,6 +987,14 @@ button:focus-visible { outline: none; box-shadow: var(--ring); }
920
987
  align-items: center;
921
988
  gap: .45rem;
922
989
  }
990
+ .task-collapse-btn {
991
+ width: 1.85rem;
992
+ height: 1.85rem;
993
+ padding: 0;
994
+ text-align: center;
995
+ line-height: 1;
996
+ font-size: 1rem;
997
+ }
923
998
  .task-back-btn {
924
999
  padding: .2rem .45rem;
925
1000
  border: 1px solid var(--line);
@@ -983,25 +1058,36 @@ button:focus-visible { outline: none; box-shadow: var(--ring); }
983
1058
  width: 100%;
984
1059
  display: inline-grid;
985
1060
  grid-template-columns: auto minmax(0, 1fr);
986
- gap: .55rem;
1061
+ gap: .42rem;
987
1062
  align-items: center;
1063
+ color: var(--text);
988
1064
  text-align: left;
989
1065
  background: var(--bg-soft);
990
1066
  border: 1px solid var(--line);
991
- color: var(--text);
1067
+ padding: .68rem .8rem;
992
1068
  box-shadow: none;
993
1069
  padding: .45rem .6rem;
994
1070
  }
995
1071
  .check-toggle:hover { filter: none; border-color: color-mix(in srgb, var(--line) 55%, var(--accent)); }
996
1072
  .check-mark {
997
1073
  width: 1.2rem;
998
- height: 1.2rem;
1074
+ gap: .5rem;
999
1075
  border: 1px solid color-mix(in srgb, var(--line) 60%, var(--accent));
1000
1076
  border-radius: .3rem;
1001
1077
  display: inline-flex;
1002
- align-items: center;
1078
+ font-size: .93rem;
1003
1079
  justify-content: center;
1004
1080
  font-weight: 700;
1081
+ .milestone-open-row {
1082
+ width: 100%;
1083
+ text-align: left;
1084
+ box-shadow: var(--shadow-sm);
1085
+ }
1086
+ .milestone-open-row:hover {
1087
+ filter: none;
1088
+ border-color: color-mix(in srgb, var(--line) 60%, var(--accent));
1089
+ box-shadow: var(--shadow-md);
1090
+ }
1005
1091
  color: var(--accent);
1006
1092
  background: #fff;
1007
1093
  }
@@ -1236,19 +1322,21 @@ input[type="file"]::file-selector-button:hover {
1236
1322
  }
1237
1323
  .burndown-preview-link:hover { text-decoration: underline; }
1238
1324
  .burndown-preview-thumb {
1239
- display: inline-flex;
1325
+ display: block;
1240
1326
  border: 1px solid var(--line);
1241
1327
  border-radius: var(--radius-sm);
1242
1328
  overflow: hidden;
1243
- max-width: min(430px, calc(100vw - 44px));
1244
- max-height: 240px;
1329
+ width: 100%;
1330
+ max-width: 100%;
1331
+ max-height: 180px;
1245
1332
  background: var(--card);
1246
1333
  }
1247
1334
  .burndown-preview-thumb img {
1248
1335
  display: block;
1249
1336
  width: 100%;
1337
+ max-width: 100%;
1250
1338
  height: auto;
1251
- object-fit: cover;
1339
+ object-fit: contain;
1252
1340
  }
1253
1341
  .burndown-preview-text {
1254
1342
  margin: 0;
@@ -1279,13 +1367,34 @@ input[type="file"]::file-selector-button:hover {
1279
1367
  .pane-resizer { display: none; }
1280
1368
  .side-panel { position: static; max-height: none; }
1281
1369
  .panel-scroll { max-height: none; }
1370
+ .content { padding-top: calc(var(--controls-height) + .25rem); }
1371
+ .content-toolbar {
1372
+ top: var(--topbar-height);
1373
+ left: 1.4rem;
1374
+ right: 1.4rem;
1375
+ }
1282
1376
  .board, .summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1283
1377
  }
1284
1378
  @media (max-width: 700px) {
1285
- .topbar, .new-task-form { flex-direction: column; align-items: stretch; }
1379
+ .topbar { flex-direction: column; align-items: stretch; }
1380
+ .new-task-form { width: 100%; }
1381
+ .new-task-form .new-task-btn { width: 100%; }
1286
1382
  .board, .summary-grid { grid-template-columns: 1fr; }
1287
1383
  .timeline-row { grid-template-columns: 1fr; }
1288
1384
  .layout { padding: .5rem; }
1385
+ .content { padding-top: calc(var(--controls-height) + .6rem); }
1386
+ .content-toolbar {
1387
+ left: .5rem;
1388
+ right: .5rem;
1389
+ }
1390
+ }
1391
+
1392
+ @media (max-width: 900px) {
1393
+ .mb-actions {
1394
+ width: 100%;
1395
+ justify-content: flex-start;
1396
+ }
1397
+ .mc-stats.compact { width: 100%; }
1289
1398
  }
1290
1399
 
1291
1400
  /* --- Milestones --- */
@@ -1316,6 +1425,55 @@ input[type="file"]::file-selector-button:hover {
1316
1425
  overflow: hidden;
1317
1426
  box-shadow: var(--shadow-sm);
1318
1427
  }
1428
+ .milestone-list {
1429
+ display: grid;
1430
+ gap: .7rem;
1431
+ }
1432
+ .milestone-list-item {
1433
+ display: grid;
1434
+ gap: .42rem;
1435
+ background: var(--card);
1436
+ color: var(--text);
1437
+ border: 1px solid var(--line);
1438
+ border-left: 4px solid var(--ms-color, var(--accent));
1439
+ border-radius: var(--radius-sm);
1440
+ padding: .68rem .8rem;
1441
+ box-shadow: var(--shadow-sm);
1442
+ }
1443
+ .milestone-open-row {
1444
+ width: 100%;
1445
+ text-align: left;
1446
+ }
1447
+ .milestone-open-row:hover {
1448
+ filter: none;
1449
+ border-color: color-mix(in srgb, var(--line) 60%, var(--accent));
1450
+ box-shadow: var(--shadow-md);
1451
+ }
1452
+ .milestone-list-head {
1453
+ display: flex;
1454
+ align-items: center;
1455
+ justify-content: space-between;
1456
+ gap: .5rem;
1457
+ flex-wrap: wrap;
1458
+ }
1459
+ .milestone-list-title {
1460
+ display: flex;
1461
+ align-items: center;
1462
+ gap: .45rem;
1463
+ min-width: 0;
1464
+ }
1465
+ .milestone-list-title strong {
1466
+ font-size: .93rem;
1467
+ min-width: 0;
1468
+ }
1469
+ .mc-stats.compact { gap: .5rem; font-size: .74rem; }
1470
+ .mc-projects.compact { gap: .25rem; }
1471
+ .mc-projects.compact .project { font-size: .72rem; padding: .1rem .42rem; }
1472
+ .clamp-1 {
1473
+ overflow: hidden;
1474
+ text-overflow: ellipsis;
1475
+ white-space: nowrap;
1476
+ }
1319
1477
  .milestone-open {
1320
1478
  flex: 1;
1321
1479
  display: grid;
@@ -1360,11 +1518,13 @@ input[type="file"]::file-selector-button:hover {
1360
1518
  box-shadow: var(--shadow-sm);
1361
1519
  }
1362
1520
  .mb-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
1363
- .mb-title { display: flex; align-items: flex-start; gap: .55rem; }
1521
+ .mb-title { display: flex; align-items: flex-start; gap: .55rem; min-width: 0; flex: 1 1 18rem; }
1364
1522
  .mb-flag { color: var(--ms-color, var(--accent)); font-size: 1rem; line-height: 1.4; }
1365
1523
  .mb-title h2 { margin: 0; font-size: 1.1rem; }
1366
1524
  .mb-meta { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; margin-top: .25rem; font-size: .78rem; color: var(--muted); }
1367
- .mb-actions { display: flex; align-items: center; gap: .5rem; }
1525
+ .mb-actions { display: flex; align-items: center; justify-content: flex-end; gap: .5rem; flex: 1 1 16rem; flex-wrap: wrap; }
1526
+ .mb-actions .btn-link,
1527
+ .mb-actions .pill { white-space: nowrap; }
1368
1528
  .mb-rollup { display: grid; gap: .45rem; }
1369
1529
  .mb-stats { display: flex; flex-wrap: wrap; gap: .9rem; font-size: .8rem; color: var(--muted); }
1370
1530
  .mb-stats .done strong { color: var(--done); }
@@ -326,6 +326,57 @@
326
326
  });
327
327
  });
328
328
 
329
+ function getMainLayout() {
330
+ return document.getElementById('app-main');
331
+ }
332
+ function collapseTaskPanel() {
333
+ var main = getMainLayout();
334
+ if (!main) return;
335
+ main.classList.add('panel-collapsed');
336
+ }
337
+ function expandTaskPanel() {
338
+ var main = getMainLayout();
339
+ if (!main) return;
340
+ main.classList.remove('panel-collapsed');
341
+ }
342
+ document.addEventListener('click', function (e) {
343
+ var btn = e.target.closest('[data-panel-collapse]');
344
+ if (!btn) return;
345
+ collapseTaskPanel();
346
+ });
347
+ document.addEventListener('click', function (e) {
348
+ var panelTrigger = e.target.closest('[hx-target="#task-panel"]');
349
+ if (!panelTrigger) return;
350
+ expandTaskPanel();
351
+ });
352
+ document.addEventListener('pointerdown', function (e) {
353
+ var panelTrigger = e.target.closest('[hx-target="#task-panel"]');
354
+ if (!panelTrigger) return;
355
+ expandTaskPanel();
356
+ }, true);
357
+ document.body.addEventListener('htmx:beforeRequest', function (e) {
358
+ var trigger = e && e.detail ? e.detail.elt : null;
359
+ if (!trigger) return;
360
+ var panelTarget = trigger.getAttribute && trigger.getAttribute('hx-target');
361
+ if (panelTarget === '#task-panel' || trigger.closest('[hx-target="#task-panel"]')) {
362
+ expandTaskPanel();
363
+ }
364
+ });
365
+ document.body.addEventListener('htmx:beforeSwap', function (e) {
366
+ var target = e && e.detail ? e.detail.target : null;
367
+ if (!target) return;
368
+ var panel = target.id === 'task-panel' ? target : target.closest ? target.closest('#task-panel') : null;
369
+ if (panel) expandTaskPanel();
370
+ });
371
+ document.body.addEventListener('htmx:afterSwap', function (e) {
372
+ var target = e && e.detail ? e.detail.target : null;
373
+ if (!target) return;
374
+ var panel = target.id === 'task-panel' ? target : target.closest ? target.closest('#task-panel') : null;
375
+ if (!panel) return;
376
+ var hasContent = !!panel.children.length && (panel.textContent || '').trim().length > 0;
377
+ if (hasContent) expandTaskPanel();
378
+ });
379
+
329
380
  function paneBounds() {
330
381
  var max = Math.min(Math.floor(window.innerWidth * 0.62), 820);
331
382
  var min = 300;
@@ -1,24 +1,6 @@
1
- <main id="app-main" class="layout" data-resizable-layout>
1
+ <main id="app-main" class="layout{% if not selected_task and not selected_milestone %} panel-collapsed{% endif %}" data-resizable-layout>
2
2
  <section class="content">
3
- <section class="summary-grid">
4
- <div class="summary-card">
5
- <span class="label">Total tasks</span>
6
- <strong>{{ model.total }}</strong>
7
- </div>
8
- <div class="summary-card">
9
- <span class="label">Done</span>
10
- <strong>{{ model.counts.get('done', 0) }}</strong>
11
- </div>
12
- <div class="summary-card">
13
- <span class="label">Working</span>
14
- <strong>{{ model.counts.get('working', 0) }}</strong>
15
- </div>
16
- <div class="summary-card">
17
- <span class="label">Blocked</span>
18
- <strong>{{ model.counts.get('blocked', 0) }}</strong>
19
- </div>
20
- </section>
21
-
3
+ <div class="content-toolbar">
22
4
  <div class="filter-bar">
23
5
  <details class="filter-menu">
24
6
  <summary>
@@ -187,12 +169,12 @@
187
169
 
188
170
  <div class="view-bar">
189
171
  <div class="view-switch">
190
- <button class="{% if filters.view == 'list' %}active{% endif %}" hx-get="/partials/main?view=list{% if filters.query %}&{{ filters.query }}{% endif %}" hx-target="#app-main" hx-swap="outerHTML">Task List</button>
191
- <button class="{% if filters.view == 'board' %}active{% endif %}" hx-get="/partials/main?view=board{% if filters.query %}&{{ filters.query }}{% endif %}" hx-target="#app-main" hx-swap="outerHTML">Task Board</button>
192
- <button class="{% if filters.view == 'gantt' %}active{% endif %}" hx-get="/partials/main?view=gantt{% if filters.query %}&{{ filters.query }}{% endif %}" hx-target="#app-main" hx-swap="outerHTML">Gantt</button>
193
- <button class="{% if filters.view == 'calendar' %}active{% endif %}" hx-get="/partials/main?view=calendar{% if filters.query %}&{{ filters.query }}{% endif %}" hx-target="#app-main" hx-swap="outerHTML">Calendar</button>
194
- <button class="{% if filters.view == 'milestones' %}active{% endif %}" hx-get="/partials/main?view=milestones{% if filters.query %}&{{ filters.query }}{% endif %}" hx-target="#app-main" hx-swap="outerHTML">Milestones</button>
195
- <button class="{% if filters.view == 'projects' %}active{% endif %}" hx-get="/partials/main?view=projects{% if filters.query %}&{{ filters.query }}{% endif %}" hx-target="#app-main" hx-swap="outerHTML">Projects</button>
172
+ <button class="{% if filters.view == 'list' %}active{% endif %}" hx-get="/partials/main?view=list{% if filters.query %}&{{ filters.query }}{% endif %}{% if filters.panel_task %}&panel_task={{ filters.panel_task }}{% endif %}" hx-target="#app-main" hx-swap="outerHTML">Task List</button>
173
+ <button class="{% if filters.view == 'board' %}active{% endif %}" hx-get="/partials/main?view=board{% if filters.query %}&{{ filters.query }}{% endif %}{% if filters.panel_task %}&panel_task={{ filters.panel_task }}{% endif %}" hx-target="#app-main" hx-swap="outerHTML">Task Board</button>
174
+ <button class="{% if filters.view == 'gantt' %}active{% endif %}" hx-get="/partials/main?view=gantt{% if filters.query %}&{{ filters.query }}{% endif %}{% if filters.panel_task %}&panel_task={{ filters.panel_task }}{% endif %}" hx-target="#app-main" hx-swap="outerHTML">Gantt</button>
175
+ <button class="{% if filters.view == 'calendar' %}active{% endif %}" hx-get="/partials/main?view=calendar{% if filters.query %}&{{ filters.query }}{% endif %}{% if filters.panel_task %}&panel_task={{ filters.panel_task }}{% endif %}" hx-target="#app-main" hx-swap="outerHTML">Calendar</button>
176
+ <button class="{% if filters.view == 'milestones' %}active{% endif %}" hx-get="/partials/main?view=milestones{% if filters.query %}&{{ filters.query }}{% endif %}{% if filters.panel_task %}&panel_task={{ filters.panel_task }}{% endif %}" hx-target="#app-main" hx-swap="outerHTML">Milestones</button>
177
+ <button class="{% if filters.view == 'projects' %}active{% endif %}" hx-get="/partials/main?view=projects{% if filters.query %}&{{ filters.query }}{% endif %}{% if filters.panel_task %}&panel_task={{ filters.panel_task }}{% endif %}" hx-target="#app-main" hx-swap="outerHTML">Projects</button>
196
178
  </div>
197
179
  {% if filters.view == 'calendar' %}
198
180
  <div class="calendar-nav">
@@ -226,7 +208,7 @@
226
208
  </div>
227
209
  {% endif %}
228
210
  <form class="new-task-form" hx-post="/tasks/create" hx-target="#app-main" hx-swap="outerHTML">
229
- <input name="title" placeholder="New task title">
211
+ <input type="hidden" name="title" value="New task">
230
212
  {% for p in filters.projects %}<input type="hidden" name="f_project" value="{{ p }}">{% endfor %}
231
213
  <input type="hidden" name="f_from" value="{{ filters.date_from }}">
232
214
  <input type="hidden" name="f_to" value="{{ filters.date_to }}">
@@ -237,9 +219,10 @@
237
219
  <input type="hidden" name="f_stale_days" value="{{ filters.stale_days }}">
238
220
  <input type="hidden" name="f_calendar_month" value="{{ filters.calendar_month }}">
239
221
  <input type="hidden" name="f_calendar_year" value="{{ filters.calendar_year }}">
240
- <button type="submit">Create Task</button>
222
+ <button type="submit" class="new-task-btn">New task</button>
241
223
  </form>
242
224
  </div>
225
+ </div>
243
226
 
244
227
  {% if selected_milestone and filters.view != 'milestones' %}
245
228
  {% include "partials/milestone_banner.html" %}
@@ -268,16 +251,11 @@
268
251
  title="Drag to resize details pane. Double-click to reset."
269
252
  aria-orientation="vertical"
270
253
  ></button>
271
- <aside id="task-panel" class="side-panel">
254
+ <aside id="task-panel" class="side-panel{% if not selected_task and not selected_milestone %} side-panel-empty{% endif %}">
272
255
  {% if selected_task %}
273
256
  {% include "partials/task_panel.html" %}
274
257
  {% elif selected_milestone %}
275
258
  {% include "partials/milestone_panel.html" %}
276
- {% else %}
277
- <div class="empty-panel">
278
- <h2>Select a task</h2>
279
- <p>Details will appear when a task is selected.</p>
280
- </div>
281
259
  {% endif %}
282
260
  </aside>
283
261
  </main>
@@ -109,7 +109,7 @@
109
109
  return;
110
110
  }
111
111
  if (ext === 'pdf') {
112
- hoverCard.innerHTML += '<iframe class="burndown-preview-frame" src="' + url + '#toolbar=0" title="Attachment preview"></iframe>';
112
+ hoverCard.innerHTML += '<div class="burndown-preview-body muted">PDF preview is not shown inline here. Use “Open in new tab” to view it.</div>';
113
113
  return;
114
114
  }
115
115
  if (isTextExt(ext)) {
@@ -4,7 +4,9 @@
4
4
  <span class="ms-status {{ selected_milestone.status }}">{{ selected_milestone.status }}</span>
5
5
  <h2>{{ selected_milestone.title }}</h2>
6
6
  </div>
7
- <button class="btn-link" hx-get="/partials/main?milestone={{ selected_milestone.id }}&view={{ filters.view if filters.view != 'milestones' else 'board' }}{% if filters.show_closed %}&show_closed=1{% endif %}&stale_days={{ filters.stale_days }}" hx-target="#app-main" hx-swap="outerHTML" title="Filter the board to this milestone">Filter tasks</button>
7
+ <div class="task-panel-title-row">
8
+ <button type="button" class="btn-link task-collapse-btn" data-panel-collapse title="Collapse details" aria-label="Collapse details">×</button>
9
+ </div>
8
10
  </div>
9
11
 
10
12
  <form hx-post="/milestones/{{ selected_milestone.id }}/save" hx-target="#app-main" hx-swap="outerHTML" class="task-form">
@@ -5,38 +5,45 @@
5
5
  </div>
6
6
 
7
7
  {% if milestones %}
8
- <div class="milestone-cards">
8
+ <div class="milestone-list">
9
9
  {% for m in milestones %}
10
10
  {% set r = milestone_rollups.get(m.id) %}
11
- <div class="milestone-card {{ m.status }}" style="--ms-color: {{ m.color or '#3567e0' }}">
12
- <button class="milestone-open" hx-get="/partials/main?milestone={{ m.id }}&view=board{% if filters.show_closed %}&show_closed=1{% endif %}&stale_days={{ filters.stale_days }}" hx-target="#app-main" hx-swap="outerHTML" title="Filter tasks to this milestone">
13
- <div class="mc-head">
11
+ <button
12
+ class="milestone-list-item milestone-open-row {{ m.status }}"
13
+ style="--ms-color: {{ m.color or '#3567e0' }}"
14
+ hx-get="/partials/main?milestone={{ m.id }}&view=board{% if filters.show_closed %}&show_closed=1{% endif %}&stale_days={{ filters.stale_days }}"
15
+ hx-target="#app-main"
16
+ hx-swap="outerHTML"
17
+ title="Open milestone view"
18
+ >
19
+ <div class="milestone-list-head">
20
+ <div class="milestone-list-title">
14
21
  <span class="mc-flag" aria-hidden="true">◆</span>
15
22
  <strong>{{ m.title }}</strong>
16
23
  <span class="ms-status {{ m.status }}">{{ m.status }}</span>
17
24
  </div>
18
- {% if m.summary %}<p class="mc-summary">{{ m.summary }}</p>{% endif %}
19
- <div class="mc-projects">
20
- {% for proj in m.projects %}
21
- <span class="project" style="background: {{ project_colors.get(proj, '#2e6fd8') }}22; color: {{ project_colors.get(proj, '#2e6fd8') }}">{{ proj }}</span>
22
- {% else %}
23
- <span class="muted">No projects assigned</span>
24
- {% endfor %}
25
- </div>
26
- <div class="mc-progress" title="{{ r.progress }}% complete">
27
- <div style="width: {{ r.progress }}%; background: {{ m.color or '#3567e0' }}"></div>
28
- </div>
29
- <div class="mc-stats">
25
+ <div class="mc-stats compact">
30
26
  <span>{{ r.total }} task{{ '' if r.total == 1 else 's' }}</span>
31
27
  <span>{{ r.done }} done</span>
32
28
  <span>{{ r.progress }}%</span>
33
29
  {% if m.target_date %}<span class="mc-target">🎯 {{ m.target_date }}</span>{% endif %}
34
30
  </div>
35
- </button>
36
- <div class="mc-foot">
37
- <button class="btn-link" hx-get="/milestones/{{ m.id }}/panel?view=milestones{% if filters.show_closed %}&show_closed=1{% endif %}&stale_days={{ filters.stale_days }}" hx-target="#task-panel" hx-swap="innerHTML">Details</button>
38
31
  </div>
39
- </div>
32
+ {% if m.summary %}<p class="mc-summary clamp-1">{{ m.summary }}</p>{% endif %}
33
+ <div class="mc-projects compact">
34
+ {% for proj in m.projects[:3] %}
35
+ <span class="project" style="background: {{ project_colors.get(proj, '#2e6fd8') }}22; color: {{ project_colors.get(proj, '#2e6fd8') }}">{{ proj }}</span>
36
+ {% endfor %}
37
+ {% if m.projects|length > 3 %}
38
+ <span class="muted">+{{ m.projects|length - 3 }} more</span>
39
+ {% elif m.projects|length == 0 %}
40
+ <span class="muted">No projects assigned</span>
41
+ {% endif %}
42
+ </div>
43
+ <div class="mc-progress" title="{{ r.progress }}% complete">
44
+ <div style="width: {{ r.progress }}%; background: {{ m.color or '#3567e0' }}"></div>
45
+ </div>
46
+ </button>
40
47
  {% endfor %}
41
48
  </div>
42
49
  {% else %}
@@ -1,6 +1,7 @@
1
1
  <div class="panel-scroll">
2
2
  <div class="panel-heading sticky">
3
3
  <div class="task-panel-title-row">
4
+ <button type="button" class="btn-link task-collapse-btn" data-panel-collapse title="Collapse details" aria-label="Collapse details">×</button>
4
5
  {% if filters.milestone %}
5
6
  <button
6
7
  type="button"
@@ -313,7 +314,7 @@
313
314
  return;
314
315
  }
315
316
  if (ext === 'pdf') {
316
- hoverCard.innerHTML += '<iframe class="burndown-preview-frame" src="' + url + '#toolbar=0" title="Attachment preview"></iframe>';
317
+ hoverCard.innerHTML += '<div class="burndown-preview-body muted">PDF preview is not shown inline here. Use “Open in new tab” to view it.</div>';
317
318
  return;
318
319
  }
319
320
  if (isTextExt(ext)) {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taskunity
3
- Version: 2026.3
3
+ Version: 2026.4
4
4
  Summary: A local, file-backed productivity app for program/task tracking.
5
5
  Author: Taskunity Contributors
6
6
  License: MIT
File without changes
File without changes
File without changes