@ingcreators/annot-product-docs-astro 0.2.2 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,165 @@
1
1
  # @ingcreators/annot-product-docs-astro
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [6124d59]
8
+ - Updated dependencies [b5d52f6]
9
+ - Updated dependencies [fa712fd]
10
+ - Updated dependencies [0c7ac26]
11
+ - Updated dependencies [f09a6b1]
12
+ - Updated dependencies [0d19345]
13
+ - Updated dependencies [64dc6e8]
14
+ - Updated dependencies [691bec5]
15
+ - Updated dependencies [9697f27]
16
+ - Updated dependencies [266b05a]
17
+ - @ingcreators/annot-annotator@0.6.0
18
+ - @ingcreators/annot-product-docs@0.4.0
19
+ - @ingcreators/annot-core@0.3.0
20
+ - @ingcreators/annot-playwright@0.4.1
21
+
22
+ ## 0.3.0
23
+
24
+ ### Minor Changes
25
+
26
+ - f5dc7cb: **`@ingcreators/annot-product-docs-astro/playwright` becomes a
27
+ deprecated re-export** — Phase 4 of
28
+ `docs/plans/playwright-screenshot-fixture-relayer.md`. The fixture
29
+ that originally lived here moved to `@ingcreators/annot-playwright`
30
+ (generic patch) + `@ingcreators/annot-product-docs` (MDX resolver)
31
+ in Phases 1–3 of the plan. Phase 4 deletes the duplicate code
32
+ from this package and converts the `/playwright` subpath into a
33
+ shim that re-exports the canonical surface so existing callers
34
+ keep compiling.
35
+
36
+ ```ts
37
+ // Was:
38
+ import { test } from "@ingcreators/annot-product-docs-astro/playwright";
39
+
40
+ // Now (recommended):
41
+ import { test } from "@ingcreators/annot-product-docs"; // with MDX
42
+ // or
43
+ import { test } from "@ingcreators/annot-playwright"; // without MDX
44
+ ```
45
+
46
+ The deprecated subpath emits a one-time
47
+ `process.emitWarning("DeprecationWarning", …)` at import time so
48
+ the migration prompt shows up in CI logs. **Reference equality
49
+ preserved** — `test`, `expect`, `patchScreenshot`,
50
+ `rebaseAnnotations`, `describeAnnotation` are reference-equal to
51
+ their canonical homes; a new
52
+ `packages/product-docs-astro/src/playwright/index.test.ts`
53
+ asserts this.
54
+
55
+ **Removal target**: `@ingcreators/annot-product-docs-astro@0.5.0`,
56
+ matching the OQ-2 decision (b) in the parent plan — visible
57
+ deprecation, known sunset.
58
+
59
+ ## render.ts switches to the canonical helpers
60
+
61
+ `renderAnnotatedScreen()` previously carried its own copies of
62
+ `resolveMdxAnnotations` / `parseSnapshotBoxes` /
63
+ `buildBadgeAnnotations` / `svgFromBadges` / `svgFromBboxAnnotations`
64
+ / `emptyAnnotationsSvg`. Phase 2 of the plan moved the canonical
65
+ home into `@ingcreators/annot-product-docs`; this PR deletes the
66
+ duplicates from `product-docs-astro/render.ts` and consumes the
67
+ ones in product-docs going forward.
68
+
69
+ `resolveMdxAnnotations` + `svgFromBboxAnnotations` are re-exported
70
+ from `render.ts` for one deprecation cycle so existing callers
71
+ that imported them from `@ingcreators/annot-product-docs-astro`
72
+ keep compiling. `parseSnapshotBoxes` is dropped from the public
73
+ surface — new code should import it from
74
+ `@ingcreators/annot-product-docs` directly.
75
+
76
+ ## peerDependencies cleanup
77
+
78
+ `@playwright/test` is removed from `peerDependencies` (and from
79
+ `peerDependenciesMeta`). The package no longer has a Playwright
80
+ relationship to advertise — the `/playwright` subpath is purely
81
+ a re-export shim, and its types flow through the
82
+ `@ingcreators/annot-product-docs` workspace dep transitively.
83
+ This matches the OQ-3 decision (b) in the parent plan.
84
+
85
+ ## Verified
86
+ - `pnpm -r typecheck` — 20 packages, all pass.
87
+ - `pnpm test` — 252 files, 3641 tests, 0 failures. New
88
+ `playwright/index.test.ts` (5 reference-equality assertions)
89
+ passes; the deprecation `process.emitWarning` fires on
90
+ import (visible in vitest output) but does not break anything.
91
+ - `pnpm lint` — exit 0; 29 pre-existing warnings unchanged.
92
+ - `pnpm --filter @ingcreators/annot-product-docs-astro build` —
93
+ emits `dist/index.js` (3.96 kB / 1.71 kB gzip) +
94
+ `dist/playwright/index.js` (0.85 kB / 0.39 kB gzip) — the
95
+ shrunken subpath bundle reflects the re-export-only shape.
96
+
97
+ ### Patch Changes
98
+
99
+ - 85d40e6: **Docs + CLAUDE.md + plan archive** — Phase 5 (final) of
100
+ `docs/plans/_done/playwright-screenshot-fixture-relayer.md`.
101
+ Refreshes the doc surfaces, the operational CLAUDE.md notes,
102
+ and archives the relayer plan into `_done/`.
103
+
104
+ ## Doc surface updates
105
+ - `packages/docs-site/src/content/docs/product-docs/playwright-fixture.mdx`
106
+ — recommended import paths updated; new "Choosing your import"
107
+ section covers the `@ingcreators/annot-product-docs` (MDX)
108
+ vs. `@ingcreators/annot-playwright` (no MDX) split; companion
109
+ helpers section now imports from the canonical homes; codegen
110
+ workflow example swapped to `@ingcreators/annot-product-docs`.
111
+ - `packages/docs-site/src/content/docs/api/create-annotator.mdx`
112
+ — "From a Playwright test" example imports from the canonical
113
+ home; mentions the no-MDX alternative.
114
+ - `packages/playwright/README.md` — adds the
115
+ `page.screenshot({ annot: { … } })` (recommended) section
116
+ above the existing `annotator.annotateScreenshot(...)` flow;
117
+ documents the `annotSourceResolvers` extension hook + the
118
+ coordinate-rebase helpers.
119
+ - `packages/product-docs/README.md` — adds a `page.screenshot({
120
+ annot })` Playwright fixture section explaining the
121
+ productDocs.sync + MDX-resolver bundle.
122
+ - `packages/product-docs-astro/README.md` — replaces the
123
+ Playwright fixture section with a Migration note pointing at
124
+ the canonical homes; documents the `0.5.0` removal target.
125
+
126
+ ## CLAUDE.md monorepo layout
127
+ - `playwright/` entry now describes the canonical
128
+ `page.screenshot({ annot })` patch + `annotSourceResolvers`
129
+ extension hook. Version bumped to 0.4.0.
130
+ - `product-docs/` entry mentions the `productDocs` fixture
131
+ rename + the MDX-aware resolver registration via the hook
132
+ registry. Version bumped to 0.3.0.
133
+ - `product-docs-astro/` entry calls out the deprecated
134
+ `/playwright` re-export shim + 0.5.0 removal target.
135
+ `@playwright/test` no longer a peer dep. Version bumped to
136
+ 0.3.0.
137
+
138
+ ## Plan archive
139
+ - `docs/plans/playwright-screenshot-fixture-relayer.md` →
140
+ `docs/plans/_done/playwright-screenshot-fixture-relayer.md`.
141
+ Status header switched to `Done` with the four landing PRs
142
+ enumerated. Internal `./_done/...` link paths updated to
143
+ `./...` (the plan is itself inside `_done/` now).
144
+ - `docs/plans/README.md` — removed the active entry, added a
145
+ "Recently landed plans" row pointing at the archived plan
146
+ with a multi-phase summary covering all four landing PRs.
147
+ - `docs/plans/living-spec-authoring-roadmap.md` — three link
148
+ references updated to point at the archived path; the "How
149
+ this relates" line switched from "Already Draft" to "Landed
150
+ 2026-05-22 (PRs #962 / #963 / #964 / #966)".
151
+
152
+ No source code changes; verified via `pnpm -r typecheck`,
153
+ `pnpm test`, `pnpm lint` regardless to confirm the doc/comment
154
+ edits parse cleanly.
155
+
156
+ - Updated dependencies [f979374]
157
+ - Updated dependencies [5778902]
158
+ - Updated dependencies [96e7625]
159
+ - Updated dependencies [85d40e6]
160
+ - @ingcreators/annot-playwright@0.4.0
161
+ - @ingcreators/annot-product-docs@0.3.0
162
+
3
163
  ## 0.2.2
4
164
 
5
165
  ### Patch Changes
package/README.md CHANGED
@@ -89,41 +89,30 @@ 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)
92
+ ## Migration — Playwright fixture moved
93
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:
94
+ > The Playwright fixture that previously lived at
95
+ > `@ingcreators/annot-product-docs-astro/playwright` moved out
96
+ > of this package as part of
97
+ > [`docs/plans/_done/playwright-screenshot-fixture-relayer.md`](https://github.com/ingcreators/annot/blob/main/docs/plans/_done/playwright-screenshot-fixture-relayer.md)
98
+ > (Phases 1–4). Code authoring a Playwright spec should pick
99
+ > one of the two canonical homes:
102
100
 
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.
101
+ | Use case | Import |
102
+ |---|---|
103
+ | Living-product-docs tour (MDX-linked `annot.mdx`) | `import { test } from "@ingcreators/annot-product-docs";` |
104
+ | VRT specs / marketing screenshots / AI agent flows (no MDX) | `import { test } from "@ingcreators/annot-playwright";` |
105
+
106
+ The `@ingcreators/annot-product-docs-astro/playwright` subpath
107
+ remains as a deprecated re-export so existing callers still
108
+ compile; it emits a `DeprecationWarning` at import time and
109
+ will be removed in `@ingcreators/annot-product-docs-astro@0.5.0`.
110
+ `@playwright/test` is no longer a peer dep of this package —
111
+ the canonical homes carry the relationship now.
112
+
113
+ `renderAnnotatedScreen` (the Astro Image Service) lives on as
114
+ the build-time renderer for the `<Screen>` block; this section
115
+ isn't going anywhere.
127
116
 
128
117
  ## Tier
129
118
 
package/dist/cache.d.ts CHANGED
@@ -4,8 +4,24 @@
4
4
  * consumer the next time they build. Keep this small (a single
5
5
  * digit per breaking change) — large jumps don't help and just
6
6
  * obscure the upgrade history.
7
+ *
8
+ * Bumped to 2 in Phase 1h of
9
+ * `docs/plans/living-spec-authoring-roadmap.md` — the renderer now
10
+ * prefers PNG XMP `annot:elementTree` bboxes over the legacy
11
+ * `annot:snapshot` MDX comment block when both are present. Forcing
12
+ * a cold cache avoids the stale-cache window where a migrated MDX
13
+ * (legacy comment block stripped) + new PNG XMP would otherwise
14
+ * map to the same cache key as the pre-migration state.
15
+ *
16
+ * Known limitation: post-migration, re-running the Playwright tour
17
+ * mutates the PNG XMP without touching the MDX. Today's cache key
18
+ * doesn't include PNG content, so the cache won't auto-invalidate
19
+ * on tour re-runs. Workaround until that's fixed: delete the
20
+ * `.astro/annot-product-docs/` cache directory between tour runs
21
+ * (or bump this version every time the tour is expected to change
22
+ * output). A follow-up will fold a PNG content hash into the key.
7
23
  */
8
- export declare const RENDER_PIPELINE_VERSION = 1;
24
+ export declare const RENDER_PIPELINE_VERSION = 2;
9
25
  export interface CacheKeyInput {
10
26
  mdxSource: string;
11
27
  screenId: string;
@@ -16,6 +32,13 @@ export interface CacheKeyInput {
16
32
  * historical key shape for flat callers.
17
33
  */
18
34
  editable?: boolean;
35
+ /**
36
+ * Phase 2b: raw bytes of the `<Screen annotations="…">` yaml,
37
+ * when set. Folded into the cache key so editing the yaml busts
38
+ * the cached PNG. Undefined when the screen uses the inline
39
+ * `<Overlay>` path (legacy callers' key shape is preserved).
40
+ */
41
+ annotationsYamlSource?: string;
19
42
  }
20
43
  /**
21
44
  * Compute the cache key for a `<Screen>` block. Stable across
@@ -0,0 +1,32 @@
1
+ ---
2
+ // `<AnnotCallout>` — Phase 2b of
3
+ // `docs/plans/living-spec-authoring-roadmap.md`. Caption for one
4
+ // callout entry on a `<Screen annotations="…">`. Replaces the
5
+ // inline `<Overlay match intent number>` form: match / intent /
6
+ // number now live in the screen's annotation yaml, and this
7
+ // component carries only the visible body.
8
+ //
9
+ // MDX usage:
10
+ // <Screen id="login" src="./shots/login.png"
11
+ // annotations="./annotations/login.yaml">
12
+ // <AnnotCallout for="o1">**Email** — registered address.</AnnotCallout>
13
+ // <AnnotCallout for="o2">**Password** — characters hidden.</AnnotCallout>
14
+ // </Screen>
15
+ //
16
+ // The visible badge / number / intent border is composed onto the
17
+ // annotated PNG by the Image Service (see `../render.ts`). The
18
+ // HTML side emits the body inside a `<li>` so the legend
19
+ // auto-numbers via the surrounding `<ol class="annot-screen-overlays">`
20
+ // counter — no per-call `number` prop required.
21
+
22
+ interface Props {
23
+ /** Targets `overlays[].id` in the screen's annotation yaml. */
24
+ for: string;
25
+ }
26
+ const { for: forId } = Astro.props;
27
+ ---
28
+ <li class="annot-callout" data-callout-for={forId}>
29
+ <div class="annot-callout-body">
30
+ <slot />
31
+ </div>
32
+ </li>
@@ -0,0 +1,186 @@
1
+ ---
2
+ // `<AnnotEditButton>` — Phase 5d of
3
+ // `docs/plans/living-spec-authoring-roadmap.md`. Mountable
4
+ // "✏️ Edit" button that routes visitors to the annot-cloud
5
+ // editor at `${cloudUrl}/embed?...`. Phase 5 ships three embed
6
+ // modes:
7
+ //
8
+ // - `newTab` (default): click → `window.open(...)`. Works
9
+ // under every Enterprise constraint (CSP / SSO / on-prem /
10
+ // air-gapped). See OQ-09 in
11
+ // `living-spec-authoring-roadmap.md`.
12
+ // - `disabled`: button is not rendered. For read-only docs /
13
+ // air-gapped deployments without on-prem annot-cloud.
14
+ // - `inline` (opt-in): modal `<iframe>` mounted on the docs
15
+ // site. Requires `<AnnotEditorIframeModal />` (5e) to be
16
+ // mounted somewhere on the same page (typically in the
17
+ // layout). If the modal isn't present, `inline` gracefully
18
+ // degrades to `newTab` with a one-time `console.warn`.
19
+ //
20
+ // MDX usage:
21
+ //
22
+ // <AnnotEditButton
23
+ // repo="ingcreators/annot"
24
+ // pngPath="docs/shots/login.png"
25
+ // annotationsPath="docs/annotations/login.annotations.yaml"
26
+ // />
27
+ //
28
+ // `cloudUrl` defaults to `https://annot.work`. Override via
29
+ // per-call prop OR (in Phase 5f) `annot-docs.config.ts`'s
30
+ // `editor.cloudUrl` default. The defaults landed here are
31
+ // what Phase 5f will lift into the config; Phase 5d ships
32
+ // per-call props only.
33
+
34
+ import type { EmbedMode } from "@ingcreators/annot-embed-protocol";
35
+ import { ANNOT_EDITOR_CONFIG } from "../editor-config-virtual.js";
36
+
37
+ interface Props {
38
+ /** Owner/repo slug, e.g. `"ingcreators/annot"`. */
39
+ repo: string;
40
+ /** Path within the repo of the PNG being edited. */
41
+ pngPath: string;
42
+ /** Path within the repo of the linked `.annotations.yaml`. */
43
+ annotationsPath: string;
44
+ /** Embed mode. Defaults to the integration-supplied
45
+ * `editor.embedMode` (or `"newTab"` if unset). */
46
+ mode?: EmbedMode;
47
+ /** Cloud editor origin override. Defaults to the
48
+ * integration-supplied `editor.cloudUrl` (or
49
+ * `"https://annot.work"` if unset). */
50
+ cloudUrl?: string;
51
+ /** Visible button label. Defaults to `"✏️ Edit"`. */
52
+ label?: string;
53
+ }
54
+
55
+ const {
56
+ repo,
57
+ pngPath,
58
+ annotationsPath,
59
+ mode = ANNOT_EDITOR_CONFIG.embedMode,
60
+ cloudUrl = ANNOT_EDITOR_CONFIG.cloudUrl,
61
+ label = "✏️ Edit",
62
+ } = Astro.props;
63
+ ---
64
+ {
65
+ mode !== "disabled" && (
66
+ <button
67
+ type="button"
68
+ class="annot-edit-button"
69
+ data-annot-edit-mode={mode}
70
+ data-annot-edit-cloud-url={cloudUrl}
71
+ data-annot-edit-repo={repo}
72
+ data-annot-edit-png-path={pngPath}
73
+ data-annot-edit-annotations-path={annotationsPath}
74
+ aria-label={`Edit ${pngPath}`}
75
+ >
76
+ {label}
77
+ </button>
78
+ )
79
+ }
80
+
81
+ <script>
82
+ import { encodeEmbedRequestUrl } from "@ingcreators/annot-embed-protocol";
83
+
84
+ // One-time warning when a `mode="inline"` button is clicked
85
+ // but `<AnnotEditorIframeModal />` isn't on the page. The
86
+ // warning lives at the page-script level so multiple buttons
87
+ // share it.
88
+ let warnedMissingModal = false;
89
+
90
+ interface ModalApi {
91
+ open: (options: { cloudUrl: string; editorUrl: string }) => void;
92
+ }
93
+
94
+ function findModalApi(): ModalApi | undefined {
95
+ return (window as unknown as { __annotEditorIframeModal?: ModalApi })
96
+ .__annotEditorIframeModal;
97
+ }
98
+
99
+ function activate(button: HTMLButtonElement): void {
100
+ const mode = button.dataset.annotEditMode ?? "newTab";
101
+ const cloudUrl = button.dataset.annotEditCloudUrl ?? "";
102
+ const repo = button.dataset.annotEditRepo ?? "";
103
+ const pngPath = button.dataset.annotEditPngPath ?? "";
104
+ const annotationsPath = button.dataset.annotEditAnnotationsPath ?? "";
105
+
106
+ button.addEventListener("click", () => {
107
+ // Resolve the effective mode. `inline` falls back to
108
+ // `newTab` if `<AnnotEditorIframeModal />` isn't on the
109
+ // page so the button still does something useful (warn
110
+ // once so authors notice in DevTools).
111
+ let effectiveMode: "newTab" | "inline" = mode === "inline" ? "inline" : "newTab";
112
+ const modalApi = effectiveMode === "inline" ? findModalApi() : undefined;
113
+ if (effectiveMode === "inline" && !modalApi) {
114
+ if (!warnedMissingModal) {
115
+ // biome-ignore lint/suspicious/noConsole: degraded-UX surface for the missing modal.
116
+ console.warn(
117
+ '[<AnnotEditButton>] mode="inline" requires <AnnotEditorIframeModal /> on the page; falling back to mode="newTab". Mount the modal component in your layout to enable the inline flow.',
118
+ );
119
+ warnedMissingModal = true;
120
+ }
121
+ effectiveMode = "newTab";
122
+ }
123
+
124
+ const returnUrl = window.location.href.split("#")[0];
125
+ try {
126
+ const url = encodeEmbedRequestUrl({
127
+ cloudUrl,
128
+ repo,
129
+ pngPath,
130
+ annotationsPath,
131
+ returnUrl,
132
+ mode: effectiveMode,
133
+ });
134
+ if (effectiveMode === "inline" && modalApi) {
135
+ modalApi.open({ cloudUrl, editorUrl: url });
136
+ return;
137
+ }
138
+ window.open(url, "_blank", "noopener,noreferrer");
139
+ } catch (error) {
140
+ // biome-ignore lint/suspicious/noConsole: encoder errors surface as runtime warnings.
141
+ console.error("[<AnnotEditButton>] failed to build embed URL:", error);
142
+ }
143
+ });
144
+ }
145
+
146
+ function init(): void {
147
+ const buttons = document.querySelectorAll<HTMLButtonElement>(
148
+ "button.annot-edit-button",
149
+ );
150
+ for (const button of Array.from(buttons)) {
151
+ activate(button);
152
+ }
153
+ }
154
+
155
+ if (document.readyState === "loading") {
156
+ document.addEventListener("DOMContentLoaded", init);
157
+ } else {
158
+ init();
159
+ }
160
+ </script>
161
+
162
+ <style>
163
+ .annot-edit-button {
164
+ display: inline-flex;
165
+ align-items: center;
166
+ gap: 0.4em;
167
+ padding: 0.4em 0.8em;
168
+ font-family: inherit;
169
+ font-size: 0.9em;
170
+ line-height: 1.2;
171
+ color: var(--annot-edit-button-fg, #ffffff);
172
+ background: var(--annot-edit-button-bg, #3b82f6);
173
+ border: 0;
174
+ border-radius: 0.4em;
175
+ cursor: pointer;
176
+ transition: background 0.15s ease-out;
177
+ }
178
+ .annot-edit-button:hover,
179
+ .annot-edit-button:focus-visible {
180
+ background: var(--annot-edit-button-bg-hover, #2563eb);
181
+ outline: none;
182
+ }
183
+ .annot-edit-button:focus-visible {
184
+ box-shadow: 0 0 0 2px var(--annot-edit-button-focus-ring, #93c5fd);
185
+ }
186
+ </style>
@@ -0,0 +1,186 @@
1
+ ---
2
+ // `<AnnotEditCompleteListener>` — Phase 5g of
3
+ // `docs/plans/living-spec-authoring-roadmap.md`.
4
+ //
5
+ // Invisible client-side component that surfaces a transient
6
+ // toast when the cloud editor returns control to the docs site
7
+ // after a save. Two trigger paths:
8
+ //
9
+ // - `newTab` mode: the cloud editor redirects to
10
+ // `${returnUrl}#edit-complete=<editId>`. On client-side mount
11
+ // this component parses `window.location.hash` via the 5b
12
+ // `parseEmbedReturnHash` codec, renders a toast when the
13
+ // hash carries an `EmbedReturnSignal` of kind `"complete"`,
14
+ // and dispatches an `annot:edit-complete` `CustomEvent` on
15
+ // `document` so downstream listeners (analytics, custom
16
+ // rebuild-status badges) can react.
17
+ //
18
+ // - `inline` mode: the 5e `<AnnotEditorIframeModal>` already
19
+ // dismisses itself on `EditCommitted`. This component
20
+ // listens for the same lifecycle event via an
21
+ // `annot-editor-iframe-modal-event` `CustomEvent` the 5e
22
+ // modal dispatches on `document`, then renders the same
23
+ // toast.
24
+ //
25
+ // Mount once per page (typically inside the layout). Multiple
26
+ // instances are safe — only the first activates; the rest
27
+ // short-circuit on init via the `data-annot-edit-complete-listener-active`
28
+ // marker.
29
+
30
+ interface Props {
31
+ /** Toast message override. Defaults to "Edit saved — site
32
+ * rebuilds in ~1 minute." */
33
+ message?: string;
34
+ /** Auto-dismiss timeout in milliseconds. Defaults to 8000.
35
+ * Set to `0` to keep the toast visible until the user
36
+ * dismisses it manually. */
37
+ dismissAfterMs?: number;
38
+ }
39
+
40
+ const { message = "Edit saved — site rebuilds in ~1 minute.", dismissAfterMs = 8000 } =
41
+ Astro.props;
42
+ ---
43
+
44
+ <div
45
+ class="annot-edit-complete-listener"
46
+ data-annot-edit-complete-listener-root
47
+ data-annot-edit-complete-message={message}
48
+ data-annot-edit-complete-dismiss-ms={dismissAfterMs}
49
+ aria-live="polite"
50
+ aria-atomic="true"
51
+ >
52
+ <slot />
53
+ </div>
54
+
55
+ <script>
56
+ import { parseEmbedReturnHash } from "@ingcreators/annot-embed-protocol";
57
+
58
+ const ROOT_SELECTOR = "[data-annot-edit-complete-listener-root]";
59
+ const ACTIVE_MARKER = "data-annot-edit-complete-listener-active";
60
+ const TOAST_DISMISS_FALLBACK_MS = 8000;
61
+
62
+ interface Detail {
63
+ readonly editId: string;
64
+ readonly source: "newTab" | "inline";
65
+ }
66
+
67
+ function findRoot(): HTMLElement | null {
68
+ // Pick the FIRST listener mounted on the page. Multiple
69
+ // copies are safe; subsequent ones short-circuit.
70
+ return document.querySelector<HTMLElement>(ROOT_SELECTOR);
71
+ }
72
+
73
+ function showToast(root: HTMLElement, detail: Detail): void {
74
+ const message = root.dataset.annotEditCompleteMessage ?? "Edit saved.";
75
+ const dismissAfterMs = Number(
76
+ root.dataset.annotEditCompleteDismissMs ?? TOAST_DISMISS_FALLBACK_MS,
77
+ );
78
+
79
+ const toast = document.createElement("div");
80
+ toast.className = "annot-edit-complete-toast";
81
+ toast.setAttribute("role", "status");
82
+ toast.dataset.annotEditCompleteId = detail.editId;
83
+ toast.dataset.annotEditCompleteSource = detail.source;
84
+ toast.textContent = message;
85
+
86
+ const dismissBtn = document.createElement("button");
87
+ dismissBtn.type = "button";
88
+ dismissBtn.className = "annot-edit-complete-toast-dismiss";
89
+ dismissBtn.setAttribute("aria-label", "Dismiss notification");
90
+ dismissBtn.textContent = "×";
91
+ dismissBtn.addEventListener("click", () => {
92
+ toast.remove();
93
+ });
94
+ toast.appendChild(dismissBtn);
95
+
96
+ root.appendChild(toast);
97
+
98
+ if (Number.isFinite(dismissAfterMs) && dismissAfterMs > 0) {
99
+ window.setTimeout(() => {
100
+ toast.remove();
101
+ }, dismissAfterMs);
102
+ }
103
+
104
+ document.dispatchEvent(
105
+ new CustomEvent("annot:edit-complete", { detail }),
106
+ );
107
+ }
108
+
109
+ function consumeReturnHash(root: HTMLElement): void {
110
+ const signal = parseEmbedReturnHash(window.location.hash);
111
+ if (!signal || signal.kind !== "complete") return;
112
+ showToast(root, { editId: signal.editId, source: "newTab" });
113
+ // Clear the hash so a page refresh doesn't re-toast.
114
+ if (typeof window.history.replaceState === "function") {
115
+ const cleanUrl = `${window.location.pathname}${window.location.search}`;
116
+ window.history.replaceState(null, "", cleanUrl);
117
+ }
118
+ }
119
+
120
+ function attachInlineListener(root: HTMLElement): void {
121
+ document.addEventListener("annot:editor-iframe-committed", (event: Event) => {
122
+ const detail = (event as CustomEvent<{ editId?: string }>).detail;
123
+ const editId = detail?.editId ?? "";
124
+ if (!editId) return;
125
+ showToast(root, { editId, source: "inline" });
126
+ });
127
+ }
128
+
129
+ function init(): void {
130
+ const root = findRoot();
131
+ if (!root) return;
132
+ if (root.hasAttribute(ACTIVE_MARKER)) return;
133
+ root.setAttribute(ACTIVE_MARKER, "1");
134
+
135
+ consumeReturnHash(root);
136
+ attachInlineListener(root);
137
+ }
138
+
139
+ if (document.readyState === "loading") {
140
+ document.addEventListener("DOMContentLoaded", init);
141
+ } else {
142
+ init();
143
+ }
144
+ </script>
145
+
146
+ <style>
147
+ .annot-edit-complete-listener {
148
+ position: fixed;
149
+ bottom: 1.5rem;
150
+ right: 1.5rem;
151
+ z-index: 9999;
152
+ display: flex;
153
+ flex-direction: column;
154
+ gap: 0.5rem;
155
+ pointer-events: none;
156
+ }
157
+ .annot-edit-complete-toast {
158
+ display: flex;
159
+ align-items: center;
160
+ gap: 0.75rem;
161
+ padding: 0.75rem 1rem;
162
+ color: var(--annot-edit-toast-fg, #ffffff);
163
+ background: var(--annot-edit-toast-bg, #16a34a);
164
+ border-radius: 0.4em;
165
+ box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.25);
166
+ font-family: inherit;
167
+ font-size: 0.95em;
168
+ line-height: 1.3;
169
+ pointer-events: auto;
170
+ }
171
+ .annot-edit-complete-toast-dismiss {
172
+ margin-left: auto;
173
+ background: transparent;
174
+ border: 0;
175
+ color: inherit;
176
+ font-size: 1.25em;
177
+ line-height: 1;
178
+ cursor: pointer;
179
+ padding: 0 0.25em;
180
+ }
181
+ .annot-edit-complete-toast-dismiss:hover,
182
+ .annot-edit-complete-toast-dismiss:focus-visible {
183
+ opacity: 0.8;
184
+ outline: none;
185
+ }
186
+ </style>