@olddevs/ui 4.1.0 → 4.2.0

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 (141) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/components/actions/Toggle.d.ts.map +1 -1
  3. package/dist/components/actions/Toggle.js +1 -1
  4. package/dist/components/actions/Toggle.js.map +1 -1
  5. package/dist/components/actions/Truncate.d.ts.map +1 -1
  6. package/dist/components/actions/Truncate.js +2 -2
  7. package/dist/components/actions/Truncate.js.map +1 -1
  8. package/dist/components/app-shell/MobileTabBar.d.ts.map +1 -1
  9. package/dist/components/app-shell/MobileTabBar.js +1 -1
  10. package/dist/components/app-shell/MobileTabBar.js.map +1 -1
  11. package/dist/components/app-shell/NavItem.d.ts.map +1 -1
  12. package/dist/components/app-shell/NavItem.js +3 -1
  13. package/dist/components/app-shell/NavItem.js.map +1 -1
  14. package/dist/components/content/AvatarGroup.d.ts.map +1 -1
  15. package/dist/components/content/AvatarGroup.js +2 -1
  16. package/dist/components/content/AvatarGroup.js.map +1 -1
  17. package/dist/components/content/Table.d.ts.map +1 -1
  18. package/dist/components/content/Table.js +2 -1
  19. package/dist/components/content/Table.js.map +1 -1
  20. package/dist/components/content/VirtualGrid.d.ts.map +1 -1
  21. package/dist/components/content/VirtualGrid.js +4 -1
  22. package/dist/components/content/VirtualGrid.js.map +1 -1
  23. package/dist/components/inputs/Checkbox.d.ts.map +1 -1
  24. package/dist/components/inputs/Checkbox.js +3 -0
  25. package/dist/components/inputs/Checkbox.js.map +1 -1
  26. package/dist/components/inputs/CheckboxCard.d.ts.map +1 -1
  27. package/dist/components/inputs/CheckboxCard.js +2 -2
  28. package/dist/components/inputs/CheckboxCard.js.map +1 -1
  29. package/dist/components/inputs/RadioGroup.d.ts.map +1 -1
  30. package/dist/components/inputs/RadioGroup.js +5 -3
  31. package/dist/components/inputs/RadioGroup.js.map +1 -1
  32. package/dist/components/inputs/Select.d.ts.map +1 -1
  33. package/dist/components/inputs/Select.js +6 -6
  34. package/dist/components/inputs/Select.js.map +1 -1
  35. package/dist/components/inputs/Slider.d.ts.map +1 -1
  36. package/dist/components/inputs/Slider.js +2 -2
  37. package/dist/components/inputs/Slider.js.map +1 -1
  38. package/dist/components/navigation/Accordion.d.ts.map +1 -1
  39. package/dist/components/navigation/Accordion.js +1 -1
  40. package/dist/components/navigation/Accordion.js.map +1 -1
  41. package/dist/components/navigation/MegaMenu.d.ts +156 -0
  42. package/dist/components/navigation/MegaMenu.d.ts.map +1 -0
  43. package/dist/components/navigation/MegaMenu.js +512 -0
  44. package/dist/components/navigation/MegaMenu.js.map +1 -0
  45. package/dist/components/navigation/TreeView.d.ts.map +1 -1
  46. package/dist/components/navigation/TreeView.js +6 -3
  47. package/dist/components/navigation/TreeView.js.map +1 -1
  48. package/dist/components/navigation/useMenuAim.d.ts +41 -0
  49. package/dist/components/navigation/useMenuAim.d.ts.map +1 -0
  50. package/dist/components/navigation/useMenuAim.js +114 -0
  51. package/dist/components/navigation/useMenuAim.js.map +1 -0
  52. package/dist/components/overlays/Menu.d.ts +19 -1
  53. package/dist/components/overlays/Menu.d.ts.map +1 -1
  54. package/dist/components/overlays/Menu.js +16 -4
  55. package/dist/components/overlays/Menu.js.map +1 -1
  56. package/dist/editor/RichTextEditor.d.ts.map +1 -1
  57. package/dist/editor/RichTextEditor.js +4 -2
  58. package/dist/editor/RichTextEditor.js.map +1 -1
  59. package/dist/editor/features/table/TableMenuButton.js +1 -1
  60. package/dist/editor/features/table/TableMenuButton.js.map +1 -1
  61. package/dist/editor/styles/prose.d.ts +7 -0
  62. package/dist/editor/styles/prose.d.ts.map +1 -1
  63. package/dist/editor/styles/prose.js +1 -1
  64. package/dist/editor/styles/prose.js.map +1 -1
  65. package/dist/editor/ui/BlockHandle.js +1 -1
  66. package/dist/editor/ui/BlockHandle.js.map +1 -1
  67. package/dist/editor/ui/FormatMenus.d.ts.map +1 -1
  68. package/dist/editor/ui/FormatMenus.js +3 -2
  69. package/dist/editor/ui/FormatMenus.js.map +1 -1
  70. package/dist/{editor/hooks → hooks}/useSubmenuPlacement.d.ts +14 -1
  71. package/dist/hooks/useSubmenuPlacement.d.ts.map +1 -0
  72. package/dist/hooks/useSubmenuPlacement.js +10 -0
  73. package/dist/hooks/useSubmenuPlacement.js.map +1 -0
  74. package/dist/i18n/defaults.d.ts.map +1 -1
  75. package/dist/i18n/defaults.js +3 -0
  76. package/dist/i18n/defaults.js.map +1 -1
  77. package/dist/i18n/messages.d.ts +8 -0
  78. package/dist/i18n/messages.d.ts.map +1 -1
  79. package/dist/index.d.ts +2 -0
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +1 -0
  82. package/dist/index.js.map +1 -1
  83. package/dist/lib/choice-card.d.ts +13 -0
  84. package/dist/lib/choice-card.d.ts.map +1 -1
  85. package/dist/lib/choice-card.js +2 -0
  86. package/dist/lib/choice-card.js.map +1 -1
  87. package/dist/locales/ru.d.ts.map +1 -1
  88. package/dist/locales/ru.js +3 -0
  89. package/dist/locales/ru.js.map +1 -1
  90. package/docs/api/messages.md +6 -0
  91. package/docs/components/app-shell/mobile-tab-bar.en.md +3 -3
  92. package/docs/components/app-shell/mobile-tab-bar.md +2 -2
  93. package/docs/components/app-shell/side-nav.en.md +7 -3
  94. package/docs/components/app-shell/side-nav.md +6 -2
  95. package/docs/components/index.en.md +2 -1
  96. package/docs/components/index.md +1 -0
  97. package/docs/components/inputs/form.en.md +2 -2
  98. package/docs/components/inputs/form.md +1 -1
  99. package/docs/components/inputs/rich-text-editor.en.md +2 -2
  100. package/docs/components/inputs/rich-text-editor.md +1 -1
  101. package/docs/components/navigation/mega-menu.en.md +273 -0
  102. package/docs/components/navigation/mega-menu.md +258 -0
  103. package/docs/components/overlays/menu.en.md +22 -4
  104. package/docs/components/overlays/menu.md +20 -4
  105. package/docs/getting-started.en.md +5 -5
  106. package/docs/getting-started.md +4 -4
  107. package/docs/glossary.en.md +6 -1
  108. package/docs/glossary.md +5 -0
  109. package/llms.txt +1 -0
  110. package/package.json +1 -1
  111. package/src/components/actions/Toggle.tsx +10 -3
  112. package/src/components/actions/Truncate.tsx +5 -3
  113. package/src/components/app-shell/MobileTabBar.tsx +9 -1
  114. package/src/components/app-shell/NavItem.tsx +6 -1
  115. package/src/components/content/AvatarGroup.tsx +6 -2
  116. package/src/components/content/Table.tsx +15 -3
  117. package/src/components/content/VirtualGrid.tsx +11 -1
  118. package/src/components/inputs/Checkbox.tsx +5 -0
  119. package/src/components/inputs/CheckboxCard.tsx +2 -0
  120. package/src/components/inputs/RadioGroup.tsx +10 -1
  121. package/src/components/inputs/Select.tsx +113 -106
  122. package/src/components/inputs/Slider.tsx +6 -2
  123. package/src/components/navigation/Accordion.tsx +4 -1
  124. package/src/components/navigation/MegaMenu.tsx +1223 -0
  125. package/src/components/navigation/TreeView.tsx +70 -49
  126. package/src/components/navigation/useMenuAim.ts +202 -0
  127. package/src/components/overlays/Menu.tsx +47 -4
  128. package/src/editor/RichTextEditor.tsx +6 -3
  129. package/src/editor/features/table/TableMenuButton.tsx +1 -1
  130. package/src/editor/styles/prose.ts +1 -1
  131. package/src/editor/ui/BlockHandle.tsx +1 -1
  132. package/src/editor/ui/FormatMenus.tsx +17 -2
  133. package/src/{editor/hooks → hooks}/useSubmenuPlacement.ts +18 -15
  134. package/src/i18n/defaults.ts +4 -0
  135. package/src/i18n/messages.ts +9 -0
  136. package/src/index.ts +17 -0
  137. package/src/lib/choice-card.ts +17 -0
  138. package/src/locales/ru.ts +4 -0
  139. package/dist/editor/hooks/useSubmenuPlacement.d.ts.map +0 -1
  140. package/dist/editor/hooks/useSubmenuPlacement.js +0 -20
  141. package/dist/editor/hooks/useSubmenuPlacement.js.map +0 -1
@@ -0,0 +1,258 @@
1
+ ---
2
+ component: MegaMenu
3
+ exports: [MegaMenu, MegaMenuCategory, MegaMenuGroup, MegaMenuLink, MegaMenuTrigger, MegaMenuButton]
4
+ category: navigation
5
+ status: stable
6
+ source: src/components/navigation/MegaMenu.tsx
7
+ story: src/components/navigation/MegaMenu.stories.tsx
8
+ ---
9
+
10
+ # MegaMenu
11
+
12
+ ## Кратко для AI
13
+
14
+ `MegaMenu` — каталог магазина в духе Amazon: слева Столбец категорий, справа от наведённой Категории — её
15
+ Витрина с колонками ссылок и любым своим содержимым. Это навигация по ссылкам, а не меню команд: разметка по
16
+ шаблону WAI-ARIA Disclosure Navigation (`nav`, кнопки с `aria-expanded`, настоящие `a`). Категории —
17
+ `MegaMenuCategory` прямыми детьми, колонки — `MegaMenuGroup`, ссылки — `MegaMenuLink`. Тот же каталог
18
+ открывается выпадающим оверлеем от кнопки: `MegaMenuTrigger` с `MegaMenuButton` и неизменным `MegaMenu`.
19
+
20
+ ## Когда использовать
21
+
22
+ - Каталог с десятком и больше разделов, у каждого — подразделы колонками: магазин, маркетплейс, справочный
23
+ портал.
24
+ - Разделы надо показать сразу, а подразделы — по наведению, не уводя пользователя со страницы.
25
+ - В Витрине рядом со ссылками нужно своё содержимое: промо-карточка, картинка, подборка.
26
+
27
+ ## Когда не использовать
28
+
29
+ - Для команд над объектом («Переименовать», «Удалить») — `Menu`: там действия, а не переходы.
30
+ - Для навигации приложения с текущим разделом — `SideNav`.
31
+ - Для глубокой иерархии, которую раскрывают на месте, — `TreeView`.
32
+ - Для двух-трёх ссылок без подразделов — обычный список ссылок или `Tabs`.
33
+
34
+ ## Минимальный пример
35
+
36
+ ```tsx
37
+ import { MegaMenu, MegaMenuCategory, MegaMenuGroup, MegaMenuLink } from "@olddevs/ui";
38
+
39
+ <MegaMenu aria-label="Каталог">
40
+ <MegaMenuCategory id="computers" label="Компьютеры" href="/computers">
41
+ <MegaMenuGroup title="Ноутбуки" href="/computers/laptops">
42
+ <MegaMenuLink href="/computers/laptops/gaming">Игровые</MegaMenuLink>
43
+ <MegaMenuLink href="/computers/laptops/ultrabooks" current>
44
+ Ультрабуки
45
+ </MegaMenuLink>
46
+ </MegaMenuGroup>
47
+ </MegaMenuCategory>
48
+ <MegaMenuCategory id="deals" label="Скидки дня" href="/deals" />
49
+ </MegaMenu>;
50
+ ```
51
+
52
+ ## Пропы
53
+
54
+ `MegaMenu`:
55
+
56
+ | Проп | По умолчанию | Смысл |
57
+ | ------------------------------------------ | ------------ | ------------------------------------------------------------------------------------------ |
58
+ | `aria-label` | — | Доступное имя навигации, обязательно: на странице с несколькими `nav` их различают по нему |
59
+ | `value` / `defaultValue` / `onValueChange` | `null` | Открытая Категория (её `id`) или `null`, управляемо или нет |
60
+ | `showcaseWidth` | `"40rem"` | Ширина Витрины, одна на все Категории: каталог не прыгает при переходе между ними |
61
+ | `className` | — | Классы на `nav`; ширина Столбца по умолчанию — `w-64` |
62
+
63
+ `MegaMenuCategory`: `id` — ключ для `value`; `label` — подпись в Столбце и имя Витрины; `icon`; `href` или
64
+ `link` — адрес раздела целиком; `disabled`; дети — содержимое Витрины. У Категории с Витриной адрес становится
65
+ заголовком-ссылкой в начале Витрины, у Категории без детей сама строка Столбца — ссылка. `link` — ссылка роутера
66
+ пустым элементом (`link={<Link href="/x" />}`), подпись уезжает внутрь; это отдельный проп, а не `asChild`,
67
+ потому что дети Категории — её Витрина.
68
+
69
+ `MegaMenuGroup`: `title` — заголовок колонки, он же подпись списка; `href` делает заголовок ссылкой; дети —
70
+ `MegaMenuLink`, каждая уезжает в свой пункт списка.
71
+
72
+ `MegaMenuLink`: обычные пропы `a`, `icon`, `current` и `asChild`. `current` ставит `aria-current="page"` и вид
73
+ текущей: текущую страницу кит не вычисляет, правила сравнения адресов у каждого роутера свои. `asChild` выводит
74
+ ссылку элементом роутера: `<MegaMenuLink asChild><Link href="/x">Подпись</Link></MegaMenuLink>`.
75
+
76
+ ## Наведение и Витрина
77
+
78
+ Наведение мышью на Категорию открывает её Витрину сразу, без задержки. Подсвечена ровно та Категория, чья
79
+ Витрина открыта: подсветка идёт по `aria-expanded`, а не по `:hover`. Наведение на Категорию без Витрины
80
+ закрывает открытую — с той же оговоркой про Прицел, что ниже.
81
+
82
+ Витрина закрывается, когда курсор ушёл и со Столбца, и с Витрины, через 150 мс: короткий выход за край не
83
+ сбрасывает каталог. Закрывают её и нажатие указателем вне каталога, и уход фокуса со страницы каталога.
84
+
85
+ Содержимое Витрины монтируется только у открытой Категории: каталог из сотни ссылок и картинок не грузит всё
86
+ разом. Сам регион есть всегда, скрытым: на него указывает `aria-controls` кнопки.
87
+
88
+ Витрина — потомок своей Категории в DOM, а не портал. Поэтому порядок Tab идёт «Категория → её ссылки → дальше
89
+ по странице», и Витрина встаёт вплотную к Столбцу без расчёта позиции. Предок с `overflow: hidden` её обрежет:
90
+ ставьте каталог туда, где рядом есть место.
91
+
92
+ Витрина стоит со стороны конца строки — справа, а при письме справа налево слева. Если там не хватает места, а с
93
+ другой стороны хватает, она переходит на другую сторону; сторону кит меряет заранее и при смене размера окна,
94
+ поэтому стрелка «к Витрине» сразу смотрит туда, где Витрина окажется.
95
+
96
+ ## Касание и узкий экран
97
+
98
+ Касание наведения не знает, поэтому Категорию открывает само касание (`click`), без Прицела. Указатель с
99
+ `pointerType` «touch» или «pen» не открывает Категорию наведением и не закрывает Витрину уходом: перо
100
+ ведёт себя как палец. Повторное касание открытой Категории её не закрывает и никуда не ведёт: закрывает
101
+ Витрину касание вне каталога или Escape.
102
+
103
+ Мышь ведёт себя, как раньше: наведение, Прицел и закрытие через 150 мс остаются, пока экран широкий.
104
+
105
+ На узком экране — меньше 640px, граница та же, что у подменю `Menu` и подменю Редактора, — Витрине некуда
106
+ встать сбоку: справа от Столбца её обрезал бы край окна. Поэтому каталог идёт «спуском»:
107
+
108
+ - Столбец занимает всю ширину, подписи Категорий переносятся, а не обрезаются.
109
+ - Открытая Категория (касанием, щелчком, Enter или Пробелом) заменяет Столбец своей Витриной. Витрина
110
+ начинается строкой «Назад» с названием Категории; если у Категории есть своя ссылка, название показывает она,
111
+ строкой ниже, а не строка «Назад». Ниже — колонки ссылок, одна под другой.
112
+ - «Назад» возвращает Столбец и ставит фокус на Категорию, из которой человек пришёл. Escape внутри
113
+ Витрины работает как «Назад».
114
+ - Фокус при спуске переходит на «Назад»: кнопка Категории скрыта вместе со Столбцом, и фокус на ней
115
+ пропал бы. Стрелка в сторону Столбца в Витрине на узком экране ничего не делает: Столбца рядом нет.
116
+ - Прицела на узком экране нет, наведение мышью Категорию не открывает, а уход мыши не отменяет спуск:
117
+ заменять Столбец под курсором значило бы вырывать его из-под руки.
118
+ - Разметка остаётся по шаблону disclosure: кнопка Категории сохраняет `aria-expanded` и `aria-controls`
119
+ на показанный регион (сама кнопка при спуске скрыта), регион подписан именем Категории.
120
+
121
+ Подпись «Назад» берётся из каталога сообщений: ключ `megaMenu.back`, по умолчанию «Back»; русский каталог
122
+ отдаёт «Назад».
123
+
124
+ Ширину кит читает через `matchMedia`, а не через CSS: от неё зависит состав разметки (прячется Столбец), а
125
+ не только вид. До гидратации и на сервере хук возвращает «широкий» (ADR-0018): сервер ширины не знает, и
126
+ разметка с него всегда обычная. На телефоне после гидратации Витрина, открытая `defaultValue`, один раз
127
+ перерисовывается спуском. На телефоне `nav` занимает всю ширину классом `max-sm:w-full`; `w-*` из `className` действует на
128
+ широком экране.
129
+
130
+ ## Прицел
131
+
132
+ Курсор идёт от Категории к её Витрине наискосок и по пути задевает соседние Категории. Если бы наведение
133
+ открывало их сразу, Витрина менялась бы у человека под рукой, и до ссылки было бы не дотянуться. Прицел смотрит,
134
+ куда движется курсор: пока он идёт в сторону открытой Витрины, соседняя Категория наведение не перехватывает.
135
+
136
+ - Направление последнего шага курсора сравнивается с сектором от его прежней точки к двум углам ближнего края
137
+ Витрины — это и есть треугольник Каменса из раздела «Откуда это». Шаг в сектор — курсор целится.
138
+ - Одно движение мимо прощается, два подряд — нет: тогда открывается Категория под курсором.
139
+ - Курсор остановился на 300 мс — открывается Категория под ним, даже если он целился. Меню не «залипает».
140
+ - Курсор дошёл до Витрины — отложенная смена отменяется.
141
+ - Щелчок по Категории открывает её сразу, мимо Прицела. Клавиатура Прицела не знает вовсе.
142
+ - Только мышь, и только на широком экране: касание и перо открывают Категорию сразу, а на узком экране
143
+ Прицела нет вовсе.
144
+ - Первое наведение, пока Витрины нет, открывает Категорию без всякой задержки: целиться ещё не во что.
145
+
146
+ Модель та же, что у подменю react-aria (`Menu` с `SubmenuTrigger`), чтобы два меню кита вели себя одинаково.
147
+ Отличий два. Смена Категории откладывается, а не гасится `pointer-events: none` на Столбце: иначе пропала бы
148
+ подсветка под курсором и щелчок по соседней Категории. И время остановки — 300 мс, как в плагине Каменса (о нём — в «Откуда это»), а не секунда,
149
+ как у react-aria: его подменю короткое, а Столбец каталога листают.
150
+
151
+ ## Откуда это
152
+
153
+ Приём пришёл из Amazon. К 2013 году меню «Shop by Department» на amazon.com уже работало так: столбец отделов
154
+ слева, большая панель справа, и переключение без всякой задержки — при этом наискосок к панели курсор проходил,
155
+ не сбивая её. Как это устроено, разобрал Бен Каменс.
156
+
157
+ - **6 марта 2013** — Бен Каменс, разработчик Khan Academy, публикует разбор «Breaking down Amazon's mega
158
+ dropdown». Он показывает, что Amazon не тормозит открытие задержкой, как большинство выпадающих меню, а следит
159
+ за направлением курсора: пока тот внутри треугольника между курсором и углами панели, подменю не меняется.
160
+ Khan Academy ставит приём в своё новое меню «Learn».
161
+ - **Март 2013** — Каменс выкладывает плагин `jQuery-menu-aim` на GitHub: треугольник, допуск у углов и
162
+ задержка 300 мс для курсора, который остановился.
163
+ - **7 марта 2013** — пересказ статьи на Хабре, с которого приём разошёлся по рунету.
164
+ - **Дальше** — приём уходит в библиотеки: «безопасный многоугольник» `safePolygon()` в Floating UI, «grace
165
+ area» подменю в Radix, «безопасный треугольник» `useSafelyMouseToSubmenu` в react-aria. В react-aria подменю
166
+ вышли альфой 13 февраля 2024 года (v1.1.0) и стали стабильными 1 мая 2024 года (v1.2.0).
167
+ - **2026** — `MegaMenu` в `@olddevs/ui`: модель react-aria и пауза Каменса.
168
+
169
+ Первоисточники:
170
+ [статья Бена Каменса](https://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown),
171
+ [репозиторий `jQuery-menu-aim`](https://github.com/kamens/jQuery-menu-aim),
172
+ [пересказ на Хабре](https://habr.com/ru/articles/171905/).
173
+
174
+ ## Клавиатура и имена
175
+
176
+ - Столбец — одна остановка Tab. Стрелки вверх и вниз, Home и End ходят по Категориям; выключенные
177
+ пропускаются. Вернувшись Tab'ом, пользователь попадает на ту Категорию, где был.
178
+ - Enter, Пробел и стрелка в сторону Витрины открывают её и ставят фокус на первую ссылку. У Категории-ссылки
179
+ Enter — переход.
180
+ - Внутри Витрины работает обычный Tab. Стрелка в сторону Столбца возвращает фокус на Категорию, Escape — тоже,
181
+ и закрывает Витрину. Tab за последней ссылкой уходит дальше по странице и закрывает Витрину.
182
+ - Пока Витрина открыта, она следует за фокусом стрелок, как следует за наведением.
183
+ - Стрелка «к Витрине» — та, что смотрит туда, где Витрина стоит: при письме справа налево и у края экрана
184
+ стрелки меняются местами.
185
+ - Скринридер слышит навигацию с именем из `aria-label`, Категории — кнопками «развёрнуто / свёрнуто», Витрину
186
+ — регионом с именем Категории, колонку — списком с именем заголовка.
187
+
188
+ ## Выпадающий вид
189
+
190
+ Каталог в шапке открывают кнопкой «Каталог», а не держат на странице. Разметка Категорий та же, что во встроенном
191
+ виде, меняется только обёртка: `MegaMenu` кладут в `MegaMenuTrigger` вторым после кнопки `MegaMenuButton`.
192
+
193
+ ```tsx
194
+ import {
195
+ MegaMenu,
196
+ MegaMenuButton,
197
+ MegaMenuCategory,
198
+ MegaMenuGroup,
199
+ MegaMenuLink,
200
+ MegaMenuTrigger,
201
+ } from "@olddevs/ui";
202
+ import { ListIcon } from "@olddevs/ui/icons";
203
+
204
+ <MegaMenuTrigger>
205
+ <MegaMenuButton icon={<ListIcon size={16} />}>Каталог</MegaMenuButton>
206
+ <MegaMenu aria-label="Каталог">
207
+ <MegaMenuCategory id="computers" label="Компьютеры" href="/computers">
208
+ <MegaMenuGroup title="Ноутбуки">
209
+ <MegaMenuLink href="/computers/laptops/gaming">Игровые</MegaMenuLink>
210
+ </MegaMenuGroup>
211
+ </MegaMenuCategory>
212
+ </MegaMenu>
213
+ </MegaMenuTrigger>;
214
+ ```
215
+
216
+ `MegaMenuTrigger`: `open`, `defaultOpen` и `onOpenChange` — открытость, управляемо или нет, как у других
217
+ оверлеев кита. Первый ребёнок — кнопка, все остальные уходят в оверлей; разбор идёт по позиции, поэтому кнопку
218
+ можно обернуть своим компонентом, а `MegaMenu` — вынести в свой. `MegaMenuButton` принимает вид кнопки кита
219
+ (`variant`, `tone`, `size`, `iconOnly`, `selected`), `icon`, `disabled`, `aria-label` и `className`; вид тот же, что у
220
+ `MenuButton`.
221
+
222
+ - **Открытие.** Щелчок, Enter, Пробел и стрелка вниз на кнопке. Наведение каталог не открывает: он не должен
223
+ выпрыгивать от случайного движения мыши.
224
+ - **Фокус.** При открытии фокус встаёт на первую доступную Категорию (если одна из них уже открыта через `value` или
225
+ `defaultValue` — на неё), Витрина закрыта до выбора. Дальше Столбец и Витрина работают как во встроенном виде.
226
+ - **Закрытие.** Escape на Категории, нажатие вне каталога, повторный щелчок по кнопке, переход по любой ссылке
227
+ (Категория, заголовок колонки, `MegaMenuLink`, в том числе ссылки роутера) и Tab за последним элементом.
228
+ Первый Escape внутри Витрины закрывает только Витрину и возвращает фокус на её Категорию; оверлей закрывает
229
+ следующий.
230
+ - **Возврат фокуса.** На кнопку фокус возвращается при Escape и при повторном щелчке. После Tab за последним
231
+ элементом фокус идёт дальше по странице, за кнопкой; Shift+Tab перед первым элементом возвращает на кнопку. После
232
+ нажатия вне каталога фокус остаётся там, куда нажал пользователь.
233
+ - **Скринридер.** Кнопка — раскрыватель с `aria-expanded` и `aria-controls`, без `aria-haspopup`: оверлей — не
234
+ меню и не диалог, а та же навигация с ссылками. Роли `dialog` у оверлея нет.
235
+ - **Слои.** Оверлей лежит на слое `popover`: выше затемнения открытого `Dialog`, ниже подтверждения. Внутри окна
236
+ первый Escape закрывает каталог, следующий — окно. Если фокус в открытой Витрине, самый первый Escape закрывает
237
+ только её и возвращает фокус на Категорию.
238
+ - **Высота.** Оверлей не выше видимой области от кнопки до края окна. Столбец и Витрина прокручиваются каждый
239
+ внутри себя (`overscroll-contain`), а не оверлей целиком: иначе рамка оверлея обрезала бы Витрину, которая
240
+ стоит рядом со Столбцом. На iOS высота ограничена `min(24rem, 60dvh)`, как у панели `Menu`.
241
+ - **Появление.** Плавное, по шкале движения кита; при `prefers-reduced-motion` без анимации.
242
+ - **Сервер.** На сервере рисуется закрытая кнопка; оверлей монтируется в портале после открытия.
243
+
244
+ ## Связанные исходники и проверка
245
+
246
+ - Реализация: `src/components/navigation/MegaMenu.tsx`, Прицел — `src/components/navigation/useMenuAim.ts`.
247
+ - Тесты: `tests/mega-menu.test.tsx` — роли и `aria-controls`, наведение и монтирование только открытой
248
+ Витрины, закрытие с задержкой, roving tabindex, Enter, Escape и стрелки, Tab за последней ссылкой,
249
+ Категория-ссылка, выключенная Категория, `current`, `asChild` и `link`, управляемое значение, нажатие вне
250
+ каталога, рендер на сервере; `tests/mega-menu-aim.test.tsx` — Прицел на подменённой геометрии и поддельном
251
+ времени, переворот у края; `tests/mega-menu-touch.test.tsx` — касание и перо, подмена `matchMedia`, спуск
252
+ вместо Столбца, «Назад» и фокус, Escape, подписи из каталога сообщений, рендер на сервере; браузерный тест `tests-visual/mega-menu.spec.ts` — настоящая мышь наискосок и вниз
253
+ по Столбцу.
254
+ - Выпадающий вид: `tests/mega-menu-dropdown.test.tsx` — открытие щелчком и клавишами с фокусом на первой
255
+ Категории, все пути закрытия и возврат фокуса, Escape в Витрине, `open` / `defaultOpen` / `onOpenChange`, слой
256
+ `popover`, порядок Escape внутри `Dialog`, рендер на сервере.
257
+ - История: `src/components/navigation/MegaMenu.stories.tsx` — в том числе `DropdownInTopBar`, мегаменю в шапке; на ширине
258
+ телефона — `PhoneColumn` и `PhoneDrillDown`.
@@ -5,7 +5,7 @@ category: overlays
5
5
  status: stable
6
6
  source: src/components/overlays/Menu.tsx
7
7
  story: src/components/overlays/Menu.stories.tsx
8
- sourceHash: cedb4db63593
8
+ sourceHash: 473c4ffa0e0a
9
9
  ---
10
10
 
11
11
  # Menu
@@ -15,7 +15,11 @@ sourceHash: cedb4db63593
15
15
  `Menu` is a compound action menu opened by `MenuTrigger`. Inside `MenuTrigger`,
16
16
  use the dedicated `MenuButton`, not a regular `Button`: only it reads react-aria's
17
17
  `ButtonContext`. The menu suits heterogeneous commands, sections, selected modes
18
- and submenus; it is not for choosing a field value.
18
+ and submenus; it is not for choosing a field value. A nested `Menu` inside
19
+ `SubmenuTrigger` opens beside its item on its own, and below it on a narrow
20
+ screen: you do not need to set `placement`. The "aim" (protection that keeps a
21
+ submenu from closing while the pointer travels toward it) comes from react-aria;
22
+ do not write your own.
19
23
 
20
24
  ## When to use
21
25
 
@@ -52,8 +56,9 @@ the props that set the button's look, `icon`, `children`, `disabled`, `aria-labe
52
56
  and `className`; `aria-label` is required if the button has no text, and with
53
57
  `iconOnly` it never does. `Menu` accepts the usual `AriaMenuProps`, plus `offset`,
54
58
  the gap between the panel and the button in pixels (default 6), and `placement`,
55
- the side where the panel opens (default `bottom start`; also `bottom end`,
56
- `top start`, `top end`, `right top` and `left top`).
59
+ the side where the panel opens (default for the root menu `bottom start`; also
60
+ `bottom end`, `top start`, `top end`, `right top` and `left top`; the submenu
61
+ default is described below).
57
62
 
58
63
  `MenuItem` accepts the `AriaMenuItem` props, `icon`, `shortcut`, `tone`,
59
64
  `disabled` and `className`; pass the action in `onAction`. `MenuSection` accepts
@@ -61,6 +66,19 @@ the side where the panel opens (default `bottom start`; also `bottom end`,
61
66
  `selectedKeys` and `onSelectionChange` on the section, not `checked` on the item.
62
67
  `MenuSeparator` separates groups.
63
68
 
69
+ A submenu, a `Menu` inside a `SubmenuTrigger`, opens to the right of its item by
70
+ default, with its top edge level with the item. When there is not enough room on
71
+ the right, react-aria flips it to the left. In a right-to-left layout everything is
72
+ mirrored: the submenu opens to the left of its item. On a narrow screen (up to 639px wide)
73
+ there is no room on either side, so the submenu opens below its item, over the
74
+ parent menu. An explicit `placement` on the nested `Menu` wins over the default,
75
+ phones included. The root menu opens as before, at `bottom start`. The Editor's
76
+ submenus follow the same rule.
77
+
78
+ Moving the pointer diagonally toward a submenu neither closes it nor switches to a
79
+ neighboring item: this "aim" is handled by react-aria's `SubmenuTrigger`. The kit
80
+ does not replace it and adds none of its own, so do not write one on top of it.
81
+
64
82
  react-aria sets the menu panel's height from the visible area. Items that do
65
83
  not fit scroll inside the panel (`overflow-y: auto`, the scroll does not leak to
66
84
  the page) instead of being rendered past its edge over the content. On iOS the panel
@@ -14,7 +14,10 @@ story: src/components/overlays/Menu.stories.tsx
14
14
  `Menu` — составное меню действий, открываемое `MenuTrigger`. Внутри `MenuTrigger`
15
15
  используйте специальный `MenuButton`, а не обычный `Button`: только он читает
16
16
  `ButtonContext` react-aria. Меню подходит для разнородных команд, разделов, выбранных режимов и
17
- подменю; не для выбора значения поля.
17
+ подменю; не для выбора значения поля. Вложенный `Menu` в `SubmenuTrigger` сам
18
+ раскрывается сбоку от пункта, на узком экране под ним: `placement` задавать не
19
+ нужно. «Прицел» (защита подменю от случайного закрытия при движении курсора к
20
+ нему) даёт react-aria, свой не пишите.
18
21
 
19
22
  ## Когда использовать
20
23
 
@@ -50,9 +53,9 @@ story: src/components/overlays/Menu.stories.tsx
50
53
  button look props, `icon`, `children`, `disabled`, `aria-label` и `className`;
51
54
  `aria-label` обязателен, если у кнопки нет подписи; с `iconOnly` её нет всегда.
52
55
  `Menu` принимает обычные `AriaMenuProps`, плюс `offset` — отступ панели от кнопки
53
- в пикселях (умолчание 6) и `placement` — сторона раскрытия (умолчание
54
- `bottom start`; ещё `bottom end`, `top start`, `top end`, `right top`,
55
- `left top`).
56
+ в пикселях (умолчание 6) и `placement` — сторона раскрытия (умолчание корневого
57
+ меню `bottom start`; ещё `bottom end`, `top start`, `top end`, `right top`,
58
+ `left top`; умолчание подменю — ниже).
56
59
 
57
60
  `MenuItem` принимает props `AriaMenuItem`, `icon`, `shortcut`, `tone`, `disabled`
58
61
  и `className`; действие передавайте в `onAction`. `MenuSection` принимает
@@ -60,6 +63,19 @@ button look props, `icon`, `children`, `disabled`, `aria-label` и `className`;
60
63
  `selectionMode`, `selectedKeys` и `onSelectionChange` на section, а не `checked`
61
64
  на item. `MenuSeparator` разделяет группы.
62
65
 
66
+ Подменю — `Menu` внутри `SubmenuTrigger` — по умолчанию раскрывается справа от
67
+ своего пункта, верхним краем вровень с ним. Если справа не хватает места,
68
+ react-aria переворачивает подменю влево. При письме справа налево всё зеркально:
69
+ подменю раскрывается слева от пункта. На узком экране (до 639px шириной) места
70
+ сбоку нет ни справа, ни слева, и подменю встаёт под своим пунктом, поверх
71
+ родительского меню. Явный `placement` у вложенного `Menu` побеждает умолчание, в
72
+ том числе на телефоне. Корневое меню раскрывается как прежде — `bottom start`.
73
+ То же правило у подменю Редактора.
74
+
75
+ Движение курсора к подменю по диагонали его не закрывает и не переключает на
76
+ соседний пункт: этот «Прицел» работает в `SubmenuTrigger` из react-aria. Кит его
77
+ не подменяет и своего не добавляет — не пишите поверх него свой.
78
+
63
79
  Высоту панели меню задаёт react-aria по видимой области. Пункты, которые в неё
64
80
  не влезли, прокручиваются внутри панели (`overflow-y: auto`, прокрутка не
65
81
  уходит на страницу), а не рисуются за её краем поверх содержимого. На iOS
@@ -1,5 +1,5 @@
1
1
  ---
2
- sourceHash: 19857ad0f54d
2
+ sourceHash: 5f89459633fb
3
3
  ---
4
4
 
5
5
  # Setting up `@olddevs/ui`
@@ -132,10 +132,10 @@ peer dependencies are treated as external, and the metric is gzip:
132
132
  | `cn` and `tableStateToParams` | 0.6 kB |
133
133
  | one `Button` | 1.6 kB |
134
134
  | `DataTable` | 9.0 kB |
135
- | the whole kit (`import *`) | 71.1 kB |
136
- | `RichTextEditor` from `@olddevs/ui/editor`, without the engine, "start" | 70.4 kB |
137
- | `RichTextEditor` with the Tiptap engine, "start" (static graph, `basic`) | 295.1 kB |
138
- | `RichTextEditor` with the Tiptap engine, "full" (with all lazy chunks, `full`) | 338.9 kB |
135
+ | the whole kit (`import *`) | 76.4 kB |
136
+ | `RichTextEditor` from `@olddevs/ui/editor`, without the engine, "start" | 70.6 kB |
137
+ | `RichTextEditor` with the Tiptap engine, "start" (static graph, `basic`) | 295.3 kB |
138
+ | `RichTextEditor` with the Tiptap engine, "full" (with all lazy chunks, `full`) | 339.1 kB |
139
139
  | `RichTextView` from `@olddevs/ui/editor`, Tiptap engine included in the measurement | 12.0 kB |
140
140
  | `sanitizeRichText` and `richTextSanitizeConfig` from `@olddevs/ui/editor/schema`, Tiptap engine included | 3.6 kB |
141
141
  | `highlightCodeBlocks` from `@olddevs/ui/editor/highlight`, with lowlight | 22.6 kB |
@@ -129,10 +129,10 @@ peer-зависимости внешние, метрика gzip:
129
129
  | `cn` и `tableStateToParams` | 0,6 кБ |
130
130
  | одна `Button` | 1,6 кБ |
131
131
  | `DataTable` | 9,0 кБ |
132
- | весь кит целиком (`import *`) | 71,1 кБ |
133
- | `RichTextEditor` из `@olddevs/ui/editor`, без движка, «старт» | 70,4 кБ |
134
- | `RichTextEditor` вместе с движком Tiptap, «старт» (статический граф, `basic`) | 295,1 кБ |
135
- | `RichTextEditor` вместе с движком Tiptap, «полный» (со всеми ленивыми чанками, `full`) | 338,9 кБ |
132
+ | весь кит целиком (`import *`) | 76,4 кБ |
133
+ | `RichTextEditor` из `@olddevs/ui/editor`, без движка, «старт» | 70,6 кБ |
134
+ | `RichTextEditor` вместе с движком Tiptap, «старт» (статический граф, `basic`) | 295,3 кБ |
135
+ | `RichTextEditor` вместе с движком Tiptap, «полный» (со всеми ленивыми чанками, `full`) | 339,1 кБ |
136
136
  | `RichTextView` из `@olddevs/ui/editor`, движок Tiptap в замер включён | 12,0 кБ |
137
137
  | `sanitizeRichText` и `richTextSanitizeConfig` из `@olddevs/ui/editor/schema`, движок Tiptap в замер включён | 3,6 кБ |
138
138
  | `highlightCodeBlocks` из `@olddevs/ui/editor/highlight`, вместе с lowlight | 22,6 кБ |
@@ -1,5 +1,5 @@
1
1
  ---
2
- sourceHash: 7b328de94ead
2
+ sourceHash: 2524e6589eaa
3
3
  ---
4
4
 
5
5
  # Glossary of `@olddevs/ui`
@@ -124,6 +124,11 @@ search for a single word.
124
124
  | **tag** | `Tag` in `TagGroup` and `TagInput`: a compact element with text and an optional remove button | The Editor's Mark and a field label |
125
125
  | **close button** | The button with an "×" that closes a dialog, drawer, popover or notification; in a search field it is the **clear button** | A "Cancel" button. Do not write: cross |
126
126
  | **shortcut list** | `ShortcutsHelp`: a dialog with a list of keyboard shortcuts | The Editor's Shortcuts reference: that one opens from the Toolbar and describes the Editor's shortcuts |
127
+ | **mega menu** | `MegaMenu`: a category column and the showcase of the hovered category, an Amazon-style catalog | `Menu`: that one is for commands, not navigation. Do not write: mega dropdown |
128
+ | **category** | `MegaMenuCategory`: a row of the category column with a label, an icon and its own showcase | A menu item (`MenuItem`) |
129
+ | **category column** | The vertical list of categories in a mega menu | `SideNav`: that one expands branches in place |
130
+ | **showcase** | The area next to the category column with the open category's content: link columns, promos | The Editor toolbar, a popover |
131
+ | **aim** | Deferring the category switch while the pointer cuts diagonally toward the open showcase | An opening delay: aim never slows down a direct hover |
127
132
 
128
133
  ## Editor
129
134
 
package/docs/glossary.md CHANGED
@@ -120,6 +120,11 @@
120
120
  | **Тег** | `Tag` в `TagGroup` и `TagInput`: компактный элемент с текстом и необязательной кнопкой удаления | Меткой Редактора (`Mark`) и подписью поля. Не пишем: таг |
121
121
  | **Кнопка закрытия** | Кнопка с иконкой «×», которая закрывает окно, ящик, поповер или уведомление; у поля поиска — **кнопка очистки** | Кнопкой «Отмена». Не пишем: крестик |
122
122
  | **Свод сочетаний** | `ShortcutsHelp`: окно со списком сочетаний клавиш | Справочником Редактора: тот открывается из Панели и описывает сочетания Редактора |
123
+ | **Мегаменю** | `MegaMenu`: Столбец категорий и Витрина наведённой Категории — каталог в духе Amazon | `Menu`: тот для команд, а не для навигации. Не пишем: мега-дропдаун |
124
+ | **Категория** | `MegaMenuCategory`: строка Столбца с подписью, иконкой и своей Витриной | Пунктом меню (`MenuItem`) |
125
+ | **Столбец категорий** | Вертикальный список Категорий Мегаменю | `SideNav`: тот раскрывает ветки на месте |
126
+ | **Витрина** | Область рядом со Столбцом с содержимым открытой Категории: колонки ссылок, промо | Панелью Редактора и поповером |
127
+ | **Прицел** | Отложенная смена Категории, пока курсор идёт наискосок к открытой Витрине | Задержкой открытия: прямое наведение Прицел не тормозит |
123
128
 
124
129
  ## Редактор
125
130
 
package/llms.txt CHANGED
@@ -108,6 +108,7 @@
108
108
  - [Breadcrumbs](docs/components/navigation/breadcrumbs.md): `Breadcrumbs` показывает путь до текущей страницы.
109
109
  - [Pagination](docs/components/navigation/pagination.md): `Pagination` — controlled pagination для уже разбитой на страницы коллекции.
110
110
  - [Stepper](docs/components/navigation/stepper.md): `Stepper` показывает порядок шагов мастера; текущий индекс задаёт родитель (controlled).
111
+ - [MegaMenu](docs/components/navigation/mega-menu.md): `MegaMenu` — каталог магазина в духе Amazon: слева Столбец категорий, справа от наведённой Категории — её Витрина с колонками ссылок и любым своим содержимым.
111
112
  - [TreeView](docs/components/navigation/tree-view.md): Иерархия с раскрытием, выбором и клавиатурой на `Tree` из react-aria.
112
113
  - [SelectionBar и useStickyOffset](docs/components/navigation/selection-bar.md): `SelectionBar` — полоса массовых действий: «Выбрано 12» и что с этим можно сделать.
113
114
  - [ContextualSaveBar](docs/components/navigation/contextual-save-bar.md): Полоса несохранённых изменений: появляется с первой правкой формы, показывает «есть несохранённые изменения» (или их число) и две кнопки — сохранить и отменить.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olddevs/ui",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "Olddevs UI — design system: tokens, themes and accessible UI primitives",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -194,10 +194,17 @@ export const Toggle = forwardRef<HTMLButtonElement, ToggleProps>(function Toggle
194
194
  оно обёрнуто в свой `<label for>`.
195
195
  */
196
196
  return (
197
- <div className={cn("flex items-center justify-between gap-4", className)}>
197
+ <div className={cn("group/toggle flex items-center justify-between gap-4", className)}>
198
198
  {/* Приглушается только текст: дорожка приглушает себя сама (`disabled:opacity-50`),
199
- и общая прозрачность строки дала бы ей 25 % вместо 50 % — иначе, чем без подписи. */}
200
- <div className={cn("flex min-w-0 flex-col", disabled && "opacity-50")}>
199
+ и общая прозрачность строки дала бы ей 25 % вместо 50 % — иначе, чем без подписи.
200
+ `group-has-[[role=switch]:disabled]/toggle:` — для кнопки, выключенной предком-`fieldset`:
201
+ свойство `disabled` у неё при этом ложно, и одно оно текст не погасило бы. */}
202
+ <div
203
+ className={cn(
204
+ "flex min-w-0 flex-col group-has-[[role=switch]:disabled]/toggle:opacity-50",
205
+ disabled && "opacity-50"
206
+ )}
207
+ >
201
208
  <label
202
209
  id={labelId}
203
210
  htmlFor={switchId}
@@ -68,10 +68,12 @@ export function Truncate({ children, lines = 1, side = "top", className, ...rest
68
68
  // клавиатурой не нужен и мешал бы.
69
69
  tabIndex={truncated ? 0 : undefined}
70
70
  className={cn(
71
- "focus-visible:outline-accent-strong block min-w-0 focus-visible:outline-2 focus-visible:outline-offset-2",
71
+ "focus-visible:outline-accent-strong min-w-0 focus-visible:outline-2 focus-visible:outline-offset-2",
72
72
  lines === 1
73
- ? "truncate"
74
- : // Многострочная обрезка: `line-clamp` считает строки сам, и число
73
+ ? "block truncate"
74
+ : // `block` сюда нельзя: в CSS он стоит позже `[display:-webkit-box]`,
75
+ // перебивает его, и `line-clamp` молча не работает.
76
+ // Многострочная обрезка: `line-clamp` считает строки сам, и число
75
77
  // приходит переменной — иначе класс собирался бы конкатенацией, а
76
78
  // такого текста в исходниках нет и Tailwind его не увидит.
77
79
  "[display:-webkit-box] overflow-hidden [-webkit-box-orient:vertical]",
@@ -263,7 +263,15 @@ export const MobileTabBarItem = forwardRef<HTMLAnchorElement, MobileTabBarItemPr
263
263
  {custom ? (
264
264
  <span className="absolute top-[-2px] -right-1.5 flex">{custom}</span>
265
265
  ) : shown ? (
266
- <Badge overlay dot={dot} className="top-[-2px] -right-1.5">
266
+ // Значок держится ЛЕВОЙ кромкой у правого верхнего угла иконки и растёт
267
+ // вправо: привязка правой кромкой (`-right-1.5`) растила широкий «99+» или
268
+ // «Beta» влево, на иконку. Сдвиг `-ml-*` повторяет прежнее положение точки
269
+ // (8px) и короткого числа (16px): они выглядят как раньше.
270
+ <Badge
271
+ overlay
272
+ dot={dot}
273
+ className={cn("top-[-2px] right-auto left-full", dot ? "-ml-0.5" : "-ml-2.5")}
274
+ >
267
275
  {count ?? text}
268
276
  </Badge>
269
277
  ) : null}
@@ -68,7 +68,12 @@ export interface NavItemProps extends Omit<ComponentPropsWithoutRef<"a">, "child
68
68
  const BASE =
69
69
  "focus-visible:outline-accent-strong flex items-center gap-3 rounded-control border " +
70
70
  "px-3 py-[9px] text-[13px] leading-5 no-underline transition-colors " +
71
- "focus-visible:outline-2 focus-visible:outline-offset-2";
71
+ "focus-visible:outline-2 focus-visible:outline-offset-2 " +
72
+ // Недоступный пункт красится по `aria-disabled`, как у `Button`: у ссылки нет
73
+ // родного `disabled`, и без этого правила пункт, выключенный атрибутом ARIA,
74
+ // выглядел бы рабочим — с тем же цветом и курсором-указателем.
75
+ "aria-disabled:opacity-50 aria-disabled:cursor-default " +
76
+ "aria-disabled:hover:bg-transparent aria-disabled:hover:text-fg-1";
72
77
 
73
78
  const STATE = {
74
79
  active: "border-line bg-bg-3 text-fg-0 font-semibold",
@@ -56,6 +56,9 @@ export function AvatarGroup({
56
56
  const members = names.map((entry) => (typeof entry === "string" ? { name: entry } : entry));
57
57
  const shown = members.slice(0, max);
58
58
  const rest = members.length - shown.length;
59
+ // Перекрытие по размеру круга: на 28px прежние 8px плюс кольцо срезали
60
+ // инициалы слева и плюс у «+N»; на 64px они не мешают.
61
+ const overlap = size === "lg" ? "-ml-2" : "-ml-1";
59
62
 
60
63
  return (
61
64
  <span
@@ -67,7 +70,7 @@ export function AvatarGroup({
67
70
  <span
68
71
  key={`${name}-${index}`}
69
72
  aria-hidden
70
- className={cn("ring-bg-1 relative rounded-full ring-2", index > 0 && "-ml-2")}
73
+ className={cn("ring-bg-1 relative rounded-full ring-2", index > 0 && overlap)}
71
74
  style={{ zIndex: shown.length - index }}
72
75
  >
73
76
  {/* `alt=""`: круг скрыт, имя объявляет группа — подпись у фото была бы вторым чтением. */}
@@ -80,7 +83,8 @@ export function AvatarGroup({
80
83
  className={cn(
81
84
  // `text-fg-1`, а не `fg-2`: на подложке `bg-3` вторая ступень не держит 4.5 —
82
85
  // поймал аудит axe. Остаток «+N» это содержимое, а не подсказка.
83
- "bg-bg-3 text-fg-1 ring-bg-1 relative -ml-2 flex items-center justify-center rounded-full font-semibold ring-2",
86
+ "bg-bg-3 text-fg-1 ring-bg-1 relative flex items-center justify-center rounded-full font-semibold ring-2",
87
+ overlap,
84
88
  size === "lg" ? "text-md h-16 w-16" : "text-caption h-7 w-7"
85
89
  )}
86
90
  >
@@ -298,6 +298,13 @@ export function Table<T extends { id: Key }>({
298
298
  /** Сколько ячеек в строке — столько же обязана занять детальная. */
299
299
  const span = columns.length + (selectable ? 1 : 0) + (expandable ? 1 : 0);
300
300
 
301
+ /*
302
+ Ширина служебных колонок (выбор, раскрытие) идёт тем же путём, что и
303
+ `column.width`: `width` действует только внутри `ResizableTableContainer`,
304
+ вне его колонка растягивается наравне с данными, поэтому нужен CSS-стиль.
305
+ */
306
+ const serviceWidth = resizable ? { width: 40 } : { style: { width: 40 } };
307
+
301
308
  const table = (
302
309
  <AriaTable
303
310
  {...rest}
@@ -339,7 +346,7 @@ export function Table<T extends { id: Key }>({
339
346
  {selectable ? (
340
347
  <Column
341
348
  className="border-line-soft bg-bg-1 text-fg-2 border-b px-3 py-2 text-left font-semibold"
342
- width={40}
349
+ {...serviceWidth}
343
350
  >
344
351
  {/*
345
352
  Флажок «выделить всё» — родной RAC (`slot="selection"`), а не наш
@@ -354,7 +361,7 @@ export function Table<T extends { id: Key }>({
354
361
  {expandable ? (
355
362
  <Column
356
363
  id={`${COLUMN_KEY}od-expander`}
357
- width={40}
364
+ {...serviceWidth}
358
365
  className="border-line-soft bg-bg-1 border-b px-3 py-2"
359
366
  >
360
367
  {/*
@@ -401,7 +408,12 @@ export function Table<T extends { id: Key }>({
401
408
  />
402
409
  ) : null}
403
410
  {resizable ? (
404
- <ColumnResizer className="bg-line-soft hover:bg-accent-strong ml-auto w-px cursor-col-resize" />
411
+ <ColumnResizer
412
+ className={cn(
413
+ "bg-line-soft hover:bg-accent-strong w-px cursor-col-resize",
414
+ column.align !== "end" && "ml-auto"
415
+ )}
416
+ />
405
417
  ) : null}
406
418
  </span>
407
419
  )}