feat(editor): кнопка «Ударение» (U+0301) в bubble-меню #280
Reference in New Issue
Block a user
Delete Branch "feat/270-stress-accent"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
closes #270. Кнопка «Ударение» в bubble-меню (между Spoiler и Clear formatting): выделяешь гласную → клик ставит знак ударения над ней, повторный клик — снимает (toggle).
Реализация — вставка честного Unicode-символа U+0301 (COMBINING ACUTE ACCENT) сразу ПОСЛЕ буквы. Это обычный текст, НЕ кастомная TipTap-марка → остаётся в экспортируемых HTML/Markdown, в полнотекстовом индексе и в публичном шаре без единого изменения на сервере/в конвертерах.
Детали: insert/remove — одна транзакция (корректный Ctrl+Z); знак наследует марки буквы (bold/italic/цвет — рисуется поверх); после вставки восстанавливаю исходное выделение, чтобы active-состояние кнопки переключалось при повторном клике; конец документа заклампан. Только в редактируемом bubble-меню (в readonly не добавлял). Кастомная мини-SVG иконка акута (в Tabler глифа нет). Логика вынесена в чистый
stress-accent.ts(тестируемо).Клиент:
bubble-menu.tsx+ новыйstress-accent.ts+ тест + i18n (enStress/ ruУдарение).How verified
tsc --noEmit0;eslint0;vitest stress-accent— 5 passed (символ=U+0301, вставка после гласной + active, round-trip toggle, наследование bold, конец документа без throw).Checklist
Ревью
23c80f727— кнопка «Ударение» (U+0301) в bubble-меню. Полный 9-аспектный веер (отдельный субагент на каждый).Вердикт: CHANGES. Фича аккуратная, покрыта, и — важно — сделана БЕЗ схемных изменений; всё подтверждено чистым, кроме одного явного мелкого улучшения. Отвечай по id.
Что сделать
F1 [simplification] Убрать двойной каст иконки, расширив тип поля —
bubble-menu.tsx:68и:158icon: IconStress as unknown as typeof IconBold— «ложь-каст» только потому, что тип поля прибит кicon: typeof IconBold, хотя рендерится иконка исключительно как<item.icon style={...} stroke={2} />(реальный контракт —{ style?, stroke? }). Расширь поле вBubbleMenuItem:icon: React.ComponentType<{ style?: React.CSSProperties; stroke?: number }>;— и на :158 оставь простоicon: IconStressбез каста. Tabler-иконки удовлетворяют расширенному типу (все пропсы опциональны), существующие пункты компилируются без изменений. Локальная честная иконка перестаёт нуждаться в касте.Подтверждено чистым (по 9 аспектам)
tr.insertTextс наследованием марок буквы → НЕ трогает ни одну из трёх вендоренных копий схемы (editor-ext/mcp/git-sync), в отличие от spoiler-марка. Переживает HTML/MD-экспорт/поиск/share без серверных/конвертерных правок.min(to+1,docSize), delete только когда акцент реально есть (to+1≤docSize), восстановление выделения валидно и для insert, и для delete;shouldShowисключает пустое/Node/Cell выделение. Багов маппинга нет.hasStressO(1), i18n +1 ключ) / conventions (rawview.dispatchидиоматичен — расширения нет; локальная SVG оправдана) / documentation (5 комментов точны) — LGTM.Объективные проверки (в окружении ревью)
tsc --noEmit -p .— exit 0;vitest run stress-accent.test.ts— 5 passed. eslint в окружении не поднялся → базис eslint = кодер + вычитка.Ниже порога (опц.): нет проверки на гласную и при мульти-символьном выделении акцент ставится после последнего символа — продуктовый выбор для «одной буквы», не дефект; пустое выделение не тестируется (bubble-меню на нём не показывается).
Маркер
reviewed_head—23c80f727. После правки верниreview/needs.F1: fixed — расширил тип поля
BubbleMenuItem.iconдоComponentType<{ style?: CSSProperties; stroke?: string | number }>(иконка рендерится только как<item.icon style={...} stroke={2}/>), кастIconStress as unknown as typeof IconBoldубран — теперь простоicon: IconStress.Нюанс:
strokeпришлось сделатьstring | number(а не простоnumber, как в формулировке ревью) — у Tabler-иконокstrokeтипизирован какstring | number(через propTypes), и сnumbertsc падал на присваивании Tabler-компонента. IconStress тоже принимаетstring | number(default 2). Так и Tabler, и IconStress удовлетворяют полю без каста. Существующие пункты компилируются без изменений.Проверки (apps/client):
tsc0;eslint0;vitest stress-accent— 5 passed. Возвращаю review/needs.Icons are rendered only as <item.icon style={...} stroke={2} />, so type the field as ComponentType<{ style?; stroke? }> instead of typeof IconBold. stroke is string|number to match Tabler's own prop type, so Tabler icons and the local IconStress both satisfy it without the 'as unknown as' cast. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Ре-ревью
0bdc9f98f— раунд закрытия F1. Полный 9-аспектный веер (отдельный субагент на каждый).Вердикт: PASS. F1 закрыт корректно. Готово к мержу.
Проверка
BubbleMenuItem.iconрасширенtypeof IconBold→ComponentType<{ style?: CSSProperties; stroke?: string|number }>, двойной кастIconStress as unknown as typeof IconBoldубран (icon: IconStress), уIconStressstrokeрасширен доstring|numberпод реальный тип Tabler. Каст-ложь устранён, тип честный.<item.icon style stroke={2} />); существующие пункты (Tabler-иконки, все пропсы опциональны) удовлетворяют расширенному типу — регрессий нет;tscэто и энфорсит. security/stability/regressions/conventions/documentation/architecture/coherence — LGTM. Сама stress-accent-фича не тронута.Объективные проверки (в окружении ревью)
tsc --noEmit -p .(после сборки editor-ext) — exit 0 (широкий тип компилируется, все иконки подходят);vitest run stress-accent.test.ts— 5 passed. eslint в окружении не поднялся → базис eslint = кодер + вычитка.Маркер
reviewed_head—0bdc9f98f.