Skip to content

feat(Alert): cascade color to inner Avatar - #29

Open
IgorShevchik wants to merge 1 commit into
mainfrom
feat/alert-avatar-color
Open

feat(Alert): cascade color to inner Avatar#29
IgorShevchik wants to merge 1 commit into
mainfrom
feat/alert-avatar-color

Conversation

@IgorShevchik

Copy link
Copy Markdown
Collaborator

Summary

  • The existing top-level color prop on B24Alert now also tints the inner B24Avatar by default.
  • Explicit avatar.color still wins, since v-bind="props.avatar" is applied after :color="props.color".
  • Alert.variants.color is a superset of AvatarProps['color'] (it includes legacy entries like 'default'), so the binding casts to the Avatar color type at the template site — no API change, no theme change.

Follow-up to #24 (Avatar/AvatarGroup color), #27 (User color), #28 (ChatMessage color).

Test plan

  • pnpm run lint passes
  • pnpm run typecheck passes (root + docs + both playgrounds)
  • pnpm run test passes — new renders with color and avatar correctly snapshot witnesses the cascade (style-filled-success on the Avatar root when Alert is air-primary-success)
  • Open the docs page → Alert → Color section, the second example renders a tinted avatar
  • Verify explicit avatar.color overrides the cascade (set :avatar="{ src: '...', color: 'air-tertiary-accent' }" while color="air-primary-alert" on B24Alert — avatar should be tertiary-accent)

🤖 Generated with Claude Code

The existing top-level `color` prop now also tints the inner `B24Avatar`
by default. An explicit `avatar.color` still wins, since
`v-bind="props.avatar"` is applied after `:color="props.color"`.

`Alert.variants.color` is a superset of `AvatarProps['color']` (legacy
`'default'` etc.), so the binding casts to the Avatar color type at the
template site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@IgorShevchik
IgorShevchik force-pushed the feat/alert-avatar-color branch from c13b748 to d2502cf Compare May 13, 2026 07:06
IgorShevchik pushed a commit that referenced this pull request Aug 13, 2026
…code points

Code points are not what a reader sees. A flag is two regional indicators, a
family emoji several joined by ZWJ, `कि` a consonant plus a vowel sign. Cutting
inside one produces a *different* character rather than a broken one — 🇺🇸
truncated by a single code point re-pairs into 🇸🇺, a different country, with
nothing to signal the loss.

Both boundaries snap off the straddled cluster: the mark insertion in
`generateHighlightedText` and the truncation in `truncateHTMLFromStart`.

Chosen on measurement. Segmenting the whole string costs 455 µs at 979
characters and 52 ms at 100k. A fixed ±64 window is constant-time but wrong: a
run of flags is longer than the window, so segmentation starts mid-run and
re-pairs the indicators — reproducing the exact bug, 28 disagreements in 1044
probes. `Segments.containing()` is correct, so it ships, behind three guards: a
`< U+0300` screen, which keeps ASCII and Latin-1 off the segmenter but does
nothing for Cyrillic or CJK, both above the floor; one segmenter view per value,
built lazily; and an 8192-character ceiling, past which only the surrogate snap
applies and every multi-code-point cluster loses protection.

Two defects found in review are folded in. A region left empty by the ordering
clamps emitted a bare `<mark></mark>` with the highlight lost. A region past the
end of the value bypassed that guard entirely, because `substring()` clamps its
own arguments while the comparison did not — it emitted the same empty mark,
froze the cursor past every later region, and the trailing mark then drove
truncation into eating the text before it.

`indices` are sorted before use. That is a no-op for the search path, since Fuse
merges and sorts them itself, but `highlight()` is a published export and
`CommandPaletteGroup.postFilter` lets a caller supply its own matches;
out-of-order boundaries make `containing()` re-walk a long run of regional
indicators from its start, costing as much as the per-boundary segmenter this
avoids.

`truncateHTMLFromStart` counts retained code units and slices once, because the
snap needs an index to move. Verified equivalent over 400,078 fuzzed inputs,
including strings that themselves begin with `...`.

One fixture trap is recorded in the tests: UAX #29 glues a run of bare emoji
modifiers into a single cluster, so `'\u{1F3FF}'.repeat(20)` is one character
rather than twenty.

Closes #364

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWWrBHgfqGSbeU3V6UuMF8
IgorShevchik pushed a commit that referenced this pull request Aug 13, 2026
…code points

Code points are not what a reader sees. A flag is two regional indicators, a
family emoji several joined by ZWJ, `कि` a consonant plus a vowel sign. Cutting
inside one produces a *different* character rather than a broken one — 🇺🇸
truncated by a single code point re-pairs into 🇸🇺, a different country, with
nothing to signal the loss.

Both boundaries snap off the straddled cluster: the mark insertion in
`generateHighlightedText` and the truncation in `truncateHTMLFromStart`.

Chosen on measurement. Segmenting the whole string costs 455 µs at 979
characters and 52 ms at 100k. A fixed ±64 window is constant-time but wrong: a
run of flags is longer than the window, so segmentation starts mid-run and
re-pairs the indicators — reproducing the exact bug, 28 disagreements in 1044
probes. `Segments.containing()` is correct, so it ships, behind three guards: a
`< U+0300` screen, which keeps ASCII and Latin-1 off the segmenter but does
nothing for Cyrillic or CJK, both above the floor; one segmenter view per value,
built lazily; and an 8192-character ceiling, past which only the surrogate snap
applies and every multi-code-point cluster loses protection.

Two defects found in review are folded in. A region left empty by the ordering
clamps emitted a bare `<mark></mark>` with the highlight lost. A region past the
end of the value bypassed that guard entirely, because `substring()` clamps its
own arguments while the comparison did not — it emitted the same empty mark,
froze the cursor past every later region, and the trailing mark then drove
truncation into eating the text before it.

`indices` are sorted before use. That is a no-op for the search path, since Fuse
merges and sorts them itself, but `highlight()` is a published export and
`CommandPaletteGroup.postFilter` lets a caller supply its own matches;
out-of-order boundaries make `containing()` re-walk a long run of regional
indicators from its start, costing as much as the per-boundary segmenter this
avoids.

`truncateHTMLFromStart` counts retained code units and slices once, because the
snap needs an index to move. Verified equivalent over 400,078 fuzzed inputs,
including strings that themselves begin with `...`.

One fixture trap is recorded in the tests: UAX #29 glues a run of bare emoji
modifiers into a single cluster, so `'\u{1F3FF}'.repeat(20)` is one character
rather than twenty.

Closes #364

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWWrBHgfqGSbeU3V6UuMF8
IgorShevchik pushed a commit that referenced this pull request Aug 13, 2026
…code points

Code points are not what a reader sees. A flag is two regional indicators, a
family emoji several joined by ZWJ, `कि` a consonant plus a vowel sign. Cutting
inside one produces a *different* character rather than a broken one — 🇺🇸
truncated by a single code point re-pairs into 🇸🇺, a different country, with
nothing to signal the loss.

Both boundaries snap off the straddled cluster: the mark insertion in
`generateHighlightedText` and the truncation in `truncateHTMLFromStart`.

Chosen on measurement. Segmenting the whole string costs 455 µs at 979
characters and 52 ms at 100k. A fixed ±64 window is constant-time but wrong: a
run of flags is longer than the window, so segmentation starts mid-run and
re-pairs the indicators — reproducing the exact bug, 28 disagreements in 1044
probes. `Segments.containing()` is correct, so it ships, behind three guards: a
`< U+0300` screen, which keeps ASCII and Latin-1 off the segmenter but does
nothing for Cyrillic or CJK, both above the floor; one segmenter view per value,
built lazily; and an 8192-character ceiling, past which only the surrogate snap
applies and every multi-code-point cluster loses protection.

Two defects found in review are folded in. A region left empty by the ordering
clamps emitted a bare `<mark></mark>` with the highlight lost. A region past the
end of the value bypassed that guard entirely, because `substring()` clamps its
own arguments while the comparison did not — it emitted the same empty mark,
froze the cursor past every later region, and the trailing mark then drove
truncation into eating the text before it.

`indices` are sorted before use. That is a no-op for the search path, since Fuse
merges and sorts them itself, but `highlight()` is a published export and
`CommandPaletteGroup.postFilter` lets a caller supply its own matches;
out-of-order boundaries make `containing()` re-walk a long run of regional
indicators from its start, costing as much as the per-boundary segmenter this
avoids.

`truncateHTMLFromStart` counts retained code units and slices once, because the
snap needs an index to move. Verified equivalent over 400,078 fuzzed inputs,
including strings that themselves begin with `...`.

One fixture trap is recorded in the tests: UAX #29 glues a run of bare emoji
modifiers into a single cluster, so `'\u{1F3FF}'.repeat(20)` is one character
rather than twenty.

Closes #364

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWWrBHgfqGSbeU3V6UuMF8
IgorShevchik pushed a commit that referenced this pull request Aug 13, 2026
…code points

Code points are not what a reader sees. A flag is two regional indicators, a
family emoji several joined by ZWJ, `कि` a consonant plus a vowel sign. Cutting
inside one produces a *different* character rather than a broken one — 🇺🇸
truncated by a single code point re-pairs into 🇸🇺, a different country, with
nothing to signal the loss.

Both boundaries snap off the straddled cluster: the mark insertion in
`generateHighlightedText` and the truncation in `truncateHTMLFromStart`.

Chosen on measurement. Segmenting the whole string costs 455 µs at 979
characters and 52 ms at 100k. A fixed ±64 window is constant-time but wrong: a
run of flags is longer than the window, so segmentation starts mid-run and
re-pairs the indicators — reproducing the exact bug, 28 disagreements in 1044
probes. `Segments.containing()` is correct, so it ships, behind three guards: a
`< U+0300` screen, which keeps ASCII and Latin-1 off the segmenter but does
nothing for Cyrillic or CJK, both above the floor; one segmenter view per value,
built lazily; and an 8192-character ceiling, past which only the surrogate snap
applies and every multi-code-point cluster loses protection.

Two defects found in review are folded in. A region left empty by the ordering
clamps emitted a bare `<mark></mark>` with the highlight lost. A region past the
end of the value bypassed that guard entirely, because `substring()` clamps its
own arguments while the comparison did not — it emitted the same empty mark,
froze the cursor past every later region, and the trailing mark then drove
truncation into eating the text before it.

`indices` are sorted before use. That is a no-op for the search path, since Fuse
merges and sorts them itself, but `highlight()` is a published export and
`CommandPaletteGroup.postFilter` lets a caller supply its own matches;
out-of-order boundaries make `containing()` re-walk a long run of regional
indicators from its start, costing as much as the per-boundary segmenter this
avoids.

`truncateHTMLFromStart` counts retained code units and slices once, because the
snap needs an index to move. Verified equivalent over 400,078 fuzzed inputs,
including strings that themselves begin with `...`.

One fixture trap is recorded in the tests: UAX #29 glues a run of bare emoji
modifiers into a single cluster, so `'\u{1F3FF}'.repeat(20)` is one character
rather than twenty.

Closes #364

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWWrBHgfqGSbeU3V6UuMF8
IgorShevchik pushed a commit that referenced this pull request Aug 13, 2026
…code points

Code points are not what a reader sees. A flag is two regional indicators, a
family emoji several joined by ZWJ, `कि` a consonant plus a vowel sign. Cutting
inside one produces a *different* character rather than a broken one — 🇺🇸
truncated by a single code point re-pairs into 🇸🇺, a different country, with
nothing to signal the loss.

Both boundaries snap off the straddled cluster: the mark insertion in
`generateHighlightedText` and the truncation in `truncateHTMLFromStart`.

Chosen on measurement. Segmenting the whole string costs 455 µs at 979
characters and 52 ms at 100k. A fixed ±64 window is constant-time but wrong: a
run of flags is longer than the window, so segmentation starts mid-run and
re-pairs the indicators — reproducing the exact bug, 28 disagreements in 1044
probes. `Segments.containing()` is correct, so it ships, behind three guards: a
`< U+0300` screen, which keeps ASCII and Latin-1 off the segmenter but does
nothing for Cyrillic or CJK, both above the floor; one segmenter view per value,
built lazily; and an 8192-character ceiling, past which only the surrogate snap
applies and every multi-code-point cluster loses protection.

Two defects found in review are folded in. A region left empty by the ordering
clamps emitted a bare `<mark></mark>` with the highlight lost. A region past the
end of the value bypassed that guard entirely, because `substring()` clamps its
own arguments while the comparison did not — it emitted the same empty mark,
froze the cursor past every later region, and the trailing mark then drove
truncation into eating the text before it.

`indices` are sorted before use. That is a no-op for the search path, since Fuse
merges and sorts them itself, but `highlight()` is a published export and
`CommandPaletteGroup.postFilter` lets a caller supply its own matches;
out-of-order boundaries make `containing()` re-walk a long run of regional
indicators from its start, costing as much as the per-boundary segmenter this
avoids.

`truncateHTMLFromStart` counts retained code units and slices once, because the
snap needs an index to move. Verified equivalent over 400,078 fuzzed inputs,
including strings that themselves begin with `...`.

One fixture trap is recorded in the tests: UAX #29 glues a run of bare emoji
modifiers into a single cluster, so `'\u{1F3FF}'.repeat(20)` is one character
rather than twenty.

Closes #364

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWWrBHgfqGSbeU3V6UuMF8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant