Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ jobs:
pnpm check:release-order "$BASE_SHA"

- name: Type and content checks
run: pnpm test:spec && pnpm test:home && pnpm test:deploy:dev && pnpm test:release-update && pnpm check:release && pnpm check
run: pnpm test:spec && pnpm test:home && pnpm test:docs && pnpm test:deploy:dev && pnpm test:release-update && pnpm check:release && pnpm check
env:
GH_TOKEN: ${{ github.token }}

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ jobs:
MDBASE_TS_DIR: .sources/mdbase

- name: Check and build website
run: pnpm test:spec && pnpm test:home && pnpm check:release && pnpm check && pnpm build
run: pnpm test:spec && pnpm test:home && pnpm test:docs && pnpm check:release && pnpm check && pnpm build
env:
GH_TOKEN: ${{ github.token }}

Expand Down
53 changes: 53 additions & 0 deletions docs/app-documentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Reader and Writer documentation

Reader documentation lives under `src/pages/apps/reader`; Writer documentation lives under
`src/pages/apps/writer`. `src/data/docs-sections.ts` supplies their navigation and the existing
SDK navigation. The Reader extension privacy page remains separate and unchanged.

The guides describe committed application behaviour. They were checked against Reader commit
`74999d1be78749f7333054007f7b5247b6a79ef9` and Writer commit
`9cb61aa5350259f87680072e93100c5e431f0fd8`. Uncommitted local reliability work is not documented
as shipped behaviour. Keep the guides in step with app changes, particularly recovery, exports,
source capture and extension permissions.

## Recordings

The five recordings and posters are served from `public/videos` on the documentation origin.
Do not depend on authenticated GitHub attachments, temporary recording directories or preview
hosting for production playback. MP4s are about 1.5–3.8 MB each. Posters are resized to 960 px.

The recording agent confirmed that all videos use in-memory sample data, with no real account,
private collection content or personal data. Reader samples use public-domain James, Thoreau
and Emerson excerpts and a fictional article. Writer samples use Darwin-themed manuscripts,
public-domain quotations and fictional authors and reviewers. The live Writer demo may use
different sample content.

These are silent recordings with captions burned into the picture. `src/data/doc-videos.json`
provides a text equivalent of each sequence; review that text whenever a video is replaced.
`DocVideo.astro` uses native controls, inline playback, no autoplay or loop, and `preload="none"`.
The page loads a poster; the MP4 is requested on playback. Links permit downloading the files.

| Recording | Placement | SHA-256 of MP4 |
| --- | --- | --- |
| `reader-markdown` | Reader getting started | `facce287ffa892083736bad3c82d83cb41e22ab97914843f6260f7e0ad57f317` |
| `extension-capture` | Reader browser extension | `c57cf032886260fed0370eb206812c52844aad76a07363f31231205c8b287573` |
| `reader-library` | Reader library and views | `81892c8efe4528ee2b6fa93d38eaa2abddde7dc938352ef90fa3b8188a765349` |
| `writer-paper` | Writer getting started | `ce34aa1b8046715dd435ff730568b980116942db10986bb6b3ab06166a62cc14` |
| `writer-book` | Writer chapters and embeds | `a587ba3ec2b23d8c12fdfbea14be2038c1e1276b3bec477728e5c91366b28bd3` |

## Checks and deployment

```sh
pnpm test:docs
pnpm check
pnpm build
MDBASE_SPEC_DIR=/absolute/path/to/mdbase-spec pnpm import:spec
pnpm check:links
```

`test:docs` checks the catalogue, documentation navigation, recording assets and text descriptions,
and opt-in accessible video controls. Site CI and the production build run it too.

Production is the repository's GitHub Pages workflow (`.github/workflows/deploy.yml`), triggered
by a merge to `main`. Cloudflare Pages is only the separate development deployment. Verify
`/apps/`, both documentation roots, all five video files and their posters after deployment.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
"test:deploy:dev": "node --test scripts/deploy-pages-dev.test.mjs",
"test:release-update": "node --test scripts/connect-release-record.test.mjs",
"test:home": "node --test scripts/home-animation.test.mjs",
"test:docs": "node --test scripts/app-docs.test.mjs",
"test:spec": "node --test scripts/spec-page.test.mjs",
"test": "pnpm test:spec && pnpm test:home && pnpm test:deploy:dev && pnpm test:release-update && pnpm check:release && pnpm check && pnpm build && pnpm import:spec && pnpm check:links"
"test": "pnpm test:spec && pnpm test:home && pnpm test:docs && pnpm test:deploy:dev && pnpm test:release-update && pnpm check:release && pnpm check && pnpm build && pnpm import:spec && pnpm check:links"
},
"dependencies": {
"@astrojs/sitemap": "^3.7.3",
Expand Down
Binary file added public/videos/extension-capture.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/videos/extension-capture.mp4
Binary file not shown.
Binary file added public/videos/reader-library.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/videos/reader-library.mp4
Binary file not shown.
Binary file added public/videos/reader-markdown.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/videos/reader-markdown.mp4
Binary file not shown.
Binary file added public/videos/writer-book.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/videos/writer-book.mp4
Binary file not shown.
Binary file added public/videos/writer-paper.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/videos/writer-paper.mp4
Binary file not shown.
67 changes: 67 additions & 0 deletions scripts/app-docs.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import assert from "node:assert/strict";
import { readFileSync, existsSync } from "node:fs";
import { test } from "node:test";
import { fileURLToPath } from "node:url";

const root = new URL("../", import.meta.url);
const read = (path) => readFileSync(new URL(path, root), "utf8");
const videos = JSON.parse(read("src/data/doc-videos.json"));
const placements = {
"reader-markdown": "src/pages/apps/reader/index.astro",
"extension-capture": "src/pages/apps/reader/extension/index.astro",
"reader-library": "src/pages/apps/reader/library/index.astro",
"writer-paper": "src/pages/apps/writer/index.astro",
"writer-book": "src/pages/apps/writer/chapters/index.astro"
};

test("Reader and Writer are listed with live applications and local documentation", () => {
const catalogue = read("src/pages/apps/index.astro");
for (const app of ["reader", "writer"]) {
assert.ok(catalogue.includes(`https://${app}.mdbase.dev/`));
assert.ok(catalogue.includes(`/apps/${app}/`));
}
assert.ok(catalogue.includes("https://reader.mdbase.dev/?preview=1"));
assert.ok(catalogue.includes("https://lab.mdbase-writer.pages.dev/?demo"));
});

test("Every documentation navigation entry resolves to a site page", () => {
const sections = read("src/data/docs-sections.ts");
const links = [...sections.matchAll(/href: "([^"]+)"/g)].map((match) => match[1]);
assert.ok(links.length > 0);
assert.equal(links.filter((href) => href.startsWith("/apps/reader/")).length, 9);
assert.equal(links.filter((href) => href.startsWith("/apps/writer/")).length, 6);
for (const href of links) {
const page = new URL(`src/pages${href}index.astro`, root);
assert.ok(existsSync(page), `Missing navigation target: ${fileURLToPath(page)}`);
}
assert.match(sections, /value: release\.sdkVersion/);
});

test("Five sample recordings have local playable files, posters and text equivalents", () => {
assert.deepEqual(Object.keys(videos).sort(), Object.keys(placements).sort());
for (const [name, metadata] of Object.entries(videos)) {
const mp4 = readFileSync(new URL(`public/videos/${name}.mp4`, root));
const poster = readFileSync(new URL(`public/videos/${name}.jpg`, root));
assert.equal(mp4.subarray(4, 8).toString(), "ftyp", `${name}: MP4 container`);
assert.equal(poster.subarray(0, 2).toString("hex"), "ffd8", `${name}: JPEG poster`);
assert.ok(metadata.title && metadata.description);
assert.match(metadata.duration, /^\d+:\d{2}$/);
assert.ok(metadata.width > 0 && metadata.height > 0);
assert.ok(metadata.steps.length >= 4, `${name}: text description of the actions`);
assert.ok(read(placements[name]).includes(`<DocVideo name="${name}" />`));
}
});

test("Video controls are opt-in and have accessible labels and descriptions", () => {
const component = read("src/components/DocVideo.astro");
const attributes = component.match(/<video\s+([^>]+)>/)[1];
assert.match(attributes, /\bcontrols\b/);
assert.match(attributes, /\bplaysinline\b/);
assert.match(attributes, /preload="none"/);
assert.match(attributes, /poster=/);
assert.match(attributes, /aria-labelledby=/);
assert.match(attributes, /aria-describedby=/);
assert.doesNotMatch(attributes, /\bautoplay\b|\bloop\b/);
assert.match(component, /Text description of the recording/);
assert.match(component, /download/);
});
51 changes: 51 additions & 0 deletions src/components/DocVideo.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
import videos from "../data/doc-videos.json";

interface Props {
name: keyof typeof videos;
}

const { name } = Astro.props;
const video = videos[name];
const titleId = `video-${name}-title`;
const descriptionId = `video-${name}-description`;
const src = `/videos/${name}.mp4`;
---

<figure class="doc-video">
<figcaption id={titleId}>
<strong>{video.title}</strong>
<span>{video.duration}</span>
</figcaption>
<video
controls
playsinline
preload="none"
poster={`/videos/${name}.jpg`}
width={video.width}
height={video.height}
aria-labelledby={titleId}
aria-describedby={descriptionId}
>
<source src={src} type="video/mp4" />
<p><a href={src}>Download the recording</a> to watch it in a video player.</p>
</video>
<p id={descriptionId}>{video.description} Silent recording with on-screen captions and sample data.</p>
<details>
<summary>Text description of the recording</summary>
<ol>{video.steps.map((step) => <li>{step}</li>)}</ol>
</details>
<p class="doc-video-download"><a href={src} download>Download MP4</a></p>
</figure>

<style>
.doc-video { margin: 1.5rem 0 2rem; }
figcaption { display: flex; justify-content: space-between; gap: 1rem; margin-bottom: 0.5rem; }
figcaption span { color: var(--ink-soft); font-family: var(--mono); font-size: 0.85em; }
video { display: block; width: 100%; height: auto; background: var(--paper-soft); border: 1px solid var(--line); }
p { margin: 0.6rem 0; }
details { padding: 0.6rem 0; border-block: 1px solid var(--line); }
summary { cursor: pointer; font-weight: 600; }
summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.doc-video-download { font-size: 0.85em; }
</style>
51 changes: 11 additions & 40 deletions src/components/DocsLayout.astro
Original file line number Diff line number Diff line change
@@ -1,54 +1,25 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import release from "../data/connect-release.json";
import { docsSections, type DocsSectionId } from "../data/docs-sections";

interface Props {
title: string;
description: string;
active: string;
section?: DocsSectionId;
headings?: Array<{ href: string; label: string }>;
}

const { title, description, active, headings = [] } = Astro.props;
const docGroups = [
{
label: "Start",
docs: [
{ href: "/sdk/quickstart/", label: "Run the source-built sandbox", key: "quickstart" },
{ href: "/sdk/connect-quickstart/", label: "Connect a browser app", key: "connect-quickstart" },
{ href: "/sdk/", label: "SDK overview", key: "overview" }
]
},
{
label: "Build",
docs: [
{ href: "/sdk/portable-apps/", label: "Portable HTML apps", key: "portable-apps" },
{ href: "/sdk/manifest/", label: "Application manifest", key: "manifest" },
{ href: "/sdk/contracts/", label: "Contracts and adapters", key: "contracts" },
{ href: "/sdk/authorization/", label: "Authorization", key: "authorization" },
{ href: "/sdk/operations/", label: "Records and operations", key: "operations" },
{ href: "/sdk/routing/", label: "Authority routes", key: "routing" },
{ href: "/sdk/testing/", label: "Testing", key: "testing" },
{ href: "/sdk/notifications/", label: "Notifications and timers", key: "notifications" },
{ href: "/sdk/offline-sync/", label: "Offline sync", key: "offline-sync" }
]
},
{
label: "Reference",
docs: [
{ href: "/sdk/security/", label: "Security model", key: "security" },
{ href: "/sdk/api/", label: "API reference", key: "api" }
]
}
];
const { title, description, active, section = "sdk", headings = [] } = Astro.props;
const { label, navLabel, headerCurrent, sourcePath, meta, groups: docGroups } = docsSections[section];
---

<BaseLayout title={title} description={description} current="sdk" wide>
<BaseLayout title={title} description={description} current={headerCurrent} wide>
<div class="docs-shell">
<aside class="docs-nav">
<details open data-docs-nav>
<summary>Connect SDK</summary>
<nav aria-label="SDK documentation">
<summary>{label}</summary>
<nav aria-label={navLabel}>
{
docGroups.map((group) => (
<div class="docs-nav__group">
Expand All @@ -66,19 +37,19 @@ const docGroups = [
</nav>
</details>
<div class="docs-nav__meta">
<span>@mdbase-dev/connect</span>
<strong>{release.sdkVersion}</strong>
<span>{meta.label}</span>
<strong>{meta.value}</strong>
</div>
</aside>
<article class="docs-article">
<header class="docs-title">
<p>Connect SDK</p>
<p>{label}</p>
<h1>{title}</h1>
<span>{description}</span>
</header>
<slot />
<footer class="docs-edit">
<a href="https://github.com/mdbase-dev/mdbase.dev/tree/main/src/pages/sdk">
<a href={`https://github.com/mdbase-dev/mdbase.dev/tree/main/${sourcePath}`}>
Edit this page on GitHub
</a>
</footer>
Expand Down
69 changes: 69 additions & 0 deletions src/data/doc-videos.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"reader-markdown": {
"title": "Highlight a passage, get a Markdown file",
"duration": "0:43",
"width": 1600,
"height": 900,
"description": "Create a highlight with commentary, inspect its Markdown record, and embed it in a literature note.",
"steps": [
"The sample library opens The Principles of Psychology beside a view of its Markdown source record.",
"A passage in the section on voluntary attention is selected. The comment field opens and a note is added before Save highlight is chosen.",
"The passage becomes a yellow highlight. A new annotation file appears with its source link, document revision, quoted text, target and commentary.",
"The annotation panel lists the passage and comment. Add to literature note inserts an ordinary annotation embed into the source note."
]
},
"extension-capture": {
"title": "Save from the web with the extension",
"duration": "1:02",
"width": 1440,
"height": 900,
"description": "Save a sample article and several highlighted passages from Chrome's side panel, then open the saved copy in Reader.",
"steps": [
"A fictional article, The marginalia problem, is open in Chrome. Reader's side panel identifies the page and the destination sample collection.",
"Text is selected on the page. A comment is typed in the panel and a colour is chosen to save the page and highlight.",
"The saved passage is highlighted on the live page. Further selections are saved in other colours and appear in Saved highlights.",
"Selecting a saved highlight finds it on the page. Open in Reader opens the source's readable saved copy.",
"Reader shows the same highlights and comments on the saved copy, with a Markdown file for each annotation."
]
},
"reader-library": {
"title": "Views, annotations and panes",
"duration": "0:46",
"width": 1440,
"height": 900,
"description": "Browse table and shelf views, search annotations across sources, and open two reading panes.",
"steps": [
"The sample library switches between All sources, a Bookshelf view and a Currently reading view.",
"Annotations replaces the source list with a table of passages. A search for attention narrows the table to a matching highlight.",
"The command palette opens The Principles of Psychology. Another source, Walden, is opened beside it.",
"The two documents remain visible together. Tabs can be docked and split; the arrangement is saved per collection."
]
},
"writer-paper": {
"title": "Write, cite and typeset a paper",
"duration": "0:55",
"width": 1600,
"height": 900,
"description": "Complete a citation, correct an unknown citekey and insert a Reader quotation while the paper preview updates.",
"steps": [
"A sample paper about Darwin is open with Outline, its Markdown editor and the typeset article preview side by side.",
"A citation is typed in the introduction. Completion finds a source in the Reader library and inserts its citekey with a page locator.",
"A deliberately misspelled citekey is marked on its Markdown line. The suggested matching source corrects it, and the preview updates.",
"Sources is searched for vegetation. A highlighted quotation is inserted from the matching Reader source, including its citation and page.",
"The inserted text appears in the editor and the typeset paper. The manuscript remains an ordinary collection record."
]
},
"writer-book": {
"title": "Build a book from chapter records",
"duration": "0:49",
"width": 1600,
"height": 900,
"description": "Open and edit a chapter, reorder chapters and add a new chapter in a thesis-layout manuscript.",
"steps": [
"The sample book Patient Observation embeds Coral reefs and Earthworms as separate chapter records. The thesis preview shows a title page and chapters.",
"Coral reefs is opened from its chapter card. Text is added to that record, and the whole-book preview updates.",
"The chapters are reordered in Outline; their order and chapter numbering change in the preview.",
"Add chapter creates Afterlives and embeds it after the existing chapters. Writing in the new record produces another typeset chapter."
]
}
}
Loading
Loading