Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions .sync/PORTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,16 @@ material. Reproduce its *intent* in b24ui by editing files under `src/` only.
re-inspected here** — treat "upstream has no such parameter" as an inference
from b24ui's own history rather than a verified fact, and re-check before
acting on it. What is certain is that replaying upstream's four-parameter
signature wholesale drops behaviour shipped in v2.8.0 while **nothing goes
red**: `useTokenSearch` appears nowhere under `test/`. (Reverting the signature
alone does fail `typecheck`, since `CommandPalette.vue` passes five arguments;
it is a port that replays both files that goes quiet.) Several §2 rules here
name a guarding spec and some — locales, playground-manifest mirroring — do
not; this one does not, and #363 tracks the gap. Port the intent, keep the
fifth argument, and prefer adding that coverage over trusting this paragraph.
signature wholesale drops behaviour shipped in v2.8.0. Until #369 that
happened while **nothing went red** — `useTokenSearch` appeared nowhere under
`test/`, and reverting the signature alone only failed `typecheck`, since
`CommandPalette.vue` passes five arguments; a port that replayed both files
went quiet. Now guarded by `describe('useTokenSearch')` in
`test/utils/search.spec.ts`, which fails against a revert to upstream's
four-parameter signature, and by `describe('highlighting through fuse')` in
`test/components/CommandPalette.spec.ts`, which fails if the fifth argument
stops being forwarded from `processGroupItems`. Port the intent and keep the
fifth argument; trust those two specs over this paragraph.
- **`skills/` is b24ui-authored — never replay upstream skill or doc prose into
it.** The package was seeded from nuxt/ui's skill, and every defect the #93
audit found was an inherited upstream idiom rather than an ordinary typo:
Expand Down Expand Up @@ -358,3 +361,4 @@ forward, since every commit between the two would then never be judged.
- 2026-08-11 — review of PR #347 (issue #339): added the §2 **`highlight()` takes a fifth `useTokenSearch` argument** invariant, and corrected the `.sync/nuxt-ui.json` summary for `2a172ef` that asserted "highlight signature matches 1:1". The divergence has been in the tree since v2.8.0 and was never recorded: `c502157b` added `tokens`/`minTokenLength` and `6743f793` the parameter itself, both to `src/runtime/utils/fuse.ts`, and the port in `557a5178` renamed the file to `search.ts` — so a pickaxe on the current path returns only `557a5178`, a genuine upstream port, unless you pass `--follow`. That rename, not the trailer convention, is what hid it; `Upstream:` trailers are too rare (16 of ~3200 commits) to carry an inference either way. It has no test coverage; #363 tracks that. Worth recording how the error was found: the "byte-identical with upstream" premise originated **here**, in `595923b9` (PR #338), was repeated in #339, and was inherited in good faith by the external contributor whose PR prompted the check — nuxt/ui itself has still not been inspected, so the divergence is established from b24ui's history alone. Last reviewed: 2026-08-11.
- 2026-08-12 — the sync is manual by decision; the automation is removed. Deleted `.sync/PLAN.md` (the dispatcher/porter/on-merge design, its phase plan and its cron) and `.sync/RUNBOOK.md` (an incident playbook whose every row diagnosed one of those workflows). Dropped `sync_enabled` from the ledger — a kill-switch for a dispatcher that will not exist reads as "the sync is off" to anyone who finds it, which was already misleading while this file's own procedure ran twelve ports past it — and `stats`, Phase-4 telemetry that was never written to (`noop_ratio: 0` against an actual 47/226). Folded the one runbook row that survives manual work into §6: a cursor SHA that vanishes under an upstream force-push must be moved to the nearest surviving ancestor with a tracking issue, never skipped forward. §6 now spells out the procedure that was previously only implied by the workflows — parent-order reconstruction, verbatim diffs, the gate order with `docs:generate` and `deploy.yml`'s env, ledger reconciliation including the last-entry case, and the `behind` rebase. Also corrected `color-map.json`: `warning` mapped to `air-primary-alert`, the same token as `error`, so the table said the two upstream colors were interchangeable; `air-primary-warning` exists and is used 50 times in `src/theme/`. Last reviewed: 2026-08-12.
- 2026-08-12 — rebuilt `icon-map.json` and gave it a guard (the content of the closed PR #67, verified rather than imported). The map is now *derived*: for every icon key both sides define — `src/theme/icons.ts` upstream, `src/runtime/dictionary/icons.ts` here — the row is (upstream's lucide name → whatever our dictionary maps that key to), 37 pairs from a 43×39 key intersection at cursor `3dbca02`. Beware the obvious shortcut when re-checking this: the installed `@nuxt/ui@4.8.2` in `node_modules` (pulled in transitively by `nuxtseo-layer-devtools`) is **older than the sync cursor** and is missing keys — three separate reviewers read it and concluded `star` was fabricated and the intersection was 36. Read the raw file at the cursor SHA instead. The derivation turned up three errors in the values #67 proposed, each of which resolves to a real icon and so would have failed no import: `i-lucide-rotate-cw` for what upstream calls `i-lucide-rotate-ccw` (`reload`), `i-lucide-circle-check` for `copyCheck`'s `i-lucide-copy-check`, and `i-lucide-refresh-cw`, which no upstream key uses. It also surfaced seven derivable pairs #67 missed — `drag`, `panelClose`, `panelOpen`, `star`, `stop`, `copyCheck`, `reload` — and, separately, `i-lucide-terminal`, the **only** `i-lucide-*` literal upstream hardcodes under `src/` (`src/theme/prose/code-icon.ts`), which neither the old map nor #67 had even though `prose/CodeIcon.vue` has answered it all along. `error` and `success` gained judgement rows rather than staying unmapped: our `caution` carries a `// this for error` comment, and `copyCheck` already owns the glyph `success` would want. The five entries #67 dropped (`activity`, `arrow-up-to-line`, `house`, `settings`, `user`) are kept — they match no key on either side, which is the hardcoded-literal case the map exists for. **Correcting the record on the five values #67 changed** (`check`, `chevronDown`, `chevronUp`, `minus`, `x`): they are wrong because the map must agree with the dictionary, *not* — as an earlier draft of this entry claimed — because the library never renders them. It does. `Checkbox.vue` renders `main/CheckIcon` and `actions/Minus20Icon`, `Badge.vue` renders `actions/Cross20Icon`, `Button.vue` renders `outline/ChevronDownSIcon`; roughly half of the icon paths under `src/` are hardcoded in components that never read the dictionary, which is #380. That discovery also reshaped the guard: `test/utils/icon-map.spec.ts` allows any icon used anywhere in `src/` rather than only the dictionary's — the narrower rule rejected `terminal`, a correct row — while separately requiring every *derived* row to equal what its semantic key resolves to. That last check is the one with teeth: without it, pointing `i-lucide-check` at another icon the dictionary genuinely uses passed every other assertion. It guards wrong rows, not stale ones; nothing here notices if upstream renames a default. No `.sync/log/` or ledger entry, since this is not a port of an upstream commit — same as #343, #346, #351 and #377. Last reviewed: 2026-08-12.
- 2026-08-12 — coverage for #363: gave the §2 **`highlight()` takes a fifth `useTokenSearch` argument** invariant a guard. It was recorded during the review of #347 but left untested, and the bullet said so. The parameter and the token-search logic around it are b24ui-only — `c502157b` added `tokens`/`minTokenLength` and `6743f793` the parameter itself, both against the file's old name `src/runtime/utils/fuse.ts`, both shipped in v2.8.0. Immediately before `c502157b` the function took four parameters and computed no `minTokenLength` at all, which is what marks it as locally authored; the `Upstream:` trailer convention is too sparse (16 of ~3200 commits) to carry an inference either way. The later port `557a5178` renamed `fuse.ts` to `search.ts` and carried the divergence across, so a pickaxe on the current path returns only that port — pass `--follow` to see the two commits that introduced it. Until now it had no test at all, so replaying upstream's four-parameter signature would have dropped a shipped feature with nothing going red. Upstream itself has not been re-inspected; treat "upstream has no such parameter" as an inference from b24ui's own history. Last reviewed: 2026-08-12.
39 changes: 39 additions & 0 deletions test/components/CommandPalette.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,4 +317,43 @@ describe('CommandPalette', () => {
window.HTMLElement.prototype.scrollIntoView = original
}
})

describe('highlighting through fuse', () => {
// Everything above sets `labelHtml`/`suffixHtml`/`descriptionHtml` by hand,
// which is exactly the path that skips `highlight()`. These mount the real
// wiring instead: `processGroupItems` forwards
// `fuse.fuseOptions.useTokenSearch` as the fifth argument, and Fuse only
// populates `matches` when `includeMatches` is set — which this component
// leaves to the caller, so no other case in this file reaches `highlight()`
// at all.
function paletteWith(label: string, searchTerm: string, useTokenSearch?: boolean, threshold = 0.1) {
return mountSuspended(CommandPalette, {
props: {
groups: [{ id: 'g', items: [{ label }] }],
searchTerm,
fuse: { fuseOptions: { includeMatches: true, useTokenSearch, threshold, ignoreLocation: true } }
} as any
})
}

it('marks the match', async () => {
const wrapper = await paletteWith('alpha beta', 'alpha')

expect(wrapper.find('mark').exists()).toBe(true)
expect(wrapper.find('mark').text()).toContain('alpha')
})

it('marks each word of a multi-word query only when token search is on', async () => {
// Fuse returns the two words as separate regions, six and four characters
// long. Off, the threshold is the whole query — ten — and neither reaches
// it, so nothing is marked; on, it drops to the shortest word and both do.
// An exact-match fixture would not show the difference: Fuse then returns
// one region as long as the query, which clears either threshold.
const off = await paletteWith('alpha xxxx beta', 'alpha beta', false, 0.4)
const on = await paletteWith('alpha xxxx beta', 'alpha beta', true, 0.4)

expect(off.find('mark').exists()).toBe(false)
expect(on.find('mark').exists()).toBe(true)
})
})
})
46 changes: 46 additions & 0 deletions test/utils/search.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,52 @@ describe('highlight', () => {
expect(highlight({ label: 'foo' }, 'foo', 'label')).toBeUndefined()
})

describe('useTokenSearch', () => {
// b24ui-only fifth argument (see `.sync/PORTING.md` §2). It only moves
// `minTokenLength`, the threshold a region must reach to be marked at all:
// off, that is the length of the whole search term; on, the length of the
// shortest word in it — so multi-word queries highlight each word instead of
// nothing.
function highlightWith(value: string, indices: [number, number][], searchTerm: string, useTokenSearch?: boolean) {
return highlight({ label: value, matches: [{ key: 'label', value, indices }] }, searchTerm, 'label', undefined, useTokenSearch)
}

it('marks a region shorter than the whole term', () => {
// The region is 5 long. `alpha beta` is 10, so the full-term threshold
// rejects it; the shortest token, `beta`, is 4, so token search accepts it.
expect(highlightWith('alpha beta', [[0, 4]], 'alpha beta', true)).toBe('<mark>alpha</mark> beta')
})

it('leaves that same region unmarked when off', () => {
expect(highlightWith('alpha beta', [[0, 4]], 'alpha beta', false)).toBe('alpha beta')
})

it('is off by default — the path `CommandPalette` takes unless `fuseOptions` opts in', () => {
expect(highlightWith('alpha beta', [[0, 4]], 'alpha beta')).toBe('alpha beta')
})

it('still refuses a region shorter than the shortest token', () => {
// `be` is 2; the shortest token `beta` is 4. Token search lowers the bar,
// it does not remove it.
expect(highlightWith('alpha beta', [[6, 7]], 'alpha beta', true)).toBe('alpha beta')
})

it('falls back to the whole term when the tokenizer matches nothing', () => {
// `/[\p{L}\p{M}\p{N}_]+/gu` finds no token in an emoji-only query, so
// `tokens` is empty and the threshold stays `searchTerm.length` — 2 code
// units for one emoji — rather than collapsing to zero and marking
// everything.
expect(highlightWith('\u{1F600}\u{1F600}\u{1F600}', [[0, 1]], '\u{1F600}', true))
.toBe('<mark>\u{1F600}</mark>\u{1F600}\u{1F600}')
})

it('takes the shortest token, not the first or the longest', () => {
// Tokens are `considerable` (12) and `ab` (2); only a 2-long threshold
// admits this region.
expect(highlightWith('xy considerable', [[0, 1]], 'considerable ab', true)).toBe('<mark>xy</mark> considerable')
})
})

describe('truncation from the start', () => {
// Matches a high surrogate not followed by a low one, or a low surrogate not
// preceded by a high one — i.e. half of an astral character.
Expand Down