@laitszkin/apollo-toolkit 3.10.0 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
  3. package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
  4. package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
  5. package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
  6. package/generate-spec/SKILL.md +17 -15
  7. package/generate-spec/agents/openai.yaml +1 -1
  8. package/generate-spec/references/TEMPLATE_SPEC.md +103 -84
  9. package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
  10. package/init-project-html/SKILL.md +82 -126
  11. package/init-project-html/agents/openai.yaml +17 -8
  12. package/init-project-html/lib/atlas/assets/architecture.css +140 -0
  13. package/init-project-html/lib/atlas/assets/viewer.client.js +93 -0
  14. package/init-project-html/lib/atlas/cli.js +995 -0
  15. package/init-project-html/lib/atlas/layout.js +229 -0
  16. package/init-project-html/lib/atlas/render.js +485 -0
  17. package/init-project-html/lib/atlas/schema.js +310 -0
  18. package/init-project-html/lib/atlas/state.js +402 -0
  19. package/init-project-html/references/TEMPLATE_SPEC.md +123 -84
  20. package/init-project-html/sample-demo/resources/project-architecture/assets/architecture.css +139 -1058
  21. package/init-project-html/sample-demo/resources/project-architecture/assets/viewer.client.js +93 -0
  22. package/init-project-html/sample-demo/resources/project-architecture/atlas/atlas.index.yaml +34 -0
  23. package/init-project-html/sample-demo/resources/project-architecture/atlas/features/get-invite-codes.yaml +159 -0
  24. package/init-project-html/sample-demo/resources/project-architecture/atlas/features/invite-code-registration.yaml +160 -0
  25. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/index.html +67 -52
  26. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/invite-code-generator.html +48 -163
  27. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/invite-issuance-service.html +70 -196
  28. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/postgresql.html +64 -163
  29. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/public-api.html +68 -150
  30. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/web-get-invite-ui.html +65 -138
  31. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/index.html +61 -51
  32. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/postgresql.html +66 -159
  33. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/public-api.html +63 -143
  34. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/registration-service.html +77 -188
  35. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/web-register-ui.html +65 -138
  36. package/init-project-html/sample-demo/resources/project-architecture/index.html +232 -335
  37. package/init-project-html/scripts/architecture.js +65 -247
  38. package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
  39. package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
  40. package/package.json +6 -2
  41. package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
  42. package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
  43. package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
  44. package/spec-to-project-html/SKILL.md +61 -63
  45. package/spec-to-project-html/agents/openai.yaml +14 -8
  46. package/spec-to-project-html/references/TEMPLATE_SPEC.md +96 -83
  47. package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
@@ -1,129 +1,85 @@
1
1
  ---
2
2
  name: init-project-html
3
3
  description: >-
4
- Build the HTML architecture atlas. Macro `index.html` is ONE SVG showing feature-modules (clusters) AND every sub-module (nodes inside) together, with many-to-many edges, producer/consumer loops, and at least one cross-feature `m-edge--cross` data-row when applicable; macro MUST index every sub-module page in `atlas-submodule-index`. Each `features/<slug>/<sub-module>.html` describes ONLY itself: `sub-io` (function I/O) + `sub-vars` (variables-with-business-purpose) + `sub-dataflow` (internal flow) + `sub-errors`. Feature `index.html` stays lightweight (story + submodule-nav). Read strategy avoids context loss: list feature modules first; with subagents, dispatch one read-only subagent per feature and aggregate summaries before drawing; without subagents, read one feature, emit its pages, update macro incrementally, drop details, then move on — never read the whole codebase before writing. Distinctive typography; no purple-gradient/Inter defaults.
4
+ Declare the project HTML architecture atlas through `apltk architecture` CLI verbs instead of hand-writing SVG/HTML. The tool owns layout, no-overlap, DOM, CSS, and pan/zoom; the agent only declares features, sub-modules, functions, variables, dataflow steps, errors, and edges. Macro `index.html` always shows feature clusters with their sub-modules and every cross/intra-feature edge; each sub-module page describes only itself (function I/O + variables-with-business-purpose + internal dataflow + local errors); feature index pages stay lightweight. Read strategy avoids context loss: list feature modules first, then either dispatch one read-only subagent per feature and aggregate summaries before declaring, or process one feature at a time end-to-end. Anchor every declaration to repo evidence.
5
5
  ---
6
6
 
7
7
  # Init Project HTML
8
8
 
9
9
  ## Dependencies
10
10
 
11
- - Required: none (the template asset `references/architecture.css` ships with this skill and is copied at runtime).
12
- - Conditional: `spec-to-project-html` when the same atlas needs spec-driven refresh — that skill obeys the rules below.
13
- - Optional: `align-project-documents` when `docs/features` already names capabilities.
14
- - Optional: local `frontend-design` skill for bolder, non-generic visual direction.
15
- - Fallback: codebase too large for one pass → document scanned roots, explicit omissions, phased plan. **MUST NOT** pretend untread paths were verified.
11
+ - Required: none (the CLI ships its own layout engine, CSS, and pan/zoom client; `apltk architecture` is installed with this toolkit).
12
+ - Conditional: `spec-to-project-html` when the same atlas needs spec-driven refresh — that skill uses the same CLI with `--spec`.
13
+ - Optional: `align-project-documents` when `docs/features` already names the user-visible capabilities.
14
+ - Fallback: codebase too large for one pass → use the subagent strategy below, document scanned roots and explicit omissions in `meta.summary`. **MUST NOT** declare components for code paths that were never read.
16
15
 
17
16
  ## Non-negotiables
18
17
 
19
- ### Rule 1 — Cross-submodule interactions live only on the macro page
18
+ ### Rule 1 — Use the CLI; never hand-author atlas HTML
20
19
 
21
- - **MUST** render `resources/project-architecture/index.html` as **one SVG figure** showing both layers together:
22
- - Layer A: **feature ↔ feature** user-visible journeys.
23
- - Layer B: **sub-module ↔ sub-module** concrete calls / returns / data-row hand-offs that realise Layer A.
24
- - **MUST** draw each feature as an `m-cluster` (dashed frame); sub-modules are clickable nodes inside their cluster.
25
- - **MUST** include at least one node-pair carrying **≥2 edges** (e.g. service → db with SELECT / INSERT / UPDATE) to prove multi-edge support.
26
- - **MUST** include at least one **producer ↔ consumer** loop (call/return pair, or cyclic data-row).
27
- - **MUST** use **`m-edge--cross`** dashed-heavy edges for cross-feature data-row hand-offs whenever the system has them.
28
- - **MUST** include `<nav class="atlas-submodule-index">` linking every sub-module page in the atlas.
20
+ The atlas state lives in `resources/project-architecture/atlas/` (`atlas.index.yaml` + per-feature YAMLs). Every change goes through `apltk architecture <verb> ...`. After any mutation, the CLI re-renders `resources/project-architecture/**/*.html` with the correct layout, CSS, pan/zoom, and ARIA — agents **MUST NOT** edit those HTML files by hand or invent additional ones.
29
21
 
30
- ### Rule 2 — Sub-module pages describe **only themselves**
22
+ ### Rule 2 — Sub-module pages describe only themselves
31
23
 
32
- Each `features/<slug>/<sub-module>.html` contains exactly:
24
+ What the CLI emits on each sub-module page is fixed:
33
25
 
34
- 1. `sub-io` function I/O table (signature + side-effect chip + one-line purpose).
35
- 2. `sub-vars` — **variables-with-business-purpose** table: every identifier this sub-module holds or threads between its functions (params, local state, struct fields, DB columns, config knobs, counters, time anchors) with type, scope chip (`sub-vars__scope--call|tx|persist|instance|loop`), and **business purpose** in business language (why it exists / which branch it decides / what breaks without it).
36
- 3. `sub-dataflow` internal value flow between this sub-module's own functions (optional small SVG; **never** draw external sub-modules here).
37
- 4. `sub-errors` errors this sub-module raises/returns (condition + meaning; HTTP mapping may be one line).
26
+ - function I/O table (`function add --feature X --submodule Y --name fn --in ... --out ... --side ... --purpose ...`),
27
+ - variables-with-business-purpose table (`variable add --name v --type T --scope call|tx|persist|instance|loop --purpose ...`),
28
+ - internal dataflow steps (`dataflow add --step "..."`),
29
+ - local errors (`error add --name ErrX --when ... --means ...`).
38
30
 
39
- **Forbidden** on sub-module pages: "I call X / Y calls me" narrative, cross-boundary `flow-edge-manifest`, cross-boundary `flow-return-row`, or any structure describing inter-submodule choreography. If unavoidable, end with one sentence + link "see macro atlas for cross-boundary interactions".
31
+ Cross-boundary narratives ("I call X", "Y calls me") **MUST** be expressed as macro edges (`edge add --from X/sub --to Y/sub --kind call|return|data-row|failure --label ...`), never as sub-module page prose. The CLI enforces this by design there is no verb to add cross-boundary text to a sub-module page.
40
32
 
41
33
  ### Rule 3 — Read order: never let the codebase wipe the context window
42
34
 
43
- Real production codebases dwarf the main agent's context. Reading the whole repo before writing pushes early details out by the end, making macro edges and sub-module pages contradict each other. **MUST** follow one of these two strategies:
35
+ Real production codebases dwarf the main agent's context. Reading the whole repo before declaring pushes early details out by the end, making macro edges and sub-module declarations contradict each other. **MUST** follow one of:
44
36
 
45
- - **With subagents (preferred):** main agent first builds the **feature-module list** (slug + entry + boundary resources only), then dispatches **one read-only subagent per feature**. Each subagent deep-reads and returns ONLY a structured summary (sub-module list; per sub-module: function I/O, variables-with-business-purpose, internal flow steps, errors raised; outbound edges to other features). Main agent collects every summary and **only then** emits the macro SVG and all pages in one pass.
46
- - **Without subagents:** process features **one at a time** — read feature A end-to-end, **immediately** emit `features/A/` (overview + every sub-module page) and incrementally add A's cluster, nodes, and known edges to the macro. Mark edges pointing at unread features with `data-edge-target-pending="<future-slug>"`. **Drop A's function-level details from working memory**, then read feature B. After every feature is written, resolve all pending placeholders in the macro. **Never** read everything before writing.
37
+ - **With subagents (preferred):** first enumerate the **feature-module list** (slug + entry + boundary resources only). Then dispatch **one read-only subagent per feature**. Each subagent deep-reads and returns ONLY a structured summary (sub-module list with kind/role; per sub-module: function I/O, variables-with-business-purpose, internal dataflow steps, errors; outbound edges to other features). The main agent collects every summary first, then drives the CLI in one pass to declare every component.
38
+ - **Without subagents:** process features **one at a time** — read feature A end-to-end, then declare it via the CLI (`feature add`, `submodule add`, `function add`, `variable add`, `dataflow add`, `error add`, intra-feature `edge add`). For cross-feature edges that point at not-yet-declared features, declare the placeholder feature with `feature add --slug <future> --title <future>` first, then add the edge; subsequent passes set the real title/story. Drop A's function-level details from working memory before reading feature B.
47
39
 
48
- Both paths share one invariant: at any moment the main agent only holds *current-feature details + cross-feature boundary notes*.
40
+ Both paths share one invariant: at any moment the main agent only holds *current-feature details + cross-feature boundary notes*. Run `apltk architecture validate` after the last feature to catch dangling edges, unknown endpoints, or missing required fields.
49
41
 
50
- ### Rule 4 — Per-page section contracts
42
+ ### Rule 4 — Evidence over invention
51
43
 
52
- Each page type has a fixed required set of sections. Violating the contract is a Rule 1 or Rule 2 violation by definition.
53
-
54
- **A) Macro `resources/project-architecture/index.html`** (the only page that draws interactions):
55
-
56
- 1. `<header class="atlas-header">` — title + generation timestamp.
57
- 2. `<section class="atlas-summary">` — prose explaining why feature modules and sub-modules sit in one diagram; cite a real producer+consumer / multi-edge case.
58
- 3. `<section class="atlas-legend">` — explains the four edge types: solid arrow = **call**, thin dashed = **return**, warm-tone heavy dashed = **data-row**, plus a note that multiple edges between the same node pair are allowed.
59
- 4. `<section class="flow-section flow-section--macro">` with **one** `<figure class="flow-chart flow-chart--macro flow-chart--svg">` honouring every MUST in Rule 1, plus a mirrored `<ol class="flow-edge-manifest flow-edge-manifest--macro">` (one row per edge, with `data-edge-id` + `data-edge-kind`).
60
- 5. `<nav class="atlas-submodule-index">` — links to every sub-module page across all features.
61
- 6. `<nav class="atlas-features">` (optional) — middle-tier nav to each feature index.
62
-
63
- **B) Feature overview `features/<feature-slug>/index.html`** (intentionally lightweight; no cross-submodule flowchart):
64
-
65
- 1. `<nav class="breadcrumb">` (`../../index.html` → macro).
66
- 2. `<header>` — user-visible capability name.
67
- 3. `<section class="feature-story">` — 1–3 paragraphs of user story; close with one sentence + link back to the macro.
68
- 4. `<section class="submodule-nav">` — table of every sub-module page (link + one-line own responsibility).
69
- 5. Optional `<section class="feature-trace">` for spec-ID traceability.
70
-
71
- **Forbidden on feature index**: `flow-chart--submodules`, `flow-chart--end-to-end`, `flow-edge-manifest`, or any other cross-submodule structure — those belong on the macro.
72
-
73
- **C) Sub-module `features/<feature-slug>/<sub-module-slug>.html`** (self-only; matches Rule 2):
74
-
75
- 1. `<nav class="breadcrumb">`: `../../index.html` · `./index.html` · current sub-module.
76
- 2. `<header>` — sub-module name + **own** responsibility (one paragraph; no "who I talk to").
77
- 3. `<section class="sub-io">` — columns `function | signature (in / out) | side effects | purpose`; side-effect chips `pure` / `io` / `write` / `tx` / `lock` / `network` / …
78
- 4. `<section class="sub-vars">` — columns `variable | type | lifecycle / scope | business purpose`; cover **every** business-branch-affecting identifier; scope chip values listed in Rule 2; business purpose in business language (no bland "stores some value").
79
- 5. `<section class="sub-dataflow">` — numbered steps; optional small `<svg class="sub-dataflow__svg">` (nodes = internal variables/functions only; height ≤ 240, width ≤ 720).
80
- 6. `<section class="sub-errors">` — errors raised/returned (condition + meaning).
81
- 7. footer: one-sentence links back to `../../index.html` and `./index.html`.
82
-
83
- If a sub-module truly holds no business-branch-affecting variables (rare; usually pure pass-through), `sub-vars` MUST still appear as a single-row table that says so with a one-line justification.
84
-
85
- ### Rule 5 — Naming, IDs, and shared assets
86
-
87
- - **`feature-slug`**: kebab-case, matching the **user-language capability** (e.g. `invite-code-registration`).
88
- - **`sub-module-slug`**: kebab-case, identifying the implementation boundary (e.g. `web-register-ui`, `public-api`, `registration-service`, `postgresql`).
89
- - **`data-feature-id` / `data-submodule-id`**: optional node attributes; **MUST** match directory/file names.
90
- - **`data-edge-id`**: stable id per macro edge (mirrored in the manifest row).
91
- - **`data-edge-kind`**: one of `call` | `return` | `data-row` | `failure`.
92
- - **Shared CSS**: on first run, copy `references/architecture.css` to `resources/project-architecture/assets/architecture.css`. Root `index.html` → `href="assets/architecture.css"`; everything under `features/<feature-slug>/` → `href="../../assets/architecture.css"`.
93
-
94
- ### Rule 6 — Accessibility and styling minima
95
-
96
- - The macro SVG `<figure>` **MUST** carry an `aria-label` plus the mirrored `<ol class="flow-edge-manifest">` so screen-reader users get the full edge list without chasing pixels.
97
- - Cluster and node links **MUST** wrap their `<g>` in `<a>` and provide readable `<text>` (browsers expose `<text>` as the accessible name).
98
- - Sub-module `sub-io` and `sub-vars` tables **MUST** use semantic `<thead>` / `<tbody>`.
99
- - Colour MUST NOT be the sole semantic carrier: edge differences encode via line style (solid / dashed / heavy-dashed) AND colour.
100
- - Every diagram is static SVG — respect `prefers-reduced-motion` by not adding animation.
101
- - Quality bar: high contrast, print-friendly, restrained palette; avoid the "AI-default purple gradient" and Inter look-alike. Pair a recognisable display face (e.g. `Fraunces`) with `Plus Jakarta Sans`. Edges use `currentColor` so light/dark adapt; pages stay readable across widths via SVG `viewBox`.
102
-
103
- ### Rule 7 — Other binding rules
104
-
105
- - **MUST** read the entire production codebase along language boundaries; record skipped roots and the reason in `atlas-summary`.
106
- - **MUST** anchor every user-visible capability to a concrete entry; **never** invent modules or integrations.
107
- - **MUST** migrate legacy single-file `features/<slug>.html` or feature-level cross-submodule flowcharts to the new layering on first touch.
108
-
109
- > Reference cheat sheets (DOM examples, the macro SVG class-hook table, vocabulary glossary) live in `references/TEMPLATE_SPEC.md`. That file is reference material only — every binding rule already lives here.
44
+ - **MUST** anchor every declared feature, sub-module, function, variable, dataflow step, and edge to a concrete path / symbol / SQL / config; record scanned roots and any deliberate omissions in `meta.summary` via `apltk architecture meta set --summary "..."`.
45
+ - **MUST NOT** invent modules, integrations, or sub-modules just because the diagram "looks balanced". Empty rows are valid; lies are not.
110
46
 
111
47
  ## Standards (summary)
112
48
 
113
- - **Evidence**: every node and edge traces to a path/symbol/SQL/config.
114
- - **Execution**: feature-module list (shallow) → branch by environment (subagent fan-out or sequential read-write) → pending-edge resolution + cross-link check.
115
- - **Quality**: macro SVG carries at least one multi-edge case, one loop, and one cross-feature edge (when applicable); sub-module pages stay self-only; print-friendly contrast; zero `pending` edges remaining.
116
- - **Output**: `resources/project-architecture/` tree obeying Rules 1–7anything else counts as a violation.
49
+ - **Evidence**: every CLI declaration traces to a path/symbol/SQL/config; uncertain areas surface as `TBD` strings or are omitted with a recorded reason.
50
+ - **Execution**: feature-module list (shallow) → branch by environment (subagent fan-out or sequential read-declare) → `apltk architecture validate` handover report.
51
+ - **Quality**: macro SVG carries every cross-feature data-row edge that exists in the system; sub-module declarations are self-only; pan/zoom + Fit work in the rendered HTML; `apltk architecture validate` returns OK.
52
+ - **Output**: `resources/project-architecture/atlas/` (YAML state) + `resources/project-architecture/**/*.html` (renderer output) both managed by the CLI.
53
+
54
+ ## How to use `apltk architecture`
55
+
56
+ Each verb is a single mutation; the CLI auto-renders after every change. Pass `--no-render` when batching mutations. Global flags: `--project <root>`, `--spec <spec_dir>` (writes to overlay; see `spec-to-project-html`), `--no-render`, `--no-open`.
57
+
58
+ | Verb | Use it to… |
59
+ | --- | --- |
60
+ | `apltk architecture meta set --title ... --summary ...` | Set the macro title + atlas summary (read order, scanned roots, deliberate omissions). |
61
+ | `apltk architecture actor add --id end-user --label "End user"` | Add a top-level actor (optional; appears in macro context). |
62
+ | `apltk architecture feature add --slug <kebab> --title "..." --story "..." [--depends-on a,b]` | Declare a feature module (user-visible capability). |
63
+ | `apltk architecture submodule add --feature <slug> --slug <kebab> --kind ui\|api\|service\|db\|pure-fn\|queue\|external --role "..."` | Declare a sub-module under a feature. |
64
+ | `apltk architecture function add --feature X --submodule Y --name fn --in "T1, T2" --out "R \| ErrX" --side pure\|io\|write\|tx\|lock\|network --purpose "..."` | Add a function I/O row. |
65
+ | `apltk architecture variable add --feature X --submodule Y --name v --type T --scope call\|tx\|persist\|instance\|loop --purpose "..."` | Add a variable-with-business-purpose row. |
66
+ | `apltk architecture dataflow add --feature X --submodule Y --step "..." [--at N]` | Append (or insert at index `N`) an internal dataflow step. |
67
+ | `apltk architecture dataflow reorder --feature X --submodule Y --from i --to j` | Move a step within the same sub-module. |
68
+ | `apltk architecture error add --feature X --submodule Y --name ErrCode --when "..." --means "..."` | Add a local error row. |
69
+ | `apltk architecture edge add --from <feature>[/sub] --to <feature>[/sub] --kind call\|return\|data-row\|failure --label "..." [--id <stable>]` | Add an edge. Intra-feature edges (both endpoints in the same feature with sub-modules) land in the feature YAML; cross-feature edges land in `atlas.index.yaml`. |
70
+ | `apltk architecture feature\|submodule\|function\|variable\|error\|edge remove ...` | Remove a previously declared component. Removing a feature drops every sub-module and edge that referenced it. |
71
+ | `apltk architecture feature\|submodule set ...` | Update fields (e.g. retitle a feature, change a sub-module's role) without re-adding. |
72
+ | `apltk architecture render` | Force-regenerate HTML from current YAML state (useful after editing YAML directly, though hand-editing is discouraged). |
73
+ | `apltk architecture validate` | Schema + referential integrity check; fails on dangling edges, unknown enums, duplicate slugs. |
74
+ | `apltk architecture undo` | Revert the most recent mutation (single-level snapshot). |
75
+ | `apltk architecture open` | Open the rendered macro `index.html` in a browser. |
76
+ | `apltk architecture diff` | Render the paginated before/after viewer for every active spec under `docs/plans/**/architecture_diff/`. |
117
77
 
118
78
  ## Workflow
119
79
 
120
80
  ### 1) Whole-repo inventory — list feature modules, not function bodies
121
81
 
122
- Scan the shipped source for **user-visible capabilities** (each one = one feature module): entry routes, CLI commands, UI pages, cron jobs, runners, event handlers, CDC streams. For each entry record only:
123
-
124
- - kebab-case `slug` + one-line user story.
125
- - **Boundary points**: entry symbol/file, outbound resources (DB tables, queue topics, external services, shared cache keys).
126
- - Candidate cross-feature edges (mark as candidates; do not verify yet).
82
+ Scan the shipped source for **user-visible capabilities** (each one = one feature module): entry routes, CLI commands, UI pages, cron jobs, runners, event handlers, CDC streams. Record only kebab-case slug + one-line user story + boundary points (entry symbol, outbound DB tables / queue topics / external services).
127
83
 
128
84
  - **Pause →** Is the list actually complete? Note skipped roots with reason; no silent skips.
129
85
  - **Pause →** Did I dive into function bodies here? Roll back — keep only structural notes.
@@ -132,50 +88,50 @@ Scan the shipped source for **user-visible capabilities** (each one = one featur
132
88
 
133
89
  #### 2A) With subagents (preferred)
134
90
 
135
- Dispatch one **read-only** subagent per feature. Require this summary template (no source-code excerpts):
91
+ Dispatch one read-only subagent per feature. Require this summary template (no source-code excerpts):
136
92
 
137
93
  > **Feature `<slug>` summary**
138
- > - Sub-module list: one row per `<sub-module-slug>` (kind: ui / api / service / db / pure-fn / queue / external).
139
- > - Per sub-module: function signatures (in / out / errors), side-effect chip, variables-with-business-purpose, internal flow steps, errors raised.
140
- > - **Outbound boundaries**: which other features' sub-modules this one calls (call edges); which DB tables / topics / cache keys it touches that **another feature also reads or writes** (data-row / data-topic edges; mark direction).
141
-
142
- Main agent collects every summary, does **not** re-read source, then emits in one pass:
143
-
144
- 1. Macro `index.html` (single SVG: clusters + multi-edge + call/return loop + cross-feature `m-edge--cross` + `atlas-submodule-index` + `flow-edge-manifest--macro`).
145
- 2. `features/<slug>/index.html` per feature (lightweight: user story + `submodule-nav`).
146
- 3. Per sub-module: self-only page with `sub-io` + `sub-vars` + `sub-dataflow` (small SVG if useful) + `sub-errors`.
147
- 4. Copy `references/architecture.css` to `resources/project-architecture/assets/`.
94
+ > - Sub-module list: one row per `<sub-module-slug>` (kind: ui / api / service / db / pure-fn / queue / external; one-line role).
95
+ > - Per sub-module: function signatures (in / out / side / purpose), variables-with-business-purpose, internal dataflow steps, errors raised.
96
+ > - **Outbound boundaries**: call edges to other features' sub-modules; data-row edges through shared DB tables / topics / cache keys with another feature; mark direction.
148
97
 
149
- - **Pause →** Every cross-feature edge in the summaries has both source and target sub-module nodes actually drawn in the SVG? If not, add the node or split one out.
98
+ Main agent collects every summary, then drives the CLI in one pass:
150
99
 
151
- #### 2B) Without subagents — feature-by-feature read-write loop
100
+ ```bash
101
+ apltk architecture meta set --title "..." --summary "..." --no-render
102
+ apltk architecture feature add --slug <slug> --title "..." --story "..." --no-render
103
+ # repeat submodule / function / variable / dataflow / error / edge add for the whole atlas
104
+ apltk architecture render
105
+ apltk architecture validate
106
+ ```
152
107
 
153
- Process the list from step 1 one feature at a time (topological hint: read-from data sources and pure helpers first, user-facing entries last). Per feature, run the full inner loop:
108
+ #### 2B) Without subagents feature-by-feature read-declare loop
154
109
 
155
- 1. **Deep-read** every sub-module of this feature (functions, variables, errors, cross-feature edges).
156
- 2. **Write to disk immediately**:
157
- - `features/<slug>/index.html` (lightweight).
158
- - One page per sub-module (self-only: `sub-io` + `sub-vars` + `sub-dataflow` + `sub-errors`).
159
- - For `sub-vars`, sweep every identifier (params, local state, struct fields, DB columns, config, counters, `now`) — anything that influences a business branch or external promise **must** appear.
160
- 3. **Patch the macro `index.html` incrementally**: add this feature's cluster + sub-module nodes + intra-feature edges; mark edges pointing at unread features as `data-edge-target-pending="<future-slug>"` and note them in `figcaption`; mirror new edges in `flow-edge-manifest--macro`.
161
- 4. **Drop function-level details from working memory**; keep only lightweight notes (cluster id, pending-edge list); do not re-read this feature's bodies when handling the next one.
162
- 5. Return to step 1 for the next feature.
110
+ Process the list from step 1 one feature at a time (topological hint: read-from data sources and pure helpers first, user-facing entries last). Per feature:
163
111
 
164
- After the last feature, do a final pass:
112
+ 1. **Deep-read** every sub-module of this feature.
113
+ 2. **Declare immediately** via the CLI (`feature add`, then `submodule add` × N, then `function`/`variable`/`dataflow`/`error` rows, then intra-feature `edge add`).
114
+ 3. **Cross-feature edges**: if the target feature has not been declared yet, declare a placeholder with `feature add --slug <future> --title <future>`, add the edge, then refine the placeholder on a later pass.
115
+ 4. **Drop function-level details from working memory** before moving to the next feature.
165
116
 
166
- - Resolve every pending placeholder in the macro (no `pending` residue allowed).
167
- - Verify `atlas-submodule-index` lists every sub-module page.
168
- - Confirm `architecture.css` is copied into `assets/`.
117
+ After the last feature:
169
118
 
170
- - **Pause →** Pending edges remaining after the last feature? Either step 1's list missed a feature or step 3 dropped an edge — re-read and patch. Silent deletion is forbidden.
119
+ - Refine any placeholder features that were used to carry cross-feature edges.
120
+ - Run `apltk architecture validate` — must return OK.
171
121
 
172
122
  ### 3) Handover report
173
123
 
174
- Report: feature count, macro edge counts (call/return/cross), sub-module page count, uncovered paths + reasons, the read strategy actually used (2A or 2B), and stable IDs to hand off to `spec-to-project-html`.
124
+ Report: feature count, sub-module count, macro edge counts (call / return / data-row / failure), uncovered paths + reasons, the read strategy actually used (2A or 2B), and the location of the rendered atlas (`resources/project-architecture/index.html`).
175
125
 
176
126
  ## Sample hints
177
127
 
178
- - Multiple SQL paths on `service ↔ db` → draw **separate edges**; do not merge.
179
- - Retry loops between `service ↔ generator(pure)` → call/return pair in the macro plus a `retry` side note; sub-module pages still describe only their own function I/O.
180
- - Cross-feature DB hand-off (A writes, B reads) → macro uses `m-edge--cross`; sub-module pages each describe only their own INSERT / SELECT functions.
181
- - Third-party systems → render as `m-sub--ext` (when extended) or cluster-external nodes; the trust boundary must be visible.
128
+ - Multiple SQL paths on `service ↔ db` → call `edge add` once per SQL path so the macro shows them as separate edges.
129
+ - Retry loops between `service ↔ generator(pure)` → call/return pair in the macro plus a dataflow step in the service describing the retry budget.
130
+ - Cross-feature DB hand-off (A writes, B reads) → declare both sides' `INSERT_*` / `SELECT_*` functions on the `db` sub-module, then `edge add --kind data-row` from producer feature/submodule to consumer feature/submodule.
131
+ - Third-party systems → declare as `--kind external` sub-modules; the trust boundary becomes visible because the renderer styles them differently.
132
+
133
+ ## References
134
+
135
+ - `lib/atlas/schema.js` — single source of truth for component fields, enums, and validation. `references/TEMPLATE_SPEC.md` mirrors that schema as a quick-reference cheat sheet.
136
+ - `lib/atlas/cli.js` — full verb dispatch (run `apltk architecture --help` for the live usage).
137
+ - `init-project-html/sample-demo/` — end-to-end YAML + rendered HTML for two features.
@@ -1,13 +1,22 @@
1
1
  interface:
2
2
  display_name: "init-project-html"
3
- short_description: "Bootstrap an HTML architecture atlas with a single macro feature × sub-module SVG"
3
+ short_description: "Declare the project HTML architecture atlas through `apltk architecture` CLI verbs"
4
4
  default_prompt: >-
5
- Use $init-project-html. Its `SKILL.md` is the authoritative rulebook (Rules 1–7); `references/TEMPLATE_SPEC.md` is a reference cheat sheet (vocabulary, class hooks, DOM snippets).
5
+ Use $init-project-html. Its `SKILL.md` is the authoritative rulebook; `references/TEMPLATE_SPEC.md` is a component-schema cheat sheet.
6
+ NEVER hand-author files under `resources/project-architecture/**/*.html`. The atlas state lives in YAML under `resources/project-architecture/atlas/` and every mutation goes through `apltk architecture <verb> ...`; the CLI owns layout, no-overlap, DOM, CSS, ARIA, and pan/zoom.
6
7
  Read strategy to avoid context loss on large codebases. STEP 1: enumerate the feature-module list (slug + entry + boundary resources) WITHOUT diving into function bodies.
7
8
  STEP 2: branch by environment.
8
- (2A, preferred) if subagents are available, dispatch ONE read-only subagent per feature; each returns ONLY a structured summary (sub-modules; per sub-module: function I/O, variables-with-business-purpose, internal data flow, errors raised; outbound edges to other features). The main agent collects every summary first, then emits the whole atlas in one pass.
9
- (2B, no subagents) process features ONE AT A TIME — deep-read feature A, IMMEDIATELY emit `features/A/index.html` + every sub-module page, incrementally patch the macro `index.html` to add A's cluster and known edges (mark edges pointing at unread features with `data-edge-target-pending="<future-slug>"`), drop A's function-level details from working memory, then move to feature B. After every feature is written, resolve every pending placeholder in the macro. NEVER read the whole codebase before writing.
10
- Macro `index.html` MUST be a SINGLE SVG covering BOTH layers in one diagram: feature-modules as `m-cluster` frames AND every sub-module as a clickable node inside its cluster, with many-to-many edges (same node pair may carry multiple edges, e.g. SELECT/INSERT/UPDATE), producer ↔ consumer call/return loops, and cross-feature `m-edge--cross` data-row edges where applicable. The macro MUST also include `<nav class="atlas-submodule-index">` linking every sub-module page.
11
- Each `features/<slug>/<sub-module>.html` describes ONLY itself: `sub-io` function I/O table + `sub-vars` variables-with-business-purpose table (every parameter / local state / struct field / DB column / config / counter / time anchor that influences a business branch, with type, scope chip `sub-vars__scope--call|tx|persist|instance|loop`, and a business-language purpose explaining why it exists and which branch it decides) + `sub-dataflow` internal flow + `sub-errors`. FORBIDDEN on sub-module pages: cross-boundary `flow-edge-manifest`, `flow-return-row`, or any "who I call / who calls me" narrative.
12
- Feature `index.html` stays lightweight: user story + `submodule-nav` table only — no cross-submodule flowchart.
13
- Copy `references/architecture.css` into `resources/project-architecture/assets/`. Mark TBD when evidence is missing. Keep IDs stable for $spec-to-project-html. Report which read strategy (2A or 2B) was actually used.
9
+ (2A, preferred) if subagents are available, dispatch ONE read-only subagent per feature; each returns ONLY a structured summary (sub-modules with kind/role; per sub-module: function I/O, variables-with-business-purpose, internal dataflow steps, errors raised; outbound edges to other features). The main agent collects every summary first, then runs the CLI in one batched pass.
10
+ (2B, no subagents) process features ONE AT A TIME — deep-read feature A, IMMEDIATELY drive the CLI (`feature add`, `submodule add` x N, `function add` / `variable add` / `dataflow add` / `error add` rows, intra-feature `edge add`), drop A's function-level details from working memory, then move to feature B. For cross-feature edges pointing at a not-yet-declared feature, declare a placeholder with `feature add --slug <future> --title <future>` first and refine its title/story on a later pass.
11
+ CLI verbs to declare components (always kebab-case slugs; pass `--no-render` to batch then call `apltk architecture render` once at the end):
12
+ `apltk architecture meta set --title "..." --summary "..."`,
13
+ `apltk architecture actor add --id <kebab> --label "..."`,
14
+ `apltk architecture feature add --slug <kebab> --title "..." --story "..." [--depends-on a,b]`,
15
+ `apltk architecture submodule add --feature X --slug Y --kind ui|api|service|db|pure-fn|queue|external --role "..."`,
16
+ `apltk architecture function add --feature X --submodule Y --name fn --in "T1, T2" --out "R | ErrX" --side pure|io|write|tx|lock|network --purpose "..."`,
17
+ `apltk architecture variable add --feature X --submodule Y --name v --type T --scope call|tx|persist|instance|loop --purpose "..."`,
18
+ `apltk architecture dataflow add --feature X --submodule Y --step "..." [--at N]`,
19
+ `apltk architecture error add --feature X --submodule Y --name ErrCode --when "..." --means "..."`,
20
+ `apltk architecture edge add --from <feature>[/sub] --to <feature>[/sub] --kind call|return|data-row|failure --label "..." [--id <stable>]`.
21
+ Intra-feature edges (both endpoints in the same feature with sub-modules) land in the feature YAML; cross-feature edges land in `atlas.index.yaml`. After the last mutation run `apltk architecture validate` — must return OK; resolve every reported error before reporting completion.
22
+ Each sub-module page describes ONLY itself (`sub-io` function I/O + `sub-vars` variables-with-business-purpose + `sub-dataflow` internal flow + `sub-errors` local errors) — express any cross-boundary interaction as a macro edge, never as sub-module page prose. Anchor every declaration to a concrete path / symbol / SQL / config; mark TBD when evidence is missing; record scanned roots and deliberate omissions in `meta.summary`. Report which read strategy (2A or 2B) was actually used and the location of the rendered atlas (`resources/project-architecture/index.html`).
@@ -0,0 +1,140 @@
1
+ /* architecture.css — styling for the declarative atlas. The CLI copies
2
+ * this file into <outDir>/assets/. Style hooks (class names) are owned
3
+ * by render.js so agents never need to touch HTML by hand. */
4
+
5
+ :root {
6
+ color-scheme: light dark;
7
+ --bg: #0f172a;
8
+ --panel: #111827;
9
+ --panel-soft: #1f2937;
10
+ --text: #e5e7eb;
11
+ --muted: #9ca3af;
12
+ --border: #334155;
13
+ --accent: #38bdf8;
14
+ --kind-ui: #38bdf8;
15
+ --kind-api: #818cf8;
16
+ --kind-service: #34d399;
17
+ --kind-db: #fbbf24;
18
+ --kind-pure-fn: #cbd5e1;
19
+ --kind-queue: #a78bfa;
20
+ --kind-external: #fb7185;
21
+ --edge-call: #60a5fa;
22
+ --edge-return: #94a3b8;
23
+ --edge-data-row: #fbbf24;
24
+ --edge-failure: #fb7185;
25
+ }
26
+
27
+ * { box-sizing: border-box; }
28
+ html, body { margin: 0; padding: 0; background: var(--bg); color: var(--text); font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
29
+
30
+ a { color: var(--accent); text-decoration: none; }
31
+ a:hover { text-decoration: underline; }
32
+
33
+ h1, h2 { margin: 0 0 12px; font-weight: 600; letter-spacing: -0.01em; }
34
+ h1 { font-size: 28px; }
35
+ h2 { font-size: 18px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; font-size: 13px; }
36
+
37
+ p { line-height: 1.55; color: var(--text); }
38
+
39
+ /* ---- atlas (macro) ---- */
40
+ .atlas-header { padding: 28px 40px 16px; border-bottom: 1px solid var(--border); background: var(--panel); }
41
+ .atlas-summary { color: var(--muted); max-width: 80ch; margin: 8px 0 0; }
42
+
43
+ .atlas-main { display: grid; grid-template-columns: minmax(0, 3fr) minmax(280px, 1fr); gap: 24px; padding: 24px 40px 48px; }
44
+
45
+ .atlas-canvas { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px; position: relative; }
46
+ .atlas-canvas__toolbar { position: absolute; top: 16px; right: 16px; display: flex; gap: 4px; z-index: 2; }
47
+ .atlas-canvas__toolbar button { background: var(--panel-soft); color: var(--text); border: 1px solid var(--border); padding: 4px 12px; border-radius: 6px; cursor: pointer; font-size: 13px; }
48
+ .atlas-canvas__toolbar button:hover { border-color: var(--accent); color: var(--accent); }
49
+
50
+ .atlas-canvas__viewport { width: 100%; max-height: 72vh; overflow: hidden; border-radius: 8px; background: #0b1220; }
51
+ .atlas-canvas__viewport.is-grabbing { cursor: grabbing; }
52
+ .atlas-canvas__viewport:not(.is-grabbing) { cursor: grab; }
53
+
54
+ .atlas-svg { width: 100%; height: auto; max-height: 72vh; display: block; user-select: none; touch-action: none; }
55
+
56
+ .atlas-legend { list-style: none; padding: 12px 4px 0; margin: 0; display: flex; gap: 18px; flex-wrap: wrap; font-size: 12px; color: var(--muted); }
57
+ .atlas-legend li { display: inline-flex; align-items: center; gap: 6px; }
58
+ .legend-swatch { display: inline-block; width: 18px; height: 4px; border-radius: 2px; }
59
+ .legend-swatch--call { background: var(--edge-call); }
60
+ .legend-swatch--return { background: var(--edge-return); }
61
+ .legend-swatch--data-row { background: var(--edge-data-row); }
62
+ .legend-swatch--failure { background: var(--edge-failure); }
63
+
64
+ .atlas-index { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px; max-height: 80vh; overflow: auto; }
65
+
66
+ .atlas-submodule-index { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 10px; }
67
+ .atlas-submodule-index__item a { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 4px 8px; padding: 8px 10px; background: var(--panel-soft); border: 1px solid var(--border); border-radius: 8px; color: inherit; }
68
+ .atlas-submodule-index__item a:hover { border-color: var(--accent); }
69
+ .atlas-submodule-index__feature { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; grid-column: 1 / 3; }
70
+ .atlas-submodule-index__sub { font-weight: 600; }
71
+ .atlas-submodule-index__kind { font-size: 11px; padding: 1px 8px; border-radius: 999px; background: var(--panel); border: 1px solid var(--border); color: var(--muted); }
72
+ .atlas-submodule-index__role { margin: 4px 10px 0; font-size: 12px; color: var(--muted); }
73
+
74
+ /* ---- SVG macro ---- */
75
+ .m-cluster__bg { fill: rgba(15, 23, 42, 0.55); stroke: var(--border); stroke-width: 1; }
76
+ .m-cluster__title { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 14px; fill: var(--text); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; }
77
+ .m-node rect { fill: var(--panel-soft); stroke: var(--border); stroke-width: 1; transition: stroke 120ms ease; }
78
+ .m-node:hover rect { stroke: var(--accent); }
79
+ .m-node__title { font-size: 13px; font-weight: 600; fill: var(--text); }
80
+ .m-node__kind { font-size: 11px; fill: var(--muted); }
81
+ .m-node__role { font-size: 11px; fill: var(--muted); }
82
+
83
+ .m-node--ui rect { stroke: var(--kind-ui); }
84
+ .m-node--api rect { stroke: var(--kind-api); }
85
+ .m-node--service rect { stroke: var(--kind-service); }
86
+ .m-node--db rect { stroke: var(--kind-db); }
87
+ .m-node--pure-fn rect { stroke: var(--kind-pure-fn); }
88
+ .m-node--queue rect { stroke: var(--kind-queue); }
89
+ .m-node--external rect { stroke: var(--kind-external); }
90
+
91
+ .m-edge path { stroke-width: 1.6; }
92
+ .m-edge--call path { stroke: var(--edge-call); }
93
+ .m-edge--return path { stroke: var(--edge-return); stroke-dasharray: 6 4; }
94
+ .m-edge--data-row path { stroke: var(--edge-data-row); }
95
+ .m-edge--failure path { stroke: var(--edge-failure); }
96
+
97
+ .m-arrow path { fill: currentColor; }
98
+ .m-arrow--call { color: var(--edge-call); }
99
+ .m-arrow--return { color: var(--edge-return); }
100
+ .m-arrow--data-row { color: var(--edge-data-row); }
101
+ .m-arrow--failure { color: var(--edge-failure); }
102
+
103
+ .m-edge__label { fill: var(--muted); font-size: 11px; }
104
+
105
+ /* ---- feature page ---- */
106
+ .feature-header, .submodule-header { padding: 24px 40px 12px; border-bottom: 1px solid var(--border); background: var(--panel); }
107
+ .feature-breadcrumb, .submodule-breadcrumb { font-size: 13px; color: var(--muted); margin-bottom: 8px; }
108
+ .feature-depends { font-size: 13px; color: var(--muted); margin: 8px 0 0; }
109
+ .feature-main, .submodule-main { padding: 24px 40px 48px; display: flex; flex-direction: column; gap: 32px; }
110
+ .feature-story p { max-width: 80ch; }
111
+
112
+ .submodule-nav { list-style: none; padding: 0; margin: 0; display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
113
+ .submodule-card { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; }
114
+ .submodule-card__link { display: flex; align-items: center; justify-content: space-between; color: inherit; font-weight: 600; }
115
+ .submodule-card__kind { font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border); color: var(--muted); }
116
+ .submodule-card__role { margin: 8px 0 0; font-size: 13px; color: var(--muted); }
117
+
118
+ .feature-edges__list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 8px; }
119
+ .feature-edges__item { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 2fr); gap: 8px; padding: 8px 12px; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; font-size: 13px; }
120
+ .feature-edges__kind { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; align-self: center; }
121
+ .feature-edges__item--call { border-left: 4px solid var(--edge-call); }
122
+ .feature-edges__item--return { border-left: 4px solid var(--edge-return); }
123
+ .feature-edges__item--data-row { border-left: 4px solid var(--edge-data-row); }
124
+ .feature-edges__item--failure { border-left: 4px solid var(--edge-failure); }
125
+
126
+ /* ---- submodule page ---- */
127
+ .submodule-kind { display: inline-block; font-size: 12px; padding: 2px 10px; border-radius: 999px; border: 1px solid var(--border); color: var(--muted); margin-left: 8px; vertical-align: middle; text-transform: uppercase; letter-spacing: 0.06em; }
128
+ .submodule-role { color: var(--muted); margin: 8px 0 0; max-width: 80ch; }
129
+
130
+ .sub-table { width: 100%; border-collapse: collapse; font-size: 13px; }
131
+ .sub-table th, .sub-table td { padding: 8px 12px; border-bottom: 1px solid var(--border); text-align: left; vertical-align: top; }
132
+ .sub-table th { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; background: var(--panel); }
133
+
134
+ .sub-section__empty { color: var(--muted); font-style: italic; font-size: 13px; }
135
+
136
+ .sub-dataflow__svg { width: 100%; max-width: 480px; }
137
+ .sub-dataflow__step rect { fill: var(--panel); stroke: var(--border); }
138
+ .sub-dataflow__step text { fill: var(--text); font-size: 13px; }
139
+ .sub-dataflow__arrow { stroke: var(--muted); stroke-width: 1.4; }
140
+ .sub-dataflow__empty { color: var(--muted); font-style: italic; }
@@ -0,0 +1,93 @@
1
+ /* viewer.client.js — pan/zoom for the macro atlas SVG. No deps; wires
2
+ * onto any element marked [data-pan-zoom-viewport] containing one
3
+ * [data-atlas-svg] SVG. Mouse wheel zooms around the cursor; drag pans;
4
+ * toolbar buttons handle +/-/Fit; keyboard arrows pan. */
5
+
6
+ (function () {
7
+ 'use strict';
8
+
9
+ const viewport = document.querySelector('[data-pan-zoom-viewport]');
10
+ if (!viewport) return;
11
+ const svg = viewport.querySelector('[data-atlas-svg]');
12
+ if (!svg) return;
13
+
14
+ const initial = svg.getAttribute('viewBox');
15
+ if (!initial) return;
16
+ const [ix, iy, iw, ih] = initial.split(/\s+/).map(Number);
17
+ const state = { x: ix, y: iy, w: iw, h: ih };
18
+
19
+ function apply() {
20
+ svg.setAttribute('viewBox', `${state.x} ${state.y} ${state.w} ${state.h}`);
21
+ }
22
+
23
+ function zoom(factor, cx, cy) {
24
+ const newW = Math.max(40, Math.min(state.w * factor, iw * 8));
25
+ const newH = newW * (state.h / state.w);
26
+ if (cx == null) { cx = state.x + state.w / 2; cy = state.y + state.h / 2; }
27
+ state.x = cx - (cx - state.x) * (newW / state.w);
28
+ state.y = cy - (cy - state.y) * (newH / state.h);
29
+ state.w = newW;
30
+ state.h = newH;
31
+ apply();
32
+ }
33
+
34
+ function clientToSvg(evt) {
35
+ const rect = svg.getBoundingClientRect();
36
+ const xRatio = (evt.clientX - rect.left) / rect.width;
37
+ const yRatio = (evt.clientY - rect.top) / rect.height;
38
+ return { x: state.x + xRatio * state.w, y: state.y + yRatio * state.h };
39
+ }
40
+
41
+ viewport.addEventListener('wheel', function (evt) {
42
+ if (!evt.ctrlKey && !evt.metaKey && Math.abs(evt.deltaY) < 4 && Math.abs(evt.deltaX) < 4) return;
43
+ evt.preventDefault();
44
+ const factor = evt.deltaY > 0 ? 1.1 : 1 / 1.1;
45
+ const pt = clientToSvg(evt);
46
+ zoom(factor, pt.x, pt.y);
47
+ }, { passive: false });
48
+
49
+ let dragging = null;
50
+ viewport.addEventListener('pointerdown', function (evt) {
51
+ if (evt.button !== 0) return;
52
+ dragging = { x: evt.clientX, y: evt.clientY };
53
+ viewport.classList.add('is-grabbing');
54
+ viewport.setPointerCapture(evt.pointerId);
55
+ });
56
+ viewport.addEventListener('pointermove', function (evt) {
57
+ if (!dragging) return;
58
+ const rect = svg.getBoundingClientRect();
59
+ const dx = ((evt.clientX - dragging.x) / rect.width) * state.w;
60
+ const dy = ((evt.clientY - dragging.y) / rect.height) * state.h;
61
+ state.x -= dx;
62
+ state.y -= dy;
63
+ dragging = { x: evt.clientX, y: evt.clientY };
64
+ apply();
65
+ });
66
+ function endDrag(evt) {
67
+ if (!dragging) return;
68
+ dragging = null;
69
+ viewport.classList.remove('is-grabbing');
70
+ try { viewport.releasePointerCapture(evt.pointerId); } catch (e) { /* ignore */ }
71
+ }
72
+ viewport.addEventListener('pointerup', endDrag);
73
+ viewport.addEventListener('pointercancel', endDrag);
74
+ viewport.addEventListener('pointerleave', endDrag);
75
+
76
+ document.addEventListener('keydown', function (evt) {
77
+ if (evt.target && (evt.target.tagName === 'INPUT' || evt.target.tagName === 'TEXTAREA')) return;
78
+ const step = state.w * 0.08;
79
+ if (evt.key === 'ArrowLeft') { state.x -= step; apply(); }
80
+ else if (evt.key === 'ArrowRight') { state.x += step; apply(); }
81
+ else if (evt.key === 'ArrowUp') { state.y -= step; apply(); }
82
+ else if (evt.key === 'ArrowDown') { state.y += step; apply(); }
83
+ else if (evt.key === '+' || evt.key === '=') { zoom(1 / 1.2); }
84
+ else if (evt.key === '-' || evt.key === '_') { zoom(1.2); }
85
+ else if (evt.key === '0') { state.x = ix; state.y = iy; state.w = iw; state.h = ih; apply(); }
86
+ });
87
+
88
+ document.querySelectorAll('[data-pan-zoom="zoom-in"]').forEach((btn) => btn.addEventListener('click', () => zoom(1 / 1.2)));
89
+ document.querySelectorAll('[data-pan-zoom="zoom-out"]').forEach((btn) => btn.addEventListener('click', () => zoom(1.2)));
90
+ document.querySelectorAll('[data-pan-zoom="fit"]').forEach((btn) => btn.addEventListener('click', () => {
91
+ state.x = ix; state.y = iy; state.w = iw; state.h = ih; apply();
92
+ }));
93
+ })();