@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,131 @@
1
+ // APG Alert — https://www.w3.org/WAI/ARIA/apg/patterns/alert/
2
+ //
3
+ // A brief, important message in a role=alert live region. The APG contract the
4
+ // machine checks:
5
+ // - a message rendered or changed AFTER page load is announced;
6
+ // - a message already present when the page finished loading is NOT announced
7
+ // (screen readers ignore alerts present before load completes);
8
+ // - an alert never takes keyboard focus (that is the alertdialog pattern);
9
+ // - an alert never disappears on its own (WCAG 2.2.3): there is no timer
10
+ // event, the only way out is the user's `dismiss`.
11
+ //
12
+ // Events:
13
+ // show render a new message (mounts the alert, or replaces its text)
14
+ // load the page finishes loading
15
+ // dismiss the user closes the alert
16
+ // focusTask / focusOther the user moves focus around the page
17
+ //
18
+ // ids: every `show` takes a fresh message id; `announced` is the id the
19
+ // assistive technology last spoke (0 = nothing yet), `preloaded` the id that
20
+ // was already rendered when the page finished loading (0 = none).
21
+ //
22
+ // ponytail: queueing / politeness (aria-live=assertive vs polite) is not
23
+ // modelled; add a `queue` var if overlapping alerts matter.
24
+ module apg_alert {
25
+ var loaded: bool // page load completed
26
+ var mounted: bool // the alert element is in the DOM
27
+ var role: str // rendered role: "alert" | "none"
28
+ var message: int // id of the message currently shown (0 = none)
29
+ var nextId: int // the next message id to hand out
30
+ var announced: int // last message id announced by AT (0 = none)
31
+ var announcements: int // number of announcements so far
32
+ var preloaded: int // message id present at load time (0 = none)
33
+ var focus: str // "task" | "other" | "alert"
34
+ var dismissals: int // user dismissals
35
+
36
+ action init = all {
37
+ loaded' = false,
38
+ mounted' = false,
39
+ role' = "none",
40
+ message' = 0,
41
+ nextId' = 1,
42
+ announced' = 0,
43
+ announcements' = 0,
44
+ preloaded' = 0,
45
+ focus' = "task",
46
+ dismissals' = 0
47
+ }
48
+
49
+ // before load the message renders silently; after load it is announced
50
+ action show = all {
51
+ loaded' = loaded,
52
+ mounted' = true,
53
+ role' = "alert",
54
+ message' = nextId,
55
+ nextId' = nextId + 1,
56
+ announced' = if (loaded) nextId else announced,
57
+ announcements' = if (loaded) announcements + 1 else announcements,
58
+ preloaded' = if (loaded) preloaded else nextId,
59
+ focus' = focus,
60
+ dismissals' = dismissals
61
+ }
62
+
63
+ action load = all {
64
+ loaded' = true,
65
+ mounted' = mounted,
66
+ role' = role,
67
+ message' = message,
68
+ nextId' = nextId,
69
+ announced' = announced,
70
+ announcements' = announcements,
71
+ preloaded' = if (loaded) preloaded else message,
72
+ focus' = focus,
73
+ dismissals' = dismissals
74
+ }
75
+
76
+ action dismiss = all {
77
+ loaded' = loaded,
78
+ mounted' = false,
79
+ role' = "none",
80
+ message' = 0,
81
+ nextId' = nextId,
82
+ announced' = announced,
83
+ announcements' = announcements,
84
+ preloaded' = preloaded,
85
+ focus' = focus,
86
+ dismissals' = if (mounted) dismissals + 1 else dismissals
87
+ }
88
+
89
+ action focusTask = all {
90
+ loaded' = loaded,
91
+ mounted' = mounted,
92
+ role' = role,
93
+ message' = message,
94
+ nextId' = nextId,
95
+ announced' = announced,
96
+ announcements' = announcements,
97
+ preloaded' = preloaded,
98
+ focus' = "task",
99
+ dismissals' = dismissals
100
+ }
101
+
102
+ action focusOther = all {
103
+ loaded' = loaded,
104
+ mounted' = mounted,
105
+ role' = role,
106
+ message' = message,
107
+ nextId' = nextId,
108
+ announced' = announced,
109
+ announcements' = announcements,
110
+ preloaded' = preloaded,
111
+ focus' = "other",
112
+ dismissals' = dismissals
113
+ }
114
+
115
+ action step = any { init, show, load, dismiss, focusTask, focusOther }
116
+
117
+ // APG: an alert never affects keyboard focus
118
+ val FocusNeverOnAlert: bool = focus != "alert"
119
+ // the live-region role is present exactly while the alert is mounted
120
+ val RoleWhenMounted: bool = (role == "alert") == mounted
121
+ // a mounted alert carries a message, an unmounted one carries none
122
+ val MountedHasMessage: bool = mounted == (message != 0)
123
+ // alerts present before load completes are never announced
124
+ val PreloadedNotAnnounced: bool = or(preloaded == 0, announced != preloaded)
125
+ // after load, a live message is the one that was announced
126
+ val LiveMessageAnnounced: bool = or(not(and(loaded, mounted)), or(message == preloaded, announced == message))
127
+ // nothing is announced before the page has loaded
128
+ val SilentBeforeLoad: bool = or(loaded, announcements == 0)
129
+ // ids and counters only move forward
130
+ val CountersConsistent: bool = and(announced < nextId, and(announcements >= 0, dismissals >= 0))
131
+ }
@@ -0,0 +1,455 @@
1
+ // APG Alert and Message Dialogs pattern — the discard-notes confirmation.
2
+ //
3
+ // Source: https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/
4
+ // sections: About This Pattern, Keyboard Interaction (defers to the modal
5
+ // dialog pattern), WAI-ARIA Roles, States, and Properties
6
+ // example: https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/examples/alertdialog/
7
+ // keyboard: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/ (Keyboard
8
+ // Interaction, Notes 1-2)
9
+ //
10
+ // A page with a Notes textarea, Save and Discard buttons; Discard opens an
11
+ // alertdialog with No and Yes buttons. Focus is one encoded target
12
+ // `layer * 100 + position`: layer 0 is the page (1 textarea, 2 Save,
13
+ // 3 Discard), layer 1 the alertdialog (1 No, 2 Yes).
14
+ //
15
+ // Modelled
16
+ // - open: activating an enabled Discard opens the alertdialog and moves focus
17
+ // to the LEAST DESTRUCTIVE action ("No"); the invoker is remembered.
18
+ // - Tab / Shift+Tab: cycle (wrap) within the dialog, never leaving it.
19
+ // - Escape or "No": close without discarding; "Yes": discard the notes and
20
+ // storage, then close. Focus returns to the invoker (Discard); when the
21
+ // buttons use aria-disabled (the example) the emptied Discard stays
22
+ // focusable, so focus really lands there.
23
+ // - Discard is aria-disabled while the textarea is empty and Save while the
24
+ // textarea equals storage; aria-disabled controls stay in the tab sequence
25
+ // but cannot be activated. USE_ARIA_DISABLED=false models the HTML
26
+ // `disabled` variant for Discard (it drops out of the tab sequence and the
27
+ // return focus falls back to the textarea).
28
+ // - Ctrl/Cmd+S in the textarea saves; typing/clearing edit the textarea.
29
+ // - focus_attempt (pointer click / programmatic focus) is refused outside
30
+ // the active layer; pointer_outside never dismisses an alertdialog (it
31
+ // demands a response).
32
+ // - ARIA contract (derived defs): role=alertdialog, aria-modal=true, a name
33
+ // (aria-labelledby: the "Confirmation" heading) and a REQUIRED
34
+ // aria-describedby referencing the message.
35
+ //
36
+ // Request vs commit (map ticket rt1p). The dialog's `isOpen` is the controlled
37
+ // value: `activate` and `escape` are requests carrying the open intent
38
+ // (`requested`). A request fires onValueChange (`requests` counts the calls)
39
+ // when the intent differs from the current open state, and then:
40
+ // uncontrolled the request is accepted: the state commits the intent
41
+ // controlled, holds the host ignores it ("rejected"): the state is untouched
42
+ // controlled, accepts the host writes the intent back (`accepting`)
43
+ // `valueProp` is the host's copy (meaningful while `controlled`); `setValue` /
44
+ // `hostToggle` are the host writing it, the only commit while controlled.
45
+ // `control` / `acceptRequests` configure the host (init is literal-only). A
46
+ // rejected open/close request leaves focus, the return target and the notes
47
+ // untouched too, so the whole transition is gated by the rejection.
48
+ //
49
+ // Invariants for the model: ControlledFollowsProp (in controlled mode no event
50
+ // commits), RejectedRequestUntouched, AcceptedRequestCommits.
51
+ //
52
+ // Deliberately omitted
53
+ // - the non-modal Alert pattern shown beside it (the "saved" alert), which
54
+ // never moves focus or changes this machine's state;
55
+ // - Save's own aria-disabled/disabled variant (only Discard has the variant);
56
+ // - stacking an alertdialog over another modal (see dialog-modal.qnt);
57
+ // - the notes' real text: contents are three revisions (0 empty, 1, 2);
58
+ // - the system alert sound, visual dimming, aria-hidden legacy handling.
59
+ module apg_alertdialog {
60
+ var isOpen: bool // the alertdialog is open
61
+ var focus: int // layer * 100 + position
62
+ var ret: int // focus target to restore on close (0 = none)
63
+ var interacted: bool // focus moved since the dialog opened
64
+ var notes: int // textarea content revision (0 = empty)
65
+ var stored: int // localStorage content revision (0 = empty)
66
+ var requests: int // number of requests that fired onValueChange
67
+ var controlled: bool // the host owns the open state (`value` prop present)
68
+ var accepting: bool // controlled host that answers every request with its intent
69
+ var valueProp: bool // the controlled value prop (meaningful while controlled)
70
+ var requested: bool // intent of the last request
71
+ var outcome: str // "none" | "accepted" | "rejected"
72
+ var base: bool // open state when the last request was received
73
+
74
+ // --- configuration -----------------------------------------------------------
75
+ def USE_ARIA_DISABLED: bool = true
76
+ def NO_POS: int = 1 // least destructive action
77
+ def YES_POS: int = 2 // destructive action
78
+ def FALLBACK_TARGET: int = 1 // textarea, when the invoker cannot take focus
79
+
80
+ // --- ARIA contract -----------------------------------------------------------
81
+ def dialogRole: str = "alertdialog"
82
+ def ariaModal(open: bool): bool = open
83
+ def hasAccessibleName: bool = true // aria-labelledby -> heading "Confirmation"
84
+ def hasDescription: bool = true // aria-describedby is required for alertdialog
85
+ def contractOk(open: bool): bool =
86
+ and(dialogRole == "alertdialog", ariaModal(open), hasAccessibleName, hasDescription)
87
+ def discardAriaDisabled: bool = notes == 0
88
+ def saveAriaDisabled: bool = notes == stored
89
+ def pageInert: bool = isOpen
90
+
91
+ // --- derived state -------------------------------------------------------------
92
+ def discardFocusable: bool = or(USE_ARIA_DISABLED, notes != 0)
93
+ def layer: int = if (isOpen) 1 else 0
94
+ def focusLayer: int = focus / 100
95
+ def focusPos: int = focus % 100
96
+ def tabCount(l: int): int =
97
+ if (l == 1) 2 else if (discardFocusable) 3 else 2
98
+ def validPos(l: int, pos: int): bool = and(pos >= 1, pos <= tabCount(l))
99
+ def validTarget(target: int): bool =
100
+ and(target / 100 == layer, validPos(layer, target % 100))
101
+ def nextPos(pos: int, n: int): int = if (pos >= n) 1 else pos + 1
102
+ def prevPos(pos: int, n: int): int = if (pos <= 1) n else pos - 1
103
+ def isDestructive(pos: int): bool = pos == YES_POS
104
+ // focus restored on close, given the notes revision after the close
105
+ def retFocus(notesAfter: int): int =
106
+ if (and(ret == 3, not(or(USE_ARIA_DISABLED, notesAfter != 0)))) FALLBACK_TARGET else ret
107
+
108
+ // --- transition kernel ---------------------------------------------------------
109
+ // ev: 1 escape, 2 activate, 3 tab, 4 shift_tab, 5 focus_attempt, 6 ctrl_s,
110
+ // 7 type_text, 8 clear_text, 9 pointer_outside
111
+ // mode: 0 none, 1 open, 2 close (dismiss), 3 close (confirm discard),
112
+ // 4 save, 5 type, 6 clear
113
+ def activateMode: int =
114
+ if (isOpen) (if (focus == 100 + NO_POS) 2 else if (focus == 100 + YES_POS) 3 else 0)
115
+ else if (and(focus == 3, notes != 0)) 1
116
+ else if (and(focus == 2, notes != stored)) 4
117
+ else 0
118
+ def modeOf(ev: int): int =
119
+ if (ev == 1) (if (isOpen) 2 else 0)
120
+ else if (ev == 2) activateMode
121
+ else if (ev == 6) (if (and(not(isOpen), focus == 1, notes != stored)) 4 else 0)
122
+ else if (ev == 7) (if (and(not(isOpen), focus == 1)) 5 else 0)
123
+ else if (ev == 8) (if (and(not(isOpen), focus == 1)) 6 else 0)
124
+ else 0
125
+
126
+ def nextOpen(ev: int): bool =
127
+ if (modeOf(ev) == 1) true
128
+ else if (or(modeOf(ev) == 2, modeOf(ev) == 3)) false
129
+ else isOpen
130
+ def nextNotes(ev: int): int =
131
+ if (or(modeOf(ev) == 3, modeOf(ev) == 6)) 0
132
+ else if (modeOf(ev) == 5) (if (notes == 1) 2 else 1)
133
+ else notes
134
+ def nextStored(ev: int): int =
135
+ if (modeOf(ev) == 3) 0
136
+ else if (modeOf(ev) == 4) notes
137
+ else stored
138
+ def nextRet(ev: int): int =
139
+ if (modeOf(ev) == 1) focus
140
+ else if (or(modeOf(ev) == 2, modeOf(ev) == 3)) 0
141
+ else ret
142
+ def nextInteracted(ev: int, arg: int): bool =
143
+ if (modeOf(ev) == 1) false
144
+ else if (or(ev == 3, ev == 4)) true
145
+ else if (and(ev == 5, validTarget(arg))) true
146
+ else interacted
147
+ def nextFocus(ev: int, arg: int): int =
148
+ if (modeOf(ev) == 1) 100 + NO_POS
149
+ else if (modeOf(ev) == 2) retFocus(notes)
150
+ else if (modeOf(ev) == 3) retFocus(0)
151
+ else if (ev == 3) layer * 100 + nextPos(focusPos, tabCount(layer))
152
+ else if (ev == 4) layer * 100 + prevPos(focusPos, tabCount(layer))
153
+ else if (and(ev == 5, validTarget(arg))) arg
154
+ else focus
155
+
156
+ // --- request / commit (map ticket rt1p) ----------------------------------
157
+ def moved(a: bool, b: bool): int = if (a == b) 0 else 1
158
+ def holds: bool = and(controlled, not(accepting))
159
+ def follows: bool = and(controlled, accepting)
160
+ def verdict: str = if (holds) "rejected" else "accepted"
161
+ // the open state after a request for `want`
162
+ def landed(h: bool, v: bool, want: bool): bool = if (h) v else want
163
+ // the value prop after a request for `want`
164
+ def propAfter(f: bool, p: bool, want: bool): bool = if (f) want else p
165
+ // the open intent of each request event
166
+ def wantOpenActivate: bool = nextOpen(2)
167
+ def wantOpenEscape: bool = nextOpen(1)
168
+ // only a request that actually changes the open state can be rejected
169
+ def rejectActivate: bool = and(holds, wantOpenActivate != isOpen)
170
+ def rejectEscape: bool = and(holds, wantOpenEscape != isOpen)
171
+
172
+ // --- host writes (the only commit while controlled) ----------------------
173
+ // opening keeps the least destructive focus and remembers Discard as invoker
174
+ def setOpen(c: bool, v: bool): bool = if (c) v else false
175
+ def openingSet(c: bool, v: bool, open: bool): bool = and(c, and(v, not(open)))
176
+ def closingSet(c: bool, v: bool, open: bool): bool = and(c, and(not(v), open))
177
+ def toggleOpen(c: bool, open: bool): bool = if (c) not(open) else open
178
+ def toggleOpening(c: bool, open: bool): bool = and(c, not(open))
179
+ def toggleClosing(c: bool, open: bool): bool = and(c, open)
180
+
181
+ action init = all {
182
+ isOpen' = false,
183
+ focus' = 1,
184
+ ret' = 0,
185
+ interacted' = false,
186
+ notes' = 1,
187
+ stored' = 1,
188
+ requests' = 0,
189
+ controlled' = false,
190
+ accepting' = false,
191
+ valueProp' = false,
192
+ requested' = false,
193
+ outcome' = "none",
194
+ base' = false
195
+ }
196
+
197
+ // request: Escape closes the dialog (a no-op while already closed)
198
+ action escape = all {
199
+ isOpen' = landed(holds, isOpen, wantOpenEscape),
200
+ focus' = if (rejectEscape) focus else nextFocus(1, 0),
201
+ ret' = if (rejectEscape) ret else nextRet(1),
202
+ interacted' = if (rejectEscape) interacted else nextInteracted(1, 0),
203
+ notes' = if (rejectEscape) notes else nextNotes(1),
204
+ stored' = if (rejectEscape) stored else nextStored(1),
205
+ requests' = requests + moved(isOpen, wantOpenEscape),
206
+ controlled' = controlled,
207
+ accepting' = accepting,
208
+ valueProp' = propAfter(follows, valueProp, wantOpenEscape),
209
+ requested' = if (isOpen == wantOpenEscape) requested else wantOpenEscape,
210
+ outcome' = if (isOpen == wantOpenEscape) outcome else verdict,
211
+ base' = if (isOpen == wantOpenEscape) base else isOpen
212
+ }
213
+
214
+ // request: activate (Discard opens, "No" closes, "Yes" discards and closes,
215
+ // Save stores; only the open-changing paths are requests)
216
+ action activate = all {
217
+ isOpen' = landed(holds, isOpen, wantOpenActivate),
218
+ focus' = if (rejectActivate) focus else nextFocus(2, 0),
219
+ ret' = if (rejectActivate) ret else nextRet(2),
220
+ interacted' = if (rejectActivate) interacted else nextInteracted(2, 0),
221
+ notes' = if (rejectActivate) notes else nextNotes(2),
222
+ stored' = if (rejectActivate) stored else nextStored(2),
223
+ requests' = requests + moved(isOpen, wantOpenActivate),
224
+ controlled' = controlled,
225
+ accepting' = accepting,
226
+ valueProp' = propAfter(follows, valueProp, wantOpenActivate),
227
+ requested' = if (isOpen == wantOpenActivate) requested else wantOpenActivate,
228
+ outcome' = if (isOpen == wantOpenActivate) outcome else verdict,
229
+ base' = if (isOpen == wantOpenActivate) base else isOpen
230
+ }
231
+
232
+ action tab = all {
233
+ isOpen' = nextOpen(3),
234
+ focus' = nextFocus(3, 0),
235
+ ret' = nextRet(3),
236
+ interacted' = nextInteracted(3, 0),
237
+ notes' = nextNotes(3),
238
+ stored' = nextStored(3),
239
+ requests' = requests,
240
+ controlled' = controlled,
241
+ accepting' = accepting,
242
+ valueProp' = valueProp,
243
+ requested' = requested,
244
+ outcome' = outcome,
245
+ base' = base
246
+ }
247
+
248
+ action shift_tab = all {
249
+ isOpen' = nextOpen(4),
250
+ focus' = nextFocus(4, 0),
251
+ ret' = nextRet(4),
252
+ interacted' = nextInteracted(4, 0),
253
+ notes' = nextNotes(4),
254
+ stored' = nextStored(4),
255
+ requests' = requests,
256
+ controlled' = controlled,
257
+ accepting' = accepting,
258
+ valueProp' = valueProp,
259
+ requested' = requested,
260
+ outcome' = outcome,
261
+ base' = base
262
+ }
263
+
264
+ // pointer click / programmatic focus on any encoded target (valid or not)
265
+ action focus_attempt = {
266
+ nondet target = Set(0, 1, 2, 3, 4, 100, 101, 102, 103).oneOf()
267
+ all {
268
+ isOpen' = nextOpen(5),
269
+ focus' = nextFocus(5, target),
270
+ ret' = nextRet(5),
271
+ interacted' = nextInteracted(5, target),
272
+ notes' = nextNotes(5),
273
+ stored' = nextStored(5),
274
+ requests' = requests,
275
+ controlled' = controlled,
276
+ accepting' = accepting,
277
+ valueProp' = valueProp,
278
+ requested' = requested,
279
+ outcome' = outcome,
280
+ base' = base
281
+ }
282
+ }
283
+
284
+ action ctrl_s = all {
285
+ isOpen' = nextOpen(6),
286
+ focus' = nextFocus(6, 0),
287
+ ret' = nextRet(6),
288
+ interacted' = nextInteracted(6, 0),
289
+ notes' = nextNotes(6),
290
+ stored' = nextStored(6),
291
+ requests' = requests,
292
+ controlled' = controlled,
293
+ accepting' = accepting,
294
+ valueProp' = valueProp,
295
+ requested' = requested,
296
+ outcome' = outcome,
297
+ base' = base
298
+ }
299
+
300
+ action type_text = all {
301
+ isOpen' = nextOpen(7),
302
+ focus' = nextFocus(7, 0),
303
+ ret' = nextRet(7),
304
+ interacted' = nextInteracted(7, 0),
305
+ notes' = nextNotes(7),
306
+ stored' = nextStored(7),
307
+ requests' = requests,
308
+ controlled' = controlled,
309
+ accepting' = accepting,
310
+ valueProp' = valueProp,
311
+ requested' = requested,
312
+ outcome' = outcome,
313
+ base' = base
314
+ }
315
+
316
+ action clear_text = all {
317
+ isOpen' = nextOpen(8),
318
+ focus' = nextFocus(8, 0),
319
+ ret' = nextRet(8),
320
+ interacted' = nextInteracted(8, 0),
321
+ notes' = nextNotes(8),
322
+ stored' = nextStored(8),
323
+ requests' = requests,
324
+ controlled' = controlled,
325
+ accepting' = accepting,
326
+ valueProp' = valueProp,
327
+ requested' = requested,
328
+ outcome' = outcome,
329
+ base' = base
330
+ }
331
+
332
+ // a click on the inert page never dismisses an alertdialog
333
+ action pointer_outside = all {
334
+ isOpen' = nextOpen(9),
335
+ focus' = nextFocus(9, 0),
336
+ ret' = nextRet(9),
337
+ interacted' = nextInteracted(9, 0),
338
+ notes' = nextNotes(9),
339
+ stored' = nextStored(9),
340
+ requests' = requests,
341
+ controlled' = controlled,
342
+ accepting' = accepting,
343
+ valueProp' = valueProp,
344
+ requested' = requested,
345
+ outcome' = outcome,
346
+ base' = base
347
+ }
348
+
349
+ // commit: the host writes the controlled open state (a no-op while uncontrolled)
350
+ action setValue = {
351
+ nondet v = Set(true, false).oneOf()
352
+ all {
353
+ isOpen' = if (controlled) setOpen(true, v) else isOpen,
354
+ focus' = if (openingSet(controlled, v, isOpen)) (100 + NO_POS)
355
+ else if (closingSet(controlled, v, isOpen)) retFocus(notes) else focus,
356
+ ret' = if (openingSet(controlled, v, isOpen)) 3
357
+ else if (closingSet(controlled, v, isOpen)) 0 else ret,
358
+ interacted' = if (openingSet(controlled, v, isOpen)) false else interacted,
359
+ notes' = notes,
360
+ stored' = stored,
361
+ requests' = requests,
362
+ controlled' = controlled,
363
+ accepting' = accepting,
364
+ valueProp' = if (controlled) setOpen(true, v) else valueProp,
365
+ requested' = requested,
366
+ outcome' = if (controlled) "none" else outcome,
367
+ base' = base
368
+ }
369
+ }
370
+
371
+ // commit: the host flips its own copy of the open state
372
+ action hostToggle = all {
373
+ isOpen' = if (controlled) toggleOpen(true, isOpen) else isOpen,
374
+ focus' = if (toggleOpening(controlled, isOpen)) (100 + NO_POS)
375
+ else if (toggleClosing(controlled, isOpen)) retFocus(notes) else focus,
376
+ ret' = if (toggleOpening(controlled, isOpen)) 3
377
+ else if (toggleClosing(controlled, isOpen)) 0 else ret,
378
+ interacted' = if (toggleOpening(controlled, isOpen)) false else interacted,
379
+ notes' = notes,
380
+ stored' = stored,
381
+ requests' = requests,
382
+ controlled' = controlled,
383
+ accepting' = accepting,
384
+ valueProp' = if (controlled) toggleOpen(true, isOpen) else valueProp,
385
+ requested' = requested,
386
+ outcome' = if (controlled) "none" else outcome,
387
+ base' = base
388
+ }
389
+
390
+ // configuration: the host starts owning the open state, adopting the current one
391
+ action control = all {
392
+ isOpen' = isOpen,
393
+ focus' = focus,
394
+ ret' = ret,
395
+ interacted' = interacted,
396
+ notes' = notes,
397
+ stored' = stored,
398
+ requests' = requests,
399
+ controlled' = true,
400
+ accepting' = accepting,
401
+ valueProp' = isOpen,
402
+ requested' = requested,
403
+ outcome' = outcome,
404
+ base' = base
405
+ }
406
+
407
+ // configuration: the controlled host answers every request with its intent
408
+ action acceptRequests = all {
409
+ isOpen' = isOpen,
410
+ focus' = focus,
411
+ ret' = ret,
412
+ interacted' = interacted,
413
+ notes' = notes,
414
+ stored' = stored,
415
+ requests' = requests,
416
+ controlled' = controlled,
417
+ accepting' = true,
418
+ valueProp' = valueProp,
419
+ requested' = requested,
420
+ outcome' = outcome,
421
+ base' = base
422
+ }
423
+
424
+ action step = any {
425
+ init, escape, activate, tab, shift_tab, focus_attempt, ctrl_s, type_text,
426
+ clear_text, pointer_outside, setValue, hostToggle, control, acceptRequests
427
+ }
428
+
429
+ // --- invariants ------------------------------------------------------------
430
+ // focus never leaves the alertdialog while open (page when closed)
431
+ val FocusInActiveLayer: bool = focusLayer == layer
432
+ // focus sits on a real, focusable element (a disabled Discard is not one
433
+ // unless it uses aria-disabled)
434
+ val FocusPositionValid: bool = validPos(layer, focusPos)
435
+ // the dialog is only reachable through an enabled Discard, so notes exist
436
+ // (a controlled host may still force it open with an empty textarea)
437
+ val OpenImpliesNotes: bool = or(not(isOpen), or(notes != 0, controlled))
438
+ // return focus: the invoker (Discard) while open, nothing remembered when closed
439
+ val ReturnTargetIsInvoker: bool = if (isOpen) ret == 3 else ret == 0
440
+ // initial focus is the least destructive action until the user moves focus
441
+ val InitialFocusLeastDestructive: bool =
442
+ or(not(isOpen), interacted, not(isDestructive(focusPos)))
443
+ // role=alertdialog, aria-modal=true, accessible name and description
444
+ val AlertDialogHonoursContract: bool = or(not(isOpen), contractOk(isOpen))
445
+ // the page is inert exactly while the dialog is open
446
+ val PageInertIffOpen: bool = if (isOpen) focusLayer == 1 else not(pageInert)
447
+ // callbacks only ever count up from zero
448
+ val RequestsNonNegative: bool = requests >= 0
449
+ // controlled: only the value prop sets the state, so no event commits
450
+ val ControlledFollowsProp: bool = or(not(controlled), isOpen == valueProp)
451
+ // a rejected request leaves the state as it found it
452
+ val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), isOpen == base)
453
+ // an accepted request committed exactly its intent
454
+ val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), isOpen == requested)
455
+ }
@@ -0,0 +1,88 @@
1
+ // APG Breadcrumb — https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/
2
+ //
3
+ // A trail of links to the parent pages of the current page, in hierarchical
4
+ // order; the current page is the LAST item. The APG contract the machine
5
+ // checks, as the trail changes while the user navigates:
6
+ // - the link to the current page carries aria-current="page";
7
+ // - if the current item is not a link, aria-current is optional;
8
+ // - aria-current never sits on an ancestor (at most one item, and it is the
9
+ // last one).
10
+ // The navigation landmark + its label are a static DOM contract, see
11
+ // specs/apg/STATIC.md. Keyboard: not applicable (plain links).
12
+ //
13
+ // Events:
14
+ // descend the user goes one level deeper (a new last item)
15
+ // ancestor the user follows an ancestor link; the trail is cut
16
+ // back to that item, which becomes the current page
17
+ // renderLink / renderText the current item is a link / plain text
18
+ // toggleMark optional aria-current on a non-link current item
19
+ //
20
+ // currentIndex: 1-based index of the item with aria-current="page" (0 = none).
21
+ module apg_breadcrumb {
22
+ var depth: int // items in the trail (the current page is item `depth`)
23
+ var currentIsLink: bool // the last item is rendered as a link
24
+ var currentIndex: int // item carrying aria-current=page (0 = none)
25
+ var navigations: int // ancestor links followed
26
+
27
+ def deeper: int = if (depth < 4) depth + 1 else depth
28
+ def cutTo(t: int): int = if (t < depth) t else depth
29
+ def remark(d: int): int = if (currentIndex == 0) 0 else d
30
+
31
+ action init = all {
32
+ depth' = 1,
33
+ currentIsLink' = true,
34
+ currentIndex' = 1,
35
+ navigations' = 0
36
+ }
37
+
38
+ action descend = all {
39
+ depth' = deeper,
40
+ currentIsLink' = currentIsLink,
41
+ currentIndex' = remark(deeper),
42
+ navigations' = navigations
43
+ }
44
+
45
+ action ancestor = {
46
+ nondet target = Set(1, 2, 3).oneOf()
47
+ all {
48
+ depth' = cutTo(target),
49
+ currentIsLink' = currentIsLink,
50
+ currentIndex' = remark(cutTo(target)),
51
+ navigations' = if (target < depth) navigations + 1 else navigations
52
+ }
53
+ }
54
+
55
+ action renderLink = all {
56
+ depth' = depth,
57
+ currentIsLink' = true,
58
+ currentIndex' = depth,
59
+ navigations' = navigations
60
+ }
61
+
62
+ action renderText = all {
63
+ depth' = depth,
64
+ currentIsLink' = false,
65
+ currentIndex' = currentIndex,
66
+ navigations' = navigations
67
+ }
68
+
69
+ action toggleMark = all {
70
+ depth' = depth,
71
+ currentIsLink' = currentIsLink,
72
+ currentIndex' = if (currentIsLink) currentIndex else if (currentIndex == 0) depth else 0,
73
+ navigations' = navigations
74
+ }
75
+
76
+ action step = any { init, descend, ancestor, renderLink, renderText, toggleMark }
77
+
78
+ // APG: aria-current=page is on the last item or absent, never on an ancestor
79
+ val CurrentIsLastOrAbsent: bool = or(currentIndex == 0, currentIndex == depth)
80
+ // APG: the link to the current page always has aria-current=page
81
+ val LinkCurrentIsMarked: bool = or(not(currentIsLink), currentIndex == depth)
82
+ // the trail always holds the current page
83
+ val TrailNotEmpty: bool = depth >= 1
84
+ // the trail is bounded
85
+ val TrailBounded: bool = depth <= 4
86
+ // the counter only grows from zero
87
+ val NavigationsNonNegative: bool = navigations >= 0
88
+ }