@cssints/apg-specs 0.0.0-stage → 0.5.2

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 (260) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -2
  3. package/dist/apg/accordion.bridge.d.mts +35 -0
  4. package/dist/apg/accordion.bridge.mjs +326 -0
  5. package/dist/apg/accordion.model.json +8397 -0
  6. package/dist/apg/alert.bridge.d.mts +22 -0
  7. package/dist/apg/alert.bridge.mjs +86 -0
  8. package/dist/apg/alert.model.json +871 -0
  9. package/dist/apg/alertdialog.bridge.d.mts +21 -0
  10. package/dist/apg/alertdialog.bridge.mjs +221 -0
  11. package/dist/apg/alertdialog.model.json +5232 -0
  12. package/dist/apg/breadcrumb.bridge.d.mts +26 -0
  13. package/dist/apg/breadcrumb.bridge.mjs +120 -0
  14. package/dist/apg/breadcrumb.model.json +574 -0
  15. package/dist/apg/button.bridge.d.mts +35 -0
  16. package/dist/apg/button.bridge.mjs +208 -0
  17. package/dist/apg/button.model.json +5814 -0
  18. package/dist/apg/carousel.bridge.d.mts +44 -0
  19. package/dist/apg/carousel.bridge.mjs +262 -0
  20. package/dist/apg/carousel.model.json +8963 -0
  21. package/dist/apg/checkbox-group.bridge.d.mts +25 -0
  22. package/dist/apg/checkbox-group.bridge.mjs +276 -0
  23. package/dist/apg/checkbox-group.model.json +3863 -0
  24. package/dist/apg/checkbox.bridge.d.mts +34 -0
  25. package/dist/apg/checkbox.bridge.mjs +253 -0
  26. package/dist/apg/checkbox.model.json +3158 -0
  27. package/dist/apg/combobox-autocomplete.bridge.d.mts +14 -0
  28. package/dist/apg/combobox-autocomplete.bridge.mjs +326 -0
  29. package/dist/apg/combobox-autocomplete.model.json +11586 -0
  30. package/dist/apg/combobox-dialog.bridge.d.mts +17 -0
  31. package/dist/apg/combobox-dialog.bridge.mjs +250 -0
  32. package/dist/apg/combobox-dialog.model.json +8981 -0
  33. package/dist/apg/combobox-grid.bridge.d.mts +17 -0
  34. package/dist/apg/combobox-grid.bridge.mjs +227 -0
  35. package/dist/apg/combobox-grid.model.json +8554 -0
  36. package/dist/apg/combobox-select-only.bridge.d.mts +24 -0
  37. package/dist/apg/combobox-select-only.bridge.mjs +304 -0
  38. package/dist/apg/combobox-select-only.model.json +7846 -0
  39. package/dist/apg/combobox-tree.bridge.d.mts +17 -0
  40. package/dist/apg/combobox-tree.bridge.mjs +211 -0
  41. package/dist/apg/combobox-tree.model.json +9345 -0
  42. package/dist/apg/dialog-modal.bridge.d.mts +35 -0
  43. package/dist/apg/dialog-modal.bridge.mjs +186 -0
  44. package/dist/apg/dialog-modal.model.json +7580 -0
  45. package/dist/apg/disclosure.bridge.d.mts +25 -0
  46. package/dist/apg/disclosure.bridge.mjs +202 -0
  47. package/dist/apg/disclosure.model.json +4703 -0
  48. package/dist/apg/feed.bridge.d.mts +31 -0
  49. package/dist/apg/feed.bridge.mjs +246 -0
  50. package/dist/apg/feed.model.json +4523 -0
  51. package/dist/apg/grid-selection.bridge.d.mts +13 -0
  52. package/dist/apg/grid-selection.bridge.mjs +160 -0
  53. package/dist/apg/grid-selection.model.json +6974 -0
  54. package/dist/apg/grid-sort.bridge.d.mts +12 -0
  55. package/dist/apg/grid-sort.bridge.mjs +138 -0
  56. package/dist/apg/grid-sort.model.json +3799 -0
  57. package/dist/apg/grid.bridge.d.mts +54 -0
  58. package/dist/apg/grid.bridge.mjs +441 -0
  59. package/dist/apg/grid.model.json +3802 -0
  60. package/dist/apg/link.bridge.d.mts +24 -0
  61. package/dist/apg/link.bridge.mjs +126 -0
  62. package/dist/apg/link.model.json +694 -0
  63. package/dist/apg/listbox-multi.bridge.d.mts +10 -0
  64. package/dist/apg/listbox-multi.bridge.mjs +261 -0
  65. package/dist/apg/listbox-multi.model.json +10893 -0
  66. package/dist/apg/listbox-rearrangeable.bridge.d.mts +20 -0
  67. package/dist/apg/listbox-rearrangeable.bridge.mjs +237 -0
  68. package/dist/apg/listbox-rearrangeable.model.json +17592 -0
  69. package/dist/apg/listbox-shared.d.mts +58 -0
  70. package/dist/apg/listbox-shared.mjs +171 -0
  71. package/dist/apg/listbox-single.bridge.d.mts +10 -0
  72. package/dist/apg/listbox-single.bridge.mjs +196 -0
  73. package/dist/apg/listbox-single.model.json +8024 -0
  74. package/dist/apg/listbox-typeahead.bridge.d.mts +10 -0
  75. package/dist/apg/listbox-typeahead.bridge.mjs +153 -0
  76. package/dist/apg/listbox-typeahead.model.json +5615 -0
  77. package/dist/apg/menu-button.bridge.d.mts +29 -0
  78. package/dist/apg/menu-button.bridge.mjs +259 -0
  79. package/dist/apg/menu-button.model.json +5706 -0
  80. package/dist/apg/menu-checkable.bridge.d.mts +25 -0
  81. package/dist/apg/menu-checkable.bridge.mjs +226 -0
  82. package/dist/apg/menu-checkable.model.json +4277 -0
  83. package/dist/apg/menu-menubar.bridge.d.mts +27 -0
  84. package/dist/apg/menu-menubar.bridge.mjs +273 -0
  85. package/dist/apg/menu-menubar.model.json +9540 -0
  86. package/dist/apg/meter.bridge.d.mts +28 -0
  87. package/dist/apg/meter.bridge.mjs +159 -0
  88. package/dist/apg/meter.model.json +1185 -0
  89. package/dist/apg/player.bridge.d.mts +24 -0
  90. package/dist/apg/player.bridge.mjs +342 -0
  91. package/dist/apg/player.model.json +11013 -0
  92. package/dist/apg/radio.bridge.d.mts +35 -0
  93. package/dist/apg/radio.bridge.mjs +371 -0
  94. package/dist/apg/radio.model.json +5011 -0
  95. package/dist/apg/slider-multithumb.bridge.d.mts +34 -0
  96. package/dist/apg/slider-multithumb.bridge.mjs +365 -0
  97. package/dist/apg/slider-multithumb.model.json +5774 -0
  98. package/dist/apg/slider.bridge.d.mts +54 -0
  99. package/dist/apg/slider.bridge.mjs +359 -0
  100. package/dist/apg/slider.model.json +4881 -0
  101. package/dist/apg/spinbutton.bridge.d.mts +41 -0
  102. package/dist/apg/spinbutton.bridge.mjs +327 -0
  103. package/dist/apg/spinbutton.model.json +7571 -0
  104. package/dist/apg/switch.bridge.d.mts +30 -0
  105. package/dist/apg/switch.bridge.mjs +243 -0
  106. package/dist/apg/switch.model.json +3159 -0
  107. package/dist/apg/tabs.bridge.d.mts +41 -0
  108. package/dist/apg/tabs.bridge.mjs +294 -0
  109. package/dist/apg/tabs.model.json +7659 -0
  110. package/dist/apg/timeline.bridge.d.mts +36 -0
  111. package/dist/apg/timeline.bridge.mjs +265 -0
  112. package/dist/apg/timeline.model.json +6189 -0
  113. package/dist/apg/toolbar.bridge.d.mts +25 -0
  114. package/dist/apg/toolbar.bridge.mjs +307 -0
  115. package/dist/apg/toolbar.model.json +6616 -0
  116. package/dist/apg/tooltip.bridge.d.mts +36 -0
  117. package/dist/apg/tooltip.bridge.mjs +208 -0
  118. package/dist/apg/tooltip.model.json +8295 -0
  119. package/dist/apg/treegrid.bridge.d.mts +28 -0
  120. package/dist/apg/treegrid.bridge.mjs +311 -0
  121. package/dist/apg/treegrid.model.json +17358 -0
  122. package/dist/apg/treeview.bridge.d.mts +26 -0
  123. package/dist/apg/treeview.bridge.mjs +396 -0
  124. package/dist/apg/treeview.model.json +13745 -0
  125. package/dist/apg/windowsplitter.bridge.d.mts +36 -0
  126. package/dist/apg/windowsplitter.bridge.mjs +242 -0
  127. package/dist/apg/windowsplitter.model.json +6253 -0
  128. package/dist/canvas/camera.model.json +6568 -0
  129. package/dist/canvas/canvas.bridge.d.mts +190 -0
  130. package/dist/canvas/canvas.bridge.mjs +729 -0
  131. package/dist/canvas/pointer.model.json +17095 -0
  132. package/dist/scenario.d.mts +233 -0
  133. package/dist/scenario.mjs +313 -0
  134. package/package.json +57 -4
  135. package/scenarios/apg/README.md +221 -0
  136. package/scenarios/apg/accordion.base-ui.json +1005 -0
  137. package/scenarios/apg/accordion.react-aria.json +929 -0
  138. package/scenarios/apg/alert.base-ui.json +81 -0
  139. package/scenarios/apg/alert.react-aria.json +98 -0
  140. package/scenarios/apg/alertdialog.base-ui.json +331 -0
  141. package/scenarios/apg/alertdialog.react-aria.json +225 -0
  142. package/scenarios/apg/breadcrumb.react-aria.json +312 -0
  143. package/scenarios/apg/button.ariakit.json +136 -0
  144. package/scenarios/apg/button.base-ui.json +747 -0
  145. package/scenarios/apg/button.react-aria.json +443 -0
  146. package/scenarios/apg/carousel.apg.json +909 -0
  147. package/scenarios/apg/checkbox.ariakit.json +299 -0
  148. package/scenarios/apg/checkbox.base-ui.json +1126 -0
  149. package/scenarios/apg/checkbox.react-aria.json +518 -0
  150. package/scenarios/apg/combobox.ariakit.json +945 -0
  151. package/scenarios/apg/combobox.base-ui.json +2444 -0
  152. package/scenarios/apg/combobox.react-aria.json +845 -0
  153. package/scenarios/apg/dialog-modal.ariakit.json +1102 -0
  154. package/scenarios/apg/dialog-modal.base-ui.json +775 -0
  155. package/scenarios/apg/dialog-modal.react-aria.json +638 -0
  156. package/scenarios/apg/disclosure.apg.json +349 -0
  157. package/scenarios/apg/disclosure.ariakit.json +163 -0
  158. package/scenarios/apg/disclosure.base-ui.json +451 -0
  159. package/scenarios/apg/disclosure.react-aria.json +643 -0
  160. package/scenarios/apg/feed.apg.json +442 -0
  161. package/scenarios/apg/grid.ariakit.json +197 -0
  162. package/scenarios/apg/grid.base-ui.json +720 -0
  163. package/scenarios/apg/grid.react-aria.json +969 -0
  164. package/scenarios/apg/landmarks.ariakit.json +93 -0
  165. package/scenarios/apg/landmarks.react-aria.json +330 -0
  166. package/scenarios/apg/link.ariakit.json +83 -0
  167. package/scenarios/apg/link.base-ui.json +88 -0
  168. package/scenarios/apg/link.react-aria.json +186 -0
  169. package/scenarios/apg/listbox.ariakit.json +78 -0
  170. package/scenarios/apg/listbox.base-ui.json +163 -0
  171. package/scenarios/apg/listbox.react-aria.json +1118 -0
  172. package/scenarios/apg/menu-button.ariakit.json +924 -0
  173. package/scenarios/apg/menu-button.base-ui.json +1155 -0
  174. package/scenarios/apg/menu-button.react-aria.json +1080 -0
  175. package/scenarios/apg/menubar.ariakit.json +758 -0
  176. package/scenarios/apg/menubar.base-ui.json +673 -0
  177. package/scenarios/apg/meter.base-ui.json +243 -0
  178. package/scenarios/apg/meter.react-aria.json +201 -0
  179. package/scenarios/apg/radio.ariakit.json +459 -0
  180. package/scenarios/apg/radio.base-ui.json +581 -0
  181. package/scenarios/apg/radio.react-aria.json +576 -0
  182. package/scenarios/apg/slider-multithumb.base-ui.json +316 -0
  183. package/scenarios/apg/slider-multithumb.react-aria.json +363 -0
  184. package/scenarios/apg/slider.base-ui.json +544 -0
  185. package/scenarios/apg/slider.react-aria.json +456 -0
  186. package/scenarios/apg/spinbutton.base-ui.json +1688 -0
  187. package/scenarios/apg/spinbutton.react-aria.json +426 -0
  188. package/scenarios/apg/switch.base-ui.json +456 -0
  189. package/scenarios/apg/switch.react-aria.json +303 -0
  190. package/scenarios/apg/table.ariakit.json +65 -0
  191. package/scenarios/apg/table.react-aria.json +221 -0
  192. package/scenarios/apg/tabs.ariakit.json +329 -0
  193. package/scenarios/apg/tabs.base-ui.json +875 -0
  194. package/scenarios/apg/tabs.react-aria.json +644 -0
  195. package/scenarios/apg/toolbar.ariakit.json +595 -0
  196. package/scenarios/apg/toolbar.base-ui.json +1189 -0
  197. package/scenarios/apg/toolbar.react-aria.json +383 -0
  198. package/scenarios/apg/tooltip.ariakit.json +441 -0
  199. package/scenarios/apg/tooltip.base-ui.json +411 -0
  200. package/scenarios/apg/tooltip.react-aria.json +454 -0
  201. package/scenarios/apg/treegrid.apg.json +2558 -0
  202. package/scenarios/apg/treegrid.react-aria.json +5094 -0
  203. package/scenarios/apg/treeview.apg.json +928 -0
  204. package/scenarios/apg/windowsplitter.apg.json +557 -0
  205. package/scenarios/canvas/README.md +69 -0
  206. package/scenarios/canvas/camera.contract.json +237 -0
  207. package/scenarios/canvas/camera.excalidraw.json +57 -0
  208. package/scenarios/canvas/marquee.excalidraw.json +122 -0
  209. package/scenarios/canvas/pan.excalidraw.json +46 -0
  210. package/scenarios/canvas/pointer.contract.json +528 -0
  211. package/scenarios/parts.json +291 -0
  212. package/shared/aria.qnt +186 -0
  213. package/shared/details-group.qnt +50 -0
  214. package/shared/focusgroup.qnt +152 -0
  215. package/shared/invokers.qnt +81 -0
  216. package/shared/popover.qnt +71 -0
  217. package/specs/apg/STATIC.md +103 -0
  218. package/specs/apg/accordion.qnt +578 -0
  219. package/specs/apg/alert.qnt +131 -0
  220. package/specs/apg/alertdialog.qnt +455 -0
  221. package/specs/apg/breadcrumb.qnt +88 -0
  222. package/specs/apg/button.qnt +445 -0
  223. package/specs/apg/carousel.qnt +732 -0
  224. package/specs/apg/checkbox-group.qnt +388 -0
  225. package/specs/apg/checkbox.qnt +394 -0
  226. package/specs/apg/combobox-autocomplete.qnt +764 -0
  227. package/specs/apg/combobox-dialog.qnt +627 -0
  228. package/specs/apg/combobox-grid.qnt +632 -0
  229. package/specs/apg/combobox-select-only.qnt +626 -0
  230. package/specs/apg/combobox-tree.qnt +633 -0
  231. package/specs/apg/dialog-modal.qnt +499 -0
  232. package/specs/apg/disclosure.qnt +309 -0
  233. package/specs/apg/feed.qnt +485 -0
  234. package/specs/apg/grid-selection.qnt +512 -0
  235. package/specs/apg/grid-sort.qnt +330 -0
  236. package/specs/apg/grid.qnt +360 -0
  237. package/specs/apg/link.qnt +119 -0
  238. package/specs/apg/listbox-multi.qnt +799 -0
  239. package/specs/apg/listbox-rearrangeable.qnt +801 -0
  240. package/specs/apg/listbox-single.qnt +511 -0
  241. package/specs/apg/listbox-typeahead.qnt +396 -0
  242. package/specs/apg/menu-button.qnt +363 -0
  243. package/specs/apg/menu-checkable.qnt +304 -0
  244. package/specs/apg/menu-menubar.qnt +531 -0
  245. package/specs/apg/meter.qnt +129 -0
  246. package/specs/apg/player.qnt +1081 -0
  247. package/specs/apg/radio.qnt +394 -0
  248. package/specs/apg/slider-multithumb.qnt +441 -0
  249. package/specs/apg/slider.qnt +396 -0
  250. package/specs/apg/spinbutton.qnt +494 -0
  251. package/specs/apg/switch.qnt +389 -0
  252. package/specs/apg/tabs.qnt +412 -0
  253. package/specs/apg/timeline.qnt +612 -0
  254. package/specs/apg/toolbar.qnt +364 -0
  255. package/specs/apg/tooltip.qnt +553 -0
  256. package/specs/apg/treegrid.qnt +983 -0
  257. package/specs/apg/treeview.qnt +1004 -0
  258. package/specs/apg/windowsplitter.qnt +487 -0
  259. package/specs/canvas/camera.qnt +407 -0
  260. package/specs/canvas/pointer.qnt +841 -0
@@ -0,0 +1,729 @@
1
+ import { Unsupported } from "../scenario.mjs";
2
+ //#region src/canvas/canvas.bridge.ts
3
+ const specEvent = (tag, params = {}) => ({
4
+ _tag: tag,
5
+ ...params
6
+ });
7
+ /**
8
+ * Canvas vocabulary <-> `specs/canvas/camera.qnt` and `specs/canvas/pointer.qnt`.
9
+ *
10
+ * Two machines, so two bridges (`cameraBridge`, `pointerBridge`). Both parse a scenario step into
11
+ * a typed, library-neutral OP (`parseCameraOp`, `parsePointerOp`); the spec side turns the op into
12
+ * spec events, the DOM driver (packages/headless/test) turns the same op into real input. Anything
13
+ * outside the model's pools throws `Unsupported`, which skips the scenario on every target.
14
+ *
15
+ * Why steps are pool-shaped: the specs pick every selector from a small fixed pool (one nondet per
16
+ * action), so a step may only name a pool member - a palette position `p0..p11`, a pan delta from
17
+ * {-9e6, -400, 0, 5, 9e6}, a zoom factor from {1/2, 3/4, 5/4, 3/2, 2, 16, 1/16}, a named anchor.
18
+ *
19
+ * Observation targets (probe / `expect.attrs`):
20
+ * `layer` `--canvas-x` `--canvas-y` `--canvas-zoom` the rendered camera custom properties
21
+ * `node<i>` `aria-selected` `--node-x` `--node-y` `content-visibility`
22
+ * `live` `text` the live region
23
+ * `state` `mode owner selection cursor editing guides culled announcements` (pointer)
24
+ * `camera viewport tweening reducedMotion` (camera)
25
+ * `state` reads the primitive's own snapshot (`Canvas.peek()`), JSON-formatted by `formatValue`.
26
+ *
27
+ * Not an oracle: `culled` is an independent TS port of the spec's `culled` def (defs are not
28
+ * callable from the reference model), so it checks the primitive against the port, not the spec.
29
+ */
30
+ /** pointer.qnt's three nodes, bottom of the z-order first. */
31
+ const NODES = [
32
+ {
33
+ id: "0",
34
+ x: 100,
35
+ y: 100,
36
+ width: 200,
37
+ height: 100,
38
+ label: "Node A"
39
+ },
40
+ {
41
+ id: "1",
42
+ x: 250,
43
+ y: 150,
44
+ width: 200,
45
+ height: 150,
46
+ label: "Node B"
47
+ },
48
+ {
49
+ id: "2",
50
+ x: 600,
51
+ y: 400,
52
+ width: 100,
53
+ height: 100,
54
+ label: "Node C"
55
+ }
56
+ ];
57
+ /** pointer.qnt's VW / VH: the viewport its culling predicate uses. */
58
+ const POINTER_VIEWPORT = {
59
+ width: 1024,
60
+ height: 768
61
+ };
62
+ /** pointer.qnt's screen palette (px): `p<k>` is `PALETTE[k]`. */
63
+ const PALETTE = [
64
+ [150, 130],
65
+ [152, 131],
66
+ [154, 130],
67
+ [155, 130],
68
+ [280, 170],
69
+ [400, 250],
70
+ [650, 450],
71
+ [60, 60],
72
+ [320, 220],
73
+ [500, 350],
74
+ [800, 600],
75
+ [600, 60]
76
+ ];
77
+ /** pointer.qnt's `extX(k)` / `extY(k)`: the positions a host move can set (the last is far past the world bound). */
78
+ const HOST_POSITIONS = [
79
+ [0, 0],
80
+ [320, 200],
81
+ [-300, -300],
82
+ [5e6, -5e6]
83
+ ];
84
+ /** pointer.qnt's `zoomPreset(level)`, as zoom values. */
85
+ const ZOOM_LEVELS = [
86
+ 1 / 16,
87
+ 1 / 2,
88
+ 1,
89
+ 2,
90
+ 8
91
+ ];
92
+ const VIEWPORTS = [
93
+ [320, 240],
94
+ [800, 600],
95
+ [1024, 768],
96
+ [1920, 1080],
97
+ [4096, 2160]
98
+ ];
99
+ const PAN_DELTAS = [
100
+ -9e6,
101
+ -400,
102
+ 0,
103
+ 5,
104
+ 9e6
105
+ ];
106
+ const ZOOM_FACTORS = [
107
+ 1 / 2,
108
+ 3 / 4,
109
+ 5 / 4,
110
+ 3 / 2,
111
+ 2,
112
+ 16,
113
+ 1 / 16
114
+ ];
115
+ const ANCHORS = [
116
+ "top-left",
117
+ "centre",
118
+ "bottom-right",
119
+ "quarter"
120
+ ];
121
+ const CENTRE_POINTS = [
122
+ [0, 0],
123
+ [1e3, 500],
124
+ [-25e4, 4e5],
125
+ [999e3, -999e3]
126
+ ];
127
+ const FIXED_ONE = 4096;
128
+ const TONE = 256;
129
+ /** camera.qnt's `anchorX` / `anchorY`: where an anchor name sits in a viewport of this size. */
130
+ const anchorPoint = (anchor, width, height) => {
131
+ switch (anchor) {
132
+ case "top-left": return [0, 0];
133
+ case "centre": return [Math.floor(width / 2), Math.floor(height / 2)];
134
+ case "bottom-right": return [width, height];
135
+ case "quarter": return [Math.floor(width / 4), Math.floor(3 * height / 4)];
136
+ }
137
+ };
138
+ const MIN_ZOOM_GOAL = 0;
139
+ const MAX_ZOOM_GOAL = 1;
140
+ const UNIT_ZOOM_GOAL = 2;
141
+ const ANCHOR_COUNT = 4;
142
+ const PAN_POOL = 5;
143
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
144
+ const indexIn = (pool, value, what) => {
145
+ const index = pool.indexOf(value);
146
+ if (index < 0) throw new Unsupported(`${what} ${JSON.stringify(value)} is not in the spec pool`);
147
+ return index;
148
+ };
149
+ const anchorOf = (value) => {
150
+ const index = ANCHORS.findIndex((name) => name === value);
151
+ if (index < 0) throw new Unsupported(`anchor ${JSON.stringify(value)}`);
152
+ return index;
153
+ };
154
+ const anchorName = (index) => ANCHORS[index] ?? "top-left";
155
+ const zoomGoalKinds = /* @__PURE__ */ new Map([
156
+ ["min", MIN_ZOOM_GOAL],
157
+ ["max", MAX_ZOOM_GOAL],
158
+ ["100%", UNIT_ZOOM_GOAL]
159
+ ]);
160
+ const ZOOM_GOAL_VALUES = [
161
+ 256,
162
+ 32768,
163
+ FIXED_ONE
164
+ ];
165
+ /** A camera step -> an op; `Unsupported` for anything the camera machine does not model. */
166
+ const parseCameraOp = (step) => {
167
+ if (step.event !== "prop") throw new Unsupported(`event ${step.event}`);
168
+ const { value } = step;
169
+ switch (step.prop) {
170
+ case "resize":
171
+ if (typeof value !== "number" || value < 0 || value >= VIEWPORTS.length) throw new Unsupported(`viewport ${JSON.stringify(value)}`);
172
+ return {
173
+ op: "resize",
174
+ viewport: value
175
+ };
176
+ case "motion": return {
177
+ op: "motion",
178
+ reduced: value === true
179
+ };
180
+ case "pan": {
181
+ if (!Array.isArray(value) || value.length !== 2) throw new Unsupported("pan needs [dx, dy]");
182
+ const [dx, dy] = value;
183
+ if (typeof dx !== "number" || typeof dy !== "number") throw new Unsupported("pan needs [dx, dy] numbers");
184
+ indexIn(PAN_DELTAS, dx, "pan dx");
185
+ indexIn(PAN_DELTAS, dy, "pan dy");
186
+ return {
187
+ op: "pan",
188
+ dx,
189
+ dy
190
+ };
191
+ }
192
+ case "zoom":
193
+ if (!isRecord(value) || typeof value["factor"] !== "number") throw new Unsupported("zoom needs { factor, anchor }");
194
+ indexIn(ZOOM_FACTORS, value["factor"], "zoom factor");
195
+ return {
196
+ op: "zoom",
197
+ factor: Math.round(value["factor"] * FIXED_ONE),
198
+ anchor: anchorName(anchorOf(value["anchor"]))
199
+ };
200
+ case "zoomLimit":
201
+ if (!isRecord(value) || value["limit"] !== "min" && value["limit"] !== "max") throw new Unsupported("zoomLimit needs { limit: min | max, anchor }");
202
+ return {
203
+ op: "zoomLimit",
204
+ limit: value["limit"],
205
+ anchor: anchorName(anchorOf(value["anchor"]))
206
+ };
207
+ case "tween": {
208
+ if (!isRecord(value)) throw new Unsupported("tween needs a goal");
209
+ const centre = value["centre"];
210
+ if (Array.isArray(centre)) {
211
+ const [x, y] = centre;
212
+ if (typeof x !== "number" || typeof y !== "number") throw new Unsupported("centre needs [x, y] numbers");
213
+ if (CENTRE_POINTS.findIndex((point) => point[0] === x && point[1] === y) < 0) throw new Unsupported(`centre [${x}, ${y}] is not in the spec pool`);
214
+ return {
215
+ op: "tween",
216
+ goal: {
217
+ kind: "centre",
218
+ x,
219
+ y
220
+ }
221
+ };
222
+ }
223
+ const kind = zoomGoalKinds.get(String(value["zoom"]));
224
+ if (kind === void 0) throw new Unsupported(`tween goal ${JSON.stringify(value)}`);
225
+ return {
226
+ op: "tween",
227
+ goal: {
228
+ kind: "zoom",
229
+ zoom: ZOOM_GOAL_VALUES[kind] ?? FIXED_ONE,
230
+ anchor: anchorName(anchorOf(value["anchor"]))
231
+ }
232
+ };
233
+ }
234
+ case "tick": return { op: "tick" };
235
+ case "cancelTween": return { op: "cancelTween" };
236
+ default: throw new Unsupported(`prop ${step.prop ?? ""}`);
237
+ }
238
+ };
239
+ const cameraEvents = (op) => {
240
+ switch (op.op) {
241
+ case "resize": return [specEvent("resize", { vp: BigInt(op.viewport) })];
242
+ case "motion": return [specEvent("set_motion", { reduced: op.reduced })];
243
+ case "pan": {
244
+ const ix = indexIn(PAN_DELTAS, op.dx, "pan dx");
245
+ const iy = indexIn(PAN_DELTAS, op.dy, "pan dy");
246
+ return [specEvent("pan_by", { delta: BigInt(iy * PAN_POOL + ix) })];
247
+ }
248
+ case "zoom": {
249
+ const factor = indexIn(ZOOM_FACTORS.map((f) => Math.round(f * FIXED_ONE)), op.factor, "zoom factor");
250
+ const anchor = ANCHORS.indexOf(op.anchor);
251
+ return [specEvent("zoom_at", { gesture: BigInt(anchor + ANCHOR_COUNT * factor) })];
252
+ }
253
+ case "zoomLimit": {
254
+ const anchor = ANCHORS.indexOf(op.anchor);
255
+ return [specEvent("zoom_limit", { limit: BigInt(anchor + ANCHOR_COUNT * (op.limit === "max" ? 1 : 0)) })];
256
+ }
257
+ case "tween": {
258
+ if (op.goal.kind === "centre") {
259
+ const { x, y } = op.goal;
260
+ const index = CENTRE_POINTS.findIndex((point) => point[0] === x && point[1] === y);
261
+ return [specEvent("request_tween", { goal: BigInt(12 + index) })];
262
+ }
263
+ const { zoom } = op.goal;
264
+ const kind = ZOOM_GOAL_VALUES.findIndex((value) => value === zoom);
265
+ return [specEvent("request_tween", { goal: BigInt(kind * ANCHOR_COUNT + ANCHORS.indexOf(op.goal.anchor)) })];
266
+ }
267
+ case "tick": return [specEvent("tween_tick")];
268
+ case "cancelTween": return [specEvent("cancel_tween")];
269
+ }
270
+ };
271
+ const NODE_TARGET = /^node(\d+)$/;
272
+ const pointerOf = (value) => {
273
+ if (value === void 0 || value === 1) return 1;
274
+ if (value === 2) return 2;
275
+ throw new Unsupported(`pointer id ${JSON.stringify(value)}`);
276
+ };
277
+ const paletteOf = (target) => {
278
+ const match = /^p(\d+)$/.exec(target ?? "");
279
+ const index = match === null ? -1 : Number(match[1]);
280
+ if (index < 0 || index >= PALETTE.length) throw new Unsupported(`palette position ${target ?? ""}`);
281
+ return index;
282
+ };
283
+ const parsePlace = ({ target, value }) => {
284
+ const match = NODE_TARGET.exec(target ?? "");
285
+ const node = match === null ? -1 : Number(match[1]);
286
+ if (node < 0 || node >= NODES.length) throw new Unsupported(`place needs a node target, got ${target ?? ""}`);
287
+ const position = HOST_POSITIONS.findIndex((point) => Array.isArray(value) && point[0] === value[0] && point[1] === value[1]);
288
+ if (position < 0) throw new Unsupported(`host position ${JSON.stringify(value)} is not in the spec pool`);
289
+ return {
290
+ op: "place",
291
+ node,
292
+ position
293
+ };
294
+ };
295
+ /**
296
+ * A pointer step -> an op. Pointer input is `{ event: pointerdown | pointermove | pointerup |
297
+ * pointercancel, target: "p<k>", value: <pointer id, default 1> }`; modifiers and parameters are
298
+ * `prop` steps (`shift`, `space`, `snap`, `marquee`, `zoom`, `edit`).
299
+ */
300
+ const parsePointerOp = (step) => {
301
+ const { value } = step;
302
+ switch (step.event) {
303
+ case "pointerdown": return {
304
+ op: "down",
305
+ pointer: pointerOf(value),
306
+ at: paletteOf(step.target)
307
+ };
308
+ case "pointermove": return {
309
+ op: "move",
310
+ pointer: pointerOf(value),
311
+ at: paletteOf(step.target)
312
+ };
313
+ case "pointerup": return {
314
+ op: "up",
315
+ pointer: pointerOf(value)
316
+ };
317
+ case "pointercancel": return {
318
+ op: "cancel",
319
+ pointer: pointerOf(value)
320
+ };
321
+ case "prop": switch (step.prop) {
322
+ case "shift":
323
+ case "space":
324
+ case "snap": return {
325
+ op: step.prop,
326
+ on: value === true
327
+ };
328
+ case "marquee":
329
+ if (value === "contain" || value === "overlap") return {
330
+ op: "marquee",
331
+ mode: value
332
+ };
333
+ throw new Unsupported(`marquee ${JSON.stringify(value)}`);
334
+ case "zoom": {
335
+ const level = ZOOM_LEVELS.findIndex((zoom) => zoom === value);
336
+ if (level < 0) throw new Unsupported(`zoom level ${JSON.stringify(value)}`);
337
+ return {
338
+ op: "zoom",
339
+ level
340
+ };
341
+ }
342
+ case "edit": return { op: "edit" };
343
+ case "place": return parsePlace(step);
344
+ default: throw new Unsupported(`prop ${step.prop ?? ""}`);
345
+ }
346
+ default: throw new Unsupported(`event ${step.event}`);
347
+ }
348
+ };
349
+ const POINTER_POOL = PALETTE.length;
350
+ const pointerEvents = (op) => {
351
+ switch (op.op) {
352
+ case "down": return [specEvent("down", { touch: BigInt((op.pointer - 1) * POINTER_POOL + op.at) })];
353
+ case "move": return [specEvent("move", { touch: BigInt((op.pointer - 1) * POINTER_POOL + op.at) })];
354
+ case "up": return [specEvent("up", { lift: BigInt(op.pointer) })];
355
+ case "cancel": return [specEvent("cancel_pointer", { lost: BigInt(op.pointer) })];
356
+ case "shift": return [specEvent("set_shift", { held: op.on })];
357
+ case "space": return [specEvent("set_space", { held: op.on })];
358
+ case "snap": return [specEvent("set_snap", { snapping: op.on })];
359
+ case "marquee": return [specEvent("set_marquee_mode", { choice: op.mode })];
360
+ case "zoom": return [specEvent("zoom_to", { level: BigInt(op.level) })];
361
+ case "edit": return [specEvent("toggle_edit")];
362
+ case "place": return [specEvent("host_move", { place: BigInt(op.position * NODES.length + op.node) })];
363
+ }
364
+ };
365
+ /** Scenario `config` -> what both targets are mounted with. */
366
+ const cameraConfig = (config) => {
367
+ let reducedMotion = false;
368
+ for (const [key, value] of Object.entries(config)) if (key === "reducedMotion" && typeof value === "boolean") reducedMotion = value;
369
+ else throw new Unsupported(`config ${key}=${JSON.stringify(value)}`);
370
+ return { reducedMotion };
371
+ };
372
+ const pointerConfig = (config) => {
373
+ let marquee = "contain";
374
+ let snap = false;
375
+ for (const [key, value] of Object.entries(config)) if (key === "marquee" && (value === "contain" || value === "overlap")) marquee = value;
376
+ else if (key === "snap" && typeof value === "boolean") snap = value;
377
+ else throw new Unsupported(`config ${key}=${JSON.stringify(value)}`);
378
+ return {
379
+ marquee,
380
+ snap
381
+ };
382
+ };
383
+ /** How a `state` value is read as an attribute: a string as is, `null` = absent, the rest JSON. */
384
+ const formatValue = (value) => {
385
+ if (value === null || value === void 0) return null;
386
+ return typeof value === "string" ? value : JSON.stringify(value);
387
+ };
388
+ const num = (value) => Number(value);
389
+ const world = (fixed) => String(fixed / TONE);
390
+ const layerAttr = (tx, ty, z, name) => {
391
+ switch (name) {
392
+ case "--canvas-x": return world(tx);
393
+ case "--canvas-y": return world(ty);
394
+ case "--canvas-zoom": return String(z / FIXED_ONE);
395
+ default: throw new Unsupported(`attribute ${name} on layer`);
396
+ }
397
+ };
398
+ const stateAttr = (value, name) => {
399
+ if (!Object.hasOwn(value, name)) throw new Unsupported(`state ${name}`);
400
+ return formatValue(value[name]);
401
+ };
402
+ const cameraValue = (state) => ({
403
+ camera: {
404
+ tx: num(state["tx"]),
405
+ ty: num(state["ty"]),
406
+ z: num(state["z"])
407
+ },
408
+ viewport: {
409
+ width: num(state["vw"]),
410
+ height: num(state["vh"])
411
+ },
412
+ tweening: state["active"] === true,
413
+ reducedMotion: state["rm"] === true
414
+ });
415
+ const cameraBridge = {
416
+ setup: (config) => cameraConfig(config).reducedMotion ? [specEvent("set_motion", { reduced: true })] : [],
417
+ event: (step) => cameraEvents(parseCameraOp(step)),
418
+ value: cameraValue,
419
+ focus: () => null,
420
+ attr: (state, target, name) => {
421
+ if (target === "layer") return layerAttr(num(state["tx"]), num(state["ty"]), num(state["z"]), name);
422
+ if (target === "state") return stateAttr(cameraValue(state), name);
423
+ throw new Unsupported(`attribute ${name} on ${target}`);
424
+ },
425
+ visible: () => [],
426
+ events: () => []
427
+ };
428
+ const bitSet = (mask, bit) => Math.floor(mask / 2 ** bit) % 2 === 1;
429
+ const maskIds = (mask) => NODES.flatMap((node, bit) => bitSet(mask, bit) ? [node.id] : []);
430
+ const nodePosition = (state, index) => {
431
+ const dragging = state["mode"] === "dragging" && bitSet(num(state["sel"]), index);
432
+ const dx = dragging ? num(state["ddx"]) : 0;
433
+ const dy = dragging ? num(state["ddy"]) : 0;
434
+ return [num(state[`n${index}x`]) + dx, num(state[`n${index}y`]) + dy];
435
+ };
436
+ const exempt = (state, index) => bitSet(num(state["sel"]), index) || num(state["cursor"]) === index || num(state["editing"]) === index;
437
+ const screenToWorldFp = (s, t, z) => {
438
+ const tz = FIXED_ONE * TONE;
439
+ return Math.floor((2 * s * tz + z) / (2 * z)) - t;
440
+ };
441
+ /** Independent port of pointer.qnt's `inView` / `culled`. */
442
+ const culledNodes = (state) => {
443
+ const tx = num(state["tx"]);
444
+ const ty = num(state["ty"]);
445
+ const z = num(state["z"]);
446
+ const left = screenToWorldFp(0, tx, z);
447
+ const right = screenToWorldFp(POINTER_VIEWPORT.width, tx, z);
448
+ const top = screenToWorldFp(0, ty, z);
449
+ const bottom = screenToWorldFp(POINTER_VIEWPORT.height, ty, z);
450
+ return NODES.flatMap((node, index) => {
451
+ const [x, y] = nodePosition(state, index);
452
+ return x * TONE < right && (x + node.width) * TONE > left && y * TONE < bottom && (y + node.height) * TONE > top || exempt(state, index) ? [] : [node.id];
453
+ });
454
+ };
455
+ const idOf = (index) => NODES[index]?.id ?? null;
456
+ const pointerValue = (state) => ({
457
+ mode: String(state["mode"]),
458
+ owner: num(state["owner"]) === 0 ? null : num(state["owner"]),
459
+ selection: maskIds(num(state["sel"])),
460
+ cursor: idOf(num(state["cursor"])),
461
+ editing: idOf(num(state["editing"])),
462
+ guides: {
463
+ x: bitSet(num(state["guide"]), 0),
464
+ y: bitSet(num(state["guide"]), 1)
465
+ },
466
+ culled: culledNodes(state),
467
+ announcements: num(state["annCount"])
468
+ });
469
+ /** The text the primitive's default messages give a settled selection. */
470
+ const selectionText = (ids) => {
471
+ const labels = ids.map((id) => NODES.find((node) => node.id === id)?.label ?? id);
472
+ if (labels.length === 0) return "Selection cleared";
473
+ return labels.length === 1 ? `${labels[0] ?? ""} selected` : `${labels.length} nodes selected: ${labels.join(", ")}`;
474
+ };
475
+ const pointerBridge = {
476
+ setup: (config) => {
477
+ const { marquee, snap } = pointerConfig(config);
478
+ return [...marquee === "contain" ? [] : [specEvent("set_marquee_mode", { choice: marquee })], ...snap ? [specEvent("set_snap", { snapping: true })] : []];
479
+ },
480
+ event: (step) => pointerEvents(parsePointerOp(step)),
481
+ value: pointerValue,
482
+ focus: (state) => {
483
+ const cursor = num(state["cursor"]);
484
+ return cursor < 0 ? null : `node${cursor}`;
485
+ },
486
+ attr: (state, target, name) => {
487
+ if (target === "layer") return layerAttr(num(state["tx"]), num(state["ty"]), num(state["z"]), name);
488
+ if (target === "state") return stateAttr(pointerValue(state), name);
489
+ if (target === "live" && name === "text") return num(state["annCount"]) === 0 ? "" : selectionText(maskIds(num(state["annSel"])));
490
+ const match = NODE_TARGET.exec(target);
491
+ const index = match === null ? -1 : Number(match[1]);
492
+ if (index >= 0 && index < NODES.length) {
493
+ const [x, y] = nodePosition(state, index);
494
+ switch (name) {
495
+ case "aria-selected": return String(bitSet(num(state["sel"]), index));
496
+ case "--node-x": return String(x);
497
+ case "--node-y": return String(y);
498
+ case "content-visibility": return exempt(state, index) ? "visible" : "auto";
499
+ }
500
+ }
501
+ throw new Unsupported(`attribute ${name} on ${target}`);
502
+ },
503
+ visible: () => [],
504
+ events: (before, after) => {
505
+ const out = [];
506
+ if (num(before["annCount"]) !== num(after["annCount"])) out.push(["onSelectionChange", maskIds(num(after["annSel"]))]);
507
+ const moves = NODES.flatMap((node, index) => {
508
+ const [bx, by] = [num(before[`n${index}x`]), num(before[`n${index}y`])];
509
+ const [ax, ay] = [num(after[`n${index}x`]), num(after[`n${index}y`])];
510
+ return bx === ax && by === ay ? [] : [{
511
+ id: node.id,
512
+ x: ax,
513
+ y: ay
514
+ }];
515
+ });
516
+ if (moves.length > 0) out.push(["onNodesMove", moves]);
517
+ return out;
518
+ }
519
+ };
520
+ const paletteIndexes = PALETTE.map((_, index) => index);
521
+ const press = (event, at, pointer) => ({
522
+ event,
523
+ target: `p${at}`,
524
+ value: pointer
525
+ });
526
+ /** The user-level steps a pointer differential walk draws from. */
527
+ const pointerSteps = [
528
+ ...paletteIndexes.map((at) => press("pointerdown", at, 1)),
529
+ ...paletteIndexes.flatMap((at) => [press("pointermove", at, 1), press("pointermove", at, 1)]),
530
+ press("pointerdown", 4, 2),
531
+ press("pointerdown", 7, 2),
532
+ press("pointermove", 9, 2),
533
+ press("pointermove", 5, 2),
534
+ {
535
+ event: "pointerup",
536
+ value: 1
537
+ },
538
+ {
539
+ event: "pointerup",
540
+ value: 1
541
+ },
542
+ {
543
+ event: "pointerup",
544
+ value: 2
545
+ },
546
+ {
547
+ event: "pointercancel",
548
+ value: 1
549
+ },
550
+ {
551
+ event: "pointercancel",
552
+ value: 2
553
+ },
554
+ {
555
+ event: "prop",
556
+ prop: "shift",
557
+ value: true
558
+ },
559
+ {
560
+ event: "prop",
561
+ prop: "shift",
562
+ value: false
563
+ },
564
+ {
565
+ event: "prop",
566
+ prop: "space",
567
+ value: true
568
+ },
569
+ {
570
+ event: "prop",
571
+ prop: "space",
572
+ value: false
573
+ },
574
+ {
575
+ event: "prop",
576
+ prop: "snap",
577
+ value: true
578
+ },
579
+ {
580
+ event: "prop",
581
+ prop: "snap",
582
+ value: false
583
+ },
584
+ {
585
+ event: "prop",
586
+ prop: "marquee",
587
+ value: "contain"
588
+ },
589
+ {
590
+ event: "prop",
591
+ prop: "marquee",
592
+ value: "overlap"
593
+ },
594
+ ...ZOOM_LEVELS.map((value) => ({
595
+ event: "prop",
596
+ prop: "zoom",
597
+ value
598
+ })),
599
+ {
600
+ event: "prop",
601
+ prop: "edit"
602
+ },
603
+ ...NODES.flatMap((_, node) => HOST_POSITIONS.map(([x, y]) => ({
604
+ event: "prop",
605
+ prop: "place",
606
+ target: `node${node}`,
607
+ value: [x, y]
608
+ })))
609
+ ];
610
+ const pointerProbe = {
611
+ attrs: [
612
+ ["layer", "--canvas-x"],
613
+ ["layer", "--canvas-y"],
614
+ ["layer", "--canvas-zoom"],
615
+ ["state", "mode"],
616
+ ["state", "owner"],
617
+ ["state", "selection"],
618
+ ["state", "cursor"],
619
+ ["state", "editing"],
620
+ ["state", "guides"],
621
+ ["state", "culled"],
622
+ ["state", "announcements"],
623
+ ["live", "text"],
624
+ ...NODES.flatMap((_, index) => [
625
+ [`node${index}`, "aria-selected"],
626
+ [`node${index}`, "--node-x"],
627
+ [`node${index}`, "--node-y"],
628
+ [`node${index}`, "content-visibility"]
629
+ ])
630
+ ],
631
+ events: ["onSelectionChange", "onNodesMove"]
632
+ };
633
+ const anchorSteps = ANCHORS.map((anchor) => anchor);
634
+ /** The user-level steps a camera differential walk draws from. */
635
+ const cameraSteps = [
636
+ ...VIEWPORTS.map((_, value) => ({
637
+ event: "prop",
638
+ prop: "resize",
639
+ value
640
+ })),
641
+ {
642
+ event: "prop",
643
+ prop: "motion",
644
+ value: true
645
+ },
646
+ {
647
+ event: "prop",
648
+ prop: "motion",
649
+ value: false
650
+ },
651
+ ...PAN_DELTAS.flatMap((dx) => [
652
+ -400,
653
+ 5,
654
+ 9e6
655
+ ].map((dy) => ({
656
+ event: "prop",
657
+ prop: "pan",
658
+ value: [dx, dy]
659
+ }))),
660
+ ...ZOOM_FACTORS.flatMap((factor) => anchorSteps.map((anchor) => ({
661
+ event: "prop",
662
+ prop: "zoom",
663
+ value: {
664
+ factor,
665
+ anchor
666
+ }
667
+ }))),
668
+ ...anchorSteps.flatMap((anchor) => [
669
+ {
670
+ event: "prop",
671
+ prop: "zoomLimit",
672
+ value: {
673
+ limit: "min",
674
+ anchor
675
+ }
676
+ },
677
+ {
678
+ event: "prop",
679
+ prop: "zoomLimit",
680
+ value: {
681
+ limit: "max",
682
+ anchor
683
+ }
684
+ },
685
+ {
686
+ event: "prop",
687
+ prop: "tween",
688
+ value: {
689
+ zoom: "100%",
690
+ anchor
691
+ }
692
+ },
693
+ {
694
+ event: "prop",
695
+ prop: "tween",
696
+ value: {
697
+ zoom: "max",
698
+ anchor
699
+ }
700
+ }
701
+ ]),
702
+ ...CENTRE_POINTS.map(([x, y]) => ({
703
+ event: "prop",
704
+ prop: "tween",
705
+ value: { centre: [x, y] }
706
+ })),
707
+ ...Array.from({ length: 12 }, () => ({
708
+ event: "prop",
709
+ prop: "tick"
710
+ })),
711
+ {
712
+ event: "prop",
713
+ prop: "cancelTween"
714
+ }
715
+ ];
716
+ const cameraProbe = {
717
+ attrs: [
718
+ ["layer", "--canvas-x"],
719
+ ["layer", "--canvas-y"],
720
+ ["layer", "--canvas-zoom"],
721
+ ["state", "camera"],
722
+ ["state", "viewport"],
723
+ ["state", "tweening"],
724
+ ["state", "reducedMotion"]
725
+ ],
726
+ events: []
727
+ };
728
+ //#endregion
729
+ export { ANCHORS, CENTRE_POINTS, HOST_POSITIONS, NODES, PALETTE, PAN_DELTAS, POINTER_VIEWPORT, VIEWPORTS, ZOOM_FACTORS, ZOOM_LEVELS, anchorPoint, cameraBridge, cameraConfig, cameraProbe, cameraSteps, cameraValue, formatValue, parseCameraOp, parsePointerOp, pointerBridge, pointerConfig, pointerProbe, pointerSteps, pointerValue, selectionText };