bitboss-ui 3.0.0-beta.21 → 3.0.0-beta.22

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 (183) hide show
  1. package/README.md +55 -3
  2. package/bin/bitboss-ui-mcp.mjs +419 -9
  3. package/bin/bitboss-ui.mjs +293 -22
  4. package/dist/ai/BbAccordion.md +21 -23
  5. package/dist/ai/BbAlert.md +6 -12
  6. package/dist/ai/BbAvatar.md +60 -24
  7. package/dist/ai/BbBadge.md +12 -20
  8. package/dist/ai/BbBaseButton.md +14 -16
  9. package/dist/ai/BbBaseCheckboxIcon.md +6 -10
  10. package/dist/ai/BbBaseRadioIcon.md +6 -10
  11. package/dist/ai/BbBaseSwitchIcon.md +5 -9
  12. package/dist/ai/BbBreadcrumbs.md +23 -34
  13. package/dist/ai/BbButton.md +35 -47
  14. package/dist/ai/BbCheckbox.md +21 -37
  15. package/dist/ai/BbCheckboxGroup.md +13 -13
  16. package/dist/ai/BbCollapsible.md +14 -26
  17. package/dist/ai/BbColorInput.md +13 -23
  18. package/dist/ai/BbColorPalette.md +23 -35
  19. package/dist/ai/BbConfirm.md +17 -32
  20. package/dist/ai/BbDatePickerInput.md +15 -21
  21. package/dist/ai/BbDialog.md +159 -62
  22. package/dist/ai/BbDropdown.md +21 -25
  23. package/dist/ai/BbDropdownButton.md +13 -30
  24. package/dist/ai/BbDropzone.md +9 -11
  25. package/dist/ai/BbIcon.md +44 -33
  26. package/dist/ai/BbIndicator.md +2 -2
  27. package/dist/ai/BbNumberInput.md +18 -20
  28. package/dist/ai/BbOffCanvas.md +42 -42
  29. package/dist/ai/BbPagination.md +21 -23
  30. package/dist/ai/BbPopover.md +15 -17
  31. package/dist/ai/BbProgress.md +40 -51
  32. package/dist/ai/BbRadio.md +9 -9
  33. package/dist/ai/BbRadioGroup.md +7 -11
  34. package/dist/ai/BbRating.md +8 -12
  35. package/dist/ai/BbSelect.md +32 -36
  36. package/dist/ai/BbSelectPopover.md +19 -31
  37. package/dist/ai/BbSlider.md +15 -19
  38. package/dist/ai/BbSmoothHeight.md +16 -31
  39. package/dist/ai/BbSpinner.md +118 -74
  40. package/dist/ai/BbSwitch.md +25 -18
  41. package/dist/ai/BbSwitchGroup.md +9 -14
  42. package/dist/ai/BbTable.md +102 -55
  43. package/dist/ai/BbTabs.md +59 -82
  44. package/dist/ai/BbTag.md +12 -23
  45. package/dist/ai/BbTextInput.md +20 -26
  46. package/dist/ai/BbTextarea.md +28 -34
  47. package/dist/ai/BbToast.md +14 -34
  48. package/dist/ai/BbTooltip.md +22 -20
  49. package/dist/ai/BbTree.md +46 -72
  50. package/dist/ai/changelog.json +169 -62
  51. package/dist/ai/components.json +1467 -9
  52. package/dist/ai/composables/useConfirm.md +5 -2
  53. package/dist/ai/composables/useToast.md +4 -2
  54. package/dist/ai/guides/agent-contract.md +7 -4
  55. package/dist/ai/guides/ai-router.md +37 -22
  56. package/dist/ai/guides/component-picker.md +23 -18
  57. package/dist/ai/guides/design-language.md +141 -13
  58. package/dist/ai/guides/design-tokens.md +29 -4
  59. package/dist/ai/guides/icons-policy.md +46 -21
  60. package/dist/ai/guides/installation-and-plugin-setup.md +253 -17
  61. package/dist/ai/guides/migration/components/bb-rating.md +26 -1
  62. package/dist/ai/guides/migration/components/bb-tooltip.md +18 -0
  63. package/dist/ai/guides/migration/v2-to-v3.md +50 -23
  64. package/dist/ai/index.md +1 -1
  65. package/dist/ai/recipes/inertia/approvals-inbox.md +3 -3
  66. package/dist/ai/recipes/inertia/inline-edit-workspace.md +2 -2
  67. package/dist/ai/recipes/inertia/onboarding.md +2 -2
  68. package/dist/ai/recipes/inertia/ownership-atlas.md +6 -6
  69. package/dist/ai/recipes/inertia/record-form.md +5 -5
  70. package/dist/ai/recipes/inertia/records-workspace.md +4 -4
  71. package/dist/ai/recipes/inertia/upload-center.md +6 -6
  72. package/dist/ai/recipes/inertia/wizard-form.md +5 -5
  73. package/dist/ai/recipes/nuxt/approvals-inbox.md +3 -3
  74. package/dist/ai/recipes/nuxt/inline-edit-workspace.md +2 -2
  75. package/dist/ai/recipes/nuxt/onboarding.md +2 -2
  76. package/dist/ai/recipes/nuxt/record-form.md +5 -5
  77. package/dist/ai/recipes/nuxt/records-workspace.md +4 -4
  78. package/dist/ai/recipes/nuxt/upload-center.md +6 -6
  79. package/dist/ai/recipes/nuxt/wizard-form.md +5 -5
  80. package/dist/ai/recipes/vue/approvals-inbox.md +3 -3
  81. package/dist/ai/recipes/vue/inline-edit-workspace.md +2 -2
  82. package/dist/ai/recipes/vue/onboarding.md +2 -2
  83. package/dist/ai/recipes/vue/record-form.md +10 -10
  84. package/dist/ai/recipes/vue/records-workspace.md +4 -4
  85. package/dist/ai/recipes/vue/upload-center.md +6 -6
  86. package/dist/ai/recipes/vue/wizard-form.md +5 -5
  87. package/dist/ai/source/BbBadge.md +1 -1
  88. package/dist/ai/source/BbBaseCheckbox.md +1 -1
  89. package/dist/ai/source/BbBaseDatePicker.md +5 -5
  90. package/dist/ai/source/BbBaseDatePickerInput.md +2 -2
  91. package/dist/ai/source/BbBaseRadio.md +1 -1
  92. package/dist/ai/source/BbBaseRating.md +5 -5
  93. package/dist/ai/source/BbBaseSelect.md +1 -1
  94. package/dist/ai/source/BbBaseSlider.md +5 -5
  95. package/dist/ai/source/BbBaseSwitch.md +1 -1
  96. package/dist/ai/source/BbBaseTimePickerInput.md +1 -1
  97. package/dist/ai/source/BbBreadcrumbs.md +20 -1
  98. package/dist/ai/source/BbButton.md +2 -2
  99. package/dist/ai/source/BbDialog.md +39 -8
  100. package/dist/ai/source/BbDropdown.md +1 -1
  101. package/dist/ai/source/BbDropzone.md +5 -5
  102. package/dist/ai/source/BbIcon.md +23 -10
  103. package/dist/ai/source/BbOffCanvas.md +50 -28
  104. package/dist/ai/source/BbPagination.md +2 -2
  105. package/dist/ai/source/BbPopover.md +12 -1
  106. package/dist/ai/source/BbSelectPopover.md +45 -7
  107. package/dist/ai/source/BbSpinner.md +2 -2
  108. package/dist/ai/source/BbTable.md +11 -11
  109. package/dist/ai/source/BbTooltip.md +57 -10
  110. package/dist/ai/source/ChipsBox.md +1 -1
  111. package/dist/ai/source/CommaBox.md +1 -1
  112. package/dist/ai/source/GuidesComponent.md +14 -7
  113. package/dist/ai/source/OptionsContainer.md +1 -1
  114. package/dist/components/BbBadge/BadgeBodyContent.vue_vue_type_script_setup_true_lang.js +1 -1
  115. package/dist/components/BbBadge/BbBadge.vue_vue_type_script_setup_true_lang.js +1 -1
  116. package/dist/components/BbBaseCheckbox/BbBaseCheckbox.vue_vue_type_script_setup_true_lang.js +1 -1
  117. package/dist/components/BbBaseDatePicker/BbBaseDatePicker.vue_vue_type_script_setup_true_lang.js +4 -4
  118. package/dist/components/BbBaseDatePicker/BbBaseDatePickerInputDaySelector.vue_vue_type_script_setup_true_lang.js +1 -1
  119. package/dist/components/BbBaseDatePickerInput/BbBaseDatePickerInput.vue_vue_type_script_setup_true_lang.js +1 -1
  120. package/dist/components/BbBaseInputContainer/BbBaseInputContainer.vue.d.ts +1 -1
  121. package/dist/components/BbBaseRadio/BbBaseRadio.vue_vue_type_script_setup_true_lang.js +1 -1
  122. package/dist/components/BbBaseRating/BbBaseRating.vue_vue_type_script_setup_true_lang.js +3 -3
  123. package/dist/components/BbBaseSelect/BbBaseSelect.vue_vue_type_script_setup_true_lang.js +1 -1
  124. package/dist/components/BbBaseSlider/BbBaseSlider.vue_vue_type_script_setup_true_lang.js +3 -3
  125. package/dist/components/BbBaseSwitch/BbBaseSwitch.vue_vue_type_script_setup_true_lang.js +1 -1
  126. package/dist/components/BbBaseTimePickerInput/BbBaseTimePickerInput.vue_vue_type_script_setup_true_lang.js +1 -1
  127. package/dist/components/BbBreadcrumbs/BbBreadcrumbs.vue_vue_type_script_setup_true_lang.js +71 -71
  128. package/dist/components/BbButton/BbButton.vue_vue_type_script_setup_true_lang.js +2 -2
  129. package/dist/components/BbDialog/BbDialog.vue_vue_type_script_setup_true_lang.js +102 -97
  130. package/dist/components/BbDropdown/AdaptiveDropdown.vue_vue_type_script_setup_true_lang.js +1 -1
  131. package/dist/components/BbDropdown/BbDropdown.vue_vue_type_script_setup_true_lang.js +58 -58
  132. package/dist/components/BbDropzone/BbDropzone.vue_vue_type_script_setup_true_lang.js +2 -2
  133. package/dist/components/BbIcon/BbIcon.vue_vue_type_script_setup_true_lang.js +33 -35
  134. package/dist/components/BbIcon/iconCache.d.ts +30 -0
  135. package/dist/components/BbIcon/iconCache.js +21 -3
  136. package/dist/components/BbOffCanvas/BbOffCanvas.vue_vue_type_script_setup_true_lang.js +57 -56
  137. package/dist/components/BbPagination/BbPagination.vue_vue_type_script_setup_true_lang.js +2 -2
  138. package/dist/components/BbPopover/BbPopover.vue_vue_type_script_setup_true_lang.js +138 -138
  139. package/dist/components/BbSelectPopover/BbSelectPopover.vue_vue_type_script_setup_true_lang.js +43 -43
  140. package/dist/components/BbSpinner/BbSpinner.vue_vue_type_script_setup_true_lang.js +1 -1
  141. package/dist/components/BbSpinner/types.d.ts +1 -1
  142. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +8 -8
  143. package/dist/components/BbTable/BbTableDataRow.js +2 -2
  144. package/dist/components/BbToast/BbToastRegion.vue_vue_type_script_setup_true_lang.js +62 -62
  145. package/dist/components/BbTooltip/BbTooltip.vue_vue_type_script_setup_true_lang.js +81 -78
  146. package/dist/components/BbTooltip/types.d.ts +8 -1
  147. package/dist/components/ChipsBox/ChipsBox.vue_vue_type_script_setup_true_lang.js +1 -1
  148. package/dist/components/CommaBox/CommaBox.vue_vue_type_script_setup_true_lang.js +1 -1
  149. package/dist/components/OptionsContainer/OptionsContainer.vue_vue_type_script_setup_true_lang.js +1 -1
  150. package/dist/composables/useConfirm.d.ts +22 -0
  151. package/dist/composables/useConfirm.js +43 -40
  152. package/dist/composables/useSafeAreaInternal.js +1 -1
  153. package/dist/composables/useToast.d.ts +16 -0
  154. package/dist/composables/useToast.js +20 -16
  155. package/dist/composables/useViewportWidth.d.ts +20 -6
  156. package/dist/composables/useViewportWidth.js +8 -7
  157. package/dist/icon-scan.d.ts +276 -0
  158. package/dist/icon-scan.js +485 -0
  159. package/dist/icons-whitelist.d.ts +5 -4
  160. package/dist/llms-full.txt +1979 -1527
  161. package/dist/llms-medium.txt +461 -74
  162. package/dist/llms.txt +1 -1
  163. package/dist/nuxt.js +10 -7
  164. package/dist/plugin.js +1 -0
  165. package/dist/runtime/nuxt-plugin.js +2 -1
  166. package/dist/styles.css +1 -1
  167. package/dist/types/Config.d.ts +18 -0
  168. package/dist/utils/cssLayers.d.ts +91 -0
  169. package/dist/utils/cssLayers.js +43 -0
  170. package/dist/utils/injectLibraryStyles.d.ts +3 -1
  171. package/dist/utils/injectLibraryStyles.js +24 -23
  172. package/dist/vite-plugin.d.ts +74 -11
  173. package/dist/vite.js +292 -253
  174. package/llms.txt +1 -1
  175. package/package.json +7 -5
  176. package/scripts/lib/ensure-manifest.mjs +28 -5
  177. package/scripts/lib/eslint-plugin.d.ts +35 -0
  178. package/scripts/lib/eslint-plugin.mjs +299 -3
  179. package/scripts/lib/hand-roll-hints.mjs +50 -6
  180. package/scripts/lib/html-attributes.mjs +42 -0
  181. package/scripts/lib/mcp-config.mjs +38 -34
  182. package/scripts/lib/public-type-exports.mjs +246 -0
  183. package/scripts/lib/validate-bb-markup.mjs +205 -12
package/llms.txt CHANGED
@@ -10,7 +10,7 @@ Note: the `dist/` paths below resolve inside the installed npm package (`node_mo
10
10
  - [Complete knowledge base](https://llms-bitboss-ui.bitboss.it/dist/llms-full.txt): Everything concatenated (~2 MB). Bulk ingestion only — too large to prompt with.
11
11
  - [AI Router](dist/ai/guides/ai-router.md): **Start here** — tiny hop map for any coding agent (which file to open next).
12
12
  - [Component index](dist/ai/index.md): Full component catalogue with agent rules, grouped by category. Each entry links a typed contract (props/events/slots/v-model).
13
- - [Component manifest](dist/ai/components.json): Machine-readable API surface of every component, for programmatic validation.
13
+ - [Component manifest](dist/ai/components.json): Machine-readable API surface of every component, for programmatic validation. Its `types` array lists the public TYPE exports with a `usage` tier (`api` = the ones you reach for) and, on the `api` ones, a `usedBy` array naming the component and prop or event each is the type OF — `BbTableColumn` is `usedBy` `BbTable.columns`, a value you pass, not a tag you write.
14
14
  - CLI / MCP: `npx bitboss-ui check` (markup vs components.json), `npx bitboss-ui mcp` (search/get/validate), `npx bitboss-ui ai-init [--mcp]`.
15
15
  - [Agent Contract](dist/ai/guides/agent-contract.md): Mandatory rules before writing any component usage code.
16
16
  - [Installation & Plugin Setup](dist/ai/guides/installation-and-plugin-setup.md): Canonical installation, style handling, and plugin configuration.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitboss-ui",
3
- "version": "3.0.0-beta.21",
3
+ "version": "3.0.0-beta.22",
4
4
  "description": "Vue 3 component library by BitBoss: forms, selects, layout, overlays, and more.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -102,6 +102,7 @@
102
102
  "@tanstack/vue-virtual": "^3.13.24",
103
103
  "@vueuse/core": "^14.3.0",
104
104
  "dayjs": "^1.11.20",
105
+ "ignore": "^7.0.9",
105
106
  "lodash.deburr": "^4.1.0",
106
107
  "lodash.throttle": "^4.1.1",
107
108
  "maska": "^3.2.0",
@@ -148,6 +149,7 @@
148
149
  "@rollup/plugin-terser": "^1.0.0",
149
150
  "@storybook/addon-docs": "^10.4.6",
150
151
  "@storybook/vue3-vite": "^10.4.6",
152
+ "@tailwindcss/postcss": "^4.3.3",
151
153
  "@types/lodash.deburr": "^4.1.9",
152
154
  "@types/lodash.throttle": "^4.1.9",
153
155
  "@types/mime": "^4.0.0",
@@ -161,7 +163,7 @@
161
163
  "@vitest/browser-playwright": "^4.1.10",
162
164
  "@vue/language-core": "~3.1.5",
163
165
  "@vue/test-utils": "^2.4.11",
164
- "autoprefixer": "^10.5.0",
166
+ "autoprefixer": "^10.5.5",
165
167
  "concurrently": "^9.2.1",
166
168
  "eslint": "^9.39.4",
167
169
  "eslint-config-prettier": "^10.1.8",
@@ -179,16 +181,16 @@
179
181
  "playwright": "^1.61.1",
180
182
  "postcss": "^8.5.14",
181
183
  "postcss-html": "^1.8.1",
182
- "postcss-import": "^16.1.1",
184
+ "postcss-nested": "^8.0.1",
183
185
  "postcss-value-parser": "^4.2.0",
184
186
  "prettier": "3.8.3",
185
- "prettier-plugin-tailwindcss": "^0.8.0",
187
+ "prettier-plugin-tailwindcss": "^0.8.1",
186
188
  "query-string": "^9.4.1",
187
189
  "rollup-plugin-typescript2": "^0.37.0",
188
190
  "storybook": "^10.4.6",
189
191
  "stylelint": "^17.11.0",
190
192
  "stylelint-order": "^8.1.1",
191
- "tailwindcss": "^3.4.10",
193
+ "tailwindcss": "^4.3.3",
192
194
  "typescript": "^6.0.3",
193
195
  "vee-validate": "^4.15.1",
194
196
  "vite": "^8.0.12",
@@ -22,6 +22,15 @@ const ROOT = fileURLToPath(new URL('../..', import.meta.url));
22
22
  /** The one path every validator defaults to. */
23
23
  export const MANIFEST_PATH = join(ROOT, 'dist/ai/components.json');
24
24
 
25
+ /**
26
+ * `list_changes` reads this, and `generate-docs` does NOT write it — only the
27
+ * full build does, via `scripts/generate-ai-changelog.mjs`. So an unbuilt gate
28
+ * job got a `dist/ai` that looked complete and was not: the MCP server answered
29
+ * `list_changes` with "changelog.json not found", `check:mcp` failed three
30
+ * assertions and then died on the missing release list.
31
+ */
32
+ export const CHANGELOG_PATH = join(ROOT, 'dist/ai/changelog.json');
33
+
25
34
  /**
26
35
  * Generate the contract when absent. Cheap (~2.5s) and idempotent, so callers
27
36
  * can invoke it unconditionally.
@@ -30,10 +39,24 @@ export const MANIFEST_PATH = join(ROOT, 'dist/ai/components.json');
30
39
  * @returns {string} the manifest path.
31
40
  */
32
41
  export function ensureManifest(label = 'ensure-manifest') {
33
- if (existsSync(MANIFEST_PATH)) return MANIFEST_PATH;
34
- console.log(
35
- `[${label}] dist/ai/components.json missing — running generate-docs…`
36
- );
37
- execSync('npm run generate-docs', { cwd: ROOT, stdio: 'inherit' });
42
+ if (!existsSync(MANIFEST_PATH)) {
43
+ console.log(
44
+ `[${label}] dist/ai/components.json missing — running generate-docs…`
45
+ );
46
+ execSync('npm run generate-docs', { cwd: ROOT, stdio: 'inherit' });
47
+ }
48
+
49
+ // Separate check: `generate-docs` writes the contracts but not the
50
+ // changelog, so a tree can have one and not the other.
51
+ if (!existsSync(CHANGELOG_PATH)) {
52
+ console.log(
53
+ `[${label}] dist/ai/changelog.json missing — running generate-ai-changelog…`
54
+ );
55
+ execSync('node scripts/generate-ai-changelog.mjs', {
56
+ cwd: ROOT,
57
+ stdio: 'inherit',
58
+ });
59
+ }
60
+
38
61
  return MANIFEST_PATH;
39
62
  }
@@ -28,8 +28,42 @@ export type BitbossUiRuleName =
28
28
  | 'no-unknown-slots'
29
29
  | 'no-active-class-on-root-link'
30
30
  | 'no-navigation-attr-fallthrough'
31
+ | 'no-unknown-event'
32
+ | 'no-unknown-token'
31
33
  | 'no-reverse-false';
32
34
 
35
+ /** Options accepted by `bitboss-ui/no-unknown-token`. */
36
+ export interface NoUnknownTokenOptions {
37
+ /**
38
+ * Token names to always permit — for `--bb-*` names you declare in your own
39
+ * stylesheet, which this rule cannot see. The alternative, and usually the
40
+ * better one, is a fallback: `var(--bb-mine, #fff)` is never reported.
41
+ */
42
+ allowTokens?: string[];
43
+ /**
44
+ * Stylesheet to read the declared token list from. Defaults to the
45
+ * `dist/styles.css` inside the installed package, so a normal install needs
46
+ * no configuration. A path that does not resolve disarms the rule rather
47
+ * than failing: no data, no accusation.
48
+ */
49
+ stylesPath?: string;
50
+ }
51
+
52
+ /** Options accepted by `bitboss-ui/no-unknown-event`. */
53
+ export interface NoUnknownEventOptions {
54
+ /** Component names to skip entirely. */
55
+ allowComponents?: string[];
56
+ /**
57
+ * Event names to always permit, on any component.
58
+ *
59
+ * The escape hatch for a codebase that leans on a specific fallthrough
60
+ * listener on purpose — `['close']` where every dialog wrapper listens for
61
+ * the native `<dialog>` event — without disabling the rule and losing the
62
+ * renamed-event half of it.
63
+ */
64
+ allowEvents?: string[];
65
+ }
66
+
33
67
  /** Options accepted by `bitboss-ui/no-reverse-false`. */
34
68
  export interface NoReverseFalseOptions {
35
69
  /** Component names to skip entirely. */
@@ -71,6 +105,7 @@ export const requirePartnerProp: Rule.RuleModule;
71
105
  export const noUnknownSlots: Rule.RuleModule;
72
106
  export const noActiveClassOnRootLink: Rule.RuleModule;
73
107
  export const noNavigationAttrFallthrough: Rule.RuleModule;
108
+ export const noUnknownEvent: Rule.RuleModule;
74
109
  export const noReverseFalse: Rule.RuleModule;
75
110
 
76
111
  /**
@@ -50,20 +50,47 @@
50
50
  * BbBadgeButton), is not an anchor. The attribute renders, inert, and
51
51
  * nothing ever warns (Bottega P3-R2, BTG-006: `<BbBadge href>` painted a
52
52
  * non-interactive pill).
53
+ * - `no-unknown-event` — an `@event` a Bb* component does not emit. The
54
+ * event twin of `no-navigation-attr-fallthrough`, and the only rule the
55
+ * recommended preset sets to `warn`: an undeclared listener falls through
56
+ * to the root element, so it may be a deliberate native listener rather
57
+ * than a mistake. `<BbDialog @close>` is the case that prompted it — live,
58
+ * because that root is a native `<dialog>`, but outside the contract and
59
+ * unordered against the component's own `hidden`; a consumer's migration
60
+ * report filed it as dead code. `bitboss-ui check` has flagged this since
61
+ * Q10.12 via the shared `checkEvent`; the editor never did, because every
62
+ * rule here read `bind`/`model` and skipped `on`.
63
+ * - `no-unknown-token` — a bare `var(--bb-…)` naming a token the installed
64
+ * package does not declare. The first rule here that reads CSS rather than
65
+ * the template AST, and the first promoted from a `bitboss-ui check`
66
+ * ADVISORY HINT: the same consumer report that filed `no-unknown-event`
67
+ * made the structural point that `check` is what nobody runs while ESLint
68
+ * runs in the editor and in pre-commit. Only this hint crossed over, of the
69
+ * three `check` carries. It is the only one stating a fact rather than a
70
+ * guess — an undefined custom property is invalid-at-computed-value-time,
71
+ * so CSS discards the entire declaration. The hand-rolled avatar and
72
+ * spinner detectors stay in `check` on purpose: they are guesses (a round
73
+ * `<img>` is sometimes a logo), and a rule that fires on correct code
74
+ * spends the trust every other rule here depends on.
53
75
  */
54
76
 
55
77
  import { createRequire } from 'node:module';
78
+ import { dirname, join } from 'node:path';
79
+ import { fileURLToPath } from 'node:url';
80
+ import { findTokenUses, loadKnownTokens } from './hand-roll-hints.mjs';
56
81
  import {
57
82
  checkNavAttrFallthrough,
58
83
  checkPartnerProps,
59
84
  createComponentChecker,
60
85
  loadManifest,
61
86
  NAV_ATTRS,
87
+ unknownComponentFinding,
62
88
  PARTNER_PROP_PAIRS,
63
89
  toCamel,
64
90
  } from './validate-bb-markup.mjs';
65
91
  import { matchNameStyle, planAttributeFix } from './migrate-fix.mjs';
66
92
 
93
+ const HERE = dirname(fileURLToPath(import.meta.url));
67
94
  const BB_TAG = /^Bb[A-Za-z0-9]+$/;
68
95
  /** `bitboss-ui` and its subpath entrypoints (`bitboss-ui/validated`, …). */
69
96
  const BITBOSS_IMPORT = /^bitboss-ui(\/|$)/;
@@ -150,8 +177,15 @@ const noUnknownAttributes = {
150
177
  },
151
178
  ],
152
179
  messages: {
153
- unknownComponent:
154
- 'component `{{component}}` is not in the bitboss-ui manifest',
180
+ /*
181
+ * One message with the hint folded in, rather than the bare
182
+ * "not in the manifest" this used to print. The three reasons a `Bb*`
183
+ * tag is unknown want three different answers — a public TYPE written
184
+ * as a tag, a typo, an app-owned component — and the editor is exactly
185
+ * where the reader acts on the answer without opening a terminal. See
186
+ * `unknownComponentFinding`.
187
+ */
188
+ unknownComponent: '{{message}} — {{hint}}',
155
189
  finding: '{{message}}',
156
190
  },
157
191
  },
@@ -262,7 +296,7 @@ const noUnknownAttributes = {
262
296
  context.report({
263
297
  node: node.startTag,
264
298
  messageId: 'unknownComponent',
265
- data: { component: name },
299
+ data: unknownComponentFinding(name, manifest),
266
300
  });
267
301
  return;
268
302
  }
@@ -919,6 +953,105 @@ const noNavigationAttrFallthrough = {
919
953
  },
920
954
  };
921
955
 
956
+ /**
957
+ * An `@event` on a `Bb*` component that names nothing the component emits.
958
+ *
959
+ * The check itself is not new — `createComponentChecker`'s `checkEvent` has
960
+ * driven it in `bitboss-ui check` and the MCP `validate` tool since Q10.12.
961
+ * What was new is that the EDITOR never ran it: every rule in this plugin read
962
+ * `bind` and `model` directives and skipped `on` entirely, so the one gate a
963
+ * consuming agent actually works against was the one gate blind to it.
964
+ *
965
+ * That is how `<WhatsAppConnectedCard @close>` shipped. The migration report
966
+ * that found it called the handler dead code; it was not. BbDialog's root is a
967
+ * native `<dialog>`, so the listener bound as a real DOM listener and fired.
968
+ * `close` is in `ELEMENT_DOM_EVENTS` for that reason, and `checkEvent` returns
969
+ * it as a `fallthrough` finding rather than an unknown one.
970
+ *
971
+ * Reported at `warn` in the recommended preset — alone among these rules, all
972
+ * of which are `error`. That asymmetry is the point. An unknown prop is a typo
973
+ * with one correct fix. An undeclared event listener may be a deliberate native
974
+ * listener on a root the author knows, so this rule flags the contract, not a
975
+ * mistake, and leaves `eslint-disable-next-line` as the answer for anyone who
976
+ * meant it.
977
+ * @type {import('eslint').Rule.RuleModule}
978
+ */
979
+ const noUnknownEvent = {
980
+ meta: {
981
+ type: 'problem',
982
+ docs: {
983
+ description:
984
+ "Flag an `@event` on a Bb* component that the component does not emit. Vue passes an undeclared listener through to the root element, so the markup compiles clean and `vue-tsc` stays quiet outside `strictTemplates`: a renamed event (beta.9's `resize` → `resize:column`) reads as working code, and an element-specific DOM event like `@close` binds to whatever the root happens to be — live on BbDialog, whose root is a native `<dialog>`, inert on BbAlert. A third case is neither: `@success` on BbButton binds the declared `onSuccess` PROP, so it runs, and the rule points at the documented `:on-success` spelling instead. Universal DOM events (`@click`, `@keydown`) are always allowed.",
985
+ recommended: true,
986
+ },
987
+ schema: [
988
+ {
989
+ type: 'object',
990
+ properties: {
991
+ manifestPath: { type: 'string' },
992
+ failClosed: { type: 'boolean' },
993
+ allowComponents: { type: 'array', items: { type: 'string' } },
994
+ allowEvents: { type: 'array', items: { type: 'string' } },
995
+ },
996
+ additionalProperties: false,
997
+ },
998
+ ],
999
+ messages: {
1000
+ // Two ids, not one: the fallthrough case is a different claim about
1001
+ // the code, and a consumer suppressing one should not lose the other.
1002
+ unknownEvent: '{{message}} — {{hint}}',
1003
+ fallthroughEvent: '{{message}}. {{hint}}',
1004
+ },
1005
+ },
1006
+
1007
+ create(context) {
1008
+ const options = context.options[0] ?? {};
1009
+ const manifest = getManifest(options.manifestPath, options);
1010
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
1011
+ const services = sourceCode.parserServices;
1012
+ if (!manifest || !services?.defineTemplateBodyVisitor) return {};
1013
+
1014
+ const allowComps = new Set(options.allowComponents ?? []);
1015
+ const allowEvents = new Set(options.allowEvents ?? []);
1016
+
1017
+ return services.defineTemplateBodyVisitor({
1018
+ VElement(node) {
1019
+ const name = toPascalTag(node.rawName);
1020
+ if (!BB_TAG.test(name) || allowComps.has(name)) return;
1021
+ const surface = manifest.get(name);
1022
+ if (!surface) return; // unknown component — no-unknown-attributes owns this
1023
+
1024
+ const checker = createComponentChecker(surface);
1025
+
1026
+ for (const attr of node.startTag.attributes) {
1027
+ if (!attr.directive || attr.key.name.name !== 'on') continue;
1028
+ const arg = attr.key.argument;
1029
+ // `v-on="obj"` (no arg) and `@[expr]` (dynamic) are unbounded.
1030
+ if (!arg || arg.type !== 'VIdentifier') continue;
1031
+
1032
+ // `vue-eslint-parser` splits `@click.stop` into an argument plus
1033
+ // modifiers, so the raw name here is already bare — unlike the
1034
+ // compiler AST `checkEvent`'s other caller walks, which keeps the
1035
+ // dots. `checkEvent` strips them either way; nothing to do here.
1036
+ const rawName = arg.rawName ?? arg.name;
1037
+ if (allowEvents.has(rawName)) continue;
1038
+
1039
+ const finding = checker.checkEvent(rawName);
1040
+ if (!finding) continue;
1041
+
1042
+ context.report({
1043
+ node: attr.key,
1044
+ messageId: finding.fallthrough
1045
+ ? 'fallthroughEvent'
1046
+ : 'unknownEvent',
1047
+ data: { message: finding.message, hint: finding.hint ?? '' },
1048
+ });
1049
+ }
1050
+ },
1051
+ });
1052
+ },
1053
+ };
1054
+
922
1055
  /*
923
1056
  * The three v3 controls whose `reverse` prop kept its NAME but inverted its
924
1057
  * MEANING between v2 and v3. Hardcoded on purpose: this is a fact about one
@@ -1062,6 +1195,151 @@ const noReverseFalse = {
1062
1195
  },
1063
1196
  };
1064
1197
 
1198
+ /*
1199
+ * Every `--bb-*` the installed package declares, keyed by resolved stylesheet
1200
+ * path. Same caching shape as `manifestCache`, including the miss: an absent
1201
+ * stylesheet caches an EMPTY set, and the rule reads that as "say nothing".
1202
+ *
1203
+ * No warning on a miss, unlike the manifest. `dist/styles.css` is a BUILD
1204
+ * artifact, absent in a source checkout that has not run `vite build`, so a
1205
+ * console warning here would fire constantly in this repo's own dev loop.
1206
+ * A missing manifest means every rule is silently inactive, which is worth
1207
+ * shouting about; a missing stylesheet disarms exactly one advisory rule.
1208
+ */
1209
+ const tokenCache = new Map();
1210
+ function getKnownTokens(stylesPath) {
1211
+ const resolved = stylesPath ?? join(HERE, '..', '..', 'dist', 'styles.css');
1212
+ if (!tokenCache.has(resolved))
1213
+ tokenCache.set(resolved, loadKnownTokens(resolved));
1214
+ return tokenCache.get(resolved);
1215
+ }
1216
+
1217
+ /**
1218
+ * A bare `var(--bb-…)` in a `<style>` block naming a token the installed
1219
+ * package does not declare.
1220
+ *
1221
+ * WHY THIS ONE MOVED TO ESLINT AND ITS TWO SIBLINGS DID NOT. `bitboss-ui
1222
+ * check` has carried three advisory hints — this, plus the hand-rolled avatar
1223
+ * and spinner detectors. A consumer's migration report (2026-09-09) made the
1224
+ * structural point that `check` is what nobody runs, while ESLint runs in the
1225
+ * editor and in pre-commit, so a check-only warning does not really exist.
1226
+ *
1227
+ * Only this one crossed over, on the owner's ruling, because only this one is
1228
+ * a FACT rather than a guess. An undeclared custom property is
1229
+ * invalid-at-computed-value-time: CSS discards the WHOLE declaration, so the
1230
+ * rule silently does nothing — there is no reading of `var(--bb-canvas)`, with
1231
+ * no such token and no fallback, that is correct code. The avatar and spinner
1232
+ * detectors are guesses by design (a round `<img>` is sometimes a logo), and a
1233
+ * rule that fires on correct code spends the trust that makes every other rule
1234
+ * here worth reading. They stay in `check`, where they cannot fail a build.
1235
+ *
1236
+ * A FALLBACK DISARMS IT, exactly as in the CLI hint: `var(--bb-canvas, #f8fafc)`
1237
+ * is a deliberate override hook with defined behaviour and nothing silently
1238
+ * disappears. Shared with the CLI through `findTokenUses`, so the two can never
1239
+ * disagree about what counts as a use.
1240
+ *
1241
+ * Reported at `warn`, joining `no-unknown-event`: the token may be one the
1242
+ * consumer means to define in their own stylesheet, which this rule cannot see.
1243
+ * @type {import('eslint').Rule.RuleModule}
1244
+ */
1245
+ const noUnknownToken = {
1246
+ meta: {
1247
+ type: 'problem',
1248
+ docs: {
1249
+ description:
1250
+ 'Flag a bare `var(--bb-…)` naming a token bitboss-ui does not declare. CSS treats an undefined custom property as invalid-at-computed-value-time and throws away the entire declaration, so the rule silently does nothing rather than falling back — an invented token name is invisible at runtime and in review. Supplying a fallback (`var(--bb-x, #fff)`) is defined behaviour and never reported. Reads the token list from the installed `dist/styles.css`, so it can never claim a token is invented when your version declares it; with no stylesheet to read, the rule stays silent.',
1251
+ recommended: true,
1252
+ },
1253
+ schema: [
1254
+ {
1255
+ type: 'object',
1256
+ properties: {
1257
+ stylesPath: { type: 'string' },
1258
+ allowTokens: { type: 'array', items: { type: 'string' } },
1259
+ },
1260
+ additionalProperties: false,
1261
+ },
1262
+ ],
1263
+ messages: {
1264
+ unknownToken:
1265
+ '`{{name}}` is not a bitboss-ui token. An undefined custom property invalidates the whole declaration, so this rule silently does nothing at runtime. Check the token list in the design-tokens guide, or supply a fallback (`var({{name}}, …)`) if you define it yourself.',
1266
+ },
1267
+ },
1268
+
1269
+ create(context) {
1270
+ const options = context.options[0] ?? {};
1271
+ const known = getKnownTokens(options.stylesPath);
1272
+ // No stylesheet to compare against — no data, no accusation.
1273
+ if (known.size === 0) return {};
1274
+
1275
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
1276
+ const services = sourceCode.parserServices;
1277
+ if (!services?.getDocumentFragment) return {};
1278
+ const allowTokens = new Set(options.allowTokens ?? []);
1279
+
1280
+ /**
1281
+ * Report every unknown token in one chunk of CSS text. `offset` is that
1282
+ * chunk's own start in the FILE, so a match position inside it maps
1283
+ * straight onto a source location.
1284
+ */
1285
+ const reportIn = (css, offset) => {
1286
+ for (const use of findTokenUses(css)) {
1287
+ if (known.has(use.name) || allowTokens.has(use.name)) continue;
1288
+ const start = offset + use.index;
1289
+ context.report({
1290
+ loc: {
1291
+ start: sourceCode.getLocFromIndex(start),
1292
+ // `var(` + the name, which is the part worth underlining.
1293
+ end: sourceCode.getLocFromIndex(start + use.name.length + 4),
1294
+ },
1295
+ messageId: 'unknownToken',
1296
+ data: { name: use.name },
1297
+ });
1298
+ }
1299
+ };
1300
+
1301
+ /*
1302
+ * The two places real CSS lives in an SFC: a `<style>` block, and a
1303
+ * static `style="…"` attribute. Both are scanned, because `check` reads
1304
+ * the whole file and a rule that saw only one of them would put the two
1305
+ * tools back into the disagreement this rule exists to end.
1306
+ *
1307
+ * A BOUND `:style` is deliberately NOT scanned: it is a JS expression,
1308
+ * the token name may be computed, and guessing at string literals inside
1309
+ * it is how a rule starts firing on correct code.
1310
+ */
1311
+ const walk = (node) => {
1312
+ if (!node || node.type !== 'VElement') return;
1313
+
1314
+ if (node.rawName === 'style') {
1315
+ for (const text of node.children ?? [])
1316
+ if (text.type === 'VText' && typeof text.value === 'string')
1317
+ reportIn(text.value, text.range[0]);
1318
+ }
1319
+
1320
+ for (const attr of node.startTag?.attributes ?? []) {
1321
+ if (attr.directive || attr.key?.name !== 'style') continue;
1322
+ const value = attr.value;
1323
+ if (!value || typeof value.value !== 'string') continue;
1324
+ // `range` spans the quotes; the value starts one character in.
1325
+ reportIn(value.value, value.range[0] + 1);
1326
+ }
1327
+
1328
+ for (const child of node.children ?? []) walk(child);
1329
+ };
1330
+
1331
+ return {
1332
+ // The template-body visitors every sibling rule uses cannot see a
1333
+ // `<style>` block; the document fragment is the whole SFC.
1334
+ 'Program:exit'() {
1335
+ const fragment = services.getDocumentFragment();
1336
+ if (!fragment) return;
1337
+ for (const child of fragment.children ?? []) walk(child);
1338
+ },
1339
+ };
1340
+ },
1341
+ };
1342
+
1065
1343
  const plugin = {
1066
1344
  meta: { name: 'bitboss-ui', version: '1.0.0' },
1067
1345
  rules: {
@@ -1071,6 +1349,8 @@ const plugin = {
1071
1349
  'no-unknown-slots': noUnknownSlots,
1072
1350
  'no-active-class-on-root-link': noActiveClassOnRootLink,
1073
1351
  'no-navigation-attr-fallthrough': noNavigationAttrFallthrough,
1352
+ 'no-unknown-event': noUnknownEvent,
1353
+ 'no-unknown-token': noUnknownToken,
1074
1354
  'no-reverse-false': noReverseFalse,
1075
1355
  },
1076
1356
  };
@@ -1130,6 +1410,20 @@ plugin.configs = {
1130
1410
  'bitboss-ui/no-unknown-slots': 'error',
1131
1411
  'bitboss-ui/no-active-class-on-root-link': 'error',
1132
1412
  'bitboss-ui/no-navigation-attr-fallthrough': 'error',
1413
+ // The only `warn` in this preset, deliberately. Every other rule
1414
+ // here describes markup with exactly one correct fix. An
1415
+ // undeclared event listener does not: it may be a considered
1416
+ // native listener on a root the author knows, and erroring on a
1417
+ // real JS event would make the plugin wrong about working code.
1418
+ // Warning states the contract and leaves the choice; the CLI
1419
+ // (`bitboss-ui check`) still reports it the same way it always
1420
+ // has, so nothing silently relaxes in CI.
1421
+ 'bitboss-ui/no-unknown-event': 'warn',
1422
+ // Also `warn`, for a different reason: the fact is certain (the
1423
+ // declaration is discarded), but the token may be one the consumer
1424
+ // intends to define in their own stylesheet, which this rule cannot
1425
+ // see. `var(--bb-x, fallback)` is the documented way to say so.
1426
+ 'bitboss-ui/no-unknown-token': 'warn',
1133
1427
  'bitboss-ui/no-reverse-false': 'error',
1134
1428
  },
1135
1429
  },
@@ -1144,6 +1438,8 @@ export {
1144
1438
  noUnknownSlots,
1145
1439
  noActiveClassOnRootLink,
1146
1440
  noNavigationAttrFallthrough,
1441
+ noUnknownEvent,
1442
+ noUnknownToken,
1147
1443
  noReverseFalse,
1148
1444
  resolveOptionalTsParser,
1149
1445
  };
@@ -26,6 +26,8 @@
26
26
  * Hinting about someone else's composable is not this file's job.
27
27
  */
28
28
 
29
+ import { existsSync, readFileSync } from 'node:fs';
30
+
29
31
  /**
30
32
  * @typedef {{ line: number, component: string, message: string }} Hint
31
33
  */
@@ -77,6 +79,53 @@ function detectSpinner(source) {
77
79
  };
78
80
  }
79
81
 
82
+ /**
83
+ * Every bare `var(--bb-…)` USE in the source, with the offset it starts at.
84
+ *
85
+ * Split out from `detectUnknownTokens` so the ESLint rule
86
+ * (`no-unknown-token`) reports each occurrence at its own location instead of
87
+ * lumping them onto one line the way the CLI hint does. One regex, one
88
+ * fallback rule, two consumers — the two can no longer disagree about what
89
+ * counts as a use.
90
+ *
91
+ * @param {string} source
92
+ * @returns {{ name: string, index: number }[]}
93
+ */
94
+ export function findTokenUses(source) {
95
+ return (
96
+ [...source.matchAll(/var\(\s*(--bb-[a-z0-9-]+)\s*([,)])/gi)]
97
+ // `,` opens a fallback; `)` closes the function with none.
98
+ .filter((m) => m[2] === ')')
99
+ .map((m) => ({ name: m[1].toLowerCase(), index: m.index }))
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Every `--bb-*` the INSTALLED package declares, read from the stylesheet the
105
+ * consumer actually ships rather than hardcoded, so this can never claim a
106
+ * token is invented when their version does declare it.
107
+ *
108
+ * A missing stylesheet yields an EMPTY set, and every caller treats that as
109
+ * "say nothing". No data, no accusation.
110
+ *
111
+ * @param {string} stylesPath path to the installed `dist/styles.css`
112
+ * @returns {Set<string>}
113
+ */
114
+ export function loadKnownTokens(stylesPath) {
115
+ if (!stylesPath || !existsSync(stylesPath)) return new Set();
116
+ try {
117
+ return new Set(
118
+ [
119
+ ...readFileSync(stylesPath, 'utf-8').matchAll(
120
+ /(--bb-[a-z0-9-]+)\s*:/gi
121
+ ),
122
+ ].map((m) => m[1].toLowerCase())
123
+ );
124
+ } catch {
125
+ return new Set();
126
+ }
127
+ }
128
+
80
129
  /**
81
130
  * A `var(--bb-…)` name the library does not declare. CSS treats an undefined
82
131
  * custom property as invalid-at-computed-value-time and throws away the WHOLE
@@ -99,12 +148,7 @@ function detectSpinner(source) {
99
148
  */
100
149
  function detectUnknownTokens(source, knownTokens) {
101
150
  if (!knownTokens || knownTokens.size === 0) return null;
102
- const used = new Set(
103
- [...source.matchAll(/var\(\s*(--bb-[a-z0-9-]+)\s*([,)])/gi)]
104
- // `,` opens a fallback; `)` closes the function with none.
105
- .filter((m) => m[2] === ')')
106
- .map((m) => m[1].toLowerCase())
107
- );
151
+ const used = new Set(findTokenUses(source).map((u) => u.name));
108
152
  const unknown = [...used].filter((name) => !knownTokens.has(name));
109
153
  if (unknown.length === 0) return null;
110
154
  return {
@@ -280,3 +280,45 @@ export const NATIVE_DOM_EVENTS = new Set([
280
280
  'transitionrun',
281
281
  'transitionstart',
282
282
  ]);
283
+
284
+ /**
285
+ * Real DOM events that fire on only a HANDFUL of element types, so they are
286
+ * deliberately NOT in `NATIVE_DOM_EVENTS` above.
287
+ *
288
+ * These are the reason the event gate can't simply say "unknown event". A
289
+ * listener named here is not a typo and it is not dead code: Vue binds it to
290
+ * whatever the component's root element happens to be, and if that root is the
291
+ * one element that dispatches it, the handler really does run.
292
+ *
293
+ * `<BbDialog @close>` is the case that named this set. BbDialog's root IS a
294
+ * native `<dialog>`, so `@close` binds as a genuine DOM listener and fires on
295
+ * `dialog.close()` — a task queued by the browser, unordered against the
296
+ * component's own `hidden` emit, and dispatched for native closes that never
297
+ * touched `v-model` (a `<form method="dialog">` submit) so the model goes
298
+ * stale. A consuming agent's migration report read it as "a dead `@close`
299
+ * handler shipped"; it was live, mistimed, and outside the component contract.
300
+ *
301
+ * Blanket-allowing them (moving them up into `NATIVE_DOM_EVENTS`) would hide
302
+ * that. Treating them as unknown would call working markup a typo. So they get
303
+ * their own diagnostic — see `checkEvent`'s `fallthrough` finding — and, in
304
+ * ESLint, their own severity.
305
+ */
306
+ export const ELEMENT_DOM_EVENTS = new Set([
307
+ // <dialog>
308
+ 'cancel',
309
+ 'close',
310
+ // <details>, <dialog>, popover
311
+ 'beforetoggle',
312
+ // <form>
313
+ 'formdata',
314
+ // media elements
315
+ 'canplay',
316
+ 'ended',
317
+ 'loadeddata',
318
+ 'loadedmetadata',
319
+ 'pause',
320
+ 'play',
321
+ 'playing',
322
+ 'timeupdate',
323
+ 'volumechange',
324
+ ]);