taskunity 2026.1__py3-none-any.whl

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.
@@ -0,0 +1,397 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>{{ app_name }} · {{ workspace_name }}</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="/static/app.css">
11
+ <script src="/static/chart.umd.min.js"></script>
12
+ <script src="/static/chartjs-adapter-date-fns.bundle.min.js"></script>
13
+ <script src="/static/htmx.min.js"></script>
14
+ </head>
15
+ <body>
16
+ <header class="topbar">
17
+ <div class="brand">
18
+ <span class="brand-mark">{{ app_name[:1]|upper }}</span>
19
+ <div>
20
+ <h1>{{ app_name }}</h1>
21
+ <p>{{ workspace_name }}</p>
22
+ </div>
23
+ </div>
24
+ <div class="settings-anchor">
25
+ <button class="settings-btn" id="settings-btn" aria-label="Settings" title="Settings">⚙</button>
26
+ <div class="settings-popup" id="settings-popup" hidden>
27
+ <div class="settings-popup-title">Theme</div>
28
+ <div class="theme-options">
29
+ <button class="theme-option" data-theme-opt="system" title="Follow system preference">
30
+ <span>💻</span>System
31
+ </button>
32
+ <button class="theme-option" data-theme-opt="light" title="Light mode">
33
+ <span>☀️</span>Light
34
+ </button>
35
+ <button class="theme-option" data-theme-opt="dark" title="Dark mode">
36
+ <span>🌙</span>Dark
37
+ </button>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </header>
42
+ {% block body %}{% endblock %}
43
+ <script>
44
+ (function () {
45
+ // --- App-wide localStorage key constants ---
46
+ var THEME_KEY = 'taskunity.theme';
47
+ var PANE_KEY = 'taskunity.sidePaneWidth';
48
+
49
+ // --- Theme ---
50
+ (function () {
51
+ function applyTheme(theme) {
52
+ if (theme === 'dark') {
53
+ document.documentElement.setAttribute('data-theme', 'dark');
54
+ } else if (theme === 'light') {
55
+ document.documentElement.setAttribute('data-theme', 'light');
56
+ } else {
57
+ document.documentElement.removeAttribute('data-theme');
58
+ }
59
+ document.querySelectorAll('.theme-option').forEach(function (btn) {
60
+ btn.classList.toggle('active', btn.dataset.themeOpt === (theme || 'system'));
61
+ });
62
+ }
63
+ var saved = localStorage.getItem(THEME_KEY) || 'system';
64
+ applyTheme(saved);
65
+ document.addEventListener('click', function (e) {
66
+ var opt = e.target.closest('[data-theme-opt]');
67
+ if (opt) {
68
+ var t = opt.dataset.themeOpt;
69
+ localStorage.setItem(THEME_KEY, t);
70
+ applyTheme(t);
71
+ return;
72
+ }
73
+ var btn = e.target.closest('#settings-btn');
74
+ if (btn) {
75
+ var popup = document.getElementById('settings-popup');
76
+ if (popup) popup.hidden = !popup.hidden;
77
+ if (popup && !popup.hidden) {
78
+ var saved2 = localStorage.getItem(THEME_KEY) || 'system';
79
+ document.querySelectorAll('.theme-option').forEach(function (b) {
80
+ b.classList.toggle('active', b.dataset.themeOpt === saved2);
81
+ });
82
+ }
83
+ return;
84
+ }
85
+ var popup2 = document.getElementById('settings-popup');
86
+ if (popup2 && !e.target.closest('.settings-anchor')) popup2.hidden = true;
87
+ });
88
+ })();
89
+
90
+ function setProject(picker, value, color) {
91
+ var hidden = picker.querySelector('input[name="project"]');
92
+ var current = picker.querySelector('.project-dd-current');
93
+ if (hidden) hidden.value = value;
94
+ if (current) {
95
+ if (color) {
96
+ current.innerHTML = '<span class="swatch" style="background:' + color + '"></span>';
97
+ current.appendChild(document.createTextNode(value));
98
+ } else {
99
+ current.textContent = value;
100
+ }
101
+ }
102
+ picker.querySelectorAll('.project-dd-item').forEach(function (b) {
103
+ b.classList.toggle('selected', b.dataset.value === value);
104
+ });
105
+ var dd = picker.querySelector('details');
106
+ if (dd) dd.open = false;
107
+ }
108
+ document.addEventListener('click', function (e) {
109
+ var item = e.target.closest('.project-dd-item');
110
+ if (item) {
111
+ var picker = item.closest('[data-project-picker]');
112
+ if (picker) setProject(picker, item.dataset.value, item.dataset.color);
113
+ return;
114
+ }
115
+ document.querySelectorAll('.project-dd[open]').forEach(function (dd) {
116
+ if (!dd.contains(e.target)) dd.open = false;
117
+ });
118
+ });
119
+ document.addEventListener('keydown', function (e) {
120
+ if (e.target.classList && e.target.classList.contains('project-dd-input') && e.key === 'Enter') {
121
+ e.preventDefault();
122
+ var val = e.target.value.trim();
123
+ if (!val) return;
124
+ var picker = e.target.closest('[data-project-picker]');
125
+ if (picker) setProject(picker, val, '');
126
+ }
127
+ });
128
+
129
+ function depRefresh(picker) {
130
+ var hidden = picker.querySelector('input[name="depends_on"]');
131
+ var ids = Array.prototype.map.call(picker.querySelectorAll('.dep-chip'), function (c) { return c.dataset.id; });
132
+ if (hidden) hidden.value = ids.join(', ');
133
+ }
134
+ function depFilter(picker) {
135
+ var input = picker.querySelector('.dep-search-input');
136
+ var menu = picker.querySelector('.dep-menu');
137
+ var empty = picker.querySelector('.dep-empty');
138
+ var q = (input.value || '').trim().toLowerCase();
139
+ if (q === '') { menu.hidden = true; if (empty) empty.hidden = true; return; }
140
+ var shown = 0;
141
+ picker.querySelectorAll('.dep-option').forEach(function (o) {
142
+ var already = !!picker.querySelector('.dep-chip[data-id="' + o.dataset.id + '"]');
143
+ var match = o.dataset.search.indexOf(q) !== -1;
144
+ var show = match && !already;
145
+ o.hidden = !show;
146
+ if (show) shown++;
147
+ });
148
+ if (empty) empty.hidden = shown !== 0;
149
+ menu.hidden = false;
150
+ }
151
+ function depAdd(picker, id, title) {
152
+ if (picker.querySelector('.dep-chip[data-id="' + id + '"]')) return;
153
+ var chips = picker.querySelector('.dep-chips');
154
+ var chip = document.createElement('span');
155
+ chip.className = 'dep-chip';
156
+ chip.dataset.id = id;
157
+ chip.appendChild(document.createTextNode(title));
158
+ var x = document.createElement('button');
159
+ x.type = 'button';
160
+ x.className = 'dep-chip-x';
161
+ x.setAttribute('data-dep-remove', '');
162
+ x.setAttribute('aria-label', 'Remove dependency');
163
+ x.textContent = '×';
164
+ chip.appendChild(x);
165
+ chips.appendChild(chip);
166
+ depRefresh(picker);
167
+ var input = picker.querySelector('.dep-search-input');
168
+ if (input) input.value = '';
169
+ var menu = picker.querySelector('.dep-menu');
170
+ if (menu) menu.hidden = true;
171
+ }
172
+ document.addEventListener('input', function (e) {
173
+ if (e.target.classList && e.target.classList.contains('dep-search-input')) {
174
+ var picker = e.target.closest('[data-dep-picker]');
175
+ if (picker) depFilter(picker);
176
+ }
177
+ });
178
+ document.addEventListener('focusin', function (e) {
179
+ if (e.target.classList && e.target.classList.contains('dep-search-input')) {
180
+ var picker = e.target.closest('[data-dep-picker]');
181
+ if (picker) depFilter(picker);
182
+ }
183
+ });
184
+ document.addEventListener('click', function (e) {
185
+ var opt = e.target.closest('.dep-option');
186
+ if (opt) {
187
+ var picker = opt.closest('[data-dep-picker]');
188
+ var titleEl = opt.querySelector('.dep-option-title');
189
+ if (picker) depAdd(picker, opt.dataset.id, titleEl ? titleEl.textContent : opt.dataset.id);
190
+ return;
191
+ }
192
+ var rm = e.target.closest('[data-dep-remove]');
193
+ if (rm) {
194
+ var picker2 = rm.closest('[data-dep-picker]');
195
+ var chip = rm.closest('.dep-chip');
196
+ if (chip) chip.remove();
197
+ if (picker2) depRefresh(picker2);
198
+ return;
199
+ }
200
+ document.querySelectorAll('[data-dep-picker]').forEach(function (p) {
201
+ if (!p.contains(e.target)) {
202
+ var m = p.querySelector('.dep-menu');
203
+ if (m) m.hidden = true;
204
+ }
205
+ });
206
+ });
207
+
208
+ function msAddFilter(box) {
209
+ var input = box.querySelector('.ms-add-input');
210
+ var menu = box.querySelector('.ms-add-menu');
211
+ var empty = box.querySelector('.dep-empty');
212
+ var q = (input.value || '').trim().toLowerCase();
213
+ var shown = 0;
214
+ box.querySelectorAll('.ms-add-option').forEach(function (o) {
215
+ var match = q === '' ? true : o.dataset.search.indexOf(q) !== -1;
216
+ o.hidden = !match;
217
+ if (match) shown++;
218
+ });
219
+ if (empty) empty.hidden = shown !== 0;
220
+ if (menu) menu.hidden = false;
221
+ }
222
+ document.addEventListener('input', function (e) {
223
+ if (e.target.classList && e.target.classList.contains('ms-add-input')) {
224
+ var box = e.target.closest('[data-ms-add]');
225
+ if (box) msAddFilter(box);
226
+ }
227
+ });
228
+ document.addEventListener('focusin', function (e) {
229
+ if (e.target.classList && e.target.classList.contains('ms-add-input')) {
230
+ var box = e.target.closest('[data-ms-add]');
231
+ if (box) msAddFilter(box);
232
+ }
233
+ });
234
+ document.addEventListener('click', function (e) {
235
+ if (e.target.closest('.ms-add-option')) return;
236
+ document.querySelectorAll('[data-ms-add]').forEach(function (box) {
237
+ if (!box.contains(e.target)) {
238
+ var m = box.querySelector('.ms-add-menu');
239
+ if (m) m.hidden = true;
240
+ }
241
+ });
242
+ });
243
+
244
+ // Milestone project chips: add via select, remove via x
245
+ document.addEventListener('change', function (e) {
246
+ if (!(e.target.classList && e.target.classList.contains('ms-proj-select'))) return;
247
+ var sel = e.target;
248
+ var name = sel.value;
249
+ if (!name) return;
250
+ var box = sel.closest('[data-ms-projects]');
251
+ if (!box) return;
252
+ var chips = box.querySelector('.ms-proj-chips');
253
+ if (chips.querySelector('.ms-proj-chip[data-name="' + name + '"]')) { sel.value = ''; return; }
254
+ var color = sel.options[sel.selectedIndex].dataset.color || '#2e6fd8';
255
+ var chip = document.createElement('span');
256
+ chip.className = 'ms-proj-chip';
257
+ chip.dataset.name = name;
258
+ var sw = document.createElement('span');
259
+ sw.className = 'swatch';
260
+ sw.style.background = color;
261
+ chip.appendChild(sw);
262
+ chip.appendChild(document.createTextNode(name));
263
+ var hid = document.createElement('input');
264
+ hid.type = 'hidden';
265
+ hid.name = 'projects';
266
+ hid.value = name;
267
+ chip.appendChild(hid);
268
+ var x = document.createElement('button');
269
+ x.type = 'button';
270
+ x.className = 'ms-proj-x';
271
+ x.setAttribute('data-ms-proj-remove', '');
272
+ x.setAttribute('aria-label', 'Remove project');
273
+ x.textContent = '×';
274
+ chip.appendChild(x);
275
+ chips.appendChild(chip);
276
+ sel.remove(sel.selectedIndex);
277
+ sel.value = '';
278
+ });
279
+ document.addEventListener('click', function (e) {
280
+ var pr = e.target.closest('[data-ms-proj-remove]');
281
+ if (!pr) return;
282
+ var chip = pr.closest('.ms-proj-chip');
283
+ var box = pr.closest('[data-ms-projects]');
284
+ if (!chip || !box) return;
285
+ var name = chip.dataset.name;
286
+ var sel = box.querySelector('.ms-proj-select');
287
+ chip.remove();
288
+ if (sel && !Array.prototype.some.call(sel.options, function (o) { return o.value === name; })) {
289
+ var o = document.createElement('option');
290
+ o.value = name;
291
+ o.textContent = name;
292
+ sel.appendChild(o);
293
+ }
294
+ });
295
+
296
+ document.addEventListener('change', function (e) {
297
+ if (!(e.target.classList && e.target.classList.contains('hide-old-toggle'))) return;
298
+ var form = e.target.closest('form');
299
+ if (!form) return;
300
+ var hidden = form.querySelector('[data-show-closed-hidden]');
301
+ if (hidden) hidden.value = e.target.checked ? '1' : '0';
302
+ });
303
+
304
+ function setComposerTab(root, name) {
305
+ if (!root) return;
306
+ root.querySelectorAll('[data-activity-tab]').forEach(function (btn) {
307
+ btn.classList.toggle('active', btn.dataset.activityTab === name);
308
+ });
309
+ root.querySelectorAll('[data-activity-pane]').forEach(function (pane) {
310
+ pane.hidden = pane.dataset.activityPane !== name;
311
+ });
312
+ }
313
+ document.addEventListener('click', function (e) {
314
+ var tab = e.target.closest('[data-activity-tab]');
315
+ if (!tab) return;
316
+ var root = tab.closest('[data-activity-composer]');
317
+ if (root) setComposerTab(root, tab.dataset.activityTab);
318
+ });
319
+ document.querySelectorAll('[data-activity-composer]').forEach(function (root) {
320
+ setComposerTab(root, 'note');
321
+ });
322
+ document.body.addEventListener('htmx:afterSwap', function () {
323
+ document.querySelectorAll('[data-activity-composer]').forEach(function (root) {
324
+ var active = root.querySelector('[data-activity-tab].active');
325
+ setComposerTab(root, active ? active.dataset.activityTab : 'note');
326
+ });
327
+ });
328
+
329
+ function paneBounds() {
330
+ var max = Math.min(Math.floor(window.innerWidth * 0.62), 820);
331
+ var min = 300;
332
+ if (max < min) max = min;
333
+ return { min: min, max: max };
334
+ }
335
+ function setPaneWidth(layout, px) {
336
+ if (!layout) return;
337
+ var b = paneBounds();
338
+ var clamped = Math.max(b.min, Math.min(b.max, Math.round(px)));
339
+ layout.style.setProperty('--side-pane-width', clamped + 'px');
340
+ }
341
+ function initPaneResizer(root) {
342
+ var layout = (root || document).querySelector('[data-resizable-layout]');
343
+ var resizer = (root || document).querySelector('[data-pane-resizer]');
344
+ if (!layout || !resizer) return;
345
+ var saved = parseInt(localStorage.getItem(PANE_KEY) || '', 10);
346
+ if (!Number.isNaN(saved)) setPaneWidth(layout, saved);
347
+ var dragging = false;
348
+ function widthFromPointer(clientX) {
349
+ var rect = layout.getBoundingClientRect();
350
+ return rect.right - clientX;
351
+ }
352
+ function commit(px) {
353
+ setPaneWidth(layout, px);
354
+ var current = parseInt(getComputedStyle(layout).getPropertyValue('--side-pane-width'), 10);
355
+ if (!Number.isNaN(current)) localStorage.setItem(PANE_KEY, String(current));
356
+ }
357
+ resizer.addEventListener('mousedown', function (e) {
358
+ dragging = true;
359
+ layout.classList.add('resizing');
360
+ e.preventDefault();
361
+ });
362
+ document.addEventListener('mousemove', function (e) {
363
+ if (!dragging) return;
364
+ commit(widthFromPointer(e.clientX));
365
+ });
366
+ document.addEventListener('mouseup', function () {
367
+ if (!dragging) return;
368
+ dragging = false;
369
+ layout.classList.remove('resizing');
370
+ });
371
+ resizer.addEventListener('dblclick', function () {
372
+ localStorage.removeItem(PANE_KEY);
373
+ layout.style.removeProperty('--side-pane-width');
374
+ });
375
+ resizer.addEventListener('keydown', function (e) {
376
+ if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
377
+ var step = e.shiftKey ? 40 : 16;
378
+ var current = parseInt(getComputedStyle(layout).getPropertyValue('--side-pane-width'), 10);
379
+ if (Number.isNaN(current)) current = 430;
380
+ if (e.key === 'ArrowLeft') current += step;
381
+ if (e.key === 'ArrowRight') current -= step;
382
+ commit(current);
383
+ e.preventDefault();
384
+ });
385
+ window.addEventListener('resize', function () {
386
+ var current = parseInt(getComputedStyle(layout).getPropertyValue('--side-pane-width'), 10);
387
+ if (!Number.isNaN(current)) setPaneWidth(layout, current);
388
+ });
389
+ }
390
+ initPaneResizer(document);
391
+ document.body.addEventListener('htmx:afterSwap', function () {
392
+ initPaneResizer(document);
393
+ });
394
+ })();
395
+ </script>
396
+ </body>
397
+ </html>
@@ -0,0 +1,4 @@
1
+ {% extends "base.html" %}
2
+ {% block body %}
3
+ {% include "partials/main.html" %}
4
+ {% endblock %}
@@ -0,0 +1,24 @@
1
+ <section class="panel">
2
+ <div class="panel-heading"><h2>Board</h2></div>
3
+ <div class="board">
4
+ {% for status in statuses %}
5
+ <div class="column">
6
+ <h3>{{ status|capitalize }} <span>{{ model.by_status.get(status, [])|length }}</span></h3>
7
+ {% for task in model.by_status.get(status, []) %}
8
+ <button class="task-card {{ task.status }}"{% if task.project %} style="border-left-color: {{ project_colors.get(task.project, '#3567e0') }}"{% endif %} hx-get="/tasks/{{ task.id }}/panel?view={{ filters.view }}{% if filters.query %}&{{ filters.query }}{% endif %}" hx-target="#task-panel" hx-swap="innerHTML">
9
+ <strong>{{ task.title }}</strong>
10
+ {% if task.due_date %}<small>due {{ task.due_date }}</small>{% endif %}
11
+ {% if task.summary %}<p>{{ task.summary }}</p>{% endif %}
12
+ <div class="meta">
13
+ <span class="priority-tag {{ task.priority }}">{{ task.priority }}</span>
14
+ {% if task.project %}<span class="project" style="background: {{ project_colors.get(task.project, '#2e6fd8') }}22; color: {{ project_colors.get(task.project, '#2e6fd8') }}">{{ task.project }}</span>{% endif %}
15
+ </div>
16
+ <div class="mini-progress" title="{{ task.percent_complete }}% complete">
17
+ <div style="width: {{ task.percent_complete }}%"></div>
18
+ </div>
19
+ </button>
20
+ {% endfor %}
21
+ </div>
22
+ {% endfor %}
23
+ </div>
24
+ </section>
@@ -0,0 +1,25 @@
1
+ <div class="schedule-body">
2
+ <div class="calendar-headline">
3
+ <p class="schedule-range">{{ calendar.current_month_label }}</p>
4
+ <p class="calendar-subtle">Move by month with the arrows or jump controls.</p>
5
+ </div>
6
+ <div class="calendar-carousel">
7
+ <div class="cal-head">
8
+ {% for label in calendar.weekdays %}<div>{{ label }}</div>{% endfor %}
9
+ </div>
10
+ {% for week in calendar.weeks %}
11
+ <div class="cal-week">
12
+ {% for day in week %}
13
+ <div class="cal-day{% if not day.in_month %} out{% endif %}{% if not day.in_range %} muted-day{% endif %}{% if day.today %} today{% endif %}">
14
+ <span class="cal-date">{{ day.date.day }}</span>
15
+ {% for task in day.tasks %}
16
+ <button class="cal-task {{ task.status }}"
17
+ hx-get="/tasks/{{ task.id }}/panel?view={{ filters.view }}{% if filters.query %}&{{ filters.query }}{% endif %}" hx-target="#task-panel" hx-swap="innerHTML"
18
+ title="{{ task.title }}{% if task.project %} · {{ task.project }}{% endif %}">{{ task.title }}</button>
19
+ {% endfor %}
20
+ </div>
21
+ {% endfor %}
22
+ </div>
23
+ {% endfor %}
24
+ </div>
25
+ </div>