@open-agent-toolkit/cli 0.2.5 → 0.2.6

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 (103) hide show
  1. package/assets/docs/cli-utilities/configuration.md +40 -0
  2. package/assets/docs/cli-utilities/tool-packs.md +31 -0
  3. package/assets/docs/workflows/projects/artifacts.md +27 -0
  4. package/assets/docs/workflows/skills/explainer-kit.md +125 -0
  5. package/assets/docs/workflows/skills/index.md +4 -0
  6. package/assets/migration/pjm-restructure.md +12 -0
  7. package/assets/public-package-versions.json +4 -4
  8. package/assets/skills/explainer-kit/SKILL.md +95 -0
  9. package/assets/skills/explainer-kit/examples/project-explainer/content.md +31 -0
  10. package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +22 -0
  11. package/assets/skills/explainer-kit/examples/project-recap/content.md +34 -0
  12. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +22 -0
  13. package/assets/skills/explainer-kit/examples/theme-bundle.json +87 -0
  14. package/assets/skills/explainer-kit/palettes/ember.json +37 -0
  15. package/assets/skills/explainer-kit/palettes/forest.json +37 -0
  16. package/assets/skills/explainer-kit/palettes/neutral.json +37 -0
  17. package/assets/skills/explainer-kit/palettes/ocean.json +37 -0
  18. package/assets/skills/explainer-kit/palettes/violet.json +37 -0
  19. package/assets/skills/explainer-kit/profiles/clean.json +42 -0
  20. package/assets/skills/explainer-kit/profiles/editorial.json +42 -0
  21. package/assets/skills/explainer-kit/profiles/technical.json +39 -0
  22. package/assets/skills/explainer-kit/recipes/engineer-tour.json +33 -0
  23. package/assets/skills/explainer-kit/recipes/program-recap.json +34 -0
  24. package/assets/skills/explainer-kit/recipes/project-explainer.json +33 -0
  25. package/assets/skills/explainer-kit/recipes/project-recap.json +34 -0
  26. package/assets/skills/explainer-kit/references/contracts.md +101 -0
  27. package/assets/skills/explainer-kit/references/destination-contract.md +94 -0
  28. package/assets/skills/explainer-kit/references/extension-contract.md +71 -0
  29. package/assets/skills/explainer-kit/references/fact-base-contract.md +119 -0
  30. package/assets/skills/explainer-kit/schemas/build-record.schema.json +92 -0
  31. package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +53 -0
  32. package/assets/skills/explainer-kit/schemas/fact-base.schema.json +129 -0
  33. package/assets/skills/explainer-kit/schemas/manifest.schema.json +193 -0
  34. package/assets/skills/explainer-kit/schemas/publish-receipt.schema.json +97 -0
  35. package/assets/skills/explainer-kit/schemas/publish-request.schema.json +32 -0
  36. package/assets/skills/explainer-kit/schemas/run-request.schema.json +132 -0
  37. package/assets/skills/explainer-kit/schemas/theme.schema.json +217 -0
  38. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +180 -0
  39. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +680 -0
  40. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +538 -0
  41. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +494 -0
  42. package/assets/skills/explainer-kit/scripts/lib/fs-safe.mjs +218 -0
  43. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +512 -0
  44. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +289 -0
  45. package/assets/skills/explainer-kit/scripts/lib/records.mjs +231 -0
  46. package/assets/skills/explainer-kit/scripts/lib/render.mjs +485 -0
  47. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +587 -0
  48. package/assets/skills/explainer-kit/scripts/lib/safe-paths.mjs +96 -0
  49. package/assets/skills/explainer-kit/scripts/lib/theme.mjs +332 -0
  50. package/assets/skills/explainer-kit/scripts/publish.mjs +76 -0
  51. package/assets/skills/explainer-kit/scripts/record-durability.mjs +64 -0
  52. package/assets/skills/explainer-kit/scripts/render-qa.mjs +273 -0
  53. package/assets/skills/explainer-kit/scripts/run.mjs +778 -0
  54. package/assets/skills/explainer-kit/scripts/validate.mjs +59 -0
  55. package/assets/skills/explainer-kit/templates/deck-shell.html +233 -0
  56. package/assets/skills/explainer-kit/templates/diagram-shell.html +219 -0
  57. package/assets/skills/explainer-kit/templates/engineer-tour.html +273 -0
  58. package/assets/skills/explainer-kit/templates/house-style.html +218 -0
  59. package/assets/skills/oat-explainer-kit/SKILL.md +92 -0
  60. package/assets/skills/oat-explainer-kit/references/config-contract.md +67 -0
  61. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +143 -0
  62. package/assets/skills/oat-explainer-kit/references/migration.md +143 -0
  63. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +103 -0
  64. package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +127 -0
  65. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +392 -0
  66. package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +130 -0
  67. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +293 -0
  68. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +209 -0
  69. package/assets/skills/oat-explainer-kit/scripts/resolve-paths.mjs +166 -0
  70. package/assets/skills/oat-explainer-kit/scripts/run.mjs +284 -0
  71. package/assets/skills/oat-project-autonomous/SKILL.md +22 -1
  72. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +5 -1
  73. package/assets/skills/oat-project-complete/SKILL.md +166 -28
  74. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +5 -1
  75. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  76. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +17 -0
  77. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +5 -1
  78. package/assets/skills/oat-project-plan/SKILL.md +30 -1
  79. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +5 -1
  80. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +5 -1
  81. package/assets/skills/oat-project-summary/SKILL.md +20 -1
  82. package/dist/commands/config/index.d.ts.map +1 -1
  83. package/dist/commands/config/index.js +232 -1
  84. package/dist/commands/init/tools/shared/skill-manifest.d.ts +2 -2
  85. package/dist/commands/init/tools/shared/skill-manifest.d.ts.map +1 -1
  86. package/dist/commands/init/tools/shared/skill-manifest.js +2 -0
  87. package/dist/commands/project/archive/archive-utils.d.ts +12 -0
  88. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  89. package/dist/commands/project/archive/archive-utils.js +320 -8
  90. package/dist/commands/project/archive/index.d.ts.map +1 -1
  91. package/dist/commands/project/archive/index.js +1 -0
  92. package/dist/commands/project/archive/push-runner.d.ts +1 -0
  93. package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
  94. package/dist/commands/project/archive/push-runner.js +9 -2
  95. package/dist/config/oat-config.d.ts +26 -0
  96. package/dist/config/oat-config.d.ts.map +1 -1
  97. package/dist/config/oat-config.js +88 -0
  98. package/dist/config/resolve.d.ts.map +1 -1
  99. package/dist/config/resolve.js +18 -0
  100. package/dist/validation/project-state.d.ts +3 -0
  101. package/dist/validation/project-state.d.ts.map +1 -1
  102. package/dist/validation/project-state.js +91 -0
  103. package/package.json +2 -2
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile } from 'node:fs/promises';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ import { validateContract } from './lib/contracts.mjs';
7
+
8
+ export async function runValidationCli(
9
+ argv = process.argv.slice(2),
10
+ io = console,
11
+ ) {
12
+ const [kind, inputPath] = argv;
13
+ if (!kind || !inputPath) {
14
+ const result = {
15
+ valid: false,
16
+ errors: [
17
+ {
18
+ path: '$',
19
+ code: 'usage',
20
+ message: 'Usage: validate.mjs <contract-kind> <json-path>',
21
+ },
22
+ ],
23
+ };
24
+ io.log(JSON.stringify(result, null, 2));
25
+ return 1;
26
+ }
27
+
28
+ try {
29
+ const value = JSON.parse(await readFile(inputPath, 'utf8'));
30
+ const result = validateContract(kind, value);
31
+ io.log(JSON.stringify(result, null, 2));
32
+ return result.valid ? 0 : 1;
33
+ } catch (error) {
34
+ io.log(
35
+ JSON.stringify(
36
+ {
37
+ valid: false,
38
+ errors: [
39
+ {
40
+ path: '$',
41
+ code: 'input',
42
+ message: error instanceof Error ? error.message : String(error),
43
+ },
44
+ ],
45
+ },
46
+ null,
47
+ 2,
48
+ ),
49
+ );
50
+ return 1;
51
+ }
52
+ }
53
+
54
+ if (
55
+ process.argv[1] &&
56
+ pathToFileURL(process.argv[1]).href === import.meta.url
57
+ ) {
58
+ process.exitCode = await runValidationCli();
59
+ }
@@ -0,0 +1,233 @@
1
+ <!doctype html>
2
+ <!--
3
+ Neutral production shell. Worked content: ../examples/
4
+ TEMPLATE CONTRACT: THEME_CSS, TITLE, DESCRIPTION, SLIDES
5
+ SLIDES contains semantic section.slide elements with escaped content.
6
+ -->
7
+ <html lang="en">
8
+ <head>
9
+ <meta charset="utf-8" />
10
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
11
+ <title>{{TITLE}}</title>
12
+ <meta name="description" content="{{DESCRIPTION}}" />
13
+ <script>
14
+ document.documentElement.classList.add('js');
15
+ </script>
16
+ <style>
17
+ :root {
18
+ --canvas: hsl(210 20% 98%);
19
+ --panel: hsl(0 0% 100%);
20
+ --border: hsl(210 14% 80%);
21
+ --ink: hsl(215 24% 16%);
22
+ --muted: hsl(215 12% 38%);
23
+ --accent: hsl(215 28% 32%);
24
+ --sans: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
25
+ --serif: ui-serif, Georgia, serif;
26
+ --mono: ui-monospace, SFMono-Regular, Consolas, monospace;
27
+ {{THEME_CSS}}
28
+ }
29
+ * {
30
+ box-sizing: border-box;
31
+ }
32
+ html,
33
+ body {
34
+ margin: 0;
35
+ min-height: 100%;
36
+ }
37
+ body {
38
+ background: var(--canvas);
39
+ color: var(--ink);
40
+ font-family: var(--sans);
41
+ }
42
+ .deck {
43
+ display: flex;
44
+ width: 100%;
45
+ height: 100dvh;
46
+ overflow-x: auto;
47
+ overflow-y: hidden;
48
+ scroll-snap-type: x mandatory;
49
+ scroll-behavior: smooth;
50
+ overscroll-behavior-x: contain;
51
+ }
52
+ .slide {
53
+ flex: 0 0 100%;
54
+ width: 100%;
55
+ min-width: 0;
56
+ height: 100dvh;
57
+ overflow: hidden;
58
+ scroll-snap-align: start;
59
+ scroll-snap-stop: always;
60
+ display: grid;
61
+ place-items: center;
62
+ padding: clamp(2rem, 7vw, 7rem);
63
+ border-inline-end: 1px solid var(--border);
64
+ }
65
+ .slide__content {
66
+ width: min(100%, 72rem);
67
+ max-width: 100%;
68
+ max-height: 100%;
69
+ min-width: 0;
70
+ overflow-x: auto;
71
+ overflow-y: auto;
72
+ overflow-wrap: anywhere;
73
+ padding: 0.25rem;
74
+ }
75
+ .slide h1,
76
+ .slide h2 {
77
+ max-width: 24ch;
78
+ font-family: var(--serif);
79
+ font-size: clamp(2rem, 6vw, 4.5rem);
80
+ line-height: 1.08;
81
+ margin-block: 0 1rem;
82
+ text-wrap: balance;
83
+ }
84
+ .slide p,
85
+ .slide li {
86
+ max-width: 52ch;
87
+ font-size: clamp(1rem, 2.2vw, 1.45rem);
88
+ line-height: 1.5;
89
+ }
90
+ .table-scroll,
91
+ pre {
92
+ max-width: 100%;
93
+ overflow-x: auto;
94
+ }
95
+ .deck-controls {
96
+ position: fixed;
97
+ z-index: 3;
98
+ inset: auto 1rem 1rem auto;
99
+ display: flex;
100
+ align-items: center;
101
+ gap: 0.4rem;
102
+ border: 1px solid var(--border);
103
+ border-radius: 999px;
104
+ background: var(--panel);
105
+ padding: 0.35rem;
106
+ font: 0.75rem var(--mono);
107
+ }
108
+ .deck-controls button {
109
+ border: 0;
110
+ border-radius: 50%;
111
+ background: transparent;
112
+ color: var(--ink);
113
+ min-width: 2rem;
114
+ min-height: 2rem;
115
+ cursor: pointer;
116
+ }
117
+ .deck-controls button:focus-visible {
118
+ outline: 2px solid var(--accent);
119
+ outline-offset: 2px;
120
+ }
121
+ html:not(.js) .deck {
122
+ display: block;
123
+ height: auto;
124
+ overflow: visible;
125
+ scroll-snap-type: none;
126
+ }
127
+ html:not(.js) .slide {
128
+ min-height: auto;
129
+ height: auto;
130
+ overflow: visible;
131
+ border-block-end: 1px solid var(--border);
132
+ }
133
+ html:not(.js) .slide__content {
134
+ max-height: none;
135
+ overflow-x: auto;
136
+ overflow-y: visible;
137
+ }
138
+ @media (prefers-reduced-motion: reduce) {
139
+ .deck {
140
+ scroll-behavior: auto;
141
+ }
142
+ }
143
+ @media print {
144
+ .deck {
145
+ display: block;
146
+ width: auto;
147
+ height: auto;
148
+ overflow: visible;
149
+ scroll-snap-type: none;
150
+ }
151
+ .slide {
152
+ display: block;
153
+ width: auto;
154
+ height: auto;
155
+ min-height: auto;
156
+ overflow: visible;
157
+ break-after: page;
158
+ border: 0;
159
+ padding: 2rem 0;
160
+ }
161
+ .slide__content {
162
+ width: auto;
163
+ max-height: none;
164
+ overflow: visible;
165
+ }
166
+ .deck-controls {
167
+ display: none;
168
+ }
169
+ }
170
+ </style>
171
+ </head>
172
+ <body>
173
+ <noscript>
174
+ <p>This deck remains available below as a readable vertical document.</p>
175
+ </noscript>
176
+ <main class="deck" aria-label="{{TITLE}}">{{SLIDES}}</main>
177
+ <nav class="deck-controls" aria-label="Slide navigation">
178
+ <button type="button" data-direction="-1" aria-label="Previous slide">
179
+
180
+ </button>
181
+ <span id="deck-counter" aria-live="polite"></span>
182
+ <button type="button" data-direction="1" aria-label="Next slide">
183
+
184
+ </button>
185
+ </nav>
186
+ <script>
187
+ (() => {
188
+ const deck = document.querySelector('.deck');
189
+ const slides = [...document.querySelectorAll('.slide')];
190
+ const counter = document.querySelector('#deck-counter');
191
+ let current = 0;
192
+
193
+ const update = (index) => {
194
+ current = Math.max(0, Math.min(slides.length - 1, index));
195
+ counter.textContent = `${current + 1} / ${slides.length}`;
196
+ };
197
+ const go = (index) => {
198
+ update(index);
199
+ slides[current]?.scrollIntoView({
200
+ behavior: 'smooth',
201
+ block: 'nearest',
202
+ inline: 'start',
203
+ });
204
+ };
205
+ document.querySelectorAll('[data-direction]').forEach((button) => {
206
+ button.addEventListener('click', () =>
207
+ go(current + Number(button.dataset.direction)),
208
+ );
209
+ });
210
+ addEventListener('keydown', (event) => {
211
+ if (event.target.closest('input, textarea, [contenteditable]'))
212
+ return;
213
+ if (['ArrowRight', 'ArrowDown'].includes(event.key)) {
214
+ event.preventDefault();
215
+ go(current + 1);
216
+ } else if (['ArrowLeft', 'ArrowUp'].includes(event.key)) {
217
+ event.preventDefault();
218
+ go(current - 1);
219
+ }
220
+ });
221
+ const observer = new IntersectionObserver(
222
+ (entries) => {
223
+ const visible = entries.find((entry) => entry.isIntersecting);
224
+ if (visible) update(slides.indexOf(visible.target));
225
+ },
226
+ { root: deck, threshold: 0.6 },
227
+ );
228
+ slides.forEach((slide) => observer.observe(slide));
229
+ update(0);
230
+ })();
231
+ </script>
232
+ </body>
233
+ </html>
@@ -0,0 +1,219 @@
1
+ <!doctype html>
2
+ <!--
3
+ Neutral production shell. Worked content: ../examples/
4
+ TEMPLATE CONTRACT: THEME_CSS, TITLE, DESCRIPTION, DIAGRAM, LEGEND
5
+ DIAGRAM contains validated inline SVG children; external diagram runtimes are not allowed.
6
+ -->
7
+ <html lang="en">
8
+ <head>
9
+ <meta charset="utf-8" />
10
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
11
+ <title>{{TITLE}}</title>
12
+ <meta name="description" content="{{DESCRIPTION}}" />
13
+ <style>
14
+ :root {
15
+ --canvas: hsl(210 20% 98%);
16
+ --panel: hsl(0 0% 100%);
17
+ --border: hsl(210 14% 80%);
18
+ --ink: hsl(215 24% 16%);
19
+ --muted: hsl(215 12% 38%);
20
+ --accent: hsl(215 28% 32%);
21
+ --sans: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
22
+ --mono: ui-monospace, SFMono-Regular, Consolas, monospace;
23
+ {{THEME_CSS}}
24
+ }
25
+ * {
26
+ box-sizing: border-box;
27
+ }
28
+ body {
29
+ min-height: 100vh;
30
+ margin: 0;
31
+ padding: clamp(1rem, 4vw, 3rem);
32
+ background: var(--canvas);
33
+ color: var(--ink);
34
+ font-family: var(--sans);
35
+ }
36
+ main {
37
+ max-width: 80rem;
38
+ margin: auto;
39
+ }
40
+ header {
41
+ max-width: 55rem;
42
+ margin-block-end: 1.5rem;
43
+ }
44
+ h1 {
45
+ margin-block: 0 0.4rem;
46
+ font-size: clamp(2rem, 5vw, 3.5rem);
47
+ line-height: 1.1;
48
+ text-wrap: balance;
49
+ }
50
+ header p,
51
+ .diagram-hint {
52
+ color: var(--muted);
53
+ }
54
+ .diagram-shell {
55
+ position: relative;
56
+ min-width: 0;
57
+ }
58
+ .diagram-hint {
59
+ font: 0.75rem/1.4 var(--mono);
60
+ }
61
+ .diagram-viewport {
62
+ position: relative;
63
+ width: 100%;
64
+ min-height: min(65vh, 40rem);
65
+ overflow: auto;
66
+ border: 1px solid var(--border);
67
+ border-radius: 0.75rem;
68
+ background: var(--panel);
69
+ cursor: grab;
70
+ }
71
+ .diagram-viewport.is-panning {
72
+ cursor: grabbing;
73
+ user-select: none;
74
+ }
75
+ .diagram-canvas {
76
+ display: block;
77
+ width: 100%;
78
+ min-width: 42rem;
79
+ height: auto;
80
+ transform-origin: top left;
81
+ }
82
+ .diagram-canvas text {
83
+ fill: var(--ink);
84
+ font-family: var(--sans);
85
+ }
86
+ .diagram-canvas .node {
87
+ fill: var(--panel);
88
+ stroke: var(--accent);
89
+ }
90
+ .diagram-canvas .edge {
91
+ fill: none;
92
+ stroke: var(--muted);
93
+ }
94
+ .zoom-controls {
95
+ position: absolute;
96
+ z-index: 2;
97
+ inset: 0.6rem 0.6rem auto auto;
98
+ display: flex;
99
+ gap: 0.25rem;
100
+ padding: 0.25rem;
101
+ border: 1px solid var(--border);
102
+ border-radius: 0.5rem;
103
+ background: var(--panel);
104
+ }
105
+ .zoom-controls button {
106
+ min-width: 2rem;
107
+ min-height: 2rem;
108
+ border: 0;
109
+ background: transparent;
110
+ color: var(--ink);
111
+ cursor: pointer;
112
+ }
113
+ .legend {
114
+ display: flex;
115
+ flex-wrap: wrap;
116
+ gap: 1rem;
117
+ margin-block-start: 1rem;
118
+ color: var(--muted);
119
+ font: 0.8rem/1.5 var(--mono);
120
+ }
121
+ @media (max-width: 45rem) {
122
+ .diagram-canvas {
123
+ min-width: 34rem;
124
+ }
125
+ }
126
+ </style>
127
+ </head>
128
+ <body>
129
+ <main>
130
+ <header>
131
+ <h1 id="diagram-title">{{TITLE}}</h1>
132
+ <p id="diagram-description">{{DESCRIPTION}}</p>
133
+ </header>
134
+ <section class="diagram-shell" aria-labelledby="diagram-title">
135
+ <p class="diagram-hint">
136
+ Use the controls or Ctrl/Command + wheel to zoom; drag to pan.
137
+ </p>
138
+ <div class="zoom-controls" aria-label="Diagram zoom">
139
+ <button type="button" data-zoom="in" aria-label="Zoom in">+</button>
140
+ <button type="button" data-zoom="out" aria-label="Zoom out">−</button>
141
+ <button type="button" data-zoom="reset" aria-label="Reset zoom">
142
+
143
+ </button>
144
+ </div>
145
+ <div class="diagram-viewport">
146
+ <svg
147
+ class="diagram-canvas"
148
+ viewBox="0 0 1200 720"
149
+ role="img"
150
+ aria-labelledby="diagram-title diagram-description"
151
+ xmlns="http://www.w3.org/2000/svg"
152
+ >
153
+ {{DIAGRAM}}
154
+ </svg>
155
+ </div>
156
+ <div class="legend" aria-label="Diagram legend">{{LEGEND}}</div>
157
+ </section>
158
+ </main>
159
+ <script>
160
+ (() => {
161
+ const viewport = document.querySelector('.diagram-viewport');
162
+ const canvas = document.querySelector('.diagram-canvas');
163
+ let zoom = 1;
164
+ let dragging = false;
165
+ let originX = 0;
166
+ let originY = 0;
167
+ let scrollX = 0;
168
+ let scrollY = 0;
169
+ const apply = () => {
170
+ canvas.style.transform = `scale(${zoom})`;
171
+ };
172
+ document.querySelectorAll('[data-zoom]').forEach((button) => {
173
+ button.addEventListener('click', () => {
174
+ const action = button.dataset.zoom;
175
+ zoom =
176
+ action === 'reset'
177
+ ? 1
178
+ : Math.min(
179
+ 3,
180
+ Math.max(0.5, zoom * (action === 'in' ? 1.2 : 0.8)),
181
+ );
182
+ apply();
183
+ });
184
+ });
185
+ viewport.addEventListener(
186
+ 'wheel',
187
+ (event) => {
188
+ if (!event.ctrlKey && !event.metaKey) return;
189
+ event.preventDefault();
190
+ zoom = Math.min(
191
+ 3,
192
+ Math.max(0.5, zoom * (event.deltaY < 0 ? 1.1 : 0.9)),
193
+ );
194
+ apply();
195
+ },
196
+ { passive: false },
197
+ );
198
+ viewport.addEventListener('pointerdown', (event) => {
199
+ dragging = true;
200
+ originX = event.clientX;
201
+ originY = event.clientY;
202
+ scrollX = viewport.scrollLeft;
203
+ scrollY = viewport.scrollTop;
204
+ viewport.classList.add('is-panning');
205
+ viewport.setPointerCapture(event.pointerId);
206
+ });
207
+ viewport.addEventListener('pointermove', (event) => {
208
+ if (!dragging) return;
209
+ viewport.scrollLeft = scrollX - (event.clientX - originX);
210
+ viewport.scrollTop = scrollY - (event.clientY - originY);
211
+ });
212
+ viewport.addEventListener('pointerup', () => {
213
+ dragging = false;
214
+ viewport.classList.remove('is-panning');
215
+ });
216
+ })();
217
+ </script>
218
+ </body>
219
+ </html>