@homecode/ui 5.7.8 → 5.8.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.
package/dist/esm/index.js CHANGED
@@ -28,6 +28,7 @@ export { LangSelector } from './src/components/LangSelector/LangSelector.js';
28
28
  export { Lazy } from './src/components/Lazy/Lazy.js';
29
29
  export { LightBox } from './src/components/LightBox/LightBox.js';
30
30
  export { Menu } from './src/components/Menu/Menu.js';
31
+ export { NestedMenu, NestedMenuItemRow, NestedMenuLabel } from './src/components/NestedMenu/NestedMenu.js';
31
32
  export { Notifications, NotificationsStore } from './src/components/Notifications/Notifications.js';
32
33
  export { Paranja } from './src/components/Paranja/Paranja.js';
33
34
  export { ANIMATION_DURATION, Popup } from './src/components/Popup/Popup.js';
@@ -59,6 +59,7 @@ import '../../services/i18n.js';
59
59
  import '../Portal/Portal.js';
60
60
  import '../Paranja/Paranja.styl.js';
61
61
  import '../LightBox/LightBox.styl.js';
62
+ import '../NestedMenu/NestedMenu.js';
62
63
  import '../Notifications/store.js';
63
64
  import '../Notifications/Notifications.styl.js';
64
65
  import '../PopupMenu/PopupMenu.styl.js';
@@ -54,6 +54,7 @@ import '../Heading/Heading.js';
54
54
  import '../../services/i18n.js';
55
55
  import '../LightBox/LightBox.styl.js';
56
56
  import '../Menu/Menu.js';
57
+ import '../NestedMenu/NestedMenu.js';
57
58
  import '../Notifications/store.js';
58
59
  import '../Notifications/Notifications.styl.js';
59
60
  import '../PopupMenu/PopupMenu.styl.js';
@@ -0,0 +1,118 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import cn from 'classnames';
3
+ import { useRef, useState, useEffect, useLayoutEffect } from 'react';
4
+ import { Icon } from '../Icon/Icon.js';
5
+ import S from './NestedMenu.styl.js';
6
+
7
+ const MOBILE_MQ = '(max-width: 720px)';
8
+ const HOVER_MQ = '(hover: hover) and (pointer: fine)';
9
+ function useMedia(query) {
10
+ const [matches, setMatches] = useState(() => typeof window !== 'undefined' && window.matchMedia(query).matches);
11
+ useEffect(() => {
12
+ const mq = window.matchMedia(query);
13
+ const sync = () => setMatches(mq.matches);
14
+ sync();
15
+ mq.addEventListener('change', sync);
16
+ return () => mq.removeEventListener('change', sync);
17
+ }, [query]);
18
+ return matches;
19
+ }
20
+ function NestedMenuLabel({ children, className }) {
21
+ return jsx("span", { className: cn(S.label, className), children: children });
22
+ }
23
+ function NestedMenuItemRow({ children, className, danger, disabled, href, target, rel, onClick, }) {
24
+ const classes = cn(S.row, danger && S.itemDanger, className);
25
+ if (href) {
26
+ return (jsx("a", { className: classes, href: href, target: target, rel: rel, onClick: onClick, children: children }));
27
+ }
28
+ return (jsx("button", { type: "button", className: classes, disabled: disabled, onClick: onClick, children: children }));
29
+ }
30
+ function NestedMenuComponent({ trigger, items, open, onOpenChange, align = 'end', className, }) {
31
+ const rootRef = useRef(null);
32
+ const [activeId, setActiveId] = useState(null);
33
+ const [flipLeft, setFlipLeft] = useState(false);
34
+ const hoverTimer = useRef(0);
35
+ const stacked = useMedia(MOBILE_MQ);
36
+ const canHover = useMedia(HOVER_MQ);
37
+ const active = items.find(item => item.id === activeId);
38
+ function close() {
39
+ onOpenChange(false);
40
+ setActiveId(null);
41
+ }
42
+ function openSub(id, announce = false) {
43
+ setActiveId(id);
44
+ if (announce) {
45
+ items.find(item => item.id === id)?.onSubmenuOpen?.();
46
+ }
47
+ }
48
+ useEffect(() => {
49
+ if (!open) {
50
+ setActiveId(null);
51
+ return undefined;
52
+ }
53
+ const onKey = (e) => {
54
+ if (e.key === 'Escape')
55
+ close();
56
+ };
57
+ const onPointer = (e) => {
58
+ if (!rootRef.current?.contains(e.target))
59
+ close();
60
+ };
61
+ document.addEventListener('keydown', onKey);
62
+ document.addEventListener('pointerdown', onPointer);
63
+ return () => {
64
+ document.removeEventListener('keydown', onKey);
65
+ document.removeEventListener('pointerdown', onPointer);
66
+ };
67
+ }, [open]);
68
+ useLayoutEffect(() => {
69
+ if (!open || !rootRef.current)
70
+ return;
71
+ const rect = rootRef.current.getBoundingClientRect();
72
+ setFlipLeft(window.innerWidth - rect.right < 280);
73
+ }, [open, activeId]);
74
+ useEffect(() => () => window.clearTimeout(hoverTimer.current), []);
75
+ function onItemEnter(id) {
76
+ if (!canHover || stacked)
77
+ return;
78
+ window.clearTimeout(hoverTimer.current);
79
+ const item = items.find(entry => entry.id === id);
80
+ if (item?.disabled || !item?.submenu) {
81
+ setActiveId(null);
82
+ return;
83
+ }
84
+ openSub(id, false);
85
+ }
86
+ function onItemLeave() {
87
+ if (!canHover || stacked)
88
+ return;
89
+ hoverTimer.current = window.setTimeout(() => setActiveId(null), 160);
90
+ }
91
+ function onItemClick(id) {
92
+ const item = items.find(entry => entry.id === id);
93
+ if (!item || item.disabled)
94
+ return;
95
+ if (item.onClick && !item.submenu) {
96
+ item.onClick();
97
+ close();
98
+ return;
99
+ }
100
+ if (activeId === id && !stacked) {
101
+ setActiveId(null);
102
+ return;
103
+ }
104
+ openSub(id, true);
105
+ }
106
+ function renderItems() {
107
+ return items.map(item => (jsxs("div", { className: S.itemWrap, onMouseEnter: () => onItemEnter(item.id), onMouseLeave: onItemLeave, children: [jsxs("button", { type: "button", className: cn(S.item, activeId === item.id && S.itemActive, item.danger && S.itemDanger, item.disabled && S.itemDisabled, item.className), role: "menuitem", disabled: item.disabled, "aria-haspopup": item.submenu ? 'menu' : undefined, "aria-expanded": item.submenu ? activeId === item.id : undefined, onClick: () => onItemClick(item.id), children: [item.icon && (jsx("span", { className: S.icon, "aria-hidden": true, children: item.icon })), jsx("span", { className: S.label, children: item.label }), item.hint != null && item.hint !== '' && (jsx("span", { className: S.hint, children: item.hint })), item.submenu && (jsx(Icon, { className: S.chevron, type: "chevronRight", size: "xs" }))] }), !stacked && activeId === item.id && item.submenu && (jsx("div", { className: cn(S.submenu, flipLeft && S.submenuLeft), role: "menu", onMouseEnter: () => {
108
+ window.clearTimeout(hoverTimer.current);
109
+ }, children: item.submenu }))] }, item.id)));
110
+ }
111
+ return (jsxs("div", { ref: rootRef, className: cn(S.root, className), children: [jsx("div", { className: S.trigger, onClick: () => onOpenChange(!open), children: trigger }), open && (jsxs("div", { className: cn(S.popup, align === 'end' && S.alignEnd), role: "menu", children: [stacked && active && (jsxs("div", { className: S.stacked, children: [jsxs("button", { type: "button", className: S.back, onClick: () => setActiveId(null), children: [jsx(Icon, { type: "chevronLeft", size: "xs" }), jsx("span", { className: S.label, children: active.label })] }), jsx("div", { className: S.stackedBody, children: active.submenu })] })), (!stacked || !active) && renderItems()] }))] }));
112
+ }
113
+ const NestedMenu = Object.assign(NestedMenuComponent, {
114
+ Item: NestedMenuItemRow,
115
+ Label: NestedMenuLabel,
116
+ });
117
+
118
+ export { NestedMenu, NestedMenuItemRow, NestedMenuLabel };
@@ -0,0 +1,7 @@
1
+ import styleInject from '../../../node_modules/style-inject/dist/style-inject.es.js';
2
+
3
+ var css_248z = ".NestedMenu_root__ybyGA{display:inline-flex;position:relative}.NestedMenu_trigger__zBeNI{display:inline-flex}.NestedMenu_popup__q0uYO{background:var(--decent-color);border:1px solid var(--accent-color-alpha-200);border-radius:10px;box-shadow:0 8px 28px rgba(0,0,0,.45);left:0;max-width:260px;min-width:200px;padding:4px;position:absolute;top:calc(100% + 6px);z-index:20}.NestedMenu_alignEnd__L4AZi{left:auto;right:0}.NestedMenu_itemWrap__Qmc0B{position:relative}.NestedMenu_back__dNsDV,.NestedMenu_item__tgbZm,.NestedMenu_row__48g5c{align-items:center;background:none;border:none;border-radius:6px;color:inherit;cursor:pointer;display:flex;font:inherit;font-size:13px;font-weight:500;gap:8px;letter-spacing:.01em;line-height:1.3;padding:6px 10px;text-align:left;text-decoration:none;width:100%}.NestedMenu_back__dNsDV.NestedMenu_itemActive__RfPZH,.NestedMenu_back__dNsDV:hover,.NestedMenu_item__tgbZm.NestedMenu_itemActive__RfPZH,.NestedMenu_item__tgbZm:hover,.NestedMenu_row__48g5c.NestedMenu_itemActive__RfPZH,.NestedMenu_row__48g5c:hover{background:var(--accent-color-alpha-100)}.NestedMenu_back__dNsDV.NestedMenu_itemDisabled__evVk1,.NestedMenu_back__dNsDV:disabled,.NestedMenu_item__tgbZm.NestedMenu_itemDisabled__evVk1,.NestedMenu_item__tgbZm:disabled,.NestedMenu_row__48g5c.NestedMenu_itemDisabled__evVk1,.NestedMenu_row__48g5c:disabled{cursor:default;opacity:.55;pointer-events:none}.NestedMenu_itemDanger__oAJbP{color:var(--active-color)}.NestedMenu_itemDanger__oAJbP .NestedMenu_icon__4g2Yp{color:inherit;opacity:1}.NestedMenu_icon__4g2Yp{display:inline-flex;flex-shrink:0;opacity:.55}.NestedMenu_icon__4g2Yp svg{display:block}.NestedMenu_label__Jnm6l{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.NestedMenu_hint__hotNP{flex-shrink:0;font-weight:400;opacity:.5}.NestedMenu_chevron__8i-Po{flex-shrink:0;opacity:.45}.NestedMenu_row__48g5c svg{color:var(--active-color);flex-shrink:0}.NestedMenu_submenu__Tx1Ll{background:var(--decent-color);border:1px solid var(--accent-color-alpha-200);border-radius:10px;box-shadow:0 8px 28px rgba(0,0,0,.45);left:calc(100% + 6px);max-height:min(360px,calc(100vh - 24px));max-width:280px;min-width:220px;overflow-x:hidden;overflow-y:auto;padding:4px;position:absolute;top:-4px;z-index:21}.NestedMenu_submenu__Tx1Ll:before{bottom:0;content:\"\";left:-6px;position:absolute;top:0;width:6px}.NestedMenu_submenuLeft__y18Nb{left:auto;right:calc(100% + 6px)}.NestedMenu_submenuLeft__y18Nb:before{left:auto;right:-6px}.NestedMenu_stacked__rMiOi{display:flex;flex-direction:column;max-height:min(360px,calc(100vh - 24px));overflow-y:auto}.NestedMenu_back__dNsDV{font-weight:650;margin-bottom:2px}.NestedMenu_stackedBody__Fbkma{min-width:0}";
4
+ var S = {"root":"NestedMenu_root__ybyGA","trigger":"NestedMenu_trigger__zBeNI","popup":"NestedMenu_popup__q0uYO","alignEnd":"NestedMenu_alignEnd__L4AZi","itemWrap":"NestedMenu_itemWrap__Qmc0B","item":"NestedMenu_item__tgbZm","row":"NestedMenu_row__48g5c","back":"NestedMenu_back__dNsDV","itemActive":"NestedMenu_itemActive__RfPZH","itemDisabled":"NestedMenu_itemDisabled__evVk1","itemDanger":"NestedMenu_itemDanger__oAJbP","icon":"NestedMenu_icon__4g2Yp","label":"NestedMenu_label__Jnm6l","hint":"NestedMenu_hint__hotNP","chevron":"NestedMenu_chevron__8i-Po","submenu":"NestedMenu_submenu__Tx1Ll","submenuLeft":"NestedMenu_submenuLeft__y18Nb","stacked":"NestedMenu_stacked__rMiOi","stackedBody":"NestedMenu_stackedBody__Fbkma"};
5
+ styleInject(css_248z);
6
+
7
+ export { S as default };
@@ -1,6 +1,6 @@
1
1
  import styleInject from '../../../node_modules/style-inject/dist/style-inject.es.js';
2
2
 
3
- var css_248z = ".SlashSuggestionList_root__9KCb1{-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);background-color:var(--decent-color-alpha-700);border-radius:8px;border-radius:var(--radius-lg,8px);box-shadow:inset 0 0 0 1px var(--accent-color-alpha-50),0 8px 24px rgba(0,0,0,.2);color:var(--foreground);max-width:90vw;min-width:220px;overflow:hidden;position:relative}.SlashSuggestionList_root__9KCb1:before{background-color:var(--accent-color-alpha-50);bottom:0;content:\"\";left:0;pointer-events:none;position:absolute;right:0;top:0}.dark .SlashSuggestionList_root__9KCb1{box-shadow:inset 0 0 0 1px var(--accent-color-alpha-50),0 8px 24px rgba(0,0,0,.5)}.SlashSuggestionList_menu__s-jK3{margin:0;max-height:40vh}.SlashSuggestionList_item__9JBeY{align-items:center;border-radius:var(--p-2);display:flex;flex-direction:row;gap:var(--p-2);height:auto!important;justify-content:flex-start;min-height:0;overflow:visible!important;padding:var(--p-1) var(--p-2)!important;position:relative;text-overflow:clip!important;white-space:normal!important;width:100%}.SlashSuggestionList_item__9JBeY:hover{background-color:var(--accent-color-alpha-50)}.SlashSuggestionList_itemHighlighted__cStZr,.SlashSuggestionList_item__9JBeY:hover{box-shadow:inset 100vw 0 0 0 var(--accent-color-alpha-200)}.SlashSuggestionList_itemIcon__hTtxe{color:var(--accent-color-alpha-800);flex-shrink:0}.SlashSuggestionList_itemText__uwNbZ{align-items:flex-start;display:flex;flex-direction:column;min-width:0}.SlashSuggestionList_itemLabel__OMaov{font-weight:600}.SlashSuggestionList_itemDesc__fAdcA{color:var(--accent-color-alpha-600);font-size:80%;white-space:normal}.SlashSuggestionList_mention__aT45p{align-items:center;background:var(--muted);border-radius:var(--p-1);display:inline-flex;line-height:1.4;margin:0 var(--p-1);padding:0 var(--p-1);-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:baseline;white-space:nowrap}";
3
+ var css_248z = ".SlashSuggestionList_root__9KCb1{-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);background-color:var(--decent-color-alpha-700);border-radius:8px;border-radius:var(--radius-lg,8px);box-shadow:inset 0 0 0 1px var(--accent-color-alpha-50),0 8px 24px rgba(0,0,0,.2);color:var(--foreground);max-width:90vw;min-width:220px;overflow:hidden;position:relative}.SlashSuggestionList_root__9KCb1:before{background-color:var(--accent-color-alpha-50);bottom:0;content:\"\";left:0;pointer-events:none;position:absolute;right:0;top:0}.dark .SlashSuggestionList_root__9KCb1{box-shadow:inset 0 0 0 1px var(--accent-color-alpha-50),0 8px 24px rgba(0,0,0,.5)}.SlashSuggestionList_menu__s-jK3{margin:0;max-height:40vh}.SlashSuggestionList_item__9JBeY{align-items:center;border-radius:var(--p-2);display:flex;flex-direction:row;gap:var(--p-2);height:auto!important;justify-content:flex-start;min-height:0;overflow:visible!important;padding:var(--p-1) var(--p-2)!important;position:relative;text-overflow:clip!important;white-space:normal!important;width:100%}.SlashSuggestionList_itemHighlighted__cStZr{box-shadow:inset 100vw 0 0 0 var(--accent-color-alpha-200)}.SlashSuggestionList_itemIcon__hTtxe{color:var(--accent-color-alpha-800);flex-shrink:0}.SlashSuggestionList_itemText__uwNbZ{align-items:flex-start;display:flex;flex-direction:column;min-width:0}.SlashSuggestionList_itemLabel__OMaov{font-weight:600}.SlashSuggestionList_itemDesc__fAdcA{color:var(--accent-color-alpha-600);font-size:80%;white-space:normal}.SlashSuggestionList_mention__aT45p{align-items:center;background:var(--muted);border-radius:var(--p-1);display:inline-flex;line-height:1.4;margin:0 var(--p-1);padding:0 var(--p-1);transition:background-color .1s ease-in-out;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:baseline;white-space:nowrap}.SlashSuggestionList_mention__aT45p:hover{background-color:var(--accent-color-alpha-200)}";
4
4
  var S = {"root":"SlashSuggestionList_root__9KCb1","menu":"SlashSuggestionList_menu__s-jK3","item":"SlashSuggestionList_item__9JBeY","itemHighlighted":"SlashSuggestionList_itemHighlighted__cStZr","itemIcon":"SlashSuggestionList_itemIcon__hTtxe","itemText":"SlashSuggestionList_itemText__uwNbZ","itemLabel":"SlashSuggestionList_itemLabel__OMaov","itemDesc":"SlashSuggestionList_itemDesc__fAdcA","mention":"SlashSuggestionList_mention__aT45p"};
5
5
  styleInject(css_248z);
6
6
 
@@ -0,0 +1,9 @@
1
+ import * as T from './NestedMenu.types';
2
+ export type { NestedMenuItem, Props as NestedMenuProps, } from './NestedMenu.types';
3
+ export declare function NestedMenuLabel({ children, className }: T.NestedMenuLabelProps): import("react").JSX.Element;
4
+ export declare function NestedMenuItemRow({ children, className, danger, disabled, href, target, rel, onClick, }: T.NestedMenuRowProps): import("react").JSX.Element;
5
+ declare function NestedMenuComponent({ trigger, items, open, onOpenChange, align, className, }: T.Props): import("react").JSX.Element;
6
+ export declare const NestedMenu: typeof NestedMenuComponent & {
7
+ Item: typeof NestedMenuItemRow;
8
+ Label: typeof NestedMenuLabel;
9
+ };
@@ -0,0 +1,35 @@
1
+ import { ReactNode } from 'react';
2
+ export type NestedMenuItem = {
3
+ id: string;
4
+ label: ReactNode;
5
+ icon?: ReactNode;
6
+ hint?: ReactNode;
7
+ submenu?: ReactNode;
8
+ onSubmenuOpen?: () => void;
9
+ onClick?: () => void;
10
+ danger?: boolean;
11
+ disabled?: boolean;
12
+ className?: string;
13
+ };
14
+ export type Props = {
15
+ trigger: ReactNode;
16
+ items: NestedMenuItem[];
17
+ open: boolean;
18
+ onOpenChange: (open: boolean) => void;
19
+ align?: 'start' | 'end';
20
+ className?: string;
21
+ };
22
+ export type NestedMenuRowProps = {
23
+ children: ReactNode;
24
+ className?: string;
25
+ danger?: boolean;
26
+ disabled?: boolean;
27
+ href?: string;
28
+ target?: string;
29
+ rel?: string;
30
+ onClick?: () => void;
31
+ };
32
+ export type NestedMenuLabelProps = {
33
+ children: ReactNode;
34
+ className?: string;
35
+ };
@@ -28,6 +28,7 @@ export * from './LangSelector/LangSelector';
28
28
  export * from './Lazy/Lazy';
29
29
  export * from './LightBox/LightBox';
30
30
  export * from './Menu/Menu';
31
+ export * from './NestedMenu/NestedMenu';
31
32
  export * from './Notifications/Notifications';
32
33
  export * from './Paranja/Paranja';
33
34
  export * from './Popup/Popup';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homecode/ui",
3
- "version": "5.7.8",
3
+ "version": "5.8.0",
4
4
  "description": "React UI components library",
5
5
  "scripts": {
6
6
  "tests": "jest",