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
@@ -22,15 +22,23 @@
22
22
  * `bitboss-ui.allowComponents`) exempts an app-owned component
23
23
  * whose name happens to start with `Bb`.
24
24
  *
25
+ * `--manifest <path>` judges against ANOTHER version's
26
+ * components.json instead of the installed one — answers "what
27
+ * breaks if I upgrade" BEFORE upgrading. A path, not a version:
28
+ * no network call inside a CI validator.
29
+ *
25
30
  * SCOPE: `check` knows the INSTALLED v3 API and nothing about v2.
26
31
  * "was removed from X" is a recorded v2→v3 break; "is not in X's
27
32
  * API" only says the prop is not a v3 prop of that component —
28
33
  * it may never have been one anywhere. `printFindingLegend` says
29
34
  * so in the output, because two migrating fleets each spent real
30
35
  * time discovering it the hard way.
36
+ * changelog Breaking changes between two releases — `--since <version>`
37
+ * answers "what breaks if I bump", without MCP. Reads the
38
+ * installed `dist/ai/changelog.json`.
31
39
  * mcp Start a stdio MCP server exposing the dist/ai knowledge base
32
- * (search_components, get_component, list_recipes, get_recipe,
33
- * validate) to MCP-capable agent harnesses.
40
+ * (search_components, get_component, list_changes, list_recipes,
41
+ * get_recipe, validate) to MCP-capable agent harnesses.
34
42
  *
35
43
  * Plain ESM JavaScript on purpose: shipped as-is in the package, no build.
36
44
  */
@@ -44,13 +52,18 @@ import {
44
52
  statSync,
45
53
  writeFileSync,
46
54
  } from 'node:fs';
47
- import { findHandRollHints } from '../scripts/lib/hand-roll-hints.mjs';
48
- import { dirname, isAbsolute, join, relative } from 'node:path';
55
+ import {
56
+ findHandRollHints,
57
+ loadKnownTokens,
58
+ } from '../scripts/lib/hand-roll-hints.mjs';
59
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
49
60
  import { fileURLToPath } from 'node:url';
50
61
  import { parseArgs } from 'node:util';
51
62
  import {
52
63
  ensureMcpConfigs,
53
64
  missingMcpPeers,
65
+ MCP_BIN_PATH,
66
+ MCP_PEERS,
54
67
  } from '../scripts/lib/mcp-config.mjs';
55
68
  import {
56
69
  loadManifest,
@@ -88,7 +101,7 @@ function coreRules(version) {
88
101
  5. **Icons**: discover names first; never invent them (\`${KNOWLEDGE_BASE}/guides/icons-policy.md\`).
89
102
  6. **Item-driven components**: declarative \`items\` / \`item-text\` / \`item-value\`, \`dependencies\`, \`enforce-coherence\` — no manual watchers for coherence.
90
103
  7. **Whole pages**: one recipe under \`${KNOWLEDGE_BASE}/recipes/{vue|nuxt|inertia}/\` (see the router table).
91
- 8. **CSS near components**: \`${KNOWLEDGE_BASE}/guides/design-language.md\` \`--bb-*\` tokens only; one primary action per view region.
104
+ 8. **CSS near components**: \`${KNOWLEDGE_BASE}/guides/design-language.md\`. Two token tiers, and reaching for the wrong one is why apps end up re-skinning by class: \`--bb-*\` is the small GLOBAL theme tier (brand knobs, ~90 names, no numeric ramp — there is no \`--bb-primary-800\`), while each component declares its own **unprefixed** \`--*\` tokens for its look (\`.bb-table { --cell-h: 32px }\`). A missing \`--bb-*\` name is not a gap in the library — it means the knob you want is a component token. Set it on the element that DECLARES it (grep \`${KNOWLEDGE_BASE}/source/<ComponentName>.md\`); a value set further out is masked, not inherited. Never override \`.bb-*__*\` internals, and never target \`.bb-base-*\` / \`.bb-common-*\` at all. **A Tailwind utility on a \`Bb*\` component that does nothing** is not a library bug: Tailwind v4 emits layered CSS and unlayered CSS outranks it, whatever the specificity. The fix is in the BUILD plugin, not in CSS — \`cssLayers: TAILWIND_V4_LAYERS\` (imported from \`bitboss-ui/vite\`) in \`vite.config.*\` / \`nuxt.config.*\`; see \`${KNOWLEDGE_BASE}/guides/installation-and-plugin-setup.md\` § Cascade layers. One primary action per view region.
92
105
  9. **Variants are yours to add**: the shipped \`variant\` names are a floor, not a ceiling. When the design needs a colour the library doesn't ship (soft status badges, a brand button tone), **register the name** in the build plugin's \`badgeVariants\`/\`buttonVariants\`/\`alertVariants\`/… and **style \`.bb-badge--<name>\`** (etc.) in a project stylesheet loaded after \`bitboss-ui/styles.css\`. Do both halves — a registered name with no CSS ships an unstyled pill, and CSS with no registration fails \`vue-tsc\`. Never write an unregistered name, and never downgrade the design to a neutral shipped variant to avoid registering. Canonical palette + snippet: \`${KNOWLEDGE_BASE}/guides/design-language.md\` § Getting soft semantic badges.
93
106
  10. **After writing bitboss-ui markup**, run \`npx bitboss-ui check\` — validates \`Bb*\` usage against the installed \`components.json\` and catches invented/removed props before review.
94
107
 
@@ -519,6 +532,29 @@ function printFindingLegend(findings) {
519
532
  ' "restoring" anything: the second case needs deleting,\n' +
520
533
  ' not porting.'
521
534
  );
535
+ // A type written as a tag is its own category: the name IS ours, so neither
536
+ // "check the spelling" nor the allowlist applies, and a reader who skims the
537
+ // message alone would reach for one of them.
538
+ if (
539
+ findings.some((f) => / is a type export, not a component$/.test(f.message))
540
+ )
541
+ rows.push(
542
+ ' "is a type export" a real export, but a TYPE — it types a prop, it does not\n' +
543
+ ' render. Import it (`import type { … }`) and pass the\n' +
544
+ ' value; never allowlist it, which would only silence\n' +
545
+ ' every other check on that tag.'
546
+ );
547
+ if (findings.some((f) => f.fallthrough))
548
+ rows.push(
549
+ ' ⚠ (vs ✗) markup that is not a typo and may work today: an\n' +
550
+ ' undeclared listener Vue binds to the root ELEMENT,\n' +
551
+ ' so whether it fires depends on what that root is —\n' +
552
+ " an internal detail, on the component's schedule or\n" +
553
+ " the element's. `<BbDialog @close>` runs (that root is\n" +
554
+ ' a native <dialog>); `<BbAlert @close>` does not.\n' +
555
+ ' Still reported, because neither is the contract. In\n' +
556
+ ' ESLint this is `no-unknown-event`, a warning.'
557
+ );
522
558
  if (rows.length === 0) return;
523
559
  console.error('\nReading these findings:');
524
560
  for (const row of rows) console.error(row);
@@ -532,13 +568,156 @@ function printFindingLegend(findings) {
532
568
  * v-models, and `<template #slot>` names) in `.vue` and `.md` files against
533
569
  * the installed components.json manifest.
534
570
  */
535
- function checkCommand(globs, { json: jsonMode, allowEmpty, allowComponents }) {
571
+ /**
572
+ * The version a components.json describes. `loadManifest` returns the indexed
573
+ * component map, not the envelope, so read the field straight off the file.
574
+ */
575
+ /**
576
+ * `changelog [--since <v>] [--until <v>] [--json]` — what breaks between two
577
+ * releases, for anyone not driving the MCP server (a human at a terminal, CI,
578
+ * or an agent whose harness has no MCP). Same data and same range rules as the
579
+ * `list_changes` tool; this is the transport-free path to it.
580
+ */
581
+ function changelogCommand({ since, until, json: jsonMode }) {
582
+ const path = join(PACKAGE_ROOT, 'dist', 'ai', 'changelog.json');
583
+ if (!existsSync(path)) {
584
+ console.error(
585
+ `[bitboss-ui] changelog: not found at ${path} — reinstall bitboss-ui (or run \`npm run build\` when working in the library repo).`
586
+ );
587
+ process.exit(1);
588
+ }
589
+ const doc = JSON.parse(readFileSync(path, 'utf-8'));
590
+ const releases = doc.releases ?? [];
591
+
592
+ for (const [label, value] of [
593
+ ['--since', since],
594
+ ['--until', until],
595
+ ]) {
596
+ if (value != null && !releases.includes(value)) {
597
+ console.error(
598
+ `[bitboss-ui] changelog: unknown ${label} version '${value}'.\n` +
599
+ `Known releases: ${releases.join(', ')}`
600
+ );
601
+ process.exit(1);
602
+ }
603
+ }
604
+
605
+ const ranged = since != null || until != null;
606
+ const rows = (doc.breaking ?? []).filter((entry) => {
607
+ if (!ranged) return true;
608
+ const at = entry.firstReleasedIn;
609
+ if (at == null) return until == null;
610
+ if (since != null && compareReleaseVersions(at, since) <= 0) return false;
611
+ if (until != null && compareReleaseVersions(at, until) > 0) return false;
612
+ return true;
613
+ });
614
+
615
+ if (jsonMode) {
616
+ console.log(
617
+ JSON.stringify(
618
+ {
619
+ version: doc.version,
620
+ since: since ?? null,
621
+ until: until ?? doc.version,
622
+ count: rows.length,
623
+ breaking: rows,
624
+ guide: doc.guide,
625
+ },
626
+ null,
627
+ 2
628
+ )
629
+ );
630
+ return;
631
+ }
632
+
633
+ const range = ranged
634
+ ? `${since ?? 'the start of v3'} → ${until ?? doc.version}`
635
+ : `the whole v2 → v3 upgrade`;
636
+ if (rows.length === 0) {
637
+ // "Nothing breaks" and "I could not tell you" must not look alike.
638
+ console.log(
639
+ `[bitboss-ui] changelog: no BREAKING changes in ${range}.\n` +
640
+ ' Features and fixes are deliberately not tracked here, so this\n' +
641
+ ' means nothing breaks — not that nothing changed.'
642
+ );
643
+ return;
644
+ }
645
+ console.log(
646
+ `[bitboss-ui] changelog: ${rows.length} breaking change(s), ${range}\n`
647
+ );
648
+ for (const entry of rows) {
649
+ const at = entry.firstReleasedIn ?? 'unreleased';
650
+ if (entry.kind === 'rename') {
651
+ console.log(
652
+ ` [${at}] ${entry.components.join(', ')}: \`${entry.from}\` → \`${entry.to}\``
653
+ );
654
+ } else {
655
+ console.log(` [${at}] ${entry.description}`);
656
+ }
657
+ if (entry.guide) console.log(` ${entry.guide}`);
658
+ }
659
+ console.log(`\nFull migration guide: ${doc.guide}`);
660
+ }
661
+
662
+ /** Numeric, prerelease-aware ordering; the twin of the MCP server's copy. */
663
+ function compareReleaseVersions(a, b) {
664
+ const parse = (v) => {
665
+ const [core, pre = ''] = String(v).split('-');
666
+ const nums = core.split('.').map((n) => Number.parseInt(n, 10) || 0);
667
+ const [tag = '', n = ''] = pre.split('.');
668
+ return { nums, tag, n: Number.parseInt(n, 10) || 0 };
669
+ };
670
+ const x = parse(a);
671
+ const y = parse(b);
672
+ for (let i = 0; i < 3; i += 1) {
673
+ if ((x.nums[i] ?? 0) !== (y.nums[i] ?? 0))
674
+ return (x.nums[i] ?? 0) - (y.nums[i] ?? 0);
675
+ }
676
+ if (!x.tag && y.tag) return 1;
677
+ if (x.tag && !y.tag) return -1;
678
+ if (x.tag !== y.tag) return x.tag < y.tag ? -1 : 1;
679
+ return x.n - y.n;
680
+ }
681
+
682
+ function manifestVersionOf(manifestPath) {
683
+ try {
684
+ return JSON.parse(readFileSync(manifestPath, 'utf-8')).version ?? 'unknown';
685
+ } catch {
686
+ return 'unknown';
687
+ }
688
+ }
689
+
690
+ function checkCommand(
691
+ globs,
692
+ { json: jsonMode, allowEmpty, allowComponents, manifest: manifestOverride }
693
+ ) {
536
694
  const projectRoot = process.cwd();
537
695
 
538
- const manifestPath = join(PACKAGE_ROOT, 'dist', 'ai', 'components.json');
696
+ /*
697
+ * `--manifest` answers the question `check` could not: "which of my files
698
+ * break in the version I am about to move to?" (reported 2026-09-09). By
699
+ * default the manifest is the INSTALLED one, so the only way to see an
700
+ * upgrade's damage was to perform the upgrade first — exactly backwards for
701
+ * the one moment the answer is worth having.
702
+ *
703
+ * Deliberately a PATH, not a version: fetching a version would put a network
704
+ * call inside a validator that consumers run in CI, with a registry outage
705
+ * as a new failure mode. Getting the file is one documented command
706
+ * (`npm pack bitboss-ui@<version>`, or the public CDN copy), and pointing at
707
+ * it keeps this offline and reproducible.
708
+ */
709
+ const installedManifest = join(PACKAGE_ROOT, 'dist', 'ai', 'components.json');
710
+ const manifestPath = manifestOverride
711
+ ? resolve(projectRoot, manifestOverride)
712
+ : installedManifest;
539
713
  if (!existsSync(manifestPath)) {
540
714
  console.error(
541
- `[bitboss-ui] Manifest not found at ${manifestPath} — reinstall bitboss-ui (or run \`npm run build\` when working in the library repo).`
715
+ manifestOverride
716
+ ? `[bitboss-ui] --manifest: no file at ${manifestPath}.\n` +
717
+ 'Point it at a components.json from the version you are moving to:\n' +
718
+ ' npm pack bitboss-ui@<version> && tar -xzf bitboss-ui-<version>.tgz\n' +
719
+ ' npx bitboss-ui check --manifest package/dist/ai/components.json'
720
+ : `[bitboss-ui] Manifest not found at ${manifestPath} — reinstall bitboss-ui (or run \`npm run build\` when working in the library repo).`
542
721
  );
543
722
  process.exit(1);
544
723
  }
@@ -551,16 +730,14 @@ function checkCommand(globs, { json: jsonMode, allowEmpty, allowComponents }) {
551
730
  * Absent stylesheet → an empty set → the unknown-token hint stays silent,
552
731
  * which is the right failure: no data, no accusation.
553
732
  */
554
- const stylesPath = join(PACKAGE_ROOT, 'dist', 'styles.css');
555
- const knownTokens = existsSync(stylesPath)
556
- ? new Set(
557
- [
558
- ...readFileSync(stylesPath, 'utf-8').matchAll(
559
- /(--bb-[a-z0-9-]+)\s*:/gi
560
- ),
561
- ].map((m) => m[1].toLowerCase())
562
- )
563
- : new Set();
733
+ // Under `--manifest`, take the sheet from beside THAT manifest
734
+ // (`<pkg>/dist/ai/components.json` `<pkg>/dist/styles.css`). Reading the
735
+ // installed sheet while judging props against another version would mix two
736
+ // releases in one verdict — the exact confusion this flag exists to end.
737
+ const stylesPath = manifestOverride
738
+ ? join(dirname(dirname(manifestPath)), 'styles.css')
739
+ : join(PACKAGE_ROOT, 'dist', 'styles.css');
740
+ const knownTokens = loadKnownTokens(stylesPath);
564
741
  const files = resolveCheckFiles(projectRoot, globs);
565
742
 
566
743
  // CHK-ALLOWCOMPONENTS: `Bb` is a naming convention, not proof of ownership.
@@ -613,9 +790,37 @@ function checkCommand(globs, { json: jsonMode, allowEmpty, allowComponents }) {
613
790
  );
614
791
  };
615
792
 
793
+ /*
794
+ * Which API the verdict is against. Silent when it is the installed one —
795
+ * that is the assumption, and a banner on every run trains people to skip
796
+ * it. Loud under `--manifest`, because "12 findings" means something
797
+ * completely different when it describes a version you have not installed
798
+ * yet, and a scrollback with no marker is indistinguishable from a normal
799
+ * run.
800
+ */
801
+ const judgedAgainst = manifestOverride
802
+ ? manifestVersionOf(manifestPath)
803
+ : packageVersion();
804
+ if (manifestOverride && !jsonMode) {
805
+ console.log(
806
+ `[bitboss-ui] check: judging against bitboss-ui ${judgedAgainst} (--manifest), NOT the installed ${packageVersion()}.\n` +
807
+ ' Findings describe the version you are moving TO.'
808
+ );
809
+ }
810
+
616
811
  if (jsonMode) {
617
812
  console.log(
618
- JSON.stringify({ findings, hints, files: files.length }, null, 2)
813
+ JSON.stringify(
814
+ {
815
+ findings,
816
+ hints,
817
+ files: files.length,
818
+ manifestVersion: judgedAgainst,
819
+ installedVersion: packageVersion(),
820
+ },
821
+ null,
822
+ 2
823
+ )
619
824
  );
620
825
  if (findings.length > 0) process.exit(1);
621
826
  if (files.length === 0 && globs.length > 0 && !allowEmpty) process.exit(1);
@@ -668,8 +873,13 @@ function checkCommand(globs, { json: jsonMode, allowEmpty, allowComponents }) {
668
873
  // were indistinguishable, so every automated fix had to re-parse the
669
874
  // file to place them. The line comes straight off the template AST.
670
875
  const at = finding.line != null ? `line ${finding.line}: ` : '';
876
+ // A `fallthrough` finding is not a typo and may be doing something
877
+ // today — a native listener reaching the root element. It still counts,
878
+ // but marking it `✗` alongside a genuinely broken prop is what let one
879
+ // migration report read a live `<BbDialog @close>` as dead code.
880
+ const mark = finding.fallthrough ? '⚠' : '✗';
671
881
  console.error(
672
- ` ${at}${where ? `[${where.trim()}] ` : ''}${finding.message}`
882
+ ` ${mark} ${at}${where ? `[${where.trim()}] ` : ''}${finding.message}`
673
883
  );
674
884
  if (finding.hint) console.error(` hint: ${finding.hint}`);
675
885
  }
@@ -697,7 +907,7 @@ Commands:
697
907
  VS Code/Copilot (.vscode/mcp.json), and Windsurf (~/.codeium
698
908
  global) — merges, never overwrites other servers.
699
909
  check [glob…] [--json] [--allow-empty] [--allow-component <Name>]
700
- [--no-hints]
910
+ [--no-hints] [--manifest <path>]
701
911
  Validate \`Bb*\` markup in .vue/.md files against the
702
912
  installed components.json manifest (unknown/removed props,
703
913
  bad v-models, unknown \`<template #slot>\` names,
@@ -710,6 +920,17 @@ Commands:
710
920
  nothing (a typo'd CI path validates zero files silently) —
711
921
  pass \`--allow-empty\` when an empty match is expected.
712
922
 
923
+ \`--manifest <path>\` judges against another version's
924
+ components.json instead of the installed one — the answer
925
+ to "which of my files break if I upgrade", BEFORE you
926
+ upgrade. The banner names both versions so a findings list
927
+ is never mistaken for one about your current install.
928
+ npm pack bitboss-ui@3.0.0-beta.21
929
+ tar -xzf bitboss-ui-3.0.0-beta.21.tgz
930
+ npx bitboss-ui check --manifest package/dist/ai/components.json
931
+ A path, not a version, on purpose: fetching one would put
932
+ a registry call inside a validator people run in CI.
933
+
713
934
  \`--allow-component <Name>\` (repeatable) exempts a
714
935
  component your APP owns whose name starts with \`Bb\` —
715
936
  the prefix is a convention, not proof of provenance.
@@ -730,6 +951,16 @@ Commands:
730
951
  \`no-default-slot\`, \`inert-nav-attr\`,
731
952
  \`missing-partner-prop\`, \`partner-prop\`,
732
953
  \`validated-only-prop\`.
954
+ changelog [--since <version>] [--until <version>] [--json]
955
+ Breaking changes between two releases — "what breaks if I
956
+ bump". \`--since\` is the version you are ON, and is
957
+ exclusive. With no range, every break in the v2 → v3
958
+ upgrade. Only BREAKING changes are tracked, so an empty
959
+ result means nothing breaks, not that nothing changed.
960
+ npx bitboss-ui changelog --since 3.0.0-beta.20
961
+ Pair it with \`check --manifest\` to turn the list into the
962
+ files in YOUR app that break.
963
+
733
964
  mcp Start a stdio MCP server exposing the dist/ai knowledge
734
965
  base (search_components, get_component, list_recipes,
735
966
  get_recipe, validate) to MCP-capable agent harnesses.
@@ -776,6 +1007,7 @@ switch (command) {
776
1007
  json: { type: 'boolean' },
777
1008
  'allow-empty': { type: 'boolean' },
778
1009
  'allow-component': { type: 'string', multiple: true },
1010
+ manifest: { type: 'string' },
779
1011
  // Accepted and ignored here — read straight off argv inside
780
1012
  // checkCommand, but parseArgs is strict, so it must be declared.
781
1013
  'no-hints': { type: 'boolean' },
@@ -786,6 +1018,20 @@ switch (command) {
786
1018
  json: values.json ?? false,
787
1019
  allowEmpty: values['allow-empty'] ?? false,
788
1020
  allowComponents: values['allow-component'] ?? [],
1021
+ manifest: values.manifest,
1022
+ });
1023
+ break;
1024
+ }
1025
+ case 'changelog': {
1026
+ const { values } = parseCommandArgs(command, commandArgs, {
1027
+ since: { type: 'string' },
1028
+ until: { type: 'string' },
1029
+ json: { type: 'boolean' },
1030
+ });
1031
+ changelogCommand({
1032
+ since: values.since,
1033
+ until: values.until,
1034
+ json: values.json ?? false,
789
1035
  });
790
1036
  break;
791
1037
  }
@@ -802,9 +1048,34 @@ switch (command) {
802
1048
  ({ main } = await import('./bitboss-ui-mcp.mjs'));
803
1049
  } catch (error) {
804
1050
  if (error?.code !== 'ERR_MODULE_NOT_FOUND') throw error;
1051
+ // Running from an npx cache is a DIFFERENT failure wearing the same
1052
+ // error: the peers resolve relative to the package we were launched
1053
+ // from, so a cached copy can never see the project's node_modules no
1054
+ // matter what is installed there. Telling that user to install peers
1055
+ // they already have is what sent a consumer down a dead end
1056
+ // (2026-09-09). `ai-init --mcp` now writes the local binary instead
1057
+ // of `npx`, so the fix is to rewrite the config.
1058
+ const fromNpxCache = /[/\\]_npx[/\\]/.test(
1059
+ fileURLToPath(import.meta.url)
1060
+ );
1061
+ const projectHasPeers = missingMcpPeers(process.cwd()).length === 0;
1062
+ if (fromNpxCache && projectHasPeers) {
1063
+ console.error(
1064
+ 'bitboss-ui mcp was launched through `npx`, which runs it from the npx cache —\n' +
1065
+ 'its optional peers resolve there, not in your project, so the server cannot start\n' +
1066
+ `even though this project has them installed (${MCP_PEERS.join(', ')}).\n\n` +
1067
+ 'Fix the harness config to invoke the installed binary:\n\n' +
1068
+ ' npx bitboss-ui ai-init --mcp\n\n' +
1069
+ `which writes \`node ${MCP_BIN_PATH} mcp\`. To run it right now:\n\n` +
1070
+ ` node ${MCP_BIN_PATH} mcp\n\n` +
1071
+ `Resolution failed with: ${error.message}`
1072
+ );
1073
+ process.exitCode = 1;
1074
+ break;
1075
+ }
805
1076
  console.error(
806
1077
  'bitboss-ui mcp needs two optional peer dependencies that are not installed:\n' +
807
- '\n npm i -D @modelcontextprotocol/sdk zod\n\n' +
1078
+ `\n npm i -D ${MCP_PEERS.join(' ')}\n\n` +
808
1079
  'They are optional because they are ~12 MB and only this subcommand uses them.\n' +
809
1080
  `Resolution failed with: ${error.message}`
810
1081
  );
@@ -47,13 +47,13 @@ collapsed state and strips padding/borders you put on the panel itself.
47
47
  ```vue
48
48
  <template>
49
49
  <div
50
- class="max-w-md overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
50
+ class="max-w-md overflow-hidden rounded-(--bb-radius) border border-(--bb-border)"
51
51
  >
52
52
  <BbAccordion
53
53
  v-for="(faq, index) in faqs"
54
54
  :key="faq.question"
55
55
  v-model="faq.open"
56
- :class="index ? 'border-t border-[color:var(--bb-border)]' : ''"
56
+ :class="index ? 'border-t border-(--bb-border)' : ''"
57
57
  >
58
58
  <template #header="{ value }">
59
59
  <div
@@ -61,7 +61,7 @@ collapsed state and strips padding/borders you put on the panel itself.
61
61
  >
62
62
  <span>{{ faq.question }}</span>
63
63
  <span
64
- class="shrink-0 text-[color:var(--bb-text-muted)] transition-transform duration-200"
64
+ class="shrink-0 text-(--bb-text-muted) transition-transform duration-200"
65
65
  :class="value ? 'rotate-180' : ''"
66
66
  >
67
67
  <BbIcon icon="lucide:chevron-down" size="sm" />
@@ -69,7 +69,7 @@ collapsed state and strips padding/borders you put on the panel itself.
69
69
  </div>
70
70
  </template>
71
71
 
72
- <p class="px-3 pb-2.5 text-sm text-[color:var(--bb-text-muted)]">
72
+ <p class="px-3 pb-2.5 text-sm text-(--bb-text-muted)">
73
73
  {{ faq.answer }}
74
74
  </p>
75
75
  </BbAccordion>
@@ -121,13 +121,13 @@ on the row, badge included, toggles the panel.
121
121
  ```vue
122
122
  <template>
123
123
  <div
124
- class="max-w-md overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
124
+ class="max-w-md overflow-hidden rounded-(--bb-radius) border border-(--bb-border)"
125
125
  >
126
126
  <BbAccordion
127
127
  v-for="(endpoint, index) in endpoints"
128
128
  :key="`${endpoint.method}:${endpoint.path}`"
129
129
  v-model="endpoint.open"
130
- :class="index ? 'border-t border-[color:var(--bb-border)]' : ''"
130
+ :class="index ? 'border-t border-(--bb-border)' : ''"
131
131
  >
132
132
  <template #header="{ value }">
133
133
  <div class="flex w-full items-center gap-2 px-3 py-2 text-left">
@@ -136,7 +136,7 @@ on the row, badge included, toggles the panel.
136
136
  }}</BbBadge>
137
137
  <code class="truncate font-mono text-xs">{{ endpoint.path }}</code>
138
138
  <span
139
- class="ml-auto shrink-0 text-[color:var(--bb-text-muted)] transition-transform duration-200"
139
+ class="ml-auto shrink-0 text-(--bb-text-muted) transition-transform duration-200"
140
140
  :class="value ? 'rotate-180' : ''"
141
141
  >
142
142
  <BbIcon icon="lucide:chevron-down" size="sm" />
@@ -145,7 +145,7 @@ on the row, badge included, toggles the panel.
145
145
  </template>
146
146
 
147
147
  <div class="flex flex-col gap-1.5 px-3 pb-2.5">
148
- <p class="text-xs text-[color:var(--bb-text-muted)]">
148
+ <p class="text-xs text-(--bb-text-muted)">
149
149
  {{ endpoint.summary }}
150
150
  </p>
151
151
  <dl class="flex flex-col gap-1 text-xs">
@@ -155,7 +155,7 @@ on the row, badge included, toggles the panel.
155
155
  class="flex items-baseline gap-2"
156
156
  >
157
157
  <dt class="shrink-0 font-mono">{{ param.name }}</dt>
158
- <dd class="text-[color:var(--bb-text-muted)]">
158
+ <dd class="text-(--bb-text-muted)">
159
159
  {{ param.type }} · {{ param.description }}
160
160
  </dd>
161
161
  </div>
@@ -263,7 +263,7 @@ Both slots — `header` and the default (body) slot — receive the same scope,
263
263
  ```vue
264
264
  <template>
265
265
  <div
266
- class="max-w-md overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
266
+ class="max-w-md overflow-hidden rounded-(--bb-radius) border border-(--bb-border)"
267
267
  >
268
268
  <BbAccordion v-model="notes">
269
269
  <template #header="{ value }">
@@ -272,7 +272,7 @@ Both slots — `header` and the default (body) slot — receive the same scope,
272
272
  >
273
273
  <span>Release notes</span>
274
274
  <span
275
- class="shrink-0 text-[color:var(--bb-text-muted)] transition-transform duration-200"
275
+ class="shrink-0 text-(--bb-text-muted) transition-transform duration-200"
276
276
  :class="value ? 'rotate-180' : ''"
277
277
  >
278
278
  <BbIcon icon="lucide:chevron-down" size="sm" />
@@ -281,7 +281,7 @@ Both slots — `header` and the default (body) slot — receive the same scope,
281
281
  </template>
282
282
  <template #default="{ toggle }">
283
283
  <div class="flex flex-col items-start gap-1.5 px-3 pb-2.5">
284
- <p class="text-sm text-[color:var(--bb-text-muted)]">
284
+ <p class="text-sm text-(--bb-text-muted)">
285
285
  Dropdown keyboard navigation, smoother select popovers, and a new
286
286
  color picker.
287
287
  </p>
@@ -317,12 +317,12 @@ to `null`). Prefer a stable key over an index when the list can reorder.
317
317
  ```vue
318
318
  <template>
319
319
  <div
320
- class="max-w-md overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
320
+ class="max-w-md overflow-hidden rounded-(--bb-radius) border border-(--bb-border)"
321
321
  >
322
322
  <BbAccordion
323
323
  v-for="(plan, index) in plans"
324
324
  :key="plan.name"
325
- :class="index ? 'border-t border-[color:var(--bb-border)]' : ''"
325
+ :class="index ? 'border-t border-(--bb-border)' : ''"
326
326
  :model-value="openIndex === index"
327
327
  @update:model-value="openIndex = $event ? index : null"
328
328
  >
@@ -331,9 +331,7 @@ to `null`). Prefer a stable key over an index when the list can reorder.
331
331
  class="flex w-full items-center justify-between gap-2 px-3 py-2 text-left text-sm"
332
332
  >
333
333
  <span class="font-medium">{{ plan.name }}</span>
334
- <span
335
- class="flex items-center gap-1.5 text-[color:var(--bb-text-muted)]"
336
- >
334
+ <span class="flex items-center gap-1.5 text-(--bb-text-muted)">
337
335
  {{ plan.price }}
338
336
  <span
339
337
  class="shrink-0 transition-transform duration-200"
@@ -346,7 +344,7 @@ to `null`). Prefer a stable key over an index when the list can reorder.
346
344
  </template>
347
345
 
348
346
  <ul
349
- class="flex flex-col gap-1 px-3 pb-2.5 text-sm text-[color:var(--bb-text-muted)]"
347
+ class="flex flex-col gap-1 px-3 pb-2.5 text-sm text-(--bb-text-muted)"
350
348
  >
351
349
  <li v-for="feature in plan.features" :key="feature">{{ feature }}</li>
352
350
  </ul>
@@ -394,7 +392,7 @@ padding on the nested body content.
394
392
  ```vue
395
393
  <template>
396
394
  <div
397
- class="max-w-md overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
395
+ class="max-w-md overflow-hidden rounded-(--bb-radius) border border-(--bb-border)"
398
396
  >
399
397
  <BbAccordion v-model="notifications">
400
398
  <template #header="{ value }">
@@ -403,7 +401,7 @@ padding on the nested body content.
403
401
  >
404
402
  <span>Notifications</span>
405
403
  <span
406
- class="shrink-0 text-[color:var(--bb-text-muted)] transition-transform duration-200"
404
+ class="shrink-0 text-(--bb-text-muted) transition-transform duration-200"
407
405
  :class="value ? 'rotate-180' : ''"
408
406
  >
409
407
  <BbIcon icon="lucide:chevron-down" size="sm" />
@@ -416,7 +414,7 @@ padding on the nested body content.
416
414
  <BbAccordion v-model="advanced">
417
415
  <template #header="{ value }">
418
416
  <div
419
- class="flex w-full items-center justify-between gap-2 py-1 text-left text-sm text-[color:var(--bb-text-muted)]"
417
+ class="flex w-full items-center justify-between gap-2 py-1 text-left text-sm text-(--bb-text-muted)"
420
418
  >
421
419
  <span>Advanced</span>
422
420
  <span
@@ -427,7 +425,7 @@ padding on the nested body content.
427
425
  </span>
428
426
  </div>
429
427
  </template>
430
- <div class="pb-1 pt-1">
428
+ <div class="pt-1 pb-1">
431
429
  <BbCheckbox v-model="quiet" label="Quiet hours (22:00–08:00)" />
432
430
  </div>
433
431
  </BbAccordion>
@@ -470,7 +468,7 @@ get a near-instant transition automatically.
470
468
  <template #header>
471
469
  <span class="px-3 py-2 text-sm font-medium">Shipping details</span>
472
470
  </template>
473
- <p class="px-3 pb-3 text-sm text-[color:var(--bb-text-muted)]">
471
+ <p class="px-3 pb-3 text-sm text-(--bb-text-muted)">
474
472
  Rendered up front so its text is present even while collapsed.
475
473
  </p>
476
474
  </BbAccordion>
@@ -129,12 +129,10 @@ Bind `v-model` when the visibility matters to you:
129
129
 
130
130
  ```vue
131
131
  <template>
132
- <div
133
- class="flex max-w-md flex-col gap-3 rounded-[var(--bb-radius)] border p-3"
134
- >
132
+ <div class="flex max-w-md flex-col gap-3 rounded-(--bb-radius) border p-3">
135
133
  <div class="flex flex-col gap-0.5">
136
134
  <p class="text-sm font-medium">Account</p>
137
- <p class="text-xs text-[color:var(--bb-text-muted)]">
135
+ <p class="text-xs text-(--bb-text-muted)">
138
136
  Confirm your email to invite teammates and manage billing.
139
137
  </p>
140
138
  </div>
@@ -233,12 +231,10 @@ genuine brand art; everything semantic stays a provider icon.
233
231
 
234
232
  ```vue
235
233
  <template>
236
- <div
237
- class="flex max-w-md flex-col gap-3 rounded-[var(--bb-radius)] border p-3"
238
- >
234
+ <div class="flex max-w-md flex-col gap-3 rounded-(--bb-radius) border p-3">
239
235
  <div class="flex flex-col gap-0.5">
240
236
  <p class="text-sm font-medium">Integrations</p>
241
- <p class="text-xs text-[color:var(--bb-text-muted)]">
237
+ <p class="text-xs text-(--bb-text-muted)">
242
238
  Connected payment providers for Northwind Billing.
243
239
  </p>
244
240
  </div>
@@ -279,12 +275,10 @@ list of problems changes.
279
275
 
280
276
  ```vue
281
277
  <template>
282
- <div
283
- class="flex max-w-md flex-col gap-3 rounded-[var(--bb-radius)] border p-3"
284
- >
278
+ <div class="flex max-w-md flex-col gap-3 rounded-(--bb-radius) border p-3">
285
279
  <div class="flex flex-col gap-0.5">
286
280
  <p class="text-sm font-medium">Publish checklist</p>
287
- <p class="text-xs text-[color:var(--bb-text-muted)]">
281
+ <p class="text-xs text-(--bb-text-muted)">
288
282
  Northwind Q3 release notes — blocked until both items are done.
289
283
  </p>
290
284
  </div>