hzl-web 2.0.0 → 2.2.0
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.
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +110 -8
- package/dist/server.js.map +1 -1
- package/dist/server.test.js +82 -0
- package/dist/server.test.js.map +1 -1
- package/dist/ui/apple-touch-icon.png +0 -0
- package/dist/ui/favicon-96x96.png +0 -0
- package/dist/ui/favicon.ico +0 -0
- package/dist/ui/index.html +887 -100
- package/dist/ui/site.webmanifest +25 -0
- package/dist/ui/sw.js +76 -0
- package/dist/ui/web-app-manifest-192x192.png +0 -0
- package/dist/ui/web-app-manifest-512x512.png +0 -0
- package/dist/ui-embed.d.ts +7 -0
- package/dist/ui-embed.d.ts.map +1 -1
- package/dist/ui-embed.js +21 -7
- package/dist/ui-embed.js.map +1 -1
- package/package.json +3 -2
package/dist/ui/index.html
CHANGED
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<
|
|
6
|
+
<meta name="theme-color" content="#1a1a1a">
|
|
7
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
8
|
+
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96">
|
|
9
|
+
<link rel="shortcut icon" href="/favicon.ico">
|
|
10
|
+
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
|
11
|
+
<meta name="apple-mobile-web-app-title" content="HZL">
|
|
12
|
+
<link rel="manifest" href="/site.webmanifest">
|
|
13
|
+
<title>HZL</title>
|
|
7
14
|
<style>
|
|
8
15
|
:root {
|
|
9
16
|
--bg-primary: #1a1a1a;
|
|
@@ -42,7 +49,8 @@
|
|
|
42
49
|
.header {
|
|
43
50
|
display: flex;
|
|
44
51
|
align-items: center;
|
|
45
|
-
justify-content:
|
|
52
|
+
justify-content: flex-start;
|
|
53
|
+
gap: 14px;
|
|
46
54
|
padding: 12px 16px;
|
|
47
55
|
background: var(--bg-secondary);
|
|
48
56
|
border-bottom: 1px solid var(--border);
|
|
@@ -55,6 +63,7 @@
|
|
|
55
63
|
display: flex;
|
|
56
64
|
align-items: center;
|
|
57
65
|
gap: 8px;
|
|
66
|
+
flex-shrink: 0;
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
.logo {
|
|
@@ -66,13 +75,15 @@
|
|
|
66
75
|
.header-filters {
|
|
67
76
|
display: flex;
|
|
68
77
|
align-items: center;
|
|
69
|
-
gap:
|
|
78
|
+
gap: 8px;
|
|
79
|
+
flex: 1;
|
|
80
|
+
min-width: 0;
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
.filter-group {
|
|
73
84
|
display: flex;
|
|
74
85
|
align-items: center;
|
|
75
|
-
gap:
|
|
86
|
+
gap: 8px;
|
|
76
87
|
position: relative;
|
|
77
88
|
}
|
|
78
89
|
|
|
@@ -87,9 +98,11 @@
|
|
|
87
98
|
background: var(--bg-primary);
|
|
88
99
|
color: var(--text-primary);
|
|
89
100
|
border: 1px solid var(--border);
|
|
90
|
-
padding:
|
|
91
|
-
border-radius:
|
|
101
|
+
padding: 0 44px 0 12px;
|
|
102
|
+
border-radius: 6px;
|
|
92
103
|
cursor: pointer;
|
|
104
|
+
min-height: 42px;
|
|
105
|
+
line-height: 1.2;
|
|
93
106
|
}
|
|
94
107
|
|
|
95
108
|
select:focus {
|
|
@@ -97,10 +110,69 @@
|
|
|
97
110
|
border-color: var(--accent);
|
|
98
111
|
}
|
|
99
112
|
|
|
113
|
+
.task-search-group {
|
|
114
|
+
gap: 6px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.task-search-input {
|
|
118
|
+
width: 220px;
|
|
119
|
+
font-family: var(--font-mono);
|
|
120
|
+
font-size: 12px;
|
|
121
|
+
background: var(--bg-primary);
|
|
122
|
+
color: var(--text-primary);
|
|
123
|
+
border: 1px solid var(--border);
|
|
124
|
+
padding: 0 12px;
|
|
125
|
+
border-radius: 6px;
|
|
126
|
+
min-height: 42px;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.task-search-input:focus {
|
|
130
|
+
outline: none;
|
|
131
|
+
border-color: var(--accent);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.task-search-clear {
|
|
135
|
+
border: 1px solid var(--border);
|
|
136
|
+
border-radius: 4px;
|
|
137
|
+
background: var(--bg-primary);
|
|
138
|
+
color: var(--text-secondary);
|
|
139
|
+
font-family: var(--font-mono);
|
|
140
|
+
font-size: 12px;
|
|
141
|
+
line-height: 1;
|
|
142
|
+
padding: 4px 8px;
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.task-search-clear:hover {
|
|
147
|
+
border-color: var(--accent);
|
|
148
|
+
color: var(--text-primary);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.task-search-clear[hidden] {
|
|
152
|
+
display: none;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.task-search-meta {
|
|
156
|
+
min-width: 0;
|
|
157
|
+
width: 0;
|
|
158
|
+
overflow: hidden;
|
|
159
|
+
font-size: 11px;
|
|
160
|
+
color: var(--text-muted);
|
|
161
|
+
text-align: right;
|
|
162
|
+
font-variant-numeric: tabular-nums;
|
|
163
|
+
transition: width 120ms ease;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.task-search-group.active .task-search-meta {
|
|
167
|
+
width: 56px;
|
|
168
|
+
}
|
|
169
|
+
|
|
100
170
|
.header-right {
|
|
101
171
|
display: flex;
|
|
102
172
|
align-items: center;
|
|
103
173
|
gap: 12px;
|
|
174
|
+
margin-left: auto;
|
|
175
|
+
flex-shrink: 0;
|
|
104
176
|
}
|
|
105
177
|
|
|
106
178
|
.connection-indicator {
|
|
@@ -134,8 +206,9 @@
|
|
|
134
206
|
background: var(--bg-primary);
|
|
135
207
|
color: var(--text-primary);
|
|
136
208
|
border: 1px solid var(--border);
|
|
137
|
-
|
|
138
|
-
|
|
209
|
+
min-height: 42px;
|
|
210
|
+
padding: 0 14px;
|
|
211
|
+
border-radius: 6px;
|
|
139
212
|
cursor: pointer;
|
|
140
213
|
}
|
|
141
214
|
|
|
@@ -143,6 +216,63 @@
|
|
|
143
216
|
border-color: var(--accent);
|
|
144
217
|
}
|
|
145
218
|
|
|
219
|
+
.settings-shortcuts-btn {
|
|
220
|
+
width: 100%;
|
|
221
|
+
font-family: var(--font-mono);
|
|
222
|
+
font-size: 12px;
|
|
223
|
+
background: var(--bg-primary);
|
|
224
|
+
color: var(--text-secondary);
|
|
225
|
+
border: 1px solid var(--border);
|
|
226
|
+
padding: 6px 8px;
|
|
227
|
+
border-radius: 4px;
|
|
228
|
+
cursor: pointer;
|
|
229
|
+
text-align: left;
|
|
230
|
+
white-space: nowrap;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.settings-shortcuts-btn:hover {
|
|
234
|
+
color: var(--text-primary);
|
|
235
|
+
border-color: var(--accent);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.settings-view-select {
|
|
239
|
+
width: 100%;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.collapse-parents-actions {
|
|
243
|
+
display: flex;
|
|
244
|
+
gap: 6px;
|
|
245
|
+
margin-top: 2px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.collapse-parents-btn {
|
|
249
|
+
flex: 1;
|
|
250
|
+
font-family: var(--font-mono);
|
|
251
|
+
font-size: 11px;
|
|
252
|
+
background: var(--bg-primary);
|
|
253
|
+
color: var(--text-secondary);
|
|
254
|
+
border: 1px solid var(--border);
|
|
255
|
+
padding: 5px 6px;
|
|
256
|
+
border-radius: 4px;
|
|
257
|
+
cursor: pointer;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.collapse-parents-btn:hover:not(:disabled) {
|
|
261
|
+
color: var(--text-primary);
|
|
262
|
+
border-color: var(--accent);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.collapse-parents-btn:disabled {
|
|
266
|
+
opacity: 0.45;
|
|
267
|
+
cursor: not-allowed;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.collapse-parents-meta {
|
|
271
|
+
margin-top: 5px;
|
|
272
|
+
font-size: 10px;
|
|
273
|
+
color: var(--text-muted);
|
|
274
|
+
}
|
|
275
|
+
|
|
146
276
|
/* Column Visibility Dropdown */
|
|
147
277
|
.columns-toggle {
|
|
148
278
|
font-family: var(--font-mono);
|
|
@@ -207,11 +337,25 @@
|
|
|
207
337
|
background: var(--bg-primary);
|
|
208
338
|
color: var(--text-secondary);
|
|
209
339
|
border: 1px solid var(--border);
|
|
210
|
-
|
|
211
|
-
|
|
340
|
+
width: 42px;
|
|
341
|
+
height: 42px;
|
|
342
|
+
padding: 0;
|
|
343
|
+
border-radius: 6px;
|
|
212
344
|
cursor: pointer;
|
|
213
345
|
}
|
|
214
346
|
|
|
347
|
+
#dateFilter {
|
|
348
|
+
min-width: 150px;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
#projectFilter {
|
|
352
|
+
min-width: 220px;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
#assigneeFilter {
|
|
356
|
+
min-width: 180px;
|
|
357
|
+
}
|
|
358
|
+
|
|
215
359
|
.settings-toggle:hover {
|
|
216
360
|
border-color: var(--accent);
|
|
217
361
|
color: var(--text-primary);
|
|
@@ -484,6 +628,26 @@
|
|
|
484
628
|
margin-bottom: 6px;
|
|
485
629
|
}
|
|
486
630
|
|
|
631
|
+
.card-subtask-toggle {
|
|
632
|
+
display: inline-flex;
|
|
633
|
+
align-items: center;
|
|
634
|
+
gap: 4px;
|
|
635
|
+
border: 1px solid var(--border);
|
|
636
|
+
border-radius: 4px;
|
|
637
|
+
background: var(--bg-primary);
|
|
638
|
+
color: var(--text-muted);
|
|
639
|
+
font-family: var(--font-mono);
|
|
640
|
+
font-size: 11px;
|
|
641
|
+
padding: 2px 6px;
|
|
642
|
+
margin-bottom: 6px;
|
|
643
|
+
cursor: pointer;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.card-subtask-toggle:hover {
|
|
647
|
+
color: var(--text-primary);
|
|
648
|
+
border-color: var(--accent);
|
|
649
|
+
}
|
|
650
|
+
|
|
487
651
|
.card-blocked {
|
|
488
652
|
font-size: 10px;
|
|
489
653
|
color: var(--status-blocked);
|
|
@@ -977,6 +1141,91 @@
|
|
|
977
1141
|
display: block;
|
|
978
1142
|
}
|
|
979
1143
|
|
|
1144
|
+
.shortcuts-modal-overlay {
|
|
1145
|
+
position: fixed;
|
|
1146
|
+
inset: 0;
|
|
1147
|
+
background: rgba(0, 0, 0, 0.65);
|
|
1148
|
+
display: none;
|
|
1149
|
+
align-items: center;
|
|
1150
|
+
justify-content: center;
|
|
1151
|
+
z-index: 300;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
.shortcuts-modal-overlay.open {
|
|
1155
|
+
display: flex;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
.shortcuts-modal {
|
|
1159
|
+
width: min(460px, calc(100vw - 24px));
|
|
1160
|
+
background: var(--bg-secondary);
|
|
1161
|
+
border: 1px solid var(--border);
|
|
1162
|
+
border-radius: 8px;
|
|
1163
|
+
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
|
|
1164
|
+
overflow: hidden;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
.shortcuts-header {
|
|
1168
|
+
display: flex;
|
|
1169
|
+
align-items: center;
|
|
1170
|
+
justify-content: space-between;
|
|
1171
|
+
padding: 12px 16px;
|
|
1172
|
+
border-bottom: 1px solid var(--border);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
.shortcuts-title {
|
|
1176
|
+
font-size: 14px;
|
|
1177
|
+
font-weight: 600;
|
|
1178
|
+
color: var(--text-primary);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
.shortcuts-close {
|
|
1182
|
+
border: none;
|
|
1183
|
+
background: transparent;
|
|
1184
|
+
color: var(--text-muted);
|
|
1185
|
+
font-size: 18px;
|
|
1186
|
+
cursor: pointer;
|
|
1187
|
+
line-height: 1;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
.shortcuts-close:hover {
|
|
1191
|
+
color: var(--text-primary);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
.shortcuts-body {
|
|
1195
|
+
padding: 14px 16px 16px;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
.shortcuts-list {
|
|
1199
|
+
display: grid;
|
|
1200
|
+
grid-template-columns: auto 1fr;
|
|
1201
|
+
gap: 8px 12px;
|
|
1202
|
+
align-items: center;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
.shortcut-key {
|
|
1206
|
+
display: inline-block;
|
|
1207
|
+
min-width: 34px;
|
|
1208
|
+
padding: 1px 8px;
|
|
1209
|
+
border: 1px solid var(--border);
|
|
1210
|
+
border-radius: 4px;
|
|
1211
|
+
background: var(--bg-primary);
|
|
1212
|
+
color: var(--text-primary);
|
|
1213
|
+
font-size: 11px;
|
|
1214
|
+
text-align: center;
|
|
1215
|
+
font-weight: 600;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
.shortcut-desc {
|
|
1219
|
+
color: var(--text-secondary);
|
|
1220
|
+
font-size: 12px;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
.shortcuts-note {
|
|
1224
|
+
margin-top: 12px;
|
|
1225
|
+
font-size: 11px;
|
|
1226
|
+
color: var(--text-muted);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
980
1229
|
/* Activity Panel */
|
|
981
1230
|
.activity-panel {
|
|
982
1231
|
position: fixed;
|
|
@@ -1100,41 +1349,6 @@
|
|
|
1100
1349
|
margin-top: 2px;
|
|
1101
1350
|
}
|
|
1102
1351
|
|
|
1103
|
-
/* View Toggle */
|
|
1104
|
-
.view-toggle {
|
|
1105
|
-
display: flex;
|
|
1106
|
-
gap: 0;
|
|
1107
|
-
background: var(--bg-primary);
|
|
1108
|
-
border-radius: 6px;
|
|
1109
|
-
padding: 2px;
|
|
1110
|
-
border: 1px solid var(--border);
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
.view-btn {
|
|
1114
|
-
padding: 6px 12px;
|
|
1115
|
-
border: none;
|
|
1116
|
-
background: transparent;
|
|
1117
|
-
color: var(--text-secondary);
|
|
1118
|
-
cursor: pointer;
|
|
1119
|
-
border-radius: 4px;
|
|
1120
|
-
font-family: var(--font-mono);
|
|
1121
|
-
font-size: 12px;
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
.view-btn.active {
|
|
1125
|
-
background: var(--accent);
|
|
1126
|
-
color: var(--bg-primary);
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
.view-btn:hover:not(.active):not(:disabled) {
|
|
1130
|
-
color: var(--text-primary);
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
.view-btn:disabled {
|
|
1134
|
-
opacity: 0.5;
|
|
1135
|
-
cursor: not-allowed;
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
1352
|
/* Graph Container */
|
|
1139
1353
|
.graph-container {
|
|
1140
1354
|
flex: 1;
|
|
@@ -1175,13 +1389,66 @@
|
|
|
1175
1389
|
@media (max-width: 768px) {
|
|
1176
1390
|
.header {
|
|
1177
1391
|
flex-wrap: wrap;
|
|
1392
|
+
row-gap: 10px;
|
|
1393
|
+
align-items: center;
|
|
1394
|
+
align-content: flex-start;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
.header-left {
|
|
1398
|
+
order: 1;
|
|
1399
|
+
flex: 0 0 auto;
|
|
1400
|
+
min-height: 42px;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
.header-right {
|
|
1404
|
+
order: 2;
|
|
1405
|
+
flex: 0 0 auto;
|
|
1406
|
+
margin-left: auto;
|
|
1407
|
+
min-height: 42px;
|
|
1178
1408
|
gap: 8px;
|
|
1179
1409
|
}
|
|
1180
1410
|
|
|
1181
1411
|
.header-filters {
|
|
1182
1412
|
order: 3;
|
|
1413
|
+
flex: 0 0 100%;
|
|
1183
1414
|
width: 100%;
|
|
1184
|
-
|
|
1415
|
+
max-width: 100%;
|
|
1416
|
+
min-width: 100%;
|
|
1417
|
+
display: none;
|
|
1418
|
+
flex-direction: column;
|
|
1419
|
+
align-items: stretch;
|
|
1420
|
+
gap: 8px;
|
|
1421
|
+
border-top: 1px solid var(--border);
|
|
1422
|
+
padding-top: 8px;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
.header-filters.open {
|
|
1426
|
+
display: flex;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
.filter-group {
|
|
1430
|
+
width: 100%;
|
|
1431
|
+
max-width: 100%;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
.task-search-group {
|
|
1435
|
+
width: 100%;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
.task-search-input {
|
|
1439
|
+
flex: 1;
|
|
1440
|
+
width: 100%;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
#dateFilter,
|
|
1444
|
+
#projectFilter,
|
|
1445
|
+
#assigneeFilter {
|
|
1446
|
+
min-width: 0;
|
|
1447
|
+
width: 100%;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
.task-search-meta {
|
|
1451
|
+
display: none;
|
|
1185
1452
|
}
|
|
1186
1453
|
|
|
1187
1454
|
.board {
|
|
@@ -1192,10 +1459,6 @@
|
|
|
1192
1459
|
min-height: calc(100vh - 150px);
|
|
1193
1460
|
}
|
|
1194
1461
|
|
|
1195
|
-
.view-toggle {
|
|
1196
|
-
margin-left: auto;
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
1462
|
.mobile-tabs {
|
|
1200
1463
|
display: flex;
|
|
1201
1464
|
overflow-x: auto;
|
|
@@ -1428,12 +1691,7 @@
|
|
|
1428
1691
|
<header class="header">
|
|
1429
1692
|
<div class="header-left">
|
|
1430
1693
|
<button class="hamburger" id="hamburgerBtn">☰</button>
|
|
1431
|
-
<span class="logo">
|
|
1432
|
-
<div class="view-toggle" id="viewToggle">
|
|
1433
|
-
<button class="view-btn active" data-view="kanban">Kanban</button>
|
|
1434
|
-
<button class="view-btn" data-view="calendar">Calendar</button>
|
|
1435
|
-
<button class="view-btn" data-view="graph">Graph</button>
|
|
1436
|
-
</div>
|
|
1694
|
+
<span class="logo">HZL</span>
|
|
1437
1695
|
</div>
|
|
1438
1696
|
<div class="header-filters">
|
|
1439
1697
|
<div class="filter-group">
|
|
@@ -1455,6 +1713,17 @@
|
|
|
1455
1713
|
<option value="">Any Agent</option>
|
|
1456
1714
|
</select>
|
|
1457
1715
|
</div>
|
|
1716
|
+
<div class="filter-group task-search-group" id="taskSearchGroup">
|
|
1717
|
+
<input
|
|
1718
|
+
type="search"
|
|
1719
|
+
id="taskSearchInput"
|
|
1720
|
+
class="task-search-input"
|
|
1721
|
+
placeholder="Find task (/)"
|
|
1722
|
+
aria-label="Search tasks"
|
|
1723
|
+
>
|
|
1724
|
+
<button type="button" class="task-search-clear" id="taskSearchClear" hidden>×</button>
|
|
1725
|
+
<span class="task-search-meta" id="taskSearchMeta"></span>
|
|
1726
|
+
</div>
|
|
1458
1727
|
<div class="filter-group settings-group">
|
|
1459
1728
|
<button class="settings-toggle" id="settingsToggle" title="Settings">
|
|
1460
1729
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
@@ -1463,6 +1732,14 @@
|
|
|
1463
1732
|
</svg>
|
|
1464
1733
|
</button>
|
|
1465
1734
|
<div class="settings-dropdown" id="settingsDropdown">
|
|
1735
|
+
<div class="settings-section">
|
|
1736
|
+
<label class="settings-label" for="viewFilter">View</label>
|
|
1737
|
+
<select id="viewFilter" class="settings-view-select" aria-label="Select view">
|
|
1738
|
+
<option value="kanban">Kanban</option>
|
|
1739
|
+
<option value="calendar">Calendar</option>
|
|
1740
|
+
<option value="graph">Graph</option>
|
|
1741
|
+
</select>
|
|
1742
|
+
</div>
|
|
1466
1743
|
<div class="settings-section">
|
|
1467
1744
|
<label class="settings-label">Refresh</label>
|
|
1468
1745
|
<select id="refreshFilter">
|
|
@@ -1498,6 +1775,25 @@
|
|
|
1498
1775
|
<input type="checkbox" id="showSubtasks" checked> Show subtasks
|
|
1499
1776
|
</label>
|
|
1500
1777
|
</div>
|
|
1778
|
+
<div class="settings-section">
|
|
1779
|
+
<label class="settings-label">Parent View</label>
|
|
1780
|
+
<div class="collapse-parents-actions">
|
|
1781
|
+
<button type="button" class="collapse-parents-btn" id="collapseAllParentsBtn">Collapse all</button>
|
|
1782
|
+
<button type="button" class="collapse-parents-btn" id="expandAllParentsBtn">Expand all</button>
|
|
1783
|
+
</div>
|
|
1784
|
+
<div class="collapse-parents-meta" id="collapseParentsMeta"></div>
|
|
1785
|
+
</div>
|
|
1786
|
+
<div class="settings-section">
|
|
1787
|
+
<button
|
|
1788
|
+
type="button"
|
|
1789
|
+
class="settings-shortcuts-btn"
|
|
1790
|
+
id="shortcutsBtn"
|
|
1791
|
+
title="Keyboard shortcuts (?)"
|
|
1792
|
+
aria-label="Keyboard shortcuts"
|
|
1793
|
+
>
|
|
1794
|
+
Shortcuts (?)
|
|
1795
|
+
</button>
|
|
1796
|
+
</div>
|
|
1501
1797
|
</div>
|
|
1502
1798
|
</div>
|
|
1503
1799
|
</div>
|
|
@@ -1613,6 +1909,24 @@
|
|
|
1613
1909
|
</div>
|
|
1614
1910
|
</div>
|
|
1615
1911
|
|
|
1912
|
+
<div class="shortcuts-modal-overlay" id="shortcutsModalOverlay">
|
|
1913
|
+
<div class="shortcuts-modal">
|
|
1914
|
+
<div class="shortcuts-header">
|
|
1915
|
+
<span class="shortcuts-title">Keyboard Shortcuts</span>
|
|
1916
|
+
<button type="button" class="shortcuts-close" id="shortcutsClose">×</button>
|
|
1917
|
+
</div>
|
|
1918
|
+
<div class="shortcuts-body">
|
|
1919
|
+
<div class="shortcuts-list">
|
|
1920
|
+
<span class="shortcut-key">/</span><span class="shortcut-desc">Focus task search</span>
|
|
1921
|
+
<span class="shortcut-key">a</span><span class="shortcut-desc">Toggle activity panel</span>
|
|
1922
|
+
<span class="shortcut-key">?</span><span class="shortcut-desc">Open this shortcuts dialog</span>
|
|
1923
|
+
<span class="shortcut-key">Esc</span><span class="shortcut-desc">Close open dialogs/panels</span>
|
|
1924
|
+
</div>
|
|
1925
|
+
<div class="shortcuts-note">Shortcuts are disabled while typing in inputs.</div>
|
|
1926
|
+
</div>
|
|
1927
|
+
</div>
|
|
1928
|
+
</div>
|
|
1929
|
+
|
|
1616
1930
|
<script>
|
|
1617
1931
|
// State
|
|
1618
1932
|
let tasks = [];
|
|
@@ -1641,6 +1955,8 @@
|
|
|
1641
1955
|
const TASK_ACTIVITY_DISPLAY_LIMIT = 20;
|
|
1642
1956
|
let activeTab = 'ready';
|
|
1643
1957
|
let activeView = 'kanban';
|
|
1958
|
+
let taskSearchQuery = '';
|
|
1959
|
+
let collapsedParents = new Set();
|
|
1644
1960
|
let graphInstance = null;
|
|
1645
1961
|
let graphInitialized = false;
|
|
1646
1962
|
let nodeStatusMap = new Map();
|
|
@@ -1649,14 +1965,23 @@
|
|
|
1649
1965
|
let copyFeedbackTimer = null;
|
|
1650
1966
|
let calendarYear = new Date().getFullYear();
|
|
1651
1967
|
let calendarMonth = new Date().getMonth(); // 0-indexed
|
|
1968
|
+
let initialTaskIdFromUrl = null;
|
|
1969
|
+
let initialActivityPanelOpen = false;
|
|
1652
1970
|
|
|
1653
1971
|
// DOM Elements
|
|
1654
1972
|
const dateFilter = document.getElementById('dateFilter');
|
|
1973
|
+
const taskSearchInput = document.getElementById('taskSearchInput');
|
|
1974
|
+
const taskSearchClear = document.getElementById('taskSearchClear');
|
|
1975
|
+
const taskSearchMeta = document.getElementById('taskSearchMeta');
|
|
1976
|
+
const taskSearchGroup = document.getElementById('taskSearchGroup');
|
|
1655
1977
|
const projectFilter = document.getElementById('projectFilter');
|
|
1656
1978
|
const assigneeFilter = document.getElementById('assigneeFilter');
|
|
1657
1979
|
const refreshFilter = document.getElementById('refreshFilter');
|
|
1658
1980
|
const connectionDot = document.getElementById('connectionDot');
|
|
1659
1981
|
const connectionText = document.getElementById('connectionText');
|
|
1982
|
+
const shortcutsBtn = document.getElementById('shortcutsBtn');
|
|
1983
|
+
const shortcutsModalOverlay = document.getElementById('shortcutsModalOverlay');
|
|
1984
|
+
const shortcutsClose = document.getElementById('shortcutsClose');
|
|
1660
1985
|
const activityBtn = document.getElementById('activityBtn');
|
|
1661
1986
|
const activityPanel = document.getElementById('activityPanel');
|
|
1662
1987
|
const activityClose = document.getElementById('activityClose');
|
|
@@ -1673,12 +1998,16 @@
|
|
|
1673
1998
|
const mobileTabs = document.getElementById('mobileTabs');
|
|
1674
1999
|
const settingsToggle = document.getElementById('settingsToggle');
|
|
1675
2000
|
const settingsDropdown = document.getElementById('settingsDropdown');
|
|
2001
|
+
const viewFilter = document.getElementById('viewFilter');
|
|
1676
2002
|
const showSubtasksCheckbox = document.getElementById('showSubtasks');
|
|
1677
|
-
const
|
|
2003
|
+
const collapseAllParentsBtn = document.getElementById('collapseAllParentsBtn');
|
|
2004
|
+
const expandAllParentsBtn = document.getElementById('expandAllParentsBtn');
|
|
2005
|
+
const collapseParentsMeta = document.getElementById('collapseParentsMeta');
|
|
1678
2006
|
const board = document.getElementById('board');
|
|
1679
2007
|
const calendarContainer = document.getElementById('calendarContainer');
|
|
1680
2008
|
const graphContainer = document.getElementById('graphContainer');
|
|
1681
2009
|
const graphLoading = document.getElementById('graphLoading');
|
|
2010
|
+
let pendingProjectPreference = null;
|
|
1682
2011
|
let pendingAssigneePreference = null;
|
|
1683
2012
|
let pendingActivityAssigneePreference = null;
|
|
1684
2013
|
const columnScrollTimers = new WeakMap();
|
|
@@ -1768,17 +2097,266 @@
|
|
|
1768
2097
|
});
|
|
1769
2098
|
}
|
|
1770
2099
|
|
|
2100
|
+
function normalizeTaskSearchQuery(value) {
|
|
2101
|
+
if (typeof value !== 'string') return '';
|
|
2102
|
+
return value.trim().replace(/\s+/g, ' ').slice(0, 120);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
function getTaskSearchQuery() {
|
|
2106
|
+
return taskSearchQuery.toLowerCase();
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
function taskMatchesSearch(task, query) {
|
|
2110
|
+
if (!query) return true;
|
|
2111
|
+
const terms = query.split(' ');
|
|
2112
|
+
const haystack = [
|
|
2113
|
+
task.task_id,
|
|
2114
|
+
task.title,
|
|
2115
|
+
task.project,
|
|
2116
|
+
getAssigneeValue(task.assignee),
|
|
2117
|
+
task.description,
|
|
2118
|
+
Array.isArray(task.tags) ? task.tags.join(' ') : '',
|
|
2119
|
+
Array.isArray(task.blocked_by) ? task.blocked_by.join(' ') : '',
|
|
2120
|
+
]
|
|
2121
|
+
.filter(Boolean)
|
|
2122
|
+
.join(' ')
|
|
2123
|
+
.toLowerCase();
|
|
2124
|
+
return terms.every((term) => haystack.includes(term));
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
function getParentTaskIds(taskList = tasks) {
|
|
2128
|
+
return taskList
|
|
2129
|
+
.filter((task) => (task.subtask_total ?? 0) > 0)
|
|
2130
|
+
.map((task) => task.task_id);
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
function pruneCollapsedParents(taskList = tasks) {
|
|
2134
|
+
const validParents = new Set(getParentTaskIds(taskList));
|
|
2135
|
+
let changed = false;
|
|
2136
|
+
for (const parentId of Array.from(collapsedParents)) {
|
|
2137
|
+
if (!validParents.has(parentId)) {
|
|
2138
|
+
collapsedParents.delete(parentId);
|
|
2139
|
+
changed = true;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
return changed;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
function updateCollapseControls() {
|
|
2146
|
+
const parentIds = getParentTaskIds(tasks);
|
|
2147
|
+
const collapsedCount = parentIds.filter((parentId) => collapsedParents.has(parentId)).length;
|
|
2148
|
+
const showSubtasks = showSubtasksCheckbox.checked;
|
|
2149
|
+
|
|
2150
|
+
collapseAllParentsBtn.disabled = !showSubtasks || parentIds.length === 0 || collapsedCount === parentIds.length;
|
|
2151
|
+
expandAllParentsBtn.disabled = !showSubtasks || collapsedCount === 0;
|
|
2152
|
+
|
|
2153
|
+
if (parentIds.length === 0) {
|
|
2154
|
+
collapseParentsMeta.textContent = 'No parent tasks';
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
if (!showSubtasks) {
|
|
2159
|
+
collapseParentsMeta.textContent = 'Enable "Show subtasks" to expand by parent';
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
collapseParentsMeta.textContent = `${collapsedCount}/${parentIds.length} collapsed`;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
function toggleParentCollapsed(parentId) {
|
|
2167
|
+
if (!parentId) return;
|
|
2168
|
+
if (collapsedParents.has(parentId)) {
|
|
2169
|
+
collapsedParents.delete(parentId);
|
|
2170
|
+
} else {
|
|
2171
|
+
collapsedParents.add(parentId);
|
|
2172
|
+
}
|
|
2173
|
+
savePreferences();
|
|
2174
|
+
renderBoard();
|
|
2175
|
+
renderActivity();
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
function collapseAllParents() {
|
|
2179
|
+
if (!showSubtasksCheckbox.checked) return;
|
|
2180
|
+
for (const parentId of getParentTaskIds(tasks)) {
|
|
2181
|
+
collapsedParents.add(parentId);
|
|
2182
|
+
}
|
|
2183
|
+
savePreferences();
|
|
2184
|
+
renderBoard();
|
|
2185
|
+
renderActivity();
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
function expandAllParents() {
|
|
2189
|
+
for (const parentId of getParentTaskIds(tasks)) {
|
|
2190
|
+
collapsedParents.delete(parentId);
|
|
2191
|
+
}
|
|
2192
|
+
savePreferences();
|
|
2193
|
+
renderBoard();
|
|
2194
|
+
renderActivity();
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
function parseYearMonth(value) {
|
|
2198
|
+
const match = /^(\d{4})-(0[1-9]|1[0-2])$/.exec(value);
|
|
2199
|
+
if (!match) return null;
|
|
2200
|
+
return {
|
|
2201
|
+
year: Number(match[1]),
|
|
2202
|
+
month: Number(match[2]) - 1,
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
function getActiveTaskId() {
|
|
2207
|
+
const taskId = selectedTask?.task?.task_id;
|
|
2208
|
+
return typeof taskId === 'string' && taskId ? taskId : null;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
function setShortcutsModalOpen(open) {
|
|
2212
|
+
shortcutsModalOverlay.classList.toggle('open', open);
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
function setActivityPanelOpen(open, options = {}) {
|
|
2216
|
+
const { persist = true } = options;
|
|
2217
|
+
activityPanel.classList.toggle('open', open);
|
|
2218
|
+
if (persist) {
|
|
2219
|
+
syncUrlState();
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
function setActiveTab(status, options = {}) {
|
|
2224
|
+
const { persist = true } = options;
|
|
2225
|
+
if (!COLUMNS.includes(status)) return;
|
|
2226
|
+
activeTab = status;
|
|
2227
|
+
document.querySelectorAll('.mobile-tab').forEach((tab) => {
|
|
2228
|
+
tab.classList.toggle('active', tab.dataset.status === activeTab);
|
|
2229
|
+
});
|
|
2230
|
+
document.querySelectorAll('.mobile-cards').forEach((cards) => {
|
|
2231
|
+
cards.classList.toggle('active', cards.dataset.status === activeTab);
|
|
2232
|
+
});
|
|
2233
|
+
if (persist) {
|
|
2234
|
+
savePreferences();
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
function buildUrlStateParams() {
|
|
2239
|
+
const params = new URLSearchParams();
|
|
2240
|
+
|
|
2241
|
+
if (activeView !== 'kanban') {
|
|
2242
|
+
params.set('view', activeView);
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
if (activeView === 'calendar') {
|
|
2246
|
+
const month = String(calendarMonth + 1).padStart(2, '0');
|
|
2247
|
+
params.set('month', `${calendarYear}-${month}`);
|
|
2248
|
+
} else if (dateFilter.value !== '3d') {
|
|
2249
|
+
params.set('since', dateFilter.value);
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
if (projectFilter.value) params.set('project', projectFilter.value);
|
|
2253
|
+
if (assigneeFilter.value) params.set('assignee', assigneeFilter.value);
|
|
2254
|
+
if (taskSearchQuery) params.set('q', taskSearchQuery);
|
|
2255
|
+
if (!showSubtasksCheckbox.checked) params.set('subtasks', '0');
|
|
2256
|
+
if (activeTab !== 'ready') params.set('tab', activeTab);
|
|
2257
|
+
if (activityAssigneeFilter.value) params.set('activity_assignee', activityAssigneeFilter.value);
|
|
2258
|
+
if (activityKeywordFilter.value.trim()) params.set('activity_q', activityKeywordFilter.value.trim());
|
|
2259
|
+
if (activityPanel.classList.contains('open')) params.set('activity', '1');
|
|
2260
|
+
|
|
2261
|
+
const taskId = getActiveTaskId();
|
|
2262
|
+
if (taskId) params.set('task', taskId);
|
|
2263
|
+
|
|
2264
|
+
return params;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
function syncUrlState() {
|
|
2268
|
+
const params = buildUrlStateParams();
|
|
2269
|
+
const query = params.toString();
|
|
2270
|
+
const nextUrl = `${window.location.pathname}${query ? `?${query}` : ''}${window.location.hash || ''}`;
|
|
2271
|
+
history.replaceState(null, '', nextUrl);
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
function applyUrlStateOverrides() {
|
|
2275
|
+
const params = new URLSearchParams(window.location.search);
|
|
2276
|
+
let preferredView = null;
|
|
2277
|
+
|
|
2278
|
+
const since = params.get('since');
|
|
2279
|
+
const validDateValues = new Set(Array.from(dateFilter.options).map((option) => option.value));
|
|
2280
|
+
if (since && validDateValues.has(since)) {
|
|
2281
|
+
dateFilter.value = since;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
const view = params.get('view');
|
|
2285
|
+
if (view && (view === 'kanban' || view === 'calendar' || view === 'graph')) {
|
|
2286
|
+
preferredView = view;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
const monthParam = params.get('month');
|
|
2290
|
+
if (monthParam) {
|
|
2291
|
+
const parsedMonth = parseYearMonth(monthParam);
|
|
2292
|
+
if (parsedMonth) {
|
|
2293
|
+
calendarYear = parsedMonth.year;
|
|
2294
|
+
calendarMonth = parsedMonth.month;
|
|
2295
|
+
if (!preferredView) preferredView = 'calendar';
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
const project = params.get('project');
|
|
2300
|
+
if (project !== null) {
|
|
2301
|
+
pendingProjectPreference = project;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
const assignee = params.get('assignee');
|
|
2305
|
+
if (assignee !== null) {
|
|
2306
|
+
pendingAssigneePreference = assignee;
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
const activityAssignee = params.get('activity_assignee');
|
|
2310
|
+
if (activityAssignee !== null) {
|
|
2311
|
+
pendingActivityAssigneePreference = activityAssignee;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
const activityKeyword = params.get('activity_q');
|
|
2315
|
+
if (activityKeyword !== null) {
|
|
2316
|
+
activityKeywordFilter.value = activityKeyword;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
const searchQuery = params.get('q');
|
|
2320
|
+
if (searchQuery !== null) {
|
|
2321
|
+
taskSearchQuery = normalizeTaskSearchQuery(searchQuery);
|
|
2322
|
+
taskSearchInput.value = taskSearchQuery;
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
const subtasks = params.get('subtasks');
|
|
2326
|
+
if (subtasks === '0') showSubtasksCheckbox.checked = false;
|
|
2327
|
+
if (subtasks === '1') showSubtasksCheckbox.checked = true;
|
|
2328
|
+
|
|
2329
|
+
const tab = params.get('tab');
|
|
2330
|
+
if (tab && COLUMNS.includes(tab)) {
|
|
2331
|
+
activeTab = tab;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
const taskId = params.get('task');
|
|
2335
|
+
if (taskId && taskId.trim()) {
|
|
2336
|
+
initialTaskIdFromUrl = taskId.trim();
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
initialActivityPanelOpen = params.get('activity') === '1';
|
|
2340
|
+
|
|
2341
|
+
return { preferredView };
|
|
2342
|
+
}
|
|
2343
|
+
|
|
1771
2344
|
// Load saved preferences
|
|
1772
2345
|
function loadPreferences() {
|
|
2346
|
+
let preferredView = null;
|
|
1773
2347
|
const saved = localStorage.getItem('hzl-dashboard-prefs');
|
|
1774
2348
|
if (saved) {
|
|
1775
2349
|
try {
|
|
1776
2350
|
const prefs = JSON.parse(saved);
|
|
1777
2351
|
if (prefs.dateFilter) dateFilter.value = prefs.dateFilter;
|
|
1778
|
-
if (prefs.projectFilter)
|
|
2352
|
+
if (typeof prefs.projectFilter === 'string') pendingProjectPreference = prefs.projectFilter;
|
|
1779
2353
|
if (typeof prefs.assigneeFilter === 'string') pendingAssigneePreference = prefs.assigneeFilter;
|
|
1780
2354
|
if (typeof prefs.activityAssigneeFilter === 'string') pendingActivityAssigneePreference = prefs.activityAssigneeFilter;
|
|
1781
2355
|
if (typeof prefs.activityKeywordFilter === 'string') activityKeywordFilter.value = prefs.activityKeywordFilter;
|
|
2356
|
+
if (typeof prefs.taskSearch === 'string') {
|
|
2357
|
+
taskSearchQuery = normalizeTaskSearchQuery(prefs.taskSearch);
|
|
2358
|
+
taskSearchInput.value = taskSearchQuery;
|
|
2359
|
+
}
|
|
1782
2360
|
if (prefs.refreshFilter) refreshFilter.value = prefs.refreshFilter;
|
|
1783
2361
|
if (Array.isArray(prefs.columnVisibility)) {
|
|
1784
2362
|
settingsDropdown.querySelectorAll('.column-checkboxes input[type="checkbox"]').forEach(cb => {
|
|
@@ -1789,11 +2367,36 @@
|
|
|
1789
2367
|
if (prefs.showSubtasks !== undefined) {
|
|
1790
2368
|
showSubtasksCheckbox.checked = prefs.showSubtasks;
|
|
1791
2369
|
}
|
|
2370
|
+
if (Array.isArray(prefs.collapsedParents)) {
|
|
2371
|
+
collapsedParents = new Set(
|
|
2372
|
+
prefs.collapsedParents.filter((value) => typeof value === 'string' && value.length > 0)
|
|
2373
|
+
);
|
|
2374
|
+
}
|
|
2375
|
+
if (typeof prefs.activeTab === 'string' && COLUMNS.includes(prefs.activeTab)) {
|
|
2376
|
+
activeTab = prefs.activeTab;
|
|
2377
|
+
}
|
|
1792
2378
|
if (prefs.activeView && prefs.activeView !== 'kanban') {
|
|
1793
|
-
|
|
1794
|
-
setTimeout(() => setActiveView(prefs.activeView), 100);
|
|
2379
|
+
preferredView = prefs.activeView;
|
|
1795
2380
|
}
|
|
1796
|
-
} catch
|
|
2381
|
+
} catch {}
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
const urlOverrides = applyUrlStateOverrides();
|
|
2385
|
+
if (urlOverrides.preferredView) {
|
|
2386
|
+
preferredView = urlOverrides.preferredView;
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
if (initialActivityPanelOpen) {
|
|
2390
|
+
setActivityPanelOpen(true, { persist: false });
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
setActiveTab(activeTab, { persist: false });
|
|
2394
|
+
updateTaskSearchUi();
|
|
2395
|
+
updateCollapseControls();
|
|
2396
|
+
|
|
2397
|
+
if (preferredView && preferredView !== 'kanban') {
|
|
2398
|
+
// Defer view switch until after ForceGraph may have loaded
|
|
2399
|
+
setTimeout(() => setActiveView(preferredView), 100);
|
|
1797
2400
|
}
|
|
1798
2401
|
}
|
|
1799
2402
|
|
|
@@ -1804,23 +2407,30 @@
|
|
|
1804
2407
|
assigneeFilter: assigneeFilter.value,
|
|
1805
2408
|
activityAssigneeFilter: activityAssigneeFilter.value,
|
|
1806
2409
|
activityKeywordFilter: activityKeywordFilter.value,
|
|
2410
|
+
taskSearch: taskSearchQuery,
|
|
1807
2411
|
refreshFilter: refreshFilter.value,
|
|
1808
2412
|
columnVisibility: Array.from(
|
|
1809
2413
|
settingsDropdown.querySelectorAll('.column-checkboxes input[type="checkbox"]:checked')
|
|
1810
2414
|
).map(cb => cb.value),
|
|
1811
2415
|
showSubtasks: showSubtasksCheckbox.checked,
|
|
2416
|
+
collapsedParents: Array.from(collapsedParents),
|
|
1812
2417
|
activeView: activeView,
|
|
2418
|
+
activeTab: activeTab,
|
|
1813
2419
|
};
|
|
1814
2420
|
localStorage.setItem('hzl-dashboard-prefs', JSON.stringify(prefs));
|
|
2421
|
+
syncUrlState();
|
|
1815
2422
|
}
|
|
1816
2423
|
|
|
1817
2424
|
// Graph View Functions
|
|
1818
2425
|
function handleGraphLibError() {
|
|
1819
2426
|
console.warn('[hzl] force-graph CDN failed to load');
|
|
1820
|
-
const
|
|
1821
|
-
if (
|
|
1822
|
-
|
|
1823
|
-
|
|
2427
|
+
const graphOption = viewFilter.querySelector('option[value="graph"]');
|
|
2428
|
+
if (graphOption) {
|
|
2429
|
+
graphOption.disabled = true;
|
|
2430
|
+
graphOption.textContent = 'Graph (unavailable)';
|
|
2431
|
+
}
|
|
2432
|
+
if (activeView === 'graph') {
|
|
2433
|
+
setActiveView('kanban');
|
|
1824
2434
|
}
|
|
1825
2435
|
}
|
|
1826
2436
|
|
|
@@ -2161,7 +2771,22 @@
|
|
|
2161
2771
|
const countLabel = visibleCount === totalCount
|
|
2162
2772
|
? `${visibleCount} ${label}`
|
|
2163
2773
|
: `${visibleCount}/${totalCount} ${label}`;
|
|
2164
|
-
|
|
2774
|
+
if (showSubtasks) {
|
|
2775
|
+
const isCollapsed = collapsedParents.has(task.task_id);
|
|
2776
|
+
const symbol = isCollapsed ? '▶' : '▼';
|
|
2777
|
+
subtaskHtml = `
|
|
2778
|
+
<button
|
|
2779
|
+
type="button"
|
|
2780
|
+
class="card-subtask-toggle"
|
|
2781
|
+
data-action="toggle-subtasks"
|
|
2782
|
+
data-parent-id="${escapeHtml(task.task_id)}"
|
|
2783
|
+
aria-expanded="${isCollapsed ? 'false' : 'true'}"
|
|
2784
|
+
title="${isCollapsed ? 'Expand subtasks' : 'Collapse subtasks'}"
|
|
2785
|
+
>${symbol} [${countLabel}]</button>
|
|
2786
|
+
`;
|
|
2787
|
+
} else {
|
|
2788
|
+
subtaskHtml = `<div class="card-subtask-count">[${countLabel}]</div>`;
|
|
2789
|
+
}
|
|
2165
2790
|
}
|
|
2166
2791
|
|
|
2167
2792
|
// Build progress badge
|
|
@@ -2224,8 +2849,47 @@
|
|
|
2224
2849
|
);
|
|
2225
2850
|
}
|
|
2226
2851
|
|
|
2852
|
+
function updateTaskSearchUi() {
|
|
2853
|
+
const hasQuery = taskSearchQuery.length > 0;
|
|
2854
|
+
taskSearchGroup.classList.toggle('active', hasQuery);
|
|
2855
|
+
taskSearchClear.hidden = !hasQuery;
|
|
2856
|
+
|
|
2857
|
+
if (!hasQuery) {
|
|
2858
|
+
taskSearchMeta.textContent = '';
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
const totalCandidates = getFilteredBoardTasks(tasks, { applySearchFilter: false }).length;
|
|
2863
|
+
const matchedCount = getFilteredBoardTasks(tasks).length;
|
|
2864
|
+
const label = totalCandidates === 1 ? 'task' : 'tasks';
|
|
2865
|
+
taskSearchMeta.textContent = `${matchedCount}/${totalCandidates} ${label}`;
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
function applyTaskSearch(value, options = {}) {
|
|
2869
|
+
const { persist = true } = options;
|
|
2870
|
+
const normalized = normalizeTaskSearchQuery(value);
|
|
2871
|
+
taskSearchInput.value = normalized;
|
|
2872
|
+
if (normalized === taskSearchQuery) return;
|
|
2873
|
+
|
|
2874
|
+
taskSearchQuery = normalized;
|
|
2875
|
+
updateTaskSearchUi();
|
|
2876
|
+
updateAssigneeOptions();
|
|
2877
|
+
updateActivityAssigneeOptions();
|
|
2878
|
+
renderBoard();
|
|
2879
|
+
renderActivity();
|
|
2880
|
+
|
|
2881
|
+
if (persist) {
|
|
2882
|
+
savePreferences();
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2227
2886
|
function getFilteredBoardTasks(taskList = tasks, options = {}) {
|
|
2228
|
-
const {
|
|
2887
|
+
const {
|
|
2888
|
+
onlyVisibleColumns = false,
|
|
2889
|
+
applyAssigneeFilter = true,
|
|
2890
|
+
applySearchFilter = true,
|
|
2891
|
+
applyCollapsedParents = true,
|
|
2892
|
+
} = options;
|
|
2229
2893
|
const showSubtasks = showSubtasksCheckbox.checked;
|
|
2230
2894
|
|
|
2231
2895
|
let filtered = showSubtasks ? taskList : taskList.filter(task => !task.parent_id);
|
|
@@ -2239,6 +2903,26 @@
|
|
|
2239
2903
|
filtered = filtered.filter(task => getAssigneeValue(task.assignee) === assigneeFilter.value);
|
|
2240
2904
|
}
|
|
2241
2905
|
|
|
2906
|
+
if (applySearchFilter) {
|
|
2907
|
+
const query = getTaskSearchQuery();
|
|
2908
|
+
if (query) {
|
|
2909
|
+
filtered = filtered.filter((task) => taskMatchesSearch(task, query));
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
if (showSubtasks && applyCollapsedParents) {
|
|
2914
|
+
// Search mode should always show matching subtasks, regardless of collapsed parents.
|
|
2915
|
+
const query = getTaskSearchQuery();
|
|
2916
|
+
if (!query) {
|
|
2917
|
+
const visibleTaskIds = new Set(filtered.map((task) => task.task_id));
|
|
2918
|
+
filtered = filtered.filter((task) => {
|
|
2919
|
+
if (!task.parent_id) return true;
|
|
2920
|
+
if (!visibleTaskIds.has(task.parent_id)) return true;
|
|
2921
|
+
return !collapsedParents.has(task.parent_id);
|
|
2922
|
+
});
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2242
2926
|
return filtered;
|
|
2243
2927
|
}
|
|
2244
2928
|
|
|
@@ -2483,17 +3167,20 @@
|
|
|
2483
3167
|
function navPrev() {
|
|
2484
3168
|
calendarMonth--;
|
|
2485
3169
|
if (calendarMonth < 0) { calendarMonth = 11; calendarYear--; }
|
|
3170
|
+
syncUrlState();
|
|
2486
3171
|
requestPoll();
|
|
2487
3172
|
}
|
|
2488
3173
|
function navNext() {
|
|
2489
3174
|
calendarMonth++;
|
|
2490
3175
|
if (calendarMonth > 11) { calendarMonth = 0; calendarYear++; }
|
|
3176
|
+
syncUrlState();
|
|
2491
3177
|
requestPoll();
|
|
2492
3178
|
}
|
|
2493
3179
|
function navToday() {
|
|
2494
3180
|
const today = new Date();
|
|
2495
3181
|
calendarYear = today.getFullYear();
|
|
2496
3182
|
calendarMonth = today.getMonth();
|
|
3183
|
+
syncUrlState();
|
|
2497
3184
|
requestPoll();
|
|
2498
3185
|
}
|
|
2499
3186
|
|
|
@@ -2591,6 +3278,7 @@
|
|
|
2591
3278
|
const showSubtasks = showSubtasksCheckbox.checked;
|
|
2592
3279
|
const emojiMap = buildEmojiMap(tasks);
|
|
2593
3280
|
const visibleTasks = getFilteredBoardTasks(tasks);
|
|
3281
|
+
const emptyMessage = taskSearchQuery ? 'No matching tasks' : 'No tasks';
|
|
2594
3282
|
|
|
2595
3283
|
const columns = groupTasksByStatus(visibleTasks);
|
|
2596
3284
|
|
|
@@ -2601,7 +3289,7 @@
|
|
|
2601
3289
|
|
|
2602
3290
|
if (container) {
|
|
2603
3291
|
if (statusTasks.length === 0) {
|
|
2604
|
-
container.innerHTML =
|
|
3292
|
+
container.innerHTML = `<div class="empty-column">${emptyMessage}</div>`;
|
|
2605
3293
|
} else {
|
|
2606
3294
|
container.innerHTML = statusTasks.map(task => {
|
|
2607
3295
|
const emojiInfo = emojiMap.get(task.task_id);
|
|
@@ -2615,6 +3303,8 @@
|
|
|
2615
3303
|
|
|
2616
3304
|
// Render mobile cards for active tab
|
|
2617
3305
|
renderMobileCards(showSubtasks, emojiMap);
|
|
3306
|
+
updateTaskSearchUi();
|
|
3307
|
+
updateCollapseControls();
|
|
2618
3308
|
|
|
2619
3309
|
// Card click handlers are set up via event delegation in init
|
|
2620
3310
|
}
|
|
@@ -2641,13 +3331,14 @@
|
|
|
2641
3331
|
if (!container) return;
|
|
2642
3332
|
|
|
2643
3333
|
const visibleTasks = getFilteredBoardTasks(tasks);
|
|
3334
|
+
const emptyMessage = taskSearchQuery ? 'No matching tasks' : 'No tasks';
|
|
2644
3335
|
|
|
2645
3336
|
const columns = groupTasksByStatus(visibleTasks);
|
|
2646
3337
|
|
|
2647
3338
|
container.innerHTML = Object.entries(columns).map(([status, statusTasks]) => `
|
|
2648
3339
|
<div class="mobile-cards ${status === activeTab ? 'active' : ''}" data-status="${status}">
|
|
2649
3340
|
${statusTasks.length === 0
|
|
2650
|
-
?
|
|
3341
|
+
? `<div class="empty-column">${emptyMessage}</div>`
|
|
2651
3342
|
: statusTasks.map(task => {
|
|
2652
3343
|
const emojiInfo = emojiMap.get(task.task_id);
|
|
2653
3344
|
return renderCard(task, emojiInfo, showSubtasks);
|
|
@@ -2901,6 +3592,7 @@
|
|
|
2901
3592
|
|
|
2902
3593
|
modalBody.innerHTML = html;
|
|
2903
3594
|
modalOverlay.classList.add('open');
|
|
3595
|
+
syncUrlState();
|
|
2904
3596
|
|
|
2905
3597
|
// Attach tab switching handlers (DOM-only, no re-fetch)
|
|
2906
3598
|
modalBody.querySelectorAll('.modal-tab').forEach(tab => {
|
|
@@ -2953,12 +3645,16 @@
|
|
|
2953
3645
|
}
|
|
2954
3646
|
|
|
2955
3647
|
function closeModal() {
|
|
3648
|
+
const wasOpen = modalOverlay.classList.contains('open');
|
|
2956
3649
|
modalOverlay.classList.remove('open');
|
|
2957
3650
|
selectedTask = null;
|
|
2958
3651
|
modalTaskIdValue.textContent = '-';
|
|
2959
3652
|
modalTaskIdCopy.dataset.taskId = '';
|
|
2960
3653
|
modalTaskIdCopy.disabled = true;
|
|
2961
3654
|
setTaskIdCopyFeedback('idle');
|
|
3655
|
+
if (wasOpen) {
|
|
3656
|
+
syncUrlState();
|
|
3657
|
+
}
|
|
2962
3658
|
}
|
|
2963
3659
|
|
|
2964
3660
|
// Live updates + refresh
|
|
@@ -2996,11 +3692,12 @@
|
|
|
2996
3692
|
]);
|
|
2997
3693
|
|
|
2998
3694
|
tasks = newTasks;
|
|
3695
|
+
const collapsedParentsPruned = pruneCollapsedParents(tasks);
|
|
2999
3696
|
const selectionUpdate = updateAssigneeOptions(pendingAssigneePreference);
|
|
3000
3697
|
pendingAssigneePreference = null;
|
|
3001
3698
|
const activitySelectionUpdate = updateActivityAssigneeOptions(pendingActivityAssigneePreference);
|
|
3002
3699
|
pendingActivityAssigneePreference = null;
|
|
3003
|
-
if (selectionUpdate.reset || activitySelectionUpdate.reset) {
|
|
3700
|
+
if (selectionUpdate.reset || activitySelectionUpdate.reset || collapsedParentsPruned) {
|
|
3004
3701
|
savePreferences();
|
|
3005
3702
|
}
|
|
3006
3703
|
|
|
@@ -3010,9 +3707,12 @@
|
|
|
3010
3707
|
}
|
|
3011
3708
|
|
|
3012
3709
|
// Update project filter
|
|
3013
|
-
const currentProject = projectFilter.value;
|
|
3710
|
+
const currentProject = pendingProjectPreference ?? projectFilter.value;
|
|
3014
3711
|
projectFilter.innerHTML = '<option value="">All projects</option>' +
|
|
3015
|
-
stats.projects.map(p => `<option value="${escapeHtml(p)}"
|
|
3712
|
+
stats.projects.map(p => `<option value="${escapeHtml(p)}">${escapeHtml(p)}</option>`).join('');
|
|
3713
|
+
const hasProject = currentProject && stats.projects.includes(currentProject);
|
|
3714
|
+
projectFilter.value = hasProject ? currentProject : '';
|
|
3715
|
+
pendingProjectPreference = null;
|
|
3016
3716
|
|
|
3017
3717
|
if (activeView === 'calendar') {
|
|
3018
3718
|
renderCalendar();
|
|
@@ -3021,6 +3721,14 @@
|
|
|
3021
3721
|
updateGraphData();
|
|
3022
3722
|
}
|
|
3023
3723
|
renderActivity();
|
|
3724
|
+
updateTaskSearchUi();
|
|
3725
|
+
syncUrlState();
|
|
3726
|
+
|
|
3727
|
+
if (initialTaskIdFromUrl) {
|
|
3728
|
+
const taskToOpen = initialTaskIdFromUrl;
|
|
3729
|
+
initialTaskIdFromUrl = null;
|
|
3730
|
+
void openTaskModal(taskToOpen);
|
|
3731
|
+
}
|
|
3024
3732
|
|
|
3025
3733
|
lastPollTime = Date.now();
|
|
3026
3734
|
lastPollError = false;
|
|
@@ -3457,12 +4165,27 @@
|
|
|
3457
4165
|
return '';
|
|
3458
4166
|
}
|
|
3459
4167
|
|
|
4168
|
+
function isTypingTarget(target) {
|
|
4169
|
+
if (!target || !(target instanceof Element)) return false;
|
|
4170
|
+
if (target.closest('input, textarea, select')) return true;
|
|
4171
|
+
return target.isContentEditable;
|
|
4172
|
+
}
|
|
4173
|
+
|
|
3460
4174
|
// Event listeners
|
|
3461
4175
|
dateFilter.addEventListener('change', () => {
|
|
3462
4176
|
savePreferences();
|
|
3463
4177
|
requestPoll();
|
|
3464
4178
|
});
|
|
3465
4179
|
|
|
4180
|
+
taskSearchInput.addEventListener('input', () => {
|
|
4181
|
+
applyTaskSearch(taskSearchInput.value);
|
|
4182
|
+
});
|
|
4183
|
+
|
|
4184
|
+
taskSearchClear.addEventListener('click', () => {
|
|
4185
|
+
applyTaskSearch('');
|
|
4186
|
+
taskSearchInput.focus();
|
|
4187
|
+
});
|
|
4188
|
+
|
|
3466
4189
|
projectFilter.addEventListener('change', () => {
|
|
3467
4190
|
savePreferences();
|
|
3468
4191
|
requestPoll();
|
|
@@ -3493,12 +4216,26 @@
|
|
|
3493
4216
|
requestPoll();
|
|
3494
4217
|
});
|
|
3495
4218
|
|
|
4219
|
+
shortcutsBtn.addEventListener('click', () => {
|
|
4220
|
+
setShortcutsModalOpen(true);
|
|
4221
|
+
});
|
|
4222
|
+
|
|
4223
|
+
shortcutsClose.addEventListener('click', () => {
|
|
4224
|
+
setShortcutsModalOpen(false);
|
|
4225
|
+
});
|
|
4226
|
+
|
|
4227
|
+
shortcutsModalOverlay.addEventListener('click', (e) => {
|
|
4228
|
+
if (e.target === shortcutsModalOverlay) {
|
|
4229
|
+
setShortcutsModalOpen(false);
|
|
4230
|
+
}
|
|
4231
|
+
});
|
|
4232
|
+
|
|
3496
4233
|
activityBtn.addEventListener('click', () => {
|
|
3497
|
-
|
|
4234
|
+
setActivityPanelOpen(true);
|
|
3498
4235
|
});
|
|
3499
4236
|
|
|
3500
4237
|
activityClose.addEventListener('click', () => {
|
|
3501
|
-
|
|
4238
|
+
setActivityPanelOpen(false);
|
|
3502
4239
|
});
|
|
3503
4240
|
|
|
3504
4241
|
activityList.addEventListener('click', (e) => {
|
|
@@ -3525,8 +4262,32 @@
|
|
|
3525
4262
|
document.addEventListener('keydown', (e) => {
|
|
3526
4263
|
if (e.key === 'Escape') {
|
|
3527
4264
|
closeModal();
|
|
3528
|
-
|
|
4265
|
+
setActivityPanelOpen(false);
|
|
4266
|
+
setShortcutsModalOpen(false);
|
|
3529
4267
|
settingsDropdown.classList.remove('open');
|
|
4268
|
+
return;
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
if (isTypingTarget(e.target)) {
|
|
4272
|
+
return;
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
if (e.key === '/') {
|
|
4276
|
+
e.preventDefault();
|
|
4277
|
+
taskSearchInput.focus();
|
|
4278
|
+
taskSearchInput.select();
|
|
4279
|
+
return;
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
if (e.key === '?') {
|
|
4283
|
+
e.preventDefault();
|
|
4284
|
+
setShortcutsModalOpen(true);
|
|
4285
|
+
return;
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4288
|
+
if (e.key.toLowerCase() === 'a') {
|
|
4289
|
+
e.preventDefault();
|
|
4290
|
+
setActivityPanelOpen(!activityPanel.classList.contains('open'));
|
|
3530
4291
|
}
|
|
3531
4292
|
});
|
|
3532
4293
|
|
|
@@ -3554,20 +4315,13 @@
|
|
|
3554
4315
|
mobileTabs.addEventListener('click', (e) => {
|
|
3555
4316
|
const tab = e.target.closest('.mobile-tab');
|
|
3556
4317
|
if (!tab) return;
|
|
3557
|
-
|
|
3558
|
-
activeTab = tab.dataset.status;
|
|
3559
|
-
|
|
3560
|
-
document.querySelectorAll('.mobile-tab').forEach(t => t.classList.remove('active'));
|
|
3561
|
-
tab.classList.add('active');
|
|
3562
|
-
|
|
3563
|
-
document.querySelectorAll('.mobile-cards').forEach(c => c.classList.remove('active'));
|
|
3564
|
-
document.querySelector(`.mobile-cards[data-status="${activeTab}"]`)?.classList.add('active');
|
|
4318
|
+
setActiveTab(tab.dataset.status);
|
|
3565
4319
|
});
|
|
3566
4320
|
|
|
3567
4321
|
// Hamburger menu (toggle filters on mobile)
|
|
3568
4322
|
hamburgerBtn.addEventListener('click', () => {
|
|
3569
4323
|
const filters = document.querySelector('.header-filters');
|
|
3570
|
-
filters.
|
|
4324
|
+
filters.classList.toggle('open');
|
|
3571
4325
|
});
|
|
3572
4326
|
|
|
3573
4327
|
// Settings dropdown
|
|
@@ -3603,19 +4357,33 @@
|
|
|
3603
4357
|
renderActivity();
|
|
3604
4358
|
});
|
|
3605
4359
|
|
|
3606
|
-
|
|
4360
|
+
collapseAllParentsBtn.addEventListener('click', () => {
|
|
4361
|
+
collapseAllParents();
|
|
4362
|
+
});
|
|
4363
|
+
|
|
4364
|
+
expandAllParentsBtn.addEventListener('click', () => {
|
|
4365
|
+
expandAllParents();
|
|
4366
|
+
});
|
|
4367
|
+
|
|
4368
|
+
// View selection
|
|
3607
4369
|
function setActiveView(view) {
|
|
4370
|
+
const allowedViews = new Set(['kanban', 'calendar', 'graph']);
|
|
4371
|
+
if (!allowedViews.has(view)) {
|
|
4372
|
+
view = 'kanban';
|
|
4373
|
+
}
|
|
4374
|
+
|
|
4375
|
+
const graphOption = viewFilter.querySelector('option[value="graph"]');
|
|
4376
|
+
if (view === 'graph' && graphOption && graphOption.disabled) {
|
|
4377
|
+
view = 'kanban';
|
|
4378
|
+
}
|
|
4379
|
+
|
|
3608
4380
|
// Dismiss calendar popover when leaving calendar view
|
|
3609
4381
|
if (activeView === 'calendar' && view !== 'calendar') {
|
|
3610
4382
|
dismissPopover();
|
|
3611
4383
|
}
|
|
3612
4384
|
|
|
3613
4385
|
activeView = view;
|
|
3614
|
-
|
|
3615
|
-
// Update toggle buttons
|
|
3616
|
-
viewToggle.querySelectorAll('.view-btn').forEach(btn => {
|
|
3617
|
-
btn.classList.toggle('active', btn.dataset.view === view);
|
|
3618
|
-
});
|
|
4386
|
+
viewFilter.value = view;
|
|
3619
4387
|
|
|
3620
4388
|
// Show/hide containers
|
|
3621
4389
|
board.style.display = view === 'kanban' ? '' : 'none';
|
|
@@ -3642,27 +4410,46 @@
|
|
|
3642
4410
|
requestPoll();
|
|
3643
4411
|
}
|
|
3644
4412
|
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
setActiveView(btn.dataset.view);
|
|
4413
|
+
viewFilter.addEventListener('change', () => {
|
|
4414
|
+
if (viewFilter.value !== activeView) {
|
|
4415
|
+
setActiveView(viewFilter.value);
|
|
3649
4416
|
}
|
|
3650
4417
|
});
|
|
3651
4418
|
|
|
3652
4419
|
// Event delegation for card clicks (avoids re-adding handlers on every render)
|
|
3653
|
-
|
|
3654
|
-
const
|
|
3655
|
-
if (
|
|
3656
|
-
|
|
4420
|
+
function handleCardContainerClick(e) {
|
|
4421
|
+
const toggle = e.target.closest('[data-action="toggle-subtasks"]');
|
|
4422
|
+
if (toggle) {
|
|
4423
|
+
e.preventDefault();
|
|
4424
|
+
e.stopPropagation();
|
|
4425
|
+
const parentId = toggle.dataset.parentId;
|
|
4426
|
+
toggleParentCollapsed(parentId);
|
|
4427
|
+
return;
|
|
4428
|
+
}
|
|
3657
4429
|
|
|
3658
|
-
document.getElementById('mobileCardsContainer').addEventListener('click', (e) => {
|
|
3659
4430
|
const card = e.target.closest('.card');
|
|
3660
4431
|
if (card) openTaskModal(card.dataset.taskId);
|
|
3661
|
-
}
|
|
4432
|
+
}
|
|
4433
|
+
|
|
4434
|
+
board.addEventListener('click', handleCardContainerClick);
|
|
4435
|
+
document.getElementById('mobileCardsContainer').addEventListener('click', handleCardContainerClick);
|
|
4436
|
+
|
|
4437
|
+
function registerServiceWorker() {
|
|
4438
|
+
if (!('serviceWorker' in navigator)) {
|
|
4439
|
+
return;
|
|
4440
|
+
}
|
|
4441
|
+
|
|
4442
|
+
window.addEventListener('load', () => {
|
|
4443
|
+
navigator.serviceWorker.register('/sw.js', { scope: '/' }).catch(() => {
|
|
4444
|
+
// PWA support is optional; ignore registration failures.
|
|
4445
|
+
});
|
|
4446
|
+
});
|
|
4447
|
+
}
|
|
3662
4448
|
|
|
3663
4449
|
// Initialize
|
|
3664
4450
|
loadPreferences();
|
|
3665
4451
|
bindColumnScrollIndicators();
|
|
4452
|
+
registerServiceWorker();
|
|
3666
4453
|
connectEventStream();
|
|
3667
4454
|
requestPoll();
|
|
3668
4455
|
|