Initial commit — Singular Particular Space v1
Homepage (site/index.html): integration-v14 promoted, Writings section integrated with 33 pieces clustered by type (stories/essays/miscellany), Writings welcome lightbox, content frame at 98% opacity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
297
skills/annotated-writing/annotated-writing-build.md
Normal file
297
skills/annotated-writing/annotated-writing-build.md
Normal file
@@ -0,0 +1,297 @@
|
||||
# Annotated Writing Build
|
||||
|
||||
Instructions for a build agent. Input: a completed content map `.md` file. Output: a single-file `.html` artefact. Do not perform literary analysis. Do not rewrite or improve the source text. Execute the map.
|
||||
|
||||
---
|
||||
|
||||
## Two-Agent Protocol
|
||||
|
||||
This agent receives a content map. It does not produce content maps. If asked to do both in one context, refuse the analysis task and request the content map as input.
|
||||
|
||||
**What the build agent reads from the content map:**
|
||||
- Tab definitions (TOON)
|
||||
- Decoder metadata (TOON) + decoder bodies (YAML)
|
||||
- Lightbox definitions (YAML)
|
||||
- Accordion content (YAML)
|
||||
- Bias notes (YAML)
|
||||
- Further reading (TOON)
|
||||
- Annotated source text (with `[DECODER]`, `[LIGHTBOX]`, `[ORDER]`, `[LETTER-*]` markers)
|
||||
|
||||
---
|
||||
|
||||
## Reading TOON in the Content Map
|
||||
|
||||
Content maps use TOON for uniform arrays. Parse as follows:
|
||||
|
||||
**Tabular array syntax:**
|
||||
```toon
|
||||
arrayName[N]{field1,field2,field3}:
|
||||
value1,value2,value3
|
||||
value1,value2,value3
|
||||
```
|
||||
`[N]` is the declared row count — validate against actual rows. `{fields}` is the column header — same for every row. Values follow CSV quoting rules: strings containing commas are double-quoted.
|
||||
|
||||
**YAML scalar syntax** (for body text — not TOON):
|
||||
```yaml
|
||||
key: >
|
||||
Block scalar. Paragraph text. May contain commas freely.
|
||||
Blank line = new paragraph in rendered output.
|
||||
```
|
||||
|
||||
Both may appear in the same content map. TOON for metadata; YAML for prose.
|
||||
|
||||
---
|
||||
|
||||
## Aesthetic Direction
|
||||
|
||||
The aesthetic is set per-project by the user. The content map may include an `aesthetic:` key. If absent, infer from context clues in the source text. When in doubt, ask before building.
|
||||
|
||||
Regardless of aesthetic, these rules apply:
|
||||
- Choose fonts that are distinctive and specific to the aesthetic. Never: Arial, Inter, Roboto, system-ui.
|
||||
- Use CSS custom properties for all colors. No hardcoded hex values in component CSS.
|
||||
- Dark or light themes are both valid. Commit to one — do not default to white-on-grey.
|
||||
- Background texture adds atmosphere. Scanlines, grain, parchment patterns, noise — pick one and use it subtly.
|
||||
- Tab navigation is sticky. One tab visible at a time via JS class toggle.
|
||||
|
||||
---
|
||||
|
||||
## Component Library
|
||||
|
||||
All components must be implemented as specified. Do not substitute or simplify.
|
||||
|
||||
---
|
||||
|
||||
### Decoder
|
||||
|
||||
Inline interactive element. Wraps a phrase in prose. Floating panel on click.
|
||||
|
||||
**Critical structural rule:** Every element inside a decoder must be a `<span>`, never a `<div>`. Decoders live inside `<p>` tags. A `<div>` inside a `<p>` silently ejects the panel from the DOM. Use `<span>` with `display: block` for block-like rendering.
|
||||
|
||||
```html
|
||||
<span class="dc" id="[id]">
|
||||
<button class="dc-btn [color-class]" onclick="tog('[id]',this)">[phrase from text]</button>
|
||||
<span class="dc-panel [color-class]">
|
||||
<span class="d-tag [color-class]">[tag text]</span>
|
||||
<span class="d-head">[label]</span>
|
||||
[body text]
|
||||
<a class="d-link [color-class]" href="[url]" target="_blank">→ [link label]</a>
|
||||
</span>
|
||||
</span>
|
||||
```
|
||||
|
||||
**Required CSS:**
|
||||
```css
|
||||
.dc { display: inline; position: relative; }
|
||||
.dc-panel {
|
||||
display: none;
|
||||
position: absolute; /* never relative */
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
z-index: 500;
|
||||
width: 300px; /* adjust to aesthetic */
|
||||
}
|
||||
.dc-panel.open { display: block; }
|
||||
.dc-panel.flip { left: auto; right: 0; }
|
||||
```
|
||||
|
||||
**Required JS:**
|
||||
```js
|
||||
function tog(id, btn) {
|
||||
const w = document.getElementById(id);
|
||||
const p = w.querySelector('.dc-panel');
|
||||
const open = p.classList.contains('open');
|
||||
document.querySelectorAll('.dc-panel.open').forEach(x => x.classList.remove('open'));
|
||||
document.querySelectorAll('.dc-btn.open').forEach(x => x.classList.remove('open'));
|
||||
if (!open) {
|
||||
p.classList.add('open'); btn.classList.add('open');
|
||||
const r = p.getBoundingClientRect();
|
||||
p.classList.toggle('flip', r.right > window.innerWidth - 16);
|
||||
}
|
||||
}
|
||||
document.addEventListener('click', e => {
|
||||
if (!e.target.closest('.dc')) {
|
||||
document.querySelectorAll('.dc-panel.open').forEach(x => x.classList.remove('open'));
|
||||
document.querySelectorAll('.dc-btn.open').forEach(x => x.classList.remove('open'));
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Trigger visual requirements:**
|
||||
- Must look clickable (cursor pointer, border-bottom dashed or dotted)
|
||||
- Must indicate open/closed state (chevron via `::after`, or similar)
|
||||
- Color coding must match the scheme defined in the content map
|
||||
|
||||
---
|
||||
|
||||
### Lightbox
|
||||
|
||||
Full-screen overlay. Triggered by an inline button in the prose.
|
||||
|
||||
```html
|
||||
<!-- Trigger — inline in prose -->
|
||||
<button class="lb-t [color-class]" onclick="lb('[id]')">[trigger phrase]</button>
|
||||
|
||||
<!-- Lightbox — at end of body, before script tag -->
|
||||
<div class="lb-overlay" id="[id]" onclick="closeLbOv(event,'[id]')">
|
||||
<div class="lb-box [color-class]">
|
||||
<div class="lb-head [color-class]">
|
||||
<div>
|
||||
<span class="lb-eyebrow [color-class]">[eyebrow]</span>
|
||||
<span class="lb-title">[title]</span>
|
||||
</div>
|
||||
<button class="lb-close" onclick="closeLb('[id]')">✕</button>
|
||||
</div>
|
||||
<div class="lb-body">
|
||||
<!-- h3 per section, p per paragraph, lb-src link at end -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Required CSS:**
|
||||
```css
|
||||
.lb-overlay { display: none; position: fixed; inset: 0; z-index: 2000;
|
||||
align-items: center; justify-content: center; padding: 20px; }
|
||||
.lb-overlay.open { display: flex; }
|
||||
.lb-box { max-height: 88vh; overflow-y: auto; }
|
||||
.lb-head { position: sticky; top: 0; }
|
||||
```
|
||||
|
||||
**Required JS:**
|
||||
```js
|
||||
function lb(id) { document.getElementById(id).classList.add('open'); document.body.style.overflow = 'hidden'; }
|
||||
function closeLb(id) { document.getElementById(id).classList.remove('open'); document.body.style.overflow = ''; }
|
||||
function closeLbOv(e, id) { if (e.target === document.getElementById(id)) closeLb(id); }
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') {
|
||||
document.querySelectorAll('.lb-overlay.open').forEach(x => x.classList.remove('open'));
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Accordion
|
||||
|
||||
Expandable sections within educational tabs. One open at a time.
|
||||
|
||||
The accordion panel uses `<div>` — it is not inside prose, so block elements are safe here.
|
||||
|
||||
```html
|
||||
<div class="acc">
|
||||
<button class="acc-btn" onclick="acc(this)">
|
||||
<span>[heading]</span>
|
||||
<span class="acc-arrow [color-class]">▾</span>
|
||||
</button>
|
||||
<div class="acc-panel">
|
||||
<!-- p tags per paragraph -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Required JS:**
|
||||
```js
|
||||
function acc(btn) {
|
||||
const p = btn.nextElementSibling;
|
||||
const open = btn.classList.contains('open');
|
||||
document.querySelectorAll('.acc-btn.open').forEach(b => {
|
||||
b.classList.remove('open'); b.nextElementSibling.classList.remove('open');
|
||||
});
|
||||
if (!open) { btn.classList.add('open'); p.classList.add('open'); }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bias Note
|
||||
|
||||
A styled callout block at the top of each analytical tab, before the first accordion.
|
||||
|
||||
```html
|
||||
<div class="bias [color-class]">
|
||||
<strong>A note:</strong> [bias note text from content map]
|
||||
</div>
|
||||
```
|
||||
|
||||
Not collapsible. Always visible. One per analytical tab.
|
||||
|
||||
---
|
||||
|
||||
### Tab Navigation
|
||||
|
||||
```html
|
||||
<nav>
|
||||
<button class="active [color-class]" onclick="go('[id]',this,'[color]')">[label]</button>
|
||||
<!-- one per tab -->
|
||||
</nav>
|
||||
```
|
||||
|
||||
```js
|
||||
function go(id, btn, col) {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('nav button').forEach(b => { b.classList.remove('active'); /* remove all color classes */ });
|
||||
document.getElementById('tab-' + id).classList.add('active');
|
||||
btn.classList.add('active', col || 'white');
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
```
|
||||
|
||||
Tab content divs:
|
||||
```html
|
||||
<div class="tab [active-on-load]" id="tab-[id]">
|
||||
<!-- tab content -->
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Source Text — Markers to HTML
|
||||
|
||||
Translate content map markers into HTML components:
|
||||
|
||||
| Marker | Renders as |
|
||||
|---|---|
|
||||
| `[DECODER:id] phrase [/DECODER]` | `<span class="dc" id="id">` decoder component |
|
||||
| `[LIGHTBOX:id] phrase [/LIGHTBOX]` | `<button class="lb-t">` trigger |
|
||||
| `[ORDER] "text" [/ORDER]` | Styled display block — dialogue/order separator |
|
||||
| `[LETTER-START:sender]` | Opening of styled letter card |
|
||||
| `[LETTER-END]` | Close of letter card |
|
||||
| `[PULL-QUOTE] text [/PULL-QUOTE]` | Styled pull-quote block |
|
||||
| `[EDITORIAL] text [/EDITORIAL]` | Styled editorial aside |
|
||||
| `[SECTION-BREAK]` | `<hr>` with aesthetic treatment |
|
||||
|
||||
Letter senders each get a distinct visual treatment (border color, label, optional stamp). Define sender colors in CSS variables using the project color scheme.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
Single `.html` file. All CSS in `<style>` in `<head>`. All JS in `<script>` immediately before `</body>`. No external dependencies except Google Fonts (loaded via `<link>`). All lightboxes placed together at the end of `<body>`, before `<script>`. All external links use `target="_blank"`.
|
||||
|
||||
---
|
||||
|
||||
## Critical Bugs (Permanent Reference)
|
||||
|
||||
**1. Block elements inside `<p>` tags silently break the DOM.**
|
||||
`<div>` inside `<p>` is invalid HTML. The browser auto-closes the `<p>`, ejecting the decoder panel. Every decoder element must be a `<span>`.
|
||||
|
||||
**2. Decoder panel must be `position: absolute`.**
|
||||
`position: relative` expands the parent span when the panel opens, collapsing the surrounding text. Use `position: absolute`.
|
||||
|
||||
**3. Decoder panels clip off the right edge.**
|
||||
After opening, check `panel.getBoundingClientRect().right > window.innerWidth - 16`. If true, add `.flip` class (`left: auto; right: 0`).
|
||||
|
||||
**4. Lightboxes placed inside tab content divs break scroll.**
|
||||
Place all lightbox overlays as direct children of `<body>`, after all tab content, before `<script>`.
|
||||
|
||||
---
|
||||
|
||||
## Output Requirements
|
||||
|
||||
- Single `.html` file
|
||||
- Source text reproduced exactly — no edits, no corrections (errors in the original stay)
|
||||
- All decoders, lightboxes, accordions, and tabs from the content map implemented
|
||||
- Bias notes present on every analytical tab
|
||||
- All external links open in new tab
|
||||
- Passes a basic sanity check: all decoder IDs referenced in prose exist in the JS/HTML
|
||||
Reference in New Issue
Block a user