@kiwa-test/component 0.2.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/dist/index.cjs ADDED
@@ -0,0 +1,802 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ addHandler: () => addHandler,
24
+ appendChild: () => appendChild,
25
+ buildButton: () => buildButton,
26
+ buildCard: () => buildCard,
27
+ buildForm: () => buildForm,
28
+ buildInput: () => buildInput,
29
+ buildModal: () => buildModal,
30
+ componentFixtures: () => componentFixtures,
31
+ createCanvas: () => createCanvas,
32
+ createChromaticVisualMock: () => createChromaticVisualMock,
33
+ createNode: () => createNode,
34
+ createPlaywrightCTMock: () => createPlaywrightCTMock,
35
+ createStoryRegistry: () => createStoryRegistry,
36
+ findByRole: () => findByRole,
37
+ findByText: () => findByText,
38
+ fireEvent: () => fireEvent,
39
+ hashMarkup: () => hashMarkup,
40
+ query: () => query,
41
+ renderMarkup: () => renderMarkup
42
+ });
43
+ module.exports = __toCommonJS(index_exports);
44
+
45
+ // src/dom.ts
46
+ var import_node_crypto = require("crypto");
47
+ function createNode(tag, options = {}) {
48
+ const node = {
49
+ tag,
50
+ attrs: options.attrs ?? {},
51
+ children: [],
52
+ handlers: {},
53
+ parent: null,
54
+ ...options.text !== void 0 ? { text: options.text } : {},
55
+ ...options.value !== void 0 ? { value: options.value } : {}
56
+ };
57
+ for (const child of options.children ?? []) {
58
+ appendChild(node, child);
59
+ }
60
+ if (options.on) {
61
+ for (const [event, handler] of Object.entries(options.on)) {
62
+ addHandler(node, event, handler);
63
+ }
64
+ }
65
+ return node;
66
+ }
67
+ function appendChild(parent, child) {
68
+ parent.children.push(child);
69
+ child.parent = parent;
70
+ }
71
+ function addHandler(node, event, handler) {
72
+ if (!node.handlers[event]) {
73
+ node.handlers[event] = [];
74
+ }
75
+ node.handlers[event].push(handler);
76
+ }
77
+ function fireEvent(node, event) {
78
+ const handlers = node.handlers[event.type];
79
+ if (!handlers) return;
80
+ for (const handler of handlers) {
81
+ handler(event);
82
+ }
83
+ }
84
+ function createCanvas(root) {
85
+ return {
86
+ root,
87
+ getByText: (text) => {
88
+ const found = findByText(root, text);
89
+ if (!found) {
90
+ throw new Error(`Canvas.getByText \u2014 no node with text ${JSON.stringify(text)}`);
91
+ }
92
+ return found;
93
+ },
94
+ getByRole: (role, options) => {
95
+ const found = findByRole(root, role, options?.name);
96
+ if (!found) {
97
+ const nameSuffix = options?.name ? ` name=${JSON.stringify(options.name)}` : "";
98
+ throw new Error(`Canvas.getByRole \u2014 no node with role=${role}${nameSuffix}`);
99
+ }
100
+ return found;
101
+ },
102
+ querySelector: (selector) => query(root, selector, false)[0] ?? null,
103
+ querySelectorAll: (selector) => query(root, selector, true),
104
+ toMarkup: () => renderMarkup(root)
105
+ };
106
+ }
107
+ function renderMarkup(node) {
108
+ const attrs = Object.entries(node.attrs).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => ` ${k}=${JSON.stringify(v)}`).join("");
109
+ const valueAttr = node.value !== void 0 ? ` value=${JSON.stringify(node.value)}` : "";
110
+ const openTag = `<${node.tag}${attrs}${valueAttr}>`;
111
+ const inner = [
112
+ node.text ?? "",
113
+ ...node.children.map(renderMarkup)
114
+ ].join("");
115
+ return `${openTag}${inner}</${node.tag}>`;
116
+ }
117
+ function hashMarkup(markup) {
118
+ return (0, import_node_crypto.createHash)("sha256").update(markup).digest("hex").slice(0, 16);
119
+ }
120
+ function findByText(node, text) {
121
+ for (const child of node.children) {
122
+ const found = findByText(child, text);
123
+ if (found) return found;
124
+ }
125
+ if (nodeText(node) === text) return node;
126
+ return null;
127
+ }
128
+ function findByRole(node, role, accessibleName) {
129
+ const implicit = implicitRole(node);
130
+ const nodeRole = node.attrs["role"] ?? implicit;
131
+ if (nodeRole === role) {
132
+ if (accessibleName === void 0) return node;
133
+ const name = accessibleNameOf(node);
134
+ if (name === accessibleName) return node;
135
+ }
136
+ for (const child of node.children) {
137
+ const found = findByRole(child, role, accessibleName);
138
+ if (found) return found;
139
+ }
140
+ return null;
141
+ }
142
+ function nodeText(node) {
143
+ if (node.text !== void 0) return node.text.trim();
144
+ return node.children.map((c) => c.text ?? "").join("").trim();
145
+ }
146
+ function implicitRole(node) {
147
+ switch (node.tag) {
148
+ case "button":
149
+ return "button";
150
+ case "a":
151
+ return node.attrs["href"] ? "link" : null;
152
+ case "h1":
153
+ case "h2":
154
+ case "h3":
155
+ case "h4":
156
+ case "h5":
157
+ case "h6":
158
+ return "heading";
159
+ case "input": {
160
+ const type = node.attrs["type"] ?? "text";
161
+ if (type === "checkbox") return "checkbox";
162
+ if (type === "radio") return "radio";
163
+ if (type === "submit" || type === "button") return "button";
164
+ return "textbox";
165
+ }
166
+ case "textarea":
167
+ return "textbox";
168
+ case "nav":
169
+ return "navigation";
170
+ case "main":
171
+ return "main";
172
+ default:
173
+ return null;
174
+ }
175
+ }
176
+ function accessibleNameOf(node) {
177
+ const label = node.attrs["aria-label"];
178
+ if (label) return label;
179
+ return nodeText(node);
180
+ }
181
+ function query(root, selector, all) {
182
+ const parts = selector.trim().split(/\s+/);
183
+ let candidates = collectTree(root);
184
+ for (const part of parts) {
185
+ candidates = candidates.filter((n) => matchesSimple(n, part));
186
+ if (!all && candidates.length > 0) {
187
+ return [candidates[0]];
188
+ }
189
+ }
190
+ return all ? candidates : candidates.length > 0 ? [candidates[0]] : [];
191
+ }
192
+ function collectTree(root) {
193
+ const out = [root];
194
+ for (const child of root.children) {
195
+ out.push(...collectTree(child));
196
+ }
197
+ return out;
198
+ }
199
+ function matchesSimple(node, selector) {
200
+ let rest = selector;
201
+ const tagMatch = rest.match(/^([a-zA-Z][a-zA-Z0-9_-]*)/);
202
+ if (tagMatch) {
203
+ if (node.tag !== tagMatch[1]) return false;
204
+ rest = rest.slice(tagMatch[0].length);
205
+ }
206
+ while (rest.length > 0) {
207
+ const attr = rest.match(/^\[([a-zA-Z0-9_-]+)(=(\"[^\"]*\"|'[^']*'|[^\]]+))?\]/);
208
+ if (attr) {
209
+ const name = attr[1];
210
+ const raw = attr[3];
211
+ if (raw === void 0) {
212
+ if (node.attrs[name] === void 0) return false;
213
+ } else {
214
+ const unquoted = raw.replace(/^["']|["']$/g, "");
215
+ if (node.attrs[name] !== unquoted) return false;
216
+ }
217
+ rest = rest.slice(attr[0].length);
218
+ continue;
219
+ }
220
+ const cls = rest.match(/^\.([a-zA-Z0-9_-]+)/);
221
+ if (cls) {
222
+ const nodeClasses = (node.attrs["class"] ?? "").split(/\s+/).filter(Boolean);
223
+ if (!nodeClasses.includes(cls[1])) return false;
224
+ rest = rest.slice(cls[0].length);
225
+ continue;
226
+ }
227
+ const id = rest.match(/^#([a-zA-Z0-9_-]+)/);
228
+ if (id) {
229
+ if (node.attrs["id"] !== id[1]) return false;
230
+ rest = rest.slice(id[0].length);
231
+ continue;
232
+ }
233
+ return false;
234
+ }
235
+ return true;
236
+ }
237
+
238
+ // src/storybook.ts
239
+ function createStoryRegistry() {
240
+ const registry = /* @__PURE__ */ new Map();
241
+ const buildId = (title, storyName) => {
242
+ const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
243
+ return `${norm(title)}--${norm(storyName)}`;
244
+ };
245
+ const requireEntry = (title, storyName) => {
246
+ const id = buildId(title, storyName);
247
+ const entry = registry.get(id);
248
+ if (!entry) {
249
+ throw new Error(`StoryRegistry \u2014 no entry for ${id}`);
250
+ }
251
+ return entry;
252
+ };
253
+ return {
254
+ register(meta) {
255
+ for (const [storyName, story] of Object.entries(meta.stories)) {
256
+ const id = buildId(meta.title, storyName);
257
+ const mergedArgs = { ...meta.args ?? {}, ...story.args ?? {} };
258
+ const mergedParams = {
259
+ ...meta.parameters ?? {},
260
+ ...story.parameters ?? {},
261
+ ...meta.parameters?.chromatic || story.parameters?.chromatic ? {
262
+ chromatic: {
263
+ ...meta.parameters?.chromatic ?? {},
264
+ ...story.parameters?.chromatic ?? {}
265
+ }
266
+ } : {},
267
+ ...meta.parameters?.a11y || story.parameters?.a11y ? {
268
+ a11y: {
269
+ ...meta.parameters?.a11y ?? {},
270
+ ...story.parameters?.a11y ?? {}
271
+ }
272
+ } : {}
273
+ };
274
+ const entry = {
275
+ id,
276
+ title: meta.title,
277
+ storyName,
278
+ args: mergedArgs,
279
+ render: meta.render,
280
+ parameters: mergedParams,
281
+ ...story.play ? {
282
+ play: story.play
283
+ } : {}
284
+ };
285
+ registry.set(id, entry);
286
+ }
287
+ },
288
+ list() {
289
+ return Array.from(registry.values());
290
+ },
291
+ get(title, storyName) {
292
+ return requireEntry(title, storyName);
293
+ },
294
+ mount(title, storyName, overrideArgs) {
295
+ const entry = requireEntry(title, storyName);
296
+ const args = { ...entry.args, ...overrideArgs ?? {} };
297
+ const root = entry.render(args);
298
+ const canvas = createCanvas(root);
299
+ return { canvas, entry: { ...entry, args } };
300
+ },
301
+ async play(title, storyName, canvas, args) {
302
+ const entry = requireEntry(title, storyName);
303
+ const mergedArgs = { ...entry.args, ...args ?? {} };
304
+ const steps = [];
305
+ const context = {
306
+ canvasElement: canvas,
307
+ args: mergedArgs,
308
+ step: async (label, fn) => {
309
+ try {
310
+ await fn();
311
+ steps.push({ label, ok: true });
312
+ } catch (error) {
313
+ steps.push({ label, ok: false, error: error.message });
314
+ throw error;
315
+ }
316
+ }
317
+ };
318
+ if (!entry.play) {
319
+ return { steps: [], ok: true };
320
+ }
321
+ try {
322
+ await entry.play(context);
323
+ return { steps, ok: steps.every((s) => s.ok) };
324
+ } catch {
325
+ return { steps, ok: false };
326
+ }
327
+ },
328
+ runA11y(title, storyName, canvas) {
329
+ const entry = requireEntry(title, storyName);
330
+ if (entry.parameters.a11y?.disable) {
331
+ return { violations: [] };
332
+ }
333
+ const injected = entry.parameters.a11y?.injectViolations ?? [];
334
+ const detected = detectHeuristicViolations(canvas);
335
+ return { violations: [...injected, ...detected] };
336
+ }
337
+ };
338
+ }
339
+ function detectHeuristicViolations(canvas) {
340
+ const violations = [];
341
+ const buttons = canvas.querySelectorAll("button");
342
+ for (const btn of buttons) {
343
+ if (!hasAccessibleName(btn)) {
344
+ violations.push({
345
+ id: "button-name",
346
+ impact: "critical",
347
+ description: "Buttons must have discernible text",
348
+ nodes: [{ target: ["button"], html: "<button></button>" }]
349
+ });
350
+ }
351
+ }
352
+ const imgs = canvas.querySelectorAll("img");
353
+ for (const img of imgs) {
354
+ if (img.attrs["alt"] === void 0) {
355
+ violations.push({
356
+ id: "image-alt",
357
+ impact: "serious",
358
+ description: "Images must have alternate text",
359
+ nodes: [{ target: ["img"], html: "<img />" }]
360
+ });
361
+ }
362
+ }
363
+ const inputs = canvas.querySelectorAll("input");
364
+ for (const input of inputs) {
365
+ const type = input.attrs["type"] ?? "text";
366
+ if (type === "hidden" || type === "submit" || type === "button") continue;
367
+ if (!input.attrs["aria-label"] && !input.attrs["aria-labelledby"] && !hasAssociatedLabel(input, canvas)) {
368
+ violations.push({
369
+ id: "label",
370
+ impact: "critical",
371
+ description: "Form elements must have labels",
372
+ nodes: [{ target: ["input"], html: "<input />" }]
373
+ });
374
+ }
375
+ }
376
+ return violations;
377
+ }
378
+ function hasAccessibleName(node) {
379
+ if (node.attrs["aria-label"]) return true;
380
+ const text = collectText(node).trim();
381
+ return text.length > 0;
382
+ }
383
+ function collectText(node) {
384
+ const parts = [];
385
+ if (node.text) parts.push(node.text);
386
+ for (const child of node.children) {
387
+ parts.push(collectText(child));
388
+ }
389
+ return parts.join("");
390
+ }
391
+ function hasAssociatedLabel(input, canvas) {
392
+ const id = input.attrs["id"];
393
+ if (!id) return false;
394
+ const labels = canvas.querySelectorAll(`label[for=${JSON.stringify(id)}]`);
395
+ return labels.length > 0;
396
+ }
397
+
398
+ // src/playwright-ct.ts
399
+ function createPlaywrightCTMock() {
400
+ const active = /* @__PURE__ */ new Set();
401
+ return {
402
+ mount(render, args) {
403
+ const root = render(args);
404
+ const canvas = createCanvas(root);
405
+ const record = { root, canvas };
406
+ active.add(record);
407
+ return buildLocator(record, () => active.delete(record));
408
+ },
409
+ activeMounts() {
410
+ return active.size;
411
+ },
412
+ unmountAll() {
413
+ active.clear();
414
+ }
415
+ };
416
+ }
417
+ function buildLocator(record, dispose) {
418
+ return {
419
+ canvas: record.canvas,
420
+ root: record.root,
421
+ getByText: (text) => createTextLocator(record.canvas, text),
422
+ getByRole: (role, options) => createRoleLocator(record.canvas, role, options?.name),
423
+ unmount: () => {
424
+ clearHandlers(record.root);
425
+ dispose();
426
+ }
427
+ };
428
+ }
429
+ function createTextLocator(canvas, text) {
430
+ return {
431
+ click: async () => {
432
+ const node = canvas.getByText(text);
433
+ fireEvent(node, buildEvent("click", node));
434
+ },
435
+ fill: async (value) => {
436
+ const node = canvas.getByText(text);
437
+ node.value = value;
438
+ fireEvent(node, buildEvent("input", node, value));
439
+ },
440
+ textContent: async () => {
441
+ const node = tryFind(() => canvas.getByText(text));
442
+ return node ? nodeText2(node) : null;
443
+ },
444
+ count: async () => {
445
+ return tryFind(() => canvas.getByText(text)) ? 1 : 0;
446
+ },
447
+ node: () => tryFind(() => canvas.getByText(text))
448
+ };
449
+ }
450
+ function createRoleLocator(canvas, role, name) {
451
+ return {
452
+ click: async () => {
453
+ const node = canvas.getByRole(role, name !== void 0 ? { name } : void 0);
454
+ fireEvent(node, buildEvent("click", node));
455
+ },
456
+ fill: async (value) => {
457
+ const node = canvas.getByRole(role, name !== void 0 ? { name } : void 0);
458
+ node.value = value;
459
+ fireEvent(node, buildEvent("input", node, value));
460
+ },
461
+ textContent: async () => {
462
+ const node = tryFind(() => canvas.getByRole(role, name !== void 0 ? { name } : void 0));
463
+ return node ? nodeText2(node) : null;
464
+ },
465
+ count: async () => {
466
+ return tryFind(() => canvas.getByRole(role, name !== void 0 ? { name } : void 0)) ? 1 : 0;
467
+ },
468
+ node: () => tryFind(() => canvas.getByRole(role, name !== void 0 ? { name } : void 0))
469
+ };
470
+ }
471
+ function buildEvent(type, target, value) {
472
+ return {
473
+ type,
474
+ target,
475
+ ...value !== void 0 ? { value } : {}
476
+ };
477
+ }
478
+ function tryFind(fn) {
479
+ try {
480
+ return fn();
481
+ } catch {
482
+ return null;
483
+ }
484
+ }
485
+ function nodeText2(node) {
486
+ const parts = [];
487
+ if (node.text) parts.push(node.text);
488
+ for (const child of node.children) {
489
+ parts.push(nodeText2(child));
490
+ }
491
+ return parts.join("").trim();
492
+ }
493
+ function clearHandlers(node) {
494
+ node.handlers = {};
495
+ for (const child of node.children) {
496
+ clearHandlers(child);
497
+ }
498
+ }
499
+
500
+ // src/chromatic.ts
501
+ function createChromaticVisualMock(config = {}) {
502
+ const baselines = /* @__PURE__ */ new Map();
503
+ const currents = /* @__PURE__ */ new Map();
504
+ const reviews = [];
505
+ const defaultViewport = config.defaultViewport ?? "default";
506
+ const defaultThreshold = config.defaultDiffThreshold ?? 0;
507
+ const nowFn = config.now ?? (() => Date.now());
508
+ const keyOf = (storyId, viewport) => `${storyId}::${viewport}`;
509
+ return {
510
+ reset() {
511
+ baselines.clear();
512
+ currents.clear();
513
+ reviews.length = 0;
514
+ },
515
+ seedBaseline(input) {
516
+ const baseline = {
517
+ storyId: input.storyId,
518
+ viewport: input.viewport,
519
+ markup: input.markup,
520
+ hash: hashMarkup(input.markup),
521
+ capturedAt: input.capturedAt ?? nowFn()
522
+ };
523
+ baselines.set(keyOf(input.storyId, input.viewport), baseline);
524
+ return baseline;
525
+ },
526
+ capture(input) {
527
+ const viewport = input.viewport ?? defaultViewport;
528
+ if (input.entry.parameters.chromatic?.disable) {
529
+ return {
530
+ storyId: input.entry.id,
531
+ viewport,
532
+ baselineHash: "",
533
+ currentHash: "",
534
+ changed: false,
535
+ pixelDiffRatio: 0,
536
+ status: "passed",
537
+ threshold: defaultThreshold
538
+ };
539
+ }
540
+ const markup = input.canvas.toMarkup();
541
+ const current = {
542
+ storyId: input.entry.id,
543
+ viewport,
544
+ markup,
545
+ hash: hashMarkup(markup),
546
+ capturedAt: input.now ?? nowFn()
547
+ };
548
+ currents.set(keyOf(input.entry.id, viewport), current);
549
+ const baseline = baselines.get(keyOf(input.entry.id, viewport));
550
+ const threshold = input.entry.parameters.chromatic?.diffThreshold ?? defaultThreshold;
551
+ if (!baseline) {
552
+ baselines.set(keyOf(input.entry.id, viewport), current);
553
+ return {
554
+ storyId: input.entry.id,
555
+ viewport,
556
+ baselineHash: "",
557
+ currentHash: current.hash,
558
+ changed: false,
559
+ pixelDiffRatio: 0,
560
+ status: "new",
561
+ threshold
562
+ };
563
+ }
564
+ const changed = baseline.hash !== current.hash;
565
+ const pixelDiffRatio = changed ? 1 : 0;
566
+ const status = pixelDiffRatio > threshold ? "failed" : "passed";
567
+ return {
568
+ storyId: input.entry.id,
569
+ viewport,
570
+ baselineHash: baseline.hash,
571
+ currentHash: current.hash,
572
+ changed,
573
+ pixelDiffRatio,
574
+ status,
575
+ threshold
576
+ };
577
+ },
578
+ captureAll(input) {
579
+ if (input.entry.parameters.chromatic?.disable) {
580
+ return [];
581
+ }
582
+ const viewports = input.entry.parameters.chromatic?.viewports?.length ? input.entry.parameters.chromatic.viewports : [defaultViewport];
583
+ const now = input.now ?? nowFn();
584
+ return viewports.map(
585
+ (viewport) => this.capture({
586
+ entry: input.entry,
587
+ canvas: input.canvas,
588
+ viewport,
589
+ now
590
+ })
591
+ );
592
+ },
593
+ review(input) {
594
+ const key = keyOf(input.storyId, input.viewport);
595
+ const entry = {
596
+ storyId: input.storyId,
597
+ viewport: input.viewport,
598
+ action: input.action,
599
+ reviewedAt: input.reviewedAt ?? nowFn()
600
+ };
601
+ reviews.push(entry);
602
+ if (input.action === "accept") {
603
+ const current = currents.get(key);
604
+ if (!current) {
605
+ throw new Error(
606
+ `ChromaticVisualMock.review \u2014 cannot accept ${key}, no current capture found. Run capture() first.`
607
+ );
608
+ }
609
+ baselines.set(key, { ...current, capturedAt: entry.reviewedAt });
610
+ }
611
+ return entry;
612
+ },
613
+ reviewHistory() {
614
+ return [...reviews];
615
+ },
616
+ baselines() {
617
+ return Array.from(baselines.values());
618
+ }
619
+ };
620
+ }
621
+
622
+ // src/fixture.ts
623
+ var buildButton = (args) => {
624
+ const variant = args.variant ?? "primary";
625
+ const attrs = {
626
+ class: `btn btn-${variant}`,
627
+ type: "button"
628
+ };
629
+ if (args.disabled) {
630
+ attrs["disabled"] = "true";
631
+ attrs["aria-disabled"] = "true";
632
+ }
633
+ return createNode("button", {
634
+ attrs,
635
+ text: args.label,
636
+ ...args.onClick && !args.disabled ? { on: { click: args.onClick } } : {}
637
+ });
638
+ };
639
+ var buildInput = (args) => {
640
+ const wrapper = createNode("div", { attrs: { class: "input-group" } });
641
+ const label = createNode("label", {
642
+ attrs: { for: args.id, class: "input-label" },
643
+ text: args.label
644
+ });
645
+ const inputAttrs = {
646
+ id: args.id,
647
+ name: args.id,
648
+ type: args.type ?? "text",
649
+ class: "input-field"
650
+ };
651
+ if (args.required) inputAttrs["required"] = "true";
652
+ if (args.placeholder) inputAttrs["placeholder"] = args.placeholder;
653
+ const input = createNode("input", {
654
+ attrs: inputAttrs,
655
+ value: args.value ?? "",
656
+ ...args.onChange ? { on: { input: args.onChange } } : {}
657
+ });
658
+ appendChild(wrapper, label);
659
+ appendChild(wrapper, input);
660
+ return wrapper;
661
+ };
662
+ var buildForm = (args) => {
663
+ const form = createNode("form", {
664
+ attrs: { class: "form", role: "form" }
665
+ });
666
+ const heading = createNode("h2", {
667
+ attrs: { class: "form-title" },
668
+ text: args.title
669
+ });
670
+ appendChild(form, heading);
671
+ const fieldValues = /* @__PURE__ */ new Map();
672
+ for (const field of args.fields) {
673
+ fieldValues.set(field.id, field.value ?? "");
674
+ const wrapper = buildInput({
675
+ id: field.id,
676
+ label: field.label,
677
+ ...field.type !== void 0 ? { type: field.type } : {},
678
+ ...field.required !== void 0 ? { required: field.required } : {},
679
+ value: field.value ?? "",
680
+ onChange: (event) => {
681
+ if (event.value !== void 0) {
682
+ fieldValues.set(field.id, event.value);
683
+ }
684
+ }
685
+ });
686
+ appendChild(form, wrapper);
687
+ }
688
+ const submit = createNode("button", {
689
+ attrs: {
690
+ type: "submit",
691
+ class: "form-submit btn btn-primary"
692
+ },
693
+ text: args.submitLabel ?? "Submit",
694
+ on: {
695
+ click: () => {
696
+ for (const field of args.fields) {
697
+ if (field.required && !fieldValues.get(field.id)) {
698
+ return;
699
+ }
700
+ }
701
+ args.onSubmit?.(Object.fromEntries(fieldValues));
702
+ }
703
+ }
704
+ });
705
+ appendChild(form, submit);
706
+ return form;
707
+ };
708
+ var buildModal = (args) => {
709
+ if (!args.open) {
710
+ return createNode("div", {
711
+ attrs: { class: "modal modal-closed", "aria-hidden": "true" }
712
+ });
713
+ }
714
+ const backdrop = createNode("div", {
715
+ attrs: { class: "modal-backdrop", role: "presentation" },
716
+ ...args.closeOnBackdrop !== false && args.onClose ? { on: { click: () => args.onClose?.() } } : {}
717
+ });
718
+ const dialog = createNode("div", {
719
+ attrs: {
720
+ class: "modal-dialog",
721
+ role: "dialog",
722
+ "aria-modal": "true",
723
+ "aria-labelledby": "modal-title"
724
+ }
725
+ });
726
+ const heading = createNode("h2", {
727
+ attrs: { id: "modal-title", class: "modal-title" },
728
+ text: args.title
729
+ });
730
+ const body = createNode("p", {
731
+ attrs: { class: "modal-body" },
732
+ text: args.body
733
+ });
734
+ const closeBtn = createNode("button", {
735
+ attrs: {
736
+ type: "button",
737
+ class: "modal-close",
738
+ "aria-label": "Close"
739
+ },
740
+ text: "x",
741
+ ...args.onClose ? { on: { click: () => args.onClose?.() } } : {}
742
+ });
743
+ appendChild(dialog, heading);
744
+ appendChild(dialog, body);
745
+ appendChild(dialog, closeBtn);
746
+ appendChild(backdrop, dialog);
747
+ return backdrop;
748
+ };
749
+ var buildCard = (args) => {
750
+ const variant = args.variant ?? "default";
751
+ const card = createNode("article", {
752
+ attrs: { class: `card card-${variant}` }
753
+ });
754
+ const heading = createNode("h3", {
755
+ attrs: { class: "card-title" },
756
+ text: args.title
757
+ });
758
+ const body = createNode("p", {
759
+ attrs: { class: "card-body" },
760
+ text: args.body
761
+ });
762
+ appendChild(card, heading);
763
+ appendChild(card, body);
764
+ if (args.footer) {
765
+ const footer = createNode("footer", {
766
+ attrs: { class: "card-footer" },
767
+ text: args.footer
768
+ });
769
+ appendChild(card, footer);
770
+ }
771
+ return card;
772
+ };
773
+ var componentFixtures = {
774
+ Button: buildButton,
775
+ Input: buildInput,
776
+ Form: buildForm,
777
+ Modal: buildModal,
778
+ Card: buildCard
779
+ };
780
+ // Annotate the CommonJS export names for ESM import in node:
781
+ 0 && (module.exports = {
782
+ addHandler,
783
+ appendChild,
784
+ buildButton,
785
+ buildCard,
786
+ buildForm,
787
+ buildInput,
788
+ buildModal,
789
+ componentFixtures,
790
+ createCanvas,
791
+ createChromaticVisualMock,
792
+ createNode,
793
+ createPlaywrightCTMock,
794
+ createStoryRegistry,
795
+ findByRole,
796
+ findByText,
797
+ fireEvent,
798
+ hashMarkup,
799
+ query,
800
+ renderMarkup
801
+ });
802
+ //# sourceMappingURL=index.cjs.map