bitboss-ui 3.0.0-beta.0 → 3.0.0-beta.2

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 (35) hide show
  1. package/bin/bitboss-ui.mjs +133 -12
  2. package/dist/ai/changelog.json +1 -1
  3. package/dist/ai/components.json +2 -2
  4. package/dist/ai/guides/ai-router.md +2 -2
  5. package/dist/ai/guides/design-tokens.md +46 -6
  6. package/dist/ai/guides/installation-and-plugin-setup.md +71 -5
  7. package/dist/ai/guides/migration/components/bb-alert.md +37 -0
  8. package/dist/ai/guides/migration/components/bb-avatar.md +47 -8
  9. package/dist/ai/guides/migration/components/bb-badge.md +23 -1
  10. package/dist/ai/guides/migration/components/bb-button.md +64 -0
  11. package/dist/ai/guides/migration/components/bb-checkbox-group.md +55 -1
  12. package/dist/ai/guides/migration/components/bb-date-picker-input.md +9 -2
  13. package/dist/ai/guides/migration/components/bb-dialog.md +121 -11
  14. package/dist/ai/guides/migration/components/bb-icon.md +42 -0
  15. package/dist/ai/guides/migration/components/bb-offcanvas.md +35 -1
  16. package/dist/ai/guides/migration/components/bb-rating.md +52 -1
  17. package/dist/ai/guides/migration/components/bb-select.md +48 -0
  18. package/dist/ai/guides/migration/components/bb-table.md +156 -10
  19. package/dist/ai/guides/migration/components/bb-tabs.md +79 -1
  20. package/dist/ai/guides/migration/components/bb-text-input.md +23 -1
  21. package/dist/ai/guides/migration/components/bb-toast.md +44 -10
  22. package/dist/ai/guides/migration/components/use-confirm.md +48 -13
  23. package/dist/ai/guides/migration/v2-to-v3.md +626 -108
  24. package/dist/ai/index.md +9 -9
  25. package/dist/ai/source/BbDialog.md +0 -3
  26. package/dist/ai/source/BbDropdown.md +24 -1
  27. package/dist/ai/source/BbDropdownGroup.md +24 -1
  28. package/dist/index.d.ts +2 -1
  29. package/dist/llms-full.txt +1814 -367
  30. package/dist/llms-medium.txt +82 -16
  31. package/dist/llms.txt +11 -11
  32. package/dist/styles.css +1 -1
  33. package/llms.txt +12 -12
  34. package/package.json +2 -1
  35. package/scripts/lib/validate-bb-markup.mjs +105 -17
package/llms.txt CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  > Vue 3 component library by BitBoss: forms, selects, layout, overlays, and more.
4
4
 
5
- Note: the `dist/` paths below resolve inside the installed npm package (`node_modules/bitboss-ui/`) and, when this file is fetched over HTTP, against https://cdn.jsdelivr.net/npm/bitboss-ui@alpha. `dist/` is generated and not committed, so when browsing the repository the sources are `ai/` (guides, recipes, composables) and `src/components/<Name>/<Name>.guide.md` (per-component guides).
5
+ Note: the `dist/` paths below resolve inside the installed npm package (`node_modules/bitboss-ui/`) and, when this file is fetched over HTTP, against https://cdn.jsdelivr.net/npm/bitboss-ui@beta. `dist/` is generated and not committed, so when browsing the repository the sources are `ai/` (guides, recipes, composables) and `src/components/<Name>/<Name>.guide.md` (per-component guides).
6
6
 
7
7
  ## AI Knowledge Base
8
8
 
9
- - [Core knowledge base](https://cdn.jsdelivr.net/npm/bitboss-ui@alpha/dist/llms-medium.txt): **Fetch this if you can only fetch one file** — hard rules, setup, component picker, design language, and the full component catalogue in ~70 KB.
10
- - [Complete knowledge base](https://cdn.jsdelivr.net/npm/bitboss-ui@alpha/dist/llms-full.txt): Everything concatenated (~2 MB). Bulk ingestion only — too large to prompt with.
9
+ - [Core knowledge base](https://cdn.jsdelivr.net/npm/bitboss-ui@beta/dist/llms-medium.txt): **Fetch this if you can only fetch one file** — hard rules, setup, component picker, design language, and the full component catalogue in ~70 KB.
10
+ - [Complete knowledge base](https://cdn.jsdelivr.net/npm/bitboss-ui@beta/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
13
  - [Component manifest](dist/ai/components.json): Machine-readable API surface of every component, for programmatic validation.
@@ -27,40 +27,40 @@ Note: the `dist/` paths below resolve inside the installed npm package (`node_mo
27
27
  - [Inertia Helpers](dist/ai/guides/inertia-helpers.md): App-side toolkit for Laravel + Inertia apps — typed page props, lazy props, URL-driven filters, paginated/selection page contexts, promise forms, and navigation guards that pair with bitboss-ui pages.
28
28
  - [Migration v2 → v3](dist/ai/guides/migration/v2-to-v3.md): Complete upgrade guide from bitboss-ui 2.x to 3.0 — plugin-based setup, token overhaul, variant registries, global behavioral changes, and the silent-failure audit playbook.
29
29
  - [Migration v2→v3: BbAlert](dist/ai/guides/migration/components/bb-alert.md): showClose inverted to hideClose, theme replaced by typed variant.
30
- - [Migration v2→v3: BbAvatar](dist/ai/guides/migration/components/bb-avatar.md): The color prop is removed — the initials fallback is always primary.
30
+ - [Migration v2→v3: BbAvatar](dist/ai/guides/migration/components/bb-avatar.md): The color prop is removed — retheme the fallback through the --bg-color / --text-color locals.
31
31
  - [Migration v2→v3: BbBadge (and where your v2 badges went)](dist/ai/guides/migration/components/bb-badge.md): The v2 BbBadge (floating count/dot) is now BbIndicator; the v3 BbBadge is a new inline pill that also replaces BbChip.
32
32
  - [Migration v2→v3: BbBreadcrumbs](dist/ai/guides/migration/components/bb-breadcrumbs.md): Item keys must be strings, the text divider prop is gone, per-item slot names are normalized.
33
33
  - [Migration v2→v3: BbButton](dist/ai/guides/migration/components/bb-button.md): Async click handlers now auto-track loading (behavior flip), built-in tooltip props removed, variant locked to the registry.
34
34
  - [Migration v2→v3: BbCheckbox / BbRadio / BbSwitch](dist/ai/guides/migration/components/bb-checkbox.md): reverse polarity inverted (explicit values flip, default layout unchanged), checked narrowed to boolean, color removed, BbRadio's name no longer required.
35
- - [Migration v2→v3: BbCheckboxGroup / BbRadioGroup / BbSwitchGroup](dist/ai/guides/migration/components/bb-checkbox-group.md): labelPosition renamed legendPosition, per-item disabled is now inert (use selectable), color and the option affix slots removed, BbRadioGroup's name no longer required.
35
+ - [Migration v2→v3: BbCheckboxGroup / BbRadioGroup / BbSwitchGroup](dist/ai/guides/migration/components/bb-checkbox-group.md): labelPosition renamed legendPosition, per-item disabled is now inert (use selectable), color and the option affix slots removed, BbRadioGroup's name no longer required, and the shared bb-cr-container CSS block split into one per group.
36
36
  - [Migration v2→v3: BbChip (removed)](dist/ai/guides/migration/components/bb-chip.md): BbChip is gone — BbBadge with clearable is the replacement; non-clearable chips were badges all along.
37
37
  - [Migration v2→v3: BbCollapsible](dist/ai/guides/migration/components/bb-collapsible.md): The tag prop is removed; the root is always a div.
38
38
  - [Migration v2→v3: BbColorInput](dist/ai/guides/migration/components/bb-color-input.md): picker renamed to eyeDropper.
39
39
  - [Migration v2→v3: BbColorPalette](dist/ai/guides/migration/components/bb-color-palette.md): flip inverted to disableFlip, showArrow removed, picker renamed eyeDropper, update:open dropped, model updates never emit null.
40
40
  - [Migration v2→v3: BbDatePickerInput](dist/ai/guides/migration/components/bb-date-picker-input.md): allowWriting inverted to disableWriting and hidePopover renamed to disableCalendar, both with a changed token set; the width prop is removed; adaptive mobile sheet by default; non-floating output carries the local offset instead of UTC `Z` (new `utc` prop restores v2 output).
41
- - [Migration v2→v3: BbDialog](dist/ai/guides/migration/components/bb-dialog.md): showClose inverted to hideClose, several presentation props removed, default widths changed, adaptive bottom sheet on mobile by default.
41
+ - [Migration v2→v3: BbDialog](dist/ai/guides/migration/components/bb-dialog.md): showClose inverted to hideClose, several presentation props and the #close/#description slots removed, default widths changed, adaptive bottom sheet on mobile by default, --bb-dialog-* tokens replaced by --bb-panel-p plus locals.
42
42
  - [Migration v2→v3: BbDropdown](dist/ai/guides/migration/components/bb-dropdown.md): Items now expect text as the label and can carry variants/submenus; arrowPadding removed; adaptive mobile sheet on by default.
43
43
  - [Migration v2→v3: BbDropdownButton](dist/ai/guides/migration/components/bb-dropdown-button.md): autoLoading renamed to disableAutoLoading, disabledWhileLoading and theme removed.
44
44
  - [Migration v2→v3: BbDropzone](dist/ai/guides/migration/components/bb-dropzone.md): modelValue is typed, accept no longer throws and takes bare tokens, and the hidden native-submit input only renders when name is set.
45
45
  - [Migration v2→v3: BbIcon](dist/ai/guides/migration/components/bb-icon.md): The required prop is renamed type→icon, color is removed, and the xxl size key becomes 2xl.
46
46
  - [Migration v2→v3: BbIntersection](dist/ai/guides/migration/components/bb-intersection.md): Removed with no replacement — it wrapped IntersectionObserver and carried no styling; use @vueuse/core's useIntersectionObserver.
47
- - [Migration v2→v3: BbOffCanvas](dist/ai/guides/migration/components/bb-offcanvas.md): direction→side; showClose→hideClose; size defaults change; adaptive drawer configs unify on offCanvasProps.
47
+ - [Migration v2→v3: BbOffCanvas](dist/ai/guides/migration/components/bb-offcanvas.md): direction→side; showClose→hideClose; description and the #close/#description slots removed; size defaults change; adaptive drawer configs unify on offCanvasProps.
48
48
  - [Migration v2→v3: BbPagination](dist/ai/guides/migration/components/bb-pagination.md): querykey renamed to queryKey; the loading prop is removed.
49
49
  - [Migration v2→v3: BbPopover](dist/ai/guides/migration/components/bb-popover.md): Built-in close button, restoreFocus, theme, and the arrow are gone; close affordances move into your content.
50
- - [Migration v2→v3: BbRating](dist/ai/guides/migration/components/bb-rating.md): color removed; name no longer required.
50
+ - [Migration v2→v3: BbRating](dist/ai/guides/migration/components/bb-rating.md): color removed; name no longer required; the star state modifier renamed --highlighted to --filled/--empty; --bb-rating-size demoted to a local --size; input-position finally works.
51
51
  - [Migration v2→v3: BbSelect](dist/ai/guides/migration/components/bb-select.md): allowWriting inverted to disableWriting, filterBy narrowed to string[], prefill value renamed, chevron/options slots removed, and two behavior changes (second click, mount-time coherence).
52
52
  - [Migration v2→v3: BbSelectPopover](dist/ai/guides/migration/components/bb-select-popover.md): flip and allowWriting inverted, arrow props removed, filterBy narrowed, options slots replaced by header/footer; shares BbSelect's behavior changes.
53
53
  - [Migration v2→v3: BbSlider](dist/ai/guides/migration/components/bb-slider.md): thumbTranslate inverted to disableThumbTranslate; string model values and color removed.
54
54
  - [Migration v2→v3: BbSmoothHeight](dist/ai/guides/migration/components/bb-smooth-height.md): The `tag` prop is narrowed to `div | span`, and the inner measuring element now mirrors it instead of always being a `<div>`.
55
55
  - [Migration v2→v3: BbSpinner](dist/ai/guides/migration/components/bb-spinner.md): The color prop is removed; spinners follow currentColor.
56
- - [Migration v2→v3: BbTable](dist/ai/guides/migration/components/bb-table.md): allowSelectAll inverted to disableSelectAll; loading no longer blanks populated tables and now inerts rows; #loading is first-load-only; #no-data fills a full-width table cell instead of replacing the row.
57
- - [Migration v2→v3: BbTabs (was BbTab)](dist/ai/guides/migration/components/bb-tabs.md): Component renamed; querykey→queryKey; animations default on; label slots renamed and all dynamic names/URL slugs normalized.
56
+ - [Migration v2→v3: BbTable](dist/ai/guides/migration/components/bb-table.md): allowSelectAll inverted to disableSelectAll; loading no longer blanks populated tables and now inerts rows; #loading is first-load-only; #no-data fills a full-width table cell instead of replacing the row; columns gained sortable + rowClass, which collide with same-named consumer fields.
57
+ - [Migration v2→v3: BbTabs (was BbTab)](dist/ai/guides/migration/components/bb-tabs.md): Component renamed; querykey→queryKey; animations default on; label slots renamed and all dynamic names/URL slugs normalized; the entire .bb-tab BEM tree renamed to .bb-tabs / .bb-tabs-list / .bb-tabs-panes.
58
58
  - [Migration v2→v3: BbTag](dist/ai/guides/migration/components/bb-tag.md): modelValue is strictly string[] now, multiple is removed, and duplicates are rejected case-insensitively by default.
59
- - [Migration v2→v3: BbTextInput](dist/ai/guides/migration/components/bb-text-input.md): The type prop narrows to the text-like input types — number/date values must move to their dedicated components.
59
+ - [Migration v2→v3: BbTextInput](dist/ai/guides/migration/components/bb-text-input.md): The type prop narrows to the text-like input types — number/date values must move to their dedicated components; a bare `floating` attribute was never a prop, the spelling is label-mode="floating".
60
60
  - [Migration v2→v3: BbToast and toast()](dist/ai/guides/migration/components/bb-toast.md): Host placement becomes position with a corner-based value set; toast() options rewritten (variant, duration, actions, portal).
61
61
  - [Migration v2→v3: BbTooltip](dist/ai/guides/migration/components/bb-tooltip.md): timeout renamed to delay; theme replaced by typed variant; hideArrow is now a declared prop; block removed; modelValue is new.
62
62
  - [Migration v2→v3: BbTree](dist/ai/guides/migration/components/bb-tree.md): Item model rewritten — payload moves under meta, open becomes expanded, identifier becomes itemValue, expansion is a v-model.
63
- - [Migration v2→v3: confirm() / useConfirm](dist/ai/guides/migration/components/use-confirm.md): yesText/onYes/noText/onNo collapse into yes/no configs, actions becomes an array-or-false, labels are localized, footer buttons default md.
63
+ - [Migration v2→v3: confirm() / useConfirm](dist/ai/guides/migration/components/use-confirm.md): yesText/onYes/noText/onNo collapse into yes/no configs, actions becomes an array-or-false, theme→variant, timeout→duration, autoClose flips to default-true, labels are localized, footer buttons default md.
64
64
  - [Migration v2→v3: useBroadcastChannelInstance](dist/ai/guides/migration/components/use-broadcast-channel-instance.md): Removed — cross-tab messaging is not a UI concern; use useBroadcastChannel from @vueuse/core.
65
65
  - [Page Shell](dist/ai/guides/page-shell.md): App-side page layout — topbar, content, and a right details sidebar driven by a stack-based context (push a record to peek it, pop to go back). The implementation ships in the per-platform Layout Scaffold recipe; this guide covers when and how to use it.
66
66
  - [Validated Forms (vee-validate)](dist/ai/guides/validated-forms.md): The `bitboss-ui/validated` entrypoint — form controls pre-wired to vee-validate. Global rules give an effort-free `rules="required|email"` string syntax; BbForm hosts the form; requiredAsterisk marks required fields.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitboss-ui",
3
- "version": "3.0.0-beta.0",
3
+ "version": "3.0.0-beta.2",
4
4
  "description": "Vue 3 component library by BitBoss: forms, selects, layout, overlays, and more.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -39,6 +39,7 @@
39
39
  "check:doc-drift": "node scripts/check-doc-drift.mjs",
40
40
  "check:default-docs": "node --experimental-strip-types scripts/check-default-docs.ts",
41
41
  "check:doc-ts": "node scripts/check-doc-ts.mjs",
42
+ "check:dist-tag": "node scripts/check-dist-tag.mjs",
42
43
  "check:doc-links": "node scripts/check-doc-links.mjs",
43
44
  "check:docs": "node --experimental-strip-types scripts/generate-component-docs.ts --strict",
44
45
  "check:fixture-types": "node scripts/check-fixture-types.mjs",
@@ -44,6 +44,11 @@ const PROP_DIRECTIVE = 7;
44
44
  export const toCamel = (attr) =>
45
45
  attr.replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
46
46
 
47
+ /** `hideLabel` → `hide-label`, `fullscreen` → `fullscreen`. The spelling a
48
+ * template author actually has to type. */
49
+ export const toKebab = (name) =>
50
+ name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
51
+
47
52
  /**
48
53
  * Components with `slotKey()`-normalized, per-item/column/tab/day DYNAMIC
49
54
  * named slots — the set of valid names is runtime data (row column keys,
@@ -124,6 +129,14 @@ function editDistance(a, b) {
124
129
  * Nearest candidate name to `key` within a length-scaled edit-distance budget,
125
130
  * or null when nothing is close. Powers "did you mean" hints on unknown props
126
131
  * (`hideLabl` → `hideLabel`) without matching unrelated names.
132
+ *
133
+ * The self-match guard compares the RAW names, not the distance. It used to be
134
+ * `dist > 0`, which also silenced the one near-miss the kebab/camel boundary
135
+ * actually produces: a multi-word kebab spelling of a single-word prop.
136
+ * `full-screen` camel-cases to `fullScreen`, whose lowercased distance to the
137
+ * real `fullscreen` is 0 — so the hint was suppressed exactly where it was
138
+ * needed most (30 inert `full-screen` sites in one migrated app). Same trap on
139
+ * `read-only`/`readonly`, `auto-focus`/`autofocus`, `max-length`/`maxlength`.
127
140
  * @param {string} key camelCase attribute the author wrote
128
141
  * @param {Iterable<string>} candidates camelCase valid names
129
142
  */
@@ -131,10 +144,11 @@ export function nearestName(key, candidates) {
131
144
  let best = null;
132
145
  let bestDist = Infinity;
133
146
  for (const cand of candidates) {
147
+ if (cand === key) continue;
134
148
  // Short names collide by chance (`to`/`id`); demand a tighter match.
135
149
  const budget = Math.min(key.length, cand.length) < 6 ? 1 : 2;
136
150
  const dist = editDistance(key.toLowerCase(), cand.toLowerCase());
137
- if (dist > 0 && dist <= budget && dist < bestDist) {
151
+ if (dist <= budget && dist < bestDist) {
138
152
  best = cand;
139
153
  bestDist = dist;
140
154
  }
@@ -272,7 +286,7 @@ const INTENT_ALIASES = {
272
286
  * @typedef {{ name: string, new?: string | null, since?: string, migration?: string, silent?: boolean }} RemovedEntry
273
287
  * @typedef {{ name: string, props: Array<{ name: string }>, models: Array<{ name: string }>, referenceOnly?: boolean, removed?: RemovedEntry[], renamed?: Array<{ from: string, to: string }> }} ComponentSurface
274
288
  * @typedef {Map<string, ComponentSurface>} ManifestMap
275
- * @typedef {{ component: string, attr: string, kind: 'unknown-prop' | 'unknown-model' | 'removed-prop' | 'unknown-slot', message: string, hint?: string }} Finding
289
+ * @typedef {{ component: string, attr: string, line?: number, kind: 'unknown-prop' | 'unknown-model' | 'removed-prop' | 'unknown-slot', message: string, hint?: string }} Finding
276
290
  */
277
291
 
278
292
  /**
@@ -352,18 +366,31 @@ function parseSnippet(code) {
352
366
  /**
353
367
  * @param {string} code
354
368
  * @param {ManifestMap} manifest
369
+ * @param {{ allowComponents?: Iterable<string> }} [options]
370
+ * `allowComponents` — tag names the caller owns. The `Bb` prefix is a naming
371
+ * convention, not proof of provenance: an app that ships its own
372
+ * `<BbRichEditor>` is not writing invalid library markup, it just picked a
373
+ * colliding name. Listed tags are skipped entirely (no unknown-component
374
+ * finding, no prop findings), their children still walked. Mirrors the
375
+ * ESLint rules' `allowComponents` option so both gates configure alike.
355
376
  * @returns {{ findings: Finding[] }}
356
377
  */
357
- export function validateVueSnippet(code, manifest) {
378
+ export function validateVueSnippet(code, manifest, options = {}) {
358
379
  /** @type {Finding[]} */
359
380
  const findings = [];
360
381
  const ast = parseSnippet(code);
361
382
  if (!ast) return { findings };
362
383
 
384
+ const allowComponents = new Set(options.allowComponents ?? []);
385
+
363
386
  /** @param {{ type: number, tag?: string, props?: unknown[], children?: unknown[] }} node */
364
387
  const walk = (node) => {
365
388
  if (!node || typeof node !== 'object') return;
366
- if (node.type === NODE_ELEMENT && BB_TAG_NAME.test(node.tag)) {
389
+ if (
390
+ node.type === NODE_ELEMENT &&
391
+ BB_TAG_NAME.test(node.tag) &&
392
+ !allowComponents.has(node.tag)
393
+ ) {
367
394
  validateElement(node, manifest, findings);
368
395
  }
369
396
  for (const child of node.children ?? []) walk(child);
@@ -541,6 +568,35 @@ export function createComponentChecker(surface, options = {}) {
541
568
  };
542
569
  }
543
570
 
571
+ /*
572
+ * The prop IS on this component — the author just spelled it
573
+ * with the wrong word boundaries. Ranked above the
574
+ * owners-elsewhere and fuzzy branches because a same-component
575
+ * hit is the answer; being told `fullscreen` "belongs to some
576
+ * other component" would be actively wrong.
577
+ *
578
+ * The only shape that reaches here is a name whose camel form
579
+ * differs from a real prop by case alone, which is what a kebab
580
+ * spelling of a one-word prop (`full-screen` → `fullScreen` vs
581
+ * `fullscreen`) produces. A genuinely multi-word prop written in
582
+ * kebab (`hide-label`) camel-cases to the real name and never
583
+ * gets here at all, so this can never nag correct markup.
584
+ */
585
+ const lowerKey = key.toLowerCase();
586
+ const caseOnly = [...propKeys].find(
587
+ (cand) => cand.toLowerCase() === lowerKey
588
+ );
589
+ if (caseOnly) {
590
+ const correct = toKebab(caseOnly);
591
+ return {
592
+ attr: attrName,
593
+ kind: 'unknown-prop',
594
+ message: `prop \`${attrName}\` is not in ${surface.name}'s API — did you mean \`${correct}\`?`,
595
+ hint: `Attribute names are camel-cased before matching (\`${attrName}\` → \`${key}\`), so only the exact spelling \`${correct}\` binds — anything else falls through to the DOM and does nothing.`,
596
+ to: correct,
597
+ };
598
+ }
599
+
544
600
  // The prop is real — on OTHER components. Exact hits elsewhere
545
601
  // beat a fuzzy neighbour here: someone who wrote `hide-close`
546
602
  // on a BbTooltip did not typo, they generalised a prop that
@@ -1038,6 +1094,18 @@ function compilerItemsMayBeAsync(node) {
1038
1094
  */
1039
1095
  function validateElement(node, manifest, findings) {
1040
1096
  const name = node.tag;
1097
+ /*
1098
+ * CHK-LINE: every finding carries the 1-based source line, so an agent (or
1099
+ * a human with an editor) can jump straight to it instead of re-parsing the
1100
+ * file to find which of three identical `full-screen` findings is which.
1101
+ * The numbers are exact, not approximate: `parseSnippet` wraps the code as
1102
+ * `<template>${code}</template>` with NO added newline, so AST lines map
1103
+ * 1:1 onto the file. (Columns do NOT — line 1 is shifted by the 10-char
1104
+ * wrapper — which is why only the line is emitted.)
1105
+ */
1106
+ const tagLine = node.loc?.start?.line;
1107
+ /** The line an attribute was written on, falling back to the tag's. */
1108
+ const propLine = (prop) => prop?.loc?.start?.line ?? tagLine;
1041
1109
  const surface = manifest.get(name);
1042
1110
  if (!surface) {
1043
1111
  // Unknown Bb* tag — surface as a finding so recipes can't invent
@@ -1045,9 +1113,12 @@ function validateElement(node, manifest, findings) {
1045
1113
  findings.push({
1046
1114
  component: name,
1047
1115
  attr: '',
1116
+ line: tagLine,
1048
1117
  kind: 'unknown-prop',
1049
1118
  message: `component \`${name}\` is not in the bitboss-ui manifest`,
1050
- hint: 'Check the spelling against dist/ai/index.md Consumer API.',
1119
+ hint:
1120
+ 'Check the spelling against dist/ai/index.md Consumer API. If your app owns this component, allowlist it: ' +
1121
+ `\`bitboss-ui.allowComponents: ["${name}"]\` in package.json, or \`--allow-component ${name}\`.`,
1051
1122
  });
1052
1123
  return;
1053
1124
  }
@@ -1062,7 +1133,8 @@ function validateElement(node, manifest, findings) {
1062
1133
  // Dynamic argument (`v-model:[name]`) can't be checked statically.
1063
1134
  if (prop.arg && !prop.arg.isStatic) continue;
1064
1135
  const finding = checker.checkModel(prop.arg?.content ?? 'modelValue');
1065
- if (finding) findings.push({ component: name, ...finding });
1136
+ if (finding)
1137
+ findings.push({ component: name, ...finding, line: propLine(prop) });
1066
1138
  continue;
1067
1139
  }
1068
1140
  if (prop.name !== 'bind') continue; // @events, v-if/for/show/slot, …
@@ -1070,14 +1142,16 @@ function validateElement(node, manifest, findings) {
1070
1142
  const finding =
1071
1143
  checker.checkAttr(prop.arg.content) ??
1072
1144
  checkNavAttrFallthrough(manifest, name, prop.arg.content);
1073
- if (finding) findings.push({ component: name, ...finding });
1145
+ if (finding)
1146
+ findings.push({ component: name, ...finding, line: propLine(prop) });
1074
1147
  } else if (prop.type === PROP_ATTRIBUTE) {
1075
1148
  if (!VALID_ATTR_NAME.test(prop.name)) continue; // recovered garbage
1076
1149
  const attrName = prop.name.split('.')[0];
1077
1150
  const finding =
1078
1151
  checker.checkAttr(attrName) ??
1079
1152
  checkNavAttrFallthrough(manifest, name, attrName);
1080
- if (finding) findings.push({ component: name, ...finding });
1153
+ if (finding)
1154
+ findings.push({ component: name, ...finding, line: propLine(prop) });
1081
1155
  }
1082
1156
  }
1083
1157
 
@@ -1099,6 +1173,7 @@ function validateElement(node, manifest, findings) {
1099
1173
  findings.push({
1100
1174
  component: name,
1101
1175
  attr: '(children)',
1176
+ line: tagLine,
1102
1177
  kind: 'no-default-slot',
1103
1178
  message:
1104
1179
  `${name} has no default slot — children passed to it render nothing, ` +
@@ -1118,7 +1193,8 @@ function validateElement(node, manifest, findings) {
1118
1193
  // (unbounded) can't be — or don't need to be — checked.
1119
1194
  if (!arg || !arg.isStatic) continue;
1120
1195
  const finding = checker.checkSlot(arg.content);
1121
- if (finding) findings.push({ component: name, ...finding });
1196
+ if (finding)
1197
+ findings.push({ component: name, ...finding, line: propLine(prop) });
1122
1198
  }
1123
1199
  }
1124
1200
 
@@ -1130,7 +1206,7 @@ function validateElement(node, manifest, findings) {
1130
1206
  hasSlot: (slotName) => compilerHasSlot(node, slotName),
1131
1207
  itemsMayBeAsync: compilerItemsMayBeAsync(node),
1132
1208
  })) {
1133
- findings.push({ component: name, attr: '', ...finding });
1209
+ findings.push({ component: name, attr: '', ...finding, line: tagLine });
1134
1210
  }
1135
1211
 
1136
1212
  // SWP-015: these two were ESLint-only, so `bitboss-ui check` and the MCP
@@ -1143,22 +1219,27 @@ function validateElement(node, manifest, findings) {
1143
1219
  staticProp: (attr) => compilerStaticProp(node, attr),
1144
1220
  };
1145
1221
  for (const finding of checkTableSlotProps(name, slotCtx)) {
1146
- findings.push({ component: name, ...finding });
1222
+ findings.push({ component: name, ...finding, line: tagLine });
1147
1223
  }
1148
1224
  for (const finding of checkRootLinkActiveClass(name, slotCtx)) {
1149
- findings.push({ component: name, ...finding });
1225
+ findings.push({ component: name, ...finding, line: tagLine });
1150
1226
  }
1151
1227
  }
1152
1228
 
1153
1229
  /**
1154
1230
  * Extract fenced code blocks from markdown.
1231
+ *
1232
+ * `bodyLine` is the 1-based line of the fence's FIRST body line in the whole
1233
+ * document — the offset that turns a fence-relative finding line into a line a
1234
+ * reader can jump to. Indent stripping never changes line counts, so it is
1235
+ * exact.
1155
1236
  * @param {string} md
1156
1237
  * @param {{ lang?: string }} [opts]
1157
- * @returns {Array<{ lang: string, code: string, info: string, index: number }>}
1238
+ * @returns {Array<{ lang: string, code: string, info: string, index: number, bodyLine: number }>}
1158
1239
  */
1159
1240
  export function extractFences(md, opts = {}) {
1160
1241
  const want = opts.lang?.toLowerCase();
1161
- /** @type {Array<{ lang: string, code: string, info: string, index: number }>} */
1242
+ /** @type {Array<{ lang: string, code: string, info: string, index: number, bodyLine: number }>} */
1162
1243
  const out = [];
1163
1244
  FENCE_REGEX.lastIndex = 0;
1164
1245
  let match;
@@ -1176,7 +1257,9 @@ export function extractFences(md, opts = {}) {
1176
1257
  indent.length > 0
1177
1258
  ? rawCode.replace(new RegExp(`^ {1,${indent.length}}`, 'gm'), '')
1178
1259
  : rawCode;
1179
- out.push({ lang, code, info, index });
1260
+ // The opening fence is on `openLine`; the body starts on the next line.
1261
+ const openLine = md.slice(0, match.index).split('\n').length;
1262
+ out.push({ lang, code, info, index, bodyLine: openLine + 1 });
1180
1263
  index += 1;
1181
1264
  }
1182
1265
  return out;
@@ -1198,19 +1281,24 @@ export function blankFences(md) {
1198
1281
  /**
1199
1282
  * @param {string} md
1200
1283
  * @param {ManifestMap} manifest
1284
+ * @param {{ allowComponents?: Iterable<string> }} [options] see `validateVueSnippet`
1201
1285
  * @returns {{ findings: Array<Finding & { fenceIndex: number, fenceInfo: string }> }}
1202
1286
  */
1203
- export function validateMarkdown(md, manifest) {
1287
+ export function validateMarkdown(md, manifest, options = {}) {
1204
1288
  /** @type {Array<Finding & { fenceIndex: number, fenceInfo: string }>} */
1205
1289
  const findings = [];
1206
1290
  for (const fence of extractFences(md, { lang: 'vue' })) {
1207
1291
  const { findings: fenceFindings } = validateVueSnippet(
1208
1292
  fence.code,
1209
- manifest
1293
+ manifest,
1294
+ options
1210
1295
  );
1211
1296
  for (const f of fenceFindings) {
1212
1297
  findings.push({
1213
1298
  ...f,
1299
+ // Fence-relative → document-absolute, so the printed line is the
1300
+ // line of the .md file itself.
1301
+ line: f.line == null ? undefined : fence.bodyLine + f.line - 1,
1214
1302
  fenceIndex: fence.index,
1215
1303
  fenceInfo: fence.info,
1216
1304
  });