pentimento 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/LICENSE +21 -0
- package/README.md +93 -0
- package/assets/chrome.js +16 -0
- package/assets/theme.css +286 -0
- package/assets/viewer.js +105 -0
- package/dist/cli.js +289 -0
- package/dist/core.js +223 -0
- package/dist/render.js +433 -0
- package/dist/semdiff.js +125 -0
- package/dist/verify.js +120 -0
- package/dist/viewer.js +239 -0
- package/package.json +52 -0
- package/skill/SKILL.md +68 -0
- package/skill/references/archetypes.md +81 -0
- package/skill/references/directives.md +107 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lucas Traba
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Pentimento
|
|
2
|
+
|
|
3
|
+
Living documents for humans and AI agents: one canonical markdown file, revision history in a hidden folder beside it, and a deterministic HTML render.
|
|
4
|
+
|
|
5
|
+
A pentimento is a trace of earlier brushwork visible under the surface of a painting.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Two problems led here.
|
|
10
|
+
|
|
11
|
+
Plans that get revised stop being reviewable. By the third revision nobody remembers what changed or why, so the reader either re-reads the whole document or asks the author.
|
|
12
|
+
|
|
13
|
+
And agents asked to produce HTML directly produce visual noise. Every page comes out different, none of them look owned, and the iterations can't be diffed.
|
|
14
|
+
|
|
15
|
+
Pentimento treats both as the same problem. The author, human or agent, writes markdown plus a small set of directives. A renderer with one fixed stylesheet decides how everything looks. Meaningful versions are snapshotted into numbered revisions with a note about what changed and why, and each render embeds a word-level diff against the previous revision, so the reader sees exactly what moved.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g pentimento
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Node 20.13+ (the viewer's recursive file watch needs it). Rendered pages make no external requests.
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pentimento snapshot Plan.md --summary "Initial draft"
|
|
29
|
+
pentimento render Plan.md # → plan.html, open it anywhere
|
|
30
|
+
# ...revise Plan.md...
|
|
31
|
+
pentimento snapshot Plan.md --summary "Tightened scope" --why "Feedback round 1"
|
|
32
|
+
pentimento render Plan.md # same page, now with a "What changed in r002" panel
|
|
33
|
+
pentimento diff Plan.md # terminal diff of the latest two revisions
|
|
34
|
+
pentimento serve . # local viewer: revision picker, diffs, comments
|
|
35
|
+
pentimento verify . # consistency check, exits nonzero for CI
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`--author` defaults to your git `user.name`.
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
Your file, say `Plan.md`, keeps its name and location. `pentimento snapshot` copies the current state to `.history/Plan/r001.md`, `r002.md` and so on, and appends an entry (summary, why, author, timestamp) to an append-only `.history/Plan/meta.yml`. Everything is a plain text file. You can grep the history, sync it, commit it, or hand it to an agent. Nothing about it requires git, and nothing about it conflicts with git.
|
|
43
|
+
|
|
44
|
+
`pentimento render` produces a single self-contained HTML page: table of contents, the revision log as an evolution strip, an archetype badge, light and dark themes, three switchable palettes. The "What changed" panel appears from the second revision on, with unchanged sections collapsed. `pentimento diff <doc> rA rB --html` renders the same word-level view for any two revisions.
|
|
45
|
+
|
|
46
|
+
`pentimento verify <dir>` cross-checks every document's frontmatter against its history files and meta entries, and fails loudly when they drift.
|
|
47
|
+
|
|
48
|
+
## Comments and the review loop
|
|
49
|
+
|
|
50
|
+
Run `pentimento serve .` and open a document. Select any text and a comment button appears; the comment is saved to `meta.yml` with the quoted text and surrounding context, so it stays attached even after the section moves. Open comments render as a panel on the page with the quotes highlighted in place.
|
|
51
|
+
|
|
52
|
+
For the other side of the loop:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pentimento comments Plan.md # list comments
|
|
56
|
+
pentimento address Plan.md # print open comments, formatted for an agent to act on
|
|
57
|
+
pentimento resolve Plan.md c-2026-07-08-001 --rev r003 # close a comment against the revision that fixed it
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
You can also comment without the viewer (`pentimento comment Plan.md --text "..." --quote "..."`) or leave inline `%% @c: a note %%` markers in the markdown, which snapshot extracts into `meta.yml`.
|
|
61
|
+
|
|
62
|
+
## Writing documents
|
|
63
|
+
|
|
64
|
+
A Pentimento document is markdown with two optional frontmatter keys:
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
---
|
|
68
|
+
Archetype: implementation # implementation | brainstorm | audit | design-doc
|
|
69
|
+
Palette: iris # iris | verdigris | mist
|
|
70
|
+
---
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Rich elements come from `:::` directives: callouts, verdict banners, severity-graded findings, phase timelines, side-by-side diffs, constrained SVG figures. The full vocabulary is in [`skill/references/directives.md`](skill/references/directives.md), and [`skill/references/archetypes.md`](skill/references/archetypes.md) has a section skeleton per document type.
|
|
74
|
+
|
|
75
|
+
The rules that keep output consistent: no custom CSS, no inline styles, no scripts, no hand-written HTML. If a document needs something the vocabulary can't say, the vocabulary grows in a tool release, not in a document.
|
|
76
|
+
|
|
77
|
+
## For writers
|
|
78
|
+
|
|
79
|
+
The same mechanics work for anything you revise seriously: song lyrics, essays, long-lived notes. The file stays where your other files are (Obsidian vaults work well, and `.history/` stays hidden there), every draft you cared enough to name is preserved, and `meta.yml` remembers why you changed the chorus.
|
|
80
|
+
|
|
81
|
+
## For agents
|
|
82
|
+
|
|
83
|
+
Point your agent at [`skill/SKILL.md`](skill/SKILL.md); for Claude Code, drop it in `.claude/skills/`. The contract: write markdown against an archetype skeleton, snapshot with a reader-facing summary, render, publish. On feedback, revise and snapshot again; the render shows the reviewer what moved. Comments left in the viewer come back through `pentimento address`.
|
|
84
|
+
|
|
85
|
+
## Design lineage
|
|
86
|
+
|
|
87
|
+
The markdown-source, HTML-view split is a response to Thariq Shihipar's *Unreasonable Effectiveness of HTML* and its critics. The anchored-comment loop follows Google Antigravity's artifact comments. The `:::` directives are Pandoc/Quarto-style fenced divs on purpose. Pentimento's own plan is maintained with Pentimento; see [`PLAN.md`](PLAN.md) and its `.history/`.
|
|
88
|
+
|
|
89
|
+
## Status
|
|
90
|
+
|
|
91
|
+
v0.3. Young project, released tooling. Known limits: directives don't nest, Windows is untested, and while the renderer strips active content from figures, rendering hostile markdown is not a supported use case. Documents created under this tool's earlier name (`Vellum: true` frontmatter) are read as-is and migrated on their next snapshot.
|
|
92
|
+
|
|
93
|
+
MIT © Lucas Traba
|
package/assets/chrome.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const root = document.documentElement
|
|
3
|
+
const btns = [...document.querySelectorAll('.pbtn')]
|
|
4
|
+
const sync = () => {
|
|
5
|
+
const p = root.dataset.palette || 'verdigris'
|
|
6
|
+
btns.forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.p === p)))
|
|
7
|
+
}
|
|
8
|
+
const apply = (p) => {
|
|
9
|
+
if (p === 'verdigris') delete root.dataset.palette
|
|
10
|
+
else root.dataset.palette = p
|
|
11
|
+
try { localStorage.setItem('pentimento-palette', p) } catch (e) {}
|
|
12
|
+
sync()
|
|
13
|
+
}
|
|
14
|
+
sync()
|
|
15
|
+
btns.forEach((b) => b.addEventListener('click', () => apply(b.dataset.p)))
|
|
16
|
+
})()
|
package/assets/theme.css
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/* pentimento.css — the entire visual surface of a rendered plan.
|
|
2
|
+
Agents never write CSS; they compose markdown + directives against this file. */
|
|
3
|
+
|
|
4
|
+
:root {
|
|
5
|
+
--bg: #F7F8F6; --raise: #FFFFFF; --ink: #1E2529; --soft: #5A6670;
|
|
6
|
+
--line: #DDE3E0; --accent: #2E7D6E; --accent-deep: #1F5F54;
|
|
7
|
+
--wash: rgba(46,125,110,.08); --code-bg: #EEF1EF;
|
|
8
|
+
--crit: #B3382E; --high: #A66B12; --med: #5A6670;
|
|
9
|
+
--add-bg: #E7F2EB; --add-ink: #22593B; --del-bg: #F8E9E7; --del-ink: #7A2C24;
|
|
10
|
+
--impl: #3B6EA5; --brain: #7A5EA8; --audit: #A66B12; --design: #2E7D6E;
|
|
11
|
+
--serif: 'Charter','Bitstream Charter','Iowan Old Style',Georgia,serif;
|
|
12
|
+
--sans: 'Seravek','Segoe UI',Ubuntu,'Helvetica Neue',Arial,sans-serif;
|
|
13
|
+
--mono: ui-monospace,'Cascadia Code','JetBrains Mono',Menlo,Consolas,monospace;
|
|
14
|
+
}
|
|
15
|
+
@media (prefers-color-scheme: dark) {
|
|
16
|
+
:root {
|
|
17
|
+
--bg: #131719; --raise: #1B2124; --ink: #E6E4DC; --soft: #98A29E;
|
|
18
|
+
--line: #2A3236; --accent: #4FB3A0; --accent-deep: #6BC7B5;
|
|
19
|
+
--wash: rgba(79,179,160,.10); --code-bg: #1F2629;
|
|
20
|
+
--crit: #E0685E; --high: #D9A24A; --med: #98A29E;
|
|
21
|
+
--add-bg: #1C2E24; --add-ink: #8FCFA9; --del-bg: #33211F; --del-ink: #E39C93;
|
|
22
|
+
--impl: #7AA5D6; --brain: #A98FD6; --audit: #D9A24A; --design: #4FB3A0;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
:root[data-theme="light"] {
|
|
26
|
+
--bg: #F7F8F6; --raise: #FFFFFF; --ink: #1E2529; --soft: #5A6670;
|
|
27
|
+
--line: #DDE3E0; --accent: #2E7D6E; --accent-deep: #1F5F54;
|
|
28
|
+
--wash: rgba(46,125,110,.08); --code-bg: #EEF1EF;
|
|
29
|
+
--crit: #B3382E; --high: #A66B12; --med: #5A6670;
|
|
30
|
+
--add-bg: #E7F2EB; --add-ink: #22593B; --del-bg: #F8E9E7; --del-ink: #7A2C24;
|
|
31
|
+
--impl: #3B6EA5; --brain: #7A5EA8; --audit: #A66B12; --design: #2E7D6E;
|
|
32
|
+
}
|
|
33
|
+
:root[data-theme="dark"] {
|
|
34
|
+
--bg: #131719; --raise: #1B2124; --ink: #E6E4DC; --soft: #98A29E;
|
|
35
|
+
--line: #2A3236; --accent: #4FB3A0; --accent-deep: #6BC7B5;
|
|
36
|
+
--wash: rgba(79,179,160,.10); --code-bg: #1F2629;
|
|
37
|
+
--crit: #E0685E; --high: #D9A24A; --med: #98A29E;
|
|
38
|
+
--add-bg: #1C2E24; --add-ink: #8FCFA9; --del-bg: #33211F; --del-ink: #E39C93;
|
|
39
|
+
--impl: #7AA5D6; --brain: #A98FD6; --audit: #D9A24A; --design: #4FB3A0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
body { background: var(--bg); color: var(--ink); font: 16px/1.65 var(--sans); margin: 0; }
|
|
43
|
+
.wrap { max-width: 74ch; margin: 0 auto; padding: 2.5rem 1.25rem 5rem; }
|
|
44
|
+
main section { margin-top: 3rem; }
|
|
45
|
+
|
|
46
|
+
h1, h2, h3 { font-family: var(--serif); line-height: 1.2; text-wrap: balance; margin: 0; }
|
|
47
|
+
h1 { font-size: clamp(1.9rem, 5vw, 2.6rem); font-weight: 600; letter-spacing: -.01em; }
|
|
48
|
+
h2 { font-size: 1.45rem; font-weight: 600; margin-bottom: 1rem; position: relative; }
|
|
49
|
+
h3 { font-size: 1.1rem; font-weight: 600; margin: 1.75rem 0 .5rem; }
|
|
50
|
+
p { margin: .75rem 0; max-width: 68ch; }
|
|
51
|
+
a { color: var(--accent-deep); }
|
|
52
|
+
code { font-family: var(--mono); font-size: .86em; background: var(--code-bg); padding: .1em .35em; border-radius: 3px; }
|
|
53
|
+
pre.block { background: var(--code-bg); border: 1px solid var(--line); border-radius: 6px;
|
|
54
|
+
padding: .8rem 1rem; overflow-x: auto; font: .82rem/1.55 var(--mono); }
|
|
55
|
+
pre.block code { background: none; padding: 0; }
|
|
56
|
+
ul, ol.plain { padding-left: 1.2rem; margin: .75rem 0; }
|
|
57
|
+
li { margin: .35rem 0; max-width: 66ch; }
|
|
58
|
+
li::marker { color: var(--accent); }
|
|
59
|
+
hr { border: none; border-top: 1px solid var(--line); margin: 2rem 0; }
|
|
60
|
+
strong { font-weight: 650; }
|
|
61
|
+
|
|
62
|
+
.eyebrow { font-family: var(--sans); font-size: .72rem; font-weight: 600; letter-spacing: .14em;
|
|
63
|
+
text-transform: uppercase; color: var(--accent-deep); display: block; margin-bottom: .5rem; }
|
|
64
|
+
|
|
65
|
+
/* header ribbon */
|
|
66
|
+
header.doc { border-bottom: 1px solid var(--line); padding-bottom: 1.5rem; }
|
|
67
|
+
.meta-row { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-bottom: 1rem; }
|
|
68
|
+
.badge { font-size: .72rem; font-weight: 600; letter-spacing: .1em; text-transform: uppercase;
|
|
69
|
+
padding: .2rem .6rem; border-radius: 3px; color: var(--bg); background: var(--design); }
|
|
70
|
+
.badge-implementation { background: var(--impl); }
|
|
71
|
+
.badge-brainstorm { background: var(--brain); }
|
|
72
|
+
.badge-audit { background: var(--audit); }
|
|
73
|
+
.badge-design-doc { background: var(--design); }
|
|
74
|
+
.chip { font-family: var(--mono); font-size: .75rem; padding: .2rem .55rem; border: 1px solid var(--line);
|
|
75
|
+
border-radius: 999px; color: var(--soft); background: var(--raise); }
|
|
76
|
+
.standfirst { font-family: var(--serif); font-size: 1.15rem; font-style: italic; color: var(--soft);
|
|
77
|
+
margin-top: .75rem; max-width: 58ch; }
|
|
78
|
+
|
|
79
|
+
/* evolution strip */
|
|
80
|
+
.evolution { display: flex; gap: .9rem; align-items: baseline; background: var(--wash);
|
|
81
|
+
border: 1px solid var(--line); border-radius: 6px; padding: .8rem 1rem; margin-top: 1.5rem; }
|
|
82
|
+
.evolution + .evolution { margin-top: .5rem; }
|
|
83
|
+
.evolution .rev { font-family: var(--mono); font-size: .8rem; font-weight: 700; color: var(--accent-deep); flex: none; }
|
|
84
|
+
.evolution .what { font-size: .88rem; color: var(--soft); }
|
|
85
|
+
.evolution .what strong { color: var(--ink); font-weight: 600; }
|
|
86
|
+
|
|
87
|
+
/* block anchors — the comment substrate */
|
|
88
|
+
[id] > .anch { opacity: 0; margin-left: .4rem; font-family: var(--mono); font-size: .7em; font-weight: 600;
|
|
89
|
+
text-decoration: none; color: var(--accent); vertical-align: .12em; }
|
|
90
|
+
h2:hover .anch, h3:hover .anch, .callout:hover .anch { opacity: 1; }
|
|
91
|
+
h2:focus-within .anch, h3:focus-within .anch { opacity: 1; }
|
|
92
|
+
.anch:focus { opacity: 1; outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
|
|
93
|
+
|
|
94
|
+
/* callouts */
|
|
95
|
+
.callout { border: 1px solid var(--line); border-radius: 6px; padding: .8rem 1rem; margin: 1rem 0; background: var(--raise); }
|
|
96
|
+
.callout .label { font-size: .7rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; display: block; margin-bottom: .3rem; }
|
|
97
|
+
.callout.decision { background: var(--wash); }
|
|
98
|
+
.callout.decision .label, .callout.info .label { color: var(--accent-deep); }
|
|
99
|
+
.callout.risk .label, .callout.warn .label { color: var(--crit); }
|
|
100
|
+
.callout p { margin: .25rem 0; font-size: .95rem; }
|
|
101
|
+
|
|
102
|
+
/* tables */
|
|
103
|
+
.tablewrap { overflow-x: auto; margin: 1rem 0; }
|
|
104
|
+
table { border-collapse: collapse; width: 100%; font-size: .92rem; background: var(--raise);
|
|
105
|
+
border: 1px solid var(--line); border-radius: 6px; }
|
|
106
|
+
th { font-size: .72rem; letter-spacing: .1em; text-transform: uppercase; color: var(--soft);
|
|
107
|
+
text-align: left; padding: .6rem .8rem; border-bottom: 1px solid var(--line); }
|
|
108
|
+
td { padding: .6rem .8rem; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
109
|
+
tr:last-child td { border-bottom: none; }
|
|
110
|
+
.dot { display: inline-block; width: .65em; height: .65em; border-radius: 50%; margin-right: .45em; vertical-align: baseline; }
|
|
111
|
+
.dot-impl { background: var(--impl); } .dot-brain { background: var(--brain); }
|
|
112
|
+
.dot-audit { background: var(--audit); } .dot-design { background: var(--design); }
|
|
113
|
+
.dot-verdigris { background: #2E7D6E; } .dot-mist { background: #0070F3; } .dot-iris { background: #7C5CBF; }
|
|
114
|
+
|
|
115
|
+
/* figures / diagrams */
|
|
116
|
+
.diagram { background: var(--raise); border: 1px solid var(--line); border-radius: 6px;
|
|
117
|
+
padding: 1rem; margin: 1rem 0; overflow-x: auto; }
|
|
118
|
+
.diagram svg { display: block; min-width: 560px; width: 100%; height: auto; }
|
|
119
|
+
.diagram text { font-family: var(--mono); font-size: 12px; fill: var(--ink); }
|
|
120
|
+
.diagram .nodebox { fill: var(--bg); stroke: var(--line); }
|
|
121
|
+
.diagram .accentbox { fill: var(--wash); stroke: var(--accent); }
|
|
122
|
+
.diagram .flow { stroke: var(--soft); fill: none; marker-end: url(#arr); }
|
|
123
|
+
.diagram .lbl { fill: var(--soft); font-size: 10.5px; }
|
|
124
|
+
|
|
125
|
+
/* diff */
|
|
126
|
+
.diff { border: 1px solid var(--line); border-radius: 6px; overflow: hidden; margin: 1rem 0; background: var(--raise); }
|
|
127
|
+
.diff-head { font-family: var(--mono); font-size: .78rem; color: var(--soft); padding: .5rem .9rem;
|
|
128
|
+
border-bottom: 1px solid var(--line); background: var(--code-bg); }
|
|
129
|
+
.diff-cols { display: grid; grid-template-columns: 1fr 1fr; }
|
|
130
|
+
.diff-pane { min-width: 0; overflow-x: auto; }
|
|
131
|
+
.diff-pane + .diff-pane { border-left: 1px solid var(--line); }
|
|
132
|
+
.diff-pane .pane-label { font-size: .68rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
|
|
133
|
+
color: var(--soft); padding: .45rem .9rem .1rem; }
|
|
134
|
+
.diff pre { margin: 0; padding: .5rem 0 .8rem; font: .78rem/1.55 var(--mono); }
|
|
135
|
+
.diff pre span { display: block; padding: 0 .9rem; white-space: pre; }
|
|
136
|
+
.diff .add { background: var(--add-bg); color: var(--add-ink); }
|
|
137
|
+
.diff .del { background: var(--del-bg); color: var(--del-ink); }
|
|
138
|
+
@media (max-width: 700px) {
|
|
139
|
+
.diff-cols { grid-template-columns: 1fr; }
|
|
140
|
+
.diff-pane + .diff-pane { border-left: none; border-top: 1px solid var(--line); }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* timeline */
|
|
144
|
+
.timeline { list-style: none; padding: 0; margin: 1rem 0; }
|
|
145
|
+
.timeline li { display: grid; grid-template-columns: 2.2rem 1fr; gap: .9rem; padding: .9rem 0;
|
|
146
|
+
border-bottom: 1px solid var(--line); margin: 0; max-width: none; }
|
|
147
|
+
.timeline li:last-child { border-bottom: none; }
|
|
148
|
+
.ph { font-family: var(--serif); font-size: 1.3rem; font-weight: 600; color: var(--accent-deep);
|
|
149
|
+
font-variant-numeric: tabular-nums; }
|
|
150
|
+
.timeline h3 { margin: 0 0 .25rem; display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
|
|
151
|
+
.timeline p { font-size: .93rem; color: var(--soft); margin: 0; }
|
|
152
|
+
.pill { font-size: .68rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
|
|
153
|
+
padding: .12rem .5rem; border-radius: 999px; border: 1px solid var(--accent); color: var(--accent-deep); }
|
|
154
|
+
.pill.later { border-color: var(--line); color: var(--soft); }
|
|
155
|
+
.pill.done { border-color: var(--accent); color: var(--bg); background: var(--accent); }
|
|
156
|
+
|
|
157
|
+
/* verdict + findings */
|
|
158
|
+
.verdict { display: grid; grid-template-columns: repeat(3, 1fr); gap: .6rem; margin: 1rem 0; }
|
|
159
|
+
.verdict > div { background: var(--raise); border: 1px solid var(--line); border-radius: 6px; padding: .7rem .9rem; }
|
|
160
|
+
.verdict .q { font-size: .7rem; letter-spacing: .1em; text-transform: uppercase; color: var(--soft); display: block; }
|
|
161
|
+
.verdict .a { font-family: var(--serif); font-size: 1.05rem; font-weight: 600; }
|
|
162
|
+
@media (max-width: 700px) { .verdict { grid-template-columns: 1fr; } }
|
|
163
|
+
.finding { display: grid; grid-template-columns: 3.4rem 1fr; gap: .8rem; padding: .55rem 0;
|
|
164
|
+
border-bottom: 1px solid var(--line); font-size: .93rem; }
|
|
165
|
+
.finding:last-child { border-bottom: none; }
|
|
166
|
+
.finding > div { max-width: none; }
|
|
167
|
+
.sev { font-family: var(--mono); font-size: .7rem; font-weight: 700; letter-spacing: .05em; padding-top: .15rem; }
|
|
168
|
+
.sev.c { color: var(--crit); } .sev.h { color: var(--high); } .sev.m { color: var(--med); }
|
|
169
|
+
details { margin: .75rem 0; }
|
|
170
|
+
summary { cursor: pointer; font-size: .85rem; color: var(--accent-deep); }
|
|
171
|
+
summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
172
|
+
|
|
173
|
+
/* revision diff (what changed) */
|
|
174
|
+
details.changes { margin: .75rem 0 0; }
|
|
175
|
+
.rdiff { border: 1px solid var(--line); border-radius: 6px; background: var(--raise); padding: .4rem 1rem .7rem; margin-top: .5rem; }
|
|
176
|
+
.rdiff-block { font-size: .9rem; line-height: 1.55; margin: .65rem 0; white-space: pre-wrap; overflow-wrap: break-word; }
|
|
177
|
+
.rdiff-block.codey { font-family: var(--mono); font-size: .76rem; overflow-x: auto; }
|
|
178
|
+
.rdiff ins { background: var(--add-bg); color: var(--add-ink); text-decoration: none; border-radius: 2px; padding: 0 .08em; }
|
|
179
|
+
.rdiff del { background: var(--del-bg); color: var(--del-ink); border-radius: 2px; padding: 0 .08em; }
|
|
180
|
+
.rdiff-skip { color: var(--soft); font-size: .76rem; letter-spacing: .04em; margin: .55rem 0; }
|
|
181
|
+
.rdiff-ctx { color: var(--soft); font-weight: 600; font-size: .82rem; margin: .9rem 0 .15rem; }
|
|
182
|
+
|
|
183
|
+
/* footer */
|
|
184
|
+
footer.doc { margin-top: 4rem; padding-top: 1.25rem; border-top: 1px solid var(--line);
|
|
185
|
+
font-size: .8rem; color: var(--soft); }
|
|
186
|
+
footer.doc code { background: none; padding: 0; }
|
|
187
|
+
|
|
188
|
+
/* table of contents */
|
|
189
|
+
.toc { background: var(--raise); border: 1px solid var(--line); border-radius: 6px;
|
|
190
|
+
padding: .6rem 1rem; margin: 1.5rem 0 0; }
|
|
191
|
+
.toc summary { font-size: .72rem; font-weight: 600; letter-spacing: .14em; text-transform: uppercase;
|
|
192
|
+
color: var(--accent-deep); cursor: pointer; }
|
|
193
|
+
.toc summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
194
|
+
.toc ol { list-style: none; padding: .4rem 0 .3rem; margin: 0; columns: 2; column-gap: 2rem; }
|
|
195
|
+
.toc li { margin: .3rem 0; break-inside: avoid; max-width: none; }
|
|
196
|
+
.toc a { text-decoration: none; font-size: .92rem; }
|
|
197
|
+
.toc a:hover { text-decoration: underline; }
|
|
198
|
+
.toc .n { font-family: var(--mono); font-size: .75em; color: var(--soft); margin-right: .5em;
|
|
199
|
+
font-variant-numeric: tabular-nums; }
|
|
200
|
+
@media (max-width: 700px) { .toc ol { columns: 1; } }
|
|
201
|
+
|
|
202
|
+
/* palette switcher — fixed chrome, not agent-writable */
|
|
203
|
+
.palettes { display: flex; gap: .4rem; margin-left: auto; }
|
|
204
|
+
.pbtn { font: 600 .75rem var(--sans); color: var(--soft); background: var(--raise);
|
|
205
|
+
border: 1px solid var(--line); border-radius: 999px; padding: .2rem .6rem; cursor: pointer;
|
|
206
|
+
display: inline-flex; align-items: center; gap: .35em; }
|
|
207
|
+
.pbtn .dot { margin: 0; }
|
|
208
|
+
.pbtn[aria-pressed="true"] { border-color: var(--accent); color: var(--ink); }
|
|
209
|
+
.pbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
210
|
+
|
|
211
|
+
/* palettes: Mist (apple/vercel) and Iris (violet); Verdigris is the default token set above */
|
|
212
|
+
:root[data-palette="mist"] { --bg:#FAFAFA; --raise:#FFFFFF; --ink:#171717; --soft:#666666; --line:#E4E4E7;
|
|
213
|
+
--accent:#0070F3; --accent-deep:#0761D1; --wash:rgba(0,112,243,.06); --code-bg:#F2F2F2; --design:#0070F3; }
|
|
214
|
+
@media (prefers-color-scheme: dark) {
|
|
215
|
+
:root[data-palette="mist"] { --bg:#0A0A0A; --raise:#151515; --ink:#EDEDED; --soft:#8A8A8A; --line:#262626;
|
|
216
|
+
--accent:#3291FF; --accent-deep:#52A8FF; --wash:rgba(50,145,255,.10); --code-bg:#1A1A1A; --design:#3291FF; }
|
|
217
|
+
}
|
|
218
|
+
:root[data-theme="light"][data-palette="mist"] { --bg:#FAFAFA; --raise:#FFFFFF; --ink:#171717; --soft:#666666;
|
|
219
|
+
--line:#E4E4E7; --accent:#0070F3; --accent-deep:#0761D1; --wash:rgba(0,112,243,.06); --code-bg:#F2F2F2; --design:#0070F3; }
|
|
220
|
+
:root[data-theme="dark"][data-palette="mist"] { --bg:#0A0A0A; --raise:#151515; --ink:#EDEDED; --soft:#8A8A8A;
|
|
221
|
+
--line:#262626; --accent:#3291FF; --accent-deep:#52A8FF; --wash:rgba(50,145,255,.10); --code-bg:#1A1A1A; --design:#3291FF; }
|
|
222
|
+
[data-palette="mist"] h1, [data-palette="mist"] h2, [data-palette="mist"] h3, [data-palette="mist"] .ph {
|
|
223
|
+
font-family: var(--sans); letter-spacing: -.015em; }
|
|
224
|
+
[data-palette="mist"] .standfirst { font-family: var(--sans); font-style: normal; font-size: 1.02rem; }
|
|
225
|
+
|
|
226
|
+
:root[data-palette="iris"] { --bg:#F8F7FB; --raise:#FFFFFF; --ink:#241E33; --soft:#6A6284; --line:#E3DEEF;
|
|
227
|
+
--accent:#7C5CBF; --accent-deep:#5E44A0; --wash:rgba(124,92,191,.08); --code-bg:#EFEDF6; --design:#7C5CBF; }
|
|
228
|
+
@media (prefers-color-scheme: dark) {
|
|
229
|
+
:root[data-palette="iris"] { --bg:#151020; --raise:#1E1830; --ink:#EAE6F4; --soft:#A29AC0; --line:#332A4D;
|
|
230
|
+
--accent:#A78BE0; --accent-deep:#BCA8EA; --wash:rgba(167,139,224,.12); --code-bg:#241D38; --design:#A78BE0; }
|
|
231
|
+
}
|
|
232
|
+
:root[data-theme="light"][data-palette="iris"] { --bg:#F8F7FB; --raise:#FFFFFF; --ink:#241E33; --soft:#6A6284;
|
|
233
|
+
--line:#E3DEEF; --accent:#7C5CBF; --accent-deep:#5E44A0; --wash:rgba(124,92,191,.08); --code-bg:#EFEDF6; --design:#7C5CBF; }
|
|
234
|
+
:root[data-theme="dark"][data-palette="iris"] { --bg:#151020; --raise:#1E1830; --ink:#EAE6F4; --soft:#A29AC0;
|
|
235
|
+
--line:#332A4D; --accent:#A78BE0; --accent-deep:#BCA8EA; --wash:rgba(167,139,224,.12); --code-bg:#241D38; --design:#A78BE0; }
|
|
236
|
+
|
|
237
|
+
/* comments */
|
|
238
|
+
details.comments { margin: .75rem 0 0; }
|
|
239
|
+
.vcomment { border: 1px solid var(--line); border-left: 3px solid var(--high); border-radius: 6px;
|
|
240
|
+
background: var(--raise); padding: .6rem .9rem; margin: .5rem 0; font-size: .9rem; }
|
|
241
|
+
.vcomment blockquote { margin: .3rem 0; padding: .1rem .6rem; border-left: 2px solid var(--line);
|
|
242
|
+
color: var(--soft); font-style: italic; }
|
|
243
|
+
.vcomment p { margin: .3rem 0; }
|
|
244
|
+
.vc-anchor { font-family: var(--mono); font-size: .75rem; }
|
|
245
|
+
.vc-meta { font-size: .72rem; color: var(--soft); }
|
|
246
|
+
mark.vc-mark { background: color-mix(in srgb, var(--high) 22%, transparent); color: inherit;
|
|
247
|
+
border-bottom: 1px dotted var(--high); padding: 0 .05em; border-radius: 2px; }
|
|
248
|
+
.vc-add { position: absolute; z-index: 30; font: 600 .78rem var(--sans); color: var(--bg); background: var(--accent);
|
|
249
|
+
border: none; border-radius: 999px; padding: .3rem .7rem; cursor: pointer; }
|
|
250
|
+
.vc-form { position: fixed; bottom: 3.8rem; left: 50%; transform: translateX(-50%);
|
|
251
|
+
width: min(34rem, calc(100vw - 2rem)); background: var(--raise); border: 1px solid var(--line);
|
|
252
|
+
border-radius: 8px; padding: .8rem; z-index: 40; box-shadow: 0 6px 24px rgba(0, 0, 0, .18); }
|
|
253
|
+
.vc-form blockquote { margin: 0 0 .5rem; padding: .1rem .6rem; border-left: 2px solid var(--accent);
|
|
254
|
+
color: var(--soft); font-size: .85rem; font-style: italic; max-height: 4.5rem; overflow: hidden; }
|
|
255
|
+
.vc-form textarea { width: 100%; min-height: 4rem; font: inherit; color: var(--ink); background: var(--bg);
|
|
256
|
+
border: 1px solid var(--line); border-radius: 6px; padding: .5rem; box-sizing: border-box; }
|
|
257
|
+
.vc-form textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
258
|
+
.vc-row { display: flex; gap: .5rem; justify-content: flex-end; margin-top: .5rem; }
|
|
259
|
+
.vc-form button, .vc-resolve { font: 600 .78rem var(--sans); border-radius: 6px; padding: .3rem .8rem;
|
|
260
|
+
cursor: pointer; border: 1px solid var(--line); background: var(--bg); color: var(--ink); }
|
|
261
|
+
.vc-form .vc-save { background: var(--accent); border-color: var(--accent); color: var(--bg); }
|
|
262
|
+
.vc-resolve { display: block; margin-top: .4rem; }
|
|
263
|
+
.vc-form button:focus-visible, .vc-resolve:focus-visible, .vc-add:focus-visible {
|
|
264
|
+
outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
265
|
+
|
|
266
|
+
/* viewer chrome (pentimento serve) */
|
|
267
|
+
.vbar-pad { height: 3.2rem; }
|
|
268
|
+
.vbar { position: fixed; bottom: 0; left: 0; right: 0; display: flex; align-items: center; gap: .8rem;
|
|
269
|
+
padding: .5rem 1rem; background: var(--raise); border-top: 1px solid var(--line);
|
|
270
|
+
font-size: .82rem; z-index: 10; flex-wrap: wrap; }
|
|
271
|
+
.vbar a { text-decoration: none; }
|
|
272
|
+
.vbar-name { font-family: var(--mono); font-size: .75rem; color: var(--soft);
|
|
273
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 40%; }
|
|
274
|
+
.vbar select { font: inherit; color: var(--ink); background: var(--bg); border: 1px solid var(--line);
|
|
275
|
+
border-radius: 4px; padding: .15rem .4rem; max-width: 16rem; }
|
|
276
|
+
.vbar select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
277
|
+
.vcards { display: grid; gap: .8rem; margin-top: 2rem; }
|
|
278
|
+
.vcard { display: block; background: var(--raise); border: 1px solid var(--line); border-radius: 6px;
|
|
279
|
+
padding: .9rem 1.1rem; text-decoration: none; color: var(--ink); }
|
|
280
|
+
.vcard:hover { border-color: var(--accent); }
|
|
281
|
+
.vcard h3 { margin: .4rem 0 .2rem; }
|
|
282
|
+
.vcard p { margin: 0; font-size: .88rem; color: var(--soft); }
|
|
283
|
+
|
|
284
|
+
@media (prefers-reduced-motion: reduce) {
|
|
285
|
+
* { animation: none !important; transition: none !important; }
|
|
286
|
+
}
|
package/assets/viewer.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const cfg = window.__pentimento
|
|
3
|
+
if (!cfg) return
|
|
4
|
+
const main = document.querySelector('main')
|
|
5
|
+
const post = (url, body) =>
|
|
6
|
+
fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
|
|
7
|
+
|
|
8
|
+
// highlight commented quotes (best effort: quote within a single text node)
|
|
9
|
+
for (const c of cfg.comments || []) {
|
|
10
|
+
if (!c.quote || !main) continue
|
|
11
|
+
const walker = document.createTreeWalker(main, NodeFilter.SHOW_TEXT)
|
|
12
|
+
let node
|
|
13
|
+
while ((node = walker.nextNode())) {
|
|
14
|
+
const i = node.textContent.indexOf(c.quote)
|
|
15
|
+
if (i < 0) continue
|
|
16
|
+
const range = document.createRange()
|
|
17
|
+
range.setStart(node, i)
|
|
18
|
+
range.setEnd(node, i + c.quote.length)
|
|
19
|
+
const mark = document.createElement('mark')
|
|
20
|
+
mark.className = 'vc-mark'
|
|
21
|
+
mark.dataset.cid = c.id
|
|
22
|
+
mark.title = c.text
|
|
23
|
+
try { range.surroundContents(mark) } catch (e) { /* quote spans elements — panel still shows it */ }
|
|
24
|
+
break
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// resolve buttons on the open-comments panel
|
|
29
|
+
document.querySelectorAll('.vcomment[data-cid]').forEach((el) => {
|
|
30
|
+
const b = document.createElement('button')
|
|
31
|
+
b.className = 'vc-resolve'
|
|
32
|
+
b.type = 'button'
|
|
33
|
+
b.textContent = 'Resolve'
|
|
34
|
+
b.addEventListener('click', () => post('/api/resolve', { rel: cfg.rel, id: el.dataset.cid }))
|
|
35
|
+
el.appendChild(b)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
if (!cfg.canComment || !main) return
|
|
39
|
+
|
|
40
|
+
// select text → floating comment button → comment form
|
|
41
|
+
const btn = document.createElement('button')
|
|
42
|
+
btn.className = 'vc-add'
|
|
43
|
+
btn.type = 'button'
|
|
44
|
+
btn.textContent = '💬 Comment'
|
|
45
|
+
btn.hidden = true
|
|
46
|
+
document.body.appendChild(btn)
|
|
47
|
+
let pending = null
|
|
48
|
+
|
|
49
|
+
const nearestAnchor = (startNode) => {
|
|
50
|
+
let el = startNode.nodeType === 1 ? startNode : startNode.parentElement
|
|
51
|
+
if (!el) return ''
|
|
52
|
+
const closest = el.closest('[id]')
|
|
53
|
+
if (closest && main.contains(closest)) return '#' + closest.id
|
|
54
|
+
let best = ''
|
|
55
|
+
for (const cand of main.querySelectorAll('[id]')) {
|
|
56
|
+
if (cand.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING) best = '#' + cand.id
|
|
57
|
+
}
|
|
58
|
+
return best
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
document.addEventListener('mouseup', () => {
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
const s = getSelection()
|
|
64
|
+
if (!s || s.isCollapsed || !main.contains(s.anchorNode)) { btn.hidden = true; return }
|
|
65
|
+
const quote = s.toString().trim()
|
|
66
|
+
if (!quote || quote.length > 600) { btn.hidden = true; return }
|
|
67
|
+
const range = s.getRangeAt(0)
|
|
68
|
+
const r = range.getBoundingClientRect()
|
|
69
|
+
btn.style.top = (scrollY + r.bottom + 8) + 'px'
|
|
70
|
+
btn.style.left = (scrollX + Math.max(8, r.left)) + 'px'
|
|
71
|
+
const startText = range.startContainer.textContent || ''
|
|
72
|
+
const endText = range.endContainer.textContent || ''
|
|
73
|
+
pending = {
|
|
74
|
+
quote,
|
|
75
|
+
prefix: startText.slice(Math.max(0, range.startOffset - 32), range.startOffset),
|
|
76
|
+
suffix: endText.slice(range.endOffset, range.endOffset + 32),
|
|
77
|
+
anchor: nearestAnchor(range.startContainer),
|
|
78
|
+
}
|
|
79
|
+
btn.hidden = false
|
|
80
|
+
}, 0)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
btn.addEventListener('click', () => {
|
|
84
|
+
btn.hidden = true
|
|
85
|
+
if (!pending) return
|
|
86
|
+
const sel = pending
|
|
87
|
+
const form = document.createElement('div')
|
|
88
|
+
form.className = 'vc-form'
|
|
89
|
+
form.innerHTML =
|
|
90
|
+
'<blockquote></blockquote>' +
|
|
91
|
+
'<textarea placeholder="Leave a comment for the agent…"></textarea>' +
|
|
92
|
+
'<div class="vc-row"><button class="vc-cancel" type="button">Cancel</button>' +
|
|
93
|
+
'<button class="vc-save" type="button">Save comment</button></div>'
|
|
94
|
+
form.querySelector('blockquote').textContent = sel.quote
|
|
95
|
+
document.body.appendChild(form)
|
|
96
|
+
form.querySelector('textarea').focus()
|
|
97
|
+
form.querySelector('.vc-cancel').addEventListener('click', () => form.remove())
|
|
98
|
+
form.querySelector('.vc-save').addEventListener('click', async () => {
|
|
99
|
+
const text = form.querySelector('textarea').value.trim()
|
|
100
|
+
if (!text) return
|
|
101
|
+
await post('/api/comment', { rel: cfg.rel, text, ...sel })
|
|
102
|
+
form.remove() // the meta.yml write triggers the SSE reload
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
})()
|