@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.117 → 2.0.0-next.119

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 (251) hide show
  1. package/.storybook/manager.ts +18 -16
  2. package/bundle/openbridge-webcomponents.bundle.js +37 -33
  3. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  4. package/custom-elements.json +30 -24
  5. package/dist/ar/poi/poi-base.d.ts +6 -0
  6. package/dist/ar/poi/poi-base.d.ts.map +1 -1
  7. package/dist/ar/poi/poi-base.js.map +1 -1
  8. package/dist/ar/poi-button/poi-button.d.ts +2 -2
  9. package/dist/ar/poi-button/poi-button.js.map +1 -1
  10. package/dist/automation/vertical-line/vertical-line.d.ts +6 -0
  11. package/dist/automation/vertical-line/vertical-line.d.ts.map +1 -1
  12. package/dist/automation/vertical-line/vertical-line.js.map +1 -1
  13. package/dist/components/accordion-card/accordion-card.d.ts.map +1 -1
  14. package/dist/components/accordion-card/accordion-card.js.map +1 -1
  15. package/dist/components/advice-message-item/advice-message-item.d.ts +4 -2
  16. package/dist/components/advice-message-item/advice-message-item.d.ts.map +1 -1
  17. package/dist/components/advice-message-item/advice-message-item.js +4 -2
  18. package/dist/components/advice-message-item/advice-message-item.js.map +1 -1
  19. package/dist/components/app-menu/app-menu.d.ts +1 -1
  20. package/dist/components/app-menu/app-menu.js +1 -1
  21. package/dist/components/app-menu/app-menu.js.map +1 -1
  22. package/dist/components/brilliance-menu/brilliance-menu.d.ts +8 -4
  23. package/dist/components/brilliance-menu/brilliance-menu.d.ts.map +1 -1
  24. package/dist/components/brilliance-menu/brilliance-menu.js +8 -4
  25. package/dist/components/brilliance-menu/brilliance-menu.js.map +1 -1
  26. package/dist/components/check-button/check-button.js.map +1 -1
  27. package/dist/components/checkbox/checkbox.d.ts +2 -2
  28. package/dist/components/checkbox/checkbox.js.map +1 -1
  29. package/dist/components/command-menu/command-menu.d.ts +1 -1
  30. package/dist/components/command-menu/command-menu.js +1 -1
  31. package/dist/components/command-menu/command-menu.js.map +1 -1
  32. package/dist/components/dropdown-button/dropdown-button.d.ts +2 -2
  33. package/dist/components/dropdown-button/dropdown-button.js +2 -2
  34. package/dist/components/dropdown-button/dropdown-button.js.map +1 -1
  35. package/dist/components/menu-button/menu-button.d.ts.map +1 -1
  36. package/dist/components/menu-button/menu-button.js.map +1 -1
  37. package/dist/components/navigation-item/navigation-item.d.ts +1 -5
  38. package/dist/components/navigation-item/navigation-item.d.ts.map +1 -1
  39. package/dist/components/navigation-item/navigation-item.js +0 -8
  40. package/dist/components/navigation-item/navigation-item.js.map +1 -1
  41. package/dist/components/navigation-item-group/navigation-item-group.d.ts +2 -1
  42. package/dist/components/navigation-item-group/navigation-item-group.d.ts.map +1 -1
  43. package/dist/components/navigation-item-group/navigation-item-group.js +2 -1
  44. package/dist/components/navigation-item-group/navigation-item-group.js.map +1 -1
  45. package/dist/components/notification-button/notification-button.d.ts +1 -1
  46. package/dist/components/notification-button/notification-button.js +1 -1
  47. package/dist/components/notification-button/notification-button.js.map +1 -1
  48. package/dist/components/notification-message-item/notification-message-item.d.ts +4 -2
  49. package/dist/components/notification-message-item/notification-message-item.d.ts.map +1 -1
  50. package/dist/components/notification-message-item/notification-message-item.js +4 -2
  51. package/dist/components/notification-message-item/notification-message-item.js.map +1 -1
  52. package/dist/components/pivot-item/pivot-item.d.ts +1 -1
  53. package/dist/components/pivot-item/pivot-item.js +1 -1
  54. package/dist/components/pivot-item/pivot-item.js.map +1 -1
  55. package/dist/components/pivot-item-group/pivot-item-group.d.ts +1 -0
  56. package/dist/components/pivot-item-group/pivot-item-group.d.ts.map +1 -1
  57. package/dist/components/pivot-item-group/pivot-item-group.js.map +1 -1
  58. package/dist/components/slide-button/slide-button.d.ts.map +1 -1
  59. package/dist/components/slide-button/slide-button.js.map +1 -1
  60. package/dist/components/slider/slider.d.ts +2 -2
  61. package/dist/components/slider/slider.js +2 -2
  62. package/dist/components/slider/slider.js.map +1 -1
  63. package/dist/components/slider-double/slider-double.d.ts +2 -2
  64. package/dist/components/slider-double/slider-double.js +2 -2
  65. package/dist/components/slider-double/slider-double.js.map +1 -1
  66. package/dist/components/stepper-box/stepper-box.d.ts +4 -2
  67. package/dist/components/stepper-box/stepper-box.d.ts.map +1 -1
  68. package/dist/components/stepper-box/stepper-box.js +4 -2
  69. package/dist/components/stepper-box/stepper-box.js.map +1 -1
  70. package/dist/components/system-button/system-button.d.ts +1 -1
  71. package/dist/components/system-button/system-button.js +1 -1
  72. package/dist/components/system-button/system-button.js.map +1 -1
  73. package/dist/components/tabbed-card/tabbed-card.d.ts.map +1 -1
  74. package/dist/components/tabbed-card/tabbed-card.js.map +1 -1
  75. package/dist/components/toggle-button-option/toggle-button-option.d.ts +2 -1
  76. package/dist/components/toggle-button-option/toggle-button-option.d.ts.map +1 -1
  77. package/dist/components/toggle-button-option/toggle-button-option.js +2 -1
  78. package/dist/components/toggle-button-option/toggle-button-option.js.map +1 -1
  79. package/dist/components/toggle-button-vertical-group/toggle-button-vertical-group.js.map +1 -1
  80. package/dist/components/toggle-switch/toggle-switch.d.ts +1 -1
  81. package/dist/components/toggle-switch/toggle-switch.js +1 -1
  82. package/dist/components/toggle-switch/toggle-switch.js.map +1 -1
  83. package/dist/components/top-bar/top-bar.d.ts.map +1 -1
  84. package/dist/components/top-bar/top-bar.js.map +1 -1
  85. package/dist/components/topbar-message-item/topbar-message-item.js.map +1 -1
  86. package/dist/integration-systems/integration-dropdown-button/integration-dropdown-button.d.ts +1 -1
  87. package/dist/integration-systems/integration-dropdown-button/integration-dropdown-button.js +1 -1
  88. package/dist/integration-systems/integration-dropdown-button/integration-dropdown-button.js.map +1 -1
  89. package/eslint.config.mjs +355 -0
  90. package/package.json +3 -1
  91. package/script/check-slot-event-docs.ts +36 -3
  92. package/script/eslint-rules.test.ts +230 -0
  93. package/src/ar/building-blocks/poi-graphic-line/poi-graphic-line.stories.ts +1 -1
  94. package/src/ar/building-blocks/poi-header/poi-header.stories.ts +1 -1
  95. package/src/ar/building-blocks/poi-line/poi-line.stories.ts +1 -1
  96. package/src/ar/building-blocks/poi-pointer/poi-pointer.stories.ts +1 -1
  97. package/src/ar/building-blocks/poi-selection-frame/poi-selection-frame.stories.ts +1 -1
  98. package/src/ar/building-blocks/ruler-pointer/ruler-pointer.stories.ts +1 -1
  99. package/src/ar/chart-object-vessel-button/chart-object-vessel-button.stories.ts +1 -1
  100. package/src/ar/poi/poi-aton.stories.ts +1 -1
  101. package/src/ar/poi/poi-base.ts +6 -0
  102. package/src/ar/poi/poi-data.stories.ts +1 -1
  103. package/src/ar/poi/poi-vessel.stories.ts +1 -1
  104. package/src/ar/poi/poi.stories.ts +1 -1
  105. package/src/ar/poi-button/poi-button-aton.stories.ts +1 -1
  106. package/src/ar/poi-button/poi-button-data.stories.ts +1 -1
  107. package/src/ar/poi-button/poi-button-vessel.stories.ts +1 -1
  108. package/src/ar/poi-button/poi-button.stories.ts +1 -1
  109. package/src/ar/poi-button/poi-button.ts +2 -2
  110. package/src/ar/poi-card/poi-card.stories.ts +1 -1
  111. package/src/ar/poi-card-header/poi-card-header.stories.ts +1 -1
  112. package/src/ar/poi-controller/poi-controller.stories.ts +1 -1
  113. package/src/ar/poi-group/poi-group.stories.ts +1 -1
  114. package/src/ar/poi-layer/poi-layer.stories.ts +1 -1
  115. package/src/ar/poi-layer-stack/poi-layer-stack.stories.ts +1 -1
  116. package/src/ar/poi-object/poi-object-aton.stories.ts +1 -1
  117. package/src/ar/poi-object/poi-object-data.stories.ts +1 -1
  118. package/src/ar/poi-object/poi-object-vessel.stories.ts +1 -1
  119. package/src/ar/poi-object/poi-object.stories.ts +1 -1
  120. package/src/automation/automation-input-modal/automation-input-modal.stories.ts +1 -1
  121. package/src/automation/automation-readout/automation-readout.stories.ts +1 -1
  122. package/src/automation/automation-tank/automation-tank.stories.ts +1 -1
  123. package/src/automation/bus/bus.stories.ts +1 -1
  124. package/src/automation/corner-line/corner-line.stories.ts +1 -1
  125. package/src/automation/direction-line/direction-line.stories.ts +1 -1
  126. package/src/automation/end-point-line/end-point-line.stories.ts +1 -1
  127. package/src/automation/horizontal-line/horizontal-line.stories.ts +1 -1
  128. package/src/automation/line-cross/line-cross.stories.ts +1 -1
  129. package/src/automation/line-overlap/line-overlap.stories.ts +1 -1
  130. package/src/automation/three-way-line/three-way-line.stories.ts +1 -1
  131. package/src/automation/vertical-line/vertical-line.stories.ts +1 -1
  132. package/src/automation/vertical-line/vertical-line.ts +2 -1
  133. package/src/bars-graphs/area-graph/area-graph.stories.ts +1 -1
  134. package/src/bars-graphs/donut-chart/donut-chart.stories.ts +1 -1
  135. package/src/bars-graphs/line-graph/line-graph.stories.ts +1 -1
  136. package/src/bars-graphs/pie-chart/pie-chart.stories.ts +1 -1
  137. package/src/bars-graphs/polar-chart/polar-chart.stories.ts +1 -1
  138. package/src/bars-graphs/radial-bar-chart/radial-bar-chart.stories.ts +1 -1
  139. package/src/building-blocks/bar-horizontal/bar-horizontal.stories.ts +1 -1
  140. package/src/building-blocks/bar-vertical/bar-vertical.stories.ts +1 -1
  141. package/src/building-blocks/chart-line/chart-line-base.stories.ts +1 -1
  142. package/src/building-blocks/instrument-radial/instrument-radial.stories.ts +1 -1
  143. package/src/building-blocks/readout-block/readout-block.stories.ts +1 -1
  144. package/src/components/accordion-card/accordion-card.ts +2 -1
  145. package/src/components/advice-floating-item/advice-floating-item.stories.ts +1 -1
  146. package/src/components/advice-menu-item/advice-menu-item.stories.ts +1 -1
  147. package/src/components/advice-message-item/advice-message-item.stories.ts +1 -1
  148. package/src/components/advice-message-item/advice-message-item.ts +4 -2
  149. package/src/components/alert-frame/alert-frame.stories.ts +1 -1
  150. package/src/components/app-menu/app-menu.ts +1 -1
  151. package/src/components/attachment-list-item/attachment-list-item.stories.ts +1 -1
  152. package/src/components/audio-output/audio-output.stories.ts +1 -1
  153. package/src/components/audio-recording-item/audio-recording-item.stories.ts +1 -1
  154. package/src/components/automation-button-readout-stack/automation-button-readout-stack.stories.ts +1 -1
  155. package/src/components/badge/badge.stories.ts +1 -1
  156. package/src/components/brilliance-menu/brilliance-menu.ts +8 -4
  157. package/src/components/calendar/calendar.stories.ts +1 -1
  158. package/src/components/check-button/check-button.ts +1 -1
  159. package/src/components/checkbox/checkbox.ts +2 -2
  160. package/src/components/command-button/command-button.stories.ts +1 -1
  161. package/src/components/command-menu/command-menu.stories.ts +1 -1
  162. package/src/components/command-menu/command-menu.ts +1 -1
  163. package/src/components/communication-table/communication-table.stories.ts +1 -1
  164. package/src/components/context-menu-input/context-menu-input.stories.ts +1 -1
  165. package/src/components/date-item/date-item.stories.ts +1 -1
  166. package/src/components/dropdown-button/dropdown-button.ts +2 -2
  167. package/src/components/event-item/event-item.stories.ts +1 -1
  168. package/src/components/event-list/event-list.stories.ts +1 -1
  169. package/src/components/form-container/form-container.stories.ts +1 -0
  170. package/src/components/form-footer-container/form-footer-container.stories.ts +1 -0
  171. package/src/components/form-group/form-group.stories.ts +1 -0
  172. package/src/components/form-item/form-item.stories.ts +1 -1
  173. package/src/components/keyboard-full/keyboard-full.stories.ts +1 -1
  174. package/src/components/keyboard-numeric/keyboard-numeric.stories.ts +1 -1
  175. package/src/components/menu-button/menu-button.stories.ts +1 -1
  176. package/src/components/menu-button/menu-button.ts +8 -4
  177. package/src/components/message-menu-item/message-menu-item.stories.ts +1 -1
  178. package/src/components/navigation-item/navigation-item.ts +1 -10
  179. package/src/components/navigation-item-group/navigation-item-group.ts +2 -1
  180. package/src/components/notification-badge-button/notification-badge-button.stories.ts +1 -1
  181. package/src/components/notification-button/notification-button.ts +1 -1
  182. package/src/components/notification-floating-item/notification-floating-item.stories.ts +1 -1
  183. package/src/components/notification-menu-item/notification-menu-item.stories.ts +1 -1
  184. package/src/components/notification-message-item/notification-message-item.stories.ts +1 -1
  185. package/src/components/notification-message-item/notification-message-item.ts +4 -2
  186. package/src/components/pivot-item/pivot-item.ts +1 -1
  187. package/src/components/pivot-item-group/pivot-item-group.ts +1 -0
  188. package/src/components/rich-button/rich-button.stories.ts +1 -1
  189. package/src/components/sequence-card/sequence-card.stories.ts +1 -1
  190. package/src/components/sequence-connector/sequence-connector.stories.ts +1 -1
  191. package/src/components/sequence-item/sequence-item.stories.ts +1 -0
  192. package/src/components/sequence-loading-spinner/sequence-loading-spinner.stories.ts +1 -1
  193. package/src/components/sequence-modal/sequence-modal.stories.ts +1 -1
  194. package/src/components/sequence-step/sequence-step.stories.ts +1 -0
  195. package/src/components/sequence-toolbar/sequence-toolbar.stories.ts +1 -0
  196. package/src/components/slide-button/slide-button.stories.ts +1 -1
  197. package/src/components/slide-button/slide-button.ts +2 -1
  198. package/src/components/slider/slider.ts +2 -2
  199. package/src/components/slider-double/slider-double.ts +2 -2
  200. package/src/components/split-button/split-button.stories.ts +1 -1
  201. package/src/components/status-indicator/status-indicator.stories.ts +1 -1
  202. package/src/components/stepper-box/stepper-box.ts +4 -2
  203. package/src/components/system-button/system-button.ts +5 -5
  204. package/src/components/tabbed-card/tabbed-card.ts +2 -1
  205. package/src/components/table/table.stories.ts +1 -1
  206. package/src/components/table-header-item/table-header-item.stories.ts +1 -1
  207. package/src/components/textbox/textbox.stories.ts +1 -1
  208. package/src/components/title-container/title-container.stories.ts +1 -1
  209. package/src/components/toggle-button-option/toggle-button-option.ts +2 -1
  210. package/src/components/toggle-button-three-state/toggle-button-three-state.stories.ts +1 -1
  211. package/src/components/toggle-button-vertical-group/toggle-button-vertical-group.ts +2 -2
  212. package/src/components/toggle-switch/toggle-switch.ts +1 -1
  213. package/src/components/toggletip/toggletip.stories.ts +1 -1
  214. package/src/components/top-bar/top-bar.ts +8 -4
  215. package/src/components/topbar-message-item/topbar-message-item.ts +2 -2
  216. package/src/components/tree-navigation/tree-navigation.stories.ts +1 -1
  217. package/src/components/tree-navigation-item/tree-navigation-item.stories.ts +1 -1
  218. package/src/integration-systems/integration-app-bar/integration-app-bar.stories.ts +1 -1
  219. package/src/integration-systems/integration-bar/integration-bar.stories.ts +1 -1
  220. package/src/integration-systems/integration-bar-dropdown/integration-bar-dropdown.stories.ts +1 -1
  221. package/src/integration-systems/integration-button/integration-button.stories.ts +1 -1
  222. package/src/integration-systems/integration-dropdown-button/integration-dropdown-button.stories.ts +1 -1
  223. package/src/integration-systems/integration-dropdown-button/integration-dropdown-button.ts +1 -1
  224. package/src/integration-systems/integration-fleet-button/integration-fleet-button.stories.ts +1 -1
  225. package/src/integration-systems/integration-tabs/integration-tabs.stories.ts +1 -1
  226. package/src/integration-systems/integration-vessel-menu/integration-vessel-menu.stories.ts +1 -1
  227. package/src/integration-systems/integration-vessel-selector/integration-vessel-selector.stories.ts +1 -1
  228. package/src/navigation-instruments/azimuth-thruster-labeled/azimuth-thruster-labeled.stories.ts +1 -1
  229. package/src/navigation-instruments/current/current.stories.ts +1 -1
  230. package/src/navigation-instruments/draft-trim/draft-trim.stories.ts +1 -1
  231. package/src/navigation-instruments/gauge-radial-proportional/gauge-radial-proportional.stories.ts +1 -1
  232. package/src/navigation-instruments/graph-mini/graph-mini.stories.ts +1 -1
  233. package/src/navigation-instruments/indicator-graph/indicator-graph.stories.ts +1 -1
  234. package/src/navigation-instruments/instrument-field/instrument-field.stories.ts +1 -1
  235. package/src/navigation-instruments/pitch-roll-heave/pitch-roll-heave.stories.ts +1 -1
  236. package/src/navigation-instruments/pitch-roll-yaw/pitch-roll-yaw.stories.ts +1 -1
  237. package/src/navigation-instruments/position-deviation/position-deviation.stories.ts +1 -1
  238. package/src/navigation-instruments/readout/readout.stories.ts +1 -1
  239. package/src/navigation-instruments/readout-list/readout-list.stories.ts +1 -1
  240. package/src/navigation-instruments/readout-list-item/readout-list-item.stories.ts +1 -1
  241. package/src/navigation-instruments/speed-arrows/speed-arrows.stories.ts +1 -1
  242. package/src/navigation-instruments/speed-directions/speed-directions.stories.ts +1 -1
  243. package/src/navigation-instruments/top-view-propulsion/top-view-propulsion.stories.ts +1 -1
  244. package/src/navigation-instruments/velocity-projection-plot/velocity-projection-plot.stories.ts +1 -1
  245. package/src/navigation-instruments/watch/watch.stories.ts +1 -1
  246. package/src/navigation-instruments/watch-flat/watch-flat.stories.ts +1 -1
  247. package/src/navigation-instruments/wind-propulsion/wind-propulsion.stories.ts +1 -1
  248. package/src/pages/alert-detail-page/alert-detail-page.stories.ts +1 -1
  249. package/src/pages/alert-list-page-small/alert-list-page-small.stories.ts +1 -1
  250. package/vitest.browser.config.ts +4 -2
  251. package/vitest.config.ts +10 -0
package/eslint.config.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import typescriptEslint from '@typescript-eslint/eslint-plugin';
2
2
  import globals from 'globals';
3
3
  import tsParser from '@typescript-eslint/parser';
4
+ import fs from 'node:fs';
4
5
  import path from 'node:path';
5
6
  import {fileURLToPath} from 'node:url';
6
7
  import js from '@eslint/js';
@@ -159,6 +160,174 @@ function toTitleCase(segment) {
159
160
  });
160
161
  }
161
162
 
163
+ // --- Component lifecycle tags (AGENTS.md § 3) -------------------------------
164
+ // The class-level JSDoc tag on an `@customElement` class is the single source
165
+ // of truth for a component's lifecycle state; a story's `meta.tags` mirrors it.
166
+
167
+ const LIFECYCLE_JSDOC_TAGS = ['stable', 'beta', 'experimental', 'deprecated'];
168
+
169
+ // `@stable` deliberately maps to no story tag, so a sidebar badge always means
170
+ // "there is a caveat here" rather than decorating every entry.
171
+ const LIFECYCLE_STORY_TAG = {
172
+ beta: 'beta',
173
+ experimental: 'experimental',
174
+ deprecated: 'deprecated',
175
+ };
176
+
177
+ // Story tags this rule owns and will rewrite. `wip` and `alpha` are the retired
178
+ // names, listed so the autofix removes them.
179
+ const OWNED_STORY_TAGS = new Set([
180
+ 'beta',
181
+ 'experimental',
182
+ 'deprecated',
183
+ 'wip',
184
+ 'alpha',
185
+ ]);
186
+
187
+ const SRC_DIR = path.join(__dirname, 'src');
188
+
189
+ // A JSDoc block (`/** … */` — never a plain `/* … */`) immediately preceding a
190
+ // `@customElement('…')` decorator. The inner group forbids `*/` so an earlier,
191
+ // unrelated JSDoc block cannot be stretched across intervening code to reach
192
+ // the decorator.
193
+ const CUSTOM_ELEMENT_WITH_DOC =
194
+ /\/\*\*((?:(?!\*\/)[\s\S])*)\*\/\s*@customElement\(\s*['"]([^'"]+)['"]\s*\)/g;
195
+ // Anchored to the start of a line so a `@customElement(…)` written inside a
196
+ // JSDoc code fence (where the line starts with ` * `) is not mistaken for a
197
+ // real decorator.
198
+ const CUSTOM_ELEMENT_ANY =
199
+ /(?:^|\r?\n)[ \t]*@customElement\(\s*['"]([^'"]+)['"]\s*\)/g;
200
+
201
+ const lifecycleTagPatterns = new Map(
202
+ LIFECYCLE_JSDOC_TAGS.map((tag) => [
203
+ tag,
204
+ new RegExp(`^[ \\t]*\\*?[ \\t]*@${tag}\\b`, 'm'),
205
+ ])
206
+ );
207
+
208
+ // Every `@customElement` in a source text, with the lifecycle tags declared in
209
+ // its class JSDoc. Shared by both rules so they can never disagree.
210
+ function extractComponents(source) {
211
+ const components = [];
212
+ const seen = new Set();
213
+ let match;
214
+
215
+ CUSTOM_ELEMENT_WITH_DOC.lastIndex = 0;
216
+ while ((match = CUSTOM_ELEMENT_WITH_DOC.exec(source)) !== null) {
217
+ const doc = match[1];
218
+ const decoratorOffset = match[0].lastIndexOf('@customElement');
219
+ components.push({
220
+ tag: match[2],
221
+ tags: LIFECYCLE_JSDOC_TAGS.filter((tag) =>
222
+ lifecycleTagPatterns.get(tag).test(doc)
223
+ ),
224
+ index: match.index + decoratorOffset,
225
+ length: match[0].length - decoratorOffset,
226
+ });
227
+ seen.add(match[2]);
228
+ }
229
+
230
+ CUSTOM_ELEMENT_ANY.lastIndex = 0;
231
+ while ((match = CUSTOM_ELEMENT_ANY.exec(source)) !== null) {
232
+ if (seen.has(match[1])) continue;
233
+ seen.add(match[1]);
234
+ const decoratorOffset = match[0].indexOf('@customElement');
235
+ components.push({
236
+ tag: match[1],
237
+ tags: [],
238
+ index: match.index + decoratorOffset,
239
+ length: match[0].length - decoratorOffset,
240
+ });
241
+ }
242
+
243
+ return components;
244
+ }
245
+
246
+ function collectComponentSources(dir, out) {
247
+ for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
248
+ const full = path.join(dir, entry.name);
249
+ if (entry.isDirectory()) {
250
+ // Icons are generated and carry no lifecycle tags by design.
251
+ if (entry.name === 'icons' && dir === SRC_DIR) continue;
252
+ collectComponentSources(full, out);
253
+ } else if (
254
+ entry.name.endsWith('.ts') &&
255
+ !entry.name.endsWith('.stories.ts')
256
+ ) {
257
+ out.push(full);
258
+ }
259
+ }
260
+ return out;
261
+ }
262
+
263
+ // ESLint has no cross-file API, so the tag → lifecycle map is built by reading
264
+ // the sources directly. The result is cached: rebuilt at most once every two
265
+ // seconds so a long-lived editor process still picks up JSDoc edits, while a
266
+ // single CLI run over hundreds of story files pays for it only a few times.
267
+ const COMPONENT_INDEX_TTL_MS = 2000;
268
+ let componentIndex = null;
269
+ let componentIndexBuiltAt = 0;
270
+ let componentIndexPinned = false;
271
+
272
+ function getComponentIndex() {
273
+ if (componentIndexPinned) return componentIndex;
274
+
275
+ const now = Date.now();
276
+ if (componentIndex && now - componentIndexBuiltAt < COMPONENT_INDEX_TTL_MS) {
277
+ return componentIndex;
278
+ }
279
+
280
+ const index = new Map();
281
+ let files;
282
+ try {
283
+ files = collectComponentSources(SRC_DIR, []);
284
+ } catch {
285
+ files = [];
286
+ }
287
+
288
+ for (const file of files) {
289
+ let source;
290
+ try {
291
+ source = fs.readFileSync(file, 'utf8');
292
+ } catch {
293
+ continue;
294
+ }
295
+ if (!source.includes('@customElement')) continue;
296
+ for (const component of extractComponents(source)) {
297
+ index.set(component.tag, {file, tags: component.tags});
298
+ }
299
+ }
300
+
301
+ componentIndex = index;
302
+ componentIndexBuiltAt = now;
303
+ return index;
304
+ }
305
+
306
+ function unwrapTypeExpression(node) {
307
+ let current = node;
308
+ while (
309
+ current &&
310
+ (current.type === 'TSSatisfiesExpression' || current.type === 'TSAsExpression')
311
+ ) {
312
+ current = current.expression;
313
+ }
314
+ return current;
315
+ }
316
+
317
+ function findObjectProperty(objectExpression, name) {
318
+ return objectExpression.properties.find(
319
+ (property) =>
320
+ property.type === 'Property' &&
321
+ !property.computed &&
322
+ property.key.type === 'Identifier' &&
323
+ property.key.name === name
324
+ );
325
+ }
326
+
327
+ function quoteList(names) {
328
+ return names.map((name) => `'${name}'`).join(', ');
329
+ }
330
+
162
331
  // Custom plugin for local OpenBridge lint rules
163
332
  const openbridgePlugin = {
164
333
  rules: {
@@ -268,6 +437,166 @@ const openbridgePlugin = {
268
437
  };
269
438
  },
270
439
  },
440
+ 'component-lifecycle-tag': {
441
+ meta: {
442
+ type: 'suggestion',
443
+ docs: {
444
+ description:
445
+ 'Require exactly one lifecycle tag (@stable, @beta, @experimental, @deprecated) in the class JSDoc of every @customElement',
446
+ },
447
+ schema: [],
448
+ },
449
+ create(context) {
450
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
451
+
452
+ return {
453
+ 'Program:exit'() {
454
+ const text = sourceCode.getText();
455
+ if (!text.includes('@customElement')) return;
456
+
457
+ for (const component of extractComponents(text)) {
458
+ if (component.tags.length === 1) continue;
459
+
460
+ context.report({
461
+ loc: {
462
+ start: sourceCode.getLocFromIndex(component.index),
463
+ end: sourceCode.getLocFromIndex(
464
+ component.index + component.length
465
+ ),
466
+ },
467
+ message:
468
+ component.tags.length === 0
469
+ ? `<${component.tag}> has no lifecycle tag in its class JSDoc. Add exactly one of @stable, @beta, @experimental or @deprecated (see AGENTS.md § 3).`
470
+ : `<${component.tag}> declares more than one lifecycle tag (${component.tags
471
+ .map((tag) => `@${tag}`)
472
+ .join(', ')}). Exactly one is allowed.`,
473
+ });
474
+ }
475
+ },
476
+ };
477
+ },
478
+ },
479
+ 'story-lifecycle-tags': {
480
+ meta: {
481
+ type: 'problem',
482
+ docs: {
483
+ description:
484
+ "Keep a story's meta.tags lifecycle entry in sync with the class JSDoc lifecycle tag of its meta.component",
485
+ },
486
+ fixable: 'code',
487
+ schema: [],
488
+ },
489
+ create(context) {
490
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
491
+
492
+ return {
493
+ VariableDeclarator(node) {
494
+ if (node.id.type !== 'Identifier' || node.id.name !== 'meta') return;
495
+
496
+ const meta = unwrapTypeExpression(node.init);
497
+ if (!meta || meta.type !== 'ObjectExpression') return;
498
+
499
+ const componentProperty = findObjectProperty(meta, 'component');
500
+ if (
501
+ !componentProperty ||
502
+ componentProperty.value.type !== 'Literal' ||
503
+ typeof componentProperty.value.value !== 'string'
504
+ )
505
+ return;
506
+
507
+ const componentTag = componentProperty.value.value;
508
+ const component = getComponentIndex().get(componentTag);
509
+
510
+ // A component with no tag, or more than one, is reported against
511
+ // its own source file by `component-lifecycle-tag`; there is no
512
+ // single correct story tag to fix towards here.
513
+ if (!component || component.tags.length !== 1) return;
514
+
515
+ const lifecycle = component.tags[0];
516
+ const expected = LIFECYCLE_STORY_TAG[lifecycle];
517
+ const desired = expected ? [expected] : [];
518
+ const tagsProperty = findObjectProperty(meta, 'tags');
519
+
520
+ if (!tagsProperty) {
521
+ if (!expected) return;
522
+
523
+ context.report({
524
+ node: componentProperty.value,
525
+ message: `<${componentTag}> is @${lifecycle}, so meta.tags must contain '${expected}', but meta has no tags.`,
526
+ fix(fixer) {
527
+ const titleProperty = findObjectProperty(meta, 'title');
528
+ if (titleProperty) {
529
+ const next = sourceCode.getTokenAfter(titleProperty);
530
+ return next && next.value === ','
531
+ ? fixer.insertTextAfter(next, `\n tags: ['${expected}'],`)
532
+ : fixer.insertTextAfter(
533
+ titleProperty,
534
+ `,\n tags: ['${expected}']`
535
+ );
536
+ }
537
+ const first = meta.properties[0];
538
+ return first
539
+ ? fixer.insertTextBefore(
540
+ first,
541
+ `tags: ['${expected}'],\n `
542
+ )
543
+ : null;
544
+ },
545
+ });
546
+ return;
547
+ }
548
+
549
+ if (tagsProperty.value.type !== 'ArrayExpression') return;
550
+
551
+ const elements = tagsProperty.value.elements.filter(Boolean);
552
+ const owned = elements.filter(
553
+ (element) =>
554
+ element.type === 'Literal' &&
555
+ typeof element.value === 'string' &&
556
+ OWNED_STORY_TAGS.has(element.value)
557
+ );
558
+ const found = owned.map((element) => element.value);
559
+
560
+ if (
561
+ found.length === desired.length &&
562
+ found.every((name, i) => name === desired[i])
563
+ )
564
+ return;
565
+
566
+ const kept = elements.filter((element) => !owned.includes(element));
567
+ const nextElements = [
568
+ ...kept.map((element) => sourceCode.getText(element)),
569
+ ...desired.map((name) => `'${name}'`),
570
+ ];
571
+
572
+ context.report({
573
+ node: tagsProperty.value,
574
+ message: expected
575
+ ? `<${componentTag}> is @${lifecycle}, so meta.tags must carry ${quoteList(desired)}, but it carries ${found.length ? quoteList(found) : 'no lifecycle tag'}.`
576
+ : `<${componentTag}> is @${lifecycle}, so meta.tags must carry no lifecycle tag, but it carries ${quoteList(found)}.`,
577
+ fix(fixer) {
578
+ if (nextElements.length > 0) {
579
+ return fixer.replaceText(
580
+ tagsProperty.value,
581
+ `[${nextElements.join(', ')}]`
582
+ );
583
+ }
584
+ const previous = sourceCode.getTokenBefore(tagsProperty);
585
+ const next = sourceCode.getTokenAfter(tagsProperty);
586
+ const start = previous
587
+ ? previous.range[1]
588
+ : tagsProperty.range[0];
589
+ const end =
590
+ next && next.value === ','
591
+ ? next.range[1]
592
+ : tagsProperty.range[1];
593
+ return fixer.removeRange([start, end]);
594
+ },
595
+ });
596
+ },
597
+ };
598
+ },
599
+ },
271
600
  'prefer-boolean-property-default-false': {
272
601
  meta: {
273
602
  type: 'problem',
@@ -539,7 +868,9 @@ export default [
539
868
  'openbridge/prefer-enum-over-string-literal-union': 'error',
540
869
  'openbridge/prefer-boolean-property-default-false': 'error',
541
870
  'openbridge/prefer-array-property-type-and-item-interface': 'error',
871
+ 'openbridge/component-lifecycle-tag': 'warn',
542
872
  'openbridge/storybook-title-case': 'off',
873
+ 'openbridge/story-lifecycle-tags': 'off',
543
874
  // Disabled because eslint-plugin-file-extension-in-import-ts is not yet
544
875
  // compatible with ESLint v10 (it still uses deprecated context methods).
545
876
  'file-extension-in-import-ts/file-extension-in-import-ts': 'off',
@@ -574,11 +905,35 @@ export default [
574
905
  'openbridge/prefer-enum-over-string-literal-union': 'off',
575
906
  },
576
907
  },
908
+ {
909
+ // Icon components carry no lifecycle tags by design.
910
+ files: ['src/icons/**/*.ts', 'src/manual-icon/**/*.ts'],
911
+
912
+ rules: {
913
+ 'openbridge/component-lifecycle-tag': 'off',
914
+ },
915
+ },
577
916
  {
578
917
  files: ['**/*.stories.ts'],
579
918
 
580
919
  rules: {
581
920
  'openbridge/storybook-title-case': 'error',
921
+ 'openbridge/story-lifecycle-tags': 'error',
922
+ 'openbridge/component-lifecycle-tag': 'off',
582
923
  },
583
924
  },
584
925
  ];
926
+
927
+ // ESLint reads only the default export. These named exports exist so the
928
+ // lifecycle-tag rules can be unit-tested (script/eslint-rules.test.ts) without
929
+ // depending on the real state of src/.
930
+ export const __testables = {
931
+ openbridgePlugin,
932
+ extractComponents,
933
+ pinComponentIndex(entries) {
934
+ componentIndex = new Map(
935
+ Object.entries(entries).map(([tag, tags]) => [tag, {file: tag, tags}])
936
+ );
937
+ componentIndexPinned = true;
938
+ },
939
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oicl/openbridge-webcomponents-full-bundle",
3
- "version": "2.0.0-next.117",
3
+ "version": "2.0.0-next.119",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -147,6 +147,8 @@
147
147
  "lint:icons": "tsx ./script/check-icon-hex-leaks.ts",
148
148
  "lint:eslint": "eslint 'src/**/*.ts'",
149
149
  "lint:lit-analyzer": "lit-analyzer --strict --maxWarnings 0",
150
+ "lint:fix:stories": "eslint 'src/**/*.stories.ts' --fix",
151
+ "test:rules": "vitest run --project rules",
150
152
  "fix-imports": "node fix-imports.mjs",
151
153
  "fix-imports:check": "node fix-imports.mjs --dry-run",
152
154
  "test:browser": "vitest --config=vitest.browser.config.ts",
@@ -142,6 +142,20 @@ function findDispatchedEvents(code: string): Set<string> {
142
142
  return events;
143
143
  }
144
144
 
145
+ /**
146
+ * Unqualified `dispatchEvent(...)` calls. Inside a class method these resolve to
147
+ * `globalThis.dispatchEvent`, so the event lands on `window` and no consumer of
148
+ * the element ever receives it — almost always a missing `this.`.
149
+ */
150
+ function findBareDispatchCalls(code: string): Set<string> {
151
+ const events = new Set<string>();
152
+ const re =
153
+ /(^|[^.\w$])dispatchEvent\(\s*new\s+(?:Custom)?Event\s*(?:<(?:[^<>]|<[^<>]*>)*>)?\s*\(\s*['"]([^'"]+)['"]/g;
154
+ let m: RegExpExecArray | null;
155
+ while ((m = re.exec(code)) !== null) events.add(m[2]);
156
+ return events;
157
+ }
158
+
145
159
  /** Documented slot names from `@slot name` tags (`-` denotes the default slot). */
146
160
  function findDocumentedSlots(source: string): {
147
161
  names: Set<string>;
@@ -175,6 +189,16 @@ interface TagDoc {
175
189
  hasDescription: boolean;
176
190
  }
177
191
 
192
+ /**
193
+ * Only the unindented docblocks — the class-level ones whose tags `cem analyze`
194
+ * and `lit labs gen` actually read. Tags in indented (method-level) blocks reach
195
+ * neither, so demanding a description there is noise: the description belongs on
196
+ * the class tag, and the method block's own summary line describes the method.
197
+ */
198
+ function topLevelDocblocks(source: string): string {
199
+ return (source.match(/^\/\*\*[\s\S]*?\*\//gm) ?? []).join('\n');
200
+ }
201
+
178
202
  /** A plausible slot/event name (identifier or kebab-case), or `-` for default. */
179
203
  function isValidTagName(name: string): boolean {
180
204
  return name === '-' || /^[A-Za-z][\w-]*$/.test(name);
@@ -280,6 +304,14 @@ async function run(): Promise<void> {
280
304
  }
281
305
  }
282
306
 
307
+ // Bare `dispatchEvent(...)` — dispatches on `window`, not the element.
308
+ for (const name of findBareDispatchCalls(code)) {
309
+ errors.push({
310
+ file: rel,
311
+ message: `calls dispatchEvent(new …Event('${name}')) without \`this.\` — this dispatches on window, so the event never reaches consumers of the element`,
312
+ });
313
+ }
314
+
283
315
  // Phantom @slot — only for direct LitElement subclasses without dynamic
284
316
  // slot names, so inherited/dynamic slots never produce false positives.
285
317
  if (extendsLitElementDirectly(source) && !dynamicSlot) {
@@ -304,14 +336,15 @@ async function run(): Promise<void> {
304
336
  }
305
337
 
306
338
  // Empty description (warning) — a @slot/@fires tag with only a name produces
307
- // a blank cell in the manifest / Storybook controls.
308
- for (const tag of parseSlotTags(source)) {
339
+ // a blank cell in the manifest / Storybook controls. Class-level tags only.
340
+ const classDocs = topLevelDocblocks(source);
341
+ for (const tag of parseSlotTags(classDocs)) {
309
342
  if (!tag.hasDescription) {
310
343
  const label = tag.name === '-' ? 'default slot' : `@slot ${tag.name}`;
311
344
  warnings.push({file: rel, message: `${label} has no description`});
312
345
  }
313
346
  }
314
- for (const tag of parseEventTags(source)) {
347
+ for (const tag of parseEventTags(classDocs)) {
315
348
  if (!tag.hasDescription) {
316
349
  warnings.push({
317
350
  file: rel,