@apisurf/canonui 0.1.2 → 0.1.4
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 +12 -12
- package/dist/bin.js +32 -4
- package/package.json +2 -3
- package/theme/components/Copy.astro +16 -9
- package/theme/components/SectionCopy.astro +19 -13
- package/theme/styles/theme.css +11 -44
package/README.md
CHANGED
|
@@ -56,20 +56,20 @@ one stylesheet of plain CSS with tokens on `:root`.
|
|
|
56
56
|
The page is two columns: the document — title, description, a copy button, then
|
|
57
57
|
every block in order — and a rail listing its titled blocks and the headings
|
|
58
58
|
inside markdown blocks. There is no framework, and the client-side runtime is
|
|
59
|
-
five small scripts: the wide toggle, the copy button, the one that puts it
|
|
60
|
-
each section's heading, the rail's scroll tracking, and mermaid when the document holds a diagram — bundled into the site
|
|
59
|
+
five small scripts: the wide toggle, the copy button, the one that puts it
|
|
60
|
+
under each section's heading, the rail's scroll tracking, and mermaid when the document holds a diagram — bundled into the site
|
|
61
61
|
rather than fetched from a CDN, so it works offline.
|
|
62
62
|
|
|
63
|
-
| File | What it is
|
|
64
|
-
| ------------------------------------ |
|
|
65
|
-
| `theme/layouts/Site.astro` | The shell, and every slot a theme would replace
|
|
66
|
-
| `theme/components/Block.astro` | One block, drawn according to its type
|
|
67
|
-
| `theme/components/Toc.astro` | The contents rail
|
|
68
|
-
| `theme/components/Copy.astro` | The button that copies the document as markdown
|
|
69
|
-
| `theme/components/SectionCopy.astro` | The same button,
|
|
70
|
-
| `theme/pages/index.astro` | The document, at `/`
|
|
71
|
-
| `theme/styles/theme.css` | The whole of the styling
|
|
72
|
-
| `theme/lib/snapshot.ts` | The snapshot, read once at build time
|
|
63
|
+
| File | What it is |
|
|
64
|
+
| ------------------------------------ | --------------------------------------------------- |
|
|
65
|
+
| `theme/layouts/Site.astro` | The shell, and every slot a theme would replace |
|
|
66
|
+
| `theme/components/Block.astro` | One block, drawn according to its type |
|
|
67
|
+
| `theme/components/Toc.astro` | The contents rail |
|
|
68
|
+
| `theme/components/Copy.astro` | The button that copies the document as markdown |
|
|
69
|
+
| `theme/components/SectionCopy.astro` | The same button, twice under each section's heading |
|
|
70
|
+
| `theme/pages/index.astro` | The document, at `/` |
|
|
71
|
+
| `theme/styles/theme.css` | The whole of the styling |
|
|
72
|
+
| `theme/lib/snapshot.ts` | The snapshot, read once at build time |
|
|
73
73
|
|
|
74
74
|
### The markdown twin
|
|
75
75
|
|
package/dist/bin.js
CHANGED
|
@@ -118,7 +118,34 @@ function shiftHeadings(markdown, by) {
|
|
|
118
118
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
119
119
|
import { homedir } from "node:os";
|
|
120
120
|
import { existsSync, mkdirSync } from "node:fs";
|
|
121
|
-
|
|
121
|
+
|
|
122
|
+
// ../db/src/driver.ts
|
|
123
|
+
import Database from "libsql";
|
|
124
|
+
function openDatabase(file) {
|
|
125
|
+
const db = new Database(file);
|
|
126
|
+
const prepare2 = db.prepare.bind(db);
|
|
127
|
+
db.prepare = (sql) => {
|
|
128
|
+
const stmt = prepare2(sql);
|
|
129
|
+
const get = stmt.get.bind(stmt);
|
|
130
|
+
const all = stmt.all.bind(stmt);
|
|
131
|
+
const run2 = stmt.run.bind(stmt);
|
|
132
|
+
stmt.get = (...params) => withoutMetadata(get(params));
|
|
133
|
+
stmt.all = (...params) => all(params);
|
|
134
|
+
stmt.run = (...params) => run2(params);
|
|
135
|
+
return stmt;
|
|
136
|
+
};
|
|
137
|
+
db.pragma = (source, options) => {
|
|
138
|
+
const stmt = db.prepare(`PRAGMA ${source}`);
|
|
139
|
+
if (!options?.simple) return stmt.all();
|
|
140
|
+
const row = stmt.get();
|
|
141
|
+
return row && Object.values(row)[0];
|
|
142
|
+
};
|
|
143
|
+
return db;
|
|
144
|
+
}
|
|
145
|
+
function withoutMetadata(row) {
|
|
146
|
+
if (row && typeof row === "object") Reflect.deleteProperty(row, "_metadata");
|
|
147
|
+
return row;
|
|
148
|
+
}
|
|
122
149
|
|
|
123
150
|
// ../db/src/schema.ts
|
|
124
151
|
var MIGRATIONS = [
|
|
@@ -243,7 +270,7 @@ function openDb(options = {}) {
|
|
|
243
270
|
);
|
|
244
271
|
}
|
|
245
272
|
if (!options.mustExist) mkdirSync(dirname(file), { recursive: true });
|
|
246
|
-
const db =
|
|
273
|
+
const db = openDatabase(file);
|
|
247
274
|
db.pragma("journal_mode = WAL");
|
|
248
275
|
db.pragma("synchronous = NORMAL");
|
|
249
276
|
db.pragma("temp_store = MEMORY");
|
|
@@ -1276,8 +1303,9 @@ The markdown twin
|
|
|
1276
1303
|
|
|
1277
1304
|
Each top-level section \u2014 a titled block, or an h1 or h2 at the top of an
|
|
1278
1305
|
untitled markdown block, up to the next heading of its rank \u2014 has its own
|
|
1279
|
-
under /sections/, copied by the
|
|
1280
|
-
|
|
1306
|
+
under /sections/, copied by the "Copy section" button under its heading.
|
|
1307
|
+
An h1 runs on past the h2s under it, so it copies all of them. The "Copy
|
|
1308
|
+
heading" button beside it copies the heading's text alone.
|
|
1281
1309
|
|
|
1282
1310
|
The button fetches what it copies, so it wants the site served rather than
|
|
1283
1311
|
opened off the disk \u2014 canonui open is enough, and so is any host. Everything
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apisurf/canonui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "canonui — build and preview the documents that canon holds: render one into static HTML, or serve it locally.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -39,13 +39,12 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"astro": "7.2.0",
|
|
42
|
-
"
|
|
42
|
+
"libsql": "0.5.29",
|
|
43
43
|
"marked": "18.0.9",
|
|
44
44
|
"mermaid": "11.16.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@astrojs/check": "0.9.10",
|
|
48
|
-
"@types/better-sqlite3": "7.6.12",
|
|
49
48
|
"@types/node": "22.14.0",
|
|
50
49
|
"tsup": "8.3.5",
|
|
51
50
|
"typescript": "5.6.3",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
interface Props {
|
|
14
14
|
/** The .md the button copies. Left out when a script fills it in. */
|
|
15
15
|
href?: string;
|
|
16
|
-
/** What it copies, for the resting label — "document", "section". */
|
|
16
|
+
/** What it copies, for the resting label — "document", "section", "heading". */
|
|
17
17
|
what: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -109,7 +109,14 @@ const { href, what } = Astro.props;
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
/** What a button copies: the text it carries, or the .md it points at. */
|
|
113
|
+
function source(button: HTMLButtonElement): Promise<string> | null {
|
|
114
|
+
const { copy: url, copyText: text } = button.dataset;
|
|
115
|
+
if (text !== undefined) return Promise.resolve(text);
|
|
116
|
+
return url ? markdown(url) : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function copy(pending: Promise<string>): Promise<boolean> {
|
|
113
120
|
/*
|
|
114
121
|
* Handing the clipboard the pending fetch rather than awaiting it first is
|
|
115
122
|
* what keeps this working in Safari, which grants the write only while the
|
|
@@ -117,7 +124,7 @@ const { href, what } = Astro.props;
|
|
|
117
124
|
*/
|
|
118
125
|
if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) {
|
|
119
126
|
try {
|
|
120
|
-
const blob =
|
|
127
|
+
const blob = pending.then((text) => new Blob([text], { type: "text/plain" }));
|
|
121
128
|
await navigator.clipboard.write([new ClipboardItem({ "text/plain": blob })]);
|
|
122
129
|
return true;
|
|
123
130
|
} catch {
|
|
@@ -125,7 +132,7 @@ const { href, what } = Astro.props;
|
|
|
125
132
|
}
|
|
126
133
|
}
|
|
127
134
|
|
|
128
|
-
const text = await
|
|
135
|
+
const text = await pending;
|
|
129
136
|
try {
|
|
130
137
|
await navigator.clipboard.writeText(text);
|
|
131
138
|
return true;
|
|
@@ -136,14 +143,14 @@ const { href, what } = Astro.props;
|
|
|
136
143
|
|
|
137
144
|
/*
|
|
138
145
|
* Bound once on the document rather than on each button: the section buttons
|
|
139
|
-
* are put
|
|
146
|
+
* are put under their headings after this runs, and a handler per button would
|
|
140
147
|
* miss them.
|
|
141
148
|
*/
|
|
142
149
|
const timers = new WeakMap<HTMLButtonElement, ReturnType<typeof setTimeout>>();
|
|
143
150
|
|
|
144
151
|
const buttonOf = (event: Event) =>
|
|
145
152
|
event.target instanceof Element
|
|
146
|
-
? event.target.closest<HTMLButtonElement>("button[data-copy]")
|
|
153
|
+
? event.target.closest<HTMLButtonElement>("button[data-copy], button[data-copy-text]")
|
|
147
154
|
: null;
|
|
148
155
|
|
|
149
156
|
// Intent, a moment before the click. By the time it lands the document is
|
|
@@ -158,13 +165,13 @@ const { href, what } = Astro.props;
|
|
|
158
165
|
|
|
159
166
|
document.addEventListener("click", async (event) => {
|
|
160
167
|
const button = buttonOf(event);
|
|
161
|
-
const
|
|
162
|
-
if (!button || !
|
|
168
|
+
const pending = button && source(button);
|
|
169
|
+
if (!button || !pending) return;
|
|
163
170
|
|
|
164
171
|
clearTimeout(timers.get(button));
|
|
165
172
|
let ok = false;
|
|
166
173
|
try {
|
|
167
|
-
ok = await copy(
|
|
174
|
+
ok = await copy(pending);
|
|
168
175
|
} catch {
|
|
169
176
|
ok = false;
|
|
170
177
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Two copy buttons under each top-level section's heading: one for the heading
|
|
4
|
+
* alone, one for the whole section as markdown.
|
|
4
5
|
*
|
|
5
6
|
* The headings come from two places — a block's title, drawn here, and an h2
|
|
6
|
-
* inside a markdown block, drawn by marked as a string — so the
|
|
7
|
-
*
|
|
8
|
-
* `Copy` either way, so
|
|
7
|
+
* inside a markdown block, drawn by marked as a string — so the buttons are put
|
|
8
|
+
* under them in the browser, from one template, rather than written twice. They
|
|
9
|
+
* are `Copy` either way, so they look and behave like the document's own.
|
|
9
10
|
*/
|
|
10
11
|
import Copy from "./Copy.astro";
|
|
11
12
|
|
|
@@ -20,7 +21,10 @@ const { sections } = Astro.props;
|
|
|
20
21
|
{
|
|
21
22
|
sections.length > 0 && (
|
|
22
23
|
<template data-section-copy={JSON.stringify(sections)}>
|
|
23
|
-
<
|
|
24
|
+
<p class="section-actions">
|
|
25
|
+
<Copy what="heading" />
|
|
26
|
+
<Copy what="section" />
|
|
27
|
+
</p>
|
|
24
28
|
</template>
|
|
25
29
|
)
|
|
26
30
|
}
|
|
@@ -36,16 +40,18 @@ const { sections } = Astro.props;
|
|
|
36
40
|
// A titled block's anchor is on the block, and its heading is the title.
|
|
37
41
|
const target = document.getElementById(id);
|
|
38
42
|
const heading = target?.matches("h1, h2") ? target : target?.querySelector(":scope > h2");
|
|
39
|
-
const
|
|
40
|
-
if (!heading || !(
|
|
43
|
+
const actions = template?.content.firstElementChild?.cloneNode(true);
|
|
44
|
+
if (!heading || !(actions instanceof HTMLElement)) continue;
|
|
45
|
+
const [headingCopy, sectionCopy] = actions.querySelectorAll("button");
|
|
46
|
+
if (!headingCopy || !sectionCopy) continue;
|
|
41
47
|
|
|
42
48
|
const title = heading.textContent?.trim() ?? "";
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
// The heading is already on the page, so it is copied from there.
|
|
50
|
+
headingCopy.dataset.copyText = title;
|
|
51
|
+
headingCopy.setAttribute("aria-label", `Copy the heading “${title}”`);
|
|
52
|
+
sectionCopy.dataset.copy = href;
|
|
53
|
+
sectionCopy.setAttribute("aria-label", `Copy the section “${title}” as markdown`);
|
|
48
54
|
heading.classList.add("section-heading");
|
|
49
|
-
heading.
|
|
55
|
+
heading.after(actions);
|
|
50
56
|
}
|
|
51
57
|
</script>
|
package/theme/styles/theme.css
CHANGED
|
@@ -437,53 +437,20 @@ pre.mermaid[data-processed="true"] {
|
|
|
437
437
|
}
|
|
438
438
|
|
|
439
439
|
/*
|
|
440
|
-
* The same button
|
|
441
|
-
*
|
|
442
|
-
* so
|
|
440
|
+
* The same button under each section's heading, twice: one for the heading
|
|
441
|
+
* alone and one for the section it opens. On a line of their own rather than
|
|
442
|
+
* beside the title, so a long title has nothing to run into.
|
|
443
443
|
*/
|
|
444
|
-
.section-heading
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
centre on the first line rather than on all of them when the title wraps. */
|
|
448
|
-
--section-line: 1.755rem;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/* 2rem at 1.25. */
|
|
452
|
-
h1.section-heading {
|
|
453
|
-
--section-line: 2.5rem;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
.section-copy {
|
|
457
|
-
position: absolute;
|
|
458
|
-
right: 0;
|
|
459
|
-
top: calc(var(--section-line) / 2);
|
|
460
|
-
transform: translateY(-50%);
|
|
461
|
-
background: var(--bg);
|
|
462
|
-
font-weight: 400;
|
|
463
|
-
letter-spacing: normal;
|
|
464
|
-
opacity: 0;
|
|
465
|
-
transition:
|
|
466
|
-
opacity 120ms ease,
|
|
467
|
-
color 120ms ease,
|
|
468
|
-
border-color 120ms ease,
|
|
469
|
-
background-color 120ms ease;
|
|
444
|
+
h1.section-heading,
|
|
445
|
+
h2.section-heading {
|
|
446
|
+
margin-bottom: 0.4rem;
|
|
470
447
|
}
|
|
471
448
|
|
|
472
|
-
.section-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
/* Nothing to hover on a touchscreen, so there it sits after the title instead. */
|
|
479
|
-
@media (hover: none) {
|
|
480
|
-
.section-copy {
|
|
481
|
-
position: static;
|
|
482
|
-
transform: none;
|
|
483
|
-
margin-left: 0.6rem;
|
|
484
|
-
vertical-align: middle;
|
|
485
|
-
opacity: 1;
|
|
486
|
-
}
|
|
449
|
+
p.section-actions {
|
|
450
|
+
display: flex;
|
|
451
|
+
flex-wrap: wrap;
|
|
452
|
+
gap: 0.4rem;
|
|
453
|
+
margin: 0 0 0.9rem;
|
|
487
454
|
}
|
|
488
455
|
|
|
489
456
|
/* -----------------------------------------------------------------------------
|