@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,388 @@
1
+ // WAI-ARIA APG — Checkbox pattern: the tri-state (mixed) group controller.
2
+ //
3
+ // Modelled from https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/ ("About This
4
+ // Pattern": one tri-state checkbox represents and controls a group of dual-state
5
+ // checkboxes; "Keyboard Interaction": Space changes the state; "Roles, States,
6
+ // and Properties": aria-checked true / false / mixed) and the Mixed-State example
7
+ // https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/examples/checkbox-mixed/
8
+ // (Tab moves among the checkboxes; Space on the controller sets the children; the
9
+ // controller has aria-controls = the children).
10
+ //
11
+ // The controller (focus target "parent") owns no state: its aria-checked is
12
+ // DERIVED from the three children c0..c2 —
13
+ // all checked -> "true", none checked -> "false", otherwise -> "mixed".
14
+ // Activating the controller:
15
+ // all checked -> uncheck all
16
+ // mixed -> check all
17
+ // none checked -> check all, or — when `remember` is on and a mixed
18
+ // combination was seen before — restore that last
19
+ // mixed combination ("activating the overall
20
+ // checkbox a third time recreates that partially
21
+ // checked state"). Cycle: mixed -> true -> false -> mixed.
22
+ // Activating a child toggles just that child.
23
+ //
24
+ // Request vs commit (map ticket rt1p). Activating a checkbox (click / Space) is
25
+ // a *request* carrying the intended checked set (`requested`, the three children
26
+ // packed with `packed`). It fires onValueChange (`requests` counts the calls)
27
+ // when the intent differs from the current value, and then:
28
+ // uncontrolled the request is accepted: the children commit the intent
29
+ // controlled, holds the host ignores it ("rejected"): the children are untouched
30
+ // controlled, accepts the host answers with the same value (`accepting`): the
31
+ // children commit through the controlled value
32
+ // The controlled value (`valueProp`, the three checked bits packed) is the only
33
+ // thing that changes the children while `controlled`; `setValue` / `hostToggle`
34
+ // are the host writing it. `control` and `acceptRequests` are configuration
35
+ // events (init is literal-only). The mixed memory (m0..m2) is internal derived
36
+ // state, not the controlled value, and tracks the combination being left.
37
+ //
38
+ // Configuration variants: `remember` (the optional last-mixed memory) is a prop.
39
+ // The group size is a constant 3 (bool vars — the generator has no collections),
40
+ // so the controlled value is packed and every action assigns every var.
41
+ //
42
+ // Deliberate omissions:
43
+ // - per-child disabled / the disabled controller (see checkbox.qnt for disabling)
44
+ // - Space armed-on-keydown / fire-on-keyup and pointer arming (see checkbox.qnt);
45
+ // here Space and click activate directly (a collapsed key/pointer press)
46
+ // - shift+Tab, accessible names, fieldset/legend or role=group labelling
47
+ // - the page-load "default combination" fallback: with no mixed memory the
48
+ // controller checks all
49
+ // - a variable number of children (generator subset: no lists/sets in state)
50
+ module apg_checkbox_group {
51
+ var c0: bool // child checkbox 0 checked
52
+ var c1: bool // child checkbox 1 checked
53
+ var c2: bool // child checkbox 2 checked
54
+ var m0: bool // last mixed combination seen, child 0 (all false = none yet)
55
+ var m1: bool // ... child 1
56
+ var m2: bool // ... child 2
57
+ var focus: str // "none" | "parent" | "c0" | "c1" | "c2"
58
+ var remember: bool // config: the controller restores the last mixed combination
59
+ var controlled: bool // the host owns the value (`value` prop present)
60
+ var accepting: bool // controlled host that answers every request with its intent
61
+ var valueProp: int // the controlled value prop, packed with `packed`
62
+ var requested: int // intent of the last request, packed
63
+ var outcome: str // "none" | "accepted" | "rejected"
64
+ var base: int // value when the last request was received, packed
65
+ var requests: int // number of requests that fired onValueChange
66
+
67
+ def isAll(a: bool, b: bool, c: bool): bool = and(a, b, c)
68
+ def isNone(a: bool, b: bool, c: bool): bool = not(or(a, b, c))
69
+ def isMixedSet(a: bool, b: bool, c: bool): bool =
70
+ and(not(isAll(a, b, c)), not(isNone(a, b, c)))
71
+
72
+ // ARIA attribute contract of the controller (aria-controls = c0 c1 c2, constant)
73
+ def parentChecked(a: bool, b: bool, c: bool): str =
74
+ if (isAll(a, b, c)) "true" else if (isNone(a, b, c)) "false" else "mixed"
75
+
76
+ // the third activation: none checked, memory on, and a mixed combination known
77
+ def restores(a: bool, b: bool, c: bool, ma: bool, mb: bool, mc: bool, remember: bool): bool =
78
+ and(remember, isNone(a, b, c), isMixedSet(ma, mb, mc))
79
+
80
+ // one child's next value when `target` is activated
81
+ def nextChild(name: str, ci: bool, mi: bool, target: str, allChecked: bool, restore: bool): bool =
82
+ if (target == name) not(ci)
83
+ else if (target == "parent") (if (allChecked) false else if (restore) mi else true)
84
+ else ci
85
+
86
+ // the memory tracks the combination being left while the group is mixed
87
+ def keepMemory(mixedNow: bool, ci: bool, mi: bool): bool = if (mixedNow) ci else mi
88
+
89
+ // Tab order: outside -> controller -> c0 -> c1 -> c2 -> outside
90
+ def nextFocus(f: str): str =
91
+ if (f == "none") "parent"
92
+ else if (f == "parent") "c0"
93
+ else if (f == "c0") "c1"
94
+ else if (f == "c1") "c2"
95
+ else "none"
96
+
97
+ // ---- request / commit (map ticket rt1p) -------------------------------
98
+ def b2i(b: bool): int = if (b) 1 else 0
99
+ // injective for three bools: bit 0 = c0, bit 1 = c1, bit 2 = c2
100
+ def packed(a: bool, b: bool, c: bool): int =
101
+ b2i(a) + 2 * b2i(b) + 4 * b2i(c)
102
+ def bitAt(i: int, n: int): bool =
103
+ if (i == 0) n % 2 == 1 else if (i == 1) (n / 2) % 2 == 1 else (n / 4) % 2 == 1
104
+ def holds: bool = and(controlled, not(accepting))
105
+ def follows: bool = and(controlled, accepting)
106
+ def verdict: str = if (holds) "rejected" else "accepted"
107
+ def landed(h: bool, current: bool, want: bool): bool = if (h) current else want
108
+ def propAfter(f: bool, p: int, want: int): int = if (f) want else p
109
+ def nowPacked: int = packed(c0, c1, c2)
110
+ // the intended checked set of activating `target`, packed
111
+ def intent(name: str, ci: bool, mi: bool, target: str): bool =
112
+ nextChild(name, ci, mi, target, isAll(c0, c1, c2), restores(c0, c1, c2, m0, m1, m2, remember))
113
+ def intentPacked(target: str): int =
114
+ packed(
115
+ intent("c0", c0, m0, target),
116
+ intent("c1", c1, m1, target),
117
+ intent("c2", c2, m2, target)
118
+ )
119
+ def moves(target: str): bool = intentPacked(target) != nowPacked
120
+ // the host flips one bit of its own copy of the value
121
+ def flipAt(n: int, i: int): int =
122
+ if (i == 0) (if (bitAt(0, n)) n - 1 else n + 1)
123
+ else if (i == 1) (if (bitAt(1, n)) n - 2 else n + 2)
124
+ else (if (bitAt(2, n)) n - 4 else n + 4)
125
+
126
+ action init = all {
127
+ c0' = false,
128
+ c1' = false,
129
+ c2' = false,
130
+ m0' = false,
131
+ m1' = false,
132
+ m2' = false,
133
+ focus' = "none",
134
+ remember' = false,
135
+ controlled' = false,
136
+ accepting' = false,
137
+ valueProp' = 0,
138
+ requested' = 0,
139
+ outcome' = "none",
140
+ base' = 0,
141
+ requests' = 0
142
+ }
143
+
144
+ action focus_on = {
145
+ nondet target = Set("parent", "c0", "c1", "c2").oneOf()
146
+ all {
147
+ c0' = c0,
148
+ c1' = c1,
149
+ c2' = c2,
150
+ m0' = m0,
151
+ m1' = m1,
152
+ m2' = m2,
153
+ focus' = target,
154
+ remember' = remember,
155
+ controlled' = controlled,
156
+ accepting' = accepting,
157
+ valueProp' = valueProp,
158
+ requested' = requested,
159
+ outcome' = outcome,
160
+ base' = base,
161
+ requests' = requests
162
+ }
163
+ }
164
+
165
+ action blur = all {
166
+ c0' = c0,
167
+ c1' = c1,
168
+ c2' = c2,
169
+ m0' = m0,
170
+ m1' = m1,
171
+ m2' = m2,
172
+ focus' = "none",
173
+ remember' = remember,
174
+ controlled' = controlled,
175
+ accepting' = accepting,
176
+ valueProp' = valueProp,
177
+ requested' = requested,
178
+ outcome' = outcome,
179
+ base' = base,
180
+ requests' = requests
181
+ }
182
+
183
+ // Keyboard Interaction: Tab moves focus among the checkboxes
184
+ action key_tab = all {
185
+ c0' = c0,
186
+ c1' = c1,
187
+ c2' = c2,
188
+ m0' = m0,
189
+ m1' = m1,
190
+ m2' = m2,
191
+ focus' = nextFocus(focus),
192
+ remember' = remember,
193
+ controlled' = controlled,
194
+ accepting' = accepting,
195
+ valueProp' = valueProp,
196
+ requested' = requested,
197
+ outcome' = outcome,
198
+ base' = base,
199
+ requests' = requests
200
+ }
201
+
202
+ // request: Space changes the state of the focused checkbox
203
+ action key_space = all {
204
+ c0' = landed(holds, c0, intent("c0", c0, m0, focus)),
205
+ c1' = landed(holds, c1, intent("c1", c1, m1, focus)),
206
+ c2' = landed(holds, c2, intent("c2", c2, m2, focus)),
207
+ m0' = keepMemory(isMixedSet(c0, c1, c2), c0, m0),
208
+ m1' = keepMemory(isMixedSet(c0, c1, c2), c1, m1),
209
+ m2' = keepMemory(isMixedSet(c0, c1, c2), c2, m2),
210
+ focus' = focus,
211
+ remember' = remember,
212
+ controlled' = controlled,
213
+ accepting' = accepting,
214
+ valueProp' = propAfter(follows, valueProp, intentPacked(focus)),
215
+ requested' = if (moves(focus)) intentPacked(focus) else requested,
216
+ outcome' = if (moves(focus)) verdict else outcome,
217
+ base' = if (moves(focus)) nowPacked else base,
218
+ requests' = if (moves(focus)) requests + 1 else requests
219
+ }
220
+
221
+ // pointer: a click focuses its target and requests activation
222
+ action click = {
223
+ nondet target = Set("parent", "c0", "c1", "c2").oneOf()
224
+ all {
225
+ c0' = landed(holds, c0, intent("c0", c0, m0, target)),
226
+ c1' = landed(holds, c1, intent("c1", c1, m1, target)),
227
+ c2' = landed(holds, c2, intent("c2", c2, m2, target)),
228
+ m0' = keepMemory(isMixedSet(c0, c1, c2), c0, m0),
229
+ m1' = keepMemory(isMixedSet(c0, c1, c2), c1, m1),
230
+ m2' = keepMemory(isMixedSet(c0, c1, c2), c2, m2),
231
+ focus' = target,
232
+ remember' = remember,
233
+ controlled' = controlled,
234
+ accepting' = accepting,
235
+ valueProp' = propAfter(follows, valueProp, intentPacked(target)),
236
+ requested' = if (moves(target)) intentPacked(target) else requested,
237
+ outcome' = if (moves(target)) verdict else outcome,
238
+ base' = if (moves(target)) nowPacked else base,
239
+ requests' = if (moves(target)) requests + 1 else requests
240
+ }
241
+ }
242
+
243
+ // prop change: the optional last-mixed memory
244
+ action set_remember = {
245
+ nondet value = Set(true, false).oneOf()
246
+ all {
247
+ c0' = c0,
248
+ c1' = c1,
249
+ c2' = c2,
250
+ m0' = m0,
251
+ m1' = m1,
252
+ m2' = m2,
253
+ focus' = focus,
254
+ remember' = value,
255
+ controlled' = controlled,
256
+ accepting' = accepting,
257
+ valueProp' = valueProp,
258
+ requested' = requested,
259
+ outcome' = outcome,
260
+ base' = base,
261
+ requests' = requests
262
+ }
263
+ }
264
+
265
+ // commit: the host writes the controlled value (a no-op while uncontrolled)
266
+ action setValue = {
267
+ nondet v = Set(0, 1, 2, 3, 4, 5, 6, 7).oneOf()
268
+ all {
269
+ c0' = if (controlled) bitAt(0, v) else c0,
270
+ c1' = if (controlled) bitAt(1, v) else c1,
271
+ c2' = if (controlled) bitAt(2, v) else c2,
272
+ m0' = m0,
273
+ m1' = m1,
274
+ m2' = m2,
275
+ focus' = focus,
276
+ remember' = remember,
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 one child of its own copy of the value
288
+ action hostToggle = {
289
+ nondet panel = Set(0, 1, 2).oneOf()
290
+ all {
291
+ c0' = if (controlled) bitAt(0, flipAt(valueProp, panel)) else c0,
292
+ c1' = if (controlled) bitAt(1, flipAt(valueProp, panel)) else c1,
293
+ c2' = if (controlled) bitAt(2, flipAt(valueProp, panel)) else c2,
294
+ m0' = m0,
295
+ m1' = m1,
296
+ m2' = m2,
297
+ focus' = focus,
298
+ remember' = remember,
299
+ controlled' = controlled,
300
+ accepting' = accepting,
301
+ valueProp' = if (controlled) flipAt(valueProp, panel) else valueProp,
302
+ requested' = requested,
303
+ outcome' = if (controlled) "none" else outcome,
304
+ base' = base,
305
+ requests' = requests
306
+ }
307
+ }
308
+
309
+ // configuration: the host starts owning the value, adopting the current one
310
+ action control = all {
311
+ c0' = c0,
312
+ c1' = c1,
313
+ c2' = c2,
314
+ m0' = m0,
315
+ m1' = m1,
316
+ m2' = m2,
317
+ focus' = focus,
318
+ remember' = remember,
319
+ controlled' = true,
320
+ accepting' = accepting,
321
+ valueProp' = nowPacked,
322
+ requested' = requested,
323
+ outcome' = outcome,
324
+ base' = base,
325
+ requests' = requests
326
+ }
327
+
328
+ // configuration: the controlled host answers every request with its intent
329
+ action acceptRequests = all {
330
+ c0' = c0,
331
+ c1' = c1,
332
+ c2' = c2,
333
+ m0' = m0,
334
+ m1' = m1,
335
+ m2' = m2,
336
+ focus' = focus,
337
+ remember' = remember,
338
+ controlled' = controlled,
339
+ accepting' = true,
340
+ valueProp' = valueProp,
341
+ requested' = requested,
342
+ outcome' = outcome,
343
+ base' = base,
344
+ requests' = requests
345
+ }
346
+
347
+ action step = any {
348
+ init,
349
+ focus_on,
350
+ blur,
351
+ key_tab,
352
+ key_space,
353
+ click,
354
+ set_remember,
355
+ setValue,
356
+ hostToggle,
357
+ control,
358
+ acceptRequests
359
+ }
360
+
361
+ // APG: a mixed controller is mixed iff the children disagree
362
+ val MixedIffChildrenDisagree: bool =
363
+ (parentChecked(c0, c1, c2) == "mixed") == or(c0 != c1, c1 != c2)
364
+ // checked iff every child is checked
365
+ val CheckedIffAllChildrenChecked: bool =
366
+ (parentChecked(c0, c1, c2) == "true") == and(c0, c1, c2)
367
+ // not checked iff no child is checked
368
+ val UncheckedIffNoChildChecked: bool =
369
+ (parentChecked(c0, c1, c2) == "false") == not(or(c0, c1, c2))
370
+ // the memory only ever holds a genuinely mixed combination (or nothing yet)
371
+ val MemoryIsMixedOrEmpty: bool =
372
+ or(isNone(m0, m1, m2), isMixedSet(m0, m1, m2))
373
+ val FocusValid: bool = or(
374
+ focus == "none",
375
+ focus == "parent",
376
+ focus == "c0",
377
+ focus == "c1",
378
+ focus == "c2"
379
+ )
380
+ // callbacks only ever count up from zero
381
+ val RequestsNonNegative: bool = requests >= 0
382
+ // controlled: only the value prop sets the state, so no event commits
383
+ val ControlledFollowsProp: bool = or(not(controlled), nowPacked == valueProp)
384
+ // a rejected request leaves the state as it found it
385
+ val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), nowPacked == base)
386
+ // an accepted request committed exactly its intent
387
+ val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), nowPacked == requested)
388
+ }