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.
Files changed (68) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/esm/assets/src/components/controls/Slider/{Slider.css.ts.vanilla-Cqm3fQ0S.css → Slider.css.ts.vanilla-CLf_RC0A.css} +50 -47
  3. package/dist/esm/assets/src/components/navigation/Menu/{Menu.css.ts.vanilla-B5iVe5S2.css → Menu.css.ts.vanilla-Bhh1L5yl.css} +14 -11
  4. package/dist/esm/assets/src/components/primitives/EditableText/EditableText.css.ts.vanilla-CrVGgD19.css +63 -0
  5. package/dist/esm/assets/src/components/primitives/Tooltip/{Tooltip.css.ts.vanilla-CrEFoLBX.css → Tooltip.css.ts.vanilla-BqaiGrFQ.css} +12 -9
  6. package/dist/esm/assets/src/components/shell/AppShell/{AppShell.css.ts.vanilla-BYmtU0O_.css → AppShell.css.ts.vanilla-BLK2-DRL.css} +1 -0
  7. package/dist/esm/assets/src/components/shell/MenuBar/{MenuBar.css.ts.vanilla-TgPcl4yA.css → MenuBar.css.ts.vanilla-Dt3w4f-q.css} +18 -6
  8. package/dist/esm/components/controls/Slider/Slider.css.js +10 -11
  9. package/dist/esm/components/controls/Slider/Slider.css.js.map +1 -1
  10. package/dist/esm/components/controls/Slider/Slider.js +40 -7
  11. package/dist/esm/components/controls/Slider/Slider.js.map +1 -1
  12. package/dist/esm/components/editor/ViewportGizmo/ViewportGizmo.js +3 -1
  13. package/dist/esm/components/editor/ViewportGizmo/ViewportGizmo.js.map +1 -1
  14. package/dist/esm/components/editor/ViewportGizmo/useGizmoInteraction.js +21 -27
  15. package/dist/esm/components/editor/ViewportGizmo/useGizmoInteraction.js.map +1 -1
  16. package/dist/esm/components/feedback/Spinner/Spinner.js +12 -3
  17. package/dist/esm/components/feedback/Spinner/Spinner.js.map +1 -1
  18. package/dist/esm/components/navigation/Breadcrumbs/BreadcrumbEllipsis.js +1 -0
  19. package/dist/esm/components/navigation/Breadcrumbs/BreadcrumbEllipsis.js.map +1 -1
  20. package/dist/esm/components/navigation/Breadcrumbs/BreadcrumbItem.js +1 -0
  21. package/dist/esm/components/navigation/Breadcrumbs/BreadcrumbItem.js.map +1 -1
  22. package/dist/esm/components/navigation/ContextMenu/ContextMenu.js +2 -2
  23. package/dist/esm/components/navigation/ContextMenu/ContextMenu.js.map +1 -1
  24. package/dist/esm/components/navigation/Menu/Menu.css.js +11 -10
  25. package/dist/esm/components/navigation/Menu/Menu.css.js.map +1 -1
  26. package/dist/esm/components/navigation/Menu/Menu.js +2 -2
  27. package/dist/esm/components/navigation/Menu/Menu.js.map +1 -1
  28. package/dist/esm/components/navigation/PathBar/PathBarSiblingMenu.js +1 -0
  29. package/dist/esm/components/navigation/PathBar/PathBarSiblingMenu.js.map +1 -1
  30. package/dist/esm/components/primitives/EditableText/EditableText.css.js +11 -0
  31. package/dist/esm/components/primitives/EditableText/EditableText.css.js.map +1 -0
  32. package/dist/esm/components/primitives/EditableText/EditableText.js +208 -0
  33. package/dist/esm/components/primitives/EditableText/EditableText.js.map +1 -0
  34. package/dist/esm/components/primitives/EditableText/editableTextLabels.js +16 -0
  35. package/dist/esm/components/primitives/EditableText/editableTextLabels.js.map +1 -0
  36. package/dist/esm/components/primitives/IconButton/IconButton.js +4 -2
  37. package/dist/esm/components/primitives/IconButton/IconButton.js.map +1 -1
  38. package/dist/esm/components/primitives/Tooltip/Tooltip.css.js +6 -5
  39. package/dist/esm/components/primitives/Tooltip/Tooltip.css.js.map +1 -1
  40. package/dist/esm/components/primitives/Tooltip/Tooltip.js +12 -2
  41. package/dist/esm/components/primitives/Tooltip/Tooltip.js.map +1 -1
  42. package/dist/esm/components/primitives/viewport/Viewport.js +5 -2
  43. package/dist/esm/components/primitives/viewport/Viewport.js.map +1 -1
  44. package/dist/esm/components/shell/AppShell/AppShell.css.js +1 -1
  45. package/dist/esm/components/shell/MenuBar/MenuBar.css.js +7 -6
  46. package/dist/esm/components/shell/MenuBar/MenuBar.css.js.map +1 -1
  47. package/dist/esm/components/shell/MenuBar/MenuBar.js +52 -3
  48. package/dist/esm/components/shell/MenuBar/MenuBar.js.map +1 -1
  49. package/dist/esm/index.js +2 -0
  50. package/dist/esm/index.js.map +1 -1
  51. package/dist/esm/{styles.js → styles.css.js} +1 -1
  52. package/dist/esm/styles.css.js.map +1 -0
  53. package/dist/tokens/tokens.dark.css +1 -1
  54. package/dist/tokens/tokens.json +1 -1
  55. package/dist/tokens/tokens.light.css +1 -1
  56. package/dist/types/components/controls/Slider/Slider.d.ts +7 -0
  57. package/dist/types/components/editor/ViewportGizmo/ViewportGizmo.types.d.ts +26 -3
  58. package/dist/types/components/feedback/Spinner/Spinner.d.ts +6 -1
  59. package/dist/types/components/feedback/Spinner/Spinner.types.d.ts +8 -0
  60. package/dist/types/components/primitives/EditableText/EditableText.d.ts +37 -0
  61. package/dist/types/components/primitives/EditableText/EditableText.types.d.ts +158 -0
  62. package/dist/types/components/primitives/EditableText/editableTextLabels.d.ts +11 -0
  63. package/dist/types/components/primitives/IconButton/IconButton.d.ts +2 -1
  64. package/dist/types/components/primitives/viewport/Viewport.d.ts +5 -2
  65. package/dist/types/components/shell/MenuBar/MenuBar.d.ts +862 -0
  66. package/dist/types/index.d.ts +3 -0
  67. package/package.json +2 -2
  68. package/dist/esm/styles.js.map +0 -1
@@ -1,16 +1,17 @@
1
- import './../../../assets/src/components/shell/MenuBar/MenuBar.css.ts.vanilla-TgPcl4yA.css';
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 = 'MenuBar_separator__1h81mxva';
9
+ var separator = 'MenuBar_separator__1h81mxvb';
9
10
  var shortcut = 'MenuBar_shortcut__1h81mxv9';
10
- var subContainer = 'MenuBar_subContainer__1h81mxvd';
11
- var subDropdown = 'MenuBar_subDropdown__1h81mxvc';
12
- var subTrigger = 'MenuBar_subTrigger__1h81mxvb MenuBar_menuItem__1h81mxv8';
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, useContext } from 'react';
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 &#x25B6;\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 &#x25B6;\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';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -1,3 +1,3 @@
1
1
  import './assets/src/theme/darkTheme.css.ts.vanilla-BHZ3ecRn.css';
2
2
  import './assets/src/theme/globalScrollbars.css.ts.vanilla-BAJwnUEJ.css';
3
- //# sourceMappingURL=styles.js.map
3
+ //# sourceMappingURL=styles.css.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -1,4 +1,4 @@
1
- /* Entangle UI dark theme — token export (v0.11.0) */
1
+ /* Entangle UI dark theme — token export (v0.13.0) */
2
2
  /* Generated by scripts/export-tokens.ts — do not edit by hand. */
3
3
  :root {
4
4
  --etui-color-accent-error: #f44336;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://design-tokens.org/schema.json",
3
3
  "$description": "Entangle UI design tokens",
4
- "$version": "0.11.0",
4
+ "$version": "0.13.0",
5
5
  "themes": {
6
6
  "dark": {
7
7
  "colors": {
@@ -1,4 +1,4 @@
1
- /* Entangle UI light theme — token export (v0.11.0) */
1
+ /* Entangle UI light theme — token export (v0.13.0) */
2
2
  /* Generated by scripts/export-tokens.ts — do not edit by hand. */
3
3
  .etui-theme-light {
4
4
  --etui-color-accent-error: #d32f2f;
@@ -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 (horizontal drag) */
28
+ /** Yaw change in degrees. Positive = pointer dragged right / ArrowRight. */
23
29
  deltaYaw: number;
24
- /** Pitch change in degrees (vertical drag) */
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
- /** Called continuously while the user drags to orbit. */
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: React.ReactNode;
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 surfaces
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).