@olddevs/ui 4.1.1 → 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 (62) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/components/navigation/MegaMenu.d.ts +156 -0
  3. package/dist/components/navigation/MegaMenu.d.ts.map +1 -0
  4. package/dist/components/navigation/MegaMenu.js +512 -0
  5. package/dist/components/navigation/MegaMenu.js.map +1 -0
  6. package/dist/components/navigation/useMenuAim.d.ts +41 -0
  7. package/dist/components/navigation/useMenuAim.d.ts.map +1 -0
  8. package/dist/components/navigation/useMenuAim.js +114 -0
  9. package/dist/components/navigation/useMenuAim.js.map +1 -0
  10. package/dist/components/overlays/Menu.d.ts +19 -1
  11. package/dist/components/overlays/Menu.d.ts.map +1 -1
  12. package/dist/components/overlays/Menu.js +16 -4
  13. package/dist/components/overlays/Menu.js.map +1 -1
  14. package/dist/editor/features/table/TableMenuButton.js +1 -1
  15. package/dist/editor/features/table/TableMenuButton.js.map +1 -1
  16. package/dist/editor/ui/BlockHandle.js +1 -1
  17. package/dist/editor/ui/BlockHandle.js.map +1 -1
  18. package/dist/editor/ui/FormatMenus.js +1 -1
  19. package/dist/editor/ui/FormatMenus.js.map +1 -1
  20. package/dist/{editor/hooks → hooks}/useSubmenuPlacement.d.ts +14 -1
  21. package/dist/hooks/useSubmenuPlacement.d.ts.map +1 -0
  22. package/dist/hooks/useSubmenuPlacement.js +10 -0
  23. package/dist/hooks/useSubmenuPlacement.js.map +1 -0
  24. package/dist/i18n/defaults.d.ts.map +1 -1
  25. package/dist/i18n/defaults.js +3 -0
  26. package/dist/i18n/defaults.js.map +1 -1
  27. package/dist/i18n/messages.d.ts +8 -0
  28. package/dist/i18n/messages.d.ts.map +1 -1
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/locales/ru.d.ts.map +1 -1
  34. package/dist/locales/ru.js +3 -0
  35. package/dist/locales/ru.js.map +1 -1
  36. package/docs/api/messages.md +6 -0
  37. package/docs/components/index.en.md +2 -1
  38. package/docs/components/index.md +1 -0
  39. package/docs/components/navigation/mega-menu.en.md +273 -0
  40. package/docs/components/navigation/mega-menu.md +258 -0
  41. package/docs/components/overlays/menu.en.md +22 -4
  42. package/docs/components/overlays/menu.md +20 -4
  43. package/docs/getting-started.en.md +4 -4
  44. package/docs/getting-started.md +3 -3
  45. package/docs/glossary.en.md +6 -1
  46. package/docs/glossary.md +5 -0
  47. package/llms.txt +1 -0
  48. package/package.json +1 -1
  49. package/src/components/navigation/MegaMenu.tsx +1223 -0
  50. package/src/components/navigation/useMenuAim.ts +202 -0
  51. package/src/components/overlays/Menu.tsx +47 -4
  52. package/src/editor/features/table/TableMenuButton.tsx +1 -1
  53. package/src/editor/ui/BlockHandle.tsx +1 -1
  54. package/src/editor/ui/FormatMenus.tsx +1 -1
  55. package/src/{editor/hooks → hooks}/useSubmenuPlacement.ts +18 -15
  56. package/src/i18n/defaults.ts +4 -0
  57. package/src/i18n/messages.ts +9 -0
  58. package/src/index.ts +17 -0
  59. package/src/locales/ru.ts +4 -0
  60. package/dist/editor/hooks/useSubmenuPlacement.d.ts.map +0 -1
  61. package/dist/editor/hooks/useSubmenuPlacement.js +0 -20
  62. package/dist/editor/hooks/useSubmenuPlacement.js.map +0 -1
@@ -0,0 +1,202 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, type RefObject } from "react";
4
+
5
+ /*
6
+ ПРИЦЕЛ МЕГАМЕНЮ — приём Amazon, разобранный Беном Каменсом в 2013 году.
7
+
8
+ Курсор идёт от Категории к её Витрине наискосок и по пути задевает соседние
9
+ Категории. Наивное «наведение открывает» сменило бы Витрину у человека под
10
+ рукой. Прицел смотрит, КУДА движется курсор: пока он идёт в сторону открытой
11
+ Витрины, соседняя Категория наведение не перехватывает.
12
+
13
+ Модель — как у `useSafelyMouseToSubmenu` в react-aria, чтобы подменю `Menu` и
14
+ мегаменю вели себя одинаково: направление последнего шага сравнивается с
15
+ сектором от предыдущей точки к двум углам ближнего края Витрины; смена
16
+ направления прощается; обработка прорежена до кадра; только мышь.
17
+
18
+ Два осознанных расхождения с react-aria:
19
+
20
+ 1. Механизм — ОТЛОЖЕННАЯ СМЕНА Категории, а не `pointer-events: none` на
21
+ Столбце. Отключённые события гасят подсветку под курсором и съедают щелчок
22
+ по соседней Категории, а щелчок должен открывать сразу.
23
+ 2. Время сброса — 300 мс (значение Каменса), а не 1000 мс react-aria. Его
24
+ подменю узкое и короткое; Столбец каталога листают, и секунда ожидания на
25
+ каждой остановке ощущается как залипание.
26
+ */
27
+
28
+ /** Курсор стоит дольше — Категория под ним открывается, даже если он «целился». */
29
+ export const AIM_SETTLE_MS = 300;
30
+
31
+ /** Прореживание: один шаг на кадр. Чаще направление шумит пиксельной дрожью. */
32
+ const THROTTLE_MS = 16;
33
+
34
+ /**
35
+ * Сколько шагов «не туда» подряд прощается. Счётчик растёт на шаг к Витрине и
36
+ * падает на шаг мимо; целится курсор, пока счётчик выше нуля. Отсюда: одно
37
+ * случайное движение вбок прицел не сбрасывает, два подряд — сбрасывают.
38
+ */
39
+ const ALLOWED_INVALID_MOVES = 2;
40
+
41
+ /**
42
+ * Запас сектора по вертикали, пиксели. Без него курсор, идущий точно в угол
43
+ * Витрины, выпадал бы из сектора на дрожании руки.
44
+ */
45
+ const SECTOR_PADDING = 24;
46
+
47
+ interface Point {
48
+ x: number;
49
+ y: number;
50
+ }
51
+
52
+ /**
53
+ * Идёт ли шаг `from → to` к Витрине: направление шага лежит внутри сектора от
54
+ * `from` к двум углам её ближнего края.
55
+ *
56
+ * Векторное произведение, а не `atan2`: тот же сектор без разрыва угла на ±π.
57
+ * Ось y экрана направлена вниз, отсюда знаки.
58
+ */
59
+ export function isMovingToward(from: Point, to: Point, rect: DOMRect, side: "left" | "right") {
60
+ const dx = to.x - from.x;
61
+ const dy = to.y - from.y;
62
+ if (dx === 0 && dy === 0) return false;
63
+ const edge = side === "right" ? rect.left : rect.right;
64
+ // Курсор уже за ближним краем — он в Витрине, а не на пути к ней.
65
+ if (side === "right" ? from.x >= edge : from.x <= edge) return false;
66
+ // Шаг от Витрины по горизонтали не может к ней вести.
67
+ if (side === "right" ? dx <= 0 : dx >= 0) return false;
68
+ const top = { x: edge - from.x, y: rect.top - SECTOR_PADDING - from.y };
69
+ const bottom = { x: edge - from.x, y: rect.bottom + SECTOR_PADDING - from.y };
70
+ const cross = (a: Point, b: Point) => a.x * b.y - a.y * b.x;
71
+ const step = { x: dx, y: dy };
72
+ // Справа: шаг «ниже» луча к верхнему углу и «выше» луча к нижнему. Слева
73
+ // ориентация лучей зеркальна, и знаки меняются местами.
74
+ return side === "right"
75
+ ? cross(top, step) >= 0 && cross(step, bottom) >= 0
76
+ : cross(top, step) <= 0 && cross(step, bottom) <= 0;
77
+ }
78
+
79
+ const NONE = Symbol("none");
80
+
81
+ interface MenuAimOptions {
82
+ /** Открытая Категория; `null` — Витрины нет, и целиться не во что. */
83
+ value: string | null;
84
+ /** Открытая сейчас Витрина: её прямоугольник — цель. */
85
+ showcaseRef: RefObject<HTMLElement | null>;
86
+ /** Сменить Категорию (или закрыть Витрину — `null`). */
87
+ commit: (next: string | null) => void;
88
+ }
89
+
90
+ /**
91
+ * Прицел: решает, сменить Категорию под курсором сейчас или отложить.
92
+ *
93
+ * `request` зовётся из наведения на строку Столбца. Если Витрины нет или
94
+ * курсор не целится — смена сразу. Если целится — смена откладывается: курсор
95
+ * перестал целиться или стоит `AIM_SETTLE_MS` — открывается то, что под ним
96
+ * сейчас; дошёл до Витрины — смена отменяется.
97
+ *
98
+ * Прямоугольник Витрины берётся заново на каждом шаге, а не кешируется:
99
+ * прокрутка страницы и смена размера окна не оставляют его устаревшим, а цена
100
+ * — одно чтение раскладки на кадр, и только пока Витрина открыта.
101
+ */
102
+ export function useMenuAim({ value, showcaseRef, commit }: MenuAimOptions) {
103
+ const previous = useRef<Point | null>(null);
104
+ const lastStep = useRef(0);
105
+ const aim = useRef(0);
106
+ const pending = useRef<string | null | typeof NONE>(NONE);
107
+ const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
108
+ const commitRef = useRef(commit);
109
+ commitRef.current = commit;
110
+
111
+ const clearTimer = () => {
112
+ if (timer.current) clearTimeout(timer.current);
113
+ timer.current = null;
114
+ };
115
+
116
+ const flush = useCallback(() => {
117
+ clearTimer();
118
+ const next = pending.current;
119
+ pending.current = NONE;
120
+ if (next !== NONE) commitRef.current(next);
121
+ }, []);
122
+
123
+ const settleLater = useCallback(() => {
124
+ clearTimer();
125
+ timer.current = setTimeout(flush, AIM_SETTLE_MS);
126
+ }, [flush]);
127
+
128
+ // Новая Витрина — новая цель: прежний счёт к ней отношения не имеет.
129
+ useEffect(() => {
130
+ aim.current = 0;
131
+ previous.current = null;
132
+ pending.current = NONE;
133
+ clearTimer();
134
+ }, [value]);
135
+
136
+ useEffect(() => clearTimer, []);
137
+
138
+ useEffect(() => {
139
+ if (value === null) return;
140
+ const onPointerMove = (event: PointerEvent) => {
141
+ if (event.pointerType === "touch" || event.pointerType === "pen") return;
142
+ const now = Date.now();
143
+ if (now - lastStep.current < THROTTLE_MS) return;
144
+ lastStep.current = now;
145
+ const point = { x: event.clientX, y: event.clientY };
146
+ const showcase = showcaseRef.current;
147
+ const from = previous.current;
148
+ previous.current = point;
149
+ if (!showcase || !from) return;
150
+
151
+ // Курсор дошёл до Витрины — отложенная смена больше не нужна.
152
+ if (event.target instanceof Node && showcase.contains(event.target)) {
153
+ pending.current = NONE;
154
+ clearTimer();
155
+ return;
156
+ }
157
+
158
+ const rect = showcase.getBoundingClientRect();
159
+ const side = rect.left >= from.x ? "right" : "left";
160
+ aim.current = isMovingToward(from, point, rect, side)
161
+ ? Math.min(aim.current + 1, ALLOWED_INVALID_MOVES)
162
+ : Math.max(aim.current - 1, 0);
163
+
164
+ if (pending.current === NONE) return;
165
+ // Перестал целиться — открывается то, над чем он сейчас; иначе ждём
166
+ // остановки заново: каждый шаг откладывает срок.
167
+ if (aim.current === 0) flush();
168
+ else settleLater();
169
+ };
170
+ document.addEventListener("pointermove", onPointerMove);
171
+ return () => document.removeEventListener("pointermove", onPointerMove);
172
+ }, [value, showcaseRef, flush, settleLater]);
173
+
174
+ /** Курсор наведён на строку Столбца: открыть её (`null` — закрыть Витрину). */
175
+ const request = useCallback(
176
+ (next: string | null) => {
177
+ if (next === value) {
178
+ // Вернулся на открытую Категорию — отложенная смена отменяется.
179
+ pending.current = NONE;
180
+ clearTimer();
181
+ return;
182
+ }
183
+ if (value === null || aim.current === 0) {
184
+ pending.current = NONE;
185
+ clearTimer();
186
+ commitRef.current(next);
187
+ return;
188
+ }
189
+ pending.current = next;
190
+ settleLater();
191
+ },
192
+ [value, settleLater]
193
+ );
194
+
195
+ /** Отменить отложенную смену: щелчок, клавиатура, уход из меню решают сами. */
196
+ const cancel = useCallback(() => {
197
+ pending.current = NONE;
198
+ clearTimer();
199
+ }, []);
200
+
201
+ return { request, cancel };
202
+ }
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { forwardRef, type ReactNode } from "react";
3
+ import { forwardRef, useContext, type ReactNode } from "react";
4
4
  import {
5
5
  Button as AriaButton,
6
6
  Menu as AriaMenu,
@@ -8,6 +8,7 @@ import {
8
8
  MenuSection as AriaMenuSection,
9
9
  Header,
10
10
  Popover,
11
+ PopoverContext,
11
12
  Separator,
12
13
  type MenuItemProps as AriaMenuItemProps,
13
14
  type MenuProps as AriaMenuProps,
@@ -15,6 +16,7 @@ import {
15
16
  } from "react-aria-components";
16
17
  import { CheckIcon, ChevronRightIcon } from "../../icons.js";
17
18
  import { buttonClasses, type ButtonLookOptions } from "../../lib/button.js";
19
+ import { useSubmenuPlacement } from "../../hooks/useSubmenuPlacement.js";
18
20
  import { cn } from "../../lib/cn.js";
19
21
  import {
20
22
  MENU_ITEM_CLASS,
@@ -83,7 +85,12 @@ export const MenuButton = forwardRef<HTMLButtonElement, MenuButtonProps>(functio
83
85
  export interface MenuProps<T extends object = object> extends AriaMenuProps<T> {
84
86
  /** Отступ панели от кнопки, в пикселях. */
85
87
  offset?: number;
86
- /** Сторона, с которой панель раскрывается. */
88
+ /**
89
+ * Сторона, с которой панель раскрывается. Корневое меню по умолчанию — под
90
+ * кнопкой слева (`bottom start`); вложенное, внутри `SubmenuTrigger`, —
91
+ * сбоку от своего пункта, а на узком экране под ним. Явное значение
92
+ * побеждает оба умолчания.
93
+ */
87
94
  placement?: "bottom start" | "bottom end" | "top start" | "top end" | "right top" | "left top";
88
95
  }
89
96
 
@@ -115,6 +122,19 @@ export interface MenuItemProps extends Omit<AriaMenuItemProps, "children" | "cla
115
122
  * меню руками. Это же делает работоспособным `SubmenuTrigger`: он ждёт вторым
116
123
  * ребёнком именно попоувер.
117
124
  *
125
+ * ПОДМЕНЮ раскрывается сбоку от своего пункта, и это умолчание нужно
126
+ * выставлять самим. `SubmenuTrigger` отдаёт вложенному поповеру место
127
+ * `end top` через контекст, но явный проп поповера важнее контекста, а корневое
128
+ * умолчание кита `bottom start` — как раз явный проп: вложенное меню без
129
+ * особого случая раскрывалось бы под пунктом. Поэтому `Menu` узнаёт подменю по
130
+ * контексту (`trigger: "SubmenuTrigger"`, публичное поле `PopoverContext`) и
131
+ * спрашивает общий хук `useSubmenuPlacement` — тот же, что в Редакторе: на
132
+ * телефоне подменю встаёт под пунктом, на широком экране кит своего места не
133
+ * передаёт, и действует `end top` react-aria — сбоку, с учётом направления
134
+ * письма. Переворот у края экрана
135
+ * (`shouldFlip`) и «Прицел» — движение курсора к подменю по диагонали не
136
+ * закрывает его — остаются у react-aria, кит их не трогает.
137
+ *
118
138
  * ПЕРЕКЛЮЧАТЕЛИ — через выбор на разделе, а не через `checked` у пункта:
119
139
  *
120
140
  * <MenuSection label="Вид" selectionMode="multiple"
@@ -128,11 +148,34 @@ export interface MenuItemProps extends Omit<AriaMenuItemProps, "children" | "cla
128
148
  * чужую работу и расходиться с ней при первом же обновлении.
129
149
  */
130
150
  export const Menu = forwardRef<HTMLDivElement, MenuProps>(function Menu(
131
- { offset = 6, placement = "bottom start", className, ...rest },
151
+ { offset = 6, placement, className, ...rest },
132
152
  ref
133
153
  ) {
154
+ const popoverContext = useContext(PopoverContext);
155
+ // Контекст бывает пустым, со слотами или с самими пропами поповера; «подменю»
156
+ // говорит только последнее.
157
+ const isSubmenu =
158
+ popoverContext != null &&
159
+ "trigger" in popoverContext &&
160
+ popoverContext.trigger === "SubmenuTrigger";
161
+ const submenuPlacement = useSubmenuPlacement();
162
+ // Широкий экран — место не передаётся вовсе: тогда действует `end top` из
163
+ // контекста `SubmenuTrigger`. Оно логическое, и при письме справа налево
164
+ // подменю раскрывается слева, а `right top` хука уводило бы его вправо.
165
+ const resolvedPlacement =
166
+ placement ??
167
+ (isSubmenu
168
+ ? submenuPlacement === "bottom start"
169
+ ? submenuPlacement
170
+ : undefined
171
+ : "bottom start");
172
+
134
173
  return (
135
- <Popover offset={offset} placement={placement} className={cn(MENU_PANEL_CLASS, className)}>
174
+ <Popover
175
+ offset={offset}
176
+ placement={resolvedPlacement}
177
+ className={cn(MENU_PANEL_CLASS, className)}
178
+ >
136
179
  {/*
137
180
  `data-disable-focus-ring` — механизм пакета, тот же, что у `TextControl`.
138
181
  Глобальное правило `:focus-visible` в styles.css рисует контур вокруг
@@ -56,7 +56,7 @@ import {
56
56
  type TableAction,
57
57
  } from "./tableActions.js";
58
58
  import { TableSizeGrid } from "./TableSizeGrid.js";
59
- import { useSubmenuPlacement } from "../../hooks/useSubmenuPlacement.js";
59
+ import { useSubmenuPlacement } from "../../../hooks/useSubmenuPlacement.js";
60
60
  import { GLASS_MENU_CLASS } from "../../styles/glass.js";
61
61
 
62
62
  export interface TableMenuButtonProps {
@@ -42,7 +42,7 @@ import { ariaShortcut, findShortcutKeys, isApple, shortcutLabel } from "../core/
42
42
  import { blockActionsOf, blockActionVisible, type ToolList } from "../core/toolDescriptor.js";
43
43
  import { WRAPPER_NODE } from "../features/wrapper/wrapper.js";
44
44
  import { EDITOR_WRAPPER_TONES, wrapperToneFrom } from "../features/wrapper/wrapperTones.js";
45
- import { useSubmenuPlacement } from "../hooks/useSubmenuPlacement.js";
45
+ import { useSubmenuPlacement } from "../../hooks/useSubmenuPlacement.js";
46
46
  import { GLASS_MENU_CLASS } from "../styles/glass.js";
47
47
 
48
48
  /** Размер ручки и зазор до текста, px: ручка `h-6 w-5` стоит в левом поле Документа. */
@@ -45,7 +45,7 @@ import { DEFAULT_ALIGNMENT, EDITOR_ALIGNMENTS, type EditorAlignment } from "../s
45
45
  import { findShortcutKeys } from "../core/shortcuts.js";
46
46
  import type { ToolList } from "../core/toolDescriptor.js";
47
47
  import type { Tool } from "../core/tools.js";
48
- import { useSubmenuPlacement } from "../hooks/useSubmenuPlacement.js";
48
+ import { useSubmenuPlacement } from "../../hooks/useSubmenuPlacement.js";
49
49
  import { GLASS_MENU_CLASS } from "../styles/glass.js";
50
50
 
51
51
  type EditorMessages = UiMessages["editor"];
@@ -1,24 +1,25 @@
1
1
  "use client";
2
2
 
3
- import { useSyncExternalStore } from "react";
3
+ import { useMediaQuery } from "./useMediaQuery.js";
4
4
 
5
5
  /** Уже `sm` (640): на такой ширине подменю сбоку от пункта не помещается. */
6
- const QUERY = "(max-width: 639px)";
6
+ const NARROW_QUERY = "(max-width: 639px)";
7
7
 
8
- function subscribe(onChange: () => void): () => void {
9
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return () => {};
10
- const list = window.matchMedia(QUERY);
11
- list.addEventListener("change", onChange);
12
- return () => list.removeEventListener("change", onChange);
13
- }
14
-
15
- function snapshot(): boolean {
16
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
17
- return window.matchMedia(QUERY).matches;
8
+ /**
9
+ * Узкий ли экран по правилу кита — граница одна на подменю и мегаменю.
10
+ *
11
+ * Внутренний хук, как и `useSubmenuPlacement`. До гидратации и без `matchMedia`
12
+ * — `false`, то есть широкий: серверной ширины нет, а угадывать значило бы
13
+ * получить расхождение разметки (ADR-0018, см. `useMediaQuery`).
14
+ */
15
+ export function useNarrowScreen(): boolean {
16
+ return useMediaQuery(NARROW_QUERY);
18
17
  }
19
18
 
20
19
  /**
21
- * Где открыть подменю Редактора: сбоку от пункта или под ним.
20
+ * Где открыть подменю: сбоку от пункта или под ним.
21
+ *
22
+ * Правило одно на весь кит: его применяет вложенный `Menu` и подменю Редактора.
22
23
  *
23
24
  * Сбоку — привычно на десктопе. На телефоне места нет ни справа, ни слева:
24
25
  * меню само занимает середину экрана, и react-aria, перевернув подменю на
@@ -33,8 +34,10 @@ function snapshot(): boolean {
33
34
  * JS, а не CSS: место подменю — проп поповера, а не класс. Подписка — экран
34
35
  * поворачивают на лету. На сервере и без `matchMedia` — сбоку: подменю
35
36
  * открывается только в браузере, по действию пользователя.
37
+ *
38
+ * Внутренний хук: наружу не экспортируется. Потребитель кита решает за себя
39
+ * проп `placement` у `Menu`, а умолчание для вложенного меню уже здесь.
36
40
  */
37
41
  export function useSubmenuPlacement(): "bottom start" | "right top" {
38
- const narrow = useSyncExternalStore(subscribe, snapshot, () => false);
39
- return narrow ? "bottom start" : "right top";
42
+ return useNarrowScreen() ? "bottom start" : "right top";
40
43
  }
@@ -347,6 +347,10 @@ export const DEFAULT_MESSAGES: UiMessages = {
347
347
  slide: ({ index, total }) => `${index} of ${total}`,
348
348
  },
349
349
 
350
+ megaMenu: {
351
+ back: "Back",
352
+ },
353
+
350
354
  shortcutsHelp: {
351
355
  title: "Keyboard shortcuts",
352
356
  list: "Shortcuts",
@@ -591,6 +591,15 @@ export interface UiMessages {
591
591
  slide: (vars: { index: number; total: number }) => string;
592
592
  };
593
593
 
594
+ megaMenu: {
595
+ /**
596
+ * Строка возврата на узком экране: открытая Категория заменяет Столбец, и
597
+ * эта кнопка возвращает его. Имя кнопки — одно слово без названия
598
+ * Категории: название стоит рядом текстом, а регион им уже подписан.
599
+ */
600
+ back: string;
601
+ };
602
+
594
603
  shortcutsHelp: {
595
604
  /** Заголовок окна по умолчанию. Он же доступное имя диалога. */
596
605
  title: string;
package/src/index.ts CHANGED
@@ -428,6 +428,23 @@ export type { PaginationProps } from "./components/navigation/Pagination.js";
428
428
  export { Stepper } from "./components/navigation/Stepper.js";
429
429
  export type { StepperProps, Step } from "./components/navigation/Stepper.js";
430
430
 
431
+ export {
432
+ MegaMenu,
433
+ MegaMenuButton,
434
+ MegaMenuCategory,
435
+ MegaMenuGroup,
436
+ MegaMenuLink,
437
+ MegaMenuTrigger,
438
+ } from "./components/navigation/MegaMenu.js";
439
+ export type {
440
+ MegaMenuProps,
441
+ MegaMenuButtonProps,
442
+ MegaMenuCategoryProps,
443
+ MegaMenuGroupProps,
444
+ MegaMenuLinkProps,
445
+ MegaMenuTriggerProps,
446
+ } from "./components/navigation/MegaMenu.js";
447
+
431
448
  export { ToggleGroup } from "./components/actions/ToggleGroup.js";
432
449
  export type { ToggleGroupProps, ToggleGroupOption } from "./components/actions/ToggleGroup.js";
433
450
 
package/src/locales/ru.ts CHANGED
@@ -376,6 +376,10 @@ export const messages: UiMessages = {
376
376
  slide: ({ index, total }) => `${index} из ${total}`,
377
377
  },
378
378
 
379
+ megaMenu: {
380
+ back: "Назад",
381
+ },
382
+
379
383
  shortcutsHelp: {
380
384
  title: "Горячие клавиши",
381
385
  list: "Сочетания клавиш",
@@ -1 +0,0 @@
1
- {"version":3,"file":"useSubmenuPlacement.d.ts","sourceRoot":"","sources":["../../../src/editor/hooks/useSubmenuPlacement.ts"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,IAAI,cAAc,GAAG,WAAW,CAGlE"}
@@ -1,20 +0,0 @@
1
- "use client";
2
- import { useSyncExternalStore } from "react";
3
- const QUERY = "(max-width: 639px)";
4
- function subscribe(onChange) {
5
- if (typeof window === "undefined" || typeof window.matchMedia !== "function")
6
- return () => { };
7
- const list = window.matchMedia(QUERY);
8
- list.addEventListener("change", onChange);
9
- return () => list.removeEventListener("change", onChange);
10
- }
11
- function snapshot() {
12
- if (typeof window === "undefined" || typeof window.matchMedia !== "function")
13
- return false;
14
- return window.matchMedia(QUERY).matches;
15
- }
16
- export function useSubmenuPlacement() {
17
- const narrow = useSyncExternalStore(subscribe, snapshot, () => false);
18
- return narrow ? "bottom start" : "right top";
19
- }
20
- //# sourceMappingURL=useSubmenuPlacement.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useSubmenuPlacement.js","sourceRoot":"","sources":["../../../src/editor/hooks/useSubmenuPlacement.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAG7C,MAAM,KAAK,GAAG,oBAAoB,CAAC;AAEnC,SAAS,SAAS,CAAC,QAAoB;IACrC,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,UAAU,KAAK,UAAU;QAAE,OAAO,GAAG,EAAE,GAAE,CAAC,CAAC;IAC9F,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IACtC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC1C,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAC5D,CAAC;AAED,SAAS,QAAQ;IACf,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,UAAU,KAAK,UAAU;QAAE,OAAO,KAAK,CAAC;IAC3F,OAAO,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC;AAC1C,CAAC;AAmBD,MAAM,UAAU,mBAAmB;IACjC,MAAM,MAAM,GAAG,oBAAoB,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;IACtE,OAAO,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,WAAW,CAAC;AAC/C,CAAC"}