@ggui-ai/ui-visual-tester 0.19.0 → 0.21.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.
@@ -1 +1 @@
1
- {"version":3,"file":"fixture-runtime.d.ts","sourceRoot":"","sources":["../src/fixture-runtime.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fixture-runtime.d.ts","sourceRoot":"","sources":["../src/fixture-runtime.ts"],"names":[],"mappings":"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"}
@@ -26,6 +26,7 @@ import * as Wire from '@ggui-ai/wire';
26
26
  // in-repo consumers of the compile pipeline use.
27
27
  import { hoistImports, loadModule } from '@ggui-ai/design/module-loader';
28
28
  import { rewriteImports, stripMarkers, } from '@ggui-ai/design/rendering';
29
+ import { primeInputs } from './prime-inputs.js';
29
30
  function parseActionEntry(raw) {
30
31
  if (raw === null || typeof raw !== 'object')
31
32
  return null;
@@ -150,156 +151,6 @@ function collectActionCandidates(root, name, label) {
150
151
  }
151
152
  return { named: [...byValue, ...byText], fallback: rest, disabled, namedDisabled, allClickables: every };
152
153
  }
153
- // #1040: date-like types too — a wizard's first step is routinely gated on one ("preferred follow-up
154
- // date"), and a Next that never enables hides every step behind it from the probe.
155
- const TEXT_LIKE_INPUT_TYPES = new Set([
156
- 'text', 'email', 'search', 'url', 'tel', 'number', 'password', '',
157
- 'date', 'time', 'month', 'week', 'datetime-local',
158
- ]);
159
- /**
160
- * #1040: dates are primed RELATIVE TO NOW — a form routinely refuses a date in the past
161
- * ("preferred follow-up date"), and a constant sample date is a past date. Thirty days out,
162
- * clamped into the input's own [min, max] when those are set in the input's format (ISO
163
- * strings compare lexically). Time-of-day values have no past to fall into.
164
- */
165
- const pad2 = (n) => String(n).padStart(2, '0');
166
- const partsOf = (at) => ({ y: at.getFullYear(), m: at.getMonth() + 1, d: at.getDate() });
167
- /** Today and thirty days out, as the parts a date-like value is built from. */
168
- function todayAndThirtyDaysOut() {
169
- const now = new Date();
170
- const at = new Date(now);
171
- at.setDate(at.getDate() + 30);
172
- return { today: partsOf(now), later: partsOf(at) };
173
- }
174
- /**
175
- * Clamp a value into the input's [min, max] when each is set in the input's own format
176
- * (ISO-shaped strings compare lexically). `min` later than the value wins; `max` earlier than
177
- * the value wins only while it is not before `floor` (today in the same format) — a max in
178
- * the past is a form that can never be satisfied, and the probe reports that as it is.
179
- */
180
- function clampToRange(el, value, floor, format) {
181
- let out = value;
182
- if (format.test(el.min) && el.min > out)
183
- out = el.min;
184
- if (format.test(el.max) && el.max < out && el.max >= floor)
185
- out = el.max;
186
- return out;
187
- }
188
- function isoWeekOf(y, m, d) {
189
- // ISO-8601 week: Thursday of the same week decides the year.
190
- const date = new Date(Date.UTC(y, m - 1, d));
191
- const day = date.getUTCDay() || 7;
192
- date.setUTCDate(date.getUTCDate() + 4 - day);
193
- const jan1 = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
194
- const week = Math.ceil(((date.getTime() - jan1.getTime()) / 86_400_000 + 1) / 7);
195
- return `${date.getUTCFullYear()}-W${pad2(week)}`;
196
- }
197
- function sampleValueFor(el) {
198
- if (!(el instanceof HTMLInputElement))
199
- return 'probe'; // a textarea has no type to switch on
200
- switch (el.type) {
201
- case 'number':
202
- return '1';
203
- case 'email':
204
- return 'probe@example.com';
205
- case 'url':
206
- return 'https://example.com';
207
- case 'tel':
208
- return '5550100';
209
- case 'date': {
210
- const { today, later } = todayAndThirtyDaysOut();
211
- const iso = (p) => `${p.y}-${pad2(p.m)}-${pad2(p.d)}`;
212
- return clampToRange(el, iso(later), iso(today), /^\d{4}-\d{2}-\d{2}$/);
213
- }
214
- case 'time':
215
- return '09:30';
216
- case 'month': {
217
- const { today, later } = todayAndThirtyDaysOut();
218
- const ym = (p) => `${p.y}-${pad2(p.m)}`;
219
- return clampToRange(el, ym(later), ym(today), /^\d{4}-\d{2}$/);
220
- }
221
- case 'week': {
222
- const { today, later } = todayAndThirtyDaysOut();
223
- return clampToRange(el, isoWeekOf(later.y, later.m, later.d), isoWeekOf(today.y, today.m, today.d), /^\d{4}-W\d{2}$/);
224
- }
225
- case 'datetime-local': {
226
- const { today, later } = todayAndThirtyDaysOut();
227
- const at = (p, hm) => `${p.y}-${pad2(p.m)}-${pad2(p.d)}T${hm}`;
228
- return clampToRange(el, at(later, '09:30'), at(today, '00:00'), /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/);
229
- }
230
- default:
231
- return 'probe';
232
- }
233
- }
234
- /** Set a value the way React notices it: through the prototype's native setter, then an input + change event. */
235
- function setNativeValue(el, value) {
236
- const proto = el instanceof HTMLTextAreaElement
237
- ? HTMLTextAreaElement.prototype
238
- : el instanceof HTMLSelectElement
239
- ? HTMLSelectElement.prototype
240
- : HTMLInputElement.prototype;
241
- const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
242
- if (setter)
243
- setter.call(el, value);
244
- else
245
- el.value = value;
246
- el.dispatchEvent(new Event('input', { bubbles: true }));
247
- el.dispatchEvent(new Event('change', { bubbles: true }));
248
- }
249
- /**
250
- * The generic minimum a user does before pressing Send / Submit / Complete
251
- * (#1021): give every empty text-like input and textarea a value, every
252
- * unset select its first real option, every unchecked radio group its first
253
- * radio. Checkboxes are never touched — they are often the action itself.
254
- * Returns how many controls were primed.
255
- */
256
- function primeInputs(root) {
257
- let primed = 0;
258
- const editable = (el) => !el.disabled && !(el instanceof HTMLSelectElement) ? !el.readOnly : !el.disabled;
259
- for (const el of Array.from(root.querySelectorAll('input, textarea'))) {
260
- if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) || !editable(el))
261
- continue;
262
- if (el instanceof HTMLInputElement && !TEXT_LIKE_INPUT_TYPES.has(el.type))
263
- continue;
264
- if (el.value.trim().length > 0)
265
- continue;
266
- setNativeValue(el, sampleValueFor(el));
267
- if (el.value.trim().length > 0)
268
- primed += 1;
269
- }
270
- for (const el of Array.from(root.querySelectorAll('select'))) {
271
- if (!(el instanceof HTMLSelectElement) || el.disabled)
272
- continue;
273
- const current = el.options[el.selectedIndex];
274
- if (current !== undefined && current.value.trim().length > 0)
275
- continue;
276
- const first = Array.from(el.options).find((o) => o.value.trim().length > 0 && !o.disabled);
277
- if (first === undefined)
278
- continue;
279
- setNativeValue(el, first.value);
280
- primed += 1;
281
- }
282
- const seenGroups = new Set();
283
- for (const el of Array.from(root.querySelectorAll('input[type="radio"]'))) {
284
- if (!(el instanceof HTMLInputElement) || el.disabled)
285
- continue;
286
- const group = el.name || '';
287
- if (seenGroups.has(group))
288
- continue;
289
- seenGroups.add(group);
290
- const members = group
291
- ? Array.from(root.querySelectorAll(`input[type="radio"][name="${CSS.escape(group)}"]`)).filter((m) => m instanceof HTMLInputElement)
292
- : [el];
293
- if (members.some((m) => m.checked))
294
- continue;
295
- const first = members.find((m) => !m.disabled);
296
- if (first === undefined)
297
- continue;
298
- first.click();
299
- primed += 1;
300
- }
301
- return primed;
302
- }
303
154
  function countNamedDispatches(actionName) {
304
155
  return (window.__ggui_test_dispatches__ ?? []).filter((d) => d.actionName === actionName).length;
305
156
  }
@@ -0,0 +1,12 @@
1
+ export declare function sampleValueFor(el: HTMLInputElement | HTMLTextAreaElement): string;
2
+ /** Set a value the way React notices it: through the prototype's native setter, then an input + change event. */
3
+ export declare function setNativeValue(el: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, value: string): void;
4
+ /**
5
+ * The generic minimum a user does before pressing Send / Submit / Complete
6
+ * (#1021): give every empty text-like input and textarea a value, every
7
+ * unset select its first real option, every unchecked radio group its first
8
+ * radio. Checkboxes are never touched — they are often the action itself.
9
+ * Returns how many controls were primed.
10
+ */
11
+ export declare function primeInputs(root: HTMLElement): number;
12
+ //# sourceMappingURL=prime-inputs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prime-inputs.d.ts","sourceRoot":"","sources":["../src/prime-inputs.ts"],"names":[],"mappings":"AA2EA,wBAAgB,cAAc,CAAC,EAAE,EAAE,gBAAgB,GAAG,mBAAmB,GAAG,MAAM,CAmCjF;AAED,iHAAiH;AACjH,wBAAgB,cAAc,CAAC,EAAE,EAAE,gBAAgB,GAAG,mBAAmB,GAAG,iBAAiB,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAYlH;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CA0CrD"}
@@ -0,0 +1,182 @@
1
+ // oss/packages/ui-visual-tester/src/prime-inputs.ts
2
+ //
3
+ // Shared input-priming for the "generic minimum a user does before pressing
4
+ // Send / Submit / Complete" (#1021), incl. the #1040 relative-to-now date rule.
5
+ // Extracted from fixture-runtime.ts so BOTH the Playwright fixture runtime and
6
+ // the ui-gen runtime-render action-wiring probe (ggui#1187) prime inputs the
7
+ // SAME way — one typed-values table, no divergence. Pure DOM + Date only (no
8
+ // Playwright), so it is safe to import into the lightweight happy-dom check
9
+ // worker via the `@ggui-ai/ui-visual-tester/prime-inputs` subpath.
10
+ //
11
+ // Realm rule (ggui#1187 follow-up): every constructor this module needs —
12
+ // `HTMLInputElement`, `HTMLTextAreaElement`, `HTMLSelectElement`, `Event` —
13
+ // is read from the ROOT'S OWN window (`ownerDocument.defaultView`), never
14
+ // from ambient globals. The check worker installs happy-dom's `window` and
15
+ // `document` on `globalThis` without the element constructors, so an
16
+ // `instanceof HTMLInputElement` there is a `ReferenceError`, not a `false`;
17
+ // a browser page or vitest's happy-dom environment happens to have them.
18
+ // One code path, every realm.
19
+ /** The realm an element lives in. A detached document has none — that is a caller error, reported as one. */
20
+ function realmOf(el) {
21
+ const view = el.ownerDocument.defaultView;
22
+ if (view === null) {
23
+ throw new Error('primeInputs: the element is not attached to a window (ownerDocument.defaultView is null) — a detached document has no constructors to prime with');
24
+ }
25
+ return view;
26
+ }
27
+ // #1040: date-like types too — a wizard's first step is routinely gated on one ("preferred follow-up
28
+ // date"), and a Next that never enables hides every step behind it from the probe.
29
+ const TEXT_LIKE_INPUT_TYPES = new Set([
30
+ 'text', 'email', 'search', 'url', 'tel', 'number', 'password', '',
31
+ 'date', 'time', 'month', 'week', 'datetime-local',
32
+ ]);
33
+ /**
34
+ * #1040: dates are primed RELATIVE TO NOW — a form routinely refuses a date in the past
35
+ * ("preferred follow-up date"), and a constant sample date is a past date. Thirty days out,
36
+ * clamped into the input's own [min, max] when those are set in the input's format (ISO
37
+ * strings compare lexically). Time-of-day values have no past to fall into.
38
+ */
39
+ const pad2 = (n) => String(n).padStart(2, '0');
40
+ const partsOf = (at) => ({ y: at.getFullYear(), m: at.getMonth() + 1, d: at.getDate() });
41
+ /** Today and thirty days out, as the parts a date-like value is built from. */
42
+ function todayAndThirtyDaysOut() {
43
+ const now = new Date();
44
+ const at = new Date(now);
45
+ at.setDate(at.getDate() + 30);
46
+ return { today: partsOf(now), later: partsOf(at) };
47
+ }
48
+ /**
49
+ * Clamp a value into the input's [min, max] when each is set in the input's own format
50
+ * (ISO-shaped strings compare lexically). `min` later than the value wins; `max` earlier than
51
+ * the value wins only while it is not before `floor` (today in the same format) — a max in
52
+ * the past is a form that can never be satisfied, and the probe reports that as it is.
53
+ */
54
+ function clampToRange(el, value, floor, format) {
55
+ let out = value;
56
+ if (format.test(el.min) && el.min > out)
57
+ out = el.min;
58
+ if (format.test(el.max) && el.max < out && el.max >= floor)
59
+ out = el.max;
60
+ return out;
61
+ }
62
+ function isoWeekOf(y, m, d) {
63
+ // ISO-8601 week: Thursday of the same week decides the year.
64
+ const date = new Date(Date.UTC(y, m - 1, d));
65
+ const day = date.getUTCDay() || 7;
66
+ date.setUTCDate(date.getUTCDate() + 4 - day);
67
+ const jan1 = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
68
+ const week = Math.ceil(((date.getTime() - jan1.getTime()) / 86_400_000 + 1) / 7);
69
+ return `${date.getUTCFullYear()}-W${pad2(week)}`;
70
+ }
71
+ export function sampleValueFor(el) {
72
+ if (!(el instanceof realmOf(el).HTMLInputElement))
73
+ return 'probe'; // a textarea has no type to switch on
74
+ switch (el.type) {
75
+ case 'number':
76
+ return '1';
77
+ case 'email':
78
+ return 'probe@example.com';
79
+ case 'url':
80
+ return 'https://example.com';
81
+ case 'tel':
82
+ return '5550100';
83
+ case 'date': {
84
+ const { today, later } = todayAndThirtyDaysOut();
85
+ const iso = (p) => `${p.y}-${pad2(p.m)}-${pad2(p.d)}`;
86
+ return clampToRange(el, iso(later), iso(today), /^\d{4}-\d{2}-\d{2}$/);
87
+ }
88
+ case 'time':
89
+ return '09:30';
90
+ case 'month': {
91
+ const { today, later } = todayAndThirtyDaysOut();
92
+ const ym = (p) => `${p.y}-${pad2(p.m)}`;
93
+ return clampToRange(el, ym(later), ym(today), /^\d{4}-\d{2}$/);
94
+ }
95
+ case 'week': {
96
+ const { today, later } = todayAndThirtyDaysOut();
97
+ return clampToRange(el, isoWeekOf(later.y, later.m, later.d), isoWeekOf(today.y, today.m, today.d), /^\d{4}-W\d{2}$/);
98
+ }
99
+ case 'datetime-local': {
100
+ const { today, later } = todayAndThirtyDaysOut();
101
+ const at = (p, hm) => `${p.y}-${pad2(p.m)}-${pad2(p.d)}T${hm}`;
102
+ return clampToRange(el, at(later, '09:30'), at(today, '00:00'), /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/);
103
+ }
104
+ default:
105
+ return 'probe';
106
+ }
107
+ }
108
+ /** Set a value the way React notices it: through the prototype's native setter, then an input + change event. */
109
+ export function setNativeValue(el, value) {
110
+ const view = realmOf(el);
111
+ const proto = el instanceof view.HTMLTextAreaElement
112
+ ? view.HTMLTextAreaElement.prototype
113
+ : el instanceof view.HTMLSelectElement
114
+ ? view.HTMLSelectElement.prototype
115
+ : view.HTMLInputElement.prototype;
116
+ const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
117
+ if (setter)
118
+ setter.call(el, value);
119
+ else
120
+ el.value = value;
121
+ el.dispatchEvent(new view.Event('input', { bubbles: true }));
122
+ el.dispatchEvent(new view.Event('change', { bubbles: true }));
123
+ }
124
+ /**
125
+ * The generic minimum a user does before pressing Send / Submit / Complete
126
+ * (#1021): give every empty text-like input and textarea a value, every
127
+ * unset select its first real option, every unchecked radio group its first
128
+ * radio. Checkboxes are never touched — they are often the action itself.
129
+ * Returns how many controls were primed.
130
+ */
131
+ export function primeInputs(root) {
132
+ const view = realmOf(root);
133
+ let primed = 0;
134
+ const editable = (el) => !el.disabled && !el.readOnly;
135
+ for (const el of Array.from(root.querySelectorAll('input, textarea'))) {
136
+ if (!(el instanceof view.HTMLInputElement || el instanceof view.HTMLTextAreaElement) || !editable(el))
137
+ continue;
138
+ if (el instanceof view.HTMLInputElement && !TEXT_LIKE_INPUT_TYPES.has(el.type))
139
+ continue;
140
+ if (el.value.trim().length > 0)
141
+ continue;
142
+ setNativeValue(el, sampleValueFor(el));
143
+ if (el.value.trim().length > 0)
144
+ primed += 1;
145
+ }
146
+ for (const el of Array.from(root.querySelectorAll('select'))) {
147
+ if (!(el instanceof view.HTMLSelectElement) || el.disabled)
148
+ continue;
149
+ const current = el.options[el.selectedIndex];
150
+ if (current !== undefined && current.value.trim().length > 0)
151
+ continue;
152
+ const first = Array.from(el.options).find((o) => o.value.trim().length > 0 && !o.disabled);
153
+ if (first === undefined)
154
+ continue;
155
+ setNativeValue(el, first.value);
156
+ primed += 1;
157
+ }
158
+ // Radios: one pick per named group; an unnamed radio is its own group.
159
+ // Grouping is done in JS (by `name` equality), not by re-querying with the
160
+ // name interpolated into a selector — no `CSS.escape`, no realm dependency.
161
+ const radios = Array.from(root.querySelectorAll('input[type="radio"]')).filter((m) => m instanceof view.HTMLInputElement);
162
+ const seenGroups = new Set();
163
+ for (const el of radios) {
164
+ if (el.disabled)
165
+ continue;
166
+ const group = el.name;
167
+ if (group !== '') {
168
+ if (seenGroups.has(group))
169
+ continue;
170
+ seenGroups.add(group);
171
+ }
172
+ const members = group !== '' ? radios.filter((m) => m.name === group) : [el];
173
+ if (members.some((m) => m.checked))
174
+ continue;
175
+ const first = members.find((m) => !m.disabled);
176
+ if (first === undefined)
177
+ continue;
178
+ first.click();
179
+ primed += 1;
180
+ }
181
+ return primed;
182
+ }
@@ -20580,14 +20580,29 @@
20580
20580
  easeInOut: "cubic-bezier(0.4, 0, 0.2, 1)",
20581
20581
  spring: "cubic-bezier(0.175, 0.885, 0.32, 1.275)"
20582
20582
  };
20583
+ var motionVar = {
20584
+ duration: {
20585
+ fast: "var(--ggui-motion-duration-fast)",
20586
+ base: "var(--ggui-motion-duration-base)",
20587
+ slow: "var(--ggui-motion-duration-slow)"
20588
+ },
20589
+ easing: {
20590
+ /** The system's ease-in-out — the default for colour, border and size changes. */
20591
+ standard: "var(--ggui-motion-easing-standard)",
20592
+ /** The decelerating arrival — transforms and entrances. */
20593
+ emphasized: "var(--ggui-motion-easing-emphasized)",
20594
+ /** The accelerating departure. */
20595
+ exit: "var(--ggui-motion-easing-exit)"
20596
+ }
20597
+ };
20583
20598
  var transition = {
20584
20599
  none: "none",
20585
- fast: `${duration.fast} ${easing.easeInOut}`,
20586
- normal: `${duration.normal} ${easing.easeInOut}`,
20587
- slow: `${duration.slow} ${easing.easeInOut}`,
20588
- colors: `color ${duration.normal} ${easing.easeInOut}, background-color ${duration.normal} ${easing.easeInOut}, border-color ${duration.normal} ${easing.easeInOut}`,
20589
- opacity: `opacity ${duration.normal} ${easing.easeInOut}`,
20590
- transform: `transform ${duration.normal} ${easing.easeOut}`
20600
+ fast: `${motionVar.duration.fast} ${motionVar.easing.standard}`,
20601
+ normal: `${motionVar.duration.base} ${motionVar.easing.standard}`,
20602
+ slow: `${motionVar.duration.slow} ${motionVar.easing.standard}`,
20603
+ colors: `color ${motionVar.duration.base} ${motionVar.easing.standard}, background-color ${motionVar.duration.base} ${motionVar.easing.standard}, border-color ${motionVar.duration.base} ${motionVar.easing.standard}`,
20604
+ opacity: `opacity ${motionVar.duration.base} ${motionVar.easing.standard}`,
20605
+ transform: `transform ${motionVar.duration.base} ${motionVar.easing.emphasized}`
20591
20606
  };
20592
20607
 
20593
20608
  // ../design/dist/primitives/Button.js
@@ -20663,7 +20678,7 @@
20663
20678
  cursor: isDisabled ? "not-allowed" : "pointer",
20664
20679
  opacity: isDisabled ? 0.5 : 1,
20665
20680
  width: fullWidth ? "100%" : void 0,
20666
- transition: `background-color ${duration.normal} ${easing.easeInOut}, box-shadow ${duration.normal} ${easing.easeInOut}, opacity ${duration.normal} ${easing.easeInOut}`,
20681
+ transition: `background-color ${motionVar.duration.base} ${motionVar.easing.standard}, box-shadow ${motionVar.duration.base} ${motionVar.easing.standard}, opacity ${motionVar.duration.base} ${motionVar.easing.standard}`,
20667
20682
  boxShadow: "var(--ggui-shape-shadow-sm, 0 1px 2px rgba(0,0,0,0.05))",
20668
20683
  ...style
20669
20684
  }, ...rest, children: loading ? (0, import_jsx_runtime8.jsx)(Spinner, { size: 16, tone: "inherit" }) : (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [leftIcon, children, rightIcon] }) });
@@ -20708,7 +20723,7 @@
20708
20723
  border: `1px solid ${hasError ? "var(--ggui-color-error-500, #ef4444)" : "var(--ggui-color-outline, #d4d4d8)"}`,
20709
20724
  backgroundColor: disabled ? "var(--ggui-color-sunken, #fafafa)" : "var(--ggui-color-sunken, #ffffff)",
20710
20725
  color: "var(--ggui-color-onContainer, #18181b)",
20711
- transition: `border-color ${duration.normal} ${easing.easeInOut}, box-shadow ${duration.normal} ${easing.easeInOut}`,
20726
+ transition: `border-color ${motionVar.duration.base} ${motionVar.easing.standard}, box-shadow ${motionVar.duration.base} ${motionVar.easing.standard}`,
20712
20727
  boxSizing: "border-box"
20713
20728
  }, ...rest }), hasMessage && (0, import_jsx_runtime9.jsx)("span", { id: messageId, role: hasError ? "alert" : void 0, style: {
20714
20729
  fontSize: "var(--ggui-font-size-xs, 12px)",
@@ -20744,7 +20759,7 @@
20744
20759
  color: "var(--ggui-color-onContainer, #18181b)",
20745
20760
  fontSize: "var(--ggui-font-size-sm, 14px)",
20746
20761
  fontFamily: "inherit",
20747
- transition: `border-color ${duration.normal} ${easing.easeInOut}, box-shadow ${duration.normal} ${easing.easeInOut}`,
20762
+ transition: `border-color ${motionVar.duration.base} ${motionVar.easing.standard}, box-shadow ${motionVar.duration.base} ${motionVar.easing.standard}`,
20748
20763
  boxSizing: "border-box",
20749
20764
  resize: autoResize ? "none" : "vertical"
20750
20765
  }, ...rest }), (0, import_jsx_runtime10.jsxs)("div", { style: { display: "flex", justifyContent: "space-between" }, children: [hasMessage && (0, import_jsx_runtime10.jsx)("span", { id: messageId, role: hasError ? "alert" : void 0, style: {
@@ -20789,7 +20804,7 @@
20789
20804
  backgroundPosition: "right 12px center",
20790
20805
  paddingRight: "36px",
20791
20806
  boxSizing: "border-box",
20792
- transition: `border-color ${duration.normal} ${easing.easeInOut}, box-shadow ${duration.normal} ${easing.easeInOut}`
20807
+ transition: `border-color ${motionVar.duration.base} ${motionVar.easing.standard}, box-shadow ${motionVar.duration.base} ${motionVar.easing.standard}`
20793
20808
  }, ...rest, children: [placeholder && (0, import_jsx_runtime11.jsx)("option", { value: "", disabled: true, children: placeholder }), options.map((opt) => (0, import_jsx_runtime11.jsx)("option", { value: opt.value, disabled: opt.disabled, children: opt.label }, opt.value))] }), hasMessage && (0, import_jsx_runtime11.jsx)("span", { id: messageId, role: hasError ? "alert" : void 0, style: {
20794
20809
  fontSize: "var(--ggui-font-size-xs, 12px)",
20795
20810
  color: hasError ? "var(--ggui-color-error-500, #ef4444)" : "var(--ggui-color-onSunken, #52525b)"
@@ -20831,7 +20846,7 @@
20831
20846
  display: "flex",
20832
20847
  alignItems: "center",
20833
20848
  justifyContent: "center",
20834
- transition: "all 0.2s"
20849
+ transition: `all ${motionVar.duration.base} ${motionVar.easing.standard}`
20835
20850
  }, children: (checked || indeterminate) && (0, import_jsx_runtime12.jsx)("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", style: { color: "var(--ggui-color-onPrimary, #ffffff)" }, children: indeterminate ? (0, import_jsx_runtime12.jsx)("path", { d: "M2.5 6H9.5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) : (0, import_jsx_runtime12.jsx)("path", { d: "M2.5 6L5 8.5L9.5 3.5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] }), (label || description) && (0, import_jsx_runtime12.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [label && (0, import_jsx_runtime12.jsx)("span", { style: {
20836
20851
  fontSize: "var(--ggui-font-size-sm, 14px)",
20837
20852
  fontWeight: "var(--ggui-font-weight-medium, 500)",
@@ -20873,7 +20888,7 @@
20873
20888
  borderRadius: "9999px",
20874
20889
  backgroundColor: checked ? "var(--ggui-color-primary-600, #0284c7)" : "var(--ggui-color-outline, #d4d4d8)",
20875
20890
  position: "relative",
20876
- transition: `background-color ${duration.normal} ${easing.easeInOut}, box-shadow ${duration.normal} ${easing.easeInOut}`,
20891
+ transition: `background-color ${motionVar.duration.base} ${motionVar.easing.standard}, box-shadow ${motionVar.duration.base} ${motionVar.easing.standard}`,
20877
20892
  flexShrink: 0,
20878
20893
  outline: "none",
20879
20894
  boxShadow: focused ? "0 0 0 3px var(--ggui-color-primary-200, #bae6fd)" : void 0
@@ -20885,7 +20900,7 @@
20885
20900
  position: "absolute",
20886
20901
  top: (config.height - config.knob) / 2,
20887
20902
  left: checked ? config.width - config.knob - (config.height - config.knob) / 2 : (config.height - config.knob) / 2,
20888
- transition: `left ${duration.normal} ${easing.easeInOut}`,
20903
+ transition: `left ${motionVar.duration.base} ${motionVar.easing.standard}`,
20889
20904
  boxShadow: "var(--ggui-shape-shadow-sm, 0 1px 2px rgba(0,0,0,0.05))"
20890
20905
  } }) }), label && (0, import_jsx_runtime13.jsx)("span", { style: {
20891
20906
  fontSize: "var(--ggui-font-size-sm, 14px)",
@@ -20943,7 +20958,7 @@
20943
20958
  display: "flex",
20944
20959
  alignItems: "center",
20945
20960
  justifyContent: "center",
20946
- transition: "all 0.2s"
20961
+ transition: `all ${motionVar.duration.base} ${motionVar.easing.standard}`
20947
20962
  }, children: isSelected && (0, import_jsx_runtime14.jsx)("div", { style: {
20948
20963
  width: "8px",
20949
20964
  height: "8px",
@@ -20991,7 +21006,7 @@
20991
21006
  height: "6px",
20992
21007
  borderRadius: "3px",
20993
21008
  backgroundColor: disabled ? "var(--ggui-color-outline, #d4d4d8)" : "var(--ggui-color-primary-600, #0284c7)",
20994
- transition: "width 0.1s"
21009
+ transition: `width ${motionVar.duration.fast} ${motionVar.easing.standard}`
20995
21010
  } }), (0, import_jsx_runtime15.jsx)("input", { id: sliderId, type: "range", min, max, step, value, onChange: (e) => onChange?.(Number(e.target.value)), disabled, "aria-labelledby": label ? labelId : void 0, "aria-label": label ? void 0 : "Slider", "aria-valuenow": value, "aria-valuemin": min, "aria-valuemax": max, style: {
20996
21011
  position: "absolute",
20997
21012
  width: "100%",
@@ -21009,7 +21024,7 @@
21009
21024
  backgroundColor: "#ffffff",
21010
21025
  border: `2px solid ${disabled ? "var(--ggui-color-outline, #d4d4d8)" : "var(--ggui-color-primary-600, #0284c7)"}`,
21011
21026
  boxShadow: "var(--ggui-shape-shadow-sm, 0 1px 2px rgba(0,0,0,0.05))",
21012
- transition: "left 0.1s",
21027
+ transition: `left ${motionVar.duration.fast} ${motionVar.easing.standard}`,
21013
21028
  pointerEvents: "none"
21014
21029
  } })] })] });
21015
21030
  }
@@ -21237,7 +21252,7 @@
21237
21252
  height: "100%",
21238
21253
  borderRadius: height / 2,
21239
21254
  backgroundColor: barColor,
21240
- transition: indeterminate ? "none" : "width 0.3s ease",
21255
+ transition: indeterminate ? "none" : `width ${motionVar.duration.slow} ${motionVar.easing.standard}`,
21241
21256
  ...indeterminate ? {
21242
21257
  width: "30%",
21243
21258
  animation: "ggui-progress-indeterminate 1.5s ease-in-out infinite"
@@ -25869,7 +25884,7 @@
25869
25884
  color: linkColor,
25870
25885
  textDecoration: underlineStyle,
25871
25886
  cursor: "pointer",
25872
- transition: "color 0.2s",
25887
+ transition: `color ${motionVar.duration.base} ${motionVar.easing.standard}`,
25873
25888
  ...style
25874
25889
  }, onMouseEnter: (e) => {
25875
25890
  if (underline === "hover") {
@@ -26029,7 +26044,7 @@
26029
26044
  cursor: col.sortable ? "pointer" : "default"
26030
26045
  }, children: (0, import_jsx_runtime25.jsxs)("span", { style: { display: "inline-flex", alignItems: "center" }, children: [col.header, col.sortable && sortIndicator(sortKey === col.key, sortDirection)] }) }, col.key)) }) }), (0, import_jsx_runtime25.jsx)("tbody", { children: data.map((row, rowIndex) => (0, import_jsx_runtime25.jsx)("tr", { onMouseEnter: hoverable ? () => setHoveredRow(rowIndex) : void 0, onMouseLeave: hoverable ? () => setHoveredRow(null) : void 0, style: {
26031
26046
  backgroundColor: hoverable && hoveredRow === rowIndex ? "var(--ggui-color-sunken, #fafafa)" : striped && rowIndex % 2 === 1 ? "var(--ggui-color-sunken, #fafafa)" : "transparent",
26032
- transition: "background-color 0.15s ease"
26047
+ transition: `background-color ${motionVar.duration.fast} ${motionVar.easing.standard}`
26033
26048
  }, children: columns.map((col) => (0, import_jsx_runtime25.jsx)("td", { style: {
26034
26049
  padding: cellPadding,
26035
26050
  textAlign: col.align || "left",
@@ -26103,7 +26118,7 @@
26103
26118
  background: "none",
26104
26119
  border: "none",
26105
26120
  fontWeight: isActive ? "var(--ggui-font-weight-semibold, 600)" : "var(--ggui-font-weight-medium, 500)",
26106
- transition: `color ${duration.normal} ${easing.easeInOut}, background-color ${duration.normal} ${easing.easeInOut}, border-color ${duration.normal} ${easing.easeInOut}`,
26121
+ transition: `color ${motionVar.duration.base} ${motionVar.easing.standard}, background-color ${motionVar.duration.base} ${motionVar.easing.standard}, border-color ${motionVar.duration.base} ${motionVar.easing.standard}`,
26107
26122
  whiteSpace: "nowrap",
26108
26123
  flex: fullWidth ? 1 : void 0,
26109
26124
  justifyContent: fullWidth ? "center" : void 0
@@ -26397,7 +26412,7 @@
26397
26412
  var import_jsx_runtime28 = __toESM(require_jsx_runtime(), 1);
26398
26413
  var import_react18 = __toESM(require_react(), 1);
26399
26414
  var chevron = (expanded) => (0, import_jsx_runtime28.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": "true", style: {
26400
- transition: `transform ${duration.normal} ${easing.easeInOut}`,
26415
+ transition: `transform ${motionVar.duration.base} ${motionVar.easing.standard}`,
26401
26416
  transform: expanded ? "rotate(180deg)" : "rotate(0deg)",
26402
26417
  flexShrink: 0
26403
26418
  }, children: (0, import_jsx_runtime28.jsx)("path", { fillRule: "evenodd", d: "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z", clipRule: "evenodd" }) });
@@ -26470,7 +26485,7 @@
26470
26485
  fontWeight: "var(--ggui-font-weight-medium, 500)",
26471
26486
  color: "var(--ggui-color-onContainer, #18181b)",
26472
26487
  textAlign: "left",
26473
- transition: `background-color ${duration.fast} ${easing.easeInOut}`
26488
+ transition: `background-color ${motionVar.duration.fast} ${motionVar.easing.standard}`
26474
26489
  }, children: [(0, import_jsx_runtime28.jsx)("span", { children: item.title }), chevron(isExpanded)] }) }), (0, import_jsx_runtime28.jsx)("div", { id: `${uniqueId}-panel-${item.key}`, role: "region", "aria-labelledby": `${uniqueId}-header-${item.key}`, hidden: !isExpanded, style: {
26475
26490
  overflow: "hidden"
26476
26491
  }, children: isExpanded && (0, import_jsx_runtime28.jsx)("div", { style: {
@@ -26644,7 +26659,7 @@
26644
26659
  fontWeight: active ? fontWeight.medium : fontWeight.normal,
26645
26660
  textAlign: "left",
26646
26661
  cursor: disabled ? "not-allowed" : "pointer",
26647
- transition: "background-color 0.15s",
26662
+ transition: `background-color ${motionVar.duration.fast} ${motionVar.easing.standard}`,
26648
26663
  ...style
26649
26664
  }, onMouseEnter: (e) => {
26650
26665
  if (!disabled && !active) {
@@ -26962,7 +26977,7 @@
26962
26977
  fontWeight: active ? fontWeight.medium : fontWeight.normal,
26963
26978
  cursor: disabled ? "not-allowed" : "pointer",
26964
26979
  opacity: disabled ? 0.5 : 1,
26965
- transition: "all 0.15s"
26980
+ transition: `all ${motionVar.duration.fast} ${motionVar.easing.standard}`
26966
26981
  }, children: page });
26967
26982
  return (0, import_jsx_runtime36.jsxs)("nav", { "aria-label": "Pagination", className, style: {
26968
26983
  display: "flex",
@@ -27030,7 +27045,7 @@
27030
27045
  justifyContent: "center",
27031
27046
  flexShrink: 0,
27032
27047
  boxSizing: "border-box",
27033
- transition: "background-color 0.2s, border-color 0.2s, color 0.2s",
27048
+ transition: `background-color ${motionVar.duration.base} ${motionVar.easing.standard}, border-color ${motionVar.duration.base} ${motionVar.easing.standard}, color ${motionVar.duration.base} ${motionVar.easing.standard}`,
27034
27049
  ...markerStyles[state]
27035
27050
  }, children: state === "completed" ? (0, import_jsx_runtime39.jsx)(Icon, { name: "check", size: 16, tone: "inherit" }) : (0, import_jsx_runtime39.jsx)(Text, { is: "span", size: "sm", weight: "semibold", tone: "inherit", children: index + 1 }) });
27036
27051
  }
@@ -29561,6 +29576,14 @@
29561
29576
  "--ggui-font-weight-normal",
29562
29577
  "--ggui-font-weight-semibold",
29563
29578
  "--ggui-letter-spacing-heading",
29579
+ "--ggui-motion-duration-base",
29580
+ "--ggui-motion-duration-fast",
29581
+ "--ggui-motion-duration-slow",
29582
+ "--ggui-motion-easing-emphasized",
29583
+ "--ggui-motion-easing-exit",
29584
+ "--ggui-motion-easing-standard",
29585
+ "--ggui-scrim-opacity",
29586
+ "--ggui-scrim-tint",
29564
29587
  "--ggui-shape-radius-control",
29565
29588
  "--ggui-shape-radius-full",
29566
29589
  "--ggui-shape-radius-lg",
@@ -32534,6 +32557,7 @@
32534
32557
  "duration",
32535
32558
  "easing",
32536
32559
  "transition",
32560
+ "motionVar",
32537
32561
  "keyframes",
32538
32562
  "thinkingAnimation",
32539
32563
  "thinkingKeyframes",
@@ -32794,94 +32818,13 @@
32794
32818
  }
32795
32819
  }
32796
32820
 
32797
- // src/fixture-runtime.ts
32798
- function parseActionEntry(raw) {
32799
- if (raw === null || typeof raw !== "object") return null;
32800
- const r = raw;
32801
- if (typeof r.label !== "string") return null;
32802
- return { label: r.label };
32803
- }
32804
- function parseContract(raw) {
32805
- const out = {};
32806
- if (raw === null || typeof raw !== "object") return { actionSpec: out };
32807
- const v = raw;
32808
- if (v.actionSpec === null || typeof v.actionSpec !== "object") {
32809
- return { actionSpec: out };
32810
- }
32811
- for (const [name, entry] of Object.entries(
32812
- v.actionSpec
32813
- )) {
32814
- const parsed = parseActionEntry(entry);
32815
- if (parsed !== null) out[name] = parsed;
32821
+ // src/prime-inputs.ts
32822
+ function realmOf(el) {
32823
+ const view = el.ownerDocument.defaultView;
32824
+ if (view === null) {
32825
+ throw new Error("primeInputs: the element is not attached to a window (ownerDocument.defaultView is null) \u2014 a detached document has no constructors to prime with");
32816
32826
  }
32817
- return { actionSpec: out };
32818
- }
32819
- function installRegistry() {
32820
- const registry = {
32821
- react: React3,
32822
- reactDom: ReactDomClient,
32823
- primitives: primitives_exports,
32824
- components: components_exports,
32825
- compositions: compositions_exports,
32826
- interact: interact_exports,
32827
- appComponents: {},
32828
- wire: dist_exports,
32829
- adapters: {}
32830
- };
32831
- window.__ggui__ = registry;
32832
- window.__REACT = React3;
32833
- window.__GGUI_PRIMITIVES = primitives_exports;
32834
- window.__GGUI_COMPONENTS = components_exports;
32835
- window.__GGUI_COMPOSITIONS = compositions_exports;
32836
- window.__GGUI_INTERACT = interact_exports;
32837
- window.__GGUI_APP_COMPONENTS = registry.appComponents;
32838
- return registry;
32839
- }
32840
- var CLICKABLE_SELECTOR = 'button, [role="button"], input[type="submit"], input[type="button"], input[type="checkbox"], input[type="radio"], [role="switch"], [role="checkbox"], [role="menuitem"], [role="tab"], [role="option"], [data-action]';
32841
- var FALLBACK_CLICK_WAIT_MS = 600;
32842
- var MAX_NAVIGATOR_RECLICKS = 8;
32843
- var ACTION_BUDGET_FLOOR_MS = 6e3;
32844
- function norm(text) {
32845
- return text.toLowerCase().replace(/[^a-z0-9]/g, "");
32846
- }
32847
- function isControlDisabled(el) {
32848
- return el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true";
32849
- }
32850
- function collectActionCandidates(root, name, label) {
32851
- const every = Array.from(root.querySelectorAll(CLICKABLE_SELECTOR)).filter(
32852
- (el) => el instanceof HTMLElement
32853
- );
32854
- const isDisabled = isControlDisabled;
32855
- const lcName = name.toLowerCase();
32856
- const lcLabel = label.toLowerCase();
32857
- const nameKey = norm(name);
32858
- const labelKey = norm(label);
32859
- const namesAction = (el) => {
32860
- const dataAction = (el.getAttribute("data-action") ?? "").toLowerCase();
32861
- if (dataAction === lcName) return "value";
32862
- const aria = (el.getAttribute("aria-label") ?? "").toLowerCase();
32863
- const txt = (el.textContent ?? "").trim().toLowerCase();
32864
- const ariaNames = aria.length > 0 && (aria.includes(lcLabel) || nameKey.length > 0 && norm(aria).includes(nameKey));
32865
- const textNames = txt.length > 0 && (txt.includes(lcLabel) || nameKey.length > 0 && norm(txt).includes(nameKey) || labelKey.length > 0 && norm(txt).includes(labelKey));
32866
- return ariaNames || textNames ? "text" : null;
32867
- };
32868
- const disabledEls = every.filter(isDisabled);
32869
- const disabled = disabledEls.length;
32870
- const namedDisabled = disabledEls.filter((el) => namesAction(el) !== null).length;
32871
- const all = every.filter(
32872
- (el) => !isDisabled(el) && // an already-checked radio fires no change on click — nothing to observe
32873
- !(el instanceof HTMLInputElement && el.type === "radio" && el.checked)
32874
- );
32875
- const byValue = [];
32876
- const byText = [];
32877
- const rest = [];
32878
- for (const el of all) {
32879
- const how = namesAction(el);
32880
- if (how === "value") byValue.push(el);
32881
- else if (how === "text") byText.push(el);
32882
- else rest.push(el);
32883
- }
32884
- return { named: [...byValue, ...byText], fallback: rest, disabled, namedDisabled, allClickables: every };
32827
+ return view;
32885
32828
  }
32886
32829
  var TEXT_LIKE_INPUT_TYPES = /* @__PURE__ */ new Set([
32887
32830
  "text",
@@ -32921,7 +32864,7 @@
32921
32864
  return `${date.getUTCFullYear()}-W${pad2(week)}`;
32922
32865
  }
32923
32866
  function sampleValueFor(el) {
32924
- if (!(el instanceof HTMLInputElement)) return "probe";
32867
+ if (!(el instanceof realmOf(el).HTMLInputElement)) return "probe";
32925
32868
  switch (el.type) {
32926
32869
  case "number":
32927
32870
  return "1";
@@ -32957,25 +32900,27 @@
32957
32900
  }
32958
32901
  }
32959
32902
  function setNativeValue(el, value) {
32960
- const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : el instanceof HTMLSelectElement ? HTMLSelectElement.prototype : HTMLInputElement.prototype;
32903
+ const view = realmOf(el);
32904
+ const proto = el instanceof view.HTMLTextAreaElement ? view.HTMLTextAreaElement.prototype : el instanceof view.HTMLSelectElement ? view.HTMLSelectElement.prototype : view.HTMLInputElement.prototype;
32961
32905
  const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
32962
32906
  if (setter) setter.call(el, value);
32963
32907
  else el.value = value;
32964
- el.dispatchEvent(new Event("input", { bubbles: true }));
32965
- el.dispatchEvent(new Event("change", { bubbles: true }));
32908
+ el.dispatchEvent(new view.Event("input", { bubbles: true }));
32909
+ el.dispatchEvent(new view.Event("change", { bubbles: true }));
32966
32910
  }
32967
32911
  function primeInputs(root) {
32912
+ const view = realmOf(root);
32968
32913
  let primed = 0;
32969
- const editable = (el) => !el.disabled && !(el instanceof HTMLSelectElement) ? !el.readOnly : !el.disabled;
32914
+ const editable = (el) => !el.disabled && !el.readOnly;
32970
32915
  for (const el of Array.from(root.querySelectorAll("input, textarea"))) {
32971
- if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) || !editable(el)) continue;
32972
- if (el instanceof HTMLInputElement && !TEXT_LIKE_INPUT_TYPES.has(el.type)) continue;
32916
+ if (!(el instanceof view.HTMLInputElement || el instanceof view.HTMLTextAreaElement) || !editable(el)) continue;
32917
+ if (el instanceof view.HTMLInputElement && !TEXT_LIKE_INPUT_TYPES.has(el.type)) continue;
32973
32918
  if (el.value.trim().length > 0) continue;
32974
32919
  setNativeValue(el, sampleValueFor(el));
32975
32920
  if (el.value.trim().length > 0) primed += 1;
32976
32921
  }
32977
32922
  for (const el of Array.from(root.querySelectorAll("select"))) {
32978
- if (!(el instanceof HTMLSelectElement) || el.disabled) continue;
32923
+ if (!(el instanceof view.HTMLSelectElement) || el.disabled) continue;
32979
32924
  const current = el.options[el.selectedIndex];
32980
32925
  if (current !== void 0 && current.value.trim().length > 0) continue;
32981
32926
  const first = Array.from(el.options).find((o) => o.value.trim().length > 0 && !o.disabled);
@@ -32983,13 +32928,18 @@
32983
32928
  setNativeValue(el, first.value);
32984
32929
  primed += 1;
32985
32930
  }
32931
+ const radios = Array.from(root.querySelectorAll('input[type="radio"]')).filter(
32932
+ (m) => m instanceof view.HTMLInputElement
32933
+ );
32986
32934
  const seenGroups = /* @__PURE__ */ new Set();
32987
- for (const el of Array.from(root.querySelectorAll('input[type="radio"]'))) {
32988
- if (!(el instanceof HTMLInputElement) || el.disabled) continue;
32989
- const group = el.name || "";
32990
- if (seenGroups.has(group)) continue;
32991
- seenGroups.add(group);
32992
- const members = group ? Array.from(root.querySelectorAll(`input[type="radio"][name="${CSS.escape(group)}"]`)).filter((m) => m instanceof HTMLInputElement) : [el];
32935
+ for (const el of radios) {
32936
+ if (el.disabled) continue;
32937
+ const group = el.name;
32938
+ if (group !== "") {
32939
+ if (seenGroups.has(group)) continue;
32940
+ seenGroups.add(group);
32941
+ }
32942
+ const members = group !== "" ? radios.filter((m) => m.name === group) : [el];
32993
32943
  if (members.some((m) => m.checked)) continue;
32994
32944
  const first = members.find((m) => !m.disabled);
32995
32945
  if (first === void 0) continue;
@@ -32998,6 +32948,96 @@
32998
32948
  }
32999
32949
  return primed;
33000
32950
  }
32951
+
32952
+ // src/fixture-runtime.ts
32953
+ function parseActionEntry(raw) {
32954
+ if (raw === null || typeof raw !== "object") return null;
32955
+ const r = raw;
32956
+ if (typeof r.label !== "string") return null;
32957
+ return { label: r.label };
32958
+ }
32959
+ function parseContract(raw) {
32960
+ const out = {};
32961
+ if (raw === null || typeof raw !== "object") return { actionSpec: out };
32962
+ const v = raw;
32963
+ if (v.actionSpec === null || typeof v.actionSpec !== "object") {
32964
+ return { actionSpec: out };
32965
+ }
32966
+ for (const [name, entry] of Object.entries(
32967
+ v.actionSpec
32968
+ )) {
32969
+ const parsed = parseActionEntry(entry);
32970
+ if (parsed !== null) out[name] = parsed;
32971
+ }
32972
+ return { actionSpec: out };
32973
+ }
32974
+ function installRegistry() {
32975
+ const registry = {
32976
+ react: React3,
32977
+ reactDom: ReactDomClient,
32978
+ primitives: primitives_exports,
32979
+ components: components_exports,
32980
+ compositions: compositions_exports,
32981
+ interact: interact_exports,
32982
+ appComponents: {},
32983
+ wire: dist_exports,
32984
+ adapters: {}
32985
+ };
32986
+ window.__ggui__ = registry;
32987
+ window.__REACT = React3;
32988
+ window.__GGUI_PRIMITIVES = primitives_exports;
32989
+ window.__GGUI_COMPONENTS = components_exports;
32990
+ window.__GGUI_COMPOSITIONS = compositions_exports;
32991
+ window.__GGUI_INTERACT = interact_exports;
32992
+ window.__GGUI_APP_COMPONENTS = registry.appComponents;
32993
+ return registry;
32994
+ }
32995
+ var CLICKABLE_SELECTOR = 'button, [role="button"], input[type="submit"], input[type="button"], input[type="checkbox"], input[type="radio"], [role="switch"], [role="checkbox"], [role="menuitem"], [role="tab"], [role="option"], [data-action]';
32996
+ var FALLBACK_CLICK_WAIT_MS = 600;
32997
+ var MAX_NAVIGATOR_RECLICKS = 8;
32998
+ var ACTION_BUDGET_FLOOR_MS = 6e3;
32999
+ function norm(text) {
33000
+ return text.toLowerCase().replace(/[^a-z0-9]/g, "");
33001
+ }
33002
+ function isControlDisabled(el) {
33003
+ return el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true";
33004
+ }
33005
+ function collectActionCandidates(root, name, label) {
33006
+ const every = Array.from(root.querySelectorAll(CLICKABLE_SELECTOR)).filter(
33007
+ (el) => el instanceof HTMLElement
33008
+ );
33009
+ const isDisabled = isControlDisabled;
33010
+ const lcName = name.toLowerCase();
33011
+ const lcLabel = label.toLowerCase();
33012
+ const nameKey = norm(name);
33013
+ const labelKey = norm(label);
33014
+ const namesAction = (el) => {
33015
+ const dataAction = (el.getAttribute("data-action") ?? "").toLowerCase();
33016
+ if (dataAction === lcName) return "value";
33017
+ const aria = (el.getAttribute("aria-label") ?? "").toLowerCase();
33018
+ const txt = (el.textContent ?? "").trim().toLowerCase();
33019
+ const ariaNames = aria.length > 0 && (aria.includes(lcLabel) || nameKey.length > 0 && norm(aria).includes(nameKey));
33020
+ const textNames = txt.length > 0 && (txt.includes(lcLabel) || nameKey.length > 0 && norm(txt).includes(nameKey) || labelKey.length > 0 && norm(txt).includes(labelKey));
33021
+ return ariaNames || textNames ? "text" : null;
33022
+ };
33023
+ const disabledEls = every.filter(isDisabled);
33024
+ const disabled = disabledEls.length;
33025
+ const namedDisabled = disabledEls.filter((el) => namesAction(el) !== null).length;
33026
+ const all = every.filter(
33027
+ (el) => !isDisabled(el) && // an already-checked radio fires no change on click — nothing to observe
33028
+ !(el instanceof HTMLInputElement && el.type === "radio" && el.checked)
33029
+ );
33030
+ const byValue = [];
33031
+ const byText = [];
33032
+ const rest = [];
33033
+ for (const el of all) {
33034
+ const how = namesAction(el);
33035
+ if (how === "value") byValue.push(el);
33036
+ else if (how === "text") byText.push(el);
33037
+ else rest.push(el);
33038
+ }
33039
+ return { named: [...byValue, ...byText], fallback: rest, disabled, namedDisabled, allClickables: every };
33040
+ }
33001
33041
  function countNamedDispatches(actionName) {
33002
33042
  return (window.__ggui_test_dispatches__ ?? []).filter((d) => d.actionName === actionName).length;
33003
33043
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ggui-ai/ui-visual-tester",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "Behavioral validator for ggui-generated component code. Renders a component in real Chromium via Playwright, clicks each action declared in its contract, and verifies the action actually does something — mutating bound state or dispatching to the agent. Catches buttons wired to no-op handlers that structural validators miss. Playwright is an optional peer dependency, injected by the caller.",
5
5
  "keywords": [
6
6
  "ggui",
@@ -35,6 +35,11 @@
35
35
  "types": "./dist/index.d.ts",
36
36
  "import": "./dist/index.js",
37
37
  "default": "./dist/index.js"
38
+ },
39
+ "./prime-inputs": {
40
+ "types": "./dist/prime-inputs.d.ts",
41
+ "import": "./dist/prime-inputs.js",
42
+ "default": "./dist/prime-inputs.js"
38
43
  }
39
44
  },
40
45
  "peerDependencies": {
@@ -48,9 +53,9 @@
48
53
  "dependencies": {
49
54
  "react": "^19.0.0",
50
55
  "react-dom": "^19.0.0",
51
- "@ggui-ai/design": "0.19.0",
52
- "@ggui-ai/wire": "0.19.0",
53
- "@ggui-ai/protocol": "0.19.0"
56
+ "@ggui-ai/protocol": "0.21.0",
57
+ "@ggui-ai/design": "0.21.0",
58
+ "@ggui-ai/wire": "0.21.0"
54
59
  },
55
60
  "devDependencies": {
56
61
  "@playwright/test": "^1.60.0",