@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.96 → 2.0.0-next.98

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 (167) hide show
  1. package/bundle/openbridge-webcomponents.bundle.js +39 -124
  2. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  3. package/custom-elements.json +343 -142
  4. package/dist/ar/poi/poi.d.ts +4 -2
  5. package/dist/ar/poi/poi.d.ts.map +1 -1
  6. package/dist/ar/poi/poi.js.map +1 -1
  7. package/dist/ar/poi-layer/poi-layer.d.ts +1 -0
  8. package/dist/ar/poi-layer/poi-layer.d.ts.map +1 -1
  9. package/dist/ar/poi-layer/poi-layer.js.map +1 -1
  10. package/dist/ar/poi-object/poi-object-aton.d.ts +1 -0
  11. package/dist/ar/poi-object/poi-object-aton.d.ts.map +1 -1
  12. package/dist/ar/poi-object/poi-object-aton.js.map +1 -1
  13. package/dist/automation/automation-badge/automation-badge.d.ts +2 -0
  14. package/dist/automation/automation-badge/automation-badge.d.ts.map +1 -1
  15. package/dist/automation/automation-badge/automation-badge.js.map +1 -1
  16. package/dist/automation/automation-button/automation-button.d.ts +9 -0
  17. package/dist/automation/automation-button/automation-button.d.ts.map +1 -1
  18. package/dist/automation/automation-button/automation-button.js.map +1 -1
  19. package/dist/automation/automation-input-modal/automation-input-modal.d.ts +4 -0
  20. package/dist/automation/automation-input-modal/automation-input-modal.d.ts.map +1 -1
  21. package/dist/automation/automation-input-modal/automation-input-modal.js.map +1 -1
  22. package/dist/automation/automation-tank/automation-tank.css.js +8 -87
  23. package/dist/automation/automation-tank/automation-tank.css.js.map +1 -1
  24. package/dist/automation/automation-tank/automation-tank.d.ts +13 -33
  25. package/dist/automation/automation-tank/automation-tank.d.ts.map +1 -1
  26. package/dist/automation/automation-tank/automation-tank.js +12 -28
  27. package/dist/automation/automation-tank/automation-tank.js.map +1 -1
  28. package/dist/building-blocks/alert-list/alert-list.d.ts +4 -0
  29. package/dist/building-blocks/alert-list/alert-list.d.ts.map +1 -1
  30. package/dist/building-blocks/alert-list/alert-list.js.map +1 -1
  31. package/dist/building-blocks/bar-horizontal/bar-horizontal.d.ts +2 -0
  32. package/dist/building-blocks/bar-horizontal/bar-horizontal.d.ts.map +1 -1
  33. package/dist/building-blocks/bar-horizontal/bar-horizontal.js.map +1 -1
  34. package/dist/building-blocks/bar-vertical/bar-vertical.d.ts +2 -0
  35. package/dist/building-blocks/bar-vertical/bar-vertical.d.ts.map +1 -1
  36. package/dist/building-blocks/bar-vertical/bar-vertical.js.map +1 -1
  37. package/dist/components/advice-floating-item/advice-floating-item.d.ts +2 -4
  38. package/dist/components/advice-floating-item/advice-floating-item.d.ts.map +1 -1
  39. package/dist/components/advice-floating-item/advice-floating-item.js.map +1 -1
  40. package/dist/components/advice-message-item/advice-message-item.d.ts +6 -19
  41. package/dist/components/advice-message-item/advice-message-item.d.ts.map +1 -1
  42. package/dist/components/advice-message-item/advice-message-item.js.map +1 -1
  43. package/dist/components/alert-floating-item/alert-floating-item.d.ts +12 -0
  44. package/dist/components/alert-floating-item/alert-floating-item.d.ts.map +1 -1
  45. package/dist/components/alert-floating-item/alert-floating-item.js.map +1 -1
  46. package/dist/components/alert-menu/alert-menu.d.ts +6 -20
  47. package/dist/components/alert-menu/alert-menu.d.ts.map +1 -1
  48. package/dist/components/alert-menu/alert-menu.js.map +1 -1
  49. package/dist/components/alert-menu-item/alert-menu-item.d.ts +4 -0
  50. package/dist/components/alert-menu-item/alert-menu-item.d.ts.map +1 -1
  51. package/dist/components/alert-menu-item/alert-menu-item.js.map +1 -1
  52. package/dist/components/chat-message/chat-message.d.ts +3 -0
  53. package/dist/components/chat-message/chat-message.d.ts.map +1 -1
  54. package/dist/components/chat-message/chat-message.js.map +1 -1
  55. package/dist/components/elevated-card-radio/elevated-card-radio.d.ts +2 -6
  56. package/dist/components/elevated-card-radio/elevated-card-radio.d.ts.map +1 -1
  57. package/dist/components/elevated-card-radio/elevated-card-radio.js.map +1 -1
  58. package/dist/components/keyboard-numeric/keyboard-numeric.d.ts +6 -0
  59. package/dist/components/keyboard-numeric/keyboard-numeric.d.ts.map +1 -1
  60. package/dist/components/keyboard-numeric/keyboard-numeric.js.map +1 -1
  61. package/dist/components/notification-floating-item/notification-floating-item.d.ts +2 -4
  62. package/dist/components/notification-floating-item/notification-floating-item.d.ts.map +1 -1
  63. package/dist/components/notification-floating-item/notification-floating-item.js.map +1 -1
  64. package/dist/components/sequence-card/sequence-card.d.ts +9 -0
  65. package/dist/components/sequence-card/sequence-card.d.ts.map +1 -1
  66. package/dist/components/sequence-card/sequence-card.js.map +1 -1
  67. package/dist/components/sequence-item/sequence-item.d.ts +5 -0
  68. package/dist/components/sequence-item/sequence-item.d.ts.map +1 -1
  69. package/dist/components/sequence-item/sequence-item.js.map +1 -1
  70. package/dist/components/sequence-modal/sequence-modal.d.ts +4 -0
  71. package/dist/components/sequence-modal/sequence-modal.d.ts.map +1 -1
  72. package/dist/components/sequence-modal/sequence-modal.js.map +1 -1
  73. package/dist/components/sequence-step/sequence-step.d.ts.map +1 -1
  74. package/dist/components/sequence-step/sequence-step.js.map +1 -1
  75. package/dist/components/slider-double/slider-double.d.ts +4 -8
  76. package/dist/components/slider-double/slider-double.d.ts.map +1 -1
  77. package/dist/components/slider-double/slider-double.js.map +1 -1
  78. package/dist/components/status-indicator/status-indicator.d.ts +1 -0
  79. package/dist/components/status-indicator/status-indicator.d.ts.map +1 -1
  80. package/dist/components/status-indicator/status-indicator.js.map +1 -1
  81. package/dist/components/system-menu/system-menu.d.ts +2 -0
  82. package/dist/components/system-menu/system-menu.d.ts.map +1 -1
  83. package/dist/components/system-menu/system-menu.js.map +1 -1
  84. package/dist/components/table-header-item/table-header-item.d.ts +2 -0
  85. package/dist/components/table-header-item/table-header-item.d.ts.map +1 -1
  86. package/dist/components/table-header-item/table-header-item.js.map +1 -1
  87. package/dist/components/tag/tag.d.ts +1 -0
  88. package/dist/components/tag/tag.d.ts.map +1 -1
  89. package/dist/components/tag/tag.js.map +1 -1
  90. package/dist/components/toggletip/toggletip.d.ts +4 -4
  91. package/dist/components/toggletip/toggletip.js.map +1 -1
  92. package/dist/components/user-menu/user-menu.d.ts +1 -0
  93. package/dist/components/user-menu/user-menu.d.ts.map +1 -1
  94. package/dist/components/user-menu/user-menu.js.map +1 -1
  95. package/dist/integration-systems/integration-app-bar/integration-app-bar.d.ts +1 -0
  96. package/dist/integration-systems/integration-app-bar/integration-app-bar.d.ts.map +1 -1
  97. package/dist/integration-systems/integration-app-bar/integration-app-bar.js.map +1 -1
  98. package/dist/integration-systems/integration-bar/integration-bar.d.ts +0 -1
  99. package/dist/integration-systems/integration-bar/integration-bar.d.ts.map +1 -1
  100. package/dist/integration-systems/integration-bar/integration-bar.js.map +1 -1
  101. package/dist/integration-systems/integration-bar-dropdown/integration-bar-dropdown.d.ts +9 -9
  102. package/dist/integration-systems/integration-bar-dropdown/integration-bar-dropdown.js.map +1 -1
  103. package/dist/integration-systems/integration-fleet-button/integration-fleet-button.d.ts +1 -0
  104. package/dist/integration-systems/integration-fleet-button/integration-fleet-button.d.ts.map +1 -1
  105. package/dist/integration-systems/integration-fleet-button/integration-fleet-button.js.map +1 -1
  106. package/dist/integration-systems/integration-tabs/integration-tabs.d.ts +1 -0
  107. package/dist/integration-systems/integration-tabs/integration-tabs.d.ts.map +1 -1
  108. package/dist/integration-systems/integration-tabs/integration-tabs.js.map +1 -1
  109. package/dist/navigation-instruments/compass-sector/compass-sector.d.ts +0 -1
  110. package/dist/navigation-instruments/compass-sector/compass-sector.d.ts.map +1 -1
  111. package/dist/navigation-instruments/compass-sector/compass-sector.js.map +1 -1
  112. package/dist/navigation-instruments/watch/tickmark.d.ts.map +1 -1
  113. package/dist/navigation-instruments/watch/tickmark.js +17 -7
  114. package/dist/navigation-instruments/watch/tickmark.js.map +1 -1
  115. package/dist/navigation-instruments/watch/watch.d.ts.map +1 -1
  116. package/dist/navigation-instruments/watch/watch.js +2 -2
  117. package/dist/navigation-instruments/watch/watch.js.map +1 -1
  118. package/dist/navigation-instruments/wind-propulsion/wind-propulsion.d.ts +0 -1
  119. package/dist/navigation-instruments/wind-propulsion/wind-propulsion.d.ts.map +1 -1
  120. package/dist/navigation-instruments/wind-propulsion/wind-propulsion.js.map +1 -1
  121. package/dist/pages/alert-detail-page/alert-detail-page.d.ts.map +1 -1
  122. package/dist/pages/alert-detail-page/alert-detail-page.js.map +1 -1
  123. package/package.json +3 -2
  124. package/script/check-slot-event-docs.ts +353 -0
  125. package/src/ar/poi/poi.ts +4 -2
  126. package/src/ar/poi-layer/poi-layer.ts +1 -0
  127. package/src/ar/poi-object/poi-object-aton.ts +1 -0
  128. package/src/automation/automation-badge/automation-badge.ts +2 -0
  129. package/src/automation/automation-button/automation-button.ts +9 -0
  130. package/src/automation/automation-input-modal/automation-input-modal.ts +4 -0
  131. package/src/automation/automation-tank/automation-tank.css +8 -64
  132. package/src/automation/automation-tank/automation-tank.stories.ts +41 -23
  133. package/src/automation/automation-tank/automation-tank.ts +25 -65
  134. package/src/building-blocks/alert-list/alert-list.ts +4 -0
  135. package/src/building-blocks/bar-horizontal/bar-horizontal.ts +2 -0
  136. package/src/building-blocks/bar-vertical/bar-vertical.ts +2 -0
  137. package/src/components/advice-floating-item/advice-floating-item.ts +2 -4
  138. package/src/components/advice-message-item/advice-message-item.ts +6 -19
  139. package/src/components/alert-floating-item/alert-floating-item.ts +12 -0
  140. package/src/components/alert-menu/alert-menu.ts +6 -20
  141. package/src/components/alert-menu-item/alert-menu-item.ts +4 -0
  142. package/src/components/chat-message/chat-message.ts +3 -0
  143. package/src/components/elevated-card-radio/elevated-card-radio.ts +2 -6
  144. package/src/components/keyboard-numeric/keyboard-numeric.ts +6 -0
  145. package/src/components/notification-floating-item/notification-floating-item.ts +2 -4
  146. package/src/components/sequence-card/sequence-card.ts +9 -0
  147. package/src/components/sequence-item/sequence-item.ts +5 -0
  148. package/src/components/sequence-modal/sequence-modal.ts +4 -0
  149. package/src/components/sequence-step/sequence-step.ts +3 -0
  150. package/src/components/slider-double/slider-double.ts +4 -8
  151. package/src/components/status-indicator/status-indicator.ts +1 -0
  152. package/src/components/system-menu/system-menu.ts +2 -0
  153. package/src/components/table-header-item/table-header-item.ts +2 -0
  154. package/src/components/tag/tag.ts +1 -0
  155. package/src/components/toggletip/toggletip.ts +4 -4
  156. package/src/components/user-menu/user-menu.ts +1 -0
  157. package/src/integration-systems/integration-app-bar/integration-app-bar.ts +1 -0
  158. package/src/integration-systems/integration-bar/integration-bar.ts +0 -1
  159. package/src/integration-systems/integration-bar-dropdown/integration-bar-dropdown.ts +9 -9
  160. package/src/integration-systems/integration-fleet-button/integration-fleet-button.ts +1 -0
  161. package/src/integration-systems/integration-tabs/integration-tabs.ts +1 -0
  162. package/src/navigation-instruments/compass-sector/compass-sector.ts +0 -1
  163. package/src/navigation-instruments/watch/tickmark.spec.ts +99 -0
  164. package/src/navigation-instruments/watch/tickmark.ts +21 -8
  165. package/src/navigation-instruments/watch/watch.ts +7 -2
  166. package/src/navigation-instruments/wind-propulsion/wind-propulsion.ts +0 -1
  167. package/src/pages/alert-detail-page/alert-detail-page.ts +7 -0
@@ -0,0 +1,353 @@
1
+ /**
2
+ * @module SlotEventDocAudit
3
+ * @description
4
+ * Audits every registered web component (`@customElement`) so that its rendered
5
+ * `<slot>` elements and dispatched custom events are declared with matching
6
+ * `@slot` / `@fires` JSDoc tags on the class.
7
+ *
8
+ * Why this matters: `custom-elements.json` is generated by `cem analyze`, which
9
+ * discovers slots and events **only** from `@slot` / `@fires` JSDoc tags — never
10
+ * from the template or from `dispatchEvent(...)` calls. If a component renders a
11
+ * `<slot name="leading-icon">` but forgets the `@slot leading-icon` tag, the slot
12
+ * is invisible to the manifest and therefore to every downstream consumer (the
13
+ * framework wrappers, IDE autocomplete, Storybook autodocs, code playgrounds).
14
+ * A `@slot`/`@fires` tag that has no counterpart in the template/code produces the
15
+ * opposite problem: a phantom control that does nothing. See issue #1033.
16
+ *
17
+ * The checks are intentionally conservative to stay false-positive-free in CI:
18
+ *
19
+ * - **Missing slot (error):** a *static* `<slot name="literal">` or a *bare*
20
+ * default `<slot>` with no matching `@slot` tag. Slots rendered by an inherited
21
+ * base class live in the base file, so subclasses are never falsely flagged.
22
+ * - **Missing event (error):** a literal `new CustomEvent('name')` /
23
+ * `new Event('name')` with no matching `@fires`/`@event name` tag.
24
+ * - **Phantom slot (error):** a `@slot name` tag with no matching static
25
+ * `<slot name="name">` element — only evaluated for files that `extends
26
+ * LitElement` directly and contain no dynamic `<slot name="${…}">`, so
27
+ * inheritance- and dynamic-name-based false positives are skipped. A
28
+ * `slot="name"` *projection attribute* in the template does NOT suppress this
29
+ * (it exposes nothing); only a genuine imperative read of the consumer's
30
+ * children (e.g. `getAttribute('slot') === 'name'`) counts as a real slot.
31
+ * - **Empty description (warning):** a `@slot`/`@fires` tag with only a name and
32
+ * no descriptive text. These become blank cells in the manifest / Storybook
33
+ * controls; warnings do not fail CI.
34
+ *
35
+ * There is deliberately no phantom-`@fires` *error*: unlike slots (which must be
36
+ * a `<slot>` element in the component's own shadow DOM), a documented event may
37
+ * legitimately originate elsewhere — a native event bubbling from an inner
38
+ * element (`click`, `blur`), or an event re-fired by a child — so "documented
39
+ * but not locally dispatched" is not, on its own, a defect.
40
+ *
41
+ * Dynamic slot names (`<slot name="${expr}">` or `<slot name="pre-${id}-post">`)
42
+ * are skipped for the missing-slot check because their concrete names cannot be
43
+ * known statically; document them with a placeholder tag such as
44
+ * `@slot tab-<id>-icon` (see .cursor/rules/comments.mdc § Structured-tag rules).
45
+ *
46
+ * Usage:
47
+ * ```bash
48
+ * npm run lint:slots
49
+ * tsx script/check-slot-event-docs.ts
50
+ * ```
51
+ *
52
+ * Exits with code 1 when any error-level finding is present, suitable for CI.
53
+ *
54
+ * Note: standalone script; exports nothing. Regex-based like the sibling
55
+ * `check-css-*` audits — it does not build a full AST.
56
+ */
57
+
58
+ import fs from 'fs';
59
+ import path from 'path';
60
+ import {globby} from 'globby';
61
+
62
+ interface Finding {
63
+ file: string;
64
+ message: string;
65
+ }
66
+
67
+ /** Remove block and line comments so template scans ignore commented-out code. */
68
+ function stripComments(source: string): string {
69
+ return source
70
+ .replace(/\/\*[\s\S]*?\*\//g, '')
71
+ .replace(/(^|[^:])\/\/[^\n]*/g, '$1');
72
+ }
73
+
74
+ /** Static exposed slot names (`<slot name="literal">`, no interpolation). */
75
+ function findStaticSlots(code: string): Set<string> {
76
+ const slots = new Set<string>();
77
+ const re = /<slot\b([^>]*)>/g;
78
+ let m: RegExpExecArray | null;
79
+ while ((m = re.exec(code)) !== null) {
80
+ const attrs = m[1];
81
+ const nameMatch = /\bname\s*=\s*["']([^"']*)["']/.exec(attrs);
82
+ if (!nameMatch) continue; // bare/default slot handled separately
83
+ const name = nameMatch[1];
84
+ if (name.includes('${')) continue; // dynamic — cannot resolve statically
85
+ slots.add(name);
86
+ }
87
+ return slots;
88
+ }
89
+
90
+ /** True when the template renders a bare default `<slot>` (no `name` attribute). */
91
+ function hasDefaultSlot(code: string): boolean {
92
+ const re = /<slot\b([^>]*)>/g;
93
+ let m: RegExpExecArray | null;
94
+ while ((m = re.exec(code)) !== null) {
95
+ if (!/\bname\s*=/.test(m[1])) return true;
96
+ }
97
+ return false;
98
+ }
99
+
100
+ /** True when any `<slot name="${…}">` uses an interpolated (dynamic) name. */
101
+ function hasDynamicSlot(code: string): boolean {
102
+ const re = /<slot\b([^>]*)>/g;
103
+ let m: RegExpExecArray | null;
104
+ while ((m = re.exec(code)) !== null) {
105
+ const nameMatch = /\bname\s*=\s*(?:["'][^"']*["']|`[^`]*`|[^\s>]+)/.exec(
106
+ m[1]
107
+ );
108
+ if (nameMatch && nameMatch[0].includes('${')) return true;
109
+ }
110
+ return false;
111
+ }
112
+
113
+ /**
114
+ * Literal event names the host itself dispatches:
115
+ * `this.dispatchEvent(new CustomEvent('name'…))`. Restricted to `this.` so
116
+ * events re-dispatched on inner child elements (e.g. a native `<input>`) are
117
+ * not mistaken for the component's own public API.
118
+ */
119
+ function findDispatchedEvents(code: string): Set<string> {
120
+ const events = new Set<string>();
121
+ const re =
122
+ /this\.dispatchEvent\(\s*new\s+(?:Custom)?Event\s*\(\s*['"]([^'"]+)['"]/g;
123
+ let m: RegExpExecArray | null;
124
+ while ((m = re.exec(code)) !== null) events.add(m[1]);
125
+ return events;
126
+ }
127
+
128
+ /** Documented slot names from `@slot name` tags (`-` denotes the default slot). */
129
+ function findDocumentedSlots(source: string): {
130
+ names: Set<string>;
131
+ hasDefault: boolean;
132
+ } {
133
+ const names = new Set<string>();
134
+ let hasDefault = false;
135
+ const re = /@slot\s+(\S+)/g;
136
+ let m: RegExpExecArray | null;
137
+ while ((m = re.exec(source)) !== null) {
138
+ if (m[1] === '-') hasDefault = true;
139
+ else names.add(m[1]);
140
+ }
141
+ return {names, hasDefault};
142
+ }
143
+
144
+ /** Documented event names from `@fires name` / `@event name` tags. */
145
+ function findDocumentedEvents(source: string): Set<string> {
146
+ const names = new Set<string>();
147
+ // Accept both tag orders: `@fires name {Type}` and `@fires {Type} name`.
148
+ // The optional leading `{Type}` may contain one level of nested braces,
149
+ // e.g. `{CustomEvent<{date: Date}>}`.
150
+ const re = /@(?:fires|event)\s+(?:\{(?:[^{}]|\{[^{}]*\})*\}\s+)?(\S+)/g;
151
+ let m: RegExpExecArray | null;
152
+ while ((m = re.exec(source)) !== null) names.add(m[1]);
153
+ return names;
154
+ }
155
+
156
+ interface TagDoc {
157
+ name: string;
158
+ hasDescription: boolean;
159
+ }
160
+
161
+ /** A plausible slot/event name (identifier or kebab-case), or `-` for default. */
162
+ function isValidTagName(name: string): boolean {
163
+ return name === '-' || /^[A-Za-z][\w-]*$/.test(name);
164
+ }
165
+
166
+ /** Parse `@slot` tags into name + whether descriptive text follows. */
167
+ function parseSlotTags(source: string): TagDoc[] {
168
+ const out: TagDoc[] = [];
169
+ const re = /@slot[ \t]+(\S+)([^\n]*)/g;
170
+ let m: RegExpExecArray | null;
171
+ while ((m = re.exec(source)) !== null) {
172
+ if (!isValidTagName(m[1])) continue;
173
+ const rest = m[2].replace(/^\s*-\s*/, '').trim();
174
+ out.push({name: m[1], hasDescription: rest.length > 0});
175
+ }
176
+ return out;
177
+ }
178
+
179
+ /**
180
+ * Parse `@fires` / `@event` tags into name + whether descriptive text follows,
181
+ * tolerating a leading or trailing `{Type}` (with one level of nested braces).
182
+ */
183
+ function parseEventTags(source: string): TagDoc[] {
184
+ const out: TagDoc[] = [];
185
+ const type = '\\{(?:[^{}]|\\{[^{}]*\\})*\\}';
186
+ const re = new RegExp(
187
+ `@(?:fires|event)[ \\t]+(?:${type}[ \\t]+)?(\\S+)([^\\n]*)`,
188
+ 'g'
189
+ );
190
+ let m: RegExpExecArray | null;
191
+ while ((m = re.exec(source)) !== null) {
192
+ if (!isValidTagName(m[1])) continue;
193
+ const rest = m[2]
194
+ .replace(new RegExp(`^\\s*${type}\\s*`), '')
195
+ .replace(/^\s*-\s*/, '')
196
+ .trim();
197
+ out.push({name: m[1], hasDescription: rest.length > 0});
198
+ }
199
+ return out;
200
+ }
201
+
202
+ /** Whether the class extends `LitElement` directly (not a base class or mixin). */
203
+ function extendsLitElementDirectly(source: string): boolean {
204
+ return /class\s+\w+\s+extends\s+LitElement\b/.test(source);
205
+ }
206
+
207
+ /** Whether the (comment-stripped) code references `name` as a string literal. */
208
+ function referencesNameAsString(code: string, name: string): boolean {
209
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
210
+ return new RegExp(`['"\`]${escaped}['"\`]`).test(code);
211
+ }
212
+
213
+ async function run(): Promise<void> {
214
+ const cwd = process.cwd();
215
+ const files = await globby(['src/**/*.ts'], {
216
+ cwd,
217
+ ignore: ['src/**/*.stories.ts', 'src/icons/**'],
218
+ });
219
+
220
+ const errors: Finding[] = [];
221
+ const warnings: Finding[] = [];
222
+ let componentCount = 0;
223
+
224
+ for (const rel of files.sort()) {
225
+ const source = fs.readFileSync(path.join(cwd, rel), 'utf8');
226
+ if (!/@customElement\(/.test(source)) continue;
227
+ componentCount++;
228
+
229
+ const code = stripComments(source);
230
+ const staticSlots = findStaticSlots(code);
231
+ const defaultSlot = hasDefaultSlot(code);
232
+ const dynamicSlot = hasDynamicSlot(code);
233
+ const events = findDispatchedEvents(code);
234
+
235
+ const docSlots = findDocumentedSlots(source);
236
+ const docEvents = findDocumentedEvents(source);
237
+
238
+ // Missing @slot for static exposed slots.
239
+ for (const name of staticSlots) {
240
+ if (!docSlots.names.has(name)) {
241
+ errors.push({
242
+ file: rel,
243
+ message: `renders <slot name="${name}"> but has no matching @slot ${name} tag`,
244
+ });
245
+ }
246
+ }
247
+
248
+ // Missing @slot - for a bare default slot.
249
+ if (defaultSlot && !docSlots.hasDefault) {
250
+ errors.push({
251
+ file: rel,
252
+ message: `renders a default <slot> but has no matching "@slot -" tag`,
253
+ });
254
+ }
255
+
256
+ // Missing @fires for dispatched custom events.
257
+ for (const name of events) {
258
+ if (!docEvents.has(name)) {
259
+ errors.push({
260
+ file: rel,
261
+ message: `dispatches CustomEvent('${name}') but has no matching @fires ${name} tag`,
262
+ });
263
+ }
264
+ }
265
+
266
+ // Phantom @slot — only for direct LitElement subclasses without dynamic
267
+ // slot names, so inherited/dynamic slots never produce false positives.
268
+ if (extendsLitElementDirectly(source) && !dynamicSlot) {
269
+ // A `slot="name"` attribute in the template is a *projection into a child*
270
+ // — it exposes nothing and must NOT suppress the phantom warning. Strip
271
+ // those before the string-literal check so only genuine imperative reads
272
+ // (e.g. `getAttribute('slot') === 'header'`, which relocate the consumer's
273
+ // `slot="name"` children) count as a real, if non-declarative, slot.
274
+ const codeNoProjections = code.replace(
275
+ /\bslot\s*=\s*["'][^"']*["']/g,
276
+ ' '
277
+ );
278
+ for (const name of docSlots.names) {
279
+ if (name.includes('<') || name.includes('${')) continue; // placeholder
280
+ if (staticSlots.has(name)) continue; // rendered as a real <slot>
281
+ if (referencesNameAsString(codeNoProjections, name)) continue;
282
+ errors.push({
283
+ file: rel,
284
+ message: `documents @slot ${name} but renders no <slot name="${name}"> (phantom — likely a slot="${name}" projection into a child)`,
285
+ });
286
+ }
287
+ }
288
+
289
+ // Empty description (warning) — a @slot/@fires tag with only a name produces
290
+ // a blank cell in the manifest / Storybook controls.
291
+ for (const tag of parseSlotTags(source)) {
292
+ if (!tag.hasDescription) {
293
+ const label = tag.name === '-' ? 'default slot' : `@slot ${tag.name}`;
294
+ warnings.push({file: rel, message: `${label} has no description`});
295
+ }
296
+ }
297
+ for (const tag of parseEventTags(source)) {
298
+ if (!tag.hasDescription) {
299
+ warnings.push({
300
+ file: rel,
301
+ message: `@fires ${tag.name} has no description`,
302
+ });
303
+ }
304
+ }
305
+ }
306
+
307
+ console.log('Slot & event documentation audit');
308
+ console.log(`Scanned ${componentCount} registered components under src/`);
309
+
310
+ const printByFile = (
311
+ findings: Finding[],
312
+ log: (msg: string) => void,
313
+ indent = ' '
314
+ ) => {
315
+ const byFile = new Map<string, string[]>();
316
+ for (const f of findings) {
317
+ if (!byFile.has(f.file)) byFile.set(f.file, []);
318
+ byFile.get(f.file)!.push(f.message);
319
+ }
320
+ for (const [file, messages] of [...byFile.entries()].sort()) {
321
+ log(`\n ${file}`);
322
+ for (const msg of messages) log(`${indent}- ${msg}`);
323
+ }
324
+ };
325
+
326
+ if (warnings.length > 0) {
327
+ console.warn(
328
+ `\n⚠️ ${warnings.length} empty-description warning(s) (non-blocking):`
329
+ );
330
+ printByFile(warnings, (m) => console.warn(m));
331
+ }
332
+
333
+ if (errors.length > 0) {
334
+ console.error(`\nFound ${errors.length} issue(s):`);
335
+ printByFile(errors, (m) => console.error(m));
336
+ console.error(
337
+ `\n❌ Slot & event documentation audit failed. Add the missing @slot/@fires ` +
338
+ `tags (or remove the phantom ones). See .cursor/rules/comments.mdc.`
339
+ );
340
+ process.exitCode = 1;
341
+ return;
342
+ }
343
+
344
+ const suffix =
345
+ warnings.length > 0 ? ` (${warnings.length} non-blocking warning(s))` : '';
346
+ console.log(`\n✅ Slot & event documentation audit passed.${suffix}`);
347
+ }
348
+
349
+ run().catch((error: unknown) => {
350
+ const message = error instanceof Error ? error.message : String(error);
351
+ console.error(`❌ Slot & event documentation audit failed: ${message}`);
352
+ process.exitCode = 1;
353
+ });
package/src/ar/poi/poi.ts CHANGED
@@ -164,7 +164,8 @@ const POINT_POINTER_OFFSET_PX = 12;
164
164
  * ## Slots/Content
165
165
  *
166
166
  * - Default slot: Main icon/content rendered inside `obc-poi-button`.
167
- * - `header`: Optional custom header content rendered above the POI object.
167
+ * - `button`: Optional custom button element replacing the default `obc-poi-button`.
168
+ * - `header`: Optional header content. Relocated into the inner `obc-poi-button` at runtime by a `MutationObserver` (there is no `<slot name="header">` element).
168
169
  *
169
170
  * ## Events
170
171
  *
@@ -185,7 +186,8 @@ const POINT_POINTER_OFFSET_PX = 12;
185
186
  * ```
186
187
  *
187
188
  * @slot - Default POI button content.
188
- * @slot header - Optional custom header content.
189
+ * @slot button - Optional custom button element replacing the default `obc-poi-button`.
190
+ * @slot header - Optional header content, relocated into the inner `obc-poi-button` at runtime.
189
191
  * @experimental
190
192
  */
191
193
  @customElement('obc-poi')
@@ -90,6 +90,7 @@ export enum OverlapMode {
90
90
  *
91
91
  * @slot - Default slot for POI targets and optional POI groups.
92
92
  * @fires layer-resize {CustomEvent<{height:number,label:string}>} Fired when the layer height changes.
93
+ * @fires layer-selection-changed {Event} Fired when the layer's `isSelected` state changes. Bubbles.
93
94
  * @experimental
94
95
  */
95
96
  @customElement('obc-poi-layer')
@@ -29,6 +29,7 @@ export enum ObcPoiObjectAtonStyle {
29
29
  export {ObcPoiObjectState as ObcPoiObjectAtonState};
30
30
 
31
31
  /**
32
+ * @slot - Icon content rendered inside the POI object.
32
33
  * @experimental
33
34
  */
34
35
  @customElement('obc-poi-object-aton')
@@ -51,6 +51,8 @@ const ALERT_BADGE_TYPES: ObcAutomationBadgeType[] = [
51
51
  ];
52
52
 
53
53
  /**
54
+ * @slot icon-silhouette - Custom silhouette/outline icon, rendered behind the default slot content when no built-in badge type matches.
55
+ * @slot - Default slot for custom badge content, rendered when no built-in badge type matches.
54
56
  * @stable
55
57
  */
56
58
  @customElement('obc-automation-badge')
@@ -97,6 +97,15 @@ export enum AutomationButtonPositioning {
97
97
  }
98
98
 
99
99
  /**
100
+ * @slot badge-top-right - Content projected into the top-right badge position of the button.
101
+ * @slot badge-top-left - Content projected into the top-left badge position of the button.
102
+ * @slot badge-bottom-left - Content projected into the bottom-left badge position of the button.
103
+ * @slot badge-bottom-right - Content projected into the bottom-right badge position of the button.
104
+ * @slot icon - Primary icon shown inside the button (regular, double, flat and square variants).
105
+ * @slot icon-silhouette - Silhouette/outline icon shown behind the primary icon (flat variant only).
106
+ * @slot alert-icon - Custom icon for the alert frame, shown when alert==true and positioning=="point".
107
+ * @slot alert-label - Label for the alert frame, shown when alert==true and positioning=="point".
108
+ * @slot alert-timer - Timer for the alert frame, shown when alert==true and positioning=="point".
100
109
  * @stable
101
110
  */
102
111
  @customElement('obc-automation-button')
@@ -3,6 +3,10 @@ import {LitElement, html, unsafeCSS} from 'lit';
3
3
  import compentStyle from './automation-input-modal.css?inline';
4
4
 
5
5
  /**
6
+ * @slot header - Header content shown at the top of the modal.
7
+ * @slot preview - Preview content shown in the body of the modal.
8
+ * @slot action-primary - Primary action control shown in the actions row.
9
+ * @slot action-secondary - Secondary action control shown in the actions row.
6
10
  * @experimental
7
11
  */
8
12
  @customElement('obc-automation-input-modal')
@@ -488,70 +488,14 @@
488
488
  margin-top: 8px;
489
489
  }
490
490
 
491
- .rich {
492
- display: grid;
493
- /* label takes remaining space; value (right-aligned) and suffix
494
- * (left-aligned) form fixed content columns so digit right edges and
495
- * suffix left edges align vertically across all rows. */
496
- grid-template-columns: 1fr auto auto;
497
- align-items: baseline;
498
- row-gap: 4px;
499
- padding-top: 8px;
500
- white-space: nowrap;
501
- @mixin font-instrument-unit;
502
- }
503
-
504
- .rich-row {
505
- display: grid;
506
- grid-template-columns: subgrid;
507
- grid-column: 1 / -1;
508
- align-items: baseline;
509
- padding-left: var(
510
- --instrument-components-readout-new-list-item-regular-container-padding-horizontal
511
- );
512
- padding-right: var(
513
- --instrument-components-readout-new-list-item-regular-container-padding-horizontal
514
- );
515
- }
516
-
517
- .rich-label {
518
- @mixin font-instrument-label;
519
- color: var(--element-inactive-color);
520
- padding-right: 8px;
521
- text-align: left;
522
- }
523
-
524
- .rich-value {
525
- @mixin font-instrument-value-regular-neutral;
526
- color: var(--element-neutral-color);
527
- font-variant-numeric: tabular-nums;
528
- text-align: right;
529
- }
530
-
531
- .rich-suffix {
532
- display: inline-flex;
533
- align-items: baseline;
534
- /* Default 2px gap between value and unit when there is no degree/percent
535
- * glyph. When a glyph is present it sits flush against the value and the
536
- * unit follows flush after the glyph (see `.rich-glyph + .rich-unit`). */
537
- padding-left: 2px;
538
- }
539
-
540
- .rich-suffix:has(.rich-glyph) {
541
- padding-left: 0;
542
- }
543
-
544
- .rich-glyph {
545
- @mixin font-instrument-value-regular-neutral;
546
- color: var(--element-neutral-color);
547
- }
548
-
549
- .rich-unit {
550
- color: var(--element-inactive-color);
551
- }
552
-
553
- .rich-glyph + .rich-unit {
554
- padding-left: 0;
491
+ /* Rich detail rows are consumer-slotted into `slot="rich"` — canonical usage is
492
+ * an `<obc-readout-list>` of `<obc-readout-list-item>` rows, which owns its own
493
+ * cap-height (textbox) typography and cross-row column alignment. The tank only
494
+ * spaces the slotted content below the divider; the divider itself is shown only
495
+ * when the slot is filled (`?hidden` bound to `_hasRichSlot`). */
496
+ ::slotted(obc-readout-list) {
497
+ display: block;
498
+ margin-top: 8px;
555
499
  }
556
500
 
557
501
  /* The chart cell is a plain sizing/clipping box. Both child renderers —
@@ -6,10 +6,11 @@ import {
6
6
  TankPositioning,
7
7
  TankTrend,
8
8
  TankType,
9
- type TankReadoutItem,
10
9
  } from './automation-tank.js';
11
10
  import './automation-tank.js';
12
- import {html} from 'lit';
11
+ import '../../navigation-instruments/readout-list/readout-list.js';
12
+ import '../../navigation-instruments/readout-list-item/readout-list-item.js';
13
+ import {html, nothing} from 'lit';
13
14
  import {crossDecorator} from '../../storybook-util.js';
14
15
  import {AdviceType} from '../../navigation-instruments/watch/advice.js';
15
16
  import type {LinearAdvice} from '../../building-blocks/instrument-linear/advice.js';
@@ -57,13 +58,37 @@ const SAMPLE_ADVICE: LinearAdvice[] = [
57
58
  {min: 7500, max: 9000, type: AdviceType.advice, hinted: false},
58
59
  ];
59
60
 
60
- const SAMPLE_READOUT: TankReadoutItem[] = [
61
- {label: 'Temperature', value: 45, hasDegree: true, unit: 'C'},
62
- {label: 'Pressure', value: 45, unit: 'Pa'},
63
- {label: 'Flow speed', value: 45, unit: 'm/s'},
64
- ];
61
+ // Canonical rich-detail content for the tank: an `<obc-readout-list>` of
62
+ // `<obc-readout-list-item>` rows slotted into `slot="rich"`. The list owns the
63
+ // cross-row column alignment and cap-height typography, replacing the tank's
64
+ // former hand-rolled `.rich` grid.
65
+ const renderRichReadout = () => html`
66
+ <obc-readout-list slot="rich">
67
+ <obc-readout-list-item
68
+ .label=${'Temperature'}
69
+ .value=${45}
70
+ .hasDegree=${true}
71
+ .unit=${'C'}
72
+ ></obc-readout-list-item>
73
+ <obc-readout-list-item
74
+ .label=${'Pressure'}
75
+ .value=${45}
76
+ .unit=${'Pa'}
77
+ ></obc-readout-list-item>
78
+ <obc-readout-list-item
79
+ .label=${'Flow speed'}
80
+ .value=${45}
81
+ .unit=${'m/s'}
82
+ ></obc-readout-list-item>
83
+ </obc-readout-list>
84
+ `;
65
85
 
66
- const renderTank = (args: StoryArgs) => html`
86
+ // `richReadout` is slotted into `slot="rich"`; defaults to `nothing` so most
87
+ // stories render no detail rows. Kept as a separate helper (not the meta
88
+ // `render`) because Storybook calls `render(args, context)` with a second
89
+ // argument — `renderTank` below wraps it so that extra arg can't leak in as
90
+ // slotted content.
91
+ const renderTankEl = (args: StoryArgs, richReadout: unknown = nothing) => html`
67
92
  <obc-automation-tank
68
93
  .value=${args.value}
69
94
  .max=${args.max}
@@ -82,7 +107,6 @@ const renderTank = (args: StoryArgs) => html`
82
107
  .hasGraphIcon=${args.hasGraphIcon}
83
108
  .showTrendSymbol=${args.showTrendSymbol}
84
109
  .percentFractionDigits=${args.percentFractionDigits}
85
- .readout=${args.readout}
86
110
  ?alert=${args.alert}
87
111
  .alertFrameType=${args.alertFrameType}
88
112
  .alertFrameThickness=${args.alertFrameThickness}
@@ -95,9 +119,12 @@ const renderTank = (args: StoryArgs) => html`
95
119
  .badgeCommandLocked=${args.badgeCommandLocked}
96
120
  .priority=${args.priority}
97
121
  >
122
+ ${richReadout}
98
123
  </obc-automation-tank>
99
124
  `;
100
125
 
126
+ const renderTank = (args: StoryArgs) => renderTankEl(args);
127
+
101
128
  const meta: Meta<StoryArgs> = {
102
129
  title: 'Automation/Tanks/Tank',
103
130
  tags: ['autodocs', '6.0'],
@@ -120,7 +147,6 @@ const meta: Meta<StoryArgs> = {
120
147
  hasGraphIcon: false,
121
148
  showTrendSymbol: true,
122
149
  percentFractionDigits: 0,
123
- readout: [],
124
150
  alert: false,
125
151
  alertFrameType: ObcAlertFrameType.SmallSideFlip,
126
152
  alertFrameThickness: ObcAlertFrameThickness.Small,
@@ -193,11 +219,6 @@ const meta: Meta<StoryArgs> = {
193
219
  description:
194
220
  'Advice overlay bands. `min`/`max` are in the same units as `max`. Toggle visibility with `hasAdvice`. Works in all three `chartMode` variants — `bar` overlays advice pills on the static bar, `graph` and `graph-and-bar` forward them to the embedded `obc-gauge-trend`.',
195
221
  },
196
- readout: {
197
- control: {type: 'object'},
198
- description:
199
- 'Rich detail rows shown below the main percent/value block in the regular (non-compact, non-static) layout, separated by a divider. Each row: `{label, value, hasDegree?, hasPercentage?, unit}`. Empty array — nothing is rendered. Values use `percentFractionDigits` for formatting. Override the whole block via `slot="rich"`.',
200
- },
201
222
  badgeControl: {
202
223
  options: Object.values(AutomationButtonBadgeControl),
203
224
  control: {type: 'select'},
@@ -405,8 +426,8 @@ export const Rich: Story = {
405
426
  args: {
406
427
  type: TankType.atmospheric,
407
428
  chartMode: TankChartMode.graphAndBar,
408
- readout: SAMPLE_READOUT,
409
429
  },
430
+ render: (args) => renderTankEl(args, renderRichReadout()),
410
431
  };
411
432
 
412
433
  export const HorizontalRich: Story = {
@@ -414,8 +435,8 @@ export const HorizontalRich: Story = {
414
435
  type: TankType.atmospheric,
415
436
  orientation: TankOrientation.horizontal,
416
437
  chartMode: TankChartMode.graphAndBar,
417
- readout: SAMPLE_READOUT,
418
438
  },
439
+ render: (args) => renderTankEl(args, renderRichReadout()),
419
440
  };
420
441
 
421
442
  /**
@@ -431,9 +452,9 @@ export const HorizontalRich: Story = {
431
452
  * and always render it as an integer to keep their fixed-width footprint
432
453
  * stable, so fraction-digit control only applies to the non-compact layout.
433
454
  *
434
- * `percentFractionDigits` is also applied uniformly to every row of the
435
- * `readout` rich list (see the `Rich` story) all rows share one digit
436
- * count so the right-aligned numeric column stays visually consistent.
455
+ * The rich detail rows are now consumer-slotted (`slot="rich"`, canonically an
456
+ * `<obc-readout-list>`); each `<obc-readout-list-item>` owns its own
457
+ * `fractionDigits`, so `percentFractionDigits` no longer affects them.
437
458
  */
438
459
  export const WithFractionDigits: Story = {
439
460
  args: {
@@ -463,7 +484,6 @@ export const WithFractionDigits: Story = {
463
484
  .showTrendSymbol=${args.showTrendSymbol}
464
485
  .priority=${args.priority}
465
486
  .percentFractionDigits=${args.percentFractionDigits}
466
- .readout=${args.readout}
467
487
  >
468
488
  <span slot="current-value">${args.value.toFixed(2)}</span>
469
489
  <span slot="max-value">${args.max.toFixed(2)}</span>
@@ -502,7 +522,6 @@ export const WithAlertAlarm: Story = {
502
522
  .hasGraphIcon=${args.hasGraphIcon}
503
523
  .showTrendSymbol=${args.showTrendSymbol}
504
524
  .percentFractionDigits=${args.percentFractionDigits}
505
- .readout=${args.readout}
506
525
  ?alert=${args.alert}
507
526
  .alertFrameType=${args.alertFrameType}
508
527
  .alertFrameThickness=${args.alertFrameThickness}
@@ -625,7 +644,6 @@ export const Responsive: Story = {
625
644
  .hasGraphIcon=${args.hasGraphIcon}
626
645
  .showTrendSymbol=${args.showTrendSymbol}
627
646
  .percentFractionDigits=${args.percentFractionDigits}
628
- .readout=${args.readout}
629
647
  ?alert=${args.alert}
630
648
  .alertFrameType=${args.alertFrameType}
631
649
  .alertFrameThickness=${args.alertFrameThickness}