@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,441 @@
1
+ // WAI-ARIA APG — Slider (Multi-Thumb) pattern, two thumbs sharing one rail.
2
+ //
3
+ // Modelled (https://www.w3.org/WAI/ARIA/apg/patterns/slider-multithumb/ and
4
+ // its Horizontal Multi-Thumb Slider example, a hotel price range):
5
+ // - "About This Pattern": the thumbs may not pass one another, so the maximum
6
+ // of the lower thumb is the value of the upper thumb and the minimum of the
7
+ // upper thumb is the value of the lower thumb (equal values are allowed).
8
+ // - "Keyboard Interaction": each thumb is in the tab sequence and has the
9
+ // Slider pattern keys (Right/Up +1, Left/Down -1, PageUp/PageDown +/- large
10
+ // step, Home/End -> the first/last value the thumb may take); the tab
11
+ // order is constant (lo, then hi) whatever the values are.
12
+ // Home/End therefore stop at the neighbour: Home on the upper thumb sets it
13
+ // to the lower thumb's value (its current aria-valuemin), End on the lower
14
+ // thumb sets it to the upper thumb's value.
15
+ // - "WAI-ARIA Roles, States, and Properties": per thumb role=slider,
16
+ // aria-valuenow / valuemin / valuemax (the dependent bound follows the
17
+ // neighbour on every change), aria-valuetext, aria-orientation.
18
+ // - The RTL rule of the Slider spec (APG "Note 2"): horizontal + dir=rtl flips
19
+ // Left/Right; Up/Down never flip; vertical keeps Left/Right as in LTR.
20
+ //
21
+ // Request vs commit (map ticket rt1p). A key or a rail press is a *request*
22
+ // carrying an intent (the pair packed into `requested`); it fires onValueChange
23
+ // (`requests` counts the calls) when the intent differs from the current pair,
24
+ // and then:
25
+ // uncontrolled the request is accepted: the thumbs commit the intent
26
+ // controlled, holds the host ignores it ("rejected"): the thumbs are untouched
27
+ // controlled, accepts the host answers with the same value (`accepting`): the
28
+ // thumbs commit through the controlled value
29
+ // `valueProp` (the pair packed with `snap`) is the only thing that changes the
30
+ // thumbs while `controlled`; `setValue` is the host writing it. `control` /
31
+ // `acceptRequests` are configuration events (init is literal-only).
32
+ //
33
+ // Deliberate omissions (ponytail: ceilings, not bugs):
34
+ // - The independent-thumbs variant ("in some multi-thumb sliders, each thumb
35
+ // sets a value that does not depend on the other thumb values"): the spec
36
+ // fixes the linked variant, so the "tab order unchanged after passing"
37
+ // case cannot occur here. A Solid implementation of the independent variant
38
+ // should use the plain slider spec once per thumb.
39
+ // - Exactly two thumbs. The bound rules generalise to N thumbs as
40
+ // min = previous thumb, max = next thumb; Quint (and the generator) have
41
+ // no list state, so N is not expressible. The controlled pair is packed
42
+ // into one int for the same reason.
43
+ // - aria-label / aria-labelledby, non-integer values, a coarser step grid,
44
+ // dragging (only a discrete pointer press per thumb, clamped to its
45
+ // bounds), disabled, and aria-valuetext formatting (units, locale).
46
+ // - Picking the nearest thumb for a pointer press on the rail: the event
47
+ // names the thumb.
48
+ module apg_slider_multithumb {
49
+ import aria.* from "../../shared/aria"
50
+
51
+ var lo: int // lower thumb value
52
+ var hi: int // upper thumb value
53
+ var focused: str // "none" | "lo" | "hi"
54
+ var orientation: str // "horizontal" | "vertical"
55
+ var dir: str // "ltr" | "rtl"
56
+ var handled: bool // the last key was consumed (preventDefault)
57
+ var controlled: bool // the host owns the pair (`values` prop present)
58
+ var accepting: bool // controlled host that answers every request with its intent
59
+ var valueProp: int // the controlled pair, packed with `snap`
60
+ var requested: int // intent of the last request, packed
61
+ var outcome: str // "none" | "accepted" | "rejected"
62
+ var base: int // pair when the last request was received, packed
63
+ var requests: int // number of requests that fired onValueChange
64
+
65
+ // --- configuration -----------------------------------------------------
66
+ def rangeMin: int = 0
67
+ def rangeMax: int = 10
68
+ def smallStep: int = 1
69
+ def largeStep: int = 3
70
+
71
+ // --- pure behaviour ----------------------------------------------------
72
+ def clampTo(v: int, a: int, b: int): int =
73
+ if (v < a) a else if (v > b) b else v
74
+
75
+ def valueOf(t: str): int = if (t == "lo") lo else hi
76
+ // the dependent bounds: each thumb is limited by its neighbour
77
+ def minOf(t: str): int = if (t == "lo") rangeMin else lo
78
+ def maxOf(t: str): int = if (t == "lo") hi else rangeMax
79
+
80
+ // arrows and RTL mirroring: shared/aria.qnt ariaValueArrow
81
+ def keyDelta(k: str, o: str, d: str): int =
82
+ if (k == "PageUp") largeStep
83
+ else if (k == "PageDown") -largeStep
84
+ else smallStep * ariaValueArrow(k, o, d)
85
+
86
+ def isHandledKey(k: str): bool =
87
+ or(k == "Home", k == "End", keyDelta(k, "horizontal", "ltr") != 0)
88
+
89
+ // the new value of thumb t after key k (t's own bounds, not the full range)
90
+ def nextOf(t: str, k: str, o: str, d: str): int =
91
+ if (k == "Home") minOf(t)
92
+ else if (k == "End") maxOf(t)
93
+ else clampTo(valueOf(t) + keyDelta(k, o, d), minOf(t), maxOf(t))
94
+
95
+ def valueTextFor(v: int): str =
96
+ if (v <= 3) "low" else if (v <= 7) "medium" else "high"
97
+
98
+ // Tab / Shift+Tab walk the thumbs in a fixed order, independent of values
99
+ def tabNext(f: str): str =
100
+ if (f == "none") "lo" else if (f == "lo") "hi" else "none"
101
+ def tabPrev(f: str): str =
102
+ if (f == "none") "hi" else if (f == "hi") "lo" else "none"
103
+
104
+ // --- request / commit (map ticket rt1p) ----------------------------------
105
+ // injective for the 0..10 grid: lo * 11 + hi
106
+ def snap(l: int, h: int): int = l * 11 + h
107
+ def loOf(n: int): int = n / 11
108
+ def hiOf(n: int): int = n % 11
109
+ def nowPacked: int = snap(lo, hi)
110
+ // controlled host that keeps the old value: the request is rejected
111
+ def holds: bool = and(controlled, not(accepting))
112
+ // controlled host that writes the intent back through the value prop
113
+ def follows: bool = and(controlled, accepting)
114
+ def verdict: str = if (holds) "rejected" else "accepted"
115
+ def landed(h: bool, current: int, want: int): int = if (h) current else want
116
+ def propAfter(f: bool, p: int, want: int): int = if (f) want else p
117
+ // a key moves only the focused thumb; the other stays put
118
+ def nextLoFor(k: str, o: str, d: str): int =
119
+ if (focused == "lo") nextOf("lo", k, o, d) else lo
120
+ def nextHiFor(k: str, o: str, d: str): int =
121
+ if (focused == "hi") nextOf("hi", k, o, d) else hi
122
+ def keyIntent(k: str, o: str, d: str): int =
123
+ snap(nextLoFor(k, o, d), nextHiFor(k, o, d))
124
+ def keyMoves(k: str, o: str, d: str): bool = keyIntent(k, o, d) != nowPacked
125
+ def pointerLoValue(raw: int): int = clampTo(raw, minOf("lo"), maxOf("lo"))
126
+ def pointerHiValue(raw: int): int = clampTo(raw, minOf("hi"), maxOf("hi"))
127
+
128
+ // --- ARIA contract, one slider per thumb (derived from the state) --------
129
+ def ariaRole: str = "slider"
130
+ def ariaValueNowLo: int = lo
131
+ def ariaValueMinLo: int = minOf("lo")
132
+ def ariaValueMaxLo: int = maxOf("lo")
133
+ def ariaValueTextLo: str = valueTextFor(lo)
134
+ def ariaValueNowHi: int = hi
135
+ def ariaValueMinHi: int = minOf("hi")
136
+ def ariaValueMaxHi: int = maxOf("hi")
137
+ def ariaValueTextHi: str = valueTextFor(hi)
138
+ def ariaOrientation: str = orientation
139
+ def tabIndexLo: int = 0
140
+ def tabIndexHi: int = 0
141
+
142
+ // --- machine -----------------------------------------------------------
143
+ action init = all {
144
+ lo' = 3,
145
+ hi' = 7,
146
+ focused' = "none",
147
+ orientation' = "horizontal",
148
+ dir' = "ltr",
149
+ handled' = false,
150
+ controlled' = false,
151
+ accepting' = false,
152
+ valueProp' = 40,
153
+ requested' = 40,
154
+ outcome' = "none",
155
+ base' = 40,
156
+ requests' = 0
157
+ }
158
+
159
+ // click / programmatic focus on a thumb
160
+ action focusThumb = {
161
+ nondet t = Set("lo", "hi").oneOf()
162
+ all {
163
+ lo' = lo,
164
+ hi' = hi,
165
+ focused' = t,
166
+ orientation' = orientation,
167
+ dir' = dir,
168
+ handled' = false,
169
+ controlled' = controlled,
170
+ accepting' = accepting,
171
+ valueProp' = valueProp,
172
+ requested' = requested,
173
+ outcome' = outcome,
174
+ base' = base,
175
+ requests' = requests
176
+ }
177
+ }
178
+
179
+ action blur = all {
180
+ lo' = lo,
181
+ hi' = hi,
182
+ focused' = "none",
183
+ orientation' = orientation,
184
+ dir' = dir,
185
+ handled' = false,
186
+ controlled' = controlled,
187
+ accepting' = accepting,
188
+ valueProp' = valueProp,
189
+ requested' = requested,
190
+ outcome' = outcome,
191
+ base' = base,
192
+ requests' = requests
193
+ }
194
+
195
+ action tab = all {
196
+ lo' = lo,
197
+ hi' = hi,
198
+ focused' = tabNext(focused),
199
+ orientation' = orientation,
200
+ dir' = dir,
201
+ handled' = false,
202
+ controlled' = controlled,
203
+ accepting' = accepting,
204
+ valueProp' = valueProp,
205
+ requested' = requested,
206
+ outcome' = outcome,
207
+ base' = base,
208
+ requests' = requests
209
+ }
210
+
211
+ action shiftTab = all {
212
+ lo' = lo,
213
+ hi' = hi,
214
+ focused' = tabPrev(focused),
215
+ orientation' = orientation,
216
+ dir' = dir,
217
+ handled' = false,
218
+ controlled' = controlled,
219
+ accepting' = accepting,
220
+ valueProp' = valueProp,
221
+ requested' = requested,
222
+ outcome' = outcome,
223
+ base' = base,
224
+ requests' = requests
225
+ }
226
+
227
+ // request: a focused key moves its thumb (APG keyboard section)
228
+ action keydown = {
229
+ nondet key = Set(
230
+ "ArrowRight", "ArrowLeft", "ArrowUp", "ArrowDown",
231
+ "Home", "End", "PageUp", "PageDown", "Enter"
232
+ ).oneOf()
233
+ all {
234
+ lo' = landed(holds, lo, nextLoFor(key, orientation, dir)),
235
+ hi' = landed(holds, hi, nextHiFor(key, orientation, dir)),
236
+ focused' = focused,
237
+ orientation' = orientation,
238
+ dir' = dir,
239
+ handled' = and(focused != "none", isHandledKey(key)),
240
+ controlled' = controlled,
241
+ accepting' = accepting,
242
+ valueProp' = propAfter(follows, valueProp, keyIntent(key, orientation, dir)),
243
+ requested' = if (keyMoves(key, orientation, dir)) keyIntent(key, orientation, dir) else requested,
244
+ outcome' = if (keyMoves(key, orientation, dir)) verdict else outcome,
245
+ base' = if (keyMoves(key, orientation, dir)) nowPacked else base,
246
+ requests' = if (keyMoves(key, orientation, dir)) requests + 1 else requests
247
+ }
248
+ }
249
+
250
+ // request: pointer press for the lower thumb at a raw value; focuses it
251
+ action pointerLo = {
252
+ nondet raw = Set(-3, 0, 4, 7, 10, 14).oneOf()
253
+ all {
254
+ lo' = landed(holds, lo, pointerLoValue(raw)),
255
+ hi' = hi,
256
+ focused' = "lo",
257
+ orientation' = orientation,
258
+ dir' = dir,
259
+ handled' = false,
260
+ controlled' = controlled,
261
+ accepting' = accepting,
262
+ valueProp' = propAfter(follows, valueProp, snap(pointerLoValue(raw), hi)),
263
+ requested' = if (snap(pointerLoValue(raw), hi) != nowPacked) snap(pointerLoValue(raw), hi) else requested,
264
+ outcome' = if (snap(pointerLoValue(raw), hi) != nowPacked) verdict else outcome,
265
+ base' = if (snap(pointerLoValue(raw), hi) != nowPacked) nowPacked else base,
266
+ requests' = if (snap(pointerLoValue(raw), hi) != nowPacked) requests + 1 else requests
267
+ }
268
+ }
269
+
270
+ // request: pointer press for the upper thumb at a raw value; focuses it
271
+ action pointerHi = {
272
+ nondet raw = Set(-3, 0, 4, 7, 10, 14).oneOf()
273
+ all {
274
+ lo' = lo,
275
+ hi' = landed(holds, hi, pointerHiValue(raw)),
276
+ focused' = "hi",
277
+ orientation' = orientation,
278
+ dir' = dir,
279
+ handled' = false,
280
+ controlled' = controlled,
281
+ accepting' = accepting,
282
+ valueProp' = propAfter(follows, valueProp, snap(lo, pointerHiValue(raw))),
283
+ requested' = if (snap(lo, pointerHiValue(raw)) != nowPacked) snap(lo, pointerHiValue(raw)) else requested,
284
+ outcome' = if (snap(lo, pointerHiValue(raw)) != nowPacked) verdict else outcome,
285
+ base' = if (snap(lo, pointerHiValue(raw)) != nowPacked) nowPacked else base,
286
+ requests' = if (snap(lo, pointerHiValue(raw)) != nowPacked) requests + 1 else requests
287
+ }
288
+ }
289
+
290
+ action setOrientation = {
291
+ nondet o = Set("horizontal", "vertical").oneOf()
292
+ all {
293
+ lo' = lo,
294
+ hi' = hi,
295
+ focused' = focused,
296
+ orientation' = o,
297
+ dir' = dir,
298
+ handled' = false,
299
+ controlled' = controlled,
300
+ accepting' = accepting,
301
+ valueProp' = valueProp,
302
+ requested' = requested,
303
+ outcome' = outcome,
304
+ base' = base,
305
+ requests' = requests
306
+ }
307
+ }
308
+
309
+ action setDir = {
310
+ nondet d = Set("ltr", "rtl").oneOf()
311
+ all {
312
+ lo' = lo,
313
+ hi' = hi,
314
+ focused' = focused,
315
+ orientation' = orientation,
316
+ dir' = d,
317
+ handled' = false,
318
+ controlled' = controlled,
319
+ accepting' = accepting,
320
+ valueProp' = valueProp,
321
+ requested' = requested,
322
+ outcome' = outcome,
323
+ base' = base,
324
+ requests' = requests
325
+ }
326
+ }
327
+
328
+ // commit: the host writes the controlled pair (a no-op while uncontrolled)
329
+ action setValue = {
330
+ nondet v = Set(0, 10, 27, 40, 60, 120).oneOf()
331
+ all {
332
+ lo' = if (controlled) loOf(v) else lo,
333
+ hi' = if (controlled) hiOf(v) else hi,
334
+ focused' = focused,
335
+ orientation' = orientation,
336
+ dir' = dir,
337
+ handled' = false,
338
+ controlled' = controlled,
339
+ accepting' = accepting,
340
+ valueProp' = if (controlled) v else valueProp,
341
+ requested' = requested,
342
+ outcome' = if (controlled) "none" else outcome,
343
+ base' = base,
344
+ requests' = requests
345
+ }
346
+ }
347
+
348
+ // configuration: the host starts owning the pair, adopting the current one
349
+ action control = all {
350
+ lo' = lo,
351
+ hi' = hi,
352
+ focused' = focused,
353
+ orientation' = orientation,
354
+ dir' = dir,
355
+ handled' = false,
356
+ controlled' = true,
357
+ accepting' = accepting,
358
+ valueProp' = nowPacked,
359
+ requested' = requested,
360
+ outcome' = outcome,
361
+ base' = base,
362
+ requests' = requests
363
+ }
364
+
365
+ // configuration: the controlled host answers every request with its intent
366
+ action acceptRequests = all {
367
+ lo' = lo,
368
+ hi' = hi,
369
+ focused' = focused,
370
+ orientation' = orientation,
371
+ dir' = dir,
372
+ handled' = false,
373
+ controlled' = controlled,
374
+ accepting' = true,
375
+ valueProp' = valueProp,
376
+ requested' = requested,
377
+ outcome' = outcome,
378
+ base' = base,
379
+ requests' = requests
380
+ }
381
+
382
+ action step = any {
383
+ init, focusThumb, blur, tab, shiftTab, keydown,
384
+ pointerLo, pointerHi, setOrientation, setDir,
385
+ setValue, control, acceptRequests
386
+ }
387
+
388
+ // --- invariants --------------------------------------------------------
389
+ // min <= lo <= hi <= max: the thumbs are ordered and inside the range
390
+ val ThumbsOrdered: bool =
391
+ and(rangeMin <= lo, lo <= hi, hi <= rangeMax)
392
+ // aria-valuemax of the lower thumb is the upper thumb's value, and
393
+ // aria-valuemin of the upper thumb is the lower thumb's value
394
+ val BoundsFollowNeighbour: bool =
395
+ and(ariaValueMaxLo == hi, ariaValueMinHi == lo)
396
+ // the outer bounds are the full range
397
+ val OuterBoundsAreRange: bool =
398
+ and(ariaValueMinLo == rangeMin, ariaValueMaxHi == rangeMax)
399
+ // every thumb's aria-valuenow lies inside its own [valuemin, valuemax]
400
+ val NowWithinOwnBounds: bool = and(
401
+ ariaValueMinLo <= ariaValueNowLo,
402
+ ariaValueNowLo <= ariaValueMaxLo,
403
+ ariaValueMinHi <= ariaValueNowHi,
404
+ ariaValueNowHi <= ariaValueMaxHi
405
+ )
406
+ // every reachable value is on the step grid
407
+ val ValuesOnStepGrid: bool = and(
408
+ (lo - rangeMin) % smallStep == 0,
409
+ (hi - rangeMin) % smallStep == 0
410
+ )
411
+ // aria-valuetext is present for each thumb and orders with the values
412
+ val ValueTextDerived: bool = and(
413
+ ariaValueTextLo != "",
414
+ ariaValueTextHi != "",
415
+ or(lo != rangeMin, ariaValueTextLo == "low"),
416
+ or(hi != rangeMax, ariaValueTextHi == "high")
417
+ )
418
+ val OrientationAndDirValid: bool = and(
419
+ ariaOrientationKnown(ariaOrientation),
420
+ or(dir == "ltr", dir == "rtl")
421
+ )
422
+ val FocusValid: bool = or(focused == "none", focused == "lo", focused == "hi")
423
+ // a key is only consumed while a thumb has focus
424
+ val HandledImpliesFocused: bool = or(not(handled), focused != "none")
425
+ // both thumbs are in the tab sequence and the order never depends on values
426
+ val TabOrderConstant: bool = and(
427
+ ariaRole == "slider",
428
+ tabIndexLo == 0,
429
+ tabIndexHi == 0,
430
+ tabNext("none") == "lo",
431
+ tabNext("lo") == "hi"
432
+ )
433
+ // callbacks only ever count up from zero
434
+ val RequestsNonNegative: bool = requests >= 0
435
+ // controlled: only the value prop sets the pair, so no event commits
436
+ val ControlledFollowsProp: bool = or(not(controlled), nowPacked == valueProp)
437
+ // a rejected request leaves the pair as it found it
438
+ val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), nowPacked == base)
439
+ // an accepted request committed exactly its intent
440
+ val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), nowPacked == requested)
441
+ }