@schukai/monster 4.148.8 → 4.149.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 (209) hide show
  1. package/package.json +1 -1
  2. package/source/components/accessibility/stylesheet/locale-picker.mjs +61 -1
  3. package/source/components/content/image-editor.mjs +79 -11
  4. package/source/components/content/style/copy.pcss +1 -1
  5. package/source/components/content/stylesheet/camera-capture.mjs +1 -1
  6. package/source/components/content/stylesheet/copy.mjs +1 -1
  7. package/source/components/content/stylesheet/fetch-box.mjs +1 -1
  8. package/source/components/content/stylesheet/viewer.mjs +61 -1
  9. package/source/components/data/kpi-tile.mjs +5 -3
  10. package/source/components/data/metric-graph.mjs +22 -20
  11. package/source/components/data/metric.mjs +20 -18
  12. package/source/components/data/style/kpi-tile.pcss +65 -43
  13. package/source/components/data/style/metric-graph.pcss +35 -18
  14. package/source/components/data/style/metric.pcss +35 -28
  15. package/source/components/data/stylesheet/kpi-tile.mjs +61 -1
  16. package/source/components/data/stylesheet/metric-graph.mjs +61 -1
  17. package/source/components/data/stylesheet/metric.mjs +61 -1
  18. package/source/components/datatable/columnbar.mjs +4 -2
  19. package/source/components/datatable/datatable.mjs +73 -15
  20. package/source/components/datatable/filter/date-range.mjs +61 -61
  21. package/source/components/datatable/filter/date-time.mjs +5 -1
  22. package/source/components/datatable/filter/range.mjs +3 -3
  23. package/source/components/datatable/filter/select-operator.mjs +7 -1
  24. package/source/components/datatable/filter/text-operator.mjs +11 -3
  25. package/source/components/datatable/filter.mjs +16 -0
  26. package/source/components/datatable/pagination.mjs +2 -2
  27. package/source/components/datatable/style/column-bar.pcss +115 -23
  28. package/source/components/datatable/style/datatable.pcss +257 -29
  29. package/source/components/datatable/style/embedded-pagination.pcss +63 -34
  30. package/source/components/datatable/style/filter-button.pcss +1 -1
  31. package/source/components/datatable/style/filter-controls-defaults.pcss +49 -3
  32. package/source/components/datatable/style/filter-select.pcss +4 -1
  33. package/source/components/datatable/style/filter.pcss +18 -1
  34. package/source/components/datatable/stylesheet/change-button.mjs +61 -1
  35. package/source/components/datatable/stylesheet/column-bar.mjs +69 -1
  36. package/source/components/datatable/stylesheet/dataset.mjs +61 -1
  37. package/source/components/datatable/stylesheet/datatable.mjs +71 -1
  38. package/source/components/datatable/stylesheet/embedded-pagination.mjs +1 -1
  39. package/source/components/datatable/stylesheet/filter-button.mjs +1 -1
  40. package/source/components/datatable/stylesheet/filter-controls-defaults.mjs +1 -1
  41. package/source/components/datatable/stylesheet/filter-select.mjs +61 -1
  42. package/source/components/datatable/stylesheet/filter.mjs +61 -1
  43. package/source/components/datatable/stylesheet/pagination.mjs +67 -1
  44. package/source/components/datatable/stylesheet/save-button.mjs +61 -1
  45. package/source/components/datatable/stylesheet/status.mjs +61 -1
  46. package/source/components/form/action-button.mjs +2 -1
  47. package/source/components/form/button-bar.mjs +7 -2
  48. package/source/components/form/context-error.mjs +4 -3
  49. package/source/components/form/context-help.mjs +10 -4
  50. package/source/components/form/context-hint.mjs +1 -1
  51. package/source/components/form/context-info.mjs +1 -1
  52. package/source/components/form/context-note.mjs +1 -1
  53. package/source/components/form/context-success.mjs +1 -1
  54. package/source/components/form/context-warning.mjs +1 -1
  55. package/source/components/form/control-bar.mjs +300 -14
  56. package/source/components/form/credential-button.mjs +4 -2
  57. package/source/components/form/digits.mjs +28 -1
  58. package/source/components/form/field-set.mjs +30 -0
  59. package/source/components/form/login.mjs +13 -10
  60. package/source/components/form/password.mjs +6 -0
  61. package/source/components/form/popper-button.mjs +1 -1
  62. package/source/components/form/quantity.mjs +14 -6
  63. package/source/components/form/register-wizard.mjs +46 -26
  64. package/source/components/form/select.mjs +17 -10
  65. package/source/components/form/sheet.mjs +13 -2
  66. package/source/components/form/style/action-button.pcss +131 -5
  67. package/source/components/form/style/button-bar.pcss +204 -3
  68. package/source/components/form/style/control-bar.pcss +254 -26
  69. package/source/components/form/style/digits.pcss +7 -6
  70. package/source/components/form/style/input-group.pcss +65 -1
  71. package/source/components/form/style/login.pcss +1 -1
  72. package/source/components/form/style/mixin/field-set-layout.pcss +8 -0
  73. package/source/components/form/style/password.pcss +25 -1
  74. package/source/components/form/style/popper-button.pcss +1 -0
  75. package/source/components/form/style/quantity.pcss +11 -20
  76. package/source/components/form/style/select.pcss +52 -7
  77. package/source/components/form/style/sheet.pcss +70 -1
  78. package/source/components/form/style/state-button.pcss +5 -2
  79. package/source/components/form/style/toggle-switch.pcss +7 -4
  80. package/source/components/form/stylesheet/action-button.mjs +63 -1
  81. package/source/components/form/stylesheet/button-bar.mjs +153 -1
  82. package/source/components/form/stylesheet/button.mjs +61 -1
  83. package/source/components/form/stylesheet/control-bar.mjs +163 -1
  84. package/source/components/form/stylesheet/digits.mjs +1 -1
  85. package/source/components/form/stylesheet/field-layout.mjs +1 -1
  86. package/source/components/form/stylesheet/field-set.mjs +1 -1
  87. package/source/components/form/stylesheet/form.mjs +1 -1
  88. package/source/components/form/stylesheet/input-group.mjs +15 -1
  89. package/source/components/form/stylesheet/login.mjs +1 -1
  90. package/source/components/form/stylesheet/password.mjs +7 -1
  91. package/source/components/form/stylesheet/popper-button.mjs +61 -1
  92. package/source/components/form/stylesheet/quantity.mjs +1 -1
  93. package/source/components/form/stylesheet/register-wizard.mjs +1 -1
  94. package/source/components/form/stylesheet/select.mjs +3 -1
  95. package/source/components/form/stylesheet/sheet.mjs +1 -1
  96. package/source/components/form/stylesheet/state-button.mjs +3 -1
  97. package/source/components/form/stylesheet/toggle-switch.mjs +1 -1
  98. package/source/components/form/stylesheet/wizard.mjs +1 -1
  99. package/source/components/form/util/floating-ui.mjs +35 -7
  100. package/source/components/host/call-button.mjs +3 -1
  101. package/source/components/host/stylesheet/call-button.mjs +61 -1
  102. package/source/components/host/stylesheet/host.mjs +1 -1
  103. package/source/components/host/stylesheet/toggle-button.mjs +61 -1
  104. package/source/components/layout/full-screen.mjs +31 -23
  105. package/source/components/layout/iframe.mjs +5 -1
  106. package/source/components/layout/popper.mjs +34 -4
  107. package/source/components/layout/style/full-screen.pcss +94 -23
  108. package/source/components/layout/style/split-panel.pcss +57 -5
  109. package/source/components/layout/style/tabs.pcss +277 -47
  110. package/source/components/layout/style/vertical-tabs.pcss +147 -0
  111. package/source/components/layout/stylesheet/collapse.mjs +61 -1
  112. package/source/components/layout/stylesheet/details.mjs +61 -1
  113. package/source/components/layout/stylesheet/full-screen.mjs +23 -1
  114. package/source/components/layout/stylesheet/overlay.mjs +1 -1
  115. package/source/components/layout/stylesheet/panel.mjs +1 -1
  116. package/source/components/layout/stylesheet/slider.mjs +1 -1
  117. package/source/components/layout/stylesheet/split-panel.mjs +17 -1
  118. package/source/components/layout/stylesheet/tabs.mjs +97 -1
  119. package/source/components/layout/stylesheet/vertical-tabs.mjs +33 -95
  120. package/source/components/layout/tabs.mjs +63 -0
  121. package/source/components/layout/vertical-tabs.mjs +63 -0
  122. package/source/components/navigation/stylesheet/site-navigation.mjs +1 -1
  123. package/source/components/navigation/stylesheet/table-of-content.mjs +1 -1
  124. package/source/components/navigation/stylesheet/wizard-navigation.mjs +1 -1
  125. package/source/components/navigation/table-of-content.mjs +13 -0
  126. package/source/components/notify/style/notify.pcss +1 -2
  127. package/source/components/notify/stylesheet/message.mjs +1 -1
  128. package/source/components/notify/stylesheet/notify.mjs +1 -1
  129. package/source/components/state/style/log.pcss +1 -0
  130. package/source/components/state/style/state.pcss +5 -1
  131. package/source/components/state/stylesheet/log.mjs +1 -1
  132. package/source/components/state/stylesheet/state.mjs +7 -1
  133. package/source/components/style/button.css +61 -205
  134. package/source/components/style/button.pcss +78 -34
  135. package/source/components/style/common.css +1 -170
  136. package/source/components/style/common.pcss +5 -9
  137. package/source/components/style/form.css +1 -49
  138. package/source/components/style/form.pcss +24 -6
  139. package/source/components/style/link.css +1 -39
  140. package/source/components/style/link.pcss +8 -16
  141. package/source/components/style/mixin/button.pcss +19 -10
  142. package/source/components/style/mixin/property.pcss +8 -0
  143. package/source/components/style/mixin/skeleton.pcss +11 -5
  144. package/source/components/style/normalize.css +1 -144
  145. package/source/components/style/normalize.pcss +5 -5
  146. package/source/components/style/property.css +1 -387
  147. package/source/components/style/skeleton.css +1 -198
  148. package/source/components/style/skeleton.pcss +8 -2
  149. package/source/components/style/theme-modern.css +74 -0
  150. package/source/components/style/theme-modern.pcss +498 -0
  151. package/source/components/stylesheet/button.mjs +61 -1
  152. package/source/components/stylesheet/common.mjs +1 -1
  153. package/source/components/stylesheet/form.mjs +1 -1
  154. package/source/components/stylesheet/link.mjs +1 -1
  155. package/source/components/stylesheet/normalize.mjs +1 -1
  156. package/source/components/stylesheet/property.mjs +1 -1
  157. package/source/components/stylesheet/skeleton.mjs +1 -1
  158. package/source/components/stylesheet/theme-modern.mjs +110 -0
  159. package/source/components/time/stylesheet/day.mjs +61 -1
  160. package/source/components/time/stylesheet/month-calendar.mjs +61 -1
  161. package/source/components/time/timeline/stylesheet/appointment.mjs +61 -1
  162. package/source/components/time/timeline/stylesheet/segment.mjs +61 -1
  163. package/source/components/tree-menu/style/tree-menu.pcss +26 -37
  164. package/source/components/tree-menu/stylesheet/tree-menu.mjs +1 -1
  165. package/source/dom/customelement.mjs +105 -40
  166. package/source/dom/updater.mjs +381 -119
  167. package/test/cases/components/content/image-editor.mjs +19 -0
  168. package/test/cases/components/datatable/accessibility-defaults.mjs +112 -0
  169. package/test/cases/components/datatable/filter-availability.mjs +37 -0
  170. package/test/cases/components/datatable/filter-date-range-layout.mjs +40 -0
  171. package/test/cases/components/datatable/filter-select-operator.mjs +118 -0
  172. package/test/cases/components/datatable/mobile-layout.mjs +203 -0
  173. package/test/cases/components/datatable/responsive-breakpoint.mjs +41 -0
  174. package/test/cases/components/datatable/selection.mjs +149 -0
  175. package/test/cases/components/form/action-button-style.mjs +85 -0
  176. package/test/cases/components/form/button-bar.mjs +222 -3
  177. package/test/cases/components/form/context-theme-colors.mjs +59 -0
  178. package/test/cases/components/form/control-bar.mjs +228 -3
  179. package/test/cases/components/form/control-heights.mjs +103 -0
  180. package/test/cases/components/form/digits-accessibility.mjs +65 -0
  181. package/test/cases/components/form/field-set.mjs +54 -0
  182. package/test/cases/components/form/floating-ui.mjs +74 -0
  183. package/test/cases/components/form/input-group.mjs +61 -28
  184. package/test/cases/components/form/login.mjs +47 -0
  185. package/test/cases/components/form/password.mjs +63 -0
  186. package/test/cases/components/form/popper-button.mjs +30 -15
  187. package/test/cases/components/form/quantity-accessibility.mjs +46 -0
  188. package/test/cases/components/form/register-wizard.mjs +79 -0
  189. package/test/cases/components/form/select.mjs +94 -2
  190. package/test/cases/components/form/sheet.mjs +56 -2
  191. package/test/cases/components/host/toggle-button.mjs +50 -0
  192. package/test/cases/components/layout/full-screen.mjs +60 -0
  193. package/test/cases/components/layout/iframe-accessibility.mjs +44 -0
  194. package/test/cases/components/layout/popper.mjs +128 -0
  195. package/test/cases/components/layout/slit-panel.mjs +30 -1
  196. package/test/cases/components/layout/tabs.mjs +93 -0
  197. package/test/cases/components/navigation/table-of-content.mjs +18 -0
  198. package/test/cases/components/state/state-style.mjs +31 -0
  199. package/test/cases/components/style/action-menu.mjs +74 -0
  200. package/test/cases/components/style/form-controls.mjs +37 -0
  201. package/test/cases/components/style/kpi-tile.mjs +60 -0
  202. package/test/cases/components/style/legacy-theme-compatibility.mjs +137 -0
  203. package/test/cases/components/style/metric-layout.mjs +94 -0
  204. package/test/cases/components/style/skeleton.mjs +69 -0
  205. package/test/cases/components/style/theme-component-catalog.mjs +853 -0
  206. package/test/cases/components/style/theme-modern.mjs +439 -0
  207. package/test/cases/components/style/theme-review-regressions.mjs +41 -0
  208. package/test/cases/dom/customelement.mjs +136 -1
  209. package/test/cases/dom/updater.mjs +174 -0
@@ -0,0 +1,853 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { readFile, readdir } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import {
5
+ catalogFixtureData,
6
+ componentFamilies,
7
+ componentFixtures,
8
+ componentTags,
9
+ fixtureTags,
10
+ integrationNotes,
11
+ } from "../../../../development/playground/theme/component-catalog.mjs";
12
+ import {
13
+ componentReviewStorageKey,
14
+ formatComponentReviewReport,
15
+ loadComponentReviewState,
16
+ saveComponentReviewState,
17
+ summarizeComponentReviews,
18
+ } from "../../../../development/playground/theme/component-review.mjs";
19
+ import {
20
+ archiveComponentReviewHistoryNotes,
21
+ componentReviewSnapshot,
22
+ createComponentReviewSeed,
23
+ getComponentReviewHistoryEntries,
24
+ } from "../../../../development/playground/theme/component-review-history.mjs";
25
+ import themeSelectRoutes from "../../../../development/mock/theme-selects.js";
26
+ import { themeTokenPairFamilies } from "../../../../development/playground/theme/theme-token-pairs.mjs";
27
+
28
+ const projectRoot = resolve(import.meta.dirname, "../../../..");
29
+ const componentsRoot = resolve(projectRoot, "source/components");
30
+
31
+ describe("Theme Lab component catalog", () => {
32
+ it("accounts for every registered Monster custom element", async () => {
33
+ const registeredTags = await readRegisteredComponentTags();
34
+
35
+ assert.equal(new Set(componentTags).size, componentTags.length);
36
+ assert.deepEqual(
37
+ [...componentTags].sort(),
38
+ registeredTags,
39
+ "component catalog and registered custom elements must stay synchronized",
40
+ );
41
+ });
42
+
43
+ it("keeps fixture and integration metadata within the public inventory", () => {
44
+ const tags = new Set(componentTags);
45
+
46
+ for (const fixtureTag of fixtureTags) {
47
+ assert.ok(tags.has(fixtureTag), `unknown fixture tag: ${fixtureTag}`);
48
+ }
49
+
50
+ for (const integrationTag of Object.keys(integrationNotes)) {
51
+ assert.ok(
52
+ tags.has(integrationTag),
53
+ `unknown integration tag: ${integrationTag}`,
54
+ );
55
+ }
56
+
57
+ assert.equal(
58
+ componentFamilies.reduce(
59
+ (count, family) => count + family.tags.length,
60
+ 0,
61
+ ),
62
+ componentTags.length,
63
+ );
64
+ });
65
+
66
+ it("keeps audited Theme Lab fixtures semantically named", async () => {
67
+ const [index, main, catalog, styles] = await Promise.all([
68
+ readFile(
69
+ resolve(projectRoot, "development/playground/theme/index.html"),
70
+ "utf8",
71
+ ),
72
+ readFile(
73
+ resolve(projectRoot, "development/playground/theme/main.mjs"),
74
+ "utf8",
75
+ ),
76
+ readFile(
77
+ resolve(
78
+ projectRoot,
79
+ "development/playground/theme/component-catalog.mjs",
80
+ ),
81
+ "utf8",
82
+ ),
83
+ readFile(
84
+ resolve(
85
+ projectRoot,
86
+ "development/playground/theme/style/main.pcss",
87
+ ),
88
+ "utf8",
89
+ ),
90
+ ]);
91
+
92
+ assert.match(index, /<meta name="description" content="[^"]+">/u);
93
+ assert.doesNotMatch(index, /class="lab-brand"[^>]+aria-label=/u);
94
+ assert.doesNotMatch(
95
+ main,
96
+ /setAttribute\("aria-label", `\$\{tag\} controlled Theme Lab fixture`\)/u,
97
+ );
98
+ assert.match(
99
+ catalog,
100
+ /class="monster-spinner" role="status" aria-label="Loading"/u,
101
+ );
102
+ assert.match(
103
+ catalog,
104
+ /class="catalog-skeleton" role="status" aria-label="Loading preview"/u,
105
+ );
106
+ assert.doesNotMatch(
107
+ catalog,
108
+ /data-monster-option-aria-label="Save component fixture"/u,
109
+ );
110
+ assert.match(styles, /\.registry-action\s*\{[\s\S]*?font-size: 0\.875rem/u);
111
+ assert.match(
112
+ styles,
113
+ /\.component-fixture-stage p a\s*\{[\s\S]*?text-decoration: underline/u,
114
+ );
115
+ });
116
+
117
+ it("persists one review checkbox and note for every public component", () => {
118
+ const values = new Map();
119
+ const storage = {
120
+ getItem(key) {
121
+ return values.get(key) ?? null;
122
+ },
123
+ setItem(key, value) {
124
+ values.set(key, value);
125
+ },
126
+ };
127
+ const state = loadComponentReviewState(storage, componentTags);
128
+ const firstTag = componentTags[0];
129
+
130
+ assert.equal(Object.keys(state.reviews).length, componentTags.length);
131
+ assert.deepEqual(state.reviews[firstTag], {
132
+ checked: false,
133
+ note: "",
134
+ });
135
+
136
+ state.reviews[firstTag].checked = true;
137
+ state.reviews[firstTag].note = "Focus ring needs more contrast.";
138
+ saveComponentReviewState(storage, state);
139
+
140
+ const restoredState = loadComponentReviewState(storage, componentTags);
141
+ assert.deepEqual(restoredState.reviews[firstTag], {
142
+ checked: true,
143
+ note: "Focus ring needs more contrast.",
144
+ });
145
+ assert.deepEqual(summarizeComponentReviews(restoredState), {
146
+ total: componentTags.length,
147
+ reviewed: 1,
148
+ noted: 1,
149
+ });
150
+ assert.match(
151
+ formatComponentReviewReport(restoredState),
152
+ new RegExp(
153
+ `Progress: 1/${componentTags.length} reviewed[\\s\\S]*- \\[x\\] ${firstTag}[\\s\\S]*Focus ring needs more contrast`,
154
+ "u",
155
+ ),
156
+ );
157
+ assert.ok(values.has(componentReviewStorageKey));
158
+ });
159
+
160
+ it("preserves the initial user review as versioned page history", () => {
161
+ const knownTags = new Set(componentTags);
162
+ const commentedTags = componentReviewSnapshot.comments.map(
163
+ ({ tag }) => tag,
164
+ );
165
+ const reviewedTags = componentReviewSnapshot.reviewed;
166
+ const historyEntries = getComponentReviewHistoryEntries();
167
+
168
+ assert.equal(componentReviewSnapshot.comments.length, 46);
169
+ assert.equal(historyEntries.length, 59);
170
+ assert.equal(reviewedTags.length, 9);
171
+ assert.equal(new Set(commentedTags).size, commentedTags.length);
172
+ assert.equal(new Set(reviewedTags).size, reviewedTags.length);
173
+ assert.ok(commentedTags.every((tag) => knownTags.has(tag)));
174
+ assert.ok(reviewedTags.every((tag) => knownTags.has(tag)));
175
+ assert.deepEqual(
176
+ reviewedTags.filter((tag) => commentedTags.includes(tag)).sort(),
177
+ ["monster-call-button", "monster-template"],
178
+ );
179
+
180
+ const seed = createComponentReviewSeed();
181
+ const emptyStorage = {
182
+ getItem() {
183
+ return null;
184
+ },
185
+ };
186
+ const restoredState = loadComponentReviewState(
187
+ emptyStorage,
188
+ componentTags,
189
+ seed,
190
+ );
191
+ assert.deepEqual(summarizeComponentReviews(restoredState), {
192
+ total: componentTags.length,
193
+ reviewed: 9,
194
+ noted: 0,
195
+ });
196
+ assert.equal(restoredState.reviews["monster-datatable-filter"].note, "");
197
+ assert.equal(restoredState.reviews["monster-iframe"].note, "");
198
+ assert.ok(
199
+ historyEntries.every(
200
+ ({ resolution, status }) =>
201
+ resolution && ["accepted", "retest"].includes(status),
202
+ ),
203
+ );
204
+ assert.equal(
205
+ historyEntries.filter(({ status }) => status === "accepted").length,
206
+ 2,
207
+ );
208
+ assert.ok(
209
+ historyEntries.some(
210
+ ({ tag, text }) =>
211
+ tag === "monster-button-bar" &&
212
+ text.includes("ganzen popper mit buttons"),
213
+ ),
214
+ );
215
+ assert.ok(
216
+ historyEntries.some(
217
+ ({ tag, text }) =>
218
+ tag === "monster-popper" && text.includes("nicht am button"),
219
+ ),
220
+ );
221
+ assert.ok(
222
+ historyEntries.some(
223
+ ({ tag, text }) =>
224
+ tag === "monster-table-of-content" &&
225
+ text.includes("nicht am control"),
226
+ ),
227
+ );
228
+ assert.ok(
229
+ historyEntries.some(
230
+ ({ tag, text }) =>
231
+ tag === "monster-toggle-button" && text.includes("ist ein link"),
232
+ ),
233
+ );
234
+ assert.ok(
235
+ historyEntries.some(
236
+ ({ tag, text }) =>
237
+ tag === "monster-repeat-field-set" &&
238
+ text.includes("noch immer nicht"),
239
+ ),
240
+ );
241
+ assert.ok(
242
+ historyEntries.some(
243
+ ({ tag, text }) =>
244
+ tag === "monster-form" &&
245
+ text.includes("border um das select (rolle)"),
246
+ ),
247
+ );
248
+ assert.ok(
249
+ historyEntries.some(
250
+ ({ tag, text }) =>
251
+ tag === "monster-form" &&
252
+ text.includes("button sieht auch viel höeher"),
253
+ ),
254
+ );
255
+ assert.ok(
256
+ historyEntries.some(
257
+ ({ tag, text }) =>
258
+ tag === "monster-control-bar" &&
259
+ text.includes("save button hat einen scharzen rand"),
260
+ ),
261
+ );
262
+ assert.ok(
263
+ historyEntries.some(
264
+ ({ tag, text }) =>
265
+ tag === "monster-state" && text.includes("fill=currencolor"),
266
+ ),
267
+ );
268
+ });
269
+
270
+ it("archives known history notes without deleting newly written notes", () => {
271
+ const historicalText =
272
+ componentReviewSnapshot.comments.find(
273
+ ({ tag }) => tag === "monster-datatable-filter",
274
+ )?.text || "";
275
+ const state = {
276
+ reviews: {
277
+ "monster-datatable-filter": {
278
+ checked: false,
279
+ note: historicalText,
280
+ },
281
+ "monster-iframe": {
282
+ checked: false,
283
+ note: "New issue after the retest.",
284
+ },
285
+ "monster-sheet": {
286
+ checked: false,
287
+ note: "das dropdrow hat keinen rand und die menues snid unterscheiedlich zu den danderen . siehe bei den buttons.",
288
+ },
289
+ },
290
+ };
291
+
292
+ assert.equal(archiveComponentReviewHistoryNotes(state), true);
293
+ assert.equal(state.reviews["monster-datatable-filter"].note, "");
294
+ assert.equal(state.reviews["monster-sheet"].note, "");
295
+ assert.equal(
296
+ state.reviews["monster-iframe"].note,
297
+ "New issue after the retest.",
298
+ );
299
+ });
300
+
301
+ it("opens all controlled defaults and exposes adjacent review fields", async () => {
302
+ const [themeLabMain, themeLabMarkup] = await Promise.all([
303
+ readFile(
304
+ resolve(projectRoot, "development/playground/theme/main.mjs"),
305
+ "utf8",
306
+ ),
307
+ readFile(
308
+ resolve(projectRoot, "development/playground/theme/index.html"),
309
+ "utf8",
310
+ ),
311
+ ]);
312
+
313
+ assert.match(
314
+ themeLabMain,
315
+ /renderControlledComponentSandbox\(\)[\s\S]*?componentTags\.filter\(\(item\) => !fixtureTagSet\.has\(item\)\)/u,
316
+ );
317
+ assert.match(
318
+ themeLabMain,
319
+ /data-component-review-checked[\s\S]*?data-component-review-note/u,
320
+ );
321
+ assert.match(themeLabMain, /data-component-review-history-for/u);
322
+ assert.doesNotMatch(themeLabMain, /Mount default/u);
323
+ assert.match(themeLabMarkup, /id="component-review-progress"/u);
324
+ assert.match(themeLabMarkup, /data-copy-component-review/u);
325
+ assert.match(themeLabMarkup, /id="component-review-history-list"/u);
326
+ assert.match(themeLabMarkup, /All integration-safe defaults are open/u);
327
+ });
328
+
329
+ it("presents and tracks the modern theme as an automatic color scheme", async () => {
330
+ const [themeLabMain, themeLabMarkup] = await Promise.all([
331
+ readFile(
332
+ resolve(projectRoot, "development/playground/theme/main.mjs"),
333
+ "utf8",
334
+ ),
335
+ readFile(
336
+ resolve(projectRoot, "development/playground/theme/index.html"),
337
+ "utf8",
338
+ ),
339
+ ]);
340
+
341
+ assert.match(themeLabMarkup, /Modern · Auto/u);
342
+ assert.match(themeLabMarkup, /data-current-scheme/u);
343
+ assert.match(themeLabMarkup, /Issues #519 \+ #523/u);
344
+ assert.match(
345
+ themeLabMain,
346
+ /const colorSchemeQuery = window\.matchMedia\("\(prefers-color-scheme: dark\)"\)/u,
347
+ );
348
+ assert.match(
349
+ themeLabMain,
350
+ /colorSchemeQuery\.addEventListener\("change",[\s\S]*?updateThemeSummary\(\)[\s\S]*?renderContrastReport\(\)[\s\S]*?syncDarkReviewChecklist\(\)/u,
351
+ );
352
+ assert.match(
353
+ themeLabMain,
354
+ /function updateThemeSummary[\s\S]*?Modern · \$\{scheme\}[\s\S]*?prefers-color-scheme: \$\{scheme\.toLowerCase\(\)\}/u,
355
+ );
356
+ assert.match(
357
+ themeLabMain,
358
+ /function syncDarkReviewChecklist[\s\S]*?item\?\.id === "dark"[\s\S]*?checked: colorSchemeQuery\.matches[\s\S]*?disabled: false/u,
359
+ );
360
+ });
361
+
362
+ it("mounts repeat-field data in a form backed by a DOM datasource", async () => {
363
+ const themeLabMain = await readFile(
364
+ resolve(projectRoot, "development/playground/theme/main.mjs"),
365
+ "utf8",
366
+ );
367
+
368
+ assert.match(
369
+ themeLabMain,
370
+ /tag === "monster-repeat-field-set"[\s\S]*?createElement\("monster-datasource-dom"\)[\s\S]*?script\.type = "application\/json"[\s\S]*?JSON\.stringify\(\[createRepeatFieldSetDemoData\(\)\]\)[\s\S]*?datasource: \{ selector: `#\$\{datasourceId\}` \}[\s\S]*?mapping: \{ data: "", index: 0 \}[\s\S]*?data: createRepeatFieldSetDemoData\(\)[\s\S]*?fixture\.append\(datasource, form\)/u,
371
+ );
372
+ });
373
+
374
+ it("configures the control-bar save action as an actual primary button", async () => {
375
+ const themeLabMain = await readFile(
376
+ resolve(projectRoot, "development/playground/theme/main.mjs"),
377
+ "utf8",
378
+ );
379
+
380
+ assert.match(
381
+ themeLabMain,
382
+ /function appendControlBarDemo[\s\S]*?primary\.setOption\("classes\.button", "monster-button-primary"\)/u,
383
+ );
384
+ assert.doesNotMatch(
385
+ themeLabMain,
386
+ /primary\.classList\.add\("monster-button-primary"\)/u,
387
+ );
388
+ });
389
+
390
+ it("provides representative local data for tables and selection controls", () => {
391
+ assert.ok(catalogFixtureData.reviewRows.length >= 5);
392
+ for (const row of catalogFixtureData.reviewRows) {
393
+ for (const key of [
394
+ "id",
395
+ "component",
396
+ "area",
397
+ "owner",
398
+ "status",
399
+ "updated",
400
+ ]) {
401
+ assert.notEqual(row[key], undefined, `datatable row is missing ${key}`);
402
+ assert.notEqual(row[key], "", `datatable row has empty ${key}`);
403
+ }
404
+ }
405
+
406
+ for (const optionSet of [
407
+ catalogFixtureData.statusOptions,
408
+ catalogFixtureData.teamOptions,
409
+ catalogFixtureData.selectSearchOptions,
410
+ catalogFixtureData.treeOptions,
411
+ ]) {
412
+ assert.ok(optionSet.length >= 3);
413
+ assert.equal(
414
+ new Set(optionSet.map(({ value }) => value)).size,
415
+ optionSet.length,
416
+ "selection fixture values must be unique",
417
+ );
418
+ assert.ok(optionSet.every(({ label }) => label.length > 0));
419
+ }
420
+
421
+ assert.ok(catalogFixtureData.variants.length >= 4);
422
+ assert.ok(catalogFixtureData.calendarAppointments.length >= 3);
423
+ assert.ok(
424
+ catalogFixtureData.calendarAppointments.every(
425
+ ({ label, startDate, endDate }) => label && startDate && endDate,
426
+ ),
427
+ );
428
+ for (const tag of [
429
+ "monster-datatable",
430
+ "monster-dataset",
431
+ "monster-filter-select",
432
+ "monster-filter-select-operator",
433
+ "monster-select",
434
+ "monster-select-link",
435
+ "monster-tree-select",
436
+ "monster-variant-select",
437
+ ]) {
438
+ assert.ok(fixtureTags.includes(tag), `${tag} needs a populated fixture`);
439
+ }
440
+ });
441
+
442
+ it("uses accessible Bootstrap icon actions in the datatable fixture", () => {
443
+ const datatableFixture = componentFixtures.find(
444
+ ({ id }) => id === "datatable",
445
+ );
446
+
447
+ assert.ok(datatableFixture);
448
+ assert.match(
449
+ datatableFixture.html,
450
+ /data-monster-option-aria-label="Open review record details"[\s\S]*?class="catalog-datatable-action"[\s\S]*?data-monster-option-aria-label="More actions for review record"/u,
451
+ );
452
+ assert.equal(
453
+ datatableFixture.html.match(/class="catalog-datatable-action"/gu)?.length,
454
+ 2,
455
+ );
456
+ assert.equal(
457
+ datatableFixture.html.match(
458
+ /<svg[\s\S]*?aria-hidden="true"[\s\S]*?focusable="false"/gu,
459
+ )?.length,
460
+ 2,
461
+ );
462
+ assert.match(datatableFixture.html, /class="bi bi-chevron-right"/u);
463
+ assert.match(datatableFixture.html, /class="bi bi-three-dots"/u);
464
+ assert.match(
465
+ datatableFixture.html,
466
+ /<monster-action-button[\s\S]*?data-monster-option-aria-label="More actions for review record"[\s\S]*?"label":"Inspect record"[\s\S]*?"label":"Duplicate record"[\s\S]*?"label":"Archive record"[\s\S]*?class="bi bi-three-dots"[\s\S]*?<\/monster-action-button>/u,
467
+ );
468
+ assert.doesNotMatch(
469
+ datatableFixture.html,
470
+ /<span class="visually-hidden">More actions for review record<\/span>/u,
471
+ );
472
+ assert.match(
473
+ datatableFixture.html,
474
+ /"popper":\{"adaptiveSize":false,"boundaryElement":null,"placement":"bottom-end","strategy":"fixed"\}/u,
475
+ );
476
+ assert.equal(
477
+ datatableFixture.html.match(/height="18"[\s\S]*?width="18"/gu)?.length,
478
+ 2,
479
+ );
480
+ assert.match(
481
+ datatableFixture.html,
482
+ /<monster-button-bar[\s\S]*?style="--monster-control-bar-button-inline-size: 44px; --monster-control-bar-item-flex: 0 0 44px; --monster-control-padding-inline: var\(--monster-space-2\)"/u,
483
+ );
484
+ assert.doesNotMatch(
485
+ datatableFixture.html,
486
+ />\s*(?:Open|More)\s*<\/monster-button>/u,
487
+ );
488
+ });
489
+
490
+ it("uses visible semantic dots for both metric headers", () => {
491
+ const metricFixture = componentFixtures.find(({ id }) => id === "data");
492
+
493
+ assert.ok(metricFixture);
494
+ assert.match(
495
+ metricFixture.html,
496
+ /<monster-metric[\s\S]*?data-monster-option-classes-dot="monster-theme-success-1"/u,
497
+ );
498
+ assert.match(
499
+ metricFixture.html,
500
+ /<monster-metric-graph[\s\S]*?data-monster-option-classes-dot="monster-theme-error-1"/u,
501
+ );
502
+ });
503
+
504
+ it("provides inline, popper, and paginated select search fixtures", async () => {
505
+ const selectionFixture = componentFixtures.find(
506
+ ({ id }) => id === "selection-controls",
507
+ );
508
+
509
+ assert.ok(selectionFixture);
510
+ for (const id of [
511
+ "catalog-select-search-inline",
512
+ "catalog-select-search-popper",
513
+ "catalog-select-pagination",
514
+ ]) {
515
+ assert.match(selectionFixture.html, new RegExp(`id="${id}"`, "u"));
516
+ }
517
+ assert.match(
518
+ selectionFixture.html,
519
+ /"filter":\{"mode":"options","position":"inline"\}/u,
520
+ );
521
+ assert.match(
522
+ selectionFixture.html,
523
+ /"filter":\{"mode":"options","position":"popper"\}/u,
524
+ );
525
+ assert.match(
526
+ selectionFixture.html,
527
+ /\/development\/mock\/theme-select-options\.json\?filter=\{filter\}&page=\{page\}&perPage=5/u,
528
+ );
529
+ assert.match(selectionFixture.html, /"total":"pagination\.total"/u);
530
+ assert.match(selectionFixture.html, /"currentPage":"pagination\.page"/u);
531
+ assert.match(
532
+ selectionFixture.html,
533
+ /"objectsPerPage":"pagination\.perPage"/u,
534
+ );
535
+
536
+ const route = themeSelectRoutes.find(
537
+ ({ url }) => url === "/development/mock/theme-select-options.json",
538
+ );
539
+ assert.ok(route);
540
+
541
+ const requestPage = async (url) => {
542
+ let body = "";
543
+ await route.rawResponse(
544
+ { url, headers: { host: "example.test" } },
545
+ {
546
+ statusCode: null,
547
+ setHeader() {},
548
+ end(value) {
549
+ body = value;
550
+ },
551
+ },
552
+ );
553
+ return JSON.parse(body);
554
+ };
555
+ const secondPage = await requestPage(
556
+ "/development/mock/theme-select-options.json?filter=&page=2&perPage=5",
557
+ );
558
+ assert.equal(secondPage.options.length, 5);
559
+ assert.deepEqual(secondPage.pagination, {
560
+ total: 18,
561
+ page: 2,
562
+ perPage: 5,
563
+ });
564
+
565
+ const filteredPage = await requestPage(
566
+ "/development/mock/theme-select-options.json?filter=tabs&page=1&perPage=5",
567
+ );
568
+ assert.deepEqual(
569
+ filteredPage.options.map(({ value }) => value),
570
+ ["tabs", "vertical-tabs"],
571
+ );
572
+ assert.equal(filteredPage.pagination.total, 2);
573
+ });
574
+
575
+ it("centers deterministic status icons against their text groups", async () => {
576
+ const [themeLabMarkup, themeLabStyle] = await Promise.all([
577
+ readFile(
578
+ resolve(projectRoot, "development/playground/theme/index.html"),
579
+ "utf8",
580
+ ),
581
+ readFile(
582
+ resolve(projectRoot, "development/playground/theme/style/main.pcss"),
583
+ "utf8",
584
+ ),
585
+ ]);
586
+
587
+ assert.equal(themeLabMarkup.match(/class="status-symbol"/gu)?.length, 4);
588
+ assert.equal(
589
+ themeLabMarkup.match(
590
+ /<span class="status-symbol"[\s\S]*?<svg viewBox="0 0 16 16"/gu,
591
+ )?.length,
592
+ 4,
593
+ );
594
+ assert.match(themeLabStyle, /\.status-card\s*\{\s*align-items:\s*center;/u);
595
+ assert.match(
596
+ themeLabStyle,
597
+ /\.status-symbol svg\s*\{[\s\S]*?display:\s*block;/u,
598
+ );
599
+ });
600
+
601
+ it("shows every themed foreground and background pair as a text tile", async () => {
602
+ const [themeLabMarkup, themeLabScript] = await Promise.all([
603
+ readFile(
604
+ resolve(projectRoot, "development/playground/theme/index.html"),
605
+ "utf8",
606
+ ),
607
+ readFile(
608
+ resolve(projectRoot, "development/playground/theme/main.mjs"),
609
+ "utf8",
610
+ ),
611
+ ]);
612
+ const expectedFamilies = [
613
+ "primary",
614
+ "primary-disabled",
615
+ "secondary",
616
+ "tertiary",
617
+ "destructive",
618
+ "danger",
619
+ "success",
620
+ "warning",
621
+ "error",
622
+ "selection",
623
+ ];
624
+
625
+ assert.deepEqual(
626
+ themeTokenPairFamilies.map(({ id }) => id),
627
+ expectedFamilies,
628
+ );
629
+ assert.match(themeLabMarkup, /id="token-pair-catalog"/u);
630
+ assert.match(themeLabScript, /renderTokenPairCatalog\(\);/u);
631
+ assert.match(themeLabScript, /monster-theme-\$\{family\.id\}-\$\{index\}/u);
632
+ assert.match(
633
+ themeLabScript,
634
+ /--monster-bg-color-\$\{family\.id\}-\$\{index\}/u,
635
+ );
636
+ assert.equal(themeTokenPairFamilies.length * 4, 40);
637
+ });
638
+
639
+ it("keeps contrast color samples inside semantic table cells", async () => {
640
+ const themeLabMarkup = await readFile(
641
+ resolve(projectRoot, "development/playground/theme/index.html"),
642
+ "utf8",
643
+ );
644
+
645
+ assert.equal(
646
+ themeLabMarkup.match(/<td><span data-color-sample><\/span><\/td>/gu)
647
+ ?.length,
648
+ 6,
649
+ );
650
+ assert.doesNotMatch(themeLabMarkup, /<td data-color-sample/u);
651
+ });
652
+
653
+ it("keeps familiar bordered pills for table statuses", async () => {
654
+ const themeLabStyle = await readFile(
655
+ resolve(projectRoot, "development/playground/theme/style/main.pcss"),
656
+ "utf8",
657
+ );
658
+
659
+ assert.match(
660
+ themeLabStyle,
661
+ /\.status-pill\s*\{[\s\S]*?border:\s*1px solid currentColor;[\s\S]*?border-radius:\s*10rem;[\s\S]*?padding:\s*var\(--monster-space-1\) var\(--monster-space-2\);/u,
662
+ );
663
+ assert.doesNotMatch(
664
+ themeLabStyle,
665
+ /:root\[data-monster-theme-name="modern"\] \.status-pill/u,
666
+ );
667
+ });
668
+
669
+ it("keeps multiline display headings clear of adjacent selection rows", async () => {
670
+ const themeLabStyle = await readFile(
671
+ resolve(projectRoot, "development/playground/theme/style/main.pcss"),
672
+ "utf8",
673
+ );
674
+
675
+ assert.match(themeLabStyle, /\.hero h1\s*\{[\s\S]*?line-height:\s*1\.25;/u);
676
+ assert.match(
677
+ themeLabStyle,
678
+ /\.section-heading h2\s*\{[\s\S]*?line-height:\s*1\.25;/u,
679
+ );
680
+ });
681
+
682
+ it("renders six Nix-supplied Google Font proposal tiles", async () => {
683
+ const [
684
+ themeLabMarkup,
685
+ themeLabStyle,
686
+ themeLabFontStyle,
687
+ themeLabScript,
688
+ flakeSource,
689
+ commonPackagesSource,
690
+ ] = await Promise.all([
691
+ readFile(
692
+ resolve(projectRoot, "development/playground/theme/index.html"),
693
+ "utf8",
694
+ ),
695
+ readFile(
696
+ resolve(projectRoot, "development/playground/theme/style/main.pcss"),
697
+ "utf8",
698
+ ),
699
+ readFile(
700
+ resolve(projectRoot, "development/playground/theme/style/fonts.pcss"),
701
+ "utf8",
702
+ ),
703
+ readFile(
704
+ resolve(projectRoot, "development/playground/theme/main.mjs"),
705
+ "utf8",
706
+ ),
707
+ readFile(resolve(projectRoot, "flake.nix"), "utf8"),
708
+ readFile(resolve(projectRoot, "nix/config/common-packages.nix"), "utf8"),
709
+ ]);
710
+ const proposalFonts = [
711
+ ["Atkinson Hyperlegible Next", "AtkinsonHyperlegibleNext-Regular"],
712
+ ["Figtree", "Figtree-Regular"],
713
+ ["IBM Plex Sans", "IBMPlexSans-Regular"],
714
+ ["DM Sans", "DMSans-Regular"],
715
+ ["Bricolage Grotesque", "BricolageGrotesque-Regular"],
716
+ ["Lexend", "Lexend-Regular"],
717
+ ];
718
+
719
+ assert.match(themeLabMarkup, /id="typography-proposals"/u);
720
+ assert.equal(
721
+ themeLabMarkup.match(/data-font-proposal(?:\s|>)/gu)?.length,
722
+ 6,
723
+ );
724
+ for (const [family, localName] of proposalFonts) {
725
+ assert.match(
726
+ themeLabMarkup,
727
+ new RegExp(`data-font-family="${family}"`, "u"),
728
+ );
729
+ assert.ok(
730
+ themeLabFontStyle.includes(`local("${localName}")`),
731
+ `${family} must resolve from the local Nix font package`,
732
+ );
733
+ }
734
+ assert.equal(themeLabFontStyle.match(/@font-face/gu)?.length, 6);
735
+ assert.match(
736
+ themeLabMarkup,
737
+ /03 · Humanist[\s\S]*?<code>IBM Plex Sans<\/code>/u,
738
+ );
739
+ assert.doesNotMatch(themeLabMarkup, /fonts\.googleapis\.com/u);
740
+ assert.match(themeLabScript, /import "\.\/style\/fonts\.pcss";/u);
741
+ assert.match(commonPackagesSource, /\bgoogle-fonts\b/u);
742
+ assert.match(
743
+ flakeSource,
744
+ /FONTCONFIG_FILE = fontConfig;[\s\S]*FONTCONFIG_FILE = fontConfig;/u,
745
+ );
746
+ assert.match(
747
+ themeLabMarkup,
748
+ /data-font-proposal[\s\S]*?<h3>[\s\S]*?<h4>[\s\S]*?<p>[\s\S]*?<button/u,
749
+ );
750
+ assert.match(
751
+ themeLabStyle,
752
+ /\.font-proposal-grid\s*\{[\s\S]*?grid-template-columns:\s*repeat\(3,\s*minmax\(0,\s*1fr\)\);/u,
753
+ );
754
+ assert.match(
755
+ themeLabStyle,
756
+ /\.font-proposal-actions button\s*\{[\s\S]*?font-family:\s*var\(--font-proposal-family\);[\s\S]*?min-height:\s*3rem;/u,
757
+ );
758
+ });
759
+
760
+ it("gives the date-range sandbox enough room for its popper", async () => {
761
+ const themeLabStyle = await readFile(
762
+ resolve(projectRoot, "development/playground/theme/style/main.pcss"),
763
+ "utf8",
764
+ );
765
+
766
+ assert.match(
767
+ themeLabStyle,
768
+ /\.sandbox-specimen\[data-sandbox-tag="monster-filter-date-range"\][\s\S]*?min-height:\s*34rem;/u,
769
+ );
770
+ });
771
+
772
+ it("bounds the controlled iframe fixture without viewport-height feedback", async () => {
773
+ const themeLabStyle = await readFile(
774
+ resolve(projectRoot, "development/playground/theme/style/main.pcss"),
775
+ "utf8",
776
+ );
777
+
778
+ assert.match(
779
+ themeLabStyle,
780
+ /\.sandbox-specimen\[data-sandbox-tag="monster-iframe"\][\s\S]*?block-size:\s*18rem;[\s\S]*?min-height:\s*0;/u,
781
+ );
782
+ });
783
+
784
+ it("uses an editorial hierarchy instead of repeated rounded cards", async () => {
785
+ const themeLabStyle = await readFile(
786
+ resolve(projectRoot, "development/playground/theme/style/main.pcss"),
787
+ "utf8",
788
+ );
789
+
790
+ assert.match(
791
+ themeLabStyle,
792
+ /:root\[data-monster-theme-name="modern"\] \.lab-header\s*\{[\s\S]*?border-radius:\s*0;[\s\S]*?box-shadow:\s*none;/u,
793
+ );
794
+ assert.match(
795
+ themeLabStyle,
796
+ /:root\[data-monster-theme-name="modern"\] \.token-pair-card\s*\{[\s\S]*?border-top:\s*3px solid currentColor;[\s\S]*?box-shadow:\s*none;/u,
797
+ );
798
+ assert.match(
799
+ themeLabStyle,
800
+ /:root\[data-monster-theme-name="modern"\] \.component-family-navigation a\s*\{[\s\S]*?border-bottom:\s*1px solid/u,
801
+ );
802
+ });
803
+
804
+ it("uses semantic state and focus tokens in audited component styles", async () => {
805
+ const metricStyle = await readFile(
806
+ resolve(componentsRoot, "data/style/metric.pcss"),
807
+ "utf8",
808
+ );
809
+ const buttonStyle = await readFile(
810
+ resolve(componentsRoot, "style/button.pcss"),
811
+ "utf8",
812
+ );
813
+ const treeMenuStyle = await readFile(
814
+ resolve(componentsRoot, "tree-menu/style/tree-menu.pcss"),
815
+ "utf8",
816
+ );
817
+
818
+ assert.match(buttonStyle, /\.monster-button-destructive/u);
819
+ assert.match(buttonStyle, /--monster-bg-color-destructive-1/u);
820
+ assert.match(metricStyle, /--monster-theme-success/u);
821
+ assert.match(metricStyle, /--monster-theme-error/u);
822
+ assert.doesNotMatch(metricStyle, /\.positive[\s\S]*--monster-color-red/u);
823
+ assert.match(treeMenuStyle, /:focus-visible/u);
824
+ assert.match(treeMenuStyle, /--monster-focus-ring-color/u);
825
+ assert.doesNotMatch(treeMenuStyle, /prefers-color-scheme/u);
826
+ });
827
+ });
828
+
829
+ async function readRegisteredComponentTags() {
830
+ const entries = await readdir(componentsRoot, {
831
+ recursive: true,
832
+ withFileTypes: true,
833
+ });
834
+ const tags = new Set();
835
+
836
+ for (const entry of entries) {
837
+ if (!entry.isFile() || !entry.name.endsWith(".mjs")) {
838
+ continue;
839
+ }
840
+
841
+ const filename = resolve(entry.parentPath, entry.name);
842
+ const source = await readFile(filename, "utf8");
843
+ if (!/^\s*registerCustomElement\(/mu.test(source)) {
844
+ continue;
845
+ }
846
+
847
+ const match = source.match(/return\s+"(monster-[^"]+)"/u);
848
+ assert.ok(match, `registered component has no static tag in ${filename}`);
849
+ tags.add(match[1]);
850
+ }
851
+
852
+ return [...tags].sort();
853
+ }