@ingcreators/annot-product-docs-astro 0.3.0 → 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 +19 -0
- package/dist/cache.d.ts +24 -1
- package/dist/components/AnnotCallout.astro +32 -0
- package/dist/components/AnnotEditButton.astro +186 -0
- package/dist/components/AnnotEditCompleteListener.astro +186 -0
- package/dist/components/AnnotEditorIframeModal.astro +202 -0
- package/dist/components/Screen.astro +28 -8
- package/dist/editor-config-virtual.d.ts +12 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +96 -41
- package/dist/integration.d.ts +58 -8
- package/package.json +14 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
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
|
+
|
|
3
22
|
## 0.3.0
|
|
4
23
|
|
|
5
24
|
### Minor Changes
|
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 =
|
|
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>
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
---
|
|
2
|
+
// `<AnnotEditorIframeModal>` — Phase 5e of
|
|
3
|
+
// `docs/plans/living-spec-authoring-roadmap.md`. The inline-
|
|
4
|
+
// mode counterpart to `<AnnotEditButton>`'s default newTab
|
|
5
|
+
// behaviour. Mount once per page (typically in the docs site's
|
|
6
|
+
// layout) and any `<AnnotEditButton mode="inline">` on the
|
|
7
|
+
// same page opens the modal instead of a new tab.
|
|
8
|
+
//
|
|
9
|
+
// Lazy-loaded: the modal HTML + script only ship if this
|
|
10
|
+
// component is mounted. Pages that only use `mode="newTab"`
|
|
11
|
+
// (the default per OQ-09) never download the modal code.
|
|
12
|
+
// `<AnnotEditButton mode="inline">` on a page without
|
|
13
|
+
// `<AnnotEditorIframeModal />` gracefully degrades to newTab
|
|
14
|
+
// + a one-time `console.warn`.
|
|
15
|
+
//
|
|
16
|
+
// MDX usage in a layout:
|
|
17
|
+
//
|
|
18
|
+
// ---
|
|
19
|
+
// import { AnnotEditorIframeModal } from "@ingcreators/annot-product-docs-astro/components/AnnotEditorIframeModal.astro";
|
|
20
|
+
// ---
|
|
21
|
+
// <slot />
|
|
22
|
+
// <AnnotEditorIframeModal />
|
|
23
|
+
//
|
|
24
|
+
// Then any `<AnnotEditButton mode="inline" ... />` in MDX
|
|
25
|
+
// opens the modal on click. The modal listens for
|
|
26
|
+
// `EditCommitted` / `EditAbandoned` / `ResizeNeeded` via the
|
|
27
|
+
// 5c postMessage dispatcher and dismisses itself accordingly.
|
|
28
|
+
|
|
29
|
+
interface Props {
|
|
30
|
+
/** Modal aria-label. Overridable for localisation. */
|
|
31
|
+
ariaLabel?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { ariaLabel = "Annot editor" } = Astro.props;
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
<dialog
|
|
38
|
+
class="annot-editor-iframe-modal"
|
|
39
|
+
data-annot-edit-modal-root
|
|
40
|
+
aria-label={ariaLabel}
|
|
41
|
+
>
|
|
42
|
+
<iframe
|
|
43
|
+
class="annot-editor-iframe-modal-frame"
|
|
44
|
+
title={ariaLabel}
|
|
45
|
+
allow="clipboard-read; clipboard-write"
|
|
46
|
+
referrerpolicy="origin"
|
|
47
|
+
></iframe>
|
|
48
|
+
</dialog>
|
|
49
|
+
|
|
50
|
+
<script>
|
|
51
|
+
import {
|
|
52
|
+
createEmbedHostMessenger,
|
|
53
|
+
type EmbedEvent,
|
|
54
|
+
type EmbedMessenger,
|
|
55
|
+
} from "@ingcreators/annot-embed-protocol";
|
|
56
|
+
|
|
57
|
+
interface OpenModalOptions {
|
|
58
|
+
readonly cloudUrl: string;
|
|
59
|
+
readonly editorUrl: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const MAX_HEIGHT_FRACTION = 0.95;
|
|
63
|
+
const DEFAULT_FRAME_HEIGHT_PX = 720;
|
|
64
|
+
|
|
65
|
+
function findModal(): HTMLDialogElement | null {
|
|
66
|
+
return document.querySelector<HTMLDialogElement>(
|
|
67
|
+
"dialog[data-annot-edit-modal-root]",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function findFrame(modal: HTMLDialogElement): HTMLIFrameElement | null {
|
|
72
|
+
return modal.querySelector<HTMLIFrameElement>(
|
|
73
|
+
"iframe.annot-editor-iframe-modal-frame",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function clampHeight(requested: number): number {
|
|
78
|
+
if (!Number.isFinite(requested) || requested <= 0) {
|
|
79
|
+
return DEFAULT_FRAME_HEIGHT_PX;
|
|
80
|
+
}
|
|
81
|
+
const max = window.innerHeight * MAX_HEIGHT_FRACTION;
|
|
82
|
+
return Math.min(requested, max);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function openModal(options: OpenModalOptions): void {
|
|
86
|
+
const modal = findModal();
|
|
87
|
+
if (!modal) {
|
|
88
|
+
// biome-ignore lint/suspicious/noConsole: lazy-mount contract violation surfaces here.
|
|
89
|
+
console.warn(
|
|
90
|
+
"[<AnnotEditorIframeModal>] dialog element not found — was the component mounted on this page?",
|
|
91
|
+
);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const frame = findFrame(modal);
|
|
95
|
+
if (!frame) return;
|
|
96
|
+
|
|
97
|
+
frame.style.height = `${DEFAULT_FRAME_HEIGHT_PX}px`;
|
|
98
|
+
frame.src = options.editorUrl;
|
|
99
|
+
|
|
100
|
+
let messenger: EmbedMessenger | null = null;
|
|
101
|
+
|
|
102
|
+
const teardown = (): void => {
|
|
103
|
+
if (messenger) {
|
|
104
|
+
messenger.cleanup();
|
|
105
|
+
messenger = null;
|
|
106
|
+
}
|
|
107
|
+
frame.removeAttribute("src");
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const onEvent = (event: EmbedEvent): void => {
|
|
111
|
+
if (event.type === "EditCommitted") {
|
|
112
|
+
// Forward to the docs-site's `<AnnotEditCompleteListener>` so the
|
|
113
|
+
// post-edit toast surfaces consistently across newTab + inline
|
|
114
|
+
// modes (Phase 5g).
|
|
115
|
+
document.dispatchEvent(
|
|
116
|
+
new CustomEvent("annot:editor-iframe-committed", {
|
|
117
|
+
detail: {
|
|
118
|
+
editId: event.editId,
|
|
119
|
+
commitSha: event.commitSha,
|
|
120
|
+
branch: event.branch,
|
|
121
|
+
prUrl: event.prUrl,
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
modal.close();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (event.type === "EditAbandoned") {
|
|
129
|
+
modal.close();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (event.type === "ResizeNeeded") {
|
|
133
|
+
frame.style.height = `${clampHeight(event.height)}px`;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
modal.addEventListener(
|
|
138
|
+
"close",
|
|
139
|
+
() => {
|
|
140
|
+
teardown();
|
|
141
|
+
},
|
|
142
|
+
{ once: true },
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Set up the host messenger after the iframe loads — its
|
|
146
|
+
// `contentWindow` is null until then. Once loaded, attach
|
|
147
|
+
// the messenger; subsequent postMessages from the cloud
|
|
148
|
+
// editor route through it.
|
|
149
|
+
frame.addEventListener(
|
|
150
|
+
"load",
|
|
151
|
+
() => {
|
|
152
|
+
try {
|
|
153
|
+
messenger = createEmbedHostMessenger({
|
|
154
|
+
frame,
|
|
155
|
+
expectedOrigin: options.cloudUrl,
|
|
156
|
+
onEvent,
|
|
157
|
+
});
|
|
158
|
+
} catch (error) {
|
|
159
|
+
// biome-ignore lint/suspicious/noConsole: surface failed host-messenger setup.
|
|
160
|
+
console.error("[<AnnotEditorIframeModal>] failed to attach host messenger:", error);
|
|
161
|
+
modal.close();
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
{ once: true },
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
modal.showModal();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Expose the open function for `<AnnotEditButton>`'s inline-
|
|
171
|
+
// mode click handler to call. Keep the surface narrow — just
|
|
172
|
+
// `open` — so future expansion (close-from-outside,
|
|
173
|
+
// post-event listener API) is additive.
|
|
174
|
+
type ModalApi = {
|
|
175
|
+
open: (options: OpenModalOptions) => void;
|
|
176
|
+
};
|
|
177
|
+
(window as unknown as { __annotEditorIframeModal?: ModalApi }).__annotEditorIframeModal =
|
|
178
|
+
{ open: openModal };
|
|
179
|
+
</script>
|
|
180
|
+
|
|
181
|
+
<style>
|
|
182
|
+
.annot-editor-iframe-modal {
|
|
183
|
+
width: min(95vw, 1280px);
|
|
184
|
+
max-width: 95vw;
|
|
185
|
+
max-height: 95vh;
|
|
186
|
+
padding: 0;
|
|
187
|
+
border: 0;
|
|
188
|
+
border-radius: 0.6em;
|
|
189
|
+
background: var(--annot-editor-modal-bg, #ffffff);
|
|
190
|
+
box-shadow: 0 0.5em 2em rgba(0, 0, 0, 0.4);
|
|
191
|
+
overflow: hidden;
|
|
192
|
+
}
|
|
193
|
+
.annot-editor-iframe-modal::backdrop {
|
|
194
|
+
background: rgba(0, 0, 0, 0.5);
|
|
195
|
+
}
|
|
196
|
+
.annot-editor-iframe-modal-frame {
|
|
197
|
+
display: block;
|
|
198
|
+
width: 100%;
|
|
199
|
+
min-height: 360px;
|
|
200
|
+
border: 0;
|
|
201
|
+
}
|
|
202
|
+
</style>
|
|
@@ -1,27 +1,47 @@
|
|
|
1
1
|
---
|
|
2
2
|
// `<Screen>` — annotated screenshot block.
|
|
3
3
|
//
|
|
4
|
-
// MDX usage:
|
|
4
|
+
// MDX usage (legacy, inline `<Overlay>` callouts):
|
|
5
5
|
// <Screen id="login" src="./shots/login.png">
|
|
6
6
|
// <Overlay match={...} number={1}>...</Overlay>
|
|
7
7
|
// </Screen>
|
|
8
8
|
//
|
|
9
|
+
// MDX usage (Phase 2b, yaml-driven callouts):
|
|
10
|
+
// <Screen id="login" src="./shots/login.png"
|
|
11
|
+
// annotations="./annotations/login.yaml">
|
|
12
|
+
// <AnnotCallout for="o1">...</AnnotCallout>
|
|
13
|
+
// </Screen>
|
|
14
|
+
//
|
|
9
15
|
// The annotated PNG is produced at build time by the Image
|
|
10
16
|
// Service in `@ingcreators/annot-product-docs-astro` (see
|
|
11
|
-
// `../render.ts`).
|
|
12
|
-
//
|
|
13
|
-
//
|
|
17
|
+
// `../render.ts`). When `annotations` is set, the service reads
|
|
18
|
+
// the yaml + composes badges from `overlays[]`; otherwise it
|
|
19
|
+
// resolves the inline `<Overlay match>` children. At render time
|
|
20
|
+
// we just emit an `<img>` that points at the cache hash; the
|
|
21
|
+
// Astro pipeline resolves the final URL.
|
|
14
22
|
|
|
15
23
|
interface Props {
|
|
16
24
|
id: string;
|
|
17
25
|
src: string;
|
|
18
26
|
alt?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Phase 2b of `docs/plans/living-spec-authoring-roadmap.md`.
|
|
29
|
+
* Path (relative to the MDX file) to an `.annotations.yaml`
|
|
30
|
+
* file describing the screen's overlays. When set, the Image
|
|
31
|
+
* Service prefers yaml-driven badge composition over the
|
|
32
|
+
* legacy inline `<Overlay>` children.
|
|
33
|
+
*/
|
|
34
|
+
annotations?: string;
|
|
19
35
|
}
|
|
20
|
-
const { id, src, alt } = Astro.props;
|
|
36
|
+
const { id, src, alt, annotations } = Astro.props;
|
|
21
37
|
---
|
|
22
|
-
<figure
|
|
38
|
+
<figure
|
|
39
|
+
class="annot-screen"
|
|
40
|
+
data-screen-id={id}
|
|
41
|
+
data-annotations-path={annotations}
|
|
42
|
+
>
|
|
23
43
|
<img src={src} alt={alt ?? `Screen ${id}`} loading="lazy" />
|
|
24
|
-
<
|
|
44
|
+
<ol class="annot-screen-overlays" data-screen-overlays={id}>
|
|
25
45
|
<slot />
|
|
26
|
-
</
|
|
46
|
+
</ol>
|
|
27
47
|
</figure>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { EmbedMode } from '@ingcreators/annot-embed-protocol';
|
|
2
|
+
export interface ResolvedEditorConfig {
|
|
3
|
+
readonly embedMode: EmbedMode;
|
|
4
|
+
readonly cloudUrl: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Phase 5 OSS-side built-in defaults. Mirror the per-component
|
|
8
|
+
* defaults declared inside `<AnnotEditButton>` (`mode="newTab"`,
|
|
9
|
+
* `cloudUrl="https://annot.work"`) so consumers without the
|
|
10
|
+
* integration installed still see consistent behaviour.
|
|
11
|
+
*/
|
|
12
|
+
export declare const ANNOT_EDITOR_CONFIG: ResolvedEditorConfig;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export type { CacheKeyInput, FileCache } from './cache.js';
|
|
2
2
|
export { cacheKey, createFileCache, createMemoryCache, RENDER_PIPELINE_VERSION, } from './cache.js';
|
|
3
3
|
export type { GraphDirection, GraphEdge, Match, OverlayIntent, ScreenListEntry, TransitionEntry, } from './components/types.js';
|
|
4
|
-
export type {
|
|
5
|
-
export {
|
|
4
|
+
export type { ResolvedEditorConfig } from './editor-config-virtual.js';
|
|
5
|
+
export { ANNOT_EDITOR_CONFIG } from './editor-config-virtual.js';
|
|
6
|
+
export type { ProductDocsEditorOptions, ProductDocsIntegrationOptions, } from './integration.js';
|
|
7
|
+
export { editorConfigVirtualPlugin, productDocsIntegration, resolveEditorConfig, } from './integration.js';
|
|
6
8
|
export type { RenderAnnotatedScreenOptions, RenderResult, } from './render.js';
|
|
7
9
|
export { renderAnnotatedScreen, resolveMdxAnnotations, svgFromBboxAnnotations } from './render.js';
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { createHash as e } from "node:crypto";
|
|
2
2
|
import { mkdir as t, readFile as n, writeFile as r } from "node:fs/promises";
|
|
3
3
|
import { dirname as i, isAbsolute as a, join as o, resolve as s } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { burnRedactions as c, createAnnotator as l } from "@ingcreators/annot-annotator";
|
|
5
|
+
import { readElementTreePng as u } from "@ingcreators/annot-core";
|
|
6
|
+
import { buildBadgeAnnotations as d, buildBadgeAnnotationsFromYaml as f, buildRasterRedactRegionsFromYaml as p, buildShapeAnnotationsFromYaml as m, elementTreeToBoxedEntries as h, emptyAnnotationsSvg as g, parseAnnotationsYaml as _, parseMdxFile as v, parseSnapshotBoxes as y, resolveMdxAnnotations as b, svgFromBboxAnnotations as x, svgFromBboxAnnotations as S, warnLegacyOverlay as C } from "@ingcreators/annot-product-docs";
|
|
6
7
|
//#region src/cache.ts
|
|
7
|
-
var
|
|
8
|
-
function
|
|
8
|
+
var w = 2;
|
|
9
|
+
function T(t) {
|
|
9
10
|
let n = e("sha256");
|
|
10
|
-
return n.update("
|
|
11
|
+
return n.update("v2\n"), n.update(t.mdxSource), n.update("\0"), n.update(t.screenId), t.editable && n.update("\0editable"), t.annotationsYamlSource !== void 0 && (n.update("\0annotationsYaml\0"), n.update(t.annotationsYamlSource)), n.digest("hex");
|
|
11
12
|
}
|
|
12
|
-
function
|
|
13
|
+
function E(e) {
|
|
13
14
|
return {
|
|
14
15
|
dir: e,
|
|
15
16
|
async get(t) {
|
|
@@ -27,7 +28,7 @@ function v(e) {
|
|
|
27
28
|
}
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
|
-
function
|
|
31
|
+
function D() {
|
|
31
32
|
let e = /* @__PURE__ */ new Map();
|
|
32
33
|
return {
|
|
33
34
|
dir: ":memory:",
|
|
@@ -40,79 +41,133 @@ function y() {
|
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
//#endregion
|
|
43
|
-
//#region src/
|
|
44
|
-
var
|
|
45
|
-
|
|
44
|
+
//#region src/editor-config-virtual.ts
|
|
45
|
+
var O = {
|
|
46
|
+
embedMode: "newTab",
|
|
47
|
+
cloudUrl: "https://annot.work"
|
|
48
|
+
}, k = "@ingcreators/annot-product-docs-astro", A = {
|
|
49
|
+
embedMode: "newTab",
|
|
50
|
+
cloudUrl: "https://annot.work"
|
|
51
|
+
}, j = "virtual:annot-docs/editor-config", M = `\0${j}`, N = "editor-config-virtual";
|
|
52
|
+
function P(e) {
|
|
53
|
+
return {
|
|
54
|
+
name: "annot-docs:editor-config-virtual",
|
|
55
|
+
enforce: "pre",
|
|
56
|
+
resolveId(e) {
|
|
57
|
+
return e === j || e.endsWith(`${N}.ts`) || e.endsWith(`${N}.js`) ? M : null;
|
|
58
|
+
},
|
|
59
|
+
load(t) {
|
|
60
|
+
return t === M ? `export const ANNOT_EDITOR_CONFIG = ${JSON.stringify(e)};\n` : null;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function F(e) {
|
|
65
|
+
return {
|
|
66
|
+
embedMode: e?.embedMode ?? A.embedMode,
|
|
67
|
+
cloudUrl: e?.cloudUrl ?? A.cloudUrl
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function I(e = {}) {
|
|
46
71
|
let t = {
|
|
47
72
|
contentDir: e.contentDir ?? "docs",
|
|
48
73
|
configPath: e.configPath ?? "annot-docs.config.ts",
|
|
74
|
+
editor: e.editor,
|
|
49
75
|
verbose: e.verbose ?? !1
|
|
50
|
-
};
|
|
76
|
+
}, n = F(t.editor);
|
|
51
77
|
return {
|
|
52
|
-
name:
|
|
53
|
-
hooks: { "astro:config:setup": ({ logger: e }) => {
|
|
54
|
-
t.verbose && e.info(`${
|
|
78
|
+
name: k,
|
|
79
|
+
hooks: { "astro:config:setup": ({ logger: e, updateConfig: r }) => {
|
|
80
|
+
t.verbose && e.info(`${k} installed — contentDir=${t.contentDir} configPath=${t.configPath} editor.embedMode=${n.embedMode} editor.cloudUrl=${n.cloudUrl}`), r({ vite: { plugins: [P(n)] } });
|
|
55
81
|
} }
|
|
56
82
|
};
|
|
57
83
|
}
|
|
58
84
|
//#endregion
|
|
59
85
|
//#region src/render.ts
|
|
60
|
-
async function
|
|
61
|
-
let t = e.cwd ?? process.cwd(), n = a(e.mdxPath) ? e.mdxPath : s(t, e.mdxPath), r = await
|
|
86
|
+
async function L(e) {
|
|
87
|
+
let t = e.cwd ?? process.cwd(), n = a(e.mdxPath) ? e.mdxPath : s(t, e.mdxPath), r = await v(n);
|
|
62
88
|
if (!r) throw Error(`renderAnnotatedScreen: ${e.mdxPath} has no \`annot:\` frontmatter — cannot render.`);
|
|
63
89
|
let o = r.screens.find((t) => t.id === e.screenId);
|
|
64
90
|
if (!o) throw Error(`renderAnnotatedScreen: ${e.mdxPath} has no <Screen id="${e.screenId}"> block.`);
|
|
65
|
-
let
|
|
91
|
+
let u = R(e.editable), _ = i(n), b = await B(o.annotations, _), x = T({
|
|
66
92
|
mdxSource: r.source,
|
|
67
93
|
screenId: e.screenId,
|
|
68
|
-
editable:
|
|
94
|
+
editable: u !== null,
|
|
95
|
+
annotationsYamlSource: b?.source
|
|
69
96
|
});
|
|
70
97
|
if (e.cache) {
|
|
71
|
-
let t = await e.cache.get(
|
|
98
|
+
let t = await e.cache.get(x);
|
|
72
99
|
if (t) return {
|
|
73
100
|
bytes: t,
|
|
74
101
|
fromCache: !0,
|
|
75
102
|
hadBoundingBoxes: !0
|
|
76
103
|
};
|
|
77
104
|
}
|
|
78
|
-
let
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
105
|
+
let w = e.basePngBytes ?? await V(r, o.src, _), E = H(w), D, O = z(w);
|
|
106
|
+
D = O ? h(O) : y(r.commentBlocks.snapshot ?? "");
|
|
107
|
+
let k = b && b.file.annotations ? p(b.file.annotations, D) : [], A = k.length > 0 ? await c(w, k) : w;
|
|
108
|
+
!b && o.overlays.length > 0 && C({
|
|
109
|
+
mdxPath: e.mdxPath,
|
|
110
|
+
screenId: e.screenId,
|
|
111
|
+
overlayCount: o.overlays.length
|
|
112
|
+
});
|
|
113
|
+
let j = b ? f(b.file.overlays, D, E) : d(o.overlays, D, E), M = [...b && b.file.annotations ? m(b.file.annotations, D, E) : [], ...j], N, P;
|
|
114
|
+
if (M.length === 0) if (P = k.length > 0, u !== null) {
|
|
115
|
+
let e = `data:image/png;base64,${Buffer.from(A).toString("base64")}`;
|
|
116
|
+
N = l({ loadSystemFonts: !0 }).toEditablePng({
|
|
82
117
|
originalDataUrl: e,
|
|
83
|
-
annotationsSvg:
|
|
84
|
-
width:
|
|
85
|
-
height:
|
|
86
|
-
tags:
|
|
118
|
+
annotationsSvg: g(),
|
|
119
|
+
width: E.width,
|
|
120
|
+
height: E.height,
|
|
121
|
+
tags: u.tags
|
|
87
122
|
});
|
|
88
|
-
} else
|
|
123
|
+
} else N = A;
|
|
89
124
|
else {
|
|
90
|
-
let e = `data:image/png;base64,${Buffer.from(
|
|
125
|
+
let e = `data:image/png;base64,${Buffer.from(A).toString("base64")}`, t = S(M), n = l({ loadSystemFonts: !0 }), r = {
|
|
91
126
|
originalDataUrl: e,
|
|
92
127
|
annotationsSvg: t,
|
|
93
|
-
width:
|
|
94
|
-
height:
|
|
128
|
+
width: E.width,
|
|
129
|
+
height: E.height
|
|
95
130
|
};
|
|
96
|
-
|
|
131
|
+
N = u === null ? n.toPng(r) : n.toEditablePng({
|
|
97
132
|
...r,
|
|
98
|
-
tags:
|
|
99
|
-
}),
|
|
133
|
+
tags: u.tags
|
|
134
|
+
}), P = !0;
|
|
100
135
|
}
|
|
101
|
-
return e.cache && await e.cache.set(
|
|
102
|
-
bytes:
|
|
136
|
+
return e.cache && await e.cache.set(x, N), {
|
|
137
|
+
bytes: N,
|
|
103
138
|
fromCache: !1,
|
|
104
|
-
hadBoundingBoxes:
|
|
139
|
+
hadBoundingBoxes: P
|
|
105
140
|
};
|
|
106
141
|
}
|
|
107
|
-
function
|
|
142
|
+
function R(e) {
|
|
108
143
|
return e ? e === !0 ? {} : e : null;
|
|
109
144
|
}
|
|
110
|
-
|
|
145
|
+
function z(e) {
|
|
146
|
+
try {
|
|
147
|
+
return u(e);
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function B(e, t) {
|
|
153
|
+
if (!e) return null;
|
|
154
|
+
let r = a(e) ? e : s(t, e), i;
|
|
155
|
+
try {
|
|
156
|
+
i = await n(r, "utf8");
|
|
157
|
+
} catch (t) {
|
|
158
|
+
throw Error(`renderAnnotatedScreen: <Screen annotations="${e}"> — failed to read ${r}: ${t.message}`);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
file: _(i),
|
|
162
|
+
source: i
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
async function V(e, t, r) {
|
|
111
166
|
if (!t) throw Error("renderAnnotatedScreen: <Screen src=...> is required — cannot render without a base image.");
|
|
112
167
|
let i = await n(a(t) ? t : s(r, t));
|
|
113
168
|
return new Uint8Array(i.buffer, i.byteOffset, i.byteLength);
|
|
114
169
|
}
|
|
115
|
-
function
|
|
170
|
+
function H(e) {
|
|
116
171
|
if (e.length < 24) throw Error("renderAnnotatedScreen: base PNG too short to contain IHDR.");
|
|
117
172
|
let t = new DataView(e.buffer, e.byteOffset, e.byteLength);
|
|
118
173
|
return {
|
|
@@ -121,4 +176,4 @@ function T(e) {
|
|
|
121
176
|
};
|
|
122
177
|
}
|
|
123
178
|
//#endregion
|
|
124
|
-
export {
|
|
179
|
+
export { O as ANNOT_EDITOR_CONFIG, w as RENDER_PIPELINE_VERSION, T as cacheKey, E as createFileCache, D as createMemoryCache, P as editorConfigVirtualPlugin, I as productDocsIntegration, L as renderAnnotatedScreen, F as resolveEditorConfig, b as resolveMdxAnnotations, x as svgFromBboxAnnotations };
|
package/dist/integration.d.ts
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
|
+
import { EmbedMode } from '@ingcreators/annot-embed-protocol';
|
|
1
2
|
import { AstroIntegration } from 'astro';
|
|
3
|
+
import { ResolvedEditorConfig } from './editor-config-virtual.js';
|
|
4
|
+
export interface EditorConfigVitePlugin {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly enforce?: "pre" | "post";
|
|
7
|
+
readonly resolveId: (id: string) => string | null;
|
|
8
|
+
readonly load: (id: string) => string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface ProductDocsEditorOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Default embed mode for every `<AnnotEditButton>` in the
|
|
13
|
+
* site. Per-call `mode` prop wins when set. Defaults to
|
|
14
|
+
* `"newTab"`.
|
|
15
|
+
*/
|
|
16
|
+
embedMode?: EmbedMode;
|
|
17
|
+
/**
|
|
18
|
+
* Cloud editor origin override for on-prem deployments
|
|
19
|
+
* (e.g. `"https://annot.internal.example.com"`). Per-call
|
|
20
|
+
* `cloudUrl` prop wins when set. Defaults to
|
|
21
|
+
* `"https://annot.work"`.
|
|
22
|
+
*/
|
|
23
|
+
cloudUrl?: string;
|
|
24
|
+
}
|
|
2
25
|
export interface ProductDocsIntegrationOptions {
|
|
3
26
|
/**
|
|
4
27
|
* Directory (relative to the Astro project root) that contains
|
|
@@ -13,6 +36,11 @@ export interface ProductDocsIntegrationOptions {
|
|
|
13
36
|
* `meta` defaults + per-book template lookups.
|
|
14
37
|
*/
|
|
15
38
|
configPath?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Project-wide `<AnnotEditButton>` defaults. Phase 5f of
|
|
41
|
+
* `docs/plans/living-spec-authoring-roadmap.md`.
|
|
42
|
+
*/
|
|
43
|
+
editor?: ProductDocsEditorOptions;
|
|
16
44
|
/**
|
|
17
45
|
* If `true`, the integration logs a debug line on every
|
|
18
46
|
* config:setup. Useful for verifying the integration is
|
|
@@ -20,6 +48,28 @@ export interface ProductDocsIntegrationOptions {
|
|
|
20
48
|
*/
|
|
21
49
|
verbose?: boolean;
|
|
22
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Vite plugin that:
|
|
53
|
+
*
|
|
54
|
+
* 1. Generates a virtual module (`virtual:annot-docs/editor-config`)
|
|
55
|
+
* exporting the resolved editor config.
|
|
56
|
+
*
|
|
57
|
+
* 2. Aliases every import of the package's
|
|
58
|
+
* `editor-config-virtual.ts` to the virtual module so
|
|
59
|
+
* `<AnnotEditButton>` picks up the project-wide defaults.
|
|
60
|
+
*
|
|
61
|
+
* Exported here for vitest. Consumers don't need to import it
|
|
62
|
+
* directly — `productDocsIntegration` wires it up automatically.
|
|
63
|
+
*/
|
|
64
|
+
export declare function editorConfigVirtualPlugin(resolved: ResolvedEditorConfig): EditorConfigVitePlugin;
|
|
65
|
+
/**
|
|
66
|
+
* Merge the per-call editor options into the built-in defaults.
|
|
67
|
+
* Future revision (Phase 5f follow-up if needed) also reads
|
|
68
|
+
* `annot-docs.config.ts`'s `editor` config + merges in the
|
|
69
|
+
* middle of this precedence ladder; today the integration
|
|
70
|
+
* option is the only override layer.
|
|
71
|
+
*/
|
|
72
|
+
export declare function resolveEditorConfig(editor: ProductDocsEditorOptions | undefined): ResolvedEditorConfig;
|
|
23
73
|
/**
|
|
24
74
|
* Astro integration factory. Drop into `astro.config.mjs`:
|
|
25
75
|
*
|
|
@@ -28,19 +78,19 @@ export interface ProductDocsIntegrationOptions {
|
|
|
28
78
|
* import { productDocsIntegration } from "@ingcreators/annot-product-docs-astro";
|
|
29
79
|
*
|
|
30
80
|
* export default defineConfig({
|
|
31
|
-
* integrations: [
|
|
81
|
+
* integrations: [
|
|
82
|
+
* productDocsIntegration({
|
|
83
|
+
* editor: {
|
|
84
|
+
* embedMode: "newTab",
|
|
85
|
+
* cloudUrl: "https://annot.internal.example.com",
|
|
86
|
+
* },
|
|
87
|
+
* }),
|
|
88
|
+
* ],
|
|
32
89
|
* });
|
|
33
90
|
* ```
|
|
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
91
|
*/
|
|
39
92
|
export declare function productDocsIntegration(options?: ProductDocsIntegrationOptions): AstroIntegration;
|
|
40
93
|
/**
|
|
41
94
|
* 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
95
|
*/
|
|
46
96
|
export default productDocsIntegration;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ingcreators/annot-product-docs-astro",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|
|
@@ -39,6 +39,10 @@
|
|
|
39
39
|
},
|
|
40
40
|
"./components/Screen.astro": "./dist/components/Screen.astro",
|
|
41
41
|
"./components/Overlay.astro": "./dist/components/Overlay.astro",
|
|
42
|
+
"./components/AnnotCallout.astro": "./dist/components/AnnotCallout.astro",
|
|
43
|
+
"./components/AnnotEditButton.astro": "./dist/components/AnnotEditButton.astro",
|
|
44
|
+
"./components/AnnotEditorIframeModal.astro": "./dist/components/AnnotEditorIframeModal.astro",
|
|
45
|
+
"./components/AnnotEditCompleteListener.astro": "./dist/components/AnnotEditCompleteListener.astro",
|
|
42
46
|
"./components/Transition.astro": "./dist/components/Transition.astro",
|
|
43
47
|
"./components/TransitionTable.astro": "./dist/components/TransitionTable.astro",
|
|
44
48
|
"./components/HistoryEntry.astro": "./dist/components/HistoryEntry.astro",
|
|
@@ -54,17 +58,18 @@
|
|
|
54
58
|
"access": "public"
|
|
55
59
|
},
|
|
56
60
|
"devDependencies": {
|
|
57
|
-
"@playwright/test": "^1.
|
|
58
|
-
"astro": "^6.
|
|
61
|
+
"@playwright/test": "^1.61.1",
|
|
62
|
+
"astro": "^6.4.8",
|
|
59
63
|
"typescript": "^6.0.3",
|
|
60
|
-
"vite": "^8.0.
|
|
61
|
-
"vite-plugin-dts": "^5.0.
|
|
64
|
+
"vite": "^8.0.16",
|
|
65
|
+
"vite-plugin-dts": "^5.0.3"
|
|
62
66
|
},
|
|
63
67
|
"dependencies": {
|
|
64
|
-
"@ingcreators/annot-
|
|
65
|
-
"@ingcreators/annot-
|
|
66
|
-
"@ingcreators/annot-
|
|
67
|
-
"@ingcreators/annot-
|
|
68
|
+
"@ingcreators/annot-core": "0.3.0",
|
|
69
|
+
"@ingcreators/annot-annotator": "0.6.0",
|
|
70
|
+
"@ingcreators/annot-embed-protocol": "0.1.0",
|
|
71
|
+
"@ingcreators/annot-product-docs": "0.4.0",
|
|
72
|
+
"@ingcreators/annot-playwright": "0.4.1"
|
|
68
73
|
},
|
|
69
74
|
"peerDependencies": {
|
|
70
75
|
"astro": "^5.0.0 || ^6.0.0"
|