entangle-ui 0.11.0 → 0.12.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 (44) hide show
  1. package/CHANGELOG.md +73 -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/Tooltip/{Tooltip.css.ts.vanilla-CrEFoLBX.css → Tooltip.css.ts.vanilla-BqaiGrFQ.css} +12 -9
  5. package/dist/esm/assets/src/components/shell/AppShell/{AppShell.css.ts.vanilla-BYmtU0O_.css → AppShell.css.ts.vanilla-BLK2-DRL.css} +1 -0
  6. package/dist/esm/assets/src/components/shell/MenuBar/{MenuBar.css.ts.vanilla-TgPcl4yA.css → MenuBar.css.ts.vanilla-Dt3w4f-q.css} +18 -6
  7. package/dist/esm/components/controls/Slider/Slider.css.js +10 -11
  8. package/dist/esm/components/controls/Slider/Slider.css.js.map +1 -1
  9. package/dist/esm/components/controls/Slider/Slider.js +40 -7
  10. package/dist/esm/components/controls/Slider/Slider.js.map +1 -1
  11. package/dist/esm/components/feedback/Spinner/Spinner.js +12 -3
  12. package/dist/esm/components/feedback/Spinner/Spinner.js.map +1 -1
  13. package/dist/esm/components/navigation/ContextMenu/ContextMenu.js +2 -2
  14. package/dist/esm/components/navigation/ContextMenu/ContextMenu.js.map +1 -1
  15. package/dist/esm/components/navigation/Menu/Menu.css.js +11 -10
  16. package/dist/esm/components/navigation/Menu/Menu.css.js.map +1 -1
  17. package/dist/esm/components/navigation/Menu/Menu.js +2 -2
  18. package/dist/esm/components/navigation/Menu/Menu.js.map +1 -1
  19. package/dist/esm/components/primitives/IconButton/IconButton.js +4 -2
  20. package/dist/esm/components/primitives/IconButton/IconButton.js.map +1 -1
  21. package/dist/esm/components/primitives/Tooltip/Tooltip.css.js +6 -5
  22. package/dist/esm/components/primitives/Tooltip/Tooltip.css.js.map +1 -1
  23. package/dist/esm/components/primitives/Tooltip/Tooltip.js +12 -2
  24. package/dist/esm/components/primitives/Tooltip/Tooltip.js.map +1 -1
  25. package/dist/esm/components/primitives/viewport/Viewport.js +5 -2
  26. package/dist/esm/components/primitives/viewport/Viewport.js.map +1 -1
  27. package/dist/esm/components/shell/AppShell/AppShell.css.js +1 -1
  28. package/dist/esm/components/shell/MenuBar/MenuBar.css.js +7 -6
  29. package/dist/esm/components/shell/MenuBar/MenuBar.css.js.map +1 -1
  30. package/dist/esm/components/shell/MenuBar/MenuBar.js +52 -3
  31. package/dist/esm/components/shell/MenuBar/MenuBar.js.map +1 -1
  32. package/dist/esm/{styles.js → styles.css.js} +1 -1
  33. package/dist/esm/styles.css.js.map +1 -0
  34. package/dist/tokens/tokens.dark.css +1 -1
  35. package/dist/tokens/tokens.json +1 -1
  36. package/dist/tokens/tokens.light.css +1 -1
  37. package/dist/types/components/controls/Slider/Slider.d.ts +7 -0
  38. package/dist/types/components/feedback/Spinner/Spinner.d.ts +6 -1
  39. package/dist/types/components/feedback/Spinner/Spinner.types.d.ts +8 -0
  40. package/dist/types/components/primitives/IconButton/IconButton.d.ts +2 -1
  41. package/dist/types/components/primitives/viewport/Viewport.d.ts +5 -2
  42. package/dist/types/components/shell/MenuBar/MenuBar.d.ts +862 -0
  43. package/package.json +2 -2
  44. package/dist/esm/styles.js.map +0 -1
@@ -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;;"}
@@ -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.12.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.12.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.12.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
@@ -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
 
@@ -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).