@cssints/apg-specs 0.0.0-stage → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,396 @@
1
+ // WAI-ARIA APG — Slider pattern (single thumb).
2
+ //
3
+ // Modelled (https://www.w3.org/WAI/ARIA/apg/patterns/slider/):
4
+ // - "Keyboard Interaction": Right/Up Arrow +1 step, Left/Down Arrow -1 step,
5
+ // Home -> first allowed value, End -> last allowed value, PageUp/PageDown
6
+ // (optional) +/- a large step. Keys act only while the thumb has focus
7
+ // ("Focus is placed on the slider ... also known as the thumb").
8
+ // - "Note 2": reversing the arrow direction. Modelled as the RTL rule: in a
9
+ // horizontal slider under dir=rtl, Left Arrow increases and Right Arrow
10
+ // decreases; Up/Down never flip. In a vertical slider Left/Right keep their
11
+ // LTR meaning (APG lists all four arrows for the vertical temperature
12
+ // example), so orientation only changes the RTL rule and aria-orientation.
13
+ // - "WAI-ARIA Roles, States, and Properties": role=slider, aria-valuenow,
14
+ // aria-valuemin, aria-valuemax, aria-valuetext, aria-orientation
15
+ // (default horizontal), as derived defs of the state.
16
+ // - Examples (slider-color-viewer, slider-temperature, slider-rating,
17
+ // slider-seek): the range 0..10 stands in for all four; `valueText` is the
18
+ // aria-valuetext hook (a low/medium/high band, like the rating example's
19
+ // "Very Dissatisfied ... Very Satisfied").
20
+ // - Configuration variants (bean qtmy): `configure` remounts the widget with
21
+ // a packed variant int, because init is literal-only and the generator
22
+ // allows one nondet per action. Variant 0 is step 1 / large step 3 / value
23
+ // 5; variant 1 is step 2 / large step 4 / value 6 (large step on the grid,
24
+ // so a page jump never leaves it). The range stays 0..10 so one walk
25
+ // alphabet is valid for every variant and a step that does not divide the
26
+ // range is still a deliberate omission.
27
+ //
28
+ // Request vs commit (map ticket rt1p). A key or a rail press is a *request*
29
+ // carrying an intent (`requested`); it fires onValueChange (`requests` counts
30
+ // the calls) when the intent differs from the current value, and then:
31
+ // uncontrolled the request is accepted: the value commits the intent
32
+ // controlled, holds the host ignores it ("rejected"): the value is untouched
33
+ // controlled, accepts the host answers with the same value (`accepting`): the
34
+ // value commits through the controlled prop, not the event
35
+ // `valueProp` is the only thing that changes the value while `controlled`;
36
+ // `setValue` / `hostToggle` are the host writing it. `control` / `acceptRequests`
37
+ // are configuration events (init is literal-only). Invariants for the model:
38
+ // ControlledFollowsProp (no event commits while controlled),
39
+ // RejectedRequestUntouched, AcceptedRequestCommits.
40
+ //
41
+ // Deliberate omissions (ponytail: ceilings, not bugs):
42
+ // - aria-label / aria-labelledby (labelling is a rendering concern).
43
+ // - Non-integer values and a step that does not divide the range: the range
44
+ // is 0..10 and every variant's step divides it, so `End` and a pointer
45
+ // press always land on the grid.
46
+ // - Dragging (pointer capture, move events): only a discrete pointer
47
+ // "press at value" is modelled, snapped to the step grid and clamped.
48
+ // - Disabled / read-only, touch assistive-technology key synthesis, and
49
+ // aria-valuetext formatting (units, locale): a real widget formats the text
50
+ // from its own value; the spec only fixes WHEN the text is derived (always,
51
+ // from valuenow).
52
+ // - The range (0..10) is fixed; `configure` varies step, large step and the
53
+ // starting value. The bridge rejects a config outside the variant table.
54
+ module apg_slider {
55
+ import aria.* from "../../shared/aria"
56
+
57
+ var flavor: int // configuration variant (0 | 1), see `configure`
58
+ var value: int // the current value (aria-valuenow)
59
+ var focused: bool // the thumb has keyboard focus
60
+ var orientation: str // "horizontal" | "vertical"
61
+ var dir: str // "ltr" | "rtl"
62
+ var handled: bool // the last key was consumed (preventDefault)
63
+ var controlled: bool // the host owns the value (`value` prop present)
64
+ var accepting: bool // controlled host that answers every request with its intent
65
+ var valueProp: int // the controlled value prop (meaningful while controlled)
66
+ var requested: int // intent of the last request
67
+ var outcome: str // "none" | "accepted" | "rejected"
68
+ var base: int // value when the last request was received
69
+ var requests: int // number of requests that fired onValueChange
70
+
71
+ // --- configuration -----------------------------------------------------
72
+ def rangeMin: int = 0
73
+ def rangeMax: int = 10
74
+ // packed variant (0 | 1); see the header and `configure`
75
+ def variantSmallStep(v: int): int = if (v == 1) 2 else 1
76
+ def variantLargeStep(v: int): int = if (v == 1) 4 else 3
77
+ def variantStart(v: int): int = if (v == 1) 6 else 5
78
+ def smallStep: int = variantSmallStep(flavor)
79
+ def largeStep: int = variantLargeStep(flavor)
80
+
81
+ // --- pure behaviour ----------------------------------------------------
82
+ def clamp(v: int): int =
83
+ if (v < rangeMin) rangeMin else if (v > rangeMax) rangeMax else v
84
+
85
+ // a raw pointer position lands on the nearest step (the widget corrects an
86
+ // off-grid press; APG's Home/End name "first/last allowed value")
87
+ def snap(v: int): int =
88
+ rangeMin + ((v - rangeMin + smallStep / 2) / smallStep) * smallStep
89
+
90
+ def pointerValue(raw: int): int = clamp(snap(raw))
91
+
92
+ // signed change for a key; 0 for keys that do not change by a delta
93
+ // (arrows and RTL mirroring: shared/aria.qnt ariaValueArrow)
94
+ def keyDelta(k: str, o: str, d: str): int =
95
+ if (k == "PageUp") largeStep
96
+ else if (k == "PageDown") -largeStep
97
+ else smallStep * ariaValueArrow(k, o, d)
98
+
99
+ def isHandledKey(k: str): bool =
100
+ or(k == "Home", k == "End", keyDelta(k, "horizontal", "ltr") != 0)
101
+
102
+ def nextValue(v: int, k: str, o: str, d: str): int =
103
+ if (k == "Home") rangeMin
104
+ else if (k == "End") rangeMax
105
+ else clamp(v + keyDelta(k, o, d))
106
+
107
+ def valueTextFor(v: int): str =
108
+ if (v <= 3) "low" else if (v <= 7) "medium" else "high"
109
+
110
+ // --- request / commit (map ticket rt1p) ----------------------------------
111
+ def moved(before: int, after: int): int = if (before == after) 0 else 1
112
+ // a focused key that would land somewhere else is a request; an inert key
113
+ // (delta 0, or a Home/End already at the bound) is not
114
+ def wants(f: bool, current: int, want: int): bool = and(f, want != current)
115
+ def willRequest(f: bool, k: str, v: int, o: str, d: str): bool =
116
+ wants(f, v, nextValue(v, k, o, d))
117
+ // controlled host that keeps the old value: the request is rejected
118
+ def holds: bool = and(controlled, not(accepting))
119
+ // controlled host that writes the intent back through the value prop
120
+ def follows: bool = and(controlled, accepting)
121
+ def verdict: str = if (holds) "rejected" else "accepted"
122
+ // the value after a request for `want`
123
+ def landed(h: bool, current: int, want: int): int = if (h) current else want
124
+ // the value prop after a request for `want`
125
+ def propAfter(f: bool, p: int, want: int): int = if (f) want else p
126
+
127
+ // --- ARIA contract (derived from the state) -----------------------------
128
+ def ariaRole: str = "slider"
129
+ def ariaValueNow: int = value
130
+ def ariaValueMin: int = rangeMin
131
+ def ariaValueMax: int = rangeMax
132
+ def ariaValueText: str = valueTextFor(value)
133
+ def ariaOrientation: str = orientation
134
+ def tabIndex: int = 0
135
+
136
+ // --- machine -----------------------------------------------------------
137
+ action init = all {
138
+ flavor' = 0,
139
+ value' = 5,
140
+ focused' = false,
141
+ orientation' = "horizontal",
142
+ dir' = "ltr",
143
+ handled' = false,
144
+ controlled' = false,
145
+ accepting' = false,
146
+ valueProp' = 5,
147
+ requested' = 5,
148
+ outcome' = "none",
149
+ base' = 5,
150
+ requests' = 0
151
+ }
152
+
153
+ action focus = all {
154
+ flavor' = flavor,
155
+ value' = value,
156
+ focused' = true,
157
+ orientation' = orientation,
158
+ dir' = dir,
159
+ handled' = false,
160
+ controlled' = controlled,
161
+ accepting' = accepting,
162
+ valueProp' = valueProp,
163
+ requested' = requested,
164
+ outcome' = outcome,
165
+ base' = base,
166
+ requests' = requests
167
+ }
168
+
169
+ action blur = all {
170
+ flavor' = flavor,
171
+ value' = value,
172
+ focused' = false,
173
+ orientation' = orientation,
174
+ dir' = dir,
175
+ handled' = false,
176
+ controlled' = controlled,
177
+ accepting' = accepting,
178
+ valueProp' = valueProp,
179
+ requested' = requested,
180
+ outcome' = outcome,
181
+ base' = base,
182
+ requests' = requests
183
+ }
184
+
185
+ // request: a focused key moves the value (APG keyboard section)
186
+ action keydown = {
187
+ nondet key = Set(
188
+ "ArrowRight", "ArrowLeft", "ArrowUp", "ArrowDown",
189
+ "Home", "End", "PageUp", "PageDown", "Enter"
190
+ ).oneOf()
191
+ all {
192
+ flavor' = flavor,
193
+ value' = if (and(focused, isHandledKey(key))) landed(holds, value, nextValue(value, key, orientation, dir)) else value,
194
+ focused' = focused,
195
+ orientation' = orientation,
196
+ dir' = dir,
197
+ handled' = and(focused, isHandledKey(key)),
198
+ controlled' = controlled,
199
+ accepting' = accepting,
200
+ valueProp' = if (willRequest(focused, key, value, orientation, dir)) propAfter(follows, valueProp, nextValue(value, key, orientation, dir)) else valueProp,
201
+ requested' = if (willRequest(focused, key, value, orientation, dir)) nextValue(value, key, orientation, dir) else requested,
202
+ outcome' = if (willRequest(focused, key, value, orientation, dir)) verdict else outcome,
203
+ base' = if (willRequest(focused, key, value, orientation, dir)) value else base,
204
+ requests' = if (willRequest(focused, key, value, orientation, dir)) requests + 1 else requests
205
+ }
206
+ }
207
+
208
+ // request: pointer press on the track at a raw value; focuses the thumb, clamps
209
+ action pointer = {
210
+ nondet raw = Set(-3, 0, 4, 7, 10, 14).oneOf()
211
+ all {
212
+ flavor' = flavor,
213
+ value' = landed(holds, value, pointerValue(raw)),
214
+ focused' = true,
215
+ orientation' = orientation,
216
+ dir' = dir,
217
+ handled' = false,
218
+ controlled' = controlled,
219
+ accepting' = accepting,
220
+ valueProp' = propAfter(follows, valueProp, pointerValue(raw)),
221
+ requested' = if (wants(true, value, pointerValue(raw))) pointerValue(raw) else requested,
222
+ outcome' = if (wants(true, value, pointerValue(raw))) verdict else outcome,
223
+ base' = if (wants(true, value, pointerValue(raw))) value else base,
224
+ requests' = if (wants(true, value, pointerValue(raw))) requests + 1 else requests
225
+ }
226
+ }
227
+
228
+ action setOrientation = {
229
+ nondet o = Set("horizontal", "vertical").oneOf()
230
+ all {
231
+ flavor' = flavor,
232
+ value' = value,
233
+ focused' = focused,
234
+ orientation' = o,
235
+ dir' = dir,
236
+ handled' = false,
237
+ controlled' = controlled,
238
+ accepting' = accepting,
239
+ valueProp' = valueProp,
240
+ requested' = requested,
241
+ outcome' = outcome,
242
+ base' = base,
243
+ requests' = requests
244
+ }
245
+ }
246
+
247
+ action setDir = {
248
+ nondet d = Set("ltr", "rtl").oneOf()
249
+ all {
250
+ flavor' = flavor,
251
+ value' = value,
252
+ focused' = focused,
253
+ orientation' = orientation,
254
+ dir' = d,
255
+ handled' = false,
256
+ controlled' = controlled,
257
+ accepting' = accepting,
258
+ valueProp' = valueProp,
259
+ requested' = requested,
260
+ outcome' = outcome,
261
+ base' = base,
262
+ requests' = requests
263
+ }
264
+ }
265
+
266
+ // commit: the host writes the controlled value (a no-op while uncontrolled)
267
+ // a host write is a value on the step grid (0..10 is even for both variants)
268
+ action setValue = {
269
+ nondet v = Set(0, 2, 4, 6, 8, 10).oneOf()
270
+ all {
271
+ flavor' = flavor,
272
+ value' = if (controlled) v else value,
273
+ focused' = focused,
274
+ orientation' = orientation,
275
+ dir' = dir,
276
+ handled' = false,
277
+ controlled' = controlled,
278
+ accepting' = accepting,
279
+ valueProp' = if (controlled) v else valueProp,
280
+ requested' = requested,
281
+ outcome' = if (controlled) "none" else outcome,
282
+ base' = base,
283
+ requests' = requests
284
+ }
285
+ }
286
+
287
+ // commit: the host flips its own copy of the value between the range ends
288
+ // (an outside two-state control)
289
+ action hostToggle = all {
290
+ flavor' = flavor,
291
+ value' = if (controlled) (if (valueProp == rangeMax) rangeMin else rangeMax) else value,
292
+ focused' = focused,
293
+ orientation' = orientation,
294
+ dir' = dir,
295
+ handled' = false,
296
+ controlled' = controlled,
297
+ accepting' = accepting,
298
+ valueProp' = if (controlled) (if (valueProp == rangeMax) rangeMin else rangeMax) else valueProp,
299
+ requested' = requested,
300
+ outcome' = if (controlled) "none" else outcome,
301
+ base' = base,
302
+ requests' = requests
303
+ }
304
+
305
+ // configuration: the host starts owning the value, adopting the current one
306
+ action control = all {
307
+ flavor' = flavor,
308
+ value' = value,
309
+ focused' = focused,
310
+ orientation' = orientation,
311
+ dir' = dir,
312
+ handled' = false,
313
+ controlled' = true,
314
+ accepting' = accepting,
315
+ valueProp' = value,
316
+ requested' = requested,
317
+ outcome' = outcome,
318
+ base' = base,
319
+ requests' = requests
320
+ }
321
+
322
+ // configuration: the controlled host answers every request with its intent
323
+ action acceptRequests = all {
324
+ flavor' = flavor,
325
+ value' = value,
326
+ focused' = focused,
327
+ orientation' = orientation,
328
+ dir' = dir,
329
+ handled' = false,
330
+ controlled' = controlled,
331
+ accepting' = true,
332
+ valueProp' = valueProp,
333
+ requested' = requested,
334
+ outcome' = outcome,
335
+ base' = base,
336
+ requests' = requests
337
+ }
338
+
339
+ // configuration: remount with a packed variant (step, large step, start).
340
+ // Resets the widget to uncontrolled and horizontal/ltr, like a fresh mount.
341
+ action configure = {
342
+ nondet pick = Set(0, 1).oneOf()
343
+ all {
344
+ flavor' = pick,
345
+ value' = variantStart(pick),
346
+ focused' = false,
347
+ orientation' = "horizontal",
348
+ dir' = "ltr",
349
+ handled' = false,
350
+ controlled' = false,
351
+ accepting' = false,
352
+ valueProp' = variantStart(pick),
353
+ requested' = variantStart(pick),
354
+ outcome' = "none",
355
+ base' = variantStart(pick),
356
+ requests' = 0
357
+ }
358
+ }
359
+
360
+ action step = any {
361
+ init, configure, focus, blur, keydown, pointer, setOrientation, setDir,
362
+ setValue, hostToggle, control, acceptRequests
363
+ }
364
+
365
+ // --- invariants --------------------------------------------------------
366
+ // min <= value <= max
367
+ val ValueInRange: bool = and(value >= rangeMin, value <= rangeMax)
368
+ // the exposed aria-valuenow sits inside the exposed [valuemin, valuemax]
369
+ val AriaNowWithinBounds: bool =
370
+ and(ariaValueMin <= ariaValueNow, ariaValueNow <= ariaValueMax)
371
+ // every reachable value is on the step grid
372
+ val ValueOnStepGrid: bool = (value - rangeMin) % smallStep == 0
373
+ // aria-valuetext is always present and the endpoints read as the extremes
374
+ val ValueTextDerived: bool = and(
375
+ ariaValueText != "",
376
+ or(value != rangeMin, ariaValueText == "low"),
377
+ or(value != rangeMax, ariaValueText == "high")
378
+ )
379
+ // aria-orientation is one of the two ARIA tokens; direction is ltr/rtl
380
+ val OrientationAndDirValid: bool = and(
381
+ ariaOrientationKnown(ariaOrientation),
382
+ or(dir == "ltr", dir == "rtl")
383
+ )
384
+ // a key is only consumed while the thumb has focus
385
+ val HandledImpliesFocused: bool = or(not(handled), focused)
386
+ // the slider thumb is always in the tab sequence
387
+ val InTabSequence: bool = and(ariaRole == "slider", tabIndex == 0)
388
+ // callbacks only ever count up from zero
389
+ val RequestsNonNegative: bool = requests >= 0
390
+ // controlled: only the value prop sets the state, so no event commits
391
+ val ControlledFollowsProp: bool = or(not(controlled), value == valueProp)
392
+ // a rejected request leaves the state as it found it
393
+ val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), value == base)
394
+ // an accepted request committed exactly its intent
395
+ val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), value == requested)
396
+ }