reviewzy 0.1.0 → 0.3.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/.env.example +5 -0
- package/README.md +29 -10
- package/package.json +1 -1
- package/src/config.ts +4 -0
- package/src/dashboard/mount.ts +118 -1
- package/src/dashboard/repo-link.ts +126 -0
- package/src/dashboard/static/components.css +6780 -534
- package/src/dashboard/static/cursor-rules.js +83 -0
- package/src/dashboard/static/cursor.css +105 -0
- package/src/dashboard/static/cursor.js +358 -0
- package/src/dashboard/static/dashboard.css +139 -77
- package/src/dashboard/static/dashboard.js +147 -29
- package/src/dashboard/static/fonts/jetbrainsmono-latin-ext.woff2 +0 -0
- package/src/dashboard/static/fonts/jetbrainsmono-latin.woff2 +0 -0
- package/src/dashboard/static/fonts/onest-cyrillic.woff2 +0 -0
- package/src/dashboard/static/fonts/onest-latin-ext.woff2 +0 -0
- package/src/dashboard/static/fonts/onest-latin.woff2 +0 -0
- package/src/dashboard/static/icons.svg +184 -0
- package/src/dashboard/static/tokens.css +421 -110
- package/src/dashboard/static/ui.js +1992 -0
- package/src/dashboard/views.tsx +600 -282
- package/src/dashboard/word-diff.ts +89 -0
- package/src/db/human-save.ts +112 -0
- package/src/db/migrations.ts +38 -0
- package/src/db/queries.ts +36 -13
- package/src/db/sweep.ts +2 -2
- package/src/mcp/await-approved.ts +17 -10
- package/src/mcp/fetch-approved.ts +11 -6
- package/src/mcp/file-entries.ts +95 -20
- package/src/mcp/list-entries.ts +54 -29
- package/src/mcp/mark-applied.ts +6 -6
- package/src/mcp/route.ts +6 -9
- package/src/mcp/server.ts +8 -3
- package/src/shim/daemon.ts +23 -16
- package/src/shim/main.ts +26 -5
|
@@ -2,170 +2,232 @@
|
|
|
2
2
|
reviewzy — dashboard page styles
|
|
3
3
|
============================================================ */
|
|
4
4
|
|
|
5
|
-
/*
|
|
5
|
+
/* The one override that retunes the column: the navbar's card and inner
|
|
6
|
+
column, the page width and this shell all read it. */
|
|
7
|
+
:root { --page-w: 1400px; }
|
|
8
|
+
|
|
9
|
+
/* ---- Page shell (navbar shell) ---- */
|
|
6
10
|
.app-shell { min-height: 100vh; display: flex; flex-direction: column; }
|
|
7
|
-
.main { padding: var(--space-10) var(--space-
|
|
11
|
+
.main { padding: var(--space-10) var(--space-8); max-width: var(--page-w); margin: 0 auto; width: 100%; }
|
|
8
12
|
|
|
9
|
-
/* ---- Page header: the eyebrow / title / lede triplet ---- */
|
|
10
|
-
.page-
|
|
11
|
-
.page-
|
|
12
|
-
.page-title { font: 550 var(--text-2xl)/1.15 var(--font-sans); color: var(--text); letter-spacing: -0.02em; margin-bottom: var(--space-3); }
|
|
13
|
-
.page-lede { font-size: var(--text-base); color: var(--text-dim); max-width: 540px; }
|
|
13
|
+
/* ---- Page header: the eyebrow / title / lede triplet (login/editor pages) ---- */
|
|
14
|
+
.page-eyebrow { color: var(--accent-text); margin-bottom: var(--space-2); }
|
|
15
|
+
.page-lede { font-size: var(--t-3); color: var(--text-3); max-width: 540px; }
|
|
14
16
|
|
|
15
17
|
/* ---- Filter form ---- */
|
|
16
|
-
.filters { display: grid; grid-template-columns: minmax(0, 2fr) 200px 200px
|
|
18
|
+
.filters { display: grid; grid-template-columns: minmax(0, 2fr) 200px 200px; gap: var(--space-3); align-items: end; margin-bottom: var(--space-8); }
|
|
17
19
|
@media (max-width: 768px) { .filters { grid-template-columns: 1fr; } }
|
|
18
20
|
|
|
19
21
|
/* ---- Entries region ---- */
|
|
20
22
|
/* The loading strip is hidden until htmx marks the request in flight (hx-indicator adds the
|
|
21
23
|
.htmx-request class); it sits beside the swap target, so a swap never removes it. */
|
|
22
|
-
#entries-loading { display: none; align-items: center; gap: var(--space-2); font-size: var(--
|
|
24
|
+
#entries-loading { display: none; align-items: center; gap: var(--space-2); font-size: var(--t-2); color: var(--text-3); padding: var(--space-2) 0; }
|
|
23
25
|
#entries-loading.htmx-request { display: flex; }
|
|
24
|
-
/* The spinner reuses the @keyframes spin components.css ships for .btn-loading. */
|
|
25
|
-
.spinner {
|
|
26
|
-
width: 12px; height: 12px;
|
|
27
|
-
border: 2px solid var(--line-strong);
|
|
28
|
-
border-top-color: var(--accent);
|
|
29
|
-
border-radius: 50%;
|
|
30
|
-
animation: spin 0.6s linear infinite;
|
|
31
|
-
flex-shrink: 0;
|
|
32
|
-
}
|
|
33
26
|
|
|
34
27
|
/* ---- Partial state ---- */
|
|
35
28
|
.partial-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin-bottom: var(--space-4); }
|
|
36
|
-
.partial-count { font-size: var(--
|
|
29
|
+
.partial-count { font-size: var(--t-2); color: var(--text-3); }
|
|
37
30
|
|
|
38
31
|
/* ---- Project groups ---- */
|
|
39
32
|
.project-group { margin-bottom: var(--space-10); }
|
|
40
33
|
.project-header { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-4); }
|
|
41
|
-
.project-name { font: 500 var(--
|
|
34
|
+
.project-name { font: 500 var(--t-5)/1.2 var(--font-mono); color: var(--text); letter-spacing: -0.01em; }
|
|
42
35
|
.batch { margin-bottom: var(--space-6); }
|
|
43
36
|
.batch-header { display: flex; align-items: baseline; gap: var(--space-3); margin-bottom: var(--space-2); }
|
|
44
|
-
.batch-
|
|
37
|
+
.batch-meta { font: 400 var(--t-2)/1 var(--font-sans); color: var(--text-4); }
|
|
45
38
|
|
|
46
39
|
/* ---- Entry table cells ----
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
.
|
|
51
|
-
.
|
|
52
|
-
.file
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
40
|
+
The table lays out fixed with a colgroup so the title column takes the remainder and the row
|
|
41
|
+
never overflows; select, status, file, and actions are the fixed columns. The status pill has
|
|
42
|
+
its own column, so the title truncates at one consistent point and the pill stays tight. */
|
|
43
|
+
.col-select { width: 48px; }
|
|
44
|
+
.col-status { width: 106px; }
|
|
45
|
+
.col-file { width: 200px; }
|
|
46
|
+
.col-actions { width: 120px; }
|
|
47
|
+
.entry-head {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: var(--space-2);
|
|
51
|
+
}
|
|
52
|
+
.entry-title {
|
|
53
|
+
flex: 1 1 auto;
|
|
54
|
+
min-width: 0;
|
|
55
|
+
font: 400 var(--t-3)/1.4 var(--font-sans);
|
|
56
|
+
color: var(--text);
|
|
58
57
|
white-space: nowrap;
|
|
59
58
|
overflow: hidden;
|
|
60
59
|
text-overflow: ellipsis;
|
|
61
60
|
}
|
|
62
|
-
.entry-
|
|
63
|
-
|
|
64
|
-
-
|
|
65
|
-
|
|
61
|
+
.entry-filer {
|
|
62
|
+
flex-shrink: 0;
|
|
63
|
+
font-size: var(--t-2);
|
|
64
|
+
color: var(--text-4);
|
|
65
|
+
white-space: nowrap;
|
|
66
|
+
}
|
|
67
|
+
.cell-file {
|
|
68
|
+
font-family: var(--font-mono);
|
|
69
|
+
font-size: var(--t-2);
|
|
70
|
+
color: var(--text-4);
|
|
71
|
+
white-space: nowrap;
|
|
66
72
|
overflow: hidden;
|
|
73
|
+
text-overflow: ellipsis;
|
|
67
74
|
}
|
|
68
|
-
.cell-filedby { color: var(--text-dim); white-space: nowrap; }
|
|
69
|
-
.constraint-summary { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--text-dim); white-space: nowrap; }
|
|
70
75
|
|
|
71
76
|
/* ---- Row actions and batch approve ----
|
|
72
77
|
A control mid-request ignores further activations: htmx marks the requesting element with
|
|
73
78
|
.htmx-request while the request is in flight, and pointer-events drops the duplicate click a
|
|
74
79
|
double-click would otherwise fire. */
|
|
75
80
|
.htmx-request { pointer-events: none; }
|
|
76
|
-
.cell-
|
|
77
|
-
.cell-
|
|
78
|
-
.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
.cell-actions { white-space: nowrap; display: inline-flex; align-items: center; gap: var(--space-1); }
|
|
82
|
+
.table-wrap tbody td.cell-actions { padding-left: var(--space-1); padding-right: var(--space-1); }
|
|
83
|
+
.batch-bar {
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
margin-bottom: var(--space-4);
|
|
87
|
+
/* Sticks just below the navbar so the approve button stays reachable while rows scroll
|
|
88
|
+
under it; the background and z-index stop table rows from showing through the bar. */
|
|
89
|
+
position: sticky;
|
|
90
|
+
top: var(--navbar-h);
|
|
91
|
+
z-index: 10;
|
|
92
|
+
padding: var(--space-2) 0;
|
|
93
|
+
background: var(--bg);
|
|
94
|
+
}
|
|
95
|
+
.batch-header .checkbox-label { align-self: center; }
|
|
96
|
+
/* ---- Collapsible project sections and batches ----
|
|
97
|
+
The chevron points down expanded and turns when collapsed; the state is applied by dashboard.js
|
|
98
|
+
from localStorage, so the server ships everything open and the viewer's choice re-applies. */
|
|
99
|
+
.collapse-toggle { align-self: center; }
|
|
100
|
+
.collapse-toggle svg { transition: transform var(--dur-tint) var(--ease); }
|
|
101
|
+
.collapse-toggle[aria-expanded="false"] svg { transform: rotate(-90deg); }
|
|
102
|
+
.project-header .collapse-toggle, .batch-header .collapse-toggle { margin-left: auto; }
|
|
81
103
|
.notice-list { margin: 0; padding-left: var(--space-5); display: flex; flex-direction: column; gap: var(--space-1); }
|
|
82
104
|
|
|
83
|
-
/* ---- Diff view: the anchored snippet vs the entry's text, mono on
|
|
105
|
+
/* ---- Diff view: the anchored snippet vs the entry's text, mono on the well, anchor distinct ---- */
|
|
84
106
|
.diff-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); margin-bottom: var(--space-8); }
|
|
85
107
|
@media (max-width: 900px) { .diff-grid { grid-template-columns: 1fr; } }
|
|
86
108
|
.diff-code {
|
|
87
|
-
font: 400 var(--
|
|
88
|
-
background: var(--
|
|
109
|
+
font: 400 var(--t-2)/1.6 var(--font-mono);
|
|
110
|
+
background: var(--well);
|
|
89
111
|
border: 1px solid var(--line);
|
|
90
|
-
border-radius: var(--
|
|
112
|
+
border-radius: var(--r-1);
|
|
91
113
|
padding: var(--space-4);
|
|
92
114
|
white-space: pre-wrap;
|
|
93
115
|
word-break: break-word;
|
|
94
|
-
color: var(--text-
|
|
116
|
+
color: var(--text-2);
|
|
95
117
|
margin: 0;
|
|
96
118
|
}
|
|
97
119
|
.diff-line { display: block; }
|
|
98
120
|
.diff-anchor {
|
|
99
|
-
background: var(--accent-
|
|
121
|
+
background: var(--accent-quiet);
|
|
100
122
|
border-left: 2px solid var(--accent);
|
|
101
123
|
color: var(--text);
|
|
102
124
|
padding-left: var(--space-2);
|
|
103
125
|
}
|
|
104
126
|
|
|
105
|
-
/* ---- Empty
|
|
106
|
-
.empty-
|
|
107
|
-
.empty-icon { margin: 0 auto var(--space-4); width: 32px; height: 32px; color: var(--text-faint); display: flex; align-items: center; justify-content: center; }
|
|
108
|
-
.empty-state h3 { font: 500 var(--text-lg)/1.3 var(--font-sans); color: var(--text); margin-bottom: var(--space-2); }
|
|
109
|
-
.empty-state p { font-size: var(--text-sm); color: var(--text-dim); max-width: 420px; margin: 0 auto var(--space-4); }
|
|
127
|
+
/* ---- Empty state actions: the shipped .empty-state lays out one button, this row holds two ---- */
|
|
128
|
+
.empty-actions { display: flex; gap: var(--space-2); justify-content: center; }
|
|
110
129
|
|
|
111
130
|
/* ---- Error box (cloned from #error-box by dashboard.js) ---- */
|
|
112
131
|
.callout-content { min-width: 0; }
|
|
113
132
|
.callout-actions { margin-top: var(--space-2); }
|
|
114
133
|
|
|
115
134
|
/* ---- Entry editor ---- */
|
|
116
|
-
/* The one live region
|
|
117
|
-
.
|
|
118
|
-
position: absolute;
|
|
119
|
-
width: 1px;
|
|
120
|
-
height: 1px;
|
|
121
|
-
margin: -1px;
|
|
122
|
-
padding: 0;
|
|
123
|
-
overflow: hidden;
|
|
124
|
-
clip: rect(0 0 0 0);
|
|
125
|
-
white-space: nowrap;
|
|
126
|
-
border: 0;
|
|
127
|
-
}
|
|
128
|
-
.editor-grid { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); gap: var(--space-8); align-items: start; }
|
|
135
|
+
/* The one live region, kept out of sight by the shipped .sr-only utility. */
|
|
136
|
+
.editor-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--space-8); align-items: start; }
|
|
129
137
|
.editor-main { min-width: 0; }
|
|
130
138
|
.editor-rail { display: flex; flex-direction: column; gap: var(--space-6); position: sticky; top: var(--space-8); }
|
|
131
139
|
@media (max-width: 900px) {
|
|
132
140
|
.editor-grid { grid-template-columns: 1fr; }
|
|
133
141
|
.editor-rail { position: static; }
|
|
134
142
|
}
|
|
143
|
+
/* The editor's title card header: the title falls back to the anchor text, which can be one long
|
|
144
|
+
unbroken string, so it wraps inside the card instead of overflowing or shoving the status pill. */
|
|
145
|
+
.editor-title { min-width: 0; overflow-wrap: anywhere; }
|
|
146
|
+
/* The entry text ships verbatim, often carrying markdown backticks and significant whitespace;
|
|
147
|
+
mono gives every glyph, space included, one full cell so both stay countable while editing. */
|
|
148
|
+
#editor-text { font-family: var(--font-mono); }
|
|
149
|
+
|
|
150
|
+
/* ---- Editor prev/next rails ----
|
|
151
|
+
One full-height fixed strip per side, below the navbar, so the previous and next entries are
|
|
152
|
+
a single click away while reading. The strip sits inside the page's inline padding, clear of content. */
|
|
153
|
+
.editor-nav-btn {
|
|
154
|
+
position: fixed;
|
|
155
|
+
top: var(--navbar-h);
|
|
156
|
+
bottom: 0;
|
|
157
|
+
width: 44px;
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
justify-content: center;
|
|
161
|
+
color: var(--text-3);
|
|
162
|
+
text-decoration: none;
|
|
163
|
+
z-index: 40;
|
|
164
|
+
transition: background var(--dur-tint) var(--ease), color var(--dur-tint) var(--ease);
|
|
165
|
+
}
|
|
166
|
+
.editor-nav-prev { left: 0; }
|
|
167
|
+
.editor-nav-next { right: 0; }
|
|
168
|
+
.editor-nav-btn:hover { background: var(--surface-2); color: var(--text); }
|
|
169
|
+
.editor-nav-btn.is-disabled { color: var(--text-4); opacity: 0.4; pointer-events: none; }
|
|
170
|
+
.editor-nav-btn svg { width: 18px; height: 18px; }
|
|
171
|
+
@media (max-width: 900px) { .editor-nav { display: none; } }
|
|
135
172
|
#editor-region > .callout { margin-bottom: var(--space-4); }
|
|
136
173
|
#editor-region > .btn { margin-top: var(--space-4); }
|
|
137
174
|
.editor-actions { margin-top: var(--space-4); display: flex; align-items: center; gap: var(--space-2); }
|
|
138
175
|
/* The in-flight indicator for an approve or reject from the editor: htmx's own rules fade it in. */
|
|
139
|
-
.editor-actions .htmx-indicator { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--
|
|
176
|
+
.editor-actions .htmx-indicator { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--t-2); color: var(--text-3); }
|
|
177
|
+
|
|
178
|
+
/* The notes card: the agent's row reuses the constraint row shape; the human's textarea and its
|
|
179
|
+
save button sit below it. */
|
|
180
|
+
.notes-field { margin-top: var(--space-2); }
|
|
181
|
+
.notes-actions { margin-top: var(--space-2); display: flex; justify-content: flex-end; }
|
|
140
182
|
|
|
141
183
|
/* Constraint panel: live count and placeholder checklist; the server owns the refusal. */
|
|
142
184
|
.constraint-row { display: flex; gap: var(--space-3); padding: 6px 0; }
|
|
143
185
|
.constraint-row + .constraint-row { border-top: 1px solid var(--line); }
|
|
144
|
-
.constraint-label { flex-shrink: 0; font:
|
|
145
|
-
.constraint-value { min-width: 0; color: var(--text-
|
|
146
|
-
.char-count { color: var(--text-
|
|
186
|
+
.constraint-label { flex-shrink: 0; font: 500 11px/1.4 var(--font-sans); letter-spacing: var(--caps); text-transform: uppercase; color: var(--text-3); width: 96px; padding-top: 2px; }
|
|
187
|
+
.constraint-value { min-width: 0; color: var(--text-2); font-size: var(--t-2); }
|
|
188
|
+
.char-count { color: var(--text-2); font-size: var(--t-2); }
|
|
147
189
|
.char-count.over { color: var(--danger); }
|
|
148
190
|
.placeholder-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
|
|
149
191
|
.placeholder-item { display: flex; align-items: center; gap: var(--space-2); }
|
|
150
|
-
.placeholder-state { font-size: var(--
|
|
192
|
+
.placeholder-state { font-size: var(--t-2); color: var(--text-3); min-width: 52px; }
|
|
151
193
|
.placeholder-state.is-present { color: var(--success); }
|
|
152
|
-
|
|
194
|
+
|
|
195
|
+
/* The context card's secondary block: the provenance rows (hash, filer, stale note) collapsed
|
|
196
|
+
behind a summary, so the anchor and the agent-supplied context rows lead the card. */
|
|
197
|
+
.context-secondary { margin-top: var(--space-3); }
|
|
198
|
+
.context-secondary summary { cursor: pointer; font-size: var(--t-2); color: var(--text-3); }
|
|
199
|
+
.context-secondary .info-table { margin-top: var(--space-2); }
|
|
200
|
+
|
|
201
|
+
/* Repo row link: the brand icon beside the shortened owner/repo path, linking out to the host. */
|
|
202
|
+
.repo-link { display: inline-flex; align-items: center; gap: 6px; color: var(--text); text-decoration: none; }
|
|
203
|
+
.repo-link:hover { color: var(--accent-text); }
|
|
204
|
+
.repo-icon { width: 14px; height: 14px; flex-shrink: 0; fill: currentColor; opacity: var(--icon-dim); }
|
|
205
|
+
.placeholder-token { font: 400 var(--t-2)/1 var(--font-mono); color: var(--text); background: var(--well); border: 1px solid var(--line); border-radius: var(--r-1); padding: 2px 6px; }
|
|
206
|
+
|
|
207
|
+
/* Entry images: one image per row at the card's width, a remove form below each, and the
|
|
208
|
+
one-file upload form closing the card. The file input flexes to whatever width remains. */
|
|
209
|
+
.image-list { list-style: none; margin: 0 0 var(--space-3); padding: 0; display: flex; flex-direction: column; gap: var(--space-3); }
|
|
210
|
+
.entry-image { display: block; max-width: 100%; height: auto; border: 1px solid var(--line); border-radius: var(--r-1); }
|
|
211
|
+
.image-skip { margin: 0; font-size: var(--t-2); color: var(--text-3); }
|
|
212
|
+
.image-remove { margin-top: var(--space-1); }
|
|
213
|
+
.image-upload { display: flex; gap: var(--space-2); align-items: center; }
|
|
214
|
+
.image-upload .input { flex: 1; min-width: 0; }
|
|
153
215
|
|
|
154
216
|
/* Revision history: newest first; the text clamps like the list's entry text. */
|
|
155
|
-
.history-empty { font-size: var(--
|
|
217
|
+
.history-empty { font-size: var(--t-2); color: var(--text-4); padding: var(--space-2) 0; }
|
|
156
218
|
.revision-list { list-style: none; margin: 0; padding: 0; }
|
|
157
219
|
.revision { padding: var(--space-3) 0; }
|
|
158
220
|
.revision + .revision { border-top: 1px solid var(--line); }
|
|
159
221
|
.revision-head { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-2); }
|
|
160
|
-
.revision-time { font-size: var(--
|
|
222
|
+
.revision-time { font-size: var(--t-2); color: var(--text-4); }
|
|
161
223
|
.revision-head .btn { margin-left: auto; }
|
|
162
224
|
.revision-text {
|
|
163
225
|
display: -webkit-box;
|
|
164
226
|
-webkit-line-clamp: 2;
|
|
165
227
|
-webkit-box-orient: vertical;
|
|
166
228
|
overflow: hidden;
|
|
167
|
-
font-size: var(--
|
|
168
|
-
color: var(--text-
|
|
229
|
+
font-size: var(--t-2);
|
|
230
|
+
color: var(--text-2);
|
|
169
231
|
}
|
|
170
232
|
|
|
171
233
|
/* ---- Login page: the navbar shell, then one centered card ---- */
|
|
@@ -1,35 +1,11 @@
|
|
|
1
|
-
// reviewzy dashboard behaviors.
|
|
1
|
+
// reviewzy dashboard behaviors. Loads as a module after htmx.min.js.
|
|
2
|
+
// The navbar's ink, its dock state and the table header label boxes come from ui.js;
|
|
3
|
+
// initTables is re-run after every htmx swap so a freshly rendered table head gets its boxes.
|
|
4
|
+
import { initTables } from "./ui.js";
|
|
5
|
+
|
|
2
6
|
(() => {
|
|
3
7
|
"use strict";
|
|
4
8
|
|
|
5
|
-
// --- Navbar ink (cloudy-ui navbar component) ---
|
|
6
|
-
// Positions the sliding underline under the active link; re-positions on resize without animating.
|
|
7
|
-
const navbarNav = document.getElementById("navbar-nav");
|
|
8
|
-
const navbarInk = document.getElementById("navbar-ink");
|
|
9
|
-
|
|
10
|
-
function positionNavbarInk() {
|
|
11
|
-
if (!navbarNav || !navbarInk) return;
|
|
12
|
-
const active = navbarNav.querySelector(".navbar-link.active");
|
|
13
|
-
if (!active) {
|
|
14
|
-
navbarInk.style.opacity = "0";
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
const navRect = navbarNav.getBoundingClientRect();
|
|
18
|
-
const linkRect = active.getBoundingClientRect();
|
|
19
|
-
navbarInk.style.left = `${linkRect.left - navRect.left}px`;
|
|
20
|
-
navbarInk.style.width = `${linkRect.width}px`;
|
|
21
|
-
navbarInk.style.opacity = "1";
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
window.addEventListener("resize", () => {
|
|
25
|
-
navbarInk.style.transition = "none";
|
|
26
|
-
positionNavbarInk();
|
|
27
|
-
requestAnimationFrame(() => {
|
|
28
|
-
navbarInk.style.transition = "";
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
requestAnimationFrame(positionNavbarInk);
|
|
32
|
-
|
|
33
9
|
// --- Filter errors ---
|
|
34
10
|
// A failed filter request (HTTP error or network error) replaces the list with the error box
|
|
35
11
|
// from the page's #error-box template; the retry button re-submits the filter form.
|
|
@@ -60,6 +36,34 @@
|
|
|
60
36
|
form.addEventListener("htmx:sendError", showFilterError);
|
|
61
37
|
}
|
|
62
38
|
|
|
39
|
+
// --- Filter auto-submit ---
|
|
40
|
+
// The Apply button is gone: changing a select or typing in the search fires the filter request
|
|
41
|
+
// itself. requestSubmit() reaches htmx's submit listener on the form; the debounce collapses a
|
|
42
|
+
// burst of keystrokes into one request. Only the change and input events below trigger a submit,
|
|
43
|
+
// and both fire on user action alone, so a programmatic value reset (which sets .value directly
|
|
44
|
+
// and emits no event) never re-fires.
|
|
45
|
+
function submitFilters() {
|
|
46
|
+
const target = document.getElementById("filters");
|
|
47
|
+
if (target && typeof target.requestSubmit === "function") {
|
|
48
|
+
target.requestSubmit();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let searchTimer;
|
|
53
|
+
function scheduleSearchSubmit() {
|
|
54
|
+
clearTimeout(searchTimer);
|
|
55
|
+
searchTimer = setTimeout(submitFilters, 300);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (form) {
|
|
59
|
+
const statusSelect = document.getElementById("status");
|
|
60
|
+
const projectSelect = document.getElementById("project");
|
|
61
|
+
const searchInput = document.getElementById("q");
|
|
62
|
+
statusSelect?.addEventListener("change", submitFilters);
|
|
63
|
+
projectSelect?.addEventListener("change", submitFilters);
|
|
64
|
+
searchInput?.addEventListener("input", scheduleSearchSubmit);
|
|
65
|
+
}
|
|
66
|
+
|
|
63
67
|
// --- Style guide save errors ---
|
|
64
68
|
// A failed save (HTTP error or network error) puts the error box from the page's
|
|
65
69
|
// #style-guide-error-box template above the form; the retry button re-submits the save form.
|
|
@@ -166,4 +170,118 @@
|
|
|
166
170
|
refreshEditorMetrics();
|
|
167
171
|
textarea.focus();
|
|
168
172
|
});
|
|
173
|
+
|
|
174
|
+
// --- Batch select-all and live selection count ---
|
|
175
|
+
// The list region is swapped wholesale on every filter and action round, so no listener bound to
|
|
176
|
+
// a checkbox or the button survives a swap: change events are delegated on document, and one pass
|
|
177
|
+
// after each swap recomputes the select-all states and the approve button's count. All of it is
|
|
178
|
+
// progressive enhancement — without JS the approve button keeps its static label, stays enabled,
|
|
179
|
+
// and the native form still submits the checked rows.
|
|
180
|
+
const DRAFT_SELECTOR = 'input[type="checkbox"][name="id"]';
|
|
181
|
+
const SELECT_ALL_SELECTOR = 'input[type="checkbox"].select-all';
|
|
182
|
+
|
|
183
|
+
function selectionCount() {
|
|
184
|
+
return document.querySelectorAll(`#batch-form ${DRAFT_SELECTOR}:checked`).length;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function updateApproveButton() {
|
|
188
|
+
const button = document.querySelector('#batch-form .batch-bar button[type="submit"]');
|
|
189
|
+
if (!button) return;
|
|
190
|
+
const count = selectionCount();
|
|
191
|
+
button.textContent = count === 0 ? "Approve selected" : `Approve ${count}`;
|
|
192
|
+
button.disabled = count === 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function updateSelectAll(selectAll) {
|
|
196
|
+
const batch = selectAll.closest(".batch");
|
|
197
|
+
if (!batch) return;
|
|
198
|
+
const drafts = batch.querySelectorAll(DRAFT_SELECTOR);
|
|
199
|
+
const checked = batch.querySelectorAll(`${DRAFT_SELECTOR}:checked`).length;
|
|
200
|
+
selectAll.checked = drafts.length > 0 && checked === drafts.length;
|
|
201
|
+
selectAll.indeterminate = checked > 0 && checked < drafts.length;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function refreshSelection() {
|
|
205
|
+
for (const selectAll of document.querySelectorAll(SELECT_ALL_SELECTOR)) {
|
|
206
|
+
updateSelectAll(selectAll);
|
|
207
|
+
}
|
|
208
|
+
updateApproveButton();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
document.addEventListener("change", (event) => {
|
|
212
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
213
|
+
if (!target) return;
|
|
214
|
+
if (target.matches(SELECT_ALL_SELECTOR)) {
|
|
215
|
+
const batch = target.closest(".batch");
|
|
216
|
+
if (batch) {
|
|
217
|
+
for (const draft of batch.querySelectorAll(DRAFT_SELECTOR)) {
|
|
218
|
+
draft.checked = target.checked;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
refreshSelection();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
document.addEventListener("htmx:afterSwap", refreshSelection);
|
|
226
|
+
|
|
227
|
+
// --- Collapsible project sections and batches ---
|
|
228
|
+
// The server renders every section and batch open; JS collapses the ones this viewer collapsed
|
|
229
|
+
// before, from localStorage, and persists each toggle. Re-applied after every swap because a
|
|
230
|
+
// filter or action round replaces the whole list region. Without JS the toggles stay inert and
|
|
231
|
+
// everything stays open.
|
|
232
|
+
const COLLAPSE_STORAGE = "reviewzy.list-collapsed";
|
|
233
|
+
|
|
234
|
+
function readCollapsed() {
|
|
235
|
+
try {
|
|
236
|
+
const raw = localStorage.getItem(COLLAPSE_STORAGE);
|
|
237
|
+
const parsed = raw ? JSON.parse(raw) : {};
|
|
238
|
+
// A parsed array is not a key map: property sets on it would survive only until the next
|
|
239
|
+
// JSON.stringify, which drops them, so persistence would silently stop.
|
|
240
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
|
|
241
|
+
} catch {
|
|
242
|
+
return {};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function persistCollapsed(key, collapsed) {
|
|
247
|
+
const map = readCollapsed();
|
|
248
|
+
if (collapsed) {
|
|
249
|
+
map[key] = true;
|
|
250
|
+
} else {
|
|
251
|
+
delete map[key];
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
localStorage.setItem(COLLAPSE_STORAGE, JSON.stringify(map));
|
|
255
|
+
} catch {
|
|
256
|
+
// Storage can be unavailable (private windows); the toggle still works for the page.
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function applyCollapseState() {
|
|
261
|
+
const collapsed = readCollapsed();
|
|
262
|
+
for (const toggle of document.querySelectorAll("[data-collapse-toggle]")) {
|
|
263
|
+
const key = toggle.dataset.collapseKey ?? "";
|
|
264
|
+
const target = document.getElementById(toggle.dataset.collapseTarget ?? "");
|
|
265
|
+
if (!target) continue;
|
|
266
|
+
const hidden = collapsed[key] === true;
|
|
267
|
+
target.hidden = hidden;
|
|
268
|
+
toggle.setAttribute("aria-expanded", String(!hidden));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
document.addEventListener("click", (event) => {
|
|
273
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
274
|
+
const toggle = target ? target.closest("[data-collapse-toggle]") : null;
|
|
275
|
+
if (!toggle) return;
|
|
276
|
+
const el = document.getElementById(toggle.dataset.collapseTarget ?? "");
|
|
277
|
+
if (!el) return;
|
|
278
|
+
const hide = !el.hidden;
|
|
279
|
+
el.hidden = hide;
|
|
280
|
+
toggle.setAttribute("aria-expanded", String(!hide));
|
|
281
|
+
persistCollapsed(toggle.dataset.collapseKey ?? "", hide);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
applyCollapseState();
|
|
285
|
+
document.addEventListener("htmx:afterSwap", applyCollapseState);
|
|
286
|
+
document.addEventListener("htmx:afterSwap", initTables);
|
|
169
287
|
})();
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|