@ingcreators/annot-core 0.2.1 → 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,116 @@
1
1
  # @ingcreators/annot-core
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - b47d896: **Publish the `./element-tree` subpath** — the published tarball now
8
+ serves `@ingcreators/annot-core/element-tree` (Tier A screen-capture
9
+ model + YAML / JSON serializers + walk/find utilities).
10
+
11
+ ### Root cause
12
+
13
+ `@ingcreators/annot-playwright`'s built `dist/index.js` externalises
14
+ its `@ingcreators/annot-core/element-tree` import, but core's
15
+ `publishConfig.exports` only mapped `.`, `./xmp-bytes`, and
16
+ `./styles/*` — the subpath resolved in the workspace (dev `exports`
17
+ map to `src/`) and failed for every registry consumer:
18
+
19
+ ```
20
+ Error: Package subpath './element-tree' is not defined by "exports"
21
+ in .../node_modules/@ingcreators/annot-core/package.json imported
22
+ from .../@ingcreators/annot-playwright/dist/index.js
23
+ ```
24
+
25
+ First observed in the workflow-app docs tour after the 0.4.1
26
+ publish repaired the `writeFile` dist crash — `playwright test`
27
+ now dies at config load before any test runs.
28
+
29
+ ### Fix
30
+ - `vite.config.ts` gains an `element-tree` library entry
31
+ (`src/element-tree/index.ts` → `dist/element-tree.js`).
32
+ - `publishConfig.exports` maps `./element-tree` to the built JS +
33
+ the emitted `dist/element-tree/index.d.ts`.
34
+
35
+ ### Verification
36
+ - `pnpm --filter @ingcreators/annot-core build` emits
37
+ `dist/element-tree.js` + `dist/element-tree/index.d.ts`.
38
+ - Packed tarball installed into a scratch project resolves
39
+ `import("@ingcreators/annot-core/element-tree")` and exposes the
40
+ full serializer / walk surface.
41
+
42
+ ## 0.3.0
43
+
44
+ ### Minor Changes
45
+
46
+ - 691bec5: **Add `flattenEditablePng(pngBytes) → pngBytes`** — Phase 3j of
47
+ `docs/plans/living-spec-authoring-roadmap.md` (Phase 3
48
+ follow-up #2). The editor's editable-PNG format embeds the
49
+ original un-annotated capture + the annotations SVG in PNG
50
+ ancillary chunks for re-edit; "flatten" drops those chunks and
51
+ keeps just the visible (already-annotated) bytes.
52
+
53
+ ### `@ingcreators/annot-annotator` — new public surface
54
+
55
+ ```ts
56
+ import { flattenEditablePng } from "@ingcreators/annot-annotator";
57
+
58
+ const flat = flattenEditablePng(editablePngBytes);
59
+ // → flat PNG: same visible pixels, no Adobe XMP iTXt chunk,
60
+ // no custom svGo chunk. `readEditablePngBytes(flat)` returns
61
+ // null. File size drops significantly (the editable layer
62
+ // roughly doubled the bytes).
63
+ ```
64
+
65
+ ### `@ingcreators/annot-core/xmp-bytes` — new public surface
66
+
67
+ The implementation lives in `@ingcreators/annot-core` as
68
+ `stripPngEditableLayer` — the same chunk-walking helper that
69
+ `writePngWithMetadata` / `writePngWithTagsOnly` already used
70
+ internally to clean stale metadata before re-injecting. Now
71
+ exported so other Tier A consumers (not just annotator) can
72
+ use it directly.
73
+
74
+ annotator's `flattenEditablePng` is a one-line wrapper that
75
+ calls `stripPngEditableLayer` under a more user-facing name.
76
+
77
+ ### Why this is metadata removal, not re-rasterization
78
+
79
+ `toEditablePng` rasterizes the SVG fragment onto the base image
80
+ FIRST and embeds the editable layer as ancillary PNG chunks
81
+ (`iTXt` carrying Adobe XMP + custom `svGo` chunk). The visible
82
+ bytes are already the annotated bitmap. Flattening strips the
83
+ ancillary chunks; the IDAT pixel data stays byte-identical.
84
+ No decode, no re-encode, no `@napi-rs/canvas` round-trip.
85
+
86
+ ### Use cases
87
+ - **Publish-flat** — editor session → distribution-ready PNG;
88
+ the editable layer is dead weight for downstream consumers
89
+ (Slack drop, third-party viewers).
90
+ - **File size** — editable PNG roughly doubles in bytes
91
+ (original + SVG embedded); flattening drops the overhead.
92
+ - **Privacy hardening** — `burnRedactions` is the strong
93
+ version for _redact_ regions; flattening drops the
94
+ recoverable original entirely for _all_ annotations,
95
+ including non-redact ones whose annotated visual the
96
+ publisher wants to keep but whose original capture they
97
+ don't want shippable.
98
+
99
+ ### Internal rename in annot-core
100
+
101
+ The private `removePngMetadata` helper in
102
+ `@ingcreators/annot-core/xmp-bytes` is renamed to
103
+ `stripPngEditableLayer` (clearer name describing what it does
104
+ rather than how it's used). Internal callers in the same
105
+ module updated. No external API change for the rename itself;
106
+ `writePngWithMetadata` + `writePngWithTagsOnly` keep their
107
+ existing signatures + behaviour.
108
+
109
+ ### Compatibility
110
+
111
+ Additive on annotator + core. No behaviour change for existing
112
+ callers (the only internal rename is a private helper).
113
+
3
114
  ## 0.2.1
4
115
 
5
116
  ### Patch Changes
package/README.md CHANGED
@@ -40,7 +40,7 @@ headless tool against the SDK.
40
40
  |---------|------|---------|---|
41
41
  | `@ingcreators/annot-core` | A | Re-exports everything from `/headless` — DOM-free root entry | ✅ |
42
42
  | `@ingcreators/annot-core/headless` | A | Same as root, kept as an alias for callers that want to be explicit | workspace only (v0.1.0) |
43
- | `@ingcreators/annot-core/storage` | A | `ImageRecord`, `FolderRecord`, `PageElement`, `PageMetadata`, `StorageProvider` | workspace only (v0.1.0) |
43
+ | `@ingcreators/annot-core/storage` | A | `ImageRecord`, `FolderRecord`, `StorageProvider` | workspace only (v0.1.0) |
44
44
  | `@ingcreators/annot-core/utils` | A | `assertNonNull`, `computeDasharray`, `newIdB58`, defaults | workspace only (v0.1.0) |
45
45
  | `@ingcreators/annot-core/zip` | A | ZIP builder used by PPTX export | workspace only (v0.1.0) |
46
46
  | `@ingcreators/annot-core/encode` | A | Image encode helpers | workspace only (v0.1.0) |
@@ -1,4 +1,5 @@
1
1
  export { createHistoryCore, DEFAULT_HISTORY_DEPTH, type HistoryCore, type HistoryHooks, } from './history-core.js';
2
+ export type { OverlayRegionPickDetail } from './overlay-pick-types.js';
2
3
  export { CATEGORY_CONTROL_SHAPE, classifyPropertyElement, classifyPropertySelection, PROPERTY_CONTROL_IDS, PROPERTY_CONTROLS, PROPERTY_EFFECT_IDS, type PropertyCategory, type PropertyControlDef, type PropertyControlId, type PropertyControlOption, type PropertyControlType, type PropertyEffectId, } from './property-schema.js';
3
4
  export { computeSnap, cursorForAngle, type Rect, rotateAround, type SnapGuide, type SnapInput, type SnapResult, } from './selection-geometry.js';
4
5
  export { ANNOT_SVG_VERSION, ANNOT_SVG_VERSION_ATTR, ANNOT_SVG_VERSION_UNSTAMPED, getAnnotVersionFromString, readAnnotVersion, stampAnnotVersion, } from './svg-format.js';
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Shared types for the snapshot region pick flow — Phase 4d of
3
+ * [`docs/plans/living-spec-authoring-roadmap.md`](../../../../docs/plans/living-spec-authoring-roadmap.md).
4
+ *
5
+ * The `<annot-snapshot-overlay>` Lit element (host-ui) and the
6
+ * `OverlayTool` (editor) both deal with snapshot-region picks but
7
+ * live in packages that don't depend on each other (host-ui →
8
+ * editor, not the reverse). Lifting the detail type into Tier A
9
+ * core lets both sides import it without forcing a cycle.
10
+ *
11
+ * Pure data types — no DOM, no Node-specific APIs.
12
+ */
13
+ /**
14
+ * Payload of the `overlay-region-pick` CustomEvent fired by
15
+ * `<annot-snapshot-overlay>` when the user clicks a region.
16
+ * Consumed by `OverlayTool` to build an `OverlayProposal` for the
17
+ * intent picker dialog.
18
+ */
19
+ export interface OverlayRegionPickDetail {
20
+ /** Tree-unique identifier of the picked node (e.g. `"e3"`).
21
+ * Stable within one capture only — annotation persistence keys
22
+ * on `match: { role, name }` instead. */
23
+ ref: string;
24
+ /** ARIA role of the picked node. Always present. */
25
+ role: string;
26
+ /** Accessible name, when the node carries one. */
27
+ name?: string;
28
+ /** Page-space bounding box of the picked node. CSS px. */
29
+ bbox: {
30
+ x: number;
31
+ y: number;
32
+ width: number;
33
+ height: number;
34
+ };
35
+ }
@@ -316,5 +316,5 @@ export declare function normalizeVariantSideFields(toolId: string, newVariant: s
316
316
  export declare const TOOL_REGISTRY: Readonly<Record<string, ToolRegistryEntry>>;
317
317
  /** All tool ids the toolbar exposes. Frozen tuple form for tests
318
318
  * that assert the registry covers exactly this set. */
319
- export declare const TOOL_REGISTRY_IDS: readonly ["arrow", "shape", "highlight", "text", "freehand", "marker", "redact", "crop"];
319
+ export declare const TOOL_REGISTRY_IDS: readonly ["arrow", "shape", "highlight", "text", "freehand", "marker", "redact", "crop", "overlay"];
320
320
  export type ToolRegistryId = (typeof TOOL_REGISTRY_IDS)[number];
@@ -0,0 +1,4 @@
1
+ export { parseElementTreeFromJson, serializeElementTreeToJson, validateElementTree, } from './json.js';
2
+ export { type BBox, type ElementNode, type ElementTree, type ElementTreeSource, type ElementTreeViewport, isElementTreeShape, } from './types.js';
3
+ export { type ElementMatch, type ElementTreeVisitor, findByMatch, findByRef, flattenTree, walkTree, } from './walk.js';
4
+ export { parseElementTreeFromYaml, serializeElementTreeToYaml } from './yaml.js';
@@ -0,0 +1,21 @@
1
+ import { ElementTree } from './types.js';
2
+ /**
3
+ * Serialize an ElementTree to a JSON string. Uses 2-space indent for
4
+ * readability; pass `compact: true` for a single-line minified
5
+ * form. Optional / undefined fields are omitted from the output to
6
+ * keep the serialization round-trip stable.
7
+ */
8
+ export declare function serializeElementTreeToJson(tree: ElementTree, options?: {
9
+ compact?: boolean;
10
+ }): string;
11
+ /**
12
+ * Parse a JSON string into an ElementTree. Throws `Error` with a
13
+ * descriptive message on malformed input — the parser is strict to
14
+ * surface schema drift early.
15
+ */
16
+ export declare function parseElementTreeFromJson(json: string): ElementTree;
17
+ /**
18
+ * Strict structural validation of a parsed `unknown` payload.
19
+ * Exported so the YAML parser can share the same validation pass.
20
+ */
21
+ export declare function validateElementTree(value: unknown): ElementTree;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Top-level ElementTree wire format. Stored in PNG XMP as iTXt
3
+ * (`annot:elementTree` keyword, deflate-compressed YAML by default —
4
+ * see `@ingcreators/annot-core/xmp` extension landed in Phase 1d).
5
+ */
6
+ export interface ElementTree {
7
+ /** Schema version. Currently 1. Reader version-dispatches; writers
8
+ * always emit the current schema. */
9
+ version: 1;
10
+ /** Metadata about who produced this capture. */
11
+ source: ElementTreeSource;
12
+ /** Capture viewport. CSS px; `scale` is the device-pixel-ratio. */
13
+ viewport: ElementTreeViewport;
14
+ /** Root node. Always present. Often `role: "main"` or `"document"`
15
+ * for browser captures; for partial captures (area / element-scoped)
16
+ * the root is the captured element itself. */
17
+ root: ElementNode;
18
+ }
19
+ export interface ElementTreeSource {
20
+ /** Which capture tool produced this tree. Common values:
21
+ * `"extension"` (browser extension MAIN-world walker),
22
+ * `"playwright"` (ariaSnapshot adapter). Open-ended so future
23
+ * Figma / screen-recorder / OCR adapters slot in cleanly. */
24
+ kind: "extension" | "playwright" | (string & {});
25
+ /** ISO 8601 timestamp of when the tree was produced. */
26
+ capturedAt: string;
27
+ /** Optional human-readable tool identifier (e.g.
28
+ * `"annot-playwright@0.4.0"`). Used in `annot show-xmp` output for
29
+ * debug; not consumed programmatically. */
30
+ agent?: string;
31
+ /** Source page URL at capture time, if applicable. */
32
+ url?: string;
33
+ }
34
+ export interface ElementTreeViewport {
35
+ /** Viewport width in CSS px. */
36
+ width: number;
37
+ /** Viewport height in CSS px. */
38
+ height: number;
39
+ /** `window.devicePixelRatio` (or equivalent) at capture time. Used
40
+ * to map CSS-px bboxes onto device-px screenshot coordinates. */
41
+ scale: number;
42
+ }
43
+ /**
44
+ * Per-node payload. Children are nested in `children`; the tree is
45
+ * walked depth-first.
46
+ */
47
+ export interface ElementNode {
48
+ /** ARIA role. Always present — decorative nodes use `"generic"` or
49
+ * `"presentation"`. Browser captures fall back to the implicit
50
+ * role for the underlying tag when ARIA doesn't apply. */
51
+ role: string;
52
+ /** Accessible name. Omitted when empty. */
53
+ name?: string;
54
+ /** Page-space bounding box in CSS px. Omitted for hidden / abstract
55
+ * nodes (e.g. document root with no measurable extent). */
56
+ bbox?: BBox;
57
+ /** Tree-unique identifier in `e<n>` format (depth-first numbering
58
+ * starting at `e1`). Stable WITHIN one capture; NOT stable across
59
+ * re-captures — see OQ-11 in the roadmap. */
60
+ ref: string;
61
+ /** ARIA-derived state tokens. Single tokens like `"checked"`,
62
+ * `"pressed"`, `"expanded"`, `"selected"`, `"disabled"`,
63
+ * `"required"`, `"invalid"`. Also accepts `key=value` tokens for
64
+ * scalar states: `"level=2"`, `"valuetext=10"`. */
65
+ states?: readonly string[];
66
+ /** HTML attribute snapshot (whitelist-filtered at capture time).
67
+ * Keys are lowercase attribute names; values are the attribute
68
+ * strings. Empty when no attributes were captured. */
69
+ attributes?: Readonly<Record<string, string>>;
70
+ /** Direct text content (heading body, paragraph text, textbox
71
+ * value). Distinct from `name` for elements where they differ
72
+ * (e.g. a textbox with placeholder `"Email"` and current value
73
+ * `"alice@example.com"` has `name: "Email"`, `text:
74
+ * "alice@example.com"`). */
75
+ text?: string;
76
+ /** Children in document order. Omitted for leaf nodes. */
77
+ children?: readonly ElementNode[];
78
+ }
79
+ /**
80
+ * Axis-aligned bounding box. CSS px, page-space coordinates.
81
+ */
82
+ export interface BBox {
83
+ x: number;
84
+ y: number;
85
+ width: number;
86
+ height: number;
87
+ }
88
+ /**
89
+ * Convenience type guard for callers that receive `unknown` payloads
90
+ * and need to check whether they look like an ElementTree before
91
+ * narrowing. Light validation only — checks for the presence of the
92
+ * three required top-level keys plus `version === 1`. The full
93
+ * parsers (`parseElementTreeFromYaml` / `parseElementTreeFromJson`)
94
+ * do exhaustive structural validation.
95
+ */
96
+ export declare function isElementTreeShape(value: unknown): value is ElementTree;
@@ -0,0 +1,49 @@
1
+ import { ElementNode, ElementTree } from './types.js';
2
+ /**
3
+ * Visitor callback. Return `false` to stop traversal early; return
4
+ * `void` / `true` to keep walking. The `parents` array gives the
5
+ * ancestor chain from the root down to (but excluding) the visited
6
+ * node — empty for the root itself.
7
+ */
8
+ export type ElementTreeVisitor = (node: ElementNode, parents: readonly ElementNode[]) => boolean | void;
9
+ /**
10
+ * Depth-first walk over every node in the tree. Stops early when the
11
+ * visitor returns `false`.
12
+ */
13
+ export declare function walkTree(tree: ElementTree, visit: ElementTreeVisitor): void;
14
+ /**
15
+ * Find the unique node with the given `ref`, or `null` if none.
16
+ * Refs are tree-unique within one capture (depth-first numbering),
17
+ * so the first match is the only match.
18
+ */
19
+ export declare function findByRef(tree: ElementTree, ref: string): ElementNode | null;
20
+ /**
21
+ * Match descriptor used by annotation yaml's `match: { role, name }`
22
+ * resolver. Both fields optional independently — a `{ role: "main" }`
23
+ * match catches every node with that role regardless of name; a
24
+ * `{ name: "Sign in" }` catches every node with that name regardless
25
+ * of role. When both are set, both must match (AND semantics). `name`
26
+ * comparison is exact.
27
+ */
28
+ export interface ElementMatch {
29
+ role?: string;
30
+ name?: string;
31
+ }
32
+ /**
33
+ * Find every node satisfying the match. Returns nodes in
34
+ * depth-first document order.
35
+ *
36
+ * Returning all matches (not just the first) lets callers detect
37
+ * ambiguity — drift detection flags `duplicated` when a unique
38
+ * `match` resolves to more than one node.
39
+ */
40
+ export declare function findByMatch(tree: ElementTree, match: ElementMatch): ElementNode[];
41
+ /**
42
+ * Produce a flat array of every node in depth-first document order.
43
+ * Each entry is the live `ElementNode` reference (no copying); the
44
+ * caller must treat the result as read-only.
45
+ *
46
+ * Useful for consumers that prefer flat iteration (search index
47
+ * builders, etc.). Prefer `walkTree` when ancestor context matters.
48
+ */
49
+ export declare function flattenTree(tree: ElementTree): ElementNode[];
@@ -0,0 +1,22 @@
1
+ import { ElementTree } from './types.js';
2
+ /**
3
+ * Serialize an ElementTree to YAML.
4
+ *
5
+ * The output uses 2-space indent, sorted top-level keys, and
6
+ * deterministic field ordering inside each node so the same tree
7
+ * always serializes to byte-identical YAML. This is important for
8
+ * the XMP write path — re-saving a PNG without semantic changes
9
+ * must produce identical bytes, otherwise git history thrashes
10
+ * pointlessly.
11
+ *
12
+ * Empty optional fields (`states`, `attributes`, `children`) are
13
+ * omitted entirely. The reader treats absence and empty as
14
+ * equivalent.
15
+ */
16
+ export declare function serializeElementTreeToYaml(tree: ElementTree): string;
17
+ /**
18
+ * Parse a YAML string into an ElementTree. Throws `Error` with a
19
+ * descriptive message on malformed input (parse failure or schema
20
+ * mismatch).
21
+ */
22
+ export declare function parseElementTreeFromYaml(yaml: string): ElementTree;