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 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
@@ -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
+ })()
@@ -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
+ }
@@ -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
+ })()