reviewzy 0.2.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/README.md +1 -1
- package/package.json +1 -1
- package/src/dashboard/static/components.css +6780 -546
- 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 +52 -125
- package/src/dashboard/static/dashboard.js +6 -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 +158 -201
- package/src/db/queries.ts +1 -1
- package/src/mcp/await-approved.ts +17 -10
- package/src/mcp/fetch-approved.ts +11 -6
- package/src/mcp/file-entries.ts +45 -22
- package/src/mcp/list-entries.ts +42 -55
- package/src/mcp/mark-applied.ts +5 -5
- package/src/mcp/server.ts +8 -3
package/src/dashboard/views.tsx
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/** @jsxImportSource hono/jsx */
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { raw } from "hono/html";
|
|
2
4
|
import type { JSX } from "hono/jsx/jsx-runtime";
|
|
3
5
|
import { listRevisions, parseConstraints, parseEntryImages } from "../db/human-save.ts";
|
|
4
6
|
import type { Constraints, HumanSaveRefusal, RevisionRow, TransitionRefusal } from "../db/human-save.ts";
|
|
@@ -15,7 +17,7 @@ import type { StyleGuideForm, StyleGuideFormRefusal, StyleGuideInput } from "../
|
|
|
15
17
|
import type { Store } from "../db/store.ts";
|
|
16
18
|
import { parseRepoLink, REPO_ICON } from "./repo-link.ts";
|
|
17
19
|
import { diffWords } from "./word-diff.ts";
|
|
18
|
-
import { NAME
|
|
20
|
+
import { NAME } from "../version.ts";
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* The three dashboard filters, straight off the query string. `status` and `project` are the raw
|
|
@@ -213,23 +215,26 @@ function shortUlid(id: string): string {
|
|
|
213
215
|
/** The section the current page belongs to, marking the matching navbar link. */
|
|
214
216
|
type NavSection = "entries" | "style-guide" | "none";
|
|
215
217
|
|
|
218
|
+
/** The icon sprite, pasted into every full page's body as its first child, read off the vendored file so the two can never drift. */
|
|
219
|
+
const ICON_SPRITE = readFileSync(new URL("./static/icons.svg", import.meta.url), "utf8");
|
|
220
|
+
|
|
216
221
|
function Navbar({ signedIn, active }: { signedIn: boolean; active: NavSection }) {
|
|
217
222
|
return (
|
|
218
223
|
<nav class="navbar">
|
|
219
|
-
<div class="navbar-
|
|
220
|
-
<div class="
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
<div class="navbar-inner">
|
|
225
|
+
<div class="navbar-brand">
|
|
226
|
+
<div class="logo-name">{NAME}</div>
|
|
227
|
+
</div>
|
|
228
|
+
<div class="navbar-nav" id="navbar-nav">
|
|
229
|
+
<a class={`navbar-link${active === "entries" ? " active" : ""}`} href="/">Entries</a>
|
|
230
|
+
<a class={`navbar-link${active === "style-guide" ? " active" : ""}`} href="/style-guide">Style guide</a>
|
|
231
|
+
</div>
|
|
232
|
+
{signedIn ? (
|
|
233
|
+
<form method="post" action="/logout" class="navbar-actions">
|
|
234
|
+
<button class="btn btn-secondary btn-sm" type="submit">Sign out</button>
|
|
235
|
+
</form>
|
|
236
|
+
) : null}
|
|
227
237
|
</div>
|
|
228
|
-
{signedIn ? (
|
|
229
|
-
<form method="post" action="/logout" class="navbar-actions">
|
|
230
|
-
<button class="btn btn-ghost btn-sm" type="submit">Sign out</button>
|
|
231
|
-
</form>
|
|
232
|
-
) : null}
|
|
233
238
|
</nav>
|
|
234
239
|
);
|
|
235
240
|
}
|
|
@@ -237,8 +242,12 @@ function Navbar({ signedIn, active }: { signedIn: boolean; active: NavSection })
|
|
|
237
242
|
function PageHeader({ total }: { total: number }) {
|
|
238
243
|
return (
|
|
239
244
|
<header class="page-header">
|
|
240
|
-
<
|
|
241
|
-
|
|
245
|
+
<div class="page-header-body">
|
|
246
|
+
<h1 class="page-title">Entries</h1>
|
|
247
|
+
<div class="page-meta">
|
|
248
|
+
<span class="page-meta-item">{total} {total === 1 ? "entry" : "entries"}</span>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
242
251
|
</header>
|
|
243
252
|
);
|
|
244
253
|
}
|
|
@@ -312,11 +321,8 @@ function NoticeCallout({ notice }: { notice: ListNotice }) {
|
|
|
312
321
|
if (notice.refused.length === 0) {
|
|
313
322
|
return (
|
|
314
323
|
<div class="callout callout-success" role="status">
|
|
315
|
-
<div class="callout-icon"
|
|
316
|
-
<svg
|
|
317
|
-
<circle cx="8" cy="8" r="6.5" />
|
|
318
|
-
<path d="M5 8l2 2 4-4" />
|
|
319
|
-
</svg>
|
|
324
|
+
<div class="callout-icon">
|
|
325
|
+
<svg aria-hidden="true"><use href="#i-success" /></svg>
|
|
320
326
|
</div>
|
|
321
327
|
<div class="callout-content">
|
|
322
328
|
<div class="callout-title">
|
|
@@ -328,11 +334,8 @@ function NoticeCallout({ notice }: { notice: ListNotice }) {
|
|
|
328
334
|
}
|
|
329
335
|
return (
|
|
330
336
|
<div class="callout callout-danger" role="alert">
|
|
331
|
-
<div class="callout-icon"
|
|
332
|
-
<svg
|
|
333
|
-
<circle cx="8" cy="8" r="6.5" />
|
|
334
|
-
<path d="M10 6L6 10M6 6l4 4" />
|
|
335
|
-
</svg>
|
|
337
|
+
<div class="callout-icon">
|
|
338
|
+
<svg aria-hidden="true"><use href="#i-error" /></svg>
|
|
336
339
|
</div>
|
|
337
340
|
<div class="callout-content">
|
|
338
341
|
<div class="callout-title">
|
|
@@ -396,14 +399,9 @@ function PartialHeader({ shown, total }: { shown: number; total: number }) {
|
|
|
396
399
|
function EmptyAll() {
|
|
397
400
|
return (
|
|
398
401
|
<div class="card empty-state">
|
|
399
|
-
<
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
<path d="M1.5 9.5h3.5l1 2h4l1-2h3.5" />
|
|
403
|
-
</svg>
|
|
404
|
-
</div>
|
|
405
|
-
<h3>No entries yet</h3>
|
|
406
|
-
<p>Agents file text through the reviewzy mcp endpoint. Filed entries appear here.</p>
|
|
402
|
+
<svg aria-hidden="true"><use href="#i-file" /></svg>
|
|
403
|
+
<div class="empty-title">No entries yet</div>
|
|
404
|
+
<div>Agents file text through the reviewzy mcp endpoint. Filed entries appear here.</div>
|
|
407
405
|
</div>
|
|
408
406
|
);
|
|
409
407
|
}
|
|
@@ -417,15 +415,10 @@ function EmptyAll() {
|
|
|
417
415
|
function EmptyMatch({ revealAllHref }: { revealAllHref: string | undefined }) {
|
|
418
416
|
return (
|
|
419
417
|
<div class="card empty-state">
|
|
420
|
-
<
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
</svg>
|
|
425
|
-
</div>
|
|
426
|
-
<h3>No entries match</h3>
|
|
427
|
-
<p>Try different search terms, or clear the filters.</p>
|
|
428
|
-
{revealAllHref !== undefined ? <p>Applied and rejected entries are hidden from this view.</p> : null}
|
|
418
|
+
<svg aria-hidden="true"><use href="#i-search" /></svg>
|
|
419
|
+
<div class="empty-title">No entries match</div>
|
|
420
|
+
<div>Try different search terms, or clear the filters.</div>
|
|
421
|
+
{revealAllHref !== undefined ? <div>Applied and rejected entries are hidden from this view.</div> : null}
|
|
429
422
|
<div class="empty-actions">
|
|
430
423
|
<a href="/" class="btn btn-secondary btn-sm">Clear filters</a>
|
|
431
424
|
{revealAllHref !== undefined ? (
|
|
@@ -440,14 +433,9 @@ function EmptyMatch({ revealAllHref }: { revealAllHref: string | undefined }) {
|
|
|
440
433
|
function EmptyActive() {
|
|
441
434
|
return (
|
|
442
435
|
<div class="card empty-state">
|
|
443
|
-
<
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
<path d="M5 8l2 2 4-4" />
|
|
447
|
-
</svg>
|
|
448
|
-
</div>
|
|
449
|
-
<h3>Nothing to review</h3>
|
|
450
|
-
<p>Every filed entry is applied or rejected. New drafts appear here as agents file them.</p>
|
|
436
|
+
<svg aria-hidden="true"><use href="#i-success" /></svg>
|
|
437
|
+
<div class="empty-title">Nothing to review</div>
|
|
438
|
+
<div>Every filed entry is applied or rejected. New drafts appear here as agents file them.</div>
|
|
451
439
|
</div>
|
|
452
440
|
);
|
|
453
441
|
}
|
|
@@ -506,12 +494,10 @@ function ProjectGroupView({ group }: { group: ProjectGroup }) {
|
|
|
506
494
|
data-collapse-target={`project-batches-${group.slug}`}
|
|
507
495
|
data-collapse-key={`project:${group.slug}`}
|
|
508
496
|
>
|
|
509
|
-
<svg
|
|
510
|
-
<path d="M3 6l5 5 5-5" />
|
|
511
|
-
</svg>
|
|
497
|
+
<svg aria-hidden="true"><use href="#i-chevron-down" /></svg>
|
|
512
498
|
</button>
|
|
513
499
|
</header>
|
|
514
|
-
<div id={`project-batches-${group.slug}`}
|
|
500
|
+
<div id={`project-batches-${group.slug}`}>
|
|
515
501
|
{group.batches.map((batch) => (
|
|
516
502
|
<BatchView key={batch.id} batch={batch} />
|
|
517
503
|
))}
|
|
@@ -532,7 +518,9 @@ function BatchView({ batch }: { batch: ProjectBatch }) {
|
|
|
532
518
|
<div class="batch-header">
|
|
533
519
|
{/* Select-all toggles only this batch's draft rows. No `name`, so it never submits as
|
|
534
520
|
an entry id; dashboard.js derives checked/indeterminate from the draft checkboxes. */}
|
|
535
|
-
<
|
|
521
|
+
<label class="checkbox-label">
|
|
522
|
+
<input type="checkbox" class="select-all" aria-label={`Select all drafts in batch ${shortUlid(batch.id)}`} />
|
|
523
|
+
</label>
|
|
536
524
|
{singleFiler !== null ? <span class="batch-meta">{singleFiler}</span> : null}
|
|
537
525
|
<button
|
|
538
526
|
type="button"
|
|
@@ -544,9 +532,7 @@ function BatchView({ batch }: { batch: ProjectBatch }) {
|
|
|
544
532
|
data-collapse-target={`batch-table-${batch.id}`}
|
|
545
533
|
data-collapse-key={`batch:${batch.id}`}
|
|
546
534
|
>
|
|
547
|
-
<svg
|
|
548
|
-
<path d="M3 6l5 5 5-5" />
|
|
549
|
-
</svg>
|
|
535
|
+
<svg aria-hidden="true"><use href="#i-chevron-down" /></svg>
|
|
550
536
|
</button>
|
|
551
537
|
</div>
|
|
552
538
|
<div class="table-wrap" id={`batch-table-${batch.id}`}>
|
|
@@ -562,11 +548,11 @@ function BatchView({ batch }: { batch: ProjectBatch }) {
|
|
|
562
548
|
<tr>
|
|
563
549
|
{/* The column header stays in the accessibility tree via the sr-only span; the
|
|
564
550
|
checkbox column itself is too narrow for a visible label. */}
|
|
565
|
-
<th><span class="
|
|
566
|
-
<th>Title</th>
|
|
567
|
-
<th>Status</th>
|
|
568
|
-
<th>File</th>
|
|
569
|
-
<th>Actions</th>
|
|
551
|
+
<th class="cell-select" scope="col"><span class="sr-only">Select</span></th>
|
|
552
|
+
<th scope="col">Title</th>
|
|
553
|
+
<th scope="col">Status</th>
|
|
554
|
+
<th scope="col">File</th>
|
|
555
|
+
<th scope="col">Actions</th>
|
|
570
556
|
</tr>
|
|
571
557
|
</thead>
|
|
572
558
|
<tbody>
|
|
@@ -598,16 +584,18 @@ function EntryRowView({ entry, filerInline }: { entry: EntryRow; filerInline: bo
|
|
|
598
584
|
<tr>
|
|
599
585
|
<td class="cell-select">
|
|
600
586
|
{approve ? (
|
|
601
|
-
<
|
|
587
|
+
<label class="checkbox-label">
|
|
588
|
+
<input type="checkbox" name="id" value={entry.id} aria-label={`Select ${entry.file}`} />
|
|
589
|
+
</label>
|
|
602
590
|
) : null}
|
|
603
591
|
</td>
|
|
604
|
-
<td
|
|
592
|
+
<td title={`${entry.file} — ${entry.anchor_text}`}>
|
|
605
593
|
<div class="entry-head">
|
|
606
594
|
<span class="entry-title">{title}</span>
|
|
607
595
|
{filerInline && entry.filed_by !== null ? <span class="entry-filer"> · {entry.filed_by}</span> : null}
|
|
608
596
|
</div>
|
|
609
597
|
</td>
|
|
610
|
-
<td
|
|
598
|
+
<td>
|
|
611
599
|
<span class={`tag ${STATUS_TAG[entry.status]}`}>{STATUS_LABEL[entry.status]}</span>
|
|
612
600
|
</td>
|
|
613
601
|
<td class="cell-file" title={entry.file}>{entry.file}</td>
|
|
@@ -618,26 +606,19 @@ function EntryRowView({ entry, filerInline }: { entry: EntryRow; filerInline: bo
|
|
|
618
606
|
class="btn btn-icon"
|
|
619
607
|
style="color: var(--success)"
|
|
620
608
|
aria-label={`Approve ${entry.file}`}
|
|
621
|
-
title="Approve"
|
|
622
609
|
hx-post={`/entries/${entry.id}/approve`}
|
|
623
610
|
hx-target="#entries-list"
|
|
624
611
|
hx-swap="innerHTML"
|
|
625
612
|
hx-indicator="#entries-loading"
|
|
626
613
|
hx-disabled-elt="this"
|
|
627
614
|
>
|
|
628
|
-
<svg
|
|
629
|
-
<circle cx="8" cy="8" r="6.5" />
|
|
630
|
-
<path d="M5 8l2 2 4-4" />
|
|
631
|
-
</svg>
|
|
615
|
+
<svg aria-hidden="true"><use href="#i-check" /></svg>
|
|
632
616
|
</button>
|
|
633
617
|
) : (
|
|
634
618
|
// The approve slot stays occupied on every non-draft row: a double-click's second click
|
|
635
619
|
// lands on the disabled button (pointer-events none) rather than the next slot.
|
|
636
|
-
<button type="button" class="btn btn-icon" style="color: var(--success)" aria-label={`Approve ${entry.file}`}
|
|
637
|
-
<svg
|
|
638
|
-
<circle cx="8" cy="8" r="6.5" />
|
|
639
|
-
<path d="M5 8l2 2 4-4" />
|
|
640
|
-
</svg>
|
|
620
|
+
<button type="button" class="btn btn-icon" style="color: var(--success)" aria-label={`Approve ${entry.file}`} disabled>
|
|
621
|
+
<svg aria-hidden="true"><use href="#i-check" /></svg>
|
|
641
622
|
</button>
|
|
642
623
|
)}
|
|
643
624
|
{reject ? (
|
|
@@ -646,31 +627,22 @@ function EntryRowView({ entry, filerInline }: { entry: EntryRow; filerInline: bo
|
|
|
646
627
|
class="btn btn-icon"
|
|
647
628
|
style="color: var(--danger)"
|
|
648
629
|
aria-label={`Reject ${entry.file}`}
|
|
649
|
-
title="Reject"
|
|
650
630
|
hx-post={`/entries/${entry.id}/reject`}
|
|
651
631
|
hx-target="#entries-list"
|
|
652
632
|
hx-swap="innerHTML"
|
|
653
633
|
hx-indicator="#entries-loading"
|
|
654
634
|
hx-disabled-elt="this"
|
|
655
635
|
>
|
|
656
|
-
<svg
|
|
657
|
-
<circle cx="8" cy="8" r="6.5" />
|
|
658
|
-
<path d="M10 6L6 10M6 6l4 4" />
|
|
659
|
-
</svg>
|
|
636
|
+
<svg aria-hidden="true"><use href="#i-x" /></svg>
|
|
660
637
|
</button>
|
|
661
638
|
) : (
|
|
662
639
|
// The reject slot stays occupied on applied and rejected rows for the same guard.
|
|
663
|
-
<button type="button" class="btn btn-icon" style="color: var(--danger)" aria-label={`Reject ${entry.file}`}
|
|
664
|
-
<svg
|
|
665
|
-
<circle cx="8" cy="8" r="6.5" />
|
|
666
|
-
<path d="M10 6L6 10M6 6l4 4" />
|
|
667
|
-
</svg>
|
|
640
|
+
<button type="button" class="btn btn-icon" style="color: var(--danger)" aria-label={`Reject ${entry.file}`} disabled>
|
|
641
|
+
<svg aria-hidden="true"><use href="#i-x" /></svg>
|
|
668
642
|
</button>
|
|
669
643
|
)}
|
|
670
|
-
<a class="btn btn-icon" href={`/entries/${entry.id}`} aria-label={`Edit ${entry.file}`}
|
|
671
|
-
<svg
|
|
672
|
-
<path d="M11.5 2.5l2 2L5 13H3v-2z" />
|
|
673
|
-
</svg>
|
|
644
|
+
<a class="btn btn-icon" href={`/entries/${entry.id}`} aria-label={`Edit ${entry.file}`}>
|
|
645
|
+
<svg aria-hidden="true"><use href="#i-edit" /></svg>
|
|
674
646
|
</a>
|
|
675
647
|
</td>
|
|
676
648
|
</tr>
|
|
@@ -682,11 +654,8 @@ function ErrorBoxTemplate() {
|
|
|
682
654
|
return (
|
|
683
655
|
<template id="error-box">
|
|
684
656
|
<div class="callout callout-danger" role="alert">
|
|
685
|
-
<div class="callout-icon"
|
|
686
|
-
<svg
|
|
687
|
-
<circle cx="8" cy="8" r="6.5" />
|
|
688
|
-
<path d="M10 6L6 10M6 6l4 4" />
|
|
689
|
-
</svg>
|
|
657
|
+
<div class="callout-icon">
|
|
658
|
+
<svg aria-hidden="true"><use href="#i-error" /></svg>
|
|
690
659
|
</div>
|
|
691
660
|
<div class="callout-content">
|
|
692
661
|
<div class="callout-title">The list could not be loaded</div>
|
|
@@ -700,6 +669,46 @@ function ErrorBoxTemplate() {
|
|
|
700
669
|
);
|
|
701
670
|
}
|
|
702
671
|
|
|
672
|
+
/** The saved theme, restored before the stylesheet, or a light-theme visitor sees a dark flash. */
|
|
673
|
+
const THEME_RESTORE = raw(
|
|
674
|
+
'const saved = localStorage.getItem("cloudy-theme"); if (saved) document.documentElement.dataset.theme = saved;',
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
/** The head shared by every full page: the three stylesheets in order, then htmx and the dashboard module. */
|
|
678
|
+
function PageHead({ title }: { title: string }) {
|
|
679
|
+
return (
|
|
680
|
+
<head>
|
|
681
|
+
<meta charset="utf-8" />
|
|
682
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
683
|
+
{/* The empty data: URL kills the browser's /favicon.ico probe, which would 404 under /static. */}
|
|
684
|
+
<link rel="icon" href="data:," />
|
|
685
|
+
<title>{title}</title>
|
|
686
|
+
<script>{THEME_RESTORE}</script>
|
|
687
|
+
<link rel="stylesheet" href="/static/tokens.css" />
|
|
688
|
+
<link rel="stylesheet" href="/static/components.css" />
|
|
689
|
+
<link rel="stylesheet" href="/static/cursor.css" />
|
|
690
|
+
<link rel="stylesheet" href="/static/dashboard.css" />
|
|
691
|
+
<script src="/static/htmx.min.js"></script>
|
|
692
|
+
<script type="module" src="/static/dashboard.js"></script>
|
|
693
|
+
</head>
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** The sprite as the body's first child; cursor.js creates its own canvas and nothing else may enter that slot. */
|
|
698
|
+
function IconSprite() {
|
|
699
|
+
return <>{raw(ICON_SPRITE)}</>;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** The two controller modules, at the end of the body, exactly as the wiring prescribes. */
|
|
703
|
+
function PageModules() {
|
|
704
|
+
return (
|
|
705
|
+
<>
|
|
706
|
+
<script type="module" src="/static/cursor.js"></script>
|
|
707
|
+
<script type="module" src="/static/ui.js"></script>
|
|
708
|
+
</>
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
|
|
703
712
|
/** The full page for a plain navigation; the mount wraps it in the doctype. `signedIn` is whether the navbar offers sign-out. */
|
|
704
713
|
export function dashboardPage(
|
|
705
714
|
store: Store,
|
|
@@ -710,19 +719,9 @@ export function dashboardPage(
|
|
|
710
719
|
const vm = loadList(store, params);
|
|
711
720
|
return (
|
|
712
721
|
<html lang="en" data-theme="dark">
|
|
713
|
-
<
|
|
714
|
-
<meta charset="utf-8" />
|
|
715
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
716
|
-
{/* The empty data: URL kills the browser's /favicon.ico probe, which would 404 under /static. */}
|
|
717
|
-
<link rel="icon" href="data:," />
|
|
718
|
-
<title>Entries · {NAME}</title>
|
|
719
|
-
<link rel="stylesheet" href="/static/tokens.css" />
|
|
720
|
-
<link rel="stylesheet" href="/static/components.css" />
|
|
721
|
-
<link rel="stylesheet" href="/static/dashboard.css" />
|
|
722
|
-
<script src="/static/htmx.min.js"></script>
|
|
723
|
-
<script src="/static/dashboard.js" defer></script>
|
|
724
|
-
</head>
|
|
722
|
+
<PageHead title={`Entries · ${NAME}`} />
|
|
725
723
|
<body>
|
|
724
|
+
<IconSprite />
|
|
726
725
|
<div class="app-shell">
|
|
727
726
|
<Navbar signedIn={signedIn} active="entries" />
|
|
728
727
|
<main class="main">
|
|
@@ -740,6 +739,7 @@ export function dashboardPage(
|
|
|
740
739
|
<ErrorBoxTemplate />
|
|
741
740
|
</main>
|
|
742
741
|
</div>
|
|
742
|
+
<PageModules />
|
|
743
743
|
</body>
|
|
744
744
|
</html>
|
|
745
745
|
);
|
|
@@ -754,26 +754,18 @@ export function dashboardPage(
|
|
|
754
754
|
export function loginPage(next: string | undefined, error: string | undefined = undefined): JSX.Element {
|
|
755
755
|
return (
|
|
756
756
|
<html lang="en" data-theme="dark">
|
|
757
|
-
<
|
|
758
|
-
<meta charset="utf-8" />
|
|
759
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
760
|
-
{/* The empty data: URL kills the browser's /favicon.ico probe, which would 404 under /static. */}
|
|
761
|
-
<link rel="icon" href="data:," />
|
|
762
|
-
<title>Sign in · {NAME}</title>
|
|
763
|
-
<link rel="stylesheet" href="/static/tokens.css" />
|
|
764
|
-
<link rel="stylesheet" href="/static/components.css" />
|
|
765
|
-
<link rel="stylesheet" href="/static/dashboard.css" />
|
|
766
|
-
<script src="/static/htmx.min.js"></script>
|
|
767
|
-
<script src="/static/dashboard.js" defer></script>
|
|
768
|
-
</head>
|
|
757
|
+
<PageHead title={`Sign in · ${NAME}`} />
|
|
769
758
|
<body>
|
|
759
|
+
<IconSprite />
|
|
770
760
|
<div class="app-shell">
|
|
771
761
|
<Navbar signedIn={false} active="none" />
|
|
772
762
|
<main class="main login-main">
|
|
773
763
|
<header class="page-header">
|
|
774
|
-
<div class="
|
|
775
|
-
|
|
776
|
-
|
|
764
|
+
<div class="page-header-body">
|
|
765
|
+
<div class="label page-eyebrow">Dashboard</div>
|
|
766
|
+
<h1 class="page-title">Sign in</h1>
|
|
767
|
+
<p class="page-lede">The dashboard is password protected. Enter the password to continue.</p>
|
|
768
|
+
</div>
|
|
777
769
|
</header>
|
|
778
770
|
{error !== undefined ? <DangerCallout title={error} /> : null}
|
|
779
771
|
<form method="post" action="/login" class="card login-card">
|
|
@@ -796,6 +788,7 @@ export function loginPage(next: string | undefined, error: string | undefined =
|
|
|
796
788
|
</form>
|
|
797
789
|
</main>
|
|
798
790
|
</div>
|
|
791
|
+
<PageModules />
|
|
799
792
|
</body>
|
|
800
793
|
</html>
|
|
801
794
|
);
|
|
@@ -902,11 +895,8 @@ export function formatTransitionRefusal(
|
|
|
902
895
|
function DangerCallout({ title, body }: { title: string; body?: string }) {
|
|
903
896
|
return (
|
|
904
897
|
<div class="callout callout-danger" role="alert">
|
|
905
|
-
<div class="callout-icon"
|
|
906
|
-
<svg
|
|
907
|
-
<circle cx="8" cy="8" r="6.5" />
|
|
908
|
-
<path d="M10 6L6 10M6 6l4 4" />
|
|
909
|
-
</svg>
|
|
898
|
+
<div class="callout-icon">
|
|
899
|
+
<svg aria-hidden="true"><use href="#i-error" /></svg>
|
|
910
900
|
</div>
|
|
911
901
|
<div class="callout-content">
|
|
912
902
|
<div class="callout-title">{title}</div>
|
|
@@ -1017,7 +1007,7 @@ function RevisionHistory({ revisions }: { revisions: readonly RevisionRow[] }) {
|
|
|
1017
1007
|
<time class="revision-time" datetime={new Date(revision.created_at).toISOString()}>
|
|
1018
1008
|
{new Date(revision.created_at).toLocaleString()}
|
|
1019
1009
|
</time>
|
|
1020
|
-
<button class="btn btn-
|
|
1010
|
+
<button class="btn btn-secondary btn-sm" type="button" data-use-revision>
|
|
1021
1011
|
Use
|
|
1022
1012
|
</button>
|
|
1023
1013
|
</div>
|
|
@@ -1157,7 +1147,7 @@ function ImagesCard({ entry }: { entry: EntryRow }) {
|
|
|
1157
1147
|
<ul class="image-list">
|
|
1158
1148
|
{images.map((item, index) =>
|
|
1159
1149
|
isRenderableImage(item) ? (
|
|
1160
|
-
<li
|
|
1150
|
+
<li key={index}>
|
|
1161
1151
|
<img class="entry-image" src={item} alt={`Image ${index + 1}`} loading="lazy" />
|
|
1162
1152
|
<form
|
|
1163
1153
|
class="image-remove"
|
|
@@ -1167,11 +1157,11 @@ function ImagesCard({ entry }: { entry: EntryRow }) {
|
|
|
1167
1157
|
hx-target="#editor-view"
|
|
1168
1158
|
hx-swap="outerHTML"
|
|
1169
1159
|
>
|
|
1170
|
-
<button class="btn btn-
|
|
1160
|
+
<button class="btn btn-secondary btn-sm" type="submit">Remove</button>
|
|
1171
1161
|
</form>
|
|
1172
1162
|
</li>
|
|
1173
1163
|
) : (
|
|
1174
|
-
<li
|
|
1164
|
+
<li key={index}>
|
|
1175
1165
|
<p class="image-skip">Image {index + 1} not shown: unsupported source.</p>
|
|
1176
1166
|
</li>
|
|
1177
1167
|
),
|
|
@@ -1297,7 +1287,7 @@ function DiffView({ entry }: { entry: EntryRow }) {
|
|
|
1297
1287
|
<span class="diff-line diff-anchor">
|
|
1298
1288
|
{diff.before.map((token, index) =>
|
|
1299
1289
|
token.kind === "removed" ? (
|
|
1300
|
-
<span class="diff-
|
|
1290
|
+
<span class="diff-remove" key={index}>{token.text}</span>
|
|
1301
1291
|
) : (
|
|
1302
1292
|
token.text
|
|
1303
1293
|
),
|
|
@@ -1313,7 +1303,7 @@ function DiffView({ entry }: { entry: EntryRow }) {
|
|
|
1313
1303
|
<pre class="diff-code">
|
|
1314
1304
|
{diff.after.map((token, index) =>
|
|
1315
1305
|
token.kind === "added" ? (
|
|
1316
|
-
<span class="diff-
|
|
1306
|
+
<span class="diff-add" key={index}>{token.text}</span>
|
|
1317
1307
|
) : (
|
|
1318
1308
|
token.text
|
|
1319
1309
|
),
|
|
@@ -1362,12 +1352,9 @@ function EditorNav({ prevId, nextId }: { prevId: string | null; nextId: string |
|
|
|
1362
1352
|
/** One rail: a chevron on a full-height strip, or an inert placeholder at the boundary. */
|
|
1363
1353
|
function EditorNavButton({ side, targetId }: { side: "prev" | "next"; targetId: string | null }) {
|
|
1364
1354
|
const label = side === "prev" ? "Previous entry" : "Next entry";
|
|
1365
|
-
const chevron = side === "prev" ? "M10 3L5 8l5 5" : "M6 3l5 5-5 5";
|
|
1366
1355
|
const cls = `editor-nav-btn editor-nav-${side}`;
|
|
1367
1356
|
const icon = (
|
|
1368
|
-
<svg
|
|
1369
|
-
<path d={chevron} />
|
|
1370
|
-
</svg>
|
|
1357
|
+
<svg aria-hidden="true"><use href={side === "prev" ? "#i-chevron-left" : "#i-chevron-right"} /></svg>
|
|
1371
1358
|
);
|
|
1372
1359
|
if (targetId === null) {
|
|
1373
1360
|
return (
|
|
@@ -1377,7 +1364,7 @@ function EditorNavButton({ side, targetId }: { side: "prev" | "next"; targetId:
|
|
|
1377
1364
|
);
|
|
1378
1365
|
}
|
|
1379
1366
|
return (
|
|
1380
|
-
<a class={cls} href={`/entries/${targetId}`} aria-label={label}
|
|
1367
|
+
<a class={cls} href={`/entries/${targetId}`} aria-label={label}>
|
|
1381
1368
|
{icon}
|
|
1382
1369
|
</a>
|
|
1383
1370
|
);
|
|
@@ -1387,34 +1374,27 @@ function EditorNavButton({ side, targetId }: { side: "prev" | "next"; targetId:
|
|
|
1387
1374
|
export function editorPage(vm: EditorViewModel, state: EditorState | undefined = undefined, signedIn = false): JSX.Element {
|
|
1388
1375
|
return (
|
|
1389
1376
|
<html lang="en" data-theme="dark">
|
|
1390
|
-
<
|
|
1391
|
-
<meta charset="utf-8" />
|
|
1392
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1393
|
-
{/* The empty data: URL kills the browser's /favicon.ico probe, which would 404 under /static. */}
|
|
1394
|
-
<link rel="icon" href="data:," />
|
|
1395
|
-
<title>Edit entry · {NAME}</title>
|
|
1396
|
-
<link rel="stylesheet" href="/static/tokens.css" />
|
|
1397
|
-
<link rel="stylesheet" href="/static/components.css" />
|
|
1398
|
-
<link rel="stylesheet" href="/static/dashboard.css" />
|
|
1399
|
-
<script src="/static/htmx.min.js"></script>
|
|
1400
|
-
<script src="/static/dashboard.js" defer></script>
|
|
1401
|
-
</head>
|
|
1377
|
+
<PageHead title={`Edit entry · ${NAME}`} />
|
|
1402
1378
|
<body>
|
|
1379
|
+
<IconSprite />
|
|
1403
1380
|
<div class="app-shell">
|
|
1404
1381
|
<Navbar signedIn={signedIn} active="entries" />
|
|
1405
1382
|
<main class="main">
|
|
1406
1383
|
<header class="page-header">
|
|
1407
|
-
<div class="
|
|
1408
|
-
|
|
1409
|
-
|
|
1384
|
+
<div class="page-header-body">
|
|
1385
|
+
<div class="label page-eyebrow">Entry review</div>
|
|
1386
|
+
<h1 class="page-title">Edit entry</h1>
|
|
1387
|
+
<p class="page-lede">Saving the text approves the entry and releases it to agents.</p>
|
|
1388
|
+
</div>
|
|
1410
1389
|
</header>
|
|
1411
1390
|
<EditorView vm={vm} state={state} />
|
|
1412
1391
|
<EditorNav prevId={vm.prevId} nextId={vm.nextId} />
|
|
1413
1392
|
{/* The one live region: announcements on constraint-state flips only, so a screen reader
|
|
1414
1393
|
is not flooded with a per-keystroke value. Lives outside the swap region. */}
|
|
1415
|
-
<span id="editor-live" class="
|
|
1394
|
+
<span id="editor-live" class="sr-only" aria-live="polite"></span>
|
|
1416
1395
|
</main>
|
|
1417
1396
|
</div>
|
|
1397
|
+
<PageModules />
|
|
1418
1398
|
</body>
|
|
1419
1399
|
</html>
|
|
1420
1400
|
);
|
|
@@ -1434,30 +1414,23 @@ export function editorViewFragment(vm: EditorViewModel, state: EditorState | und
|
|
|
1434
1414
|
export function notFoundPage(signedIn = false): JSX.Element {
|
|
1435
1415
|
return (
|
|
1436
1416
|
<html lang="en" data-theme="dark">
|
|
1437
|
-
<
|
|
1438
|
-
<meta charset="utf-8" />
|
|
1439
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1440
|
-
{/* The empty data: URL kills the browser's /favicon.ico probe, which would 404 under /static. */}
|
|
1441
|
-
<link rel="icon" href="data:," />
|
|
1442
|
-
<title>Entry not found · {NAME}</title>
|
|
1443
|
-
<link rel="stylesheet" href="/static/tokens.css" />
|
|
1444
|
-
<link rel="stylesheet" href="/static/components.css" />
|
|
1445
|
-
<link rel="stylesheet" href="/static/dashboard.css" />
|
|
1446
|
-
<script src="/static/htmx.min.js"></script>
|
|
1447
|
-
<script src="/static/dashboard.js" defer></script>
|
|
1448
|
-
</head>
|
|
1417
|
+
<PageHead title={`Entry not found · ${NAME}`} />
|
|
1449
1418
|
<body>
|
|
1419
|
+
<IconSprite />
|
|
1450
1420
|
<div class="app-shell">
|
|
1451
1421
|
<Navbar signedIn={signedIn} active="entries" />
|
|
1452
1422
|
<main class="main">
|
|
1453
1423
|
<header class="page-header">
|
|
1454
|
-
<div class="
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1424
|
+
<div class="page-header-body">
|
|
1425
|
+
<div class="label page-eyebrow">Entry review</div>
|
|
1426
|
+
<h1 class="page-title">Entry not found</h1>
|
|
1427
|
+
<p class="page-lede">This entry doesn't exist. Return to the list to find it.</p>
|
|
1428
|
+
</div>
|
|
1458
1429
|
</header>
|
|
1430
|
+
<a href="/" class="btn btn-secondary">Back to entries</a>
|
|
1459
1431
|
</main>
|
|
1460
1432
|
</div>
|
|
1433
|
+
<PageModules />
|
|
1461
1434
|
</body>
|
|
1462
1435
|
</html>
|
|
1463
1436
|
);
|
|
@@ -1543,11 +1516,8 @@ function sectionToForm(section: StyleGuideInput): StyleGuideForm {
|
|
|
1543
1516
|
function InfoCallout({ title, body }: { title: string; body?: string }) {
|
|
1544
1517
|
return (
|
|
1545
1518
|
<div class="callout callout-info">
|
|
1546
|
-
<div class="callout-icon"
|
|
1547
|
-
<svg
|
|
1548
|
-
<circle cx="8" cy="8" r="6.5" />
|
|
1549
|
-
<path d="M8 7.5v3.5M8 5.5v.01" />
|
|
1550
|
-
</svg>
|
|
1519
|
+
<div class="callout-icon">
|
|
1520
|
+
<svg aria-hidden="true"><use href="#i-info" /></svg>
|
|
1551
1521
|
</div>
|
|
1552
1522
|
<div class="callout-content">
|
|
1553
1523
|
<div class="callout-title">{title}</div>
|
|
@@ -1607,11 +1577,8 @@ function StyleGuideRegion({ vm, state }: { vm: StyleGuideViewModel; state: Style
|
|
|
1607
1577
|
<div id="style-guide-region">
|
|
1608
1578
|
{state?.notice.kind === "saved" ? (
|
|
1609
1579
|
<div class="callout callout-success" role="status">
|
|
1610
|
-
<div class="callout-icon"
|
|
1611
|
-
<svg
|
|
1612
|
-
<circle cx="8" cy="8" r="6.5" />
|
|
1613
|
-
<path d="M5 8l2 2 4-4" />
|
|
1614
|
-
</svg>
|
|
1580
|
+
<div class="callout-icon">
|
|
1581
|
+
<svg aria-hidden="true"><use href="#i-success" /></svg>
|
|
1615
1582
|
</div>
|
|
1616
1583
|
<div class="callout-content">
|
|
1617
1584
|
<div class="callout-title">Guide saved</div>
|
|
@@ -1686,11 +1653,8 @@ function StyleGuideErrorBoxTemplate() {
|
|
|
1686
1653
|
return (
|
|
1687
1654
|
<template id="style-guide-error-box">
|
|
1688
1655
|
<div class="callout callout-danger" role="alert">
|
|
1689
|
-
<div class="callout-icon"
|
|
1690
|
-
<svg
|
|
1691
|
-
<circle cx="8" cy="8" r="6.5" />
|
|
1692
|
-
<path d="M10 6L6 10M6 6l4 4" />
|
|
1693
|
-
</svg>
|
|
1656
|
+
<div class="callout-icon">
|
|
1657
|
+
<svg aria-hidden="true"><use href="#i-error" /></svg>
|
|
1694
1658
|
</div>
|
|
1695
1659
|
<div class="callout-content">
|
|
1696
1660
|
<div class="callout-title">The guide could not be saved</div>
|
|
@@ -1712,32 +1676,25 @@ export function styleGuidePage(
|
|
|
1712
1676
|
): JSX.Element {
|
|
1713
1677
|
return (
|
|
1714
1678
|
<html lang="en" data-theme="dark">
|
|
1715
|
-
<
|
|
1716
|
-
<meta charset="utf-8" />
|
|
1717
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1718
|
-
{/* The empty data: URL kills the browser's /favicon.ico probe, which would 404 under /static. */}
|
|
1719
|
-
<link rel="icon" href="data:," />
|
|
1720
|
-
<title>Style guide · {NAME}</title>
|
|
1721
|
-
<link rel="stylesheet" href="/static/tokens.css" />
|
|
1722
|
-
<link rel="stylesheet" href="/static/components.css" />
|
|
1723
|
-
<link rel="stylesheet" href="/static/dashboard.css" />
|
|
1724
|
-
<script src="/static/htmx.min.js"></script>
|
|
1725
|
-
<script src="/static/dashboard.js" defer></script>
|
|
1726
|
-
</head>
|
|
1679
|
+
<PageHead title={`Style guide · ${NAME}`} />
|
|
1727
1680
|
<body>
|
|
1681
|
+
<IconSprite />
|
|
1728
1682
|
<div class="app-shell">
|
|
1729
1683
|
<Navbar signedIn={signedIn} active="style-guide" />
|
|
1730
1684
|
<main class="main">
|
|
1731
1685
|
<header class="page-header">
|
|
1732
|
-
<div class="
|
|
1733
|
-
|
|
1734
|
-
|
|
1686
|
+
<div class="page-header-body">
|
|
1687
|
+
<div class="label page-eyebrow">Voice</div>
|
|
1688
|
+
<h1 class="page-title">Style guide</h1>
|
|
1689
|
+
<p class="page-lede">The voice agents read before drafting, starting with the global guide and adding each project's own rules below.</p>
|
|
1690
|
+
</div>
|
|
1735
1691
|
</header>
|
|
1736
1692
|
<StyleGuideProjects vm={vm} />
|
|
1737
1693
|
<StyleGuideRegion vm={vm} state={state} />
|
|
1738
1694
|
<StyleGuideErrorBoxTemplate />
|
|
1739
1695
|
</main>
|
|
1740
1696
|
</div>
|
|
1697
|
+
<PageModules />
|
|
1741
1698
|
</body>
|
|
1742
1699
|
</html>
|
|
1743
1700
|
);
|