@ingcreators/annot-product-docs-astro 0.1.0 → 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,271 @@
1
+ # @ingcreators/annot-product-docs-astro
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2e92c97: First publish of the living-product-docs package family. Phases
8
+ 1-5 + 7 of `docs/plans/living-product-docs.md` landed across
9
+ PRs 876-899; this entry flips the three packages from
10
+ `private: true` to publishable and stamps `0.1.0`.
11
+
12
+ ### `@ingcreators/annot-product-docs`
13
+ - MDX parser (`parseMdx` / `parseMdxFile`) — Remark / unified
14
+ pipeline that walks `.mdx` files with `annot:` frontmatter
15
+ and extracts `<Screen>` / `<Overlay>` / `<Transition>` /
16
+ `<HistoryEntry>` / `<ScreenList>` JSX components.
17
+ - Match resolver (`parseSnapshot` / `resolveMatch` /
18
+ `resolveOverlays`) for the Playwright `aria-snapshot`
19
+ YAML, honouring `match.under` disambiguation and emitting
20
+ `not-found` / `ambiguous` / `renamed` / `role-changed` /
21
+ `live-mismatch` diagnostics.
22
+ - `screen` fixture extending `@ingcreators/annot-playwright`
23
+ with `screen.capture({ id, mdxPath })` that re-syncs
24
+ `annot:snapshot` + `annot:attributes` MDX comment blocks
25
+ in place.
26
+ - Drift detector (`detectDrift` / `detectDriftFromYaml`) — six
27
+ finding kinds (added / removed / renamed / role-changed /
28
+ duplicated / attribute-drift) with severity buckets.
29
+ - `annot-docs` CLI (`init` / `sync` / `lint`) with `--json`
30
+ / `--ci` / `--fix` flags + a sample GitHub Actions workflow
31
+ emitting GitHub annotations on PR diff views.
32
+
33
+ ### `@ingcreators/annot-product-docs-astro`
34
+ - `productDocsIntegration()` Astro 5.x integration factory.
35
+ - 7 docs components: `<Screen>`, `<Overlay>`, `<Transition>`,
36
+ `<TransitionTable>`, `<HistoryEntry>`, `<ScreenList>`,
37
+ `<TransitionGraph>`. Shipped as `.astro` source under
38
+ `./components/*.astro` exports.
39
+ - Image Service (`renderAnnotatedScreen` + SHA-keyed
40
+ `createFileCache` / `createMemoryCache`) that composes the
41
+ base screenshot with overlay callouts at build time.
42
+
43
+ ### `@ingcreators/annot-product-docs-xlsx`
44
+ - MDX → normalised bundle extractor; per-role default layout
45
+ (cover / history / list / screen / reference); customer-
46
+ template support with `{var}` placeholder substitution
47
+ (including `{annot:date}` special vars + `{name:format}`
48
+ date formatting); Excel Named Range writers
49
+ (`annotImage` / `annotItemTable` / `annotHistory` /
50
+ `annotList` / `annotSnapshot` / `annotAttributes`).
51
+ - `annot-docs-xlsx render` CLI with multi-book emit + per-book
52
+ template config.
53
+
54
+ - 4768855: **`@ingcreators/annot-product-docs-astro` — new `/playwright`
55
+ subpath** that re-exports an extended Playwright `test` fixture
56
+ whose `page.screenshot()` accepts a compositional `annot: { … }`
57
+ option:
58
+
59
+ ```ts
60
+ import { test } from "@ingcreators/annot-product-docs-astro/playwright";
61
+
62
+ test("login flow", async ({ page }) => {
63
+ await page.goto("/login");
64
+ await page.screenshot({
65
+ path: "public/login.png",
66
+ annot: {
67
+ mdx: { id: "login", path: "src/content/docs/login.mdx" },
68
+ tags: { source: "docs-tour", capturedAt: new Date().toISOString() },
69
+ },
70
+ });
71
+ });
72
+ ```
73
+
74
+ The `annot` option is compositional — each field is an
75
+ independent contribution to the embedded XMP record:
76
+ - `mdx: { id, path }` — refresh the MDX's `annot:snapshot` block
77
+ against the current page, then resolve `<Overlay match>` blocks
78
+ for the named `<Screen>`.
79
+ - `overlays: BboxAnnotation[]` — caller-supplied annotations
80
+ (same DSL `@ingcreators/annot-annotator` accepts). Merged with
81
+ MDX-derived overlays when both are present.
82
+ - `tags: Record<string, string>` — provenance metadata written
83
+ verbatim into the XMP. No auto-fill — callers write the
84
+ `WELL_KNOWN_TAG_KEYS` they want.
85
+ - `editable: boolean` (default `true`) — toggle between
86
+ "annotations preserved as SVG layer + embedded original"
87
+ (re-editable in Annot Cloud) and "annotations baked into
88
+ visible pixels" (flat PNG, no XMP layer).
89
+
90
+ `page.screenshot()` calls WITHOUT `annot` fall through to
91
+ vanilla Playwright byte-for-byte — codegen / DevTools Recorder
92
+ output keeps working unedited.
93
+
94
+ Phase 1 of
95
+ `docs/plans/playwright-screenshot-annot-fixture.md`. Phase 2 will
96
+ add the same interception on `locator.screenshot()` with
97
+ coordinate rebasing for sub-region overlays.
98
+
99
+ Two helpers also exported from the main `@ingcreators/annot-product-docs-astro`
100
+ entry for callers who want to compose annotations themselves:
101
+ - `resolveMdxAnnotations({ mdxPath, screenId, dims })` — extract
102
+ the MDX's `<Overlay>` blocks into a typed `BboxNumberedBadgeAnnotation[]`
103
+ (the underlying step the fixture uses internally).
104
+ - `svgFromBboxAnnotations(annotations)` — wrap a
105
+ `BboxAnnotation[]` into a single-root `<svg>` ready for
106
+ `Annotator.toEditablePng()` / `toPng()`.
107
+
108
+ **`@ingcreators/annot-core` — new `writePngWithTagsOnly`
109
+ helper** exported from `/xmp-bytes` (and re-exported from
110
+ `/xmp`). Writes `tags` into a PNG's XMP iTXt chunk without
111
+ embedding an original capture or annotations layer — for the
112
+ "PNG with provenance metadata sidecar" path (CI failure
113
+ screenshots, VRT references, etc.). The resulting bytes are
114
+ still a valid PNG and the Annot editor treats them as a normal
115
+ PNG (no `<annot:annotations>` element → not editable round-trip;
116
+ opens as fresh canvas).
117
+
118
+ - 49b5585: **`locator.screenshot({ annot: { … } })` support** — Phase 2 of
119
+ `docs/plans/playwright-screenshot-annot-fixture.md`. The fixture
120
+ now patches `Locator.prototype.screenshot` alongside
121
+ `Page.prototype.screenshot`, and overlay coordinates are
122
+ automatically rebased into the cropped image's coordinate space:
123
+
124
+ ```ts
125
+ await page.locator("header").screenshot({
126
+ path: "header.png",
127
+ annot: {
128
+ overlays: [
129
+ // page-space bbox — rebased onto the header-locator's clip
130
+ { type: "rect", bbox: { x: 120, y: 60, width: 50, height: 30 } },
131
+ ],
132
+ },
133
+ });
134
+ ```
135
+
136
+ Overlays whose page-space bbox falls outside the locator's
137
+ bounding box are dropped (warning + skip per Open Question 4)
138
+ and surfaced via:
139
+ - `console.warn(...)` to stderr — always
140
+ - `test.info().annotations` as a `warning` entry when running
141
+ under Playwright — best-effort, guarded so vitest unit tests
142
+ don't crash
143
+
144
+ `page.screenshot({ clip, annot })` honours `clip` the same way —
145
+ explicit clip + auto rebase. Mirrors vanilla
146
+ `page.screenshot({ clip })` semantics; only the overlay-rebase
147
+ behaviour is annot-specific.
148
+
149
+ When the locator has no bounding box (off-screen / hidden), the
150
+ fixture throws a friendly diagnostic asking the caller to use
151
+ a stable selector / `waitFor()`.
152
+
153
+ **Coordinate-rebase API** — exported alongside the fixture for
154
+ callers who want to compose annotations themselves:
155
+
156
+ ```ts
157
+ import { rebaseAnnotations } from "@ingcreators/annot-product-docs-astro/playwright";
158
+ // Or for direct algorithmic use without the fixture:
159
+ // import { rebaseAnnotations } from "@ingcreators/annot-product-docs-astro/playwright/rebase";
160
+
161
+ const { kept, dropped } = rebaseAnnotations(annotations, clip);
162
+ ```
163
+
164
+ Returns `{ kept, dropped }` for each shape in the
165
+ `BboxAnnotation` union (rect / numberedBadge / circle / arrow /
166
+ text / callout). Raw SVG fragments (`type: "raw"`) pass through
167
+ unchanged — the caller is responsible for emitting clip-space
168
+ coords inside arbitrary SVG.
169
+
170
+ `numberedBadge`'s `imageWidth` / `imageHeight` are also rebased
171
+ to the clip dimensions so `placement: "auto"` picks the corner
172
+ against the cropped image edge rather than the page edge.
173
+
174
+ - 730fab7: `renderAnnotatedScreen()` gains an optional `editable?: boolean |
175
+ { tags?: Record<string, string> }` field. Pass `true` (or an
176
+ object) and the function routes through the new
177
+ `Annotator.toEditablePng()` path: the returned PNG carries the
178
+ same visible callouts plus the original capture + the
179
+ annotations SVG embedded in XMP / custom `svGo` chunk, so
180
+ re-opening the file in the Annot editor / Cloud restores the
181
+ overlays as selectable / movable / restylable objects rather
182
+ than a flat bitmap.
183
+
184
+ ```ts
185
+ const result = await renderAnnotatedScreen({
186
+ mdxPath: "docs/app/index.mdx",
187
+ screenId: "app-overview",
188
+ basePngBytes,
189
+ editable: {
190
+ tags: {
191
+ source: "docs-tour",
192
+ capturedAt: new Date().toISOString(),
193
+ },
194
+ },
195
+ });
196
+ await writeFile("public/app/shots/app-overview.png", result.bytes);
197
+ ```
198
+
199
+ The cache key folds in the `editable` flag, so flat and editable
200
+ variants of the same screen don't collide. Existing flat-raster
201
+ callers are byte-for-byte unaffected — the option defaults to
202
+ `undefined` (flat).
203
+
204
+ The `CacheKeyInput` type gains a parallel `editable?: boolean`
205
+ field; pure helpers that compute the cache key directly should
206
+ forward the bit when threading the flag through.
207
+
208
+ Internal note: this PR also adds `@ingcreators/annot-core` as a
209
+ devDependency so tests can import `readEditablePngBytes` from
210
+ `/xmp-bytes` for round-trip verification. Runtime dependencies
211
+ are unchanged.
212
+
213
+ ### Patch Changes
214
+
215
+ - 657a685: **Republish with `dist/` included.** The `0.1.0` tarballs of all
216
+ three packages shipped to npm without their `dist/` directory —
217
+ the `publish.yml` workflow's pre-pack `pnpm build` step had only
218
+ filtered four other packages, so `pnpm pack` packed the three
219
+ `product-docs*` packages against empty `dist/`s. The
220
+ `publishConfig.main` (`./dist/index.js`) consequently pointed at
221
+ a missing file, breaking `npm install` for every consumer.
222
+
223
+ The source fix landed in
224
+ [#947](https://github.com/ingcreators/annot/pull/947) with two
225
+ defences:
226
+ 1. Three new `--filter` lines in the workflow's build step so
227
+ all seven publishable packages get built before pack.
228
+ 2. A per-package `prepack` script (`pnpm run build`) so even a
229
+ misconfigured workflow (or a manual `pnpm pack` / `pnpm
230
+ publish`) refreshes `dist/` before packing.
231
+
232
+ No source-code changes in any of the three packages — only the
233
+ packaging is fixed. This patch publish exists solely to deliver
234
+ working tarballs to the registry; the public API surface is
235
+ byte-identical to `0.1.0`.
236
+
237
+ Verified locally:
238
+
239
+ ```
240
+ $ pnpm --filter @ingcreators/annot-product-docs pack --dry-run
241
+ Tarball Contents
242
+ bin/annot-docs.mjs
243
+ dist/cli.d.ts
244
+ dist/config.d.ts
245
+ dist/drift.d.ts
246
+ dist/fixture.d.ts
247
+ dist/index.d.ts
248
+ dist/index.js
249
+ dist/mdx.d.ts
250
+ dist/resolver.d.ts
251
+ dist/types-config.d.ts
252
+ dist/types.d.ts
253
+ LICENSE
254
+ package.json
255
+ README.md
256
+ ```
257
+
258
+ Before the fix the same command produced 4 files (LICENSE +
259
+ README + package.json + bin/annot-docs.mjs), no compiled code.
260
+
261
+ - Updated dependencies [806badc]
262
+ - Updated dependencies [2e8d397]
263
+ - Updated dependencies [780985d]
264
+ - Updated dependencies [df1a429]
265
+ - Updated dependencies [2e92c97]
266
+ - Updated dependencies [5e74421]
267
+ - Updated dependencies [4768855]
268
+ - Updated dependencies [657a685]
269
+ - @ingcreators/annot-annotator@0.5.0
270
+ - @ingcreators/annot-core@0.2.0
271
+ - @ingcreators/annot-product-docs@0.2.0
package/README.md CHANGED
@@ -9,7 +9,7 @@ docs-MDX components into an Astro site and ships an Image Service
9
9
  that renders annotated PNGs from `<Screen>` blocks at build time.
10
10
 
11
11
  Phase 2 of
12
- [`docs/plans/living-product-docs.md`](https://github.com/ingcreators/annot/blob/main/docs/plans/living-product-docs.md).
12
+ [`docs/plans/_done/living-product-docs.md`](https://github.com/ingcreators/annot/blob/main/docs/plans/_done/living-product-docs.md).
13
13
 
14
14
  ## Install
15
15
 
@@ -89,6 +89,42 @@ When the stored snapshot lacks `[box=x,y,w,h]` markers (no
89
89
  Playwright tour has run yet), the function returns the base
90
90
  PNG verbatim with `hadBoundingBoxes: false`.
91
91
 
92
+ ## Playwright fixture (`/playwright` subpath)
93
+
94
+ For Playwright specs producing screenshots of a documented
95
+ screen, the `./playwright` subpath ships an extended `test`
96
+ fixture that intercepts `page.screenshot()` and
97
+ `locator.screenshot()`. Calls carrying a compositional
98
+ `annot: { mdx | overlays | tags | editable }` option get a
99
+ one-line capture pipeline that replaces the previous
100
+ `page.screenshot` + `screen.capture` + `renderAnnotatedScreen`
101
+ + `writeFile` quartet:
102
+
103
+ ```ts
104
+ import { test } from "@ingcreators/annot-product-docs-astro/playwright";
105
+
106
+ test("app overview", async ({ page }) => {
107
+ await page.goto("https://annot.work/app/");
108
+ await page.screenshot({
109
+ path: "public/app/shots/app-overview.png",
110
+ annot: {
111
+ mdx: { id: "app-overview", path: "src/content/docs/app/index.mdx" },
112
+ tags: { source: "docs-tour", capturedAt: new Date().toISOString() },
113
+ },
114
+ });
115
+ });
116
+ ```
117
+
118
+ Calls without `annot` (or with `annot: true` / `{}`) fall
119
+ through to vanilla Playwright byte-for-byte — codegen-emitted
120
+ calls keep working unedited. See the docs at
121
+ [`annot.work/docs/product-docs/playwright-fixture/`](https://annot.work/docs/product-docs/playwright-fixture/)
122
+ for the compositional vocabulary, locator screenshot semantics,
123
+ and the codegen→hand-edit workflow.
124
+
125
+ `@playwright/test` is an **optional peer** — Astro-only users
126
+ don't need to install it.
127
+
92
128
  ## Tier
93
129
 
94
130
  Tier B-render — Astro build-time, no live editor.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Bumped any time the rendering pipeline changes the bytes it
3
+ * produces for the same input. Forces a cold cache on every
4
+ * consumer the next time they build. Keep this small (a single
5
+ * digit per breaking change) — large jumps don't help and just
6
+ * obscure the upgrade history.
7
+ */
8
+ export declare const RENDER_PIPELINE_VERSION = 1;
9
+ export interface CacheKeyInput {
10
+ mdxSource: string;
11
+ screenId: string;
12
+ /**
13
+ * Whether the render targets a re-editable PNG (vs the default flat
14
+ * raster). Folded into the cache key so flat / editable variants of
15
+ * the same screen don't collide. Defaults to `false` to preserve the
16
+ * historical key shape for flat callers.
17
+ */
18
+ editable?: boolean;
19
+ }
20
+ /**
21
+ * Compute the cache key for a `<Screen>` block. Stable across
22
+ * runs as long as the MDX source bytes and the screen id are
23
+ * unchanged.
24
+ */
25
+ export declare function cacheKey(input: CacheKeyInput): string;
26
+ export interface FileCache {
27
+ /** Returns the cached bytes for `key`, or `null` on a miss. */
28
+ get(key: string): Promise<Uint8Array | null>;
29
+ /** Writes `bytes` under `key`. Overwrites silently. */
30
+ set(key: string, bytes: Uint8Array): Promise<void>;
31
+ /** Filesystem path the cache writes into. */
32
+ readonly dir: string;
33
+ }
34
+ /**
35
+ * Disk-backed cache rooted at `dir`. The directory is created on
36
+ * first `set`; reads of nonexistent paths return `null`. Files
37
+ * are flat (no subdirs), one per key — SHA-256 collisions are
38
+ * a non-concern given the keyspace.
39
+ */
40
+ export declare function createFileCache(dir: string): FileCache;
41
+ /**
42
+ * In-memory cache — useful in tests and as a fallback when the
43
+ * caller doesn't want disk persistence. Same shape as the disk
44
+ * cache for swap-in convenience.
45
+ */
46
+ export declare function createMemoryCache(): FileCache;
@@ -0,0 +1,34 @@
1
+ ---
2
+ // `<HistoryEntry>` — single revision-history row.
3
+ //
4
+ // MDX usage (inside a `role: history` doc):
5
+ // <HistoryEntry version="1.0" date="2026-03-15" author="Alice">
6
+ // Initial draft, requirements v2.0.
7
+ // </HistoryEntry>
8
+ //
9
+ // Multiple entries stack vertically; the Excel adapter renders
10
+ // them into the workbook's history sheet via the `annotHistory`
11
+ // named range.
12
+
13
+ interface Props {
14
+ version: string;
15
+ date: string;
16
+ author?: string;
17
+ }
18
+ const { version, date, author } = Astro.props;
19
+ ---
20
+ <div
21
+ class="annot-history-entry"
22
+ data-version={version}
23
+ data-date={date}
24
+ data-author={author}
25
+ >
26
+ <div class="annot-history-meta">
27
+ <span class="annot-history-version">v{version}</span>
28
+ <time class="annot-history-date" datetime={date}>{date}</time>
29
+ {author && <span class="annot-history-author">{author}</span>}
30
+ </div>
31
+ <div class="annot-history-body">
32
+ <slot />
33
+ </div>
34
+ </div>
@@ -0,0 +1,50 @@
1
+ ---
2
+ // `<Overlay>` — caption for one numbered callout on a screen.
3
+ //
4
+ // MDX usage:
5
+ // <Overlay match={{ role: "textbox", name: "Email" }}
6
+ // intent="required" number={1}>
7
+ // **Email** — enter your registered email.
8
+ // </Overlay>
9
+ //
10
+ // Build-time positioning of the visual callout on the
11
+ // screenshot is handled by the Image Service. This component
12
+ // emits the prose body as a `<li>` in a description list
13
+ // alongside the screen — readers see the screenshot + a
14
+ // numbered legend in parallel.
15
+
16
+ interface Match {
17
+ role: string;
18
+ name: string;
19
+ under?: Match;
20
+ }
21
+ type Intent =
22
+ | "info"
23
+ | "warning"
24
+ | "error"
25
+ | "success"
26
+ | "neutral"
27
+ | "required"
28
+ | "action";
29
+
30
+ interface Props {
31
+ match: Match;
32
+ intent?: Intent;
33
+ number?: number;
34
+ }
35
+ const { match, intent, number } = Astro.props;
36
+ ---
37
+ <li
38
+ class="annot-overlay"
39
+ data-intent={intent}
40
+ data-overlay-number={number}
41
+ data-match-role={match.role}
42
+ data-match-name={match.name}
43
+ >
44
+ {number !== undefined && (
45
+ <span class="annot-overlay-number" aria-hidden="true">{number}</span>
46
+ )}
47
+ <div class="annot-overlay-body">
48
+ <slot />
49
+ </div>
50
+ </li>
@@ -0,0 +1,27 @@
1
+ ---
2
+ // `<Screen>` — annotated screenshot block.
3
+ //
4
+ // MDX usage:
5
+ // <Screen id="login" src="./shots/login.png">
6
+ // <Overlay match={...} number={1}>...</Overlay>
7
+ // </Screen>
8
+ //
9
+ // The annotated PNG is produced at build time by the Image
10
+ // Service in `@ingcreators/annot-product-docs-astro` (see
11
+ // `../render.ts`). At render time we just emit an `<img>` that
12
+ // points at the cache hash; the Astro pipeline resolves the
13
+ // final URL.
14
+
15
+ interface Props {
16
+ id: string;
17
+ src: string;
18
+ alt?: string;
19
+ }
20
+ const { id, src, alt } = Astro.props;
21
+ ---
22
+ <figure class="annot-screen" data-screen-id={id}>
23
+ <img src={src} alt={alt ?? `Screen ${id}`} loading="lazy" />
24
+ <div class="annot-screen-overlays" data-screen-overlays={id}>
25
+ <slot />
26
+ </div>
27
+ </figure>
@@ -0,0 +1,58 @@
1
+ ---
2
+ // `<ScreenList>` — auto-enumerated list of every `role: screen`
3
+ // MDX in the same `book`.
4
+ //
5
+ // MDX usage (inside a `role: list` doc):
6
+ // <ScreenList book="Screen spec" sort="byId" entries={...} />
7
+ //
8
+ // The `entries` prop is normally populated by the Astro
9
+ // integration's `astro:config:setup` walker (Phase 4 polish).
10
+ // Until that's wired up, callers pass `entries` explicitly so
11
+ // the component renders deterministically in tests and in
12
+ // dev-mode-without-build.
13
+
14
+ export interface ScreenListEntry {
15
+ id: string;
16
+ title?: string;
17
+ href?: string;
18
+ order?: number;
19
+ }
20
+
21
+ interface Props {
22
+ book?: string;
23
+ sort?: "byId" | "byOrder" | "byFilePath";
24
+ entries: ScreenListEntry[];
25
+ }
26
+ const { book, sort = "byId", entries } = Astro.props;
27
+
28
+ const sortedEntries = [...entries].sort((a, b) => {
29
+ if (sort === "byOrder") {
30
+ return (a.order ?? 100) - (b.order ?? 100);
31
+ }
32
+ // byId / byFilePath both fall back to alphabetical for
33
+ // determinism.
34
+ return a.id.localeCompare(b.id);
35
+ });
36
+ ---
37
+ <table class="annot-screen-list" data-book={book} data-sort={sort}>
38
+ <thead>
39
+ <tr>
40
+ <th>ID</th>
41
+ <th>Screen</th>
42
+ </tr>
43
+ </thead>
44
+ <tbody>
45
+ {sortedEntries.map((entry) => (
46
+ <tr data-screen-id={entry.id}>
47
+ <td>{entry.id}</td>
48
+ <td>
49
+ {entry.href ? (
50
+ <a href={entry.href}>{entry.title ?? entry.id}</a>
51
+ ) : (
52
+ <span>{entry.title ?? entry.id}</span>
53
+ )}
54
+ </td>
55
+ </tr>
56
+ ))}
57
+ </tbody>
58
+ </table>
@@ -0,0 +1,48 @@
1
+ ---
2
+ // `<Transition>` — describes one screen-to-screen transition.
3
+ //
4
+ // MDX usage:
5
+ // <Transition
6
+ // trigger={{ role: "button", name: "Sign in" }}
7
+ // on="click"
8
+ // to="dashboard"
9
+ // >
10
+ // Successful login redirects to the dashboard.
11
+ // </Transition>
12
+ //
13
+ // Renders inline as part of the screen's prose; for a tabular
14
+ // list of every transition on the screen, wrap in
15
+ // `<TransitionTable>` instead.
16
+
17
+ interface Match {
18
+ role: string;
19
+ name: string;
20
+ under?: Match;
21
+ }
22
+
23
+ interface Props {
24
+ trigger: Match;
25
+ on?: string;
26
+ to?: string;
27
+ }
28
+ const { trigger, on, to } = Astro.props;
29
+ ---
30
+ <div
31
+ class="annot-transition"
32
+ data-trigger-role={trigger.role}
33
+ data-trigger-name={trigger.name}
34
+ data-on={on}
35
+ data-to={to}
36
+ >
37
+ <div class="annot-transition-summary">
38
+ <code class="annot-transition-trigger">{trigger.name}</code>
39
+ {on && <span class="annot-transition-event">{on}</span>}
40
+ {to && (
41
+ <span class="annot-transition-arrow" aria-hidden="true">→</span>
42
+ )}
43
+ {to && <code class="annot-transition-target">{to}</code>}
44
+ </div>
45
+ <div class="annot-transition-body">
46
+ <slot />
47
+ </div>
48
+ </div>
@@ -0,0 +1,46 @@
1
+ ---
2
+ // `<TransitionGraph>` — Mermaid-rendered cross-screen graph.
3
+ //
4
+ // MDX usage (typically in a `role: list` doc):
5
+ // <TransitionGraph entries={...} title="Auth flow" />
6
+ //
7
+ // Emits a `<pre class="mermaid">` block containing a Mermaid
8
+ // flowchart definition. Rendering happens client-side via the
9
+ // Mermaid runtime the consumer adds to their Astro site
10
+ // (`<script type="module" src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs">`),
11
+ // or build-time via Mermaid CLI / `@napi-rs/canvas` if the
12
+ // consumer opts in.
13
+
14
+ export interface GraphEdge {
15
+ from: string;
16
+ to: string;
17
+ label?: string;
18
+ }
19
+
20
+ interface Props {
21
+ entries: GraphEdge[];
22
+ title?: string;
23
+ direction?: "TB" | "LR" | "BT" | "RL";
24
+ }
25
+ const { entries, title, direction = "TB" } = Astro.props;
26
+
27
+ const escape = (id: string): string => id.replace(/[^A-Za-z0-9_]/g, "_");
28
+
29
+ const mermaid = ["flowchart " + direction];
30
+ if (title) {
31
+ mermaid.push(`%% ${title.replace(/\r?\n/g, " ")}`);
32
+ }
33
+ for (const edge of entries) {
34
+ const from = escape(edge.from);
35
+ const to = escape(edge.to);
36
+ const label = edge.label
37
+ ? `|${edge.label.replace(/\|/g, "&#124;")}|`
38
+ : "";
39
+ mermaid.push(` ${from}[${edge.from}] -->${label} ${to}[${edge.to}]`);
40
+ }
41
+ const definition = mermaid.join("\n");
42
+ ---
43
+ <figure class="annot-transition-graph" data-direction={direction}>
44
+ {title && <figcaption>{title}</figcaption>}
45
+ <pre class="mermaid">{definition}</pre>
46
+ </figure>
@@ -0,0 +1,55 @@
1
+ ---
2
+ // `<TransitionTable>` — tabular list of every `<Transition>`
3
+ // on the page. Pass an array of transition descriptors as the
4
+ // `entries` prop; the table renders them with one row per
5
+ // transition. The reader sees "trigger / event / target" as
6
+ // columns plus a body cell with the prose.
7
+ //
8
+ // MDX usage:
9
+ // <TransitionTable
10
+ // entries={[
11
+ // { trigger: "Sign in", on: "click", to: "dashboard", body: "..." },
12
+ // { trigger: "Sign up", on: "click", to: "signup", body: "..." },
13
+ // ]}
14
+ // />
15
+ //
16
+ // The companion `<Transition>` component is the inline variant
17
+ // for free-flowing prose. Both compile to data attributes the
18
+ // reader CSS can style consistently.
19
+
20
+ export interface TransitionEntry {
21
+ trigger: string;
22
+ on?: string;
23
+ to?: string;
24
+ body?: string;
25
+ }
26
+
27
+ interface Props {
28
+ entries: TransitionEntry[];
29
+ }
30
+ const { entries } = Astro.props;
31
+ ---
32
+ <table class="annot-transition-table">
33
+ <thead>
34
+ <tr>
35
+ <th>Trigger</th>
36
+ <th>Event</th>
37
+ <th>Target</th>
38
+ <th>Notes</th>
39
+ </tr>
40
+ </thead>
41
+ <tbody>
42
+ {entries.map((entry) => (
43
+ <tr
44
+ data-trigger={entry.trigger}
45
+ data-on={entry.on}
46
+ data-to={entry.to}
47
+ >
48
+ <td>{entry.trigger}</td>
49
+ <td>{entry.on ?? "—"}</td>
50
+ <td>{entry.to ?? "—"}</td>
51
+ <td set:html={entry.body ?? ""} />
52
+ </tr>
53
+ ))}
54
+ </tbody>
55
+ </table>
@@ -0,0 +1,21 @@
1
+ import { MatchKey } from '@ingcreators/annot-product-docs';
2
+ export type Match = MatchKey;
3
+ export type OverlayIntent = "info" | "warning" | "error" | "success" | "neutral" | "required" | "action";
4
+ export interface TransitionEntry {
5
+ trigger: string;
6
+ on?: string;
7
+ to?: string;
8
+ body?: string;
9
+ }
10
+ export interface ScreenListEntry {
11
+ id: string;
12
+ title?: string;
13
+ href?: string;
14
+ order?: number;
15
+ }
16
+ export interface GraphEdge {
17
+ from: string;
18
+ to: string;
19
+ label?: string;
20
+ }
21
+ export type GraphDirection = "TB" | "LR" | "BT" | "RL";
@@ -0,0 +1,7 @@
1
+ export type { CacheKeyInput, FileCache } from './cache.js';
2
+ export { cacheKey, createFileCache, createMemoryCache, RENDER_PIPELINE_VERSION, } from './cache.js';
3
+ export type { GraphDirection, GraphEdge, Match, OverlayIntent, ScreenListEntry, TransitionEntry, } from './components/types.js';
4
+ export type { ProductDocsIntegrationOptions } from './integration.js';
5
+ export { productDocsIntegration } from './integration.js';
6
+ export type { RenderAnnotatedScreenOptions, RenderResult, } from './render.js';
7
+ export { parseSnapshotBoxes, renderAnnotatedScreen, resolveMdxAnnotations, svgFromBboxAnnotations, } from './render.js';
package/dist/index.js ADDED
@@ -0,0 +1,180 @@
1
+ import { createHash as e } from "node:crypto";
2
+ import { mkdir as t, readFile as n, writeFile as r } from "node:fs/promises";
3
+ import { dirname as i, isAbsolute as a, join as o, resolve as s } from "node:path";
4
+ import { bboxAnnotationsToSvg as c, createAnnotator as l } from "@ingcreators/annot-annotator";
5
+ import { parseMdxFile as u } from "@ingcreators/annot-product-docs";
6
+ //#region src/cache.ts
7
+ var d = 1;
8
+ function f(t) {
9
+ let n = e("sha256");
10
+ return n.update("v1\n"), n.update(t.mdxSource), n.update("\0"), n.update(t.screenId), t.editable && n.update("\0editable"), n.digest("hex");
11
+ }
12
+ function p(e) {
13
+ return {
14
+ dir: e,
15
+ async get(t) {
16
+ try {
17
+ let r = await n(o(e, `${t}.png`));
18
+ return new Uint8Array(r.buffer, r.byteOffset, r.byteLength);
19
+ } catch (e) {
20
+ if (e.code === "ENOENT") return null;
21
+ throw e;
22
+ }
23
+ },
24
+ async set(n, a) {
25
+ let s = o(e, `${n}.png`);
26
+ await t(i(s), { recursive: !0 }), await r(s, a);
27
+ }
28
+ };
29
+ }
30
+ function m() {
31
+ let e = /* @__PURE__ */ new Map();
32
+ return {
33
+ dir: ":memory:",
34
+ async get(t) {
35
+ return e.get(t) ?? null;
36
+ },
37
+ async set(t, n) {
38
+ e.set(t, n);
39
+ }
40
+ };
41
+ }
42
+ //#endregion
43
+ //#region src/integration.ts
44
+ var h = "@ingcreators/annot-product-docs-astro";
45
+ function g(e = {}) {
46
+ let t = {
47
+ contentDir: e.contentDir ?? "docs",
48
+ configPath: e.configPath ?? "annot-docs.config.ts",
49
+ verbose: e.verbose ?? !1
50
+ };
51
+ return {
52
+ name: h,
53
+ hooks: { "astro:config:setup": ({ logger: e }) => {
54
+ t.verbose && e.info(`${h} installed — contentDir=${t.contentDir} configPath=${t.configPath}`);
55
+ } }
56
+ };
57
+ }
58
+ //#endregion
59
+ //#region src/render.ts
60
+ async function _(e) {
61
+ let t = e.cwd ?? process.cwd(), n = a(e.mdxPath) ? e.mdxPath : s(t, e.mdxPath), r = await u(n);
62
+ if (!r) throw Error(`renderAnnotatedScreen: ${e.mdxPath} has no \`annot:\` frontmatter — cannot render.`);
63
+ let o = r.screens.find((t) => t.id === e.screenId);
64
+ if (!o) throw Error(`renderAnnotatedScreen: ${e.mdxPath} has no <Screen id="${e.screenId}"> block.`);
65
+ let c = v(e.editable), d = f({
66
+ mdxSource: r.source,
67
+ screenId: e.screenId,
68
+ editable: c !== null
69
+ });
70
+ if (e.cache) {
71
+ let t = await e.cache.get(d);
72
+ if (t) return {
73
+ bytes: t,
74
+ fromCache: !0,
75
+ hadBoundingBoxes: !0
76
+ };
77
+ }
78
+ let p = e.basePngBytes ?? await T(r, o.src, i(n)), m = E(p), h = b(r.commentBlocks.snapshot ?? ""), g = S(o.overlays, h, m), _, x;
79
+ if (g.length === 0) if (x = !1, c !== null) {
80
+ let e = `data:image/png;base64,${Buffer.from(p).toString("base64")}`;
81
+ _ = l({ loadSystemFonts: !0 }).toEditablePng({
82
+ originalDataUrl: e,
83
+ annotationsSvg: y(),
84
+ width: m.width,
85
+ height: m.height,
86
+ tags: c.tags
87
+ });
88
+ } else _ = p;
89
+ else {
90
+ let e = `data:image/png;base64,${Buffer.from(p).toString("base64")}`, t = C(g), n = l({ loadSystemFonts: !0 }), r = {
91
+ originalDataUrl: e,
92
+ annotationsSvg: t,
93
+ width: m.width,
94
+ height: m.height
95
+ };
96
+ _ = c === null ? n.toPng(r) : n.toEditablePng({
97
+ ...r,
98
+ tags: c.tags
99
+ }), x = !0;
100
+ }
101
+ return e.cache && await e.cache.set(d, _), {
102
+ bytes: _,
103
+ fromCache: !1,
104
+ hadBoundingBoxes: x
105
+ };
106
+ }
107
+ function v(e) {
108
+ return e ? e === !0 ? {} : e : null;
109
+ }
110
+ function y() {
111
+ return "<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>";
112
+ }
113
+ function b(e) {
114
+ let t = [];
115
+ for (let n of e.split(/\r?\n/)) {
116
+ if (!n.trim()) continue;
117
+ let e = n.match(/^\s*-\s+([a-z]+)(?:\s+"([^"]*?)")?/);
118
+ if (!e) continue;
119
+ let r = e[1] ?? "", i = e[2] ?? "", a = n.match(/\[ref=(e\d+)\]/)?.[1], o = n.match(/\[box=(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)\]/);
120
+ !a || !o || t.push({
121
+ role: r,
122
+ name: i,
123
+ ref: a,
124
+ box: {
125
+ x: Number.parseFloat(o[1]),
126
+ y: Number.parseFloat(o[2]),
127
+ width: Number.parseFloat(o[3]),
128
+ height: Number.parseFloat(o[4])
129
+ }
130
+ });
131
+ }
132
+ return t;
133
+ }
134
+ async function x(e) {
135
+ let t = e.cwd ?? process.cwd(), n = await u(a(e.mdxPath) ? e.mdxPath : s(t, e.mdxPath));
136
+ if (!n) throw Error(`resolveMdxAnnotations: ${e.mdxPath} has no \`annot:\` frontmatter — cannot resolve.`);
137
+ let r = n.screens.find((t) => t.id === e.screenId);
138
+ if (!r) throw Error(`resolveMdxAnnotations: ${e.mdxPath} has no <Screen id="${e.screenId}"> block.`);
139
+ let i = b(n.commentBlocks.snapshot ?? "");
140
+ return S(r.overlays, i, e.dims);
141
+ }
142
+ function S(e, t, n) {
143
+ let r = [], i = 1;
144
+ for (let a of e) {
145
+ let e = t.find((e) => e.role === a.match.role && e.name === a.match.name);
146
+ if (!e) continue;
147
+ let o = a.number ?? i++;
148
+ r.push({
149
+ type: "numberedBadge",
150
+ bbox: e.box,
151
+ number: o,
152
+ placement: "auto",
153
+ imageWidth: n.width,
154
+ imageHeight: n.height,
155
+ intent: a.intent === "action" ? "warning" : a.intent === "required" ? "error" : "info"
156
+ });
157
+ }
158
+ return r;
159
+ }
160
+ function C(e) {
161
+ return `<svg xmlns="http://www.w3.org/2000/svg">${c(e)}</svg>`;
162
+ }
163
+ function w(e) {
164
+ return e.length === 0 ? "<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>" : `<svg xmlns="http://www.w3.org/2000/svg">${c(e)}</svg>`;
165
+ }
166
+ async function T(e, t, r) {
167
+ if (!t) throw Error("renderAnnotatedScreen: <Screen src=...> is required — cannot render without a base image.");
168
+ let i = await n(a(t) ? t : s(r, t));
169
+ return new Uint8Array(i.buffer, i.byteOffset, i.byteLength);
170
+ }
171
+ function E(e) {
172
+ if (e.length < 24) throw Error("renderAnnotatedScreen: base PNG too short to contain IHDR.");
173
+ let t = new DataView(e.buffer, e.byteOffset, e.byteLength);
174
+ return {
175
+ width: t.getUint32(16, !1),
176
+ height: t.getUint32(20, !1)
177
+ };
178
+ }
179
+ //#endregion
180
+ export { d as RENDER_PIPELINE_VERSION, f as cacheKey, p as createFileCache, m as createMemoryCache, b as parseSnapshotBoxes, g as productDocsIntegration, _ as renderAnnotatedScreen, x as resolveMdxAnnotations, w as svgFromBboxAnnotations };
@@ -0,0 +1,46 @@
1
+ import { AstroIntegration } from 'astro';
2
+ export interface ProductDocsIntegrationOptions {
3
+ /**
4
+ * Directory (relative to the Astro project root) that contains
5
+ * the `annot:`-frontmatter MDX files the integration should
6
+ * walk for the Image Service + component-config defaults.
7
+ * Default: `"docs"`.
8
+ */
9
+ contentDir?: string;
10
+ /**
11
+ * Path to the `annot-docs.config.ts` config file. Default:
12
+ * `"annot-docs.config.ts"`. Future PRs read this for the
13
+ * `meta` defaults + per-book template lookups.
14
+ */
15
+ configPath?: string;
16
+ /**
17
+ * If `true`, the integration logs a debug line on every
18
+ * config:setup. Useful for verifying the integration is
19
+ * installed correctly in an Astro project. Default: `false`.
20
+ */
21
+ verbose?: boolean;
22
+ }
23
+ /**
24
+ * Astro integration factory. Drop into `astro.config.mjs`:
25
+ *
26
+ * ```js
27
+ * import { defineConfig } from "astro/config";
28
+ * import { productDocsIntegration } from "@ingcreators/annot-product-docs-astro";
29
+ *
30
+ * export default defineConfig({
31
+ * integrations: [productDocsIntegration()],
32
+ * });
33
+ * ```
34
+ *
35
+ * Phase 2 PR 1 ships the scaffold — the hooks are no-ops aside
36
+ * from optional verbose logging. PR 2 adds the Image Service;
37
+ * PR 3 wires up the seven docs components.
38
+ */
39
+ export declare function productDocsIntegration(options?: ProductDocsIntegrationOptions): AstroIntegration;
40
+ /**
41
+ * Default-export shape some Astro integration authors prefer.
42
+ * Both `import productDocsIntegration from ...` (default) and
43
+ * `import { productDocsIntegration }` (named) resolve to the
44
+ * same factory; consumers can pick whichever feels natural.
45
+ */
46
+ export default productDocsIntegration;
@@ -0,0 +1,72 @@
1
+ import { BboxAnnotation } from '@ingcreators/annot-annotator';
2
+ /**
3
+ * Compositional options for `page.screenshot({ annot })` /
4
+ * `locator.screenshot({ annot })`. Each field is an independent
5
+ * contribution to the embedded XMP record:
6
+ *
7
+ * - `mdx` — refresh + resolve MDX `<Overlay>` annotations
8
+ * - `overlays`— inline `BboxAnnotation[]` (the DSL accepted by
9
+ * `@ingcreators/annot-annotator`)
10
+ * - `tags` — provenance metadata written verbatim into the XMP
11
+ * - `editable`— bake-vs-preserve toggle (default `true`)
12
+ *
13
+ * `annot: true` / `annot: {}` is treated as a no-op shorthand — the
14
+ * fixture detects no contribution and the screenshot falls through
15
+ * to vanilla Playwright behaviour.
16
+ */
17
+ export interface AnnotScreenshotOptions {
18
+ /** Refresh the MDX `annot:snapshot` block and resolve the
19
+ * `<Screen id>`'s overlays. The MDX file is rewritten in-place
20
+ * with the current page's aria-snapshot before overlays resolve.
21
+ */
22
+ mdx?: {
23
+ id: string;
24
+ path: string;
25
+ };
26
+ /** Caller-supplied annotations — merged with MDX-derived ones if
27
+ * `mdx` is also set. Same DSL `@ingcreators/annot-annotator`
28
+ * accepts. */
29
+ overlays?: BboxAnnotation[];
30
+ /** Provenance metadata written verbatim into the PNG's XMP. The
31
+ * fixture adds no defaults; callers who want `WELL_KNOWN_TAG_KEYS`
32
+ * (`source` / `screen` / `capturedAt` / `commit`) write them. */
33
+ tags?: Record<string, string>;
34
+ /** When `true` (default): annotations stored as SVG in XMP +
35
+ * original capture embedded → re-editable in Annot Cloud. When
36
+ * `false`: annotations baked into the visible pixels, no XMP
37
+ * layer, no embedded original — flat PNG, no round-trip. */
38
+ editable?: boolean;
39
+ }
40
+ declare module "@playwright/test" {
41
+ interface PageScreenshotOptions {
42
+ annot?: AnnotScreenshotOptions;
43
+ }
44
+ interface LocatorScreenshotOptions {
45
+ annot?: AnnotScreenshotOptions;
46
+ }
47
+ }
48
+ /**
49
+ * Idempotent prototype patch — wrap `screenshot` to intercept the
50
+ * `annot` opt while falling through to the original method when
51
+ * absent / empty. Exported for unit tests; production usage flows
52
+ * through the fixture `extend({ page })` body which calls this once
53
+ * per worker on the first page (and once for the locator prototype).
54
+ */
55
+ export declare function patchScreenshot(proto: {
56
+ screenshot: (opts?: unknown) => Promise<Buffer>;
57
+ }): void;
58
+ /**
59
+ * `test = base.extend({ page })` — drop-in for `@playwright/test`'s
60
+ * `test` plus a one-time patch of `Page.prototype.screenshot` AND
61
+ * `Locator.prototype.screenshot` (per worker process) so calls
62
+ * carrying `annot: { ... }` run the annot pipeline above.
63
+ *
64
+ * The base `test` is `@ingcreators/annot-product-docs`'s test which
65
+ * already extends `@ingcreators/annot-playwright` — callers get
66
+ * `page` + `annotator` + `screen` plus the annot-aware screenshot.
67
+ */
68
+ export declare const test: import('@playwright/test').TestType<import('@playwright/test').PlaywrightTestArgs & import('@playwright/test').PlaywrightTestOptions & {
69
+ annotator: import('@ingcreators/annot-playwright').PlaywrightAnnotator;
70
+ } & {
71
+ screen: import('@ingcreators/annot-product-docs').Screen;
72
+ }, import('@playwright/test').PlaywrightWorkerArgs & import('@playwright/test').PlaywrightWorkerOptions>;
@@ -0,0 +1,5 @@
1
+ export { expect } from '@playwright/test';
2
+ export type { AnnotScreenshotOptions } from './fixture.js';
3
+ export { patchScreenshot, test } from './fixture.js';
4
+ export type { Clip, RebaseResult } from './rebase.js';
5
+ export { describeAnnotation, rebaseAnnotations } from './rebase.js';
@@ -0,0 +1,36 @@
1
+ import { BboxAnnotation } from '@ingcreators/annot-annotator';
2
+ export interface Clip {
3
+ x: number;
4
+ y: number;
5
+ width: number;
6
+ height: number;
7
+ }
8
+ export interface RebaseResult {
9
+ /** Annotations whose coordinates were rebased into clip-space. */
10
+ kept: BboxAnnotation[];
11
+ /** Annotations that fell outside `clip` and were dropped. The
12
+ * caller surfaces these as `RenderResult.droppedOverlays` /
13
+ * `test.info().annotations` warnings. */
14
+ dropped: BboxAnnotation[];
15
+ }
16
+ /**
17
+ * Rebase + filter a `BboxAnnotation[]` against a clip rectangle.
18
+ *
19
+ * - `kept` = annotations whose visible coords were translated by
20
+ * `(-clip.x, -clip.y)`. The new coords are in the clipped image's
21
+ * coordinate space (0,0 at the top-left of the screenshot).
22
+ * - `dropped` = annotations whose bbox / endpoints fell outside the
23
+ * clip. Returned verbatim (un-rebased) so the caller can log
24
+ * which originals were skipped.
25
+ *
26
+ * `numberedBadge`'s `imageWidth` / `imageHeight` are also rebased
27
+ * to match the clip dimensions, so `placement: "auto"` picks the
28
+ * corner against the cropped image edge rather than the page edge.
29
+ */
30
+ export declare function rebaseAnnotations(annotations: BboxAnnotation[], clip: Clip): RebaseResult;
31
+ /**
32
+ * Format a short identifier for an annotation so dropped diagnostics
33
+ * stay readable. We don't have a stable id field on the DSL types —
34
+ * use `type` + first bbox coords as a heuristic.
35
+ */
36
+ export declare function describeAnnotation(ann: BboxAnnotation): string;
@@ -0,0 +1,129 @@
1
+ import { BboxNumberedBadgeAnnotation } from '@ingcreators/annot-annotator';
2
+ import { FileCache } from './cache.js';
3
+ export interface RenderAnnotatedScreenOptions {
4
+ /** Path to the `.mdx` file (absolute or relative to cwd). */
5
+ mdxPath: string;
6
+ /** Must match a `<Screen id="...">` inside the MDX. */
7
+ screenId: string;
8
+ /** Optional cache. When supplied, identical inputs short-circuit to a cached buffer. */
9
+ cache?: FileCache;
10
+ /** Override the cwd used to resolve relative `mdxPath`. */
11
+ cwd?: string;
12
+ /**
13
+ * Override the base PNG that `<Screen src>` would otherwise
14
+ * point at. Useful when the on-disk PNG and the served URL
15
+ * diverge — e.g. an Astro site that serves PNGs from
16
+ * `public/` while the MDX `<Screen src>` carries the
17
+ * absolute browser URL. The caller hands in the bytes
18
+ * directly and `loadBasePng` is skipped.
19
+ */
20
+ basePngBytes?: Uint8Array;
21
+ /**
22
+ * Emit a re-editable PNG instead of a flat raster. When set,
23
+ * the visible pixels match the default rasterised output but
24
+ * the file additionally carries the original (un-annotated)
25
+ * base image + the annotations SVG in the PNG's XMP metadata
26
+ * + custom `svGo` chunk, so re-opening the file in Annot Cloud
27
+ * / the editor / VSCode hosts restores the annotations as
28
+ * selectable / movable objects.
29
+ *
30
+ * Pass `true` for the defaults, or an object to set the optional
31
+ * `tags` field on the embedded XMP (e.g. `source` / `screen` /
32
+ * `capturedAt` provenance metadata — see
33
+ * `@ingcreators/annot-annotator`'s `EditableInput`).
34
+ *
35
+ * When no overlay bboxes resolve (the snapshot block has no
36
+ * `[box=…]` markers yet), the base PNG is wrapped as an editable
37
+ * PNG with an empty annotations layer — re-opening still works,
38
+ * the editor just shows the un-annotated capture.
39
+ */
40
+ editable?: boolean | {
41
+ tags?: Record<string, string>;
42
+ };
43
+ }
44
+ export interface RenderResult {
45
+ /** Annotated PNG bytes. */
46
+ bytes: Uint8Array;
47
+ /** Whether the result came from cache. Useful for logging. */
48
+ fromCache: boolean;
49
+ /**
50
+ * `false` when the source had no bbox markers and we
51
+ * returned the base PNG verbatim. The Image Service surfaces
52
+ * this so an Astro plugin can warn the author to run the
53
+ * Playwright tour.
54
+ */
55
+ hadBoundingBoxes: boolean;
56
+ }
57
+ /**
58
+ * Top-level renderer used by both the Astro Image Service and
59
+ * vitest test cases. Steps:
60
+ *
61
+ * 1. Parse the MDX via `parseMdxFile`.
62
+ * 2. Find the `<Screen id>` matching `screenId`. Throw with a
63
+ * friendly diagnostic if missing.
64
+ * 3. Compute the cache key from the MDX source + `screenId`.
65
+ * On a cache hit, return the bytes immediately.
66
+ * 4. Load the base PNG referenced by `<Screen src>`.
67
+ * 5. Parse the stored `annot:snapshot` block for bbox markers.
68
+ * If absent, return base PNG verbatim.
69
+ * 6. Build a typed `BboxCalloutAnnotation[]` from the overlays
70
+ * + bbox data, hand it to `createAnnotator().toPng(...)`,
71
+ * and return the result.
72
+ */
73
+ export declare function renderAnnotatedScreen(options: RenderAnnotatedScreenOptions): Promise<RenderResult>;
74
+ interface BoxedEntry {
75
+ role: string;
76
+ name: string;
77
+ ref: string;
78
+ box: {
79
+ x: number;
80
+ y: number;
81
+ width: number;
82
+ height: number;
83
+ };
84
+ }
85
+ /**
86
+ * Parse aria-snapshot YAML for entries that carry both `[ref=…]`
87
+ * and `[box=x,y,w,h]` markers. `box` is the Playwright addition
88
+ * available when `ariaSnapshot({ boxes: true })` was passed.
89
+ *
90
+ * Exposed for unit testing. Returns an empty array if no entries
91
+ * have boxes — the caller falls back to a non-annotated render.
92
+ */
93
+ export declare function parseSnapshotBoxes(yaml: string): BoxedEntry[];
94
+ /**
95
+ * Resolve the `<Overlay>` blocks inside `screenId` against the MDX
96
+ * file's stored `annot:snapshot` block and return a typed annotation
97
+ * array ready to feed into `createAnnotator().toEditablePng()`.
98
+ *
99
+ * Exposed so the Playwright fixture can compose MDX-derived overlays
100
+ * with caller-supplied inline overlays before rasterisation. Throws
101
+ * the same diagnostics `renderAnnotatedScreen` would (no annot
102
+ * frontmatter / no matching `<Screen id>`).
103
+ *
104
+ * Returns an empty array when no `<Overlay>` matches a bbox marker
105
+ * — e.g. the snapshot was never captured, or `<Overlay match>`
106
+ * targets an element that's no longer in the page.
107
+ */
108
+ export declare function resolveMdxAnnotations(opts: {
109
+ mdxPath: string;
110
+ screenId: string;
111
+ dims: {
112
+ width: number;
113
+ height: number;
114
+ };
115
+ cwd?: string;
116
+ }): Promise<BboxNumberedBadgeAnnotation[]>;
117
+ /**
118
+ * Wrap a generic `BboxAnnotation[]` (the union DSL accepted by
119
+ * `@ingcreators/annot-annotator`) into a single-root `<svg>` ready
120
+ * for the annotator's `annotationsSvg` input. Mirrors `svgFromBadges`
121
+ * but stays type-permissive — the Playwright fixture's inline overlays
122
+ * can be any `BboxAnnotation` shape, not just numbered badges.
123
+ *
124
+ * Returns the empty wrapper `<svg/>` when `annotations` is empty —
125
+ * lets the editor open the file with no annotations layer rather
126
+ * than throwing.
127
+ */
128
+ export declare function svgFromBboxAnnotations(annotations: import('@ingcreators/annot-annotator').BboxAnnotation[]): string;
129
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingcreators/annot-product-docs-astro",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Astro integration for `@ingcreators/annot-product-docs`. Wires the MDX components (`<Screen>` / `<Overlay>` / `<Transition>` / `<HistoryEntry>` / `<ScreenList>` / `<TransitionGraph>`) into an Astro site and supplies the Image Service that renders annotated PNGs from `<Screen>` blocks at build time. Phase 2 of docs/plans/living-product-docs.md.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "ingcreators",
@@ -22,7 +22,7 @@
22
22
  "docs",
23
23
  "documentation",
24
24
  "screen-spec",
25
- "画面設計書",
25
+ "screen-specifications",
26
26
  "image-service"
27
27
  ],
28
28
  "type": "module",
@@ -33,6 +33,10 @@
33
33
  "types": "./dist/index.d.ts",
34
34
  "default": "./dist/index.js"
35
35
  },
36
+ "./playwright": {
37
+ "types": "./dist/playwright/index.d.ts",
38
+ "default": "./dist/playwright/index.js"
39
+ },
36
40
  "./components/Screen.astro": "./dist/components/Screen.astro",
37
41
  "./components/Overlay.astro": "./dist/components/Overlay.astro",
38
42
  "./components/Transition.astro": "./dist/components/Transition.astro",
@@ -50,17 +54,25 @@
50
54
  "access": "public"
51
55
  },
52
56
  "devDependencies": {
53
- "astro": "^5.0.0",
57
+ "@playwright/test": "^1.50.0",
58
+ "astro": "^6.3.6",
54
59
  "typescript": "^6.0.3",
55
- "vite": "^6.4.2",
56
- "vite-plugin-dts": "^5.0.0"
60
+ "vite": "^8.0.13",
61
+ "vite-plugin-dts": "^5.0.1"
57
62
  },
58
63
  "dependencies": {
59
- "@ingcreators/annot-annotator": "0.3.0",
60
- "@ingcreators/annot-product-docs": "0.1.0"
64
+ "@ingcreators/annot-product-docs": "0.2.0",
65
+ "@ingcreators/annot-annotator": "0.5.0",
66
+ "@ingcreators/annot-core": "0.2.0"
61
67
  },
62
68
  "peerDependencies": {
63
- "astro": "^5.0.0"
69
+ "@playwright/test": "^1.42.0",
70
+ "astro": "^5.0.0 || ^6.0.0"
71
+ },
72
+ "peerDependenciesMeta": {
73
+ "@playwright/test": {
74
+ "optional": true
75
+ }
64
76
  },
65
77
  "scripts": {
66
78
  "build": "vite build",