@cssints/apg-specs 0.5.2 → 0.6.0
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.
- package/README.md +9 -4
- package/dist/patterns/resizable-panels.bridge.d.mts +44 -0
- package/dist/patterns/resizable-panels.bridge.mjs +287 -0
- package/dist/patterns/resizable-panels.model.json +11221 -0
- package/package.json +8 -1
- package/scenarios/patterns/README.md +12 -0
- package/scenarios/patterns/resizable-panels.spec.json +539 -0
- package/specs/patterns/resizable-panels.qnt +831 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @cssints/apg-specs — APG widget specs
|
|
2
2
|
|
|
3
|
-
The Quint specs behind [`@cssints/headless`](../headless/README.md): one spec per WAI-ARIA APG pattern (plus the canvas camera/pointer contract
|
|
3
|
+
The Quint specs behind [`@cssints/headless`](../headless/README.md): one spec per WAI-ARIA APG pattern, one per non-APG pattern composed of them (resizable panels), plus the canvas camera/pointer contract; the generated models and typed scaffolding, the `SpecBridge` for each pattern, the scenario sets (ported upstream ones and our own), and the spec-only verification. It is the test oracle of the widgets and of `native/headless`, and it is published so that widget libraries outside this repository can replay the same specs and scenarios against their own code.
|
|
4
4
|
|
|
5
5
|
The generated files (`*.model.generated.json`, `*.scaffolding.generated.ts`) are written by the code generation of [`@quint-machine/core`](https://www.npmjs.com/package/@quint-machine/core) and are never edited by hand. Ticket ids like `effects-of-atmosphere-dx00` in the comments of the specs and tests are tickets of the repository these files came from.
|
|
6
6
|
|
|
@@ -9,12 +9,15 @@ The generated files (`*.model.generated.json`, `*.scaffolding.generated.ts`) are
|
|
|
9
9
|
```
|
|
10
10
|
specs/apg/<pattern>.qnt # APG pattern specs (one behavior-machine each)
|
|
11
11
|
specs/canvas/<machine>.qnt # canvas camera + pointer specs
|
|
12
|
+
specs/patterns/<machine>.qnt # non-APG patterns composed of APG widgets (resizable-panels)
|
|
12
13
|
shared/<module>.qnt # vocabulary + pure defs: aria, focusgroup, popover, invokers, details-group
|
|
13
14
|
src/<service>/<machine>.model.generated.json # generated model document
|
|
14
15
|
src/<service>/<machine>.scaffolding.generated.ts # generated typed scaffolding
|
|
15
16
|
src/apg/<pattern>.bridge.ts # scenario vocabulary <-> spec events/state
|
|
17
|
+
src/patterns/<machine>.bridge.ts # the same, for a non-APG pattern
|
|
16
18
|
src/scenario.ts # scenario decoder + replay/differential-walk runtime
|
|
17
19
|
scenarios/apg/*.json, scenarios/canvas/*.json # ported upstream scenario sets
|
|
20
|
+
scenarios/patterns/*.json # scenarios written from our own pattern specs
|
|
18
21
|
scenarios/parts.json # part names per pattern
|
|
19
22
|
test/<service>/<machine>.test.ts # spec-only verification
|
|
20
23
|
scripts/generate-all.ts # regenerates every spec under specs/
|
|
@@ -31,7 +34,6 @@ Run in `packages/apg-specs`:
|
|
|
31
34
|
- `bun run test` — spec-only verification (`vp test run`). The exhaustive explorations (player, camera) take a minute each and can time out on a loaded machine; run them alone before calling a failure real.
|
|
32
35
|
- `bun run typecheck` — `tsc-rs` over `src`, `test` and `scripts`.
|
|
33
36
|
- `bun run check` — all of the above, in order.
|
|
34
|
-
|
|
35
37
|
- `bun run build` — `vp pack`: `dist/` (the runner and the bridges as ESM with `.d.mts`, the generated models as `.json`).
|
|
36
38
|
|
|
37
39
|
## Install and use
|
|
@@ -47,9 +49,12 @@ npm install @cssints/apg-specs effect @quint-machine/core
|
|
|
47
49
|
| `@cssints/apg-specs/scenario` | the scenario decoder, `replayScenario`, `differentialWalk`, `specDriver`, `Unsupported`, driver and bridge types |
|
|
48
50
|
| `@cssints/apg-specs/apg/<pattern>.bridge` | the `SpecBridge`, `<pattern>Config`, `<pattern>Steps` and `<pattern>Probe` of a pattern |
|
|
49
51
|
| `@cssints/apg-specs/apg/<pattern>.model.json` | the generated model document (`src/apg/<pattern>.model.generated.json` in the repository) |
|
|
52
|
+
| `@cssints/apg-specs/patterns/<machine>.bridge` | the same for a non-APG pattern composed of APG widgets (`resizable-panels`) |
|
|
53
|
+
| `@cssints/apg-specs/patterns/<machine>.model.json` | its generated model document (`src/patterns/<machine>.model.generated.json`) |
|
|
50
54
|
| `@cssints/apg-specs/canvas/{canvas.bridge,camera.model.json,pointer.model.json}` | the canvas bridge and models |
|
|
51
55
|
| `@cssints/apg-specs/scenarios/apg/<pattern>.<set>.json` | a ported scenario set (also `scenarios/canvas/*.json`, `scenarios/parts.json`) |
|
|
52
|
-
| `@cssints/apg-specs/
|
|
56
|
+
| `@cssints/apg-specs/scenarios/patterns/<machine>.spec.json` | a scenario set written from our own pattern spec |
|
|
57
|
+
| `@cssints/apg-specs/specs/apg/<pattern>.qnt` | the raw Quint spec (also `specs/canvas/*.qnt`, `specs/patterns/*.qnt`, `shared/*.qnt`, which the specs import) |
|
|
53
58
|
|
|
54
59
|
```ts
|
|
55
60
|
import {
|
|
@@ -63,4 +68,4 @@ import buttonModel from "@cssints/apg-specs/apg/button.model.json" with { type:
|
|
|
63
68
|
import baseUi from "@cssints/apg-specs/scenarios/apg/button.base-ui.json" with { type: "json" };
|
|
64
69
|
```
|
|
65
70
|
|
|
66
|
-
In this repository the `development` (vite, tsc) and `bun` export conditions point at `src/`. The scenario format is specified in `scenarios/apg/README.md
|
|
71
|
+
In this repository the `development` (vite, tsc) and `bun` export conditions point at `src/`. The scenario format is specified in `scenarios/apg/README.md`; `scenarios/patterns/` holds the scenarios of our own non-APG patterns (same format, no upstream).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ActionStep, Probe, ScenarioConfig, SpecBridge } from "../scenario.mjs";
|
|
2
|
+
//#region src/patterns/resizable-panels.bridge.d.ts
|
|
3
|
+
export type PanelsOrientation = "vertical" | "horizontal";
|
|
4
|
+
/** The layouts the spec reaches by a host write (hostEqual / hostWide / ...). */
|
|
5
|
+
export declare const PANELS_LAYOUTS: {
|
|
6
|
+
readonly initial: readonly [20, 40, 40];
|
|
7
|
+
readonly equal: readonly [30, 30, 40];
|
|
8
|
+
readonly wide: readonly [50, 30, 20];
|
|
9
|
+
readonly narrow: readonly [10, 40, 50];
|
|
10
|
+
readonly collapsed: readonly [0, 50, 50];
|
|
11
|
+
};
|
|
12
|
+
export type PanelsLayout = keyof typeof PANELS_LAYOUTS;
|
|
13
|
+
export declare const PANEL_COUNT = 3;
|
|
14
|
+
export declare const PANEL_MIN = 10;
|
|
15
|
+
export declare const PANEL_MAX = 80;
|
|
16
|
+
/**
|
|
17
|
+
* Scenario `config` -> what both targets are mounted with. The spec is the
|
|
18
|
+
* three-pane instance (MIN 10, MAX 80, 10% steps, pane 0 collapsible at 0), so
|
|
19
|
+
* any other geometry is not modelled. `layout` only leaves the initial layout
|
|
20
|
+
* through a host write, which needs `controlled`; an uncontrolled scenario that
|
|
21
|
+
* starts elsewhere is `Unsupported` (it would have to be reached by keys, which
|
|
22
|
+
* no ported scenario needs).
|
|
23
|
+
*/
|
|
24
|
+
export interface PanelsConfig {
|
|
25
|
+
readonly orientation: PanelsOrientation;
|
|
26
|
+
readonly layout: PanelsLayout;
|
|
27
|
+
readonly sizes: ReadonlyArray<number>;
|
|
28
|
+
readonly controlled: boolean;
|
|
29
|
+
readonly accepts: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare const resizablePanelsConfig: (config: ScenarioConfig) => PanelsConfig;
|
|
32
|
+
/**
|
|
33
|
+
* Resizable panels vocabulary <-> `specs/patterns/resizable-panels.qnt`. Parts:
|
|
34
|
+
* `separator:0` / `separator:1` and `pane:0..2`; keys only act on the focused
|
|
35
|
+
* separator (focus it first, as a user would). The drag is its own vocabulary
|
|
36
|
+
* (`dragStart` on a separator, `dragBy` with a ±10 delta, `dragEnd`), so the
|
|
37
|
+
* spec's snapshot + delta model and the DOM's real pointer events see the same
|
|
38
|
+
* steps. `hostLayout` is the host writing a whole layout (controlled only).
|
|
39
|
+
*/
|
|
40
|
+
export declare const resizablePanelsBridge: SpecBridge;
|
|
41
|
+
/** The user-level steps a differential walk draws from. */
|
|
42
|
+
export declare const resizablePanelsSteps: ReadonlyArray<ActionStep>;
|
|
43
|
+
export declare const resizablePanelsProbe: Probe;
|
|
44
|
+
//#endregion
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { Unsupported } from "../scenario.mjs";
|
|
2
|
+
//#region src/patterns/resizable-panels.bridge.ts
|
|
3
|
+
const specEvent = (tag, params = {}) => ({
|
|
4
|
+
_tag: tag,
|
|
5
|
+
...params
|
|
6
|
+
});
|
|
7
|
+
const num = (value) => value === void 0 ? 0 : Number(value);
|
|
8
|
+
/** The layouts the spec reaches by a host write (hostEqual / hostWide / ...). */
|
|
9
|
+
const PANELS_LAYOUTS = {
|
|
10
|
+
initial: [
|
|
11
|
+
20,
|
|
12
|
+
40,
|
|
13
|
+
40
|
|
14
|
+
],
|
|
15
|
+
equal: [
|
|
16
|
+
30,
|
|
17
|
+
30,
|
|
18
|
+
40
|
|
19
|
+
],
|
|
20
|
+
wide: [
|
|
21
|
+
50,
|
|
22
|
+
30,
|
|
23
|
+
20
|
|
24
|
+
],
|
|
25
|
+
narrow: [
|
|
26
|
+
10,
|
|
27
|
+
40,
|
|
28
|
+
50
|
|
29
|
+
],
|
|
30
|
+
collapsed: [
|
|
31
|
+
0,
|
|
32
|
+
50,
|
|
33
|
+
50
|
|
34
|
+
]
|
|
35
|
+
};
|
|
36
|
+
const PANEL_COUNT = 3;
|
|
37
|
+
const PANEL_MIN = 10;
|
|
38
|
+
const PANEL_MAX = 80;
|
|
39
|
+
const isLayout = (value) => typeof value === "string" && Object.hasOwn(PANELS_LAYOUTS, value);
|
|
40
|
+
const resizablePanelsConfig = (config) => {
|
|
41
|
+
let orientation = "vertical";
|
|
42
|
+
let layout = "initial";
|
|
43
|
+
let controlled = false;
|
|
44
|
+
let accepts = true;
|
|
45
|
+
for (const [key, raw] of Object.entries(config)) if (key === "orientation" && (raw === "vertical" || raw === "horizontal")) orientation = raw;
|
|
46
|
+
else if (key === "layout" && isLayout(raw)) layout = raw;
|
|
47
|
+
else if (key === "controlled" && typeof raw === "boolean") controlled = raw;
|
|
48
|
+
else if (key === "accepts" && typeof raw === "boolean") accepts = raw;
|
|
49
|
+
else if (key === "collapsible" && raw === true) continue;
|
|
50
|
+
else if (key === "panes" && raw === 3) continue;
|
|
51
|
+
else if (key === "min" && raw === 10) continue;
|
|
52
|
+
else if (key === "max" && raw === 80) continue;
|
|
53
|
+
else throw new Unsupported(`config ${key}=${JSON.stringify(raw)}`);
|
|
54
|
+
if (!controlled && layout !== "initial") throw new Unsupported(`layout ${layout} without a controlled host`);
|
|
55
|
+
return {
|
|
56
|
+
orientation,
|
|
57
|
+
layout,
|
|
58
|
+
sizes: PANELS_LAYOUTS[layout],
|
|
59
|
+
controlled,
|
|
60
|
+
accepts
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
const separatorIndex = (target) => {
|
|
64
|
+
const match = /^separator:(\d+)$/.exec(target ?? "separator:0");
|
|
65
|
+
if (match === null) throw new Unsupported(`part ${target ?? ""}`);
|
|
66
|
+
const index = Number(match[1]);
|
|
67
|
+
if (index < 0 || index >= 2) throw new Unsupported(`separator ${index}`);
|
|
68
|
+
return index;
|
|
69
|
+
};
|
|
70
|
+
const HOST_EVENTS = {
|
|
71
|
+
initial: "hostEqual",
|
|
72
|
+
equal: "hostEqual",
|
|
73
|
+
wide: "hostWide",
|
|
74
|
+
narrow: "hostNarrow",
|
|
75
|
+
collapsed: "hostCollapsed"
|
|
76
|
+
};
|
|
77
|
+
const KEY_EVENTS = {
|
|
78
|
+
ArrowLeft: "left",
|
|
79
|
+
ArrowRight: "right",
|
|
80
|
+
ArrowUp: "up",
|
|
81
|
+
ArrowDown: "down",
|
|
82
|
+
Home: "home",
|
|
83
|
+
End: "end",
|
|
84
|
+
Enter: "enter"
|
|
85
|
+
};
|
|
86
|
+
const isKeyEvent = (key) => key in KEY_EVENTS;
|
|
87
|
+
/**
|
|
88
|
+
* Resizable panels vocabulary <-> `specs/patterns/resizable-panels.qnt`. Parts:
|
|
89
|
+
* `separator:0` / `separator:1` and `pane:0..2`; keys only act on the focused
|
|
90
|
+
* separator (focus it first, as a user would). The drag is its own vocabulary
|
|
91
|
+
* (`dragStart` on a separator, `dragBy` with a ±10 delta, `dragEnd`), so the
|
|
92
|
+
* spec's snapshot + delta model and the DOM's real pointer events see the same
|
|
93
|
+
* steps. `hostLayout` is the host writing a whole layout (controlled only).
|
|
94
|
+
*/
|
|
95
|
+
const resizablePanelsBridge = {
|
|
96
|
+
setup: (config) => {
|
|
97
|
+
const c = resizablePanelsConfig(config);
|
|
98
|
+
const events = [];
|
|
99
|
+
if (c.orientation === "horizontal") events.push(specEvent("orientHorizontal"));
|
|
100
|
+
if (c.controlled) {
|
|
101
|
+
events.push(specEvent("control"));
|
|
102
|
+
if (c.accepts) events.push(specEvent("acceptRequests"));
|
|
103
|
+
if (c.layout !== "initial") events.push(specEvent(HOST_EVENTS[c.layout]));
|
|
104
|
+
}
|
|
105
|
+
return events;
|
|
106
|
+
},
|
|
107
|
+
event: (step) => {
|
|
108
|
+
switch (step.event) {
|
|
109
|
+
case "focus": return [specEvent(`focusSep${separatorIndex(step.target)}`)];
|
|
110
|
+
case "blur": return [specEvent("blur")];
|
|
111
|
+
case "key": {
|
|
112
|
+
const key = step.key ?? "";
|
|
113
|
+
if (!isKeyEvent(key)) throw new Unsupported(`key ${key}`);
|
|
114
|
+
return [specEvent(KEY_EVENTS[key])];
|
|
115
|
+
}
|
|
116
|
+
case "dragStart": return [specEvent(`dragDown${separatorIndex(step.target)}`)];
|
|
117
|
+
case "dragBy": {
|
|
118
|
+
const delta = step.value;
|
|
119
|
+
if (delta === 10) return [specEvent("dragNext")];
|
|
120
|
+
if (delta === -10) return [specEvent("dragPrev")];
|
|
121
|
+
throw new Unsupported(`drag delta ${JSON.stringify(delta)}`);
|
|
122
|
+
}
|
|
123
|
+
case "dragEnd":
|
|
124
|
+
case "dragCancel": return [specEvent("dragUp")];
|
|
125
|
+
case "hover":
|
|
126
|
+
case "unhover": return [];
|
|
127
|
+
case "prop": switch (step.prop) {
|
|
128
|
+
case "orientation":
|
|
129
|
+
if (step.value === "horizontal") return [specEvent("orientHorizontal")];
|
|
130
|
+
if (step.value === "vertical") return [specEvent("orientVertical")];
|
|
131
|
+
throw new Unsupported(`orientation ${JSON.stringify(step.value)}`);
|
|
132
|
+
case "hostLayout":
|
|
133
|
+
if (!isLayout(step.value)) throw new Unsupported(`layout ${JSON.stringify(step.value)}`);
|
|
134
|
+
if (step.value === "initial") throw new Unsupported("layout initial is not a host write");
|
|
135
|
+
return [specEvent(HOST_EVENTS[step.value])];
|
|
136
|
+
default: throw new Unsupported(`prop ${step.prop ?? ""}`);
|
|
137
|
+
}
|
|
138
|
+
default: throw new Unsupported(`event ${step.event}`);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
value: (state) => [
|
|
142
|
+
num(state["s0"]),
|
|
143
|
+
num(state["s1"]),
|
|
144
|
+
num(state["s2"])
|
|
145
|
+
],
|
|
146
|
+
focus: (state) => {
|
|
147
|
+
switch (state["focus"]) {
|
|
148
|
+
case "sep0": return "separator:0";
|
|
149
|
+
case "sep1": return "separator:1";
|
|
150
|
+
default: return null;
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
attr: (state, target, name) => {
|
|
154
|
+
const index = separatorIndex(target);
|
|
155
|
+
switch (name) {
|
|
156
|
+
case "role": return "separator";
|
|
157
|
+
case "aria-orientation": return state["o"] === "horizontal" ? "horizontal" : "vertical";
|
|
158
|
+
case "aria-valuenow": return String(num(state[`s${index}`]));
|
|
159
|
+
case "aria-valuemin": return String(10);
|
|
160
|
+
case "aria-valuemax": return String(80);
|
|
161
|
+
case "tabindex": return "0";
|
|
162
|
+
default: throw new Unsupported(`attribute ${name} on ${target}`);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
visible: () => [],
|
|
166
|
+
events: (before, after) => before["requests"] === after["requests"] ? [] : [["onSizesChange", [
|
|
167
|
+
num(after["rq0"]),
|
|
168
|
+
num(after["rq1"]),
|
|
169
|
+
num(after["rq2"])
|
|
170
|
+
]]]
|
|
171
|
+
};
|
|
172
|
+
/** The user-level steps a differential walk draws from. */
|
|
173
|
+
const resizablePanelsSteps = [
|
|
174
|
+
{
|
|
175
|
+
event: "focus",
|
|
176
|
+
target: "separator:0"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
event: "focus",
|
|
180
|
+
target: "separator:1"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
event: "blur",
|
|
184
|
+
target: "separator:0"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
event: "key",
|
|
188
|
+
key: "ArrowLeft",
|
|
189
|
+
target: "separator:0"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
event: "key",
|
|
193
|
+
key: "ArrowRight",
|
|
194
|
+
target: "separator:1"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
event: "key",
|
|
198
|
+
key: "ArrowUp",
|
|
199
|
+
target: "separator:0"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
event: "key",
|
|
203
|
+
key: "ArrowDown",
|
|
204
|
+
target: "separator:1"
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
event: "key",
|
|
208
|
+
key: "Home",
|
|
209
|
+
target: "separator:0"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
event: "key",
|
|
213
|
+
key: "End",
|
|
214
|
+
target: "separator:1"
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
event: "key",
|
|
218
|
+
key: "Enter",
|
|
219
|
+
target: "separator:0"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
event: "key",
|
|
223
|
+
key: "Enter",
|
|
224
|
+
target: "separator:1"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
event: "prop",
|
|
228
|
+
prop: "orientation",
|
|
229
|
+
value: "horizontal"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
event: "prop",
|
|
233
|
+
prop: "orientation",
|
|
234
|
+
value: "vertical"
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
event: "prop",
|
|
238
|
+
prop: "hostLayout",
|
|
239
|
+
value: "wide"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
event: "prop",
|
|
243
|
+
prop: "hostLayout",
|
|
244
|
+
value: "narrow"
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
event: "prop",
|
|
248
|
+
prop: "hostLayout",
|
|
249
|
+
value: "collapsed"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
event: "prop",
|
|
253
|
+
prop: "hostLayout",
|
|
254
|
+
value: "equal"
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
event: "dragStart",
|
|
258
|
+
target: "separator:0"
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
event: "dragStart",
|
|
262
|
+
target: "separator:1"
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
event: "dragBy",
|
|
266
|
+
value: 10
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
event: "dragBy",
|
|
270
|
+
value: -10
|
|
271
|
+
},
|
|
272
|
+
{ event: "dragEnd" }
|
|
273
|
+
];
|
|
274
|
+
const resizablePanelsProbe = {
|
|
275
|
+
attrs: [
|
|
276
|
+
["separator:0", "role"],
|
|
277
|
+
["separator:0", "aria-orientation"],
|
|
278
|
+
["separator:0", "aria-valuenow"],
|
|
279
|
+
["separator:0", "aria-valuemin"],
|
|
280
|
+
["separator:0", "aria-valuemax"],
|
|
281
|
+
["separator:0", "tabindex"],
|
|
282
|
+
["separator:1", "aria-valuenow"]
|
|
283
|
+
],
|
|
284
|
+
events: ["onSizesChange"]
|
|
285
|
+
};
|
|
286
|
+
//#endregion
|
|
287
|
+
export { PANELS_LAYOUTS, PANEL_COUNT, PANEL_MAX, PANEL_MIN, resizablePanelsBridge, resizablePanelsConfig, resizablePanelsProbe, resizablePanelsSteps };
|