@excom/quark 0.1.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.
Files changed (130) hide show
  1. package/.rush/temp/chunked-rush-logs/quark.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/quark.build_package-metas.chunks.jsonl +1 -0
  3. package/.rush/temp/operation/apply-exports/all.log +1 -0
  4. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  5. package/.rush/temp/operation/apply-exports/state.json +3 -0
  6. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  7. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  8. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  9. package/.rush/temp/shrinkwrap-deps.json +3 -0
  10. package/config/rig.json +6 -0
  11. package/index.ts +4 -0
  12. package/language.ts +7 -0
  13. package/package.json +45 -0
  14. package/rush-logs/quark.apply-exports.cache.log +1 -0
  15. package/rush-logs/quark.apply-exports.log +1 -0
  16. package/rush-logs/quark.build_package-metas.cache.log +1 -0
  17. package/rush-logs/quark.build_package-metas.log +1 -0
  18. package/src/actions.ts +445 -0
  19. package/src/ast.ts +187 -0
  20. package/src/bindings.ts +115 -0
  21. package/src/builtin-modules.ts +401 -0
  22. package/src/constants.ts +47 -0
  23. package/src/cycle-check.ts +121 -0
  24. package/src/devtools-hook.ts +303 -0
  25. package/src/element-api.ts +109 -0
  26. package/src/evaluator.ts +317 -0
  27. package/src/form-controls.ts +69 -0
  28. package/src/language-tables.ts +137 -0
  29. package/src/language.ts +1225 -0
  30. package/src/observer.ts +110 -0
  31. package/src/paint.ts +305 -0
  32. package/src/properties.ts +818 -0
  33. package/src/props.ts +94 -0
  34. package/src/quark-element.ts +7 -0
  35. package/src/quark-internal.ts +365 -0
  36. package/src/quark.ts +817 -0
  37. package/src/resolvers.ts +867 -0
  38. package/src/rule.ts +800 -0
  39. package/src/scope-id.ts +47 -0
  40. package/src/selector-utils.ts +430 -0
  41. package/src/settle.ts +117 -0
  42. package/src/types.ts +155 -0
  43. package/src/utils.ts +79 -0
  44. package/src/variables.ts +359 -0
  45. package/support/demos/css-variables.html +18 -0
  46. package/support/demos/dispatch.html +20 -0
  47. package/support/demos/events.html +9 -0
  48. package/support/demos/iterate.html +13 -0
  49. package/support/demos/js-api.html +17 -0
  50. package/support/demos/provider-list.html +16 -0
  51. package/support/demos/toggle-content.html +14 -0
  52. package/support/demos/unset.html +12 -0
  53. package/support/demos/view-transition.html +36 -0
  54. package/support/docs/ATTRIBUTES.md +37 -0
  55. package/support/docs/AT_RULES.md +45 -0
  56. package/support/docs/BUILTINS.md +57 -0
  57. package/support/docs/CONTENT.md +40 -0
  58. package/support/docs/CSS_VARIABLES.md +16 -0
  59. package/support/docs/DECLARATIONS.md +26 -0
  60. package/support/docs/DELAY.md +37 -0
  61. package/support/docs/DIAGNOSTICS.md +32 -0
  62. package/support/docs/DISPATCH.md +81 -0
  63. package/support/docs/ELEMENT_PROPERTIES.md +34 -0
  64. package/support/docs/EXPRESSIONS.md +31 -0
  65. package/support/docs/INTERNAL.md +8 -0
  66. package/support/docs/JS_API.md +53 -0
  67. package/support/docs/JS_WRITES.md +36 -0
  68. package/support/docs/LIMITATIONS.md +26 -0
  69. package/support/docs/LOOP_GUARD.md +30 -0
  70. package/support/docs/METHODS.md +43 -0
  71. package/support/docs/MODULES.md +111 -0
  72. package/support/docs/NO_REVERSION.md +18 -0
  73. package/support/docs/ON.md +87 -0
  74. package/support/docs/REACTIVITY.md +24 -0
  75. package/support/docs/README.md +143 -0
  76. package/support/docs/SELECTORS.md +87 -0
  77. package/support/docs/SHEETS.md +67 -0
  78. package/support/docs/SYNTAX.md +36 -0
  79. package/support/docs/USE.md +24 -0
  80. package/support/docs/VALUES.md +27 -0
  81. package/support/docs/VARIABLES.md +47 -0
  82. package/support/docs/VIEW_TRANSITION.md +64 -0
  83. package/support/docs-sections.json +65 -0
  84. package/support/package-meta.json +208 -0
  85. package/support/scripts/build-language-docs.mjs +196 -0
  86. package/support/tests/__snapshots__/css-variables.view.test.ts.snap +23 -0
  87. package/support/tests/__snapshots__/dispatch.view.test.ts.snap +23 -0
  88. package/support/tests/__snapshots__/events.view.test.ts.snap +23 -0
  89. package/support/tests/__snapshots__/iterate.view.test.ts.snap +23 -0
  90. package/support/tests/__snapshots__/js-api.view.test.ts.snap +23 -0
  91. package/support/tests/__snapshots__/provider-list.view.test.ts.snap +23 -0
  92. package/support/tests/__snapshots__/quark-features.test.ts.snap +1079 -0
  93. package/support/tests/__snapshots__/quark-with-providers.test.ts.snap +89 -0
  94. package/support/tests/__snapshots__/quark.test.ts.snap +221 -0
  95. package/support/tests/__snapshots__/selectors.test.ts.snap +133 -0
  96. package/support/tests/__snapshots__/toggle-content.view.test.ts.snap +23 -0
  97. package/support/tests/__snapshots__/unset.view.test.ts.snap +23 -0
  98. package/support/tests/__snapshots__/view-transition.view.test.ts.snap +23 -0
  99. package/support/tests/actions.test.ts +437 -0
  100. package/support/tests/builtin-modules.test.ts +337 -0
  101. package/support/tests/builtins.test.ts +189 -0
  102. package/support/tests/css-variables.view.test.ts +35 -0
  103. package/support/tests/delay.test.ts +244 -0
  104. package/support/tests/devtools-hook.test.ts +496 -0
  105. package/support/tests/diagnostics.test.ts +224 -0
  106. package/support/tests/dispatch.view.test.ts +43 -0
  107. package/support/tests/element-api.test.ts +202 -0
  108. package/support/tests/evaluator.test.ts +237 -0
  109. package/support/tests/events.view.test.ts +40 -0
  110. package/support/tests/form-controls.test.ts +452 -0
  111. package/support/tests/helpers.ts +354 -0
  112. package/support/tests/internals.test.ts +568 -0
  113. package/support/tests/iterate.view.test.ts +40 -0
  114. package/support/tests/js-api.view.test.ts +45 -0
  115. package/support/tests/language-docs.test.ts +147 -0
  116. package/support/tests/lifecycle.test.ts +440 -0
  117. package/support/tests/loop-guard.test.ts +323 -0
  118. package/support/tests/provider-list.view.test.ts +47 -0
  119. package/support/tests/quark-features.test.ts +2887 -0
  120. package/support/tests/quark-with-providers.test.ts +169 -0
  121. package/support/tests/quark.test.ts +359 -0
  122. package/support/tests/resolvers.test.ts +346 -0
  123. package/support/tests/selector-analysis.test.ts +281 -0
  124. package/support/tests/selectors.test.ts +670 -0
  125. package/support/tests/toggle-content.view.test.ts +40 -0
  126. package/support/tests/unset.view.test.ts +42 -0
  127. package/support/tests/view-helpers.ts +81 -0
  128. package/support/tests/view-transition.test.ts +1069 -0
  129. package/support/tests/view-transition.view.test.ts +76 -0
  130. package/tsconfig.json +5 -0
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
@@ -0,0 +1 @@
1
+ Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "nonCachedDurationMs": 34.50040100000001
3
+ }
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "nonCachedDurationMs": 136.85359500000004
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "../../packages/quark": "../../packages/quark:/Dfg1eutq4WwzojYHY4AwMIg381idv5duTihFLNbavs=:"
3
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
3
+ "rigPackageName": "@excom/heft-rig",
4
+ "rigProfile": "default"
5
+ }
6
+
package/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ // Main entry point for @excom/quark package
2
+ export type { QuarkElementApi } from "./src/element-api";
3
+ export { Quark } from "./src/quark";
4
+ export * as QuarkTypes from "./src/types";
package/language.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `@excom/quark/language`: documented language metadata (keywords,
3
+ * declaration kinds, at-rules, built-ins, allowed methods, combinators,
4
+ * pseudo-classes) for the README language reference. Off the main entry
5
+ * so the prose stays out of app bundles.
6
+ */
7
+ export * from "./src/language";
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@excom/quark",
3
+ "version": "0.1.0",
4
+ "description": "CSS-like DOM orchestration language for binding, rendering, and events",
5
+ "license": "ISC",
6
+ "engines": {
7
+ "node": ">=24.13.0"
8
+ },
9
+ "type": "module",
10
+ "dependencies": {
11
+ "@excom/kit-devtools": "^0.1.0",
12
+ "@excom/hash-object": "^0.1.0",
13
+ "@excom/kit-logger": "^0.1.0",
14
+ "@excom/kit-utils": "^0.1.0",
15
+ "@excom/quark-parser": "^0.1.0"
16
+ },
17
+ "peerDependencies": {},
18
+ "devDependencies": {
19
+ "@excom/heft-rig": "^0.1.0",
20
+ "@excom/provider-fetch": "^0.1.0",
21
+ "@excom/provider-storage": "^0.1.0"
22
+ },
23
+ "repository": {
24
+ "url": "excom-dev/nucleus",
25
+ "directory": "packages/quark"
26
+ },
27
+ "homepage": "https://github.com/excom-dev/nucleus/tree/main/packages/quark/support/docs/README.md",
28
+ "bugs": "https://github.com/excom-dev/nucleus/issues",
29
+ "keywords": [
30
+ "quark"
31
+ ],
32
+ "excom": {
33
+ "packageType": "library"
34
+ },
35
+ "scripts": {
36
+ "build": "node node_modules/@excom/heft-rig/scripts/vite-build.mjs",
37
+ "build:watch": "node node_modules/@excom/heft-rig/scripts/vite-build-watch.mjs",
38
+ "format": "node node_modules/@excom/heft-rig/scripts/format.mjs",
39
+ "test": "node node_modules/@excom/heft-rig/scripts/vitest.mjs",
40
+ "coverage": "node node_modules/@excom/heft-rig/scripts/coverage.mjs",
41
+ "dev": "node node_modules/@excom/heft-rig/scripts/vite-dev.mjs",
42
+ "preview": "node node_modules/@excom/heft-rig/scripts/vite-preview.mjs",
43
+ "build:package-metas": "node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs"
44
+ }
45
+ }
@@ -0,0 +1 @@
1
+ Caching has been disabled for this project's "apply-exports" command.
@@ -0,0 +1 @@
1
+ Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
@@ -0,0 +1 @@
1
+ This project does not define the caching behavior of the "build:package-metas" command, so caching has been disabled.
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
package/src/actions.ts ADDED
@@ -0,0 +1,445 @@
1
+ /**
2
+ * `@dispatch` / `@command` statements: the outgoing half of `@on`.
3
+ * Accepted inside `@on … { }` blocks (and `@delay` blocks within them)
4
+ * only, so a dispatch is always caused by an event the sheet heard —
5
+ * never by a rule matching. Options are evaluated per event, in the
6
+ * block's scope (`event`, `target`, `element`, `$bindings`).
7
+ *
8
+ * @on click { @dispatch cart-add (detail: (sku: $sku), target: "cart-view"); }
9
+ * @on click { @command --refresh (target: "#feed"); }
10
+ *
11
+ * Dispatch is synchronous at the end of the block (after its writes are
12
+ * queued, before they paint): an event is an occurrence, not a delivery
13
+ * of State. Chains are one loop-guard hop per dispatch, so a cycle is cut
14
+ * like a write cycle.
15
+ */
16
+ import {
17
+ type EventName,
18
+ eventNamesText,
19
+ type ListenerOptionSource,
20
+ listenerOptionsText,
21
+ } from "./ast";
22
+ import { isNoop } from "./constants";
23
+ import { publicize } from "./devtools-hook";
24
+ import { resolveExpression } from "./resolvers";
25
+ import type { Rule } from "./rule";
26
+ import type { QuarkOptions } from "./types";
27
+ import { QuarkLogger } from "./utils";
28
+ import { formToJson, LoopGuard, selectAll, tc } from "@excom/kit-utils";
29
+
30
+ export type ActionKind = "dispatch" | "command";
31
+
32
+ /** Event-init flags an `@dispatch` may set; bare = `true`. */
33
+ const DISPATCH_FLAGS = ["bubbles", "cancelable", "composed"] as const;
34
+ type DispatchFlag = (typeof DISPATCH_FLAGS)[number];
35
+ const DISPATCH_DEFAULTS: Record<DispatchFlag, boolean> = {
36
+ bubbles: true,
37
+ cancelable: true,
38
+ composed: false,
39
+ };
40
+
41
+ /**
42
+ * Native commands the fallback invokes directly when the browser has no
43
+ * Invoker Commands API (`button.commandForElement`).
44
+ */
45
+ const NATIVE_COMMANDS: Record<string, string> = {
46
+ "show-modal": "showModal",
47
+ close: "close",
48
+ "request-close": "requestClose",
49
+ "show-popover": "showPopover",
50
+ "hide-popover": "hidePopover",
51
+ "toggle-popover": "togglePopover",
52
+ };
53
+
54
+ const supportsInvokers = (): boolean =>
55
+ typeof HTMLButtonElement !== "undefined" &&
56
+ "commandForElement" in HTMLButtonElement.prototype;
57
+
58
+ interface ResolvedTargets {
59
+ /** Where to dispatch / invoke; empty when nothing matched. */
60
+ targets: EventTarget[];
61
+ /** `host: window | document` was used. */
62
+ onHost: boolean;
63
+ }
64
+
65
+ export class Action {
66
+ kind: ActionKind;
67
+ /** Event / command names in source order. */
68
+ names: string[];
69
+ optionSources: ListenerOptionSource[];
70
+ /** Display key: `@dispatch cart-add (detail: $d)`. */
71
+ key: string;
72
+ parent: Rule;
73
+ /** Option / target problems already reported, per element. */
74
+ private warned = new WeakMap<Element, Set<string>>();
75
+ constructor({
76
+ kind,
77
+ names,
78
+ optionSources,
79
+ parent,
80
+ }: {
81
+ kind: ActionKind;
82
+ names: EventName[];
83
+ optionSources: ListenerOptionSource[];
84
+ parent: Rule;
85
+ }) {
86
+ this.kind = kind;
87
+ this.names = names.map((n) => n.name);
88
+ this.optionSources = optionSources;
89
+ this.key = `@${kind} ${eventNamesText(names)}${listenerOptionsText(optionSources)}`;
90
+ this.parent = parent;
91
+ }
92
+
93
+ private warnOnce(element: Element, topic: string, message: string) {
94
+ let topics = this.warned.get(element);
95
+ if (!topics) this.warned.set(element, (topics = new Set()));
96
+ if (topics.has(topic)) return;
97
+ topics.add(topic);
98
+ QuarkLogger.warn({
99
+ method: this.kind,
100
+ message: `Quark: ${this.key} — ${message} (${this.parent.selector})`,
101
+ element,
102
+ });
103
+ }
104
+
105
+ private selectTargets(
106
+ element: HTMLElement,
107
+ selector: string
108
+ ): Element[] | undefined {
109
+ return tc(() => selectAll(selector, { scope: element }) ?? []);
110
+ }
111
+
112
+ /** `target:` / `host:` → the event targets; the matched element by default. */
113
+ private resolveTargets(
114
+ element: HTMLElement,
115
+ host: unknown,
116
+ target: unknown,
117
+ targetText: string | null | undefined
118
+ ): ResolvedTargets {
119
+ if (host === "window" || host === "document") {
120
+ return {
121
+ targets: [
122
+ host === "window"
123
+ ? ((element.ownerDocument.defaultView as EventTarget) ?? window)
124
+ : element.ownerDocument,
125
+ ],
126
+ onHost: true,
127
+ };
128
+ }
129
+ if (targetText === undefined) return { targets: [element], onHost: false };
130
+ let targets: Element[] | undefined;
131
+ if (typeof target === "string") {
132
+ targets = this.selectTargets(element, target);
133
+ if (!targets) {
134
+ this.warnOnce(
135
+ element,
136
+ "target",
137
+ `target "${target}" is not a valid selector`
138
+ );
139
+ return { targets: [], onHost: false };
140
+ }
141
+ } else if (target instanceof Element) {
142
+ targets = [target];
143
+ } else if (
144
+ target &&
145
+ typeof target === "object" &&
146
+ Symbol.iterator in target
147
+ ) {
148
+ targets = Array.from(target as Iterable<unknown>).filter(
149
+ (t): t is Element => t instanceof Element
150
+ );
151
+ } else {
152
+ targets = [];
153
+ }
154
+ if (!targets.length) {
155
+ this.warnOnce(
156
+ element,
157
+ "target",
158
+ `target ${typeof target === "string" ? `"${target}"` : String(target)} matches no element`
159
+ );
160
+ }
161
+ return { targets, onHost: false };
162
+ }
163
+
164
+ run(element: HTMLElement, options: QuarkOptions) {
165
+ const sheet = this.parent.quarkInstance;
166
+ const args = {
167
+ element: element as HTMLElement & { _q_: unknown },
168
+ key: this.key,
169
+ value: null as string | null,
170
+ options: { ...options, rule: this.parent },
171
+ hash: sheet.hash,
172
+ };
173
+ const evaluate = (text: string) =>
174
+ resolveExpression({
175
+ ...args,
176
+ key: `${this.key} option`,
177
+ value: text,
178
+ } as Parameters<typeof resolveExpression>[0]);
179
+ let detail: unknown;
180
+ let hasDetail = false;
181
+ let form: unknown;
182
+ let hostOption: unknown;
183
+ let target: unknown;
184
+ let targetText: string | null | undefined;
185
+ const flags: Partial<Record<DispatchFlag, boolean>> = {};
186
+ for (const { name, text, ident } of this.optionSources) {
187
+ if (name === "target") {
188
+ targetText = text;
189
+ target = text === null ? undefined : evaluate(text);
190
+ if (text === null) {
191
+ this.warnOnce(
192
+ element,
193
+ name,
194
+ `option "target" needs a selector or an element`
195
+ );
196
+ targetText = undefined;
197
+ }
198
+ } else if (this.kind === "command") {
199
+ this.warnOnce(
200
+ element,
201
+ name,
202
+ `option "${name}" is not a @command option`
203
+ );
204
+ } else if (name === "detail") {
205
+ hasDetail = text !== null;
206
+ detail = text === null ? undefined : evaluate(text);
207
+ if (text === null)
208
+ this.warnOnce(element, name, `option "detail" needs a value`);
209
+ } else if (name === "form") {
210
+ form = text === null ? undefined : evaluate(text);
211
+ if (text === null)
212
+ this.warnOnce(
213
+ element,
214
+ name,
215
+ `option "form" needs a form or a selector`
216
+ );
217
+ } else if (name === "host") {
218
+ if (ident === "window" || ident === "document") hostOption = ident;
219
+ else
220
+ this.warnOnce(
221
+ element,
222
+ name,
223
+ `option "host" must be window or document`
224
+ );
225
+ } else if ((DISPATCH_FLAGS as readonly string[]).includes(name)) {
226
+ const value = text === null ? true : evaluate(text);
227
+ if (typeof value === "boolean") flags[name as DispatchFlag] = value;
228
+ else this.warnOnce(element, name, `option "${name}" needs a boolean`);
229
+ } else {
230
+ this.warnOnce(element, name, `unknown option "${name}"`);
231
+ }
232
+ }
233
+ if (isNoop(detail) || isNoop(target) || isNoop(form)) return;
234
+ const { targets, onHost } = this.resolveTargets(
235
+ element,
236
+ hostOption,
237
+ target,
238
+ targetText
239
+ );
240
+ if (this.kind === "command" && onHost) return;
241
+ const formValues = this.formValues(element, form);
242
+ if (formValues === false) return;
243
+ const publish = (
244
+ name: string,
245
+ phase: "dispatched" | "dropped",
246
+ extra: Record<string, unknown> = {}
247
+ ) =>
248
+ publicize(["quark", this.kind], {
249
+ weakElement: new WeakRef(element),
250
+ tag: element.localName,
251
+ selector: this.parent.selector,
252
+ ruleId: this.parent.id,
253
+ sheetId: sheet.id,
254
+ runId: options.runId ?? null,
255
+ name,
256
+ targets: targets.length,
257
+ phase,
258
+ ...extra,
259
+ });
260
+ for (const name of this.names) {
261
+ // the enclosing event re-dispatched from its own handler is a loop by construction
262
+ if (
263
+ this.kind === "dispatch" &&
264
+ options.event &&
265
+ options.event.type === name
266
+ ) {
267
+ this.warnOnce(
268
+ element,
269
+ `same:${name}`,
270
+ `refusing to dispatch "${name}" from its own @on ${name} block`
271
+ );
272
+ publish(name, "dropped", { reason: "same-event" });
273
+ continue;
274
+ }
275
+ // an unmatched `target:` already warned; nothing to send to
276
+ targets.forEach((eventTarget) => {
277
+ const done =
278
+ this.kind === "dispatch"
279
+ ? this.dispatch(eventTarget, name, {
280
+ detail: hasDetail ? detail : undefined,
281
+ formValues,
282
+ flags,
283
+ })
284
+ : this.command(eventTarget as Element, name, element);
285
+ if (done === false) {
286
+ publish(name, "dropped", { reason: "loop-guard" });
287
+ } else if (done === null) {
288
+ publish(name, "dropped", { reason: "unsupported" });
289
+ } else {
290
+ publish(name, "dispatched", done);
291
+ }
292
+ });
293
+ }
294
+ }
295
+
296
+ /** `form:` → its values (`formToJson`); `false` when the option is unusable. */
297
+ private formValues(
298
+ element: HTMLElement,
299
+ form: unknown
300
+ ): Record<string, unknown> | undefined | false {
301
+ if (form === undefined) return undefined;
302
+ let formElement: unknown = form;
303
+ if (typeof form === "string") {
304
+ const found = this.selectTargets(element, form);
305
+ if (!found) {
306
+ this.warnOnce(
307
+ element,
308
+ "form",
309
+ `form "${form}" is not a valid selector`
310
+ );
311
+ return false;
312
+ }
313
+ formElement = found.find((el) => el instanceof HTMLFormElement);
314
+ }
315
+ if (!(formElement instanceof HTMLFormElement)) {
316
+ this.warnOnce(
317
+ element,
318
+ "form",
319
+ `form ${typeof form === "string" ? `"${form}"` : String(form)} is not a <form>`
320
+ );
321
+ return false;
322
+ }
323
+ return formToJson(formElement) as Record<string, unknown>;
324
+ }
325
+
326
+ /** Dispatch one `CustomEvent`; `false` when the loop guard dropped it. */
327
+ private dispatch(
328
+ target: EventTarget,
329
+ name: string,
330
+ {
331
+ detail,
332
+ formValues,
333
+ flags,
334
+ }: {
335
+ detail: unknown;
336
+ formValues: Record<string, unknown> | undefined;
337
+ flags: Partial<Record<DispatchFlag, boolean>>;
338
+ }
339
+ ): Record<string, unknown> | false {
340
+ const init: CustomEventInit = {
341
+ bubbles: flags.bubbles ?? DISPATCH_DEFAULTS.bubbles,
342
+ cancelable: flags.cancelable ?? DISPATCH_DEFAULTS.cancelable,
343
+ composed: flags.composed ?? DISPATCH_DEFAULTS.composed,
344
+ };
345
+ // form values are the base detail; an explicit map merges over them
346
+ const merged =
347
+ formValues &&
348
+ detail &&
349
+ typeof detail === "object" &&
350
+ !Array.isArray(detail)
351
+ ? { ...formValues, ...(detail as Record<string, unknown>) }
352
+ : (detail ?? formValues);
353
+ if (merged !== undefined) init.detail = merged;
354
+ const event = new CustomEvent(name, init);
355
+ // `dispatchEvent` returns false when canceled; the guard's false means dropped
356
+ const dispatched = LoopGuard.write(target, `@dispatch ${name}`, () =>
357
+ LoopGuard.run(LoopGuard.current() + 1, () => {
358
+ target.dispatchEvent(event);
359
+ return true;
360
+ })
361
+ );
362
+ if (dispatched === false) return false;
363
+ return {
364
+ bubbles: init.bubbles,
365
+ cancelable: init.cancelable,
366
+ composed: init.composed,
367
+ defaultPrevented: event.defaultPrevented,
368
+ };
369
+ }
370
+
371
+ /**
372
+ * Invoke one command on `target`: through a hidden invoker button
373
+ * where the browser has the Command API (native and custom commands
374
+ * alike), else a synthetic `command` event for custom `--names` and a
375
+ * direct method call for the native ones. `null` when unsupported.
376
+ */
377
+ private command(
378
+ target: Element,
379
+ name: string,
380
+ source: HTMLElement
381
+ ): Record<string, unknown> | false | null {
382
+ const invoke = (): "invoker" | "event" | "method" | null => {
383
+ if (supportsInvokers()) {
384
+ const button = target.ownerDocument.createElement(
385
+ "button"
386
+ ) as HTMLButtonElement & {
387
+ commandForElement: Element;
388
+ command: string;
389
+ };
390
+ button.type = "button";
391
+ button.style.display = "none";
392
+ source.ownerDocument.body.appendChild(button);
393
+ button.commandForElement = target;
394
+ button.command = name;
395
+ try {
396
+ button.click();
397
+ } finally {
398
+ button.remove();
399
+ }
400
+ return "invoker";
401
+ }
402
+ if (name.startsWith("--")) {
403
+ const init = { bubbles: false, cancelable: true, composed: true };
404
+ const CommandEventCtor = (
405
+ globalThis as {
406
+ CommandEvent?: new (type: string, init: object) => Event;
407
+ }
408
+ ).CommandEvent;
409
+ const event = CommandEventCtor
410
+ ? new CommandEventCtor("command", { ...init, command: name, source })
411
+ : Object.defineProperties(new CustomEvent("command", init), {
412
+ command: { value: name },
413
+ source: { value: source },
414
+ });
415
+ target.dispatchEvent(event);
416
+ return "event";
417
+ }
418
+ const method = NATIVE_COMMANDS[name];
419
+ const fn =
420
+ method && (target as unknown as Record<string, unknown>)[method];
421
+ if (typeof fn === "function") {
422
+ fn.call(target);
423
+ return "method";
424
+ }
425
+ return null;
426
+ };
427
+ let via: "invoker" | "event" | "method" | null = null;
428
+ const done = LoopGuard.write(target, `@command ${name}`, () =>
429
+ LoopGuard.run(LoopGuard.current() + 1, () => {
430
+ via = invoke();
431
+ return true;
432
+ })
433
+ );
434
+ if (done === false) return false;
435
+ if (via === null) {
436
+ this.warnOnce(
437
+ source,
438
+ `command:${name}`,
439
+ `command "${name}" is not supported here (custom commands start with --)`
440
+ );
441
+ return null;
442
+ }
443
+ return { via };
444
+ }
445
+ }