entangle-ui 0.11.0 → 0.13.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/CHANGELOG.md +81 -0
- package/dist/esm/assets/src/components/controls/Slider/{Slider.css.ts.vanilla-Cqm3fQ0S.css → Slider.css.ts.vanilla-CLf_RC0A.css} +50 -47
- package/dist/esm/assets/src/components/navigation/Menu/{Menu.css.ts.vanilla-B5iVe5S2.css → Menu.css.ts.vanilla-Bhh1L5yl.css} +14 -11
- package/dist/esm/assets/src/components/primitives/EditableText/EditableText.css.ts.vanilla-CrVGgD19.css +63 -0
- package/dist/esm/assets/src/components/primitives/Tooltip/{Tooltip.css.ts.vanilla-CrEFoLBX.css → Tooltip.css.ts.vanilla-BqaiGrFQ.css} +12 -9
- package/dist/esm/assets/src/components/shell/AppShell/{AppShell.css.ts.vanilla-BYmtU0O_.css → AppShell.css.ts.vanilla-BLK2-DRL.css} +1 -0
- package/dist/esm/assets/src/components/shell/MenuBar/{MenuBar.css.ts.vanilla-TgPcl4yA.css → MenuBar.css.ts.vanilla-Dt3w4f-q.css} +18 -6
- package/dist/esm/components/controls/Slider/Slider.css.js +10 -11
- package/dist/esm/components/controls/Slider/Slider.css.js.map +1 -1
- package/dist/esm/components/controls/Slider/Slider.js +40 -7
- package/dist/esm/components/controls/Slider/Slider.js.map +1 -1
- package/dist/esm/components/editor/ViewportGizmo/ViewportGizmo.js +3 -1
- package/dist/esm/components/editor/ViewportGizmo/ViewportGizmo.js.map +1 -1
- package/dist/esm/components/editor/ViewportGizmo/useGizmoInteraction.js +21 -27
- package/dist/esm/components/editor/ViewportGizmo/useGizmoInteraction.js.map +1 -1
- package/dist/esm/components/feedback/Spinner/Spinner.js +12 -3
- package/dist/esm/components/feedback/Spinner/Spinner.js.map +1 -1
- package/dist/esm/components/navigation/Breadcrumbs/BreadcrumbEllipsis.js +1 -0
- package/dist/esm/components/navigation/Breadcrumbs/BreadcrumbEllipsis.js.map +1 -1
- package/dist/esm/components/navigation/Breadcrumbs/BreadcrumbItem.js +1 -0
- package/dist/esm/components/navigation/Breadcrumbs/BreadcrumbItem.js.map +1 -1
- package/dist/esm/components/navigation/ContextMenu/ContextMenu.js +2 -2
- package/dist/esm/components/navigation/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/esm/components/navigation/Menu/Menu.css.js +11 -10
- package/dist/esm/components/navigation/Menu/Menu.css.js.map +1 -1
- package/dist/esm/components/navigation/Menu/Menu.js +2 -2
- package/dist/esm/components/navigation/Menu/Menu.js.map +1 -1
- package/dist/esm/components/navigation/PathBar/PathBarSiblingMenu.js +1 -0
- package/dist/esm/components/navigation/PathBar/PathBarSiblingMenu.js.map +1 -1
- package/dist/esm/components/primitives/EditableText/EditableText.css.js +11 -0
- package/dist/esm/components/primitives/EditableText/EditableText.css.js.map +1 -0
- package/dist/esm/components/primitives/EditableText/EditableText.js +208 -0
- package/dist/esm/components/primitives/EditableText/EditableText.js.map +1 -0
- package/dist/esm/components/primitives/EditableText/editableTextLabels.js +16 -0
- package/dist/esm/components/primitives/EditableText/editableTextLabels.js.map +1 -0
- package/dist/esm/components/primitives/IconButton/IconButton.js +4 -2
- package/dist/esm/components/primitives/IconButton/IconButton.js.map +1 -1
- package/dist/esm/components/primitives/Tooltip/Tooltip.css.js +6 -5
- package/dist/esm/components/primitives/Tooltip/Tooltip.css.js.map +1 -1
- package/dist/esm/components/primitives/Tooltip/Tooltip.js +12 -2
- package/dist/esm/components/primitives/Tooltip/Tooltip.js.map +1 -1
- package/dist/esm/components/primitives/viewport/Viewport.js +5 -2
- package/dist/esm/components/primitives/viewport/Viewport.js.map +1 -1
- package/dist/esm/components/shell/AppShell/AppShell.css.js +1 -1
- package/dist/esm/components/shell/MenuBar/MenuBar.css.js +7 -6
- package/dist/esm/components/shell/MenuBar/MenuBar.css.js.map +1 -1
- package/dist/esm/components/shell/MenuBar/MenuBar.js +52 -3
- package/dist/esm/components/shell/MenuBar/MenuBar.js.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/{styles.js → styles.css.js} +1 -1
- package/dist/esm/styles.css.js.map +1 -0
- package/dist/tokens/tokens.dark.css +1 -1
- package/dist/tokens/tokens.json +1 -1
- package/dist/tokens/tokens.light.css +1 -1
- package/dist/types/components/controls/Slider/Slider.d.ts +7 -0
- package/dist/types/components/editor/ViewportGizmo/ViewportGizmo.types.d.ts +26 -3
- package/dist/types/components/feedback/Spinner/Spinner.d.ts +6 -1
- package/dist/types/components/feedback/Spinner/Spinner.types.d.ts +8 -0
- package/dist/types/components/primitives/EditableText/EditableText.d.ts +37 -0
- package/dist/types/components/primitives/EditableText/EditableText.types.d.ts +158 -0
- package/dist/types/components/primitives/EditableText/editableTextLabels.d.ts +11 -0
- package/dist/types/components/primitives/IconButton/IconButton.d.ts +2 -1
- package/dist/types/components/primitives/viewport/Viewport.d.ts +5 -2
- package/dist/types/components/shell/MenuBar/MenuBar.d.ts +862 -0
- package/dist/types/index.d.ts +3 -0
- package/package.json +2 -2
- package/dist/esm/styles.js.map +0 -1
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import './../../../assets/src/components/shell/MenuBar/MenuBar.css.ts.vanilla-
|
|
1
|
+
import './../../../assets/src/components/shell/MenuBar/MenuBar.css.ts.vanilla-Dt3w4f-q.css';
|
|
2
2
|
import { createRuntimeFn } from '@vanilla-extract/recipes/createRuntimeFn';
|
|
3
3
|
|
|
4
4
|
var dropdown = 'MenuBar_dropdown__1h81mxv7';
|
|
5
|
+
var indicatorSlot = 'MenuBar_indicatorSlot__1h81mxva';
|
|
5
6
|
var menuBarRoot = createRuntimeFn({defaultClassName:'MenuBar_menuBarRoot__1h81mxv0',variantClassNames:{size:{sm:'MenuBar_menuBarRoot_size_sm__1h81mxv1',md:'MenuBar_menuBarRoot_size_md__1h81mxv2'}},defaultVariants:{size:'md'},compoundVariants:[]});
|
|
6
7
|
var menuContainer = 'MenuBar_menuContainer__1h81mxv3';
|
|
7
8
|
var menuItem = 'MenuBar_menuItem__1h81mxv8';
|
|
8
|
-
var separator = '
|
|
9
|
+
var separator = 'MenuBar_separator__1h81mxvb';
|
|
9
10
|
var shortcut = 'MenuBar_shortcut__1h81mxv9';
|
|
10
|
-
var subContainer = '
|
|
11
|
-
var subDropdown = '
|
|
12
|
-
var subTrigger = '
|
|
11
|
+
var subContainer = 'MenuBar_subContainer__1h81mxve';
|
|
12
|
+
var subDropdown = 'MenuBar_subDropdown__1h81mxvd';
|
|
13
|
+
var subTrigger = 'MenuBar_subTrigger__1h81mxvc MenuBar_menuItem__1h81mxv8';
|
|
13
14
|
var trigger = createRuntimeFn({defaultClassName:'MenuBar_trigger__1h81mxv4',variantClassNames:{active:{true:'MenuBar_trigger_active_true__1h81mxv5',false:'MenuBar_trigger_active_false__1h81mxv6'}},defaultVariants:{active:false},compoundVariants:[]});
|
|
14
15
|
|
|
15
|
-
export { dropdown, menuBarRoot, menuContainer, menuItem, separator, shortcut, subContainer, subDropdown, subTrigger, trigger };
|
|
16
|
+
export { dropdown, indicatorSlot, menuBarRoot, menuContainer, menuItem, separator, shortcut, subContainer, subDropdown, subTrigger, trigger };
|
|
16
17
|
//# sourceMappingURL=MenuBar.css.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MenuBar.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"MenuBar.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
-
import { useState, useRef, useMemo, useCallback, useEffect, useId, createContext
|
|
3
|
+
import { useState, useRef, useMemo, useCallback, useEffect, useContext, useId, createContext } from 'react';
|
|
4
|
+
import { CheckIcon } from '../../Icons/CheckIcon.js';
|
|
5
|
+
import { CircleIcon } from '../../Icons/CircleIcon.js';
|
|
4
6
|
import { cx } from '../../../utils/cx.js';
|
|
5
|
-
import { menuBarRoot, separator, subTrigger, subDropdown, subContainer, shortcut, menuItem, trigger, dropdown, menuContainer } from './MenuBar.css.js';
|
|
7
|
+
import { menuBarRoot, separator, subTrigger, subDropdown, subContainer, indicatorSlot, shortcut, menuItem, trigger, dropdown, menuContainer } from './MenuBar.css.js';
|
|
6
8
|
|
|
7
9
|
// --- Context ---
|
|
8
10
|
const MenuBarContext = /*#__PURE__*/ createContext({
|
|
@@ -16,7 +18,7 @@ const MenuBarContext = /*#__PURE__*/ createContext({
|
|
|
16
18
|
});
|
|
17
19
|
const useMenuBar = () => useContext(MenuBarContext);
|
|
18
20
|
const SUBMENU_CLOSE_DELAY = 150;
|
|
19
|
-
const MENU_ITEM_SELECTOR = '[role="menuitem"]:not(:disabled)';
|
|
21
|
+
const MENU_ITEM_SELECTOR = '[role="menuitem"]:not(:disabled), [role="menuitemcheckbox"]:not(:disabled), [role="menuitemradio"]:not(:disabled)';
|
|
20
22
|
const ChevronRight = () => (jsx("span", { style: {
|
|
21
23
|
fontSize: '8px',
|
|
22
24
|
lineHeight: 1,
|
|
@@ -33,6 +35,50 @@ const MenuBarItem = ({ onClick, shortcut: shortcutText, disabled = false, icon,
|
|
|
33
35
|
return (jsxs("button", { role: "menuitem", onClick: handleClick, disabled: disabled, className: cx(menuItem, className), style: style, "data-testid": testId, ref: ref, type: "button", tabIndex: -1, ...rest, children: [icon && jsx("span", { children: icon }), jsx("span", { children: children }), shortcutText && jsx("span", { className: shortcut, children: shortcutText })] }));
|
|
34
36
|
};
|
|
35
37
|
MenuBarItem.displayName = 'MenuBar.Item';
|
|
38
|
+
// --- Checkable items (checkbox / radio) ---
|
|
39
|
+
const MenuBarRadioContext =
|
|
40
|
+
/*#__PURE__*/ createContext(null);
|
|
41
|
+
const MenuBarCheckboxItem = ({ checked, defaultChecked = false, onCheckedChange, shortcut: shortcutText, indicator = jsx(CheckIcon, { size: "sm" }), closeOnClick = false, disabled = false, children, className, style, testId, ref, ...rest }) => {
|
|
42
|
+
const { setOpenMenuId } = useMenuBar();
|
|
43
|
+
const [internalChecked, setInternalChecked] = useState(defaultChecked);
|
|
44
|
+
const isControlled = checked !== undefined;
|
|
45
|
+
const isChecked = isControlled ? checked : internalChecked;
|
|
46
|
+
const handleClick = useCallback(() => {
|
|
47
|
+
const next = !isChecked;
|
|
48
|
+
if (!isControlled)
|
|
49
|
+
setInternalChecked(next);
|
|
50
|
+
onCheckedChange?.(next);
|
|
51
|
+
if (closeOnClick)
|
|
52
|
+
setOpenMenuId(null);
|
|
53
|
+
}, [isChecked, isControlled, onCheckedChange, closeOnClick, setOpenMenuId]);
|
|
54
|
+
return (jsxs("button", { role: "menuitemcheckbox", "aria-checked": isChecked, onClick: handleClick, disabled: disabled, className: cx(menuItem, className), style: style, "data-testid": testId, ref: ref, type: "button", tabIndex: -1, ...rest, children: [jsx("span", { className: indicatorSlot, "aria-hidden": "true", children: isChecked ? indicator : null }), jsx("span", { children: children }), shortcutText && jsx("span", { className: shortcut, children: shortcutText })] }));
|
|
55
|
+
};
|
|
56
|
+
MenuBarCheckboxItem.displayName = 'MenuBar.CheckboxItem';
|
|
57
|
+
const MenuBarRadioGroup = ({ value, defaultValue, onValueChange, children, className, style, testId, ref, ...rest }) => {
|
|
58
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
59
|
+
const isControlled = value !== undefined;
|
|
60
|
+
const selectedValue = isControlled ? value : internalValue;
|
|
61
|
+
const setValue = useCallback((next) => {
|
|
62
|
+
if (!isControlled)
|
|
63
|
+
setInternalValue(next);
|
|
64
|
+
onValueChange?.(next);
|
|
65
|
+
}, [isControlled, onValueChange]);
|
|
66
|
+
const contextValue = useMemo(() => ({ value: selectedValue, setValue }), [selectedValue, setValue]);
|
|
67
|
+
return (jsx(MenuBarRadioContext.Provider, { value: contextValue, children: jsx("div", { role: "group", className: className, style: style, "data-testid": testId, ref: ref, ...rest, children: children }) }));
|
|
68
|
+
};
|
|
69
|
+
MenuBarRadioGroup.displayName = 'MenuBar.RadioGroup';
|
|
70
|
+
const MenuBarRadioItem = ({ value, shortcut: shortcutText, indicator = jsx(CircleIcon, { size: "sm" }), closeOnClick = false, disabled = false, children, className, style, testId, ref, ...rest }) => {
|
|
71
|
+
const { setOpenMenuId } = useMenuBar();
|
|
72
|
+
const group = useContext(MenuBarRadioContext);
|
|
73
|
+
const isChecked = group?.value === value;
|
|
74
|
+
const handleClick = useCallback(() => {
|
|
75
|
+
group?.setValue(value);
|
|
76
|
+
if (closeOnClick)
|
|
77
|
+
setOpenMenuId(null);
|
|
78
|
+
}, [group, value, closeOnClick, setOpenMenuId]);
|
|
79
|
+
return (jsxs("button", { role: "menuitemradio", "aria-checked": isChecked, onClick: handleClick, disabled: disabled, className: cx(menuItem, className), style: style, "data-testid": testId, ref: ref, type: "button", tabIndex: -1, ...rest, children: [jsx("span", { className: indicatorSlot, "aria-hidden": "true", children: isChecked ? indicator : null }), jsx("span", { children: children }), shortcutText && jsx("span", { className: shortcut, children: shortcutText })] }));
|
|
80
|
+
};
|
|
81
|
+
MenuBarRadioItem.displayName = 'MenuBar.RadioItem';
|
|
36
82
|
const MenuBarSub = ({ label, children, disabled = false, className, style, testId, ref, ...rest }) => {
|
|
37
83
|
const { menuOffset } = useMenuBar();
|
|
38
84
|
const [open, setOpen] = useState(false);
|
|
@@ -214,6 +260,9 @@ MenuBarRoot.displayName = 'MenuBar';
|
|
|
214
260
|
const MenuBar = /*#__PURE__*/ Object.assign(MenuBarRoot, {
|
|
215
261
|
Menu: MenuBarMenu,
|
|
216
262
|
Item: MenuBarItem,
|
|
263
|
+
CheckboxItem: MenuBarCheckboxItem,
|
|
264
|
+
RadioGroup: MenuBarRadioGroup,
|
|
265
|
+
RadioItem: MenuBarRadioItem,
|
|
217
266
|
Sub: MenuBarSub,
|
|
218
267
|
Separator: MenuBarSeparator,
|
|
219
268
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MenuBar.js","sources":["src/components/shell/MenuBar/MenuBar.tsx"],"sourcesContent":["'use client';\n\nimport React, {\n createContext,\n useContext,\n useState,\n useCallback,\n useRef,\n useEffect,\n useId,\n KeyboardEvent,\n useMemo,\n} from 'react';\n\nimport { cx } from '@/utils/cx';\n\nimport {\n menuBarRoot,\n menuContainer,\n trigger,\n dropdown,\n menuItem,\n shortcut,\n separator,\n subTrigger,\n subDropdown,\n subContainer,\n} from './MenuBar.css';\n\nimport type {\n MenuBarProps,\n MenuBarMenuProps,\n MenuBarItemProps,\n MenuBarSubProps,\n MenuBarSeparatorProps,\n MenuBarContextValue,\n} from './MenuBar.types';\n\n// --- Context ---\n\nconst MenuBarContext = /*#__PURE__*/ createContext<MenuBarContextValue>({\n size: 'md',\n menuOffset: 2,\n openMenuId: null,\n setOpenMenuId: () => {},\n registerMenu: () => {},\n unregisterMenu: () => {},\n menuIds: [],\n});\n\nconst useMenuBar = () => useContext(MenuBarContext);\n\nconst SUBMENU_CLOSE_DELAY = 150;\n\nconst MENU_ITEM_SELECTOR = '[role=\"menuitem\"]:not(:disabled)';\n\nconst ChevronRight = () => (\n <span\n style={{\n fontSize: '8px',\n lineHeight: 1,\n }}\n >\n ▶\n </span>\n);\n\n// --- Sub-components ---\n\nconst MenuBarItem: React.FC<MenuBarItemProps> = ({\n onClick,\n shortcut: shortcutText,\n disabled = false,\n icon,\n children,\n\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n const { setOpenMenuId } = useMenuBar();\n\n const handleClick = useCallback(() => {\n if (onClick) {\n onClick();\n setOpenMenuId(null);\n }\n }, [onClick, setOpenMenuId]);\n\n return (\n <button\n role=\"menuitem\"\n onClick={handleClick}\n disabled={disabled}\n className={cx(menuItem, className)}\n style={style}\n data-testid={testId}\n ref={ref}\n type=\"button\"\n tabIndex={-1}\n {...rest}\n >\n {icon && <span>{icon}</span>}\n <span>{children}</span>\n {shortcutText && <span className={shortcut}>{shortcutText}</span>}\n </button>\n );\n};\n\nMenuBarItem.displayName = 'MenuBar.Item';\n\nconst MenuBarSub: React.FC<MenuBarSubProps> = ({\n label,\n children,\n disabled = false,\n\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n const { menuOffset } = useMenuBar();\n const [open, setOpen] = useState(false);\n const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const handleEnter = useCallback(() => {\n if (closeTimer.current) {\n clearTimeout(closeTimer.current);\n closeTimer.current = null;\n }\n if (!disabled) setOpen(true);\n }, [disabled]);\n\n const handleLeave = useCallback(() => {\n closeTimer.current = setTimeout(() => {\n setOpen(false);\n }, SUBMENU_CLOSE_DELAY);\n }, []);\n\n useEffect(() => {\n return () => {\n if (closeTimer.current) clearTimeout(closeTimer.current);\n };\n }, []);\n\n return (\n <div\n ref={ref}\n className={cx(subContainer, className)}\n style={style}\n data-testid={testId}\n onMouseEnter={handleEnter}\n onMouseLeave={handleLeave}\n {...rest}\n >\n <button\n className={subTrigger}\n role=\"menuitem\"\n aria-haspopup=\"true\"\n aria-expanded={open}\n disabled={disabled}\n type=\"button\"\n tabIndex={-1}\n >\n <span>{label}</span>\n <ChevronRight />\n </button>\n {open && (\n <div\n className={subDropdown}\n role=\"menu\"\n style={{ left: `calc(100% + ${menuOffset}px)` }}\n >\n {children}\n </div>\n )}\n </div>\n );\n};\n\nMenuBarSub.displayName = 'MenuBar.Sub';\n\nconst MenuBarSeparator: React.FC<MenuBarSeparatorProps> = ({\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n return (\n <div\n role=\"separator\"\n className={cx(separator, className)}\n style={style}\n data-testid={testId}\n ref={ref}\n {...rest}\n />\n );\n};\n\nMenuBarSeparator.displayName = 'MenuBar.Separator';\n\n// --- MenuBarMenu (top-level dropdown trigger) ---\n\nconst MenuBarMenu: React.FC<MenuBarMenuProps> = ({\n label,\n children,\n disabled = false,\n\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n const menuId = useId();\n const {\n openMenuId,\n setOpenMenuId,\n registerMenu,\n unregisterMenu,\n menuOffset,\n } = useMenuBar();\n const triggerRef = useRef<HTMLButtonElement>(null);\n const dropdownRef = useRef<HTMLDivElement>(null);\n const isOpen = openMenuId === menuId;\n\n useEffect(() => {\n registerMenu(menuId);\n return () => {\n unregisterMenu(menuId);\n };\n }, [menuId, registerMenu, unregisterMenu]);\n\n const handleClick = useCallback(() => {\n if (disabled) return;\n setOpenMenuId(isOpen ? null : menuId);\n }, [disabled, isOpen, menuId, setOpenMenuId]);\n\n const handleMouseEnter = useCallback(() => {\n // Open on hover only when another menu is already open\n if (openMenuId !== null && openMenuId !== menuId && !disabled) {\n setOpenMenuId(menuId);\n }\n }, [openMenuId, menuId, disabled, setOpenMenuId]);\n\n const handleKeyDown = useCallback(\n (e: KeyboardEvent<HTMLButtonElement>) => {\n if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setOpenMenuId(menuId);\n // Focus first item in dropdown after open\n setTimeout(() => {\n const firstItem = dropdownRef.current?.querySelector(\n MENU_ITEM_SELECTOR\n ) as HTMLElement | null;\n firstItem?.focus();\n }, 0);\n }\n if (e.key === 'Escape') {\n e.preventDefault();\n setOpenMenuId(null);\n triggerRef.current?.focus();\n }\n },\n [menuId, setOpenMenuId]\n );\n\n const handleDropdownKeyDown = useCallback(\n (e: KeyboardEvent<HTMLDivElement>) => {\n const items = Array.from(\n dropdownRef.current?.querySelectorAll<HTMLElement>(\n MENU_ITEM_SELECTOR\n ) ?? []\n );\n const currentIndex = items.indexOf(document.activeElement as HTMLElement);\n\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n const next = currentIndex + 1 < items.length ? currentIndex + 1 : 0;\n items[next]?.focus();\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n const prev =\n currentIndex - 1 >= 0 ? currentIndex - 1 : items.length - 1;\n items[prev]?.focus();\n } else if (e.key === 'Escape') {\n e.preventDefault();\n setOpenMenuId(null);\n triggerRef.current?.focus();\n }\n },\n [setOpenMenuId]\n );\n\n // Close dropdown when clicking outside\n useEffect(() => {\n if (!isOpen) return;\n\n const handleOutsideClick = (e: MouseEvent) => {\n const target = e.target as Node;\n if (\n !triggerRef.current?.contains(target) &&\n !dropdownRef.current?.contains(target)\n ) {\n setOpenMenuId(null);\n }\n };\n\n document.addEventListener('mousedown', handleOutsideClick);\n return () => {\n document.removeEventListener('mousedown', handleOutsideClick);\n };\n }, [isOpen, setOpenMenuId]);\n\n return (\n <div\n ref={ref}\n className={cx(menuContainer, className)}\n style={style}\n data-testid={testId}\n {...rest}\n >\n <button\n ref={triggerRef}\n className={trigger({ active: isOpen })}\n onClick={handleClick}\n onMouseEnter={handleMouseEnter}\n onKeyDown={handleKeyDown}\n disabled={disabled}\n type=\"button\"\n role=\"menuitem\"\n aria-haspopup=\"true\"\n aria-expanded={isOpen}\n tabIndex={-1}\n >\n {label}\n </button>\n {isOpen && (\n <div\n ref={dropdownRef}\n className={dropdown}\n role=\"menu\"\n aria-label={label}\n // Focusable programmatically so the open menu can receive focus and\n // handle arrow-key navigation across its items.\n tabIndex={-1}\n onKeyDown={handleDropdownKeyDown}\n style={{ top: `calc(100% + ${menuOffset}px)` }}\n >\n {children}\n </div>\n )}\n </div>\n );\n};\n\nMenuBarMenu.displayName = 'MenuBar.Menu';\n\n// --- Root Component ---\n\nconst MenuBarRoot: React.FC<MenuBarProps> = ({\n size = 'md',\n menuOffset = 2,\n children,\n\n className,\n style,\n testId,\n ref: externalRef,\n ...rest\n}) => {\n const [openMenuId, setOpenMenuId] = useState<string | null>(null);\n // Registered child menu ids, kept in state so the context value stays\n // reactive (previously a ref + forceUpdate, which read the ref during render).\n const [menuIds, setMenuIds] = useState<string[]>([]);\n const safeMenuOffset = Math.max(0, menuOffset);\n const barRef = useRef<HTMLDivElement>(null);\n const setBarRef = useMemo(\n () => (node: HTMLDivElement | null) => {\n barRef.current = node;\n if (typeof externalRef === 'function') {\n externalRef(node);\n } else if (externalRef && typeof externalRef === 'object') {\n externalRef.current = node;\n }\n },\n [externalRef]\n );\n\n const registerMenu = useCallback((id: string) => {\n setMenuIds(prev => (prev.includes(id) ? prev : [...prev, id]));\n }, []);\n\n const unregisterMenu = useCallback((id: string) => {\n setMenuIds(prev => prev.filter(m => m !== id));\n }, []);\n\n const handleBarKeyDown = useCallback(\n (e: KeyboardEvent<HTMLDivElement>) => {\n if (!barRef.current) return;\n\n const triggers = Array.from(\n barRef.current.querySelectorAll<HTMLElement>(\n ':scope > div > [role=\"menuitem\"]'\n )\n );\n const currentIndex = triggers.indexOf(\n document.activeElement as HTMLElement\n );\n\n if (e.key === 'ArrowRight') {\n e.preventDefault();\n const next = currentIndex + 1 < triggers.length ? currentIndex + 1 : 0;\n triggers[next]?.focus();\n // If menu is open, switch to next menu\n if (openMenuId !== null) {\n triggers[next]?.click();\n }\n } else if (e.key === 'ArrowLeft') {\n e.preventDefault();\n const prev =\n currentIndex - 1 >= 0 ? currentIndex - 1 : triggers.length - 1;\n triggers[prev]?.focus();\n if (openMenuId !== null) {\n triggers[prev]?.click();\n }\n }\n },\n [openMenuId]\n );\n\n const contextValue = useMemo(\n () => ({\n size,\n menuOffset: safeMenuOffset,\n openMenuId,\n setOpenMenuId,\n registerMenu,\n unregisterMenu,\n menuIds,\n }),\n [\n size,\n safeMenuOffset,\n openMenuId,\n setOpenMenuId,\n registerMenu,\n unregisterMenu,\n menuIds,\n ]\n );\n\n return (\n <MenuBarContext.Provider value={contextValue}>\n <div\n ref={setBarRef}\n className={cx(menuBarRoot({ size }), className)}\n style={style}\n data-testid={testId}\n role=\"menubar\"\n onKeyDown={handleBarKeyDown}\n tabIndex={0}\n {...rest}\n >\n {children}\n </div>\n </MenuBarContext.Provider>\n );\n};\n\nMenuBarRoot.displayName = 'MenuBar';\n\n// --- Compound Component ---\n\nexport const MenuBar = /*#__PURE__*/ Object.assign(MenuBarRoot, {\n Menu: MenuBarMenu,\n Item: MenuBarItem,\n Sub: MenuBarSub,\n Separator: MenuBarSeparator,\n});\n"],"names":[],"mappings":";;;;;;AAsCA;AAEA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACD;AAED;AAEA;AAEA;AAEA;AAGM;AACA;AACD;AAML;AAEA;AAaE;AAEA;;AAEI;;;AAGJ;AAEA;AAkBF;AAEA;AAEA;AAWE;;AAEA;AAEA;AACE;AACE;AACA;;AAEF;;AACF;AAEA;AACE;;;;;AAMA;;AAC0B;AAC1B;;;AAoCJ;AAEA;AAEA;AAOE;AAUF;AAEA;AAEA;AAEA;AAWE;AACA;AAOA;AACA;AACA;;;AAIE;;AAEA;;AAGF;AACE;;;;AAIF;;;;;;AAOA;AAEI;;;;;;;;;AAWA;;;AAGE;;AAEJ;AAIF;AAEI;;AAOA;;AAEE;AACA;;AACK;;;AAIL;;AACK;;;AAGL;;AAEJ;;;AAMA;;AAEA;AACE;;;;;AAOF;AAEA;AACA;AACE;AACF;AACF;AAEA;;;;AAwCF;AAEA;AAEA;AAEA;;;;;;AAgBE;;AAGI;AACA;;;AAEO;AACL;;AAEJ;AAIF;;;AAIA;AACE;;AAGF;;;AAII;;AASA;;AAEE;AACA;;AAEA;AACE;;;AAEG;;;AAIL;AACA;AACE;;;AAGN;AAIF;;AAGI;;;;;;AAMD;;;;;;;;AASA;;AAmBL;AAEA;AAEA;AAEO;AACL;AACA;AACA;AACA;AACD;;"}
|
|
1
|
+
{"version":3,"file":"MenuBar.js","sources":["src/components/shell/MenuBar/MenuBar.tsx"],"sourcesContent":["'use client';\n\nimport React, {\n createContext,\n useContext,\n useState,\n useCallback,\n useRef,\n useEffect,\n useId,\n KeyboardEvent,\n useMemo,\n} from 'react';\n\nimport { CheckIcon } from '@/components/Icons/CheckIcon';\nimport { CircleIcon } from '@/components/Icons/CircleIcon';\nimport { cx } from '@/utils/cx';\n\nimport {\n menuBarRoot,\n menuContainer,\n trigger,\n dropdown,\n menuItem,\n shortcut,\n separator,\n subTrigger,\n subDropdown,\n subContainer,\n indicatorSlot,\n} from './MenuBar.css';\n\nimport type {\n MenuBarProps,\n MenuBarMenuProps,\n MenuBarItemProps,\n MenuBarSubProps,\n MenuBarSeparatorProps,\n MenuBarContextValue,\n MenuBarCheckboxItemProps,\n MenuBarRadioGroupProps,\n MenuBarRadioItemProps,\n MenuBarRadioGroupContextValue,\n} from './MenuBar.types';\n\n// --- Context ---\n\nconst MenuBarContext = /*#__PURE__*/ createContext<MenuBarContextValue>({\n size: 'md',\n menuOffset: 2,\n openMenuId: null,\n setOpenMenuId: () => {},\n registerMenu: () => {},\n unregisterMenu: () => {},\n menuIds: [],\n});\n\nconst useMenuBar = () => useContext(MenuBarContext);\n\nconst SUBMENU_CLOSE_DELAY = 150;\n\nconst MENU_ITEM_SELECTOR =\n '[role=\"menuitem\"]:not(:disabled), [role=\"menuitemcheckbox\"]:not(:disabled), [role=\"menuitemradio\"]:not(:disabled)';\n\nconst ChevronRight = () => (\n <span\n style={{\n fontSize: '8px',\n lineHeight: 1,\n }}\n >\n ▶\n </span>\n);\n\n// --- Sub-components ---\n\nconst MenuBarItem: React.FC<MenuBarItemProps> = ({\n onClick,\n shortcut: shortcutText,\n disabled = false,\n icon,\n children,\n\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n const { setOpenMenuId } = useMenuBar();\n\n const handleClick = useCallback(() => {\n if (onClick) {\n onClick();\n setOpenMenuId(null);\n }\n }, [onClick, setOpenMenuId]);\n\n return (\n <button\n role=\"menuitem\"\n onClick={handleClick}\n disabled={disabled}\n className={cx(menuItem, className)}\n style={style}\n data-testid={testId}\n ref={ref}\n type=\"button\"\n tabIndex={-1}\n {...rest}\n >\n {icon && <span>{icon}</span>}\n <span>{children}</span>\n {shortcutText && <span className={shortcut}>{shortcutText}</span>}\n </button>\n );\n};\n\nMenuBarItem.displayName = 'MenuBar.Item';\n\n// --- Checkable items (checkbox / radio) ---\n\nconst MenuBarRadioContext =\n /*#__PURE__*/ createContext<MenuBarRadioGroupContextValue | null>(null);\n\nconst MenuBarCheckboxItem: React.FC<MenuBarCheckboxItemProps> = ({\n checked,\n defaultChecked = false,\n onCheckedChange,\n shortcut: shortcutText,\n indicator = <CheckIcon size=\"sm\" />,\n closeOnClick = false,\n disabled = false,\n children,\n\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n const { setOpenMenuId } = useMenuBar();\n const [internalChecked, setInternalChecked] = useState(defaultChecked);\n const isControlled = checked !== undefined;\n const isChecked = isControlled ? checked : internalChecked;\n\n const handleClick = useCallback(() => {\n const next = !isChecked;\n if (!isControlled) setInternalChecked(next);\n onCheckedChange?.(next);\n if (closeOnClick) setOpenMenuId(null);\n }, [isChecked, isControlled, onCheckedChange, closeOnClick, setOpenMenuId]);\n\n return (\n <button\n role=\"menuitemcheckbox\"\n aria-checked={isChecked}\n onClick={handleClick}\n disabled={disabled}\n className={cx(menuItem, className)}\n style={style}\n data-testid={testId}\n ref={ref}\n type=\"button\"\n tabIndex={-1}\n {...rest}\n >\n <span className={indicatorSlot} aria-hidden=\"true\">\n {isChecked ? indicator : null}\n </span>\n <span>{children}</span>\n {shortcutText && <span className={shortcut}>{shortcutText}</span>}\n </button>\n );\n};\n\nMenuBarCheckboxItem.displayName = 'MenuBar.CheckboxItem';\n\nconst MenuBarRadioGroup: React.FC<MenuBarRadioGroupProps> = ({\n value,\n defaultValue,\n onValueChange,\n children,\n\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n const [internalValue, setInternalValue] = useState<string | undefined>(\n defaultValue\n );\n const isControlled = value !== undefined;\n const selectedValue = isControlled ? value : internalValue;\n\n const setValue = useCallback(\n (next: string) => {\n if (!isControlled) setInternalValue(next);\n onValueChange?.(next);\n },\n [isControlled, onValueChange]\n );\n\n const contextValue = useMemo(\n () => ({ value: selectedValue, setValue }),\n [selectedValue, setValue]\n );\n\n return (\n <MenuBarRadioContext.Provider value={contextValue}>\n <div\n role=\"group\"\n className={className}\n style={style}\n data-testid={testId}\n ref={ref}\n {...rest}\n >\n {children}\n </div>\n </MenuBarRadioContext.Provider>\n );\n};\n\nMenuBarRadioGroup.displayName = 'MenuBar.RadioGroup';\n\nconst MenuBarRadioItem: React.FC<MenuBarRadioItemProps> = ({\n value,\n shortcut: shortcutText,\n indicator = <CircleIcon size=\"sm\" />,\n closeOnClick = false,\n disabled = false,\n children,\n\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n const { setOpenMenuId } = useMenuBar();\n const group = useContext(MenuBarRadioContext);\n const isChecked = group?.value === value;\n\n const handleClick = useCallback(() => {\n group?.setValue(value);\n if (closeOnClick) setOpenMenuId(null);\n }, [group, value, closeOnClick, setOpenMenuId]);\n\n return (\n <button\n role=\"menuitemradio\"\n aria-checked={isChecked}\n onClick={handleClick}\n disabled={disabled}\n className={cx(menuItem, className)}\n style={style}\n data-testid={testId}\n ref={ref}\n type=\"button\"\n tabIndex={-1}\n {...rest}\n >\n <span className={indicatorSlot} aria-hidden=\"true\">\n {isChecked ? indicator : null}\n </span>\n <span>{children}</span>\n {shortcutText && <span className={shortcut}>{shortcutText}</span>}\n </button>\n );\n};\n\nMenuBarRadioItem.displayName = 'MenuBar.RadioItem';\n\nconst MenuBarSub: React.FC<MenuBarSubProps> = ({\n label,\n children,\n disabled = false,\n\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n const { menuOffset } = useMenuBar();\n const [open, setOpen] = useState(false);\n const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const handleEnter = useCallback(() => {\n if (closeTimer.current) {\n clearTimeout(closeTimer.current);\n closeTimer.current = null;\n }\n if (!disabled) setOpen(true);\n }, [disabled]);\n\n const handleLeave = useCallback(() => {\n closeTimer.current = setTimeout(() => {\n setOpen(false);\n }, SUBMENU_CLOSE_DELAY);\n }, []);\n\n useEffect(() => {\n return () => {\n if (closeTimer.current) clearTimeout(closeTimer.current);\n };\n }, []);\n\n return (\n <div\n ref={ref}\n className={cx(subContainer, className)}\n style={style}\n data-testid={testId}\n onMouseEnter={handleEnter}\n onMouseLeave={handleLeave}\n {...rest}\n >\n <button\n className={subTrigger}\n role=\"menuitem\"\n aria-haspopup=\"true\"\n aria-expanded={open}\n disabled={disabled}\n type=\"button\"\n tabIndex={-1}\n >\n <span>{label}</span>\n <ChevronRight />\n </button>\n {open && (\n <div\n className={subDropdown}\n role=\"menu\"\n style={{ left: `calc(100% + ${menuOffset}px)` }}\n >\n {children}\n </div>\n )}\n </div>\n );\n};\n\nMenuBarSub.displayName = 'MenuBar.Sub';\n\nconst MenuBarSeparator: React.FC<MenuBarSeparatorProps> = ({\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n return (\n <div\n role=\"separator\"\n className={cx(separator, className)}\n style={style}\n data-testid={testId}\n ref={ref}\n {...rest}\n />\n );\n};\n\nMenuBarSeparator.displayName = 'MenuBar.Separator';\n\n// --- MenuBarMenu (top-level dropdown trigger) ---\n\nconst MenuBarMenu: React.FC<MenuBarMenuProps> = ({\n label,\n children,\n disabled = false,\n\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n const menuId = useId();\n const {\n openMenuId,\n setOpenMenuId,\n registerMenu,\n unregisterMenu,\n menuOffset,\n } = useMenuBar();\n const triggerRef = useRef<HTMLButtonElement>(null);\n const dropdownRef = useRef<HTMLDivElement>(null);\n const isOpen = openMenuId === menuId;\n\n useEffect(() => {\n registerMenu(menuId);\n return () => {\n unregisterMenu(menuId);\n };\n }, [menuId, registerMenu, unregisterMenu]);\n\n const handleClick = useCallback(() => {\n if (disabled) return;\n setOpenMenuId(isOpen ? null : menuId);\n }, [disabled, isOpen, menuId, setOpenMenuId]);\n\n const handleMouseEnter = useCallback(() => {\n // Open on hover only when another menu is already open\n if (openMenuId !== null && openMenuId !== menuId && !disabled) {\n setOpenMenuId(menuId);\n }\n }, [openMenuId, menuId, disabled, setOpenMenuId]);\n\n const handleKeyDown = useCallback(\n (e: KeyboardEvent<HTMLButtonElement>) => {\n if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setOpenMenuId(menuId);\n // Focus first item in dropdown after open\n setTimeout(() => {\n const firstItem = dropdownRef.current?.querySelector(\n MENU_ITEM_SELECTOR\n ) as HTMLElement | null;\n firstItem?.focus();\n }, 0);\n }\n if (e.key === 'Escape') {\n e.preventDefault();\n setOpenMenuId(null);\n triggerRef.current?.focus();\n }\n },\n [menuId, setOpenMenuId]\n );\n\n const handleDropdownKeyDown = useCallback(\n (e: KeyboardEvent<HTMLDivElement>) => {\n const items = Array.from(\n dropdownRef.current?.querySelectorAll<HTMLElement>(\n MENU_ITEM_SELECTOR\n ) ?? []\n );\n const currentIndex = items.indexOf(document.activeElement as HTMLElement);\n\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n const next = currentIndex + 1 < items.length ? currentIndex + 1 : 0;\n items[next]?.focus();\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n const prev =\n currentIndex - 1 >= 0 ? currentIndex - 1 : items.length - 1;\n items[prev]?.focus();\n } else if (e.key === 'Escape') {\n e.preventDefault();\n setOpenMenuId(null);\n triggerRef.current?.focus();\n }\n },\n [setOpenMenuId]\n );\n\n // Close dropdown when clicking outside\n useEffect(() => {\n if (!isOpen) return;\n\n const handleOutsideClick = (e: MouseEvent) => {\n const target = e.target as Node;\n if (\n !triggerRef.current?.contains(target) &&\n !dropdownRef.current?.contains(target)\n ) {\n setOpenMenuId(null);\n }\n };\n\n document.addEventListener('mousedown', handleOutsideClick);\n return () => {\n document.removeEventListener('mousedown', handleOutsideClick);\n };\n }, [isOpen, setOpenMenuId]);\n\n return (\n <div\n ref={ref}\n className={cx(menuContainer, className)}\n style={style}\n data-testid={testId}\n {...rest}\n >\n <button\n ref={triggerRef}\n className={trigger({ active: isOpen })}\n onClick={handleClick}\n onMouseEnter={handleMouseEnter}\n onKeyDown={handleKeyDown}\n disabled={disabled}\n type=\"button\"\n role=\"menuitem\"\n aria-haspopup=\"true\"\n aria-expanded={isOpen}\n tabIndex={-1}\n >\n {label}\n </button>\n {isOpen && (\n <div\n ref={dropdownRef}\n className={dropdown}\n role=\"menu\"\n aria-label={label}\n // Focusable programmatically so the open menu can receive focus and\n // handle arrow-key navigation across its items.\n tabIndex={-1}\n onKeyDown={handleDropdownKeyDown}\n style={{ top: `calc(100% + ${menuOffset}px)` }}\n >\n {children}\n </div>\n )}\n </div>\n );\n};\n\nMenuBarMenu.displayName = 'MenuBar.Menu';\n\n// --- Root Component ---\n\nconst MenuBarRoot: React.FC<MenuBarProps> = ({\n size = 'md',\n menuOffset = 2,\n children,\n\n className,\n style,\n testId,\n ref: externalRef,\n ...rest\n}) => {\n const [openMenuId, setOpenMenuId] = useState<string | null>(null);\n // Registered child menu ids, kept in state so the context value stays\n // reactive (previously a ref + forceUpdate, which read the ref during render).\n const [menuIds, setMenuIds] = useState<string[]>([]);\n const safeMenuOffset = Math.max(0, menuOffset);\n const barRef = useRef<HTMLDivElement>(null);\n const setBarRef = useMemo(\n () => (node: HTMLDivElement | null) => {\n barRef.current = node;\n if (typeof externalRef === 'function') {\n externalRef(node);\n } else if (externalRef && typeof externalRef === 'object') {\n externalRef.current = node;\n }\n },\n [externalRef]\n );\n\n const registerMenu = useCallback((id: string) => {\n setMenuIds(prev => (prev.includes(id) ? prev : [...prev, id]));\n }, []);\n\n const unregisterMenu = useCallback((id: string) => {\n setMenuIds(prev => prev.filter(m => m !== id));\n }, []);\n\n const handleBarKeyDown = useCallback(\n (e: KeyboardEvent<HTMLDivElement>) => {\n if (!barRef.current) return;\n\n const triggers = Array.from(\n barRef.current.querySelectorAll<HTMLElement>(\n ':scope > div > [role=\"menuitem\"]'\n )\n );\n const currentIndex = triggers.indexOf(\n document.activeElement as HTMLElement\n );\n\n if (e.key === 'ArrowRight') {\n e.preventDefault();\n const next = currentIndex + 1 < triggers.length ? currentIndex + 1 : 0;\n triggers[next]?.focus();\n // If menu is open, switch to next menu\n if (openMenuId !== null) {\n triggers[next]?.click();\n }\n } else if (e.key === 'ArrowLeft') {\n e.preventDefault();\n const prev =\n currentIndex - 1 >= 0 ? currentIndex - 1 : triggers.length - 1;\n triggers[prev]?.focus();\n if (openMenuId !== null) {\n triggers[prev]?.click();\n }\n }\n },\n [openMenuId]\n );\n\n const contextValue = useMemo(\n () => ({\n size,\n menuOffset: safeMenuOffset,\n openMenuId,\n setOpenMenuId,\n registerMenu,\n unregisterMenu,\n menuIds,\n }),\n [\n size,\n safeMenuOffset,\n openMenuId,\n setOpenMenuId,\n registerMenu,\n unregisterMenu,\n menuIds,\n ]\n );\n\n return (\n <MenuBarContext.Provider value={contextValue}>\n <div\n ref={setBarRef}\n className={cx(menuBarRoot({ size }), className)}\n style={style}\n data-testid={testId}\n role=\"menubar\"\n onKeyDown={handleBarKeyDown}\n tabIndex={0}\n {...rest}\n >\n {children}\n </div>\n </MenuBarContext.Provider>\n );\n};\n\nMenuBarRoot.displayName = 'MenuBar';\n\n// --- Compound Component ---\n\nexport const MenuBar = /*#__PURE__*/ Object.assign(MenuBarRoot, {\n Menu: MenuBarMenu,\n Item: MenuBarItem,\n CheckboxItem: MenuBarCheckboxItem,\n RadioGroup: MenuBarRadioGroup,\n RadioItem: MenuBarRadioItem,\n Sub: MenuBarSub,\n Separator: MenuBarSeparator,\n});\n"],"names":[],"mappings":";;;;;;;;AA6CA;AAEA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACD;AAED;AAEA;AAEA;AAGA;AAGM;AACA;AACD;AAML;AAEA;AAaE;AAEA;;AAEI;;;AAGJ;AAEA;AAkBF;AAEA;AAEA;AAEA;AACE;AAEF;AAgBE;;AAEA;;AAGA;AACE;AACA;;AACA;AACA;;AACF;AAEA;AAqBF;AAEA;AAEA;;AAeE;;AAGA;AAEI;;AACA;AACF;;AASF;AAcF;AAEA;AAEA;AAcE;AACA;AACA;AAEA;AACE;AACA;;;AAGF;AAqBF;AAEA;AAEA;AAWE;;AAEA;AAEA;AACE;AACE;AACA;;AAEF;;AACF;AAEA;AACE;;;;;AAMA;;AAC0B;AAC1B;;;AAoCJ;AAEA;AAEA;AAOE;AAUF;AAEA;AAEA;AAEA;AAWE;AACA;AAOA;AACA;AACA;;;AAIE;;AAEA;;AAGF;AACE;;;;AAIF;;;;;;AAOA;AAEI;;;;;;;;;AAWA;;;AAGE;;AAEJ;AAIF;AAEI;;AAOA;;AAEE;AACA;;AACK;;;AAIL;;AACK;;;AAGL;;AAEJ;;;AAMA;;AAEA;AACE;;;;;AAOF;AAEA;AACA;AACE;AACF;AACF;AAEA;;;;AAwCF;AAEA;AAEA;AAEA;;;;;;AAgBE;;AAGI;AACA;;;AAEO;AACL;;AAEJ;AAIF;;;AAIA;AACE;;AAGF;;;AAII;;AASA;;AAEE;AACA;;AAEA;AACE;;;AAEG;;;AAIL;AACA;AACE;;;AAGN;AAIF;;AAGI;;;;;;AAMD;;;;;;;;AASA;;AAmBL;AAEA;AAEA;AAEO;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACD;;"}
|
package/dist/esm/index.js
CHANGED
|
@@ -32,6 +32,8 @@ export { Checkbox } from './components/primitives/Checkbox/Checkbox.js';
|
|
|
32
32
|
export { CheckboxGroup } from './components/primitives/Checkbox/CheckboxGroup.js';
|
|
33
33
|
export { Code } from './components/primitives/Code/Code.js';
|
|
34
34
|
export { Collapsible } from './components/primitives/Collapsible/Collapsible.js';
|
|
35
|
+
export { EditableText } from './components/primitives/EditableText/EditableText.js';
|
|
36
|
+
export { DEFAULT_EDITABLE_TEXT_LABELS, resolveEditableTextLabels } from './components/primitives/EditableText/editableTextLabels.js';
|
|
35
37
|
export { HoverCard } from './components/primitives/HoverCard/HoverCard.js';
|
|
36
38
|
export { Icon } from './components/primitives/Icon/Icon.js';
|
|
37
39
|
export { IconButton } from './components/primitives/IconButton/IconButton.js';
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
package/dist/tokens/tokens.json
CHANGED
|
@@ -96,6 +96,13 @@ interface SliderBaseProps extends Omit<BaseComponent, 'onChange'> {
|
|
|
96
96
|
* @default true
|
|
97
97
|
*/
|
|
98
98
|
showTooltip?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Placement of the value tooltip relative to the thumb. The tooltip is
|
|
101
|
+
* portaled to `document.body`, so it is never clipped by an ancestor's
|
|
102
|
+
* `overflow`.
|
|
103
|
+
* @default "top"
|
|
104
|
+
*/
|
|
105
|
+
tooltipPlacement?: 'top' | 'bottom';
|
|
99
106
|
/**
|
|
100
107
|
* Show tick marks along the track
|
|
101
108
|
* @default false
|
|
@@ -17,11 +17,17 @@ interface GizmoOrientation {
|
|
|
17
17
|
/**
|
|
18
18
|
* Orbital rotation delta during a drag gesture.
|
|
19
19
|
* The app applies this to its camera orbit controller.
|
|
20
|
+
*
|
|
21
|
+
* Sign convention (before any `invertYaw` / `invertPitch`): dragging the
|
|
22
|
+
* pointer right — or pressing `ArrowRight` — emits a positive `deltaYaw`;
|
|
23
|
+
* dragging up — or `ArrowUp` — emits a positive `deltaPitch`. Which way the
|
|
24
|
+
* camera actually turns is the consumer's choice; flip it with the
|
|
25
|
+
* `invertYaw` / `invertPitch` props if the motion feels mirrored.
|
|
20
26
|
*/
|
|
21
27
|
interface OrbitDelta {
|
|
22
|
-
/** Yaw change in degrees
|
|
28
|
+
/** Yaw change in degrees. Positive = pointer dragged right / ArrowRight. */
|
|
23
29
|
deltaYaw: number;
|
|
24
|
-
/** Pitch change in degrees
|
|
30
|
+
/** Pitch change in degrees. Positive = pointer dragged up / ArrowUp. */
|
|
25
31
|
deltaPitch: number;
|
|
26
32
|
}
|
|
27
33
|
/**
|
|
@@ -109,7 +115,24 @@ interface ViewportGizmoBaseProps extends Omit<BaseComponent, 'onChange'> {
|
|
|
109
115
|
* @default true
|
|
110
116
|
*/
|
|
111
117
|
constrainPitch?: boolean;
|
|
112
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Invert the horizontal orbit direction. When true, the emitted `deltaYaw`
|
|
120
|
+
* sign is flipped, so the consumer's camera turns the opposite way for the
|
|
121
|
+
* same drag / arrow-key gesture. Use this when the orbit feels mirrored
|
|
122
|
+
* relative to your camera controller (e.g. Three.js OrbitControls).
|
|
123
|
+
* @default false
|
|
124
|
+
*/
|
|
125
|
+
invertYaw?: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Invert the vertical orbit direction. When true, the emitted `deltaPitch`
|
|
128
|
+
* sign is flipped (dragging up turns the camera down).
|
|
129
|
+
* @default false
|
|
130
|
+
*/
|
|
131
|
+
invertPitch?: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Called continuously while the user drags to orbit.
|
|
134
|
+
* See {@link OrbitDelta} for the delta sign convention.
|
|
135
|
+
*/
|
|
113
136
|
onOrbit?: (delta: OrbitDelta) => void;
|
|
114
137
|
/** Called when the user finishes an orbit drag. */
|
|
115
138
|
onOrbitEnd?: (finalOrientation: GizmoOrientation) => void;
|
|
@@ -5,13 +5,17 @@ import React from 'react';
|
|
|
5
5
|
* Loading / activity indicator.
|
|
6
6
|
*
|
|
7
7
|
* Announces itself via `role="status"` and `aria-label`; honors
|
|
8
|
-
* `prefers-reduced-motion` by halting animation and dimming the indicator.
|
|
8
|
+
* `prefers-reduced-motion` by halting animation and dimming the indicator. Pass
|
|
9
|
+
* `decorative` to opt out of the live region when nested inside one (e.g. a
|
|
10
|
+
* `StatusBar`).
|
|
9
11
|
*
|
|
10
12
|
* @example
|
|
11
13
|
* ```tsx
|
|
12
14
|
* <Spinner />
|
|
13
15
|
* <Spinner variant="dots" size="lg" />
|
|
14
16
|
* <Spinner showLabel label="Fetching results..." />
|
|
17
|
+
* // Inside a StatusBar (already role="status"):
|
|
18
|
+
* <Spinner decorative size="sm" />
|
|
15
19
|
* ```
|
|
16
20
|
*/
|
|
17
21
|
declare const Spinner: React.NamedExoticComponent<{
|
|
@@ -20,6 +24,7 @@ declare const Spinner: React.NamedExoticComponent<{
|
|
|
20
24
|
color?: SpinnerColor | undefined;
|
|
21
25
|
label?: string | undefined;
|
|
22
26
|
showLabel?: boolean | undefined;
|
|
27
|
+
decorative?: boolean | undefined;
|
|
23
28
|
id?: string | undefined | undefined;
|
|
24
29
|
className?: string | undefined | undefined;
|
|
25
30
|
testId?: string | undefined;
|
|
@@ -35,6 +35,14 @@ interface SpinnerBaseProps extends BaseComponent<HTMLSpanElement> {
|
|
|
35
35
|
* @default false
|
|
36
36
|
*/
|
|
37
37
|
showLabel?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Render the spinner as purely decorative: `role="presentation"` +
|
|
40
|
+
* `aria-hidden`, with no live region. Use when the spinner sits inside an
|
|
41
|
+
* existing live region (e.g. `StatusBar`, which is itself `role="status"`) so
|
|
42
|
+
* the activity is not announced twice.
|
|
43
|
+
* @default false
|
|
44
|
+
*/
|
|
45
|
+
decorative?: boolean;
|
|
38
46
|
}
|
|
39
47
|
type SpinnerProps = Prettify<SpinnerBaseProps>;
|
|
40
48
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { EditableTextProps } from './EditableText.types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Text that looks like `<Text>` but turns into an inline editor when the user
|
|
6
|
+
* activates it — the editor-UI pattern for renaming layers, nodes, assets, and
|
|
7
|
+
* scene objects in place.
|
|
8
|
+
*
|
|
9
|
+
* The idle state renders a real `<Text>` (so it inherits every typography prop),
|
|
10
|
+
* and the editing state renders a chrome-less `<input>` that shares the exact
|
|
11
|
+
* same typography recipe, so the swap is visually seamless. The edit field
|
|
12
|
+
* auto-sizes to its content via a hidden sizer — no measurement effects.
|
|
13
|
+
*
|
|
14
|
+
* Editing commits on `Enter` (and on blur when `submitOnBlur`), cancels on
|
|
15
|
+
* `Escape`. `onChange` fires on commit only, with the new value.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* // Uncontrolled — click the text to rename in place
|
|
20
|
+
* <EditableText defaultValue="Untitled Layer" onChange={setName} />
|
|
21
|
+
*
|
|
22
|
+
* // Controlled, heading-styled, double-click to edit (rename convention)
|
|
23
|
+
* <EditableText
|
|
24
|
+
* variant="heading"
|
|
25
|
+
* value={name}
|
|
26
|
+
* onChange={setName}
|
|
27
|
+
* activationMode="double"
|
|
28
|
+
* placeholder="Name this node"
|
|
29
|
+
* />
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
declare const EditableText: {
|
|
33
|
+
({ value, defaultValue, onChange, placeholder, as, variant, size, weight, color, lineHeight, align, truncate, mono, activationMode, disabled, readOnly, selectOnEdit, submitOnBlur, maxLength, labels: labelsProp, "aria-label": ariaLabel, onEditStart, onEditEnd, onKeyDown, className, style, testId, ref, }: EditableTextProps): React.ReactElement;
|
|
34
|
+
displayName: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export { EditableText };
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { TextElement, TextVariant, TextSize, TextWeight, TextColor, TextLineHeight, TextAlign } from '../Text/Text.js';
|
|
2
|
+
import { Prettify } from '../../../types/utilities.js';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* How a pointer starts an edit session.
|
|
7
|
+
*
|
|
8
|
+
* Keyboard activation (Enter / F2 while the display is focused) always works,
|
|
9
|
+
* independent of this setting.
|
|
10
|
+
* - `single`: a single click on the text starts editing.
|
|
11
|
+
* - `double`: a double click starts editing; single clicks are left free for
|
|
12
|
+
* selection or row activation (the file/node-rename convention).
|
|
13
|
+
*/
|
|
14
|
+
type EditableTextActivationMode = 'single' | 'double';
|
|
15
|
+
/**
|
|
16
|
+
* Why an edit session ended.
|
|
17
|
+
* - `commit`: the draft was accepted (Enter, or blur with `submitOnBlur`).
|
|
18
|
+
* - `cancel`: the draft was discarded (Escape, or blur without `submitOnBlur`).
|
|
19
|
+
*/
|
|
20
|
+
type EditableTextEndReason = 'commit' | 'cancel';
|
|
21
|
+
/**
|
|
22
|
+
* Every user-facing string EditableText renders. Override any subset via the
|
|
23
|
+
* `labels` prop; the rest fall back to {@link DEFAULT_EDITABLE_TEXT_LABELS}.
|
|
24
|
+
*/
|
|
25
|
+
interface EditableTextLabels {
|
|
26
|
+
/**
|
|
27
|
+
* Accessible name for the edit field while editing, and the fallback
|
|
28
|
+
* accessible name for the display element when the value is empty (and no
|
|
29
|
+
* `placeholder` is set). An explicit `aria-label` prop still wins.
|
|
30
|
+
* @default 'Edit text'
|
|
31
|
+
*/
|
|
32
|
+
editLabel: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Imperative handle exposed through `ref`. Lets an external control (e.g. a
|
|
36
|
+
* toolbar "Rename" button) drive the edit session and focus.
|
|
37
|
+
*/
|
|
38
|
+
interface EditableTextHandle {
|
|
39
|
+
/** Enter edit mode programmatically (no-op when disabled or read-only). */
|
|
40
|
+
edit: () => void;
|
|
41
|
+
/** Leave edit mode, committing the current draft. */
|
|
42
|
+
commit: () => void;
|
|
43
|
+
/** Leave edit mode, discarding the current draft. */
|
|
44
|
+
cancel: () => void;
|
|
45
|
+
/** Focus the edit field while editing, otherwise the display element. */
|
|
46
|
+
focus: () => void;
|
|
47
|
+
/** Whether an edit session is currently active. */
|
|
48
|
+
isEditing: () => boolean;
|
|
49
|
+
/** The root DOM element for the current state (display element or input). */
|
|
50
|
+
getElement: () => HTMLElement | null;
|
|
51
|
+
}
|
|
52
|
+
interface EditableTextBaseProps {
|
|
53
|
+
/**
|
|
54
|
+
* The committed text value (controlled).
|
|
55
|
+
*/
|
|
56
|
+
value?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Initial committed value (uncontrolled).
|
|
59
|
+
*/
|
|
60
|
+
defaultValue?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Called with the new value when an edit is committed. Fires on commit only
|
|
63
|
+
* (Enter / blur), not on every keystroke.
|
|
64
|
+
*/
|
|
65
|
+
onChange?: (value: string) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Placeholder shown when the value is empty, both in the display and the edit
|
|
68
|
+
* field. Rendered in the muted placeholder color.
|
|
69
|
+
*/
|
|
70
|
+
placeholder?: string;
|
|
71
|
+
/**
|
|
72
|
+
* HTML element rendered for the display (idle) state.
|
|
73
|
+
* @default 'span'
|
|
74
|
+
*/
|
|
75
|
+
as?: TextElement;
|
|
76
|
+
/**
|
|
77
|
+
* Semantic typography variant, matching `Text`.
|
|
78
|
+
* @default 'body'
|
|
79
|
+
*/
|
|
80
|
+
variant?: TextVariant;
|
|
81
|
+
/** Text size, matching `Text`. Overrides the variant size. */
|
|
82
|
+
size?: TextSize;
|
|
83
|
+
/** Text weight, matching `Text`. Overrides the variant weight. */
|
|
84
|
+
weight?: TextWeight;
|
|
85
|
+
/**
|
|
86
|
+
* Text color, matching `Text`.
|
|
87
|
+
* @default 'primary'
|
|
88
|
+
*/
|
|
89
|
+
color?: TextColor;
|
|
90
|
+
/** Line height, matching `Text`. */
|
|
91
|
+
lineHeight?: TextLineHeight;
|
|
92
|
+
/** Text alignment, matching `Text`. */
|
|
93
|
+
align?: TextAlign;
|
|
94
|
+
/** Truncate the display text with an ellipsis on overflow. */
|
|
95
|
+
truncate?: boolean;
|
|
96
|
+
/** Use the monospace font family (also implied by `variant="code"`). */
|
|
97
|
+
mono?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* How a pointer starts an edit session.
|
|
100
|
+
* @default 'single'
|
|
101
|
+
*/
|
|
102
|
+
activationMode?: EditableTextActivationMode;
|
|
103
|
+
/**
|
|
104
|
+
* Disable the field entirely — no editing, no hover affordance, not
|
|
105
|
+
* focusable.
|
|
106
|
+
* @default false
|
|
107
|
+
*/
|
|
108
|
+
disabled?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Render the value but do not allow editing. Still selectable/focusable.
|
|
111
|
+
* @default false
|
|
112
|
+
*/
|
|
113
|
+
readOnly?: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Select the whole value when an edit session starts.
|
|
116
|
+
* @default true
|
|
117
|
+
*/
|
|
118
|
+
selectOnEdit?: boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Commit the draft when the edit field loses focus. When `false`, blurring
|
|
121
|
+
* discards the draft instead.
|
|
122
|
+
* @default true
|
|
123
|
+
*/
|
|
124
|
+
submitOnBlur?: boolean;
|
|
125
|
+
/** Maximum number of characters accepted by the edit field. */
|
|
126
|
+
maxLength?: number;
|
|
127
|
+
/**
|
|
128
|
+
* String overrides for localization. Pass a stable reference (memoize it) —
|
|
129
|
+
* it is low-frequency config.
|
|
130
|
+
*/
|
|
131
|
+
labels?: Partial<EditableTextLabels>;
|
|
132
|
+
/**
|
|
133
|
+
* Explicit accessible name. Wins over the value/placeholder-derived name and
|
|
134
|
+
* over `labels.editLabel`.
|
|
135
|
+
*/
|
|
136
|
+
'aria-label'?: string;
|
|
137
|
+
/** Called when an edit session starts. */
|
|
138
|
+
onEditStart?: () => void;
|
|
139
|
+
/** Called when an edit session ends, with the reason. */
|
|
140
|
+
onEditEnd?: (reason: EditableTextEndReason) => void;
|
|
141
|
+
/** Key-down handler forwarded to the edit field (runs after built-ins). */
|
|
142
|
+
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
|
143
|
+
/** Additional CSS class name applied to the current root element. */
|
|
144
|
+
className?: string;
|
|
145
|
+
/** Inline styles applied to the current root element. */
|
|
146
|
+
style?: React.CSSProperties;
|
|
147
|
+
/** Test identifier for automated testing. */
|
|
148
|
+
testId?: string;
|
|
149
|
+
/** Imperative handle — see {@link EditableTextHandle}. */
|
|
150
|
+
ref?: React.Ref<EditableTextHandle>;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Props for the EditableText component with a prettified type for better
|
|
154
|
+
* IntelliSense.
|
|
155
|
+
*/
|
|
156
|
+
type EditableTextProps = Prettify<EditableTextBaseProps>;
|
|
157
|
+
|
|
158
|
+
export type { EditableTextActivationMode, EditableTextBaseProps, EditableTextEndReason, EditableTextHandle, EditableTextLabels, EditableTextProps };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { EditableTextLabels } from './EditableText.types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* English defaults for every user-facing string EditableText renders. Override
|
|
5
|
+
* any subset by passing `labels` to `<EditableText>`; the rest fall back here.
|
|
6
|
+
*/
|
|
7
|
+
declare const DEFAULT_EDITABLE_TEXT_LABELS: EditableTextLabels;
|
|
8
|
+
/** Merge a partial `labels` override onto the English defaults. */
|
|
9
|
+
declare function resolveEditableTextLabels(overrides: Partial<EditableTextLabels> | undefined): EditableTextLabels;
|
|
10
|
+
|
|
11
|
+
export { DEFAULT_EDITABLE_TEXT_LABELS, resolveEditableTextLabels };
|
|
@@ -58,7 +58,8 @@ type IconButtonRadius = 'none' | 'sm' | 'md' | 'lg' | 'full';
|
|
|
58
58
|
* ```
|
|
59
59
|
*/
|
|
60
60
|
declare const IconButton: React.NamedExoticComponent<{
|
|
61
|
-
children
|
|
61
|
+
children?: React.ReactNode;
|
|
62
|
+
icon?: React.ReactNode;
|
|
62
63
|
size?: IconButtonSize | undefined;
|
|
63
64
|
variant?: IconButtonVariant | undefined;
|
|
64
65
|
radius?: IconButtonRadius | undefined;
|
|
@@ -2,8 +2,11 @@ import React from 'react';
|
|
|
2
2
|
import { ViewportProps } from './Viewport.types.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Viewport — pan/zoom canvas + HTML container for editor-style
|
|
6
|
-
* such as node graphs, timelines, and 2D world editors.
|
|
5
|
+
* Viewport — **2D only.** A pan/zoom canvas + HTML container for editor-style
|
|
6
|
+
* surfaces such as node graphs, timelines, and 2D world editors. Despite the
|
|
7
|
+
* name it is *not* a 3D viewport: it owns a 2D pan/zoom transform, not a camera.
|
|
8
|
+
* For 3D, host your own WebGL/WebGPU canvas (e.g. inside an `AppShell.Dock`) and
|
|
9
|
+
* reach for `Viewport` only for 2D editor surfaces.
|
|
7
10
|
*
|
|
8
11
|
* Composes:
|
|
9
12
|
* - Multiple perf-isolated `<ViewportLayer>` canvases (one draw cycle each).
|