eklavya 1.26.0 → 1.27.1
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 +48 -42
- package/dist/artifacts.js +214 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/assets/artifact-template.html +144 -0
- package/dist/assets/dashboard.html +131 -11
- package/dist/assets/tokens.css +6 -4
- package/dist/assets/tutor/SKILL.md +7 -0
- package/dist/cli-memory.js +6 -3
- package/dist/cli-memory.js.map +1 -1
- package/dist/cli.js +85 -2
- package/dist/cli.js.map +1 -1
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/dashboard.js +41 -1
- package/dist/dashboard.js.map +1 -1
- package/dist/hooks/memory-lib.js +1 -1
- package/dist/hooks/memory-lib.js.map +1 -1
- package/dist/hooks/session-start.js +10 -2
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/install-lock.js +155 -0
- package/dist/install-lock.js.map +1 -0
- package/dist/install.js +64 -45
- package/dist/install.js.map +1 -1
- package/dist/memory/reservation.js +15 -5
- package/dist/memory/reservation.js.map +1 -1
- package/dist/memory/worker.js +72 -31
- package/dist/memory/worker.js.map +1 -1
- package/dist/migrations/015_event_link_indexes.sql +24 -0
- package/dist/paths.js +8 -0
- package/dist/paths.js.map +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/agents/explainer.md +57 -0
- package/dist/plugin/agents/tutor.md +7 -0
- package/dist/plugin/cli/CLAUDE.md +60 -108
- package/dist/plugin/hooks/CLAUDE.md +118 -357
- package/dist/plugin/hooks/run.mjs +104 -21
- package/dist/plugin/skills/CLAUDE.md +106 -291
- package/dist/plugin/skills/tutor/SKILL.md +7 -0
- package/dist/tools/config_tools.js +4 -0
- package/dist/tools/config_tools.js.map +1 -1
- package/dist/tools/record_attempt.js +25 -1
- package/dist/tools/record_attempt.js.map +1 -1
- package/dist/update.js +7 -83
- package/dist/update.js.map +1 -1
- package/dist/user-skill/eklavya/SKILL.md +8 -0
- package/dist/user-skill/eklavya-artifacts/SKILL.md +135 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Eklavya
|
|
2
2
|
|
|
3
|
-
[Eklavya](https://github.com/ProjectAJ14/eklavya)
|
|
4
|
-
|
|
3
|
+
[Eklavya](https://github.com/ProjectAJ14/eklavya) teaches the concepts behind
|
|
4
|
+
agent-written code and keeps local project memory. This package includes the
|
|
5
|
+
Claude Code plugin, installer, CLI and MCP server.
|
|
5
6
|
|
|
6
|
-
## Install
|
|
7
|
+
## Install in Claude Code
|
|
8
|
+
|
|
9
|
+
With Node 22 or newer:
|
|
7
10
|
|
|
8
11
|
```bash
|
|
9
12
|
npx eklavya install
|
|
10
13
|
```
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
is how you upgrade; `npx eklavya uninstall` removes it and keeps your history unless you pass
|
|
16
|
-
`--purge`.
|
|
15
|
+
Follow the settings prompts, then restart Claude Code. The installer registers
|
|
16
|
+
the plugin, installs the managed runtime and chat/artifact skills, and prepares
|
|
17
|
+
local storage. `/eklavya:setup` lets you revisit settings.
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
Read the [installation guide](https://eklavya-run.web.app/docs/installing/) for
|
|
20
|
+
verification and [updates and removal](https://eklavya-run.web.app/docs/updates/)
|
|
21
|
+
for maintenance. Uninstall preserves learner data unless you explicitly use
|
|
22
|
+
`--purge`.
|
|
20
23
|
|
|
21
|
-
##
|
|
24
|
+
## Standalone MCP connection
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
editor-agnostic.
|
|
26
|
+
An MCP client can connect directly:
|
|
25
27
|
|
|
26
28
|
```json
|
|
27
29
|
{
|
|
@@ -31,41 +33,45 @@ editor-agnostic.
|
|
|
31
33
|
}
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
This exposes tools. Automatic capture, checkpoints and session instructions
|
|
37
|
+
depend on host integration; an MCP connection alone does not provide Claude
|
|
38
|
+
Code's learning loop.
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
## Available tools
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
| Tool | Purpose |
|
|
42
|
+
| Purpose | Tools |
|
|
43
43
|
|---|---|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
| `get_concept_graph` | a domain in prerequisite order |
|
|
51
|
-
| `get_config` / `set_config` | global and per-repo settings |
|
|
44
|
+
| Learner state and concepts | `get_learner_profile`, `log_session_concepts`, `upsert_concepts`, `get_concept_graph` |
|
|
45
|
+
| Questions and assessment | `get_session_quiz_plan`, `record_attempt`, `get_gate_status` |
|
|
46
|
+
| Settings | `get_config`, `set_config` |
|
|
47
|
+
| Find and read memory | `memory_search`, `memory_get`, `memory_timeline`, `memory_file_history`, `memory_status` |
|
|
48
|
+
| Maintain memory | `memory_write`, `memory_correct`, `memory_delete`, `memory_collections` |
|
|
49
|
+
| Inspect code | `code_outline`, `code_find_symbol` |
|
|
52
50
|
|
|
53
|
-
|
|
51
|
+
Search first, choose relevant entries, then use `memory_get` for their full
|
|
52
|
+
content. Session-aware learning and configuration tools resolve the current session
|
|
53
|
+
when `session_id` is omitted. In `memory_timeline`, it instead filters results
|
|
54
|
+
to one session; omission shows the project timeline. Other tools may not accept
|
|
55
|
+
that argument.
|
|
54
56
|
|
|
55
|
-
## CLI
|
|
57
|
+
## CLI and data
|
|
56
58
|
|
|
57
59
|
```bash
|
|
58
|
-
eklavya
|
|
59
|
-
eklavya
|
|
60
|
-
eklavya
|
|
61
|
-
eklavya
|
|
62
|
-
eklavya
|
|
63
|
-
eklavya config set focus concept # what it teaches
|
|
64
|
-
eklavya config set focus learn --topic caching
|
|
65
|
-
eklavya export-rules --out rules.md # the tutor pedagogy as Markdown
|
|
66
|
-
eklavya dashboard [--port <n>] [--no-open] # your learning history, served locally and opened
|
|
67
|
-
# in your browser (--no-open just prints the URL)
|
|
68
|
-
eklavya db-path
|
|
60
|
+
npx eklavya doctor
|
|
61
|
+
npx eklavya memory status
|
|
62
|
+
npx eklavya config get
|
|
63
|
+
npx eklavya dashboard
|
|
64
|
+
npx eklavya db-path
|
|
69
65
|
```
|
|
70
66
|
|
|
67
|
+
The [CLI reference](https://eklavya-run.web.app/docs/cli/) lists all commands and
|
|
68
|
+
flags. [Configuration](https://eklavya-run.web.app/docs/configuration/) lists
|
|
69
|
+
defaults and scopes. The [memory guide](https://eklavya-run.web.app/docs/memory/)
|
|
70
|
+
covers retrieval, processing, providers, privacy and sync.
|
|
71
|
+
|
|
72
|
+
State is local under `~/.eklavya/`, including `knowledge.db` (SQLite, WAL).
|
|
73
|
+
`EKLAVYA_HOME` and `EKLAVYA_DB` override those paths. Questions and memory have
|
|
74
|
+
independent switches. External processing is optional and explicitly configured;
|
|
75
|
+
see [your data](https://eklavya-run.web.app/docs/your-data/).
|
|
76
|
+
|
|
71
77
|
MIT.
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifacts: self-contained HTML pages an agent writes to explain something —
|
|
3
|
+
* a concept the developer got wrong, a design, a report — kept under
|
|
4
|
+
* `~/.eklavya/artifacts/<project slug>/`.
|
|
5
|
+
*
|
|
6
|
+
* Three rules shape this file.
|
|
7
|
+
*
|
|
8
|
+
* **The files are the index.** There is no table. `listArtifacts` reads each
|
|
9
|
+
* page's `<head>` for the metadata `createArtifact` stamped into it, so a page
|
|
10
|
+
* deleted, renamed or copied in by hand is reflected on the next read, and
|
|
11
|
+
* there is nothing to fall out of step with the disk. The ceiling is a scan of
|
|
12
|
+
* every file per dashboard load; `HEAD_BYTES` keeps each read small, and a few
|
|
13
|
+
* thousand pages is still milliseconds.
|
|
14
|
+
*
|
|
15
|
+
* **The design tokens are inlined at creation, not copied into the template.**
|
|
16
|
+
* `tokens.css` is the one palette the site and the dashboard share; a second
|
|
17
|
+
* hand-kept copy inside a template is the copy nobody updates. Inlining makes
|
|
18
|
+
* the page self-contained — its "download HTML" button hands over a file that
|
|
19
|
+
* still themes and prints — without a second source.
|
|
20
|
+
*
|
|
21
|
+
* **Every path in is untrusted.** `resolveArtifact` is what the dashboard
|
|
22
|
+
* serves through, and it refuses anything that does not land on an `.html`
|
|
23
|
+
* file directly inside one project folder.
|
|
24
|
+
*/
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
import { findRepoConfig, mainRepoRoot } from './config.js';
|
|
29
|
+
import { artifactsDir, projectSlug } from './paths.js';
|
|
30
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
/** Enough of a file to hold its `<head>`: the metadata is written first. */
|
|
32
|
+
const HEAD_BYTES = 16 * 1024;
|
|
33
|
+
/** What a page is filed under: the main checkout behind a worktree, else the directory itself. */
|
|
34
|
+
export function artifactProject(cwd = process.cwd()) {
|
|
35
|
+
const { repoRoot } = findRepoConfig(cwd);
|
|
36
|
+
if (repoRoot)
|
|
37
|
+
return mainRepoRoot(repoRoot);
|
|
38
|
+
try {
|
|
39
|
+
return fs.realpathSync(path.resolve(cwd));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return path.resolve(cwd);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function kebab(title) {
|
|
46
|
+
const s = title
|
|
47
|
+
.normalize('NFKD')
|
|
48
|
+
.replace(/[̀-ͯ]/g, '')
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
51
|
+
.replace(/^-+|-+$/g, '')
|
|
52
|
+
.slice(0, 60)
|
|
53
|
+
.replace(/-+$/, '');
|
|
54
|
+
return s || 'artifact';
|
|
55
|
+
}
|
|
56
|
+
const escHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
57
|
+
function assetPath(name) {
|
|
58
|
+
return path.join(moduleDir, 'assets', name);
|
|
59
|
+
}
|
|
60
|
+
/** The shared tokens, minus the remote font import (the template links the fonts itself). */
|
|
61
|
+
function inlineTokens() {
|
|
62
|
+
try {
|
|
63
|
+
const css = fs.readFileSync(assetPath('tokens.css'), 'utf8');
|
|
64
|
+
return css.replace(/@import\s+url\([^)]*\)[^;]*;\s*/gi, '');
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// A build without the tokens still writes the page; it renders unstyled
|
|
68
|
+
// until the tokens are restored, rather than failing the command.
|
|
69
|
+
return '';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Writes the template, filled in, to a fresh file and returns its path. Never
|
|
74
|
+
* overwrites: a second page with the same title on the same day gets `-2`.
|
|
75
|
+
*/
|
|
76
|
+
export function createArtifact(opts) {
|
|
77
|
+
const title = opts.title.trim();
|
|
78
|
+
if (!title)
|
|
79
|
+
throw new Error('an artifact needs a title');
|
|
80
|
+
const now = opts.now ?? new Date();
|
|
81
|
+
const project = artifactProject(opts.cwd);
|
|
82
|
+
const folder = projectSlug(project);
|
|
83
|
+
const dir = path.join(artifactsDir(), folder);
|
|
84
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
85
|
+
// The local date, not UTC: the filename is for the person reading their folder.
|
|
86
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
87
|
+
const day = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
88
|
+
const stem = `${day}-${kebab(title)}`;
|
|
89
|
+
let file = `${stem}.html`;
|
|
90
|
+
for (let n = 2; fs.existsSync(path.join(dir, file)); n++)
|
|
91
|
+
file = `${stem}-${n}.html`;
|
|
92
|
+
const description = (opts.description ?? '').trim();
|
|
93
|
+
const kind = opts.kind ?? 'artifact';
|
|
94
|
+
const fill = {
|
|
95
|
+
'{{TITLE}}': escHtml(title),
|
|
96
|
+
'{{DESCRIPTION}}': escHtml(description),
|
|
97
|
+
'{{PROJECT}}': escHtml(project),
|
|
98
|
+
'{{PROJECT_NAME}}': escHtml(path.basename(project) || project),
|
|
99
|
+
'{{KIND}}': kind,
|
|
100
|
+
'{{CONCEPT}}': escHtml(opts.concept ?? ''),
|
|
101
|
+
'{{CREATED}}': now.toISOString(),
|
|
102
|
+
'{{EYEBROW}}': kind === 'explainer' ? 'Explainer' : 'Artifact',
|
|
103
|
+
};
|
|
104
|
+
let html = fs.readFileSync(assetPath('artifact-template.html'), 'utf8');
|
|
105
|
+
// Tokens first: the CSS contains no placeholders, but user text could.
|
|
106
|
+
html = html.replace('/*{{TOKENS}}*/', () => inlineTokens());
|
|
107
|
+
html = html.replace(/\{\{[A-Z_]+\}\}/g, (m) => fill[m] ?? m);
|
|
108
|
+
const target = path.join(dir, file);
|
|
109
|
+
fs.writeFileSync(target, html, { flag: 'wx' });
|
|
110
|
+
return { path: target, id: `${folder}/${file}`, project };
|
|
111
|
+
}
|
|
112
|
+
function meta(head, name) {
|
|
113
|
+
const re = new RegExp(`<meta\\s+name="${name}"\\s+content="([^"]*)"`, 'i');
|
|
114
|
+
const m = re.exec(head);
|
|
115
|
+
return m ? unescHtml(m[1]) : null;
|
|
116
|
+
}
|
|
117
|
+
function unescHtml(s) {
|
|
118
|
+
return s.replace(/&(amp|lt|gt|quot|#39);/g, (_, e) => ({ amp: '&', lt: '<', gt: '>', quot: '"', '#39': "'" })[e]);
|
|
119
|
+
}
|
|
120
|
+
function readHead(file) {
|
|
121
|
+
const fd = fs.openSync(file, 'r');
|
|
122
|
+
try {
|
|
123
|
+
const buf = Buffer.alloc(HEAD_BYTES);
|
|
124
|
+
const n = fs.readSync(fd, buf, 0, HEAD_BYTES, 0);
|
|
125
|
+
return buf.subarray(0, n).toString('utf8');
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
fs.closeSync(fd);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Every artifact on disk, newest first. A page with no Eklavya metadata — one
|
|
133
|
+
* copied in by hand — is still listed, titled from its `<title>` or filename.
|
|
134
|
+
* Never throws: an unreadable file costs that one row.
|
|
135
|
+
*/
|
|
136
|
+
export function listArtifacts(root = artifactsDir()) {
|
|
137
|
+
const rows = [];
|
|
138
|
+
let folders;
|
|
139
|
+
try {
|
|
140
|
+
folders = fs.readdirSync(root, { withFileTypes: true });
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return rows;
|
|
144
|
+
}
|
|
145
|
+
for (const folder of folders) {
|
|
146
|
+
// The same rules `resolveArtifact` serves by, so nothing is listed that
|
|
147
|
+
// the dashboard would then refuse: no dot-names, no symlinks — a link's
|
|
148
|
+
// `<head>` would otherwise be read from wherever it points.
|
|
149
|
+
if (!folder.isDirectory() || folder.name.startsWith('.'))
|
|
150
|
+
continue;
|
|
151
|
+
let files;
|
|
152
|
+
try {
|
|
153
|
+
files = fs.readdirSync(path.join(root, folder.name));
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
for (const name of files) {
|
|
159
|
+
if (!/\.html?$/i.test(name) || name.startsWith('.'))
|
|
160
|
+
continue;
|
|
161
|
+
const file = path.join(root, folder.name, name);
|
|
162
|
+
try {
|
|
163
|
+
const st = fs.lstatSync(file);
|
|
164
|
+
if (!st.isFile())
|
|
165
|
+
continue;
|
|
166
|
+
const head = readHead(file);
|
|
167
|
+
const title = /<title>([^<]*)<\/title>/i.exec(head)?.[1]?.trim();
|
|
168
|
+
const kind = meta(head, 'eklavya:kind');
|
|
169
|
+
rows.push({
|
|
170
|
+
id: `${folder.name}/${name}`,
|
|
171
|
+
title: title ? unescHtml(title) : name.replace(/\.html?$/i, ''),
|
|
172
|
+
description: meta(head, 'description') ?? '',
|
|
173
|
+
project: meta(head, 'eklavya:project') || null,
|
|
174
|
+
kind: kind === 'explainer' ? 'explainer' : 'artifact',
|
|
175
|
+
concept: meta(head, 'eklavya:concept') || null,
|
|
176
|
+
created: meta(head, 'eklavya:created') || st.mtime.toISOString(),
|
|
177
|
+
bytes: st.size,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return rows.sort((a, b) => (a.created < b.created ? 1 : a.created > b.created ? -1 : 0));
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The file behind an id, or null. Exactly `<folder>/<file>.html`, both plain
|
|
189
|
+
* names — no separators, no `..`, no leading dot — and the resolved path must
|
|
190
|
+
* still sit inside the root after symlinks, so a link planted in a project
|
|
191
|
+
* folder cannot serve a file from elsewhere on the machine.
|
|
192
|
+
*/
|
|
193
|
+
export function resolveArtifact(id, root = artifactsDir()) {
|
|
194
|
+
const parts = id.split('/');
|
|
195
|
+
if (parts.length !== 2)
|
|
196
|
+
return null;
|
|
197
|
+
const plain = /^[^/\\\0]+$/;
|
|
198
|
+
for (const p of parts)
|
|
199
|
+
if (!plain.test(p) || p.startsWith('.'))
|
|
200
|
+
return null;
|
|
201
|
+
if (!/\.html?$/i.test(parts[1]))
|
|
202
|
+
return null;
|
|
203
|
+
try {
|
|
204
|
+
const realRoot = fs.realpathSync(root);
|
|
205
|
+
const real = fs.realpathSync(path.join(root, parts[0], parts[1]));
|
|
206
|
+
if (path.dirname(path.dirname(real)) !== realRoot)
|
|
207
|
+
return null;
|
|
208
|
+
return fs.statSync(real).isFile() ? real : null;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=artifacts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifacts.js","sourceRoot":"","sources":["../src/artifacts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>{{TITLE}}</title>
|
|
7
|
+
<meta name="description" content="{{DESCRIPTION}}">
|
|
8
|
+
<meta name="eklavya:project" content="{{PROJECT}}">
|
|
9
|
+
<meta name="eklavya:kind" content="{{KIND}}">
|
|
10
|
+
<meta name="eklavya:concept" content="{{CONCEPT}}">
|
|
11
|
+
<meta name="eklavya:created" content="{{CREATED}}">
|
|
12
|
+
<!-- Eklavya type. Offline, the stacks fall back to system faces; nothing breaks. -->
|
|
13
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
14
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
15
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@600;800;900&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap">
|
|
16
|
+
<style>
|
|
17
|
+
/* Eklavya design tokens, inlined from web/public/tokens.css when this page was
|
|
18
|
+
created. Ink (dark) is the default on screen and in the PDF; data-mode="paper"
|
|
19
|
+
on <html> switches to the paper ground. Components below name ROLES only. */
|
|
20
|
+
/*{{TOKENS}}*/
|
|
21
|
+
</style>
|
|
22
|
+
<style>
|
|
23
|
+
:root{ --maxw:880px; }
|
|
24
|
+
|
|
25
|
+
*{box-sizing:border-box}
|
|
26
|
+
body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.65 var(--font-body)}
|
|
27
|
+
::selection{background:var(--spot);color:var(--spot-ink)}
|
|
28
|
+
main{max-width:var(--maxw);margin:0 auto;padding:64px 16px 96px}
|
|
29
|
+
|
|
30
|
+
.eyebrow{margin:0 0 14px;font:400 11px/1 var(--font-mono);letter-spacing:var(--tracking-caps-wide);
|
|
31
|
+
text-transform:uppercase;color:var(--spot)}
|
|
32
|
+
h1{font:900 clamp(34px,5.4vw,64px)/.95 var(--font-disp);letter-spacing:var(--tracking-display);margin:0 0 .35em}
|
|
33
|
+
h2{font:800 clamp(24px,3vw,34px)/1 var(--font-disp);letter-spacing:var(--tracking-tight);
|
|
34
|
+
margin:2.4em 0 .7em;padding-top:1em;border-top:1px solid var(--line-2)}
|
|
35
|
+
h3{font:800 18px/1.3 var(--font-disp);letter-spacing:var(--tracking-snug);margin:1.8em 0 .4em}
|
|
36
|
+
p,li{color:var(--ink)}
|
|
37
|
+
.lede{color:var(--dim);font-size:17px;max-width:620px;margin:0 0 2.4em}
|
|
38
|
+
.muted{color:var(--dim)}
|
|
39
|
+
a{color:var(--spot);text-decoration:none;border-bottom:1px solid var(--spot)}
|
|
40
|
+
strong{font-weight:600}
|
|
41
|
+
|
|
42
|
+
code{font-family:var(--font-mono);font-size:.88em;background:var(--mass);padding:.1em .38em}
|
|
43
|
+
pre{background:var(--panel);border:1px solid var(--line-2);padding:14px 16px;overflow:auto;font-size:13px}
|
|
44
|
+
pre code{background:none;padding:0}
|
|
45
|
+
|
|
46
|
+
table{width:100%;border-collapse:collapse;margin:1.2em 0;font-size:14px}
|
|
47
|
+
th,td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--line-2);vertical-align:top}
|
|
48
|
+
th{font:400 11px var(--font-mono);letter-spacing:var(--tracking-caps);text-transform:uppercase;color:var(--faint)}
|
|
49
|
+
td.num,th.num{text-align:right;font-family:var(--font-mono);font-variant-numeric:tabular-nums}
|
|
50
|
+
blockquote{margin:1.2em 0;padding:.2em 0 .2em 16px;border-left:2px solid var(--spot);color:var(--dim)}
|
|
51
|
+
|
|
52
|
+
.card{background:var(--panel);border:1px solid var(--line);padding:24px}
|
|
53
|
+
.card h3{margin-top:0}
|
|
54
|
+
.card p{color:var(--dim);font-size:14.5px;margin:0}
|
|
55
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin:1.4em 0}
|
|
56
|
+
.chip{display:inline-block;font:12px var(--font-mono);color:var(--spot);background:var(--spot-soft);
|
|
57
|
+
border:1px solid var(--spot);padding:2px 9px}
|
|
58
|
+
.chip.warn{color:var(--warning);border-color:var(--warning);background:color-mix(in srgb,var(--warning) 12%,transparent)}
|
|
59
|
+
.chip.bad{color:var(--error);border-color:var(--error);background:color-mix(in srgb,var(--error) 12%,transparent)}
|
|
60
|
+
.stat{border:1px solid var(--line-2);padding:18px 20px}
|
|
61
|
+
.stat .k{font:400 11px var(--font-mono);letter-spacing:var(--tracking-caps);text-transform:uppercase;color:var(--faint)}
|
|
62
|
+
.stat .v{font:900 40px/1.1 var(--font-disp);letter-spacing:var(--tracking-tight);margin-top:6px}
|
|
63
|
+
.stat .d{color:var(--dim);font-size:13px}
|
|
64
|
+
.callout{border:1px solid var(--spot);background:var(--spot-soft);padding:14px 18px;margin:1.4em 0}
|
|
65
|
+
|
|
66
|
+
figure{margin:1.8em 0;background:var(--panel);border:1px solid var(--line-2);
|
|
67
|
+
padding:20px;text-align:center;break-inside:avoid}
|
|
68
|
+
figure svg{max-width:100%;height:auto}
|
|
69
|
+
figcaption{margin-top:12px;color:var(--dim);font-size:13px;text-align:center}
|
|
70
|
+
.svg-ink{fill:var(--ink)} .svg-muted{fill:var(--dim)}
|
|
71
|
+
.svg-box{fill:var(--mass);stroke:var(--line-2);stroke-width:1}
|
|
72
|
+
.svg-box-spot{fill:var(--spot-soft);stroke:var(--spot);stroke-width:1.5}
|
|
73
|
+
.svg-line{stroke:var(--faint);stroke-width:1.5;fill:none}
|
|
74
|
+
.svg-line-spot{stroke:var(--spot);stroke-width:1.5;fill:none}
|
|
75
|
+
.svg-label{font:500 13px var(--font-body);fill:var(--ink)}
|
|
76
|
+
.svg-small{font:11px var(--font-mono);fill:var(--dim)}
|
|
77
|
+
|
|
78
|
+
.actions{position:fixed;top:16px;right:16px;z-index:50;display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}
|
|
79
|
+
.actions button{display:inline-flex;gap:7px;align-items:center;padding:8px 14px;
|
|
80
|
+
border:1px solid var(--line-2);border-radius:0;background:var(--panel);color:var(--ink);
|
|
81
|
+
font:500 12px var(--font-mono);letter-spacing:.06em;cursor:pointer}
|
|
82
|
+
.actions button:hover{border-color:var(--spot);color:var(--spot)}
|
|
83
|
+
.actions button:focus-visible{outline:none;box-shadow:var(--ring)}
|
|
84
|
+
|
|
85
|
+
@media screen{
|
|
86
|
+
body{background-image:linear-gradient(var(--grid) 1px,transparent 1px),
|
|
87
|
+
linear-gradient(90deg,var(--grid) 1px,transparent 1px);background-size:72px 72px}
|
|
88
|
+
main{background:var(--bg);border-left:1px solid var(--line-2);border-right:1px solid var(--line-2);
|
|
89
|
+
padding-left:40px;padding-right:40px;min-height:100vh}
|
|
90
|
+
}
|
|
91
|
+
@media (max-width:640px){ main{padding:76px 16px 64px!important} .actions{top:12px;right:12px} }
|
|
92
|
+
|
|
93
|
+
@media print{
|
|
94
|
+
/* Dark in the PDF too. The @page margin is canvas and prints white, so the
|
|
95
|
+
page has none, a fixed layer covers the sheet, and the gutter lives on
|
|
96
|
+
<main>, repeated on every page fragment by box-decoration-break. */
|
|
97
|
+
*{-webkit-print-color-adjust:exact;print-color-adjust:exact}
|
|
98
|
+
html,body{background:var(--bg);color:var(--ink)}
|
|
99
|
+
@page{margin:0}
|
|
100
|
+
body::before{content:"";position:fixed;inset:0;background:var(--bg);z-index:-1}
|
|
101
|
+
.actions{display:none}
|
|
102
|
+
body{font-size:11pt}
|
|
103
|
+
main{padding:12mm 16mm;max-width:none;-webkit-box-decoration-break:clone;box-decoration-break:clone}
|
|
104
|
+
h1,h2,h3{break-after:avoid;break-inside:avoid}
|
|
105
|
+
p,li{orphans:3;widows:3}
|
|
106
|
+
/* Anything that can outgrow a page must be allowed to split. */
|
|
107
|
+
table,pre,section,div{break-inside:auto}
|
|
108
|
+
thead{display:table-header-group}
|
|
109
|
+
tfoot{display:table-footer-group}
|
|
110
|
+
tr,td,th,li,.card,.stat{break-inside:avoid}
|
|
111
|
+
figure,blockquote{break-inside:avoid}
|
|
112
|
+
figure svg,figure img{max-height:170mm}
|
|
113
|
+
a{color:inherit;border:0}
|
|
114
|
+
}
|
|
115
|
+
</style>
|
|
116
|
+
</head>
|
|
117
|
+
<body>
|
|
118
|
+
<div class="actions">
|
|
119
|
+
<button type="button" onclick="window.print()" aria-label="Download this page as PDF">PDF</button>
|
|
120
|
+
<button type="button" onclick="saveHtml()" aria-label="Download this page as an HTML file">HTML</button>
|
|
121
|
+
</div>
|
|
122
|
+
<script>
|
|
123
|
+
function saveHtml(){
|
|
124
|
+
var name=(location.pathname.split('/').pop()||'').replace(/\.html?$/,'')||'artifact';
|
|
125
|
+
var blob=new Blob(['<!DOCTYPE html>\n'+document.documentElement.outerHTML],{type:'text/html'});
|
|
126
|
+
var a=document.createElement('a');
|
|
127
|
+
a.href=URL.createObjectURL(blob); a.download=name+'.html';
|
|
128
|
+
document.body.appendChild(a); a.click(); a.remove();
|
|
129
|
+
setTimeout(function(){URL.revokeObjectURL(a.href)},1000);
|
|
130
|
+
}
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<main>
|
|
134
|
+
<p class="eyebrow">{{EYEBROW}} · {{PROJECT_NAME}}</p>
|
|
135
|
+
<h1>{{TITLE}}</h1>
|
|
136
|
+
<p class="lede">{{DESCRIPTION}}</p>
|
|
137
|
+
|
|
138
|
+
<!-- CONTENT: replace this comment with the page. Sections under <h2>, a
|
|
139
|
+
<figure> with an inline SVG (the .svg-* classes) for every sequence,
|
|
140
|
+
branch or before/after, a <figcaption> that states the takeaway. -->
|
|
141
|
+
|
|
142
|
+
</main>
|
|
143
|
+
</body>
|
|
144
|
+
</html>
|